Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0543bd6044
|
|||
|
17d25b2e0a
|
|||
|
1a4b99b201
|
|||
|
9115f6ecfa
|
|||
|
918fcb051c
|
|||
|
acbe3597d6
|
|||
|
3566c6b2da
|
|||
|
8b044f6dab
|
|||
|
4c4b963aef
|
|||
|
38ac972960
|
|||
|
becd3aa15d
|
|||
|
ac907308c0
|
|||
|
fa2fe6a15d
|
|||
|
927255f44c
|
|||
|
0318cb7e4a
|
20
.github/workflows/bearer-pr.yml
vendored
Normal file
20
.github/workflows/bearer-pr.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Bearer PR Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rule_check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
|
- name: Bearer
|
||||||
|
uses: bearer/bearer-action@828eeb928ce2f4a7ca5ed57fb8b59508cb8c79bc # v2
|
||||||
|
with:
|
||||||
|
diff: true
|
||||||
29
.github/workflows/bearer.yml
vendored
Normal file
29
.github/workflows/bearer.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Bearer Master
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rule_check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
|
- name: Bearer
|
||||||
|
uses: bearer/bearer-action@828eeb928ce2f4a7ca5ed57fb8b59508cb8c79bc # v2
|
||||||
|
with:
|
||||||
|
format: sarif
|
||||||
|
output: results.sarif
|
||||||
|
|
||||||
|
- name: Upload SARIF file
|
||||||
|
if: always()
|
||||||
|
uses: github/codeql-action/upload-sarif@2827891b2e5e0510dceab8c3619f4fe255451277 # v4
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
|
category: bearer-security-scan
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ node_modules
|
|||||||
|
|
||||||
# local storage
|
# local storage
|
||||||
storage/
|
storage/
|
||||||
|
|
||||||
|
*.pyc
|
||||||
@@ -25,7 +25,11 @@ WORKDIR /app
|
|||||||
|
|
||||||
# Install Python deps
|
# Install Python deps
|
||||||
COPY ./requirements.txt .
|
COPY ./requirements.txt .
|
||||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev && \
|
RUN apk add --no-cache --virtual .build-deps \
|
||||||
|
gcc \
|
||||||
|
musl-dev \
|
||||||
|
linux-headers \
|
||||||
|
python3-dev && \
|
||||||
pip install -r requirements.txt && \
|
pip install -r requirements.txt && \
|
||||||
apk del .build-deps
|
apk del .build-deps
|
||||||
|
|
||||||
|
|||||||
23
Makefile
Normal file
23
Makefile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.PHONY: install run clean
|
||||||
|
|
||||||
|
VENV = venv
|
||||||
|
PYTHON = $(VENV)/bin/python
|
||||||
|
PIP = $(VENV)/bin/pip
|
||||||
|
|
||||||
|
install: $(VENV)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
$(VENV):
|
||||||
|
python3 -m venv $(VENV)
|
||||||
|
$(PIP) install --upgrade pip
|
||||||
|
$(PIP) install -r requirements.txt
|
||||||
|
|
||||||
|
run: install
|
||||||
|
$(PYTHON) meshchat.py
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(VENV)
|
||||||
|
rm -rf node_modules
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
301
README.md
301
README.md
@@ -1,297 +1,36 @@
|
|||||||
<p align="center">
|
# Reticulum MeshChatX
|
||||||
<a href="https://github.com/liamcottle/reticulum-meshchat"><img src="./logo/logo-chat-bubble.png" width="150"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 align="center">Reticulum MeshChat</h2>
|
Custom fork of [Reticulum MeshChat](https://github.com/liamcottle/reticulum-meshchat)
|
||||||
|
|
||||||
<p align="center">
|
## Features of this fork
|
||||||
<a href="https://discord.gg/APQSQZNV7t"><img src="https://img.shields.io/badge/Discord-Liam%20Cottle's%20Discord-%237289DA?style=flat&logo=discord" alt="discord"/></a>
|
|
||||||
<a href="https://twitter.com/liamcottle"><img src="https://img.shields.io/badge/Twitter-@liamcottle-%231DA1F2?style=flat&logo=twitter" alt="twitter"/></a>
|
|
||||||
<br/>
|
|
||||||
<a href="https://ko-fi.com/liamcottle"><img src="https://img.shields.io/badge/Donate%20a%20Coffee-liamcottle-yellow?style=flat&logo=buy-me-a-coffee" alt="donate on ko-fi"/></a>
|
|
||||||
<a href="./donate.md"><img src="https://img.shields.io/badge/Donate%20Bitcoin-bc1qy22smke8n4c54evdxmp7lpy9p0e6m9tavtlg2q-%23FF9900?style=flat&logo=bitcoin" alt="donate bitcoin"/></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## What is Reticulum MeshChat?
|
- More stats in about page.
|
||||||
|
- Exe and Appimage builds with Python 3.13 and Node.js 22
|
||||||
|
- Actions are pinned to full-length SHA hashes.
|
||||||
|
- Docker images are smaller and use SHA256 hashes for the images.
|
||||||
|
|
||||||
A simple mesh network communications app powered by the [Reticulum Network Stack](https://github.com/markqvist/Reticulum).
|
## Usage
|
||||||
|
|
||||||
<img src="./screenshots/screenshot.png">
|
Check [releases](https://github.com/Sudo-Ivan/reticulum-meshchatX/releases) for pre-built binaries or appimages.
|
||||||
|
|
||||||
## What does it do?
|
## Building
|
||||||
|
|
||||||
- It can send and receive messages, files and audio calls with peers;
|
```bash
|
||||||
- Over your local network through Ethernet and WiFi, completely automatically.
|
make install
|
||||||
- Over the internet by connecting through a server [hosted by yourself](https://reticulum.network/manual/interfaces.html#tcp-server-interface) or [the community](https://reticulum.network/connect.html).
|
make build
|
||||||
- Over low-powered, license-free, ISM band LoRa Radio, with an [RNode](https://github.com/markqvist/RNode_Firmware).
|
|
||||||
- ...and via [any other interface](https://reticulum.network/manual/interfaces.html) supported by the Reticulum Network Stack.
|
|
||||||
- It communicates securely. Messages can only be decrypted by the intended destination.
|
|
||||||
- It can communicate with any other existing [LXMF](https://github.com/markqvist/lxmf) client, such as [Sideband](https://github.com/markqvist/Sideband/) and [Nomadnet](https://github.com/markqvist/nomadnet).
|
|
||||||
- It can download files and browse micron pages (decentralised websites) hosted on [Nomad Network](https://github.com/markqvist/nomadnet) nodes.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Supports sending and receiving messages between [Reticulum MeshChat](https://github.com/liamcottle/reticulum-meshchat), [Sideband](https://github.com/markqvist/Sideband/) and [Nomadnet](https://github.com/markqvist/nomadnet).
|
|
||||||
- Supports receiving and saving images and attachments sent from Sideband.
|
|
||||||
- Supports sending images, voice recordings and file attachments.
|
|
||||||
- Supports saving inbound and outbound messages to a local database.
|
|
||||||
- Supports sending an announce to the network.
|
|
||||||
- Supports setting a custom display name to send in your announce.
|
|
||||||
- Supports viewing and searching peers discovered from announces.
|
|
||||||
- Supports auto resending undelivered messages when an announce is received from the recipient.
|
|
||||||
- Supports sending messages to and syncing messages from [LXMF Propagation Nodes](https://github.com/markqvist/lxmf?tab=readme-ov-file#propagation-nodes).
|
|
||||||
- Supports running a local LXMF Propagation Node so other users can use your device for message storage and retrieval.
|
|
||||||
- Support for browsing pages, and downloading files hosted on Nomad Network Nodes.
|
|
||||||
|
|
||||||
## Beta Features
|
|
||||||
|
|
||||||
- Support for Audio Calls to other [Reticulum MeshChat](https://github.com/liamcottle/reticulum-meshchat) users.
|
|
||||||
- Audio is encoded with [codec2](https://github.com/drowe67/codec2) to support low bandwidth links.
|
|
||||||
- Using a microphone requires using the web ui over localhost or https, due to [AudioWorklet](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet) secure context.
|
|
||||||
- I have tested two-way audio calls over LoRa with a single hop. It works well when a [reasonable bitrate](https://unsigned.io/understanding-lora-parameters/) is configured on the RNode.
|
|
||||||
- Some browsers such as FireFox don't work as expected. Try using a Chromium based browser if running via the command line.
|
|
||||||
|
|
||||||
## Download
|
|
||||||
|
|
||||||
You can download the latest version for Windows, Mac and Linux from the [releases](https://github.com/liamcottle/reticulum-meshchat/releases) page.
|
|
||||||
|
|
||||||
Alternatively, you can download the source and run it manually from a command line.
|
|
||||||
|
|
||||||
See the ["How to use it?"](#how-to-use-it) section, further down on how to do this.
|
|
||||||
|
|
||||||
## Other Installation Methods
|
|
||||||
|
|
||||||
- [Running MeshChat on Docker](./docs/meshchat_on_docker.md)
|
|
||||||
- [Running MeshChat on a Raspberry Pi](./docs/meshchat_on_raspberry_pi.md)
|
|
||||||
- [Running MeshChat on Android with Termux](./docs/meshchat_on_android_with_termux.md)
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
Once you've downloaded, installed and launched Reticulum MeshChat, there's a few things you need to do in order to start communicating with other people on the network.
|
|
||||||
|
|
||||||
1. Create an Identity
|
|
||||||
2. Configure your Display Name
|
|
||||||
3. Send an Announce
|
|
||||||
4. Discover Peers and start sending messages
|
|
||||||
5. Configuring additional Network Interfaces
|
|
||||||
|
|
||||||
**Create an Identity**
|
|
||||||
|
|
||||||
On the Reticulum Network, anyone can have any number of Identities. You may opt to use your real name, or you may decide to be completely anonymous. The choice is yours.
|
|
||||||
|
|
||||||
A Reticulum Identity is a public/private key-pair. You control the private key used to generate destination addresses, encrypt content and prove receipt of data with unforgeable delivery acknowledgements.
|
|
||||||
|
|
||||||
Your public key is shared with the network when you send an announce, and allows others on the network to automatically discover a route to a destination you control.
|
|
||||||
|
|
||||||
At this time, Reticulum MeshChat generates a new Identity the first time you launch it. A future update will allow you to create and manage multiple identities.
|
|
||||||
|
|
||||||
For now, if you want to change, or reset your identity, you can access the identity file at `~/.reticulum-meshchat/identity`.
|
|
||||||
|
|
||||||
**Configure your Display Name**
|
|
||||||
|
|
||||||
The next thing you should do, is set a display name. Your display name is what everyone else on the network will see when looking for someone to communicate with from the Peers list.
|
|
||||||
|
|
||||||
You can do this in the `My Identity` section in the bottom left corner. Enter a new display name, and then press `Save`.
|
|
||||||
|
|
||||||
**Send an Announce**
|
|
||||||
|
|
||||||
When using the Reticulum Network, in order to be contactable, you need to send an `Announce`. You can send an announce as often, or as infrequently as you like.
|
|
||||||
|
|
||||||
Sending an announce allows other peers on the network to discover the next-hop across the network their packets should take to arrive at a destination that your identity controls.
|
|
||||||
|
|
||||||
If you never send an announce, you will be invisible and no one will ever be able to send anything to you.
|
|
||||||
|
|
||||||
When you move across the network, and change entrypoints, such as moving from your home WiFi network, to plugging in to an Ethernet port in a local library or even climbing a mountain and using an RNode over LoRa radio, other peers on the network will only know the previous path to your destinations.
|
|
||||||
|
|
||||||
To allow them to discover the new path their packets should take to reach you, you should send an announce.
|
|
||||||
|
|
||||||
**Discover Peers and start sending messages**
|
|
||||||
|
|
||||||
In the Reticulum Network, you can control an unlimited number of destination addresses. One of these can be an [LXMF](https://github.com/markqvist/lxmf) delivery address.
|
|
||||||
|
|
||||||
Your Reticulum Identity allows you to have an LXMF address. Think of an LXMF address as your very own, secure, end-to-end encrypted, unspoofable, email address routed over a mesh network.
|
|
||||||
|
|
||||||
When someone else on the network announces themselves (more specifically, their LXMF address), they will show up in the Peers tab.
|
|
||||||
|
|
||||||
You can click on any of these discovered peers to open a messaging interface. From here, you can send text messages, files and inline images. If they respond, their messages will show up there too.
|
|
||||||
|
|
||||||
As well as being able to announce your LXMF address and discover others, Reticulum MeshChat can also discover [Nomad Network](https://github.com/markqvist/nomadnet) nodes hosted by other users. From the Nodes tab, you are free to explore pages and download files they may be publicly sharing on the network.
|
|
||||||
|
|
||||||
A future update is planned to allow you to host your own Node and share pages and files with other peers on the network. For now, you could use the official [Nomad Network](https://github.com/markqvist/nomadnet) client to do this.
|
|
||||||
|
|
||||||
Remember, in order to connect with other peers or nodes, they must announce on the network. So don't forget to announce if you want to be discovered!
|
|
||||||
|
|
||||||
**Configuring additional Network Interfaces**
|
|
||||||
|
|
||||||
> TODO: this section is yet to be written. For now, you can check out the [official documentation for configuring interfaces](https://reticulum.network/manual/interfaces.html) in the Reticulum config file. This file is located at `~/.reticulum/config`
|
|
||||||
|
|
||||||
## How does it work?
|
|
||||||
|
|
||||||
- A python script ([meshchat.py](./meshchat.py)) runs a Reticulum instance and a WebSocket server.
|
|
||||||
- The web page sends and receives LXMF packets encoded in json via the WebSocket.
|
|
||||||
- Web Browser -> WebSocket -> Python Reticulum -> (configured interfaces) -> (destination)
|
|
||||||
- LXMF messages sent and received are saved to a local SQLite database.
|
|
||||||
|
|
||||||
## How to use it?
|
|
||||||
|
|
||||||
It is recommended that you [download](#download) a standalone application.
|
|
||||||
|
|
||||||
If you don't want to, or a release is unavailable for your device, you will need to;
|
|
||||||
|
|
||||||
- install [Python 3](https://www.python.org/downloads/)
|
|
||||||
- install [NodeJS v18+](https://nodejs.org/en)
|
|
||||||
- clone the source code from this repo
|
|
||||||
- install all dependencies
|
|
||||||
- then run `meshchat.py`.
|
|
||||||
|
|
||||||
```
|
|
||||||
# clone repo
|
|
||||||
git clone https://github.com/liamcottle/reticulum-meshchat
|
|
||||||
cd reticulum-meshchat
|
|
||||||
|
|
||||||
# install nodejs deps
|
|
||||||
# if you want to build electron binaries, remove "--omit=dev"
|
|
||||||
# if you're using termux, add "--ignore-scripts" to fix error with esbuild
|
|
||||||
npm install --omit=dev
|
|
||||||
|
|
||||||
# build frontend vue components
|
|
||||||
npm run build-frontend
|
|
||||||
|
|
||||||
# install python deps
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# run meshchat
|
|
||||||
python meshchat.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> NOTE: You should now be able to access the web interface at http://localhost:8000
|
### Building in Docker
|
||||||
|
|
||||||
For a full list of command line options, you can run;
|
```bash
|
||||||
|
make docker-build
|
||||||
```
|
|
||||||
python meshchat.py --help
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
The build will be in the `dist` directory.
|
||||||
usage: meshchat.py [-h] [--host [HOST]] [--port [PORT]] [--headless] [--identity-file IDENTITY_FILE] [--identity-base64 IDENTITY_BASE64] [--generate-identity-file GENERATE_IDENTITY_FILE] [--generate-identity-base64]
|
|
||||||
[--reticulum-config-dir RETICULUM_CONFIG_DIR] [--storage-dir STORAGE_DIR]
|
|
||||||
|
|
||||||
ReticulumMeshChat
|
## Development
|
||||||
|
|
||||||
options:
|
```bash
|
||||||
-h, --help show this help message and exit
|
make develop
|
||||||
--host [HOST] The address the web server should listen on.
|
|
||||||
--port [PORT] The port the web server should listen on.
|
|
||||||
--headless Web browser will not automatically launch when this flag is passed.
|
|
||||||
--identity-file IDENTITY_FILE
|
|
||||||
Path to a Reticulum Identity file to use as your LXMF address.
|
|
||||||
--identity-base64 IDENTITY_BASE64
|
|
||||||
A base64 encoded Reticulum Identity to use as your LXMF address.
|
|
||||||
--generate-identity-file GENERATE_IDENTITY_FILE
|
|
||||||
Generates and saves a new Reticulum Identity to the provided file path and then exits.
|
|
||||||
--generate-identity-base64
|
|
||||||
Outputs a randomly generated Reticulum Identity as base64 and then exits.
|
|
||||||
--reticulum-config-dir RETICULUM_CONFIG_DIR
|
|
||||||
Path to a Reticulum config directory for the RNS stack to use (e.g: ~/.reticulum)
|
|
||||||
--storage-dir STORAGE_DIR
|
|
||||||
Path to a directory for storing databases and config files (default: ./storage)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using an existing Reticulum Identity
|
|
||||||
|
|
||||||
The first time you run this application, a new Reticulum identity is generated and saved to `storage/identity`.
|
|
||||||
|
|
||||||
If you want to use an existing identity;
|
|
||||||
|
|
||||||
- You can overwrite `storage/identity` with another identity file.
|
|
||||||
- Or, you can pass in a custom identity file path as a command line argument.
|
|
||||||
|
|
||||||
To use a custom identity file, provide the `--identity-file` argument followed by the path to your custom identity file.
|
|
||||||
|
|
||||||
```
|
|
||||||
python meshchat.py --identity-file ./custom_identity_file
|
|
||||||
```
|
|
||||||
|
|
||||||
If you would like to generate a new identity, you can use the [rnid](https://reticulum.network/manual/using.html#the-rnid-utility) utility provided by Reticulum.
|
|
||||||
|
|
||||||
```
|
|
||||||
rnid --generate ./new_identity_file
|
|
||||||
```
|
|
||||||
|
|
||||||
If you don't have access to the `rnid` command, you can use the following:
|
|
||||||
|
|
||||||
```
|
|
||||||
python meshchat.py --generate-identity-file ./new_identity_file
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, you can provide a base64 encoded private key, like so;
|
|
||||||
|
|
||||||
```
|
|
||||||
python meshchat.py --identity-base64 "GCN6mMhVemdNIK/fw97C1zvU17qjQPFTXRBotVckeGmoOwQIF8VOjXwNNem3CUOJZCQQpJuc/4U94VSsC39Phw=="
|
|
||||||
```
|
|
||||||
|
|
||||||
> NOTE: this is a randomly generated identity for example purposes. Do not use it, it has been leaked!
|
|
||||||
|
|
||||||
## Build Electron Application
|
|
||||||
|
|
||||||
Reticulum MeshChat can be run from source via a command line, as explained above, or as a standalone application.
|
|
||||||
|
|
||||||
To run as a standalone application, we need to compile the python script and dependencies to an executable with [cxfreeze](https://github.com/marcelotduarte/cx_Freeze) and then build an [Electron](https://www.electronjs.org/) app which includes a bundled browser that can interact with the compiled python executable.
|
|
||||||
|
|
||||||
This allows for the entire application to be run by double clicking a single file without the need for a user to manually install python, nor run any commands in a command line application.
|
|
||||||
|
|
||||||
To build a `.exe` when running on Windows or a `.dmg` when running on a Mac, run the following;
|
|
||||||
|
|
||||||
```
|
|
||||||
pip install -r requirements.txt
|
|
||||||
npm install
|
|
||||||
npm run dist
|
|
||||||
```
|
|
||||||
|
|
||||||
> Note: cxfreeze only supports building an executable for the current platform. You will need a Mac to build for Mac, and a Windows PC to build for Windows.
|
|
||||||
|
|
||||||
Once completed, you should have a `.exe` or a `.dmg` in the `dist` folder.
|
|
||||||
|
|
||||||
## Local Development
|
|
||||||
|
|
||||||
I normally run the following commands to work on the project locally.
|
|
||||||
|
|
||||||
**Install dependencies**
|
|
||||||
|
|
||||||
```
|
|
||||||
pip install -r requirements.txt
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
**Build and run Electron App**
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run electron
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**or; Build and run MeshChat Server**
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run build-frontend
|
|
||||||
python3 meshchat.py --headless
|
|
||||||
```
|
|
||||||
|
|
||||||
I build the vite app everytime without hot reload, since MeshChat expects everything over its own port, not the vite server port. I will attempt to fix this in the future.
|
|
||||||
|
|
||||||
## TODO
|
|
||||||
|
|
||||||
- [ ] button to forget announces
|
|
||||||
|
|
||||||
# Notes
|
|
||||||
|
|
||||||
**LXMF Router**
|
|
||||||
|
|
||||||
- By default, the LXMF router rejects inbound messages larger than 1mb.
|
|
||||||
- LXMF clients are likely to have [this default limit](https://github.com/markqvist/LXMF/blob/c426c93cc5d63a3dae18ad2264b1299a7ad9e46c/LXMF/LXMRouter.py#L38), and your messages will [fail to send](https://github.com/markqvist/LXMF/blob/c426c93cc5d63a3dae18ad2264b1299a7ad9e46c/LXMF/LXMRouter.py#L1428).
|
|
||||||
- MeshChat has increased the receive limit to 10mb to allow for larger attachments.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|||||||
77
meshchat.py
77
meshchat.py
@@ -22,6 +22,7 @@ import webbrowser
|
|||||||
|
|
||||||
from peewee import SqliteDatabase
|
from peewee import SqliteDatabase
|
||||||
from serial.tools import list_ports
|
from serial.tools import list_ports
|
||||||
|
import psutil
|
||||||
|
|
||||||
import database
|
import database
|
||||||
from src.backend.announce_handler import AnnounceHandler
|
from src.backend.announce_handler import AnnounceHandler
|
||||||
@@ -147,6 +148,12 @@ class ReticulumMeshChat:
|
|||||||
# remember websocket clients
|
# remember websocket clients
|
||||||
self.websocket_clients: List[web.WebSocketResponse] = []
|
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
|
# register audio call identity
|
||||||
self.audio_call_manager = AudioCallManager(identity=self.identity)
|
self.audio_call_manager = AudioCallManager(identity=self.identity)
|
||||||
self.audio_call_manager.register_incoming_call_callback(self.on_incoming_audio_call)
|
self.audio_call_manager.register_incoming_call_callback(self.on_incoming_audio_call)
|
||||||
@@ -954,6 +961,34 @@ class ReticulumMeshChat:
|
|||||||
# get app info
|
# get app info
|
||||||
@routes.get("/api/v1/app/info")
|
@routes.get("/api/v1/app/info")
|
||||||
async def index(request):
|
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({
|
return web.json_response({
|
||||||
"app_info": {
|
"app_info": {
|
||||||
"version": self.get_app_version(),
|
"version": self.get_app_version(),
|
||||||
@@ -966,6 +1001,25 @@ class ReticulumMeshChat:
|
|||||||
"reticulum_config_path": self.reticulum.configpath,
|
"reticulum_config_path": self.reticulum.configpath,
|
||||||
"is_connected_to_shared_instance": self.reticulum.is_connected_to_shared_instance,
|
"is_connected_to_shared_instance": self.reticulum.is_connected_to_shared_instance,
|
||||||
"is_transport_enabled": self.reticulum.transport_enabled(),
|
"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
|
# handle successful file download
|
||||||
def on_file_download_success(file_name, file_bytes):
|
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({
|
AsyncUtils.run_async(client.send_str(json.dumps({
|
||||||
"type": "nomadnet.file.download",
|
"type": "nomadnet.file.download",
|
||||||
"nomadnet_file_download": {
|
"nomadnet_file_download": {
|
||||||
@@ -2282,6 +2346,7 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# download the file
|
# download the file
|
||||||
downloader = NomadnetFileDownloader(destination_hash, file_path, on_file_download_success, on_file_download_failure, on_file_download_progress)
|
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())
|
AsyncUtils.run_async(downloader.download())
|
||||||
|
|
||||||
# handle downloading a page from a nomadnet node
|
# handle downloading a page from a nomadnet node
|
||||||
@@ -3054,6 +3119,9 @@ class ReticulumMeshChat:
|
|||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [call.audio]")
|
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
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||||
|
|
||||||
@@ -3075,6 +3143,9 @@ class ReticulumMeshChat:
|
|||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.delivery]")
|
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
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||||
|
|
||||||
@@ -3100,6 +3171,9 @@ class ReticulumMeshChat:
|
|||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.propagation]")
|
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
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||||
|
|
||||||
@@ -3184,6 +3258,9 @@ class ReticulumMeshChat:
|
|||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [nomadnetwork.node]")
|
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
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||||
|
|
||||||
|
|||||||
1900
package-lock.json
generated
1900
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "reticulum-meshchat",
|
"name": "reticulum-meshchat",
|
||||||
"version": "2.2.1",
|
"version": "2.32.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "electron/main.js",
|
"main": "electron/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
aiohttp>=3.12.14
|
aiohttp>=3.12.14
|
||||||
cx_freeze>=7.0.0
|
cx_freeze>=7.0.0
|
||||||
lxmf>=0.9.2
|
lxmf>=0.9.3
|
||||||
peewee>=3.18.1
|
peewee>=3.18.1
|
||||||
rns>=1.0.1
|
psutil>=7.1.3
|
||||||
|
rns>=1.0.3
|
||||||
websockets>=14.2
|
websockets>=14.2
|
||||||
|
|||||||
@@ -64,6 +64,141 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- system resources -->
|
||||||
|
<div v-if="appInfo && appInfo.memory_usage" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||||
|
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||||
|
System Resources
|
||||||
|
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||||
|
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||||
|
Live
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||||
|
|
||||||
|
<!-- memory usage -->
|
||||||
|
<div class="flex p-1">
|
||||||
|
<div class="mr-auto">
|
||||||
|
<div>Memory Usage (RSS)</div>
|
||||||
|
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.memory_usage.rss) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- virtual memory -->
|
||||||
|
<div class="flex p-1">
|
||||||
|
<div class="mr-auto">
|
||||||
|
<div>Virtual Memory Size</div>
|
||||||
|
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.memory_usage.vms) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- network statistics -->
|
||||||
|
<div v-if="appInfo && appInfo.network_stats" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||||
|
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||||
|
Network Statistics
|
||||||
|
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||||
|
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||||
|
Live
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||||
|
|
||||||
|
<!-- bytes sent -->
|
||||||
|
<div class="flex p-1">
|
||||||
|
<div class="mr-auto">
|
||||||
|
<div>Data Sent</div>
|
||||||
|
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.network_stats.bytes_sent) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- bytes received -->
|
||||||
|
<div class="flex p-1">
|
||||||
|
<div class="mr-auto">
|
||||||
|
<div>Data Received</div>
|
||||||
|
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.network_stats.bytes_recv) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- packets sent -->
|
||||||
|
<div class="p-1">
|
||||||
|
<div>Packets Sent</div>
|
||||||
|
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.network_stats.packets_sent) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- packets received -->
|
||||||
|
<div class="p-1">
|
||||||
|
<div>Packets Received</div>
|
||||||
|
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.network_stats.packets_recv) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- reticulum statistics -->
|
||||||
|
<div v-if="appInfo && appInfo.reticulum_stats" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||||
|
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||||
|
Reticulum Statistics
|
||||||
|
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||||
|
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||||
|
Live
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||||
|
|
||||||
|
<!-- total paths -->
|
||||||
|
<div class="p-1">
|
||||||
|
<div>Total Paths</div>
|
||||||
|
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.total_paths) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- announces per second -->
|
||||||
|
<div class="p-1">
|
||||||
|
<div>Announces per Second</div>
|
||||||
|
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.announces_per_second) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- announces per minute -->
|
||||||
|
<div class="p-1">
|
||||||
|
<div>Announces per Minute</div>
|
||||||
|
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.announces_per_minute) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- announces per hour -->
|
||||||
|
<div class="p-1">
|
||||||
|
<div>Announces per Hour</div>
|
||||||
|
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.announces_per_hour) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- download statistics -->
|
||||||
|
<div v-if="appInfo && appInfo.download_stats" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||||
|
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||||
|
Download Statistics
|
||||||
|
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||||
|
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||||
|
Live
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||||
|
|
||||||
|
<!-- average download speed -->
|
||||||
|
<div class="p-1">
|
||||||
|
<div>Average Download Speed</div>
|
||||||
|
<div class="text-sm text-gray-700 dark:text-zinc-400">
|
||||||
|
<span v-if="appInfo.download_stats.avg_download_speed_bps !== null">
|
||||||
|
{{ formatBytesPerSecond(appInfo.download_stats.avg_download_speed_bps) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>No downloads yet</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- reticulum status -->
|
<!-- reticulum status -->
|
||||||
<div v-if="appInfo" class="bg-white dark:bg-zinc-900 rounded shadow">
|
<div v-if="appInfo" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||||
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">Reticulum Status</div>
|
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">Reticulum Status</div>
|
||||||
@@ -126,11 +261,21 @@ export default {
|
|||||||
return {
|
return {
|
||||||
appInfo: null,
|
appInfo: null,
|
||||||
config: null,
|
config: null,
|
||||||
|
updateInterval: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getAppInfo();
|
this.getAppInfo();
|
||||||
this.getConfig();
|
this.getConfig();
|
||||||
|
// Update stats every 5 seconds
|
||||||
|
this.updateInterval = setInterval(() => {
|
||||||
|
this.getAppInfo();
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.updateInterval) {
|
||||||
|
clearInterval(this.updateInterval);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async getAppInfo() {
|
async getAppInfo() {
|
||||||
@@ -166,6 +311,12 @@ export default {
|
|||||||
formatBytes: function(bytes) {
|
formatBytes: function(bytes) {
|
||||||
return Utils.formatBytes(bytes);
|
return Utils.formatBytes(bytes);
|
||||||
},
|
},
|
||||||
|
formatNumber: function(num) {
|
||||||
|
return Utils.formatNumber(num);
|
||||||
|
},
|
||||||
|
formatBytesPerSecond: function(bytesPerSecond) {
|
||||||
|
return Utils.formatBytesPerSecond(bytesPerSecond);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isElectron() {
|
isElectron() {
|
||||||
|
|||||||
@@ -124,7 +124,12 @@
|
|||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-auto">Downloading: {{ nodeFilePath }} ({{ nodeFileProgress }}%)</div>
|
<div class="my-auto">
|
||||||
|
Downloading: {{ nodeFilePath }} ({{ nodeFileProgress }}%)
|
||||||
|
<span v-if="nodeFileDownloadSpeed !== null" class="ml-2 text-sm">
|
||||||
|
- {{ formatBytesPerSecond(nodeFileDownloadSpeed) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -179,6 +184,7 @@ import DialogUtils from "../../js/DialogUtils";
|
|||||||
import WebSocketConnection from "../../js/WebSocketConnection";
|
import WebSocketConnection from "../../js/WebSocketConnection";
|
||||||
import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
|
import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
|
||||||
import GlobalEmitter from "../../js/GlobalEmitter";
|
import GlobalEmitter from "../../js/GlobalEmitter";
|
||||||
|
import Utils from "../../js/Utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NomadNetworkPage',
|
name: 'NomadNetworkPage',
|
||||||
@@ -213,6 +219,10 @@ export default {
|
|||||||
isDownloadingNodeFile: false,
|
isDownloadingNodeFile: false,
|
||||||
nodeFilePath: null,
|
nodeFilePath: null,
|
||||||
nodeFileProgress: 0,
|
nodeFileProgress: 0,
|
||||||
|
nodeFileDownloadStartTime: null,
|
||||||
|
nodeFileLastProgressTime: null,
|
||||||
|
nodeFileLastProgressValue: 0,
|
||||||
|
nodeFileDownloadSpeed: null,
|
||||||
|
|
||||||
nomadnetPageDownloadCallbacks: {},
|
nomadnetPageDownloadCallbacks: {},
|
||||||
nomadnetFileDownloadCallbacks: {},
|
nomadnetFileDownloadCallbacks: {},
|
||||||
@@ -756,26 +766,72 @@ export default {
|
|||||||
this.isDownloadingNodeFile = true;
|
this.isDownloadingNodeFile = true;
|
||||||
this.nodeFilePath = parsedUrl.path.split("/").pop();
|
this.nodeFilePath = parsedUrl.path.split("/").pop();
|
||||||
this.nodeFileProgress = 0;
|
this.nodeFileProgress = 0;
|
||||||
|
this.nodeFileDownloadStartTime = Date.now();
|
||||||
|
this.nodeFileLastProgressTime = Date.now();
|
||||||
|
this.nodeFileLastProgressValue = 0;
|
||||||
|
this.nodeFileDownloadSpeed = null;
|
||||||
|
|
||||||
// start file download
|
// start file download
|
||||||
this.downloadNomadNetFile(destinationHash, parsedUrl.path, (fileName, fileBytesBase64) => {
|
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
|
// no longer downloading
|
||||||
this.isDownloadingNodeFile = false;
|
this.isDownloadingNodeFile = false;
|
||||||
|
|
||||||
// download file to browser
|
// download file to browser
|
||||||
this.downloadFileFromBase64(fileName, fileBytesBase64);
|
this.downloadFileFromBase64(fileName, fileBytesBase64);
|
||||||
|
|
||||||
|
// Clear speed after a moment
|
||||||
|
setTimeout(() => {
|
||||||
|
this.nodeFileDownloadSpeed = null;
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
}, (failureReason) => {
|
}, (failureReason) => {
|
||||||
|
|
||||||
// no longer downloading
|
// no longer downloading
|
||||||
this.isDownloadingNodeFile = false;
|
this.isDownloadingNodeFile = false;
|
||||||
|
this.nodeFileDownloadSpeed = null;
|
||||||
|
|
||||||
// show error message
|
// show error message
|
||||||
DialogUtils.alert(`Failed to download file: ${failureReason}`);
|
DialogUtils.alert(`Failed to download file: ${failureReason}`);
|
||||||
|
|
||||||
}, (progress) => {
|
}, (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;
|
return;
|
||||||
@@ -829,6 +885,9 @@ export default {
|
|||||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
|
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
formatBytesPerSecond: function(bytesPerSecond) {
|
||||||
|
return Utils.formatBytesPerSecond(bytesPerSecond);
|
||||||
|
},
|
||||||
onNodeClick: function(node) {
|
onNodeClick: function(node) {
|
||||||
|
|
||||||
// update selected node
|
// update selected node
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ class Utils {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static formatNumber(num) {
|
||||||
|
if(num === 0){
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return num.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
static parseSeconds(secondsToFormat) {
|
static parseSeconds(secondsToFormat) {
|
||||||
secondsToFormat = Number(secondsToFormat);
|
secondsToFormat = Number(secondsToFormat);
|
||||||
var days = Math.floor(secondsToFormat / (3600 * 24));
|
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) {
|
static formatFrequency(hz) {
|
||||||
|
|
||||||
if(hz === 0 || hz == null){
|
if(hz === 0 || hz == null){
|
||||||
|
|||||||
Reference in New Issue
Block a user