Zum Inhalt

energy_logger.tc

energy_logger.tc — stand-in SD logger for the Energy_Manager (.61)

Source on GitHub

// energy_logger.tc — stand-in SD logger for the Energy_Manager (.61)
// ---------------------------------------------------------------------------
// The Energy_Manager (.61, Scripter) still reads every meter/sensor and
// broadcasts the values as UDP fleet globals (g:...), but its SD card died so
// its own writes to /energy_15_log.txt fail. This TinyC script runs on ANY
// TinyC device that HAS a working filesystem (SD preferred), receives those
// same globals, and appends a row every 15 minutes in the EXACT column format
// of energy_15_log.txt — so the data continues seamlessly and can later be
// merged back / charted by solar_dashboard.tc, core2_energy.tc, etc.
//
// The cumulative kWh counters (WR1=t_ga, WR2=t_gh, WR3=t_gg, WB=t_wb, SD=sedt,
// ZWZI/ZWZO, GAS, H20 ...) keep counting on .61's meters even while its SD is
// dead, so these rows continue the same series with no discontinuity.
//
// It ALSO writes the parallel .ind index (TSTAMP<TAB>byteoffset) exactly like
// the Energy_Manager, so the port-82 "@from_to" fast range-extract keeps working
// on the new file.
//
// DEPLOY:
//   node scratchpad/tc_compile.mjs tasmota/tinyc/examples/energy_logger.tc energy_logger.tcb
//   upload energy_logger.tcb to a TinyC device WITH a filesystem, run it in a slot.
//   To continue the existing history, first copy the 29 MB energy_15_log.txt
//   (+ .ind) from the SD backup onto the new device; otherwise it starts fresh
//   with a header. Verify with the ELOGNOW console command (forces one row now).
//
// NOTE: this logger only READS globals (never assigns them), so it is safe to
// run alongside other receivers (house_monitor.tc, energy_dashboard.tc).
//
// Two columns (BTMP_a = boiler sml[29], RTMP_a = Rücklauf sml[30]) are NOT
// broadcast by .61, so they are written as 0.0. If you want them, add
// `g:bltmp=sml[29]` and `g:rltmp=sml[30]` on .61 and two globals + fields here.
// ---------------------------------------------------------------------------

#define LOG      "/energy_15_log.txt"
#define LOGI     "/energy_15_log.ind"
#define LOGD     "/energy_m_log.txt"     // daily rollup — one row at midnight
#define LOGDI    "/energy_m_log.ind"
#define SLOT_MIN  15                     // one row every 15 minutes (0..95/day)

// ---- fleet globals broadcast by the Energy_Manager (.61) — RECEIVE-ONLY ----
// CRITICAL: declare these WITHOUT an initializer. `global float x = 0.0;` compiles
// to STORE_GLOBAL_UDP at startup, which BROADCASTS x=0 over the multicast and
// clobbers .61's real value across the whole fleet. `global float x;` only receives.
global float t_wb;   // WB     Wallbox total (kWh)          sml[12]
global float t_ga;   // WR1    Solar Garage total (kWh)     sml[23]  <-- Fronius
global float t_gh;   // WR2    Solar Gartenhaus total (kWh) sml[24]
global float t_gg;   // WR3    Solar Garten total (kWh)     sml[25]
global float atmp;   // ATMP_a Aussentemperatur (°C)
global float bw_ww;   // WWTMP_a Brauchwasser (°C)
global float scol;   // SKTMP_a Solarkollektor (°C)
global float ssp;   // SSTMP_a Solarspeicher (°C)
global float sedt;   // SD     Solar Hausdach total (kWh)   sml[36]
global float zwzi;   // ZWZI   2-Richtungszähler in (kWh)   sml[39]
global float zwzo;   // ZWZO   2-Richtungszähler out (kWh)  sml[40]
global float t_ws;   // H20    Wasser total (m³)            sml[1]
global float t_gs;   // GAS    Gas total (m³)               sml[2]
global float pwl;   // PWL_a  Powerwall SOC (%)
global float sip;   // SIP_a  PW Netz (W)
global float sop;   // SOP_a  PW Solar (W)
global float bip;   // BIP_a  PW Batterie (W)
global float hip;   // HIP_a  PW Haus (W)
global float rper;   // RCAP_a PW remaining (%)
global float klima;   // KLI_a  Klima (W)  — logged negated
global float pwp;   // PWP_a  Pool-WP (W) — logged negated
global float t_kpwp;   // WP     Klima+Pool total (kWh)
global float bwwp;   // BWWP   Brauchwasser-WP total (kWh)
global float train;   // RAIN   Regen total (mm)
global float hwp;   // HWP_a  Heizungs-WP current (W)
global float t_hwp;   // HWP    Heizungs-WP total (kWh)
global float ktmp;   // KTMP_a Kellertemperatur (°C)
global float avgt;   // AVTMP_a gleitender Tagesmittel (°C)

// ---- momentary-value globals, WEB DASHBOARD ONLY (not logged), receive-only ----
global float sedc;   // Solar Hausdach momentary (W)
global float wrga;   // Solar Garage momentary (W, negative = producing)
global float wrgh;   // Solar Gartenhaus momentary (W)
global float wrgg;   // Solar Garten momentary (W)
global float auto;   // Wallbox momentary (W)
global float zwzc;   // Hauszähler momentary (W)
global float rtemp;  // Regen Außentemperatur (°C)
global float spmp;   // Solarpumpe (0/1)

// ---- working state ----
int  last_slot = -1;         // last written 15-min slot (0..95); -1 = not primed
int  last_day  = -1;         // last day written to the daily file; -1 = not primed
int  rows      = 0;          // rows written this session
char g_msg[160];             // scratch for logs / responses
char scratch[256];           // scratch for web rows

// ── SML meters (full .61 Energy_Manager replacement, staged) ──
// When enabled, load /sml_meter.def and read the house meters DIRECTLY instead of
// receiving fleet globals. REQUIRES: firmware rebuilt with USE_SML_M (+ the SML
// defines in user_config_override.h) AND the descriptor pins remapped for .190's
// hardware. Off by default — leave off until firmware + wiring are ready.
persist watch int sml_activ;

// Shared built row. NOTE: we do NOT pass the path as a char[] param — a literal
// string passed through a fn hits the const-ref gap and the ref form of fileOpen
// returns -1. So the row is built once here and each file uses LITERAL paths.
char g_line[600];            // the built 31-field data row
char g_ts[24];               // its "D.M.YY H:MM" timestamp
char g_first[24];            // first timestamp in the 15-min log (WebUI "Log vom")
char g_last[24];             // last  timestamp in the 15-min log (WebUI "Log bis")
char dbgbuf[4200];           // scratch for ELOGIX/ELOGRT range-extraction diagnostics

// Write the 31-column header to an already-open handle (matches .61 exactly).
void writeHeaderH(int h) {
    char hd[400];
    strcpy(hd, "TSTAMP\tWB\tWR1\tWR2\tWR3\tATMP_a\tWWTMP_a\tBTMP_a\tRTMP_a\tSKTMP_a\tSSTMP_a\t");
    hd += "SD\tZWZI\tZWZO\tH20\tGAS\tPWL_a\tSIP_a\tSOP_a\tBIP_a\tHIP_a\t";
    hd += "RCAP_a\tKLI_a\tPWP_a\tWP\tBWWP\tRAIN\tHWP_a\tHWP\tKTMP_a\tAVTMP_a\n";
    fileWrite(h, hd, strlen(hd));
}

// Fill g_ts + g_line from the current globals (chunked sprintf keeps each small).
void buildRow() {
    timeStamp(g_ts);                     // "2026-07-02T11:03:00"
    timeConvert(g_ts, 1);                // -> "2.7.26 11:03"  (Energy_Manager format)
    char c2[256];
    sprintf(g_line, "%s\t%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t", g_ts, t_wb, t_ga, t_gh, t_gg, atmp);
    sprintf(c2, "%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t", bw_ww, 0.0, 0.0, scol, ssp);          g_line += c2; // WWTMP,BTMP,RTMP,SKTMP,SSTMP
    sprintf(c2, "%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t", sedt, zwzi, zwzo, t_ws, t_gs);         g_line += c2; // SD,ZWZI,ZWZO,H20,GAS
    sprintf(c2, "%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t", pwl, sip, sop, bip, hip);              g_line += c2; // PWL,SIP,SOP,BIP,HIP
    sprintf(c2, "%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t", rper, 0.0-klima, 0.0-pwp, t_kpwp, bwwp); g_line += c2; // RCAP,KLI,PWP,WP,BWWP
    sprintf(c2, "%.3f\t%.3f\t%.3f\t%.3f\t%.3f\n", train, hwp, t_hwp, ktmp, avgt);        g_line += c2; // RAIN,HWP_a,HWP,KTMP,AVTMP
}

// Append the built row + its .ind entry. daily=0 -> 15-min file, 1 -> daily file.
void writeRow(int daily) {
    buildRow();
    int h; int hi; int off;
    char idx[48];
    if (daily) {
        if (fileExists(LOGD) == 0) { int wh = fileOpen(LOGD, "w"); if (wh >= 0) { writeHeaderH(wh); fileClose(wh); } }
        h = fileOpen(LOGD, "a");
        if (h < 0) { addLog("ELOG: LOGD open FAIL"); return; }
        fileSeek(h, 0, 2); off = fileTell(h);
        fileWrite(h, g_line, strlen(g_line)); fileClose(h);
        sprintf(idx, "%s\t%d\n", g_ts, off);
        hi = fileOpen(LOGDI, "a"); if (hi >= 0) { fileWrite(hi, idx, strlen(idx)); fileClose(hi); }
    } else {
        if (fileExists(LOG) == 0) { int wh = fileOpen(LOG, "w"); if (wh >= 0) { writeHeaderH(wh); fileClose(wh); } }
        h = fileOpen(LOG, "a");
        if (h < 0) { addLog("ELOG: LOG open FAIL"); return; }
        fileSeek(h, 0, 2); off = fileTell(h);
        fileWrite(h, g_line, strlen(g_line)); fileClose(h);
        sprintf(idx, "%s\t%d\n", g_ts, off);
        hi = fileOpen(LOGI, "a"); if (hi >= 0) { fileWrite(hi, idx, strlen(idx)); fileClose(hi); }
    }
    rows = rows + 1;
    sprintf(g_msg, "ELOG: wrote %s daily=%d WR1=%.3f off=%d", g_ts, daily, t_ga, off);
    addLog(g_msg);
}

// Read the FIRST and LAST row timestamps of the 15-min log into g_first/g_last, so
// the WebUI can show the log's date span (the Scripter "Log vom" / "Log bis").
// Mirrors Scripter's fextract -2/-1 (xdrv_10 ~2444): FIRST = skip the TSTAMP header
// line and take line 2; LAST = seek from the end, scan back to the previous '\n'.
// Pure seeks + two tiny reads (head + tail) — NEVER scans the whole 29 MB file (the
// built-in fileRange() would read every line). back must exceed one row (~340 B);
// 768 is well past that AND past Scripter's own 256-byte FEXT_MAX_LINE_LENGTH.
void read_log_span() {
    g_first[0] = 0; g_last[0] = 0;
    int h = fileOpen(LOG, "r");
    if (h < 0) { return; }
    fileSeek(h, 0, 2);
    int sz = fileTell(h);
    char c[800];
    int i; int j; int st; int n; int f0;

    // first data row — skip the "TSTAMP..." header line if present
    fileSeek(h, 0, 0);
    n = fileRead(h, c, 300);
    if (n > 0) {
        st = 0;
        f0 = c[0] & 0xFF;
        if (f0 < 48 || f0 > 57) {                        // line 1 not a digit -> header
            while (st < n && c[st] != 10) { st = st + 1; }
            st = st + 1;                                 // start of line 2
        }
        i = 0; j = st;
        while (j < n && c[j] != 9 && c[j] != 10 && c[j] != 13 && i < 23) {
            g_first[i] = c[j]; i = i + 1; j = j + 1;
        }
        g_first[i] = 0;
    }

    // last data row — read the tail (bigger than one ~300-450 B row so the line
    // BEFORE it, hence its leading \n, is captured), take the last line's 1st field
    int back = 768;
    if (back > sz) { back = sz; }
    fileSeek(h, sz - back, 0);
    n = fileRead(h, c, back);
    while (n > 0 && (c[n-1] == 10 || c[n-1] == 13)) { n = n - 1; }   // trim trailing NL
    st = n;
    while (st > 0 && c[st-1] != 10) { st = st - 1; }                 // back to line start
    i = 0; j = st;
    while (j < n && c[j] != 9 && c[j] != 10 && c[j] != 13 && i < 23) {
        g_last[i] = c[j]; i = i + 1; j = j + 1;
    }
    g_last[i] = 0;
    fileClose(h);
}

void main() {
    last_slot = -1;
    rows = 0;
    addCommand("ELOG");          // console: ELOGnow (force a row), ELOGstat (status)
    addLog("ELOG: energy_logger started (15-min -> /energy_15_log.txt)");
    // globals auto-arrive via UDP; first row is written at the next 15-min boundary.
}

void EverySecond() {
    // SML-enable checkbox toggled -> (re)load the meter descriptor + restart SML.
    // Needs the USE_SML_M firmware; a no-op on a build without it.
    if (changed(sml_activ)) {
        snapshot(sml_activ);
        if (sml_activ) {
            smlScripterLoad("/sml_meter.def");
            tasmCmd("Sensor53 r", g_msg);            // force SML to re-read the descriptor
            addLog("ELOG: SML meters ON (/sml_meter.def loaded)");
        } else {
            addLog("ELOG: SML meters OFF (reboot to fully release the serial ports)");
        }
    }

    if (tasm_year < 2025) { return; }                    // wait for NTP time

    // guard: the 3 solar totals are always large (>1) once received; all-zero =
    // .61 not yet heard, so skip writing rather than corrupt the log with a zero row.
    float sum3 = t_ga + t_gh + t_gg;
    int have = 0;
    if (sum3 >= 1.0) { have = 1; }

    // 15-min log — one row per 0..95 slot boundary
    int slot = (tasm_hour * 60 + tasm_minute) / SLOT_MIN;
    if (last_slot < 0) { last_slot = slot; }              // prime at boot (no stale first row)
    else if (slot != last_slot) {
        last_slot = slot;
        if (have) { writeRow(0); }
        else { addLog("ELOG: skip 15-min write — no globals yet"); }
    }

    // daily rollup — one row at midnight when the day changes (mirrors .61)
    if (last_day < 0) { last_day = tasm_day; }            // prime at boot
    else if (tasm_day != last_day) {
        last_day = tasm_day;
        if (have) { writeRow(1); }
    }
}

// ── Web dashboard — mirrors the Energy_Manager (.61) >W page layout ──
// Renders inline on the main Tasmota page. Magenta section headers + yellow
// values, same sections/order as the Scripter original. Fields that came from
// per-meter sml[] sub-readings (phase V/A, boiler/return temps, meter IDs) are
// omitted until this script reads the meters directly (the future full replace).
void web_section(char title[]) {
    sprintf(scratch, "{s}<hr>{m}<hr>{e}{s}<span style='color:magenta;'>%s</span>{m}{e}", title);
    webSend(scratch);
}
void row_i(char label[], int v, char unit[]) {
    sprintf(scratch, "{s}%s{m}<span style='color:yellow;'>%d %s</span>{e}", label, v, unit);
    webSend(scratch);
}
void row_f1(char label[], float v, char unit[]) {
    sprintf(scratch, "{s}%s{m}<span style='color:yellow;'>%.1f %s</span>{e}", label, v, unit);
    webSend(scratch);
}
void row_f2(char label[], float v, char unit[]) {
    sprintf(scratch, "{s}%s{m}<span style='color:yellow;'>%.2f %s</span>{e}", label, v, unit);
    webSend(scratch);
}
void row_f3(char label[], float v, char unit[]) {
    sprintf(scratch, "{s}%s{m}<span style='color:yellow;'>%.3f %s</span>{e}", label, v, unit);
    webSend(scratch);
}
void row_f4(char label[], float v, char unit[]) {
    sprintf(scratch, "{s}%s{m}<span style='color:yellow;'>%.4f %s</span>{e}", label, v, unit);
    webSend(scratch);
}

void WebCall() {
    sprintf(scratch, "{s}<h2 style='color:#2ecc71;text-align:center;margin:2px;'>%02d:%02d:%02d</h2>{m}%02d.%02d.%04d{e}",
            tasm_hour, tasm_minute, tasm_second, tasm_day, tasm_month, tasm_year);
    webSend(scratch);

    // control: read the meters directly via SML instead of the fleet globals
    web_section("Steuerung");
    webCheckbox(sml_activ, "SML Zähler direkt lesen (statt Fleet-Globals)");

    web_section("Wasser");
    row_f2("Zählerstand", t_ws, "m³");

    // Gas section dropped — heat pump now, no gas meter (GAS column kept in the
    // log file for format/column compatibility with the historical data).

    web_section("Heizung");
    row_f1("Außentemperatur", atmp, "°C");
    row_f1("Mittlere Außentemp.", avgt, "°C");
    row_f1("Kellertemperatur", ktmp, "°C");
    row_f1("Warmwasser", bw_ww, "°C");

    web_section("Solarthermie");
    row_f1("Solarkollektor", scol, "°C");
    row_f1("Solarspeicher", ssp, "°C");
    row_i("Solarpumpe", (int)spmp, "");

    web_section("Regen");
    row_i("Außentemperatur", (int)rtemp, "°C");
    row_f1("Regen total", train, "mm");

    web_section("Haupt Hauszähler");
    row_f4("Verbrauch", zwzi, "kWh");
    row_f4("Einspeisung", zwzo, "kWh");
    row_i("aktueller Verbrauch", (int)zwzc, "W");

    web_section("Wallbox");
    row_i("aktuell", (int)auto, "W");
    row_f3("Total", t_wb, "kWh");

    web_section("Klimaanlage");
    row_i("aktuell", (int)(0.0 - klima), "W");
    row_f3("Total", t_kpwp, "kWh");

    web_section("Poolwärmepumpe");
    row_i("aktuell", (int)(0.0 - pwp), "W");

    web_section("Heizungswärmepumpe");
    row_i("aktuell", (int)(0.0 - hwp), "W");
    row_f3("Total", t_hwp, "kWh");

    web_section("Solar Hausdach");
    row_i("aktuell", (int)sedc, "W");
    row_f3("Total", sedt, "kWh");

    web_section("Solar Garage");
    row_i("aktuell", (int)(0.0 - wrga), "W");
    row_f3("Total", t_ga, "kWh");

    web_section("Solar Gartenhaus");
    row_i("aktuell", (int)(0.0 - wrgh), "W");
    row_f3("Total", t_gh, "kWh");

    web_section("Solar Garten");
    row_i("aktuell", (int)(0.0 - wrgg), "W");
    row_f3("Total", t_gg, "kWh");

    web_section("Powerwall");
    sprintf(scratch, "{s}Batterie Füllstand{m}<span style='color:yellow;'>%.0f%% (%.2f kWh)</span>{e}", pwl, pwl / 100.0 * 13.5);
    webSend(scratch);
    sprintf(scratch, "{s}Netz{m}<span style='color:yellow;'>%.0f W</span>{e}", sip); webSend(scratch);
    sprintf(scratch, "{s}Solar{m}<span style='color:#2ecc71;'>%.0f W</span>{e}", sop); webSend(scratch);
    sprintf(scratch, "{s}Batterie{m}<span style='color:yellow;'>%.0f W</span>{e}", bip); webSend(scratch);
    sprintf(scratch, "{s}Haus{m}<span style='color:#e74c3c;'>%.0f W</span>{e}", hip); webSend(scratch);

    web_section("Logfile");
    read_log_span();                                       // first/last 15-min-log dates
    sprintf(scratch, "{s}Log vom{m}%s{e}", g_first); webSend(scratch);
    sprintf(scratch, "{s}Log bis{m}%s{e}", g_last);  webSend(scratch);
    row_i("Log Größe", fileSize(LOG) / 1000, "kB");
    row_i("Zeilen (Sitzung)", rows, "");

    web_section("System");
    row_i("System Heap", tasm_heap / 1000, "kB");
    // Download via TinyC's port-82 file server (streams on a core-1 task) instead of
    // /ufsd on port 80 — the big-file stream would otherwise wedge the main loop.
    // location.hostname keeps it on this device whether reached by IP or hostname.
    webSend("{s}Tages-Log{m}<a href=\"#\" onclick=\"location.href='//'+location.hostname+':82/ufs/energy_m_log.txt'\">download</a>{e}");
    webSend("{s}15-min-Log{m}<a href=\"#\" onclick=\"location.href='//'+location.hostname+':82/ufs/energy_15_log.txt'\">download</a>{e}");
}

void Command(char cmd[]) {
    // "ELOG" prefix is stripped: ELOGNOW->"NOW", ELOGSTAT->"STAT", ELOGDAY->"DAY"
    if (strFind(cmd, "NOW") >= 0) {                        // force a 15-min row now (test)
        writeRow(0);
        responseCmnd("ELOG: 15-min row written");
    } else if (strFind(cmd, "DAY") >= 0) {                 // force a daily row now (test)
        writeRow(1);
        responseCmnd("ELOG: daily row written");
    } else if (strFind(cmd, "STAT") >= 0) {
        sprintf(g_msg, "ELOG rows=%d 15m=%d day=%d WR1=%.3f WR2=%.3f WR3=%.3f pwl=%.1f",
                rows, fileSize(LOG), fileSize(LOGD), t_ga, t_gh, t_gg, pwl);
        responseCmnd(g_msg);
    } else if (strFind(cmd, "IX") >= 0) {                  // inspect the .ind (coverage + offsets)
        int h; int sz; int n; int st; int i; int back;
        char f1[48]; char l1[48];
        f1[0] = 0; l1[0] = 0;
        h = fileOpen(LOGI, "r");
        if (h < 0) { responseCmnd("IX: LOGI open FAIL"); return; }
        fileSeek(h, 0, 2); sz = fileTell(h);
        // first data entry: read head, skip TSTAMP header line, take line 2 up to \n
        fileSeek(h, 0, 0); n = fileRead(h, dbgbuf, 400);
        st = 0;
        if (n > 0 && (dbgbuf[0] < 48 || dbgbuf[0] > 57)) {
            while (st < n && dbgbuf[st] != 10) { st = st + 1; } st = st + 1;
        }
        i = 0;
        while (st < n && dbgbuf[st] != 10 && dbgbuf[st] != 13 && i < 47) { f1[i] = dbgbuf[st]; i = i + 1; st = st + 1; }
        f1[i] = 0;
        // last data entry: read tail, trim NL, back up to prev \n
        back = 300; if (back > sz) { back = sz; }
        fileSeek(h, sz - back, 0); n = fileRead(h, dbgbuf, back);
        while (n > 0 && (dbgbuf[n-1] == 10 || dbgbuf[n-1] == 13)) { n = n - 1; }
        st = n; while (st > 0 && dbgbuf[st-1] != 10) { st = st - 1; }
        i = 0;
        while (st < n && dbgbuf[st] != 10 && dbgbuf[st] != 13 && i < 47) { l1[i] = dbgbuf[st]; i = i + 1; st = st + 1; }
        l1[i] = 0;
        fileClose(h);
        sprintf(g_msg, "IX size=%d logsz=%d first=[%s] last=[%s]", sz, fileSize(LOG), f1, l1);
        responseCmnd(g_msg);
    } else if (strFind(cmd, "RT") >= 0) {                  // range-test: parse the log tail, count July-26 rows
        int h; int sz; int n; int st; int k; int D; int M; int Y;
        int july; int tot; int i;
        char t0[24]; char t1[24];
        h = fileOpen(LOG, "r");
        if (h < 0) { responseCmnd("RT: LOG open FAIL"); return; }
        fileSeek(h, 0, 2); sz = fileTell(h);
        n = 4000; if (n > sz) { n = sz; }
        fileSeek(h, sz - n, 0); n = fileRead(h, dbgbuf, n);
        fileClose(h);
        st = 0; while (st < n && dbgbuf[st] != 10) { st = st + 1; } st = st + 1;  // drop partial 1st line
        july = 0; tot = 0; t0[0] = 0; t1[0] = 0;
        while (st < n) {
            D = 0; M = 0; Y = 0; k = st;
            while (k < n && dbgbuf[k] >= 48 && dbgbuf[k] <= 57) { D = D * 10 + (dbgbuf[k] - 48); k = k + 1; }
            if (k < n && dbgbuf[k] == 46) { k = k + 1; }
            while (k < n && dbgbuf[k] >= 48 && dbgbuf[k] <= 57) { M = M * 10 + (dbgbuf[k] - 48); k = k + 1; }
            if (k < n && dbgbuf[k] == 46) { k = k + 1; }
            while (k < n && dbgbuf[k] >= 48 && dbgbuf[k] <= 57) { Y = Y * 10 + (dbgbuf[k] - 48); k = k + 1; }
            if (M >= 1 && M <= 12 && D >= 1 && D <= 31) {
                tot = tot + 1;
                i = 0; k = st;
                while (k < n && dbgbuf[k] != 9 && dbgbuf[k] != 10 && dbgbuf[k] != 13 && i < 23) { t1[i] = dbgbuf[k]; i = i + 1; k = k + 1; }
                t1[i] = 0;
                if (t0[0] == 0) { strcpy(t0, t1); }
                if (Y == 26 && M == 7) { july = july + 1; }
            }
            while (st < n && dbgbuf[st] != 10) { st = st + 1; } st = st + 1;
        }
        sprintf(g_msg, "RT tail=%d tot=%d july26=%d first=[%s] last=[%s]", n, tot, july, t0, t1);
        responseCmnd(g_msg);
    } else {
        responseCmnd("ELOG: ELOGnow | ELOGday | ELOGstat | ELOGix | ELOGrt");
    }
}