Parts list
| Part | Qty | Approx. | Buy |
|---|---|---|---|
| ESP32 DevKit V1 (30-pin) | 1 | ₹380 | Buy ESP32 DevKit V1 (30-pin) |
| 1.8" ST7735 SPI colour TFT (128×160, red PCB) | 1 | ₹330 | Buy 1.8" ST7735 SPI colour TFT (128×160, red PCB) |
| Jumper wires (female–female) | 8 | ₹40 | Buy Jumper wires (female–female) |
| USB power bank or 5 V adapter | 1 | ₹300 | Buy USB power bank or 5 V adapter |
| Total | ₹1,050 |
Affiliate disclosure: “Buy” links may earn FlashReady a small commission at no extra cost to you. Prices are indicative.
Wiring
| ESP32 DevKit V1 | 1.8" ST7735 colour TFT (128×160) | Wire | Notes |
|---|---|---|---|
| D5 | CS | purple | |
| RX2 | A0 | orange | GPIO16 (printed RX2). A0 is the data/command (DC) pin |
| TX2 | RESET | white | GPIO17 (printed TX2) |
| D18 | SCK | yellow | |
| D23 | SDA | blue | SDA here is SPI data (MOSI), not I2C |
| 3V3 | LED | red | Backlight always on |
| 3V3 | VCC | red | |
| GND | GND | black |
Board quirks: It only receives: every router broadcasts its name and channel about ten times a second for anyone to hear, and this is what phones use to list networks. It never connects to, or sends anything to, a network that isn’t yours. In India the 2.4 GHz band has channels 1–13. Only 1, 6 and 11 don’t overlap, so the sketch recommends one of those three — a router on channel 3 or 9 interferes with two groups at once. Bars use a log scale: one strong neighbour through the wall would otherwise flatten every other bar. The small number above a bar is how many networks sit exactly on that channel. 5 GHz isn’t shown — the ESP32’s radio is 2.4 GHz only. If your phone and router both do 5 GHz, that band is usually far less crowded.
Code
// Wi-Fi channel analyzer — ESP32 DevKit V1 + 1.8" ST7735 TFT
// TFT on VSPI (SCK D18, MOSI/SDA D23), CS D5, A0/DC D16 (RX2), RESET D17 (TX2), LED + VCC to 3V3.
// Every 30 s it listens for the 2.4 GHz networks around you (the beacons every router
// broadcasts), draws how crowded each channel 1–13 is, and suggests the quietest of 1, 6 and 11
// for your own router. It never connects to or sends anything to other networks.
#include <WiFi.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <math.h>
#include "fr_settings.h"
const char* MY_SSID = "YOUR_SSID"; // optional: your network, drawn in green
// Filled in from the FlashReady flasher over USB (kept on the board), or type the value above.
FrSetting SETTINGS[] = {{"my_ssid", &MY_SSID, false}};
constexpr uint8_t TFT_CS = 5;
constexpr uint8_t TFT_DC = 16;
constexpr uint8_t TFT_RST = 17;
constexpr unsigned long SCAN_EVERY_MS = 30000;
Adafruit_ST7735 tft(TFT_CS, TFT_DC, TFT_RST);
float load[14]; // interference score per channel 1–13 (index 0 unused)
int count[14]; // networks centred on each channel
int myChannel = 0;
void scan() {
int n = WiFi.scanNetworks(false, true); // blocking, include hidden networks
for (int c = 0; c < 14; c++) load[c] = 0, count[c] = 0;
myChannel = 0;
Serial.printf("\n%d networks\n", n < 0 ? 0 : n);
for (int i = 0; i < n; i++) {
int ch = WiFi.channel(i), rssi = WiFi.RSSI(i);
if (ch < 1 || ch > 13) continue;
count[ch]++;
// A 20 MHz network spills two channels either side, weaker the further away.
float mw = powf(10.0f, rssi / 10.0f) * 1e6f; // received power in nW, so numbers stay readable
for (int d = -2; d <= 2; d++) {
int c = ch + d;
if (c >= 1 && c <= 13) load[c] += mw * (d == 0 ? 1.0f : abs(d) == 1 ? 0.5f : 0.15f);
}
bool mine = *MY_SSID && WiFi.SSID(i) == MY_SSID;
if (mine) myChannel = ch;
Serial.printf("ch%2d %4d dBm %s%s\n", ch, rssi, WiFi.SSID(i).length() ? WiFi.SSID(i).c_str() : "(hidden)", mine ? " <- yours" : "");
}
WiFi.scanDelete();
}
int bestOf1611() {
int best = 1;
for (int c : {6, 11})
if (load[c] < load[best]) best = c;
return best;
}
void draw() {
tft.fillScreen(ST77XX_BLACK);
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(2, 2);
tft.print("2.4 GHz channel load");
float peak = 1;
for (int c = 1; c <= 13; c++) peak = max(peak, load[c]);
for (int c = 1; c <= 13; c++) {
int x = 4 + (c - 1) * 12;
// Log scale: one strong neighbour shouldn't flatten every other bar.
int h = load[c] > 0 ? (int)(80.0f * log10f(1 + 9 * load[c] / peak)) : 0;
uint16_t col = c == myChannel ? ST77XX_GREEN : (c == 1 || c == 6 || c == 11) ? ST77XX_CYAN : ST77XX_BLUE;
tft.fillRect(x, 100 - h, 9, h, col);
tft.setCursor(x + (c < 10 ? 2 : 0), 104);
tft.print(c);
if (count[c]) {
tft.setCursor(x + 2, 90 - h);
tft.print(count[c]);
}
}
int best = bestOf1611();
tft.setCursor(2, 116);
tft.setTextColor(ST77XX_YELLOW);
tft.printf("Best: ch %d", best);
if (myChannel) tft.printf(" yours: %d", myChannel);
Serial.printf("Quietest of 1/6/11: channel %d%s\n", best, myChannel && myChannel != best ? " (consider moving)" : "");
}
void setup() {
Serial.begin(115200);
frSettingsBegin(SETTINGS);
tft.initR(INITR_BLACKTAB);
tft.setRotation(1); // landscape, 160 × 128
tft.fillScreen(ST77XX_BLACK);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(2, 60);
tft.print("Scanning 2.4 GHz...");
WiFi.mode(WIFI_STA);
WiFi.disconnect();
}
void loop() {
scan();
draw();
delay(SCAN_EVERY_MS);
}// Wi-Fi channel analyzer — ESP32 DevKit V1 + 1.8" ST7735 TFT
// TFT on VSPI (SCK D18, MOSI/SDA D23), CS D5, A0/DC D16 (RX2), RESET D17 (TX2), LED + VCC to 3V3.
// Every 30 s it listens for the 2.4 GHz networks around you (the beacons every router
// broadcasts), draws how crowded each channel 1–13 is, and suggests the quietest of 1, 6 and 11
// for your own router. It never connects to or sends anything to other networks.
#include <WiFi.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <math.h>
#include "fr_settings.h"
const char* MY_SSID = "YOUR_SSID"; // optional: your network, drawn in green
// Filled in from the FlashReady flasher over USB (kept on the board), or type the value above.
FrSetting SETTINGS[] = {{"my_ssid", &MY_SSID, false}};
constexpr uint8_t TFT_CS = 5;
constexpr uint8_t TFT_DC = 16;
constexpr uint8_t TFT_RST = 17;
constexpr unsigned long SCAN_EVERY_MS = 30000;
Adafruit_ST7735 tft(TFT_CS, TFT_DC, TFT_RST);
float load[14]; // interference score per channel 1–13 (index 0 unused)
int count[14]; // networks centred on each channel
int myChannel = 0;
void scan() {
int n = WiFi.scanNetworks(false, true); // blocking, include hidden networks
for (int c = 0; c < 14; c++) load[c] = 0, count[c] = 0;
myChannel = 0;
Serial.printf("\n%d networks\n", n < 0 ? 0 : n);
for (int i = 0; i < n; i++) {
int ch = WiFi.channel(i), rssi = WiFi.RSSI(i);
if (ch < 1 || ch > 13) continue;
count[ch]++;
// A 20 MHz network spills two channels either side, weaker the further away.
float mw = powf(10.0f, rssi / 10.0f) * 1e6f; // received power in nW, so numbers stay readable
for (int d = -2; d <= 2; d++) {
int c = ch + d;
if (c >= 1 && c <= 13) load[c] += mw * (d == 0 ? 1.0f : abs(d) == 1 ? 0.5f : 0.15f);
}
bool mine = *MY_SSID && WiFi.SSID(i) == MY_SSID;
if (mine) myChannel = ch;
Serial.printf("ch%2d %4d dBm %s%s\n", ch, rssi, WiFi.SSID(i).length() ? WiFi.SSID(i).c_str() : "(hidden)", mine ? " <- yours" : "");
}
WiFi.scanDelete();
}
int bestOf1611() {
int best = 1;
for (int c : {6, 11})
if (load[c] < load[best]) best = c;
return best;
}
void draw() {
tft.fillScreen(ST77XX_BLACK);
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(2, 2);
tft.print("2.4 GHz channel load");
float peak = 1;
for (int c = 1; c <= 13; c++) peak = max(peak, load[c]);
for (int c = 1; c <= 13; c++) {
int x = 4 + (c - 1) * 12;
// Log scale: one strong neighbour shouldn't flatten every other bar.
int h = load[c] > 0 ? (int)(80.0f * log10f(1 + 9 * load[c] / peak)) : 0;
uint16_t col = c == myChannel ? ST77XX_GREEN : (c == 1 || c == 6 || c == 11) ? ST77XX_CYAN : ST77XX_BLUE;
tft.fillRect(x, 100 - h, 9, h, col);
tft.setCursor(x + (c < 10 ? 2 : 0), 104);
tft.print(c);
if (count[c]) {
tft.setCursor(x + 2, 90 - h);
tft.print(count[c]);
}
}
int best = bestOf1611();
tft.setCursor(2, 116);
tft.setTextColor(ST77XX_YELLOW);
tft.printf("Best: ch %d", best);
if (myChannel) tft.printf(" yours: %d", myChannel);
Serial.printf("Quietest of 1/6/11: channel %d%s\n", best, myChannel && myChannel != best ? " (consider moving)" : "");
}
void setup() {
Serial.begin(115200);
frSettingsBegin(SETTINGS);
tft.initR(INITR_BLACKTAB);
tft.setRotation(1); // landscape, 160 × 128
tft.fillScreen(ST77XX_BLACK);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(2, 60);
tft.print("Scanning 2.4 GHz...");
WiFi.mode(WIFI_STA);
WiFi.disconnect();
}
void loop() {
scan();
draw();
delay(SCAN_EVERY_MS);
}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 esp32 on esp32 3.3.12 with Adafruit ST7735 and ST7789 Library 1.12.0 · 3 Nov 2026
How it works
In a flat in Mumbai, Bengaluru or Gurugram your phone can list 30 or 40 Wi-Fi networks. They all share the same small 2.4 GHz band, and a router left on “auto” often parks itself on a busy channel. The result is slow speeds and dropped video calls in the evening even though your plan is fine. This analyzer shows the crowding and the best channel to choose in your router’s settings.
How it works
Every 30 seconds the ESP32 runs an active Wi-Fi scan — the same one your phone does when you open Wi-Fi settings — and gets each network’s channel and signal strength (RSSI). A 20 MHz Wi-Fi signal is wider than one channel step (5 MHz), so a network on channel 6 also disturbs 4, 5, 7 and 8. The sketch adds each network’s received power to its own channel and, at a half and roughly a sixth, to the two channels either side.
The result is drawn as 13 bars on the ST7735: cyan for the three non-overlapping channels, blue for the rest, green for your own network’s channel. The bottom line names the quietest of 1, 6 and 11, and the Serial Monitor lists every network found.
Using the result
- Open your router’s admin page (often 192.168.0.1 or 192.168.1.1; the address and password are on its label).
- Find Wireless / WLAN → 2.4 GHz → Channel, change “Auto” to the recommended channel and save.
- Leave the channel width at 20 MHz for 2.4 GHz in a crowded building; 40 MHz takes two-thirds of the band and collides with everybody.
ISP-supplied routers (JioFiber, Airtel Xstream, BSNL and others) sometimes hide the channel setting under an “advanced” menu, and a few lock it. If you can’t change it, at least move devices that can to 5 GHz.
Why these parts
The ESP32 has a 2.4 GHz radio and a scan function built into its Wi-Fi library, so no extra radio is needed. The 1.8″ ST7735 is the cheapest colour screen that makes a bar chart readable; it uses the ESP32’s VSPI pins (D18 clock, D23 data) plus three control lines.
Troubleshooting
My own network isn’t green
Set the name in the flasher (optional “Your Wi-Fi name”) or type it into MY_SSID. It must match exactly, including capitals. A 5 GHz-only network won’t appear at all.
The screen stays white
Check CS on D5, A0 on RX2 (GPIO16), RESET on TX2 (GPIO17), SCK on D18 and SDA on D23.
The recommendation changes every scan
Normal when two channels are close. Take readings at the busy hour (evening) for a few days and pick the channel that wins most often.
Upgrades
- Walk around with it on a power bank and compare rooms.
- Count networks per channel over a day and log the busiest hour to Serial.
- Add a button to freeze the screen on the current scan.



