/*
 * data model for PPKr javascript configurator
 * A.Omelianchuk, 01-dec-2020
 *
 *  there are boxes
 *  in boxes there are modules
 *  in modules there are units
 *  some units are fixed built-in, defined in schema for unit or for board
 *  some units are  variable - abstract in schema (for AU on AL or may be for SK on 485)
 *  some units are soft - can be "created" == only on selected moduls (with SPI flash)
 *

 global variable



 "sys" - main configuration tree
        example of "sys" see in config file saved

 is root of unit tree
  _typeName = _System
  in it
    _subUnits:{"_Box":{}}
        in it _subUnits:{"Module":{})
            Modules have real module _typeName !
            in Module _subUnits has "Boot":{} and other subUnits defined in module's type
            
            note! 
            _desc.__.subUnits describes all those subunits

 any unit object has:
    _typeName  - name of type - obligatory
    _desc - ref to unit descriptor in schema
    _up - ref to upper unit in tree (except _System) ======================  ATTENTION !! circular links - so no garbage collection will be made if simply "del" unit
    _sut - subtype in upper unit - name of subtype
    _sun - number of this subtype in upper unit - started from 1
    _subUnits - in it
        <subtype name>:{} , inside:
            keyed by N (1 ... qty) refs to units
        Note! sun above qty possible, only for units, found in hardware and not fitted in qty if there are lot of units, configured by operator but not found in hardware
            
            
    status = "status" got from hardware
    config = "config" set by operator or got from hardware
    
    any other subelement not starting with "_" - also some message from unit, described in "_desc"

    _realType --- in HW/SW units - name of type if got at least one packet from this unit
    
    _configured --- only for subunits with    hardware == true    or    software == true.
                        note !!! should check 
                        _up._desc.__.subUnits[_sut].hardware   
                        or similar software
            _configured == true means this unit is in config (manually or "approved"), else - only got from PPK
            
    _sunPPK --- set if 
            - unit is "hardware" and has "hardwareID"
            - config from unit with this "sun" got from PPK
            - SN (hardwareID) in config from PPK is equal to that of some other "sun"
        then _sunPPK is set to that "sun", so to go directly there.
        all msgs from PPK from this "sun" will be put into that "sun" unit.

        exception
        if 
        
                
        Note!
        data got from PPK are put into box/module and other HW units according to SN - (into sunPPK if any)
        NOT according to SUID
        and only if there is already
                

 *
 *  units have types, which can be derived from other types.
 *  T1 is compatible to T2 == T1 is derived from T2
 *




  global vars:

  var sendingUnit; //  ref to "unit" from "sys" tree
                    non-null ==> polling will load all parents and all chidren recursively of this unit. will be cleared after all done or after 10 retries with errors

  var sendingUnitSut; // if non-empty limits upload to "sut" subunits of this unit



  ///////////////////////////////////////////////////////////
  helper functions


  function setValueInMsg(unit, msgName, paramName, value) { // returns value corrected to possible 


  function setSubscribe(u, force){  ////   ref to unit from "sys"  tree
  will subscribe to apropriate module and additionally will once a second request info/status/config   from this very unit  ===  intended for currently selected unit in UI
  force == true ===> will resubscribe ("justcontinue":1) else will restart subscription only if new module


  function clearAll(u, sut){
        delete all subunits of stated subUnitTypeName from this unit


  function deleteUnit(u){
        correctly removes all circular links before deleting object itself



  function addUnitToSut(u,curSut){
        creates subunit of stated subUnitTypeName on any free subUnitNumber (number used is set as _sut in the new unit)
        also fills all fixed subunits in new unit
        returns newly created subunit



function u2string(u)
   returns ID of unit as string (it is used as parameter in software units - inputs/outputs - to connect them to hardware unts

function string2u(str)
    reverse action - find unit for given string ID


  function changeUnitSun(cu,newSun){ // returns true if OK

  function changeUnitType(cu,newType){ // name of type  ===== returns true if OK.
        used to change actual type of any abstract non-fixed  subunit. e.g.     A3DPI ==> IPR


  send command to hardware
 * u = unit object, cmd = {"status":{...}} or smth similar - any message which is rw=true"
function sendCommand(u,cmd){
    Note: send "config" to unit creates it in hardware if not yet existent

    special case - delete unit in hardware
function sendCommandDelete(u){


 function readConfigFromFile(){
        asks user to select file and loads into "sys" instead of current


 function saveConfigToFile(){
        asks user where to save config and saves it


  function saveCfg() {
            saves to local storage


  function restoreCfg() {
            restores config from localStorage

  function initializeModel(callBack) {
            restoreCfg
            if failed - builds default cfg
            then calls "callback" (see description in schema.js)




       function startLog(time,qty){
            starts reading from PPK log from TIME. - it is time as in PPK - seconds from 1 jan 2000
            will try to get "qty" records
            if time==0 will continue previous set

                if full will call 
                        clearLog();
                
        on every new record got from PPK will call
            displayLog(records)   with object of "records" message (see schema.json)
        "record" in it now is BINARY !! as rcvd from PPKr
            === todo === convert record to smth more eatable


checkUpdateFirmware(u,fw)            call to check if this u8 array is compatible with this unit to be uploaded as firmware
returns
0 = ok
1 = unit type update not supported
2 = firmware not for this type of board
3 = unknown error - bad parameters
            
            
================
calls to "view":
================



  NOTE!
  widely used function from view.js
was =    putLog(string)
        e.g.
        putLog("two units exchanged: #"+oldSun+" and #"+newSun);
        supposed to be displayed or stored as some technical Log or maybe displayed as "bottomline string" (status) or smth similar

    now = systemLog(critical,text)
    critical : 
        true if I previously used "alert" 
        false if I used putLog or displayString





    was = markOnline(online,box,mod) ; == when changed state "online" -- also stores state in var "online"

    now = modelChanged(reason, unit, details) 
    reason:
    "online" - details = {box:77, mod:99}
    "offline"
    "beforeDelete" - parameter "unit" valid
    "afterDelete" - unit already deleted from "sys" tree, please clean up garbage
    "changed" - parameter "unit" valid, details = as uChanged described below previously
    "force" - was forceUpdateUnitView

    was = viewDelete(u); called before attempt to delete unit - mark which unit to be deleted and redraw view if necessary. unit still present
    was = postDelete(); after delete done. unit deleted (though may be retained in view , protected from garbage collection
    
    was = updateUnitView(u, uChanged); called if some data got from PPK were changed.
        u ==> unit in question
        uChanged = smth like {"status":{"on":1}}  ===  object with messages changed. messages contain OLD values. unit itself always contains the most uptodate values
        if uChanged = {"config":{}} then there is in unit (may be empty) subelement _configPPK -- there always is config got from PPK, "config" itself never changed other then by operator
    
    
   was = function forceUpdateUnitView(u)
    called if unit's "offline" state changed

   

  
   
 *
 *
 */

var thisBox = 255; // address
var thisModule = 255; // address

var thisModuleBootParams = {}; // cur module boot status

var online; // hardware answering at least with some data, also calls "modelChanged"

// upload to PPK hardware configuration
var sending1unit;
var sendingUnit; // non-null ==> polling will load all parents and all chidren recursively of this unit. will clear after finished
var sendingUnitSut; // limit to "sut" subunits of this unit - use from "list of subunits"
var curSendingUnit; // set null to start from beginning
var sendingUnitStage;
var sendingUnitRetries = 0;
var curPollingModule;
var curViewUnit;
var curViewSut;

// here box == 255 == todo, mod = ??? == todo
var sys; // superUnit - all other in _subUnits - current config
var ppk; // superUnit - all other in _subUnits - actual config read from hardware

var incorrectBackConfig;

//////////_61
function processMessage(message) {
    // message MUST be (from:uint8arr, to:uint8arr, id:arrayofint, cmd:{"config":{}}
    function clearConf(uu) {
        let su = uu._subUnits;
        for (let sut in su) {
            let sutu = su[sut];
            for (let sun in sutu) {
                let sutusu = sutu[sun];
                sutusu.config = {};
                clearConf(sutusu);
            }
        }
    }
    lastGotReply = Date.now();

    let changedUnits = [];

    let unitSN; // unit with appropriate SN
    let unitSUN; // unit as stated in SUN from PPK

    let m; // cur module unit
    let b; // cur box unit
    let chgdb = false;
    let chgdm = false;
    // in case of discrepancy they are selected by SN

        if ((!online) && message.to.box && (message.to.box != 255) && message.to.mod && (message.to.mod != 255)) {
            thisBox = message.to.box;
            thisModule = message.to.mod;
            systemLog(
                false,
                "connected to box #" + thisBox + ", mod #" + thisModule
            );
            modelChanged("online", undefined, {
                box: thisBox,
                mod: thisModule,
            });
            online = true;
        }

    if (message.from.unit == 0 && message.cmd.status) {
        // check for box/module - this is done from BOOT answers - special case
        let boxSun = message.cmd.status.boxAddress;
        let modSun = message.cmd.status.moduleAddress;
        let snt = message.cmd.status.snType;
        let moduleTypeNum = snt >> 24;
        let sn = snt & 0xffffff;
        let moduleType = schema.typeSN.Module[moduleTypeNum];
        if (moduleType && Array.isArray(moduleType)) moduleType = moduleType[0];
        // todo - multimodule => use typeSN, not just typeName, select appropriate typeSN / typeName
        else moduleType = "Module";


        if (!sys) {
            sys = {};
            fillUnit(sys, "_System");
            changedUnits.push(sys);
        }

        // ensure box [boxsun] exists
        if (!sys._subUnits._Box[boxSun]) {
            // in PPK boxsun present, be sure it is present in config.
            if (moduleType == "RingUPSmodule") {
                let nb = checkSubUnit(sys, boxSun, "_Box", "_Box", null); // cfg only if creating on message from PPKr
                clearConf(nb);
                changedUnits.push(nb);
                nb._configured = false; // just in PPK
                nb._realType = "_Box";
                if (!nb.status) nb.status = {};
                if (stableSys) {
                    modelChanged("changed", sys);
                    modelChanged("changed", nb);
                }
            }
        }

        b = sys._subUnits._Box[boxSun];

        if (!b) return;

        chgdb |= b._realType != "_Box";
        chgdb |= b._configOK != true;
        b._realType = "_Box";
        b._configOK = true;
        if (!b.status) b.status = {};

        if (!b._subUnits.Module[modSun]) {
            // in PPK boxsun present, be sure it is present in config
            let nm = checkSubUnit(b, modSun, "Module", moduleType, {}); // cfg only if creating on message from PPKr
            clearConf(nm);
            changedUnits.push(nm);
            changedUnits.push(nm._subUnits.Boot["1"]);
            nm._configured = false; // just in PPK
        }
        m = b._subUnits.Module[modSun];

        //////////////// here thisbox
        if (message.from.mod == 255) {
            // this
            // if (message.from.box == 255) {
            // sure really this very module, direct serial connect
            if (
                (thisBox != boxSun || thisModule != modSun) &&
                boxSun != 255 &&
                modSun != 255
            ) {
                systemLog(
                    false,
                    "connected to box #" + boxSun + ", mod #" + modSun
                );
                if (stableSys && boxSun != 255 && modSun != 255) {
                    modelChanged("online", undefined, {
                        box: boxSun,
                        mod: modSun,
                    });
                    online = true;
                    thisBox = boxSun;
                    thisModule = modSun;
                }
            }
            thisModuleBootParams = message.cmd.status;
            b._thisModule = modSun; // just to set mod in packets from this module in another box == may be 255 if bad address
            // } else if (message.from.box > 0 && message.from.box < 255) {
            //     // from ring from ringmodule in another box
            //     b._thisModule = modSun; // just to set mod in packets from this module in another box == may be 255 if bad address
            // }
        }
    } // boot status

    if (b) {
        chgdb |= b._offline != false;
        if (b._offline) chgdb = true;
        chgdb |= b._realType != b._typeName;
        b._offline = false;
        b._realType = b._typeName;
        if (!b.status) b.status = {};
        if (chgdb && stableSys) {
            modelChanged("force", b);
        }
    }
    if (m) {
        chgdm |= m._realType != m._typeName;
        chgdm |= m._offline != false;
        m._realType = m._typeName;
        m._offline = false;
        if (!m.status) m.status = {};
        if (chgdm && stableSys) {
            modelChanged("changed", m);
        }
    }

    if (message.from.box == 255 && thisBox && thisBox != 255)
        message.from.box = thisBox;
    if (!b) b = sys._subUnits._Box[message.from.box];
    if (!b) return;
    if (message.from.mod == 255 && b._thisModule)
        message.from.mod = b._thisModule;
    if (message.from.box == 255 || message.from.mod == 255) return; // wait for first reply from This Boot

    if (b._sunPPK) b = sys._subUnits._Box[b._sunPPK];
    m = b._subUnits.Module[message.from.mod];
    if (m._sunPPK) m = b._subUnits.Module[m._sunPPK];

    if (!m) return;

    let u;
    let uid = message.id;
    let l = uid.length; // skip box/mod
    let log = false;
    if (l < 2) return; // system or box
    l -= 2;

    if (!m._realType) m._realType = "Module";

    let uChanged;

    if (message.from.unit == 0) u = m._subUnits.Boot[1];
    else if (message.from.unit == 1) {
        // cpu
        try {
            // seek for unit
            let cu = m;
            let protect = 100;
            while (l && cu && protect--) {
                let sid = uid[--l];
                if (sid) {
                    // still not 0 = endofsuids
                    let sutn =
                        ((sid & 0xf0) >> 4) |
                        ((sid & 0x0f0000) >> 12) |
                        ((sid & 0xffff00000000) >> 24);
                    let sun =
                        (sid & 0x0f) |
                        ((sid & 0xff00) >> 4) |
                        ((sid & 0xfff00000) >> 8);
                    let ud_ = cu._desc.__;
                    let sut = ud_.subUnitNumbers[sutn];
                    if (sut) {
                        if (!cu._subUnits[sut]) cu._subUnits[sut] = {};
                        let nu = cu._subUnits[sut][sun];
                        if (!nu) {
                            // probably it's last in chain, we can create
                            if (l === 2) {
                                // only 0 and type left, we can add new unit in config
                                let realTN = schema.unitTypesNumbers[uid[0]];
                                let thisud = schema.unitTypes[realTN];
                                if (
                                    unitTypeCompatible(realTN, sut) &&
                                    (ud_.subUnits[sut].hardware ||
                                        ud_.subUnits[sut].software)
                                ) {
                                    u = checkSubUnit(
                                        cu,
                                        sun,
                                        sut,
                                        realTN,
                                        message.cmd.config
                                    );
                                    clearConf(u);
                                    u._realType = realTN;
                                    if (stableSys)
                                        modelChanged("changed", cu, sut);
                                    uChanged = message.cmd;
                                    if (
                                        ud_.subUnits[sut].hardware &&
                                        !message.cmd.config &&
                                        (!u.config ||
                                            !Object.getOwnPropertyNames(
                                                u.config
                                            ).length)
                                    )
                                        sendCommand(u, {
                                            config: {},
                                        });
                                }
                            }
                            break; // while.     u may be undefined
                        }
                        cu = nu;
                    } else console.log("undef1");
                } else {
                    // done chain of sids, got source unit
                    if (l == 1) {
                        // ok, only type left
                        let t = uid[0]; // type ==
                        if (!t) {
                            if (cu == curSendingUnit && cu._deleted)
                                getNextCurSending();
                            if (cu._deleted) {
                                let uu = cu._up;
                                changedUnits.push(uu);
                                if (cu == curSendingUnit) getNextCurSending();

                                deleteUnit(cu);
                                if (stableSys) {
                                    modelChanged("force", uu);
                                }
                            } else cu._realType = undefined;
                        }
                        let utn = schema.unitTypesNumbers[t];
                        if (!unitTypeCompatible(utn, cu._typeName)) {
                            // absent in PPK or differs
                            if (cu == curSendingUnit) {
                                if (stableSys) {
                                    modelChanged("force", cu);
                                }
                                if (cu._deleted && !t) {
                                    // ok answer on config-delete
                                } else if (20 < ++sendingUnitRetries) {
                                    // stopSendingUnit("30 bad replies");
                                    getNextCurSending();
                                    systemLog(false, "30 bad replies");
                                }
                            }
                            return; // cannot do anything with such a unit
                        }
                        u = cu;
                        if (!cu._realType) changedUnits.push(u);
                        cu._realType = utn;
                    }
                    break; // anyway, found or not "u"
                }
                // no sid == end of path, cu found (or not)
            } // while <path searching>
        } catch {} // if no
    } // else - not from unit nor from boot, u = undefined

    if (!u) return;

    // have got "u"

    if (u._typeName == "RingUPSmodule" && message.cmd.info) {
        // check reboot
        let rebootTime = Math.floor(
            Date.now() / 1000 - message.cmd.info.timeFromStart
        );
        if (!u._lastRebootTime || u._lastRebootTime < rebootTime) {
            u._lastRebootTime = rebootTime + 2; // to be sure, module starts at least 3 sec
            u._log = undefined;
            delete u._log;
        }
    }

    if (u._idString != message.idString) {
        if (!u._idString) u._idString = message.idString;
        // else if(message.idString.length>4)    todo - smth - unwanted msgs  now when changing au address
        //     putLog("idString (" + message.idString + ") != unit (" + u._idString + ")");
    }

    if (message.to.mark == 0x5a && !message.cmd.subscribe) {
        // sent by subscription - but not immediate reply to subscription command
        if (m == curPollingModule) {
            // if (message.cmd.config) // config may create units, even empty (
            // fillRequest({
            //     "id": message.id,
            //     "unitType": message.unitType,
            //     "cmd": {
            //         "confirm": {}
            //     }
            // }, 1, 90);
            fillRequest(
                {
                    id: message.id,
                    unitType: message.unitType,
                    cmd: {
                        status: {},
                    },
                },
                0x13,
                message.from.mark
            ); // category = 0x13 , tag retain done-ao -- necessary for new firmware - should retain "tag" ("mark") in answer
            // else
            //     fillRequest({
            //         "id": message.id,
            //         "unitType": message.unitType,
            //         "cmd": {
            //             "confirm": {}
            //         }
            //     });
            //sendCommand(u, { "confirm": {} });
        } else {
            let mu = u;
            while (mu && mu._sut != "Module") mu = mu._up;
            if (mu)
                sendCommand(mu, {
                    subscribe: {
                        justContinue: 4,
                    },
                }); // unsubscribe
            return;
        }
    }

    for (let msgn in message.cmd) {
        // in fact only one msg, but just for future extentions
        let msg = message.cmd[msgn];
        if (msgn == "subscribe") continue; // just ignore answers - not to send them next time todo - repeat subscribe until OK
        if (msgn == "records") {
            processLogData(u, message.cmd);
            continue;
        }
        // any other commands.
        if (u._desc && u._desc[msgn] && u._desc[msgn].wo) continue; // do not save WO commands

        if (msgn == "config") {
            // very special, probably we shall check for SN
            if (!u.config) u.config = {};
            if (u == curSendingUnit) {
                if (stableSys) modelChanged("force", u);
                let ok = true;
                // if(u._deleted)
                //     ok=!message.unitType;
                //             else
                for (p in u._desc.config)
                    if (
                        p[0] != "_" &&
                        typeof msg[p] == "number" &&
                        typeof u.config[p] == "number" &&
                        !(u._desc.config[p].fuzzy || u._desc.config[p].ro)
                    )
                        ok &= u.config[p] == msg[p];
                if (!ok) {
                    systemLog(
                        false,
                        "incorrect config back from " + getUnitName(u)
                    );
                    incorrectBackConfig++;
                    if (incorrectBackConfig > 3) ok = true;
                }
                if (ok) getNextCurSending();
            }

            u._configOK = true;

            if (!u.config && !u._deleted) {
                // first time. let config be as in PPK
                changedUnits.push(u);
                u.config = msg;
                if (!uChanged) {
                    uChanged = {};
                }
                uChanged.config = {};
                u._configOK = true;
            } else
                for (let p in msg) {
                    if (!u._configPPK) u._configPPK = {};
                    if (!u._config) u._config = {};
                    if (p === "unitID") msg[p] = Uint8toString(msg[p]);
                    if (
                        u.config[p] === undefined ||
                        (u._desc.config[p].hardwareID && !u.config[p]) ||
                        !u._configured
                    ) {
                        // first read cfg parameter from PPK - just use it
                        if (u.config[p] != msg[p]) {
                            if (!uChanged) uChanged = {};
                            uChanged.config = {};
                            u._configPPK[p] = msg[p]; // to update view if displayed now
                        }
                        u.config[p] = msg[p];
                        // if (u._configPPK && u._configPPK[p] !== undefined) {
                        //     delete u._configPPK[p];
                        //     if (!uChanged) uChanged = {};
                        //     uChanged.config = {};
                        // }
                    } else if (msg[p] != u.config[p]) {
                        if (!uChanged) uChanged = {};
                        uChanged.config = {};
                        u._configPPK[p] = msg[p];
                        if (u._desc[msgn][p].ro) u.config[p] = msg[p];
                        if (u.config[p] == u._configPPK[p]) {
                            u._configPPK[p] = undefined;
                            delete u._configPPK[p];
                        } else u._configOK = false;
                    } else if (u._configPPK) {
                        if (u._configPPK[p] !== undefined) {
                            if (!uChanged) uChanged = {};
                            uChanged.config = {};
                        }
                        delete u._configPPK[p];
                    }
                    if (
                        typeof u._configPPK[p] != "object" &&
                        typeof u._configPPK[p] != "undefined" &&
                        u._configPPK[p] != u.config[p]
                    )
                        u._configOK = false;
                }
            continue;
        } else {
            let recalcF = false;
            if (msgn == "status")
                if (!u.status || u.status.fault != msg.fault) recalcF = true;
            if (!u[msgn]) {
                u[msgn] = msg;
                uChanged = message.cmd;
            } else
                for (let p in msg) {
                    if (
                        typeof msg[p] == "number" ||
                        typeof msg[p] === "string"
                    ) {
                        if (u[msgn][p] != msg[p]) {
                            if (!uChanged) uChanged = {};
                            if (!uChanged[msgn]) uChanged[msgn] = {};
                            uChanged[msgn][p] = u[msgn][p]; // save old in case it is usefull
                            u[msgn][p] = msg[p];
                        }
                    } else if (Array.isArray(msg[p])) {
                        if (!Array.isArray(u[msgn][p])) u[msgn][p] = [];
                        let c = false;
                        for (let mm of msg[p])
                            if (!u[msgn][p].includes(mm)) c = true;
                        for (let mm of u[msgn][p])
                            if (!msg[p].includes(mm)) c = true;
                        if (c) {
                            if (!uChanged) uChanged = {};
                            if (!uChanged[msgn]) uChanged[msgn] = {};
                            uChanged[msgn][p] = u[msgn][p]; // save old in case it is usefull
                            u[msgn][p] = msg[p]; // fixme - ??
                        }
                    }
                }
            if (recalcF) recalcFaultsDown(u._up);
        }
    }

    if (u._offline) {
        u._offline = false;
        if (stableSys) modelChanged("force", u);
    }

    if (stableSys) {
        modelChanged("changed", u, uChanged);

        while (changedUnits.length) {
            let uu = changedUnits.shift();
            //if (uu == u) uChanged = 0;
            modelChanged("force", uu);
        }
    }
}

//////_60 == currently unused - apr 2023
function logMessage(u, msgn, msg) {
    //todo - human readable
    let m = {};
    m[msgn] = msg;
    systemLog(false, JSON.stringify(m));
}

///////_59
function checkSubUnit(up, sun, sut, tName, cfg) {
    // cfg only if creating on message from PPKr
    let res;
    try {
        res = up._subUnits[sut][sun];
        if (!res) {
            // check SN if any
            let td = schema.unitTypes[tName];
            if (
                td &&
                td.__ &&
                td.__.hardwareIDs &&
                cfg &&
                td.__.hardwareIDs.length
            ) {
                for (let s in up._subUnits[sut]) {
                    let ss = up._subUnits[sut][s];
                    if (ss._typeName == tName) {
                        let identical = true;
                        for (let his in td.__.hardwareIDs) {
                            let hisn = td.__.hardwareIDs[his];
                            identical &= cfg[hisn] == ss.config[hisn];
                            if (!cfg[hisn]) return; // never get from PPK zero-SN units
                        }
                        if (identical) return ss;
                    }
                }
            }
            res = {
                _up: up,
                _sut: sut,
                _sun: sun,
                config: cfg,
            };
            up._subUnits[sut][sun] = res;
            fillUnit(res, tName);
            systemLog(false, "created " + getUnitName(res));
            if (stableSys) modelChanged("changed", up);
        }
    } catch {}
    return res;
}

///////_58
function checkSunSut(u) {
    if (!u._desc) u._desc = schema.unitTypes[u._typeName];
    let s = u._subUnits;
    for (let stn in s) {
        if (!u._desc.__.subUnits[stn].virtual)
            for (let sn in s[stn]) {
                s[stn][sn]._sut = stn;
                s[stn][sn]._sun = sn;
                checkSunSut(s[stn][sn]);
            }
    }
}

////////_57
function queryUnit(u) {
    sendCommand(u, {
        status: {},
    });
    if (!u._offline)
        sendCommand(u, {
            config: {},
        });
}

/////////_56
function printUnit(u) {
    let ans = "{";
    for (let n in u) {
        if (typeof u[n] !== "object") {
            if (ans.length) ans += " , ";
            ans += n + ": " + JSON.stringify(u[n]);
        }
    }
    return ans + "}";
}

//////////_55
function getNextCurSending() {
    incorrectBackConfig = 0;
    //   if (!sendingUnitRetries) return; // at least once MUST send data
    sendingUnitRetries = 0;
    let done = false;
    if (!sendingUnitStage) {
        if (curSendingUnit !== sendingUnit) {
            // still higher up
            if (curSendingUnit._deleted) {
                // do not go down - they are all deleted
                stopSendingUnit("done uploading");
                return;
            }
            let uuu = sendingUnit;
            while (uuu._up !== curSendingUnit) uuu = uuu._up;
            curSendingUnit = uuu;
            //        putLog("sending to PPK: "+getUnitName(curSendingUnit));
            //      return;
        }
        if (curSendingUnit === sendingUnit) {
            // still higher up
            sendingUnitStage = 1;
        }
    } // curSendingUnit already sent
    else
        do {
            if (curSendingUnit == sendingUnit) {
                // still higher up
                if (sending1unit) {
                    stopSendingUnit("done 1");
                    return;
                }
            }
            curSendingUnit = nextUnit(
                curSendingUnit,
                sendingUnit,
                sendingUnitSut
            );
            if (!curSendingUnit) break;
            done = false;
            for (let n in curSendingUnit._desc.config)
                if (n != "__" && !curSendingUnit._desc.config[n].unused) {
                    done = true;
                    break;
                }
        } while (
            (!curSendingUnit._sut || curSendingUnit._sut == "_Box") &&
            !(
                done ||
                curSendingUnit._desc.__.hardware ||
                curSendingUnit._desc.__.software
            )
        );
    if (!curSendingUnit) {
        stopSendingUnit("done uploading");
        return;
    } else {
        if (sendingUnit && curSendingUnit) {
            let cc = "cfg";
            let ud_ = curSendingUnit._up._desc.__;
            let ud_s = ud_.subUnits[curSendingUnit._sut];
            let doDelete =
                (ud_s.software || ud_s.hardware) && curSendingUnit._deleted;
            if (doDelete && curSendingUnit._configured) doDelete = false;
            if (doDelete) {
                cc = "del";
                sendCommandDelete(curSendingUnit);
            } else {
                if (!curSendingUnit.config) curSendingUnit.config = {};
                sendCommand(curSendingUnit, {
                    config: curSendingUnit.config,
                });
            }
            systemLog(
                false,
                "sending to PPK (" + cc + "): " + getUnitName(curSendingUnit)
            );
        }
    }
}

var stageIntervalId;
var pollingStage = 0;
var cfgPar;
var subscribeTime;
var lastPollTime = 0;
var curCur = 0;
var curCurCur = 0;

var lastPollAll = 0;
var lastSendTime = 0;
var lastPollBox = 0;

var pollSun = 0;
var lastPollBoot;

var lastLogPollTime = 0;

///////_54
function buildPollingRequest() {
    if (Date.now() < lastGotReply + 15000) {
        // online milliseconds
        if (!online && thisBox != 255 && thisModule != 255) {
            online = true;
            modelChanged("online", undefined, {
                box: thisBox,
                mod: thisModule,
            });
        }
    } else {
        if (online) {
            online = false;
            thisBox = thisModule = 255;
            modelChanged("offline", undefined, {
                box: thisBox,
                mod: thisModule,
            });
        }
    }

    if (postRequests.length < 6) {
        // not too long queue
        let pollBox = 0;
        if (
            (curViewUnit &&
                curViewUnit._typeName == "_System" &&
                Date.now() > lastPollAll + 4000) ||
            Date.now() > lastPollAll + 10000
        ) {
            if (lastPollBoot) {
                lastPollBoot = 0;
                fillRequest({
                    id: [1, 0], // broadcast to all boxes, 1st module
                    unitType: "Boot",
                    cmd: {
                        status: {},
                    },
                }); // to be sure
            } else {
                lastPollBoot = 1;
                fillRequest({
                    id: [2, 0, 1, 0], // broadcast to all boxes, 1st module, check for reboot
                    unitType: "RingUPSmodule",
                    cmd: {
                        info: {},
                    },
                }); // to be sure
            }
            lastPollAll = Date.now();
        }

        if (curPollingModule) {
            if (curPollingModule._up) pollBox = curPollingModule._up._sun;
            else curPollingModule = undefined;
        } else if (curViewUnit && curViewUnit._typeName == "_Box")
            pollBox = curViewUnit._sun;
        if (pollBox && Date.now() > lastPollBox + 5000) {
            fillRequest({
                id: [0, pollBox], // broadcast to all modules of current box
                unitType: "Boot",
                cmd: {
                    status: {},
                },
            }); // to be sure
            lastPollBox = Date.now();
            return;
        }

        if (subscribeTime < Date.now() && curPollingModule) {
            sendCommand(curPollingModule, {
                subscribe: {
                    justContinue: 1, //
                },
            });
            subscribeTime = Date.now() + 10000; // once 10 sec
            return;
        }

        if (!(thisModuleBootParams && thisModuleBootParams.snType)) {
            fillRequest({
                id: [255, 255], // broadcast to all modules of current box
                unitType: "Boot",
                cmd: {
                    status: {},
                },
            }); // to be sure
            return;
        }

        // here normal poll
        // normal poll or send
        if (sendingUnit && lastSendTime < Date.now()) {
            // uploading config
            lastSendTime = Date.now() + 250 + sendingUnitRetries * 50;
            if (!curSendingUnit) {
                // first go  up to thr Module to be sure all up units are created
                // starting, go up
                curSendingUnit = sendingUnit;
                if (sendingUnit._sut == "_Box" || !sendingUnit._sut) {
                    // in the case - no need to go UP
                    sendingUnitStage = 1;
                    getNextCurSending(); // no need to send box or sys = go down deep enough
                } else {
                    while (curSendingUnit && curSendingUnit._sut != "Module")
                        curSendingUnit = curSendingUnit._up;
                    if (curSendingUnit) {
                        sendingUnitRetries = sendingUnitStage = 0; // create subunits until @sending@
                        if (curSendingUnit._typeName[0] === "_")
                            sendingUnitStage = 1;
                    } else stopSendingUnit("not found module to upload");
                }
            }
            try {
                if (30 < ++sendingUnitRetries) {
                    getNextCurSending();
                    sendingUnitRetries = 1;
                    systemLog(false, "30 retries");
                } else if (
                    curSendingUnit._typeName[0] === "_" ||
                    curSendingUnit._oldUp // unit already deleted
                )
                    getNextCurSending();
                if (sendingUnit) {
                    //if(!(sendingUnitRetries & 7))
                    if (curSendingUnit._deleted) {
                        sendCommandDelete(curSendingUnit);
                        systemLog(
                            false,
                            "sending to PPK again (delete): " +
                                getUnitName(curSendingUnit)
                        );
                    } else {
                        sendCommand(curSendingUnit, {
                            config: curSendingUnit.config,
                        });
                        systemLog(
                            false,
                            "sending to PPK again: " +
                                getUnitName(curSendingUnit)
                        );
                    }
                    return;
                }
            } catch {}
        }

        let now = Date.now();
        if (now > lastLogPollTime) {
            let m = getLogToPoll();
            if (m) {
                sendCommand(m._subUnits.Logger[1], {
                    records: {
                        index: m._log.indx,
                    },
                });

                lastLogPollTime = now + (m._log.full ? 2000 : 300);
                return;
            }
        }

        //   ======= now poll everything
        if (curViewUnit && lastPollTime < Date.now()) {
            lastPollTime = Date.now() + 350;
            let cpu = undefined; // current poll unit
            if (curViewSut && curViewUnit._subUnits[curViewSut]) {
                while (!cpu) {
                    cpu = curViewUnit._subUnits[curViewSut][pollSun];
                    if (!cpu || cpu._deleted) {
                        pollSun++;
                        if (
                            pollSun >
                            curViewUnit._desc.__.subUnits[curViewSut].qty
                        ) {
                            pollSun = 0;
                            cpu = curViewUnit;
                        }
                    }
                }
            } else cpu = curViewUnit;
            if (cpu && cpu._typeName[0] !== "_") {
                let u;
                switch (curCurCur) {
                    case 0:
                    case 2:
                        cmd = {
                            status: {},
                        };
                        if (curViewSut) pollSun++; // done-ao == bug was
                        break;
                    case 1:
                        if (!curViewSut) {
                            cmd = {
                                info: {},
                            };
                            break;
                        }
                    case 3:
                        cmd = {
                            config: {},
                        };
                        // if (curViewSut) pollSun++;  done-ao -- bug was, moved hereinbefore
                        break;
                }
                curCurCur++;
                curCurCur &= 3;
                if (cpu == curViewUnit && cmd.config) {
                    if (curCur)
                        cmd = {
                            info: {},
                        };
                    curCur = !curCur;
                }
                sendCommand(cpu, cmd);
                return;
            }
        }
    }
}

/////////_53
function changeUnitSun(cu, newSun) {
    // sun
    try {
        let oldSun = cu._sun;
        if (cu._sut == "Module") return false; // todo - currently cannot change module address

        //     if(cu._typeName == "_Box" && ( (!cu._subUnits.Module["1"])|| (!cu._subUnits.Module["1"]._subUnits.Boot["1"].status)|| (!cu._subUnits.Module["1"]._subUnits.Boot["1"].status.snType))) return false;

        if (oldSun == newSun) return false;
        if (!newSun) return false;
        // if (newSun > cu._up._desc.__.subUnits[cu._sut].qty) return false;
        let su = cu._up._subUnits[cu._sut];
        let needSunPPK = su._configOK;
        let t = su[newSun];
        if (t) {
            if (
                (cu._sut == "Module" || cu._sut == "_Box") &&
                (newSun == 255 || oldSun == 255)
            ) {
                combineUnits(cu, t); // todo -- may be 255 not used now ? rybu zavorachivali ?
                t = undefined;
            } else {
                systemLog(
                    false,
                    "two units exchanged: #" + oldSun + " and #" + newSun
                );
                t._sun = oldSun;
                t._configured = false;
                t._offline = true;
                su[oldSun] = t;
            }
        } else {
            let sud = schema.unitTypes[cu._up._typeName].__.subUnits[cu._sut];
            if (sud.software) {
                t = {
                    _offline: true,
                    _deleted: true,
                    _configured: false,
                };
                /* 
                        done-p 
                        iterated array must have "_desc". without this item changing sun is NOT working
                        (UnitChangeMenu => handleItemClick)

                    */
                for (let n of [
                    "_typeName",
                    "_sut",
                    "_sun",
                    "_up",
                    "_desc",
                    "_subUnits",
                ]) {
                    // todo-ao -- probably "_subUnits" is dangerous here. anyway, be carefull when removing separate "TS" types
                    t[n] = cu[n];
                }
                systemLog(
                    false,
                    "unit number changed from: #" + oldSun + " to #" + newSun
                );
            }
        }
        su[oldSun] = t; // to send "delete" to PPK or exchange them there
        if (!t) delete su[oldSun];
        cu._offline = true;
        cu._configured = true;
        cu._deleted = false;
        su[newSun] = cu;
        cu._sun = newSun;
    } catch {}
    if (stableSys) modelChanged("changed", cu);

    try {
        if (cu._typeName == "_Box") {
            let r = {
                id: [1, 0], // broadcast to module 1 Boot
                unitType: "Boot",
                cmd: {
                    setBoxAddress: {
                        snType: cu._subUnits.Module["1"]._subUnits.Boot["1"]
                            .status.snType,
                        newAddress: newSun,
                    },
                },
            };
            fillRequest(r); // to be sure
            fillRequest(r);
            fillRequest(r);
        }
    } catch {}
    return true;
}

///////////_52
function clearTotal() {
    deleteUnit(sys);
    //    sys = {};
    fillUnit(sys, "_System");
    //    addUnitToSut(sys,"_Box");
    modelChanged("afterLoad");
}

/////////_51
function changeUnitType(cu, newType) {
    // name of type
    if (cu._typeName === newType) return false;
    if (!unitTypeCompatible(newType, cu._sut)) return false;
    //    cu._typeName=newType; will be set in fillunit
    let ud = schema.unitTypes[newType];
    let ud_ = ud.__;
    let ud_s = ud_.subUnits;
    try {
        if (ud.config.typeSN) {
            // todo - currently disabled all this mechanics
            let n = cu.config.typeSN;
            if (
                !cu.config.typeSN ||
                !schema.typeSN[ud.config.typeSN.typeSN][n].includes(newType)
            ) {
                cu.config.typeSN = 0;
                for (let tsn in schema.typeSN[ud.config.typeSN.typeSN])
                    if (
                        schema.typeSN[ud.config.typeSN.typeSN][tsn].includes(
                            newType
                        )
                    ) {
                        cu.config.typeSN = Number(tsn);
                        break;
                    }
            }
        }
    } catch {}
    for (let sut in cu._subUnits)
        if (!ud_s[sut]) {
            cu._subUnits[sut] = undefined;
            delete cu._subUnits[sut];
        }
    for (let cmdn in cu)
        if (cmdn[0] !== "_") {
            if (!ud[cmdn]) cu[cmdn] = undefined;
            else
                for (let pn in cu[cmdn])
                    if (!ud[cmdn][pn]) cu[cmdn][pn] = undefined;
        }
    fillUnit(cu, newType);
    if (stableSys) modelChanged("changedType", cu);
    return true;
}

// ===============================  procedures to make stringID from unit and vice versa == unused now
// returns {t:type, u:unit}
/////_50
function string2u(str) {
    let fu = findUnit(makeAdrFromBin(string2uint8(str)), sys);
    if (fu) fu = fu.u;
    return fu;
}

////////_49
function u2string(u) {
    let uid = makeBinAdr(getUnitID(u));
    if (uid[0] == 0xff) uid[0] = thisBox;
    return Uint8toString(uid);
}

//////////_48
function shortID(u) {
    let uid = makeShorterAdr(getUnitID(u));
    if (uid[0] == 0xff) uid[0] = thisBox;
    return Uint8toString(uid);
}

/////////_47
function unitIdToUnit(sid) {
    return findUnit(makeAdrFromShorterBin(string2uint8(sid)), sys);
}

///////_46
function unitIdToUnit2(sid) {
    let uu = findUnit(makeAdrFromShorterBin(string2uint8(sid)), sys);
    if (uu && uu.u && uu.t && uu.u._typeName === uu.t) return uu.u;
    return sys;
}

////////_45
function checkBoxMod(u8) {
    if (u8[0] == 0xff) u8[0] = thisBox;
    if (u8[1] == 0xff) u8[1] = thisModule;
    return u8;
}

/*
 * procedures to convert unit id (uint8) to/from string;
 */
const hexd = [
    "0",
    "1",
    "2",
    "3",
    "4",
    "5",
    "6",
    "7",
    "8",
    "9",
    "A",
    "B",
    "C",
    "D",
    "E",
    "F",
];

////_44
function string2uint8(str) {
    if (str[0] == '"' || str[0] == "'") str = str.substring(1);
    if (
        (str.length && str[str.length - 1] == '"') ||
        str[str.length - 1] == "'"
    )
        str = str.substring(0, str.length - 1);
    let ab = new ArrayBuffer(300);
    let data = new Uint8Array(ab);
    let pos = 0;
    for (let i = 0; i < str.length; i++) {
        let d = str.charCodeAt(i);
        if (d <= 0x39) d -= 0x30;
        else d -= 0x37;
        if (i & 1) data[pos++] |= d & 0x0f;
        else {
            data[pos] = (d << 4) & 0xf0;
        }
    }
    return data.slice(0, pos);
}

/////_43
function byte2string(b) {
    let stt = "";
    for (let j = 2; j--; ) {
        let dd = b;
        if (j) dd >>= 4;
        dd &= 0x0f;
        stt += hexd[dd];
    }
    return stt;
}

///////////_42
function Uint8toString(u8) {
    let strt = "";
    if (u8)
        for (let i = 0; i < u8.length; i++) {
            strt += byte2string(u8[i]);
        }
    return strt;
}

// ===============================  procedures to make string from unit ID and vice versa == unused now

////////_41
function deleteUnit(u) {
    if (!u) return false;
    try {
        if (stableSys) modelChanged("beforeDelete", u);

        // kostyl for area TS
        if (
            u.config &&
            u.config.areaNum &&
            u._up &&
            u._up._typeName == "RingUPSmodule"
        ) {
            let a = u._up._subUnits.Area[u.config.areaNum];
            if (a) {
                let s = a._subUnits[u._sut];
                if (s) {
                    for (let sn in s) {
                        if (s[sn] == u) {
                            try {
                                delete s[sn];
                            } catch {}
                        }
                    }
                }
            }
        }

        for (let st in u._subUnits) // avoid possible circular links preventing heap clearance
            if (!u._desc.__.subUnits[st].virtual)
                for (let sn in u._subUnits[st]) deleteUnit(u._subUnits[st][sn]);
        try {
            delete u._up._subUnits[u._sut][u._sun];
        } catch {} // delete myself in upper unit
        //if (u == curSendingUnit)
        u._oldUp = u._up; // dont know why - sending smtms stops because deleted but not yet "next" found
        // u._up = undefined;
        if (stableSys) modelChanged("afterDelete");
        return true;
    } catch {
        systemLog(true, "cannot delete " + u._sut + " #" + u._sun);
    }
    if (stableSys) modelChanged("afterDelete");
    return false;
}

/////////////_40
function addUnitToSut(u, curSut, newType) {
    // newType may be empty
    try {
        let su = u._subUnits[curSut];
        let sud = u._desc.__.subUnits[curSut];
        if (!(sud.hardware || sud.software)) return null;
        for (let n = 1; n <= sud.qty; n++)
            if (!su[n] || su[n]._deleted) {
                let sutd = schema.unitTypes[curSut];
                let t = sud.defaultType;
                if (!t) {
                    if (sutd.__.software || sutd.__.hardware) {
                        if (!newType || !sud.allRealHeirs.includes(newType))
                            t = sud.allRealHeirs[0];
                        else t = newType;
                    } else t = curSut;
                }
                su[n] = {
                    _sun: n,
                    _configured: true,
                    _sut: curSut,
                    _up: u,
                    //          "_typeName": t
                }; // new object
                fillUnit(su[n], t);
                if (su[n]._desc.config.SN) {
                    // debug !! remove !! or may be let it be ?
                    if (!su[n].config) su[n].config = {};
                    su[n].config.SN = n + u._sun * 1000;
                }
                systemLog(false, "added " + getUnitName(su[n]));
                return su[n];
            }
    } catch {
        // todo-s uncomment to find out why problem exist?
        //systemLog(true, "cannot add more " + curSut + " to " + u._sut + " #" + u._sun);
    }
    return null;
}

//////////_39
function getModule(u) {
    while (u && u._typeName[0] !== "_" && u._typeName !== "Module") u = u._up;
    if (u._typeName !== "Module") u = null;
    return u;
}

/*
 * u = unit object, cmd = {"status":{"
 */
///////_38
function sendCommand(u, cmd, category, tag) {
    if (u._typeName[0] !== "_") {
        if (schema.unitTypes[u._typeName].__.number)
            fillRequest(
                {
                    id: getUnitID(u),
                    unitType: u._typeName,
                    cmd: cmd,
                },
                category,
                tag
            );
        else systemLog(false, "cannot send to unit type: " + u._typeName);
    }
}

///////_37
function sendCommandDelete(u) {
    if (u._typeName[0] !== "_") {
        // todo - delete Box ?
        let uid = getUnitID(u);
        uid[0] = 0; // type
        fillRequest({
            id: uid,
            unitType: "Unit",
            cmd: {
                config: {},
            },
        });
    }
}

/////////_36
function checkUnitDesc(u) {
    if (u && !u._desc) {
        u._desc = {
            __: {},
        };
        try {
            let t = u._realType;
            if (!t) t = u._typeName;
            u._desc = schema.unitTypes[t];
        } catch {}
    }
}

///////////_35
function getHardwareID(u) {
    let hid = "";
    checkUnitDesc(u);
    try {
        if (u._desc.__.hardwareIDs && u.config)
            for (let pn of u._desc.__.hardwareIDs)
                if (typeof u.config[pn] === "number") {
                    if (pn == "SN") hid += "N";
                    hid += u.config[pn];
                }
        // kostyl for CPU to show MOD#1 s/n
        if (u._sut == "_Box") {
            let sn = u._subUnits.Module[1].config.SN;
            if (sn) hid = " sn" + sn;
        }
    } catch {}

    return hid;
}

function updateCfgFromFile(e, cb) {
    let file = e.target.files[0];
    let reader = new FileReader();
    reader.onload = function () {
        loadCfg(reader.result, cb);
    };
    reader.onerror = function () {
        systemLog(true, reader.error);
    };
    reader.readAsText(file);
}

function readConfigFromFile(cb) {
    var link = document.createElement("input");
    link.type = "file";
    link.onchange = (e) => updateCfgFromFile(e, cb);
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    delete link;
}
//////////////_32
function saveConfigToFile() {
    let textCfg = createCfg();
    saveFileJSON(textCfg, "config");
}

//////////_31_origin_in_viewGray_js
function createCfg() {
    // curUnit, curType
    let cfg = {
        lang: lang,
        oper: opLvl,
        version: schema.version,
        state: sys.state,
    };

    function copySub(s, c) {
        for (let n of ["_typeName", "_sut", "_sun", "config", "_deleted"])
            c[n] = s[n];
        c._subUnits = {};
        for (let stn in s._subUnits)
            if (stn) {
                c._subUnits[stn] = {};
                try {
                    if (!s._desc.__.subUnits[stn].virtual)
                        for (let sun in s._subUnits[stn])
                            if (
                                s._subUnits[stn][sun] &&
                                sun[0] !== "_" &&
                                (s._subUnits[stn][sun]._configured ||
                                    (s._subUnits[stn][sun]._deleted &&
                                        s._subUnits[stn][sun]._sut !==
                                            "_Box") ||
                                    !(
                                        s._desc.__.subUnits[stn].hardware ||
                                        s._desc.__.subUnits[stn].software
                                    ))
                            ) {
                                c._subUnits[stn][sun] = {};
                                copySub(
                                    s._subUnits[stn][sun],
                                    c._subUnits[stn][sun]
                                );
                            }
                } catch {}
            } else console.log("undef");
    }
    copySub(sys, cfg);
    return JSON.stringify(cfg);
}

////////_30
function saveCfg() {
    localStorage.setItem("config", createCfg());
}

//////////_29
function fillUnit(obj, name) {
    let preStable = stableSys;
    stableSys = false;
    let oldName = obj._typeName;
    if (name) obj._typeName = name;
    obj._desc = schema.unitTypes[obj._typeName];
    if (!obj._subUnits) obj._subUnits = {};
    obj._idString = shortID(obj);

    if (obj._desc.config) {
        if (!obj.config) obj.config = {};
        for (pn in obj._desc.config)
            if (
                pn !== "__" &&
                ["fixed32", "varInt", "fixed64"].includes(
                    obj._desc.config[pn].format
                )
            )
                if (!(pn in obj.config)) {
                    if (obj._desc.config[pn].defaultValue)
                        // todo - made it while loading schems
                        obj.config[pn] = obj._desc.config[pn].defaultValue;
                    else {
                        if (obj._desc.config[pn].minValue)
                            obj.config[pn] = obj._desc.config[pn].minValue;
                        else obj.config[pn] = 0;
                    }
                }
        for (pn in obj.config)
            if (!(pn in obj._desc.config)) delete obj.config[pn];
    }

    if (obj._up && obj._up._desc) {
        let d_ = obj._up._desc.__;
        if (d_) {
            let ds = d_.subUnits;
            if (ds) {
                let dst = ds[obj._sut];
                if (dst && !(dst.hardware || dst.software || obj._deleted))
                    obj._configured = true;
            }
        }
    }

    let ud_ = obj._desc.__;
    let sutd = ud_.subUnits;
    if (sutd)
        for (let sutn in sutd) {
            if (!obj._subUnits[sutn]) {
                obj._subUnits[sutn] = {};
            }
            if (sutd[sutn].virtual || sutd[sutn].unused || !sutd[sutn].qty)
                for (let un in obj._subUnits[sutn])
                    deleteUnit(obj._subUnits[sutn][un]);
            if (
                !(
                    sutd[sutn].hardware ||
                    sutd[sutn].software ||
                    sutd[sutn].virtual
                )
            ) {
                // fixed
                for (let i = sutd[sutn].qty; i; i--)
                    checkSubUnit(obj, i, sutn, sutn);
                for (let un in obj._subUnits[sutn]) {
                    if (+un > sutd[sutn].qty || +un == 0) {
                        delete obj._subUnits[sutn][un]; // remove garbage - was once 5 IN in ISM2 on site
                    }
                }
            } else if ("Module" === sutn) {
                // todo fixme - use some "board" information more flexible
                if (Object.keys(obj._subUnits[sutn]).length < 3) {
                    let bd = schema.typeSN.Module;
                    for (let md in bd)
                        checkSubUnit(obj, md, "Module", bd[md][0]); // todo multiple modules
                }
            }

            if (!sutd[sutn].virtual)
                for (let i in obj._subUnits[sutn]) // clear above "qty"
                    if (i[0] !== "_") {
                        let ii = parseInt(i, 10);
                        if (
                            (ii >= sutd.qty && ii !== 255) ||
                            !unitTypeCompatible(
                                obj._subUnits[sutn][i]._typeName,
                                sutn
                            )
                        )
                            delete obj._subUnits[sutn][i];
                        else {
                            obj._subUnits[sutn][i]._up = obj;
                            fillUnit(
                                obj._subUnits[sutn][i],
                                obj._subUnits[sutn][i]._typeName
                            );
                        }
                    }
        }

    for (let sutn in obj._subUnits) if (!sutd[sutn]) delete obj._subUnits[sutn];
    if (obj == sys) stableSys = true;
    else stableSys = preStable;
}

var stableSys; // todo - what is this for ???

function alertMultipleLinks(u, ts1, ts2) {
    let alertMsg =
        "multiple links from TS \n unitID: " + u._idString + "\n unit:\n";
    alertMsg += getUnitName(u) + "\n first TSid: " + ts1._idString + "\n";
    alertMsg += "first TS:\n" + getUnitName(ts1) + "\n";
    alertMsg +=
        "second TSid: " + ts2._idString + "\n secondTS: \n" + getUnitName(ts2);
    alert(alertMsg);
}

/*
 *  cfg has [sys:]  {"config":{},"subunits":{"AL":{}, ... etc ...}}
 */
/////////_28_big_changes
function loadCfg(JSONcfg, cb = () => {}) {
    sendingUnit =
        curSendingUnit =
        curViewUnit =
        curViewSut =
        fw2updateUnit =
        curPollingModule =
            undefined;
    try {
        let cfg = JSON.parse(JSONcfg);
        if (cfg.lang) lang = cfg.lang;
        if (cfg.oper) opLvl = cfg.oper;
        if ((cfg._typeName = "_System")) {
            if (sys) deleteUnit(sys);
            sys = cfg;
            fillUnit(sys, "_System");
            initFaultsDown(sys);
        }

        function markConfigured(u) {
            u._configured = !u._deleted;
            if (u._subUnits)
                for (let st in u._subUnits)
                    if (st[1] != "_")
                        for (let sn in u._subUnits[st])
                            if (sn[0] != "_")
                                markConfigured(u._subUnits[st][sn]);
        } // function

        markConfigured(sys);

        // KOSTYL FIXME
        {
            let boxes = sys._subUnits._Box;
            for (let bn in boxes) {
                let b = boxes[bn];
                let m = b._subUnits.Module[1];
                let a = m._subUnits.Area;
                for (let n of ["Area", "InputLink", "OutputLink"]) {
                    let l = m._subUnits[n];
                    for (let ln in l) {
                        let ll = l[ln];
                        if (ll.config) {
                            ll._oldArea = ll.config.areaNum;
                            if (ll._oldArea && !ll._deleted) {
                                if (!a[ll._oldArea]) createArea(m, ll._oldArea);
                                a[ll._oldArea]._subUnits[ll._sut][ll._sun] = ll;
                            }
                        }
                    }
                }
            }
        }

        // init backLinks (kostyl ?)
        {
            let pp = sys._subUnits._Box;
            for (let pn in pp) {
                let m = pp[pn]._subUnits.Module[1];
                for (let ltn of ["InputLink", "OutputLink"]) {
                    let ll = m._subUnits[ltn];
                    for (let ln in ll) {
                        let link = ll[ln];
                        if (link.config && link.config.unitID) {
                            let u = unitIdToUnit2(link.config.unitID);
                            if (u && u._idString == link.config.unitID) {
                                if (!u.config) u.config = {};
                                uID = link._idString;
                                let ts1;
                                if (
                                    u.config.backLink &&
                                    u.config.backLink != uID
                                )
                                    ts1 = unitIdToUnit2(u.config.backLink);
                                if (ts1 && ts1._idString == u.config.backLink) {
                                    alertMultipleLinks(u, ts1, link);
                                    link.config.unitID = "";
                                } else u.config.backLink = uID;
                            }
                        }
                    }
                }
            }
        }

        modelChanged("afterLoad");

        // AO --- may be move it to modelChanged() ?
        // done-s must be here for re-render
        cb();
    } catch {
        systemLog(true, "error parsing JSON cfg");
    }
}

//////////_27
function restoreCfg() {
    let JSONcfg = localStorage.getItem("config");
    if (JSONcfg) loadCfg(JSONcfg);
    else systemLog(true, "error reading localstorage");
    modelChanged("afterLoad");
}

//////////_26_problem_is_dissapear_new_added_tabs
function initializeModel(callBack) {
    restoreCfg();
    if (!sys) {
        sys = {};
        fillUnit(sys, "_System");
    }

    checkSunSut(sys);

    startHardwarePolling();
    pollingStage = 0;
    if (stageIntervalId) window.clearInterval(stageIntervalId);
    stageIntervalId = setInterval(buildPollingRequest, 300);

    window.onbeforeunload = function (event) {
        saveValueForElement(document.activeElement);
        saveCfg();
    };

    callBack();

    return;
}

/*
 *
 * procedures to get array of usual integer [] as unit id and back, find unit pointed with such an array
 * find from any root (may be several trees - SYS and PPK at least)
 *
 * array: type 0 subunitID subunitID ... subunitID modNumber boxNumber
 * in fact similar to that in PPK binary packet but in reverse order
 */
/////////////_25
function getUnitID(u) {
    if (!u) return null;
    try {
        function makeSuID(sutn, sun) {
            // TTTTNNNTNNTN
            let id = 0;
            if (!sutn) sutn = 0;
            if (!sun) sun = 0;
            id |= (sutn << 4) & 0x00f0;
            id |= (sutn << 12) & 0x00f0000;
            id |= (sutn << 24) & 0xffff00000000;
            id |= sun & 0x00f;
            id |= (sun << 4) & 0x00ff00;
            id |= (sun << 8) & 0x0fff00000;
            return id;
        }

        let uid = [];

        if (u._typeName[0] === "_") {
            if (u._typeName === "_Box") {
                // for _system - push noting
                uid.push(u._sun); // only box number
            }
        } else {
            if (u._typeName !== "Boot") {
                // for module will push type,0,suid
                let t = schema.unitTypes[u._typeName].__.number;
                if (!t) t = 0; // abstract type - for query or delete
                uid.push(t); // type number
                uid.push(0); // end of path
            } else u = u._up; // go to module, will only push modnum+boxnum

            while (u._up && u._sut !== "Module") {
                let t = makeSuID(
                    u._up._desc.__.subUnits[u._sut].number,
                    u._sun
                );
                if (t) uid.push(t);
                else return null;
                u = u._up;
            }
            uid.push(u._sun); // mod number
            uid.push(u._up._sun); // box number
        }
        return uid;
    } catch {
        return null;
    }
}

//// returns {"t":unitTypeName from ID,"u":unit}; or null if failed
///////////_24
function findUnit(uid, root) {
    try {
        let l = uid.length;
        if (l === 0)
            return {
                t: "_System",
                u: root,
            };
        let bn = uid[--l];
        if (bn === 255 && thisBox && thisBox != 255) bn = thisBox;
        let box = root._subUnits._Box[bn];
        if (l === 0)
            return {
                t: "_Box",
                u: box,
            };
        let mn = uid[--l];
        if (mn === 255 && thisModule && thisModule != 255) mn = thisModule;
        let mod = box._subUnits.Module[mn];
        if (l === 0)
            return {
                t: "Boot",
                u: mod._subUnits.Boot[1],
            };
        let cu = mod;
        while (l) {
            let sid = uid[--l];
            if (sid) {
                let sutn =
                    ((sid & 0xf0) >> 4) |
                    ((sid & 0x0f0000) >> 12) |
                    ((sid & 0xffff00000000) >> 24);
                let sun =
                    (sid & 0x0f) |
                    ((sid & 0xff00) >> 4) |
                    ((sid & 0xfff00000) >> 8);
                let ud_ = schema.unitTypes[cu._typeName].__;
                let sut = ud_.subUnitNumbers[sutn];
                cu = cu._subUnits[sut][sun];
            } else {
                let t = uid[--l];
                let utn = schema.unitTypesNumbers[t];
                if (!utn) utn = cu._sut;
                return {
                    t: utn,
                    u: cu,
                    tn: t,
                };
            }
        }
    } catch {
        return null;
    }
}

/*
 * to iterate all units below "top"
 * returns next
 */
////////_23
function nextUnit(u, top, sut) {
    ///////////////////
    let maxSutn;
    let sutns;
    let ud_;
    let uUp = u._up;
    if (!uUp) uUp = u._oldUp;
    if (u._oldUp && u._deleted) u._up = u._oldUp = null;

    function setMaxSutn(cu) {
        // sets maxSutn & sutns
        try {
            maxSutn = 0;
            ud_ = schema.unitTypes[cu._typeName].__;
            sutns = ud_.subUnitNumbers;
            for (let n in sutns) {
                let nn = n;
                let nnm = sutns[n];
                let sud = cu._subUnits[nnm];
                if (typeof nn !== "number") nn = parseInt(n);
                if (maxSutn < nn && sud && !sud.unused && !sud.virtual)
                    maxSutn = nn;
            }
        } catch {
            sutns = {};
        }
    }

    if (!u._deleted) {
        // do not send deleted subunits - they are already deleted
        try {
            // go down
            setMaxSutn(u);
            for (let cn = maxSutn; cn >= 0; cn--) {
                let sut = sutns[cn];
                if (u == sendingUnit && sendingUnitSut) sut = sendingUnitSut;

                if (
                    sut &&
                    u._subUnits[sut] &&
                    !ud_.subUnits[sut].unused &&
                    !ud_.subUnits[sut].virtual
                ) {
                    let maxsun = 0;
                    for (let sun in u._subUnits[sut]) {
                        let ns = u._subUnits[sut][sun];
                        let n = Number(sun);
                        if (n && ns && sun != "__" && n > maxsun) {
                            maxsun = n;
                        }
                    }
                    while (maxsun) {
                        if (
                            u._subUnits[sut][maxsun] &&
                            (u._subUnits[sut][maxsun]._configured ||
                                u._subUnits[sut][maxsun]._deleted)
                        )
                            return u._subUnits[sut][maxsun];
                        else maxsun--;
                    }
                    //      else return null;
                }
                if (u == sendingUnit && sendingUnitSut) return 0; // no units below - done
            }
        } catch {}
    }

    function searchHorizontal(u) {
        try {
            setMaxSutn(uUp);
            while (maxSutn && u._sut !== sutns[maxSutn])
                // find my
                maxSutn--;
            if (u._sut !== sutns[maxSutn]) return null;
            let sun = u._sun;

            do {
                let sut = sutns[maxSutn];
                if (sut) {
                    let qty = ud_.subUnits[sut].qty;
                    //        if (qty < sun) sun = qty + 1; // not my sut ======== todo = AU above 256 in PPK ?
                    while (--sun) {
                        let ns = uUp._subUnits[sut][sun];
                        if (ns && (ns._configured || ns._deleted)) {
                            return ns; // max sun, ok
                        }
                    }
                }
                if (uUp == sendingUnit && sendingUnitSut)
                    // do not change sut
                    return 0; // no units below - done

                if (!maxSutn--) return null; // no more sut
                if (sutns[maxSutn])
                    sun = uUp._desc.__.subUnits[sutns[maxSutn]].qty + 1; // todo - get max here
            } while (1);
        } catch {}
        return null;
    }
    // no more down

    while (u != top) {
        let ns = searchHorizontal(u);
        if (ns) {
            return ns; // max sun, ok
        }
        u = uUp;
        uUp = u._up;
        if (!uUp) uUp = u._oldUp;
        if (u._oldUp) u._up = u._oldUp = null;
    }
    return null;
}

///////_22
function combineUnits(u1, u2) {
    for (let n of ["config", "status"]) {
        if (u2[n]) {
            if (!u1[n]) u1[n] = {};
            for (let p in u2[n]) u1[n][p] = u2[n][p];
        }
    }
    for (let stn in u2._subUnits)
        if (stn[0] != "_")
            for (let sn in u2._subUnits[stn])
                if (sn[0] != "_") {
                    if (u1._subUnits[stn][sn])
                        combineUnits(u1._subUnits[stn], u2._subUnits[stn]);
                    else {
                        if (stn) u1._subUnits[stn] = u2._subUnits[stn];
                    }
                }
}

//////////_21
function setSubscribe(u, force, sut) {
    // force not used = march 2023
    subscribeTime = Date.now() + 10000; // once 10 sec
    curViewUnit = u;
    curViewSut = sut;
    if (force) {
        curPollingModule = undefined;
    }
    let cpm = u;
    while (cpm && cpm._sut !== "Module") cpm = cpm._up;
    if (!cpm) return;
    if (cpm !== curPollingModule)
        sendCommand(cpm, {
            subscribe: {
                justContinue: 7, // fixme
            },
        });

    curPollingModule = cpm;
}

var logTime = 0;
var logQty = 0;
var logMax = 0;
var logUnit;
var logTimeoutId;
var doClearLog = false;
var goGetFullLog = false;

////////_20
function startLog(u, time, qty) {
    logUnit = u;
    if (!u) return;
    if (!u._logIndex) logUnit._logIndex = 0;
    if (time < 0) {
        if (time != -1) logUnit._logIndex = 0;
        time = 0; // will continue by index
    }

    logTime = time;
    logQty = qty;
    logMax = 300;
    if (logQty && logUnit) {
        goGetLog = 10;
        logTimeoutId = window.setTimeout(getLogData, 200); // set first timeout
    }
}

//////////_19
function getLogData() {
    if (logTimeoutId) {
        window.clearTimeout(logTimeoutId);
        logTimeoutId = undefined;
    } // clear if already set
    if (goGetLog && logMax) {
        logMax--;
        if (logTime && !logUnit._logIndex && !goGetFullLog)
            sendCommand(logUnit, {
                records: {
                    time: logTime,
                },
            });
        else
            sendCommand(logUnit, {
                records: {
                    index: logUnit._logIndex,
                },
            });
        logTimeoutId = window.setTimeout(getLogData, 250);
    }
}

var lastBoxToPollLog;

/////////////_18_some_strange
function processLogData(u, m) {
    if (!m.records) return false;

    // may be background reading
    {
        let mod = u._up;
        let l = mod._log;
        let bn = mod._up._sun;
        if (bn && bn == lastBoxToPollLog && l.indx == m.records.indexRequest) {
            if (!m.records.record || m.records.index < m.records.indexRequest) {
                l.full = true;
                // return false;
            } else {
                l.full = false;
                if (m.records.index + 1 != l.indx) {
                    lastLogPollTime = 0; // force fast
                    l.log.push(m.records);
                    let t = logText(m.records);
                    l.logText = t + "<br/>" + l.logText;
                    try {
                        modelChanged("logAdded", mod, t);
                    } catch {}
                    l.indx = m.records.index + 1;
                }
            }
        }
    }

    if (!logQty) {
        goGetLog = 0;
        if (goGetFullLog) finalFullLog();
        return false;
    }
    if (!logMax) return false;
    if (u != logUnit) return false;
    if (m.records.indexRequest == logUnit._logIndex || !logUnit._logIndex) {
        if (
            !m.records.record ||
            m.records.index < m.records.indexRequest ||
            !m.records.record.length ||
            !m.records.record[0]
        ) {
            if (goGetFullLog && !--goGetLog) {
                goGetFullLog = 0;
                logMod = logUnit._up;
                // if(!logMod._log)
                //     logMod._log={};
                // logMod._log.logText = fullLogText;
                // logMod._log.indx = m.records.index + 1;
                logUnit = 0;
                try {
                    finalFullLog(); // todo - view function !!
                } catch {}
                return false;
            }
            return true;
        } else {
            displayLog(m.records); // todo - view function !!
            logUnit._logIndex = m.records.index + 1;
        }
        if (logQty) logQty--;
        else {
            goGetLog = 0;
            if (goGetFullLog) finalFullLog();
        }
        logTime = 0;
        getLogData();
    }
    goGetLog = 10;
    return true;
}

//////////_17
function clearAll(u, sut) {
    if (u && u._subUnits) {
        let sus = u._subUnits[sut];
        if (sus) for (let sn in sus) if (sn[0] !== "_") deleteUnit(sus[sn]);
    }
}

/////////_16
function saveKrut() {
    let name = prompt("File name: ", "configIntellect");
    if (!name) return;
    var link = document.createElement("a");
    link.download = name + ".json";
    link.href = "data:application/json," + createCfgKrutikoff();
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    delete link;
}

////////_15
function createCfgKrutikoff() {
    let cfg = [];

    function copySub(s) {
        let tnum = s._desc.__.number;
        if (typeof tnum != "number") tnum = 0;
        let uc = {
            config: s.config,
            typeName: s._typeName,
            typeNum: tnum,
            subUnits: [],
            sut: s._sut,
            sun: s._sun,
        };
        if (!uc.config) uc.config = {};
        for (let stn in s._subUnits) {
            let subst = s._subUnits[stn];
            let subt = {
                subUnitTypeName: stn,
                subUnitsOfThisType: [],
                subUnitTypeSubNumber: s._desc.__.subUnits[stn].number,
            };
            for (let stnn in subst)
                subt.subUnitsOfThisType.push(copySub(subst[stnn]));
            uc.subUnits.push(subt);
        }
        return uc;
    }
    cfg.push(copySub(sys));
    return JSON.stringify(cfg);
}

///////_14
function buildInput(u) {
    // fixme - multibox,    schema defines
    let newin = 0;
    try {
        if (!u) return 0;
        if (!u._typeName) return 0;
        if (!u._desc) return 0;
        if (!u._desc.__.number) return 0;
        if (!u._desc.__.defaultInput) return 0;
        let m = u;
        while (m && m._typeName !== "_Box") m = m._up;
        let m1 = m._subUnits.Module[1];
        let ins = m1._subUnits.InputLink;

        let id = shortID(u);
        let nin;
        for (let n in ins)
            if (
                ins[n].config &&
                ins[n].config.unitID &&
                ins[n].config.unitID == id
            ) {
                nin = n;
                break;
            }
        if (!nin) {
            nin = addUnitToSut(m1, "InputLink");
            if (nin) {
                if (!nin.config) nin.config = {};
                nin.config.unitID = id;
                nin.config._typeName = u._desc.__.defaultInput;
                newin += 1;
            }
        }

        for (let t in u._subUnits)
            for (let n in u._subUnits[t])
                newin += buildInput(u._subUnits[t][n]);
        return newin;
    } catch {
        return newin;
    }
}

/////////_13_norm
function checkUpdateFirmware(u, fw) {
    try {
        let csut = u._sut;
        let btn = 0;
        if (csut === "Boot") {
            // very special - not defined in schema.JSON
            csut = "Module";
            if (u.status.snType) btn = u.status.snType >> 24;
            else if (u._up.config.typeSN) btn = u._up.config.typeSN;
            let bts = schema.typeSN[csut];
            if (!bts[fw[0x1b]])
                // check OK firmware
                return 2;
            if (btn && btn !== fw[0x1b]) return 2;
        } else if (csut === "SK" || csut === "BISM1") {
            // old (R08) format of firmware
            // todo - check type compatibility
            let bt = u._desc.__.boardType;
            let btf = fw[0x19];
            if (bt != btf || !bt) return 2;
        } else return 1; // cannot
    } catch {
        return 3;
    }
    return 0;
}

////////_12
function createArea(m, n) {
    let a = m._subUnits.Area;
    if (a[n]) return;
    a[n] = {
        _sun: n,
        _configured: true,
        _sut: "Area",
        _up: m,
        //          "_typeName": "FireAreaExt"
    }; // new object
    systemLog(false, "added Area #" + n);
    fillUnit(a[n], "FireAreaExt");
}

//////_11
function onAreaChange(u) {
    // unit with config.area parameter
    // KOSTYL !!!!!!!! FIXME
    let mod = u._up;
    if (mod._sut != "Module") mod = mod._up;
    let areas = mod._subUnits.Area;
    if (u._oldArea && areas[u._oldArea]) {
        delete areas[u._oldArea]._subUnits[u._sut][u._sun]; // done-ao   was == areas[u._oldArea]._subUnits[u._sut][u._sun] = undefined;    == bug - was attempt to iterate over deleted subunit
    }

    u._oldArea = u.config.areaNum;
    if (u._oldArea && !u._deleted) {
        createArea(mod, u._oldArea);
        areas[u._oldArea]._subUnits[u._sut][u._sun] = u;
    }
}

///////_10
function recalcFaultsDown(u) {
    if (!u) return;
    let q = 0;
    for (let t in u._subUnits) {
        if (t[0] != "_")
            for (let n in u._subUnits[t])
                if (n[0] != "_")
                    if (
                        ((u._subUnits[t][n].status &&
                            (u._subUnits[t][n].status.fault ||
                                u._subUnits[t][n].status.faultDown ||
                                u._subUnits[t][n].status.offline ||
                                u._subUnits[t][n].status.notSynched)) ||
                            !u._configOK) &&
                        !(
                            u._subUnits[t][n].status &&
                            u._subUnits[t][n].status.disabled
                        ) &&
                        !(
                            u._subUnits[t][n].config &&
                            u._subUnits[t][n].config.ignored
                        )
                    ) {
                        q = 1;
                        break;
                    }
        if (q) break;
    }
    if (!u.status) u.status = {};
    //  let d = (u.status.faultDown != q);
    if (u._desc && u._desc.status && u._desc.status.faultDown)
        u.status.faultDown = q;
    //    if (d)
    recalcFaultsDown(u._up);
}

////////_9
function initFaultsDown(u) {
    for (let t in u._subUnits)
        if (t[0] != "_" && !u._desc.__.subUnits[t].virtual)
            // kostyl virtual
            for (let n in u._subUnits[t])
                if (n[0] != "_") initFaultsDown(u._subUnits[t][n]);
    if (u.status && u._desc && u._desc.status && u._desc.status.faultDown)
        u.status.fault = u.status.faultDown = 0;
}

//////////_8
function startFullLog(u) {
    logUnit = u;
    if (!u) return;
    logUnit._logIndex = logTime = 0;
    goGetFullLog = true;
    goGetLog = 10;
    logQty = logMax = 500000;
    logTimeoutId = window.setTimeout(getLogData, 200); // set first timeout
}

function copyMultyCfg(cu, tu) {
    let typ = cu._typeName;
    let cf = cu._desc.config;
    if (!cf) return;

    function chkU(u) {
        try {
            if (u._typeName == typ) {
                if (!u.config) u.config = {};
                for (let cp in cu.config)
                    if (!(cf[cp].hardwareID || cf[cp].ro || cf[cp].link))
                        u.config[cp] = cu.config[cp];
            }
        } catch {}
        for (let sn in u._subUnits)
            for (let st in u._subUnits[sn]) chkU(u._subUnits[sn][st]);
    }
    chkU(tu);
}

///   todo-ao  -- very dangerous - subunits may be absent, need special processing for types, strange copy of unit to subunit --- unused in blue now --- todo todo fixme
/////////////_7
function copyMultyCfg(cu, tu) {
    let typ = cu._typeName;
    let cf = cu._desc.config;
    if (!cf) return;

    function chkU(u) {
        try {
            if (u._typeName == typ) {
                if (!u.config) u.config = {};
                for (let cp in cu.config)
                    if (!(cf[cp].hardwareID || cf[cp].ro || cf[cp].link))
                        u.config[cp] = cu.config[cp];
            }
        } catch {}
        for (let sn in u._subUnits)
            for (let st in u._subUnits[sn]) chkU(u._subUnits[sn][st]);
    }
    chkU(tu);
}

///////////_6
function dosend1Parameter(unit, message, parameter, newValue) {
    let cmd = {};
    if (!unit[message]) unit[message] = {};
    cmd[message] = {};
    cmd[message][parameter] = newValue;
    sendCommand(unit, cmd);
}

///////////_5
function exec1Command(unit, message) {
    let cmd = {};
    cmd[message] = unit[message];
    sendCommand(unit, cmd);
}

///////////_4
function setUpdateFirmware(targetFile, curUnit) {
    let file = targetFile;
    let reader = new FileReader();
    let u = curUnit;

    reader.onload = function () {
        // todo check unit type
        let u8 = new Uint8Array(reader.result);
        /*
            knowhow
            byte [0x1b] is board type
            */
        let id = "--";
        try {
            let chk = checkUpdateFirmware(u, u8);

            if (chk == 1)
                systemLog(true, "only Module update currently possible");
            // todo - do it similar for all updates
            else if (chk == 2)
                systemLog(
                    true,
                    "incompatible firmware type #" +
                        u8[0x1b] +
                        " for board type #" +
                        u._up._typeName
                );
            else if (chk != 0)
                systemLog(true, "unknown error while loading firmware");

            if (chk) return;

            if (
                !confirm(
                    "are You sure - load firmware " +
                        file.name +
                        " on this board ?"
                )
            )
                // todo - show names of board, module, and fw type
                return;
            id = getUnitID(u);
            updateFirmware(id, reader.result);
            if (
                fw2updateUnit &&
                !confirm(
                    "Currently update of " +
                        getUnitName(fw2updateUnit) +
                        " not finished. Are you sure to abort and start new update job ?"
                )
            )
                return;
            fw2updateUnit = u;
            modelChanged("boot");
        } catch {
            let s =
                "unknown error catched - id= " +
                id +
                ", typename= " +
                u._typeName;
            systemLog(true, s);
        }
    };

    reader.onerror = function () {
        systemLog(true, reader.error);
    };

    reader.readAsArrayBuffer(file);
}

//////////_3
function setValueInMsg(unit, msgName, paramName, value) {
    // returns value corrected to possible === todo - move correction to schema.js, add js validator e.g. to set possible ISM timers
    if (!unit) return;
    if (!unit._desc) return;
    if (!unit._desc[msgName]) return;
    let pd = unit._desc[msgName][paramName];
    if (!pd) return;

    if (typeof pd.minValue == "number" && pd.minValue > value)
        value = pd.minValue;
    if (typeof pd.maxValue == "number" && pd.maxValue < value)
        value = pd.maxValue;

    if (!unit[msgName]) unit[msgName] = {};
    if (unit[msgName][paramName] !== value) unit._edited = true;

    let oldV = unit[msgName][paramName];

    if (msgName == "config" && pd.link && paramName == "unitID") {
        if (oldV != value) {
            let uOld = unitIdToUnit2(oldV);
            if (uOld && uOld.config && uOld.config.backLink == oldV)
                delete uOld.config.backLink;
            let uNew = unitIdToUnit2(oldV);
            if (uNew && uNew._typeName != "_System") {
                if (!uNew.config) uNew.config = {};
                let oldBack = uNew.config.backLink;
                if (typeof oldBack != "string" || !oldBack.length)
                    uNew.config.backLink = unit._idString;
                else {
                    let tsOld = unitIdToUnit2(oldBack);
                }
            }
        }
    }

    //if (typeof(oldV) !== "number") oldV = 0;
    unit[msgName][paramName] = value;

    if (msgName === "config") {
        if (paramName === "typeSN") {
            // todo typesn -- now lot of questions how to set SN
            let boardDesc = schema.typeSN[unit._sut][value];
            if (!(boardDesc && boardDesc.includes(unit._typeName)))
                changeUnitType(unit, boardDesc[0]);
        }

        if (!unit._configPPK) unit._configPPK = {};

        if (typeof unit._configPPK[paramName] == "undefined" && !pd.notForPPK)
            unit._configPPK[paramName] = oldV;
        else if (
            typeof unit._configPPK[paramName] != "undefined" &&
            !pd.notForPPK &&
            unit._configPPK[paramName] === value
        )
            delete unit._configPPK[paramName];

        modelChanged("changed", unit, { config: {} }); // was updateunitview

        if (pd && pd.callOnAreaChange) onAreaChange(unit); // KOSTYL !!!!!!!! FIXME
    }

    if (unit._sut == "Module" && msgName === "config" && paramName == "SN") {
        // todo - introduce board of 3 modules
        let su = unit._up._subUnits.Module;
        for (let sun in su) {
            if (!su[sun].config) su[sun].config = {};
            su[sun].config.SN = value;
        }
    }

    return value;
}

///////_2
function stopSendingUnit(text) {
    sendingUnit = curSendingUnit = undefined;
    modelChanged("stopSendingUnit", undefined, text);
}

/////////////////_1
function justSendConfigToPPK(unit, sut, only1) {
    incorrectBackConfig = 0;
    sending1unit = only1;
    sendingUnitSut = sut;
    sendingUnit = unit;
    curSendingUnit = undefined;
    systemLog(false, schema.dictionary["2ppk"][lang] + " " + getUnitName(unit));
}

function markConfigured(unit, sut) {
    function markUnitConfigured(u) {
        u._deleted = false;
        u._configured = true;
        let su = u._subUnits;
        let sud = u._desc._subUnits;
        for (let sut in sud) {
            if (su[sut] && !(sud[sut].hardware && sud[sut].software)) {
                let sus = su[sut];
                for (let sun in sus) {
                    let susu = sus[sun];
                    if (!susu._configured) {
                        markUnitConfigured(susu);
                    }
                }
            }
        }
    }
    if (sut) {
        let su = unit._subUnits[sut];
        for (let sun in su) {
            markUnitConfigured(su[sun]);
        }
    }
    let uuu = unit;
    while (uuu) {
        markUnitConfigured(uuu);
        uuu = uuu._up;
    }
}

function copy2preset(unit) {
    // only config, w/o unique fields
    let result = {
        __type: unit._typeName,
    };
    try {
        let c = unit.config;
        let cd = unit._desc.config;
        for (let pn in c) {
            if (cd[pn] && !cd[pn].hardwareID && !cd[pn].link) {
                result[pn] = c[pn];
            }
        }
    } catch {}
    return result;
}

function usePreset(unit, sut, preset) {
    function copyConfig(u) {
        if (!u.config) {
            u.config = {};
        }
        for (let pn in preset) {
            if (pn != "__type" && preset[pn] != undefined) {
                setValueInMsg(u, "config", pn, preset[pn]);
            }
        }
    }

    try {
        if (sut) {
            for (let sun in unit._subUnits[sut])
                if (unit._subUnits[sut][sun]._typeName == preset.__type)
                    copyConfig(unit._subUnits[sut][sun]);
        } else {
            if (unitTypeCompatible(preset.__type, unit._sut)) {
                if (unit._typeName != preset.__type) {
                    unit._sut = preset.__type;
                    fillUnit(unit);
                }
                copyConfig();
            } else if (unitTypeCompatible(preset.__type, unit._sut)) {
                copyConfig();
            } else {
                systemLog(
                    0,
                    "pasting: incompatible types " +
                        unit._sut +
                        " and " +
                        preset.__type
                );
            }
        }
    } catch {}
}

function getModLogToPoll() {
    // returns MODULE (RING) to poll for log next
    let boxAdrList = Object.getOwnPropertyNames(sys._subUnits._Box);
    if (!boxAdrList.length) {
        lastBoxToPollLog = undefined;
        return undefined;
    } else if (!lastBoxToPollLog || !sys._subUnits._Box[lastBoxToPollLog]) {
        lastBoxToPollLog = boxAdrList[0];
        return sys._subUnits._Box[lastBoxToPollLog]._subUnits.Module["1"];
    } else {
        let lastPos = boxAdrList.indexOf(lastBoxToPollLog) + 1;
        if (!lastPos) lastPos = 1;
        let curPos = lastPos;
        let protect = 50; // max cycles
        do {
            if (curPos >= boxAdrList.length) curPos = 0;
            let m =
                sys._subUnits._Box[boxAdrList[curPos]]._subUnits.Module["1"];
            let l = m._log;
            if (!l) {
                // reboot was, reread from start
                return m;
            }
            if (!l.full) {
                return m;
            }
            curPos++;
        } while (curPos != lastPos && protect--);
        if (curPos >= boxAdrList.length) curPos = 0;
        return sys._subUnits._Box[boxAdrList[curPos]]._subUnits.Module["1"];
    }
}

function getLogToPoll() {
    // returns MODULE (RING) to poll for log next
    try {
        let m = getModLogToPoll();
        if (!m._log) m._log = { indx: 0, log: [], logText: "" };
        lastBoxToPollLog = m._up._sun;
        return m;
    } catch {
        return null;
    }
}

function logText(rec) {
    const event = new Date();
    event.setTime(1000 * (rec.time + 946684800));
    let str = "<br/>" + event.toISOString() + " [" + rec.index + "]<br/>";

    if (rec.record) {
        try {
            if (rec.record[0] == 3 && rec.record[1] == 254) {
                let v = rec.record[2] * 256 + rec.record[3];
                str += "internal log message " + v;
            } else {
                let tt = rec.record.slice(2);
                tt = checkBoxMod(tt);
                let uu = findUnit(makeAdrFromShorterBin(tt), sys);
                let p = 2;
                while (p < tt.length && tt[p]) p++;
                let t = tt.slice(0, p);

                if (uu && uu.u && uu.t && uu.u._typeName === uu.t)
                    str += getUnitName(uu.u);
                else str += "unit=" + Uint8toString(t);
                str += "<br/>";
                if (uu) {
                    while (p < tt.length && !tt[p]) p++;
                    while (p < tt.length && 0x80 & tt[p]) p++;
                    p++;

                    let an = decodeAnswerBySchemaFromProtobuf(
                        uu.tn,
                        protobuf2JSON(tt.slice(p))
                    );
                    let s = "";
                    let ud = schema.unitTypes[uu.t];
                    for (let m in an) {
                        let md = ud[m];
                        s += md.__["Title" + lang] + ": <br/>";
                        for (let p in an[m]) {
                            let pd = md[p];
                            s += '"' + pd["Title" + lang] + '": ';

                            function namval(v) {
                                if (
                                    pd.enumEN &&
                                    pd["enum" + lang] &&
                                    pd["enum" + lang][v]
                                )
                                    return (
                                        '"' +
                                        pd["enum" + lang][v].split(":", 1)[0] +
                                        '"'
                                    );
                                else return v;
                            }
                            let v = an[m][p];
                            if (Array.isArray(v))
                                for (vv of v) s += namval(vv) + ",";
                            else s += namval(v) + ";";
                            s += "<br/>";
                        }
                        str += s;
                    }
                } else str += Uint8toString(rec.record);
            }
        } catch {}
    }
    return str;
}

function saveFileJSON(json, defaultName) {
    let name = prompt("File name: ", defaultName);
    if (!name) return;
    let link = document.createElement("a");
    link.download = name + ".json";
    link.href = "data:text/json," + encodeURIComponent(json);
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    delete link;
    modelChanged("showCfg", undefined, json);
}

function saveFile(obj, defaultName) {
    let nn = JSON.stringify(obj);
    saveFileJSON(nn, defaultName);
}
