/* * 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); } }