Added the Arduino INO file
This commit is contained in:
909
telly_bme.ino
Normal file
909
telly_bme.ino
Normal file
@@ -0,0 +1,909 @@
|
||||
#include <BME280I2C.h>
|
||||
#include <Wire.h>
|
||||
#include <WiFi.h>
|
||||
#include <NetworkClient.h>
|
||||
#include <NetworkServer.h>
|
||||
#include <Preferences.h>
|
||||
#include <driver/gpio.h>
|
||||
#include <ThreeWire.h>
|
||||
#include <RtcDS1302.h>
|
||||
|
||||
#define DATA 1
|
||||
#define CLOCK 0
|
||||
#define WAKE_PIN 2
|
||||
#define RUN_PIN 8
|
||||
#define BATT_PIN 3
|
||||
|
||||
#define RTC_CLK 6
|
||||
#define RTC_DAT 4
|
||||
#define RTC_RST 5
|
||||
|
||||
#define VERSION "1.6"
|
||||
#define NAME "telly-bme"
|
||||
#define AP_SSID "telly-bme-setup"
|
||||
#define RESET_HOLD_MS 10000
|
||||
#define BLINK_MS 150
|
||||
|
||||
#define HISTORY_SIZE 10
|
||||
#define MAX_CMD_LEN 128
|
||||
|
||||
// Battery constants
|
||||
#define BATT_MAX_V 4.20f // fully charged
|
||||
#define BATT_MIN_V 3.00f // TP4056 protection cutoff
|
||||
#define BATT_CAPACITY 3000.0f // mAh — typical 18650
|
||||
#define BATT_SLEEP_MA 0.0095f // measured 9.5µA deep sleep
|
||||
#define BATT_AWAKE_MA 50.0f // measured ~50mA awake
|
||||
#define BATT_WAKE_S 60.0f // assumed awake window per wake
|
||||
#define BATT_WAKES_PER_HR 1.0f // assumed wakes per hour
|
||||
#define ADC_SAMPLES 16 // oversample for stability
|
||||
#define ADC_VREF 3.3f
|
||||
#define ADC_CAL 0.909f
|
||||
#define ADC_RESOLUTION 4095.0f
|
||||
#define DIVIDER_RATIO 2.0f // 100k+100k voltage divider
|
||||
|
||||
#define IAC 255
|
||||
|
||||
// ── Persistent config ─────────────────────────────────────────────────────────
|
||||
|
||||
Preferences prefs;
|
||||
|
||||
struct NetConfig {
|
||||
char ssid[64];
|
||||
char pass[64];
|
||||
bool staticIP;
|
||||
uint32_t ip;
|
||||
uint32_t gw;
|
||||
uint32_t sn;
|
||||
bool configured;
|
||||
};
|
||||
|
||||
NetConfig netCfg;
|
||||
|
||||
void loadNetConfig() {
|
||||
prefs.begin("net", true);
|
||||
netCfg.configured = prefs.getBool("configured", false);
|
||||
netCfg.staticIP = prefs.getBool("staticIP", false);
|
||||
netCfg.ip = prefs.getUInt("ip", 0);
|
||||
netCfg.gw = prefs.getUInt("gw", 0);
|
||||
netCfg.sn = prefs.getUInt("sn", 0);
|
||||
prefs.getString("ssid", netCfg.ssid, sizeof(netCfg.ssid));
|
||||
prefs.getString("pass", netCfg.pass, sizeof(netCfg.pass));
|
||||
prefs.end();
|
||||
}
|
||||
|
||||
void saveNetConfig() {
|
||||
prefs.begin("net", false);
|
||||
prefs.putBool("configured", netCfg.configured);
|
||||
prefs.putBool("staticIP", netCfg.staticIP);
|
||||
prefs.putUInt("ip", netCfg.ip);
|
||||
prefs.putUInt("gw", netCfg.gw);
|
||||
prefs.putUInt("sn", netCfg.sn);
|
||||
prefs.putString("ssid", netCfg.ssid);
|
||||
prefs.putString("pass", netCfg.pass);
|
||||
prefs.end();
|
||||
}
|
||||
|
||||
void eraseNetConfig() {
|
||||
prefs.begin("net", false);
|
||||
prefs.clear();
|
||||
prefs.end();
|
||||
memset(&netCfg, 0, sizeof(netCfg));
|
||||
}
|
||||
|
||||
// ── RTC DATA ATTR ─────────────────────────────────────────────────────────────
|
||||
|
||||
RTC_DATA_ATTR bool rtcValid = false;
|
||||
RTC_DATA_ATTR uint8_t rtcBSSID[6];
|
||||
RTC_DATA_ATTR int32_t rtcChannel;
|
||||
RTC_DATA_ATTR uint32_t rtcIP;
|
||||
RTC_DATA_ATTR uint32_t rtcGW;
|
||||
RTC_DATA_ATTR uint32_t rtcSN;
|
||||
RTC_DATA_ATTR uint32_t rtcDNS;
|
||||
|
||||
// ── Globals ───────────────────────────────────────────────────────────────────
|
||||
|
||||
BME280I2C bme;
|
||||
NetworkServer server(23);
|
||||
ThreeWire myWire(RTC_DAT, RTC_CLK, RTC_RST);
|
||||
RtcDS1302<ThreeWire> rtcModule(myWire);
|
||||
|
||||
unsigned long bootTime = 0;
|
||||
bool apMode = false;
|
||||
bool btnArmed = false;
|
||||
unsigned long btnSince = 0;
|
||||
|
||||
// ── LED ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
bool blinkActive = false;
|
||||
bool blinkState = false;
|
||||
unsigned long lastBlink = 0;
|
||||
|
||||
void blinkTick() {
|
||||
if (!blinkActive) return;
|
||||
if (millis() - lastBlink >= BLINK_MS) {
|
||||
blinkState = !blinkState;
|
||||
digitalWrite(RUN_PIN, blinkState ? 0 : 1);
|
||||
lastBlink = millis();
|
||||
}
|
||||
}
|
||||
|
||||
void ledOn() { blinkActive = false; digitalWrite(RUN_PIN, 0); }
|
||||
void ledOff() { blinkActive = false; digitalWrite(RUN_PIN, 1); }
|
||||
void ledBlink() { blinkActive = true; lastBlink = millis(); }
|
||||
|
||||
// ── Battery ───────────────────────────────────────────────────────────────────
|
||||
|
||||
float readBatteryVoltage() {
|
||||
// Discard first few samples to let ADC settle
|
||||
for (int i = 0; i < 5; i++) {
|
||||
analogRead(BATT_PIN);
|
||||
delay(10);
|
||||
}
|
||||
long sum = 0;
|
||||
for (int i = 0; i < ADC_SAMPLES; i++) {
|
||||
sum += analogRead(BATT_PIN);
|
||||
delay(2);
|
||||
}
|
||||
float avg = sum / (float)ADC_SAMPLES;
|
||||
float adcV = (avg / ADC_RESOLUTION) * ADC_VREF;
|
||||
float battV = adcV * DIVIDER_RATIO * ADC_CAL;
|
||||
return battV;
|
||||
}
|
||||
|
||||
int batteryPercent(float voltage) {
|
||||
if (voltage >= BATT_MAX_V) return 100;
|
||||
if (voltage <= BATT_MIN_V) return 0;
|
||||
// LiPo discharge is not linear — approximate with a piecewise curve
|
||||
// based on typical 18650 discharge profile
|
||||
struct { float v; int pct; } curve[] = {
|
||||
{ 4.20f, 100 },
|
||||
{ 4.10f, 90 },
|
||||
{ 4.00f, 80 },
|
||||
{ 3.90f, 70 },
|
||||
{ 3.80f, 60 },
|
||||
{ 3.70f, 50 },
|
||||
{ 3.60f, 40 },
|
||||
{ 3.50f, 30 },
|
||||
{ 3.40f, 20 },
|
||||
{ 3.20f, 10 },
|
||||
{ 3.00f, 0 },
|
||||
};
|
||||
int n = sizeof(curve) / sizeof(curve[0]);
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
if (voltage <= curve[i].v && voltage >= curve[i+1].v) {
|
||||
float t = (voltage - curve[i+1].v) / (curve[i].v - curve[i+1].v);
|
||||
return (int)(curve[i+1].pct + t * (curve[i].pct - curve[i+1].pct));
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Returns estimated remaining runtime in hours based on:
|
||||
// - current battery percentage
|
||||
// - measured sleep/awake current
|
||||
// - assumed duty cycle (1 wake/hr, 60s awake)
|
||||
float estimateRuntime(float voltage) {
|
||||
int pct = batteryPercent(voltage);
|
||||
float remainingMah = BATT_CAPACITY * (pct / 100.0f);
|
||||
|
||||
// Average current per hour:
|
||||
// awake portion: BATT_WAKES_PER_HR * BATT_WAKE_S seconds at BATT_AWAKE_MA
|
||||
// sleep portion: remainder of hour at BATT_SLEEP_MA
|
||||
float awakeSecsPerHr = BATT_WAKES_PER_HR * BATT_WAKE_S;
|
||||
float sleepSecsPerHr = 3600.0f - awakeSecsPerHr;
|
||||
float avgMa = ((BATT_AWAKE_MA * awakeSecsPerHr) +
|
||||
(BATT_SLEEP_MA * sleepSecsPerHr)) / 3600.0f;
|
||||
|
||||
if (avgMa <= 0) return 0;
|
||||
return remainingMah / avgMa;
|
||||
}
|
||||
|
||||
void printBattery(NetworkClient &client) {
|
||||
float voltage = readBatteryVoltage();
|
||||
int pct = batteryPercent(voltage);
|
||||
float runtime = estimateRuntime(voltage);
|
||||
|
||||
int rDays = (int)(runtime / 24);
|
||||
int rHours = (int)fmod(runtime, 24.0f);
|
||||
|
||||
// Simple bar — 20 chars wide
|
||||
int filled = pct / 5;
|
||||
String bar = "[";
|
||||
for (int i = 0; i < 20; i++) bar += (i < filled ? "#" : "-");
|
||||
bar += "]";
|
||||
|
||||
client.printf("\r\n Battery\r\n");
|
||||
client.printf(" Voltage: %.2f V\r\n", voltage);
|
||||
client.printf(" Charge: %s %d%%\r\n", bar.c_str(), pct);
|
||||
client.printf(" Est. runtime: %dd %dh\r\n", rDays, rHours);
|
||||
client.printf(" (assumes %d wake/hr, %ds awake, %.1fmA awake, %.4fmA sleep)\r\n\r\n",
|
||||
(int)BATT_WAKES_PER_HR, (int)BATT_WAKE_S,
|
||||
BATT_AWAKE_MA, BATT_SLEEP_MA);
|
||||
}
|
||||
|
||||
String batteryInline() {
|
||||
float voltage = readBatteryVoltage();
|
||||
int pct = batteryPercent(voltage);
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%.2fV %d%%", voltage, pct);
|
||||
return String(buf);
|
||||
}
|
||||
|
||||
// ── Quoted argument parser ────────────────────────────────────────────────────
|
||||
|
||||
int parseArgs(const String &str, String *args, int maxArgs) {
|
||||
int count = 0;
|
||||
int i = 0;
|
||||
int len = str.length();
|
||||
while (i < len && count < maxArgs) {
|
||||
while (i < len && str[i] == ' ') i++;
|
||||
if (i >= len) break;
|
||||
if (str[i] == '"') {
|
||||
i++;
|
||||
String token = "";
|
||||
while (i < len && str[i] != '"') token += str[i++];
|
||||
if (i < len) i++;
|
||||
args[count++] = token;
|
||||
} else {
|
||||
String token = "";
|
||||
while (i < len && str[i] != ' ') token += str[i++];
|
||||
args[count++] = token;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ── RTC helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
bool rtcGetDateTime(RtcDateTime &out) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (rtcModule.GetIsRunning() && rtcModule.IsDateTimeValid()) {
|
||||
out = rtcModule.GetDateTime();
|
||||
if (out.Year() >= 2020 && out.Year() <= 2099) return true;
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String formatDateTime(const RtcDateTime &dt) {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
|
||||
dt.Year(), dt.Month(), dt.Day(),
|
||||
dt.Hour(), dt.Minute(), dt.Second());
|
||||
return String(buf);
|
||||
}
|
||||
|
||||
bool parseDateTime(const String &s, RtcDateTime &out) {
|
||||
if (s.length() < 19) return false;
|
||||
int year = s.substring(0, 4).toInt();
|
||||
int month = s.substring(5, 7).toInt();
|
||||
int day = s.substring(8, 10).toInt();
|
||||
int hour = s.substring(11, 13).toInt();
|
||||
int min = s.substring(14, 16).toInt();
|
||||
int sec = s.substring(17, 19).toInt();
|
||||
if (year < 2020 || year > 2099) return false;
|
||||
if (month < 1 || month > 12) return false;
|
||||
if (day < 1 || day > 31) return false;
|
||||
if (hour > 23 || min > 59 || sec > 59) return false;
|
||||
out = RtcDateTime(year - 2000, month, day, hour, min, sec);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Telnet helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
void flushTelnetNegotiation(NetworkClient &client) {
|
||||
unsigned long start = millis();
|
||||
while (millis() - start < 200) {
|
||||
while (client.available()) {
|
||||
uint8_t c = client.read();
|
||||
if (c == IAC) {
|
||||
unsigned long t = millis();
|
||||
while (client.available() < 2 && millis() - t < 100);
|
||||
if (client.available() >= 2) {
|
||||
client.read();
|
||||
client.read();
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
struct History {
|
||||
String entries[HISTORY_SIZE];
|
||||
int count = 0;
|
||||
int head = 0; // index where next entry goes (ring)
|
||||
|
||||
void push(const String &cmd) {
|
||||
if (cmd.length() == 0) return;
|
||||
// Don't push duplicate of last entry
|
||||
if (count > 0) {
|
||||
int last = (head - 1 + HISTORY_SIZE) % HISTORY_SIZE;
|
||||
if (entries[last] == cmd) return;
|
||||
}
|
||||
entries[head] = cmd;
|
||||
head = (head + 1) % HISTORY_SIZE;
|
||||
if (count < HISTORY_SIZE) count++;
|
||||
}
|
||||
|
||||
// offset 1 = most recent, 2 = one before that, etc.
|
||||
String get(int offset) {
|
||||
if (offset < 1 || offset > count) return "";
|
||||
int idx = (head - offset + HISTORY_SIZE * 2) % HISTORY_SIZE;
|
||||
return entries[idx];
|
||||
}
|
||||
};
|
||||
|
||||
History cmdHistory;
|
||||
|
||||
// Redraw the current line in place
|
||||
static void redrawLine(NetworkClient &client,
|
||||
const String &buf, int cursor) {
|
||||
// Move to start of line, reprint prompt + buffer, clear tail, reposition
|
||||
client.print("\r");
|
||||
client.print(apMode ? "[setup] ~> " : "~> ");
|
||||
client.print(buf);
|
||||
client.print("\033[K"); // erase to end of line
|
||||
// Move cursor back to correct position if not at end
|
||||
int tail = buf.length() - cursor;
|
||||
if (tail > 0) {
|
||||
client.printf("\033[%dD", tail);
|
||||
}
|
||||
}
|
||||
|
||||
String readCommand(NetworkClient &client) {
|
||||
String buf = "";
|
||||
int cursor = 0; // insertion point within buf
|
||||
int histOff = 0; // 0 = live buf, 1 = most recent history entry, etc.
|
||||
String savedBuf = ""; // preserves live edit when scrolling history
|
||||
|
||||
// Escape sequence state
|
||||
enum EscState { ES_NONE, ES_ESC, ES_BRACKET } escState = ES_NONE;
|
||||
char escParam = 0;
|
||||
|
||||
while (client.connected()) {
|
||||
blinkTick();
|
||||
|
||||
if (!client.available()) continue;
|
||||
|
||||
uint8_t c = client.read();
|
||||
|
||||
// ── IAC telnet negotiation ────────────────────────────────────────────
|
||||
if (c == IAC) {
|
||||
unsigned long t = millis();
|
||||
while (client.available() < 2 && millis() - t < 100);
|
||||
if (client.available() >= 2) {
|
||||
client.read();
|
||||
client.read();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── Escape sequence parser ────────────────────────────────────────────
|
||||
if (escState == ES_ESC) {
|
||||
if (c == '[') { escState = ES_BRACKET; escParam = 0; continue; }
|
||||
escState = ES_NONE;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (escState == ES_BRACKET) {
|
||||
escState = ES_NONE;
|
||||
switch (c) {
|
||||
|
||||
case 'D': // left arrow
|
||||
if (cursor > 0) {
|
||||
cursor--;
|
||||
client.print("\033[D");
|
||||
}
|
||||
continue;
|
||||
|
||||
case 'C': // right arrow
|
||||
if (cursor < (int)buf.length()) {
|
||||
cursor++;
|
||||
client.print("\033[C");
|
||||
}
|
||||
continue;
|
||||
|
||||
case 'A': // up arrow — older history
|
||||
if (histOff == 0) savedBuf = buf;
|
||||
if (histOff < cmdHistory.count) {
|
||||
histOff++;
|
||||
buf = cmdHistory.get(histOff);
|
||||
cursor = buf.length();
|
||||
redrawLine(client, buf, cursor);
|
||||
}
|
||||
continue;
|
||||
|
||||
case 'B': // down arrow — newer history
|
||||
if (histOff > 0) {
|
||||
histOff--;
|
||||
buf = histOff == 0 ? savedBuf : cmdHistory.get(histOff);
|
||||
cursor = buf.length();
|
||||
redrawLine(client, buf, cursor);
|
||||
}
|
||||
continue;
|
||||
|
||||
case '3': // start of ESC[3~ (Delete key) — need to consume ~
|
||||
escParam = '3';
|
||||
escState = ES_BRACKET; // wait for ~
|
||||
continue;
|
||||
|
||||
case '~': // finish ESC[3~ — forward delete
|
||||
if (escParam == '3' && cursor < (int)buf.length()) {
|
||||
buf.remove(cursor, 1);
|
||||
redrawLine(client, buf, cursor);
|
||||
}
|
||||
escParam = 0;
|
||||
continue;
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Regular characters ────────────────────────────────────────────────
|
||||
switch (c) {
|
||||
|
||||
case 0x1B: // ESC
|
||||
escState = ES_ESC;
|
||||
break;
|
||||
|
||||
case '\r':
|
||||
break; // ignore CR
|
||||
|
||||
case '\n': {
|
||||
client.print("\r\n");
|
||||
String cmd = buf;
|
||||
cmd.trim();
|
||||
cmdHistory.push(cmd);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
case 0x7F: // backspace (DEL)
|
||||
case 0x08: // backspace (BS)
|
||||
if (cursor > 0) {
|
||||
buf.remove(cursor - 1, 1);
|
||||
cursor--;
|
||||
redrawLine(client, buf, cursor);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (c >= 0x20 && c < 0x7F && (int)buf.length() < MAX_CMD_LEN) {
|
||||
buf = buf.substring(0, cursor) + (char)c + buf.substring(cursor);
|
||||
cursor++;
|
||||
// If inserting at end, just echo the char — cheaper than full redraw
|
||||
if (cursor == (int)buf.length()) {
|
||||
client.write(c);
|
||||
} else {
|
||||
redrawLine(client, buf, cursor);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void printPrompt(NetworkClient &client) {
|
||||
client.print(apMode ? "[setup] ~> " : "~> ");
|
||||
}
|
||||
|
||||
// ── Sleep ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
void goToSleep() {
|
||||
ledOff();
|
||||
while (digitalRead(WAKE_PIN) == LOW) delay(10);
|
||||
delay(50);
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << WAKE_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
// ── WiFi ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
void startAP() {
|
||||
apMode = true;
|
||||
rtcValid = false;
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(AP_SSID);
|
||||
server.begin();
|
||||
ledBlink();
|
||||
}
|
||||
|
||||
void connectWiFi() {
|
||||
apMode = false;
|
||||
ledOff();
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setHostname("esp32-bme");
|
||||
|
||||
if (netCfg.staticIP && netCfg.ip != 0) {
|
||||
WiFi.config(IPAddress(netCfg.ip), IPAddress(netCfg.gw), IPAddress(netCfg.sn));
|
||||
} else if (rtcValid) {
|
||||
WiFi.config(IPAddress(rtcIP), IPAddress(rtcGW),
|
||||
IPAddress(rtcSN), IPAddress(rtcDNS));
|
||||
}
|
||||
|
||||
if (rtcValid && !netCfg.staticIP) {
|
||||
WiFi.begin(netCfg.ssid, netCfg.pass, rtcChannel, rtcBSSID, true);
|
||||
} else {
|
||||
WiFi.begin(netCfg.ssid, netCfg.pass);
|
||||
}
|
||||
|
||||
unsigned long start = millis();
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(500);
|
||||
if (millis() - start > 15000) {
|
||||
rtcValid = false;
|
||||
startAP();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
rtcChannel = WiFi.channel();
|
||||
memcpy(rtcBSSID, WiFi.BSSID(), 6);
|
||||
rtcIP = (uint32_t)WiFi.localIP();
|
||||
rtcGW = (uint32_t)WiFi.gatewayIP();
|
||||
rtcSN = (uint32_t)WiFi.subnetMask();
|
||||
rtcDNS = (uint32_t)WiFi.dnsIP();
|
||||
rtcValid = true;
|
||||
|
||||
server.begin();
|
||||
ledOn();
|
||||
}
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────────────────
|
||||
|
||||
void printBanner(NetworkClient &client) {
|
||||
client.printf("\r\n");
|
||||
client.printf(" +---------------------------------+\r\n");
|
||||
client.printf(" | %s v%s |\r\n", NAME, VERSION);
|
||||
client.printf(" | ESP32-C3 BME280 Sensor Shell |\r\n");
|
||||
if (apMode)
|
||||
client.printf(" | *** SETUP MODE *** |\r\n");
|
||||
client.printf(" +---------------------------------+\r\n");
|
||||
client.printf(" Type 'help' for available commands.\r\n\r\n");
|
||||
}
|
||||
|
||||
void printHelp(NetworkClient &client) {
|
||||
client.printf(" Commands:\r\n");
|
||||
if (apMode) {
|
||||
client.printf(" ifconfig wifi <ssid> <pass> — configure WiFi\r\n");
|
||||
client.printf(" ifconfig wifi <ssid> open — configure open WiFi\r\n");
|
||||
client.printf(" (use quotes for spaces: \"My WiFi\" \"my password\")\r\n");
|
||||
client.printf(" help — show this help\r\n");
|
||||
} else {
|
||||
client.printf(" read — single sensor reading\r\n");
|
||||
client.printf(" watch — live sensor + battery (q to stop)\r\n");
|
||||
client.printf(" battery — battery voltage, charge and runtime\r\n");
|
||||
client.printf(" date — show RTC date and time\r\n");
|
||||
client.printf(" date set <YYYY-MM-DD HH:MM:SS> — set RTC\r\n");
|
||||
client.printf(" info — device information\r\n");
|
||||
client.printf(" ifconfig — show network info\r\n");
|
||||
client.printf(" ifconfig wifi <ssid> <pass> — change WiFi (reboots)\r\n");
|
||||
client.printf(" ifconfig wifi <ssid> open — open WiFi (reboots)\r\n");
|
||||
client.printf(" ifconfig ip dhcp — use DHCP (reboots)\r\n");
|
||||
client.printf(" ifconfig ip static <ip> <gw> <sn> — set static IP (reboots)\r\n");
|
||||
client.printf(" (use quotes for spaces: \"My WiFi\" \"my password\")\r\n");
|
||||
client.printf(" help — show this help\r\n");
|
||||
client.printf(" quit — disconnect\r\n");
|
||||
client.printf(" sleep — disconnect and deep sleep\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
void cmdIfconfig(NetworkClient &client, const String &args) {
|
||||
if (args.length() == 0) {
|
||||
if (apMode) {
|
||||
client.printf(" Mode: Access Point (setup)\r\n");
|
||||
client.printf(" AP SSID: %s\r\n", AP_SSID);
|
||||
client.printf(" AP IP: %s\r\n\r\n", WiFi.softAPIP().toString().c_str());
|
||||
return;
|
||||
}
|
||||
uint8_t *bssid = WiFi.BSSID();
|
||||
client.printf("\r\n Network\r\n");
|
||||
client.printf(" Mode: STA\r\n");
|
||||
client.printf(" IP mode: %s\r\n", netCfg.staticIP ? "static" : "DHCP");
|
||||
client.printf(" Hostname: %s\r\n", WiFi.getHostname());
|
||||
client.printf(" MAC: %s\r\n", WiFi.macAddress().c_str());
|
||||
client.printf(" IP: %s\r\n", WiFi.localIP().toString().c_str());
|
||||
client.printf(" Subnet: %s\r\n", WiFi.subnetMask().toString().c_str());
|
||||
client.printf(" Gateway: %s\r\n", WiFi.gatewayIP().toString().c_str());
|
||||
client.printf(" DNS: %s\r\n", WiFi.dnsIP().toString().c_str());
|
||||
client.printf(" SSID: %s\r\n", WiFi.SSID().c_str());
|
||||
client.printf(" BSSID: %02X:%02X:%02X:%02X:%02X:%02X\r\n",
|
||||
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
|
||||
client.printf(" Channel: %d\r\n", WiFi.channel());
|
||||
client.printf(" RSSI: %d dBm\r\n", WiFi.RSSI());
|
||||
client.printf(" TX power: %d dBm\r\n", WiFi.getTxPower());
|
||||
client.printf(" WiFi cache: %s\r\n\r\n", rtcValid ? "valid" : "cold boot");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.startsWith("wifi ")) {
|
||||
String rest = args.substring(5);
|
||||
rest.trim();
|
||||
String tokens[2];
|
||||
int count = parseArgs(rest, tokens, 2);
|
||||
if (count < 2) {
|
||||
client.print("Usage: ifconfig wifi <ssid> <password> | ifconfig wifi <ssid> open\r\n");
|
||||
client.print(" Use quotes for spaces: \"My WiFi\" \"my password\"\r\n");
|
||||
return;
|
||||
}
|
||||
strncpy(netCfg.ssid, tokens[0].c_str(), sizeof(netCfg.ssid) - 1);
|
||||
strncpy(netCfg.pass, tokens[1] == "open" ? "" : tokens[1].c_str(), sizeof(netCfg.pass) - 1);
|
||||
netCfg.configured = true;
|
||||
saveNetConfig();
|
||||
rtcValid = false;
|
||||
client.printf("WiFi set to '%s'. Connecting...\r\n", netCfg.ssid);
|
||||
delay(200);
|
||||
client.print("Goodbye.\r\n");
|
||||
client.stop();
|
||||
delay(300);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
else if (args == "ip dhcp") {
|
||||
netCfg.staticIP = false;
|
||||
netCfg.ip = netCfg.gw = netCfg.sn = 0;
|
||||
saveNetConfig();
|
||||
rtcValid = false;
|
||||
client.print("Switched to DHCP. Rebooting...\r\n");
|
||||
client.stop();
|
||||
delay(300);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
else if (args.startsWith("ip static ")) {
|
||||
String rest = args.substring(10);
|
||||
rest.trim();
|
||||
int s1 = rest.indexOf(' ');
|
||||
if (s1 == -1) { client.print("Usage: ifconfig ip static <ip> <gateway> <subnet>\r\n"); return; }
|
||||
int s2 = rest.indexOf(' ', s1 + 1);
|
||||
if (s2 == -1) { client.print("Usage: ifconfig ip static <ip> <gateway> <subnet>\r\n"); return; }
|
||||
IPAddress ip, gw, sn;
|
||||
if (!ip.fromString(rest.substring(0, s1)) ||
|
||||
!gw.fromString(rest.substring(s1 + 1, s2)) ||
|
||||
!sn.fromString(rest.substring(s2 + 1))) {
|
||||
client.print("Invalid IP address format.\r\n");
|
||||
return;
|
||||
}
|
||||
netCfg.staticIP = true;
|
||||
netCfg.ip = (uint32_t)ip;
|
||||
netCfg.gw = (uint32_t)gw;
|
||||
netCfg.sn = (uint32_t)sn;
|
||||
saveNetConfig();
|
||||
rtcValid = false;
|
||||
client.printf("Static IP set to %s. Rebooting...\r\n", ip.toString().c_str());
|
||||
client.stop();
|
||||
delay(300);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
else {
|
||||
client.print("Unknown ifconfig subcommand. Type 'help' for usage.\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
void cmdDate(NetworkClient &client, const String &args) {
|
||||
if (args.length() == 0) {
|
||||
RtcDateTime now;
|
||||
if (!rtcGetDateTime(now)) {
|
||||
client.print("RTC unavailable or not set. Use: date set YYYY-MM-DD HH:MM:SS\r\n");
|
||||
return;
|
||||
}
|
||||
client.printf("%s\r\n", formatDateTime(now).c_str());
|
||||
} else if (args.startsWith("set ")) {
|
||||
String value = args.substring(4);
|
||||
value.trim();
|
||||
RtcDateTime dt;
|
||||
if (!parseDateTime(value, dt)) {
|
||||
client.print("Invalid format. Use: date set YYYY-MM-DD HH:MM:SS\r\n");
|
||||
return;
|
||||
}
|
||||
rtcModule.SetIsWriteProtected(false);
|
||||
rtcModule.SetIsRunning(true);
|
||||
rtcModule.SetDateTime(dt);
|
||||
rtcModule.SetIsWriteProtected(true);
|
||||
RtcDateTime verify;
|
||||
if (rtcGetDateTime(verify) && verify.Year() == dt.Year()) {
|
||||
client.printf("RTC set to: %s\r\n", formatDateTime(dt).c_str());
|
||||
} else {
|
||||
client.print("Warning: RTC write may have failed. Check wiring.\r\n");
|
||||
}
|
||||
} else {
|
||||
client.print("Usage: date | date set YYYY-MM-DD HH:MM:SS\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
void printInfo(NetworkClient &client) {
|
||||
unsigned long up = (millis() - bootTime) / 1000;
|
||||
unsigned long h = up / 3600;
|
||||
unsigned long m = (up % 3600) / 60;
|
||||
unsigned long s = up % 60;
|
||||
RtcDateTime now;
|
||||
bool dateValid = rtcGetDateTime(now);
|
||||
|
||||
float voltage = readBatteryVoltage();
|
||||
int pct = batteryPercent(voltage);
|
||||
|
||||
client.printf("\r\n [ %s v%s ]\r\n\r\n", NAME, VERSION);
|
||||
client.printf(" Device\r\n");
|
||||
client.printf(" Chip: ESP32-C3\r\n");
|
||||
client.printf(" Uptime: %02luh %02lum %02lus\r\n", h, m, s);
|
||||
client.printf(" Free heap: %lu bytes\r\n", ESP.getFreeHeap());
|
||||
client.printf(" Flash size: %lu bytes\r\n", ESP.getFlashChipSize());
|
||||
client.printf(" CPU freq: %lu MHz\r\n", ESP.getCpuFreqMHz());
|
||||
client.printf(" WiFi cache: %s\r\n", rtcValid ? "valid" : "cold boot");
|
||||
client.printf(" DS1302: %s\r\n", rtcModule.GetIsRunning() ? "running" : "stopped");
|
||||
client.printf(" Date/time: %s\r\n", dateValid ? formatDateTime(now).c_str() : "not set");
|
||||
client.printf(" Battery: %.2fV %d%%\r\n\r\n", voltage, pct);
|
||||
}
|
||||
|
||||
// ── Button handling ───────────────────────────────────────────────────────────
|
||||
|
||||
void checkButton() {
|
||||
if (digitalRead(WAKE_PIN) == LOW) {
|
||||
if (!btnArmed) {
|
||||
btnArmed = true;
|
||||
btnSince = millis();
|
||||
} else if (millis() - btnSince > RESET_HOLD_MS) {
|
||||
eraseNetConfig();
|
||||
rtcValid = false;
|
||||
startAP();
|
||||
btnArmed = false;
|
||||
}
|
||||
} else {
|
||||
if (btnArmed && !apMode && (millis() - btnSince > 50)) {
|
||||
goToSleep();
|
||||
}
|
||||
btnArmed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Setup & loop ──────────────────────────────────────────────────────────────
|
||||
|
||||
void setup() {
|
||||
bootTime = millis();
|
||||
|
||||
pinMode(WAKE_PIN, INPUT_PULLUP);
|
||||
pinMode(RUN_PIN, OUTPUT);
|
||||
pinMode(BATT_PIN, INPUT);
|
||||
analogReadResolution(12);
|
||||
ledOff();
|
||||
|
||||
rtcModule.Begin();
|
||||
rtcModule.SetIsWriteProtected(false);
|
||||
if (!rtcModule.GetIsRunning()) rtcModule.SetIsRunning(true);
|
||||
rtcModule.SetIsWriteProtected(true);
|
||||
|
||||
Wire.setPins(DATA, CLOCK);
|
||||
Wire.begin();
|
||||
bme.begin();
|
||||
|
||||
loadNetConfig();
|
||||
|
||||
if (!netCfg.configured) {
|
||||
startAP();
|
||||
} else {
|
||||
connectWiFi();
|
||||
}
|
||||
}
|
||||
|
||||
void loop() {
|
||||
blinkTick();
|
||||
checkButton();
|
||||
|
||||
NetworkClient client = server.accept();
|
||||
if (!client) return;
|
||||
|
||||
flushTelnetNegotiation(client);
|
||||
printBanner(client);
|
||||
printPrompt(client);
|
||||
|
||||
while (client.connected()) {
|
||||
blinkTick();
|
||||
|
||||
if (digitalRead(WAKE_PIN) == LOW) {
|
||||
if (!btnArmed) {
|
||||
btnArmed = true;
|
||||
btnSince = millis();
|
||||
} else if (millis() - btnSince > RESET_HOLD_MS) {
|
||||
client.print("\r\nFactory reset — entering setup mode.\r\n");
|
||||
client.stop();
|
||||
eraseNetConfig();
|
||||
rtcValid = false;
|
||||
startAP();
|
||||
btnArmed = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (btnArmed && !apMode && (millis() - btnSince > 50)) {
|
||||
client.print("\r\nButton pressed — sleeping.\r\n");
|
||||
client.stop();
|
||||
goToSleep();
|
||||
}
|
||||
btnArmed = false;
|
||||
}
|
||||
|
||||
String cmd = readCommand(client);
|
||||
if (!client.connected()) break;
|
||||
|
||||
if (cmd == "ifconfig") {
|
||||
cmdIfconfig(client, "");
|
||||
} else if (cmd.startsWith("ifconfig ")) {
|
||||
cmdIfconfig(client, cmd.substring(9));
|
||||
} else if (cmd == "help") {
|
||||
printHelp(client);
|
||||
|
||||
} else if (!apMode) {
|
||||
if (cmd == "read") {
|
||||
float temp = bme.temp();
|
||||
float hum = bme.hum();
|
||||
float press = bme.pres();
|
||||
client.printf("Temperature: %.1f C | Humidity: %.0f %% | Pressure: %.0f hPa\r\n",
|
||||
temp, hum, press);
|
||||
|
||||
} else if (cmd == "watch") {
|
||||
client.print("q + Enter to stop.\r\n");
|
||||
while (client.connected()) {
|
||||
blinkTick();
|
||||
float temp = bme.temp();
|
||||
float hum = bme.hum();
|
||||
float press = bme.pres();
|
||||
String batt = batteryInline();
|
||||
client.printf("\rT: %.1f C | H: %.0f %% | P: %.0f hPa | Batt: %s ",
|
||||
temp, hum, press, batt.c_str());
|
||||
bool quit = false;
|
||||
unsigned long t = millis();
|
||||
String watchCmd = "";
|
||||
while (millis() - t < 1000) {
|
||||
blinkTick();
|
||||
if (digitalRead(WAKE_PIN) == LOW) {
|
||||
client.print("\r\nButton pressed — sleeping.\r\n");
|
||||
client.stop();
|
||||
goToSleep();
|
||||
}
|
||||
if (client.available()) {
|
||||
char ch = client.read();
|
||||
if (ch == '\r') continue;
|
||||
if (ch == '\n') {
|
||||
watchCmd.trim();
|
||||
if (watchCmd == "q") { quit = true; break; }
|
||||
watchCmd = "";
|
||||
} else if ((uint8_t)ch != IAC) {
|
||||
watchCmd += ch;
|
||||
}
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
if (quit) { client.print("\r\n"); break; }
|
||||
}
|
||||
|
||||
} else if (cmd == "battery") {
|
||||
printBattery(client);
|
||||
|
||||
} else if (cmd == "date") {
|
||||
cmdDate(client, "");
|
||||
} else if (cmd.startsWith("date ")) {
|
||||
cmdDate(client, cmd.substring(5));
|
||||
} else if (cmd == "info") {
|
||||
printInfo(client);
|
||||
} else if (cmd == "quit") {
|
||||
client.print("Goodbye.\r\n");
|
||||
client.stop();
|
||||
return;
|
||||
} else if (cmd == "sleep") {
|
||||
client.print("Sleeping...\r\n");
|
||||
client.stop();
|
||||
goToSleep();
|
||||
} else if (cmd.length() > 0) {
|
||||
client.print("Unknown command.\r\n");
|
||||
}
|
||||
|
||||
} else if (cmd.length() > 0) {
|
||||
client.print("Unknown command. Use 'ifconfig wifi' to configure, or 'help'.\r\n");
|
||||
}
|
||||
|
||||
printPrompt(client);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user