📖 About This Project
Control 5 relays with your Raspberry Pi Pico W using negative logic (LOW = ON). The Pico W offers independent GPIO pins with no internal conflicts – perfect for relay control. Ideal for industrial automation, greenhouse environmental control, and multi-device switching. Each relay module channel is optocoupled for protection. Features token authentication, session management, offline backup, and built-in LED status indicator. Control everything from the OceanRemote dashboard from anywhere.
💻 Firmware Code
C++ (Arduino)
// OceanicRemote Secure Firmware - Raspberry Pi Pico W
// Version 5.4 - FULLY INDEPENDENT GPIO PINS (11,12,13,14,15)
// EACH RELAY CONTROLS ITS OWN PIN - NO CROSS TALK
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <EEPROM.h>
// ========== EEPROM CONFIGURATION ==========
#define EEPROM_SIZE 512
#define TOKEN_ADDRESS 0
#define TOKEN_MAGIC 0xAA55
#define SESSION_ADDRESS 128
#define SESSION_MAGIC 0xBB66
#define FLAG_ADDRESS 256
#define FLAG_MAGIC 0xCC77
#define VERSION_ADDRESS 260
#define FIRMWARE_VERSION 54
// ========== RELAY NAMES ==========
const char* relayNames[5] = {
"FAN1",
"Relay 2",
"Relay 3",
"Relay 4",
"Relay 5"
};
// ========== DEVICE STATE ==========
enum DeviceState {
STATE_PROVISIONING = 0,
STATE_REGISTERED = 1,
STATE_ERROR = 2
};
DeviceState currentState = STATE_PROVISIONING;
// ========== WIFI CONFIGURATION ==========
const char* WIFI_SSID = "SSID_WIF";
const char* WIFI_PASSWORD = "PASSWROD_WIFI";
// ========== SERVER CONFIGURATION ==========
const char* serverHost = "www.oceanremote.net";
// ========== REGISTRATION TOKEN ==========
const char* REGISTRATION_TOKEN = "oc_reg_He1QlhMZuLZ08aaMN9jor3AESnqfiFhLUcEigItwK7M";
// ========== OPTIMIZED TIMING ==========
const unsigned long BASE_POLL_INTERVAL = 3000;
unsigned long nextCheckTime = 0;
const int JITTER_RANGE_MS = 1500;
unsigned long lastWiFiReconnect = 0;
const unsigned long wifiReconnectInterval = 30000;
int wifiRetryCount = 0;
const int MAX_WIFI_RETRIES = 5;
int registrationRetryCount = 0;
const int MAX_REGISTRATION_RETRIES = 3;
// ========== SESSION MANAGEMENT ==========
String sessionId = "";
String permanentToken = "";
bool sessionValid = false;
bool registrationTokenUsed = false;
// ========== DEVICE ID ==========
String deviceId = "";
String macAddress = "";
// ========== RELAY STATES ==========
bool relayStates[5] = {false, false, false, false, false};
bool deviceRegistered = false;
// ***** CORRECTED PIN DEFINITIONS - FULLY INDEPENDENT *****
// GP11 (Pin15) -> Relay 1
// GP12 (Pin16) -> Relay 2
// GP13 (Pin17) -> Relay 3
// GP14 (Pin19) -> Relay 4
// GP15 (Pin20) -> Relay 5
const int relayPins[] = {11, 12, 13, 14, 15};
const int relayCount = 5;
#define LED_PIN 25 // Built-in LED (GP25 - independent)
// ========== SENSOR VARIABLES ==========
float temperature = -999;
float humidity = -999;
// ========== ERROR HANDLING ==========
void enterErrorState(String reason) {
Serial.println("");
Serial.println("========================================");
Serial.print("[ERROR] Entering error state: ");
Serial.println(reason);
Serial.println("========================================");
currentState = STATE_ERROR;
for (int i = 0; i < 3; i++) {
digitalWrite(LED_PIN, HIGH);
delay(500);
digitalWrite(LED_PIN, LOW);
delay(500);
}
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
}
// ========== EEPROM STORAGE FUNCTIONS ==========
void initEEPROM() {
Serial.println("[EEPROM] Initializing...");
EEPROM.begin(EEPROM_SIZE);
int storedVersion;
EEPROM.get(VERSION_ADDRESS, storedVersion);
Serial.printf("[EEPROM] Stored version: %d, Current: %d\n", storedVersion, FIRMWARE_VERSION);
if (storedVersion != FIRMWARE_VERSION) {
Serial.println("[EEPROM] Version mismatch - CLEARING EEPROM!");
for (int i = 0; i < EEPROM_SIZE; i++) {
EEPROM.write(i, 0);
}
EEPROM.put(VERSION_ADDRESS, FIRMWARE_VERSION);
EEPROM.commit();
Serial.println("[EEPROM] EEPROM cleared!");
}
}
void saveTokenToEEPROM(String token) {
Serial.println("[EEPROM] Saving token...");
uint16_t magic = TOKEN_MAGIC;
EEPROM.put(TOKEN_ADDRESS, magic);
int addr = TOKEN_ADDRESS + sizeof(magic);
int len = token.length();
EEPROM.put(addr, len);
addr += sizeof(len);
for (int i = 0; i < len; i++) {
EEPROM.write(addr + i, token[i]);
}
uint16_t checksum = 0;
for (int i = 0; i < len; i++) {
checksum += token[i];
}
EEPROM.put(addr + len, checksum);
EEPROM.commit();
Serial.printf("[EEPROM] Token saved\n");
}
String loadTokenFromEEPROM() {
Serial.println("[EEPROM] Loading token...");
uint16_t magic;
EEPROM.get(TOKEN_ADDRESS, magic);
if (magic != TOKEN_MAGIC) {
Serial.println("[EEPROM] No valid token found");
return "";
}
int addr = TOKEN_ADDRESS + sizeof(magic);
int len;
EEPROM.get(addr, len);
if (len <= 0 || len > 200) {
Serial.println("[EEPROM] Invalid token length");
return "";
}
addr += sizeof(len);
char buffer[256];
for (int i = 0; i < len; i++) {
buffer[i] = EEPROM.read(addr + i);
}
buffer[len] = '\0';
uint16_t storedChecksum;
EEPROM.get(addr + len, storedChecksum);
uint16_t calculatedChecksum = 0;
for (int i = 0; i < len; i++) {
calculatedChecksum += buffer[i];
}
if (storedChecksum != calculatedChecksum) {
Serial.println("[EEPROM] Checksum mismatch!");
return "";
}
String token = String(buffer);
Serial.printf("[EEPROM] Loaded token\n");
return token;
}
void clearTokenFromEEPROM() {
Serial.println("[EEPROM] Clearing token...");
for (int i = 0; i < 64; i++) {
EEPROM.write(TOKEN_ADDRESS + i, 0);
}
EEPROM.commit();
}
void saveSessionToEEPROM(String session) {
Serial.printf("[EEPROM] Saving session: %s\n", session.c_str());
uint16_t magic = SESSION_MAGIC;
EEPROM.put(SESSION_ADDRESS, magic);
int addr = SESSION_ADDRESS + sizeof(magic);
int len = session.length();
EEPROM.put(addr, len);
addr += sizeof(len);
for (int i = 0; i < len; i++) {
EEPROM.write(addr + i, session[i]);
}
EEPROM.commit();
}
String loadSessionFromEEPROM() {
uint16_t magic;
EEPROM.get(SESSION_ADDRESS, magic);
if (magic != SESSION_MAGIC) return "";
int addr = SESSION_ADDRESS + sizeof(magic);
int len;
EEPROM.get(addr, len);
if (len <= 0 || len > 200) return "";
addr += sizeof(len);
char buffer[256];
for (int i = 0; i < len; i++) {
buffer[i] = EEPROM.read(addr + i);
}
buffer[len] = '\0';
return String(buffer);
}
void clearSessionFromEEPROM() {
for (int i = 0; i < 32; i++) {
EEPROM.write(SESSION_ADDRESS + i, 0);
}
EEPROM.commit();
}
void markRegistrationUsed() {
Serial.println("[EEPROM] Marking registration as used...");
uint16_t magic = FLAG_MAGIC;
EEPROM.put(FLAG_ADDRESS, magic);
EEPROM.commit();
}
bool isRegistrationUsed() {
uint16_t magic;
EEPROM.get(FLAG_ADDRESS, magic);
return (magic == FLAG_MAGIC);
}
void clearAllStorage() {
Serial.println("[EEPROM] Clearing all storage...");
clearTokenFromEEPROM();
clearSessionFromEEPROM();
for (int i = 0; i < 32; i++) {
EEPROM.write(FLAG_ADDRESS + i, 0);
}
EEPROM.commit();
}
// ========== HELPER FUNCTIONS ==========
String generateDeviceId() {
uint8_t mac[6];
WiFi.macAddress(mac);
macAddress = String(mac[0], HEX) + String(mac[1], HEX) + String(mac[2], HEX) +
String(mac[3], HEX) + String(mac[4], HEX) + String(mac[5], HEX);
macAddress.toUpperCase();
return "PICO_" + macAddress;
}
uint8_t packRelays() {
uint8_t packed = 0;
if (relayStates[0]) packed |= 1 << 0;
if (relayStates[1]) packed |= 1 << 1;
if (relayStates[2]) packed |= 1 << 2;
if (relayStates[3]) packed |= 1 << 3;
if (relayStates[4]) packed |= 1 << 4;
return packed;
}
void unpackRelays(uint8_t packed) {
for (int i = 0; i < 5; i++) {
bool state = (packed >> i) & 1;
if (state != relayStates[i]) {
setRelay(i, state);
Serial.printf("[RELAY] %s set to %s\n", relayNames[i], state ? "ON" : "OFF");
}
}
}
void setRelay(int index, bool state) {
// Apply per-relay logic with independent pin control
switch(index) {
case 0:
// Relay 1: Negative logic (LOW = ON) - GPIO11
digitalWrite(11, state ? LOW : HIGH);
break;
case 1:
// Relay 2: Positive logic (HIGH = ON) - GPIO12
digitalWrite(12, state ? HIGH : LOW);
break;
case 2:
// Relay 3: Positive logic (HIGH = ON) - GPIO13
digitalWrite(13, state ? HIGH : LOW);
break;
case 3:
// Relay 4: Positive logic (HIGH = ON) - GPIO14
digitalWrite(14, state ? HIGH : LOW);
break;
case 4:
// Relay 5: Positive logic (HIGH = ON) - GPIO15
digitalWrite(15, state ? HIGH : LOW);
break;
default:
break;
}
relayStates[index] = state;
}
void initPins() {
// Built-in LED on GPIO25
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
// Initialize relay pins with proper initial states
pinMode(11, OUTPUT);
digitalWrite(11, HIGH); // Relay 1 OFF (negative logic)
pinMode(12, OUTPUT);
digitalWrite(12, LOW); // Relay 2 OFF (positive logic)
pinMode(13, OUTPUT);
digitalWrite(13, LOW); // Relay 3 OFF (positive logic)
pinMode(14, OUTPUT);
digitalWrite(14, LOW); // Relay 4 OFF (positive logic)
pinMode(15, OUTPUT);
digitalWrite(15, LOW); // Relay 5 OFF (positive logic)
Serial.println("");
Serial.println("[INIT] Pins initialized");
Serial.println("[RELAY] GPIO mappings (FULLY INDEPENDENT):");
Serial.println(" Relay1: GPIO11 (Physical Pin 15)");
Serial.println(" Relay2: GPIO12 (Physical Pin 16)");
Serial.println(" Relay3: GPIO13 (Physical Pin 17)");
Serial.println(" Relay4: GPIO14 (Physical Pin 19)");
Serial.println(" Relay5: GPIO15 (Physical Pin 20)");
Serial.println("");
Serial.println("[RELAY] Custom relay names:");
for (int i = 0; i < 5; i++) {
Serial.printf(" Relay %d: %s\n", i+1, relayNames[i]);
}
Serial.println("");
Serial.println("[INFO] All relay pins are completely independent!");
Serial.println("[INFO] No cross-talk or internal conflicts with these pins.");
}
void readSensor() {
temperature = -999;
humidity = -999;
Serial.println("[SENSOR] No sensor configured - sending error code -999");
}
bool connectToWiFi() {
if (WiFi.status() == WL_CONNECTED) return true;
Serial.print("[WiFi] Connecting to ");
Serial.print(WIFI_SSID);
Serial.print("... ");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 40) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println(" Connected!");
Serial.printf("[WiFi] IP: %s\n", WiFi.localIP().toString().c_str());
wifiRetryCount = 0;
return true;
}
Serial.println(" Failed!");
wifiRetryCount++;
return false;
}
void registerDevice() {
if (currentState != STATE_PROVISIONING) return;
if (registrationTokenUsed) return;
if (registrationRetryCount >= MAX_REGISTRATION_RETRIES) {
enterErrorState("Registration failed");
return;
}
if (WiFi.status() != WL_CONNECTED) return;
Serial.println("[REG] Registering...");
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
String url = "https://" + String(serverHost) + "/api/device/register/";
JsonDocument doc;
doc["token"] = REGISTRATION_TOKEN;
doc["device_id"] = deviceId;
doc["mac_address"] = macAddress;
doc["ip"] = WiFi.localIP().toString();
String jsonString;
serializeJson(doc, jsonString);
if (http.begin(client, url)) {
http.addHeader("Content-Type", "application/json");
int code = http.POST(jsonString);
Serial.printf("[REG] Response: %d\n", code);
if (code == 200) {
String payload = http.getString();
JsonDocument response;
deserializeJson(response, payload);
if (response["success"]) {
permanentToken = response["token"].as<String>();
deviceRegistered = true;
registrationTokenUsed = true;
currentState = STATE_REGISTERED;
saveTokenToEEPROM(permanentToken);
markRegistrationUsed();
Serial.println("[REG] ✓ Registered!");
registrationRetryCount = 0;
getSession();
}
} else if (code == 429) {
registrationRetryCount++;
}
http.end();
}
}
void getSession() {
if (WiFi.status() != WL_CONNECTED) return;
if (permanentToken.length() == 0) return;
Serial.println("[SESSION] Getting session...");
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
String url = "https://" + String(serverHost) + "/api/device/state/";
url += "?token=" + permanentToken;
url += "&device_id=" + deviceId;
url += "&ip=" + WiFi.localIP().toString();
if (http.begin(client, url)) {
int code = http.GET();
if (code == 200) {
String payload = http.getString();
JsonDocument doc;
deserializeJson(doc, payload);
if (doc["session"].is<String>()) {
sessionId = doc["session"].as<String>();
sessionValid = true;
saveSessionToEEPROM(sessionId);
Serial.printf("[SESSION] Got session: %s\n", sessionId.c_str());
}
}
http.end();
}
}
void updateDeviceState() {
if (WiFi.status() != WL_CONNECTED) return;
if (!deviceRegistered) return;
if (currentState != STATE_REGISTERED) return;
readSensor();
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
String url = "https://" + String(serverHost) + "/api/device/state/";
if (sessionValid && sessionId.length() > 0) {
url += "?session=" + sessionId;
} else if (permanentToken.length() > 0) {
url += "?token=" + permanentToken;
} else {
return;
}
url += "&device_id=" + deviceId;
url += "&s=" + String(packRelays(), HEX);
int tempInt = (int)(temperature * 10);
url += "&t=" + String(tempInt);
if (humidity >= 0 && humidity <= 100) {
url += "&h=" + String((int)humidity);
}
url += "&ip=" + WiFi.localIP().toString();
if (http.begin(client, url)) {
http.setTimeout(5000);
int code = http.GET();
if (code == 200) {
String payload = http.getString();
JsonDocument doc;
deserializeJson(doc, payload);
if (doc["session"].is<String>()) {
sessionId = doc["session"].as<String>();
sessionValid = true;
saveSessionToEEPROM(sessionId);
}
// Process relay states independently
bool relayChanged = false;
for (int i = 0; i < 5; i++) {
String key = "relay" + String(i+1);
if (doc[key].is<bool>()) {
bool newState = doc[key];
if (newState != relayStates[i]) {
setRelay(i, newState);
relayChanged = true;
Serial.printf("[RELAY] %s -> %s\n", relayNames[i], newState ? "ON" : "OFF");
}
}
}
// Also handle packed format as backup
if (!relayChanged && doc["s"].is<String>()) {
uint8_t serverState = (uint8_t)strtol(doc["s"], NULL, 16);
uint8_t currentRelayState = packRelays();
if (serverState != currentRelayState) {
unpackRelays(serverState);
}
}
// Blink LED briefly to show activity
digitalWrite(LED_PIN, HIGH);
delay(20);
digitalWrite(LED_PIN, LOW);
} else if (code == 401) {
Serial.println("[STATE] Token invalid");
deviceRegistered = false;
sessionValid = false;
permanentToken = "";
sessionId = "";
clearAllStorage();
currentState = STATE_PROVISIONING;
}
http.end();
}
}
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("");
Serial.println("########################################");
Serial.println("# OceanicRemote v5.4 - Pico W #");
Serial.println("# INDEPENDENT PINS: 11,12,13,14,15 #");
Serial.println("########################################");
initEEPROM();
randomSeed(analogRead(28) + micros());
deviceId = generateDeviceId();
initPins();
Serial.printf("[SYSTEM] Device ID: %s\n", deviceId.c_str());
Serial.printf("[SYSTEM] Server: %s\n", serverHost);
Serial.printf("[SYSTEM] WiFi SSID: %s\n", WIFI_SSID);
Serial.printf("[SYSTEM] Polling: %d ms\n", BASE_POLL_INTERVAL);
String savedToken = loadTokenFromEEPROM();
if (savedToken.length() > 0) {
permanentToken = savedToken;
deviceRegistered = true;
currentState = STATE_REGISTERED;
Serial.printf("[BOOT] Found saved token\n");
String savedSession = loadSessionFromEEPROM();
if (savedSession.length() > 0) {
sessionId = savedSession;
sessionValid = true;
Serial.printf("[BOOT] Found saved session: %s\n", sessionId.c_str());
}
} else {
Serial.println("[BOOT] No token - will register as NEW device");
currentState = STATE_PROVISIONING;
registrationTokenUsed = false;
registrationRetryCount = 0;
}
Serial.println("[PHASE 1] Connecting to WiFi...");
if (!connectToWiFi()) {
Serial.println("[ERROR] Cannot connect to WiFi - restarting");
delay(5000);
rp2040.restart();
}
Serial.println("[PHASE 2] Establishing server connection...");
if (!deviceRegistered && currentState == STATE_PROVISIONING) {
registerDevice();
}
if (deviceRegistered && currentState == STATE_REGISTERED) {
if (!sessionValid || sessionId.length() == 0) {
getSession();
}
updateDeviceState();
}
nextCheckTime = millis() + random(0, 5000);
Serial.println("[PHASE 3] Entering main loop...");
}
void loop() {
if (currentState == STATE_ERROR) {
delay(5000);
return;
}
unsigned long currentMillis = millis();
if (WiFi.status() == WL_CONNECTED) {
wifiRetryCount = 0;
if (currentMillis >= nextCheckTime) {
int jitter = random(-JITTER_RANGE_MS, JITTER_RANGE_MS);
nextCheckTime = currentMillis + BASE_POLL_INTERVAL + jitter;
if (deviceRegistered && currentState == STATE_REGISTERED) {
updateDeviceState();
}
}
} else {
if (currentMillis - lastWiFiReconnect >= wifiReconnectInterval) {
lastWiFiReconnect = currentMillis;
if (wifiRetryCount < MAX_WIFI_RETRIES) {
Serial.print("[WiFi] Reconnecting... ");
connectToWiFi();
} else {
Serial.println("[WiFi] Max retries - restarting");
delay(1000);
rp2040.restart();
}
}
}
delay(10);
}