Merge pull request #449 from DJ2LS/ls-mesh

This commit is contained in:
DJ2LS 2023-07-05 17:57:34 +02:00 committed by GitHub
commit 9205d23390
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1213 additions and 51 deletions

View file

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

View file

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

View file

@ -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"></div>
<p class="justify-content-center d-flex position-absolute m-0 p-0 w-100 text-white ${progressbar_bg}" style="font-size: xx-small" id="msg-${
<p class="justify-content-center d-flex position-absolute m-0 p-0 w-100 text-white" style="font-size: xx-small" id="msg-${
obj._id
}-progress-information">${percent_value} % - ${obj.bytesperminute} Bpm</p>
}-progress-information">${percent_value} - ${obj.bytesperminute} Bpm</p>
</div>
</div>
</div>
@ -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");
}

View file

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

188
gui/preload-mesh.js Normal file
View file

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

View file

@ -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 = "";

View file

@ -215,10 +215,18 @@
Settings
</button>
</li>
<li>
<button class="dropdown-item" id="tncLog" type="button">
<i class="bi bi-card-text me-2"></i>
TNC Live Logs
</button>
</li>
<li id="liMeshTable">
<button class="dropdown-item" id="meshtable" type="button">
<i class="bi bi-rocket-takeoff me-2"></i>
Mesh Table Prototype
</button>
</li>
<li>
<button
class="dropdown-item"
@ -3663,17 +3671,17 @@
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50"
>Enable 563Hz only mode</label
>Enable 250Hz only mode</label
>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="500HzModeSwitch"
id="250HzModeSwitch"
/>
<label class="form-check-label" for="500HzModeSwitch"
>563Hz</label
<label class="form-check-label" for="250HzModeSwitch"
>250Hz</label
>
</div>
</label>
@ -3796,6 +3804,21 @@
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable MESH protocol</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline ms-2">
<input
class="form-check-input"
type="checkbox"
id="enableMeshSwitch"
/>
<label class="form-check-label" for="enableMeshSwitch"
>experimental! REALLY!</label
>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50"
>Database maintenance</label
@ -3807,7 +3830,8 @@
type="button"
>
Clean
</button>
</button>&nbsp;
<div class="spinner-border text-warning invisible" role="status" id="divCleanDBSpinner"></div>
</label>
</div>
<div class="center">

95
gui/src/mesh-module.html Normal file
View file

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self';" />
<!-- Bootstrap CSS -->
<link
rel="stylesheet"
href="../node_modules/bootstrap/dist/css/bootstrap.min.css"
/>
<link rel="stylesheet" type="text/css" href="styles.css" />
<title>FreeDATA - Mesh Table</title>
</head>
<body>
<!-- bootstrap -->
<script src="../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<!-- chart.js -->
<nav class="navbar fixed-top bg-light ms-2 me-2">
<div class="input-group">
<input
type="text"
class="form-control"
style="max-width: 6rem; text-transform: uppercase"
placeholder="DXcall"
pattern="[A-Z]*"
id="dxCallMesh"
maxlength="11"
aria-label="Input group"
aria-describedby="btnGroupAddon"
/>
<button id="transmit_mesh_ping" type="button" class="btn btn-primary">mesh ping</button>
</div>
</div>
</nav>
<div class="container-fluid h-100 mt-5">
<div class="tableFixHead h-50">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">DXCall</th>
<th scope="col">Router</th>
<th scope="col">Hops</th>
<th scope="col">Score</th>
<th scope="col">SNR</th>
</tr>
</thead>
<tbody id="mesh-table">
<!-- ... -->
</tbody>
</table>
</div>
<div class="tableFixHead h-50">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">Destination</th>
<th scope="col">Router</th>
<th scope="col">Frametype</th>
<th scope="col">Payload</th>
<th scope="col">Attempt</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="mesh-signalling-table">
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>

3
tnc/.gitignore vendored
View file

@ -127,3 +127,6 @@ dmypy.json
# Pyre type checker
.pyre/
# FreeDATA config
config.ini

View file

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

View file

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

View file

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

View file

@ -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")

View file

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

554
tnc/mesh.py Normal file
View file

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

View file

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

View file

@ -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()

View file

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

View file

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

View file

@ -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()

View file

@ -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()