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

        sys._initFinished  true  when sys fully loaded and initialized



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
                          

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



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

////// ??????????????         import { getUnit } from "@mui/material/styles/cssUtils";

var curPollingModule; // todo - remove - retained only bcz Sergey sets it to undef
var loggedInUser;

var sys; // superUnit - all other in _subUnits - current config

// input as in processMessage()
// yellow result - message.b & message.m are set to box and module units
// returns true if module not found - else no need to process message itself
// fills message.b  & message.m
function processMessageCheckBoxModule(message) {
  if (!sys) {
    sys = {};
    fillUnit(sys, '_System');
  }

  // first check thisMod && thisBox && online
  if (message.from.category == 0 && message.cmd.status) {
    // rcvd status from boot of THIS module or box - preliminary box/mod in header to find b & m
    if (message.from.box == 255) message.to.box = message.from.box = message.cmd.status.boxAddress;
    if (message.from.mod == 255) message.to.mod = message.from.mod = message.cmd.status.moduleAddress;
  }

  // "to" - is our address. we are connected to it
  if (
    message.to.box &&
    message.to.box != 255 &&
    message.to.mod &&
    message.to.mod != 255 &&
    (!online || message.to.box != dispatcher.thisBox || message.to.mod != dispatcher.thisModule)
  ) {
    dispatcher.thisBox = message.to.box;
    dispatcher.thisModule = message.to.mod;

    online = true;
    systemLog(false, 'connected to box #' + dispatcher.thisBox + ', mod #' + dispatcher.thisModule);
    modelChanged('online', undefined, {
      box: dispatcher.thisBox,
      mod: dispatcher.thisModule,
    });
  }

  // replace any 255
  if (message.to.box == 255) message.to.box = dispatcher.thisBox;
  if (message.to.mod == 255) message.to.mod = dispatcher.thisModule;
  if (message.from.box == 255) message.from.box = dispatcher.thisBox;
  if (message.from.mod == 255) message.from.mod = dispatcher.thisModule;

  // check if FROM are real == cannot process message from unknown box.mod
  if (!message.from.box || message.from.box == 255 || !message.from.mod || message.from.mod == 255) return true;

  let moduleTypeNum;
  if (message.from.category == 0 && message.cmd.status) {
    // let snt = message.cmd.status.snType;
    moduleTypeNum = message.cmd.status.boardType;
    if (!moduleTypeNum) moduleTypeNum = snt >> 24;
  } else {
    moduleTypeNum = message.from.mod; // todo - here we suppose fixed module addresses !!!!!!!!
    if (!sys._subUnits._Box[message.from.box]) {
      // Box not present and not status from 1st module boot
      fillRequest({
        id: [1, message.from.box], // poll this boxes, 1st module, boot
        unitType: 'Boot',
        cmd: {
          status: {},
        },
      }); // to create this box
      return true; // no further processing until got mod1.boot.status
    }
  }
  let modtname = schema.typeSN.Module[moduleTypeNum];
  if (modtname && Array.isArray(modtname)) modtname = modtname[0];

  message.b = checkSubUnit(sys, message.from.box, '_Box'); // now single type of Box possible
  if (modtname) message.modtname = modtname; //// todo - may be not correct
  message.m = checkSubUnit(message.b, message.from.mod, 'Module', modtname); // in fact, now all Modules are already initialized

  if (message.from.category == 0 || message.from.category == 0x26) {
    // binary from module
    message.u = message.m._subUnits.Boot[1];
    return false;
  } else if (message.from.category == 1) {
    // subscription and direct answers are all form "1" or "0"
    // cpu
    let uid = message.id;
    let l = uid.length; // skip box/mod

    if (l > 2) {
      // else some error
      l -= 2;
      try {
        // seek for unit
        let cu = message.m;
        let protect = 100; // just not to hang up on any bad message
        let up = message.b;
        let sut;
        let sun;
        let sutn;
        while (l && protect--) {
          let sid = uid[--l];
          if (sid) {
            // still not 0 = not endofsuids
            sutn = ((sid & 0xf0) >> 4) | ((sid & 0x0f0000) >> 12) | ((sid & 0xffff00000000) >> 24);
            sun = (sid & 0x0f) | ((sid & 0xff00) >> 4) | ((sid & 0xfff00000) >> 8);
            sut = cu._desc.__.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) {
                  console.log('undef unit in uids chain');
                  return true;
                } // else OK - 0 and type
              }
              up = cu;
              cu = nu;
            } else {
              console.log('undef sut in uids chain - 1');
              return true;
            }
          } else {
            // done chain of sids, got source unit
            if (l == 1) {
              // ok, only type left
              let t = uid[0]; // type ==

              if (t) {
                let utn = schema.unitTypesNumbers[t];
                if (!cu) {
                  // kostyl for search and readCfg
                  let readCfg = dispatcher_tasks?.[250]?.needCfg && dispatcher_tasks[250].needCfg.includes(message.m);
                  if (unitTypeCompatible(utn, sut) && (readCfg || up._sut == 'AL')) {
                    cu = checkSubUnit(up, sun, sut, utn, message.cmd.config, readCfg); // do not create areas while readCfg
                    if (!message.cmd.config)
                      sendCommand(cu, {
                        config: {},
                      });
                  }
                }
                if (cu?._typeName != utn) {
                  cu = undefined;
                  // in PPK differs
                  return true; // cannot do anything with such a unit
                }
              } else {
                if (cu && !cu?._deleted) {
                  console.log('malformed uids chain');
                  return true;
                }
              }
              message.u = cu;
              return false; // 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
    return true;
  }
}

function processMessageBootStatus(message) {
  // called from "processMessage" if category==0 and cmd.status present, afetr all mod.box in header already fixed
  // BOOT answer.
  // check for box/module - this is done from BOOT answers - special case
  // returns Boot unit for common processing to update data
  let boxSun = message.cmd.status.boxAddress;
  let modSun = message.cmd.status.moduleAddress;
  let sn = message.cmd.status.sn;

  let b = message.b;

  if (modSun == 1) {
    // check uniqueness of box's SN (it is th SN of ring-module)
    let hid = hidStringCT({ SN: sn }, '_Box'); // force address if present in config
    if (!sys._serials) sys._serials = {};
    if (!sys._serials[hid]) sys._serials[hid] = [];
    if (sys._serials[hid][0]) {
      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 && b._subUnits.Module[1]._subUnits.Boot[1].status?.sn)
      b._hidString = hidStringCT({ SN: b._subUnits.Module[1]._subUnits.Boot[1].status?.sn }, '_Box');

    if (!b) needsetadr = boxSun;
    else if (b._hidString != hid) {
      if (!b.config?.SN) {
        if (!b.config) b.config = {};
        b.config.SN = sn;
        b._hidString = hid;
      } else {
        // busy address (another SN set for this address) find new address for this box
        let newadr = 0;
        do {
          b = sys._subUnits._Box[++newadr];
        } while (b);
        if (newadr > 31) {
          systemLog(true, 'cannot find free BOX address for BOX with SN=' + sn);
        }
        needsetadr = newadr;
      }
    }

    if (needsetadr) {
      b = checkSubUnit(sys, needsetadr, '_Box', '_Box', { SN: sn }); // cfg only if creating on message from PPKr
      // in PPK boxsun present, be sure it is present in config.
      b._configured = false; // just in PPK
      b._realType = '_Box';
      if (stableSys) {
        modelChanged('changed', sys);
        modelChanged('changed', b);
      }
      setBoxAddress(b);
    }

    message.b = b;

    let oldSN = b.config.SN;
    if (!oldSN) {
      b.config.SN = sn;
      modelChanged('changed', b, { config: { SN: true } });
    } else {
      if (!b._configPPK) b._configPPK = {};
      b._configPPK.SN = oldSN;
      modelChanged('changed', b, { config: { SN: oldSN } });
    }
  }
  message.m = checkSubUnit(message.b, modSun, 'Module', message.modtname);
  message.u = message.m._subUnits.Boot[1];
  return;
} // boot status

//////////_61
function processMessage(message) {
  // message MUST be (b:boxUnit, m:moduleUnit, from:uint8arr, to:uint8arr, id:arrayofint, cmd:{"config":{}} // cmd may have more then one command - for future extentions
  // may return new message (uint8array) to be sent. not recommended.
  if (message.from.category == 0 && message.cmd.status) processMessageBootStatus(message);

  if (message.from.category == 0x26) {
    processBinaryAnswer(message);
    return;
  }

  let u = message.u;

  if (!u) {
    console.log('unit not found as per uids chain');
    return;
  }

  // got adequate "u"
  if (!u._idString) u._idString = message.idString;

  let uChanged = {};

  for (let msgn in message.cmd) {
    // in fact only one msg, but just for future extentions
    let msg = message.cmd[msgn];

    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 = {};

      let oldConfig = u.config;

      if (!u.config && !u._deleted) {
        // first time. let config be as in PPK
        changedUnits.push(u);
        u.config = msg;
        uChanged.config = {}; // ignored parameters - full cfg will be redrawn
        u._configPPK = {}; // clear
      } else {
        if (!u._configPPK) u._configPPK = {};

        for (let p in msg) {
          if (p === 'unitID') msg[p] = Uint8toString(msg[p]); // todo move to "decode"
          const pd = u._desc[msgn][p];

          if (pd.ro || u.config[p] === undefined || (u._desc.config[p]?.hardwareID && !u._configured)) {
            u.config[p] = msg[p];
            uChanged.config = {};
            delete u._configPPK[p];
            // first read cfg parameter from PPK or not yet approved - just use it
          }

          if (msg[p] != u.config[p] && msg[p] != u._configPPK[p]) uChanged.config = {};

          let VtoCompare = u.config[p];
          if (pd.format == 'string' && pd.maxPPKlength && typeof VtoCompare == 'string')
            VtoCompare = u.config[p].substring(0, pd.maxPPKlength);

          if (msg[p] == VtoCompare) {
            if (p in u._configPPK) {
              delete u._configPPK[p];
              uChanged.config = {};
            }
          } else {
            u._configPPK[p] = msg[p]; // to update view if displayed now
            uChanged.config = {};
          }
        }
      }
      u._configOK = Object.keys(u._configPPK).length == 0;
      if (uChanged.config) uChanged.config = oldConfig; // to be same as others msgn
    } else {
      // not config
      let recalcF = false;
      if (msgn == 'status') if (!u.status || u.status.fault != msg.fault) recalcF = true;
      if (!u[msgn]) {
        u[msgn] = msg;
        uChanged[msgn] = msg;
      } 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[msgn]) uChanged[msgn] = {};
              uChanged[msgn][p] = u[msgn][p]; // save old in case it is usefull
              u[msgn][p] = msg[p];
            }
          } else if (ArrayBuffer.isView(msg[p])) {
            // todo: not sure, suppose uint8array may be only for immediate display
            if (!uChanged) uChanged = {};
            if (!uChanged[msgn]) uChanged[msgn] = {};
            uChanged[msgn][p] = u[msgn][p];
            u[msgn][p] = msg[p];
          }
        }
      if (recalcF) recalcFaultsDown(u._up);
    }
  }

  // todo - remove "_offline", use "_configOK"
  if (u._offline) {
    u._offline = false;
    if (stableSys) modelChanged('force', u);
  }

  if (stableSys && Object.keys(uChanged).length) {
    modelChanged('changed', u, uChanged);
  }
}

//////_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]) sys._serials[u._hidString] = [];
    if (sys._serials[u._hidString].length <= 1) return undefined;
    return sys._serials[u._hidString];
  } catch {}
  return undefined;
}

// hardwareID form unit
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;
}

// hardwareID from config and type
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
/// returns new or found unit
function checkSubUnit(up, sun, sut, tName, cfg, readCfgNow) {
  // cfg only if creating on message from PPKr
  let res;
  try {
    res = up._subUnits[sut][sun];
    if (res && res._typeName && unitTypeCompatible(res._typeName, sut) && schema.unitTypes[res._typeName].__.number)
      tName = res._typeName;
    else {
      if (!tName || !schema.unitTypes[tName] || !schema.unitTypes[tName].__.number || !unitTypeCompatible(tName, sut))
        tName = undefined;
      // if no heirs - the only tName is possible
      const upsubdesc = up._desc.__.subUnits[sut];
      if (!tName) {
        if (!(upsubdesc.software || upsubdesc.hardware)) tName = sut;
        else tName = upsubdesc.defaultType;
      }
      if (
        !tName &&
        !schema.unitTypes[sut]?.__?.vNumber &&
        (!schema.unitTypes[sut]?.__?.allRealHeirs || schema.unitTypes[sut].__.allRealHeirs.length == 0)
      )
        tName = sut;
      if (res && tName && res._typeName != tName) res = undefined;
    }
    if (!res) {
      if (!cfg) cfg = {};
      res = {
        _up: up,
        _sut: sut,
        _sun: sun,
        config: cfg,
      };
      up._subUnits[sut][sun] = res;
      fillUnit(res, tName, readCfgNow);
      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 = Number(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 + '}';
}

function getPacketSetBoxAddress(box, withLength) {
  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,
      },
    },
  };
  if (withLength) return getRequest(r);
  else return fillRequest(r, undefined, undefined, true);
}

function setBoxAddress(box) {
  if (!box) return;
  try {
    sendCommand(box._subUnits.Module[1]._subUnits.Boot[1], { status: {} }); // in case there are sm other box on that address
    sendCommand(box._subUnits.Module[1]._subUnits.Boot[1], { status: {} }); // in case there are sm other box on that address
    sendCommand(box._subUnits.Module[1]._subUnits.Boot[1], { status: {} }); // in case there are sm other box on that address
    const r = getPacketSetBoxAddress(box);
    addPacketToSendCommand(r);
    addPacketToSendCommand(r);
    addPacketToSendCommand(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._idString && u._idString.startsWith(oldIdString)) u._idString = u._idString.replace(oldIdString, newIdString);
    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?.config?.backLink) {
        let backLinkUnit = unitIdToUnit2(u.config.backLink);
        if (backLinkUnit?.config) backLinkUnit.config.unitID = u._idString;
      }
    }
  } 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

  if (!cu?._up?._desc || !cu._sut) {
    console.log('bad unit to change sun #' + newSun);
    return false;
  }

  let sud = cu._up._desc.__.subUnits[cu._sut];

  if (typeof newSun != 'number') newSun = Number(newSun);
  if (typeof newSun == 'number') newSun = Math.floor(newSun);

  if (typeof newSun != 'number' || newSun < 1 || newSun > sud.qty) {
    systemLog(false, 'bad sun #' + newSun);
    return false;
  }

  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;

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

    if (cu?.config?.areaNum) {
      let m = cu;
      while (m._sut != 'Module') m = m._up;
      let ar = checkSubUnit(m, cu.config.areaNum, 'Area');
      if (ar) {
        if (!ar._subUnits) ar._subUnits = {};
        if (!ar._subUnits[cu._sut]) ar._subUnits[cu._sut] = {};
        if (ar._subUnits[cu._sut][oldSun]) delete ar._subUnits[cu._sut][oldSun];
        ar._subUnits[cu._sut][newSun] = cu;
      }
    }

    cu._sun = Number(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() {
  stopSendingUnit('abort');
  abortAllFirmwareUpdates();
  const oldState = sys.state;
  sys = { _sun: 1, _sut: '_System', state: oldState };
  fillUnit(sys, '_System');
  sys._initFinished = true;
  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 (cu?.config?.backLink) {
    let backLinkUnit = unitIdToUnit2(cu.config.backLink);
    if (backLinkUnit?.config) backLinkUnit.config.unitID = cu._idString;
  }
  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] = dispatcher.thisBox;
  return Uint8toString(uid);
}

//////////_48
function shortID(u) {
  let uid = makeShorterAdr(getUnitID(u));
  if (uid[0] == 0xff) uid[0] = dispatcher.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] = dispatcher.thisBox;
  if (u8[1] == 0xff) u8[1] = dispatcher.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) {
  let ab = new ArrayBuffer(300);
  let data = new Uint8Array(ab);
  if (!str) return data.slice(0, 0);
  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 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);
}

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

    if (u?.config?.unitID) {
      const hu = unitIdToUnit2(u?.config?.unitID);
      if (hu?.config?.backLink && hu?.config?.backLink == u._idString) delete hu.config.backLink;
    }

    // kostyl for Area / _Box == systree
    switch (u._sut) {
      case '_Box':
      case 'Area':
        deleteInTree(u._treeId);
        break;
    }

    // kostyl for area TS
    if (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);
      }
      if (u._treeId) {
        deleteInTree(u._treeId);
      }
      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) {
  if (!t || !schema.unitTypes[t] || !schema.unitTypes[t].__.number || !unitTypeCompatible(t, curSut)) t = undefined;

  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) {
    if (!su[n].config) su[n].config = {};
    let c = 100000;
    if (c > su[n]._desc.config.SN.maxValue) c = 10000;
    su[n].config.SN = n + u._sun * c;
  }
  hidString(su[n]);
  setTreeIdForUnit(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?._deleted) return;
  if (!u?._up?._subUnits?.[u._sut]?.[u._sun]) return;
  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);
  }
}

function getCommand(u, cmd, broadcast) {
  if (u?._up?._subUnits?.[u._sut]?.[u._sun]) {
    let uid = getUnitID(u);
    if (broadcast) uid[uid.length - 1] = 0;
    return getRequest({
      id: uid,
      unitType: u?._typeName,
      cmd: cmd,
    });
  }
}

///////_37
function sendCommandDelete(u, returnRequest) {
  if (u._typeName[0] !== '_') {
    let uid = getUnitID(u);
    uid[0] = 0; // type
    const req = {
      id: uid,
      unitType: 'Unit',
      cmd: { config: {} },
    };
    if (returnRequest) return getRequest(req);
    else fillRequest(req);
  }
}

/////////_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', '_selectedArea']) 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
}

// MUST be called before displaying unit for edit
function prepareDefaultConfig(obj) {
  try {
    for (pn in obj._desc.config)
      if (pn !== '__') {
        if (!(pn in obj.config)) {
          if ('defaultValue' in obj._desc.config[pn]) obj.config[pn] = obj._desc.config[pn].defaultValue;
          else if (['fixed32', 'varInt', 'fixed64'].includes(obj._desc.config[pn].format)) {
            if ('minValue' in obj._desc.config[pn]) 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];
  } catch {}
}

//////////_29
var fillUnitObj;
function fillUnit(obj, name, readCfgNow) {
  let preStable = stableSys;
  try {
    if (
      name != '_System' &&
      (!name || !schema.unitTypes[name] || !schema.unitTypes[name].__.number || !unitTypeCompatible(name, obj._sut))
    )
      name = obj?._up?._desc?.__?.subUnits?.[obj?._sut]?.defaultType;
  } catch {
    name = undefined;
  }

  if (!name) name = obj._sut;
  obj._typeName = name;

  if (name == '_System' && !obj.state) {
    obj.state = {};
  }

  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 = {};

    if (!readCfgNow) prepareDefaultConfig(obj);

    if (obj.config.areaNum && !readCfgNow) {
      let m = obj;
      while (m && m._sut != 'Module') m = m._up;
      if (m) {
        let ar = checkSubUnit(m, obj.config.areaNum, 'Area');
        if (ar) {
          if (!ar._subUnits) ar._subUnits = {};
          if (!ar._subUnits[obj._sut]) ar._subUnits[obj._sut] = {};
          ar._subUnits[obj._sut][obj._sun] = obj;
        }
      }
    }
  }

  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, undefined, undefined, readCfgNow);
        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, readCfgNow);
            }
          }
    }

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

  modelChanged('force', obj);
}

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

function recalcLinks() {
  sys._initFinished = false;

  fillUnit(sys, '_System');
  initFaultsDown(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;
            }
          }
        }
      }
    }
  }

  let errText = 'CRITICAL ERROR:\n\n';
  let step = 1;
  if (sys._processedClear) step = sys._processedClear + 1;
  function clearBacklinks(u) {
    if (!u._desc) {
      let ut = u._typeName;
      if (!ut) ut = u._sut;
      u._desc = schema.unitTypes[ut];
    }

    if (u && u._processedClear == step) {
      let erst = 'cycled ' + getUnitName(u) + '\n\n';
      // systemLog(true, erst);
      errText += erst;
      systemLog(false, erst, 'red');
      return;
    }
    u._processedClear = step;
    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, 'red');

  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' && link._typeName != 'FireSensorLink')) // 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

  sys._initFinished = true;
}

function checkSys() {
  let errors;

  // 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, 'yellow');
      // 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, 'yellow');
          thisDuplicated = alreadyDuplicated = true;
          break; // for
        }
      }
    }

    u._checkCycle = top._idString;
    let ncp = [];

    if (curPathAr.length > 50) {
      let erstr = '\n\n\n\nError: too long path of areas  !\n\n ====== path:\n\n';
      for (let ii = curPathAr.length; ii--; ) erstr += '\n\n -- ' + getUnitName(unitIdToUnit2(curPathAr[ii]), 1);
      errText += erstr;
      systemLog(false, erstr, 'yellow');
      thisDuplicated = alreadyDuplicated = errors = true;
      // return;
    }

    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

        let newArId = ar._idString;
        for (let i = curPathAr.length; i--; )
          if (curPathAr[i] == newArId) {
            let erstr = '\n\n\n\nError: cycled area link at step [' + i + '] !\n\n ====== path:\n\n';
            for (let ii = curPathAr.length; ii--; ) erstr += '\n\n -- ' + getUnitName(unitIdToUnit2(curPathAr[ii]), 1);
            errText += erstr;
            systemLog(false, erstr, 'yellow');
            thisDuplicated = alreadyDuplicated = errors = true;
            // return;
          }

        if (ar && ar != sys && !thisDuplicated) checkCycles(ar, true, alreadyDuplicated);
      }
    }

    // iterate all links in this area on this ppk

    for (let lnkn in mod._subUnits.OutputLink) {
      let lnk = mod._subUnits.OutputLink[lnkn];
      if (lnk?.config?.areaNum == u._sun) {
        let ar = unitIdToUnit2(lnk?.config?.unitID);

        if (ar._sut == 'InputLink' || ar._sut == 'OutputLink' || ar._sut == 'Area') {
          let erstr = '\n\n\n\nError: link ' + getUnitName(lnk, 1) + ' points to soft ' + getUnitName(ar, 1);
          errText += erstr;
          systemLog(false, erstr, 'yellow');
          continue;
        }

        if (lnk._typeName != 'AnnunciatorLink') {
          while (ar && ar._sut != '_Box') ar = ar._up;
          if (ar && ar._sun != mod._up._sun) {
            let erstr = '\n\n\n\nError: link ' + getUnitName(lnk, 1) + ' points to other Box ' + getUnitName(ar, 1);
            errText += erstr;
            systemLog(false, erstr, 'yellow');
          }
        }
      }
    }

    for (let lnkn in mod._subUnits.InputLink) {
      let lnk = mod._subUnits.InputLink[lnkn];
      if (lnk?.config?.areaNum == u._sun) {
        let ar = unitIdToUnit2(lnk?.config?.unitID);

        if (
          ar._sut == 'InputLink' ||
          ar._sut == 'OutputLink' ||
          (lnk._typeName != 'FireAreaLink' && lnk._typeName != 'FireSensorLink' && ar._sut == 'Area')
        ) {
          let erstr = '\n\n\n\nError: link ' + getUnitName(lnk, 1) + ' points to ' + getUnitName(ar, 1);
          errText += erstr;
          systemLog(false, erstr, 'yellow');
          continue;
        }

        if ((lnk._typeName == 'FireAreaLink' || lnk._typeName == 'FireSensorLink') && ar._sut == 'Area') {
          // area link in this area

          if (ar && ar != sys && !thisDuplicated) {
            curPathAr.push(lnk._idString);

            let newArId = ar._idString;
            for (let i = curPathAr.length; i--; )
              if (curPathAr[i] == newArId) {
                let erstr = '\n\n\n\nError: cycled area link at step [' + i + '] !\n\n ====== path:\n\n';
                for (let ii = curPathAr.length; ii--; )
                  erstr += '\n\n -- ' + getUnitName(unitIdToUnit2(curPathAr[ii]), 1);
                errText += erstr;
                systemLog(false, erstr, 'yellow');
                thisDuplicated = alreadyDuplicated = true;
                //  return;
              }

            if (ar && ar != sys && !thisDuplicated) checkCycles(ar, true, alreadyDuplicated);

            curPathAr.pop();
          }
        }

        if (lnk._typeName != 'FireAreaLink' && lnk._typeName != 'FireSensorLink') {
          while (ar && ar._sut != '_Box') ar = ar._up;
          if (ar && ar._sun != mod._up._sun) {
            let erstr = '\n\n\n\nError: link ' + getUnitName(lnk, 1) + ' points to other Box ' + getUnitName(ar, 1);
            errText += erstr;
            systemLog(false, erstr, 'yellow');
          }
        }
      }
    }

    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, 'red');
  if (warnStr.length > 20) systemLog(true, warnStr, 'yellow');

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

  return errors;
}

/*
 *  cfg has [sys:]  {"config":{},"subunits":{"AL":{}, ... etc ...}}
 */
/////////_28_big_changes
function loadCfg(JSONcfg, cb = () => {}) {
  stopSendingUnit('load cfg');
  abortAllFirmwareUpdates();
  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;

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

    recalcLinks();

    checkSys();

    sysTree = makeSubTreeSubUnits(sys);

    // let te = getUnitFromTreeId('_Box.8.AL.1');
    // let tt = getTreeList(te);

    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
async function initializeModel(callBack) {
  /// todo - rewrite 'em as async + await ? probably, will be more straightforward
  function finalInitModel() {
    window.onbeforeunload = function (event) {
      saveValueForElement(document.activeElement);
      if (window.log) window.localStorage.setItem('systemLog', JSON.stringify(window.log));
      window.localStorage.setItem('noPullLog', window.noPullLog ? 'true' : 'false');
      saveCfg();
    };
    callBack();
  }

  await initDispatcher();
  dispatcher_tasks[253] = new cfgUploadDispatcher();
  dispatcher_tasks[252] = new oscillographDispatcher();
  // dispatcher_tasks[251] = new fullFlashDispatcher();
  dispatcher_tasks[101] = new sendCommandDispatcher();
  dispatcher_tasks[102] = new logReadDispatcher();
  dispatcher_tasks[250] = new subscribeDispatcher(); /// cfg download
  dispatcher_tasks[104] = new sendBroadcastDispatcher();
  dispatcher_tasks[105] = new pollStateDispatcher();
  dispatcher_tasks[0x5a] = new kostylForSearchAU();

  initDB(finalInitModel);
  try {
    let lastSystemLogString = window.localStorage.getItem('systemLog');
    let lastSystemLog = JSON.parse(lastSystemLogString);
    if (Array.isArray(lastSystemLog)) window.log = lastSystemLog;
    window.noPullLog = window.localStorage.getItem('noPullLog') == 'true';
    const chk = document.getElementById('pullLog');
    if (chk) chk.checked = window.noPullLog;
  } catch {}
  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._typeName[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(Number(u._sun)); // mod number
      uid.push(Number(u._up._sun)); // box number
    }
    return uid;
  } catch {
    return null;
  }
}

//// returns {"t":unitTypeName from ID,"u":unit}; or null if failed ===== uid is an array of bytes !!!
///////////_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 && dispatcher.thisBox && dispatcher.thisBox != 255) bn = dispatcher.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 && dispatcher.thisModule && dispatcher.thisModule != 255) mn = dispatcher.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];
        if (!cu) return null;
      } 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;
  }
}

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

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

async 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';
        let nam = '';
        if (typeof au.config.name == 'string') nam = au.config.name.replace(' ', '_');
        if (
          ['AXDPI', 'ATI', 'ISM2', 'ISM5', 'ARMINI', 'IR', 'AMK', 'ISM4', 'AR1', 'MKZ'].includes(tn) &&
          au.config.SN
        ) {
          let lin = tn + ' ' + au.config.SN + ' ' + j + ' ' + nam;
          txt += 'a ' + lin + '\r\n'; // \r\n   o ' + lin + '\r\n ';
        }
      }

      await new Promise(r => setTimeout(r, 1000));

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

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

  if (u._sut == 'Area') return checkSys();
}

///////_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     =========== this function is used by grey (Sergey) == do not change it
function setUpdateFirmware(targetFile, curUnit) {
  // call it with NONE / undefined - to abort
  let file = targetFile;
  let reader = new FileReader();
  let u = curUnit;

  if (!targetFile || !curUnit) {
    updateFirmware(null, null);
    return;
  }

  reader.onload = function () {
    // todo check unit type
    let u8 = new Uint8Array(reader.result);
    /*
            knowhow
            byte [0x1b] is board type
            */
    try {
      let chk = checkUpdateFirmware(u, u8);
      if (typeof chk == 'string') {
        systemLog(true, chk);
        return;
      }
      if (
        chk &&
        !confirm(
          'Currently update of ' + getUnitName(chk) + ' not finished. Are you sure to abort and start new update job ?',
        )
      )
        return;

      if (!confirm('are You sure - load firmware ' + file.name + ' on this board ?'))
        // todo - show names of board, module, and fw type
        return;

      updateFirmware(u, u8);
      modelChanged('boot', curUnit);
    } catch {
      let s = 'unknown error catched, typename = ' + u._typeName;
      systemLog(true, s);
    }
  };

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

  reader.readAsArrayBuffer(file);
}

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

  if (typeof pd.minValue == 'number' && pd.minValue > value) value = pd.minValue;
  if (typeof pd.maxValue == 'number' && pd.format != 'string' && pd.maxValue < value) value = pd.maxValue;
  else if (value < 0) value = 0; // kostyl. currently no negative values. or set MIN in schema
  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];

  let VtoCompare = value;
  if (pd.format == 'string' && pd.maxPPKlength && typeof VtoCompare == 'string')
    VtoCompare = value.substring(0, pd.maxPPKlength);

  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._sut != 'Area') {
          /// && (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 && 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]
    )
      unit._configPPK[paramName] = oldV;
    else if (
      typeof unit._configPPK[paramName] != 'undefined' &&
      !pd.notForPPK &&
      unit._configPPK[paramName] === VtoCompare
    )
      delete unit._configPPK[paramName];

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

    if (pd && pd.callOnAreaChange) {
      if (onAreaChange(unit)) value = unit[msgName][paramName] = 0; // 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;
}

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 && !cd[pn].notCopyPaste) {
        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]);
      }
    }
    modelChanged('changed', unit, { config: u.config });
  }

  try {
    if (sut) {
      for (let sun in unit._subUnits[sut]) if (unit._subUnits[sut][sun]._selected) 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');
}

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

/////////////////// log functions //////////////////////////
/////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////
/*

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

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


*/

var localDB; // indexedDB with all read logs

function initDB2(callBack = () => {}) {
  let boxes = sys._subUnits._Box;
  for (let b in boxes)
    boxes[b]._logData = {
      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',
      });
    }
  };
}

function processLogData(message) {
  // todo - use recordsMulty
  try {
    let msg = message.cmd.records;

    let mod = message.m;
    let box = message.b;

    if (!box._logData)
      box._logData = {
        lastPollTime: 0,
        confirmed: true, // first read same message as last in precious session. if same - continue read that+1, else
        full: false, // got up to the end
        lastIndex: 0,
      };

    let l = box._logData;

    l.full = (msg.topIndex && msg.topIndex == l.lastIndex) || (!msg.indexRequest && !msg.record);

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

    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; // read from the beginning
      }
      l.confirmed = true;
    } else {
      if (msg.index) {
        l.lastIndex = msg.index; // + 1;
        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 = mod;
              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) {
                    let chgd = false;
                    if (struct.struct?.status) {
                      us.status = struct.struct.status;
                      us._lastStatusTime = struct.date;
                      chgd = true;
                    } else {
                      // event
                      if (!us.status) us.status = {};
                      if (typeof struct.struct?.event?.stateResult == 'number') {
                        us.status.unified = struct.struct.event.stateResult;
                        chgd = true;
                      }
                      if (typeof struct.struct?.event?.vectorResult == 'number') {
                        // Area probably
                        us.status.on = struct.struct.event.vectorResult & 3;
                        us.status.fault = (struct.struct.event.vectorResult & 4) >> 2;
                        chgd = true;
                      }
                    }
                    if (chgd) modelChanged('changed', us);
                  }
                }
              }
            }
          } 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;
      }
    }
  } 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;
}

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);
  modelChanged('showCfg', null, text);
}

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 sendBinaryRequestAllAU(al) {
  // todo - move to polling class
  if (al._typeName != 'AL') return;
  let ab = new ArrayBuffer(6);
  let data = new Uint8Array(ab);
  data[0] = 0x00; // tag
  data[1] = 0x26; // category
  data[2] = al._up._up._sun; // box
  data[3] = al._up._sun; // module
  data[4] = 0x01; // box
  data[5] = al._sun - 1; // module
  addPacketToSendCommand(ab);
}

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 = Number(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;
        checkSubUnit(m1, areaNum, 'Area');
        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);
}

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

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
//////////processor for special commands - to avoid a lot of new buttons
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

function processManualCommand(curViewUnit, text) {
  if (typeof text != 'string') return;
  try {
    let words = text.split(' ');
    if (words?.length)
      switch (words[0]) {
        case 'uartDelay':
          let d = Number(words[0]);
          if (!isNaN(d)) {
            if (d > 100) d = 100;
            if (d < 1 || !d) d = 1;
            delayBtwUsartPackets = d;
          }
          break;
        case 'change':
          if (words.length > 3) {
            function chg1(u, param, from, to) {
              try {
                if (param == '_typeName') {
                  if (u?._typeName == from) changeUnitType(u, to);
                } else {
                  let params = param.split('.');
                  let uu = u;
                  for (let n = 0; n < params.length - 1; n++) if (uu[params[n]]) uu = uu[params[n]];
                  if (uu[params[params.length - 1]] == from) {
                    let toto = to;
                    if (params[0] == 'config' && params.length == 2) {
                      let desc = schema.unitTypes[u._typeName]?.config;
                      if (desc) desc = desc[params[1]];
                      if (desc?.format == 'varInt') toto = parseInt(to);
                    }
                    uu[params[params.length - 1]] = toto;
                  }
                }
              } catch {}
              try {
                if (u._subUnits)
                  for (let st in u._subUnits)
                    for (let sn in u._subUnits[st]) chg1(u._subUnits[st][sn], param, from, to);
              } catch {}
            }
            chg1(curViewUnit, words[1], words[2], words[3]);
          }
          break;
      }
  } catch {}
}

function findUnitFromStringID(id) {
  return findUnit(makeAdrFromShorterBin(string2uint8(id)))?.u;
}

//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
//////////////////////////////
// dispatcher classes
//////////////////////////////
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// main manual comman dispatcher class
101;
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////

class sendCommandDispatcher extends heartBeatDispatcher {
  #data2send = [];
  constructor() {
    super();
    this.active = 0;
    this.priority = 1;
  }

  // use inherited parent function
  // processAnswer(message) {
  // }

  getPacket() {
    let totalLength = 0;
    for (let n = this.#data2send.length; n--; ) totalLength += 1 + this.#data2send[n].byteLength;
    let ab = new ArrayBuffer(totalLength);
    if (totalLength) {
      let u8ab = new Uint8Array(ab);
      let pos = 0;
      for (let n = 0; n < this.#data2send.length; n++) {
        let u8ds = new Uint8Array(this.#data2send[n]);
        u8ab[pos++] = u8ds.length;
        for (let i = 0; i < u8ds.length; i++) u8ab[pos++] = u8ds[i];
      }
    } else ab = undefined;
    this.#data2send = []; // all packets combined in single bytearray
    this.active = 0;
    return ab;
  }

  addPacket(packet2send, tag) {
    // ArrayBuffer
    if (typeof tag != 'number') tag = 101;
    let u8a = new Uint8Array(packet2send);
    u8a[0] = tag;
    this.#data2send.push(packet2send);
    this.active = 1;
    this.timeout = 0;
  }
}

function addPacketToSendCommand(packet, tag) {
  if (!packet) return;
  if (!dispatcher_tasks?.[101]) return;
  dispatcher_tasks[101].addPacket(packet, tag);
}

//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// periodic broadcast call for new PPKs dispatcher class
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////

class sendBroadcastDispatcher extends heartBeatDispatcher {
  #counter = 0;
  #lastSetTimeTime = 0;

  constructor() {
    super();
    this.active = 1;
    this.priority = 2;
  }

  // use inherited parent function
  // processAnswer(message) {
  // }

  getPacket() {
    if (
      sys.config.autoSetTime &&
      schema.unitTypes.Clock.status.__.RWoperatorLevel <= opLvl &&
      Date.now() > this.#lastSetTimeTime + 100000
    ) {
      // once in 100 sec
      this.#lastSetTimeTime = Date.now();
      let d = new Date(Date.now());
      for (let b in sys._subUnits._Box) // first b , if any
        return getCommand(
          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(),
            },
          },
          true,
        ); // broadcast
    }

    this.#counter++;
    this.#counter &= 15;
    this.timeout = Date.now() + 2000;
    switch (this.#counter) {
      default:
        return getRequest({
          id: [0xff, 0xff], // THIS.THIS module - check online
          unitType: 'Boot',
          cmd: {
            status: {},
          },
        });
      case 12:
      case 2:
        return getRequest({
          id: [2, 0, 1, 0], // broadcast to all boxes, 1st module, check for reboot
          unitType: 'RingUPSmodule',
          cmd: {
            info: {},
          },
        });
      case 7:
        return getRequest({
          id: [1, 0], // broadcast to all boxes, 1st module
          unitType: 'Boot',
          cmd: {
            status: {},
          },
        });
    }
  }
}

/*********************************************************************************
**********************************************************************************
**********************************************************************************
**********************************************************************************
**********************************************************************************
  upload cfg dispatcher
  will load all parents and all chidren recursively of this unit. will clear after finished
**********************************************************************************
**********************************************************************************
**********************************************************************************
**********************************************************************************
**********************************************************************************/
var sendingUnit; // used by Sergey/grey
var dispatcher_tasks = {};
var uploadCfgcurSendingUnit;
///////////////// -------------- used in viewGray and Sergey
function justSendConfigToPPK(unit, sut, only1) {
  dispatcher_tasks[253].startSendCfg(unit, sut, only1);
}
///////_2 -------------- used in viewGray and Sergey
function stopSendingUnit(text) {
  if (sendingUnit) {
    sendingUnit = undefined;
    modelChanged('stopSendingUnit', undefined, text);
    setSubscribe();
    if (failedUnits.length) alert('failed upload:\r\n' + failedUnits);
  }
}
var failedUnits = '';
class cfgUploadDispatcher extends heartBeatDispatcher {
  #sending1unit;
  // curSendingUnit; // set null to start from beginning
  #sendingUnitSut; // limit to "sut" subunits of this unit - use from "list of subunits"
  #sendingUnitStage;
  #sendingUnitRetries;
  #deltaTimeout;
  #maxRetries = 10;

  constructor() {
    super();
    this.active = 0;
    this.priority = 0;
    uploadCfgcurSendingUnit = 0;
  }

  #stopIt(text) {
    this.active = 0;
    if (text) stopSendingUnit(text);
  }

  /*
   * to iterate all units below "top"
   * returns next
   */
  ////////_23
  #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 = -1; // none
        ud_ = schema.unitTypes[cu._typeName].__;
        let suts = ud_.subUnits;
        sutns = {};
        for (let subunitSut in suts) {
          let nn = suts[subunitSut].number;
          let sud = cu._subUnits[subunitSut];
          if (typeof nn !== 'number') nn = parseInt(n);
          if (maxSutn < nn && sud && !sud.unused && !sud.virtual) maxSutn = nn;
          sutns[nn] = subunitSut;
        }
      } 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 && this.#sendingUnitSut) sut = this.#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 && this.#sendingUnitSut) return 0; // no units below - done
        }
      } catch {}
    }

    function searchHorizontal(u, sutOnly) {
      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 && sutOnly)
            // 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, this.#sendingUnitSut);
      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;
  }

  //////////_55
  #getNextCurSending() {
    if (!sendingUnit) {
      stopSendingUnit('uploading stopped');
      return;
    }

    this.#sendingUnitRetries = 0;
    let done = false;
    // select unit to upload

    if (!uploadCfgcurSendingUnit) {
      // todo  - process user IDs - special case

      // first go  up to thr Module to be sure all up units are created
      // starting, go up
      uploadCfgcurSendingUnit = sendingUnit;
      this.#sendingUnitStage = 0;
      if (uploadCfgcurSendingUnit._sut == '_System' || !uploadCfgcurSendingUnit._sut) {
        this.#sendingUnitStage = 1;
        this.#getNextCurSending();
      } else while (uploadCfgcurSendingUnit._sut != '_Box') uploadCfgcurSendingUnit = uploadCfgcurSendingUnit._up;
      // we go up to the Box.... why ?
    }

    if (!this.#sendingUnitStage) {
      // currently sending units - direct parents of sendingUnit
      if (uploadCfgcurSendingUnit._deleted) {
        // do not go down - parent was deleted - they are all deleted
        stopSendingUnit('done uploading');
        return;
      }
      if (uploadCfgcurSendingUnit !== sendingUnit) {
        // still higher up, find next down to sendingUnit
        let uuu = sendingUnit;
        while (uuu._up !== uploadCfgcurSendingUnit) uuu = uuu._up;
        uploadCfgcurSendingUnit = uuu;
      }
      if (uploadCfgcurSendingUnit === sendingUnit) {
        // went down to sending unit itself, next time we will use another algorithm
        this.#sendingUnitStage = 1;
      }
    } else {
      // sendingUnit itself alreafy sent. now sending units below - all children will be sent !
      do {
        if (uploadCfgcurSendingUnit == sendingUnit) {
          // shoud we go down ?
          if (this.#sending1unit) {
            stopSendingUnit('done 1');
            return;
          }
        }

        uploadCfgcurSendingUnit = this.#nextUnit(uploadCfgcurSendingUnit, sendingUnit, this.#sendingUnitSut);
        if (!uploadCfgcurSendingUnit) break;
        done = false;
        for (let n in uploadCfgcurSendingUnit._desc.config)
          if (n != '__' && !uploadCfgcurSendingUnit._desc.config[n].unused) {
            done = true;
            break;
          }
      } while (
        (!uploadCfgcurSendingUnit._sut || uploadCfgcurSendingUnit._sut == '_Box') &&
        !(done || uploadCfgcurSendingUnit._desc.__.hardware || uploadCfgcurSendingUnit._desc.__.software)
      );
    }

    // got next unit (may be upper) or no more units
    if (!uploadCfgcurSendingUnit) {
      stopSendingUnit('done uploading');
      return;
    }

    switch (uploadCfgcurSendingUnit._sut) {
      case '_Box':
        this.#maxRetries = 12;
        break;
      case 'Module':
        this.#maxRetries = 26;
        break;
      default:
        this.#maxRetries = 10;
        break;
    }
    systemLog(false, 'sending to PPK : ' + getUnitName(uploadCfgcurSendingUnit));

    // got next unit
  }

  startSendCfg(unit, sut, only1) {
    if (!unit) return;
    this.#sending1unit = only1;
    this.#sendingUnitSut = sut;
    sendingUnit = unit; // global, visible to grey / view
    failedUnits = '';
    uploadCfgcurSendingUnit = undefined;
    this.active = 1;
    this.timeout = 0;
    systemLog(false, schema.dictionary['2ppk'][lang] + ' ' + getUnitName(unit));

    let cu = unit;
    while (cu && cu._sut != '_Box') cu = cu._up;
    this.#deltaTimeout = cu?._sun == dispatcher.thisBox ? 300 : 300 * Object.keys(sys._subUnits._Box).length;
  }

  processAnswer(message) {
    processMessage(message);

    if (uploadCfgcurSendingUnit != message.u) return;
    if (uploadCfgcurSendingUnit._sut == '_Box') return this.getPacket();

    if (uploadCfgcurSendingUnit._deleted && !message.unitType) {
      this.#getNextCurSending();
      deleteUnit(message.u);
      return this.getPacket();
    }

    if (message.cmd?.info?.timeFromStart < 10)
      // restart after erase was
      this.#sendingUnitRetries = 16; // done erase

    if (!message.cmd.config) return;
    const sent = Object.keys(message.u.config);
    const rcvd = Object.keys(message.cmd.config);
    for (let pn of sent) // compare only parameters present both in CFG and in PPK
      if (rcvd.includes(pn) && message.u.config[pn] != message.cmd.config[pn]) this.getPacket(); // bad answer, repeat
    this.#getNextCurSending();
    return this.getPacket();
  }

  getPacket() {
    this.timeout = Date.now() + this.#deltaTimeout; // to retry

    if (!sendingUnit) {
      this.active = 0;
      return null;
    }
    if (!uploadCfgcurSendingUnit) this.#getNextCurSending();
    else if (this.#maxRetries < this.#sendingUnitRetries) {
      const uname = getUnitName(uploadCfgcurSendingUnit, 1);
      systemLog(false, '10 retries - FAILED -' + uname);
      failedUnits += uname + '\r\n';
      this.#getNextCurSending();
    }

    if (!this.active) return;

    let printLog = this.#sendingUnitRetries++ == 0;

    while (uploadCfgcurSendingUnit?._sut == '_Box') {
      this.#deltaTimeout =
        uploadCfgcurSendingUnit?._sun == dispatcher.thisBox ? 300 : 300 * Object.keys(sys._subUnits._Box).length;
      if (
        uploadCfgcurSendingUnit._configPPK &&
        uploadCfgcurSendingUnit._configPPK.SN &&
        uploadCfgcurSendingUnit._configPPK.SN != uploadCfgcurSendingUnit.config.SN
      ) {
        switch (this.#sendingUnitRetries & 1) {
          case 1:
            if (this.#sendingUnitRetries == 1)
              systemLog(false, 'set box address ' + getUnitName(uploadCfgcurSendingUnit, 1));
            this.timeout = Date.now() + 500; // no matter - local or not
            return getPacketSetBoxAddress(uploadCfgcurSendingUnit);
          case 0:
            return getRequest({
              id: [1, uploadCfgcurSendingUnit._sun], // box selected, 1st module
              unitType: 'Boot',
              cmd: { status: {} },
            });
        }
      } else this.#getNextCurSending();
    }

    if (!sendingUnit) this.#stopIt();
    if (!this.active) return;

    // prepare if Module as a whole
    if (
      !this.#sending1unit &&
      uploadCfgcurSendingUnit?._sut == 'Module' &&
      ((sendingUnit._sut == 'Module' && !this.#sendingUnitSut) ||
        sendingUnit._sut == '_Box' ||
        sendingUnit._sut == '_System')
    ) {
      if (this.#sendingUnitRetries < 16) {
        if ((this.#sendingUnitRetries & 3) == 1) {
          systemLog(false, 'erasing ' + getUnitName(uploadCfgcurSendingUnit, 1) + ' == wait 3 sec');
          this.timeout = Date.now() + 3000;
          return getCommand(uploadCfgcurSendingUnit, { eraseAll: {} });
        } else {
          this.timeout = Date.now() + 500;
          return getCommand(uploadCfgcurSendingUnit, { info: {} });
        }
      } else printLog = this.#sendingUnitRetries == 3;
    }

    // really send config
    let cc = 'sending cfg ';
    if (uploadCfgcurSendingUnit) {
      let ud_ = uploadCfgcurSendingUnit?._up?._desc?.__;
      if (ud_) {
        let ud_s = ud_.subUnits[uploadCfgcurSendingUnit._sut];
        let doDelete = (ud_s.software || ud_s.hardware) && uploadCfgcurSendingUnit._deleted;
        if (doDelete) uploadCfgcurSendingUnit._configured = false;
        // if (doDelete && uploadCfgcurSendingUnit._configured) doDelete = false;
        let req;
        if (doDelete) {
          cc = 'deleting ';
          req = sendCommandDelete(uploadCfgcurSendingUnit, true);
          debugStop = true;
        } else {
          if (!uploadCfgcurSendingUnit.config) uploadCfgcurSendingUnit.config = {};
          req = getCommand(uploadCfgcurSendingUnit, { config: uploadCfgcurSendingUnit.config });
        }
        if (printLog) systemLog(false, cc + getUnitName(uploadCfgcurSendingUnit, 1));
        return req;
      }
    } //else stopSendingUnit('done');
  }
}

/*********************************************************************************
**********************************************************************************
**********************************************************************************
  subscribe dispatcher - now only for CFG to get from PPK
  250

              if (((buf->to.tag & 7) == 2 && buf->to.category == CATEGORY_CONFIRM_OK) // tag in send on subscr message = 3LSB = subscr

**********************************************************************************
**********************************************************************************
**********************************************************************************/
class subscribeDispatcher extends heartBeatDispatcher {
  #needConfirm = [];
  #totalTimeOut = 0;

  constructor() {
    super();
    this.active = 0;
    this.priority = 0;
    this.needCfg = [];
  }

  getPacket() {
    const now = Date.now();

    if (this.#totalTimeOut < now) {
      this.#totalTimeOut = now + 10000;
      if (this.needCfg.length) systemLog(false, 'failed read CFG from ' + getUnitName(this.needCfg.shift()), 'Yellow');
      return this.getPacket();
    }

    // confirm not sent immediately back - but on timeout
    if (this.#needConfirm.length) {
      this.timeout = now + 20;
      return getRequest(this.#needConfirm.shift(), 0x13);
    }

    if (this.needCfg.length) {
      /**
       * on start, subscribe on cfg
       */
      this.timeout = now + 1000; // repeat 'subscribe' if not successfull
      let m = this.needCfg[0];
      if (!m._subscriptionOk) {
        modelChanged('startReadAllCfg', m);
        if (!m._subscriptionRetries) systemLog(false, 'start read CFG from ' + getUnitName(m));
        if (7 < m._subscriptionRetries++) {
          systemLog(false, 'failed read CFG from ' + getUnitName(this.needCfg.shift()));
          return this.getPacket();
        } else return getCommand(m, { subscribe: { justContinue: 6 } }); // config and stop
      } else return;
    }

    systemLog(false, 'ALL done - read CFG');
    this.active = 0;
    recalcLinks();
    modelChanged('doneReadAllCfg');
    modelChanged('afterLoad');
  }

  processAnswer(message) {
    this.timeout = Date.now() + 10; // small delay to let other tasks to send smth
    processMessage(message); // anyway
    if (message.m != this.needCfg[0] && !message.cmd.subscribe)
      return getCommand(message.m, { subscribe: { justContinue: 4 } }); // stop
    if (message.cmd?.subscribe) {
      message.m._subscriptionOk = true;
      this.timeout = Date.now() + 1000;
    } else {
      if (message.cmd?.config) {
        this.#totalTimeOut = Date.now() + 10000;
        // this.#needConfirm.push({id: message.id, unitType: message.unitType, cmd: { status: {}, }, }); // confirm
        // sent by subscription - but not immediate reply to subscription command
      } else if (message.cmd?.status && message?.u?._sut == 'Module' && message?.u?._subscriptionOk) {
        // done cfg read
        systemLog(false, 'done read CFG from ' + getUnitName(this.needCfg.shift()));
        //        location.reload();

        // probably will never go below - not a problem
        return getCommand(message.m, { subscribe: { justContinue: 4 } }); // stop
      }
      return getRequest({ id: message.id, unitType: message.unitType, cmd: { status: {} } }, 0x13);
    }
  }

  readCfg(module) {
    this.#totalTimeOut = Date.now() + 10000;
    module._subscriptionOk = false;
    module._subscriptionRetries = 0;
    this.timeout = 0;
    this.active = 1;
    for (let i = this.needCfg.length; i--; ) if (this.needCfg[i] == module) return;
    this.needCfg.push(module);
  }
}

/*********************************************************************************
**********************************************************************************
**********************************************************************************
  POLL dispatcher - to update periodically state of "current" unit selected
    tasks[105] = poll for states
**********************************************************************************
**********************************************************************************
**********************************************************************************/
class pollStateDispatcher extends heartBeatDispatcher {
  #curViewUnit = 0;
  #curViewSut = 0;
  #step = 0;
  #pos = 0;
  #deltaTimeout = 0;

  constructor() {
    super();
    this.active = 0;
    this.priority = 2;
  }

  getPacket() {
    if (!this.#curViewUnit) {
      this.active = 0;
      return;
    }
    this.timeout = Date.now() + this.#deltaTimeout;
    let cmd = 'status';
    this.#step &= 7;
    switch (this.#step) {
      case 6:
      case 2:
        cmd = 'info';
        break;
      case 0:
        cmd = 'config';
        break;
    }
    if (!this.#curViewSut) {
      this.#step++;
      return getCommand(this.#curViewUnit, { [cmd]: {} });
    } else {
      const k = Object.keys(this.#curViewUnit._subUnits[this.#curViewSut]);
      this.#pos++;
      if (this.#pos >= k.length) {
        this.#pos = 0;
        this.#step++;
      }
      return getCommand(this.#curViewUnit._subUnits[this.#curViewSut][k[this.#pos]], { [cmd]: {} });
    }
  }

  setUnitToPoll(u, sut) {
    if (!u) {
      u = this.#curViewUnit;
      sut = this.#curViewSut;
    }
    if (u._sut[0] == '_')
      this.active = 0; // _System or _Box - not even if list of modules in PPK
    else {
      this.active = 1;
      this.timeout = 0;
    }
    this.#curViewUnit = u;
    this.#curViewSut = sut;

    while (u && u._sut != '_Box') u = u._up;
    this.#deltaTimeout = u?._sun == dispatcher.thisBox ? 300 : 500 * Object.keys(sys._subUnits._Box).length;
  }
}

//// todo - use binary commands for AU and AREA/TS
function processBinaryAnswer(message) {
  try {
    switch (message.__binary__[0]) {
      case 1: // mod 3 only, request all AU state
        if (message.m._sun != 3) return; // todo - check module type compatible
        processMultipleAU(message.m, message.__binary__);
        break;
    }
  } catch {}
}

//////////_21 ================== extensively used by grey/Sergey ====
/// now does not subscribe, but sets to poll periodically at priority 2
function setSubscribe(u, force, sut) {
  // force not used = march 2023
  if (dispatcher_tasks?.[105]) dispatcher_tasks[105].setUnitToPoll(u, sut);
}

/// used by grey/Sergey and also blue view
// this one - sets Subscribe to get CFG.
function fastReadAllCfg(u) {
  if (!u) u = sys;
  switch (u._sut) {
    case '_System':
      for (let i in u._subUnits._Box) fastReadAllCfg(u._subUnits._Box[i]);
      break;
    case '_Box':
      for (let i in u._subUnits.Module) fastReadAllCfg(u._subUnits.Module[i]);
      break;
    default: // to be sure - go up to the Module
      let cu = u;
      while (cu && cu._sut != 'Module') cu = cu._up;
      dispatcher_tasks[250].readCfg(cu);
  }
}

/*********************************************************************************
**********************************************************************************
**********************************************************************************
  LOG dispatcher - to read log if changed
  tasks[102] = log read
**********************************************************************************
**********************************************************************************
**********************************************************************************/
var noPullLog = false;
class logReadDispatcher extends heartBeatDispatcher {
  #needRead = {}; // true in _sun of Box to read faster
  #curBoxIndx = 0;
  #curFastIndx = 0;
  #readMode = 0;

  constructor() {
    super();
    this.active = 1;
    this.priority = 2;
  }

  getPacket() {
    if (noPullLog) {
      this.timeout = Date.now() + 3000;
      return;
    }

    this.timeout = Date.now() + 500;

    let k = Object.keys(this.#needRead);
    let i;
    if (k.length && 7 & this.#readMode++) {
      // fast read 7/8
      this.#curFastIndx++;
      if (this.#curFastIndx >= k.length) this.#curFastIndx = 0;
      i = k[this.#curFastIndx];
      this.timeout = Date.now() + (i == dispatcher.thisBox ? 50 : 300 * Object.keys(sys._subUnits._Box).length);
      if (!sys._subUnits?._Box?.[i]) {
        delete this.#needRead[i]; // protection if box
        return;
      }
    } else {
      k = Object.keys(sys._subUnits?._Box);
      this.#curBoxIndx++;
      if (this.#curBoxIndx >= k.length) this.#curBoxIndx = 0;
      i = k[this.#curBoxIndx];
      this.timeout = Date.now() + 300;
    }

    const b = sys._subUnits?._Box[i];
    if (!b) {
      this.timeout = Date.now() + 1300;
      return;
    }

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

    return getCommand(b._subUnits.Module[1]._subUnits.Logger[1], {
      records: {
        // todo - use recordsMulty
        index: l.lastIndex + (l.confirmed && l.lastIndex ? 1 : 0),
      },
    });
  }

  processAnswer(message) {
    if (message.cmd?.records) {
      const l = message.b._logData;
      const u = message.u;
      processLogData(message); // logDta sure exist
      if (l.full || noPullLog) return;

      this.timeout = Date.now() + 50;
      // return getCommand(u, {
      //       records: {
      //         // todo - use recordsMulty
      //            index: l.lastIndex+ ((l.confirmed && l.lastIndex) ? 1 : 0),
      //       } } );
    } else processMessage(message); // just to be
  }
}

/************************************************
 * ***********************************************
 * **************** OSCILLOSCOPE *****************
 * ***********************************************
 * ***********************************************
 * *********************************************** */

class oscillographDispatcher extends heartBeatDispatcher {
  #unit;
  #deltaT = 300;
  #filled = 0;
  #totalTimeout = 0;
  #oscilloscopeResult = { data: [], data32: 0 };

  getPacket() {
    this.timeout = Date.now() + this.#deltaT;
    if (this.#totalTimeout < Date.now()) return this.#abort();
    if (!this.active || this.#unit?._typeName != 'AL2module') return;
    if (!this.#unit.oscilloscope.done) {
      if (this.#unit.oscilloscope.doCollect) return getCommand(this.#unit, { oscilloscope: this.#unit.oscilloscope });
      else return this.#abort();
    }
    for (let part = 0; part < 12; part++)
      if (this.#filled & (1 << part)) {
        console.log('query ' + part);
        let ab = new ArrayBuffer(7);
        let data = new Uint8Array(ab);
        data[0] = 6; // length
        data[1] = 0; // tag
        data[2] = 0x26; // category
        data[3] = this.#unit._up._sun; // box
        data[4] = this.#unit._sun; // module
        data[5] = 0x02; // BinaryRequestOscilloscopeResult
        data[6] = part;
        return data.buffer;
      }
    this.active = 0; // IMHO never should be here
  }

  #abort() {
    this.active = 0;
    systemLog(false, 'error rcvng resulting data, aborted oscilloscope', 'Red');
  }

  processAnswer(message) {
    this.timeout = Date.now() + this.#deltaT;
    if (!this.#unit) return this.#abort();
    if (message.from.category == 0x26 && message.__binary__[0] == 2 && this.#unit == message.m) {
      // binary
      let arr = message.__binary__;
      if (arr[1] > 11) return this.#abort(); // FAKE RETURN VALUE

      this.#filled &= ~(1 << arr[1]);
      let dataPtr = arr[1] * 125;
      for (let inData = 2; inData < 252; ) {
        let u16 = arr[inData++];
        u16 += arr[inData++] << 8;
        this.#oscilloscopeResult.data[dataPtr++] = u16;
      }

      switch (
        arr[1] // aux parameters
      ) {
        case 0:
          this.#oscilloscopeResult.SN = arr[252] + arr[253] * 256 + arr[254] * 256 * 256;
          this.#oscilloscopeResult.adrFound = arr[252];
          this.#oscilloscopeResult.alarmFound = !(arr[253] & 1);
          break;
        case 1:
          this.#oscilloscopeResult.type = arr[252];
          this.#oscilloscopeResult.mode = arr[253];
          this.#oscilloscopeResult.address = arr[254];
        case 2:
          this.#oscilloscopeResult.command = arr[252];
          this.#oscilloscopeResult.data32 = (this.#oscilloscopeResult.data32 & 0xffff0000) + arr[253] + arr[254] * 256;
          break;
        case 3:
          this.#oscilloscopeResult.data32 =
            (this.#oscilloscopeResult.data32 & 0xffff) + arr[252] * 256 * 256 + arr[253] * 256 * 256 * 256;
          this.#oscilloscopeResult.errors = arr[254];
          break;
        case 4:
          this.#oscilloscopeResult.previousDecision = arr[252];
          this.#oscilloscopeResult.bitErrors = arr[253];
          break;
      }
      if (this.#filled) return this.getPacket();
      else {
        modelChanged('oscilloscopeReady', this.#unit, this.#oscilloscopeResult);
        this.active = 0;
      }
    } else if (message.cmd.oscilloscope) {
      if (this.#unit?.oscilloscope?.control && message.cmd.oscilloscope.doCollect) {
        this.#unit.oscilloscope.control = 0;
        modelChanged('changed', this.#unit, { oscilloscope: { control: 1 } });
      }
      // hardcoded todo - schema ? - some JS in schema to "eval"...
      if (message.cmd.oscilloscope.done && !this.#unit.oscilloscope.done) {
        this.#filled = 0xfff;
        this.#totalTimeout = Date.now() + 10000;
        // became "done"
      }

      let ch = { oscilloscope: { doCollect: this.#unit.oscilloscope.doCollect, done: this.#unit.oscilloscope.done } };
      this.#unit.oscilloscope.doCollect = message.cmd.oscilloscope.doCollect;
      this.#unit.oscilloscope.done = message.cmd.oscilloscope.done;
      modelChanged('changed', this.#unit, ch);
      return; // will send on timeout
    } else processMessage(message); // just to be
  }

  constructor() {
    super();
    this.active = 0;
    this.priority = 0;
  }

  startRcvData(unit) {
    this.#deltaT = unit?._up?._sun == dispatcher.thisBox ? 300 : 200 * Object.keys(sys._subUnits._Box).length;
    this.timeout = 0;
    this.active = 1;
    this.#unit = unit;
    this.#totalTimeout = Date.now() + 30000;
    unit.oscilloscope.doCollect = 31;
    unit.oscilloscope.control = 1;
    unit.oscilloscope.done = 0;
    this.#filled = 0x1000; // now request 12 blocks of data. done when all bits are cleared
    modelChanged('changed', unit, { oscilloscope: { doCollect: 0, done: 1 } });
  }
}

function runOneOscilloscope(unit, msg) {
  if (unit?._typeName != 'AL2module' || msg != 'oscilloscope')
    systemLog(true, 'Error calling ' + msg + ' %% ' + unit._idString);
  else dispatcher_tasks[252].startRcvData(unit);
}

/*********************************************************************************
**********************************************************************************
**********************************************************************************
  kostyl !!!!!!!!!!!!!
  tasks[0x5A] = old subscribe command !!!!!!!!!!!!!!!!!

  if (((buf->to.tag & 7) == 2 && buf->to.category == CATEGORY_CONFIRM_OK) // tag in send on subscr message = 3LSB = subscr

  and we never use any subscribe except for readCFG
  INTELLECT - DOES !!!

**********************************************************************************
**********************************************************************************
**********************************************************************************/

class kostylForSearchAU extends heartBeatDispatcher {
  constructor() {
    super();
    this.active = 0;
  }

  processAnswer(message) {
    processMessage(message);
    if (message.cmd?.config) {
      // todo - make it cleaner - this kostyl to process auto-cfg-subscribe after AU search
      return getRequest({ id: message.id, unitType: message.unitType, cmd: { status: {} } }, 0x13);
    }
  }
}

/********************************************************
 * TREE functions
 * **************************************************** */

var sysTree = {};

function getTreeNodeForTid(tid) {
  // tid = string - tree ID
  if (!sysTree) sysTree = {};
  try {
    let tu = sysTree;
    let arr = tid.split('.');
    for (let i = 0; i < arr.length; ) {
      tu = tu._subUnits[arr[i++]][arr[i++]];
    }
    return tu;
  } catch {}
}

function deleteInTree(tid) {
  // tid = string - tree ID
  if (!sysTree) sysTree = {};
  if (tid)
    try {
      let tu = sysTree;
      let arr = tid.split('.');
      for (let i = 0; i < arr.length - 1; i++) tu = tu[arr[i]];
      delete tu[arr[arr.length - 1]];
    } catch {}
}

function getTreeIdForUnit(u) {
  if (u._desc.__.treeView) {
    let tid = '';
    if (u._sut != '_System') {
      let tid = '.' + u._sut + '.' + u._sun;
      if (u?.config?.areaNum) tid = u._up._subUnits.Area[u.config.areaNum]._treeId + tid;
      else {
        let up = u;
        do up = up._up;
        while (!up._desc.__.treeView);
        tid = up._treeId + tid;
      }
    }
    return tid;
  }
}

function setTreeIdForUnit(u) {
  if (u._desc.__.treeView) {
    let tid = getTreeIdForUnit(u);
    let oldTid = u._treeId;
    if (oldTid && oldTid != tid) {
      let arr = oldTid.split('.');
      if (arr.length) {
        let tu = sysTree;
        const l = arr.length - 1;
        for (let i = 0; i < l; i++) tu = tu[arr[i]];
        delete tu[arr[l]];
      }
      u._treeId = tid;
      arr = tid.split('.');
      if (arr.length) {
        let tu = sysTree;
        const l = arr.length - 2;
        for (let i = 0; i < l; i++) tu = tu[arr[i]];
        if (!tu[arr[l]]) tu[arr[l]] = {};
        tu[arr[l]][arr[l + 1]] = makeSubTreeSubUnits(u);
      }
    }
    return tid;
  }
}

var protectRecursion = 0;
function makeSubTreeSubUnits(u) {
  protectRecursion = 0;
  return makeTreeSubUnits(u);
}

function makeTreeSubUnits(u) {
  if (255 < ++protectRecursion) return;
  function makeTreeListFilter(subs, filter) {
    let ret = {};
    for (let n in subs)
      if (typeof filter == 'undefined' || filter == subs[n]?.config?.areaNum) ret[n] = makeTreeSubUnits(subs[n]);
    return ret;
  }
  let rets = {
    _sysName: getUnitName(u, 3),
    _sun: u._sun,
    _desc: u._desc,
    _typeName: u._typeName,
    _tidString: u._treeId,
    _idString: u._idString,
    _subUnits: {},
  };
  switch (u._sut) {
    default:
      // empty subunits == should be never called for not-for-tree units
      break;
    case '_System':
      rets._subUnits._Box = makeTreeListFilter(u._subUnits._Box);
      break;
    case '_Box':
      rets._subUnits = {
        Area: makeTreeListFilter(u._subUnits.Module[1]._subUnits.Area, 0),
        AL: makeTreeListFilter(u._subUnits.Module[3]._subUnits.AL),
        SK: makeTreeListFilter(u._subUnits.Module[2]._subUnits.SK),
      };
      break;
    case 'Area':
      rets._subUnits.Area = makeTreeListFilter(u._up._subUnits.Area, u._sun);
      break;
    case 'SK':
      if (['BISM', 'BISM2', 'BISM3'].includes(u._typeName))
        rets._subUnits.BISM1 = makeTreeListFilter(u._subUnits.BISM1);
      break;
  }
  return rets;
}

function getTreeList(u) {
  function getTreeListFilter(subs, filter) {
    let ret = {};
    for (let n in subs) {
      const uu = subs[n];
      if (typeof filter == 'undefined' || filter == uu?.config?.areaNum)
        ret[n] = {
          _sysName: getUnitName(uu, 3),
          _sun: uu._sun,
          _desc: uu._desc,
          _typeName: uu._typeName,
          _tidString: uu._treeId,
          _idString: uu._idString,
        };
    }
    return ret;
  }
  switch (u._sut) {
    case 'SK':
    case 'BISM1':
      switch (u._typeName) {
        case 'BISM':
        case 'BISM1':
          return getTreeListFilter(u._subUnits.LED);
        case 'BISM2':
        case 'BISM2slave':
          return getTreeListFilter(u._subUnits.Direction);
      }
      break;
    case '_Box':
      return {
        RelayGroup: getTreeListFilter(u._subUnits.Module[3]._subUnits.RelayGroup), //
        Out: getTreeListFilter(u._subUnits.Module[2]._subUnits.Out),
      };
    case 'AL':
      return {
        AU: getTreeListFilter(u._subUnits.AU),
      };
    case 'Area':
      return {
        InputLink: getTreeListFilter(u._up._subUnits.InputLink, u._sun), //
        OutputLink: getTreeListFilter(u._up._subUnits.OutputLink, u._sun),
      };
    default:
      return {};
  }
}

function getTreeListFromTid(tid) {
  return getTreeList(getUnitFromTreeId(tid));
}

function getUnitFromTreeNode(t) {
  if (t) return unitIdToUnit2(t._idString);
}

function getUnitFromTreeId(tid) {
  return getUnitFromTreeNode(getTreeNodeForTid(tid));
}

/*
type = optional
schema.unitTypes[sut].__.allRealHeirs = array of possible types. 
schema.unitTypes[t._typeName].__.subUnits[sut].defaultType = default if omitted
*/
function newTreeSubUnit(t, sut, type) {
  const okToCreate = {
    _System: ['_Box'],
    _Box: ['Area', 'RelayGroup', 'SK'],
    AL: ['AU'],
    Area: ['Area', 'InputLink', 'OutputLink'],
  };
  // not only tree-subs, but also list-subs
  let u = getUnitFromTreeNode(t);
  const sutList = okToCreate[u._sut];
  if (sutList && typeof sutList == 'Array' && sutList.includes(sut)) {
    let newUnit = addUnitToSut(u, sut, type);
    if (newUnit) {
      if (newUnit._desc?.config?.areaNum && u._sut == 'Area') {
        if (!newUnit.config) newUnit.config = {};
        newUnit.config.areaNum = u._sun;
      }
      setTreeIdForUnit(newUnit);
    }
    return newUnit;
  }
}

function delTreeSubUnit(t, sut, sun) {
  const okToDelete = {
    _System: ['_Box'],
    _Box: ['Area', 'RelayGroup', 'SK'],
    AL: ['AU'],
    Area: ['Area', 'InputLink', 'OutputLink'],
  };
  // not only tree-subs, but also list-subs
  let u = getUnitFromTreeNode(t);
  const sutList = okToDelete[u._sut];
  if (sutList && typeof sutList == 'Array' && sutList.includes(sut) && t._subUnits?.[sut]?.[sun]) {
    deleteUnit(u);
    delete t._subUnits[sut][sun];
  }
}
