Skip to content

matter_bridge_ui.tc

matter_bridge_ui.tc — interactive remote-device bridge for matter_c (Berry-style).

Source on GitHub

// matter_bridge_ui.tc — interactive remote-device bridge for matter_c (Berry-style).
// A web table on THIS device: type a remote ESP's IP, Discover probes its HTTP
// Status for sensors + actuators, "Add" appends them as Matter endpoints (live
// rebuild, persisted to /bridge.cfg). THIS device (USE_MATTER_C build) is the
// Matter node — pair it on /mt. Remote ESPs keep running plain Tasmota.
//
// Beyond Matter, the bridge's OWN web page also:
//   • shows the LIVE sensor values / actuator state, and
//   • lets you STEER the actuators directly — a toggle button per relay/lamp and
//     a brightness slider per lamp (the first 4 actuators get web controls).
//
//   read sensors   GET /cm?cmnd=Status 10   (StatusSNS: Temperature/Humidity/Pressure)
//   read actuators GET /cm?cmnd=Status 11   (StatusSTS: POWER/POWER1.. + Dimmer)
//   control        GET /cm?cmnd=Power[n] ON|OFF  /  Dimmer <0..100>
//
// USAGE (bridge web page, "Matter Remote Bridge" section):
//   1. type the remote IP, click "Discover" — found sensors/relays/lamps listed
//   2. "Add these to Matter" — appended + the Matter model rebuilds in place
//      (duplicates of an already-bridged ip+key are skipped)
//   3. toggle/dim the actuators from here, or via your Matter controller
//   4. "Clear all bridged" wipes the config.
//
// SAFETY (project rules): ALL httpGet runs on ONE task (TaskLoop). Web widgets
// only set globals; EverySecond reads them and QUEUES a cmnd (flag set LAST) that
// TaskLoop sends — never httpGet from a web/EverySecond context. The Matter
// MODEL is (re)built by build_model() on the SAME TaskLoop right after a config
// change (do_add / clear) — matterReset/Add/Start run in the VM-task context just
// like main(), so they never race the poller and we avoid a slot self-restart.
// (Self-restart via "TinyCRun 0" is unsafe here: if the restart fires while the
// TaskLoop is blocked inside poll_all()'s multi-second httpGet, the task can't
// stop in time → "Task did not stop in time, abandoned" → TaskLoop dies.)

// ───────────── config (persisted to /bridge.cfg) ─────────────
// kind: 0=temperature 1=humidity 2=pressure 4=relay 5=dimmable light
char cfg_ip[8][16];      // remote IP per bridged item
int  cfg_kind[8];        // what it is
char cfg_key[8][16];     // JSON key ("Temperature", "POWER1", …)
char cfg_name[8][24];    // Matter endpoint label
char cfg_src[8][24];     // source remote's friendly DeviceName ("from where")
int  cfg_ep[8];          // Matter endpoint id (runtime)
float cfg_val[8];        // last polled value (sensor reading; relay/lamp = 0/1)
int  cfg_dim[8];         // last polled dimmer % (lamps)
int  ncfg = 0;

// discovery scratch (filled by TaskLoop, shown by WebCall)
char disc_ip[16];
char disc_src[24];       // discovered remote's DeviceName (falls back to IP)
int  disc_kind[8];
char disc_key[8][16];
char disc_name[8][24];
int  disc_n = 0;

char buf[512];           // shared httpGet response — TaskLoop only

// web → task handshakes
char ip_in[16];
watch int btn_disc = 0; watch int btn_add = 0; watch int btn_clear = 0;
int disc_req = 0; int add_req = 0; int clear_req = 0;

// web actuator controls — first 4 actuators (relay/lamp). webButton/webSlider
// need a distinct scalar global each (can't bind an array element), so 4 slots.
watch int ctl0 = 0; watch int ctl1 = 0; watch int ctl2 = 0; watch int ctl3 = 0;   // toggle
watch int dmv0 = 0; watch int dmv1 = 0; watch int dmv2 = 0; watch int dmv3 = 0;   // dimmer
int last_dmv0 = 0; int last_dmv1 = 0; int last_dmv2 = 0; int last_dmv3 = 0;

// control queue (MatterInvoke + EverySecond fill, TaskLoop sends; flag LAST)
char pend_ip[16]; char pending[40]; int cmd_ready = 0;
int poll_tick = 0;

// ── tiny JSON helpers (daikin dfield pattern) ──
float jget(char src[], char key[]) {            // key incl. colon -> number, else -99999
    char v[16]; int p;
    p = strFind(src, key); if (p < 0) { return -99999.0; }
    strSub(v, src, p + strlen(key), 14); return atof(v);
}
int jbool(char src[], char key[]) {             // key e.g. "POWER1" -> 1/0, -1 absent
    char v[10]; int p;
    p = strFind(src, key); if (p < 0) { return -1; }
    strSub(v, src, p + strlen(key), 8);
    if (strFind(v, "ON") >= 0) { return 1; } return 0;
}
// fetch a remote's friendly DeviceName into dst (TaskLoop only; falls back to ip).
// Uses the firmware jsonStr() builtin (Tasmota's JsonParser) — robust, unlike the
// earlier hand-rolled string slicing which tripped over TinyC quote/char quirks.
void get_devname(char ip[], char dst[]) {
    char url[64];
    dst[0] = 0;
    sprintf(url, "http://%s/cm?cmnd=DeviceName", ip); buf[0] = 0;   // {"DeviceName":"NAME"}
    if (httpGet(url, buf) > 0) { jsonStr(buf, "DeviceName", dst); }
    if (strlen(dst) < 1) { strcpy(dst, ip); }                       // fallback: show the IP
}

// queue a Tasmota cmnd to a remote (TaskLoop sends it; skip if one is pending)
void queue_cmd(char ip[], char c[]) {
    if (cmd_ready) { return; }
    strcpy(pend_ip, ip); strcpy(pending, c); cmd_ready = 1;       // flag LAST
}
// build "Power[n]%20ON|OFF" for a relay/lamp item's POWER key into c
void power_cmd(char c[], char key[], int on) {
    char suf[6];
    strSub(suf, key, 5, 4);                      // "" for POWER, "1".. for POWERn
    if (on) { sprintf(c, "Power%s%%20ON", suf); }
    else    { sprintf(c, "Power%s%%20OFF", suf); }
}

// ── persistence (flat comma tuples; strToken is 1-indexed; write 1D temp then strcpy) ──
void save_cfg() {
    char line[120]; int i; int f;
    f = fileOpen("/bridge.cfg", 1);
    if (f < 0) { return; }
    i = 0;
    while (i < ncfg) {                              // tuple: ip,kind,key,name,src,
        sprintf(line, "%s,%d,%s,%s,%s,", cfg_ip[i], cfg_kind[i], cfg_key[i], cfg_name[i], cfg_src[i]);
        fileWrite(f, line, strlen(line));
        i = i + 1;
    }
    fileClose(f);
}
void load_cfg() {
    char blob[760]; char tip[16]; char tk[16]; char tn[24]; char tf[8]; char ts[24];
    int f; int n; int t; int dots; int j;
    ncfg = 0;
    f = fileOpen("/bridge.cfg", 0);
    if (f < 0) { return; }
    n = fileRead(f, blob, 759); fileClose(f);
    if (n <= 0) { return; }
    blob[n] = 0;
    t = 1;                                          // strToken is 1-indexed
    while (ncfg < 8) {
        strToken(tip, blob, ',', t);
        if (strlen(tip) < 7) { break; }
        strToken(tf, blob, ',', t + 1);
        strToken(tk, blob, ',', t + 2);
        strToken(tn, blob, ',', t + 3);
        strToken(ts, blob, ',', t + 4);             // src — OR next-record ip (old 4-field cfg)
        strcpy(cfg_ip[ncfg], tip);   cfg_kind[ncfg] = (int)atof(tf);
        strcpy(cfg_key[ncfg], tk);   strcpy(cfg_name[ncfg], tn);
        cfg_val[ncfg] = 0.0; cfg_dim[ncfg] = 0;
        // an IP-looking 5th field (>=2 dots) means this is the OLD 4-field format
        // (ts is actually the next record's ip) → no src here, advance only 4.
        dots = 0; j = 0;
        while (ts[j] != 0) { if (ts[j] == '.') { dots = dots + 1; } j = j + 1; }
        if (dots >= 2) { strcpy(cfg_src[ncfg], cfg_ip[ncfg]); t = t + 4; }
        else           { strcpy(cfg_src[ncfg], ts);          t = t + 5; }
        ncfg = ncfg + 1;
    }
}

// ── build the Matter model from the config (main task only) ──
void build_model() {
    int i; int t;
    matterReset();
    i = 0;
    while (i < ncfg) {
        t = cfg_kind[i];
        if (t == 0) { cfg_ep[i] = matterAdd(MATTER_TEMP_SENSOR);  matterCluster(cfg_ep[i], CLUSTER_TEMP);  matterAttr(cfg_ep[i], CLUSTER_TEMP, 0, MTR_S16); }
        if (t == 1) { cfg_ep[i] = matterAdd(MATTER_HUM_SENSOR);   matterCluster(cfg_ep[i], CLUSTER_HUM);   matterAttr(cfg_ep[i], CLUSTER_HUM, 0, MTR_U16); }
        if (t == 2) { cfg_ep[i] = matterAdd(MATTER_PRESS_SENSOR); matterCluster(cfg_ep[i], CLUSTER_PRESS); matterAttr(cfg_ep[i], CLUSTER_PRESS, 0, MTR_S16); }
        if (t == 4) { cfg_ep[i] = matterAdd(MATTER_PLUG); }       // OnOff auto-attached
        if (t == 5) {
            cfg_ep[i] = matterAdd(MATTER_DIMM_LIGHT);
            matterCluster(cfg_ep[i], CLUSTER_ONOFF); matterAttr(cfg_ep[i], CLUSTER_ONOFF, 0, MTR_BOOL);
            matterCluster(cfg_ep[i], CLUSTER_LEVEL); matterAttr(cfg_ep[i], CLUSTER_LEVEL, 0, MTR_U8);
            matterSet(cfg_ep[i], CLUSTER_LEVEL, 0, 254);
        }
        matterName(cfg_ep[i], cfg_name[i]);
        cfg_val[i] = 0.0; cfg_dim[i] = 0;
        i = i + 1;
    }
    matterStart();
}

// ── discovery: probe known keys against the remote's Status (TaskLoop only) ──
void probe_sensor(char k[], int kind, char nm[]) {
    char fk[20];
    if (disc_n >= 8) { return; }
    sprintf(fk, "\"%s\":", k);
    if (strFind(buf, fk) >= 0) {
        strcpy(disc_key[disc_n], k); disc_kind[disc_n] = kind; strcpy(disc_name[disc_n], nm);
        disc_n = disc_n + 1;
    }
}
void probe_act(char k[], int kind, char nm[]) {     // relay (4) / lamp (5): key like "POWER1"
    char fk[16];
    if (disc_n >= 8) { return; }
    sprintf(fk, "\"%s\"", k);
    if (strFind(buf, fk) >= 0) {
        strcpy(disc_key[disc_n], k); disc_kind[disc_n] = kind; strcpy(disc_name[disc_n], nm);
        disc_n = disc_n + 1;
    }
}
void do_discover() {
    char url[64]; char k[16]; char nm[24]; char fk[12];
    disc_n = 0;
    get_devname(disc_ip, disc_src);                 // friendly "from where" name (or IP)
    // Probe ACTUATORS first so they're stored (and thus rendered) right under the
    // device's name+IP header, ahead of the sensor readings.
    sprintf(url, "http://%s/cm?cmnd=Status%%2011", disc_ip); buf[0] = 0;
    if (httpGet(url, buf) > 0) {
        strcpy(fk, "\"Dimmer\":");
        if (strFind(buf, fk) >= 0) {                // has a Dimmer -> dimmable light on POWER
            strcpy(k, "POWER"); strcpy(nm, "Lampe"); probe_act(k, 5, nm);
        } else {                                     // else plain relays
            strcpy(k, "POWER");  strcpy(nm, "Relais");   probe_act(k, 4, nm);
            strcpy(k, "POWER1"); strcpy(nm, "Relais 1"); probe_act(k, 4, nm);
            strcpy(k, "POWER2"); strcpy(nm, "Relais 2"); probe_act(k, 4, nm);
        }
    }
    sprintf(url, "http://%s/cm?cmnd=Status%%2010", disc_ip); buf[0] = 0;
    if (httpGet(url, buf) > 0) {
        strcpy(k, "Temperature"); strcpy(nm, "Temperatur"); probe_sensor(k, 0, nm);
        strcpy(k, "Humidity");    strcpy(nm, "Feuchte");    probe_sensor(k, 1, nm);
        strcpy(k, "Pressure");    strcpy(nm, "Luftdruck");  probe_sensor(k, 2, nm);
    }
}
void do_add() {
    int i; int j; int dup;
    i = 0;
    while (i < disc_n && ncfg < 8) {
        dup = 0; j = 0;                                  // skip if this ip+key is already bridged
        while (j < ncfg) {
            if (strcmp(cfg_ip[j], disc_ip) == 0 && strcmp(cfg_key[j], disc_key[i]) == 0) { dup = 1; }
            j = j + 1;
        }
        if (dup == 0) {
            strcpy(cfg_ip[ncfg], disc_ip);   cfg_kind[ncfg] = disc_kind[i];
            strcpy(cfg_key[ncfg], disc_key[i]); strcpy(cfg_name[ncfg], disc_name[i]);
            strcpy(cfg_src[ncfg], disc_src);                 // remember the source device
            cfg_val[ncfg] = 0.0; cfg_dim[ncfg] = 0;
            ncfg = ncfg + 1;
        }
        i = i + 1;
    }
    disc_n = 0;
    save_cfg();
    build_model();                                       // rebuild on THIS task — no slot restart
}

// ── poll every configured remote and mirror state into Matter (TaskLoop) ──
void poll_all() {
    char url[64]; char k[20]; float v; int on; int i;
    i = 0;
    while (i < ncfg) {
        if (cfg_kind[i] == 4 || cfg_kind[i] == 5) {
            sprintf(url, "http://%s/cm?cmnd=Status%%2011", cfg_ip[i]); buf[0] = 0;
            if (httpGet(url, buf) > 0) {
                sprintf(k, "\"%s\"", cfg_key[i]); on = jbool(buf, k);
                if (on >= 0) { matterSet(cfg_ep[i], CLUSTER_ONOFF, 0, on); cfg_val[i] = (float)on; }
                if (cfg_kind[i] == 5) {
                    strcpy(k, "\"Dimmer\":"); v = jget(buf, k);
                    if (v >= 0.0) { cfg_dim[i] = (int)v; matterSet(cfg_ep[i], CLUSTER_LEVEL, 0, (int)(v * 254.0 / 100.0)); }
                }
            }
        } else {
            sprintf(url, "http://%s/cm?cmnd=Status%%2010", cfg_ip[i]); buf[0] = 0;
            if (httpGet(url, buf) > 0) {
                sprintf(k, "\"%s\":", cfg_key[i]); v = jget(buf, k);
                if (cfg_kind[i] == 0 && v > -90.0) { cfg_val[i] = v; matterSetFloat(cfg_ep[i], CLUSTER_TEMP,  0, v, 100); }
                if (cfg_kind[i] == 1 && v >=  0.0) { cfg_val[i] = v; matterSetFloat(cfg_ep[i], CLUSTER_HUM,   0, v, 100); }
                if (cfg_kind[i] == 2 && v >=  0.0) { cfg_val[i] = v; matterSetFloat(cfg_ep[i], CLUSTER_PRESS, 0, v, 1); }
            }
        }
        delay(50);
        i = i + 1;
    }
}
void send_cmd() {
    char url[96]; char ip[16]; char c[40];
    strcpy(ip, pend_ip); strcpy(c, pending); cmd_ready = 0;       // snapshot before httpGet
    sprintf(url, "http://%s/cm?cmnd=%s", ip, c);
    httpGet(url, buf); addLog(url);
}

// ── a Matter controller changed a bridged relay/lamp → queue the cmnd ──
void MatterInvoke(int ep, int cluster, int cmd) {
    int i; int pct; char c[40];
    i = 0;
    while (i < ncfg) {
        if (cfg_ep[i] == ep && (cfg_kind[i] == 4 || cfg_kind[i] == 5)) {
            if (cluster == CLUSTER_LEVEL) {
                pct = (matterGet(ep, CLUSTER_LEVEL, 0) * 100) / 254;
                sprintf(c, "Dimmer%%20%d", pct); queue_cmd(cfg_ip[i], c);
            } else {
                power_cmd(c, cfg_key[i], matterGet(ep, CLUSTER_ONOFF, 0));
                queue_cmd(cfg_ip[i], c);
            }
            return;
        }
        i = i + 1;
    }
}

// ── the only HTTP task ──
void TaskLoop() {
    delay(15000);
    while (1) {
        if (cmd_ready)  { send_cmd(); }
        if (disc_req)   { do_discover(); disc_req = 0; }
        if (add_req)    { do_add();      add_req = 0; }
        if (clear_req)  { ncfg = 0; save_cfg(); build_model(); clear_req = 0; }
        if (poll_tick % 17 == 0 && ncfg > 0) { poll_all(); }
        poll_tick = poll_tick + 1;
        delay(300);
    }
}

// ── EverySecond: discovery/add/clear/restart flags + web actuator controls ──
void EverySecond() {
    char c[40]; int i; int cc; int tog; int on; int dv;
    if (btn_disc  != 0) { strcpy(disc_ip, ip_in); disc_req = 1; btn_disc = 0; }
    if (btn_add   != 0) { add_req   = 1; btn_add   = 0; }
    if (btn_clear != 0) { clear_req = 1; btn_clear = 0; }

    // first 4 actuators: toggle button + (lamp) dimmer slider -> queue cmnd
    cc = 0; i = 0;
    while (i < ncfg && cc < 4) {
        if (cfg_kind[i] == 4 || cfg_kind[i] == 5) {
            tog = 0;
            if (cc == 0 && ctl0 != 0) { ctl0 = 0; tog = 1; }
            if (cc == 1 && ctl1 != 0) { ctl1 = 0; tog = 1; }
            if (cc == 2 && ctl2 != 0) { ctl2 = 0; tog = 1; }
            if (cc == 3 && ctl3 != 0) { ctl3 = 0; tog = 1; }
            if (tog != 0) {
                on = 0; if (cfg_val[i] < 0.5) { on = 1; }     // toggle current
                power_cmd(c, cfg_key[i], on);
                queue_cmd(cfg_ip[i], c);
                cfg_val[i] = (float)on;                       // optimistic echo
                matterSet(cfg_ep[i], CLUSTER_ONOFF, 0, on);
            }
            if (cfg_kind[i] == 5) {                           // dimmer slider moved?
                dv = -1;
                if (cc == 0 && dmv0 != last_dmv0) { last_dmv0 = dmv0; dv = dmv0; }
                if (cc == 1 && dmv1 != last_dmv1) { last_dmv1 = dmv1; dv = dmv1; }
                if (cc == 2 && dmv2 != last_dmv2) { last_dmv2 = dmv2; dv = dmv2; }
                if (cc == 3 && dmv3 != last_dmv3) { last_dmv3 = dmv3; dv = dmv3; }
                if (dv >= 0) {
                    sprintf(c, "Dimmer%%20%d", dv);
                    queue_cmd(cfg_ip[i], c);
                    cfg_dim[i] = dv;
                    matterSet(cfg_ep[i], CLUSTER_LEVEL, 0, (int)((float)dv * 254.0 / 100.0));
                }
            }
            cc = cc + 1;
        }
        i = i + 1;
    }
}

// ── web UI ──
char kind_names[] = "Temp|Feuchte|Druck|?|Relais|Lampe";
void WebCall() {
    char b[200]; char kn[12]; char st[4]; char stcol[10]; char prevsrc[24]; int i; int cc; int t;
    webSend("{s}<b>Matter Remote Bridge</b>{m}{e}");
    webText(ip_in, 16, "Remote IP");
    webButton(btn_disc, "Discover|scanning…");
    if (ncfg > 0) { webButton(btn_clear, "Clear all bridged|cleared"); }   // top-level action, right after Discover

    if (disc_n > 0) {
        sprintf(b, "{s}<i>found on %s (%s)</i>{m}%d{e}", disc_src, disc_ip, disc_n); webSend(b);
        i = 0;
        while (i < disc_n) {
            strToken(kn, kind_names, '|', disc_kind[i] + 1);   // strToken is 1-indexed
            sprintf(b, "{s}&nbsp;&nbsp;%s{m}%s (%s){e}", disc_name[i], disc_key[i], kn); webSend(b);
            i = i + 1;
        }
        webButton(btn_add, "Add these to Matter|added");
    }

    // ── Bridged endpoints as a 3-column table: Name | Value/State | Control ──
    // Everything webSend()'d lands inside Tasmota's sensor <table>, so we emit our
    // own <tr> rows. The control widget (webButton/webSlider) is dropped into the
    // row's 3rd <td> — a <div><button> is valid inside a cell — so each actuator's
    // toggle/slider sits on the actuator's own line, directly under its device
    // (name+IP) header. Actuators are stored before sensors, so they list first.
    webSend("{s}<b>Bridged endpoints</b>{m}{e}");
    // styling for the bridge table (one <style>, replaced on every /?m=1 refresh)
    webSend("<style>.dh{background:#0e3a42;border-left:3px solid #4db6ac;padding:6px 8px}.rw{background:#e3e3e3;color:#222}.rw td{border-bottom:1px solid #c8c8c8;padding:3px 6px}.nm{padding-left:16px}.va{text-align:right;white-space:nowrap}.un{opacity:.6;font-size:.82em}.ip{opacity:.5;float:right;font-weight:normal}.c3{width:160px}</style>");
    cc = 0; i = 0; prevsrc[0] = 0;
    while (i < ncfg) {
        if (strcmp(cfg_src[i], prevsrc) != 0) {            // device group header (spans all 3 cols)
            strcpy(prevsrc, cfg_src[i]);
            sprintf(b, "<tr><td colspan='3' class='dh'><b style='color:#4db6ac'>&#9679;</b> <b>%s</b><span class='ip'>%s</span></td></tr>", cfg_src[i], cfg_ip[i]); webSend(b);
        }
        t = cfg_kind[i];
        if (t == 0) { sprintf(b, "<tr class='rw'><td class='nm'>%s</td><td class='va'>%.1f<span class='un'> C</span></td><td></td></tr>",   cfg_name[i], cfg_val[i]); webSend(b); }
        if (t == 1) { sprintf(b, "<tr class='rw'><td class='nm'>%s</td><td class='va'>%.0f<span class='un'> %%</span></td><td></td></tr>",  cfg_name[i], cfg_val[i]); webSend(b); }
        if (t == 2) { sprintf(b, "<tr class='rw'><td class='nm'>%s</td><td class='va'>%.0f<span class='un'> hPa</span></td><td></td></tr>", cfg_name[i], cfg_val[i]); webSend(b); }
        if (t == 4 || t == 5) {
            if (cfg_val[i] > 0.5) { strcpy(st, "ON"); strcpy(stcol, "#66bb6a"); } else { strcpy(st, "OFF"); strcpy(stcol, "#999"); }
            if (t == 5) { sprintf(b, "<tr class='rw'><td class='nm'>%s</td><td class='va' style='color:%s'>%s<span class='un'> %d%%</span></td><td class='c3'>", cfg_name[i], stcol, st, cfg_dim[i]); }
            else        { sprintf(b, "<tr class='rw'><td class='nm'>%s</td><td class='va' style='color:%s'>%s</td><td class='c3'>", cfg_name[i], stcol, st); }
            webSend(b);                                    // open the row + its control cell
            if (cc < 4) {                                  // control widget(s) go INSIDE this <td>
                if (cc == 0) { webButton(ctl0, "schalten"); }
                if (cc == 1) { webButton(ctl1, "schalten"); }
                if (cc == 2) { webButton(ctl2, "schalten"); }
                if (cc == 3) { webButton(ctl3, "schalten"); }
                if (t == 5) {
                    if (cc == 0) { webSlider(dmv0, 0, 100, "Helligkeit"); }
                    if (cc == 1) { webSlider(dmv1, 0, 100, "Helligkeit"); }
                    if (cc == 2) { webSlider(dmv2, 0, 100, "Helligkeit"); }
                    if (cc == 3) { webSlider(dmv3, 0, 100, "Helligkeit"); }
                }
                cc = cc + 1;
            }
            webSend("</td></tr>");                          // close control cell + row
        }
        i = i + 1;
    }
    sprintf(b, "{s}heap{m}%d kb{e}", tasm_heap / 1000); webSend(b);
}

int main() {
    ip_in[0] = 0; disc_n = 0; cmd_ready = 0;
    ctl0 = 0; ctl1 = 0; ctl2 = 0; ctl3 = 0;
    dmv0 = 0; dmv1 = 0; dmv2 = 0; dmv3 = 0;
    last_dmv0 = 0; last_dmv1 = 0; last_dmv2 = 0; last_dmv3 = 0;
    load_cfg();
    build_model();
    return 0;
}