diff --git a/gui/daemon.js b/gui/daemon.js index 23d04534..77c2cec4 100644 --- a/gui/daemon.js +++ b/gui/daemon.js @@ -227,7 +227,8 @@ exports.startTNC = function ( auto_tune, tx_delay, tci_ip, - tci_port + tci_port, + enable_mesh ) { var json_command = JSON.stringify({ type: "set", @@ -264,6 +265,7 @@ exports.startTNC = function ( tx_delay: tx_delay, tci_ip: tci_ip, tci_port: tci_port, + enable_mesh: enable_mesh }, ], }); diff --git a/gui/main.js b/gui/main.js index 28b86514..03ea87fd 100644 --- a/gui/main.js +++ b/gui/main.js @@ -101,7 +101,8 @@ const configDefaultSettings = "enable_auto_retry" : "False", \ "tx_delay" : 0, \ "auto_start": 0, \ - "enable_sys_notification": 1 \ + "enable_sys_notification": 1, \ + "enable_mesh_features": "False" \ }'; if (!fs.existsSync(configPath)) { @@ -127,7 +128,6 @@ for (key in parsedConfig) { } } sysInfo.info("------------------------------------------ "); - /* var chatDB = path.join(configFolder, 'chatDB.json') // create chat database file if not exists @@ -166,6 +166,7 @@ fs.mkdir(receivedFilesFolder, { let win = null; let data = null; let logViewer = null; +let meshViewer = null; var daemonProcess = null; // create a splash screen @@ -249,6 +250,29 @@ function createWindow() { } }); + meshViewer = new BrowserWindow({ + height: 900, + width: 600, + show: false, + //parent: win, + webPreferences: { + preload: require.resolve("./preload-mesh.js"), + nodeIntegration: true, + }, + }); + + meshViewer.loadFile("src/mesh-module.html"); + meshViewer.setMenuBarVisibility(false); + + // Emitted when the window is closed. + meshViewer.on("close", function (evt) { + if (meshViewer !== null) { + evt.preventDefault(); + meshViewer.hide(); + } else { + this.close(); + } + }); // Emitted when the window is closed. win.on("closed", function () { console.log("closing all windows....."); @@ -392,6 +416,11 @@ ipcMain.on("request-clear-chat-connected", () => { chat.webContents.send("action-clear-reception-status"); }); +ipcMain.on("request-update-dbclean-spinner", () => { + //Turn off dbclean spinner + win.webContents.send("action-update-dbclean-spinner"); +}); + // UPDATE TNC CONNECTION ipcMain.on("request-update-tnc-ip", (event, data) => { win.webContents.send("action-update-tnc-ip", data); @@ -404,6 +433,7 @@ ipcMain.on("request-update-daemon-ip", (event, data) => { ipcMain.on("request-update-tnc-state", (event, arg) => { win.webContents.send("action-update-tnc-state", arg); + meshViewer.send("action-update-mesh-table", arg) //data.webContents.send('action-update-tnc-state', arg); }); @@ -483,6 +513,11 @@ ipcMain.on("request-open-tnc-log", () => { logViewer.show(); }); +ipcMain.on("request-open-mesh-module", () => { + meshViewer.show(); +}); + + //file selector ipcMain.on("get-file-path", (event, data) => { dialog @@ -901,6 +936,7 @@ function close_all() { win.destroy(); chat.destroy(); logViewer.destroy(); + meshViewer.destroy(); app.quit(); } diff --git a/gui/preload-chat.js b/gui/preload-chat.js index f437f5cd..bb0170a4 100644 --- a/gui/preload-chat.js +++ b/gui/preload-chat.js @@ -683,7 +683,9 @@ ipcRenderer.on("action-update-transmission-status", (event, arg) => { document.getElementById("txtConnectedWithChat").textContent = data.dxcallsign; - console.log(data.status); +if (typeof data.uuid === undefined) return; + + //console.log(data.status); if (data.uuid !== "no-uuid") { db.get(data.uuid, { attachments: true, @@ -1054,16 +1056,16 @@ update_chat = function (obj) { } // check if wrong status message - if (obj.status == "transmit" && obj.percent == 0) { + if (obj.status == "transmit" && obj.type == "transmit" && obj.percent < 100) { var TimeDifference = new Date().getTime() / 1000 - obj.timestamp; - if (TimeDifference > 3600) { - db.upsert(obj._id, function (doc) { - if (!doc.status) { - doc.status = "failed"; - } - return doc; - }); - obj.status = "failed"; + if (TimeDifference > 21600) { //Six hours + console.log("Resetting message to failed state since in transmit status for over 6 hours:") + console.log(obj); + db.upsert(obj._id, function (doc) { + doc.status = "failed"; + return doc; + }); + obj.status = "failed"; } } if (typeof obj.new == "undefined") { @@ -1440,12 +1442,14 @@ update_chat = function (obj) { if (obj.status == "failed") { var progressbar_bg = "bg-danger"; var percent_value = "TRANSMISSION FAILED"; + //Set to 100 so progressbar background populates + obj.percent=100; } else if (obj.status == "transmitted") { var progressbar_bg = "bg-success"; var percent_value = "TRANSMITTED"; } else { var progressbar_bg = "bg-primary"; - var percent_value = obj.percent; + var percent_value = obj.percent + " %"; } //Sneak in low graphics mode if so enabled for progress bars @@ -1487,9 +1491,9 @@ update_chat = function (obj) { }%;" aria-valuenow="${ obj.percent }" aria-valuemin="0" aria-valuemax="100"> -

${percent_value} % - ${obj.bytesperminute} Bpm

+ }-progress-information">${percent_value} - ${obj.bytesperminute} Bpm

@@ -1510,8 +1514,8 @@ update_chat = function (obj) { // console.log(obj.attempt) if ( - !obj.status == "broadcast_transmit" || - !obj.status == "broadcast_received" + obj.status != "broadcast_transmit" || + obj.status != "broadcast_received" ) { document.getElementById("msg-" + obj._id + "-status").innerHTML = get_icon_for_state(obj.status); @@ -1577,8 +1581,8 @@ update_chat = function (obj) { "msg-" + obj._id + "-progress-information" ).innerHTML = "TRANSMITTED - " + obj.bytesperminute + " Bpm"; } else if ( - !obj.status == "broadcast_transmit" || - !obj.status == "broadcast_received" + obj.status != "broadcast_transmit" || + obj.status != "broadcast_received" ) { document .getElementById("msg-" + obj._id + "-progress") @@ -2909,4 +2913,5 @@ async function dbClean() { itemCount + " items removed from database. It's recommended you now restart the GUI." ); + ipcRenderer.send("request-update-dbclean-spinner"); } diff --git a/gui/preload-main.js b/gui/preload-main.js index caa8cf2b..bcf99f48 100644 --- a/gui/preload-main.js +++ b/gui/preload-main.js @@ -28,6 +28,7 @@ var appDataFolder = var configFolder = path.join(appDataFolder, "FreeDATA"); var configPath = path.join(configFolder, "config.json"); var config = require(configPath); + const contrib = [ "DK5SM", "DL4IAZ", @@ -343,9 +344,9 @@ window.addEventListener("DOMContentLoaded", () => { } if (config.low_bandwidth_mode == "True") { - document.getElementById("500HzModeSwitch").checked = true; + document.getElementById("250HzModeSwitch").checked = true; } else { - document.getElementById("500HzModeSwitch").checked = false; + document.getElementById("250HzModeSwitch").checked = false; } if (config.high_graphics == "True") { @@ -396,6 +397,21 @@ window.addEventListener("DOMContentLoaded", () => { document.getElementById("NotificationSwitch").checked = false; } + if(config.enable_mesh_features.toLowerCase() == "true"){ + document.getElementById("liMeshTable").style.visibility = "visible"; + document.getElementById("liMeshTable").style.display = "block"; + document.getElementById("enableMeshSwitch").checked = true; + + + } else { + document.getElementById("liMeshTable").style.visibility = "hidden"; + document.getElementById("liMeshTable").style.display = "none"; + document.getElementById("enableMeshSwitch").checked = false; + + } + + + // theme selector changeGuiDesign(config.theme); @@ -1142,8 +1158,8 @@ window.addEventListener("DOMContentLoaded", () => { }); // enable 500z Switch clicked - document.getElementById("500HzModeSwitch").addEventListener("click", () => { - if (document.getElementById("500HzModeSwitch").checked == true) { + document.getElementById("250HzModeSwitch").addEventListener("click", () => { + if (document.getElementById("250HzModeSwitch").checked == true) { config.low_bandwidth_mode = "True"; } else { config.low_bandwidth_mode = "False"; @@ -1243,6 +1259,22 @@ window.addEventListener("DOMContentLoaded", () => { FD.saveConfig(config, configPath); }); + // enable MESH Switch clicked + document.getElementById("enableMeshSwitch").addEventListener("click", () => { + if (document.getElementById("enableMeshSwitch").checked == true) { + config.enable_mesh_features = "True"; + document.getElementById("liMeshTable").style.visibility = "visible"; + document.getElementById("liMeshTable").style.display = "block"; + } else { + config.enable_mesh_features = "False"; + document.getElementById("liMeshTable").style.visibility = "hidden"; + document.getElementById("liMeshTable").style.display = "none"; + } + //fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + FD.saveConfig(config, configPath); + }); + + // enable is writing switch clicked document.getElementById("enable_is_writing").addEventListener("click", () => { if (document.getElementById("enable_is_writing").checked == true) { @@ -1430,6 +1462,16 @@ window.addEventListener("DOMContentLoaded", () => { var handshake = document.getElementById("hamlib_handshake").value; var tx_delay = document.getElementById("tx_delay").value; + if (document.getElementById("enableMeshSwitch").checked == true) { + var enable_mesh_features = "True"; + document.getElementById("liMeshTable").style.visibility = "visible"; + document.getElementById("liMeshTable").style.display = "block"; + } else { + var enable_mesh_features = "False"; + document.getElementById("liMeshTable").style.visibility = "hidden"; + document.getElementById("liMeshTable").style.display = "none"; + } + if (document.getElementById("scatterSwitch").checked == true) { var enable_scatter = "True"; } else { @@ -1442,7 +1484,7 @@ window.addEventListener("DOMContentLoaded", () => { var enable_fft = "False"; } - if (document.getElementById("500HzModeSwitch").checked == true) { + if (document.getElementById("250HzModeSwitch").checked == true) { var low_bandwidth_mode = "True"; } else { var low_bandwidth_mode = "False"; @@ -1515,7 +1557,6 @@ window.addEventListener("DOMContentLoaded", () => { var tx_audio_level = document.getElementById("audioLevelTX").value; var rx_buffer_size = document.getElementById("rx_buffer_size").value; - config.radiocontrol = radiocontrol; config.mycall = callsign_ssid; config.mygrid = mygrid; @@ -1544,6 +1585,7 @@ window.addEventListener("DOMContentLoaded", () => { config.tx_delay = tx_delay; config.tci_ip = tci_ip; config.tci_port = tci_port; + config.enable_mesh_features = enable_mesh_features; //fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); FD.saveConfig(config, configPath); @@ -1593,7 +1635,8 @@ window.addEventListener("DOMContentLoaded", () => { auto_tune, tx_delay, tci_ip, - tci_port + tci_port, + enable_mesh_features ); }); @@ -1601,6 +1644,10 @@ window.addEventListener("DOMContentLoaded", () => { ipcRenderer.send("request-open-tnc-log"); }); + document.getElementById("meshtable").addEventListener("click", () => { + ipcRenderer.send("request-open-mesh-module"); + }); + // stopTNC button clicked document.getElementById("stopTNC").addEventListener("click", () => { if (!confirm("Stop the TNC?")) return; @@ -1610,6 +1657,7 @@ window.addEventListener("DOMContentLoaded", () => { // btnCleanDB button clicked document.getElementById("btnCleanDB").addEventListener("click", () => { + document.getElementById("divCleanDBSpinner").classList.remove("invisible"); ipcRenderer.send("request-clean-db"); }); @@ -1755,8 +1803,15 @@ window.addEventListener("DOMContentLoaded", () => { resetSortIcon(); }); - autostart_rigctld(); + + + + autostart_rigctld(); + + + }); +//End of domcontentloaded function resetSortIcon() { document.getElementById("hslSort").remove(); @@ -1801,6 +1856,10 @@ function connectedStation(data) { prefix + data.dxcallsign; } +//Called by chat to turn off db clean spinner +ipcRenderer.on("action-update-dbclean-spinner",() =>{ + document.getElementById("divCleanDBSpinner").classList.add("invisible"); +}); //Listen for events caused by tnc 'tnc-message' rx ipcRenderer.on("action-update-reception-status", (event, arg) => { var data = arg["data"][0]; @@ -2938,6 +2997,7 @@ ipcRenderer.on("action-update-unread-messages-main", (event, data) => { }); ipcRenderer.on("run-tnc-command", (event, arg) => { + if (arg.command == "save_my_call") { sock.saveMyCall(arg.callsign); } @@ -3017,6 +3077,12 @@ ipcRenderer.on("run-tnc-command", (event, arg) => { if (arg.command == "responseSharedFile") { sock.sendResponseSharedFile(arg.dxcallsign, arg.file, arg.filedata); } + + if (arg.command == "mesh_ping") { + sock.sendMeshPing(arg.dxcallsign); + } + + }); // IPC ACTION FOR AUTO UPDATER diff --git a/gui/preload-mesh.js b/gui/preload-mesh.js new file mode 100644 index 00000000..2834a84e --- /dev/null +++ b/gui/preload-mesh.js @@ -0,0 +1,188 @@ +const path = require("path"); +const { ipcRenderer } = require("electron"); + +// https://stackoverflow.com/a/26227660 +var appDataFolder = + process.env.APPDATA || + (process.platform == "darwin" + ? process.env.HOME + "/Library/Application Support" + : process.env.HOME + "/.config"); +var configFolder = path.join(appDataFolder, "FreeDATA"); +var configPath = path.join(configFolder, "config.json"); +const config = require(configPath); + +// WINDOW LISTENER +window.addEventListener("DOMContentLoaded", () => { + + // startPing button clicked + document.getElementById("transmit_mesh_ping").addEventListener("click", () => { + var dxcallsign = document.getElementById("dxCallMesh").value.toUpperCase(); + if (dxcallsign == "" || dxcallsign == null || dxcallsign == undefined) + return; + //pauseButton(document.getElementById("transmit_mesh_ping"), 2000); + ipcRenderer.send("run-tnc-command", { + command: "mesh_ping", + dxcallsign: dxcallsign, + }); + }); + + + +}); + + +ipcRenderer.on("action-update-mesh-table", (event, arg) => { + var routes = arg.routing_table; + +if (typeof routes == "undefined") { + return; + } + + + var tbl = document.getElementById("mesh-table"); +if (tbl !== null) { + tbl.innerHTML = ""; + + } + + +for (i = 0; i < routes.length; i++) { + + var row = document.createElement("tr"); + var datetime = new Date(routes[i]["timestamp"] * 1000).toLocaleString( + navigator.language,{ + hourCycle: 'h23', + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit" + } + ); + var timestamp = document.createElement("td"); + var timestampText = document.createElement("span"); + timestampText.innerText = datetime; + timestamp.appendChild(timestampText); + + var dxcall = document.createElement("td"); + var dxcallText = document.createElement("span"); + dxcallText.innerText = routes[i]["dxcall"]; + dxcall.appendChild(dxcallText); + + var router = document.createElement("td"); + var routerText = document.createElement("span"); + routerText.innerText = routes[i]["router"]; + router.appendChild(routerText); + + var hops = document.createElement("td"); + var hopsText = document.createElement("span"); + hopsText.innerText = routes[i]["hops"]; + hops.appendChild(hopsText); + + var score = document.createElement("td"); + var scoreText = document.createElement("span"); + scoreText.innerText = routes[i]["score"]; + score.appendChild(scoreText); + + var snr = document.createElement("td"); + var snrText = document.createElement("span"); + snrText.innerText = routes[i]["snr"]; + snr.appendChild(snrText); + + row.appendChild(timestamp); + row.appendChild(dxcall); + row.appendChild(router); + row.appendChild(hops); + row.appendChild(score); + row.appendChild(snr); + + + + tbl.appendChild(row); +} + /*-------------------------------------------*/ + var routes = arg.mesh_signalling_table; + +console.log(routes) +if (typeof routes == "undefined") { + return; + } + + + var tbl = document.getElementById("mesh-signalling-table"); +if (tbl !== null) { + tbl.innerHTML = ""; + + } + + +for (i = 0; i < routes.length; i++) { + + var row = document.createElement("tr"); + var datetime = new Date(routes[i]["timestamp"] * 1000).toLocaleString( + navigator.language,{ + hourCycle: 'h23', + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit" + } + ); + var timestamp = document.createElement("td"); + var timestampText = document.createElement("span"); + timestampText.innerText = datetime; + timestamp.appendChild(timestampText); + + var destination = document.createElement("td"); + var destinationText = document.createElement("span"); + destinationText.innerText = routes[i]["destination"]; + destination.appendChild(destinationText); + + var router = document.createElement("td"); + var routerText = document.createElement("span"); + routerText.innerText = routes[i]["router"]; + router.appendChild(routerText); + + var frametype = document.createElement("td"); + var frametypeText = document.createElement("span"); + frametypeText.innerText = routes[i]["frametype"]; + frametype.appendChild(frametypeText); + + var payload = document.createElement("td"); + var payloadText = document.createElement("span"); + payloadText.innerText = routes[i]["payload"]; + payload.appendChild(payloadText); + + var attempt = document.createElement("td"); + var attemptText = document.createElement("span"); + attemptText.innerText = routes[i]["attempt"]; + attempt.appendChild(attemptText); + + var status = document.createElement("td"); + var statusText = document.createElement("span"); + statusText.innerText = routes[i]["status"]; + status.appendChild(statusText); + + + row.appendChild(timestamp); + row.appendChild(destination); + row.appendChild(router); + row.appendChild(frametype); + row.appendChild(payload); + row.appendChild(attempt); + row.appendChild(status); + + tbl.appendChild(row); +} + + +if (tbl !== null) { + + // scroll to bottom of page + // https://stackoverflow.com/a/11715670 + //window.scrollTo(0, document.body.scrollHeight); + } +}); diff --git a/gui/sock.js b/gui/sock.js index ec3893c9..3db27039 100644 --- a/gui/sock.js +++ b/gui/sock.js @@ -218,6 +218,8 @@ client.on("data", function (socketdata) { total_bytes: data["total_bytes"], arq_transmission_percent: data["arq_transmission_percent"], stations: data["stations"], + routing_table: data["routing_table"], + mesh_signalling_table: data["mesh_signalling_table"], beacon_state: data["beacon_state"], hamlib_status: data["hamlib_status"], listen: data["listen"], @@ -586,6 +588,15 @@ exports.sendPing = function (dxcallsign) { writeTncCommand(command); }; +// Send Mesh Ping +exports.sendMeshPing = function (dxcallsign) { + command = + '{"type" : "mesh", "command" : "ping", "dxcallsign" : "' + + dxcallsign + + '"}'; + writeTncCommand(command); +}; + // Send CQ exports.sendCQ = function () { command = '{"type" : "broadcast", "command" : "cqcqcq"}'; @@ -851,6 +862,8 @@ exports.sendFecIsWriting = function (mycallsign) { writeTncCommand(command); }; + + // SEND FEC TO BROADCASTCHANNEL exports.sendBroadcastChannel = function (channel, data_out, uuid) { let checksum = ""; diff --git a/gui/src/index.html b/gui/src/index.html index 1eec4f0d..52dee847 100644 --- a/gui/src/index.html +++ b/gui/src/index.html @@ -215,10 +215,18 @@ Settings +
  • +
  • +
  • + +
  • +   +
    diff --git a/gui/src/mesh-module.html b/gui/src/mesh-module.html new file mode 100644 index 00000000..01081b95 --- /dev/null +++ b/gui/src/mesh-module.html @@ -0,0 +1,95 @@ + + + + + + + + + + + + FreeDATA - Mesh Table + + + + + + +
    + + +
    + + +
    + + + + + + + + + + + + + + + +
    TimestampDXCallRouterHopsScoreSNR
    +
    + + + + +
    + + + + + + + + + + + + + + +
    TimestampDestinationRouterFrametypePayloadAttemptStatus
    +
    + + +
    + + + + + + + + diff --git a/tnc/.gitignore b/tnc/.gitignore index b6e47617..23100a5e 100644 --- a/tnc/.gitignore +++ b/tnc/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# FreeDATA config +config.ini \ No newline at end of file diff --git a/tnc/config.py b/tnc/config.py index 6b640ae0..98a93fc1 100644 --- a/tnc/config.py +++ b/tnc/config.py @@ -84,6 +84,10 @@ class CONFIG: 'port': data[23] } + self.config['MESH'] = {'#TCI settings': None, + 'enable_protocol': data[24] + } + try: with open(self.config_name, 'w') as configfile: self.config.write(configfile) diff --git a/tnc/daemon.py b/tnc/daemon.py index 4915abf4..e7bb8c50 100755 --- a/tnc/daemon.py +++ b/tnc/daemon.py @@ -286,8 +286,11 @@ class DAEMON: options.append("--tx-delay") options.append(data[21]) - - + #Mesh + print(data[24]) + if data[24] == "True": + options.append("--mesh") + print(options) # safe data to config file config.write_entire_config(data) diff --git a/tnc/data_handler.py b/tnc/data_handler.py index 71ec13a4..fba1a137 100644 --- a/tnc/data_handler.py +++ b/tnc/data_handler.py @@ -26,7 +26,7 @@ import structlog import stats import ujson as json from codec2 import FREEDV_MODE, FREEDV_MODE_USED_SLOTS -from queues import DATA_QUEUE_RECEIVED, DATA_QUEUE_TRANSMIT, RX_BUFFER +from queues import DATA_QUEUE_RECEIVED, DATA_QUEUE_TRANSMIT, RX_BUFFER, MESH_RECEIVED_QUEUE from static import FRAME_TYPE as FR_TYPE import broadcast diff --git a/tnc/helpers.py b/tnc/helpers.py index 75c1aecf..84bd23ec 100644 --- a/tnc/helpers.py +++ b/tnc/helpers.py @@ -8,10 +8,11 @@ import time from datetime import datetime,timezone import crcengine import static -from static import ARQ, AudioParam, Beacon, Channel, Daemon, HamlibParam, ModemParam, Station, Statistics, TCIParam, TNC +from static import ARQ, AudioParam, Beacon, Channel, Daemon, HamlibParam, ModemParam, Station, Statistics, TCIParam, TNC, MeshParam import structlog import numpy as np import threading +import mesh log = structlog.get_logger("helpers") diff --git a/tnc/main.py b/tnc/main.py index 6fa436e4..6754df25 100755 --- a/tnc/main.py +++ b/tnc/main.py @@ -29,11 +29,11 @@ import helpers import log_handler import modem import static -from static import ARQ, AudioParam, Beacon, Channel, Daemon, HamlibParam, ModemParam, Station, Statistics, TCIParam, TNC +from static import ARQ, AudioParam, Beacon, Channel, Daemon, HamlibParam, ModemParam, Station, Statistics, TCIParam, TNC, MeshParam import structlog import explorer import json - +import mesh log = structlog.get_logger("main") def signal_handler(sig, frame): @@ -246,6 +246,13 @@ if __name__ == "__main__": type=int, ) + PARSER.add_argument( + "--mesh", + dest="enable_mesh", + action="store_true", + help="Enable mesh protocol", + ) + ARGS = PARSER.parse_args() # set save to folder state for allowing downloading files to local file system @@ -299,6 +306,7 @@ if __name__ == "__main__": TCIParam.ip = ARGS.tci_ip TCIParam.port = ARGS.tci_port ModemParam.tx_delay = ARGS.tx_delay + MeshParam.enable_protocol = ARGS.enable_mesh except Exception as e: log.error("[DMN] Error reading config file", exception=e) @@ -349,6 +357,7 @@ if __name__ == "__main__": TCIParam.ip = str(conf.get('TCI', 'tci_ip', 'localhost')) TCIParam.port = int(conf.get('TCI', 'tci_port', '50001')) ModemParam.tx_delay = int(conf.get('TNC', 'tx_delay', '0')) + MeshParam.enable_protocol = conf.get('MESH','mesh_enable','False') except KeyError as e: log.warning("[CFG] Error reading config file near", key=str(e)) except Exception as e: @@ -395,6 +404,12 @@ if __name__ == "__main__": # start modem modem = modem.RF() + # start mesh protocol only if enabled + if MeshParam.enable_protocol: + log.info("[MESH] loading module") + # start mesh module + mesh = mesh.MeshRouter() + # optionally start explorer module if TNC.enable_explorer: log.info("[EXPLORER] Publishing to https://explorer.freedata.app", state=TNC.enable_explorer) diff --git a/tnc/mesh.py b/tnc/mesh.py new file mode 100644 index 00000000..92728199 --- /dev/null +++ b/tnc/mesh.py @@ -0,0 +1,554 @@ +# -*- coding: UTF-8 -*- +""" + +@author: DJ2LS + +HF mesh networking prototype and testing module + + import time + MeshParam.routing_table = [['AA1AA', 'direct', 0, 1.0, 25, time.time(), ], ['AA1AA', 'AA2BB', 1, 3.1, 10, time.time(), ], + ['AA3CC', 'AA2BB', 5, -4.5, -3, time.time(), ]] + + print(MeshParam.routing_table) + print("---------------------------------") + + + + +TODO: SIGNALLING FOR ACK/NACK: +- mesh-signalling burst is datac13 +- mesh-signalling frame contains [message id, status, hops, score, payload] +- frame type is 1 byte +- message id is 32bit crc --> 4bytes +- status can be ACK, NACK, PING, PINGACK --> 1byte +- payload = 14bytes - 8 bytes = 6bytes +- create a list for signalling frames, contains [message id, message-status, attempts, state, timestamp] +- on "IRS", send ACK/NACK 10 times on receiving beacon? +- on "ROUTER", receive ACK/NACK, and store it in table, also send it 10 times +- if sent 10 times, set ACK/NACK state to "done" +- if done already in list, don't reset retry counter +- delete ACK/NACK if "done" and timestamp older than 1day + +TODO: SCORE CALCULATION: +SNR: negative --> * 2 + +""" +# pylint: disable=invalid-name, line-too-long, c-extension-no-member +# pylint: disable=import-outside-toplevel, attribute-defined-outside-init + +from static import TNC, MeshParam, FRAME_TYPE, Station, ModemParam, ARQ +from codec2 import FREEDV_MODE +import numpy as np +import time +import threading +import modem +import helpers +import structlog +import ujson as json + +from queues import MESH_RECEIVED_QUEUE, MESH_QUEUE_TRANSMIT, MESH_SIGNALLING_TABLE + +class MeshRouter(): + def __init__(self): + + self.log = structlog.get_logger("RF") + + self.transmission_time_list = [ + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360, + 30, 30, 30, 60, 60, 60, 60, 60, 60, 60, 60, 60, 120, 120, 120, 120, 120, 120, 180, 180, 180, 180, 180, 180, + 360, 360, 360, 360, 360, 360 + ] + # for testing only: self.transmission_time_list = [30, 30] + self.signalling_max_attempts = len(self.transmission_time_list) + + + + self.mesh_broadcasting_thread = threading.Thread( + target=self.broadcast_routing_table, name="worker thread receive", daemon=True + ) + self.mesh_broadcasting_thread.start() + + self.mesh_rx_dispatcher_thread = threading.Thread( + target=self.mesh_rx_dispatcher, name="worker thread receive", daemon=True + ) + self.mesh_rx_dispatcher_thread.start() + + self.mesh_tx_dispatcher_thread = threading.Thread( + target=self.mesh_tx_dispatcher, name="worker thread receive", daemon=True + ) + self.mesh_tx_dispatcher_thread.start() + + self.mesh_signalling_dispatcher_thread = threading.Thread( + target=self.mesh_signalling_dispatcher, name="worker thread receive", daemon=True + ) + self.mesh_signalling_dispatcher_thread.start() + + def get_from_heard_stations(self): + """ + get data from heard stations + heard stations format: + [dxcallsign,dxgrid,int(time.time()),datatype,snr,offset,frequency] + + TNC.heard_stations.append( + [ + dxcallsign, + dxgrid, + int(time.time()), + datatype, + snr, + offset, + frequency, + ] + ) + """ + dxcallsign_position = 0 + dxgrid_position = 1 + timestamp_position = 2 + type_position = 3 + snr_position = 4 + offset_position = 5 + frequency_position = 6 + + try: + for item in TNC.heard_stations: + #print("-----------") + #print(item) + #print(item[snr_position]) + try: + #print(item[snr_position]) + snr = bytes(item[snr_position], "utf-8").split(b"/") + snr = int(float(snr[0])) + except Exception as err: + snr = int(float(item[snr_position])) + + new_router = [helpers.get_crc_24(item[dxcallsign_position]), helpers.get_crc_24(b'direct'), 0, snr, self.calculate_score_by_snr(snr), item[timestamp_position]] + self.add_router_to_routing_table(new_router) + except Exception as e: + self.log.warning("[MESH] error fetching data from heard station list", e=e) + + def add_router_to_routing_table(self, new_router): + try: + # destination callsign # router callsign # hops # rx snr # route quality # timestamp + for _, item in enumerate(MeshParam.routing_table): + # update routing entry if exists + if new_router[0] in item[0] and new_router[1] in item[1]: + #print(f"UPDATE {MeshParam.routing_table[_]} >>> {new_router}") + self.log.info(f"[MESH] [ROUTING TABLE] [UPDATE]: {MeshParam.routing_table[_]} >>> ", + update=new_router) + + MeshParam.routing_table[_] = new_router + + # add new routing entry if not exists + if new_router not in MeshParam.routing_table: + #print(f"INSERT {new_router} >>> ROUTING TABLE") + self.log.info("[MESH] [ROUTING TABLE] [INSERT]:", insert=new_router) + + MeshParam.routing_table.append(new_router) + except Exception as e: + self.log.warning("[MESH] error adding data to routing table", e=e, router=new_router) + + def broadcast_routing_table(self, interval=600): + + while True: + # always enable receiving for datac4 if broadcasting + modem.RECEIVE_DATAC4 = True + + threading.Event().wait(1) + if not ARQ.arq_state and not ModemParam.channel_busy: + try: + + # wait some time until sending routing table + threading.Event().wait(interval) + + # before we are transmitting, let us update our routing table + self.get_from_heard_stations() + + #[b'DJ2LS-0', 'direct', 0, 9.6, 9.6, 1684912305] + mesh_broadcast_frame_header = bytearray(4) + mesh_broadcast_frame_header[:1] = bytes([FRAME_TYPE.MESH_BROADCAST.value]) + mesh_broadcast_frame_header[1:4] = helpers.get_crc_24(Station.mycallsign) + + # callsign(6), router(6), hops(1), path_score(1) == 14 ==> 14 28 42 ==> 3 mesh routing entries + # callsign_crc(3), router_crc(3), hops(1), path_score(1) == 8 --> 6 + # callsign_crc(3), hops(1), path_score(1) == 5 --> 10 + + # Create a new bytearray with a fixed length of 50 + result = bytearray(50) + + # Iterate over the route subarrays and add the selected entries to the result bytearray + index = 0 + for route_id, route in enumerate(MeshParam.routing_table): + # the value 5 is the length of crc24 + hops + score + + dxcall = MeshParam.routing_table[route_id][0] + # router = MeshParam.routing_table[i][1] + hops = MeshParam.routing_table[route_id][2] + # snr = MeshParam.routing_table[i][3] + route_score = np.clip(MeshParam.routing_table[route_id][4], 0, 254) + # timestamp = MeshParam.routing_table[i][5] + result[index:index + 5] = dxcall + bytes([hops]) + bytes([route_score]) + index += 5 + + # Split the result bytearray into a list of fixed-length bytearrays + split_result = [result[i:i + 50] for i in range(0, len(result), 50)] + frame_list = [] + for _ in split_result: + # make sure payload is always 50 + _[len(_):] = bytes(50 - len(_)) + #print(len(_)) + frame_list.append(mesh_broadcast_frame_header + _) + + TNC.transmitting = True + c2_mode = FREEDV_MODE.datac4.value + self.log.info("[MESH] broadcasting routing table", frame_list=frame_list, frames=len(split_result)) + modem.MODEM_TRANSMIT_QUEUE.put([c2_mode, 1, 0, frame_list]) + + # Wait while transmitting + while TNC.transmitting: + threading.Event().wait(0.01) + except Exception as e: + self.log.warning("[MESH] broadcasting routing table", e=e) + + + def mesh_rx_dispatcher(self): + while True: + data_in = MESH_RECEIVED_QUEUE.get() + if int.from_bytes(data_in[:1], "big") in [FRAME_TYPE.MESH_BROADCAST.value]: + self.received_routing_table(data_in[:-2]) + elif int.from_bytes(data_in[:1], "big") in [FRAME_TYPE.MESH_SIGNALLING_PING.value]: + self.received_mesh_ping(data_in[:-2]) + elif int.from_bytes(data_in[:1], "big") in [FRAME_TYPE.MESH_SIGNALLING_PING_ACK.value]: + self.received_mesh_ping_ack(data_in[:-2]) + + else: + print("wrong mesh data received") + #print(data_in) + + def mesh_tx_dispatcher(self): + while True: + data = MESH_QUEUE_TRANSMIT.get() + #print(data) + if data[0] == "PING": + self.add_mesh_ping_to_signalling_table(helpers.get_crc_24(data[2]).hex(), status="awaiting_ack") + else: + print("wrong mesh command") + + + + def mesh_signalling_dispatcher(self): + # [timestamp, destination, router, frametype, payload, attempt, status] + # --------------0------------1---------2---------3--------4---------5--------6 # + + + while True: + threading.Event().wait(1.0) + for entry in MESH_SIGNALLING_TABLE: + #print(entry) + attempt = entry[5] + status = entry[6] + # check for PING cases + if entry[3] in ["PING", "PING-ACK"] and attempt < len(self.transmission_time_list) and status not in ["acknowledged"]: + + + # Calculate the transmission time with exponential increase + #transmission_time = timestamp + (2 ** attempt) * 10 + # Calculate transmission times for attempts 0 to 30 with stronger S-curves in minutes + #correction_factor = 750 + timestamp = entry[0] + #transmission_time = timestamp + (4.5 / (1 + np.exp(-1. * (attempt - 5)))) * correction_factor * attempt + transmission_time = timestamp + sum(self.transmission_time_list[:attempt]) + # check if it is time to transmit + if time.time() >= transmission_time: + entry[5] += 1 + self.log.info("[MESH] [TX] Ping", destination=entry[1]) + channel_busy_timeout = time.time() + 5 + while ModemParam.channel_busy and time.time() < channel_busy_timeout: + threading.Event().wait(0.01) + self.transmit_mesh_signalling_ping(bytes.fromhex(entry[1])) + #print("...") + + def received_routing_table(self, data_in): + try: + print("data received........") + print(data_in) + + router = data_in[1:4] # Extract the first 4 bytes (header) + payload = data_in[4:] # Extract the payload (excluding the header) + + print("Router:", router) # Output the header bytes + + for i in range(0, len(payload)-1, 5): + callsign_checksum = payload[i:i + 3] # First 3 bytes of the information (callsign_checksum) + hops = int.from_bytes(payload[i+3:i + 4], "big") # Fourth byte of the information (hops) + score = int.from_bytes(payload[i+4:i + 5], "big") # Fifth byte of the information (score) + snr = int(ModemParam.snr) + score = self.calculate_new_avg_score(score, self.calculate_score_by_snr(snr)) + timestamp = int(time.time()) + + # use case 1: add new router to table only if callsign not empty + _use_case1 = callsign_checksum.startswith(b'\x00') + + # use case 2: add new router to table only if not own callsign + _use_case2 = callsign_checksum not in [helpers.get_crc_24(Station.mycallsign)] + + # use case 3: increment hop if router not direct + if router not in [helpers.get_crc_24(b'direct')] and hops == 0: + hops += 1 + + # use case 4: if callsign is directly available skip route for only keeping shortest way in db + _use_case4 = False + for _, call in enumerate(MeshParam.routing_table): + # check if callsign already in routing table and is direct connection + if callsign_checksum in [MeshParam.routing_table[_][0]] and MeshParam.routing_table[_][1] in [helpers.get_crc_24(b'direct')]: + _use_case4 = True + + # use case N: calculate score + # TODO... + + if not _use_case1 \ + and _use_case2\ + and not _use_case4: + + print("Callsign Checksum:", callsign_checksum) + print("Hops:", hops) + print("Score:", score) + + new_router = [callsign_checksum, router, hops, snr, score, timestamp] + print(new_router) + self.add_router_to_routing_table(new_router) + + print("-------------------------") + for _, item in enumerate(MeshParam.routing_table): + print(MeshParam.routing_table[_]) + print("-------------------------") + except Exception as e: + self.log.warning("[MESH] error processing received routing broadcast", e=e) + + def calculate_score_by_snr(self, snr): + if snr < -12 or snr > 12: + raise ValueError("Value must be in the range of -12 to 12") + + score = (snr + 12) * 100 / 24 # Scale the value to the range [0, 100] + if score < 0: + score = 0 # Ensure the score is not less than 0 + elif score > 100: + score = 100 # Ensure the score is not greater than 100 + + return int(score) + + def calculate_new_avg_score(self, value_old, value): + return int((value_old + value) / 2) + + def received_mesh_ping(self, data_in): + destination = data_in[1:4].hex() + if destination == Station.mycallsign_crc.hex(): + self.log.info("[MESH] [RX] [PING] [REQ]", destination=destination, mycall=Station.mycallsign_crc.hex()) + # use case 1: set status to acknowleding if we are the receiver of a PING + self.add_mesh_ping_to_signalling_table(destination, status="acknowledging") + + channel_busy_timeout = time.time() + 5 + while ModemParam.channel_busy and time.time() < channel_busy_timeout: + threading.Event().wait(0.01) + + dxcallsign_crc = Station.mycallsign_crc + self.transmit_mesh_signalling_ping_ack(dxcallsign_crc) + + else: + self.log.info("[MESH] [RX] [PING] [REQ]", destination=destination, mycall=Station.mycallsign_crc.hex()) + # lookup if entry is already in database - if so, udpate and exit + for item in MESH_SIGNALLING_TABLE: + if item[1] == destination and item[5] >= self.signalling_max_attempts: + # use case 2: set status to forwarded if we are not the receiver of a PING and out of retries + self.add_mesh_ping_to_signalling_table(destination, status="forwarded") + return + + print("......................") + # use case 1: set status to forwarding if we are not the receiver of a PING and we don't have an entry in our database + self.add_mesh_ping_to_signalling_table(destination, status="forwarding") + + def received_mesh_ping_ack(self, data_in): + # TODO: + # Check if we have a ping callsign already in signalling table + # if PING, then override and make it a PING-ACK + # if not, then add to table + + destination = data_in[1:4].hex() + timestamp = time.time() + router = "" + frametype = "PING-ACK" + payload = "" + attempt = 0 + + + if destination == Station.mycallsign_crc.hex(): + self.log.info("[MESH] [RX] [PING] [ACK]", destination=destination, mycall=Station.mycallsign_crc.hex()) + self.add_mesh_ping_ack_to_signalling_table(destination, status="acknowledged") + else: + #status = "forwarding" + #self.add_mesh_ping_ack_to_signalling_table(destination, status) + self.log.info("[MESH] [RX] [PING] [ACK]", destination=destination, mycall=Station.mycallsign_crc.hex()) + for item in MESH_SIGNALLING_TABLE: + if item[1] == destination and item[5] >= self.signalling_max_attempts: + # use case 2: set status to forwarded if we are not the receiver of a PING and out of retries + self.add_mesh_ping_ack_to_signalling_table(destination, status="forwarded") + return + + self.add_mesh_ping_ack_to_signalling_table(destination, status="forwarding") + #dxcallsign_crc = bytes.fromhex(destination) + #self.transmit_mesh_signalling_ping_ack(dxcallsign_crc) + + print(MESH_SIGNALLING_TABLE) + + + def add_mesh_ping_to_signalling_table(self, destination, status): + timestamp = time.time() + router = "" + frametype = "PING" + payload = "" + attempt = 0 + + # [timestamp, destination, router, frametype, payload, attempt, status] + # --------------0------------1---------2---------3--------4---------5--------6-----# + new_entry = [timestamp, destination, router, frametype, payload, attempt, status] + for _, item in enumerate(MESH_SIGNALLING_TABLE): + # update entry if exists + if destination in item[1] and "PING" in item[3]: + # reset attempts if entry exists and it failed or is acknowledged + attempt = 0 if item[6] in ["failed", "acknowledged"] else item[5] + update_entry = [item[0], destination, "", "PING", "",attempt, status] + #print(f"UPDATE {MESH_SIGNALLING_TABLE[_]} >>> {update_entry}") + + self.log.info(f"[MESH] [SIGNALLING TABLE] [UPDATE]: {MESH_SIGNALLING_TABLE[_]} >>> ", update=update_entry) + + MESH_SIGNALLING_TABLE[_] = update_entry + return + + # add new routing entry if not exists + if new_entry not in MESH_SIGNALLING_TABLE: + #print(f"INSERT {new_entry} >>> SIGNALLING TABLE") + self.log.info("[MESH] [SIGNALLING TABLE] [INSERT]:", insert=new_entry) + + MESH_SIGNALLING_TABLE.append(new_entry) + + def add_mesh_ping_ack_to_signalling_table(self, destination, status): + + timestamp = time.time() + router = "" + frametype = "PING-ACK" + payload = "" + attempt = 0 + new_entry = [timestamp, destination, router, frametype, payload, attempt, status] + + for _, item in enumerate(MESH_SIGNALLING_TABLE): + # update entry if exists + if destination in item[1] and item[3] in ["PING", "PING-ACK"]: + # reset attempts if entry exists and it failed or is acknowledged + attempt = 0 if item[6] in ["failed", "acknowledged"] else item[5] + update_entry = [item[0], destination, "", "PING-ACK", "", attempt, status] + #print(f"UPDATE {MESH_SIGNALLING_TABLE[_]} >>> {update_entry}") + self.log.info(f"[MESH] [SIGNALLING TABLE] [UPDATE]: {MESH_SIGNALLING_TABLE[_]} >>> ", update=update_entry) + + MESH_SIGNALLING_TABLE[_] = update_entry + return + + # add new routing entry if not exists + if new_entry not in MESH_SIGNALLING_TABLE: + #print(f"INSERT {new_entry} >>> SIGNALLING TABLE") + self.log.info(f"[MESH] [SIGNALLING TABLE] [INSERT]: {MESH_SIGNALLING_TABLE[_]} >>> ", update=new_entry) + + MESH_SIGNALLING_TABLE.append(new_entry) + + def enqueue_frame_for_tx( + self, + frame_to_tx, # : list[bytearray], # this causes a crash on python 3.7 + c2_mode=FREEDV_MODE.sig0.value, + copies=1, + repeat_delay=0, + ) -> None: + """ + Send (transmit) supplied frame to TNC + + :param frame_to_tx: Frame data to send + :type frame_to_tx: list of bytearrays + :param c2_mode: Codec2 mode to use, defaults to datac13 + :type c2_mode: int, optional + :param copies: Number of frame copies to send, defaults to 1 + :type copies: int, optional + :param repeat_delay: Delay time before sending repeat frame, defaults to 0 + :type repeat_delay: int, optional + """ + #print(frame_to_tx[0]) + #print(frame_to_tx) + frame_type = FRAME_TYPE(int.from_bytes(frame_to_tx[0][:1], byteorder="big")).name + self.log.debug("[TNC] enqueue_frame_for_tx", c2_mode=FREEDV_MODE(c2_mode).name, data=frame_to_tx, + type=frame_type) + + # Set the TRANSMITTING flag before adding an object to the transmit queue + # TODO: This is not that nice, we could improve this somehow + TNC.transmitting = True + modem.MODEM_TRANSMIT_QUEUE.put([c2_mode, copies, repeat_delay, frame_to_tx]) + + # Wait while transmitting + while TNC.transmitting: + threading.Event().wait(0.01) + + + def transmit_mesh_signalling_ping(self, dxcallsign_crc): + + frame_type = bytes([FRAME_TYPE.MESH_SIGNALLING_PING.value]) + + ping_frame = bytearray(14) + ping_frame[:1] = frame_type + ping_frame[1:4] = dxcallsign_crc + ping_frame[4:7] = helpers.get_crc_24(Station.mycallsign) + ping_frame[7:13] = helpers.callsign_to_bytes(Station.mycallsign) + + self.enqueue_frame_for_tx([ping_frame], c2_mode=FREEDV_MODE.sig0.value) + + def transmit_mesh_signalling_ping_ack(self, dxcallsign_crc): + #dxcallsign_crc = bytes.fromhex(data[1]) + + frame_type = bytes([FRAME_TYPE.MESH_SIGNALLING_PING_ACK.value]) + + ping_frame = bytearray(14) + ping_frame[:1] = frame_type + ping_frame[1:4] = dxcallsign_crc + #ping_frame[4:7] = helpers.get_crc_24(Station.mycallsign) + #ping_frame[7:13] = helpers.callsign_to_bytes(Station.mycallsign) + + self.enqueue_frame_for_tx([ping_frame], c2_mode=FREEDV_MODE.sig0.value) \ No newline at end of file diff --git a/tnc/modem.py b/tnc/modem.py index 50ded6af..182222e3 100644 --- a/tnc/modem.py +++ b/tnc/modem.py @@ -31,7 +31,7 @@ import tci # FIXME: used for def transmit_morse # import cw from queues import DATA_QUEUE_RECEIVED, MODEM_RECEIVED_QUEUE, MODEM_TRANSMIT_QUEUE, RIGCTLD_COMMAND_QUEUE, \ - AUDIO_RECEIVED_QUEUE, AUDIO_TRANSMIT_QUEUE + AUDIO_RECEIVED_QUEUE, AUDIO_TRANSMIT_QUEUE, MESH_RECEIVED_QUEUE TESTMODE = False RXCHANNEL = "" @@ -889,6 +889,7 @@ class RF: audiobuffer.pop(nin) nin = codec2.api.freedv_nin(freedv) if nbytes == bytes_per_frame: + print(bytes(bytes_out)) # process commands only if TNC.listen = True if TNC.listen: @@ -905,6 +906,16 @@ class RF: FRAME_TYPE.ARQ_DC_OPEN_ACK_N.value ]: print("dropp") + elif int.from_bytes(bytes(bytes_out[:1]), "big") in [ + FRAME_TYPE.MESH_BROADCAST.value, + FRAME_TYPE.MESH_SIGNALLING_PING.value, + FRAME_TYPE.MESH_SIGNALLING_PING_ACK.value, + ]: + self.log.debug( + "[MDM] [demod_audio] moving data to mesh dispatcher", nbytes=nbytes + ) + MESH_RECEIVED_QUEUE.put(bytes(bytes_out)) + else: self.log.debug( "[MDM] [demod_audio] Pushing received data to received_queue", nbytes=nbytes @@ -1299,10 +1310,11 @@ class RF: raise ZeroDivisionError AudioParam.audio_dbfs = 20 * np.log10(rms / 32768) except Exception as e: - self.log.warning( - "[MDM] fft calculation error - please check your audio setup", - e=e, - ) + # FIXME: Disabled for cli cleanup + #self.log.warning( + # "[MDM] fft calculation error - please check your audio setup", + # e=e, + #) AudioParam.audio_dbfs = -100 rms_counter = 0 diff --git a/tnc/queues.py b/tnc/queues.py index 16974151..14c907a9 100644 --- a/tnc/queues.py +++ b/tnc/queues.py @@ -12,6 +12,12 @@ DATA_QUEUE_RECEIVED = queue.Queue() MODEM_RECEIVED_QUEUE = queue.Queue() MODEM_TRANSMIT_QUEUE = queue.Queue() +# Initialize FIFO queue to store received frames +MESH_RECEIVED_QUEUE = queue.Queue() +MESH_QUEUE_TRANSMIT = queue.Queue() +MESH_SIGNALLING_TABLE = [] + + # Initialize FIFO queue to store audio frames AUDIO_RECEIVED_QUEUE = queue.Queue() AUDIO_TRANSMIT_QUEUE = queue.Queue() diff --git a/tnc/sock.py b/tnc/sock.py index 6b5f84b9..20f6ce03 100644 --- a/tnc/sock.py +++ b/tnc/sock.py @@ -27,12 +27,12 @@ import time import wave import helpers import static -from static import ARQ, AudioParam, Beacon, Channel, Daemon, HamlibParam, ModemParam, Station, Statistics, TCIParam, TNC +from static import ARQ, AudioParam, Beacon, Channel, Daemon, HamlibParam, ModemParam, Station, Statistics, TCIParam, TNC, MeshParam import structlog from random import randrange import ujson as json from exceptions import NoCallsign -from queues import DATA_QUEUE_TRANSMIT, RX_BUFFER, RIGCTLD_COMMAND_QUEUE +from queues import DATA_QUEUE_TRANSMIT, RX_BUFFER, RIGCTLD_COMMAND_QUEUE, MESH_QUEUE_TRANSMIT, MESH_SIGNALLING_TABLE SOCKET_QUEUE = queue.Queue() DAEMON_QUEUE = queue.Queue() @@ -391,6 +391,16 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler): else: self.tnc_set_mode(received_json) + # GET ROUTING TABLE + if received_json["type"] == "get" and received_json["command"] == "routing_table": + self.tnc_get_mesh_routing_table(received_json) + + + # -------------- MESH ---------------- # + # MESH PING + if received_json["type"] == "mesh" and received_json["command"] == "ping": + self.tnc_mesh_ping(received_json) + except Exception as err: log.error("[SCK] JSON decoding error", e=err) @@ -406,6 +416,8 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler): ARQ.arq_session_state = "disconnecting" command_response("disconnect", True) + + except Exception as err: command_response("listen", False) log.warning( @@ -559,6 +571,40 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler): command=received_json, ) + + def tnc_mesh_ping(self, received_json): + # send ping frame and wait for ACK + try: + dxcallsign = received_json["dxcallsign"] + if not str(dxcallsign).strip(): + raise NoCallsign + + # additional step for being sure our callsign is correctly + # in case we are not getting a station ssid + # then we are forcing a station ssid = 0 + dxcallsign = helpers.callsign_to_bytes(dxcallsign) + dxcallsign = helpers.bytes_to_callsign(dxcallsign) + + # check if specific callsign is set with different SSID than the TNC is initialized + try: + mycallsign = received_json["mycallsign"] + mycallsign = helpers.callsign_to_bytes(mycallsign) + mycallsign = helpers.bytes_to_callsign(mycallsign) + + except Exception: + mycallsign = Station.mycallsign + + MESH_QUEUE_TRANSMIT.put(["PING", mycallsign, dxcallsign]) + command_response("ping", True) + except NoCallsign: + command_response("ping", False) + log.warning("[SCK] callsign required for ping", command=received_json) + except Exception as err: + command_response("ping", False) + log.warning( + "[SCK] PING command execution error", e=err, command=received_json + ) + def tnc_ping_ping(self, received_json): # send ping frame and wait for ACK @@ -761,6 +807,44 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler): "[SCK] STOP command execution error", e=err, command=received_json ) + def tnc_get_mesh_routing_table(self, received_json): + try: + if not RX_BUFFER.empty(): + output = { + "command": "routing_table", + "routes": [], + } + + for _, route in enumerate(MeshParam.routing_table): + if MeshParam.routing_table[_][0].hex() == helpers.get_crc_24(b"direct").hex(): + router = "direct" + else: + router = MeshParam.routing_table[_][0].hex() + output["routes"].append( + { + "dxcall": MeshParam.routing_table[_][0].hex(), + "router": router, + "hops": MeshParam.routing_table[_][2], + "snr": MeshParam.routing_table[_][3], + "score": MeshParam.routing_table[_][4], + "timestamp": MeshParam.routing_table[_][5], + } + ) + + + jsondata = json.dumps(output) + # self.request.sendall(bytes(jsondata, encoding)) + SOCKET_QUEUE.put(jsondata) + command_response("routing_table", True) + + except Exception as err: + command_response("routing_table", False) + log.warning( + "[SCK] Send RX buffer command execution error", + e=err, + command=received_json, + ) + def tnc_get_rx_buffer(self, received_json): try: if not RX_BUFFER.empty(): @@ -931,6 +1015,7 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler): tx_delay = str(helpers.return_key_from_object("0", startparam, "tx_delay")) tci_ip = str(helpers.return_key_from_object("127.0.0.1", startparam, "tci_ip")) tci_port = str(helpers.return_key_from_object("50001", startparam, "tci_port")) + enable_mesh = str(helpers.return_key_from_object("False", startparam, "enable_mesh")) try: # convert ssid list to python list ssid_list = str(helpers.return_key_from_object("0, 1, 2, 3, 4, 5, 6, 7, 8, 9", startparam, "ssid_list")) @@ -973,7 +1058,8 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler): enable_stats, tx_delay, tci_ip, - tci_port + tci_port, + enable_mesh ] ) command_response("start_tnc", True) @@ -1086,6 +1172,8 @@ def send_tnc_state(): "total_bytes": str(ARQ.total_bytes), "beacon_state": str(Beacon.beacon_state), "stations": [], + "routing_table": [], + "mesh_signalling_table" : [], "mycallsign": str(Station.mycallsign, encoding), "mygrid": str(Station.mygrid, encoding), "dxcallsign": str(Station.dxcallsign, encoding), @@ -1108,6 +1196,39 @@ def send_tnc_state(): "frequency": heard[6], } ) + + for _, route in enumerate(MeshParam.routing_table): + if MeshParam.routing_table[_][1].hex() == helpers.get_crc_24(b"direct").hex(): + router = "direct" + else: + router = MeshParam.routing_table[_][1].hex() + output["routing_table"].append( + { + "dxcall": MeshParam.routing_table[_][0].hex(), + "router": router, + "hops": MeshParam.routing_table[_][2], + "snr": MeshParam.routing_table[_][3], + "score": MeshParam.routing_table[_][4], + "timestamp": MeshParam.routing_table[_][5], + } + ) + + for _, entry in enumerate(MESH_SIGNALLING_TABLE): + + output["mesh_signalling_table"].append( + { + "timestamp": MESH_SIGNALLING_TABLE[_][0], + "destination": MESH_SIGNALLING_TABLE[_][1], + "router": MESH_SIGNALLING_TABLE[_][2], + "frametype": MESH_SIGNALLING_TABLE[_][3], + "payload": MESH_SIGNALLING_TABLE[_][4], + "attempt": MESH_SIGNALLING_TABLE[_][5], + "status": MESH_SIGNALLING_TABLE[_][6], + + } + ) + + #print(output) return json.dumps(output) diff --git a/tnc/static.py b/tnc/static.py index a5f6029b..b4cd839e 100644 --- a/tnc/static.py +++ b/tnc/static.py @@ -90,7 +90,11 @@ class HamlibParam: hamlib_mode: str = "" hamlib_rf: int = 0 -@dataclass +@dataclass +class MeshParam: + routing_table = [] + enable_protocol = False +@dataclass class ModemParam: tuning_range_fmin: float = -50.0 tuning_range_fmax: float = 50.0 @@ -125,7 +129,7 @@ class TCIParam: @dataclass class TNC: - version = "0.9.4-alpha.1" + version = "0.10.0-alpha.1" host: str = "0.0.0.0" port: int = 3000 SOCKET_TIMEOUT: int = 1 # seconds @@ -157,6 +161,9 @@ class FRAME_TYPE(Enum): FR_REPEAT = 62 FR_NACK = 63 BURST_NACK = 64 + MESH_BROADCAST = 100 + MESH_SIGNALLING_PING = 101 + MESH_SIGNALLING_PING_ACK = 102 CQ = 200 QRV = 201 PING = 210 diff --git a/tools/freedata_cli_tools.py b/tools/freedata_cli_tools.py index 420593a2..4f600225 100755 --- a/tools/freedata_cli_tools.py +++ b/tools/freedata_cli_tools.py @@ -29,7 +29,7 @@ def main_menu(): while True: time.sleep(0.1) title = 'Please select a command you want to run: ' - options = ['BEACON', 'PING', 'ARQ', 'LIST AUDIO DEVICES'] + options = ['BEACON', 'PING', 'ARQ', 'LIST AUDIO DEVICES','LIST ROUTING TABLE'] option, index = pick(options, title) # BEACON AREA @@ -87,6 +87,10 @@ def main_menu(): option, index = pick(device_list, "Audio devices") + elif option == 'LIST ROUTING TABLE': + run_network_command({"type": "get", "command": "routing_table"}) + + if option == '----- BACK -----': main_menu() diff --git a/tools/freedata_network_listener.py b/tools/freedata_network_listener.py index a10466db..651ccfeb 100755 --- a/tools/freedata_network_listener.py +++ b/tools/freedata_network_listener.py @@ -89,7 +89,8 @@ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: jsondata = json.loads(command) if jsondata.get('command') == "tnc_state": - pass + #pass + print(jsondata.get("routing_table")) if jsondata.get('freedata') == "tnc-message": log.info(jsondata) @@ -108,6 +109,8 @@ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: if jsondata.get('status') == 'received' and jsondata.get('arq') == 'transmission': decode_and_save_data(jsondata["data"]) + + # clear data buffer as soon as data has been read data = bytes()