// Copyright 2017, 2018, 2019, 2020 Max H. Parke KA1RBI // Copyright 2020, 2021 Michael Rose // // This file is part of OP25 // // OP25 is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 3, or (at your option) // any later version. // // OP25 is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public // License for more details. // // You should have received a copy of the GNU General Public License // along with OP25; see the file COPYING. If not, write to the Free // Software Foundation, Inc., 51 Franklin Street, Boston, MA // 02110-1301, USA. var d_debug = 0; var update_interval = 500; // UI update interval, ms (default=1000) var http_req = new XMLHttpRequest(); var counter1 = 0; var error_val = null; var current_tgid = null; var active_tgid = null; var active_nac = null; var send_busy = 0; var send_qfull = 0; var send_queue = []; var req_cb_count = 0; var request_count = 0; var nfinal_count = 0; var n200_count = 0; var r200_count = 0; var SEND_QLIMIT = 5; var summary_mode = true; var enable_changed = false; var enable_status = []; var last_srcaddr = []; var last_alg = []; var last_algid = []; var last_keyid = []; function find_parent(ele, tagname) { while (ele) { if (ele.nodeName == tagname) return (ele); else if (ele.nodeName == "HTML") return null; ele = ele.parentNode; } return null; } function f_command(ele, command) { var myrow = find_parent(ele, "TR"); var mytbl = find_parent(ele, "TABLE"); amend_d(myrow, mytbl, command); } function edit_freq(freq, to_ui) { var MHZ = 1000000.0; if (to_ui) { var f = (freq / MHZ) + ""; if (f.indexOf(".") == -1) f += ".0"; return f; } else { var f = parseFloat(freq); if (freq.indexOf(".")) f *= MHZ; return Math.round(f); } } function edit_d(d, to_ui) { var new_d = {}; var hexints = {"nac":1}; var ints = {"if_rate":1, "ppm":1, "rate":1, "offset":1, "nac":1, "logfile-workers":1, "decim-amt":1, "seek":1, "hamlib-model":1 }; var bools = {"active":1, "trunked":1, "rate":1, "offset":1, "phase2_tdma": 1, "phase2-tdma":1, "wireshark":1, "udp-player":1, "audio-if":1, "tone-detect":1, "vocoder":1, "audio":1, "pause":1 }; var floats = {"costas-alpha":1, "gain-mu":1, "calibration":1, "fine-tune":1, "gain":1, "excess-bw":1, "offset":1, "excess_bw":1}; var lists = {"blacklist":1, "whitelist":1, "cclist":1}; var freqs = {"frequency":1, "cclist":1}; for (var k in d) { if (!to_ui) { if (d[k] == "None") new_d[k] = ""; else new_d[k] = d[k]; if (k == "plot" && !d[k].length) new_d[k] = null; if (k in ints) { new_d[k] = parseInt(new_d[k]); } else if (k in floats) { new_d[k] = parseFloat(new_d[k]); } else if (k in lists) { var l = []; if (new_d[k].length) l = new_d[k].split(","); if (k in freqs && new_d[k].length) { var new_l = []; for (var i in l) new_l.push(edit_freq(l[i], to_ui)); new_d[k] = new_l; } else { new_d[k] = l; } } else if (k in freqs) { new_d[k] = edit_freq(new_d[k], to_ui); } } else { if (k in hexints) { if (d[k] == null) new_d[k] = "0x0"; else new_d[k] = "0x" + d[k].toString(16); } else if (k in ints) { if (d[k] == null) new_d[k] = ""; else new_d[k] = d[k].toString(10); } else if (k in lists) { if (k in freqs) { var new_l = []; for (var i in d[k]) { new_l.push(edit_freq(d[k][i], to_ui)); } new_d[k] = new_l.join(","); } else { if ((!d[k]) || (!d[k].length)) new_d[k] = []; else new_d[k] = d[k].join(","); } } else if (k in freqs) { new_d[k] = edit_freq(d[k], to_ui); } else { new_d[k] = d[k]; } } } return new_d; } function edit_l(cfg, to_ui) { var new_d = {"devices": [], "channels": []}; for (var device in cfg['devices']) new_d["devices"].push(edit_d(cfg['devices'][device], to_ui)); for (var channel in cfg['channels']) new_d["channels"].push(edit_d(cfg['channels'][channel], to_ui)); new_d["backend-rx"] = edit_d(cfg['backend-rx'], to_ui); return new_d; } function amend_d(myrow, mytbl, command) { var trunk_row = null; if (mytbl.id == "chtable") trunk_row = find_next(myrow, "TR"); if (command == "delete") { var ok = confirm ("Confirm delete"); if (ok) { myrow.parentNode.removeChild(myrow); if (mytbl.id == "chtable") trunk_row.parentNode.removeChild(trunk_row); } } else if (command == "clone") { var newrow = myrow.cloneNode(true); newrow.id = find_free_id("id_"); if (mytbl.id == "chtable") { var newrow2 = trunk_row.cloneNode(true); newrow2.id = "tr_" + newrow.id.substring(3); if (trunk_row.nextSibling) { myrow.parentNode.insertBefore(newrow2, trunk_row.nextSibling); myrow.parentNode.insertBefore(newrow, trunk_row.nextSibling); } else { myrow.parentNode.appendChild(newrow); myrow.parentNode.appendChild(newrow2); } } else { if (myrow.nextSibling) myrow.parentNode.insertBefore(newrow, myrow.nextSibling); else myrow.parentNode.appendChild(newrow); } } else if (command == "new") { var newrow = null; var parent = null; var pfx = "id_"; if (mytbl.id == "chtable") { newrow = document.getElementById("chrow").cloneNode(true); parent = document.getElementById("chrow").parentNode; } else if (mytbl.id == "devtable") { newrow = document.getElementById("devrow").cloneNode(true); parent = document.getElementById("devrow").parentNode; } else if (mytbl.className == "tgtable") { newrow = mytbl.querySelector(".tgrow").cloneNode(true); parent = mytbl.querySelector(".tgrow").parentNode; pfx = "tg_"; } else { return null; } newrow.style['display'] = ''; newrow.id = find_free_id(pfx); parent.appendChild(newrow); if (mytbl.id == "chtable") { var newrow2 = document.getElementById("trrow").cloneNode(true); newrow2.id = "tr_" + newrow.id.substring(3); parent.appendChild(newrow2); } return newrow; } } function nav_update(command) { var names = ["b1", "b2", "b3", "b4", "b5", "b7"]; var bmap = { "status": "b1", "settings": "b2", "rx": "b3", "help": "b4", "view": "b5", "about": "b7" }; var id = bmap[command]; for (var id1 in names) { b = document.getElementById(names[id1]); if (names[id1] == id) { b.className = "nav-button-active"; } else { b.className = "nav-button"; } } } function f_select(command) { var div_list = ["status", "settings", "rx", "help", "about"]; var orig_command = command; if (command == "rx") { command = "status"; //hack summary_mode = false; } else { summary_mode = true; } for (var i=0; i= "0" && s <= "9") return true; else return false; } function rx_update(d) { if (d["files"].length > 0) { for (var i=0; i 0) display_src = d['srcaddr']; display_alg = d['alg']; if (d['algid'] != 128) { display_keyid = d['keyid']; e_class = "red_value"; } } var html = ""; var d_sys = ("system" in d) ? d['system'].substring(0,doTruncate) : "Undefined"; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += "
" + d_sys + ""; html += "Frequency
" + d['freq'] / 1000000.0 + "
" + displayTag + ""; html += "Talkgroup ID
" + displayTgid + ""; html += "
"; html += "Encryption
" + display_alg + ""; html += "
"; html += "Key ID
" + display_keyid + ""; html += "
"; html += "Source Addr
" + display_src + ""; html += "
"; var div_s2 = document.getElementById("div_s2"); div_s2.innerHTML = html; div_s2.style["display"] = ""; active_nac = d['nac']; active_tgid = d['tgid']; if (d['tgid'] != null) { current_tgid = d['tgid']; } var fstyle = document.getElementById("valFontStyle").value; var z1 = document.getElementById("valTagFont").value; // set font size of TG Tag var z = document.getElementById("dTag"); z.style = "font-size: " + z1 + "px; " + "font-weight: " + fstyle + ";"; var z1 = document.getElementById("valSystemFont").value; // set font size of System var z = document.getElementById("dSys"); z.style = "font-size: " + z1 + "px; " + "font-weight: " + fstyle + ";"; } // adjacent sites table function adjacent_data(d) { if (Object.keys(d).length < 1) return ""; var html = "
"; // open div-adjacent html += ""; html += ""; html += ""; var ct = 0; for (var freq in d) { html += ""; } html += "
Adjacent Sites
FrequencySys IDRFSSSiteUplink
" + freq / 1000000.0 + "" + d[freq]['sysid'].toString(16) + "" + d[freq]["rfid"] + "" + d[freq]["stid"] + "" + (d[freq]["uplink"] / 1000000.0) + "
"; // close div-adjacent return html; // end adjacent sites table } function trunk_summary(d) { var nacs = []; for (var nac in d) { if (!is_digit(nac.charAt(0))) continue; nacs[nac] = 1; } var html = ""; html += "
"; html += "
"; html += ""; html += ""; for (nac in d) { if (!is_digit(nac.charAt(0))) continue; last_srcaddr[nac] = d[nac]['srcaddr']; last_alg[nac] = d[nac]['alg']; last_algid[nac] = d[nac]['algid']; last_keyid[nac] = d[nac]['keyid']; if (!(nac in enable_status)) enable_status[nac] = true; var times = []; for (var freq in d[nac]['frequency_data']) { times.push(parseInt(d[nac]['frequency_data'][freq]['last_activity'])); } var min_t = 0; if (times.length) { for (var i=0; i"; html += ""; html += ""; html += ""; html += ""; html += ""; } var display = ""; if (!enable_changed) display = "none"; html += ""; html += "
EnabledNACSystemLast ActiveTSBK Count
" + ns + "" + d[nac]['sysname'] + "" + times + "" + comma(d[nac]['tsbks']) + "
"; html += ""; html += "
"; return html; } function f_save_list(ele) { var flist = []; for (var nac in enable_status) { if (enable_status[nac]) flist.push(nac.toString(10)); } document.getElementById("save_list_row").style["display"] = "none"; enable_changed = false; send_command("settings-enable", flist.join(",")); } function f_enable_changed(ele, nac) { enable_status[nac] = ele.checked; enable_changed = true; document.getElementById("save_list_row").style["display"] = ""; } // additional system info: wacn, sysID, rfss, site id, secondary control channels, freq error function trunk_detail(d) { // json_type = trunk_update var html = ""; for (var nac in d) { if (!is_digit(nac.charAt(0))) continue; last_srcaddr[nac] = d[nac]['srcaddr']; last_alg[nac] = d[nac]['alg']; last_algid[nac] = d[nac]['algid']; last_keyid[nac] = d[nac]['keyid']; html += "
"; // open div-content html += ""; html += "
" + d[nac]["sysname"] + "
"; html += "NAC " + "0x" + parseInt(nac).toString(16) + "       "; html += d[nac]['rxchan'] / 1000000.0; html += " / "; html += d[nac]['txchan'] / 1000000.0; html += "       tsbks " + comma(d[nac]['tsbks']); html += "

"; html += "WACN: " + "0x" + parseInt(d[nac]['wacn']).toString(16) + " "; html += "System ID: " + "0x" + parseInt(d[nac]['sysid']).toString(16) + " "; html += "RFSS ID: " + d[nac]['rfid'] + " "; html += "Site ID: " + d[nac]['stid'] + "
"; if (d[nac]["secondary"].length) { html += "Secondary control channel(s): "; for (i=0; iFrequency error: " + error_val + " Hz.   "; html += "Fine tune: " + fine_tune + "
"; } // system frequencies table html += "
"; // open div-info open div-system html += ""; html += ""; html += ""; var ct = 0; for (var freq in d[nac]['frequency_data']) { tg2 = d[nac]['frequency_data'][freq]['tgids'][1]; if (tg2 == null) tg2 = " "; html += ""; } html += "
System Frequencies
FrequencyLastTalkgoupHits
" + parseInt(freq) / 1000000.0 + "" + d[nac]['frequency_data'][freq]['last_activity'] + "" + d[nac]['frequency_data'][freq]['tgids'][0] + "" + tg2 + "" + d[nac]['frequency_data'][freq]['counter'] + "
"; // close div-system // end system freqencies table html += adjacent_data(d[nac]['adjacent_data']); html += "



"; // close div-content close div-info box-br hr-separating each NAC } return html; } function update_data(d) { if (active_nac == null || active_tgid == null) return; var display_src = "—"; var display_alg = "—"; var display_keyid = "—"; var e_class = "value"; if (last_srcaddr[active_nac] != null) { display_src = last_srcaddr[active_nac]; var ele = document.getElementById("dSrc"); if (ele != null) ele.innerHTML = display_src; } if (last_algid[active_nac] == null || last_alg[active_nac] == null || last_keyid[active_nac] == null) return; display_alg = last_alg[active_nac]; if (last_algid[active_nac] != 128) { display_keyid = last_keyid[active_nac]; e_class = "red_value"; } ele = document.getElementById("dAlg"); if (ele != null) { ele.innerHTML = display_alg; ele.className = e_class; } ele = document.getElementById("dKey"); if (ele != null) ele.innerHTML = display_keyid; } function trunk_update(d) { var div_s1 = document.getElementById("div_s1"); var html; if (summary_mode) html = trunk_summary(d); else html = trunk_detail(d); div_s1.innerHTML = html; // disply hold indicator var x = document.getElementById("holdIndicator"); if (d['data']['hold_mode']) { x.style.display = "block"; } else { x.style.display = "none"; } // display last command unless it was more than 10 seconds ago x2 = d['data']['last_command']; if (x2 && d['data']['last_command_time'] > -10) { document.getElementById("lastCommand").innerHTML = "Last Command
" + x2.toUpperCase() + "
" + " " + (d['data']['last_command_time'] * -1) + " secs ago"; } else { document.getElementById("lastCommand").innerHTML = ""; } update_data(d); } function config_list(d) { var html = ""; html += ""; document.getElementById("cfg_list_area").innerHTML = html; } function config_data(d) { var cfg = edit_l(d['data'], true); open_editor(); var chtable = document.getElementById("chtable"); var devtable = document.getElementById("devtable"); var chrow = document.getElementById("chrow"); var devrow = document.getElementById("devrow"); for (var device in cfg['devices']) rollup_row("dev", amend_d(devrow, devtable, "new"), cfg['devices'][device]); for (var channel in cfg['channels']) rollup_row("ch", amend_d(chrow, chtable, "new"), cfg['channels'][channel]); rollup_rx_rows(cfg['backend-rx']); } function open_editor() { document.getElementById("edit_settings").style["display"] = ""; var rows = document.querySelectorAll(".dynrow"); var ct = 0; for (var r in rows) { var row = rows[r]; ct += 1; if (row.id && (row.id.substring(0,3) == "id_" || row.id.substring(0,3) == "tr_")) { row.parentNode.removeChild(row); } } var oldtbl = document.getElementById("rt_1"); if (oldtbl) oldtbl.parentNode.removeChild(oldtbl); var tbl = document.getElementById("rxopt-table"); var newtbl = tbl.cloneNode(true); newtbl.id = "rt_1"; newtbl.style["display"] = ""; var rxrow = newtbl.querySelector(".rxrow"); var advrow = newtbl.querySelector(".advrow"); rxrow.id = "rx_1"; advrow.id = "rx_2"; if (tbl.nextSibling) tbl.parentNode.insertBefore(newtbl, tbl.nextSibling); else tbl.parentNode.appendChild(newtbl); } function http_req_cb() { req_cb_count += 1; s = http_req.readyState; if (s != 4) { nfinal_count += 1; return; } if (http_req.status != 200) { n200_count += 1; return; } r200_count += 1; var dl = JSON.parse(http_req.responseText); var dispatch = {'trunk_update': trunk_update, 'change_freq': change_freq, 'rx_update': rx_update, 'config_data': config_data, 'config_list': config_list} for (var i=0; i= SEND_QLIMIT) { send_qfull += 1; send_queue.unshift(); } send_queue.push( d ); send_process(); } function send_process() { s = http_req.readyState; if (s != 0 && s != 4) { send_busy += 1; return; } http_req.open("POST", "/"); http_req.onreadystatechange = http_req_cb; http_req.setRequestHeader("Content-type", "application/json"); cmd = JSON.stringify( send_queue ); send_queue = []; http_req.send(cmd); } function f_scan_button(command) { if (current_tgid == null) send_command(command, -1); else send_command(command, current_tgid); } function f_goto_button(command) { var _tgid = 0; if (command == "goto") { command = "hold" if (current_tgid != null) _tgid = current_tgid; _tgid = parseInt(prompt("Enter tgid to hold.", _tgid)); if (isNaN(_tgid) || (_tgid < 0) || (_tgid > 65535)) _tgid = 0; send_command(command, _tgid); } } function f_debug() { if (!d_debug) return; var html = "

"; html += "busy " + send_busy; html += " qfull " + send_qfull; html += " sendq size " + send_queue.length; html += " requests " + request_count; html += " update int=" + update_interval; html += "
callbacks: "; html += " total=" + req_cb_count; html += " incomplete=" + nfinal_count; html += " error=" + n200_count; html += " OK=" + r200_count; html += "
"; var div_debug = document.getElementById("div_debug"); div_debug.innerHTML = html; } function find_next(e, tag) { var n = e.nextSibling; for (var i=0; i<25; i++) { if (n == null) return null; if (n.nodeName == tag) return n; n = n.nextSibling; } return null; } function find_free_id(pfx) { for (var seq = 1; seq < 5000; seq++) { var test_id = pfx + seq; var ele = document.getElementById(test_id); if (!ele) return test_id; } return null; } function f_trunked(e) { var row = find_parent(e, "TR"); var trrow = document.getElementById("tr_" + row.id.substring(3)); trrow['style']["display"] = (e.checked) ? "" : "none"; } function read_write_sel(sel_node, def) { var result = []; var elist = sel_node.querySelectorAll("option"); for (var e in elist) { var ele = elist[e]; if (def) { if (!def[sel_node.name]) return; var options = def[sel_node.name].split(","); var opts = {}; for (var o in options) opts[options[o]] = 1; if (ele.value in opts) ele.selected = true; else ele.selected = false; } else { if (ele.selected) result.push(ele.value); } } if (!def) return result.join(); } function read_write(elist, def) { var result = {}; for (var e in elist) { var ele = elist[e]; if (ele.nodeName == 'INPUT') { if (ele.type == 'text') if (def) ele.value = def[ele.name]; else result[ele.name] = ele.value; else if (ele.type == 'checkbox') if (def) ele.checked = def[ele.name]; else result[ele.name] = ele.checked; } else if (ele.nodeName == 'SELECT') { if (def) read_write_sel(ele, def); else result[ele.name] = read_write_sel(ele, def); } } if (!def) return result; } function rollup_row(which, row, def) { var elements = Array.from(row.querySelectorAll("input,select")); if (which == "ch") { var trrow = document.getElementById("tr_" + row.id.substring(3)); var trtable = trrow.querySelector("table.trtable"); elements = elements.concat(Array.from(trtable.querySelectorAll("input,select"))); if (def) trrow.style["display"] = (def["trunked"]) ? "" : "none"; } else if (which == "rx") { var advrow = document.getElementById("rx_2"); elements = elements.concat(Array.from(advrow.querySelectorAll("input,select"))); } var result = read_write(elements, def); if (which == "ch") { var tgtable = trrow.querySelector("table.tgtable"); var tgrow = trrow.querySelector("tr.tgrow"); if (def) { for (var k in def["tgids"]) { var val = def["tgids"][k]; var newrow = amend_d(tgrow, tgtable, "new"); var inputs = newrow.querySelectorAll("input"); read_write(inputs, {"tg_id": k, "tg_tag": val}); } } else { var tgids = {}; var rows = tgtable.querySelectorAll("tr.tgrow"); for (var i=0; i= 0) { alert ("TSV files not supported. First, invoke \"Edit\"; inspect the resulting configuration; then click \"Save\"."); return; } send_command("rx-start", val); } function f_load() { var sel = document.getElementById("config_select"); if (!sel) return; var val = read_write_sel(sel, null); if (!val) { alert ("You must select a configuration to edit"); return; } if (val == "New Configuration") { open_editor(); } else { send_command('config-load', val); var ele = document.getElementById("config_name"); ele.value = val; } } function show_advanced(o) { var tbl = find_parent(o, "TABLE"); var row = tbl.querySelector(".advrow"); toggle_show_hide(o, row); } function toggle_show_hide(o, ele) { if (o.value == "Show") { o.value = "Hide"; ele.style["display"] = ""; } else { o.value = "Show"; ele.style["display"] = "none"; } } function f_tags(o) { var mydiv = find_parent(o, "DIV"); var tbl = mydiv.querySelector(".tgtable"); toggle_show_hide(o, tbl); } // added UI functions - triptolemus // popout the UI to a minimal browser window function popOut() { var myWindow = window.open(window.location.href, "", "width=760,height=400"); } // toggle dark/light mode function toggleCSS() { var x = document.documentElement.getAttribute('data-theme'); if (x == "dark") { document.documentElement.setAttribute('data-theme', 'light'); } else { document.documentElement.setAttribute('data-theme', 'dark'); } } // add comma formatting to number (tsbk) function comma(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } // keyboard shortcuts document.onkeydown = function(evt) { evt = evt || window.event; if (evt.keyCode == 71) { // 'g' key - GOTO f_goto_button("goto"); } if (evt.keyCode == 76) { // 'l' key - LOCKOUT f_scan_button("lockout"); } if (evt.keyCode == 72) { // 'h' key - HOLD f_scan_button("hold"); } if (evt.keyCode == 83) { // 's' key - SKIP f_scan_button("skip"); } if (evt.keyCode == 86) { // 'v' key - VIEW (light/dark) toggleCSS(); } if (evt.keyCode == 82) { // 'r' key - RX screen f_select("rx"); } if (evt.keyCode == 74) { // 'j' key - HOME screen f_select("status"); } if (evt.keyCode == 77) { // 'm' key - MINIFY minify("nav-bar"); minify("div_images"); minify("div_s1"); } if (evt.keyCode == 66) { // 'b' key - bold document.getElementById("valFontStyle").value="bold" } if (evt.keyCode == 78) { // 'n' key - normal document.getElementById("valFontStyle").value="normal" } } function minify(div) { var x = document.getElementById(div); if (x.style.display === "none") { x.style.display = "block"; } else { x.style.display = "none"; } }