/*
 * elsewhere must be defined
 *      processMessage(msg)
 * will be called with message like {"from":{"box":255,"mod":255:"unit":0},"to":{"box":255,"mod":255:"unit":6}},"id":[array of integers], "unitType":typename, "cmd":{"boot":{"status":{"version":127}}}}
 * !!! note !! message already decoded through schema
 *
 * to send smth call  function fillRequest(request)
 * request =
 * {
 *      id = [integers to put as path],           =   [ type 0 subid subid ... subid subid mod box ]
 *  unitType = name of unit type, e.g. "AxDPI"
 *      cmd = e.g. {"config":{"threshould":14}}
 *  from / to ignored even if present
 * }
 *
 */

/*
 * global variables
 */

var debugStop = false;

var printMessages = false; // will output to terminal all messages in JSON format

var emulationAnswerOn = false;
// if true - will never send smth to server,
// and will auto update adr of firmware while uploading, as if answered OK

var lastGotReply = 0; // absolute time of last packet, millisec, to check for offline

// var postRequests = []; // queue to send

/*
 * internal vars
 */

// var curRequest; // got from queue if any
// var countRetrySend; // 3 retries, else drop request and mark OFFLINE

var pollTimeOutId; // periodic "long GET" request
// var postTimeOutId; // send if present something in the queue
// var xh; // for post
var xph; // for poll (GET)

/**
 * Provides WebSocket connection to the server, as well as sending and receiving data over the connection
 */
class WebSocketConnection {
  /**
   * Constructor automatically determines the WebSocket URL based on the current page's URL.
   */
  constructor() {
    const currentUrl = new URL(window.location.href);
    const ipAddress = currentUrl.hostname;
    const port = currentUrl.port;

    this.ws_connection = undefined;
    this.online = false;
    this.errors = 0;
    this.url = `ws://${ipAddress}:${port}/api/websocket`;
  }

  #initWebSocket() {
    console.log('Attempting to connect to a WebSocket');
    console.log('Path: ' + this.url);

    this.ws_connection = new WebSocket(this.url);
    this.ws_connection.binaryType = 'arraybuffer';

    this.ws_connection.onopen = () => {
      console.log('WebSocket connection established');
      this.online = true;
    };

    this.ws_connection.onmessage = event => {
      try {
        let fullAnswer = new Uint8Array(event.data);
        // in fact, as of may 2025, each websocket message may have only one PPKR-protocol packet.
        // but for simplicity and compatibility we are processing them in the same manner

        if (debugStop) debugStop = false;

        parseAnswer(fullAnswer);
      } catch {
        console.error('onmessage error');
      }
    };

    this.ws_connection.onerror = error => {
      console.error('WebSocket error:', error);
      this.online = false;
      this.errors++;
    };

    this.ws_connection.onclose = () => {
      console.log('WebSocket connection closed');
      this.online = false;
      console.log('Reconnect WebSocket..');
      setTimeout(() => this.#initWebSocket(), 5000);
    };
  }

  /**
   * Send data via WebSocket
   * @param {ArrayBuffer} data array buffer to send
   */
  send(data) {
    if (this.ws_connection !== undefined && this.ws_connection.readyState === WebSocket.OPEN) {
      this.ws_connection.send(data);
    } else {
      console.warn("sendMessage: WebSocket isn't open!");
    }
  }

  /**
   * Initialize WebSocket and connect
   */
  init() {
    if (this.ws_connection === undefined) {
      this.#initWebSocket();
    } else if (this.ws_connection.readyState === WebSocket.OPEN) {
      console.log('WebSocket is already open.');
    }
  }
}

async function httpGet(url) {
  try {
    const response = await fetch(url);
    if (response.ok) {
      return true;
    } else {
      console.error('Response error:', response.statusText);
      return false;
    }
  } catch (error) {
    console.error('An error occurred while executing the request:', error);
    return false;
  }
}

const websocket = new WebSocketConnection();

var noUseWebSocket = 0;
/*
 * initialize at least once
 */
var alreadyWasAttemptToOpenWebSocket;
async function startWebSocket() {
  if (noUseWebSocket) {
    dispatcher.usingWebSockets = 0;
    return;
  }

  if (dispatcher.usingWebSockets) return;
  if (!alreadyWasAttemptToOpenWebSocket) {
    systemLog(false, 'Check if server supports WebSockets..');
    alreadyWasAttemptToOpenWebSocket = true;
  }

  let getPath = '/api/server-info';
  if (window.getPathApi) getPath = window.getPathApi + getPath;
  if (await httpGet(getPath)) {
    systemLog(0, 'websocket open successfully', 'Green');
    websocket.init();
    dispatcher.usingWebSockets = true;
    if (pollTimeOutId) window.clearTimeout(pollTimeOutId);
    if (xph) xph.abort();
    // if (xh) xh.abort();
    return;
  }
}

function startPollRcv() {
  // may be called multiple times to kick stalled rcptn
  if (pollTimeOutId) window.clearTimeout(pollTimeOutId);
  pollTimeOutId = 0;
  if (xph) xph.abort();
  xph = new XMLHttpRequest();
  xph.timeout = 11000; // 10 sec max for "long GET poll" --- new Mikalay server has 1 sec delay max
  xph.responseType = 'arraybuffer';
  xph.onreadystatechange = function () {
    if (this.readyState == 4) {
      if (this.status == 502) {
        // BAD GATEWAY = simply no new packets to GET
        //            to=100;
      }
      if (this.status == 200) {
        dispatcher.timeRcvdLast = Date.now();
        try {
          // got something - fast retry to get more
          let fullAnswer = new Uint8Array(this.response);
          // let c2a = new Uint8Array(this.response);
          parseAnswer(fullAnswer); // below in this file
        } catch {}
      }
      pollTimeOutId = window.setTimeout(pollHardware, 10); // just make it async - let other tasks chance -- todo -- is it necessary ?
    }
  };
  pollTimeOutId = window.setTimeout(pollHardware, 100); // just make it async - let other tasks chance -- todo -- is it necessary ?
  //  pollHardware();
}

// call only once
async function startHardwarePolling() {
  await startWebSocket();
  if (dispatcher.usingWebSockets) return;

  startPollRcv();

  // init XHR variables for POST
  // xh = new XMLHttpRequest();
  // xh.timeout = 2000; // POST should be fast normally
  // xh.onreadystatechange = function () {
  //   // if (postTimeOutId) window.clearTimeout(postTimeOutId);
  //   // postTimeOutId = 0;
  //   if (this.readyState == 4) {
  //     if (this.status == 200) {
  //     } else {
  //     }
  //   }
  // };

  // setPostHardware(300);
  // setPostHardware(postRequests.length > 1 ? 66 : 200);
  // postTimeOutId = window.setTimeout(postHardware, postRequests.length>1 ? 66 : 200); // set first timeouts to start XHR   was 4:100
}

/*
 * low level poll/post routines
 * */

function pollHardware() {
  if (!dispatcher.usingWebSockets) {
    // usually 10 millisec after previous finish
    //  let e = new Date();
    //  console.log("poll: "+e.getTime());
    // done-s replace "/s" to "/api/s" for dev and back for prod ============ if necessary, set global variable Window.getPathApi to "/api"
    let getPath = '/s'; //#'+Date.now();
    if (window.getPathApi) getPath = window.getPathApi + getPath;
    xph.open('GET', getPath, true);
    // xph.timeout = 10000; // POST should be fast normally
    xph.send(null); // just poll by GET
  }
}

// function setPostHardware(ddt) {
//   // if (postTimeOutId) return;
//   if (ddt < 50) {
//     ddt = 50;
//   }
//   if (postTimeOutId) window.clearTimeout(postTimeOutId);
//   postTimeOutId = 0;
//   postTimeOutId = window.setTimeout(postHardware, ddt);
// }

// function postHardware() {
//   //  e = new Date();
//   //  console.log("post attempt ="+postRequests.length+"="+e.getTime());

//   if ((!dispatcher.usingWebSockets && !xh) || (dispatcher.usingWebSockets && websocket.online == false)) {
//     setPostHardware(500);
//     return;
//   }
// }

/*
 *
 * routines to convert to/from binary message
 *
 */

// moved here from model since used mainly here. should not depend from upper-level code
function byte2string(b) {
  let stt = '';
  for (let j = 2; j--; ) {
    let dd = b;
    if (j) dd >>= 4;
    dd &= 0x0f;
    stt += hexd[dd];
  }
  return stt;
}

/*
 * converts binary to {id+cmd}
 *
 * answer =
 * {
 *      id = [integers to put as path],           =   [ type 0 subid subid ... subid subid mod box ]
 *      cmd = [{format:0 value:17 ... etc}{}{}{}]
 * }
 *
 * bin = Uint8Array starting from "from" (box mod idlsb idmsb)
 */

function decodeBinAnswer(bin) {
  // {id:arrayofintegers,idString:"",cmd:{Command:{}}} === id & idstring = from
  let id = []; /// not bytes - UIDs are full integers as varInt were
  let cmd = []; /// currently (may 2025) only one message possible. for future extentions
  let len = bin.length;
  let pos = 8; // start of subid or message

  if (len < 10) return null; // no valid command, may be ping, minimum is 2 bytes if message from boot

  id.push(bin[6]); // box from
  id.push(bin[7]); // mod
  if (bin[5] == 1) {
    // normal unit
    while (pos < len) {
      function getVarInt() {
        let v = 0;
        let shift = 0;
        while (pos < len) {
          let t = bin[pos++];
          v += (t & 127) << shift;
          if (!(t & 128)) break;
          shift += 7;
        }
        return v;
      }
      let suid = getVarInt();
      id.push(suid);
      if (suid) continue;
      id.push(getVarInt()); // type
      break;
    }
  }
  id.reverse();

  ids = '';
  let p = 6;
  if (id.length > 2) {
    do ids += byte2string(bin[p]);
    while (bin[p++]);
    do ids += byte2string(bin[p]);
    while (bin[p++] & 0x80);
  } else for (; p < 8; p++) ids += byte2string(bin[p]); // only box+mod as HEX bytes

  if (pos < len) cmd = bin.subarray(pos);

  return {
    id: id,
    cmd: cmd,
    idString: ids,
  };
}

/*
 * converts {id+cmd} to binary
 *
 * request =
 * {
 *      id = [integers to put as path],           =   [ type 0 subid subid ... subid subid mod box ]
 *      cmd = [{format:0 value:17 ... etc}{}{}{}]
 * }
 *
 * returns
 *
 */

function makeBinRequest(request, category, tag) {
  // {id:uint8binary,cmd:{Command:{}}}
  if (!request.id) return; // cmd may be empty
  let ab = new ArrayBuffer(300);
  let data = new Uint8Array(ab);
  let d0 = makeBinAdr(request.id);
  //  returns   0 , 1/0 , box , mod , uid , uid .. uid , 0 , type
  if (d0.length < 4) return;
  try {
    let pos = 0;
    let len = d0.length;
    for (let i = 0; i < len; i++) data[pos++] = d0[i];

    let sm = JSON2protobuf(request.cmd);
    len = sm.length;
    for (let i = 0; i < len; i++) data[pos++] = sm[i];

    if (sm.length)
      // save command to tag - really used only for subscribe to detect asynch messages
      data[0] = sm[0];

    if (typeof category == 'number') data[1] = category;
    if (typeof tag == 'number') data[0] = tag;
    return data.buffer.slice(0, pos);
  } catch {
    return null;
  }
}

// call parameters as in fillRequest()
function getRequest(request, cat) {
  if (!request.id) return;
  let ab = fillRequest(request, cat, undefined, true);
  if (ab) {
    let ab8 = new Uint8Array(ab);
    let abc = new ArrayBuffer(ab8.length + 1);
    let abc8 = new Uint8Array(abc);
    abc8[0] = ab8.length;
    for (let i = 0; i < ab8.length; i++) abc8[i + 1] = ab8[i];
    return abc;
  }
}

/*
 * request =
 * {
 *      id = [integers to put as path],           =   [ type 0 subid subid ... subid subid mod box ] == only mod/box for Boot
 *      unitType = AxDPI or smth
 *      cmd = {"config":{"threshould":14}}   or smth similar
 * }
 * justReturn = true => do not push
 * returns request built
 */
function fillRequest(request, category, tag, justReturn) {
  // if (!online && request.unitType != 'Boot') return;

  let curReq;
  try {
    if (printMessages) systemLog(0, '<' + JSON.stringify(request), 'yellow');
    let r = {};
    r.id = request.id;
    // if(request.cmd.records)
    //    console.log(33);
    r.cmd = encodeCommandBySchemaForProtobuf(request.unitType, request.cmd); ////      cmd = [{format:0 value:17 ... etc}{}{}{}]   ===  to put as protobuf
    curReq = makeBinRequest(r, category, tag);
  } catch {}
  // if (curRequest) {
  //   // check not to put undefined - was once found while deleting multiple TSinputs in grey cfg with Area of this TSinputs unfolded
  //   if (!justReturn) postRequests.push(curRequest);
  // }
  if (!justReturn) addPacketToSendCommand(curReq, tag);
  //postHardware();
  else return curReq;

  // if(postTimeOutId) window.clearTimeout(postTimeOutId);
  // postTimeOutId = window.setTimeout(postHardware, 3); // if 3 sec failed - wait 3 sec more
}

/*
 * start / restart / stop
 */

/*
 * procedures to convert unit id as array to/from (uint8) as in PPK packets;
 * arr = [mod, box] if to boot
 * arr = [type...SuIDpath...,mod,box]  if to oter units
 *
 * returns
 *   0 , 1 , box , mod , uid , uid .. uid , 0 , type
 *
 */

function makeBinAdr(arr) {
  // gets array returns Uint8array
  let pp = arr.length;
  const ab = new ArrayBuffer(300);
  var data = new Uint8Array(ab);
  let pos = 4;
  if (pp) data[2] = arr[--pp]; // box (only one value in array)
  if (pp) data[3] = arr[--pp]; // mod if not _Box
  if (pp) data[1] = 1;
  // not just box.mod
  // units = always even sys
  else data[1] = 0; // boot = always even sys (empty array)
  data[0] = 0;

  while (pp) pos = putVarIntBuf(arr[--pp], data, pos);

  return data.slice(0, pos);
}

/*
 * procedures to convert unit id as array to/from (uint8) as in link IDs;
 */
function makeShorterAdr(arr) {
  // gets array returns Uint8array
  let pp = arr.length;
  const ab = new ArrayBuffer(300);
  var data = new Uint8Array(ab);
  let pos = 0;
  if (pp) data[pos++] = arr[--pp]; // box (only one value in array)
  if (pp) data[pos++] = arr[--pp]; // mod if not _Box
  while (pp--) pos = putVarIntBuf(arr[pp], data, pos);
  return data.slice(0, pos);
}

// function makeAdrFromBin(data) {
//   // gets array returns Uint8array
//   let arr = [];
//   let pp = data.length;
//   if (pp <= 2) return arr; // sys
//   arr.push(data[2]); // box
//   if (pp === 3) return arr; // _Box
//   arr.push(data[3]); // mod
//   if (data[1] === 0) return arr; // boot
//   let pos = 4; // now listof varInt
//   while (pos < pp) {
//     let ans = getVarIntBuf(data, pos);
//     pos = ans.p;
//     arr.push(ans.v);
//   }
//   arr.reverse();
//   return arr;
// }
// done-ao -- bug was if unit == box or boot
function makeAdrFromBin(data) {
  // gets array returns Uint8array
  let arr = [];
  let pp = data.length;
  if (pp > 2) {
    // else sys
    arr.push(data[2]); //  box
    if (pp != 3 && data[3]) {
      // else _Box
      arr.push(data[3]); // mod
      if (data[1] != 0) {
        // else boot
        let pos = 4; // now listof varInt
        while (pos < pp) {
          let ans = getVarIntBuf(data, pos);
          pos = ans.p;
          arr.push(ans.v);
        }
      }
    }
  }
  arr.reverse();
  return arr;
}

// data = reverse array [ type 0 suid...suid mod box ]
function makeAdrFromShorterBin(data) {
  // gets array returns Uint8array
  let arr = [];
  let pp = data.length;
  let pos = 0;
  while (pos < pp) {
    let ans;
    if (pos < 2) arr.push(data[pos++]);
    else {
      let ans = getVarIntBuf(data, pos);
      arr.push(ans.v);
      pos = ans.p;
    }
  }
  arr.reverse();
  return arr;
}

function compareID(id1, id2) {
  if (!id1 || !id2) return false;
  if (id1?.length !== id2?.length) return false;
  if (id1[id1.length - 1] == 255) id1[id1.length - 1] = dispatcher.thisBox;
  if (id2[id2.length - 1] == 255) id2[id2.length - 1] = dispatcher.thisBox;
  if (id1[id1.length - 2] == 255) id1[id1.length - 2] = dispatcher.thisModule;
  if (id2[id2.length - 2] == 255) id2[id2.length - 2] = dispatcher.thisModule;

  for (let n = id1.length; n--; ) {
    if (id1[n] != id2[n]) return false;
  }
  return true;
}

/*/////////////////////////////////////////////////////////////
 * main routine to process packet got by GET or from websocket
 */ ///////////////////////////////////////////////////////////

function parseAnswer(fullAnswer) {
  // uint8array, probably with several protocol packets concatenated

  lastGotReply = Date.now();


if(fullAnswer[13] == 0x13 && fullAnswer[14] == 0x12 ) {
  //debug
  let c = 21;
}

  while (fullAnswer.length) {
    let l = fullAnswer[0] + 8 + 1; // + header + length byte
    if (l > fullAnswer.length) l = fullAnswer.length;
    let answer = fullAnswer.slice(1, l);
    fullAnswer = fullAnswer.slice(l);

    function adr(u8ar) {
      // uint8array
      return {
        box: u8ar[2],
        mod: u8ar[3],
        category: u8ar[1], // now is called "category" in PPKR source code
        mark: u8ar[0], // also "tag"
      };
    }

    if (answer.length < 8) return; // not even correct header

    var a = {};
    a.to = adr(answer.subarray(0, 4));
    a.from = adr(answer.subarray(4, 8));

    const tag = a.to.mark;
    if ((tag & 0xf0) == 0xf0 && tag != 0xfe) prepareEndOfBlockDispatcher();

    let idcmd = decodeBinAnswer(answer);

    if (idcmd) {
      a.id = idcmd.id; //
      a.idString = idcmd.idString;

      let q = idcmd.id[0]; // [0] must be unit type if not boot
      if (idcmd.id.length == 2) {
        q = -1; // boot
        if (idcmd.id[1] == 255 && dispatcher.thisBox) idcmd.id[1] = dispatcher.thisBox; // !!!!!!!!!!!! to compare
        if (idcmd.id[0] == 255 && dispatcher.thisModule) idcmd.id[0] = dispatcher.thisModule;
      }

      a.unitType = schema.unitTypesNumbers[q];

      a.__binary__ = idcmd.cmd; // binary type answer, if necessary
      if (a.from.category != 0x26)
        // binary
        a.cmd = decodeAnswerBySchemaFromProtobuf(q, protobuf2JSON(idcmd.cmd));

      if (processMessageCheckBoxModule(a)) return; // no box.mod found

      if (dispatcher_tasks[tag]) {
        sendPacketDispatcher(dispatcher_tasks[tag].processAnswer(a), tag);
      } else processMessage(a); // to common processor just to be

      // if (checkUpdate(a)) {
      //   if (printMessages) systemLog(0, '>' + JSON.stringify(a), 'yellow');
      //   processMessage(a);
      // }
    }
  }
}

///////////////====================================
// dispatcher of packets to rcv
///////////////====================================
/*
variables:
  lastTimePacketRcvd - in milliseconds, when any packet - may be ping - was rcvd.
  periodicwatchDogToRcvDispatcher == just to be sure that we did not missed lastTimePacketRcvd more then by 10 000 msec - in fact 1000 after build 3473
*/

///////////////====================================
// dispatcher of packets to send
///////////////====================================
/*
tasks     = object of objects, keys are "tags" to set and to process when returned.  
Note: tags 0xf0..ff will stop any activity on subscriptions and ring - only 0xF* packets will go through the ring
except for 0xFE - which will restore normal processing

every object has parameters:
  active   ==  bool == use it
  category == when active, process all rcvd packets with this "category".
  priority ==  0..3 == 
              if 0 - use only this task until not active (probably also will inject ping packets). == reflash
              if 1 - use it each time it is possible (any task of priority-1) == upload config, do oscill, any simple manual command. may be interchanged with priority 2 or 3
              if 2 - use it when no priority-1 task is ready to send
              if 3 - use it only if no other task pending - ping, poll broadcast, ensure no commloss
  timeout == time in milliseconds (since epoch) when we will retry this task (no answer rcvd on previous packet)
and methods:
  processAnswer(message) - call as in function processMessage(message), returns (if not undefined) new packet
                          Note: cmd.__binary__ is just binary message after ID
  getPacket() - returns (if not undefined) new packet. should return packet or adjust timeout
                packets returned are as from function buildUpdate() or makeBinRequest() - ready to send ArrayBuffer
  parameter == 1 if called directly from dispatcher, on timeout (not from inside "processAnswer")


  supposed that after init - tasks are not added anymore

Note:
  tasks[100] is "ping" - it (and only it) may be used while priority-0 task is waiting for answer.
  tasks[100].timeout updated every time some packet is sent

  tasks[101] = manual commands
  tasks[102] = log read
  tasks[104] = periodic broadcast  
  tasks[105] = poll for states

  ////////////// 0-priority tasks blocking all other activity /////////////
  tasks[250] =  subscribe - now only for CFG download
  tasks[251] = full flash download
  tasks[252] = oscill download
  tasks[253] = cfg upload
  tasks[255] = firmware update processor
  ////////////// 2-priority
  tasks[254] switches off block of any non-firmware activity - send 10 times broken packet to boot

dispatcher's variables:
  timeToSendNext == time in milliseconds (since epoch - as Date.now() ) when next time attempt to send smth. same as timeToSendReady or smtms long enough to wait for answer
  timeToSendReady == time in milliseconds (since epoch - as Date.now() ) when previous packet is gone (according to length in bytes and 115200 bit rate) + 10%
  timerToSend == normally will wake exactly when timeToSend reached
  watchDogToSendDispatcher == just to be sure that we did not missed periodicTimerToSend more then by 500 msec
  lastTaskProcessedToSend == index in array of tasks. last used task. if priority0 - will be reused until not active. if priority 1 - will search for other priority1 task

on wire, tag (mark, cookie) will be set taskIndex and any answer, if rcvd, with same tag, will be dispatched at appropriate task.

*/

var dispatcher = {};
var online; // hardware answering at least with some data, also calls "modelChanged" === used by grey/Sergey

function setTimerToSendDispatcher(ddt) {
  // also called with 0 parameter when new dispatcher task got active
  // delay from "now" in milliseconds
  const now = Date.now();
  if (dispatcher.timerToSend) {
    window.clearTimeout(dispatcher.timerToSend);
    if (dispatcher.timeToSendNext > now) ddt += dispatcher.timeToSendNext - now; // if smth called "@"sendPacket" before
  }
  if (!ddt || ddt < 1) ddt = 1;
  // also if ddt is omitted
  else if (ddt > 1000) ddt = 1000;
  dispatcher.timerToSend = 0;
  dispatcher.timeToSendNext = now + ddt - 1;
  dispatcher.timerToSend = window.setTimeout(doDispatcherSend, ddt);
}

function watchDogDispatcherSendFunction() {
  if (Date.now() > dispatcher.timeToSendNext + 1100)
    // sure more then possible timeout
    // 1 second after correct time to send new packet - just to be, for sake of impossible case of failed main timer
    setTimerToSendDispatcher(100);
}

var delayBtwUsartPackets = 1; // 28june - server has 10 ms delay between packets to serial

function sendPacketDispatcher(packet, tag) {
  if (!packet || typeof packet == 'number') return;
  if ((tag & 0xf0) == 0xf0 && tag != 0xfe) prepareEndOfBlockDispatcher();
  // packet = byteArray
  if (emulationAnswerOn || !packet || !packet?.byteLength) return; // just drop packets as if sent out, or ignore NULL packets
  tag = Number(tag);
  if (tag != NaN) {
    u8a = new Uint8Array(packet);
    if (u8a.length > 1) u8a[1] = tag;
  }

  // 28june - server has 10 ms delay between packets to serial
  const dt = delayBtwUsartPackets + Math.ceil((packet.byteLength + 12) / 10); // 115200, 8N1 = 10 bits per byte == 11520 bytes/sec, and 15% safety margin
  let now = Date.now();
  dispatcher.timeToSendReady = now + dt;
  setTimerToSendDispatcher(dt);
  dispatcher_tasks[100].timeout = now + 5000; // ping delayed
  if (dispatcher.usingWebSockets) {
    websocket.send(packet);
  } else {
    let getPath = '/s';
    if (window.getPathApi) getPath = window.getPathApi + getPath;
    let xh = new XMLHttpRequest();
    xh.open('POST', getPath, true);
    xh.timeout = 2000; // POST should be fast normally
    xh.send(packet);
  }
}

// called by timers
function doDispatcherSend() {
  let now = Date.now();

  if (now > lastGotReply + 15000 && online) {
    online = false;
    // dispatcher.thisBox = dispatcher.thisModule = 255;
    modelChanged('offline', undefined, {
      box: dispatcher.thisBox,
      mod: dispatcher.thisModule,
    });
  }

  if (!sys?._initFinished) {
    setTimerToSendDispatcher(500);
    return;
  }

  if (now < dispatcher.timeToSendNext) {
    // error
    setTimerToSendDispatcher(dispatcher.timeToSendNext - now);
    return;
  }

  const keys = Object.keys(dispatcher_tasks);
  // first check this is priority-0 or there is some priority-0
  for (let i = keys.length; i--; ) {
    // just limit times to seek.
    if (dispatcher.lastTaskProcessedToSend >= keys.length) dispatcher.lastTaskProcessedToSend = 0;
    const key = keys[dispatcher.lastTaskProcessedToSend];
    let task = dispatcher_tasks[key];
    if (task.active && !task.priority) {
      // found some 0-pry task. we will process only this task or may be task0
      let newTimeout = task.timeout;
      if (now < task.timeout) {
        // priority-0 task is not ready now
        task = dispatcher_tasks[100];
        if (now < task.timeout) {
          // task-0 is not ready either
          if (task.timeout < newTimeout) newTimeout = task.timeout; // wake up for task 0
        } else {
          sendPacketDispatcher(task.getPacket(1), 100);
        }
      } else {
        sendPacketDispatcher(task.getPacket(1), key);
      }
      setTimerToSendDispatcher(newTimeout - now + 1); // no other task may be served - waiting for this task ready (or it will send when rcvd answer)
      return; // sure no more attempts to find any task to serve
    }
    dispatcher.lastTaskProcessedToSend++;
  }

  // now look for priority 1 or 2, currently lastTaskProcessedToSend incremented;
  let pri = 2;
  dispatcher.countP2 = (dispatcher.countP2 + 1) & 3;
  if (dispatcher.countP2) pri = 1; // 3 times pri-1 then once pri-2

  for (let i = keys.length; i--; ) {
    dispatcher.lastTaskProcessedToSend++; // start after last used
    // just limit times to seek
    if (dispatcher.lastTaskProcessedToSend >= keys.length) dispatcher.lastTaskProcessedToSend = 0;
    const key = keys[dispatcher.lastTaskProcessedToSend];
    let task = dispatcher_tasks[key];
    if (task.active && task.priority == pri && task.timeout <= now) {
      sendPacketDispatcher(task.getPacket(1), key);
      return;
    }
  }

  let newTimeout = now + 1000;
  // now look for priority 3, currently lastTaskProcessedToSend incremented;
  for (let i = keys.length; i--; ) {
    dispatcher.lastTaskProcessedToSend++;
    // just limit times to seek
    if (dispatcher.lastTaskProcessedToSend >= keys.length) dispatcher.lastTaskProcessedToSend = 0;
    const key = keys[dispatcher.lastTaskProcessedToSend];
    let task = dispatcher_tasks[key];
    if (task.active) {
      if (task.priority == 3 && task.timeout <= now) {
        sendPacketDispatcher(task.getPacket(1), key);
        return;
      }
      if (newTimeout > task.timeout) newTimeout = task.timeout; // nearest task ready
    }
  }
  // no packets found
  setTimerToSendDispatcher(newTimeout - now + 1);
}

/***************************************************************/
// class to ping if nothing to send, tag=100
// also ones in 10 sec broadcasts request to all PPK & all modules
/***************************************************************/
class heartBeatDispatcher {
  constructor() {
    this.active = 1; // always
    this.priority = 3;
    this.timeout = 0;
  }

  processAnswer(message) {
    return processMessage(message); // just update state if possible
  }

  getPacket() {
    let ab = new ArrayBuffer(5);
    let u8b = new Uint8Array(ab);
    u8b[0] = 4; // 0-length
    u8b[4] = u8b[3] = 0xff; // this.this
    return ab;
  }
}

async function watchDogDispatcherRcvFunction() {
  if (!noUseWebSocket && !dispatcher.usingWebSockets) await startWebSocket();
  if (!dispatcher.usingWebSockets && Date.now() > dispatcher.timeRcvdLast + 60000)
    // sure more then possible timeout
    // just to be, for sake of impossible case of failed main timer
    startPollRcv(); // just kick it if stalled
}

/***************************************************************/
// tag = 254 (0xfe)
// class to stop block of all activity - started after massive cfg upload (0xfd) or firmware upload (0xff) is done
/***************************************************************/
class endOfBlockDispatcher {
  constructor() {
    this.active = 0;
    this.priority = 2;
    this.timeout = 0;
    this.repeat = 0;
  }

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

  start() {
    this.repeat = 10;
  }

  processAnswer(message) {
    return processMessage(message); // just update state if possible
  }

  getPacket() {
    if (this.repeat) {
      this.repeat--;
      this.timeout = Date.now() + 200;
      return getRequest({
        id: [0, 0], // all.all
        unitType: 'Boot',
        cmd: {
          status: {},
        },
      });
      // let ab = new ArrayBuffer(6);
      // let u8b = new Uint8Array(ab);
      // u8b[0] = 5; // this.this, 0-length
      // u8b[5] = u8b[4] = u8b[3] = 0; // this.this, 1-length, impossible 0-command
      // return ab;
    }
    this.active = 0;
    return undefined;
  }
}

function prepareEndOfBlockDispatcher() {
  // will start after 0-priority task finished
  dispatcher_tasks[254].repeat = 10;
  dispatcher_tasks[254].active = 1;
}

/***************************************************************/
/***************************************************************/
/***************************************************************/
/***************************************************************/
/***************************************************************/
/***************************************************************/
/***************************************************************/
/***************************************************************/
// class to upload firmware for PPKR modules and BISMs
// tag = 255
/***************************************************************/

// global variables used by Sergey in grey
var fw2update; // FIXME = MUST be incapsulated in class sendFirmwareDispatcher. now used by grey typescripts. and by blue view.js also
var fwPercent = 0; // necessary to Sergey, OK

class sendFirmwareDispatcher extends heartBeatDispatcher {
  #position;
  unit2reflash;
  #bootUpdateId;
  #deltaTimeout = 0;
  #totalTimeout = 0;
  #repeat200 = 0;
  #retryCount = 0;

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

  processAnswer(a) {
    // ok answered
    if (!this.active) return;

    let cr = this.#checkMultiUpdate(a);
    if (cr) return cr;

    if (!compareID(a.id, this.#bootUpdateId)) return;

    let res;

    for (let msgn in a.cmd) {
      if (msgn == 'updateFirmware') {
        let c = a.cmd[msgn]; // bytes for SK
        if (this.#position) {
          this.#totalTimeout = Date.now() + 25 * this.#deltaTimeout; // failed update if this timeout
          if (this?.unit2reflash?._sut == 'Boot' && this?.unit2reflash?._up?._sun == 1) this.#totalTimeout += 10000;
        }
        if (a.unitType == 'Boot') {
          if (a.from.category != 0) return;
          if (this.#position || c.address <= 256) {
            // && (c.address >= this.#position || !c.address)
            // required 0 due to reboot ??? may be still answer from previous command
            if (this.#position == c.address) {
              this.timeout = Date.now() + 500; // to awoid flood
              return; // wait for timeout to retry send it
            } else this.#position = c.address;
            //if(postTimeOutId) {window.clearTimeout(postTimeOutId); postTimeOutId=0;}
            //            postStep=0; // will send fw ?????????
            res = this.getPacket();
          }
        } else {
          // BIS
          // let c = a.cmd.__binary__;
          if (a.from.category != 1) return;
          if (this.unit2reflash._sut == 'SK') {
            if (c[8] != this.unit2reflash._sun || c[9] != 0) return; // ignore
          } else {
            if (c[9] != this.unit2reflash._sun || c[8] != this.unit2reflash._up._sun) return; // ignore
          }
          this.#position = c[4] + c[5] * 256 + c[6] * 256 * 256 + c[7] * 256 * 256 * 256;
          if (c[2]) {
            systemLog(false, 'reflashing ' + getUnitName(this.unit2reflash) + ' done: OK', 'Chartreuse');
            this.stopIt(); // no multi for BIS now
          } else res = this.getPacket();
        }
      }
    }
    modelChanged('boot', this.unit2reflash);
    return res;
  }

  getPacket(f) {
    if (!this.active) return null;
    if (f) {
      this.#retryCount++;
      if (this.#retryCount > 5) this.#retryCount = 5;
      else this.#totalTimeout += 5000;
    } else this.#retryCount = 0;

    let dn = Date.now();
    let coef =
      sys?._subUnits?._Box?.[dispatcher.thisBox]?._subUnits?.Module?.[1]?._subUnits?.Ring?.[1]?.config?.baudRate;
    if (coef) coef++;
    else coef = 1;
    this.#deltaTimeout =
      200 +
      (this.#retryCount + 1) *
        (this.unit2reflash?._sun != dispatcher.thisBox ? (400 * Object.keys(sys._subUnits._Box).length) / coef : 0);
    this.timeout = dn + this.#deltaTimeout;
    // no answer timeout to retry same address write

    let cr = this.#buildMultiUpdate(f); // also checks this.#totalTimeout
    if (cr) {
      // console.log('build-get-Multi');
      return cr;
    }

    // console.log('getPac');

    if (this.#totalTimeout < dn) {
      // timeout - abort fW upload
      let infoString = 'reflashing ' + getUnitName(this.unit2reflash) + ' aborted: timeout';
      systemLog(false, infoString, 'red');
      multiUpdateErrors += infoString + '\r\n';
      this.#stepMultipleFirmwares(infoString);
      //else this.stopIt();
      //cmd={"reBoot":{}}; // done update
      modelChanged('boot', this.unit2reflash);
      return this.getPacket(f);
    }

    if (!this.active) return;

    try {
      let ml = 128;
      let ad2write = this.#position;

      if (this.unit2reflash._sut == 'Boot') {
        if (!ad2write) {
          if (!this.#repeat200) this.#totalTimeout += 5000; // to let erase all flash
          if (!(this.#repeat200++ & 3)) {
            // there is a probability that our command "write" at 0 address was not answered but successfull
            ml = 200;
          } else {
            ad2write = 200;
            ml = 56;
          }
        } else if (ad2write === 200) ml = 56; // first 256 need to be split into 200 and 56, then use 128-byte blocks
      } else {
        // BISM
        if (!this.#position) ml = 192;
        // 2 times 00 , then 2 times c0
        else if (ad2write === 192) ml = 64; // first 256 need to be split into 200 and 56, then use 128-byte blocks
      }
      let u8v = new Uint8Array(fw2update.slice(ad2write, ad2write + ml));

      fwPercent = Math.round((this.#position * 100) / fw2update.byteLength);

      if (emulationAnswerOn) this.#position += ml;

      modelChanged('boot', this.unit2reflash);
      let r = { id: this.#bootUpdateId, unitType: this.unit2reflash._typeName };
      if (this.unit2reflash._sut == 'Boot') {
        if (this.#position >= fw2update.byteLength) {
          systemLog(false, 'reflashing ' + getUnitName(this.unit2reflash) + ' done. success', 'Chartreuse');
          this.#stepMultipleFirmwares('done OK');
          //else this.stopIt();
          r.cmd = {
            reBoot: {},
          }; // done update
          modelChanged('boot', this.unit2reflash);
        } else
          r.cmd = {
            updateFirmware: {
              address: ad2write,
              data: u8v,
            },
          };
      } else {
        // bism
        const ab2 = new ArrayBuffer(300);
        var data2 = new Uint8Array(ab2);
        // file_write_request_t, then data
        let pp = 0;
        data2[pp++] = 0; // ID
        data2[pp++] = 0; // ID
        let eof;
        if (ad2write + ml >= fw2update.byteLength) {
          eof = 1;
          if (fw2update.byteLength > ad2write) ml = fw2update.byteLength - ad2write;
          else ml = 0;
        } else eof = 0;
        data2[pp++] = eof;
        data2[pp++] = ml;
        let ad = ad2write;
        for (let i = 4; i--; ) {
          data2[pp++] = ad & 0xff;
          ad = ad >> 8;
        }
        for (let i = 0; i < ml; i++) data2[pp++] = u8v[i];
        r.cmd = { updateFirmware: data2.slice(0, pp) };
      }
      return getRequest(r);
    } catch {}
    return;
  }

  // result = string
  //  called on each reflash end
  #stepMultipleFirmwares(result, color) {
    let dn = Date.now();

    if (!multipleFirmwares) {
      this.stopIt();
      return;
    }

    console.log(
      'step-Multi = ' +
        multipleFirmwares.stage +
        ' = ' +
        multipleFirmwares.curPPKsun +
        '.' +
        multipleFirmwares.curModSun,
    );

    this.#recalcDeltaTimeout();
    let cb = multipleFirmwares.curBin;
    if (cb) cb = ' with ' + cb.name;
    else cb = '';
    if (result) {
      switch (multipleFirmwares.stage) {
        case 'askModule':
        case 'askBoot':
          logMF('querying ' + multipleFirmwares.curShortName + ' = ' + result, color);
          break;
        default:
          logMF('reflashing ' + multipleFirmwares.curShortName + ' = ' + cb + ' : ' + result, color);
      }
    }

    multipleFirmwares.curModule = undefined;

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

    while (multipleFirmwares && !multipleFirmwares.curModule)
      switch (multipleFirmwares.stage) {
        case 'askModule':
        case 'askBoot':
          if (color == 'Red' && multipleFirmwares.curModSun == 1) {
            if (multipleFirmwares.curPPKsun == dispatcher.thisBox)
              abortAllFirmwareUpdates('cannot continue other PPKs and Modules', 'Red');
            else {
              multipleFirmwares.stage = 'nextPPK';
              logMF('cannot continue other PPKs and Modules', 'Red');
            }
          } else multipleFirmwares.stage = 'nextModule'; // in case cannot or should not
          break;
        // return;
        case 'writeBoot':
          logMF('boot reflash 100% done on ' + multipleFirmwares.curPPKsun + ':' + multipleFirmwares.curModSun, 'Lime');
          logMF('wait reboot for new BOOT to start ' + multipleFirmwares.curShortName);
          multipleFirmwares.stage = 'waitReBoot'; // in case cannot or should not
          this.#position = 0;
          this.#totalTimeout = dn + 15000; // fixed - to let reboot several times
          this.#wasRebooter = 1;
          multipleFirmwares.curModule =
            sys._subUnits._Box[multipleFirmwares.curPPKsun]._subUnits.Module[multipleFirmwares.curModSun];
          return;
        default:
          // seek for first ppk:mod
          multipleFirmwares.curPPKsun = dispatcher.thisBox;
          multipleFirmwares.curModSun = dispatcher.thisModule;
          if (!sys._subUnits._Box[multipleFirmwares.curPPKsun]) multipleFirmwares.stage = 'nextPPK';
          else if (!sys._subUnits._Box[multipleFirmwares.curPPKsun]._subUnits.Module[multipleFirmwares.curModSun])
            multipleFirmwares.stage = 'nextModule';
          else
            multipleFirmwares.curModule =
              sys._subUnits._Box[multipleFirmwares.curPPKsun]._subUnits.Module[multipleFirmwares.curModSun];
          multipleFirmwares.curShortName = ' ' + multipleFirmwares.curPPKsun + ':' + multipleFirmwares.curModSun;
          break;
        case 'writeApp':
          logMF('application reflash 100% done on ' + multipleFirmwares.curShortName, 'Lime');
          logMF('wait reboot for new APP to start ' + multipleFirmwares.curShortName);
          multipleFirmwares.stage = 'waitReBoot'; // in case cannot or should not
          this.#wasRebooter = 0;
          this.#position = 0;
          this.#totalTimeout = dn + 15000; // fixed - to let reboot several times
          multipleFirmwares.curModule =
            sys._subUnits._Box[multipleFirmwares.curPPKsun]._subUnits.Module[multipleFirmwares.curModSun];
          return; // wait timeout or done
        case 'nextPPK':
          if (
            multipleFirmwares.curPPKsun == dispatcher.thisBox &&
            sys._subUnits._Box?.[multipleFirmwares.curPPKsun]?._subUnits?.Module?.[1]?._subUnits?.Boot?.[1]?.status
              ?.fault
          ) {
            abortAllFirmwareUpdates("cannot use next box if no app on this box's 1-st module", 'Yellow');
            break;
          }

          multipleFirmwares.curPPKsun = getNext(sys, '_Box', multipleFirmwares.curPPKsun);
          if (!multipleFirmwares.curPPKsun) {
            abortAllFirmwareUpdates('done multiple firmwares', 'Chartreuse');
            break;
          }
          multipleFirmwares.curModSun = 0;
          multipleFirmwares.stage = 'nextModule';
        // no break
        case 'waitReBoot':
        // here only on timeout boot query
        case 'nextModule':
          multipleFirmwares.reflashAttempt = 0;
          multipleFirmwares.restartAttempt = 0;

          if (
            multipleFirmwares.curModSun == 1 &&
            sys._subUnits._Box?.[multipleFirmwares.curPPKsun]?._subUnits?.Module?.[1]?._subUnits?.Boot?.[1]?.status
              ?.fault
          ) {
            logMF('cannot use next module if no app on 1-st module', 'Yellow');
            multipleFirmwares.stage = 'nextPPK';
            break;
          }

          multipleFirmwares.curModSun = getNext(
            sys._subUnits._Box[multipleFirmwares.curPPKsun],
            'Module',
            multipleFirmwares.curModSun,
          );
          if (!multipleFirmwares.curModSun) multipleFirmwares.stage = 'nextPPK';
          else {
            multipleFirmwares.curModule =
              sys._subUnits._Box[multipleFirmwares.curPPKsun]._subUnits.Module[multipleFirmwares.curModSun];
            multipleFirmwares.curShortName = '' + multipleFirmwares.curPPKsun + ':' + multipleFirmwares.curModSun;
          }
          break;
        case 'writeBoot':
          multipleFirmwares.stage = 'askModule';
          logMF('querying Application version ' + multipleFirmwares.curPPKsun + ':' + multipleFirmwares.curModSun);
          return;
      }
    if (!multipleFirmwares) return; // no more PPK\
    multipleFirmwares.curBoot = multipleFirmwares.curModule._subUnits.Boot[1];
    multipleFirmwares.stage = 'askBoot';
    logMF('querying Boot version ' + multipleFirmwares.curShortName);
  }

  #wasRebooter = 0;
  // called before processPackage. return true if no further processing needed (never in this proc)
  #checkMultiUpdate(a) {
    processMessage(a); // to update status/info in SYS
    let dn = Date.now();
    while (multipleFirmwares) {
      let abox = a.from.box;
      let amod = a.from.mod;
      if (abox != multipleFirmwares.curPPKsun || amod != multipleFirmwares.curModSun) return false; // dont care
      switch (multipleFirmwares.stage) {
        case 'waitReBoot': // after boot or app update
          this.timeout = dn + this.#deltaTimeout;
          if (a.cmd.updateFirmware) return 1; // kostyl to skip multiple repeated answers
          if (a.m._sun == multipleFirmwares.curModSun) {
            console.log('wasRebooter= ' + this.#wasRebooter + ' = ' + JSON.stringify(a.cmd));

            if (this.#wasRebooter) {
              if (!a.from.category && a?.cmd?.status && a.cmd.status.bootVersion == multipleFirmwares.lastVersion) {
                // OK done
                multipleFirmwares.stage = 'askModule';
                logMF('rebooted after boot update - OK, version = ' + multipleFirmwares.lastVersion);
                multipleFirmwares.reflashAttempt = 0;
                multipleFirmwares.restartAttempt = 0;
                this.#totalTimeout = dn + 25 * this.#deltaTimeout;
                // sure no app after BOOT rewritten
                a.from.category = 1; // mimic module answer
                a.cmd.info = { appVersion: 0 };
                break; // process askModule as next "while"
              } // else will try to restart
            } else {
              // was app update
              if (a.from.category && a?.cmd?.info) {
                if (a.cmd.info.appVersion == multipleFirmwares.lastVersion) {
                  // reboot done successfully
                  multipleFirmwares.stage = 'nextModule';
                  multipleFirmwares.curBoot.status.fault = 0; // kostyl in case not yet got - or "step" will fail
                  this.#stepMultipleFirmwares(
                    'rebooted after app update - OK, version = ' + multipleFirmwares.lastVersion,
                  );
                  return this.getPacket();
                } else {
                  // bad version - will try to restart
                }
              } else if (!a.from.category && a?.cmd?.status && a.cmd.status.fault) {
                //
                if (multipleFirmwares.restartAttempt > 2) {
                  // at least twice do command restart
                  this.timeout = dn + 500; // to let reboot, then will ask
                  return getCommand(this.unit2reflash._up, { info: {} }); // will timeout if not
                } // else will fail below
              } else return getCommand(this.unit2reflash._up, { info: {} });
            }
          }

          console.log(' repeat? ' + multipleFirmwares.restartAttempt + ' : ' + multipleFirmwares.reflashAttempt);

          // not correct answer - restart again
          this.timeout = dn + 500; // to let reboot, then will ask
          if (multipleFirmwares.restartAttempt++ > 30) {
            console.log('multipleFirmwares.restartAttempt>30 ');

            // too many attempts to restart
            multipleFirmwares.restartAttempt = 0;
            if (multipleFirmwares.reflashAttempt++ > 2) {
              console.log('multipleFirmwares.reflashAttempt>2');

              // total fail, go to next mod/box
              if (this.unit2reflash._up._sun == 1) {
                // no use to go to next module
                if (this.unit2reflash._up._up._sun == dispatcher.thisBox) {
                  // no use to go to next box
                  abortAllFirmwareUpdates(
                    'failed to update MAIN ppk MAIN module ' +
                      (this.#wasRebooter ? 'BOOT' : 'APP' + ' @ ') +
                      getUnitName(this.unit2reflash._up),
                    'Red',
                  );
                  return;
                }
                // not thisBox - go to next box
                multipleFirmwares.stage = 'nextPPK';
                this.#stepMultipleFirmwares(
                  'failed to update MAIN module ' +
                    (this.#wasRebooter ? 'BOOT' : 'APP' + ' @ ') +
                    getUnitName(this.unit2reflash._up),
                  'Red',
                );
                return this.getPacket(); // to repeat while
              } else {
                multipleFirmwares.stage = 'nextModule';
                this.#stepMultipleFirmwares(
                  'failed to update ' +
                    (this.#wasRebooter ? 'BOOT' : 'APP' + ' @ ') +
                    getUnitName(this.unit2reflash._up),
                  'Red',
                );
                return this.getPacket(); // to repeat while
              }
            } else {
              console.log('refalsh attempt = ' + multipleFirmwares.reflashAttempt);

              // attempt to repeat last reflash
              logMF(
                'failed - repeating to update ' +
                  (this.#wasRebooter ? 'BOOT' : 'APP') +
                  ' @ ' +
                  getUnitName(this.unit2reflash._up),
                'Yellow',
              );
              this.startUpdateFirmware(multipleFirmwares.curBoot, multipleFirmwares.curBin.bin);
              // updateFirmware(multipleFirmwares.curBoot, multipleFirmwares.curBin.bin);
              multipleFirmwares.stage = this.#wasRebooter ? 'writeBoot' : 'writeApp';
              return this.getPacket(); // to repeat while
            }
          } else {
            console.log('restartAttempt= ' + multipleFirmwares.restartAttempt);
            return getCommand(this.unit2reflash, { reBoot: {} }); // boot - reboot
          }
        default:
          return false;
        case 'askBoot':
          if (a.from.category) return false; // not Boot
          if (!a.cmd.status) return false; // dont care
          this.#recalcDeltaTimeout();
          multipleFirmwares.curModBoardVersion = a.cmd.status.boardVersion;
          multipleFirmwares.curModBootVersion = a.cmd.status.bootVersion;
          multipleFirmwares.curModAppVersion = 0; // default to update app also
          multipleFirmwares.curModBoardType = a.cmd.status.boardType;
          multipleFirmwares.curBin =
            multipleFirmwares.rebooter[multipleFirmwares.curModBoardVersion][a.cmd.status.boardType];
          logMF(
            'found board version ' +
              a.cmd.status.boardVersion +
              ', boot version ' +
              a.cmd.status.bootVersion +
              ', module type ' +
              a.cmd.status.boardType,
          );

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

              multiUpdateErrors += noUpdateBoot + '\r\n';
            } else if (multipleFirmwares.curModBootVersion == multipleFirmwares.curBin?.version) {
              noUpdateBoot =
                'BOOT at ' +
                multipleFirmwares.curShortName +
                ' already has version ' +
                multipleFirmwares.curBin.version;
              color = 'Chartreuse';
            } else {
              multipleFirmwares.lastVersion = multipleFirmwares.curBin?.version;
            }

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

  #recalcDeltaTimeout(u) {
    // local or over-the-ring module
    // calculate box and timeout
    if (!u) u = this.unit2reflash;
    let cu = u;
    while (cu && cu._sut != '_Box') cu = cu._up;

    this.#deltaTimeout = 300;
    if (cu?._sun != dispatcher.thisBox) {
      this.#deltaTimeout += 200 * Object.keys(sys._subUnits._Box).length;
      if (u?._sut == 'Boot' && u?._up?._sun == 1) this.#deltaTimeout += 1000;
    }
    this.#totalTimeout = Date.now() + 25 * this.#deltaTimeout;
    if (u?._sut == 'Boot' && u?._up?._sun == 1) this.#totalTimeout += 20000;
  }

  startUpdateFirmware(u, fw) {
    //  fw = unit8array <new firmware>
    // no check on compatibility here - add one more ?
    if (!u || !fw) {
      fwPercent = 0;
      if (this.active && !multipleFirmwares) {
        systemLog(false, 'reflashing ' + getUnitName(this.unit2reflash) + ' aborted by USER', 'Red');
        modelChanged('boot', this.unit2reflash);
      }
      fw2update = this.active = false;
      return;
    }
    // ArrayBuffer
    this.#bootUpdateId = getUnitID(u);

    this.#recalcDeltaTimeout(u);

    this.#position = this.#repeat200 = 0;
    this.unit2reflash = u;
    this.active = 1;
    fwPercent = 0;

    systemLog(false, 'reflashing ' + getUnitName(this.unit2reflash) + ' started');

    fw2update = fw; // for view, both blue and grey
    this.timeout = 0;
  }

  #buildMultiUpdate(f) {
    // called before buildUpdate. return request if needed

    let dn = Date.now();
    if (this.#totalTimeout < dn) {
      // timeout - abort fW upload
      let infoString;
      switch (multipleFirmwares?.stage) {
        case 'waitReBoot':
        case 'askBoot':
        case 'askModule':
          infoString = 'querying ' + multipleFirmwares.curShortName + ' aborted: timeout';
      }
      if (infoString) {
        systemLog(false, infoString, 'red');
        multiUpdateErrors += infoString + '\r\n';
        this.#stepMultipleFirmwares(infoString, 'Red');
        //cmd={"reBoot":{}}; // done update
        modelChanged('boot', this.unit2reflash);
        return this.getPacket(f);
      }
    }

    switch (multipleFirmwares?.stage) {
      case 'waitReBoot': {
        this.timeout = dn + this.#deltaTimeout;
        if (this.#wasRebooter) {
          // boot rebooter updated
          return getCommand(this.unit2reflash, { status: {} }); // boot status
        } else {
          if (multipleFirmwares.askInfoOrStatus) {
            multipleFirmwares.askInfoOrStatus = 0;
            return getCommand(this.unit2reflash?._up, { info: {} }); // module info
          } else {
            multipleFirmwares.askInfoOrStatus = 1;
            return getCommand(this.unit2reflash, { status: {} }); // boot status
          }
        }
      }
      case 'askBoot':
        if (!f) this.#totalTimeout = dn + 5000;
        this.timeout = dn + 500;
        return getRequest(
          {
            id: [multipleFirmwares.curModSun, multipleFirmwares.curPPKsun],
            unitType: 'Boot',
            cmd: {
              status: {},
            },
          },
          undefined,
          0xff,
          true,
        ); // to create this box
      case 'askModule':
        if (!f) this.#totalTimeout = dn + 5000;
        this.timeout = dn + 500;
        return getRequest(
          {
            id: getUnitID(multipleFirmwares.curModule),
            unitType: 'Module',
            cmd: {
              info: {},
            },
          },
          undefined,
          0xff,
          true,
        );
      default:
        return undefined;
    }
  }

  setActive() {
    this.active = 1;
    this.#recalcDeltaTimeout();
    this.#stepMultipleFirmwares();
  }

  stopIt() {
    this.active = this.#bootUpdateId = fw2update = undefined;
  }
}

function stopSendingFirmware() {
  dispatcher_tasks[255].stopIt();
}

/***************************************************************/
/***************************************************************/
/***************************************************************/
/***************************************************************/
/***************************************************************/
/***************************************************************/
///////// moved from model.js - this is very close bound to hardware details
///// tests firmware file - whether it is OK for this unit
function checkUpdateFirmware(u, fw) {
  // uint8array
  try {
    if (!u || !fw) return 'bad_parameters';
    if (u != dispatcher_tasks[255]?.unit2reflash && dispatcher_tasks[255]?.active)
      return dispatcher_tasks[255]?.unit2reflash;
    let csut = u._sut;
    let btn = 0; // hardware type byte
    let lenFW = getFWlength(u, fw);
    if (lenFW > fw.length) return 'too_short_file';
    if (csut === 'Boot') csut = 'Module'; // "to" field should be for Module with 0 category
    if (csut === 'BISM1') csut = 'SK'; // "to" field should be for Module with 0 category
    let bts = schema.typeSN[csut];
    if (!bts) return 'bad_parameters';

    let btfw = 0;

    if (csut === 'Module') {
      let m = u._up;
      // very special - badly defined in schema.JSON
      if (u.status.boardType) btn = u.status.boardType;
      else {
        if (m && u._up.config) {
          if (u._up.config.typeSN) btn = u._up.config.typeSN;
          else if (u._up.config.typeSN) btn = u._up.config.typeSN;
        } else return 'bad_parameters';
      }

      btfw = fw[0x1b];
      if (u.status.boardVersion && u.status.boardVersion !== fw[0x16]) 'incompatible_firmware';
    } else if (csut === 'SK' || csut === 'BISM1') {
      // old (R08) format of firmware
      // todo - check type compatibility

      let bt = u._desc.__.boardType;
      let btf = fw[0x19];
      if (bt != btf || !bt) 'incompatible_firmware';
    } else return 'bad_parameters';

    if (!bts[btn]) return 'bad_parameters';
    if (btn != btfw) return 'incompatible_firmware';
  } catch {
    return 'bad_parameters';
  }
  return 0;
}

function getFWlength(unit, fw) {
  // uint8array
  let lenPos = 0x34; // bis
  if (unit._sut == 'Module' || unit._sut == 'Boot') lenPos = 0x28; // ppkr
  if (fw.length < lenPos + 3) return 0xffffffff;
  let len = 0;
  for (let i = 3; i--; ) len = len * 256 + fw[lenPos + i];
  return len;
}

async function initDispatcher() {
  // call only once !
  dispatcher = {
    timeToSendNext: 0,
    timeToSendReady: 0,
    timerToSend: 0,
    watchDogToSendDispatcher: 0,
    lastTaskProcessedToSend: 0,
    thisBox: 255, // address
    thisModule: 255, // address
    countP2: 0, // priority to use next
    usingWebSockets: 0,
  };

  dispatcher_tasks = {
    100: new heartBeatDispatcher(), // any 0-length packet
    255: new sendFirmwareDispatcher(), // will block any activity until done
    254: new endOfBlockDispatcher(), // will unblock any activity until done
  };

  await startHardwarePolling(); // alas - does not wait for end-of-websocket . I dont want to study promises now
  if (dispatcher.usingWebSockets && websocket.ws_connection.readyState != WebSocket.OPEN) {
    do {
      await new Promise(r => setTimeout(r, 10));
    } while (websocket.ws_connection.readyState != WebSocket.OPEN);
    systemLog(0, 'websocket finalized successfully', 'Lime');
  }

  // if (watchDogToSendDispatcher) window.clearInterval(watchDogToSendDispatcher);
  setTimerToSendDispatcher(100);
  watchDogToSendDispatcher = setInterval(watchDogDispatcherSendFunction, 500);

  // if (watchDogToRcvDispatcher) window.clearInterval(watchDogToRcvDispatcher);
  watchDogToRcvDispatcher = setInterval(watchDogDispatcherRcvFunction, 7000);
}

function clearDispatcher() {
  const keys = Object.keys(dispatcher_tasks);
  for (let i = 1; i < keys.length; i++) dispatcher_tasks[keys[i]].active = false; // stop all
}

function updateFirmware(u, fw) {
  // call with no parameters to abort
  if (u && fw) fw = fw.slice(0, getFWlength(u, fw));
  dispatcher_tasks[255].startUpdateFirmware(u, fw);
}

/******************************************************************
 * ****************************************************************
 * ****************************************************************
 * ****************************************************************
 * ***  MULTY UPDATE
 * ****************************************************************
 * ****************************************************************
 * ****************************************************************
 * ****************************************************************
 */

var multiUpdateErrors = ''; // summary of errors to alert when done
var reportMultipleFirmwares = ''; // summary to show when done
var showMultipleFirmwares = ''; // current state to show while processing on   modelChanged('boot');
var multipleFirmwares; // undefined when done, set to NONE/undef to abort, also call updateFirmware(null,null) to abort current upload
// expected struct with smth like
// { "rebooter":{"6":{"1":{bin:binImage,name:filename,version:9999}, "2":{bin:binfile...}...}, "7":{}}
//   "application":{"6":{ ... }}...}               === same as rebooter
// binary = uint8array
// state mashine will store there also
//  .curPPKsun
//  .curModSun
//  .timeOut
//  .stage
//  .curModBoardVersion
//  .curModule == ref to unit
//  .curBoot == ref to unit
//  .curBin == ref to {bin:binImage,name:filename,version:9999}

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

// parameter - as var multipleFirmwares;
function startMultipleFirmwares(mF) {
  reportMultipleFirmwares = multiUpdateErrors = showMultipleFirmwares = '';
  multipleFirmwares = mF;
  multipleFirmwares.reflashAttempt = 0;
  multipleFirmwares.restartAttempt = 0;
  multipleFirmwares.stage = undefined;
  logMF('all PPK reflashing started');
  dispatcher_tasks[255].setActive();
}

function abortAllFirmwareUpdates(reason, color) {
  if (multipleFirmwares) {
    if (!reason) reason = 'multiple firmware update aborted by user';
    if (!color) color = 'Red';
    modelChanged('boot');
    if (multiUpdateErrors.length) {
      systemLog(false, multiUpdateErrors, 'Yellow');
      if (color == 'Red' || color == 'Yellow') alert(reason);
    }
    // alert(reportMultipleFirmwares);
  }
  logMF(reason, color);
  updateFirmware(null, null);
  multipleFirmwares = undefined;
  modelChanged('boot');
}
