Mains voltage (230 V) — can kill. The low-voltage side (board, sensors, display, relay module) is safe to build yourself. The mains side must be wired by a licensed electrician, and tested with the circuit switched off at the MCB.
- Never switch a motor with a relay module. Motors pull several times their running current at start-up; hobby relay contacts weld or overheat. The relay only drives a contactor’s coil (or your pump starter’s coil input).
- Contactor rated for motors (AC-3) at or above the load’s nameplate current.
- Protect and earth: MCB on live, RCCB/ELCB on the circuit, metal bodies earthed. Switch live only — never neutral or earth.
- Keep the two worlds apart: mains and low-voltage wiring in separate compartments of a closed, non-metallic enclosure; no bare 230 V terminals.
- Fail safe: the code switches the load off when a sensor stops answering. Still fit a manual override at the starter.
Parts list
| Part | Qty | Approx. | Buy |
|---|---|---|---|
| Arduino Uno R3 (CH340 clone) | 1 | ₹450 | Buy Arduino Uno R3 (CH340 clone) |
| SIM800L GSM module + 2G-capable SIM | 1 | ₹350 | Buy SIM800L GSM module + 2G-capable SIM |
| LM2596 buck module set to 4.0 V (SIM800L supply) | 1 | ₹90 | Buy LM2596 buck module set to 4.0 V (SIM800L supply) |
| 1000 µF / 10 V capacitor across the SIM800L supply | 1 | ₹10 | Buy 1000 µF / 10 V capacitor across the SIM800L supply |
| Resistors 10 kΩ + 20 kΩ (RX divider) | 1 | ₹5 | Buy Resistors 10 kΩ + 20 kΩ (RX divider) |
| 1-channel 5 V relay module | 1 | ₹70 | Buy 1-channel 5 V relay module |
| Contactor, 230 V AC coil, AC-3 rated ≥ motor current | 1 | ₹650 | Buy Contactor, 230 V AC coil, AC-3 rated ≥ motor current |
| 12 V 2 A adapter (feeds the buck and the Uno’s barrel jack) | 1 | ₹220 | Buy 12 V 2 A adapter (feeds the buck and the Uno’s barrel jack) |
| IP65 enclosure with separate mains and low-voltage sections | 1 | ₹300 | Buy IP65 enclosure with separate mains and low-voltage sections |
| Total | ₹2,145 |
Affiliate disclosure: “Buy” links may earn FlashReady a small commission at no extra cost to you. Prices are indicative.
Wiring
| Arduino Uno R3 | Part · pin | Wire | Notes |
|---|---|---|---|
| D2 | SIM800L GSM/GPRS module (2G) · TXD | green | SoftwareSerial RX on the Uno |
| D3 | SIM800L GSM/GPRS module (2G) · RXD | yellow | Via 10 kΩ / 20 kΩ divider — The SIM800L’s serial input is about 2.8 V; the divider brings the Uno’s 5 V down |
| LM2596 buck set to 4.0 V · + | SIM800L GSM/GPRS module (2G) · VCC | red | 4.0 V / 2 A buck fed from the 12 V adapter; 1000 µF across VCC/GND at the module |
| LM2596 buck set to 4.0 V · - | SIM800L GSM/GPRS module (2G) · GND | black | |
| GND | SIM800L GSM/GPRS module (2G) · GND | black | Common ground: Uno GND and buck − both to SIM800L GND |
| D7 | 1-channel relay module (5 V coil) · IN | orange | Relay COM/NO switch the contactor coil (or starter coil input) — never the motor |
| 5V | 1-channel relay module (5 V coil) · DC+ | red | |
| GND | 1-channel relay module (5 V coil) · DC− | black | |
| Contactor coil circuit, 230 V (electrician) · L | 1-channel relay module (5 V coil) · COM | brown | Live from the MCB to COM — 230 V, wired by an electrician |
| Contactor coil circuit, 230 V (electrician) · A1 | 1-channel relay module (5 V coil) · NO | brown | NO to the contactor coil A1; coil A2 goes to neutral |
Board quirks: The SIM800L draws up to 2 A in short bursts. Powered from the Uno’s 5V pin it resets in a loop — use a 4.0 V buck module with a large capacitor right at the module. SIM800L is a 2G (GSM) module. Check your operator still runs 2G in your village before you build; many areas still do, some are switching it off. Only the owner number (the one you give the flasher, or OWNER in the code) is obeyed. Use the full +91 format exactly as your operator shows it in the SMS header. Three-phase motors and submersible pumps already have a starter: wire the relay to the starter’s coil/remote input as its manual shows, not to the motor.
Code
// GSM farm motor control — Arduino Uno R3 + SIM800L + relay module
// SMS "MOTOR ON", "MOTOR OFF" or "STATUS" from the owner's number.
// The relay module drives a CONTACTOR coil (or the motor starter's coil input), never the motor.
// SIM800L: own 4.0 V / 2 A supply, common GND; its RXD gets the Uno's TX through a divider.
#include <SoftwareSerial.h>
#include "fr_settings.h"
constexpr uint8_t SIM_RX = 2; // Uno receives on D2 <- SIM800L TXD
constexpr uint8_t SIM_TX = 3; // Uno sends on D3 -> divider -> SIM800L RXD
constexpr uint8_t RELAY_PIN = 7; // relay module IN
constexpr bool RELAY_ACTIVE_LOW = true;
constexpr unsigned long MAX_RUN_MS = 2UL * 60UL * 60UL * 1000UL; // auto-off after 2 hours
const char* OWNER = "+91XXXXXXXXXX"; // only this number can control the motor
// Filled in from the FlashReady flasher over USB (kept in EEPROM), or type the values above.
FrSetting SETTINGS[] = {{"owner_phone", &OWNER, true}};
SoftwareSerial sim(SIM_RX, SIM_TX);
bool motorOn = false;
unsigned long startedAt = 0;
String line;
String sender;
bool expectBody = false;
void setMotor(bool on) {
motorOn = on;
digitalWrite(RELAY_PIN, (on != RELAY_ACTIVE_LOW) ? HIGH : LOW);
if (on) startedAt = millis();
}
void command(const char* cmd, unsigned long waitMs) {
sim.println(cmd);
unsigned long t = millis();
while (millis() - t < waitMs) {
while (sim.available()) Serial.write(sim.read());
}
}
void sendSms(const String& to, const String& text) {
if (!to.length()) return;
sim.print(F("AT+CMGS=\""));
sim.print(to);
sim.println('"');
delay(500);
sim.print(text);
sim.write(26); // Ctrl+Z sends the message
delay(4000);
}
void handleSms(String body) {
body.trim();
body.toUpperCase();
if (!*OWNER || sender != OWNER) { // no owner number set = nobody can switch the motor
Serial.println(F("Ignored an SMS from an unknown number"));
return;
}
if (body == "MOTOR ON") {
setMotor(true);
sendSms(sender, F("Motor ON. It switches off by itself after 2 hours."));
} else if (body == "MOTOR OFF") {
setMotor(false);
sendSms(sender, F("Motor OFF."));
} else if (body == "STATUS") {
sendSms(sender, motorOn ? F("Motor is ON.") : F("Motor is OFF."));
}
}
void setup() {
digitalWrite(RELAY_PIN, RELAY_ACTIVE_LOW ? HIGH : LOW); // OFF before it becomes an output
pinMode(RELAY_PIN, OUTPUT);
Serial.begin(115200);
frSettingsBegin(SETTINGS);
if (!*OWNER) Serial.println(F("SMS control off — no owner number set. Flash from flashready.in or type it into the code."));
sim.begin(9600);
delay(5000); // give the SIM800L time to register on the network
command("AT", 500);
command("AT+CMGF=1", 500); // text mode
command("AT+CNMI=2,2,0,0,0", 500); // push new SMS straight to the serial port
Serial.println(F("Ready for SMS commands"));
}
void loop() {
while (sim.available()) {
char c = sim.read();
if (c == '\r') continue;
if (c != '\n') {
if (line.length() < 160) line += c;
continue;
}
if (line.startsWith("+CMT:")) {
int a = line.indexOf('"');
int b = line.indexOf('"', a + 1);
sender = (a >= 0 && b > a) ? line.substring(a + 1, b) : "";
expectBody = true;
} else if (expectBody) {
handleSms(line);
expectBody = false;
}
line = "";
}
if (motorOn && millis() - startedAt > MAX_RUN_MS) {
setMotor(false);
sendSms(OWNER, F("Motor switched off after 2 hours (safety limit)."));
}
}// GSM farm motor control — Arduino Uno R3 + SIM800L + relay module
// SMS "MOTOR ON", "MOTOR OFF" or "STATUS" from the owner's number.
// The relay module drives a CONTACTOR coil (or the motor starter's coil input), never the motor.
// SIM800L: own 4.0 V / 2 A supply, common GND; its RXD gets the Uno's TX through a divider.
#include <SoftwareSerial.h>
#include "fr_settings.h"
constexpr uint8_t SIM_RX = 2; // Uno receives on D2 <- SIM800L TXD
constexpr uint8_t SIM_TX = 3; // Uno sends on D3 -> divider -> SIM800L RXD
constexpr uint8_t RELAY_PIN = 7; // relay module IN
constexpr bool RELAY_ACTIVE_LOW = true;
constexpr unsigned long MAX_RUN_MS = 2UL * 60UL * 60UL * 1000UL; // auto-off after 2 hours
const char* OWNER = "+91XXXXXXXXXX"; // only this number can control the motor
// Filled in from the FlashReady flasher over USB (kept in EEPROM), or type the values above.
FrSetting SETTINGS[] = {{"owner_phone", &OWNER, true}};
SoftwareSerial sim(SIM_RX, SIM_TX);
bool motorOn = false;
unsigned long startedAt = 0;
String line;
String sender;
bool expectBody = false;
void setMotor(bool on) {
motorOn = on;
digitalWrite(RELAY_PIN, (on != RELAY_ACTIVE_LOW) ? HIGH : LOW);
if (on) startedAt = millis();
}
void command(const char* cmd, unsigned long waitMs) {
sim.println(cmd);
unsigned long t = millis();
while (millis() - t < waitMs) {
while (sim.available()) Serial.write(sim.read());
}
}
void sendSms(const String& to, const String& text) {
if (!to.length()) return;
sim.print(F("AT+CMGS=\""));
sim.print(to);
sim.println('"');
delay(500);
sim.print(text);
sim.write(26); // Ctrl+Z sends the message
delay(4000);
}
void handleSms(String body) {
body.trim();
body.toUpperCase();
if (!*OWNER || sender != OWNER) { // no owner number set = nobody can switch the motor
Serial.println(F("Ignored an SMS from an unknown number"));
return;
}
if (body == "MOTOR ON") {
setMotor(true);
sendSms(sender, F("Motor ON. It switches off by itself after 2 hours."));
} else if (body == "MOTOR OFF") {
setMotor(false);
sendSms(sender, F("Motor OFF."));
} else if (body == "STATUS") {
sendSms(sender, motorOn ? F("Motor is ON.") : F("Motor is OFF."));
}
}
void setup() {
digitalWrite(RELAY_PIN, RELAY_ACTIVE_LOW ? HIGH : LOW); // OFF before it becomes an output
pinMode(RELAY_PIN, OUTPUT);
Serial.begin(115200);
frSettingsBegin(SETTINGS);
if (!*OWNER) Serial.println(F("SMS control off — no owner number set. Flash from flashready.in or type it into the code."));
sim.begin(9600);
delay(5000); // give the SIM800L time to register on the network
command("AT", 500);
command("AT+CMGF=1", 500); // text mode
command("AT+CNMI=2,2,0,0,0", 500); // push new SMS straight to the serial port
Serial.println(F("Ready for SMS commands"));
}
void loop() {
while (sim.available()) {
char c = sim.read();
if (c == '\r') continue;
if (c != '\n') {
if (line.length() < 160) line += c;
continue;
}
if (line.startsWith("+CMT:")) {
int a = line.indexOf('"');
int b = line.indexOf('"', a + 1);
sender = (a >= 0 && b > a) ? line.substring(a + 1, b) : "";
expectBody = true;
} else if (expectBody) {
handleSms(line);
expectBody = false;
}
line = "";
}
if (motorOn && millis() - startedAt > MAX_RUN_MS) {
setMotor(false);
sendSms(OWNER, F("Motor switched off after 2 hours (safety limit)."));
}
}Arduino IDE: keep fr_settings.h in the same folder as the .ino (tab menu → New Tab, same name). Flashing from this page doesn’t need it.
// fr_settings.h — FlashReady on-board settings (Wi-Fi name, phone number, ...) over USB.
//
// Why: the pre-built file on flashready.in is the same for everyone, so your own settings
// can't be inside it. Instead the board asks for them over USB serial and keeps them in
// its EEPROM. If you use Arduino IDE you can still type them into the constants in the
// sketch — this file then changes nothing except a 2-second pause at start-up.
//
// The protocol, in plain words (one text line each way, ending in \n, at 115200 baud):
// 1. Right after Serial.begin(115200) the board prints
// FR?SETTINGS v1 keys=wifi_ssid,wifi_pass missing=wifi_pass
// "missing" lists the required settings that still have no real value.
// 2. If nothing is missing it listens for 2 seconds and then carries on. If something is
// missing it keeps listening, printing the line again every 2 seconds, for up to
// 30 seconds; then it carries on anyway and the sketch skips whatever needs the
// missing value (an alarm still sounds, it just can't send the SMS).
// 3. The flasher answers FR!SET wifi_ssid=My%20Home&wifi_pass=secret
// (values URL-encoded). The board stores them and answers FR!OK, then carries on with
// the new values. Errors: FR!ERR unknown <key>, FR!ERR long <key> (nothing is stored).
// A key that is left out keeps its stored value; "key=" (empty) removes it.
// "FR!SET" with nothing after it means "carry on now": FR!OK, nothing is stored.
// 4. FR!CLEAR wipes everything that was stored, answers FR!OK and keeps listening.
// Other lines are ignored.
//
// A stored value wins over the constant in the sketch. A constant that is still a
// placeholder ("YOUR_SSID", "your-wifi", "+91XXXXXXXXXX") or empty counts as not set.
//
// EEPROM block at FR_SETTINGS_ADDR: 'F' 'R' 0x01, length (uint16, low byte first), then
// "key=value\n" lines. Uses FR_SETTINGS_SIZE bytes; move FR_SETTINGS_ADDR with a #define
// before the #include if your sketch uses EEPROM for something else.
//
// Use in a sketch:
// const char* WIFI_SSID = "YOUR_SSID";
// FrSetting SETTINGS[] = { {"wifi_ssid", &WIFI_SSID, true} }; // key, constant, required
// void setup() { Serial.begin(115200); frSettingsBegin(SETTINGS); ... WiFi.begin(WIFI_SSID, ...); }
// After frSettingsBegin() each constant points at the value to use ("" if none is set).
#pragma once
#include <Arduino.h>
#include <EEPROM.h>
#include <string.h>
#ifndef FR_SETTINGS_ADDR
#define FR_SETTINGS_ADDR 0
#endif
#ifndef FR_SETTINGS_SIZE
#if defined(__AVR__)
#define FR_SETTINGS_SIZE 96 // 2 KB of RAM on an Uno: a phone number fits easily
#else
#define FR_SETTINGS_SIZE 512
#endif
#endif
#ifndef FR_LINE_MAX
#if defined(__AVR__)
#define FR_LINE_MAX 128
#else
#define FR_LINE_MAX 1024
#endif
#endif
#define FR_MAX_KEYS 8
#define FR_WAIT_MAX_MS 30000UL // never wait longer than this for missing settings
#define FR_DATA_MAX (FR_SETTINGS_SIZE - 5)
struct FrSetting {
const char* key; // [a-z0-9_], as in settings.json
const char** value; // the sketch's constant; replaced by the value to use
bool required;
};
// Stored settings in RAM as "key=value\0key=value\0" (only non-empty values).
static char frData[FR_DATA_MAX + 1];
static uint16_t frLen = 0;
static inline void frEepromBegin() {
#if defined(ESP32) || defined(ESP8266)
static bool started = false;
if (!started) EEPROM.begin(FR_SETTINGS_ADDR + FR_SETTINGS_SIZE);
started = true;
#endif
}
static inline void frLoad() {
frEepromBegin();
frLen = 0;
frData[0] = 0;
if (EEPROM.read(FR_SETTINGS_ADDR) != 'F' || EEPROM.read(FR_SETTINGS_ADDR + 1) != 'R' || EEPROM.read(FR_SETTINGS_ADDR + 2) != 1) return;
uint16_t n = EEPROM.read(FR_SETTINGS_ADDR + 3) | (EEPROM.read(FR_SETTINGS_ADDR + 4) << 8);
if (n > FR_DATA_MAX) return;
for (uint16_t i = 0; i < n; i++) {
char c = EEPROM.read(FR_SETTINGS_ADDR + 5 + i);
frData[i] = c == '\n' ? '\0' : c;
}
frData[n] = 0;
frLen = n;
}
static inline void frPut(int addr, uint8_t b) {
#if defined(__AVR__)
EEPROM.update(addr, b);
#else
EEPROM.write(addr, b);
#endif
}
static inline void frSave() {
frEepromBegin();
frPut(FR_SETTINGS_ADDR, 'F');
frPut(FR_SETTINGS_ADDR + 1, 'R');
frPut(FR_SETTINGS_ADDR + 2, 1);
frPut(FR_SETTINGS_ADDR + 3, frLen & 0xFF);
frPut(FR_SETTINGS_ADDR + 4, frLen >> 8);
for (uint16_t i = 0; i < frLen; i++) frPut(FR_SETTINGS_ADDR + 5 + i, frData[i] ? frData[i] : '\n');
#if defined(ESP32) || defined(ESP8266)
EEPROM.commit();
#endif
}
// The stored value for key, or nullptr.
static inline const char* frStored(const char* key) {
size_t kl = strlen(key);
for (uint16_t i = 0; i < frLen; i += strlen(frData + i) + 1) {
if (strncmp(frData + i, key, kl) == 0 && frData[i + kl] == '=') return frData + i + kl + 1;
}
return nullptr;
}
// Empty, "your_…" / "YOUR-…" / "your …", or a "+91XXXX…" phone number.
static inline bool frIsPlaceholder(const char* v) {
if (!v || !*v) return true;
if (strncasecmp(v, "your", 4) == 0 && (v[4] == '_' || v[4] == '-' || v[4] == ' ')) return true;
return v[0] == '+' && strstr(v, "XXXX") != nullptr;
}
// The stored value for key, else the fallback constant; "" if that is a placeholder.
static inline const char* frSettingsGet(const char* key, const char* fallback) {
const char* v = frStored(key);
if (!v) v = fallback;
return frIsPlaceholder(v) ? "" : v;
}
static inline bool frIsMissing(const FrSetting& s) {
return s.required && !*frSettingsGet(s.key, *s.value);
}
static inline void frRequest(FrSetting* s, uint8_t n) {
Serial.print(F("FR?SETTINGS v1 keys="));
for (uint8_t i = 0; i < n; i++) {
if (i) Serial.print(',');
Serial.print(s[i].key);
}
Serial.print(F(" missing="));
bool first = true;
for (uint8_t i = 0; i < n; i++) {
if (!frIsMissing(s[i])) continue;
if (!first) Serial.print(',');
Serial.print(s[i].key);
first = false;
}
Serial.print('\n');
}
static inline void frReply(const __FlashStringHelper* what, const char* key) {
Serial.print(what);
Serial.print(key);
Serial.print('\n');
}
static inline int8_t frHex(char c) {
if (c >= '0' && c <= '9') return c - '0';
c |= 0x20;
return (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1;
}
// Percent-decodes in place. Drops NUL, CR and LF: they can't be stored.
static inline void frDecode(char* s) {
char* o = s;
for (; *s; s++) {
char c = *s;
if (c == '%' && frHex(s[1]) >= 0 && frHex(s[2]) >= 0) {
c = (char)(frHex(s[1]) << 4 | frHex(s[2]));
s += 2;
}
if (c != '\0' && c != '\n' && c != '\r') *o++ = c;
}
*o = 0;
}
// "k1=v1&k2=v2" (cut short if the line overflowed). Saves and returns true, or replies with an error.
static inline bool frSet(FrSetting* s, uint8_t n, char* p, bool overflow) {
const char* upd[FR_MAX_KEYS] = {};
while (*p) {
char* amp = strchr(p, '&');
bool last = !amp;
if (amp) *amp = 0;
char* eq = strchr(p, '=');
if (eq) *eq = 0;
if (*p) {
if (last && overflow) {
frReply(F("FR!ERR long "), p);
return false;
}
uint8_t i = 0;
while (i < n && strcmp(s[i].key, p) != 0) i++;
if (i == n) {
frReply(F("FR!ERR unknown "), p);
return false;
}
char* v = eq ? eq + 1 : p + strlen(p); // "key" alone = "key="
frDecode(v);
upd[i] = v;
}
if (last) break;
p = amp + 1;
}
// Build the new block: for each key, the new value or the one already stored.
char next[FR_DATA_MAX + 1];
uint16_t len = 0;
for (uint8_t i = 0; i < n; i++) {
const char* v = upd[i] ? upd[i] : frStored(s[i].key);
if (!v || !*v) continue;
size_t kl = strlen(s[i].key), vl = strlen(v);
if (len + kl + vl + 2 > FR_DATA_MAX) {
frReply(F("FR!ERR long "), s[i].key);
return false;
}
memcpy(next + len, s[i].key, kl);
next[len + kl] = '=';
memcpy(next + len + kl + 1, v, vl + 1);
len += kl + vl + 2;
}
memcpy(frData, next, len);
frData[len] = 0;
frLen = len;
frSave();
return true;
}
// Handles one received line. Returns true once settings were saved (boot carries on).
static inline bool frCommand(FrSetting* s, uint8_t n, char* line, bool overflow) {
if (strcmp(line, "FR!CLEAR") == 0) {
frLen = 0;
frData[0] = 0;
frSave();
Serial.print(F("FR!OK\n"));
return false;
}
if (strncmp(line, "FR!SET", 6) != 0 || (line[6] != ' ' && line[6] != 0)) return false; // not for us
char* p = line + 6;
while (*p == ' ') p++;
if (*p && !frSet(s, n, p, overflow)) return false; // empty FR!SET = carry on, store nothing
Serial.print(F("FR!OK\n"));
return true;
}
// Call right after Serial.begin(115200). Prints the request, waits as described at the top
// (30 s at most), then points every constant in the list at the value to use ("" = not set).
static inline void frSettingsBegin(FrSetting* s, uint8_t n) {
frLoad();
char line[FR_LINE_MAX];
uint16_t len = 0;
bool overflow = false;
frRequest(s, n);
unsigned long start = millis(), quietSince = start;
while (millis() - start < FR_WAIT_MAX_MS) {
int c = Serial.read();
if (c >= 0) {
quietSince = millis();
if (c == '\r') continue;
if (c != '\n') {
if (len < FR_LINE_MAX - 1) line[len++] = (char)c;
else overflow = true;
continue;
}
line[len] = 0;
bool saved = frCommand(s, n, line, overflow);
len = 0;
overflow = false;
if (saved) break;
continue;
}
if (millis() - quietSince < 2000) {
delay(1); // also keeps the ESP8266 watchdog fed
continue;
}
len = 0; // drop half a line
overflow = false;
bool missing = false;
for (uint8_t i = 0; i < n; i++) missing |= frIsMissing(s[i]);
if (!missing) break;
frRequest(s, n);
quietSince = millis();
}
for (uint8_t i = 0; i < n; i++) *s[i].value = frSettingsGet(s[i].key, *s[i].value);
}
template <size_t N>
static inline void frSettingsBegin(FrSetting (&s)[N]) {
static_assert(N <= FR_MAX_KEYS, "fr_settings.h: at most FR_MAX_KEYS settings");
frSettingsBegin(s, (uint8_t)N);
}// fr_settings.h — FlashReady on-board settings (Wi-Fi name, phone number, ...) over USB.
//
// Why: the pre-built file on flashready.in is the same for everyone, so your own settings
// can't be inside it. Instead the board asks for them over USB serial and keeps them in
// its EEPROM. If you use Arduino IDE you can still type them into the constants in the
// sketch — this file then changes nothing except a 2-second pause at start-up.
//
// The protocol, in plain words (one text line each way, ending in \n, at 115200 baud):
// 1. Right after Serial.begin(115200) the board prints
// FR?SETTINGS v1 keys=wifi_ssid,wifi_pass missing=wifi_pass
// "missing" lists the required settings that still have no real value.
// 2. If nothing is missing it listens for 2 seconds and then carries on. If something is
// missing it keeps listening, printing the line again every 2 seconds, for up to
// 30 seconds; then it carries on anyway and the sketch skips whatever needs the
// missing value (an alarm still sounds, it just can't send the SMS).
// 3. The flasher answers FR!SET wifi_ssid=My%20Home&wifi_pass=secret
// (values URL-encoded). The board stores them and answers FR!OK, then carries on with
// the new values. Errors: FR!ERR unknown <key>, FR!ERR long <key> (nothing is stored).
// A key that is left out keeps its stored value; "key=" (empty) removes it.
// "FR!SET" with nothing after it means "carry on now": FR!OK, nothing is stored.
// 4. FR!CLEAR wipes everything that was stored, answers FR!OK and keeps listening.
// Other lines are ignored.
//
// A stored value wins over the constant in the sketch. A constant that is still a
// placeholder ("YOUR_SSID", "your-wifi", "+91XXXXXXXXXX") or empty counts as not set.
//
// EEPROM block at FR_SETTINGS_ADDR: 'F' 'R' 0x01, length (uint16, low byte first), then
// "key=value\n" lines. Uses FR_SETTINGS_SIZE bytes; move FR_SETTINGS_ADDR with a #define
// before the #include if your sketch uses EEPROM for something else.
//
// Use in a sketch:
// const char* WIFI_SSID = "YOUR_SSID";
// FrSetting SETTINGS[] = { {"wifi_ssid", &WIFI_SSID, true} }; // key, constant, required
// void setup() { Serial.begin(115200); frSettingsBegin(SETTINGS); ... WiFi.begin(WIFI_SSID, ...); }
// After frSettingsBegin() each constant points at the value to use ("" if none is set).
#pragma once
#include <Arduino.h>
#include <EEPROM.h>
#include <string.h>
#ifndef FR_SETTINGS_ADDR
#define FR_SETTINGS_ADDR 0
#endif
#ifndef FR_SETTINGS_SIZE
#if defined(__AVR__)
#define FR_SETTINGS_SIZE 96 // 2 KB of RAM on an Uno: a phone number fits easily
#else
#define FR_SETTINGS_SIZE 512
#endif
#endif
#ifndef FR_LINE_MAX
#if defined(__AVR__)
#define FR_LINE_MAX 128
#else
#define FR_LINE_MAX 1024
#endif
#endif
#define FR_MAX_KEYS 8
#define FR_WAIT_MAX_MS 30000UL // never wait longer than this for missing settings
#define FR_DATA_MAX (FR_SETTINGS_SIZE - 5)
struct FrSetting {
const char* key; // [a-z0-9_], as in settings.json
const char** value; // the sketch's constant; replaced by the value to use
bool required;
};
// Stored settings in RAM as "key=value\0key=value\0" (only non-empty values).
static char frData[FR_DATA_MAX + 1];
static uint16_t frLen = 0;
static inline void frEepromBegin() {
#if defined(ESP32) || defined(ESP8266)
static bool started = false;
if (!started) EEPROM.begin(FR_SETTINGS_ADDR + FR_SETTINGS_SIZE);
started = true;
#endif
}
static inline void frLoad() {
frEepromBegin();
frLen = 0;
frData[0] = 0;
if (EEPROM.read(FR_SETTINGS_ADDR) != 'F' || EEPROM.read(FR_SETTINGS_ADDR + 1) != 'R' || EEPROM.read(FR_SETTINGS_ADDR + 2) != 1) return;
uint16_t n = EEPROM.read(FR_SETTINGS_ADDR + 3) | (EEPROM.read(FR_SETTINGS_ADDR + 4) << 8);
if (n > FR_DATA_MAX) return;
for (uint16_t i = 0; i < n; i++) {
char c = EEPROM.read(FR_SETTINGS_ADDR + 5 + i);
frData[i] = c == '\n' ? '\0' : c;
}
frData[n] = 0;
frLen = n;
}
static inline void frPut(int addr, uint8_t b) {
#if defined(__AVR__)
EEPROM.update(addr, b);
#else
EEPROM.write(addr, b);
#endif
}
static inline void frSave() {
frEepromBegin();
frPut(FR_SETTINGS_ADDR, 'F');
frPut(FR_SETTINGS_ADDR + 1, 'R');
frPut(FR_SETTINGS_ADDR + 2, 1);
frPut(FR_SETTINGS_ADDR + 3, frLen & 0xFF);
frPut(FR_SETTINGS_ADDR + 4, frLen >> 8);
for (uint16_t i = 0; i < frLen; i++) frPut(FR_SETTINGS_ADDR + 5 + i, frData[i] ? frData[i] : '\n');
#if defined(ESP32) || defined(ESP8266)
EEPROM.commit();
#endif
}
// The stored value for key, or nullptr.
static inline const char* frStored(const char* key) {
size_t kl = strlen(key);
for (uint16_t i = 0; i < frLen; i += strlen(frData + i) + 1) {
if (strncmp(frData + i, key, kl) == 0 && frData[i + kl] == '=') return frData + i + kl + 1;
}
return nullptr;
}
// Empty, "your_…" / "YOUR-…" / "your …", or a "+91XXXX…" phone number.
static inline bool frIsPlaceholder(const char* v) {
if (!v || !*v) return true;
if (strncasecmp(v, "your", 4) == 0 && (v[4] == '_' || v[4] == '-' || v[4] == ' ')) return true;
return v[0] == '+' && strstr(v, "XXXX") != nullptr;
}
// The stored value for key, else the fallback constant; "" if that is a placeholder.
static inline const char* frSettingsGet(const char* key, const char* fallback) {
const char* v = frStored(key);
if (!v) v = fallback;
return frIsPlaceholder(v) ? "" : v;
}
static inline bool frIsMissing(const FrSetting& s) {
return s.required && !*frSettingsGet(s.key, *s.value);
}
static inline void frRequest(FrSetting* s, uint8_t n) {
Serial.print(F("FR?SETTINGS v1 keys="));
for (uint8_t i = 0; i < n; i++) {
if (i) Serial.print(',');
Serial.print(s[i].key);
}
Serial.print(F(" missing="));
bool first = true;
for (uint8_t i = 0; i < n; i++) {
if (!frIsMissing(s[i])) continue;
if (!first) Serial.print(',');
Serial.print(s[i].key);
first = false;
}
Serial.print('\n');
}
static inline void frReply(const __FlashStringHelper* what, const char* key) {
Serial.print(what);
Serial.print(key);
Serial.print('\n');
}
static inline int8_t frHex(char c) {
if (c >= '0' && c <= '9') return c - '0';
c |= 0x20;
return (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1;
}
// Percent-decodes in place. Drops NUL, CR and LF: they can't be stored.
static inline void frDecode(char* s) {
char* o = s;
for (; *s; s++) {
char c = *s;
if (c == '%' && frHex(s[1]) >= 0 && frHex(s[2]) >= 0) {
c = (char)(frHex(s[1]) << 4 | frHex(s[2]));
s += 2;
}
if (c != '\0' && c != '\n' && c != '\r') *o++ = c;
}
*o = 0;
}
// "k1=v1&k2=v2" (cut short if the line overflowed). Saves and returns true, or replies with an error.
static inline bool frSet(FrSetting* s, uint8_t n, char* p, bool overflow) {
const char* upd[FR_MAX_KEYS] = {};
while (*p) {
char* amp = strchr(p, '&');
bool last = !amp;
if (amp) *amp = 0;
char* eq = strchr(p, '=');
if (eq) *eq = 0;
if (*p) {
if (last && overflow) {
frReply(F("FR!ERR long "), p);
return false;
}
uint8_t i = 0;
while (i < n && strcmp(s[i].key, p) != 0) i++;
if (i == n) {
frReply(F("FR!ERR unknown "), p);
return false;
}
char* v = eq ? eq + 1 : p + strlen(p); // "key" alone = "key="
frDecode(v);
upd[i] = v;
}
if (last) break;
p = amp + 1;
}
// Build the new block: for each key, the new value or the one already stored.
char next[FR_DATA_MAX + 1];
uint16_t len = 0;
for (uint8_t i = 0; i < n; i++) {
const char* v = upd[i] ? upd[i] : frStored(s[i].key);
if (!v || !*v) continue;
size_t kl = strlen(s[i].key), vl = strlen(v);
if (len + kl + vl + 2 > FR_DATA_MAX) {
frReply(F("FR!ERR long "), s[i].key);
return false;
}
memcpy(next + len, s[i].key, kl);
next[len + kl] = '=';
memcpy(next + len + kl + 1, v, vl + 1);
len += kl + vl + 2;
}
memcpy(frData, next, len);
frData[len] = 0;
frLen = len;
frSave();
return true;
}
// Handles one received line. Returns true once settings were saved (boot carries on).
static inline bool frCommand(FrSetting* s, uint8_t n, char* line, bool overflow) {
if (strcmp(line, "FR!CLEAR") == 0) {
frLen = 0;
frData[0] = 0;
frSave();
Serial.print(F("FR!OK\n"));
return false;
}
if (strncmp(line, "FR!SET", 6) != 0 || (line[6] != ' ' && line[6] != 0)) return false; // not for us
char* p = line + 6;
while (*p == ' ') p++;
if (*p && !frSet(s, n, p, overflow)) return false; // empty FR!SET = carry on, store nothing
Serial.print(F("FR!OK\n"));
return true;
}
// Call right after Serial.begin(115200). Prints the request, waits as described at the top
// (30 s at most), then points every constant in the list at the value to use ("" = not set).
static inline void frSettingsBegin(FrSetting* s, uint8_t n) {
frLoad();
char line[FR_LINE_MAX];
uint16_t len = 0;
bool overflow = false;
frRequest(s, n);
unsigned long start = millis(), quietSince = start;
while (millis() - start < FR_WAIT_MAX_MS) {
int c = Serial.read();
if (c >= 0) {
quietSince = millis();
if (c == '\r') continue;
if (c != '\n') {
if (len < FR_LINE_MAX - 1) line[len++] = (char)c;
else overflow = true;
continue;
}
line[len] = 0;
bool saved = frCommand(s, n, line, overflow);
len = 0;
overflow = false;
if (saved) break;
continue;
}
if (millis() - quietSince < 2000) {
delay(1); // also keeps the ESP8266 watchdog fed
continue;
}
len = 0; // drop half a line
overflow = false;
bool missing = false;
for (uint8_t i = 0; i < n; i++) missing |= frIsMissing(s[i]);
if (!missing) break;
frRequest(s, n);
quietSince = millis();
}
for (uint8_t i = 0; i < n; i++) *s[i].value = frSettingsGet(s[i].key, *s[i].value);
}
template <size_t N>
static inline void frSettingsBegin(FrSetting (&s)[N]) {
static_assert(N <= FR_MAX_KEYS, "fr_settings.h: at most FR_MAX_KEYS settings");
frSettingsBegin(s, (uint8_t)N);
}Compiled for uno on avr 1.8.8 · 3 Nov 2026
How it works
Walking to the field at night just to start a borewell motor is a daily chore in many villages. This controller lets the owner send an SMS — MOTOR ON, MOTOR OFF or STATUS — and replies with the result.
The Uno talks to a SIM800L GSM module over SoftwareSerial on D2/D3. On start-up it switches the module to text mode and asks it to push every new SMS straight to the serial port (AT+CNMI=2,2,0,0,0). Each message arrives as a +CMT: header line holding the sender’s number, followed by the text. The sketch takes the number from the header and compares it with OWNER. Messages from any other number are ignored.
MOTOR ON energises the relay on D7, which switches the contactor coil (or the remote/coil input of an existing starter). The sketch also starts a 2-hour timer: if nobody sends OFF, the motor stops itself and an SMS explains why. A forgotten motor can drain a borewell or burn out running dry.
The module’s serial input runs at about 2.8 V, so the Uno’s 5 V TX goes through a 10 kΩ / 20 kΩ divider. Its supply is the hard part: a SIM800L pulls up to 2 A in bursts, so it gets its own 4.0 V buck module with a large capacitor, sharing only ground with the Uno.
Troubleshooting
The SIM800L’s LED blinks fast forever
Fast blinking (about once a second) means it isn’t registered. Check the antenna, the SIM (PIN lock off, 2G available) and that the supply holds 3.8–4.2 V during transmit bursts.
SMS arrive but nothing happens
Watch the Serial Monitor at 115200. If you see "Ignored an SMS from an unknown number", the sender format differs from OWNER — copy it exactly from the printed +CMT line.
The Uno resets whenever the module transmits
Shared supply sagging. Give the SIM800L its own 4.0 V buck module and join only the grounds.
The motor runs but the confirmation SMS never comes
Credit or SMS pack exhausted on the SIM, or the reply is sent while the network is busy. Check the balance and retry STATUS.
Upgrades
- Add a current-transformer or dry-run sensor and SMS an alert if the motor runs without water.
- Accept a second authorised number (family member) from a short list.
- Report the supply voltage before starting, so you don’t start the motor on very low voltage.



