Saturn49 / Fix MM1000 Node Info

// ==UserScript==
// @name         Fix MM1000 Node Info
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  Fix the Motorola MM1000's broken Node Info page
// @author       N.Lewis
// @match        http://192.168.*.*/nodes.html
// @match        http://172.*.*.*/nodes.html
// @match        http:/10.*.*.*/nodes.html
// @include      /^http://192\.168\.\d+.\d+/nodes.html/
// @include      /^http://172\.(16|17|18|19|2\d|30|31)\.\d+\.\d+/nodes.html/
// @include      /^http://10\.\d+\.\d+\.\d+/nodes.html/
// @copyright    2021, N.Lewis (https://openuserjs.org/users/Saturn49/)
// @run-at       document-end
// @license      MIT
// @grant        none
// ==/UserScript==

// Caches responses to urls.
var response_cache_ = {};
// A mapping of url to a list of functions waiting for a response.
var response_queue_ = {};

function makeSetFunction(cell)
{
    return function(text){cell.innerHTML = text;};
}

function handleBitloading(bitloading, ctx, color)
{
    if(bitloading !== null)
    {
        ctx.beginPath();
        ctx.strokeStyle = "#C0C0C0";
        ctx.lineWidth=1;
        for (var j=0;j<=10;j++)
        {
            ctx.moveTo(0, 110-j*10);
            ctx.lineTo(255, 110-j*10);
        }
        ctx.stroke();

        ctx.beginPath();
        ctx.strokeStyle = color;
        var width = 256/bitloading.length;
        for (j=0;j<bitloading.length;j++)
        {
            if (bitloading.charAt(j) == 'a')
            {
                ctx.arc(j*width,10,1,0,2*Math.PI);
            }
            else
            {
                ctx.arc(j*width,110-bitloading.charAt(j)*10,1,0,2*Math.PI);
            }

        }
        ctx.stroke();
    }
}

function makeFinishFunction(row, nodeid, bonded, self, profiles) {
    return function(moca_version) {
        var cell;
        if (self)
        {
            cell = row.insertCell(-1);
            getfield_mac_async("--gen_node_status&"+nodeid, "eui", makeSetFunction(cell));
            cell = row.insertCell(-1);
            cell.innerHTML = "Current Node";
            cell.colSpan = "5";
        }
        else
        {
            var rx_profile = 7;
            var tx_profile = 13;
            var rx_profile_second = 18;
            var tx_profile_second = 20;

            var rx_p = findProfile(profiles, nodeid, "RX Unicast NPER 100 MHz");
            var tx_p = findProfile(profiles, nodeid, "TX Unicast NPER 100 MHz");
            var rx_p2 = findProfile(profiles, nodeid, "RX Unicast NPER Sec-Ch 100 MHz");
            var tx_p2 = findProfile(profiles, nodeid, "TX Unicast NPER Sec-Ch 100 MHz");

            if(moca_version < 0x20) {
                rx_profile = 0;
                tx_profile = 3;

                rx_p = findProfile(profiles, nodeid, "RX Unicast 50 MHz");
                tx_p = findProfile(profiles, nodeid, "TX Unicast 50 MHz");
            }

            cell = row.insertCell(-1);
            cell.innerHTML = rx_p.mac_address;

            cell = row.insertCell(-1);
            getfield_num_async("--gen_node_ext_status&index&"+nodeid+"&profile_type&"+rx_profile, "avg_snr", makeSetFunction(cell));

            cell = row.insertCell(-1);
            var span = document.createElement("SPAN");
            cell.appendChild(span);
            span.innerHTML = rx_p.phy_rate;
            if(bonded) {
                span = document.createElement("SPAN");
                cell.appendChild(span);
                span.innerHTML = " / ";
                span = document.createElement("SPAN");
                cell.appendChild(span);
                span.innerHTML = rx_p2.phy_rate;
            }

            cell = row.insertCell(-1);
            span = document.createElement("SPAN");
            cell.appendChild(span);
            span.innerHTML = tx_p.phy_rate;

            if(bonded) {
                span = document.createElement("SPAN");
                cell.appendChild(span);
                span.innerHTML = " / ";
                span = document.createElement("SPAN");
                cell.appendChild(span);
                span.innerHTML = tx_p2.phy_rate;
            }

            cell = row.insertCell(-1);
            getfield_num_async("--gen_node_ext_status&index&"+nodeid+"&profile_type&"+rx_profile, "rx_power", makeSetFunction(cell));


            cell = row.insertCell(-1);
            getfield_num_async("--gen_node_ext_status&index&"+nodeid+"&profile_type&"+tx_profile, "tx_power", makeSetFunction(cell));

            let per_cell = row.insertCell(-1);

            let num_numerator = 4;
            let num_denominator = 1;
            let numerator = 0;
            let denominator = 0;
            function maybeSetVal() {
                if(num_numerator === 0 && num_denominator === 0) {
                    if(denominator === 0) {
                        per_cell.innerHTML = "0";
                    }
                    var per = Number(numerator) / Number(denominator);
                    per_cell.innerHTML = per.toExponential();
                }
            }
            function setNumerator(val){
                num_numerator--;
                numerator += val;
                maybeSetVal();
            }
            function setDenominator(val){
                num_denominator--;
                denominator += val;
                maybeSetVal();
            }

            getfield_num_async("--node_stats&index&"+nodeid, "rx_packets", setDenominator);
            getfield_num_async("--node_stats_ext&index&"+nodeid, "rx_bc_crc_error", setNumerator);
            getfield_num_async("--node_stats_ext&index&"+nodeid, "rx_uc_crc_error", setNumerator);
            getfield_num_async("--node_stats_ext&index&"+nodeid, "rx_uc_timeout_error", setNumerator);
            getfield_num_async("--node_stats_ext&index&"+nodeid, "rx_bc_timeout_error", setNumerator);


            cell = row.insertCell(-1);
            cell.align = "center";
            cell.innerHTML = "<canvas id=\"bitloading"+nodeid+"\" width=\"256\" height=\"110\"" +
                "style=\"border:1px solid #000000;\" >" +
                "</canvas><br>";
            span = document.createElement("SPAN");
            cell.appendChild(span);
            var span2 = document.createElement("SPAN");
            cell.appendChild(span2);
            span2.innerHTML = "Mhz";

            var c = document.getElementById("bitloading"+nodeid);
            let ctx = c.getContext("2d");
            getfield_num_async("--gen_node_ext_status&index&"+nodeid+"&profile_type&"+rx_profile, "central_freq", makeSetFunction(span));
            handleBitloading(rx_p.bitloading, ctx, "rgba(255, 0, 0, 0.5)");
            handleBitloading(tx_p.bitloading, ctx, "rgba(0, 0, 255, 0.5)");

            cell = row.insertCell(-1);
            cell.align = "center";

            if(bonded) {
                cell.innerHTML = "<canvas id=\"bitloading2"+nodeid+"\" width=\"256\" height=\"110\"" +
                    "style=\"border:1px solid #000000;\" >" +
                    "</canvas><br>";
                span = document.createElement("SPAN");
                cell.appendChild(span);
                span2 = document.createElement("SPAN");
                cell.appendChild(span2);
                span2.innerHTML = "Mhz";
                c = document.getElementById("bitloading2"+nodeid);
                let ctx2 = c.getContext("2d");
                getfield_num_async("--gen_node_ext_status&index&"+nodeid+"&profile_type&"+rx_profile_second, "central_freq", makeSetFunction(span));
                handleBitloading(rx_p2.bitloading, ctx2, "rgba(255, 0, 0, 0.5)");
                handleBitloading(tx_p2.bitloading, ctx2, "rgba(0, 0, 255, 0.5)");
            }
        }
    };
}

function fillfield2()
{
    var tbl = document.getElementById('nodeinfo').tBodies[0];
    var hd_cell = tbl.rows[0].insertCell(5);
    hd_cell.className = "hd";
    hd_cell.align = "center";
    hd_cell.innerHTML = "TX Phy Rate<br>Mbps";

    var bl_cell = tbl.rows[0].cells[9];
    bl_cell.innerHTML += " (<span style=\"color:red\">RX</span>/<span style=\"color:blue\">TX</span>)";

    var bl2_cell = tbl.rows[0].insertCell(-1);
    bl2_cell.className = "hd";
    bl2_cell.align = "center";
    bl2_cell.innerHTML = "Bit Loading 2nd (<span style=\"color:red\">RX</span>/<span style=\"color:blue\">TX</span>)";

    var nodemask = getfield_num2("--net", "nodes_usable_bitmask");
    var nc = getfield_num2("--net", "nc_node_id");
    var nodeid = getfield_num2("--net", "node_id");
    var bonded_nodes_bitmask = getfield_num2("--net", "bonded_nodes_bitmask");
    var i = 0;
    var table = document.getElementById("nodeinfo");

    var profiles = parseGCAP39( getUrl("/cmd.sh?GCAP.39"));


    nodemask |= (1<<nodeid);

    while (nodemask > 0)
    {
        if (nodemask & 1 !== 0)
        {
            let row = table.insertRow();
            var cell = row.insertCell(-1);
            if (i == nc)
            {
                cell.innerHTML = "*";
            }

            cell = row.insertCell(-1);
            cell.innerHTML = i;

            var bonded = (((1 << i) & bonded_nodes_bitmask) != 0);
            var self = (i == nodeid);

            getfield_num_async("--gen_node_status&"+i, "active_moca_version", makeFinishFunction(row, i, bonded, self, profiles));
        }

        nodemask = nodemask >> 1;
        i++;
    }
}

function getUrl(url) {
    if(url in response_cache_)
    {
        return response_cache_[url];
    }
    var xmlhttp;
    if (window.XMLHttpRequest) { // code for IE7+, Firefox, Chrome, Opera, Safari
        xmlhttp = new XMLHttpRequest();
    }
    else { // code for IE6, IE5
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }


    xmlhttp.open("GET", url, false);
    xmlhttp.send();

    var str = xmlhttp.responseText;

    response_cache_[url] = str;
    return response_cache_[url];
}

function getUrlAsync(url, fn) {
    if(url in response_queue_){
      response_queue_[url].push(fn);
      return;
    }

    if(url in response_cache_)
    {
        fn(response_cache_[url]);
        return;
    }

    response_queue_[url] = [];
    response_queue_[url].push(fn);

    var xmlhttp;
    if (window.XMLHttpRequest) { // code for IE7+, Firefox, Chrome, Opera, Safari
        xmlhttp = new XMLHttpRequest();
    }
    else { // code for IE6, IE5
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }


    xmlhttp.open("GET", url, true);
    xmlhttp.onload = function(e) {
        response_cache_[url] = xmlhttp.responseText;
        while(response_queue_[url].length > 0) {
          response_queue_[url].pop()(xmlhttp.responseText);
        }
        delete response_queue_[url];
    };
    xmlhttp.send();
}

function getfield_num2(cmdname, field)
{
    var xmlttp;
    var str;
    var url = new String("/cmd.sh?mocap&get&"+cmdname);

    str = getUrl(url);

    var re = new RegExp (field+"\\s*:\\s*(-?\\d*)", "g");

    var result = re.exec(str);
    if(result == null) {
        return "";
    }

    return parseInt(result[1]);
}
function matchOrDefault(regex, input, group, def){
    var result = regex.exec(input);
    if(result == null) {
        return def;
    }
    return result[group];
}

function findProfile(profiles, node, name) {
    for(var i=0; i<profiles.length; ++i){
        var profile = profiles[i];
        if(profile.nodeId == node && profile.name == name) {
            return profile;
        }
    }
    return null;
}


function parseGCAP39(response)
{
    var results = [];
    var profiles = response.split("\n\n");
    profiles.forEach(function(profile){
        var result = {};
        result.name = matchOrDefault(/(TX|RX) (Broadcast|Unicast).*/, profile, 0, "");
        result.nodeId = parseInt(matchOrDefault(/NodeID *: *(\d+)/, profile, 1, "-1"));
        result.mac_address = matchOrDefault(/MAC Address *: *(..:..:..:..:..:..)/, profile, 1, "");
        result.phy_rate = parseInt(matchOrDefault(/PHY Rate *: *(\d+)/, profile, 1, "-1"));
        result.nbas = parseInt(matchOrDefault(/NBAS *: *(\d)/, profile, 1, "-1"));
        result.cp = parseInt(matchOrDefault(/CP *: *(\d)/, profile, 1, "-1"));
        result.tx_power_adjust = parseInt(matchOrDefault(/CP *: *(\d)/, profile, 1, "-1"));

        var bl = new RegExp ("Subcarriers\\n*(\\n\\s*(\\d+)\\s*-\\s*(\\d+)\\s*:\\s*(.*))+", "gm");
        var bl_split = new RegExp("(\\d+)\\s*-\\s*(\\d+)\\s*:\\s*(.*)", "gm");

        var m = bl.exec(profile);
        if(m == null) {
            result.bitloading = null;
        } else {
            var all = m[0];
            var out = "";
            var vals = [];
            while((m = bl_split.exec(all)) !== null) {
                var left = parseInt(m[1]);
                var right = parseInt(m[2]);
                if(left < right)
                {
                    vals.push( {
                        start: left,
                        val: m[3]
                    });
                } else {
                    vals.push( {
                        start: right,
                        val: m[3].split("").reverse().join("")
                    });
                }
            }
            vals.sort(function(a, b) { return a.start - b.start; });
            vals.forEach(function(a) { out += a.val; });

            result.bitloading = out;
        }

        results.push(result);
    });

    return results;
}

function getfield_mac_async(cmdname, field, fn)
{
    var url = new String("/cmd.sh?mocap&get&"+cmdname);

    function handleResult(str)
    {
        var re = new RegExp (field+"\\s*:\\s*(..:..:..:..:..:..)", "g");
        var result = re.exec(str);
        if(result == null) {
            fn("");
            return;
        }
        fn(result[1]);
    };
    getUrlAsync(url, handleResult);
}

function getfield_num_async(cmdname, field, fn)
{
    var url = new String("/cmd.sh?mocap&get&"+cmdname);
    var re = new RegExp (field+"\\s*:\\s*(-?\\d*)", "g");

    function handleResult(str)
    {
        var result = re.exec(str);
        if(result == null) {
           fn("");
           return;
        }
        fn(parseInt(result[1]));
    };

    getUrlAsync(url, handleResult);
}



function getfield_bitloading_async(cmdname,field, ctx, color)
{
  var xmlttp;
  var str;
  var url = new String("/cmd.sh?mocap&get&"+cmdname);
  var re = new RegExp (field+":\\n*(\\s*(\\d+)\\s*-\\s*(\\d+)\\s*:\\s*(.*)\\n)+", "gm");

  var re_split = new RegExp("(\\d+)\\s*-\\s*(\\d+)\\s*:\\s*(.*)", "gm");

  function handleResult(str) {
      var m = re.exec(str);
      if(m == null) {
          handleBitloading(null, ctx);
          return;
      }
      var all = m[0];
      var out = "";
      var vals = [];
      while((m = re_split.exec(all)) !== null) {
          var left = parseInt(m[1]);
          var right = parseInt(m[2]);
          if(left < right)
          {
              vals.push( {
                  start: left,
                  val: m[3]
              });
          } else {
              vals.push( {
                  start: right,
                  val: m[3].split("").reverse().join("")
              });
          }
      }
      vals.sort(function(a, b) { return a.start - b.start; });
      vals.forEach(function(a) { out += a.val; });
      handleBitloading(out, ctx, color);
  };
  getUrlAsync(url, handleResult);
}


(function() {
    'use strict';
    window.onload = function go(){
        fillfield2();
    }
    // Your code here...
})();