From fa300d4352d22b852f9397a7d621d49b4a679ab0 Mon Sep 17 00:00:00 2001 From: Splatink Date: Sun, 15 Mar 2026 08:59:23 +0200 Subject: [PATCH] Added experimental 1.9 version --- telly-bme-v1.9.ino | 1177 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1177 insertions(+) create mode 100644 telly-bme-v1.9.ino diff --git a/telly-bme-v1.9.ino b/telly-bme-v1.9.ino new file mode 100644 index 0000000..f3a53e3 --- /dev/null +++ b/telly-bme-v1.9.ino @@ -0,0 +1,1177 @@ +/* + * telly-bme v1.9 + * ESP32-C3 BME280 sensor shell with telnet interface + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ── Pin definitions ─────────────────────────────────────────────────────────── +#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 + +// ── Config ──────────────────────────────────────────────────────────────────── +#define VERSION "1.9" +#define NAME "telly-bme" +#define AP_SSID "telly-bme-setup" +#define RESET_HOLD_MS 10000 +#define BLINK_MS 150 + +#define BATT_MAX_V 4.20f +#define BATT_MIN_V 3.00f +#define BATT_CAPACITY 3000.0f +#define BATT_SLEEP_MA 0.0095f +#define BATT_AWAKE_MA 50.0f +#define BATT_WAKE_S 60.0f +#define BATT_WAKES_PER_HR 1.0f +#define ADC_SAMPLES 16 +#define ADC_VREF 3.3f +#define ADC_RESOLUTION 4095.0f +#define DIVIDER_RATIO 2.0f +#define ADC_CAL 0.909f + +#define HISTORY_SIZE 10 +#define MAX_CMD_LEN 128 + +#define ED_MAX_LINES 200 +#define ED_MAX_LINE_LEN 256 +#define ED_MAX_FILENAME 64 + +#define IAC 255 + +// ── Key codes ──────────────────────────────────────────────────────────────── +#define KEY_NONE 0 +#define KEY_UP 1001 +#define KEY_DOWN 1002 +#define KEY_LEFT 1003 +#define KEY_RIGHT 1004 +#define KEY_HOME 1005 +#define KEY_END 1006 +#define KEY_DEL 1007 +#define KEY_BACKSPACE 1008 +#define KEY_ENTER 1009 +#define KEY_ESC 1010 + +// ── TelnetStream ───────────────────────────────────────────────────────────── +struct TelnetStream { + NetworkClient *client = nullptr; + + void begin(NetworkClient &c) { client = &c; } + + void flushNegotiation() { + unsigned long start = millis(); + while (millis() - start < 500) { + while (client->available()) { + uint8_t b = client->read(); + if (b == IAC) { + unsigned long t = millis(); + while (client->available() < 2 && millis() - t < 100); + if (client->available() >= 2) { client->read(); client->read(); } + } + } + delay(10); + } + } + + int readRaw() { + while (client->available()) { + uint8_t b = client->read(); + if (b == IAC) { + unsigned long t = millis(); + while (client->available() < 2 && millis() - t < 50); + if (client->available() >= 2) { client->read(); client->read(); } + continue; + } + return b; + } + return -1; + } + + int readRawTimeout(int timeoutMs = 150) { + unsigned long start = millis(); + while (millis() - start < (unsigned long)timeoutMs) { + int b = readRaw(); + if (b >= 0) return b; + yield(); + } + return -1; + } + + int readKey() { + int b = readRaw(); + if (b < 0) return KEY_NONE; + if (b == 0x1B) { + int b2 = readRawTimeout(80); + if (b2 < 0) return KEY_ESC; + if (b2 == '[') { + int b3 = readRawTimeout(80); + if (b3 < 0) return KEY_ESC; + if (b3 >= '0' && b3 <= '9') { + int num = b3 - '0'; + int b4 = readRawTimeout(80); + while (b4 >= '0' && b4 <= '9') { num = num*10+(b4-'0'); b4=readRawTimeout(80); } + switch (num) { + case 1: case 7: return KEY_HOME; + case 4: case 8: return KEY_END; + case 3: return KEY_DEL; + default: return KEY_NONE; + } + } + switch (b3) { + case 'A': return KEY_UP; case 'B': return KEY_DOWN; + case 'C': return KEY_RIGHT; case 'D': return KEY_LEFT; + case 'H': return KEY_HOME; case 'F': return KEY_END; + default: return KEY_NONE; + } + } + if (b2 == 'O') { + int b3 = readRawTimeout(80); + switch (b3) { + case 'H': return KEY_HOME; case 'F': return KEY_END; + case 'A': return KEY_UP; case 'B': return KEY_DOWN; + case 'C': return KEY_RIGHT;case 'D': return KEY_LEFT; + default: return KEY_NONE; + } + } + return KEY_ESC; + } + if (b == '\r') { readRawTimeout(20); return KEY_ENTER; } + if (b == '\n') return KEY_ENTER; + if (b == 0x7F || b == 0x08) return KEY_BACKSPACE; + return b; + } +} ts; + +// ── Persistent config ───────────────────────────────────────────────────────── +Preferences prefs; +struct NetConfig { + char ssid[64], pass[64]; + bool staticIP, configured; + uint32_t ip, gw, sn; +}; +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, rtcGW, rtcSN, rtcDNS; + +// ── Globals ─────────────────────────────────────────────────────────────────── +BME280I2C bme; +NetworkServer server(23); +ThreeWire myWire(RTC_DAT, RTC_CLK, RTC_RST); +RtcDS1302 rtcModule(myWire); + +unsigned long bootTime = 0; +bool apMode = false; +bool btnArmed = false; +unsigned long btnSince = 0; + +// ── LED ─────────────────────────────────────────────────────────────────────── +bool blinkActive=false, 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() { + for (int i=0;i<5;i++) { analogRead(BATT_PIN); delay(10); } + long sum=0; + for (int i=0;i=BATT_MAX_V) return 100; + if (v<=BATT_MIN_V) return 0; + struct { float v; int p; } c[] = { + {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} + }; + for (int i=0;i<10;i++) + if (v<=c[i].v&&v>=c[i+1].v) + return (int)(c[i+1].p+(v-c[i+1].v)/(c[i].v-c[i+1].v)*(c[i].p-c[i+1].p)); + return 0; +} +float estimateRuntime(float v) { + float rem=BATT_CAPACITY*(batteryPercent(v)/100.0f); + float awake=BATT_WAKES_PER_HR*BATT_WAKE_S; + float avg=(BATT_AWAKE_MA*awake+BATT_SLEEP_MA*(3600.0f-awake))/3600.0f; + return avg>0?rem/avg:0; +} +String batteryInline() { + float v=readBatteryVoltage(); + char buf[24]; snprintf(buf,sizeof(buf),"%.2fV %d%%",v,batteryPercent(v)); + return String(buf); +} + +// ── RTC ─────────────────────────────────────────────────────────────────────── +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 yr=s.substring(0,4).toInt(),mo=s.substring(5,7).toInt(), + dy=s.substring(8,10).toInt(),hr=s.substring(11,13).toInt(), + mn=s.substring(14,16).toInt(),sc=s.substring(17,19).toInt(); + if (yr<2020||yr>2099||mo<1||mo>12||dy<1||dy>31||hr>23||mn>59||sc>59) return false; + out=RtcDateTime(yr-2000,mo,dy,hr,mn,sc); return true; +} + +// ── Arg parser ──────────────────────────────────────────────────────────────── +int parseArgs(const String &str, String *args, int maxArgs) { + int count=0,i=0,len=str.length(); + while (i=len) break; + if (str[i]=='"') { + i++; String tok=""; + while (i0&&entries[(head-1+HISTORY_SIZE)%HISTORY_SIZE]==cmd) return; + entries[head]=cmd; head=(head+1)%HISTORY_SIZE; + if (countcount) return ""; + return entries[(head-offset+HISTORY_SIZE*10)%HISTORY_SIZE]; + } +} cmdHistory; + +void printPrompt(NetworkClient &client) { + client.print(apMode ? "[setup] ~> " : "~> "); +} + +static void redrawLine(NetworkClient &client, const String &buf, int cursor) { + client.print("\r"); + client.print(apMode ? "[setup] ~> " : "~> "); + client.print(buf); + client.print("\033[K"); + int tail=buf.length()-cursor; + if (tail>0) client.printf("\033[%dD",tail); +} + +String readCommand(NetworkClient &client) { + String buf=""; int cursor=0, histOff=0; String savedBuf=""; + while (client.connected()) { + blinkTick(); + int k=ts.readKey(); + if (k==KEY_NONE) { yield(); continue; } + switch (k) { + case KEY_ENTER: { + client.print("\r\n"); + String cmd=buf; cmd.trim(); + cmdHistory.push(cmd); return cmd; + } + case KEY_BACKSPACE: + if (cursor>0) { buf.remove(cursor-1,1); cursor--; redrawLine(client,buf,cursor); } + break; + case KEY_DEL: + if (cursor<(int)buf.length()) { buf.remove(cursor,1); redrawLine(client,buf,cursor); } + break; + case KEY_LEFT: + if (cursor>0) { cursor--; client.print("\033[D"); } break; + case KEY_RIGHT: + if (cursor<(int)buf.length()) { cursor++; client.print("\033[C"); } break; + case KEY_HOME: + if (cursor>0) { client.printf("\033[%dD",cursor); cursor=0; } break; + case KEY_END: + if (cursor<(int)buf.length()) { + client.printf("\033[%dC",buf.length()-cursor); cursor=buf.length(); + } break; + case KEY_UP: + if (histOff==0) savedBuf=buf; + if (histOff0) { + histOff--; + buf=(histOff==0)?savedBuf:cmdHistory.get(histOff); + cursor=buf.length(); redrawLine(client,buf,cursor); + } break; + case KEY_ESC: + buf=""; cursor=0; redrawLine(client,buf,cursor); break; + default: + if (k>=0x20&&k<0x7F&&(int)buf.length()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(); +} + +// ── ed-style line editor ────────────────────────────────────────────────────── + +struct Ed { + String lines[ED_MAX_LINES]; + int numLines = 0; + int cur = 0; // current line (0-indexed) + bool modified = false; + char filename[ED_MAX_FILENAME] = ""; + + void init(const char *fname) { + numLines=0; cur=0; modified=false; + strncpy(filename,fname,ED_MAX_FILENAME-1); + File f=LittleFS.open(filename,"r"); + if (f) { + while (f.available()&&numLinesnumLines) return -1; + return n-1; + } + + // Parse "addr1,addr2cmd args" or "addrcmd args" + // Fills a1,a2 (0-indexed line range) and cmd char and rest of args + // Returns false if unparseable + bool parseCommand(const String &input, int &a1, int &a2, char &cmd, String &args) { + a1=cur; a2=cur; args=""; + int i=0; int len=input.length(); + if (len==0) return false; + + // Read first address + String addrBuf=""; + while (i0; + int addr1=resolveAddr(addrBuf); + if (hasAddr&&addr1<0) return false; + if (hasAddr) { a1=addr1; a2=addr1; } + + // Check for comma (range) + if (i=len) { cmd='\0'; return true; } + cmd=input[i++]; + args=input.substring(i); args.trim(); + return true; + } + + // ── Read input lines until lone "." ─────────────────────────────────────── + void readInputLines(NetworkClient &cl, String *out, int &count, int maxLines) { + count=0; + cl.print(" (end with a line containing only .)\r\n"); + while (cl.connected()&&count=0) { + line=line.substring(0,found)+to+line.substring(found+from.length()); + changed=true; + } + } + return changed; + } + + // Parse s/from/to/[g] + bool parseSubst(const String &args, String &from, String &to, bool &global) { + if (args.length()<2||args[0]!='/') return false; + char delim=args[0]; + int p1=args.indexOf(delim,1); + if (p1<0) return false; + int p2=args.indexOf(delim,p1+1); + if (p2<0) return false; + from=args.substring(1,p1); + to=args.substring(p1+1,p2); + global=(args.length()>p2+1&&args[p2+1]=='g'); + return true; + } + + // ── Main run loop ───────────────────────────────────────────────────────── + void run(NetworkClient &cl) { + cl.printf(" \"%s\" %d lines\r\n", filename, numLines); + cl.print(" Commands: p n = i a c d s/// m w q q! (type 'h' for help)\r\n"); + + while (cl.connected()) { + cl.print(":"); + String input=readCommand(cl); + if (!cl.connected()) break; + input.trim(); + if (input.length()==0) { + // bare Enter — advance one line and print it + if (cur=numLines||a2=numLines) { + cl.print(" ? (address out of range)\r\n"); continue; + } + + switch (cmd) { + case '\0': + // Just an address — print that line and move there + cur=a2; + cl.printf("%s\r\n",lines[cur].c_str()); + break; + + case 'p': + for (int i=a1;i<=a2;i++) cl.printf("%s\r\n",lines[i].c_str()); + cur=a2; + break; + + case 'n': + for (int i=a1;i<=a2;i++) cl.printf("%4d %s\r\n",i+1,lines[i].c_str()); + cur=a2; + break; + + case '=': + if (input=="=") + cl.printf(" %d lines total, current line %d\r\n",numLines,cur+1); + else + cl.printf(" %d\r\n",a2+1); + break; + + case 'i': { + String newLines[ED_MAX_LINES]; int count=0; + readInputLines(cl,newLines,count,ED_MAX_LINES-numLines); + if (count>0) { + // Shift lines down + for (int i=numLines+count-1;i>=a1+count;i--) lines[i]=lines[i-count]; + for (int i=0;i0) { + for (int i=numLines+count-1;i>=insertAt+count;i--) lines[i]=lines[i-count]; + for (int i=0;i=a1+count;i--) lines[i]=lines[i-count]; + for (int i=0;i0?a1+count-1:max(0,a1-1); + modified=true; + break; + } + + case 'd': { + int delCount=a2-a1+1; + for (int i=a1;i=numLines||(dest>=a1&&dest<=a2)) { + cl.print(" ? (invalid destination)\r\n"); break; + } + int count=a2-a1+1; + // Copy lines to temp + String tmp[ED_MAX_LINES]; + for (int i=0;ia2?dest-count+1:dest+1; + if (insertAt<0) insertAt=0; + // Insert + for (int i=numLines+count-1;i>=insertAt+count;i--) lines[i]=lines[i-count]; + for (int i=0;ih_addr, sizeof(addr)); + char ipStr[16]; inet_ntoa_r(addr, ipStr, sizeof(ipStr)); + cl.printf("PING %s (%s)\r\n", host.c_str(), ipStr); + + int sock = socket(AF_INET, SOCK_RAW, IP_PROTO_ICMP); + if (sock < 0) { cl.print("ping: failed to create socket (needs root?)\r\n"); return; } + + struct timeval tv; tv.tv_sec=2; tv.tv_usec=0; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + struct sockaddr_in dest; + memset(&dest,0,sizeof(dest)); + dest.sin_family=AF_INET; + memcpy(&dest.sin_addr, he->h_addr, sizeof(dest.sin_addr)); + + int sent=0, received=0; + long minMs=99999, maxMs=0, totalMs=0; + + for (int seq=1; seq<=count; seq++) { + // Build ICMP echo request + uint8_t pkt[64]; memset(pkt,0,sizeof(pkt)); + struct icmp_echo_hdr *icmp=(struct icmp_echo_hdr*)pkt; + ICMPH_TYPE_SET(icmp, ICMP_ECHO); + ICMPH_CODE_SET(icmp, 0); + icmp->id = htons(0x1234); + icmp->seqno = htons(seq); + // Fill payload + for (int i=sizeof(struct icmp_echo_hdr);i<64;i++) pkt[i]=(uint8_t)(i&0xFF); + // Checksum + icmp->chksum=0; + uint32_t sum=0; + for (int i=0;i<64;i+=2) sum+=((uint16_t)pkt[i]<<8)|pkt[i+1]; + while (sum>>16) sum=(sum&0xFFFF)+(sum>>16); + icmp->chksum=htons(~sum); + + unsigned long t0=millis(); + int s=sendto(sock,(void*)pkt,sizeof(pkt),0,(struct sockaddr*)&dest,sizeof(dest)); + sent++; + + if (s<0) { cl.printf("Request %d: send failed\r\n",seq); delay(1000); continue; } + + // Wait for reply + uint8_t rbuf[128]; + struct sockaddr_in from; socklen_t fromLen=sizeof(from); + int r=recvfrom(sock,rbuf,sizeof(rbuf),0,(struct sockaddr*)&from,&fromLen); + long ms=(long)(millis()-t0); + + if (r>0) { + received++; + totalMs+=ms; + if (msmaxMs) maxMs=ms; + char fromIp[16]; inet_ntoa_r(from.sin_addr,fromIp,sizeof(fromIp)); + cl.printf(" %d bytes from %s: seq=%d time=%ldms\r\n",r,fromIp,seq,ms); + } else { + cl.printf(" Request %d: timeout\r\n",seq); + } + if (seq0?(int)((sent-received)*100/sent):100; + cl.printf("--- %s ping statistics ---\r\n",host.c_str()); + cl.printf(" %d sent, %d received, %d%% loss\r\n",sent,received,loss); + if (received>0) + cl.printf(" rtt min/avg/max = %ld/%ld/%ld ms\r\n",minMs,totalMs/received,maxMs); + cl.print("\r\n"); +} + +// ── curl ────────────────────────────────────────────────────────────────────── +void cmdCurl(NetworkClient &cl, const String &url) { + if (!url.startsWith("http://")&&!url.startsWith("https://")) { + cl.print("Usage: curl (http:// or https://)\r\n"); return; + } + HTTPClient http; + http.begin(url); + http.setTimeout(10000); + http.addHeader("User-Agent", NAME "/" VERSION); + + cl.printf("GET %s\r\n",url.c_str()); + int code=http.GET(); + if (code>0) { + cl.printf("HTTP %d\r\n",code); + // Print response headers we care about + String ct=http.header("Content-Type"); + if (ct.length()) cl.printf("Content-Type: %s\r\n",ct.c_str()); + cl.printf("Content-Length: %d\r\n\r\n",http.getSize()); + + // Stream response body, converting \n to \r\n + WiFiClient *stream=http.getStreamPtr(); + int remaining=http.getSize(); + unsigned long t=millis(); + while ((remaining>0||remaining==-1)&&stream->connected()&&millis()-t<15000) { + blinkTick(); + if (!stream->available()) { delay(10); continue; } + char c=stream->read(); + if (remaining>0) remaining--; + if (c=='\n') cl.print("\r\n"); + else cl.write(c); + } + cl.print("\r\n"); + } else { + cl.printf("Error: %s\r\n",http.errorToString(code).c_str()); + } + http.end(); +} + +// ── Sensor commands ─────────────────────────────────────────────────────────── +void cmdRead(NetworkClient &cl, const String &sub) { + float t=bme.temp(),h=bme.hum(),p=bme.pres(),v=readBatteryVoltage(); + int bp=batteryPercent(v); + if (sub==""||sub=="all") + cl.printf(" Temperature: %.1f C\r\n Humidity: %.0f %%\r\n" + " Pressure: %.0f hPa\r\n Battery: %.2fV %d%%\r\n",t,h,p,v,bp); + else if (sub=="temp") cl.printf("%.1f C\r\n",t); + else if (sub=="hum") cl.printf("%.0f %%\r\n",h); + else if (sub=="pressure") cl.printf("%.0f hPa\r\n",p); + else if (sub=="battery") cl.printf("%.2fV %d%%\r\n",v,bp); + else cl.printf("Unknown: '%s'. Try: temp hum pressure battery\r\n",sub.c_str()); +} + +void printBattery(NetworkClient &cl) { + float v=readBatteryVoltage(); int pct=batteryPercent(v); + float rt=estimateRuntime(v); int rd=(int)(rt/24),rh=(int)fmod(rt,24.0f); + String bar="["; for(int i=0;i<20;i++) bar+=(i \r\n"); return; } + strncpy(netCfg.ssid,t[0].c_str(),sizeof(netCfg.ssid)-1); + strncpy(netCfg.pass,t[1]=="open"?"":t[1].c_str(),sizeof(netCfg.pass)-1); + netCfg.configured=true; saveNetConfig(); rtcValid=false; + cl.printf("WiFi set to '%s'.\r\nGoodbye.\r\n",netCfg.ssid); + cl.stop(); delay(300); ESP.restart(); + } else if (args=="ip dhcp") { + netCfg.staticIP=false; netCfg.ip=netCfg.gw=netCfg.sn=0; + saveNetConfig(); rtcValid=false; + cl.print("Switched to DHCP.\r\n"); cl.stop(); delay(300); ESP.restart(); + } else if (args.startsWith("ip static ")) { + String rest=args.substring(10); rest.trim(); + int s1=rest.indexOf(' '),s2=rest.indexOf(' ',s1+1); + if (s1<0||s2<0) { cl.print("Usage: ifconfig ip static \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))) { cl.print("Invalid IP.\r\n"); return; } + netCfg.staticIP=true; netCfg.ip=(uint32_t)ip; netCfg.gw=(uint32_t)gw; netCfg.sn=(uint32_t)sn; + saveNetConfig(); rtcValid=false; + cl.printf("Static IP set to %s.\r\n",ip.toString().c_str()); + cl.stop(); delay(300); ESP.restart(); + } else cl.print("Unknown ifconfig subcommand.\r\n"); +} + +// ── date ────────────────────────────────────────────────────────────────────── +void cmdDate(NetworkClient &cl, const String &args) { + if (!args.length()) { + RtcDateTime now; + if (!rtcGetDateTime(now)) { cl.print("RTC not set.\r\n"); return; } + cl.printf("%s\r\n",formatDateTime(now).c_str()); + } else if (args.startsWith("set ")) { + String v=args.substring(4); v.trim(); RtcDateTime dt; + if (!parseDateTime(v,dt)) { cl.print("Format: YYYY-MM-DD HH:MM:SS\r\n"); return; } + rtcModule.SetIsWriteProtected(false); rtcModule.SetIsRunning(true); + rtcModule.SetDateTime(dt); rtcModule.SetIsWriteProtected(true); + RtcDateTime verify; + cl.print(rtcGetDateTime(verify)&&verify.Year()==dt.Year() + ?"RTC set.\r\n":"Warning: RTC write may have failed.\r\n"); + } else cl.print("Usage: date | date set YYYY-MM-DD HH:MM:SS\r\n"); +} + +// ── info ────────────────────────────────────────────────────────────────────── +void printInfo(NetworkClient &cl) { + unsigned long up=(millis()-bootTime)/1000; + RtcDateTime now; bool dv=rtcGetDateTime(now); float v=readBatteryVoltage(); + cl.printf("\r\n [ %s v%s ]\r\n\r\n Device\r\n" + " Chip: ESP32-C3\r\n Uptime: %02luh %02lum %02lus\r\n" + " Free heap: %lu bytes\r\n Flash: %lu bytes\r\n" + " CPU freq: %lu MHz\r\n WiFi cache: %s\r\n" + " DS1302: %s\r\n Date/time: %s\r\n Battery: %.2fV %d%%\r\n\r\n", + NAME,VERSION,up/3600,(up%3600)/60,up%60, + ESP.getFreeHeap(),ESP.getFlashChipSize(),ESP.getCpuFreqMHz(), + rtcValid?"valid":"cold boot", + rtcModule.GetIsRunning()?"running":"stopped", + dv?formatDateTime(now).c_str():"not set",v,batteryPercent(v)); +} + +// ── Banner / help ───────────────────────────────────────────────────────────── +void printBanner(NetworkClient &cl) { + cl.printf("\r\n +---------------------------------+\r\n" + " | %s v%s |\r\n" + " | ESP32-C3 BME280 Sensor Shell |\r\n",NAME,VERSION); + if (apMode) cl.print(" | *** SETUP MODE *** |\r\n"); + cl.print(" +---------------------------------+\r\n Type 'help' for commands.\r\n\r\n"); +} + +void printHelp(NetworkClient &cl) { + if (apMode) { + cl.print(" ifconfig wifi — configure WiFi\r\n" + " ifconfig wifi open — open WiFi\r\n" + " (quotes for spaces: \"My WiFi\" \"pass\")\r\n" + " help\r\n"); + } else { + cl.print(" Sensor\r\n" + " read — all sensors\r\n" + " read temp|hum|pressure|battery\r\n" + " watch — live (q+Enter to stop)\r\n" + " battery — detail + runtime\r\n" + " Time\r\n" + " date\r\n" + " date set \r\n" + " Files\r\n" + " ls\r\n" + " vi — line editor (type h inside for help)\r\n" + " cat \r\n" + " rm \r\n" + " Network\r\n" + " ping [count]\r\n" + " curl \r\n" + " ifconfig\r\n" + " ifconfig wifi \r\n" + " ifconfig ip dhcp\r\n" + " ifconfig ip static \r\n" + " System\r\n" + " info\r\n" + " help quit sleep\r\n"); + } +} + +// ── Button ──────────────────────────────────────────────────────────────────── +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; + } +} + +// ── Telnet negotiation ──────────────────────────────────────────────────────── +void negotiateTelnet(NetworkClient &client) { + const uint8_t neg[] = { + 255,251,1, // IAC WILL ECHO + 255,251,3, // IAC WILL SUPPRESS-GO-AHEAD + 255,253,3, // IAC DO SUPPRESS-GO-AHEAD + 255,254,34, // IAC DONT LINEMODE + }; + client.write(neg,sizeof(neg)); + delay(100); +} + +// ── Setup ───────────────────────────────────────────────────────────────────── +void setup() { + bootTime=millis(); + pinMode(WAKE_PIN,INPUT_PULLUP); + pinMode(RUN_PIN,OUTPUT); + pinMode(BATT_PIN,INPUT); + analogReadResolution(12); + ledOff(); + LittleFS.begin(true); + 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(); +} + +// ── Loop ────────────────────────────────────────────────────────────────────── +void loop() { + blinkTick(); + checkButton(); + + NetworkClient client=server.accept(); + if (!client) return; + + ts.begin(client); + negotiateTelnet(client); + ts.flushNegotiation(); + 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") cmdRead(client,""); + else if (cmd.startsWith("read ")) cmdRead(client,cmd.substring(5)); + else if (cmd=="watch") { + client.print("q + Enter to stop.\r\n"); + while (client.connected()) { + blinkTick(); + client.printf("\rT: %.1fC H: %.0f%% P: %.0fhPa Batt: %s ", + bme.temp(),bme.hum(),bme.pres(),batteryInline().c_str()); + bool quit=false; + unsigned long wt=millis(); String wbuf=""; + while (millis()-wt<1000&&!quit) { + blinkTick(); int k=ts.readKey(); + if (k==KEY_NONE) { yield(); continue; } + if (k==KEY_ENTER) { wbuf.trim(); if(wbuf=="q") quit=true; wbuf=""; } + else if (k>=' '&&k<0x7F) wbuf+=(char)k; + } + 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=="ls") cmdLs(client); + else if (cmd.startsWith("vi ")) { + String fn=cmd.substring(3); fn.trim(); + if (!fn.startsWith("/")) fn="/"+fn; + ed.init(fn.c_str()); ed.run(client); + } + else if (cmd.startsWith("cat ")) cmdCat(client,cmd.substring(4)); + else if (cmd.startsWith("rm ")) cmdRm(client,cmd.substring(3)); + else if (cmd.startsWith("ping ")) { + String rest=cmd.substring(5); rest.trim(); + String parts[2]; int n=parseArgs(rest,parts,2); + int count=(n>=2)?parts[1].toInt():4; + if (count<1||count>20) count=4; + cmdPing(client,parts[0],count); + } + else if (cmd.startsWith("curl ")) cmdCurl(client,cmd.substring(5)); + 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()) client.print("Unknown command. Type 'help'.\r\n"); + } else if (cmd.length()) { + client.print("Unknown command. Use 'ifconfig wifi' or 'help'.\r\n"); + } + + if (client.connected()) printPrompt(client); + } +}