Parts list
| Part | Qty | Approx. | Buy |
|---|---|---|---|
| ESP32 DevKit V1 (30-pin) | 1 | ₹380 | Buy ESP32 DevKit V1 (30-pin) |
| LDR light sensor module (4-pin, with AO) | 2 | ₹80 | Buy LDR light sensor module (4-pin, with AO) |
| 0.96" SSD1306 I2C OLED (4-pin) | 1 | ₹220 | Buy 0.96" SSD1306 I2C OLED (4-pin) |
| Black insulation tape + blu-tack (to shroud each LDR over its LED) | 1 | ₹40 | Buy Black insulation tape + blu-tack (to shroud each LDR over its LED) |
| Jumper wires (female–female, 20 cm) | 10 | ₹50 | Buy Jumper wires (female–female, 20 cm) |
| 5 V USB adapter + cable (on the router’s inverter/UPS) | 1 | ₹120 | Buy 5 V USB adapter + cable (on the router’s inverter/UPS) |
| Total | ₹890 |
Affiliate disclosure: “Buy” links may earn FlashReady a small commission at no extra cost to you. Prices are indicative.
Wiring
| ESP32 DevKit V1 | Part · pin | Wire | Notes |
|---|---|---|---|
| D34 | LDR light sensor module · AO | green | PON light. GPIO34 is ADC1 (works with Wi-Fi on) and input-only |
| 3V3 | LDR light sensor module · VCC | red | 3.3 V so AO never exceeds the ADC limit |
| GND | LDR light sensor module · GND | black | |
| D35 | LDR light sensor module #2 · AO | purple | LOS light (red on most ONTs). GPIO35, ADC1 |
| 3V3 | LDR light sensor module #2 · VCC | red | |
| GND | LDR light sensor module #2 · GND | black | |
| D22 | SSD1306 OLED 0.96″ (128×64, I2C) · SCL | yellow | |
| D21 | SSD1306 OLED 0.96″ (128×64, I2C) · SDA | blue | |
| 3V3 | SSD1306 OLED 0.96″ (128×64, I2C) · VCC | red | |
| GND | SSD1306 OLED 0.96″ (128×64, I2C) · GND | black |
Board quirks: Nothing is wired to the ONT. The LDRs only look at its lights, so there’s no risk to the ISP’s equipment and nothing to explain to the technician. Shroud each LDR so it sees only its own LED: a 1 cm tube of black tape, blu-tack around the edge. Room light leaking in is the main cause of wrong readings. LDR modules differ: most read lower when lit, some higher. The Serial Monitor prints the raw value of each; note it with the light on and off and set the threshold halfway (flip <code>LIT_IS_LOW</code> if yours rises with light). Light names vary by ISP and model: PON may be labelled “PON”, “REG” or “FIBER”; LOS may be “LOS” or “ALARM”. A blinking PON means the ONT is trying to register with the ISP’s OLT.
Code
// Fibre ONT status monitor — ESP32 DevKit V1 + 2 LDR modules + SSD1306 OLED
// Two LDR modules taped over the ONT's PON and LOS lights read them optically — no wire ever
// touches the ISP's box. PON AO → D34, LOS AO → D35 (ADC1, works with Wi-Fi on), both at 3V3.
// OLED: SDA D21, SCL D22. With the lights plus TCP checks to your router and 1.1.1.1 it tells
// a fibre cut (LOS red) from an ONT that isn't registered (PON blinking) from an ISP fault.
#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "fr_settings.h"
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASSWORD";
// Filled in from the FlashReady flasher over USB (kept on the board), or type the values above.
FrSetting SETTINGS[] = {{"wifi_ssid", &WIFI_SSID, true}, {"wifi_pass", &WIFI_PASS, true}};
constexpr uint8_t PON_PIN = 34;
constexpr uint8_t LOS_PIN = 35;
// Most LDR modules read LOWER when lit. Watch the Serial raw values with the light on and off,
// then set the threshold halfway between them (and flip LIT_IS_LOW if yours reads higher).
constexpr bool LIT_IS_LOW = true;
constexpr int PON_THRESHOLD = 2000;
constexpr int LOS_THRESHOLD = 2000;
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
enum Light { OFF, ON, BLINK };
const char* lightName(Light l) { return l == ON ? "ON" : l == BLINK ? "BLINK" : "OFF"; }
bool lit(uint8_t pin, int threshold, int& raw) {
raw = analogRead(pin);
return LIT_IS_LOW ? raw < threshold : raw > threshold;
}
// Sample both lights 40 times over 2 s and count on/off changes to spot blinking.
void readLights(Light& pon, Light& los, int& ponRaw, int& losRaw) {
int ponOn = 0, losOn = 0, ponFlips = 0, losFlips = 0;
bool pPrev = lit(PON_PIN, PON_THRESHOLD, ponRaw), lPrev = lit(LOS_PIN, LOS_THRESHOLD, losRaw);
for (int i = 0; i < 40; i++) {
bool p = lit(PON_PIN, PON_THRESHOLD, ponRaw), l = lit(LOS_PIN, LOS_THRESHOLD, losRaw);
ponOn += p;
losOn += l;
ponFlips += p != pPrev;
losFlips += l != lPrev;
pPrev = p;
lPrev = l;
delay(50);
}
pon = ponFlips >= 2 ? BLINK : ponOn > 20 ? ON : OFF;
los = losFlips >= 2 ? BLINK : losOn > 20 ? ON : OFF;
}
long probe(const IPAddress& ip, uint16_t port) {
WiFiClient c;
unsigned long t = millis();
bool ok = c.connect(ip, port, 2000);
long ms = ok ? (long)(millis() - t) : -1;
c.stop();
return ms;
}
const char* diagnose(bool wifi, long routerMs, long netMs, Light pon, Light los) {
if (los != OFF) return "FIBRE CUT (LOS)";
if (pon == BLINK) return "ONT NOT REGISTERED";
if (pon == OFF) return "ONT OFF / NO PON";
if (!wifi) return "WI-FI DOWN";
if (routerMs < 0) return "ROUTER NOT ANSWERING";
if (netMs < 0) return "ISP FAULT (PON OK)";
return "ALL OK";
}
String lastVerdict;
unsigned long verdictSince = 0;
void setup() {
Serial.begin(115200);
frSettingsBegin(SETTINGS);
analogSetPinAttenuation(PON_PIN, ADC_11db);
analogSetPinAttenuation(LOS_PIN, ADC_11db);
Wire.begin(21, 22);
if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) Serial.println("OLED not found at 0x3C");
if (*WIFI_SSID) WiFi.begin(WIFI_SSID, WIFI_PASS);
else Serial.println("Wi-Fi off — no Wi-Fi name set; only the ONT lights are watched. Flash from flashready.in or type it into the code.");
}
void loop() {
Light pon, los;
int ponRaw, losRaw;
readLights(pon, los, ponRaw, losRaw);
bool wifi = WiFi.status() == WL_CONNECTED;
long routerMs = wifi ? probe(WiFi.gatewayIP(), 53) : -1;
long netMs = wifi ? probe(IPAddress(1, 1, 1, 1), 53) : -1;
if (!*WIFI_SSID) routerMs = netMs = 0; // no Wi-Fi to test: judge on the lights only
String verdict = diagnose(wifi || !*WIFI_SSID, routerMs, netMs, pon, los);
if (verdict != lastVerdict) {
unsigned long mins = (millis() - verdictSince) / 60000UL;
if (lastVerdict.length()) Serial.printf("[%lu min] %s -> %s (after %lu min)\n", millis() / 60000UL, lastVerdict.c_str(), verdict.c_str(), mins);
lastVerdict = verdict;
verdictSince = millis();
}
Serial.printf("pon=%s(%d) los=%s(%d) router=%ldms net=%ldms -> %s\n", lightName(pon), ponRaw, lightName(los), losRaw, routerMs, netMs, verdict.c_str());
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
oled.setTextSize(1);
oled.setCursor(0, 0);
oled.printf("PON %-5s LOS %s", lightName(pon), lightName(los));
oled.setCursor(0, 12);
if (routerMs >= 0) oled.printf("Router %ld ms", routerMs);
else oled.print("Router --");
oled.setCursor(0, 22);
if (netMs >= 0) oled.printf("1.1.1.1 %ld ms", netMs);
else oled.print("1.1.1.1 --");
oled.drawFastHLine(0, 34, 128, SSD1306_WHITE);
oled.setCursor(0, 40);
oled.print(verdict);
oled.setCursor(0, 54);
oled.printf("for %lu min", (millis() - verdictSince) / 60000UL);
oled.display();
delay(3000);
}// Fibre ONT status monitor — ESP32 DevKit V1 + 2 LDR modules + SSD1306 OLED
// Two LDR modules taped over the ONT's PON and LOS lights read them optically — no wire ever
// touches the ISP's box. PON AO → D34, LOS AO → D35 (ADC1, works with Wi-Fi on), both at 3V3.
// OLED: SDA D21, SCL D22. With the lights plus TCP checks to your router and 1.1.1.1 it tells
// a fibre cut (LOS red) from an ONT that isn't registered (PON blinking) from an ISP fault.
#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "fr_settings.h"
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASSWORD";
// Filled in from the FlashReady flasher over USB (kept on the board), or type the values above.
FrSetting SETTINGS[] = {{"wifi_ssid", &WIFI_SSID, true}, {"wifi_pass", &WIFI_PASS, true}};
constexpr uint8_t PON_PIN = 34;
constexpr uint8_t LOS_PIN = 35;
// Most LDR modules read LOWER when lit. Watch the Serial raw values with the light on and off,
// then set the threshold halfway between them (and flip LIT_IS_LOW if yours reads higher).
constexpr bool LIT_IS_LOW = true;
constexpr int PON_THRESHOLD = 2000;
constexpr int LOS_THRESHOLD = 2000;
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
enum Light { OFF, ON, BLINK };
const char* lightName(Light l) { return l == ON ? "ON" : l == BLINK ? "BLINK" : "OFF"; }
bool lit(uint8_t pin, int threshold, int& raw) {
raw = analogRead(pin);
return LIT_IS_LOW ? raw < threshold : raw > threshold;
}
// Sample both lights 40 times over 2 s and count on/off changes to spot blinking.
void readLights(Light& pon, Light& los, int& ponRaw, int& losRaw) {
int ponOn = 0, losOn = 0, ponFlips = 0, losFlips = 0;
bool pPrev = lit(PON_PIN, PON_THRESHOLD, ponRaw), lPrev = lit(LOS_PIN, LOS_THRESHOLD, losRaw);
for (int i = 0; i < 40; i++) {
bool p = lit(PON_PIN, PON_THRESHOLD, ponRaw), l = lit(LOS_PIN, LOS_THRESHOLD, losRaw);
ponOn += p;
losOn += l;
ponFlips += p != pPrev;
losFlips += l != lPrev;
pPrev = p;
lPrev = l;
delay(50);
}
pon = ponFlips >= 2 ? BLINK : ponOn > 20 ? ON : OFF;
los = losFlips >= 2 ? BLINK : losOn > 20 ? ON : OFF;
}
long probe(const IPAddress& ip, uint16_t port) {
WiFiClient c;
unsigned long t = millis();
bool ok = c.connect(ip, port, 2000);
long ms = ok ? (long)(millis() - t) : -1;
c.stop();
return ms;
}
const char* diagnose(bool wifi, long routerMs, long netMs, Light pon, Light los) {
if (los != OFF) return "FIBRE CUT (LOS)";
if (pon == BLINK) return "ONT NOT REGISTERED";
if (pon == OFF) return "ONT OFF / NO PON";
if (!wifi) return "WI-FI DOWN";
if (routerMs < 0) return "ROUTER NOT ANSWERING";
if (netMs < 0) return "ISP FAULT (PON OK)";
return "ALL OK";
}
String lastVerdict;
unsigned long verdictSince = 0;
void setup() {
Serial.begin(115200);
frSettingsBegin(SETTINGS);
analogSetPinAttenuation(PON_PIN, ADC_11db);
analogSetPinAttenuation(LOS_PIN, ADC_11db);
Wire.begin(21, 22);
if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) Serial.println("OLED not found at 0x3C");
if (*WIFI_SSID) WiFi.begin(WIFI_SSID, WIFI_PASS);
else Serial.println("Wi-Fi off — no Wi-Fi name set; only the ONT lights are watched. Flash from flashready.in or type it into the code.");
}
void loop() {
Light pon, los;
int ponRaw, losRaw;
readLights(pon, los, ponRaw, losRaw);
bool wifi = WiFi.status() == WL_CONNECTED;
long routerMs = wifi ? probe(WiFi.gatewayIP(), 53) : -1;
long netMs = wifi ? probe(IPAddress(1, 1, 1, 1), 53) : -1;
if (!*WIFI_SSID) routerMs = netMs = 0; // no Wi-Fi to test: judge on the lights only
String verdict = diagnose(wifi || !*WIFI_SSID, routerMs, netMs, pon, los);
if (verdict != lastVerdict) {
unsigned long mins = (millis() - verdictSince) / 60000UL;
if (lastVerdict.length()) Serial.printf("[%lu min] %s -> %s (after %lu min)\n", millis() / 60000UL, lastVerdict.c_str(), verdict.c_str(), mins);
lastVerdict = verdict;
verdictSince = millis();
}
Serial.printf("pon=%s(%d) los=%s(%d) router=%ldms net=%ldms -> %s\n", lightName(pon), ponRaw, lightName(los), losRaw, routerMs, netMs, verdict.c_str());
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
oled.setTextSize(1);
oled.setCursor(0, 0);
oled.printf("PON %-5s LOS %s", lightName(pon), lightName(los));
oled.setCursor(0, 12);
if (routerMs >= 0) oled.printf("Router %ld ms", routerMs);
else oled.print("Router --");
oled.setCursor(0, 22);
if (netMs >= 0) oled.printf("1.1.1.1 %ld ms", netMs);
else oled.print("1.1.1.1 --");
oled.drawFastHLine(0, 34, 128, SSD1306_WHITE);
oled.setCursor(0, 40);
oled.print(verdict);
oled.setCursor(0, 54);
oled.printf("for %lu min", (millis() - verdictSince) / 60000UL);
oled.display();
delay(3000);
}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 SSD1306 2.5.17 · 3 Nov 2026
How it works
When home FTTH internet stops, the first question from customer care is always “what lights are on the box?”. The ONT (the small fibre modem, often combined with the Wi-Fi router) has two that matter:
- PON (also REG / FIBER): steady when the ONT is registered with the ISP’s equipment at the exchange; blinking while it tries; off when it has given up.
- LOS (Loss Of Signal, usually red): off when light arrives through the fibre; on or blinking when it doesn’t — a cut cable, a pinched bend, a dirty connector or a fault at the splitter box.
This monitor watches both lights with LDR modules taped over them, and at the same time times a TCP connection to your router and to 1.1.1.1. Together they give a diagnosis in plain words on the OLED, with how long it has been so.
| OLED says | Meaning | What to do |
|---|---|---|
| FIBRE CUT (LOS) | No light reaching the ONT | Call the ISP and say “LOS is red” — that gets a field technician, not a script |
| ONT NOT REGISTERED | PON blinking | Usually an ISP-side fault or a new ONT not yet activated |
| ISP FAULT (PON OK) | Fibre fine, internet down | ISP routing or PPPoE problem; a router restart sometimes helps |
| ROUTER NOT ANSWERING | Your router or Wi-Fi | Restart the router |
| ALL OK | Everything answers | — |
How it works
Every 3 seconds the ESP32 samples both LDRs 40 times over 2 seconds. If a light changes state at least twice in that window it is BLINK, otherwise it’s ON or OFF by majority. The LDRs connect to D34 and D35 — ADC1 pins, which keep working while Wi-Fi is on (ADC2 pins don’t). They are input-only, which suits a sensor.
Then it opens a TCP connection to the gateway on port 53 and to 1.1.1.1 on port 53, with a 2-second timeout — the same trick as the ISP link monitor, because the ESP32 core has no ICMP ping. Each change of verdict is printed to Serial with the minutes it lasted, so you can paste the log into a complaint.
Set-up
- Flash it, then hold each LDR against its LED and watch the Serial Monitor’s raw numbers with the LED on and off (unplug the fibre patch cord briefly to make LOS light up — plug it back gently and never look into the fibre end).
- Set
PON_THRESHOLDandLOS_THRESHOLDhalfway between the two values. - Shroud each LDR with black tape and fix it with blu-tack. Keep the OLED somewhere you’ll see it.
Power the ESP32 from the same inverter or UPS as the router, otherwise a power cut looks like a dead ONT. During a long power cut, the ISP’s own street equipment may also go down — that shows as PON off or blinking even though your fibre is fine.
Troubleshooting
LOS always shows ON
Room light is reaching the LDR, or the threshold is wrong. Cover the ONT with a cloth: if LOS turns OFF, improve the shroud; if not, look at the raw value in Serial and adjust LOS_THRESHOLD.
PON shows BLINK when it’s steady
Some ONT LEDs are driven with a fast PWM that an LDR averages, but an LDR at the edge of its threshold can still flicker. Move the threshold further from the “lit” value.
It says ROUTER NOT ANSWERING but the internet works
Your router doesn’t accept a TCP connection on port 53. Change probe(WiFi.gatewayIP(), 53) to port 80.
Upgrades
- Add an NTP clock and log each fault with its start and end time for your complaint.
- Send an ntfy alert over mobile data (a second phone hotspot) when LOS lights up.
- Combine with the router reboot controller: reboot only when PON is steady but the ISP link is down.



