diff --git a/meshchat.py b/meshchat.py
index cceb8c5..b31e4dc 100644
--- a/meshchat.py
+++ b/meshchat.py
@@ -22,6 +22,7 @@ import webbrowser
from peewee import SqliteDatabase
from serial.tools import list_ports
+import psutil
import database
from src.backend.announce_handler import AnnounceHandler
@@ -147,6 +148,12 @@ class ReticulumMeshChat:
# remember websocket clients
self.websocket_clients: List[web.WebSocketResponse] = []
+ # track announce timestamps for rate calculation
+ self.announce_timestamps = []
+
+ # track download speeds for nomadnetwork files (list of tuples: (file_size_bytes, duration_seconds))
+ self.download_speeds = []
+
# register audio call identity
self.audio_call_manager = AudioCallManager(identity=self.identity)
self.audio_call_manager.register_incoming_call_callback(self.on_incoming_audio_call)
@@ -954,6 +961,34 @@ class ReticulumMeshChat:
# get app info
@routes.get("/api/v1/app/info")
async def index(request):
+ # Get memory usage for current process
+ process = psutil.Process()
+ memory_info = process.memory_info()
+
+ # Get network I/O statistics
+ net_io = psutil.net_io_counters()
+
+ # Get total paths
+ path_table = self.reticulum.get_path_table()
+ total_paths = len(path_table)
+
+ # Calculate announce rates
+ current_time = time.time()
+ announces_per_second = len([t for t in self.announce_timestamps if current_time - t <= 1.0])
+ announces_per_minute = len([t for t in self.announce_timestamps if current_time - t <= 60.0])
+ announces_per_hour = len([t for t in self.announce_timestamps if current_time - t <= 3600.0])
+
+ # Clean up old announce timestamps (older than 1 hour)
+ self.announce_timestamps = [t for t in self.announce_timestamps if current_time - t <= 3600.0]
+
+ # Calculate average download speed
+ avg_download_speed_bps = None
+ if self.download_speeds:
+ total_bytes = sum(size for size, _ in self.download_speeds)
+ total_duration = sum(duration for _, duration in self.download_speeds)
+ if total_duration > 0:
+ avg_download_speed_bps = total_bytes / total_duration
+
return web.json_response({
"app_info": {
"version": self.get_app_version(),
@@ -966,6 +1001,25 @@ class ReticulumMeshChat:
"reticulum_config_path": self.reticulum.configpath,
"is_connected_to_shared_instance": self.reticulum.is_connected_to_shared_instance,
"is_transport_enabled": self.reticulum.transport_enabled(),
+ "memory_usage": {
+ "rss": memory_info.rss, # Resident Set Size (bytes)
+ "vms": memory_info.vms, # Virtual Memory Size (bytes)
+ },
+ "network_stats": {
+ "bytes_sent": net_io.bytes_sent,
+ "bytes_recv": net_io.bytes_recv,
+ "packets_sent": net_io.packets_sent,
+ "packets_recv": net_io.packets_recv,
+ },
+ "reticulum_stats": {
+ "total_paths": total_paths,
+ "announces_per_second": announces_per_second,
+ "announces_per_minute": announces_per_minute,
+ "announces_per_hour": announces_per_hour,
+ },
+ "download_stats": {
+ "avg_download_speed_bps": avg_download_speed_bps,
+ },
},
})
@@ -2245,6 +2299,16 @@ class ReticulumMeshChat:
# handle successful file download
def on_file_download_success(file_name, file_bytes):
+ # Track download speed
+ download_size = len(file_bytes)
+ if hasattr(downloader, 'start_time') and downloader.start_time:
+ download_duration = time.time() - downloader.start_time
+ if download_duration > 0:
+ self.download_speeds.append((download_size, download_duration))
+ # Keep only last 100 downloads for average calculation
+ if len(self.download_speeds) > 100:
+ self.download_speeds.pop(0)
+
AsyncUtils.run_async(client.send_str(json.dumps({
"type": "nomadnet.file.download",
"nomadnet_file_download": {
@@ -2282,6 +2346,7 @@ class ReticulumMeshChat:
# download the file
downloader = NomadnetFileDownloader(destination_hash, file_path, on_file_download_success, on_file_download_failure, on_file_download_progress)
+ downloader.start_time = time.time()
AsyncUtils.run_async(downloader.download())
# handle downloading a page from a nomadnet node
@@ -3054,6 +3119,9 @@ class ReticulumMeshChat:
# log received announce
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [call.audio]")
+ # track announce timestamp
+ self.announce_timestamps.append(time.time())
+
# upsert announce to database
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
@@ -3075,6 +3143,9 @@ class ReticulumMeshChat:
# log received announce
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.delivery]")
+ # track announce timestamp
+ self.announce_timestamps.append(time.time())
+
# upsert announce to database
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
@@ -3100,6 +3171,9 @@ class ReticulumMeshChat:
# log received announce
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.propagation]")
+ # track announce timestamp
+ self.announce_timestamps.append(time.time())
+
# upsert announce to database
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
@@ -3184,6 +3258,9 @@ class ReticulumMeshChat:
# log received announce
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [nomadnetwork.node]")
+ # track announce timestamp
+ self.announce_timestamps.append(time.time())
+
# upsert announce to database
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
diff --git a/package-lock.json b/package-lock.json
index 2537a75..c10b4fb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3010,6 +3010,7 @@
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"app-builder-lib": "24.13.3",
"builder-util": "24.13.1",
diff --git a/src/frontend/components/about/AboutPage.vue b/src/frontend/components/about/AboutPage.vue
index 1a5bc36..bed9a76 100644
--- a/src/frontend/components/about/AboutPage.vue
+++ b/src/frontend/components/about/AboutPage.vue
@@ -64,6 +64,141 @@
+
+
+
+ System Resources
+
+
+ Live
+
+
+
+
+
+
+
+
Memory Usage (RSS)
+
{{ formatBytes(appInfo.memory_usage.rss) }}
+
+
+
+
+
+
+
Virtual Memory Size
+
{{ formatBytes(appInfo.memory_usage.vms) }}
+
+
+
+
+
+
+
+
+
+ Network Statistics
+
+
+ Live
+
+
+
+
+
+
+
+
Data Sent
+
{{ formatBytes(appInfo.network_stats.bytes_sent) }}
+
+
+
+
+
+
+
Data Received
+
{{ formatBytes(appInfo.network_stats.bytes_recv) }}
+
+
+
+
+
+
Packets Sent
+
{{ formatNumber(appInfo.network_stats.packets_sent) }}
+
+
+
+
+
Packets Received
+
{{ formatNumber(appInfo.network_stats.packets_recv) }}
+
+
+
+
+
+
+
+
+ Reticulum Statistics
+
+
+ Live
+
+
+
+
+
+
+
Total Paths
+
{{ formatNumber(appInfo.reticulum_stats.total_paths) }}
+
+
+
+
+
Announces per Second
+
{{ formatNumber(appInfo.reticulum_stats.announces_per_second) }}
+
+
+
+
+
Announces per Minute
+
{{ formatNumber(appInfo.reticulum_stats.announces_per_minute) }}
+
+
+
+
+
Announces per Hour
+
{{ formatNumber(appInfo.reticulum_stats.announces_per_hour) }}
+
+
+
+
+
+
+
+
+ Download Statistics
+
+
+ Live
+
+
+
+
+
+
+
Average Download Speed
+
+
+ {{ formatBytesPerSecond(appInfo.download_stats.avg_download_speed_bps) }}
+
+ No downloads yet
+
+
+
+
+
+
Reticulum Status
@@ -126,11 +261,21 @@ export default {
return {
appInfo: null,
config: null,
+ updateInterval: null,
};
},
mounted() {
this.getAppInfo();
this.getConfig();
+ // Update stats every 5 seconds
+ this.updateInterval = setInterval(() => {
+ this.getAppInfo();
+ }, 5000);
+ },
+ beforeUnmount() {
+ if (this.updateInterval) {
+ clearInterval(this.updateInterval);
+ }
},
methods: {
async getAppInfo() {
@@ -166,6 +311,12 @@ export default {
formatBytes: function(bytes) {
return Utils.formatBytes(bytes);
},
+ formatNumber: function(num) {
+ return Utils.formatNumber(num);
+ },
+ formatBytesPerSecond: function(bytesPerSecond) {
+ return Utils.formatBytesPerSecond(bytesPerSecond);
+ },
},
computed: {
isElectron() {
diff --git a/src/frontend/components/nomadnetwork/NomadNetworkPage.vue b/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
index fb88e92..948df84 100644
--- a/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
+++ b/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
@@ -124,7 +124,12 @@
- Downloading: {{ nodeFilePath }} ({{ nodeFileProgress }}%)
+
+ Downloading: {{ nodeFilePath }} ({{ nodeFileProgress }}%)
+
+ - {{ formatBytesPerSecond(nodeFileDownloadSpeed) }}
+
+
@@ -179,6 +184,7 @@ import DialogUtils from "../../js/DialogUtils";
import WebSocketConnection from "../../js/WebSocketConnection";
import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
import GlobalEmitter from "../../js/GlobalEmitter";
+import Utils from "../../js/Utils";
export default {
name: 'NomadNetworkPage',
@@ -213,6 +219,10 @@ export default {
isDownloadingNodeFile: false,
nodeFilePath: null,
nodeFileProgress: 0,
+ nodeFileDownloadStartTime: null,
+ nodeFileLastProgressTime: null,
+ nodeFileLastProgressValue: 0,
+ nodeFileDownloadSpeed: null,
nomadnetPageDownloadCallbacks: {},
nomadnetFileDownloadCallbacks: {},
@@ -756,26 +766,72 @@ export default {
this.isDownloadingNodeFile = true;
this.nodeFilePath = parsedUrl.path.split("/").pop();
this.nodeFileProgress = 0;
+ this.nodeFileDownloadStartTime = Date.now();
+ this.nodeFileLastProgressTime = Date.now();
+ this.nodeFileLastProgressValue = 0;
+ this.nodeFileDownloadSpeed = null;
// start file download
this.downloadNomadNetFile(destinationHash, parsedUrl.path, (fileName, fileBytesBase64) => {
+ // Calculate final download speed based on actual file size
+ if (this.nodeFileDownloadStartTime) {
+ const totalTime = (Date.now() - this.nodeFileDownloadStartTime) / 1000; // seconds
+ const fileSizeBytes = atob(fileBytesBase64).length;
+ if (totalTime > 0) {
+ this.nodeFileDownloadSpeed = fileSizeBytes / totalTime;
+ }
+ }
+
// no longer downloading
this.isDownloadingNodeFile = false;
// download file to browser
this.downloadFileFromBase64(fileName, fileBytesBase64);
+ // Clear speed after a moment
+ setTimeout(() => {
+ this.nodeFileDownloadSpeed = null;
+ }, 2000);
+
}, (failureReason) => {
// no longer downloading
this.isDownloadingNodeFile = false;
+ this.nodeFileDownloadSpeed = null;
// show error message
DialogUtils.alert(`Failed to download file: ${failureReason}`);
}, (progress) => {
- this.nodeFileProgress = Math.round(progress * 100);
+ const currentTime = Date.now();
+ const progressValue = progress;
+ this.nodeFileProgress = Math.round(progressValue * 100);
+
+ // Calculate estimated download speed based on progress rate
+ if (this.nodeFileDownloadStartTime && progressValue > 0) {
+ const elapsedTime = (currentTime - this.nodeFileDownloadStartTime) / 1000; // seconds
+ if (elapsedTime > 0.5) { // Only calculate after at least 0.5 seconds
+ // Estimate total file size based on progress rate
+ // If we've downloaded progressValue in elapsedTime, estimate total time
+ const estimatedTotalTime = elapsedTime / progressValue;
+ // Estimate file size based on average download speed assumption
+ // We'll refine this when download completes with actual size
+ // For now, estimate based on typical mesh network file sizes (100KB-10MB range)
+ // Use a conservative estimate that will be updated when download completes
+ const estimatedFileSize = 500 * 1024; // Start with 500KB estimate
+ const estimatedBytesDownloaded = estimatedFileSize * progressValue;
+ const estimatedSpeed = estimatedBytesDownloaded / elapsedTime;
+
+ // Only update if we have a reasonable estimate
+ if (estimatedSpeed > 0 && estimatedSpeed < 100 * 1024 * 1024) { // Cap at 100MB/s
+ this.nodeFileDownloadSpeed = estimatedSpeed;
+ }
+ }
+ }
+
+ this.nodeFileLastProgressTime = currentTime;
+ this.nodeFileLastProgressValue = progressValue;
});
return;
@@ -829,6 +885,9 @@ export default {
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
},
+ formatBytesPerSecond: function(bytesPerSecond) {
+ return Utils.formatBytesPerSecond(bytesPerSecond);
+ },
onNodeClick: function(node) {
// update selected node
diff --git a/src/frontend/js/Utils.js b/src/frontend/js/Utils.js
index 74fcf6a..ca28d7b 100644
--- a/src/frontend/js/Utils.js
+++ b/src/frontend/js/Utils.js
@@ -25,6 +25,13 @@ class Utils {
}
+ static formatNumber(num) {
+ if(num === 0){
+ return '0';
+ }
+ return num.toLocaleString();
+ }
+
static parseSeconds(secondsToFormat) {
secondsToFormat = Number(secondsToFormat);
var days = Math.floor(secondsToFormat / (3600 * 24));
@@ -127,6 +134,22 @@ class Utils {
}
+ static formatBytesPerSecond(bytesPerSecond) {
+
+ if(bytesPerSecond === 0 || bytesPerSecond == null){
+ return '0 B/s';
+ }
+
+ const k = 1024;
+ const decimals = 1;
+ const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s', 'EB/s', 'ZB/s', 'YB/s'];
+
+ const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
+
+ return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
+
+ }
+
static formatFrequency(hz) {
if(hz === 0 || hz == null){