Merge remote-tracking branch 'upstream/main'

This commit is contained in:
jacob.eva 2024-12-14 21:16:40 +00:00
commit 1c461d644d
No known key found for this signature in database
GPG Key ID: 0B92E083BBCCAA1E
19 changed files with 912 additions and 162 deletions

View File

@ -20,7 +20,7 @@ RUN git clone https://github.com/jacobeva/Reticulum \
# Switch branches # Switch branches
WORKDIR "Reticulum" WORKDIR "Reticulum"
RUN git switch ble-dev #RUN git switch ble-dev
WORKDIR ".." WORKDIR ".."

View File

@ -31,6 +31,9 @@ preparewheel:
build_wheel: build_wheel:
. sbapp/venv/bin/activate; python3 setup.py sdist bdist_wheel . sbapp/venv/bin/activate; python3 setup.py sdist bdist_wheel
build_win_exe:
python -m PyInstaller sideband.spec --noconfirm
release: build_wheel apk fetchapk release: build_wheel apk fetchapk
release_docker: release_docker:

View File

@ -39,7 +39,7 @@ Sideband can run on most computing devices, but installation methods vary by dev
## On Android ## On Android
For your Android devices, you can install Sideband through F-Droid, by adding the [Between the Borders Repo](https://reticulum.betweentheborders.com/fdroid/repo/), or you can download an [APK on the latest release](https://github.com/markqvist/Sideband/releases/latest) page. Both sources are signed with the same release keys, and can be used interchangably. For your Android devices, you can install Sideband through F-Droid, by adding the [Between the Borders Repo](https://reticulum.betweentheborders.com/fdroid/repo/), or you can download an [APK on the latest release page](https://github.com/markqvist/Sideband/releases/latest). Both sources are signed with the same release keys, and can be used interchangably.
After the application is installed on your Android device, it is also possible to pull updates directly through the **Repository** section of the application. After the application is installed on your Android device, it is also possible to pull updates directly through the **Repository** section of the application.
@ -47,6 +47,8 @@ After the application is installed on your Android device, it is also possible t
On all Linux-based operating systems, Sideband is available as a `pipx`/`pip` package. This installation method **includes desktop integration**, so that Sideband will show up in your applications menu and launchers. Below are install steps for the most common recent Linux distros. For Debian 11, see the end of this section. On all Linux-based operating systems, Sideband is available as a `pipx`/`pip` package. This installation method **includes desktop integration**, so that Sideband will show up in your applications menu and launchers. Below are install steps for the most common recent Linux distros. For Debian 11, see the end of this section.
**Please note!** The very latest Python release, Python 3.13 is currently **not** compatible with the Kivy framework, that Sideband uses to render its user interface. If your Linux distribution uses Python 3.13 as its default Python installation, you will need to install an earlier version as well. Using [the latest release of Python 3.12](https://www.python.org/downloads/release/python-3127/) is recommended.
You will first need to install a few dependencies for audio messaging and Codec2 support to work: You will first need to install a few dependencies for audio messaging and Codec2 support to work:
```bash ```bash
@ -66,6 +68,10 @@ Once those are installed, install the Sideband application itself:
```bash ```bash
# Finally, install Sideband using pipx: # Finally, install Sideband using pipx:
pipx install sbapp pipx install sbapp
# If you need to specify a specific Python version,
# use something like the following:
pipx install sbapp --python python3.12
``` ```
After installation, you can now run Sideband in a number of different ways: After installation, you can now run Sideband in a number of different ways:
@ -109,8 +115,9 @@ pip install sbapp --break-system-packages
# any of the normal UI dependencies: # any of the normal UI dependencies:
pip install sbapp --no-dependencies pip install sbapp --no-dependencies
# In the above case, you will still need to # In the case of using --no-dependencies, you
# manually install the RNS and LXMF dependencies: # will still need to manually install the RNS
# and LXMF dependencies:
pip install rns lxmf pip install rns lxmf
# Install Sideband on Debian 11 and derivatives: # Install Sideband on Debian 11 and derivatives:
@ -154,28 +161,54 @@ sideband
## On macOS ## On macOS
On macOS, you can install Sideband with `pip3` or `pipx`. Due to the many different potential Python versions and install paths across macOS versions, the easiest install method is to use `pipx`. To install Sideband on macOS, you have two options available:
If you don't already have the `pipx` package manager installed, it can be installed via [Homebrew](https://brew.sh/) with `brew install pipx`. 1. An easy to install pre-built disk image package
2. A source package install for more advanced setups
#### Prebuilt Executable
You can download a disk image with Sideband for macOS (ARM and Intel) from the [latest release page](https://github.com/markqvist/Sideband/releases/latest). Simply mount the downloaded disk image, drag `Sideband` to your applications folder, and run it.
**Please note!** If you have application install restrictions enabled on your macOS install, or have restricted your system to only allow installation of application from the Apple App Store, you will need to create an exception for Sideband. The Sideband application will *never* be distributed with an Apple-controlled digital signature, as this will allow Apple to simply disable Sideband from running on your system if they decide to do so, or are forced to by authorities or other circumstances.
If you install Sideband from the DMG file, it is still recommended to install the `rns` package via the `pip` or `pipx` package manager, so you can use the RNS utility programs, like `rnstatus` to see interface and connectivity status from the terminal. If you already have Python and `pip` installed on your system, simply open a terminal window and use one of the following commands:
```bash ```bash
# Install Sideband and dependencies on macOS using pipx: # Install Reticulum and utilities with pip:
pipx install sbapp pip3 install rns
pipx ensurepath
# Run it # On some versions, you may need to use the
sideband # flag --break-system-packages to install:
pip3 install rns --break-system-packages
``` ```
Or, if you prefer to use `pip` directly, follow the instructions below. In this case, if you have not already installed Python and `pip3` on your macOS system, [download and install](https://www.python.org/downloads/) the latest version first. If you do not have Python and `pip` available, [download and install it](https://www.python.org/downloads/) first.
#### Source Package Install
For more advanced setups, including the ability to run Sideband in headless daemon mode, enable debug logging output, configuration import and export and more, you may want to install it from the source package via `pip` instead.
**Please note!** The very latest Python release, Python 3.13 is currently **not** compatible with the Kivy framework, that Sideband uses to render its user interface. If your version of macOS uses Python 3.13 as its default Python installation, you will need to install an earlier version as well. Using [the latest release of Python 3.12](https://www.python.org/downloads/release/python-3127/) is recommended.
To install Sideband via `pip`, follow these instructions:
```bash ```bash
# Install Sideband and dependencies on macOS using pip: # Install Sideband and dependencies on macOS using pip:
pip3 install sbapp --user --break-system-packages pip3 install sbapp --user --break-system-packages
# Run it: # Optionally install RNS command line utilities:
pip3 install rns
# Run Sideband from the terminal:
python3 -m sbapp.main python3 -m sbapp.main
# Enable debug logging:
python3 -m sbapp.main -v
# Start Sideband in daemon mode:
python3 -m sbapp.main -d
# If you add your pip install location to # If you add your pip install location to
# the PATH environment variable, you can # the PATH environment variable, you can
# also run Sideband simply using: # also run Sideband simply using:
@ -185,13 +218,32 @@ sideband
## On Windows ## On Windows
Even though there is currently not an automated installer, or packaged `.exe` file for Sideband on Windows, you can still install it through `pip`. If you don't already have Python installed, [download and install](https://www.python.org/downloads/) the latest version of Python. To install Sideband on Windows, you have two options available:
Please note that audio messaging functionality isn't supported on Windows yet. Please support the development if you'd like to see this feature added faster. 1. An easy to install pre-built executable package
2. A source package install for more advanced setups
**Important!** When asked by the installer, make sure to add the Python program to your PATH environment variables. If you don't do this, you will not be able to use the `pip` installer, or run the `sideband` command. #### Prebuilt Executable
When Python has been installed, you can open a command prompt and install sideband via `pip`: Simply download the packaged Windows ZIP file from the [latest release page](https://github.com/markqvist/Sideband/releases/latest), unzip the file, and run `Sideband.exe` from the unzipped directory. You can create desktop or start menu shortcuts from this executable if needed.
When running Sideband for the first time, a default Reticulum configuration file will be created, if you don't already have one. If you don't have any existing Reticulum connectivity available locally, you may want to edit the file, located at `C:\Users\USERNAME\.reticulum\config` and manually add an interface that provides connectivity to a wider network. If you just want to connect over the Internet, you can add one of the public hubs on the [Reticulum Testnet](https://reticulum.network/connect.html).
Though the ZIP file contains everything necessary to run Sideband, it is also recommended to install the Reticulum command line utilities separately, so that you can use commands like `rnstatus` and `rnsd` from the command line. This will make it easier to manage Reticulum connectivity on your system. If you do not already have Python installed on your system, [download and install it](https://www.python.org/downloads/) first.
**Important!** When asked by the installer, make sure to add the Python program to your `PATH` environment variables. If you don't do this, you will not be able to use the `pip` installer, or run any of the installed commands. When Python has been installed, you can open a command prompt and install the Reticulum package via `pip`:
```bash
pip install rns
```
#### Source Package Install
For more advanced setups, including the ability to run Sideband in headless daemon mode, enable debug logging output, configuration import and export and more, you may want to install it from the source package via `pip` instead.
In this case, you will need to [download and install the latest supported version of Python](https://www.python.org/downloads/release/python-3127/) (currently Python 3.12.7), since very latest Python release, Python 3.13 is currently **not** compatible with the Kivy framework, that Sideband uses to render its user interface. The binary package already includes a compatible Python version, so if you are running Sideband from that, there is no need to install a specific version of Python.
When Python has been installed, you can open a command prompt and install Sideband via `pip`:
```bash ```bash
pip install sbapp pip install sbapp
@ -199,7 +251,7 @@ pip install sbapp
The Sideband application can now be launched by running the command `sideband` in the command prompt. If needed, you can create a shortcut for Sideband on your desktop or in the start menu. The Sideband application can now be launched by running the command `sideband` in the command prompt. If needed, you can create a shortcut for Sideband on your desktop or in the start menu.
When running Sideband for the first time, a default Reticulum configuration file will be created, if you don't already have one. If you don't have any existing Reticulum connectivity available locally, you may want to edit the file, located at `C:\Users\USERNAME\.reticulum\config` and manually add an interface that provides connectivity to a wider network. If you just want to connect over the Internet, you can add one of the public hubs on the [Reticulum Testnet](https://reticulum.network/connect.html). Since this installation method automatically installs the `rns` and `lxmf` packages as well, you will also have access to using all the included RNS and LXMF utilities like `rnstatus`, `rnsd` and `lxmd` on your system.
# Paper Messaging Example # Paper Messaging Example
@ -236,7 +288,7 @@ You can help support the continued development of open, free and private communi
<br/> <br/>
# Development Roadmap # Planned Features
- <s>Secure and private location and telemetry sharing</s> - <s>Secure and private location and telemetry sharing</s>
- <s>Including images in messages</s> - <s>Including images in messages</s>
@ -246,15 +298,15 @@ You can help support the continued development of open, free and private communi
- <s>Using Sideband as a Reticulum Transport Instance</s> - <s>Using Sideband as a Reticulum Transport Instance</s>
- <s>Encryption keys export and import</s> - <s>Encryption keys export and import</s>
- <s>Plugin support for commands, services and telemetry</s> - <s>Plugin support for commands, services and telemetry</s>
- <s>Adding Linux .desktop file integration</s>
- <s>Sending voice messages (using Codec2 and Opus)</s> - <s>Sending voice messages (using Codec2 and Opus)</s>
- Implementing the Local Broadcasts feature - <s>Adding a Linux desktop integration</s>
- <s>Adding prebuilt Windows binaries to the releases</s>
- <s>Adding prebuilt macOS binaries to the releases</s>
- Adding a Nomad Net page browser
- LXMF sneakernet functionality - LXMF sneakernet functionality
- Network visualisation and test tools - Network visualisation and test tools
- A debug log viewer
- Better message sorting mechanism - Better message sorting mechanism
- Fix I2P status not being displayed correctly when the I2P router disappears unexpectedly - A debug log viewer
- Adding a Nomad Net page browser
# License # License
Unless otherwise noted, this work is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License][cc-by-nc-sa]. Unless otherwise noted, this work is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License][cc-by-nc-sa].

View File

@ -67,7 +67,7 @@ class GpsdLocationPlugin(SidebandTelemetryPlugin):
self.latitude = gpsd_latitude self.latitude = gpsd_latitude
self.longitude = gpsd_longitude self.longitude = gpsd_longitude
self.altitude = gpsd_altitude self.altitude = gpsd_altitude
self.speed = gpsd_speed self.speed = gpsd_speed*3.6 # Convert from m/s to km/h
self.bearing = gpsd_bearing self.bearing = gpsd_bearing
epx = result.get("epx", None); epy = result.get("epy", None) epx = result.get("epx", None); epy = result.get("epy", None)

1
main.py Normal file
View File

@ -0,0 +1 @@
import sbapp.main

View File

@ -1,4 +1,4 @@
# Entry version 20240630 # Entry version 20241128
[Desktop Entry] [Desktop Entry]
Comment[en_US]=Messaging, telemetry and remote control over LXMF Comment[en_US]=Messaging, telemetry and remote control over LXMF
Comment=Messaging, telemetry and remote control over LXMF Comment=Messaging, telemetry and remote control over LXMF

View File

@ -10,7 +10,7 @@ source.exclude_patterns = app_storage/*,venv/*,Makefile,./Makefil*,requirements,
version.regex = __version__ = ['"](.*)['"] version.regex = __version__ = ['"](.*)['"]
version.filename = %(source.dir)s/main.py version.filename = %(source.dir)s/main.py
android.numeric_version = 20241013 android.numeric_version = 20241020
requirements = kivy==2.3.0,libbz2,pillow==10.2.0,qrcode==7.3.1,usb4a,usbserial4a,able_recipe,libwebp,libogg,libopus,opusfile,numpy,cryptography,ffpyplayer,codec2,pycodec2,sh,pynacl,typing-extensions requirements = kivy==2.3.0,libbz2,pillow==10.2.0,qrcode==7.3.1,usb4a,usbserial4a,able_recipe,libwebp,libogg,libopus,opusfile,numpy,cryptography,ffpyplayer,codec2,pycodec2,sh,pynacl,typing-extensions

View File

@ -30,10 +30,13 @@ def get_variant() -> str:
version = re.findall(version_regex, version_file_data, re.M)[0] version = re.findall(version_regex, version_file_data, re.M)[0]
return version return version
except IndexError: except IndexError:
raise ValueError(f"Unable to find version string in {version_file}.") return None
__version__ = get_version() __version__ = get_version()
__variant__ = get_variant() __variant__ = get_variant()
variant_str = ""
if __variant__:
variant_str = " "+__variant__
def glob_paths(pattern): def glob_paths(pattern):
out_files = [] out_files = []
@ -60,14 +63,14 @@ package_data = {
] ]
} }
print("Freezing openCom Companion "+__version__+" "+__variant__) print("Freezing openCom Companion "+__version__+" "+variant_str)
if build_appimage: if build_appimage:
global_excludes = [".buildozer", "build", "dist"] global_excludes = [".buildozer", "build", "dist"]
# Dependencies are automatically detected, but they might need fine-tuning. # Dependencies are automatically detected, but they might need fine-tuning.
appimage_options = { appimage_options = {
"target_name": "openCom Companion", "target_name": "openCom Companion",
"target_version": __version__+" "+__variant__, "target_version": __version__+" "+variant_str,
"include_files": [], "include_files": [],
"excludes": [], "excludes": [],
"packages": ["kivy"], "packages": ["kivy"],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,6 +1,6 @@
__debug_build__ = False __debug_build__ = False
__disable_shaders__ = False __disable_shaders__ = False
__version__ = "1.1.3" __version__ = "1.2.0"
__variant__ = "" __variant__ = ""
import sys import sys
@ -24,6 +24,51 @@ import base64
import threading import threading
import RNS.vendor.umsgpack as msgpack import RNS.vendor.umsgpack as msgpack
app_ui_scaling_path = None
def apply_ui_scale():
global app_ui_scaling_path
default_scale = os.environ["KIVY_METRICS_DENSITY"] if "KIVY_METRICS_DENSITY" in os.environ else "unknown"
config_path = None
ui_scale_path = None
try:
if RNS.vendor.platformutils.is_android():
import plyer
ui_scale_path = plyer.storagepath.get_application_dir()+"/io.unsigned.sideband/files/app_storage/ui_scale"
else:
if config_path == None:
import sbapp.plyer as plyer
ui_scale_path = plyer.storagepath.get_home_dir()+"/.config/sideband/app_storage/ui_scale"
if ui_scale_path.startswith("file://"):
ui_scale_path = ui_scale_path.replace("file://", "")
else:
ui_scale_path = config_path+"/app_storage/ui_scale"
app_ui_scaling_path = ui_scale_path
except Exception as e:
RNS.log(f"Error while locating UI scale file: {e}", RNS.LOG_ERROR)
if ui_scale_path != None:
RNS.log("Default scaling factor on this platform is "+str(default_scale), RNS.LOG_NOTICE)
try:
RNS.log("Looking for scaling info in "+str(ui_scale_path))
if os.path.isfile(ui_scale_path):
scale_factor = None
with open(ui_scale_path, "r") as sf:
scale_factor = float(sf.readline())
if scale_factor != None:
if scale_factor >= 0.3 and scale_factor <= 5.0:
os.environ["KIVY_METRICS_DENSITY"] = str(scale_factor)
RNS.log("UI scaling factor set to "+str(os.environ["KIVY_METRICS_DENSITY"]), RNS.LOG_NOTICE)
elif scale_factor == 0.0:
RNS.log("Using default UI scaling factor", RNS.LOG_NOTICE)
except Exception as e:
RNS.log(f"Error while reading UI scaling factor: {e}", RNS.LOG_ERROR)
if args.export_settings: if args.export_settings:
from .sideband.core import SidebandCore from .sideband.core import SidebandCore
sideband = SidebandCore( sideband = SidebandCore(
@ -143,9 +188,11 @@ if args.daemon:
NewConv = DaemonElement; Telemetry = DaemonElement; ObjectDetails = DaemonElement; Announces = DaemonElement; NewConv = DaemonElement; Telemetry = DaemonElement; ObjectDetails = DaemonElement; Announces = DaemonElement;
Messages = DaemonElement; ts_format = DaemonElement; messages_screen_kv = DaemonElement; plyer = DaemonElement; multilingual_markup = DaemonElement; Messages = DaemonElement; ts_format = DaemonElement; messages_screen_kv = DaemonElement; plyer = DaemonElement; multilingual_markup = DaemonElement;
ContentNavigationDrawer = DaemonElement; DrawerList = DaemonElement; IconListItem = DaemonElement; escape_markup = DaemonElement; ContentNavigationDrawer = DaemonElement; DrawerList = DaemonElement; IconListItem = DaemonElement; escape_markup = DaemonElement;
SoundLoader = DaemonElement; SoundLoader = DaemonElement; BoxLayout = DaemonElement;
else: else:
apply_ui_scale()
from kivymd.app import MDApp from kivymd.app import MDApp
app_superclass = MDApp app_superclass = MDApp
from kivy.core.window import Window from kivy.core.window import Window
@ -157,6 +204,7 @@ else:
from kivy.effects.scroll import ScrollEffect from kivy.effects.scroll import ScrollEffect
from kivy.uix.screenmanager import ScreenManager from kivy.uix.screenmanager import ScreenManager
from kivy.uix.screenmanager import FadeTransition, NoTransition, SlideTransition from kivy.uix.screenmanager import FadeTransition, NoTransition, SlideTransition
from kivy.uix.boxlayout import BoxLayout
from kivymd.uix.list import OneLineIconListItem, IconLeftWidget from kivymd.uix.list import OneLineIconListItem, IconLeftWidget
from kivy.properties import StringProperty from kivy.properties import StringProperty
from kivymd.uix.button import BaseButton, MDIconButton from kivymd.uix.button import BaseButton, MDIconButton
@ -181,6 +229,7 @@ else:
from ui.layouts import * from ui.layouts import *
from ui.conversations import Conversations, MsgSync, NewConv from ui.conversations import Conversations, MsgSync, NewConv
from ui.telemetry import Telemetry from ui.telemetry import Telemetry
from ui.utilities import Utilities
from ui.objectdetails import ObjectDetails from ui.objectdetails import ObjectDetails
from ui.announces import Announces from ui.announces import Announces
from ui.messages import Messages, ts_format, messages_screen_kv from ui.messages import Messages, ts_format, messages_screen_kv
@ -208,6 +257,7 @@ else:
from .ui.conversations import Conversations, MsgSync, NewConv from .ui.conversations import Conversations, MsgSync, NewConv
from .ui.announces import Announces from .ui.announces import Announces
from .ui.telemetry import Telemetry from .ui.telemetry import Telemetry
from .ui.utilities import Utilities
from .ui.objectdetails import ObjectDetails from .ui.objectdetails import ObjectDetails
from .ui.messages import Messages, ts_format, messages_screen_kv from .ui.messages import Messages, ts_format, messages_screen_kv
from .ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem from .ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem
@ -294,6 +344,7 @@ class SidebandApp(MDApp):
self.sync_dialog = None self.sync_dialog = None
self.settings_ready = False self.settings_ready = False
self.telemetry_ready = False self.telemetry_ready = False
self.utilities_ready = False
self.connectivity_ready = False self.connectivity_ready = False
self.hardware_ready = False self.hardware_ready = False
self.repository_ready = False self.repository_ready = False
@ -306,9 +357,11 @@ class SidebandApp(MDApp):
self.service_last_available = 0 self.service_last_available = 0
self.closing_app = False self.closing_app = False
self.file_manager = None
self.attach_path = None self.attach_path = None
self.attach_type = None self.attach_type = None
self.attach_dialog = None self.attach_dialog = None
self.shared_attach_dialog = None
self.rec_dialog = None self.rec_dialog = None
self.last_msg_audio = None self.last_msg_audio = None
self.msg_sound = None self.msg_sound = None
@ -699,6 +752,13 @@ class SidebandApp(MDApp):
else: else:
RNS.log("Conversations view did not exist", RNS.LOG_DEBUG) RNS.log("Conversations view did not exist", RNS.LOG_DEBUG)
def ui_update_job():
time.sleep(0.05)
def cb(dt):
self.perform_wake_update()
Clock.schedule_once(cb, 0.1)
threading.Thread(target=ui_update_job, daemon=True).start()
RNS.log("App resumed", RNS.LOG_DEBUG) RNS.log("App resumed", RNS.LOG_DEBUG)
def on_stop(self): def on_stop(self):
@ -714,6 +774,21 @@ class SidebandApp(MDApp):
else: else:
return False return False
def perform_wake_update(self):
# This workaround mitigates a bug in Kivy on Android
# which causes the UI to turn black on app resume,
# probably due to an invalid GL draw context. By
# simply opening and immediately closing the nav
# drawer, we force the UI to do a frame redraw, which
# results in the UI actually being visible again.
if RNS.vendor.platformutils.is_android():
RNS.log("Performing app wake UI update", RNS.LOG_DEBUG)
self.root.ids.nav_drawer.set_state("open")
def cb(dt):
self.root.ids.nav_drawer.set_state("closed")
Clock.schedule_once(cb, 0)
def check_bluetooth_permissions(self): def check_bluetooth_permissions(self):
if RNS.vendor.platformutils.get_platform() == "android": if RNS.vendor.platformutils.get_platform() == "android":
mActivity = autoclass('org.kivy.android.PythonActivity').mActivity mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
@ -870,6 +945,37 @@ class SidebandApp(MDApp):
if data.lower().startswith(LXMF.LXMessage.URI_SCHEMA): if data.lower().startswith(LXMF.LXMessage.URI_SCHEMA):
action = "lxm_uri" action = "lxm_uri"
if intent_action == "android.intent.action.SEND":
try:
Intent = autoclass("android.content.Intent")
extras = intent.getExtras()
target = extras.get(Intent.EXTRA_STREAM)
mime_types = extras.get(Intent.EXTRA_MIME_TYPES)
target_uri = target.toString()
target_path = target.getPath()
target_filename = target.getLastPathSegment()
RNS.log(f"Received share intent: {target_uri} / {target_path} / {target_filename}", RNS.LOG_DEBUG)
for cf in os.listdir(self.sideband.share_cache):
rt = os.path.join(self.sideband.share_cache, cf)
os.unlink(rt)
RNS.log("Removed previously cached data: "+str(rt), RNS.LOG_DEBUG)
ContentResolver = autoclass("android.content.ContentResolver")
cr = mActivity.getContentResolver()
cache_path = os.path.join(self.sideband.share_cache, target_filename)
input_stream = cr.openInputStream(target)
with open(cache_path, "wb") as cache_file:
cache_file.write(bytes(input_stream.readAllBytes()))
RNS.log("Cached shared data from Android intent", RNS.LOG_DEBUG)
action = "shared_data"
data = {"filename": target_filename, "data_path": cache_path}
except Exception as e:
RNS.log(f"Error while getting intent action data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
if action != None: if action != None:
self.handle_action(action, data) self.handle_action(action, data)
@ -877,6 +983,16 @@ class SidebandApp(MDApp):
if action == "lxm_uri": if action == "lxm_uri":
self.ingest_lxm_uri(data) self.ingest_lxm_uri(data)
if action == "shared_data":
RNS.log("Got shared data: "+str(data))
def cb(dt):
try:
self.shared_attachment_action(data)
except Exception as e:
RNS.log("Error while handling external message attachment", RNS.LOG_ERROR)
RNS.trace_exception(e)
Clock.schedule_once(cb, 0.1)
def ingest_lxm_uri(self, lxm_uri): def ingest_lxm_uri(self, lxm_uri):
RNS.log("Ingesting LXMF paper message from URI: "+str(lxm_uri), RNS.LOG_DEBUG) RNS.log("Ingesting LXMF paper message from URI: "+str(lxm_uri), RNS.LOG_DEBUG)
self.sideband.lxm_ingest_uri(lxm_uri) self.sideband.lxm_ingest_uri(lxm_uri)
@ -1064,6 +1180,9 @@ class SidebandApp(MDApp):
self.quit_action(None) self.quit_action(None)
return True return True
def file_dropped(self, window, file_path, x, y, *args):
self.shared_attachment_action({"data_path": file_path.decode("utf-8")})
def on_start(self): def on_start(self):
self.last_exit_event = time.time() self.last_exit_event = time.time()
self.root.ids.screen_manager.transition = self.slide_transition self.root.ids.screen_manager.transition = self.slide_transition
@ -1074,6 +1193,7 @@ class SidebandApp(MDApp):
EventLoop.window.bind(on_key_down=self.keydown_event) EventLoop.window.bind(on_key_down=self.keydown_event)
EventLoop.window.bind(on_key_up=self.keyup_event) EventLoop.window.bind(on_key_up=self.keyup_event)
Window.bind(on_request_close=self.close_requested) Window.bind(on_request_close=self.close_requested)
Window.bind(on_drop_file=self.file_dropped)
if __variant__ != "": if __variant__ != "":
variant_str = " "+__variant__ variant_str = " "+__variant__
@ -1180,6 +1300,8 @@ class SidebandApp(MDApp):
self.close_sub_telemetry_action() self.close_sub_telemetry_action()
elif self.root.ids.screen_manager.current == "icons_screen": elif self.root.ids.screen_manager.current == "icons_screen":
self.close_sub_telemetry_action() self.close_sub_telemetry_action()
elif self.root.ids.screen_manager.current == "utilities_screen":
self.close_sub_utilities_action()
else: else:
self.open_conversations(direction="right") self.open_conversations(direction="right")
@ -1219,9 +1341,12 @@ class SidebandApp(MDApp):
else: else:
self.telemetry_action(self) self.telemetry_action(self)
if text == "u": if text == "y":
self.map_display_own_telemetry() self.map_display_own_telemetry()
if text == "u":
self.utilities_action()
if text == "o": if text == "o":
self.objects_action() self.objects_action()
@ -1233,6 +1358,8 @@ class SidebandApp(MDApp):
self.lxmf_sync_action(self) self.lxmf_sync_action(self)
elif self.root.ids.screen_manager.current == "telemetry_screen": elif self.root.ids.screen_manager.current == "telemetry_screen":
self.conversations_action(self, direction="right") self.conversations_action(self, direction="right")
elif self.root.ids.screen_manager.current == "rnstatus_screen":
self.utilities_screen.update_rnstatus()
elif self.root.ids.screen_manager.current == "object_details_screen": elif self.root.ids.screen_manager.current == "object_details_screen":
if not self.object_details_screen.object_hash == self.sideband.lxmf_destination.hash: if not self.object_details_screen.object_hash == self.sideband.lxmf_destination.hash:
self.converse_from_telemetry(self) self.converse_from_telemetry(self)
@ -1277,6 +1404,8 @@ class SidebandApp(MDApp):
self.close_sub_telemetry_action() self.close_sub_telemetry_action()
elif self.root.ids.screen_manager.current == "icons_screen": elif self.root.ids.screen_manager.current == "icons_screen":
self.close_sub_telemetry_action() self.close_sub_telemetry_action()
elif self.root.ids.screen_manager.current == "rnstatus_screen":
self.close_sub_utilities_action()
else: else:
self.open_conversations(direction="right") self.open_conversations(direction="right")
@ -1679,7 +1808,8 @@ class SidebandApp(MDApp):
def message_fm_exited(self, *args): def message_fm_exited(self, *args):
self.manager_open = False self.manager_open = False
self.file_manager.close() if self.file_manager != None:
self.file_manager.close()
def message_select_file_action(self, sender=None): def message_select_file_action(self, sender=None):
perm_ok = False perm_ok = False
@ -1694,11 +1824,20 @@ class SidebandApp(MDApp):
if perm_ok and path != None: if perm_ok and path != None:
try: try:
self.file_manager = MDFileManager( if self.attach_type in ["lbimg", "defimg", "hqimg"]:
exit_manager=self.message_fm_exited, self.file_manager = MDFileManager(
select_path=self.message_fm_got_path, exit_manager=self.message_fm_exited,
) select_path=self.message_fm_got_path,
# self.file_manager.ext = ["*"] # Current KivyMD preview implementation is too slow to be reliable on Android
preview=False)
else:
self.file_manager = MDFileManager(
exit_manager=self.message_fm_exited,
select_path=self.message_fm_got_path,
preview=False)
# self.file_manager.ext = []
# self.file_manager.search = "all"
self.file_manager.show(path) self.file_manager.show(path)
except Exception as e: except Exception as e:
@ -2175,6 +2314,65 @@ class SidebandApp(MDApp):
ate_dialog.open() ate_dialog.open()
def shared_attachment_action(self, attachment_data):
if not self.root.ids.screen_manager.current == "messages_screen":
if RNS.vendor.platformutils.is_android():
toast("Please select a conversation first")
else:
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
ate_dialog = MDDialog(
title="No active conversation",
text="To drop files as attachments, please open a conversation first",
buttons=[ ok_button ],
)
ok_button.bind(on_release=ate_dialog.dismiss)
ate_dialog.open()
else:
self.rec_dialog_is_open = False
def a_img_lb(sender):
self.attach_type="lbimg"
self.shared_attach_dialog.dismiss()
self.shared_attach_dialog.att_exc()
def a_img_def(sender):
self.attach_type="defimg"
self.shared_attach_dialog.dismiss()
self.shared_attach_dialog.att_exc()
def a_img_hq(sender):
self.attach_type="hqimg"
self.shared_attach_dialog.dismiss()
self.shared_attach_dialog.att_exc()
def a_file(sender):
self.attach_type="file"
self.shared_attach_dialog.dismiss()
self.shared_attach_dialog.att_exc()
if self.shared_attach_dialog == None:
ss = int(dp(18))
cancel_button = MDRectangleFlatButton(text="Cancel", font_size=dp(18))
ad_items = [
DialogItem(IconLeftWidget(icon="message-image-outline", on_release=a_img_lb), text="[size="+str(ss)+"]Low-bandwidth Image[/size]", on_release=a_img_lb),
DialogItem(IconLeftWidget(icon="file-image", on_release=a_img_def), text="[size="+str(ss)+"]Medium Image[/size]", on_release=a_img_def),
DialogItem(IconLeftWidget(icon="image-outline", on_release=a_img_hq), text="[size="+str(ss)+"]High-res Image[/size]", on_release=a_img_hq),
DialogItem(IconLeftWidget(icon="file-outline", on_release=a_file), text="[size="+str(ss)+"]File Attachment[/size]", on_release=a_file)]
self.shared_attach_dialog = MDDialog(
title="Add Attachment",
type="simple",
text="Select how you want to attach this data to the next message sent\n",
items=ad_items,
buttons=[ cancel_button ],
width_offset=dp(32),
)
cancel_button.bind(on_release=self.shared_attach_dialog.dismiss)
def att_exc():
self.message_fm_got_path(attachment_data["data_path"])
self.shared_attach_dialog.att_exc = att_exc
self.shared_attach_dialog.open()
def update_message_widgets(self): def update_message_widgets(self):
toolbar_items = self.messages_view.ids.messages_toolbar.ids.right_actions.children toolbar_items = self.messages_view.ids.messages_toolbar.ids.right_actions.children
mode_item = toolbar_items[1] mode_item = toolbar_items[1]
@ -2400,13 +2598,14 @@ class SidebandApp(MDApp):
else: else:
sl = None sl = None
sync_title = "LXMF Sync"
if not hasattr(self, "message_sync_dialog") or self.message_sync_dialog == None: if not hasattr(self, "message_sync_dialog") or self.message_sync_dialog == None:
close_button = MDRectangleFlatButton(text="Close",font_size=dp(18)) close_button = MDRectangleFlatButton(text="Close",font_size=dp(18))
stop_button = MDRectangleFlatButton(text="Stop",font_size=dp(18), theme_text_color="Custom", line_color=self.color_reject, text_color=self.color_reject) stop_button = MDRectangleFlatButton(text="Stop",font_size=dp(18), theme_text_color="Custom", line_color=self.color_reject, text_color=self.color_reject)
dialog_content = MsgSync() dialog_content = MsgSync()
dialog = MDDialog( dialog = MDDialog(
title="LXMF Sync via "+RNS.prettyhexrep(self.sideband.message_router.get_outbound_propagation_node()), title=sync_title,
type="custom", type="custom",
content_cls=dialog_content, content_cls=dialog_content,
buttons=[ stop_button, close_button ], buttons=[ stop_button, close_button ],
@ -2443,7 +2642,8 @@ class SidebandApp(MDApp):
dsp = 0 dsp = 0
self.sideband.setstate("app.flags.lxmf_sync_dialog_open", True) self.sideband.setstate("app.flags.lxmf_sync_dialog_open", True)
self.message_sync_dialog.title = f"LXMF Sync via "+RNS.prettyhexrep(self.sideband.message_router.get_outbound_propagation_node()) self.message_sync_dialog.title = sync_title
self.message_sync_dialog.d_content.ids.node_info.text = f"Via {RNS.prettyhexrep(self.sideband.message_router.get_outbound_propagation_node())}\n"
self.message_sync_dialog.d_content.ids.sync_status.text = self.sideband.get_sync_status() self.message_sync_dialog.d_content.ids.sync_status.text = self.sideband.get_sync_status()
self.message_sync_dialog.d_content.ids.sync_progress.value = dsp self.message_sync_dialog.d_content.ids.sync_progress.value = dsp
self.message_sync_dialog.d_content.ids.sync_progress.start() self.message_sync_dialog.d_content.ids.sync_progress.start()
@ -2487,11 +2687,15 @@ class SidebandApp(MDApp):
RNS.log("Error while creating conversation: "+str(e), RNS.LOG_ERROR) RNS.log("Error while creating conversation: "+str(e), RNS.LOG_ERROR)
if new_result: if new_result:
dialog.d_content.ids["n_address_field"].helper_text = ""
dialog.d_content.ids["n_address_field"].helper_text_mode = "on_focus"
dialog.d_content.ids["n_address_field"].error = False dialog.d_content.ids["n_address_field"].error = False
dialog.dismiss() dialog.dismiss()
if self.conversations_view != None: if self.conversations_view != None:
self.conversations_view.update() self.conversations_view.update()
else: else:
dialog.d_content.ids["n_address_field"].helper_text = "Invalid address, check your input"
dialog.d_content.ids["n_address_field"].helper_text_mode = "persistent"
dialog.d_content.ids["n_address_field"].error = True dialog.d_content.ids["n_address_field"].error = True
# dialog.d_content.ids["n_error_field"].text = "Could not create conversation. Check your input." # dialog.d_content.ids["n_error_field"].text = "Could not create conversation. Check your input."
@ -2585,6 +2789,72 @@ class SidebandApp(MDApp):
if no_transition: if no_transition:
self.root.ids.screen_manager.transition = self.slide_transition self.root.ids.screen_manager.transition = self.slide_transition
def configure_ui_scaling_action(self, sender=None):
global app_ui_scaling_path
try:
cancel_button = MDRectangleFlatButton(text="Cancel",font_size=dp(18))
set_button = MDRectangleFlatButton(text="Set",font_size=dp(18), theme_text_color="Custom", line_color=self.color_accept, text_color=self.color_accept)
dialog_content = UIScaling()
dialog = MDDialog(
title="UI Scaling",
type="custom",
content_cls=dialog_content,
buttons=[ set_button, cancel_button ],
# elevation=0,
)
dialog.d_content = dialog_content
dialog.d_content.ids["scaling_factor"].text = os.environ["KIVY_METRICS_DENSITY"] if "KIVY_METRICS_DENSITY" in os.environ else "0.0"
def dl_yes(s):
new_sf = 1.0
scaling_ok = False
try:
si = dialog.d_content.ids["scaling_factor"].text
sf = float(si)
if (sf >= 0.3 and sf <= 5.0) or sf == 0.0:
new_sf = sf
scaling_ok = True
except Exception as e:
RNS.log("Error while getting scaling factor from user: "+str(e), RNS.LOG_ERROR)
if scaling_ok:
dialog.d_content.ids["scaling_factor"].helper_text = ""
dialog.d_content.ids["scaling_factor"].helper_text_mode = "on_focus"
dialog.d_content.ids["scaling_factor"].error = False
dialog.dismiss()
if app_ui_scaling_path == None:
RNS.log("No path to UI scaling factor file could be found, cannot save scaling factor", RNS.LOG_ERROR)
else:
try:
with open(app_ui_scaling_path, "w") as sfile:
sfile.write(str(new_sf))
RNS.log(f"Saved configured scaling factor {new_sf} to {app_ui_scaling_path}", RNS.LOG_DEBUG)
except Exception as e:
RNS.log(f"Error while saving scaling factor {new_sf} to {app_ui_scaling_path}: {e}", RNS.LOG_ERROR)
else:
dialog.d_content.ids["scaling_factor"].helper_text = "Invalid scale factor, check your input"
dialog.d_content.ids["scaling_factor"].helper_text_mode = "persistent"
dialog.d_content.ids["scaling_factor"].error = True
def dl_no(s):
dialog.dismiss()
def dl_ds(s):
self.dialog_open = False
set_button.bind(on_release=dl_yes)
cancel_button.bind(on_release=dl_no)
dialog.bind(on_dismiss=dl_ds)
dialog.open()
self.dialog_open = True
except Exception as e:
RNS.log("Error while creating UI scaling dialog: "+str(e), RNS.LOG_ERROR)
def settings_action(self, sender=None, direction="left"): def settings_action(self, sender=None, direction="left"):
if self.settings_ready: if self.settings_ready:
self.settings_open(direction=direction) self.settings_open(direction=direction)
@ -5280,6 +5550,44 @@ class SidebandApp(MDApp):
ate_dialog.open() ate_dialog.open()
### Utilities Screen
######################################
def utilities_init(self):
if not self.utilities_ready:
self.utilities_screen = Utilities(self)
self.utilities_ready = True
def utilities_open(self, sender=None, direction="left", no_transition=False):
if no_transition:
self.root.ids.screen_manager.transition = self.no_transition
else:
self.root.ids.screen_manager.transition = self.slide_transition
self.root.ids.screen_manager.transition.direction = direction
self.root.ids.screen_manager.current = "utilities_screen"
self.root.ids.nav_drawer.set_state("closed")
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
if no_transition:
self.root.ids.screen_manager.transition = self.slide_transition
def utilities_action(self, sender=None, direction="left"):
if self.utilities_ready:
self.utilities_open(direction=direction)
else:
self.loader_action(direction=direction)
def final(dt):
self.utilities_init()
def o(dt):
self.utilities_open(no_transition=True)
Clock.schedule_once(o, ll_ot)
Clock.schedule_once(final, ll_ft)
def close_sub_utilities_action(self, sender=None):
self.utilities_action(direction="right")
### Telemetry Screen ### Telemetry Screen
###################################### ######################################
@ -6275,6 +6583,9 @@ class DialogItem(OneLineIconListItem):
class MDMapIconButton(MDIconButton): class MDMapIconButton(MDIconButton):
pass pass
class UIScaling(BoxLayout):
pass
if not args.daemon: if not args.daemon:
from kivy.base import ExceptionManager, ExceptionHandler from kivy.base import ExceptionManager, ExceptionHandler
class SidebandExceptionHandler(ExceptionHandler): class SidebandExceptionHandler(ExceptionHandler):
@ -6310,3 +6621,6 @@ def run():
if __name__ == "__main__": if __name__ == "__main__":
run() run()
if __name__ == "sbapp.main":
run()

View File

@ -13,11 +13,22 @@
<!-- This intent filter allows opening scanned LXM URLs directly in Sideband --> <!-- This intent filter allows opening scanned LXM URLs directly in Sideband -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.WEB_SEARCH" /> <action android:name="android.intent.action.WEB_SEARCH" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" /> android:resource="@xml/device_filter" />

View File

@ -10,6 +10,7 @@ import shlex
import RNS.vendor.umsgpack as msgpack import RNS.vendor.umsgpack as msgpack
import RNS.Interfaces.Interface as Interface import RNS.Interfaces.Interface as Interface
from LXMF import pn_announce_data_is_valid
import multiprocessing.connection import multiprocessing.connection
@ -46,35 +47,42 @@ class PropagationNodeDetector():
def received_announce(self, destination_hash, announced_identity, app_data): def received_announce(self, destination_hash, announced_identity, app_data):
try: try:
if app_data != None and len(app_data) > 0: if app_data != None and len(app_data) > 0:
unpacked = msgpack.unpackb(app_data) if pn_announce_data_is_valid(app_data):
node_active = unpacked[0] unpacked = msgpack.unpackb(app_data)
emitted = unpacked[1] node_active = unpacked[0]
hops = RNS.Transport.hops_to(destination_hash) emitted = unpacked[1]
hops = RNS.Transport.hops_to(destination_hash)
age = time.time() - emitted age = time.time() - emitted
if age < 0: if age < 0:
RNS.log("Warning, propagation node announce emitted in the future, possible timing inconsistency or tampering attempt.") RNS.log("Warning, propagation node announce emitted in the future, possible timing inconsistency or tampering attempt.")
if age < -1*PropagationNodeDetector.EMITTED_DELTA_GRACE: if age < -1*PropagationNodeDetector.EMITTED_DELTA_GRACE:
raise ValueError("Announce timestamp too far in the future, discarding it") raise ValueError("Announce timestamp too far in the future, discarding it")
if age > -1*PropagationNodeDetector.EMITTED_DELTA_IGNORE: if age > -1*PropagationNodeDetector.EMITTED_DELTA_IGNORE:
# age = 0 # age = 0
pass pass
RNS.log("Detected active propagation node "+RNS.prettyhexrep(destination_hash)+" emission "+str(age)+" seconds ago, "+str(hops)+" hops away") RNS.log("Detected active propagation node "+RNS.prettyhexrep(destination_hash)+" emission "+str(age)+" seconds ago, "+str(hops)+" hops away")
self.owner.log_announce(destination_hash, RNS.prettyhexrep(destination_hash).encode("utf-8"), dest_type=PropagationNodeDetector.aspect_filter) self.owner.log_announce(destination_hash, app_data, dest_type=PropagationNodeDetector.aspect_filter)
if self.owner.config["lxmf_propagation_node"] == None: if self.owner.config["lxmf_propagation_node"] == None:
if self.owner.active_propagation_node == None: if self.owner.active_propagation_node == None:
self.owner.set_active_propagation_node(destination_hash)
else:
prev_hops = RNS.Transport.hops_to(self.owner.active_propagation_node)
if hops <= prev_hops:
self.owner.set_active_propagation_node(destination_hash) self.owner.set_active_propagation_node(destination_hash)
else: else:
pass prev_hops = RNS.Transport.hops_to(self.owner.active_propagation_node)
if hops <= prev_hops:
self.owner.set_active_propagation_node(destination_hash)
else:
pass
else:
pass
else: else:
pass RNS.log(f"Received malformed propagation node announce from {RNS.prettyhexrep(destination_hash)} with data: {app_data}", RNS.LOG_DEBUG)
else:
RNS.log(f"Received malformed propagation node announce from {RNS.prettyhexrep(destination_hash)} with data: {app_data}", RNS.LOG_DEBUG)
except Exception as e: except Exception as e:
RNS.log("Error while processing received propagation node announce: "+str(e)) RNS.log("Error while processing received propagation node announce: "+str(e))
@ -170,19 +178,21 @@ class SidebandCore():
self.cache_dir = self.app_dir+"/cache" self.cache_dir = self.app_dir+"/cache"
self.rns_configdir = None self.rns_configdir = None
core_path = os.path.abspath(__file__)
if "core.pyc" in core_path:
core_path = core_path.replace("core.pyc", "core.py")
if RNS.vendor.platformutils.is_android(): if RNS.vendor.platformutils.is_android():
self.app_dir = android_app_dir+"/uk.co.liberatedsystems.occ/files/" self.app_dir = android_app_dir+"/uk.co.liberatedsystems.occ/files/"
self.cache_dir = self.app_dir+"/cache" self.cache_dir = self.app_dir+"/cache"
self.rns_configdir = self.app_dir+"/app_storage/reticulum" self.rns_configdir = self.app_dir+"/app_storage/reticulum"
self.asset_dir = self.app_dir+"/app/assets" self.asset_dir = self.app_dir+"/app/assets"
elif RNS.vendor.platformutils.is_darwin(): elif RNS.vendor.platformutils.is_darwin():
core_path = os.path.abspath(__file__)
self.asset_dir = core_path.replace("/occ/core.py", "/assets") self.asset_dir = core_path.replace("/occ/core.py", "/assets")
elif RNS.vendor.platformutils.get_platform() == "linux": elif RNS.vendor.platformutils.get_platform() == "linux":
core_path = os.path.abspath(__file__)
self.asset_dir = core_path.replace("/occ/core.py", "/assets") self.asset_dir = core_path.replace("/occ/core.py", "/assets")
elif RNS.vendor.platformutils.is_windows(): elif RNS.vendor.platformutils.is_windows():
core_path = os.path.abspath(__file__)
self.asset_dir = core_path.replace("\\occ\\core.py", "\\assets") self.asset_dir = core_path.replace("\\occ\\core.py", "\\assets")
else: else:
self.asset_dir = plyer.storagepath.get_application_dir()+"/sbapp/assets" self.asset_dir = plyer.storagepath.get_application_dir()+"/sbapp/assets"
@ -195,6 +205,10 @@ class SidebandCore():
if not os.path.isdir(self.rec_cache): if not os.path.isdir(self.rec_cache):
os.makedirs(self.rec_cache) os.makedirs(self.rec_cache)
self.share_cache = self.cache_dir+"/share"
if not os.path.isdir(self.share_cache):
os.makedirs(self.share_cache)
self.icon = self.asset_dir+"/icon.png" self.icon = self.asset_dir+"/icon.png"
self.icon_48 = self.asset_dir+"/icon_48.png" self.icon_48 = self.asset_dir+"/icon_48.png"
self.icon_32 = self.asset_dir+"/icon_32.png" self.icon_32 = self.asset_dir+"/icon_32.png"
@ -977,7 +991,8 @@ class SidebandCore():
try: try:
if app_data == None: if app_data == None:
app_data = b"" app_data = b""
app_data = msgpack.packb([app_data, stamp_cost]) if type(app_data) != bytes:
app_data = msgpack.packb([app_data, stamp_cost])
RNS.log("Received "+str(dest_type)+" announce for "+RNS.prettyhexrep(dest)+" with data: "+str(app_data), RNS.LOG_DEBUG) RNS.log("Received "+str(dest_type)+" announce for "+RNS.prettyhexrep(dest)+" with data: "+str(app_data), RNS.LOG_DEBUG)
self._db_save_announce(dest, app_data, dest_type) self._db_save_announce(dest, app_data, dest_type)
self.setstate("app.flags.new_announces", True) self.setstate("app.flags.new_announces", True)
@ -2480,12 +2495,19 @@ class SidebandCore():
try: try:
if not entry[2] in added_dests: if not entry[2] in added_dests:
app_data = entry[3] app_data = entry[3]
dest_type = entry[4]
if dest_type == "lxmf.delivery":
announced_name = LXMF.display_name_from_app_data(app_data)
announced_cost = self.message_router.get_outbound_stamp_cost(entry[2])
else:
announced_name = None
announced_cost = None
announce = { announce = {
"dest": entry[2], "dest": entry[2],
"name": LXMF.display_name_from_app_data(app_data), "name": announced_name,
"cost": LXMF.stamp_cost_from_app_data(app_data), "cost": announced_cost,
"time": entry[1], "time": entry[1],
"type": entry[4] "type": dest_type
} }
added_dests.append(entry[2]) added_dests.append(entry[2])
announces.append(announce) announces.append(announce)
@ -3488,11 +3510,12 @@ class SidebandCore():
else: else:
ifac_netkey = self.config["connect_local_ifac_passphrase"] ifac_netkey = self.config["connect_local_ifac_passphrase"]
autointerface = RNS.Interfaces.AutoInterface.AutoInterface( interface_config = {
RNS.Transport, "name": "AutoInterface",
name = "AutoInterface", "group_id": group_id
group_id = group_id }
)
autointerface = RNS.Interfaces.AutoInterface.AutoInterface(RNS.Transport, interface_config)
autointerface.OUT = True autointerface.OUT = True
if RNS.Reticulum.transport_enabled(): if RNS.Reticulum.transport_enabled():
@ -3572,6 +3595,8 @@ class SidebandCore():
else: else:
atl_long = self.config["hw_rnode_atl_long"] atl_long = self.config["hw_rnode_atl_long"]
interface_config = None
if self.config["hw_rnode_secondary_modem"]: if self.config["hw_rnode_secondary_modem"]:
if self.config["hw_rnode_sec_atl_short"] == "": if self.config["hw_rnode_sec_atl_short"] == "":
sec_atl_short = None sec_atl_short = None
@ -3611,38 +3636,46 @@ class SidebandCore():
subint_config[0][9] = sec_atl_long subint_config[0][9] = sec_atl_long
subint_config[1][10] = True # outgoing subint_config[1][10] = True # outgoing
rnodeinterface = RNS.Interfaces.Android.RNodeMultiInterface.RNodeMultiInterface( interface_config = {
RNS.Transport, "name": "RNodeMultiInterface",
"RNodeInterface", "port": target_port,
target_port, "subint_config": subint_config,
subint_config, "flow_control": False,
allow_bluetooth = False, "id_interval": self.config["hw_rnode_beaconinterval"],
force_ble = rnode_allow_bluetooth, "id_callsign": self.config["hw_rnode_beacondata"],
ble_name = bt_device_name, "st_alock": atl_short,
target_device_name = bt_device_name, "lt_alock": atl_long,
) "allow_bluetooth": False,
"target_device_name": None,
"force_ble": True,
"ble_name": bt_device_name,
"ble_addr": None,
}
rnodeinterface = RNS.Interfaces.Android.RNodeMultiInterface.RNodeMultiInterface(RNS.Transport, interface_config)
rnodeinterface.start() rnodeinterface.start()
else: else:
rnodeinterface = RNS.Interfaces.Android.RNodeInterface.RNodeInterface( interface_config = {
RNS.Transport, "name": "RNodeInterface",
"RNodeInterface", "port": target_port,
target_port, "frequency": self.config["hw_rnode_frequency"],
frequency = self.config["hw_rnode_frequency"], "bandwidth": self.config["hw_rnode_bandwidth"],
bandwidth = self.config["hw_rnode_bandwidth"], "txpower": self.config["hw_rnode_tx_power"],
txpower = self.config["hw_rnode_tx_power"], "spreadingfactor": self.config["hw_rnode_spreading_factor"],
sf = self.config["hw_rnode_spreading_factor"], "codingrate": self.config["hw_rnode_coding_rate"],
cr = self.config["hw_rnode_coding_rate"], "flow_control": False,
flow_control = None, "id_interval": self.config["hw_rnode_beaconinterval"],
id_interval = self.config["hw_rnode_beaconinterval"], "id_callsign": self.config["hw_rnode_beacondata"],
id_callsign = self.config["hw_rnode_beacondata"], "st_alock": atl_short,
allow_bluetooth = False, "lt_alock": atl_long,
force_ble = rnode_allow_bluetooth, "allow_bluetooth": False,
ble_name = bt_device_name, "target_device_name": bt_device_name,
st_alock = atl_short, "force_ble": True,
lt_alock = atl_long, "ble_name": None,
) "ble_addr": None,
}
rnodeinterface = RNS.Interfaces.Android.RNodeInterface.RNodeInterface(RNS.Transport, interface_config)
rnodeinterface.OUT = True rnodeinterface.OUT = True
if RNS.Reticulum.transport_enabled(): if RNS.Reticulum.transport_enabled():
@ -3677,6 +3710,7 @@ class SidebandCore():
except Exception as e: except Exception as e:
RNS.log("Error while adding RNode Interface. The contained exception was: "+str(e)) RNS.log("Error while adding RNode Interface. The contained exception was: "+str(e))
RNS.trace_exception(e)
self.interface_rnode = None self.interface_rnode = None
def _reticulum_log_debug(self, debug=False): def _reticulum_log_debug(self, debug=False):
@ -3751,15 +3785,14 @@ class SidebandCore():
else: else:
ifac_size = None ifac_size = None
tcpinterface = RNS.Interfaces.TCPInterface.TCPClientInterface( interface_config = {
RNS.Transport, "name": "TCPClientInterface",
"TCPClientInterface", "target_host": tcp_host,
tcp_host, "target_port": tcp_port,
tcp_port, "kiss_framing": False,
kiss_framing = False, "i2p_tunneled": False,
i2p_tunneled = False }
) tcpinterface = RNS.Interfaces.TCPInterface.TCPClientInterface(RNS.Transport, interface_config)
tcpinterface.OUT = True tcpinterface.OUT = True
if RNS.Reticulum.transport_enabled(): if RNS.Reticulum.transport_enabled():
@ -3803,13 +3836,14 @@ class SidebandCore():
else: else:
ifac_size = None ifac_size = None
i2pinterface = RNS.Interfaces.I2PInterface.I2PInterface( interface_config = {
RNS.Transport, "name": "I2PInterface",
"I2PInterface", "storagepath": RNS.Reticulum.storagepath,
RNS.Reticulum.storagepath, "peers": [self.config["connect_i2p_b32"]],
[self.config["connect_i2p_b32"]], "connectable": False,
connectable = False, }
)
i2pinterface = RNS.Interfaces.I2PInterface.I2PInterface(RNS.Transport, interface_config)
i2pinterface.OUT = True i2pinterface.OUT = True
@ -3862,16 +3896,15 @@ class SidebandCore():
else: else:
ifac_netkey = self.config["connect_serial_ifac_passphrase"] ifac_netkey = self.config["connect_serial_ifac_passphrase"]
serialinterface = RNS.Interfaces.Android.SerialInterface.SerialInterface( interface_config = {
RNS.Transport, "name": "SerialInterface",
"SerialInterface", "port": target_device["port"],
target_device["port"], "speed": self.config["hw_serial_baudrate"],
self.config["hw_serial_baudrate"], "databits": self.config["hw_serial_databits"],
self.config["hw_serial_databits"], "parity": self.config["hw_serial_parity"],
self.config["hw_serial_parity"], "stopbits": self.config["hw_serial_stopbits"],
self.config["hw_serial_stopbits"], }
) serialinterface = RNS.Interfaces.Android.SerialInterface.SerialInterface(RNS.Transport, interface_config)
serialinterface.OUT = True serialinterface.OUT = True
if RNS.Reticulum.transport_enabled(): if RNS.Reticulum.transport_enabled():
@ -3915,23 +3948,22 @@ class SidebandCore():
else: else:
ifac_netkey = self.config["connect_modem_ifac_passphrase"] ifac_netkey = self.config["connect_modem_ifac_passphrase"]
modeminterface = RNS.Interfaces.Android.KISSInterface.KISSInterface( interface_config = {
RNS.Transport, "name": "ModemInterface",
"ModemInterface", "port": target_device["port"],
target_device["port"], "speed": self.config["hw_modem_baudrate"],
self.config["hw_modem_baudrate"], "databits": self.config["hw_modem_databits"],
self.config["hw_modem_databits"], "parity": self.config["hw_modem_parity"],
self.config["hw_modem_parity"], "stopbits": self.config["hw_modem_stopbits"],
self.config["hw_modem_stopbits"], "preamble": self.config["hw_modem_preamble"],
self.config["hw_modem_preamble"], "txtail": self.config["hw_modem_tail"],
self.config["hw_modem_tail"], "persistence": self.config["hw_modem_persistence"],
self.config["hw_modem_persistence"], "slottime": self.config["hw_modem_slottime"],
self.config["hw_modem_slottime"], "flow_control": False,
False, # flow control "beacon_interval": self.config["hw_modem_beaconinterval"],
self.config["hw_modem_beaconinterval"], "beacon_data": self.config["hw_modem_beacondata"],
self.config["hw_modem_beacondata"], }
) modeminterface = RNS.Interfaces.Android.KISSInterface.KISSInterface(RNS.Transport, interface_config)
modeminterface.OUT = True modeminterface.OUT = True
if RNS.Reticulum.transport_enabled(): if RNS.Reticulum.transport_enabled():
@ -4328,7 +4360,11 @@ class SidebandCore():
try: try:
addr_b = bytes.fromhex(dest_str) addr_b = bytes.fromhex(dest_str)
self._db_create_conversation(addr_b, name, trusted) if addr_b == self.lxmf_destination.hash:
RNS.log("Cannot create conversation with own LXMF address", RNS.LOG_ERROR)
return False
else:
self._db_create_conversation(addr_b, name, trusted)
except Exception as e: except Exception as e:
RNS.log("Error while creating conversation: "+str(e), RNS.LOG_ERROR) RNS.log("Error while creating conversation: "+str(e), RNS.LOG_ERROR)
@ -4437,7 +4473,11 @@ class SidebandCore():
if not originator and LXMF.FIELD_AUDIO in message.fields and ptt_enabled: if not originator and LXMF.FIELD_AUDIO in message.fields and ptt_enabled:
self.ptt_event(message) self.ptt_event(message)
should_notify = False if self.gui_conversation() != context_dest:
if not RNS.vendor.platformutils.is_android():
should_notify = True
else:
should_notify = False
if self.is_client: if self.is_client:
should_notify = False should_notify = False

View File

@ -744,7 +744,8 @@ class Location(Sensor):
if "altitude" in self._raw: if "altitude" in self._raw:
self.altitude = self._raw["altitude"] self.altitude = self._raw["altitude"]
if "speed" in self._raw: if "speed" in self._raw:
self.speed = self._raw["speed"] # Android GPS reports speed in m/s, convert to km/h
self.speed = self._raw["speed"]*3.6
if self.speed < 0: if self.speed < 0:
self.speed = 0 self.speed = 0
if "bearing" in self._raw: if "bearing" in self._raw:

View File

@ -524,8 +524,8 @@ Builder.load_string("""
id: n_address_field id: n_address_field
max_text_length: 32 max_text_length: 32
hint_text: "Address" hint_text: "Address"
helper_text: "Error, check your input" helper_text: ""
helper_text_mode: "on_error" helper_text_mode: "on_focus"
text: "" text: ""
font_size: dp(24) font_size: dp(24)
@ -652,6 +652,10 @@ Builder.load_string("""
padding: [0, 0, 0, dp(16)] padding: [0, 0, 0, dp(16)]
height: self.minimum_height+dp(24) height: self.minimum_height+dp(24)
MDLabel:
id: node_info
text: "Unknown propagation node"
MDProgressBar: MDProgressBar:
id: sync_progress id: sync_progress
type: "determinate" type: "determinate"
@ -659,7 +663,6 @@ Builder.load_string("""
MDLabel: MDLabel:
id: sync_status id: sync_status
hint_text: "Name"
text: "Initiating sync..." text: "Initiating sync..."

View File

@ -80,6 +80,15 @@ MDNavigationLayout:
on_release: root.ids.screen_manager.app.map_action(self) on_release: root.ids.screen_manager.app.map_action(self)
OneLineIconListItem:
text: "Overview"
on_release: root.ids.screen_manager.app.overview_action(self)
IconLeftWidget:
icon: "view-dashboard-outline"
on_release: root.ids.screen_manager.app.overview_action(self)
OneLineIconListItem: OneLineIconListItem:
text: "Announce Stream" text: "Announce Stream"
on_release: root.ids.screen_manager.app.announces_action(self) on_release: root.ids.screen_manager.app.announces_action(self)
@ -107,6 +116,15 @@ MDNavigationLayout:
on_release: root.ids.screen_manager.app.telemetry_action(self) on_release: root.ids.screen_manager.app.telemetry_action(self)
OneLineIconListItem:
text: "Utilities"
on_release: root.ids.screen_manager.app.utilities_action(self)
IconLeftWidget:
icon: "tools"
on_release: root.ids.screen_manager.app.utilities_action(self)
OneLineIconListItem: OneLineIconListItem:
text: "Preferences" text: "Preferences"
on_release: root.ids.screen_manager.app.settings_action(self) on_release: root.ids.screen_manager.app.settings_action(self)
@ -1260,6 +1278,28 @@ MDScreen:
""" """
layout_settings_screen = """ layout_settings_screen = """
<UIScaling>
orientation: "vertical"
spacing: "24dp"
size_hint_y: None
height: self.minimum_height+dp(0)
MDLabel:
id: scaling_info
markup: True
text: "You can scale the entire Sideband UI by specifying a scaling factor in the field below. After setting it, restart sideband for the scaling to take effect.\\n\\nSet to 0.0 to disable scaling adjustments."
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
MDTextField:
id: scaling_factor
hint_text: "Scaling Factor"
helper_text: "From 0.3 to 5.0"
helper_text_mode: "on_focus"
text: ""
font_size: dp(24)
MDScreen: MDScreen:
name: "settings_screen" name: "settings_screen"
@ -1399,11 +1439,21 @@ MDScreen:
size_hint_y: None size_hint_y: None
height: self.texture_size[1] height: self.texture_size[1]
MDRectangleFlatIconButton:
id: appearance_ui_scaling
icon: "relative-scale"
text: "Configure UI Scaling"
padding: [dp(0), dp(14), dp(0), dp(14)]
icon_size: dp(24)
font_size: dp(16)
size_hint: [1.0, None]
on_release: root.app.configure_ui_scaling_action(self)
MDBoxLayout: MDBoxLayout:
orientation: "horizontal" orientation: "horizontal"
size_hint_y: None size_hint_y: None
padding: [0,0,dp(24),dp(0)] padding: [0,dp(14),dp(24),dp(0)]
height: dp(48) height: dp(62)
MDLabel: MDLabel:
text: "Notifications" text: "Notifications"

205
sbapp/ui/utilities.py Normal file
View File

@ -0,0 +1,205 @@
import time
import RNS
from typing import Union
from kivy.metrics import dp,sp
from kivy.lang.builder import Builder
from kivy.core.clipboard import Clipboard
from kivy.utils import escape_markup
from kivymd.uix.recycleview import MDRecycleView
from kivymd.uix.list import OneLineIconListItem
from kivymd.uix.pickers import MDColorPicker
from kivymd.icon_definitions import md_icons
from kivy.properties import StringProperty, BooleanProperty
from kivy.effects.scroll import ScrollEffect
from kivy.clock import Clock
from sideband.sense import Telemeter
import threading
from datetime import datetime
if RNS.vendor.platformutils.get_platform() == "android":
from ui.helpers import ts_format
from android.permissions import request_permissions, check_permission
else:
from .helpers import ts_format
class Utilities():
def __init__(self, app):
self.app = app
self.screen = None
self.rnstatus_screen = None
self.rnstatus_instance = None
if not self.app.root.ids.screen_manager.has_screen("utilities_screen"):
self.screen = Builder.load_string(layout_utilities_screen)
self.screen.app = self.app
self.screen.delegate = self
self.app.root.ids.screen_manager.add_widget(self.screen)
self.screen.ids.telemetry_scrollview.effect_cls = ScrollEffect
info = "\nYou can use various RNS utilities from Sideband. "
info += ""
if self.app.theme_cls.theme_style == "Dark":
info = "[color=#"+self.app.dark_theme_text_color+"]"+info+"[/color]"
self.screen.ids.telemetry_info.text = info
### rnstatus screen
######################################
def rnstatus_action(self, sender=None):
if not self.app.root.ids.screen_manager.has_screen("rnstatus_screen"):
self.rnstatus_screen = Builder.load_string(layout_rnstatus_screen)
self.rnstatus_screen.app = self.app
self.rnstatus_screen.delegate = self
self.app.root.ids.screen_manager.add_widget(self.rnstatus_screen)
self.app.root.ids.screen_manager.transition.direction = "left"
self.app.root.ids.screen_manager.current = "rnstatus_screen"
self.app.sideband.setstate("app.displaying", self.app.root.ids.screen_manager.current)
self.update_rnstatus()
def update_rnstatus(self, sender=None):
threading.Thread(target=self.update_rnstatus_job, daemon=True).start()
def update_rnstatus_job(self, sender=None):
if self.rnstatus_instance == None:
import RNS.Utilities.rnstatus as rnstatus
self.rnstatus_instance = rnstatus
import io
from contextlib import redirect_stdout
output_marker = "===begin rnstatus output==="
output = "None"
with io.StringIO() as buffer, redirect_stdout(buffer):
print(output_marker, end="")
self.rnstatus_instance.main(rns_instance=RNS.Reticulum.get_instance())
output = buffer.getvalue()
remainder = output[:output.find(output_marker)]
output = output[output.find(output_marker)+len(output_marker):]
print(remainder, end="")
def cb(dt):
self.rnstatus_screen.ids.rnstatus_output.text = f"[font=RobotoMono-Regular]{output}[/font]"
Clock.schedule_once(cb, 0.2)
if self.app.root.ids.screen_manager.current == "rnstatus_screen":
Clock.schedule_once(self.update_rnstatus, 1)
layout_utilities_screen = """
MDScreen:
name: "utilities_screen"
BoxLayout:
orientation: "vertical"
MDTopAppBar:
title: "Utilities"
anchor_title: "left"
elevation: 0
left_action_items:
[['menu', lambda x: root.app.nav_drawer.set_state("open")]]
right_action_items:
[
['close', lambda x: root.app.close_any_action(self)],
]
ScrollView:
id: telemetry_scrollview
MDBoxLayout:
orientation: "vertical"
size_hint_y: None
height: self.minimum_height
padding: [dp(28), dp(48), dp(28), dp(16)]
MDLabel:
text: "Utilities & Tools"
font_style: "H6"
MDLabel:
id: telemetry_info
markup: True
text: ""
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
MDBoxLayout:
orientation: "vertical"
spacing: "24dp"
size_hint_y: None
height: self.minimum_height
padding: [dp(0), dp(35), dp(0), dp(35)]
MDRectangleFlatIconButton:
id: rnstatus_button
icon: "wifi-check"
text: "Reticulum Status"
padding: [dp(0), dp(14), dp(0), dp(14)]
icon_size: dp(24)
font_size: dp(16)
size_hint: [1.0, None]
on_release: root.delegate.rnstatus_action(self)
disabled: False
MDRectangleFlatIconButton:
id: logview_button
icon: "list-box-outline"
text: "Log Viewer"
padding: [dp(0), dp(14), dp(0), dp(14)]
icon_size: dp(24)
font_size: dp(16)
size_hint: [1.0, None]
on_release: root.delegate.rnstatus_action(self)
disabled: True
"""
layout_rnstatus_screen = """
MDScreen:
name: "rnstatus_screen"
BoxLayout:
orientation: "vertical"
MDTopAppBar:
id: top_bar
title: "Reticulum Status"
anchor_title: "left"
elevation: 0
left_action_items:
[['menu', lambda x: root.app.nav_drawer.set_state("open")]]
right_action_items:
[
['refresh', lambda x: root.delegate.update_rnstatus()],
['close', lambda x: root.app.close_sub_utilities_action(self)],
]
MDScrollView:
id: sensors_scrollview
size_hint_x: 1
size_hint_y: None
size: [root.width, root.height-root.ids.top_bar.height]
do_scroll_x: False
do_scroll_y: True
MDGridLayout:
cols: 1
padding: [dp(28), dp(14), dp(28), dp(28)]
size_hint_y: None
height: self.minimum_height
MDLabel:
id: rnstatus_output
markup: True
text: ""
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
"""

View File

@ -99,8 +99,8 @@ setuptools.setup(
] ]
}, },
install_requires=[ install_requires=[
"rns>=0.8.4", "rns>=0.8.7",
"lxmf>=0.5.7", "lxmf>=0.5.8",
"kivy>=2.3.0", "kivy>=2.3.0",
"pillow>=10.2.0", "pillow>=10.2.0",
"qrcode", "qrcode",
@ -108,11 +108,10 @@ setuptools.setup(
"ffpyplayer", "ffpyplayer",
"sh", "sh",
"numpy<=1.26.4", "numpy<=1.26.4",
"pycodec2;platform_system!='Windows'", "pycodec2;sys.platform!='Windows' and sys.platform!='win32' and sys.platform!='darwin'",
"pyaudio;sys.platform=='linux'", "pyaudio;sys.platform=='linux'",
"pyobjus;sys.platform=='darwin'", "pyobjus;sys.platform=='darwin'",
"pyogg;sys.platform=='darwin'", "pyogg;sys.platform=='Windows' and sys.platform!='win32'",
"pyogg;platform_system=='Windows'",
], ],
python_requires='>=3.7', python_requires='>=3.7',
) )

68
sideband.spec Normal file
View File

@ -0,0 +1,68 @@
# -*- mode: python ; coding: utf-8 -*-
from kivy_deps import sdl2, glew
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
def extra_datas(mydir):
def rec_glob(p, files):
import os
import glob
for d in glob.glob(p):
if os.path.isfile(d):
files.append(d)
rec_glob("%s/*" % d, files)
files = []
rec_glob("%s/*" % mydir, files)
extra_datas = []
for f in files:
extra_datas.append((f, f, 'DATA'))
return extra_datas
a.datas += extra_datas('sbapp')
a.datas += extra_datas('RNS')
a.datas += extra_datas('LXMF')
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='Sideband',
icon="sbapp\\assets\\icon.png",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
*[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
strip=False,
upx=True,
upx_exclude=[],
name='main',
)