/*
 * 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

in sys, mod2 and AL there are hardware subunits.
possible element _subSubstitutes ==== not saved to file
in it "sut": then "sun":and there is link to main unit with same unique serial number
substituted units are absent in model
they are possible only as "not-approved" got from PPK
main unit may be also "not-approved"
if any other with this SNtype will be approved manually (created), it will became "main"

in the main unit there is "_substitutes" = array of {"up":unit, "sut":sut, "sun":N} === not saved to file
in "sys" also there is element "_serials" = "SNtype":unit pointing to main unit === not saved to file


 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)





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

   



LOG as IndexedDB:

global 
var localDB; // indexedDB with all read logs

already open as request = window.indexedDB.open("Sigma", 3)
no need to reopen. 
model.js does necessary upgrade if needed

two objectStores
"LogLast", "Log"
 
in fact, You need only "Log"
it is FULL LOG from ALL boxes

both stores have same records format:
{snAdr:"123.5", snAdrInd:123.5.762823, index:762823, datePC:65456465, timePPK:4351561, message:{}}
sample above.
snAdr is unique key in LogLast - there are only LAST records from EVERY box
snAdrInd is NOT unique key in Log - there are ALL records from ALL boxes, including already erased records
datePC also not unique key in Log

message in records - as recieved from PPK.
currently, I did only simple helper function 
    logText(msg)
    returns string on currently selected language (in fact - multi-string with some "newlines" in it )
    will improve later to return some struct with separate strings
   
 *
 *
 */

var specialTagWhileFwOrCfg = false;
var repeatAfterDoneFwOrCfg = 0; // to send "normal tag" after special

var readModCfg; // ref to current module to read cfg
var readAllModCfg; // bool if read through all modules in this box
var readAllBoxCfg; // bool if read through all boxes

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 failedUnits = '';
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;
var loggedInUser;
var ringCoef = 1; // if current pollng module is not on drect connected Box - slow pollng

// 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];
        if (sutusu.config && sutusu.config.backLink) sutusu.config = { backLink: sutusu.config.backLink };
        else 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) {
    // BOOT answer
    // 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 = message.cmd.status.boardType;
    if (!moduleTypeNum) moduleTypeNum = snt >> 24;
    let sn = message.cmd.status.sn;
    if (!sn) 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);
    }

    let hid = hidStringCT({ SN: sn }, '_Box'); // force address if present in config
    if (!sys._serials) sys._serials = {};
    if (sys._serials[hid] && sys._serials[hid][0]) {
      let b = unitIdToUnit2(sys._serials[hid][0]);
      if (b && b._sut == '_Box' && b._sun != boxSun) {
        setBoxAddress(b);
        return;
      }
    }

    let needsetadr = false;
    b = sys._subUnits._Box[boxSun];
    if (b && b._hidString != hid) {
      // busy address find new address for this box
      let newadr = 0;
      do {
        b = sys._subUnits._Box[++newadr];
      } while (b);
      boxSun = newadr;
      needsetadr = true;
    }

    // ensure box [boxsun] exists or create if we have CFG with serial number
    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', { SN: sn }); // 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);
        }
        if (needsetadr) setBoxAddress(nb);
      } else return;
    }

    b = sys._subUnits._Box[boxSun];

    if (!b) {
      // status was not from 1st module boot
      fillRequest({
        id: [1, boxSun], // poll this boxes, 1st module, boot
        unitType: 'Boot',
        cmd: {
          status: {},
        },
      }); // to create this box
      return;
    }

    if (!b.config) b.config = {};
    if (!b.config.SN) {
      b.configSN = sn;
      modelChanged('changed', b);
    } else if (b.config?.SN != sn) {
      if (!b._configPPK) b._configPPK = {};
      b._configPPK.SN = sn;
      modelChanged('changed', b);
    }
    if (b.config.SN == sn && b._configPPK && b._configPPK.SN) {
      delete b._configPPK.SN;
      modelChanged('changed', b);
    }
    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) {
                  failedUnits += getUnitName(curSendingUnit) + '\r\n';
                  // 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"

  // may be ringupsmodule was cleared - recheck log state
  if (u._typeName == 'RingUPSmodule' && message.cmd.info) {
    // check reboot
    let rebootTime = Math.floor(
      Date.now() / 1000 - message.cmd.info.timeFromStart, // Note: once a 50 days timeFromStart will roll over and we will think it was rebooted = it's ok
    );
    let box = u._up;
    if (!box._logData)
      // new box found - not in log
      box._logData = {
        lastRebootTime: 0,
        lastPollTime: 0,
        confirmed: true, // just read all from 0
        full: false,
        lastIndex: 0,
      };
    if (!box._logData.lastRebootTime || box._logData.lastRebootTime < rebootTime) {
      if (box._logData.lastRecord) box._logData.confirmed = false;
      // we have some recotd from this box - check it is still present in the box flash
      else box._logData.lastIndex = 0;
      box._logData.lastPollTime = 0;
      box._logData.full = false;
      box._logData.lastRebootTime = rebootTime + 2; // to be sure, module starts at least 3 sec
    }
  }

  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 = readModCfg || m == curPollingModule)) {
      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
      if (m == readModCfg) {
        systemLog(false, 'got CFG from ' + getUnitName(u));
        if (message.cmd.status) {
          // done
          readModCfg = undefined;
          // next m if needed
          if (readAllModCfg) {
            let foundThis = false;
            let newm;
            for (let msun in b._subUnits.Module) {
              if (foundThis) {
                newm = b._subUnits.Module[msun];
                break;
              }
              foundThis = msun == m._sun;
            }
            if (readAllBoxCfg && !newm) {
              // search for new box
              foundThis = false;
              for (let bsun in sys._subUnits._Box) {
                if (foundThis) {
                  for (let msun in sys._subUnits._Box[bsun]._subUnits.Module) {
                    newm = sys._subUnits._Box[bsun]._subUnits.Module[msun];
                    break; // first module in new box
                  }
                  break;
                }
                foundThis = bsun == b._sun;
              }
            }
            if (newm) fastReadModuleCfg(newm);
            else {
              stopFastReadAllCfg(readAllBoxCfg ? null : b);
            }
          } else stopFastReadAllCfg(m);
        }
      }
    } 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 (['records', 'recordsMulty'].includes(msgn)) {
      processLogData(u, message);
      continue;
    }
    if (msgn == 'status') u._lastStatusTime = Date.now();
    // 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)
          ) {
            if (u._desc.config[p].maxValue && u.config[p] > u._desc.config[p].maxValue)
              u.config[p] = u._desc.config[p].maxValue;
            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;
        u._configPPK = {}; // clear
      } 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
            } else if (u._configPPK && u._configPPK[p] != undefined)
               delete u._configPPK[p];
            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;
        }
      for (let p in u._configPPK) {
        if (typeof msg[p] == 'undefined') delete u._configPPK[p]; // clear garbage - was once impossible parameter
      }
      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 - ??
            }
          } else if (ArrayBuffer.isView(msg[p])) {
            if (!uChanged) uChanged = {};
            if (!uChanged[msgn]) uChanged[msgn] = {};
            uChanged[msgn][p] = u[msgn][p];
            u[msgn][p] = msg[p];
          }
        }
      if (recalcF) recalcFaultsDown(u._up);
    }
  }

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

  if (stableSys) {
    // hardcoded todo - schema ? - some JS in schema to "eval"...
    if (uChanged && uChanged?.oscilloscope && u.oscilloscope?.done && !uChanged?.oscilloscope.done) {
      // became "done"
      receiveOscilloscopeData(u);
    }

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

function checkConflicts(u) {
  try {
    if (!u._hidString) return undefined;
    if (u._hidString == '') return [];
    if (sys._serials[u._hidString].length <= 1) return undefined;
    return sys._serials[u._hidString];
  } catch {}
  return undefined;
}

function hidString(u) {
  // returns unique combination type+serial string.  if no serial returns "", if not unique hardware returns undefined
  try {
    if (!u) return undefined;
    if (!u._idString) u._idString = shortID(u);
    let hid = hidStringCT(u.config, u._typeName);
    if (hid) {
      u._hidString = hid;
      if (hid != '') {
        if (!sys._serials[hid]) sys._serials[hid] = [];
        if (!sys._serials[hid].includes(u._idString)) sys._serials[hid].push(u._idString);
      }
    }
    return hid;
  } catch {}
  return undefined;
}

function hidStringCT(config, typeName) {
  // may be called when new config for new unit rcvd
  // returns unique combination type+serial string.  if no serial returns "", if not unique hardware returns undefined
  // e.g. "_Box._Box.162"
  // or  "AU.ATI.10355"
  try {
    let td = schema.unitTypes[typeName];
    let hids = td.__.hardwareIDs;
    if (hids.length && config) {
      let substTypeName = td.__.hardwareIdType;
      let snt = td.config[hids[0]].hardwareID + '.' + (substTypeName ? substTypeName : typeName);
      for (let pn of hids) {
        // currently in fact only one serialNumber may be
        snt += '.' + config[pn];
        if (!config[pn]) return ''; // e.g. zero serial number
      }
      return snt;
    }
  } catch {}
  return undefined;
}

function checkNewParHid(u, parName, newVal) {
  // also wlil change parameter
  try {
    let oldVal = u.config[parName];
    let oldHid = u._hidString;
    u.config[parName] = newVal;
    let newHid = hidString(u);
    u._hidString = newHid;
    if (newHid != oldHid) {
      // else nothing changed
      if (sys._serials[oldHid]) {
        // busy new - need to check what to do
        let index = sys._serials[oldHid].indexOf(u._idString);
        if (index >= 0) sys._serials[oldHid].splice(index, 1);
      }
      if (newHid && newHid != '') {
        if (!sys._serials[newHid]) sys._serials[newHid] = [];
        if (!sys._serials[newHid].includes(u._idString)) sys._serials[newHid].push(u._idString);
      }
    }
  } catch {}
}

function checkNewTypeHid(u, newType) {
  // to call before some hid parameter changed
  try {
    let oldHid = u._hidString;
    let newHid = hidStringCT(u.config, newType);
    if (newHid != oldHid) {
      // else nothing changed
      if (sys._serials[oldHid]) {
        // busy new - need to check what to do
        var index = sys._serials[oldHid].indexOf(u._idString);
        if (index >= 0) sys._serials[oldHid].splice(index, 1);
      }
      if (newHid && newHid != '') {
        if (!sys._serials[newHid]) sys._serials[newHid] = [];
        if (!sys._serials[newHid].includes(u)) sys._serials[newHid].push(u._idString);
      }
    }
  } catch {}
}

///////_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(!tName || !schema.unitTypes[tName] || !schema.unitTypes[tName].__.number || !unitTypeCompatible(tName,sut)) tName=undefined; 
    if(res && tName && res._typeName != tName ) res=undefined;    
    if (!res) {
      res = {
        _up: up,
        _sut: sut,
        _sun: sun,
        config: cfg,
      };      
      up._subUnits[sut][sun] = res;
      fillUnit(res, tName);
      hidString(res);
      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() {
  if (!sendingUnit) {
    stopSendingUnit('uploading stopped');
    return;
  }

  incorrectBackConfig = 0;
  //   if (!sendingUnitRetries) return; // at least once MUST send data
  sendingUnitRetries = 0;
  let done = false;
  // select unit to upload

  if (!curSendingUnit) {
    // first go  up to thr Module to be sure all up units are created
    // starting, go up
    curSendingUnit = sendingUnit;
    sendingUnitStage = 0;
    if (curSendingUnit._sut == '_System' || !curSendingUnit._sut) getNextCurSending();
    else while (curSendingUnit._sut != '_Box') curSendingUnit = curSendingUnit._up;
  }

  if (!sendingUnitStage) {
    // currently sending units - direct parents of sendingUnit
    if (curSendingUnit._deleted) {
      // do not go down - they are all deleted
      stopSendingUnit('done uploading');
      return;
    }
    if (curSendingUnit !== sendingUnit) {
      // still higher up
      let uuu = sendingUnit;
      while (uuu._up !== curSendingUnit) uuu = uuu._up;
      curSendingUnit = uuu;
    }
    if (curSendingUnit === sendingUnit) {
      // went down to sending unit itself
      sendingUnitStage = 1;
    }
  } else {
    // sendingUnit itself alreafy sent. now sending units below
    do {
      if (curSendingUnit == sendingUnit) {
        // still higher up
        if (sending1unit) {
          stopSendingUnit('done 1');
          return;
        }
      }

      if (curSendingUnit._sut == 'Module') {
        lastPollAll = 0;
      }

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

  // got next unit (may be upper) or no more units

  if (!curSendingUnit) {
    stopSendingUnit('done uploading');
    return;
  } else {
    // got next unit
    if (
      curSendingUnit._sut == '_Box' &&
      (curSendingUnit._offline ||
        typeof curSendingUnit._offline === 'undefined' ||
        (curSendingUnit._configPPK &&
          curSendingUnit._configPPK.SN &&
          curSendingUnit._configPPK.SN != curSendingUnit.config.SN))
    ) {
      systemLog(false, 'set box address ' + curSendingUnit._hidString);
      setBoxAddress(curSendingUnit);
      setBoxAddress(curSendingUnit);
      getNextCurSending();
    }

    // really send config
    // prepare if Module as a whole
    if (
      !sending1unit &&
      curSendingUnit._sut == 'Module' &&
      ((sendingUnit._sut == 'Module' && !sendingUnitSut && !sending1unit) ||
        sendingUnit._sut == '_Box' ||
        sendingUnit._typeName == '_System')
    ) {
      // we are sending smth like module or higher - thats we will send this module as a whole
      systemLog(false, 'erasing ' + curSendingUnit._hidString);
      sendCommand(curSendingUnit, { eraseAll: {} }, undefined, 0xff);
      sendCommand(curSendingUnit, { eraseAll: {} }, undefined, 0xff);
    } else if (curSendingUnit._sut == 'Module') {
      // new module - stop subscr anyway
      sendCommand(curSendingUnit, { subscribe: { justContinue: 4 } }, undefined, 0xff); // stop subscribe
      sendCommand(curSendingUnit, { subscribe: { justContinue: 4 } }, undefined, 0xff); // stop subscribe
    }

    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._sut == '_Box') {
        setBoxAddress(curSendingUnit);
      } else {
        if (!curSendingUnit.config) curSendingUnit.config = {};
        sendCommand(
          curSendingUnit,
          {
            config: curSendingUnit.config,
          },
          undefined,
          0xff,
        );
      }
    }

    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;
var lastSetTimeTime = 0;
var lastPingTime = 0;

///////_54
// called at least every 300 msec and on every packet rcvd
function buildPollingRequest() {
  // process last reply time to get "online" state
  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) return;

  if (
    online &&
    sys.config.autoSetTime &&
    schema.unitTypes.Clock.status.__.RWoperatorLevel <= opLvl &&
    Date.now() > lastSetTimeTime + 300000 &&
    !specialTagWhileFwOrCfg
    // every 5 min
  ) {
    // once in 30 sec
    lastSetTimeTime = Date.now();
    let d = new Date(Date.now());
    for (let b in sys._subUnits._Box) {
      try {
        sendCommand(sys._subUnits._Box[b]._subUnits.Module[2]._subUnits.Clock[1], {
          status: {
            year: d.getFullYear() - 2000,
            month: d.getMonth() + 1,
            day: d.getDate(),
            hour: d.getHours(),
            min: d.getMinutes(),
            sec: d.getSeconds(),
          },
        });
      } catch {}
    }
  }

  if (postRequests.length < 6) {
    // not too long queue - else do not add new poll requests
    let pollBox = 0;
    let boxAdrList = Object.getOwnPropertyNames(sys._subUnits._Box);
    if (!(boxAdrList && boxAdrList.length)) {
      lastPollAll = 0;
      lastPollBoot = 1;
    }
    if (
      (specialTagWhileFwOrCfg && Date.now() > lastPollAll + 30000) ||
      (repeatAfterDoneFwOrCfg && !specialTagWhileFwOrCfg && Date.now() > lastPollAll + 5000)
    ) {
      // millisec
      lastPollAll = Date.now();
      let curtag = 0xfe;
      if (specialTagWhileFwOrCfg) {
        curtag = 0xff;
        repeatAfterDoneFwOrCfg = 100;
      } else {
        if (repeatAfterDoneFwOrCfg) {
          repeatAfterDoneFwOrCfg--;
        }
      }
      fillRequest(
        {
          id: [1, 0], // broadcast to all boxes, 1st module
          unitType: 'Boot',
          cmd: {
            status: {},
          },
        },
        undefined,
        curtag,
      ); // to be sure
    } else if (
      (curViewUnit && curViewUnit._typeName == '_System' && Date.now() > lastPollAll + 5000) ||
      Date.now() > lastPollAll + 100000
    ) {
      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 (Date.now() > lastPingTime + 5000) {
      fillRequest({
        id: [0xff, 0xff], // to this module of this box
        unitType: 'Boot',
        cmd: {
          status: {},
        },
      }); // to be sure
      lastPingTime = Date.now();
      return;
    }

    if (!specialTagWhileFwOrCfg) {
      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.sn || thisModuleBootParams.snType))) {
        fillRequest({
          id: [255, 255], // this-this
          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;

      try {
        if (!curSendingUnit) getNextCurSending();

        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 {
            if (curSendingUnit._sut == '_Box') setBoxAddress(curSendingUnit);
            else
              sendCommand(
                curSendingUnit,
                {
                  config: curSendingUnit.config,
                },
                undefined,
                0xff,
              );
            systemLog(false, 'sending to PPK again: ' + getUnitName(curSendingUnit));
          }
          return;
        }
      } catch {}
    }

    doPollLog(); // delay will be set depending on the new records presence

    if (specialTagWhileFwOrCfg) return;

    //   ======= now poll everything
    if (curViewUnit && lastPollTime < Date.now()) {
      lastPollTime = Date.now() + 300 * ringCoef;
      let cpu = undefined; // current poll unit
      if (curViewSut && curViewUnit._subUnits[curViewSut]) {
        curCurCur = 0; // only status for list of subunits
        while (!cpu) {
          cpu = curViewUnit._subUnits[curViewSut][pollSun];
          if (cpu?._deleted) cpu = undefined;
          if (!cpu) {
            pollSun++;
            if (pollSun > curViewUnit._desc.__.subUnits[curViewSut].qty) {
              pollSun = 0;
              cpu = curViewUnit;
            }
          }
        }
      } else cpu = curViewUnit;
      if (cpu && cpu._sut[0] !== '_') {
        let u;
        switch (curCurCur) {
          default:
            // e.g. 0
            cmd = {
              status: {},
            };
            // if (curViewSut) pollSun++; // done-ao == bug was
            break;
          case 3:
          case 5:
          case 7:
            if (!curViewSut) {
              cmd = {
                info: {},
              };
              break;
            }
          case 1:
            cmd = {
              config: {},
            };
            // if (curViewSut) pollSun++;  done-ao -- bug was, moved hereinbefore
            break;
        }
        curCurCur++;
        curCurCur &= 7;
        if (cpu == curViewUnit && cmd.config && cpu.config) {
          if (curCur)
            cmd = {
              info: {},
            };
          curCur = !curCur;
        }
        sendCommand(cpu, cmd);
        return;
      }
    }
  }
}

function setBoxAddress(box) {
  try {
    let snt = box.config.SN;
    if (!snt) return;
    snt |= 1 << 24;
    let r = {
      id: [1, 0], // broadcast to module 1 Boot
      unitType: 'Boot',
      cmd: {
        setBoxAddress: {
          snType: snt,
          newAddress: box._sun,
        },
      },
    };
    fillRequest(r); // to be sure three times
    fillRequest(r);
    fillRequest(r);
  } catch {}
}

function setModuleAddress(mod) {
  try {
    let snt = box.config.SN;
    if (!snt) return;
    snt |= 1 << 24;
    let r = {
      id: [1, 0], // broadcast to module 1 Boot
      unitType: 'Boot',
      cmd: {
        setBoxAddress: {
          snType: snt,
          newAddress: box._sun,
        },
      },
    };
    fillRequest(r); // to be sure three times
    fillRequest(r);
    fillRequest(r);
  } catch {}
}

function stripId(id) {
  for (let i = 0; i < id.length; i += 2) if (id[i] == '0' && id[i + 1] == '0') id = id.substring(0, i);
  return id;
}

// also including links to subunits - will replace ID string beginning
function updateLinks(u, oldIdString, newIdString) {
  try {
    if (u.config)
      for (let LL of ['backLink', 'unitID']) {
        if (u.config[LL] && u.config[LL].startsWith(oldIdString))
          u.config[LL] = u.config[LL].replace(oldIdString, newIdString);
      }
    if (u._idString && u._idString.startsWith(oldIdString)) u._idString = u._idString.replace(oldIdString, newIdString);
  } catch {}
  for (let ssut in u._subUnits)
    for (let ssun in u._subUnits[ssut]) updateLinks(u._subUnits[ssut][ssun], oldIdString, newIdString);
}

/////////_53
function changeUnitSun(cu, newSun) {
  // sun
  let oldSun = cu._sun;
  let t = false;

  let oldIdString = stripId(cu._idString);
  try {
    if (cu._sut == 'Module' && (cu._sun < 4 || newSun < 4)) return false;

    //     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) {
      systemLog(false, 'bad sun #' + newSun);
      return false;
    }
    let su = cu._up._subUnits[cu._sut];
    t = su[newSun];
    if (t) {
      systemLog(false, 'busy sun #' + newSun);
      return false;
    }
    let thisSud = cu._up._desc.__.subUnits[cu._sut];
    if (thisSud.qty && thisSud.qty < newSun) {
      systemLog(false, 'bad sun #' + newSun + ', max=' + thisSud.qty);
      return false;
    }
    // if (newSun > cu._up._desc.__.subUnits[cu._sut].qty) return false;

    let sud = schema.unitTypes[cu._up._typeName].__.subUnits[cu._sut];
    if (sud.software) {
      t = {
        _deleted: true,
      };
      /* 
                        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);
    }

    cu._sun = newSun;
    cu._offline = true;
    su[newSun] = cu;
    cu._idString = shortID(cu);

    delete su[oldSun];
    cu._configured = true;
    cu._deleted = false;
  } catch {}
  if (stableSys) modelChanged('changed', cu);

  let newIdString = stripId(cu._idString);

  try {
    if (cu._typeName == '_Box' && online)
      // todo - similar for module
      for (let ccuu of [t, cu]) setBoxAddress(ccuu);
  } catch {}

  updateLinks(sys, oldIdString, newIdString);

  return true;
}

///////////_52
function clearTotal() {
  deleteUnit(sys);
  sendingUnit = curSendingUnit = curViewUnit = curViewSut = fw2updateUnit = curPollingModule = undefined;
  specialTagWhileFwOrCfg = false;
  //    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) {
  try {
  let fu = findUnit(makeAdrFromBin(string2uint8(str)), sys);
  if (fu) fu = fu.u;
  return fu;
  } catch {return sys;}
}

////////_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) {
  if (!sid) return sys;
  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 {
      if (u._hidString && sys._serials[u._hidString]) {
        let index = sys._serials[u._hidString].indexOf(u);
        if (index >= 0) sys._serials[u._hidString].splice(index, 1);
      }
      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;
}

function createSubunit(u, curSut, n, t) {
  let su = u._subUnits[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;
  }
  hidString(su[n]);
  systemLog(false, 'added ' + getUnitName(su[n]));
  return su[n];
}

/////////////_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;
        }
        return createSubunit(u, curSut, n, t);
      }
  } 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);
  } else if (u._sut == '_Box') {
    sendCommand(u._subUnits.Module[1]._subUnits.Boot[1], { status: {} }, 0, tag);
  }
}

///////_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: {},
        },
      },
      undefined,
      0xff,
    );
  }
}

/////////_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];
        }
  } 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) {
  let link = document.createElement('input');
  link.type = 'file';
  link.onchange = e => updateCfgFromFile(e, cb);
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}
//////////////_32
function saveConfigToFile() {
  let cfg2save = createCfg(true);
  saveFileJSON(cfg2save, 'config');
}

//////////  14jun2023   -- merged from grey
function createCfg(noDefaultUser) {
  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 && !s._desc.__.subUnits[stn].virtual) {
        c._subUnits[stn] = {};
        try {
          if (!s._desc.__.subUnits[stn].virtual)
            /* 
                          done-p: 
                          this fix for save config in local storage. grey view does not set ._configured for children units
                          old value :  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))) {
                      */
            for (let sun in s._subUnits[stn])
              if (
                s._subUnits[stn][sun] &&
                sun[0] !== '_' &&
                (s._subUnits[stn][sun]._configured ||
                  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');
  }

  // done-s for adding _configured to subUnits when click save
  function markConfigured(unit) {
    unit._configured = !unit._deleted;
    if (unit._subUnits)
      for (let subUnit in unit._subUnits)
        if (subUnit[1] != '_' && !schema.unitTypes[unit._typeName].__.subUnits[subUnit].virtual)
          for (let sn in unit._subUnits[subUnit]) if (sn[0] != '_') markConfigured(unit._subUnits[subUnit][sn]);
  }

  markConfigured(sys);

  copySub(sys, cfg);
  if (noDefaultUser) delete cfg.config.defaultUser;
  return JSON.stringify(cfg);
}

////////_30
function saveCfg() {
  // let cfg2save = createCfg();
  cfg2save = { configID: 0, cfgItself: createCfg() };
  // cfg2save.configID = 0; // to do - multiple config versions
  const dbTran = localDB.transaction(['Config'], 'readwrite');
  const dbStConfig = dbTran.objectStore('Config');
  dbStConfig.put(cfg2save);
  // localStorage.setItem('config', cfg2save); === old way to save cfg - limited size
}

//////////_29
var fillUnitObj;
function fillUnit(obj, name) {
  let preStable = stableSys;
  if((!name) || !schema.unitTypes[name]|| !schema.unitTypes[name].__.number|| !unitTypeCompatible(name,obj._sut))
    name=schema.unitTypes[obj._sut].__.defaultType;
  if(!name) name=obj._sut;
  obj._typeName=name;
  hidString(obj);
  if (!fillUnitObj) fillUnitObj = obj;
  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 (name == '_System') {
    obj._sut = '_System';
    if (!obj._subUnits.User) obj._subUnits.User = {};
    if (!Object.keys(obj._subUnits.User).length)
      obj._subUnits.User[0] = {
        config: {
          name: 'admin',
          pin: 777,
          level: 5,
          lang: 1,
        },
        _sut: 'User',
        _sun: 0,
        _typeName: 'User',
      };
  }

  if (obj._desc.config) {
    if (!obj.config) obj.config = {};
    for (pn in obj._desc.config)
      if (pn !== '__') {
        if (!(pn in obj.config)) {
          if (obj._desc.config[pn].defaultValue) obj.config[pn] = obj._desc.config[pn].defaultValue;
          else if (['fixed32', 'varInt', 'fixed64'].includes(obj._desc.config[pn].format)) {
            if (obj._desc.config[pn].minValue) obj.config[pn] = obj._desc.config[pn].minValue;
            else obj.config[pn] = 0;
          } else if (obj._desc.config[pn].format == 'string') obj.config[pn] = '';
        }
      }
    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].unused || !sutd[sutn].qty)
        for (let un in obj._subUnits[sutn]) {
          // obj._subUnits[sutn][un]._up = obj;
          delete obj._subUnits[sutn][un];
        }
      if (!(sutd[sutn].hardware || sutd[sutn].software || sutd[sutn].virtual || sutd[sutn].unused || !sutd[sutn].qty)) {
        // fixed
        for (let i = sutd[sutn].qty; i; i--) checkSubUnit(obj, i, 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], {
              SN: obj.config.SN,
            }); // 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 == fillUnitObj) {
    stableSys = true;
    fillUnitObj = null;
  } else stableSys = preStable;
}

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

function alertMultipleLinks(u, ts1, ts2, addtext) {
  let alertMsg = 'Error: multiple links from TS \n\n unitID: ' + u._idString + '\n unit:\n';
  alertMsg += getUnitName(u) + '\n\n first TSid: ' + ts1._idString + '\n';
  alertMsg += 'first TS:\n' + getUnitName(ts1) + '\n';
  alertMsg += '\n second TSid: ' + ts2._idString + '\n secondTS: \n' + getUnitName(ts2);
  alertMsg += '\n\n press OK if you agree to delete second link\notherwise first link will be deleted;\n';
  if (addtext) alertMsg += '\n' + addtext + '\n';
  systemLog(false, alertMsg);
  let result = confirm(alertMsg);
  if (result) {
    ts2.config.unitID = '';
    u.config.backLink = ts1._idString;
  } else {
    ts1.config.unitID = '';
    u.config.backLink = ts2._idString;
  }
  return result;
}

/*
 *  cfg has [sys:]  {"config":{},"subunits":{"AL":{}, ... etc ...}}
 */
/////////_28_big_changes
function loadCfg(JSONcfg, cb = () => {}) {
  sendingUnit = curSendingUnit = curViewUnit = curViewSut = fw2updateUnit = curPollingModule = undefined;
  specialTagWhileFwOrCfg = false;
  try {
    if(!JSONcfg) {
      sys = {};
    } else {
    let cfg = JSON.parse(JSONcfg);
    try {
      if (cfg.lang) lang = cfg.lang;
      if (cfg.oper) opLvl = cfg.oper;
      if ((cfg._typeName = '_System')) {
        if (sys) deleteUnit(sys);
        sys = cfg;
      }
    } catch {}
  }
    if (!sys) 
      sys = {};
    sys._serials = {};
    sys._sut="_System";
    sys._sun=1;

    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) {
                let ar= checkSubUnit(m, ll._oldArea, 'Area');
                ar._subUnits[ll._sut][ll._sun] = ll;
              }
            }
          }
        }
      }
    }

    // count areas to send on ring
    {
      let boxes = sys._subUnits._Box;
      for (let bn in boxes) {
        let b = boxes[bn];
        let ar = b._subUnits.Module[1]._subUnits.Area;
        let count = 0;
        for (let an in ar) {
          if (ar[an].config.sendOnRing) count++;
        }
        if (count > 50) systemLog(true, 'too many (' + count + ') areas to send on ring (max 50) on PPK #' + bn);
      }
    }

    // now check for cycles
    systemLog(false, 'check for cycles');
    // every unit has _checkCycle _checkCyclePath _checkCycleBad
    // delete 'em afterwards
    let curPathAr = [];
    let top;
    let errCount = 0;
    let errText = 'CRITICAL ERROR:\n\n';
    let warnStr = 'WARNING:\n\n';
    function checkCycles(u, toptop, alreadyDuplicated) {
      if (!toptop) {
        // we check all areas as top
        top = u;
        curPathAr = [];
      }

      curPathAr.push(u._idString);

      if (toptop && u == top) {
        errCount++;
        let erstr = '\n\n\n\nError: cycle to subarea , path was: \n\n';
        for (let i = 0; i < curPathAr.length; i++) erstr += '\n\n -- ' + getUnitName(unitIdToUnit2(curPathAr[i]), 1);
        systemLog(false, erstr);
        // systemLog(true, erstr);
        errText += erstr;
        curPathAr.pop();
        return;
      }

      let thisDuplicated = false;

      if (!u._checkCycleBad && !alreadyDuplicated && u._checkCycle == top._idString) {
        // this area already mentioned in this run from this top

        for (let i = curPathAr.length - 1; i--; ) {
          let utid = curPathAr[i];
          let ut = unitIdToUnit2(utid);
          if (u._checkCyclePath.includes(utid)) {
            ut._checkCycleBad = true;
            errCount++;

            let erstr = '\n\n\n\nWarning: repeated link to reset !\n\n ====== path 1:\n\n';
            while (i < curPathAr.length) erstr += '\n\n -- ' + getUnitName(unitIdToUnit2(curPathAr[i++]), 1);
            erstr += '\n\n\n\n ====== path 2: ';
            i = u._checkCyclePath.indexOf(utid);
            while (i < u._checkCyclePath.length)
              erstr += '\n\n -- ' + getUnitName(unitIdToUnit2(u._checkCyclePath[i++]), 1);
            // systemLog(true, erstr);
            warnStr += erstr;
            systemLog(false, erstr);
            thisDuplicated = alreadyDuplicated = true;
            break; // for
          }
        }
      }

      u._checkCycle = top._idString;
      let ncp = [];
      for (let i = curPathAr.length; i--; ) ncp[i] = curPathAr[i];
      u._checkCyclePath = ncp;

      let mod = u._up;
      // iterate all subareas of current area on this ppk
      for (let arn in mod._subUnits.Area) {
        let ar = mod._subUnits.Area[arn];
        if (u._sun == ar?.config?.areaNum) {
          // subareas of this area
          if (ar && ar != sys && !thisDuplicated) checkCycles(ar, true, alreadyDuplicated);
        }
      }

      // iterate all links in this area on this ppk
      for (let lnkn in mod._subUnits.InputLink) {
        let lnk = mod._subUnits.InputLink[lnkn];
        if (lnk._typeName == 'FireAreaLink' && lnk?.config?.areaNum == u._sun) {
          // area link in this area
          let ar = unitIdToUnit2(lnk?.config?.unitID);

          if (ar && ar != sys && !thisDuplicated) {
            curPathAr.push(lnk._idString);
            checkCycles(ar, true, alreadyDuplicated);
            curPathAr.pop();
          }
        }
      }
      curPathAr.pop();
    }

    for (let bn in sys._subUnits._Box) {
      let b = sys._subUnits._Box[bn];
      let m = b._subUnits.Module[1];
      for (let an in m._subUnits.Area) checkCycles(m._subUnits.Area[an]);
    }

    if (errText.length > 20) systemLog(true, errText);
    if (warnStr.length > 20) systemLog(true, warnStr);

    systemLog(false, 'total suspicious links in config: ' + errCount);
    errText = 'CRITICAL ERROR:\n\n';

    function clearBacklinks(u) {
      if (u && u._processedClear) {
        let erst = 'cycled ' + getUnitName(u) + '\n\n';
        // systemLog(true, erst);
        errText += erst;
        systemLog(false, erst);
        return;
      }
      u._processedClear = true;
      if (u && u.config && u.config.backLink) delete u.config.backLink;

      if (u && u._checkCycle) delete u._checkCycle;
      if (u && u._checkCyclePath) delete u._checkCyclePath;
      if (u && u._checkCycleBad) delete u._checkCycleBad;

      if (u && u._backlinks) delete u._backlinks;
      for (let st in u._subUnits) {
        if (!u._desc.__.subUnits[st].virtual)
          for (let sn in u._subUnits[st]) {
            clearBacklinks(u._subUnits[st][sn]);
          }
      }
    }
    clearBacklinks(sys);

    if (errText.length > 20) systemLog(true, errText);

    systemLog(false, 'testing for multiple links');
    // 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];
          if (ll)
            for (let ln in ll) {
              let link = ll[ln];
              if (link && 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 &&
                    ((u._typeName != 'FireAreaExt' && u._typeName != 'FireArea') || link._typeName != 'FireAreaLink')   // todo - use area.allhairs
                  ) {
                    alertMultipleLinks(u, ts1, link);
                  } else u.config.backLink = uID;
                  if (link._typeName == 'FireAreaLink' && (u._typeName == 'FireArea' || u._typeName == 'FireAreaExt')) {
                    if (!u._backlinks) u._backlinks = [];
                    let failed = false;
                    for (let oldid of u._backlinks) {
                      if (oldid[0] == uID[0] && oldid[1] == uID[1]) {
                        // same PPK
                        ts1 = unitIdToUnit2(oldid);
                        alertMultipleLinks(u, ts1, link);
                        failed = true;
                      }
                    }
                    if (!failed) u._backlinks.push(uID);
                  }
                }
              }
            }
        }
      }
    }

    // init serials
    sys._serials = {};

    function initSerialsSub(u) {
      if (u) {
        hidString(u); // stores u in sys.serials
        if (u._subUnits) {
          for (let sut in u._subUnits)
            if (!u._desc.__.subUnits[sut].virtual)
              for (let sun in u._subUnits[sut]) initSerialsSub(u._subUnits[sut][sun]);
        }
      }
    }

    checkSunSut(sys);

    initSerialsSub(sys); // recursive

    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');
  }

  // if (sys.config.defaultUser)
  let pass = localStorage.getItem('forcedUserPass');
  if (typeof pass != 'string' || !pass.length) pass = sys.config.defaultUser;
  setUser(pass);
  // else changeLang('RU');
}

//////////_27
function restoreCfg(cback= () => {}) {
  let JSONcfg;
  try {
    const dbTran = localDB.transaction('Config');
    const dbSt = dbTran.objectStore('Config');
    const dbCurReq = dbSt.openCursor();
    dbCurReq.addEventListener('success', event => {
      try {
        if (event.target.result) {
          loadCfg(event.target.result.value.cfgItself);
          initDB2(cback);
        } else {
          try {
            loadCfg(localStorage.getItem('config'));
            initDB2(cback);
          } catch {}
        }
      } catch {}
    });
    dbCurReq.addEventListener('error', event => {
      try {
        loadCfg(localStorage.getItem('config'));
        initDB2(cback);
      } catch {}
    });
  } catch {
    JSONcfg = localStorage.getItem('config');
    if (JSONcfg) loadCfg(JSONcfg);
    initDB2(cback);
  }
}

//////////_26_problem_is_dissapear_new_added_tabs
function initializeModel(callBack) {
  function finalInitModel() {
    startHardwarePolling();
    pollingStage = 0;
    if (stageIntervalId) window.clearInterval(stageIntervalId);
    stageIntervalId = setInterval(buildPollingRequest, 100);
    window.onbeforeunload = function (event) {
      saveValueForElement(document.activeElement);
      saveCfg();
    };
    callBack();
  }

  initDB(finalInitModel);

  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 === 'User') {
      // kostyl - not real subunit
      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
      t = makeSuID(u._up._desc.__.subUnits[u._sut].number, u._sun);
      if (t) uid.push(t);
      uid.push(0);
      uid.push(0);
    } else if (u._sut[0] === '_') {
      if (u._sut === '_Box') {
        // for _system - push noting
        uid.push(u._sun); // only box number
      }
    } else {
      if (u._sut !== '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) {
  // input - u8 arrray started from box
  // binary uid !!!
  if (!root) root = sys;
  try {
    let l = uid.length;
    if (l === 0)
      return {
        t: '_System',
        u: root,
      };
    let bn = uid[--l];
    let cu = root;
    if (!bn) {
      // sure user
      --l; // skip module
    } else {
      if (bn === 255 && thisBox && thisBox != 255) bn = thisBox;
      let box = root._subUnits._Box[bn];
      if (!box) return null;
      if (l === 0)
        return {
          t: '_Box',
          u: box,
        };
      let mn = uid[--l];
      if (mn == 0)
        return {
          t: '_Box',
          u: box,
        };
      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],
        };
      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;
  ringCoef = Object.keys(sys._subUnits._Box).length;
  if (!ringCoef) ringCoef = 1;
  if (cpm._up._sun == thisBox) ringCoef = 1;
}

// all Log procedures refactored to IndexedDB

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

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

function saveEmu() {
  let ppks = sys._subUnits._Box;
  for (let p in ppks) {
    let b = ppks[p];
    let ALs = b._subUnits.Module[3]._subUnits.AL;
    for (let i = 2; i; i--) {
      let al = ALs[i];
      let AUs = al._subUnits.AU;
      let txt = 'c\r\nc\r\n'; // shure clear
      let name = 'box' + b.config.SN + '_' + i;
      for (let j in AUs) {
        let au = AUs[j];
        let tn = au._typeName.toUpperCase();
        if (tn == 'MKZ3') tn = 'MKZ';
        if (['AXDPI', 'ATI', 'ISM2', 'ISM5', 'ARMINI', 'IR', 'AMK', 'ISM4', 'AR1', 'MKZ'].includes(tn)) {
          let lin = tn + ' ' + au.config.SN + ' ' + j;
          txt += 'a ' + lin + '\r\n'; // \r\n   o ' + lin + '\r\n ';
        }
      }

      saveTextToFile(txt, name + '.txt');
      // let link = document.createElement('a');
      // link.download = name + '.txt';
      // link.href = 'data:text/plain,' + encodeURI(txt);
      // document.body.appendChild(link);
      // link.click();
      // document.body.removeChild(link);
    }
  }
}

/////////_16
function saveKrut() {
  // let name = prompt('File name: ', 'configIntellect');
  // if (!name) return;
  saveFileJSON(createCfgKrutikoff(), 'configIntellect');

  // let link = document.createElement('a');
  // link.download = name + '.json';
  // link.href = 'data:application/json,' + createCfgKrutikoff();
  // document.body.appendChild(link);
  // link.click();
  // document.body.removeChild(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);
}

/////////_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.boardType) btn = u.status.boardType;
      else {
        if (u && u._up && u._up.config) {
          if (u._up.config.typeSN) btn = u._up.config.typeSN;
          else if (u._up.config.typeSN) btn = u._up.config.typeSN;
        } else alert('no u._up.config');
      }
      let bts = schema.typeSN[csut];
      if (fw.length < 0x20) alert('too short file');
      if (!bts[fw[0x1b]])
        // check OK firmware
        return 2;
      if (btn && btn !== fw[0x1b]) return 2;
      if (u.status.boardVersion && u.status.boardVersion !== fw[0x16]) return 3;
    } 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]);
}

//////_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) && areas[u._oldArea]) {
    // createArea(mod, u._oldArea); // do not create - leave it to "@"checkCfg"
    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;
  recalcFaultsDown(u);
}

// refactored to IndexedDB
// //////////_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 == 3)
        systemLog(
          true,
          'incompatible firmware board version #' + u8[0x16] + ' for board type #' + u.status.boardVersion,
        );
      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);
      if (
        fw2updateUnit &&
        !confirm(
          'Currently update of ' +
            getUnitName(fw2updateUnit) +
            ' not finished. Are you sure to abort and start new update job ?',
        )
      )
        return;
        fw2updateUnit = u;
        updateFirmware(id, reader.result);
      modelChanged('boot');
    } catch {
      let s = 'unknown error catched - id= ' + id + ', typename= ' + u._typeName;
      systemLog(true, s);
    }
    postHardware();
  };

  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.format != 'string' && pd.maxValue < value) value = pd.maxValue;
  if (typeof pd.maxValue == 'number' && pd.format == 'string' && pd.maxValue < value.length)
    value = value.substring(0, pd.maxValue);

  if (!unit[msgName]) unit[msgName] = {};
  let oldV = unit[msgName][paramName];

  if (oldV !== value) {
    unit._edited = true;

    if (msgName == 'config' && pd.hardwareID) checkNewParHid(unit, paramName, value);
    else {
      if (msgName == 'config' && pd.link && paramName == 'unitID') {
        let uOld = unitIdToUnit2(oldV);
        if (uOld && uOld._typeName == '_System') uOld = undefined;
        if (uOld && uOld.config && uOld.config.backLink == unit._idString) delete uOld.config.backLink;
        let uNew = unitIdToUnit2(value);
        if (uNew && uNew._typeName != '_System' && (uNew._typeName != 'FireAreaExt' && uNew._typeName != 'FireArea')) {
          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 (!tsOld.config) tsOld.config = {};
            if (tsOld.config.unitID == value) {
              if (Window.autoClickFromPPK || !alertMultipleLinks(uNew, unit, tsOld, 'You are editing first TS')) {
                value = oldV; // answer was NOT OK == return THIS back
                if (uOld) {
                  if (!uOld.config) uOld.config = {};
                  uOld.config.backLink == unit._idString;
                }
              } else {
                uNew.config.backLink = unit._idString;
              }
            }
          }
        }
      }
    }
  }
  unit[msgName][paramName] = value;

  //if (typeof(oldV) !== "number") oldV = 0;

  if (msgName === 'config') {
    if (!unit._configPPK) unit._configPPK = {};

    if (unit._realType && 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 == '_Box' && msgName === 'config' && paramName == 'SN') {
    // kostyl to set SN in first 3 modules
    // todo - introduce board of 3 modules
    let su = unit._subUnits.Module;
    for (let sun of [1, 2, 3]) {
      // if(!su[sun]) addUnitToSut   ====  always fililed with 3 modules
      if (!su[sun].config) su[sun].config = {};
      su[sun].config.SN = value;
      // if (!su[sun]._subUnits.Boot[1].status)
      //     su[sun]._subUnits.Boot[1].status = {};
      // su[sun]._subUnits.Boot[1].status.sn = value;
    }
  }

  if (unit._sut == 'Module' && msgName === 'config' && paramName == 'SN' && unit._sun < 4) {
    // kostyl to set SN in first 3 modules
    // todo - introduce board of 3 modules
    value = unit.config.SN = unit._up.config.SN;
  }

  return value;
}

///////_2
function stopSendingUnit(text) {
  sendingUnit = curSendingUnit = undefined;
  specialTagWhileFwOrCfg = false;
  modelChanged('stopSendingUnit', undefined, text);
  let uu = curViewUnit;
  while (uu && uu._sut && uu._sut != 'Module' && uu._sut[0] != '_') uu = uu._up;
  if (uu._sut == 'Module')
    sendCommand(uu, {
      subscribe: {
        justContinue: 1, // just continue )
      },
    });
  if (failedUnits.length) alert('failed upload:\r\n' + failedUnits);
}

/////////////////_1
function justSendConfigToPPK(unit, sut, only1) {
  incorrectBackConfig = lastPollAll = 0;
  sending1unit = only1;
  sendingUnitSut = sut;
  sendingUnit = unit;
  specialTagWhileFwOrCfg = true;
  failedUnits = '';
  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 (!unitTypeCompatible(preset.__type, u._sut))
      return;
    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])
        copyConfig(unit._subUnits[sut][sun]);
    } else {
        copyConfig(unit);
    }
  } catch {}
}

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) {
  if (!defaultName) defaultName = 'json';
  saveTextToFile(json, defaultName + '.json', 'application/json');
  // modelChanged('showCfg', undefined, json);
}

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

function checkPresent(u) {
  try {
    if (!u) return sys;
    u = unitIdToUnit2(u._idString);
    if (!u) return sys;
    return u;
  } catch {
    return sys;
  }
}

function setUser(pin) {
  loggedInUser = undefined;
  try {
    for (let user in sys._subUnits.User) {
      if (sys._subUnits.User[user].config.pin == pin) {
        loggedInUser = sys._subUnits.User[user];
        break;
      }
    }
  } catch {}
  modelChanged('userSet');
}

function fastReadModuleCfg(m) {
  readModCfg = m;
  sendCommand(m, { subscribe: { justContinue: 4 } }); // fast and stop
  sendCommand(m, { subscribe: { justContinue: 4 } }); // fast and stop
  sendCommand(m, { subscribe: { justContinue: 4 } }); // fast and stop
  sendCommand(m, { subscribe: { justContinue: 6 } }); // fast and stop
  systemLog(false, 'start read CFG from ' + getUnitName(m));
}

function fastReadAllCfg(u) {
  let justStop = false;
  if (readModCfg && readModCfg._sut == 'Module') {
    // now we are reading smth == stop it
    if (readAllBoxCfg && !u) justStop = true;
    else if (u && u._sut == 'Module' && u == readModCfg) justStop = true;
    else if (u && u._sut == '_Box' && u == readModCfg._up) justStop = true;
    stopFastReadAllCfg(readAllBoxCfg ? sys : readAllModCfg ? readModCfg._up : readModCfg);
  }
  readModCfg = undefined;
  readAllBoxCfg = readAllModCfg = false;
  if (justStop) return;
  let m;
  if (!u) {
    u = sys;
    // all sys
    for (let bsun in sys._subUnits._Box) {
      for (let msun in sys._subUnits._Box[bsun]._subUnits.Module) {
        m = sys._subUnits._Box[bsun]._subUnits.Module[msun];
        break; // first module in new box
      }
      break;
    }
    if (m) readAllBoxCfg = readAllModCfg = true;
    else return;
  } else if (u._sut == '_Box') {
    for (let msun in u._subUnits.Module) {
      m = u._subUnits.Module[msun];
      break; // first module in new box
    }
    if (m) readAllModCfg = true;
  } else if (u._sut == 'Module') m = u;
  else {
    while (u._sut != 'Module' && u._sut != '_System') u = u._up;
  }
  if (m) {
    modelChanged('startReadAllCfg', u);
    if (!u) u = sys;
    let nam = getUnitName(u);
    if (nam == '') nam = 'ALL PPKR';
    systemLog(false, 'start read CFG from ' + nam);
    fastReadModuleCfg(m);
  }
}

function stopFastReadAllCfg(u) {
  modelChanged('doneReadAllCfg', u);
  readAllModCfg = readAllBoxCfg = readAllBoxCfg = false;
  readModCfg = undefined;
  if (!u) u = sys;
  let nam = getUnitName(u);
  if (nam == '') nam = 'ALL PPKR';
  systemLog(false, 'stop read CFG from ' + nam);
}

/////////////////// log functions //////////////////////////

/*

records in both stores are identical
{snAdr:"123.5", snAdrInd:123.5.762823, index:762823, datePC:65456465, timePPK:4351561, message:{}}

in sys._subIniits._Box[adr] there are
{_logData:{lastIndex:786587658, lastPollTime: 655467, confirmed:true, lastRebootTime = milliseconds from }}


*/

var localDB; // indexedDB with all read logs

var lastBoxToPollLog;
function initDB2(callBack = () => {}) {
  let boxes = sys._subUnits._Box;
  for (let b in boxes)
    boxes[b]._logData = {
      lastRebootTime: 0,
      lastPollTime: 0,
      confirmed: false,
      full: false,
      lastIndex: 0,
    };
  // now read all "last" messages to avoid requering them
  if (localDB) {
    const dbTran = localDB.transaction('LogLast');
    const dbSt = dbTran.objectStore('LogLast');
    const dbCurReq = dbSt.openCursor();
    dbCurReq.onsuccess = event => {
      const cursor = event.target.result;
      if (cursor) {
        if (cursor.value) {
          let snadr = cursor.value.snAdr;
          let snDadr = snadr.split('.'); // ["123","6564464"]
          let boxAdr = snDadr[1];
          let boxSn = snDadr[0];
          let box = sys._subUnits._Box[boxAdr];
          if (box && box.config.SN == boxSn) {
            // for every "last" record we need to read this record from ppk and check that it is that very record. then we will read later
            // if not - read all records
            box._logData.lastIndex = cursor.value.index;
            box._logData.confirmed = false;
            box._logData.lastRecord = cursor.value;
          }
        }
        cursor.continue();
      } else callBack(); // everything initialized, now background log poll will read records
    };
  }
}

function initDB(cback) {
  // Let us open our database
  const request = window.indexedDB.open('Sigma', 4);
  request.onerror = event => {
    alert('failed to open IndexedDB, errorCode = ' + request.errorCode);
  };
  request.addEventListener('success', event => {
    localDB = event.target.result;
    restoreCfg(cback);
  });
  request.onupgradeneeded = event => {
    // Save the IDBDatabase interface
    const db = event.target.result;
    const tran = event.target.transaction;
    let objectStore;
    // Create an objectStore for all records
    if (db.objectStoreNames.contains('Log')) {
      objectStore = tran.objectStore('Log');
    } else {
      objectStore = db.createObjectStore('Log', {
        autoIncrement: true,
      });
    }
    if (!objectStore.indexNames.contains('snAdrInd'))
      objectStore.createIndex('snAdrInd', 'snAdrInd', { unique: false });
    if (!objectStore.indexNames.contains('datePC')) objectStore.createIndex('datePC', 'datePC', { unique: false });

    // Create an objectStore for last record
    let objectStore2;
    if (db.objectStoreNames.contains('LogLast')) {
      objectStore2 = tran.objectStore('LogLast');
    } else {
      objectStore2 = db.createObjectStore('LogLast', {
        keyPath: 'snAdr',
      });
    }
    // Create an objectStore for config
    let objectStore3;
    if (db.objectStoreNames.contains('Config')) {
      objectStore2 = tran.objectStore('Config');
    } else {
      objectStore2 = db.createObjectStore('Config', {
        keyPath: 'configID',
      });
    }
  };
}

var noPullLog = false;
function doPollLog() {
  if (specialTagWhileFwOrCfg || noPullLog) return;
  if (Date.now() < lastLogPollTime) return;

  lastLogPollTime = Date.now() + 200; // 200millisec = before process next potential timeout
  // normaly next poll request is sent when previous answer rcvd.
  // initial requests are sent at 200 ms intervals

  if (!lastBoxToPollLog) lastBoxToPollLog = 0;

  const prevBox = lastBoxToPollLog;
  const boxes = sys._subUnits._Box;
  let b;
  do {
    b = boxes[++lastBoxToPollLog];
    if (lastBoxToPollLog > schema.unitTypes._System.__.subUnits._Box.qty) lastBoxToPollLog = 0;
  } while (lastBoxToPollLog != prevBox && !b);

  if (!b) return; // no boxes at all

  if (!b._logData)
    b._logData = {
      lastRebootTime: 0,
      lastPollTime: 0,
      confirmed: true, // probably new box - no last record
      full: false,
      lastIndex: 0,
    };
  let l = b._logData;

  if (!l.lastRecord) l.confirmed = true;

  if (Date.now() < l.lastPollTime) return; // wait for timeout

  const log = b._subUnits.Module[1]._subUnits.Logger[1];

  if (l.full && l.confirmed) l.lastPollTime = Date.now() + 5000;
  // if full - retry read after 5 sec
  else l.lastPollTime = Date.now() + 200; // else - retry read as fast as possible
 
  if (printMessages) systemLog(0,' ==< box: '+ b._sun +' == lastIndex: ' + l.lastIndex + ' == conf: '  + l.confirmed + ' == full: '+l.full, 'fuchsia');

  let ind = l.lastIndex;
  if (!ind) l.confirmed = true;
  if (l.confirmed && ind) ind++;
  sendCommand(log, {
    records: {
      // todo - use recordsMulty
      index: ind,
    },
  });
}

// called when answer from PPK rcvd
// message.cmd.records = data rcvd
function processLogData(u, message) {
  // todo - use recordsMulty
  try {
    let msg = message.cmd.records;

    let mod = u._up;
    let box = mod._up;

    if (!box._logData)
      box._logData = {
        lastRebootTime: 0,
        lastPollTime: 0,
        confirmed: true, // just read all from 0
        full: false,
        lastIndex: 0,
      };

    let l = box._logData;

    if (!msg.index) {
      if (msg.topIndex != l.lastIndex) {
        l.lastIndex = 0;
      }
      return; // empty message - no new records
    }

    if (printMessages) systemLog(0,' ==> box: '+ box._sun +' == lastIndex: ' + l.lastIndex + ' == conf: '  + l.confirmed + ' == full: '+l.full, 'fuchsia' );


    if ((l.lastIndex | 1) != (msg.indexRequest | 1)) return; // ignore +1

    if (!l.lastRecord) l.confirmed = true;

    if (!l.confirmed) {
      // first check that log still is same
      let sameMsg =
        l.lastIndex == msg.index &&
        l.lastRecord.index == msg.index &&
        l.lastRecord.timePPK == msg.time &&
        ArrayBuffer.isView(l.lastRecord.record) &&
        ArrayBuffer.isView(msg.record);
      if (sameMsg && l.lastRecord.record.length == msg.record.length)
        // looks like same
        for (let i = msg.record.length; i--; ) sameMsg &= l.lastRecord.record[i] == msg.record[i];
      if (!sameMsg) {
        l.lastIndex = 0;
      }
      //            else l.lastIndex++; // read next
      l.confirmed = true;
      l.full = false;
    } else {
      if (msg.index) {
        l.lastIndex = msg.index; // + 1;
        l.full = msg.topIndex && msg.topIndex == msg.index;
        let snAdr = '';
        if (!box.config) box.config = {};
        if (box.config.SN) snAdr += box.config.SN;
        snAdr += '.' + box._sun;
        let snAdrInd = snAdr + '.' + msg.index;
        //{snAdr:"123.5", snAdrInd:123.5.762823, index:762823, datePC:65456465, timePPK:65465, message:{}}

        const event = new Date();
        let tzdiff = event.getTimezoneOffset();
        event.setTime(1000 * (msg.time + 946684800 + tzdiff * 60));
        let struct = {
          date: event,
          index: msg.index,
        };

        let us;

        if (msg.record) {
          // in fact cannot be empty
          try {
            if (msg.record[0] == 3 && msg.record[1] == 254) {
              // length==3 && code=="internal log event"
              let v = msg.record[2] * 256 + msg.record[3]; // internal log event type
              struct.struct = { internal: { value: v } };
              us = u._up;
              struct.id = mod._idString;
              struct.type = mod._typeName;
            } else {
              let tt = msg.record.slice(2);
              tt = checkBoxMod(tt); // set "this" instead of FF
              let p = 2;
              while (p < tt.length && tt[p]) p++;
              p++;
              let typNum = 0;
              let shift = 0;
              while (p < tt.length) {
                let t = tt[p++];
                typNum += (t & 127) << shift;
                if (!(t & 128)) break;
                shift += 7;
              }
              struct.id = Uint8toString(tt.slice(0, p));
              struct.type = schema.unitTypesNumbers[typNum];
              struct.struct = decodeAnswerBySchemaFromProtobuf(typNum, protobuf2JSON(tt.slice(p)));
              us = unitIdToUnit(struct.id);
              if (us) us = us.u;
              if (struct.struct?.status || struct.struct?.event) {
                if (us) {
                  if (!us._lastStatusTime || struct.date > us._lastStatusTime) {
                    if (struct.struct?.status) {
                      us.status = struct.struct.status;
                      us._lastStatusTime = struct.date;
                    } else {
                      // event
                      if (!us.status) us.status = {};
                      us.status.unified = struct.struct?.event.stateResult;
                    }
                  }
                }
              }
            }
          } catch {}
        } else {
          struct.id = mod._idString;
          struct.type = mod._typeName;
          struct.struct = { internal: {} };
        }

        let newRec = {
          PPK_N: box._sun,
          PPK_SN: box.config.SN,
          snAdr: snAdr,
          snAdrInd: snAdrInd,
          index: msg.index,
          datePC: Date.now(),
          timePPK: msg.time,
          message: struct,
          record: msg.record,
        };

        l.lastRecord = newRec;
        try {
          let t = decodedRecordShort(newRec);
          modelChanged('logAdded', us, t);
        } catch {}

        const dbTran = localDB.transaction(['LogLast', 'Log'], 'readwrite');
        const dbStLogLast = dbTran.objectStore('LogLast');
        const dbStLog = dbTran.objectStore('Log');
        dbStLog.add(newRec);
        dbStLogLast.put(newRec);
      } else {
        l.full = true; // and do not requery next
      }
    }

    if (l.full || noPullLog || specialTagWhileFwOrCfg) {
      l.lastPollTime = Date.now() + 5000;
    } else {
      l.lastPollTime = Date.now() + 200;
      let ind = l.lastIndex;
      if (!ind) l.confirmed = true;
      if (l.confirmed && ind) ind++;
      sendCommand(u, {
        records: {
          // todo - use recordsMulty
          index: ind,
        },
      });
    }
  } catch {}
}

function eraseLog() {

  initDB();

  const dbTran = localDB.transaction(['LogLast', 'Log'], 'readwrite');
  let dbStLogLast = dbTran.objectStore('LogLast');
  dbStLogLast.clear().onsuccess= function(e){
    console.log(`the cart is clear!`);
};
  const dbTran1 = localDB.transaction(['LogLast', 'Log'], 'readwrite');
  let dbStLog = dbTran1.objectStore('Log');
  dbStLog.onerror= function(e){
    console.log(`error!`);
};
  dbStLog.clear();
  location.replace(location.href);
}

// let newRec = {
//      snAdr: snAdr,
//      snAdrInd: snAdrInd,
//      index: msg.index,
//      datePC: Date.now(),
//      timePPK: msg.time,
//      message: struct,
//      record: msg.record,
//  };
// struct.id = Uint8toString(tt.slice(0, p));
//                             struct.type = schema.unitTypesNumbers[typNum];
//                             struct.struct = decodeAnswerBySchemaFromProtobuf(
//                                 typNum,
//                                 protobuf2JSON(tt.slice(p)),
//                             );
function decodeRecordLang(record) {
  const m = record.message;
  const s = m.struct;
  let decoded = {};
  const ud = schema.unitTypes[m.type];
  if (ud) {
    const ud_ = ud.__;
    decoded.unitType = {
      name: getDict('unitType'),
      value: ud_['Title' + lang],
      description: ud_['Description' + lang],
    };
  }
  let event = new Date();

  var zz = n => ('00' + n).slice(-3);
  event.setTime(record.datePC);
  decoded.datePC = {
    name: getDict('datePC'),
    value: toISOLocal(event),
    milliseconds: zz(event.getMilliseconds()),
  };
  event.setTime(record.message.date);
  decoded.datePPK = {
    name: getDict('datePPK'),
    value: toISOLocal(event), // .toISOString(
  };
  decoded.index = {
    name: getDict('logIndex'),
    value: '' + record.index,
  };

  let un;
  let usn;
  const bn = record.snAdr.split('.')[1];
  const b = sys._subUnits._Box[bn];
  if (b) {
    un = getUnitName(b, 0);
    usn = getUnitName(b, 1);
  } else {
    un = usn = 'Box_' + bn;
  }
  decoded.boxName = {
    name: getDict('boxName'),
    value: usn,
    description: un,
  };
  const u = unitIdToUnit(m.id);
  if (u && u.u) {
    un = getUnitName(u.u, 0);
    usn = getUnitName(u.u, 1);
  } else {
    un = usn = m.id;
  }
  decoded.unitName = {
    name: getDict('unitName'),
    value: usn,
    description: un,
    __descriptor: ud,
    __value: u?.u,
    __name: m.type,
  };
  for (let msgName in s) {
    const md = ud[msgName];
    const msg = s[msgName];
    const md_ = md?.__;
    decoded.message = {
      name: getDict('messageType'),
      value: md_?.['Title' + lang],
      description: md_?.['Description' + lang],
      __descriptor: md,
      __value: msg,
      __name: msgName,
    };
    decoded.parameters = {};
    if (typeof decoded.message.value === 'undefined') {
      decoded.message.value = msgName;
      for (let parName in msg) decoded.parameters[parName] = { name: parName, value: msg[parName] };
    } else
      for (let parName in msg) {
        const pd = md?.[parName];
        if((!pd.repeated) || ((Array.isArray(msg[parName])) && msg[parName].length) ) 
          decoded.parameters[parName] = {
            name: pd?.['Title' + lang],
            value: getParVal(pd, msg[parName]),
            description: pd?.['Description' + lang],
            __descriptor: pd,
            __value: msg[parName],
            __name: parName,
          };
      }
    break;
  }
  return decoded;
}

function toISOLocal(d) {
  var z = n => ('0' + n).slice(-2);
  // var zz = n => ('00' + n).slice(-3);
  // var off = d.getTimezoneOffset();
  // var sign = off > 0 ? '-' : '+';
  // off = Math.abs(off);

  return (
    d.getFullYear() +
    '.' +
    z(d.getMonth() + 1) +
    '.' +
    z(d.getDate()) +
    ' ' +
    z(d.getHours()) +
    ':' +
    z(d.getMinutes()) +
    ':' +
    z(d.getSeconds()) //+
    // '.' +
    // zz(d.getMilliseconds()) +
    // sign +
    // z((off / 60) | 0) +
    // ':' +
    // z(off % 60)
  );
}

function decodedRecord2text(dec, lf) {
  if (!lf) lf = '<br/>';
  let text = lf;
  for (let a in dec)
    if (a != 'parameters') {
      text += dec[a].name + ' = ' + dec[a].value;
      if (a == 'datePC') text += '.' + dec[a].milliseconds;
      if (dec[a].description) text += '   (' + dec[a].description + ')';
      text += lf;
    }
  text += '  === ' + lf;
  const p = dec.parameters;
  for (let a in p) {
    text += p[a].name + ' = ' + p[a].value;
    if (p[a].description) text += '   (' + p[a].description + ')';
    text += lf;
  }
  text += lf;
  text += lf;
  return text;
}

function decodeRecordLong(record, lf) {
  return decodedRecord2text(decodeRecordLang(record), lf);
}

function decodedRecordShort(record) {
  const dec = decodeRecordLang(record);
  let pos = record.snAdrInd.indexOf('.');
  let adInd = record.snAdrInd.substring(pos + 1);

  let text = dec.datePPK.value.substring(5) + ': ' + adInd;
  if (dec.parameters?.ref?.__value) text += "'";

  let s = dec?.unitName?.value;
  if (s) {
    pos = s.indexOf('.');
    s = s.substring(pos + 1);
  }
  text += ' : ' + s;
  if (dec.message) {
    text += ' | ' + dec.message.value;
    // text += dec[a].name + ': ' + dec[a].value;
    // text += ' | ';
  }

  const p = dec.parameters;
  for (let a in p) {
    // if (p[a]?.__descriptor?.repeated) continue;
    if (p[a]?.__descriptor?.maxValue == 1 && !p[a]?.__value) continue;
    text += ' | ' + p[a].name + ': ' + p[a].value;
  }

  return text;
}

/* state machine for full PPK reflashing
stepMultipleFirmwares = called on each reflash end

*/

var multipleFirmwares;
// expected struct with smth like
// {"rebooter":{"6":{"1":{bin:binImage,name:filename,version:9999}, "2":{bin:binfile...}...}, "7":{}}
// {"application":...}               === same as rebooter
// binary = uint8array
// state mashine will store there also
//  .curPPKsun
//  .curModSun
//  .timeOut
//  .stage
//  .curModBoardVersion
//  .curModule == ref to unit
//  .curBoot == ref to unit
//  .curBin == ref to {bin:binImage,name:filename,version:9999}

var reportMultipleFirmwares = '';
function logMF(str, color) {
  systemLog(false, str);
  if (multipleFirmwares) {
    let now = new Date();
    let newstr = now.toLocaleString() + ': ' + str + '\n';
    if (color) newstr = ' <p style="color: ' + color + '; font-weight: 900" >' + newstr + '</p>';
    reportMultipleFirmwares = newstr + reportMultipleFirmwares;
  }
  modelChanged('boot');
}

function endMF() {
  logMF('done multiple firmwares', 'green');
  // alert(reportMultipleFirmwares);
  multipleFirmwares = undefined;
  specialTagWhileFwOrCfg = false;
}

function startMultipleFirmwares(mF) {
  multiUpdateErrors = '';
  multipleFirmwares = mF;
  fwUpdateTimeout = Date.now();
  multipleFirmwares.stage = undefined;
  reportMultipleFirmwares = '';
  logMF('all PPK reflashing started');
  stepMultipleFirmwares();
}

// result = string
function stepMultipleFirmwares(result, color) {
  if (!multipleFirmwares) return;
  specialTagWhileFwOrCfg = true;
  fwUpdateTimeout = Date.now();
  let cb = multipleFirmwares.curBin;
  if (cb) cb = ' with ' + cb.name;
  else cb = '';
  if (result) {
    switch (multipleFirmwares.stage) {
      case 'askModule':
      case 'askBoot':
        logMF('querying ' + multipleFirmwares.curPPKsun + '.' + multipleFirmwares.curModSun + ' : ' + result, color);
        break;
      default:
        logMF(
          'reflashing ' + multipleFirmwares.curPPKsun + '.' + multipleFirmwares.curModSun + cb + ' : ' + result,
          color,
        );
    }
  }

  multipleFirmwares.curModule = undefined;

  function getNext(u, suName, sun) {
    let max = u._desc.__.subUnits[suName]?.qty;
    if (!max) return 0;
    if (!u._subUnits[suName]) return 0;
    let th = suName == '_Box' ? thisBox : thisBox == u._sun ? thisModule : 0; // nonzero if looking where THIS was.
    while (true) {
      sun++;
      if (th) {
        if (sun > max) sun = 1;
        if (sun == th) return 0;
      } else if (sun > max) return 0;
      if (u._subUnits[suName][sun]) return sun;
    }
  }

  while (multipleFirmwares && !multipleFirmwares.curModule)
    switch (multipleFirmwares.stage) {
      case 'askModule':
      case 'askBoot':
        multipleFirmwares.stage = 'nextModule'; // in case cannot or should not
        break;
      // return;
      case 'writeBoot':
        logMF('boot reflash done on ' + multipleFirmwares.curPPKsun + ':' + multipleFirmwares.curModSun);
        multipleFirmwares.stage = 'waitReBoot'; // in case cannot or should not
        multipleFirmwares.curModule =
          sys._subUnits._Box[multipleFirmwares.curPPKsun]._subUnits.Module[multipleFirmwares.curModSun];
        return;
      default:
        // seek for first ppk:mod
        multipleFirmwares.curPPKsun = thisBox;
        multipleFirmwares.curModSun = thisModule;
        if (!sys._subUnits._Box[multipleFirmwares.curPPKsun]) multipleFirmwares.stage = 'nextPPK';
        else if (!sys._subUnits._Box[multipleFirmwares.curPPKsun]._subUnits.Module[multipleFirmwares.curModSun])
          multipleFirmwares.stage = 'nextModule';
        else
          multipleFirmwares.curModule =
            sys._subUnits._Box[multipleFirmwares.curPPKsun]._subUnits.Module[multipleFirmwares.curModSun];
        break;
      case 'writeApp':
        logMF('application reflash done on ' + multipleFirmwares.curPPKsun + ':' + multipleFirmwares.curModSun);
        multipleFirmwares.stage = 'nextModule';
        break; // will while-switch once more
      case 'nextPPK':
        multipleFirmwares.curPPKsun = getNext(sys, '_Box', multipleFirmwares.curPPKsun);
        if (!multipleFirmwares.curPPKsun) {
          endMF();
          break;
        }
        multipleFirmwares.curModSun = 0;
        multipleFirmwares.stage = 'nextModule';
      // no break
      case 'waitReBoot':
      // here only on timeout boot query
      case 'nextModule':
        multipleFirmwares.curModSun = getNext(
          sys._subUnits._Box[multipleFirmwares.curPPKsun],
          'Module',
          multipleFirmwares.curModSun,
        );
        if (!multipleFirmwares.curModSun) multipleFirmwares.stage = 'nextPPK';
        else {
          multipleFirmwares.curModule =
            sys._subUnits._Box[multipleFirmwares.curPPKsun]._subUnits.Module[multipleFirmwares.curModSun];
        }
        break;
      case 'writeBoot':
        multipleFirmwares.stage = 'askModule';
        logMF('querying Application version ' + multipleFirmwares.curPPKsun + ':' + multipleFirmwares.curModSun);
        return;
    }
  if (!multipleFirmwares) {
    if (multiUpdateErrors.length) {
      alert(multiUpdateErrors);
    }
    return; // no more PPK\
  }
  multipleFirmwares.curBoot = multipleFirmwares.curModule._subUnits.Boot[1];
  multipleFirmwares.stage = 'askBoot';
  logMF('querying Boot version ' + multipleFirmwares.curPPKsun + ':' + multipleFirmwares.curModSun);
}

var multiUpdateErrors = '';

// called before processPackage. return true if no further processing needed (never in this proc)
function checkMultiUpdate(a) {
  while (multipleFirmwares) {
    let abox = a.from.box;
    if (abox == 0xff) abox = thisBox;
    let amod = a.from.mod;
    if (amod == 0xff) amod = thisModule;
    if (abox != multipleFirmwares.curPPKsun || amod != multipleFirmwares.curModSun) return false; // dont care
    switch (multipleFirmwares.stage) {
      case 'waitReBoot':
        if (fwUpdateTimeout + 7000 > Date.now()) return; // just wait 7 seconds, then go to app reflash
        multipleFirmwares.stage = 'askModule';
        a.from.unit = 1; // mimic module answer
        a.cmd.info = { appVersion: 0 };
        break; // will while-switch once more
      default:
        return false;
      case 'askBoot':
        if (a.from.unit) return false; // not Boot
        if (!a.cmd.status) return false; // dont care
        multipleFirmwares.curModBoardVersion = a.cmd.status.boardVersion;
        multipleFirmwares.curModBootVersion = a.cmd.status.bootVersion;
        multipleFirmwares.curModAppVersion = 0; // default to update app also
        multipleFirmwares.curModBoardType = a.cmd.status.boardType;
        multipleFirmwares.curBin =
          multipleFirmwares.rebooter[multipleFirmwares.curModBoardVersion][a.cmd.status.boardType];
        logMF(
          'found board version ' +
            a.cmd.status.boardVersion +
            ', boot version ' +
            a.cmd.status.bootVersion +
            ', module type ' +
            a.cmd.status.boardType,
        );

        {
          let noUpdateBoot = false;
          let color = undefined;
          if (!multipleFirmwares.curBin.bin) {
            color = 'Purple';
            noUpdateBoot =
              'no reboot firmware found for ' +
              multipleFirmwares.curPPKsun +
              ':' +
              multipleFirmwares.curModSun +
              ' for board version ' +
              multipleFirmwares.curModBoardVersion +
              ', module board type ' +
              a.cmd.status.boardType;

            multiUpdateErrors += noUpdateBoot + '\r\n';
          } else if (multipleFirmwares.curModBootVersion == multipleFirmwares.curBin?.version) {
            noUpdateBoot =
              'BOOT at ' +
              multipleFirmwares.curPPKsun +
              ':' +
              multipleFirmwares.curModSun +
              ' already has version ' +
              multipleFirmwares.curBin.version;
          }

          if (noUpdateBoot) {
            logMF(noUpdateBoot, color);
            logMF('quering module version @ ' + multipleFirmwares.curPPKsun + ':' + multipleFirmwares.curModSun);
            multipleFirmwares.stage = 'askModule';
            if (a.cmd.status.fault) {
              // bad app - fall through to update app
              // simulate answer from module
              a.from.unit = 1;
              a.id[2] = 0;
              a.cmd.info = { appVersion: 0 };
            } else {
              multipleFirmwares.timeOut = Date.now();
              return false;
            }
          } else {
            // will rebooter it and sure need reApp afterwards
            logMF(
              'updating BOOT at ' +
                multipleFirmwares.curPPKsun +
                ':' +
                multipleFirmwares.curModSun +
                ' from [' +
                multipleFirmwares.curBin.name +
                ']',
            );
            updateFirmware(getUnitID(multipleFirmwares.curBoot), multipleFirmwares.curBin.bin);
            fw2updateUnit = multipleFirmwares.curBoot;
            multipleFirmwares.stage = 'writeBoot';
            return false;
          }
        }
      // no break = fall through if sure need reflash APP
      case 'askModule': // only if no boot update was
        if (!a.from.unit) return false; // not App
        if (!a.cmd.info) return false;
        // box,mod, and right after zero - no subunits chain
        // ID=module
        multipleFirmwares.curModAppVersion = a.cmd.info.appVersion;
        multipleFirmwares.curBin =
          multipleFirmwares.application[multipleFirmwares.curModBoardVersion][multipleFirmwares.curModBoardType];
        logMF('found application version ' + a.cmd.info.appVersion);
        {
          let noUpdateApp = false;
          let color = undefined;
          if (!multipleFirmwares.curBin.bin) {
            noUpdateApp =
              'no application firmware found for ' +
              multipleFirmwares.curPPKsun +
              ':' +
              multipleFirmwares.curModSun +
              ' for board version ' +
              multipleFirmwares.curModBoardVersion;
            color = 'Purple';
            multiUpdateErrors += noUpdateApp + '\r\n';
          } else if (multipleFirmwares.curModAppVersion == multipleFirmwares.curBin.version) {
            noUpdateApp =
              'application at ' +
              multipleFirmwares.curPPKsun +
              ':' +
              multipleFirmwares.curModSun +
              ' already has version ' +
              multipleFirmwares.curModAppVersion;
          }
          if (noUpdateApp) {
            stepMultipleFirmwares(noUpdateApp, color);
            return false;
          }
          // try to update app
          logMF(
            'updating application at ' +
              multipleFirmwares.curPPKsun +
              ':' +
              multipleFirmwares.curModSun +
              ' from [' +
              multipleFirmwares.curBin.name +
              ']',
          );
          multipleFirmwares.stage = 'writeApp';
          updateFirmware(getUnitID(multipleFirmwares.curBoot), multipleFirmwares.curBin.bin);
          fw2updateUnit = multipleFirmwares.curBoot;
          return false;
        }
    }
  }
  return false;
}

function buildMultiUpdate() {
  // called before buildUpdate. return request if needed
  switch (multipleFirmwares?.stage) {
    case 'waitReBoot':
    case 'askBoot':
      return fillRequest(
        {
          id: [multipleFirmwares.curModSun, multipleFirmwares.curPPKsun],
          unitType: 'Boot',
          cmd: {
            status: {},
          },
        },
        undefined,
        0xff,
        true,
      ); // to create this box
    case 'askModule':
      return fillRequest(
        {
          id: getUnitID(multipleFirmwares.curModule),
          unitType: 'Module',
          cmd: {
            info: {},
          },
        },
        undefined,
        0xff,
        true,
      );
    default:
      return undefined;
  }
}


function saveTextToFile(text, fileName, fileType) {
  let link = document.createElement('a');
  link.download = fileName;
  if (!fileType) fileType = 'text/plain';
  var blob = new Blob([text], {
    type: fileType,
  });
  link.href = window.URL.createObjectURL(blob);
  // link.href = 'data:'+fileType+',' + encodeURI(text);
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

function exportLog(qqty) {
  qqty = Number(qqty);
  if (typeof qqty !== 'number' || !qqty) qqty = 1000;
  // todo limit time range
  logText = '';
  let time = Date.now();
  // now read all messages to avoid requering them
  const dbTran = localDB.transaction('Log');
  const dbSt = dbTran.objectStore('Log');
  const dbIndex = dbSt.index('datePC');
  let dbCurReq;
  let qty = 0;
  dbCurReq = dbIndex.openCursor(IDBKeyRange.upperBound(time), 'prev');
  dbCurReq.onsuccess = event => {
    const cursor = event.target.result;
    if (cursor && cursor.value && qty++ < qqty) {
      logText += decodedRecordShort(cursor.value, '\r\n') + '\r\n';
      cursor.continue();
    } else {
      saveTextToFile(logText, 'log.txt');
    }
  };
}

function sendBinary(unit, message) {
  // unit below module, message -
  if (!ArrayBuffer.isView(message)) return;
  try {
    while (unit && unit._sut != 'Module') unit = unit._up;
    if (unit._sut != 'Module') return;
    let ab = new ArrayBuffer(300);
    let data = new Uint8Array(ab);
    data[0] = 0x00; // tag
    data[1] = 0x26; // category
    data[2] = unit._up._sun; // box
    data[3] = unit._sun; // module
    for (let p = 0; p < message.length; p++) data[4 + p] = message[p];
    postRequests.push(data.buffer.slice(0, message.length + 4));
  } catch {}
}

function sendBinaryRequestAllAU(al) {
  let ab = new ArrayBuffer(2);
  let data = new Uint8Array(ab);
  data[0] = 0x01; // BinaryRequestAllAU
  data[1] = al._sun - 1; // AL number, 0/1;
  sendBinary(al, data);
}

// function processBinaryAnswer(box, mod, arr) {
//   try {
//     let m = sys._subUnits._Box[box]._subUnits.Module[mod];
//     switch (arr[0]) {
//       case 1: // mod 3 only, erquest all AU state
//         if (m._sun != 3) return; // todo - check module type compatible
//         processMultipleAU(m, arr);
//         break;
//     }
//   } catch {}
// }

function processMultipleAU(m, arr) {
  if (arr[1] > 1) return;
  let al = m._subUnits.AL[arr[1] + 1];
  for (let sun = 1; sun < 256; sun++) {
    let byteInd = (sun - 1) * 7;
    let arrInd = 2 + (byteInd >> 3);
    let shft = byteInd & 7;
    let stat = arr[arrInd] + arr[arrInd + 1] * 256;
    stat = (stat >> shft) & 0x7f;
    let AU = al._subUnits.AU[sun];
    if (AU) {
      if (AU._fastStatus !== stat) {
        AU._fastStatus = stat; // todo describe in the schema
        modelChanged('changed', AU);
      }
    } else {
      if (stat & 3) {
        // todo - add unit and/or request its config - NOTE: now cannot ask config if I dont know the real type. use compatiblr type or "any"
      }
    }
  }
}

// processes single unit
function moveUnit(unit, newParent) {
  try {
    // stupid checks
    if (!unit || !newParent || unit?._up == newParent) {
      systemLog(false, 'bad parameters');
      return;
    }
    // first check compatibility
    // now - only AU to new AL === OR === SK to new mod2
    if (
      (unit._sut != 'AU' || newParent._sut != 'AL') &&
      (unit._sut != 'SK' || newParent._sut != 'Module' || newParent._sun != 2)
    ) {
      systemLog(false, 'only can move AU to new AL or SK to new MOD2');
      return;
    }

    let newSun = unit._sun; // try to save SUN
    if (newParent._subUnits[unit._sut][newSun]) {
      newSun = 255;
      while (newSun && newParent._subUnits[unit._sut][newSun]) newSun--;
    }
    if (!newSun) {
      systemLog(false, 'no spare subunit numbers in new parent');
      return;
    }

    let oldIdString = stripId(unit._idString);

    delete unit._up._subUnits[unit._sut][unit._sun];
    unit._sun = newSun;
    newParent._subUnits[unit._sut][unit._sun] = unit;
    unit._up = newParent;
    unit._idString = shortID(unit);
    let newIdString = stripId(unit._idString);
    delete sys._serials[unit._hidString];
    hidString(unit);

    // todo - check that links are on the same PPK. now one
    // let oldPPK = unit;
    // while (oldPPK && oldPPK._sut !='_Box') oldPPK=oldPPK._up;
    // let newPPK = newParent._up._up;
    // if(oldPPK==newPPK) {
    updateLinks(u, oldIdString, newIdString);
    // } else {

    // }
  } catch {
    systemLog(false, 'unknown catched error');
  }
}

// force - auto remove old TS for this unit
function createTS(unit, areaNum, TStype, force, PPK) {
  try {
    // stupid checks
    if (!unit || !areaNum || typeof areaNum != 'number' || areaNum > 255) {
      systemLog(false, 'bad parameters');
      return;
    }

    if (unit?.config?.backLink) {
      if (force) {
        systemLog(false, 'old TS removed: ' + unit?.config?.backLink);
        deleteUnit(unitIdToUnit2(unit.config.backLink));
        delete unit.config.backLink;
      } else {
        systemLog(false, 'TS already present: ' + unit?.config?.backLink);
        return;
      }
    }

    if (!TStype) {
      if (unit._desc.__.defaultInput) {
        TStype = unit._desc.__.defaultInput;
      } else if (unit._desc.__.defaultOutput) TStype = unit._desc.__.defaultOutput;
    }

    let m = unit;
    while (m && m._typeName !== '_Box') m = m._up;

    if(PPK) { 
      if(m && m._sun != PPK) {
        if(unit._sut != 'Area' && unit._sut != 'LED') {
          systemLog(false, 'cannot create cross-PPK link other then Area or Led ' + PPK);
          return;    
        }
      }
      m=sys._subUnits._Box[PPK]; }
    if(!m) {
      systemLog(false, 'PPK number bad: ' + PPK);
      return;
    }

    let m1 = m._subUnits.Module[1];
    let id = shortID(unit);
    let tss;
    let sutName;
    if (unitTypeCompatible(TStype, 'InputLink')) {
      tss = m1._subUnits.InputLink;
      sutName = 'InputLink';
    } else if (unitTypeCompatible(TStype, 'OutputLink')) {
      tss = m1._subUnits.OutputLink;
      sutName = 'OutputLink';
    } else {
      systemLog(false, 'TS type not usable: ' + TStype);
      return;
    }
    let nin;
    for (let n in tss)
      if (tss[n].config && tss[n].config.unitID && tss[n].config.unitID == id) {
        nin = tss[n];
        break;
      }

    if (!nin) {
      nin = addUnitToSut(m1, sutName);
      if (nin) {
        if (!nin.config) nin.config = {};
        nin.config.unitID = id;
        fillUnit(nin, TStype);
        // nin._typeName = TStype;
        nin.config.areaNum = areaNum;
        unit.config.backLink = nin._idString;
        if (!m1._subUnits.Area[areaNum]) createSubunit(m1, 'Area', areaNum);
        m1._subUnits.Area[areaNum]._subUnits[sutName][nin._sun] = nin;
      }
    }
    systemLog(false, 'created');
  } catch {
    systemLog(false, 'unknown catched error');
  }
}

// processes single unit
function pasteUnitCfg(unit, cfg, sourceTypeName) {
  try {
    if (!unit.config) unit.config = {};
    for (let par in cfg) {
      let parameterOK = true;
      if (sourceTypeName) {
        let srcOrigin = findCfgParameterOrigin(par, sourceTypeName);
        parameterOK = srcOrigin && srcOrigin == findCfgParameterOrigin(par, unit._typeName);
      }
      if (parameterOK && unit._desc.config[par] && !unit._desc.config[par]?.hardwareID)
        setValueInMsg(unit, 'config', par, cfg[par]);
    }
  } catch {
    systemLog(false, 'unknown catched error');
  }
}

function checkValue(msg, pd, pn) {
  if (!msg[pn] && typeof msg[pn] != 'number') {
    if (pd.defaultValue) msg[pn] = pd.defaultValue;
    else msg[pn] = 0;
  }
  if (pd.maxValue < msg[pn]) msg[pn] = Number(pd.maxValue);
}

var oscilloscopeData = [];
var oscilloscopeModule;
var oscilloscopeResult = { data32: 0 };
var oscilloscopeFilled = 0; // 3000 bytes will be 12 binary packets per 250 bytes;
function receiveOscilloscopeData(unit) {
  if (unit._typeName != 'AL2module') return;
  oscilloscopeFilled = 0xfff; // now request 12 blocks of data. done when all bits are cleared
  countDownPeriodicRequest = 100;
  deltaTimePeriodicRequest = 200;
  // force one request
  sendBinaryRequestOscill(unit);
}

function sendBinaryRequestOscill(mod) {
  if (mod?._typeName != 'AL2module') return;
  lastTimePeriodicRequest = deltaTimePeriodicRequest + Date.now();
  if (!mod.oscilloscope) mod.oscilloscope = {};
  if (mod.oscilloscope.doCollect && !mod.oscilloscope.done) {
    console.log('request');
    mod.oscilloscope.control = 0;
    exec1Command(mod, 'oscilloscope');
    return;
  } else if (!(oscilloscopeFilled & 0xfff)) oscilloscopeFilled = 0;
  for (let part = 0; part < 12; part++)
    if (oscilloscopeFilled & (1 << part)) {
      console.log('query ' + part);
      let ab = new ArrayBuffer(2);
      let data = new Uint8Array(ab);
      data[0] = 0x02; // BinaryRequestOscilloscopeResult
      data[1] = part;
      sendBinary(mod, data);
      break;
    }
}

function processBinaryAnswer(box, mod, arr) {
  try {
    let m = sys._subUnits._Box[box]._subUnits.Module[mod];
    switch (arr[0]) {
      case 1: // mod 3 only, erquest all AU state
        if (m._sun != 3) return; // todo - check module type compatible
        processMultipleAU(m, arr);
        break;
      case 2: // mod 3 only, oscilloscope data
        if (m._sun != 3) return; // todo - check module type compatible
        processOscilloscopeData(m, arr);
        break;
    }
  } catch {}
}

function processOscilloscopeData(module, arr) {
  if (!oscilloscopeModule) return;
  if (arr[1] > 11) return;
  oscilloscopeFilled &= ~(1 << arr[1]);
  let dataPtr = arr[1] * 125;
  for (let inData = 2; inData < 252; ) {
    let u16 = arr[inData++];
    u16 += arr[inData++] << 8;
    oscilloscopeData[dataPtr++] = u16;
  }
  console.log('got ' + arr[1]);

  switch (arr[1]) {
    case 0:
      oscilloscopeResult.SN = arr[252] + arr[253] * 256 + arr[254] * 256 * 256;
      oscilloscopeResult.adrFound = arr[252];
      oscilloscopeResult.alarmFound = !(arr[253] & 1);
      break;
    case 1:
      oscilloscopeResult.type = arr[252];
      oscilloscopeResult.mode = arr[253];
      oscilloscopeResult.address = arr[254];
    case 2:
      oscilloscopeResult.command = arr[252];
      oscilloscopeResult.data32 = (oscilloscopeResult.data32 & 0xffff0000) + arr[253] + arr[254] * 256;
      break;
    case 3:
      oscilloscopeResult.data32 =
        (oscilloscopeResult.data32 & 0xffff) + arr[252] * 256 * 256 + arr[253] * 256 * 256 * 256;
      oscilloscopeResult.errors = arr[254];
      break;
    case 4:
      oscilloscopeResult.previousDecision = arr[252];
      oscilloscopeResult.bitErrors = arr[253];
      break;
  }
  if (oscilloscopeFilled) sendBinaryRequestOscill(module);
  else {
    modelChanged('oscilloscopeReady', module);
    oscilloscopeModule = undefined;
    countDownPeriodicRequest = 0;
  }
}

var deltaTimePeriodicRequest = 200; // millisec if I'm not mistaken
var lastTimePeriodicRequest = 0;
var countDownPeriodicRequest = 0;
var recurcive;
function checkPeriodicRequest() {
  if (recurcive) return;
  recurcive = true;
  let curDate = Date.now();
  if (countDownPeriodicRequest && curDate > lastTimePeriodicRequest) {
    countDownPeriodicRequest--;
    lastTimePeriodicRequest = deltaTimePeriodicRequest + curDate;

    if (oscilloscopeFilled) sendBinaryRequestOscill(oscilloscopeModule);
    else {
      // here some other periodic
    }
  }
  recurcive = false;
}

function runOneOscilloscope(unit, msg) {
  if (unit?._typeName != 'AL2module' || msg != 'oscilloscope')
    systemLog(true, 'Error calling ' + msg + ' %% ' + unit._idString);
  else {
    if (!unit.oscilloscope) unit.oscilloscope = {};
    oscilloscopeModule = unit;
    countDownPeriodicRequest = 60;
    unit.oscilloscope.doCollect = 30;
    deltaTimePeriodicRequest = unit._up._sun == thisBox ? 200 : 700;
    oscilloscopeFilled = 0x1000; // now request 12 blocks of data. done when all bits are cleared
    unit.oscilloscope.control = 1;
    unit.oscilloscope.done = 0;
    exec1Command(unit, 'oscilloscope');
    modelChanged('changed', unit, { oscilloscope: { doCollect: 0, done: 1 } });
  }
}

function findLogMessage(snadrind, callBack) {
  const dbTran = localDB.transaction('Log');
  const dbSt = dbTran.objectStore('Log');
  const dbIndex = dbSt.index('snAdrInd');
  let dbCurReq = dbIndex.openCursor(snadrind);
  dbCurReq.onsuccess = event => {
    const cursor = event.target.result;
    if (cursor && cursor.value) callBack(cursor.value);
  };
}

function updateSerialsFromCsvFile(al, e) {
  let file = e.target.files[0];
  let reader = new FileReader();
  reader.onload = function () {
    const splitLines = reader.result.split(/\r?\n/);
    for (let line of splitLines) {
      const adrsn = line.split(/;|,/);
      if (adrsn.length > 1) {
        const adr = Number(adrsn[0]);
        const newsn = Number(adrsn[1]);
        if (adr > 0 && newsn > 0 && adr < 256 && newsn < 0x1000000) {
          // sure NaN also false
          if (!al._subUnits.AU[adr]) createSubunit(al, 'AU', adr); // , 'AxDPI'
          if (!al._subUnits.AU[adr].config) al._subUnits.AU[adr].config = {};
          if (al._subUnits.AU[adr].config.SN != newsn) {
            systemLog(
              false,
              'SN for AU#' + adr + ' changed from [' + al._subUnits.AU[adr].config.SN + '] to [' + newsn + ']',
            );
            al._subUnits.AU[adr].config.SN = newsn;
          }
        }
      }
    }
    modelChanged('changed', al, 'AU');
  };
  reader.onerror = function () {
    systemLog(true, reader.error);
  };
  reader.readAsText(file);
}

function readSerialsFromFile(al) {
  // AL unit to correct serials in AU subunits
  if (al && al._subUnits.AU) {
    let link = document.createElement('input');
    link.type = 'file';
    link.onchange = e => updateSerialsFromCsvFile(al, e);
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  } else systemLog(false, 'not a AL - cannot read SN file');
}

function exportCfgCsv(ppk) {
  if (!ppk || ppk?._typeName != '_Box') {
    alert('not Box for export CFG');
    return;
  } //  	EF BB BF
  let txt = '\ufeffTStype;number;area;name;AL;address;type;serial;subtype;subnumber;AUname;\n\r';
  const m1 = ppk._subUnits.Module[1];
  let ars = m1._subUnits.Area;
  let ppksn = ppk?.config?.SN;
  if(!ppksn) ppksn=0;
  let ppkn = ppk?.config?.name;
  txt += ';;;;;;;;;;;\n\r';
  txt += 'PPK;' + ppk._sun + ';;' + ppkn + ';;;;'+ppksn+';;;;\n\r';
  txt += ';;;;;;;;;;;\n\r';

  for (let ar in ars) {
    let a = ars[ar];
    let up = a.config.areaNum;
    if (!up) up = '';
    let nm = a.config.name;
    if (!nm) nm = '';
    txt += a._typeName+';' + a._sun + ';' + up + ';' + nm + ';;;;;;;;\n\r';
  }

  for (sut of ['InputLink', 'OutputLink']) {

    txt += ';;;;;;;;;;;\n\r';

    let tss = m1._subUnits[sut];
    for (let tsn in tss) {
      let ts = tss[tsn];
      if (ts) {
        let upar = ts?.config?.areaNum;
        if (!upar) upar = 0;
        let nm = ts?.config?.name;
        if (!nm) nm = '';

        txt += ts._typeName + ';' + ts._sun + ';' + upar + ';' + nm + ';';

        let uid = ts.config.unitID;
        let hard = unitIdToUnit2(uid);
        if(hard && hard._sut == "_System") hard=undefined;
        if (hard) {
          let up = hard._up;
          let au = hard;
          if (au._sut != 'AU' && au._sut != 'RelayGroup') au = up;
          if (au._sut == 'AU') {
            let sn = au?.config?.SN;
            if (!sn) sn = '';
            txt += au._up._sun + ';' + au._sun + ';' + au._typeName + ';' + sn + ';';
            if (au != hard) {
              // subunit
              txt += hard._sut + ';' + hard._sun + ';';
            } else 
              txt += ';;';
          } else if (au._sut == 'RelayGroup') {
            txt += '' + ';' + au._sun + ';' + au._typeName+';;;;';
          } else 
            txt += ';;;;;;';
        }
        txt += ';\n\r';
      }
    }
  }
  
  txt += ';;;;;;;;;;;\n\r';

  const m3 = ppk._subUnits.Module[3];
  let als = m3._subUnits.AL;
  const rh = schema.unitTypes.Relay.__.allRealHeirs;
  for (let aln in als) {
    const al=als[aln];
    const aus = al._subUnits.AU;
    for (let aun in aus) {
      const au = aus[aun];
      const ausubs = au._subUnits;
      if(ausubs) {
        for (ausn of rh) {    // ['Relay','RelayIsm4','RelayIsm5',]
          const rls = ausubs[ausn];
          if(rls) {
            let sn = au?.config?.SN;
            if (!sn) sn = '';
            let nm=au?.config?.name;
            if (!nm) nm = '';      
            for (let rln in rls) {
              const rl = rls[rln];
              if(rl && rl.config && rl.config.group) {
                let rg=m3._subUnits.RelayGroup[rl.config.group];
                let rgn = rg?.config?.name;
                if(!rgn) rgn='';
                txt += 'RelayGroup;' + rl.config.group+';;'+rgn+';' + aln + ';' + aun + ';' + au._typeName + ';'+sn+';'+ausn+';'+rln+';'+nm+';\n\r';
              }
            }
          }
        }
      }
    }
  }

  txt += ';;;;;;;;;;;\n\r';

  const rgs = m3._subUnits.RelayGroup;
  for (let rgn in rgs) {
    const rg = rgs[rgn];
    let nm=rg?.config?.name;
    if (!nm) nm = '';
    txt += 'RelayGroup;'+rg._sun+';;'+nm+';;;;;;\n\r';
  }

  txt += ';;;;;;;;;;;\n\r';

  for (let aln in als) {
    const al=als[aln];
    const aus = al._subUnits.AU;
    for (let aun in aus) {
      const au = aus[aun];
      let sn = au?.config?.SN;
      if (!sn) sn = '';
      let nm=au?.config?.name;
      if (!nm) nm = '';      
          txt += 'AU;;;;' + aln + ';' + aun + ';' + au._typeName + ';'+sn+';;;'+nm+';\n\r';
    }
  }

  saveTextToFile(txt, 'cfg' + ppk._sun + '.csv');
}

function importCfgCsv(ppk, win1251) {
  if (!ppk || ppk?._typeName != '_Box') {
    alert('not Box for export CFG');
    return;
  }
  let link = document.createElement('input');
  link.type = 'file';
  link.onchange = e => updateCfgFromCSV(e, ppk, win1251);
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

function updateCfgFromCSV(e, ppk, win1251) {
  let file = e.target.files[0];
  let reader = new FileReader();
  reader.onload = function () {
    if(win1251) {const tt = new Uint8Array(reader.result); 
      const win1251decoder = new TextDecoder("windows-1251");
      const txt = win1251decoder.decode(tt);
      CSVtoCFG(txt, ppk);
    } else
    CSVtoCFG(reader.result, ppk, win1251);
  };
  reader.onerror = function () {
    systemLog(true, reader.error);
  };
  if (win1251)
    reader.readAsArrayBuffer(file);
  else   
    reader.readAsText(file);
}

function CSVtoCFG(txt, ppk) {
  ppk._configured=true;
  const splitLines = txt.split(/\r?\n/);
  const m = ppk?._subUnits.Module[1];
  const ms = m?._subUnits;
  const ma = ppk?._subUnits.Module[3];
  const mas = ma?._subUnits;
  if (!(ms && mas)) {
    systemLog(false, 'bad PPK');
    return;
  }
  for (let line of splitLines) {
    if (line[0] == '\r') line = line.substring(1);
    // first line with headers ignored
    if(!line.length) continue;
    let par = line.split(/;|,/);
    // if (par.length <= 1) {
    //   par= line.split(/,/);
    // } 
    if (par.length > 9) { // now min 10 columns !!!
      let sut = 'InputLink';
      switch (par[0]) {
        case 'TStype':
          break;
        case 'PPK':
          {
            let sun = Number(par[1]);
            if(sun) {
              let AUsn=Number(par[7]);
              if(!AUsn) AUsn=0;
              ppk = checkSubUnit(sys, sun, '_Box',  '_Box',{});
              if(ppk && ppk.config) ppk.config.SN = AUsn;
            }
          }
          break;
        case '': // special case - not yet known
        default:
          systemLog(false, 'bad line: ' + line);
          break;
        case 'FireAreaExt':
        case 'FireArea':
          {
            let sun = Number(par[1]);
            if (sun) {
              let a = checkSubUnit(m, sun, 'Area',  par[0],{});
             // let a = ms.Area[sun];
              if (!a) a = createSubunit(m, 'Area', sun, par[0]);
              if (!a.config) a.config = {};
              if(par[3].length) {
                if(!a.config)
                  a.config={};
                if(par[3].length)                a.config.name = par[3];
              }
              a._configured=true;
              let arn=Number(par[2]);
              if(arn && arn < 256) {
                a.config.areaNum = arn;
                if(!ms.Area[arn]) createSubunit(m,'Area',arn);
              }
            }
          }
          break;
        case 'RelayGroup':
          try
          {
            let sun = Number(par[1]);
            if (sun && sun < 256) {
              let rg = checkSubUnit(ma, sun, 'RelayGroup', 'RelayGroup',{});
              rg._configured=true;
              if(par[3].length) {
                if(!rg.config)
                  rg.config={};
                rg.config.name = par[3];
              }
              let alnum = Number(par[4]);
              let AUnum = Number(par[5]);
              let AUtype=par[6];
              let AUsn=Number(par[7]);
              let AUname=(par[10]);            
              if(!AUsn) AUsn=0;
              let AUsubtype=par[8];
              if( schema.unitTypes.Relay.__.allRealHeirs.includes(AUsubtype)) {
                let AUsubnum=Number(par[9]);            
                let AUid; // undef but may be will be defined
                let au; // undef until created
                if(alnum && alnum<3 && AUnum && AUnum<256) {
                  const al=mas.AL[alnum];
                  const aus=al._subUnits.AU;
                  au = checkSubUnit(al, AUnum, 'AU', AUtype,{});
                  if(au) {
                    au._configured=true;
                    if (!au.config) au.config = {};
                    if(AUsn) au.config.SN = AUsn;
                    if(AUname.length) au.config.name=AUname;
                    let AUsubs=au._subUnits[AUsubtype];
                    if(AUsubs){ 
                      let aus = AUsubs[AUsubnum];
                      if(aus) {
                        if(!aus.config) aus.config={};
                        aus.config.group = sun;
                      }       
                    }
                  }       
                }
              }
            }
          } catch {systemLog(false, 'error on line: ' + line);}
          break;
        case 'RelayLink':
          sut = 'OutputLink';
        case 'FireSensorLink':
        case 'FireMCPLink':
        case 'TechInputLink':
        case 'FaultInputLink':
        try
          {
            let sun = Number(par[1]);
            let alnum = Number(par[4]);
            let AUnum = Number(par[5]);
            let AUtype=par[6];
            let AUsn=Number(par[7]);
            if(!AUsn) AUsn=0;
            let AUsubtype=par[8];
            let AUsubnum=Number(par[9]);            
            let AUname=(par[10]);            
            let AUid; // undef but may be will be defined
            let au; // undef until created
            if(alnum && alnum<3 && AUnum && AUnum<256) {
              const al=mas.AL[alnum];
              const aus=al._subUnits.AU;
              au = checkSubUnit(al, AUnum, 'AU', AUtype,{});
              if(au) {
                au._configured=true;
                if (!au.config) au.config = {};
                if(AUsn) au.config.SN = AUsn;
                if(AUname.length) au.config.name=AUname;
                let AUsubs=au._subUnits[AUsubtype];
                if(AUsubs && AUsubs[AUsubnum]) {
                  AUid=AUsubs[AUsubnum]._idString;
                } else { // AU itself
                  AUid=au._idString;
                }       
              }       
            } else if (AUnum && sut == 'OutputLink' && AUtype=='RelayGroup') {
              au = checkSubUnit(ma, AUnum, 'RelayGroup', 'RelayGroup',{});
              if(au) {
                au._configured=true;
                if(!au.config) au.config={};
                AUid=au._idString;
              }
            }
            if (sun) {
              let ts = ms[sut][sun];
              if(ts?._typeName != par[0]) {
                systemLog(false, 'was TS type ['+ts?._typeName+']: ' + line);
                deleteUnit(ts);
                delete ts;
              }
              if (!ts) ts = createSubunit(m, sut, sun, par[0]);
              if (!ts.config) ts.config = {};
              if(AUid) ts.config.unitID=AUid;
              if(par[3].length) {
                ts.config.name = par[3];
              }
              ts._configured=true;            
              let arn=Number(par[2]);
              if(arn && arn < 256) {
                ts.config.areaNum = arn;
                if(!ms.Area[arn]) createSubunit(m,'Area',arn);
              }
            }
          } catch {systemLog(false, 'error on line: ' + line);}
          break;
        case 'AU':
          try
          {
            let alnum = Number(par[4]);
            let AUnum = Number(par[5]);
            let AUtype=par[6];
            let AUsn=Number(par[7]);
            let AUname=(par[10]);            
            if(!AUsn) AUsn=0;
            let au; // undef until created
            if(alnum && alnum<3 && AUnum && AUnum<256 && AUtype.length) {
              const al=mas.AL[alnum];
              au = checkSubUnit(al, AUnum, 'AU', AUtype,{});
              if(au) {
                au._configured=true;
                if (!au.config) au.config = {};
                if(AUsn ) au.config.SN = AUsn;
                if(AUname.length) au.config.name=AUname;
              }       
            }
          } catch {systemLog(false, 'error on line: ' + line);}
          break;
      }
    } else {
      systemLog(false, 'bad line: ' + line);
    }
  }
  modelChanged('afterLoad', ppk);
  location.reload();
}


var buffer2dump;

function saveDumpLogger(){
  let ab = new ArrayBuffer(4096);
  let data = new Uint8Array(ab);
  saveTextToFile(b, 'dump.bin', 'application/octet-stream')
}

