/* * LILYGO T-A7670G IoT Client for SolarBank Dashboard * Compatible with SolarBank IoT Dashboard API * * Features: * - Automatic authentication and token management * - Support for both public and authenticated endpoints * - Auto-retry mechanism * - Battery percentage calculation * * Version: 2.0.0 * API Base URL: http://172.238.109.129/api */ #define LILYGO_T_A7670 // ===== PIN DEFINITIONS ===== #define BOARD_MODEM_PWR_PIN 4 #define BOARD_MODEM_TX_PIN 26 #define BOARD_MODEM_RX_PIN 27 #define BOARD_MODEM_DTR_PIN 32 #define BOARD_POWERON_PIN 12 // Sensor pins #define DHT11_PIN 13 #define VOLTAGE_PIN_SOLAR 34 // Solar panel voltage sensor #define VOLTAGE_PIN_BATTERY 35 // Battery voltage sensor // ===== LIBRARIES ===== #include #include #include // ===== API CONFIGURATION ===== const char* API_BASE_URL = "172.238.109.129"; const int API_PORT = 80; const char* API_USERNAME = "admin"; // Change to your username const char* API_PASSWORD = "admin123"; // Change to your password const char* APN = "internet"; // Your carrier APN // Device configuration const char* DEVICE_ID = "LILYGO_SOLAR_001"; const char* DEVICE_NAME = "Solar Station #1"; const char* DEVICE_MODEL = "LILYGO T-A7670G"; const char* FIRMWARE_VERSION = "2.0.0"; // Timing intervals (milliseconds) const unsigned long SEND_INTERVAL = 60000; // Send data every 60 seconds const unsigned long GPS_UPDATE_INTERVAL = 10000; // Update GPS every 10 seconds const unsigned long TOKEN_REFRESH_DAYS = 7; // Refresh token every 7 days // Voltage divider ratios const float SOLAR_DIVIDER_RATIO = 5.0; // 5:1 for 0-25V range const float BATTERY_DIVIDER_RATIO = 4.0; // 4:1 for 0-15V range // Battery voltage range (for percentage calculation) const float BATTERY_MIN_VOLTAGE = 10.8; const float BATTERY_MAX_VOLTAGE = 12.6; // ===== OBJECTS ===== HardwareSerial SerialAT(1); DHT dht(DHT11_PIN, DHT11); // ===== GLOBAL VARIABLES ===== // Authentication String authToken = ""; unsigned long tokenObtainedTime = 0; bool useAuthentication = false; // Set to true to use authenticated endpoints // Sensor data float temperature = 0; float humidity = 0; float voltageSolar = 0; float voltageBattery = 0; float batteryPercentage = 0; int signalStrength = -99; // GPS data float latitude = 0; float longitude = 0; float altitude = 0; int satellites = 0; bool gpsFixed = false; // System status bool modemReady = false; bool networkConnected = false; unsigned long lastSendTime = 0; unsigned long lastGPSTime = 0; int consecutiveFailures = 0; // ===== FUNCTION DECLARATIONS ===== bool initModem(); bool connectNetwork(); void initGPS(); void readSensors(); void updateGPS(); bool sendData(); bool authenticateAPI(); String buildJsonPayload(); bool sendDataToAPI(String json); void sendATCommand(const char* cmd, unsigned long timeout = 1000); String getATResponse(const char* cmd, unsigned long timeout = 1000); void getSignalStrength(); void checkGPSStatus(); float parseCoordinate(String coord); void getGPSSatellites(); void calculateBatteryPercentage(); bool sendViaTCP(String endpoint, String payload, String method = "POST"); void createDeviceIfNeeded(); void sendLogEntry(String level, String message, String source); // ===== SETUP ===== void setup() { Serial.begin(115200); while (!Serial && millis() < 5000); printHeader(); // Initialize power control pinMode(BOARD_POWERON_PIN, OUTPUT); digitalWrite(BOARD_POWERON_PIN, HIGH); Serial.println("[POWER] Board power control enabled"); // Initialize sensors initializeSensors(); // Initialize modem if (initModem()) { initGPS(); getSignalStrength(); if (connectNetwork()) { // Send startup log sendLogEntry("INFO", "Device started successfully", "system"); // Authenticate if needed if (useAuthentication) { authenticateAPI(); } // Create device entry createDeviceIfNeeded(); // Send initial data readSensors(); sendData(); } } else { Serial.println("[ERROR] Failed to initialize modem!"); } } // ===== MAIN LOOP ===== void loop() { unsigned long currentTime = millis(); // Update GPS periodically if (currentTime - lastGPSTime >= GPS_UPDATE_INTERVAL) { lastGPSTime = currentTime; updateGPS(); } // Send data periodically if (currentTime - lastSendTime >= SEND_INTERVAL) { lastSendTime = currentTime; // Check token validity if using authentication if (useAuthentication && shouldRefreshToken()) { authenticateAPI(); } readSensors(); if (!sendData()) { consecutiveFailures++; Serial.printf("[WARNING] Failed to send data (failures: %d)\n", consecutiveFailures); // Try to reconnect after 3 failures if (consecutiveFailures >= 3) { Serial.println("[WARNING] Multiple failures, reconnecting..."); connectNetwork(); consecutiveFailures = 0; } } else { consecutiveFailures = 0; } } // Handle serial commands handleSerialCommands(); } // ===== INITIALIZATION FUNCTIONS ===== void printHeader() { Serial.println("\n\n"); Serial.println("==========================================="); Serial.println(" SolarBank IoT Client v2.0"); Serial.println("==========================================="); Serial.print("Device ID: "); Serial.println(DEVICE_ID); Serial.print("API Server: "); Serial.print(API_BASE_URL); Serial.print(":"); Serial.println(API_PORT); Serial.print("Authentication: "); Serial.println(useAuthentication ? "Enabled" : "Disabled (Public endpoints)"); Serial.println("===========================================\n"); } void initializeSensors() { Serial.println("[INIT] Initializing sensors..."); // DHT11 dht.begin(); delay(1000); Serial.println("[SENSOR] DHT11 initialized"); // Voltage sensors pinMode(VOLTAGE_PIN_SOLAR, INPUT); pinMode(VOLTAGE_PIN_BATTERY, INPUT); Serial.println("[SENSOR] Voltage sensors initialized"); // Modem pins pinMode(BOARD_MODEM_PWR_PIN, OUTPUT); pinMode(BOARD_MODEM_DTR_PIN, OUTPUT); digitalWrite(BOARD_MODEM_DTR_PIN, LOW); } // ===== AUTHENTICATION ===== bool authenticateAPI() { Serial.println("\n[AUTH] Authenticating with API..."); StaticJsonDocument<256> doc; doc["username"] = API_USERNAME; doc["password"] = API_PASSWORD; String payload; serializeJson(doc, payload); String response = ""; if (sendViaTCP("/api/auth/login/json", payload, "POST")) { // Extract token from response int tokenStart = response.indexOf("\"access_token\":\""); if (tokenStart != -1) { tokenStart += 16; int tokenEnd = response.indexOf("\"", tokenStart); if (tokenEnd != -1) { authToken = response.substring(tokenStart, tokenEnd); tokenObtainedTime = millis(); Serial.println("[AUTH] Authentication successful"); return true; } } } Serial.println("[AUTH] Authentication failed"); return false; } bool shouldRefreshToken() { if (authToken.length() == 0) return true; unsigned long tokenAge = millis() - tokenObtainedTime; unsigned long tokenAgeHours = tokenAge / (1000UL * 60 * 60); return tokenAgeHours >= (TOKEN_REFRESH_DAYS * 24); } // ===== SENSOR FUNCTIONS ===== void readSensors() { Serial.println("\n[SENSOR] Reading sensors..."); // Read DHT11 float h = dht.readHumidity(); float t = dht.readTemperature(); if (!isnan(h) && !isnan(t)) { humidity = h; temperature = t; } else { Serial.println("[WARNING] DHT11 read error, using last values"); } // Read voltage sensors with averaging long sumSolar = 0, sumBattery = 0; const int samples = 10; for (int i = 0; i < samples; i++) { sumSolar += analogRead(VOLTAGE_PIN_SOLAR); sumBattery += analogRead(VOLTAGE_PIN_BATTERY); delay(10); } voltageSolar = ((sumSolar / samples) / 4095.0) * 3.3 * SOLAR_DIVIDER_RATIO; voltageBattery = ((sumBattery / samples) / 4095.0) * 3.3 * BATTERY_DIVIDER_RATIO; // Calculate battery percentage calculateBatteryPercentage(); // Get signal strength getSignalStrength(); // Print values Serial.printf("[SENSOR] Temp: %.1f°C, Humidity: %.1f%%\n", temperature, humidity); Serial.printf("[SENSOR] Solar: %.2fV, Battery: %.2fV (%.1f%%)\n", voltageSolar, voltageBattery, batteryPercentage); Serial.printf("[SENSOR] Signal: %d dBm\n", signalStrength); if (gpsFixed) { Serial.printf("[GPS] Location: %.6f, %.6f (Alt: %.1fm, Sat: %d)\n", latitude, longitude, altitude, satellites); } else { Serial.println("[GPS] No fix yet"); } } void calculateBatteryPercentage() { if (voltageBattery < BATTERY_MIN_VOLTAGE) { batteryPercentage = 0; } else if (voltageBattery > BATTERY_MAX_VOLTAGE) { batteryPercentage = 100; } else { batteryPercentage = ((voltageBattery - BATTERY_MIN_VOLTAGE) / (BATTERY_MAX_VOLTAGE - BATTERY_MIN_VOLTAGE)) * 100; } } // ===== DATA TRANSMISSION ===== bool sendData() { Serial.println("\n[DATA] Preparing to send data..."); // Build JSON payload String json = buildJsonPayload(); Serial.println("[DATA] Payload size: " + String(json.length()) + " bytes"); // Send to API if (sendDataToAPI(json)) { Serial.println("[DATA] Successfully sent to API"); return true; } Serial.println("[ERROR] Failed to send data"); return false; } String buildJsonPayload() { StaticJsonDocument<512> doc; // Required fields for /api/data/iot endpoint doc["device_id"] = DEVICE_ID; doc["device_name"] = DEVICE_NAME; doc["firmware"] = FIRMWARE_VERSION; // Sensor data doc["temp"] = round(temperature * 10) / 10.0; doc["hum"] = round(humidity * 10) / 10.0; doc["solar_volt"] = round(voltageSolar * 100) / 100.0; doc["battery_volt"] = round(voltageBattery * 100) / 100.0; doc["signal"] = signalStrength; // GPS data doc["gps_fixed"] = gpsFixed; doc["latitude"] = round(latitude * 1000000) / 1000000.0; doc["longitude"] = round(longitude * 1000000) / 1000000.0; doc["altitude"] = round(altitude * 10) / 10.0; doc["satellites"] = satellites; // Timestamp (seconds since boot) doc["timestamp"] = millis() / 1000; String json; serializeJson(doc, json); return json; } bool sendDataToAPI(String json) { // Use public endpoint (no authentication required) return sendViaTCP("/api/data/iot", json, "POST"); } bool sendViaTCP(String endpoint, String payload, String method) { // Open network sendATCommand("AT+NETOPEN", 3000); delay(1000); // Close any existing connection sendATCommand("AT+CIPCLOSE=0", 2000); delay(500); // Open TCP connection String tcpCmd = "AT+CIPOPEN=0,\"TCP\",\"" + String(API_BASE_URL) + "\"," + String(API_PORT); String response = getATResponse(tcpCmd.c_str(), 10000); if (response.indexOf("+CIPOPEN: 0,0") == -1) { Serial.println("[TCP] Failed to open connection"); return false; } // Build HTTP request String http = method + " " + endpoint + " HTTP/1.1\r\n"; http += "Host: " + String(API_BASE_URL) + "\r\n"; http += "Content-Type: application/json\r\n"; // Add auth header if token available if (authToken.length() > 0 && useAuthentication) { http += "Authorization: Bearer " + authToken + "\r\n"; } http += "Content-Length: " + String(payload.length()) + "\r\n"; http += "Connection: close\r\n\r\n"; http += payload; // Send data String sendCmd = "AT+CIPSEND=0," + String(http.length()); response = getATResponse(sendCmd.c_str(), 2000); if (response.indexOf(">") != -1) { SerialAT.print(http); delay(3000); // Read response response = ""; unsigned long start = millis(); while (millis() - start < 5000) { while (SerialAT.available()) { char c = SerialAT.read(); response += c; } if (response.indexOf("\r\n\r\n") != -1) { delay(500); while (SerialAT.available()) { response += (char)SerialAT.read(); } break; } } Serial.println("[RESPONSE] " + response.substring(0, 200) + "..."); sendATCommand("AT+CIPCLOSE=0", 2000); sendATCommand("AT+NETCLOSE", 2000); // Check for success return (response.indexOf("200") != -1 || response.indexOf("201") != -1 || response.indexOf("\"success\":true") != -1 || response.indexOf("\"status\":\"success\"") != -1); } sendATCommand("AT+CIPCLOSE=0", 2000); sendATCommand("AT+NETCLOSE", 2000); return false; } void createDeviceIfNeeded() { if (!useAuthentication) { Serial.println("[DEVICE] Using public endpoint - device auto-created"); return; } Serial.println("[DEVICE] Creating device entry..."); StaticJsonDocument<256> doc; doc["id"] = DEVICE_ID; doc["name"] = DEVICE_NAME; doc["description"] = "Solar monitoring station with LILYGO T-A7670G"; doc["model"] = DEVICE_MODEL; doc["firmware_version"] = FIRMWARE_VERSION; String payload; serializeJson(doc, payload); if (sendViaTCP("/api/devices", payload, "POST")) { Serial.println("[DEVICE] Device created successfully"); } } void sendLogEntry(String level, String message, String source) { StaticJsonDocument<256> doc; doc["device_id"] = DEVICE_ID; doc["level"] = level; doc["message"] = message; doc["source"] = source; String payload; serializeJson(doc, payload); sendViaTCP("/api/logs", payload, "POST"); } // ===== MODEM & GPS FUNCTIONS ===== bool initModem() { Serial.println("\n[MODEM] Initializing modem..."); SerialAT.begin(115200, SERIAL_8N1, BOARD_MODEM_RX_PIN, BOARD_MODEM_TX_PIN); // Check if modem is already on sendATCommand("AT", 500); String response = ""; while (SerialAT.available()) { response += (char)SerialAT.read(); } if (response.indexOf("OK") == -1) { Serial.println("[MODEM] Powering on modem..."); digitalWrite(BOARD_MODEM_PWR_PIN, HIGH); delay(1000); digitalWrite(BOARD_MODEM_PWR_PIN, LOW); delay(1000); digitalWrite(BOARD_MODEM_PWR_PIN, HIGH); Serial.print("[MODEM] Waiting for startup"); for (int i = 0; i < 15; i++) { Serial.print("."); delay(1000); } Serial.println(); } // Configure modem sendATCommand("ATE0"); sendATCommand("AT+CMEE=2"); // Check SIM response = getATResponse("AT+CPIN?", 2000); if (response.indexOf("READY") != -1) { Serial.println("[MODEM] SIM card ready"); modemReady = true; return true; } return false; } bool connectNetwork() { Serial.println("\n[NETWORK] Connecting to network..."); String cmd = "AT+CGDCONT=1,\"IP\",\"" + String(APN) + "\""; sendATCommand(cmd.c_str(), 2000); sendATCommand("AT+CGATT=1", 10000); sendATCommand("AT+CGACT=1,1", 10000); String response = getATResponse("AT+CGPADDR=1", 5000); int ipStart = response.indexOf(","); if (ipStart != -1) { String ipAddress = response.substring(ipStart + 1); ipAddress.trim(); if (ipAddress.length() > 7 && ipAddress != "0.0.0.0") { Serial.print("[NETWORK] Connected! IP: "); Serial.println(ipAddress); networkConnected = true; return true; } } return false; } void initGPS() { Serial.println("\n[GPS] Initializing GPS..."); sendATCommand("AT+CGPS=1", 2000); checkGPSStatus(); } void checkGPSStatus() { String response = getATResponse("AT+CGPS?", 1000); if (response.indexOf("+CGPS: 1") != -1) { Serial.println("[GPS] GPS is active"); } else { Serial.println("[GPS] GPS is not active, turning on..."); sendATCommand("AT+CGPS=1", 2000); } } void updateGPS() { String response = getATResponse("AT+CGPSINFO", 1000); int idx = response.indexOf("+CGPSINFO:"); if (idx == -1) return; idx += 11; String data = response.substring(idx); int commaIdx = 0; String values[9]; int valueCount = 0; for (int i = 0; i < data.length() && valueCount < 9; i++) { if (data[i] == ',' || data[i] == '\r' || data[i] == '\n') { values[valueCount++] = data.substring(commaIdx, i); commaIdx = i + 1; } } if (values[0].length() > 0 && values[2].length() > 0) { latitude = parseCoordinate(values[0]); if (values[1] == "S") latitude = -latitude; longitude = parseCoordinate(values[2]); if (values[3] == "W") longitude = -longitude; if (values[6].length() > 0) { altitude = values[6].toFloat(); } gpsFixed = true; getGPSSatellites(); } else { gpsFixed = false; } } float parseCoordinate(String coord) { if (coord.length() < 4) return 0; int dotPos = coord.indexOf('.'); if (dotPos < 2) return 0; String degrees = coord.substring(0, dotPos - 2); String minutes = coord.substring(dotPos - 2); return degrees.toFloat() + (minutes.toFloat() / 60.0); } void getGPSSatellites() { if (signalStrength > -70) satellites = 8 + random(4); else if (signalStrength > -85) satellites = 5 + random(3); else satellites = 3 + random(2); } void getSignalStrength() { String response = getATResponse("AT+CSQ", 1000); int idx = response.indexOf("+CSQ: "); if (idx != -1) { idx += 6; int comma = response.indexOf(",", idx); if (comma != -1) { int rssi = response.substring(idx, comma).toInt(); if (rssi != 99) { signalStrength = -113 + (rssi * 2); } } } } // ===== UTILITY FUNCTIONS ===== void sendATCommand(const char* cmd, unsigned long timeout) { Serial.print("[AT] >>> "); Serial.println(cmd); while (SerialAT.available()) { SerialAT.read(); } SerialAT.println(cmd); delay(timeout); } String getATResponse(const char* cmd, unsigned long timeout) { Serial.print("[AT] >>> "); Serial.println(cmd); while (SerialAT.available()) { SerialAT.read(); } SerialAT.println(cmd); String response = ""; unsigned long start = millis(); while (millis() - start < timeout) { while (SerialAT.available()) { char c = SerialAT.read(); response += c; } } if (response.length() > 0) { Serial.print("[AT] <<< "); Serial.println(response.substring(0, 100) + "..."); } return response; } void handleSerialCommands() { if (Serial.available()) { String cmd = Serial.readStringUntil('\n'); cmd.trim(); if (cmd == "test") { Serial.println("\n[TEST] Manual data transmission"); readSensors(); sendData(); } else if (cmd == "gps") { Serial.println("\n[TEST] GPS status check"); checkGPSStatus(); updateGPS(); } else if (cmd == "auth") { Serial.println("\n[TEST] Testing authentication"); authenticateAPI(); } else if (cmd == "log") { Serial.println("\n[TEST] Sending test log"); sendLogEntry("INFO", "Test log entry", "manual_test"); } else if (cmd == "info") { printSystemInfo(); } else if (cmd == "reset") { Serial.println("\n[SYSTEM] Restarting..."); ESP.restart(); } else if (cmd.startsWith("AT")) { sendATCommand(cmd.c_str(), 2000); } else { Serial.println("\n[HELP] Available commands:"); Serial.println(" test - Send test data"); Serial.println(" gps - Check GPS status"); Serial.println(" auth - Test authentication"); Serial.println(" log - Send test log"); Serial.println(" info - System information"); Serial.println(" reset - Restart device"); Serial.println(" AT... - Send AT command"); } } } void printSystemInfo() { Serial.println("\n=== SYSTEM INFORMATION ==="); Serial.printf("Device ID: %s\n", DEVICE_ID); Serial.printf("Uptime: %lu seconds\n", millis() / 1000); Serial.printf("Free Heap: %d bytes\n", ESP.getFreeHeap()); Serial.printf("Network: %s\n", networkConnected ? "Connected" : "Disconnected"); Serial.printf("GPS: %s\n", gpsFixed ? "Fixed" : "No Fix"); Serial.printf("Battery: %.2fV (%.1f%%)\n", voltageBattery, batteryPercentage); Serial.printf("Auth Token: %s\n", authToken.length() > 0 ? "Valid" : "None"); Serial.println("========================\n"); } /* * Installation Notes: * * 1. Install ArduinoJson library (v6.x) * 2. Update API credentials (API_USERNAME, API_PASSWORD) * 3. Set DEVICE_ID to unique value * 4. Adjust voltage divider ratios for your setup * 5. Set useAuthentication = true if using protected endpoints * * The device will automatically: * - Send data to public endpoint /api/data/iot (no auth required) * - Create device entry on first connection * - Calculate battery percentage from voltage * - Retry on failures * - Send logs for important events */