mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
feat: update LXST version to 0.5.0 for android, fix telephone initialization error handling, and add compatibility layer for LXST profiles across versions
This commit is contained in:
parent
9c739fa940
commit
026b8c5bc7
25 changed files with 334 additions and 98 deletions
|
|
@ -11,7 +11,7 @@ def repositoryBundledWheelsDir = new File(meshchatSourceDir, "public/repository-
|
|||
def chaquopyBuildPython = System.getenv("CHAQUOPY_BUILD_PYTHON")
|
||||
def pythonTempDir = new File(buildDir, "python-tmp")
|
||||
def vendorWheelDir = new File(rootProject.projectDir, "vendor")
|
||||
def lxstPatchedWheel = new File(rootProject.projectDir, "vendor/lxst-0.4.8-py3-none-any.whl")
|
||||
def lxstPatchedWheel = new File(rootProject.projectDir, "vendor/lxst-0.5.0-py3-none-any.whl")
|
||||
def bleakPatchedWheel = new File(rootProject.projectDir, "vendor/bleak-3.0.2-py3-none-any.whl")
|
||||
def rnsPatchedWheel = new File(rootProject.projectDir, "vendor/rns-1.4.0-py3-none-any.whl")
|
||||
def allAndroidAbis = ["arm64-v8a", "x86_64", "armeabi-v7a"]
|
||||
|
|
|
|||
BIN
meshchatx.rsm
BIN
meshchatx.rsm
Binary file not shown.
|
|
@ -249,12 +249,12 @@ def register_telephone_routes(routes, app):
|
|||
"path_hops": None,
|
||||
"path_interface": None,
|
||||
}
|
||||
from LXST.Primitives.Telephony import Profiles
|
||||
from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
|
||||
|
||||
mode_id = active_call["call_mode_id"]
|
||||
with contextlib.suppress(Exception):
|
||||
active_call["call_mode_name"] = Profiles.mode_name(mode_id)
|
||||
active_call["call_mode_abbrev"] = Profiles.mode_abbrevation(mode_id)
|
||||
active_call["call_mode_name"] = lxst_modes.mode_name(mode_id)
|
||||
active_call["call_mode_abbrev"] = lxst_modes.mode_abbreviation(mode_id)
|
||||
link = getattr(app.telephone_manager, "call_stats", {}).get("link")
|
||||
if link:
|
||||
active_call["tx_bytes"] = getattr(link, "txbytes", 0)
|
||||
|
|
@ -497,20 +497,21 @@ def register_telephone_routes(routes, app):
|
|||
|
||||
@routes.get("/api/v1/telephone/call-modes")
|
||||
async def telephone_call_modes(request):
|
||||
from LXST.Primitives.Telephony import Profiles
|
||||
from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
|
||||
|
||||
half = lxst_modes.mode_half_duplex()
|
||||
modes = [
|
||||
{
|
||||
"id": mode_id,
|
||||
"name": Profiles.mode_name(mode_id),
|
||||
"abbrev": Profiles.mode_abbrevation(mode_id),
|
||||
"is_half_duplex": mode_id == Profiles.MODE_HALF_DUPLEX,
|
||||
"name": lxst_modes.mode_name(mode_id),
|
||||
"abbrev": lxst_modes.mode_abbreviation(mode_id),
|
||||
"is_half_duplex": mode_id == half,
|
||||
}
|
||||
for mode_id in Profiles.available_modes()
|
||||
for mode_id in lxst_modes.available_modes()
|
||||
]
|
||||
return web.json_response(
|
||||
{
|
||||
"default_call_mode_id": Profiles.DEFAULT_MODE,
|
||||
"default_call_mode_id": lxst_modes.default_mode(),
|
||||
"call_modes": modes,
|
||||
},
|
||||
)
|
||||
|
|
@ -529,14 +530,14 @@ def register_telephone_routes(routes, app):
|
|||
int(mode_id),
|
||||
)
|
||||
app.config.telephone_call_mode_id.set(resolved)
|
||||
from LXST.Primitives.Telephony import Profiles
|
||||
from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"message": f"Switched to mode {resolved}",
|
||||
"mode_id": resolved,
|
||||
"mode_name": Profiles.mode_name(resolved),
|
||||
"is_half_duplex": resolved == Profiles.MODE_HALF_DUPLEX,
|
||||
"mode_name": lxst_modes.mode_name(resolved),
|
||||
"is_half_duplex": resolved == lxst_modes.mode_half_duplex(),
|
||||
"is_ptt_active": bool(app.telephone_manager.ptt_active),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -363,9 +363,15 @@ class IdentityContext:
|
|||
)
|
||||
|
||||
if not getattr(self.app, "emergency", False):
|
||||
self.telephone_manager.init_telephone()
|
||||
with contextlib.suppress(Exception):
|
||||
self.app.sync_telephone_call_policy(context=self)
|
||||
# Telephony must not take down mesh startup. Android may ship an
|
||||
# older LXST without duplex helpers, and audio backends can fail
|
||||
# while LXMF/RNS remain usable.
|
||||
try:
|
||||
self.telephone_manager.init_telephone()
|
||||
with contextlib.suppress(Exception):
|
||||
self.app.sync_telephone_call_policy(context=self)
|
||||
except Exception as exc:
|
||||
print(f"Telephone init failed (mesh continues): {exc}", flush=True)
|
||||
|
||||
self.voicemail_manager = VoicemailManager(
|
||||
db=self.database,
|
||||
|
|
|
|||
79
meshchatx/src/backend/lxst_profiles_compat.py
Normal file
79
meshchatx/src/backend/lxst_profiles_compat.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""LXST Profiles helpers that work across LXST versions.
|
||||
|
||||
Android previously vendored lxst 0.4.8 without call-mode helpers
|
||||
(available_modes, MODE_FULL_DUPLEX, MODE_HALF_DUPLEX). Desktop and current
|
||||
Android builds ship 0.5.0+. MeshChatX keeps this shim so identity setup never
|
||||
crashes when an older Profiles class is present.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Match LXST 0.5+ Profiles constants when the installed package lacks them.
|
||||
MODE_FULL_DUPLEX = 0x01
|
||||
MODE_HALF_DUPLEX = 0x02
|
||||
DEFAULT_MODE = MODE_FULL_DUPLEX
|
||||
|
||||
_MODE_NAMES = {
|
||||
MODE_FULL_DUPLEX: "Full Duplex",
|
||||
MODE_HALF_DUPLEX: "Half Duplex",
|
||||
}
|
||||
_MODE_ABBREVS = {
|
||||
MODE_FULL_DUPLEX: "FDX",
|
||||
MODE_HALF_DUPLEX: "HDX",
|
||||
}
|
||||
|
||||
|
||||
def import_profiles():
|
||||
"""Import LXST Profiles, or raise ImportError if LXST is unavailable."""
|
||||
from LXST.Primitives.Telephony import Profiles
|
||||
|
||||
return Profiles
|
||||
|
||||
|
||||
def duplex_modes_supported(profiles=None) -> bool:
|
||||
"""True when the installed LXST Profiles exposes call-mode helpers."""
|
||||
Profiles = profiles if profiles is not None else import_profiles()
|
||||
return callable(getattr(Profiles, "available_modes", None))
|
||||
|
||||
|
||||
def mode_full_duplex(profiles=None) -> int:
|
||||
Profiles = profiles if profiles is not None else import_profiles()
|
||||
return int(getattr(Profiles, "MODE_FULL_DUPLEX", MODE_FULL_DUPLEX))
|
||||
|
||||
|
||||
def mode_half_duplex(profiles=None) -> int:
|
||||
Profiles = profiles if profiles is not None else import_profiles()
|
||||
return int(getattr(Profiles, "MODE_HALF_DUPLEX", MODE_HALF_DUPLEX))
|
||||
|
||||
|
||||
def default_mode(profiles=None) -> int:
|
||||
Profiles = profiles if profiles is not None else import_profiles()
|
||||
return int(getattr(Profiles, "DEFAULT_MODE", DEFAULT_MODE))
|
||||
|
||||
|
||||
def available_modes(profiles=None) -> list[int]:
|
||||
"""Return duplex mode ids supported by the installed LXST build."""
|
||||
Profiles = profiles if profiles is not None else import_profiles()
|
||||
native = getattr(Profiles, "available_modes", None)
|
||||
if callable(native):
|
||||
return list(native())
|
||||
# Pre-duplex LXST: full duplex only. Half duplex needs switch_mode / squelch.
|
||||
return [mode_full_duplex(Profiles)]
|
||||
|
||||
|
||||
def mode_name(mode_id, profiles=None) -> str:
|
||||
Profiles = profiles if profiles is not None else import_profiles()
|
||||
native = getattr(Profiles, "mode_name", None)
|
||||
if callable(native):
|
||||
return str(native(mode_id))
|
||||
return _MODE_NAMES.get(int(mode_id), "Default")
|
||||
|
||||
|
||||
def mode_abbreviation(mode_id, profiles=None) -> str:
|
||||
Profiles = profiles if profiles is not None else import_profiles()
|
||||
native = getattr(Profiles, "mode_abbrevation", None)
|
||||
if callable(native):
|
||||
return str(native(mode_id))
|
||||
return _MODE_ABBREVS.get(int(mode_id), "DFLT")
|
||||
|
|
@ -171,9 +171,9 @@ class TelephoneManager:
|
|||
|
||||
def resolve_call_mode_id(self, mode_id=None):
|
||||
"""Return a valid LXST call mode id (full or half duplex)."""
|
||||
from LXST.Primitives.Telephony import Profiles
|
||||
from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
|
||||
|
||||
available = set(Profiles.available_modes())
|
||||
available = set(lxst_modes.available_modes())
|
||||
mid = mode_id
|
||||
if mid is None and self.config_manager:
|
||||
with contextlib.suppress(Exception):
|
||||
|
|
@ -183,7 +183,7 @@ class TelephoneManager:
|
|||
except (TypeError, ValueError):
|
||||
mid = None
|
||||
if mid not in available:
|
||||
mid = Profiles.DEFAULT_MODE
|
||||
mid = lxst_modes.default_mode()
|
||||
return mid
|
||||
|
||||
def apply_preferred_mode(self, mode_id=None):
|
||||
|
|
@ -199,7 +199,7 @@ class TelephoneManager:
|
|||
|
||||
def switch_mode(self, mode_id):
|
||||
"""Switch live call duplex mode via LXST signalling and local squelch."""
|
||||
from LXST.Primitives.Telephony import Profiles
|
||||
from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
|
||||
|
||||
resolved = self.resolve_call_mode_id(mode_id)
|
||||
self.preferred_mode_id = resolved
|
||||
|
|
@ -216,7 +216,7 @@ class TelephoneManager:
|
|||
self.telephone.active_call.call_mode = resolved
|
||||
# Half duplex starts listen-only. Full duplex keeps continuous TX open.
|
||||
self.ptt_active = False
|
||||
if resolved == Profiles.MODE_HALF_DUPLEX:
|
||||
if resolved == lxst_modes.mode_half_duplex():
|
||||
with contextlib.suppress(Exception):
|
||||
self.telephone.squelch_transmit(True)
|
||||
else:
|
||||
|
|
@ -226,23 +226,24 @@ class TelephoneManager:
|
|||
|
||||
def get_active_mode_id(self):
|
||||
"""Return the active call mode, preferred mode, or LXST default."""
|
||||
from LXST.Primitives.Telephony import Profiles
|
||||
from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
|
||||
|
||||
available = set(lxst_modes.available_modes())
|
||||
if self.telephone and self.telephone.active_call:
|
||||
link_mode = getattr(self.telephone.active_call, "call_mode", None)
|
||||
if link_mode in Profiles.available_modes():
|
||||
if link_mode in available:
|
||||
return link_mode
|
||||
mode = getattr(self.telephone, "active_mode", None)
|
||||
if mode in Profiles.available_modes():
|
||||
if mode in available:
|
||||
return mode
|
||||
if self.preferred_mode_id in Profiles.available_modes():
|
||||
if self.preferred_mode_id in available:
|
||||
return self.preferred_mode_id
|
||||
return self.resolve_call_mode_id()
|
||||
|
||||
def is_half_duplex(self):
|
||||
from LXST.Primitives.Telephony import Profiles
|
||||
from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
|
||||
|
||||
return self.get_active_mode_id() == Profiles.MODE_HALF_DUPLEX
|
||||
return self.get_active_mode_id() == lxst_modes.mode_half_duplex()
|
||||
|
||||
def is_transmit_squelched(self):
|
||||
"""True when LXST packetizer is squelched (half-duplex idle / PTT up)."""
|
||||
|
|
|
|||
|
|
@ -23,10 +23,12 @@
|
|||
:network-degraded-label="networkDegradedBannerLabel"
|
||||
:network-recovering="networkRecovering"
|
||||
:recover-network-label="$t('app.recover_network')"
|
||||
:open-settings-label="$t('app.open_settings')"
|
||||
:open-interfaces-label="$t('app.open_interfaces')"
|
||||
@restart-backend="onRestartBackend"
|
||||
@view-backend-logs="onViewBackendCrashReport"
|
||||
@recover-network="onRecoverNetwork"
|
||||
@open-settings="onOpenSettingsForRecovery"
|
||||
@open-interfaces="onOpenInterfacesForRecovery"
|
||||
/>
|
||||
|
||||
|
|
@ -1205,6 +1207,9 @@ export default {
|
|||
onOpenInterfacesForRecovery() {
|
||||
this.$router.push({ name: "interfaces" });
|
||||
},
|
||||
onOpenSettingsForRecovery() {
|
||||
this.$router.push({ name: "settings" });
|
||||
},
|
||||
async onRecoverNetwork() {
|
||||
if (this.networkRecovering) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,13 @@
|
|||
>
|
||||
{{ recoverNetworkLabel }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-white/10 px-3 py-1 text-xs font-semibold hover:bg-white/20"
|
||||
@click="$emit('open-settings')"
|
||||
>
|
||||
{{ openSettingsLabel }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-white/10 px-3 py-1 text-xs font-semibold hover:bg-white/20"
|
||||
|
|
@ -152,11 +159,15 @@ export default {
|
|||
type: String,
|
||||
default: "",
|
||||
},
|
||||
openSettingsLabel: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
openInterfacesLabel: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ["restart-backend", "view-backend-logs", "recover-network", "open-interfaces"],
|
||||
emits: ["restart-backend", "view-backend-logs", "recover-network", "open-settings", "open-interfaces"],
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
49
meshchatx/src/frontend/js/networkRecovery.js
Normal file
49
meshchatx/src/frontend/js/networkRecovery.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
/**
|
||||
* Classify mesh startup / degraded errors for recovery UI.
|
||||
* Not every failure is an interface config problem.
|
||||
*/
|
||||
|
||||
const INTERFACE_HINTS = [
|
||||
"interface",
|
||||
"interfaces",
|
||||
"rnode",
|
||||
"serial",
|
||||
"usb",
|
||||
"autointerface",
|
||||
"tcpclient",
|
||||
"udp",
|
||||
"i2p",
|
||||
"wlan",
|
||||
"kiss",
|
||||
"ax25",
|
||||
"pipeinterface",
|
||||
"rns config",
|
||||
"configobj",
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {unknown} error
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLikelyInterfaceRecoveryError(error) {
|
||||
const text = String(error || "").toLowerCase();
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return INTERFACE_HINTS.some((hint) => text.includes(hint));
|
||||
}
|
||||
|
||||
/**
|
||||
* Route name to open after degraded startup, or null to leave the user on the
|
||||
* current page (banner actions still apply).
|
||||
* @param {unknown} error
|
||||
* @returns {"interfaces" | null}
|
||||
*/
|
||||
export function recoveryRouteForNetworkError(error) {
|
||||
if (isLikelyInterfaceRecoveryError(error)) {
|
||||
return "interfaces";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
export const STARTUP_STAGE_LABELS = {
|
||||
http: "Getting things ready…",
|
||||
starting: "Getting the mesh ready…",
|
||||
rns: "Connecting to the mesh…",
|
||||
starting: "Getting RNS ready…",
|
||||
rns: "Starting RNS…",
|
||||
identity: "Almost there…",
|
||||
ready: "Ready",
|
||||
failed: "Startup failed",
|
||||
|
|
@ -29,7 +29,7 @@ export function interpretStartupStatus(data) {
|
|||
|
||||
if (status === "failed") {
|
||||
// HTTP is up and the backend marked UI as usable: mount the app in
|
||||
// degraded mode so interfaces/settings remain reachable for recovery.
|
||||
// degraded mode so settings/interfaces remain reachable for recovery.
|
||||
if (uiReady || networkDegraded) {
|
||||
return {
|
||||
kind: "degraded",
|
||||
|
|
@ -48,7 +48,7 @@ export function interpretStartupStatus(data) {
|
|||
}
|
||||
if (status === "starting" || status === undefined) {
|
||||
const resolvedStage = stage || "starting";
|
||||
const label = STARTUP_STAGE_LABELS[resolvedStage] || "Starting network…";
|
||||
const label = STARTUP_STAGE_LABELS[resolvedStage] || "Starting RNS…";
|
||||
// HTTP is bound and the shell may mount while RNS/identity finish.
|
||||
if (uiReady) {
|
||||
return {
|
||||
|
|
@ -101,7 +101,7 @@ export async function waitForNetworkReady(options = {}) {
|
|||
const data = await response.json();
|
||||
const interpreted = interpretStartupStatus(data);
|
||||
if (interpreted.kind === "degraded") {
|
||||
onLine(interpreted.error || "Mesh network unavailable. Opening recovery UI…");
|
||||
onLine(interpreted.error || "RNS unavailable. Opening recovery UI…");
|
||||
onDegraded(interpreted.error);
|
||||
return "degraded";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -491,12 +491,13 @@
|
|||
"rpc_key_hide": "RPC-Schlüssel verbergen",
|
||||
"refresh_community_interfaces": "Von directory.rns.recipes aktualisieren",
|
||||
"refresh_community_interfaces_busy": "Wird aktualisiert…",
|
||||
"network_starting": "Verbinde mit dem Mesh…",
|
||||
"network_degraded": "Mesh-Netzwerk nicht verfügbar. Die App läuft weiter, damit Sie Schnittstellen reparieren können, ohne Daten zu löschen.",
|
||||
"recover_network": "Netzwerk erneut versuchen",
|
||||
"network_starting": "RNS wird gestartet…",
|
||||
"network_degraded": "RNS nicht verfügbar. Die App läuft weiter, damit Sie diagnostizieren und wiederherstellen können, ohne Daten zu löschen.",
|
||||
"recover_network": "RNS erneut versuchen",
|
||||
"open_settings": "Einstellungen öffnen",
|
||||
"open_interfaces": "Schnittstellen öffnen",
|
||||
"network_recovered": "Netzwerkstapel wiederhergestellt",
|
||||
"network_recover_failed": "Netzwerkstapel konnte nicht wiederhergestellt werden. Prüfen Sie die Schnittstellen und versuchen Sie es erneut.",
|
||||
"network_recovered": "RNS wiederhergestellt",
|
||||
"network_recover_failed": "RNS konnte nicht wiederhergestellt werden. Prüfen Sie die Fehlerdetails, dann erneut versuchen oder Einstellungen und Schnittstellen prüfen.",
|
||||
"battery_unavailable": "Akkustatus nicht verfügbar",
|
||||
"battery_level_title": "Akku {percent}",
|
||||
"battery_charging_title": "Lädt {percent}",
|
||||
|
|
|
|||
|
|
@ -430,12 +430,13 @@
|
|||
"backend_process_stopped": "Reticulum backend stopped",
|
||||
"backend_reconnected": "Reconnected to backend",
|
||||
"restart_backend": "Restart backend",
|
||||
"network_starting": "Connecting to the mesh…",
|
||||
"network_degraded": "Mesh network unavailable. The app is still running so you can fix interfaces without wiping data.",
|
||||
"recover_network": "Retry network",
|
||||
"network_starting": "Starting RNS…",
|
||||
"network_degraded": "RNS unavailable. The app is still running so you can diagnose and recover without wiping data.",
|
||||
"recover_network": "Retry RNS",
|
||||
"open_settings": "Open settings",
|
||||
"open_interfaces": "Open interfaces",
|
||||
"network_recovered": "Network stack recovered",
|
||||
"network_recover_failed": "Could not recover the network stack. Check interfaces and try again.",
|
||||
"network_recovered": "RNS recovered",
|
||||
"network_recover_failed": "Could not recover RNS. Check the error details, then retry or review settings and interfaces.",
|
||||
"restart_backend_started": "Restarting backend…",
|
||||
"restart_backend_failed": "Could not restart backend",
|
||||
"memory_pressure_warning": "High memory use detected. Close large Nomad pages, voice notes, or downloads if you can, then dismiss this warning. Persistent pressure can come from hostile peer content.",
|
||||
|
|
|
|||
|
|
@ -491,12 +491,13 @@
|
|||
"rpc_key_hide": "Ocultar clave RPC",
|
||||
"refresh_community_interfaces": "Actualizar desde directory.rns.recipes",
|
||||
"refresh_community_interfaces_busy": "Actualizando…",
|
||||
"network_starting": "Conectando a la mesh…",
|
||||
"network_degraded": "Red mesh no disponible. La aplicación sigue en ejecución para que pueda reparar interfaces sin borrar datos.",
|
||||
"recover_network": "Reintentar red",
|
||||
"network_starting": "Iniciando RNS…",
|
||||
"network_degraded": "RNS no disponible. La aplicación sigue en ejecución para que pueda diagnosticar y recuperar sin borrar datos.",
|
||||
"recover_network": "Reintentar RNS",
|
||||
"open_settings": "Abrir configuración",
|
||||
"open_interfaces": "Abrir interfaces",
|
||||
"network_recovered": "Pila de red recuperada",
|
||||
"network_recover_failed": "No se pudo recuperar la pila de red. Revise las interfaces e inténtelo de nuevo.",
|
||||
"network_recovered": "RNS recuperado",
|
||||
"network_recover_failed": "No se pudo recuperar RNS. Revise el error y reintente, o revise la configuración y las interfaces.",
|
||||
"battery_unavailable": "Estado de batería no disponible",
|
||||
"battery_level_title": "Batería {percent}",
|
||||
"battery_charging_title": "Cargando {percent}",
|
||||
|
|
|
|||
|
|
@ -491,12 +491,13 @@
|
|||
"rpc_key_hide": "Piilota RPC-avain",
|
||||
"refresh_community_interfaces": "Päivitä lähteestä directory.rns.recipes",
|
||||
"refresh_community_interfaces_busy": "Päivitetään…",
|
||||
"network_starting": "Yhdistetään mesh-verkkoon…",
|
||||
"network_degraded": "Mesh-verkko ei ole käytettävissä. Sovellus pysyy käynnissä, jotta voit korjata liittymiä ilman tietojen tyhjennystä.",
|
||||
"recover_network": "Yritä verkkoa uudelleen",
|
||||
"network_starting": "Käynnistetään RNS…",
|
||||
"network_degraded": "RNS ei ole käytettävissä. Sovellus pysyy käynnissä, jotta voit diagnosoida ja palauttaa ilman tietojen tyhjennystä.",
|
||||
"recover_network": "Yritä RNS uudelleen",
|
||||
"open_settings": "Avaa asetukset",
|
||||
"open_interfaces": "Avaa liittymät",
|
||||
"network_recovered": "Verkkopinon palautus onnistui",
|
||||
"network_recover_failed": "Verkkopinoa ei voitu palauttaa. Tarkista liittymät ja yritä uudelleen.",
|
||||
"network_recovered": "RNS palautettu",
|
||||
"network_recover_failed": "RNS:ää ei voitu palauttaa. Tarkista virhetiedot, yritä uudelleen tai tarkista asetukset ja liittymät.",
|
||||
"battery_unavailable": "Akun tila ei ole saatavilla",
|
||||
"battery_level_title": "Akku {percent}",
|
||||
"battery_charging_title": "Lataa {percent}",
|
||||
|
|
|
|||
|
|
@ -491,12 +491,13 @@
|
|||
"rpc_key_hide": "Masquer la clé RPC",
|
||||
"refresh_community_interfaces": "Actualiser depuis directory.rns.recipes",
|
||||
"refresh_community_interfaces_busy": "Actualisation…",
|
||||
"network_starting": "Connexion au mesh…",
|
||||
"network_degraded": "Réseau mesh indisponible. L'application reste ouverte pour réparer les interfaces sans effacer les données.",
|
||||
"recover_network": "Réessayer le réseau",
|
||||
"network_starting": "Démarrage de RNS…",
|
||||
"network_degraded": "RNS indisponible. L'application reste ouverte pour diagnostiquer et récupérer sans effacer les données.",
|
||||
"recover_network": "Réessayer RNS",
|
||||
"open_settings": "Ouvrir les paramètres",
|
||||
"open_interfaces": "Ouvrir les interfaces",
|
||||
"network_recovered": "Pile réseau rétablie",
|
||||
"network_recover_failed": "Impossible de rétablir la pile réseau. Vérifiez les interfaces et réessayez.",
|
||||
"network_recovered": "RNS rétabli",
|
||||
"network_recover_failed": "Impossible de rétablir RNS. Vérifiez l'erreur, puis réessayez ou consultez les paramètres et les interfaces.",
|
||||
"battery_unavailable": "État de la batterie indisponible",
|
||||
"battery_level_title": "Batterie {percent}",
|
||||
"battery_charging_title": "En charge {percent}",
|
||||
|
|
|
|||
|
|
@ -491,12 +491,13 @@
|
|||
"rpc_key_hide": "Nascondi chiave RPC",
|
||||
"refresh_community_interfaces": "Aggiorna da directory.rns.recipes",
|
||||
"refresh_community_interfaces_busy": "Aggiornamento…",
|
||||
"network_starting": "Connessione alla mesh…",
|
||||
"network_degraded": "Rete mesh non disponibile. L'app resta attiva così puoi riparare le interfacce senza cancellare i dati.",
|
||||
"recover_network": "Riprova rete",
|
||||
"network_starting": "Avvio di RNS…",
|
||||
"network_degraded": "RNS non disponibile. L'app resta attiva così puoi diagnosticare e recuperare senza cancellare i dati.",
|
||||
"recover_network": "Riprova RNS",
|
||||
"open_settings": "Apri impostazioni",
|
||||
"open_interfaces": "Apri interfacce",
|
||||
"network_recovered": "Stack di rete ripristinato",
|
||||
"network_recover_failed": "Impossibile ripristinare lo stack di rete. Controlla le interfacce e riprova.",
|
||||
"network_recovered": "RNS ripristinato",
|
||||
"network_recover_failed": "Impossibile ripristinare RNS. Controlla l'errore, poi riprova o rivedi impostazioni e interfacce.",
|
||||
"battery_unavailable": "Stato batteria non disponibile",
|
||||
"battery_level_title": "Batteria {percent}",
|
||||
"battery_charging_title": "In carica {percent}",
|
||||
|
|
|
|||
|
|
@ -491,12 +491,13 @@
|
|||
"rpc_key_hide": "RPC-sleutel verbergen",
|
||||
"refresh_community_interfaces": "Vernieuwen vanaf directory.rns.recipes",
|
||||
"refresh_community_interfaces_busy": "Bezig met vernieuwen…",
|
||||
"network_starting": "Verbinden met het mesh…",
|
||||
"network_degraded": "Mesh-netwerk niet beschikbaar. De app blijft draaien zodat je interfaces kunt herstellen zonder gegevens te wissen.",
|
||||
"recover_network": "Netwerk opnieuw proberen",
|
||||
"network_starting": "RNS starten…",
|
||||
"network_degraded": "RNS niet beschikbaar. De app blijft draaien zodat je kunt diagnosticeren en herstellen zonder gegevens te wissen.",
|
||||
"recover_network": "RNS opnieuw proberen",
|
||||
"open_settings": "Instellingen openen",
|
||||
"open_interfaces": "Interfaces openen",
|
||||
"network_recovered": "Netwerkstack hersteld",
|
||||
"network_recover_failed": "Netwerkstack kon niet worden hersteld. Controleer interfaces en probeer opnieuw.",
|
||||
"network_recovered": "RNS hersteld",
|
||||
"network_recover_failed": "RNS kon niet worden hersteld. Controleer de fout, probeer opnieuw of bekijk instellingen en interfaces.",
|
||||
"battery_unavailable": "Batterijstatus niet beschikbaar",
|
||||
"battery_level_title": "Batterij {percent}",
|
||||
"battery_charging_title": "Opladen {percent}",
|
||||
|
|
|
|||
|
|
@ -491,12 +491,13 @@
|
|||
"rpc_key_hide": "Скрыть RPC-ключ",
|
||||
"refresh_community_interfaces": "Обновить из directory.rns.recipes",
|
||||
"refresh_community_interfaces_busy": "Обновление…",
|
||||
"network_starting": "Подключение к mesh…",
|
||||
"network_degraded": "Mesh-сеть недоступна. Приложение продолжает работать, чтобы вы могли исправить интерфейсы без удаления данных.",
|
||||
"recover_network": "Повторить сеть",
|
||||
"network_starting": "Запуск RNS…",
|
||||
"network_degraded": "RNS недоступен. Приложение продолжает работать, чтобы вы могли диагностировать и восстановить работу без удаления данных.",
|
||||
"recover_network": "Повторить RNS",
|
||||
"open_settings": "Открыть настройки",
|
||||
"open_interfaces": "Открыть интерфейсы",
|
||||
"network_recovered": "Сетевой стек восстановлен",
|
||||
"network_recover_failed": "Не удалось восстановить сетевой стек. Проверьте интерфейсы и попробуйте снова.",
|
||||
"network_recovered": "RNS восстановлен",
|
||||
"network_recover_failed": "Не удалось восстановить RNS. Проверьте сведения об ошибке, затем повторите попытку или проверьте настройки и интерфейсы.",
|
||||
"battery_unavailable": "Состояние батареи недоступно",
|
||||
"battery_level_title": "Батарея {percent}",
|
||||
"battery_charging_title": "Зарядка {percent}",
|
||||
|
|
|
|||
|
|
@ -491,12 +491,13 @@
|
|||
"rpc_key_hide": "隐藏 RPC 密钥",
|
||||
"refresh_community_interfaces": "从 directory.rns.recipes 刷新",
|
||||
"refresh_community_interfaces_busy": "正在刷新…",
|
||||
"network_starting": "正在连接 mesh…",
|
||||
"network_degraded": "Mesh 网络不可用。应用仍在运行,便于修复接口而无需清除数据。",
|
||||
"recover_network": "重试网络",
|
||||
"network_starting": "正在启动 RNS…",
|
||||
"network_degraded": "RNS 不可用。应用仍在运行,便于诊断和恢复而无需清除数据。",
|
||||
"recover_network": "重试 RNS",
|
||||
"open_settings": "打开设置",
|
||||
"open_interfaces": "打开接口",
|
||||
"network_recovered": "网络栈已恢复",
|
||||
"network_recover_failed": "无法恢复网络栈。请检查接口后重试。",
|
||||
"network_recovered": "RNS 已恢复",
|
||||
"network_recover_failed": "无法恢复 RNS。请查看错误详情后重试,或检查设置与接口。",
|
||||
"battery_unavailable": "无法获取电池状态",
|
||||
"battery_level_title": "电池 {percent}",
|
||||
"battery_charging_title": "充电中 {percent}",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { registerCoreContributions } from "./js/registries/registerCoreContribut
|
|||
import { installWsEventBridge } from "./js/registries/wsEventBridge.js";
|
||||
import { pluginHost } from "./js/plugins/PluginHost.js";
|
||||
import GlobalState from "./js/GlobalState.js";
|
||||
import { recoveryRouteForNetworkError } from "./js/networkRecovery.js";
|
||||
import "./js/HeapMonitor.js";
|
||||
|
||||
registerCoreContributions();
|
||||
|
|
@ -360,7 +361,7 @@ const networkReady = await waitForNetworkReady({
|
|||
onErrorState: markBootSplashError,
|
||||
onDegraded: (error) => {
|
||||
GlobalState.networkDegraded = true;
|
||||
GlobalState.networkDegradedError = error || "Mesh network unavailable";
|
||||
GlobalState.networkDegradedError = error || "RNS unavailable";
|
||||
GlobalState.networkStarting = false;
|
||||
GlobalState.networkReady = false;
|
||||
},
|
||||
|
|
@ -491,7 +492,7 @@ if (networkReady) {
|
|||
onLine: () => {},
|
||||
onDegraded: (error) => {
|
||||
GlobalState.networkDegraded = true;
|
||||
GlobalState.networkDegradedError = error || "Mesh network unavailable";
|
||||
GlobalState.networkDegradedError = error || "RNS unavailable";
|
||||
GlobalState.networkStarting = false;
|
||||
GlobalState.networkReady = false;
|
||||
},
|
||||
|
|
@ -510,16 +511,18 @@ if (networkReady) {
|
|||
GlobalState.networkStarting = false;
|
||||
GlobalState.networkReady = false;
|
||||
GlobalState.networkDegraded = true;
|
||||
GlobalState.networkDegradedError =
|
||||
GlobalState.networkDegradedError || "Mesh network startup timed out";
|
||||
GlobalState.networkDegradedError = GlobalState.networkDegradedError || "RNS startup timed out";
|
||||
}
|
||||
});
|
||||
}
|
||||
if (GlobalState.networkDegraded) {
|
||||
try {
|
||||
router.replace({ name: "interfaces" });
|
||||
} catch {
|
||||
// Route may not exist yet during early boot, but the banner still guides the user.
|
||||
const recoveryRoute = recoveryRouteForNetworkError(GlobalState.networkDegradedError);
|
||||
if (recoveryRoute) {
|
||||
try {
|
||||
router.replace({ name: recoveryRoute });
|
||||
} catch {
|
||||
// Route may not exist yet during early boot, but the banner still guides the user.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Options:
|
|||
--api-level N Android API level for wheel tag (default: 24)
|
||||
--pycodec2-version V pycodec2 version to build (default: 4.1.1)
|
||||
--numpy-version V NumPy version used during pycodec2 build (default: 1.26.2)
|
||||
--lxst-version V LXST wheel version for metadata patch (default: 0.4.8)
|
||||
--lxst-version V LXST wheel version for metadata patch (default: 0.5.0)
|
||||
--bleak-version V bleak pure-python wheel version to vendor (default: 3.0.2)
|
||||
--rns-version V rns wheel version to patch (default: 1.4.0)
|
||||
--no-lxst-patch Skip LXST metadata patch
|
||||
|
|
@ -58,7 +58,7 @@ API_LEVEL="24"
|
|||
PYCODEC2_VERSION="4.1.1"
|
||||
LIBCODEC2_VERSION="1.2.0"
|
||||
NUMPY_VERSION="1.26.2"
|
||||
LXST_VERSION="0.4.8"
|
||||
LXST_VERSION="0.5.0"
|
||||
BLEAK_VERSION="3.0.2"
|
||||
RNS_VERSION="1.4.0"
|
||||
PATCH_LXST="1"
|
||||
|
|
@ -784,6 +784,9 @@ with zipfile.ZipFile(src, "r") as zin, zipfile.ZipFile(dst, "w", compression=zip
|
|||
text = data.decode("utf-8")
|
||||
text = text.replace("Requires-Dist: numpy>=2.3.4", "Requires-Dist: numpy==${NUMPY_VERSION}")
|
||||
text = text.replace("Requires-Dist: cffi==1.15.1", "Requires-Dist: cffi>=1.15.1")
|
||||
# LXST 0.5.0 pins cffi>=2.0.0. Chaquopy Android may resolve older
|
||||
# cffi wheels, so keep the lower bound flexible like prior releases.
|
||||
text = text.replace("Requires-Dist: cffi>=2.0.0", "Requires-Dist: cffi>=1.15.1")
|
||||
data = text.encode("utf-8")
|
||||
zout.writestr(item, data)
|
||||
PY
|
||||
|
|
|
|||
53
tests/backend/test_lxst_profiles_compat.py
Normal file
53
tests/backend/test_lxst_profiles_compat.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Compat for LXST Profiles call modes on Android 0.4.x vs duplex builds."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
|
||||
|
||||
|
||||
def test_legacy_profiles_without_modes_falls_back_to_full_duplex():
|
||||
legacy = SimpleNamespace(
|
||||
BANDWIDTH_LOW=0x30,
|
||||
# No MODE_* or available_modes on Android lxst 0.4.8
|
||||
)
|
||||
assert lxst_modes.duplex_modes_supported(legacy) is False
|
||||
assert lxst_modes.available_modes(legacy) == [lxst_modes.MODE_FULL_DUPLEX]
|
||||
assert lxst_modes.default_mode(legacy) == lxst_modes.MODE_FULL_DUPLEX
|
||||
assert lxst_modes.mode_name(lxst_modes.MODE_FULL_DUPLEX, legacy) == "Full Duplex"
|
||||
assert lxst_modes.mode_abbreviation(lxst_modes.MODE_HALF_DUPLEX, legacy) == "HDX"
|
||||
|
||||
|
||||
def test_native_profiles_modes_are_forwarded():
|
||||
native = SimpleNamespace(
|
||||
MODE_FULL_DUPLEX=1,
|
||||
MODE_HALF_DUPLEX=2,
|
||||
DEFAULT_MODE=1,
|
||||
available_modes=lambda: [1, 2],
|
||||
mode_name=lambda mid: f"mode-{mid}",
|
||||
mode_abbrevation=lambda mid: f"m{mid}",
|
||||
)
|
||||
assert lxst_modes.duplex_modes_supported(native) is True
|
||||
assert lxst_modes.available_modes(native) == [1, 2]
|
||||
assert lxst_modes.mode_name(2, native) == "mode-2"
|
||||
assert lxst_modes.mode_abbreviation(1, native) == "m1"
|
||||
|
||||
|
||||
def test_telephone_manager_resolves_modes_on_legacy_profiles(monkeypatch):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from meshchatx.src.backend.telephone_manager import TelephoneManager
|
||||
|
||||
legacy = SimpleNamespace()
|
||||
monkeypatch.setattr(
|
||||
"meshchatx.src.backend.lxst_profiles_compat.import_profiles",
|
||||
lambda: legacy,
|
||||
)
|
||||
tm = TelephoneManager(identity=MagicMock())
|
||||
assert tm.resolve_call_mode_id(None) == lxst_modes.MODE_FULL_DUPLEX
|
||||
assert (
|
||||
tm.resolve_call_mode_id(lxst_modes.MODE_HALF_DUPLEX)
|
||||
== lxst_modes.MODE_FULL_DUPLEX
|
||||
)
|
||||
assert tm.is_half_duplex() is False
|
||||
|
|
@ -299,6 +299,8 @@ describe("behavior contracts: phased startup and early UI mount", () => {
|
|||
const banners = readSource("meshchatx/src/frontend/components/layout/AppShellBanners.vue");
|
||||
expect(banners).toContain("showNetworkStarting");
|
||||
expect(banners).toContain("networkStartingLabel");
|
||||
expect(banners).toContain("open-settings");
|
||||
expect(banners).toContain("open-interfaces");
|
||||
});
|
||||
|
||||
it("backend publishes ui_ready early and defers secondary identity services", () => {
|
||||
|
|
|
|||
|
|
@ -47,10 +47,7 @@
|
|||
{
|
||||
"id": "map",
|
||||
"shell": "meshchatx/src/frontend/components/map/MapPage.vue",
|
||||
"allowed_child_dirs": [
|
||||
"meshchatx/src/frontend/components/map/internal",
|
||||
"meshchatx/src/frontend/js"
|
||||
],
|
||||
"allowed_child_dirs": ["meshchatx/src/frontend/components/map/internal", "meshchatx/src/frontend/js"],
|
||||
"required_children": [
|
||||
"meshchatx/src/frontend/components/map/internal/MapClusterPanel.vue",
|
||||
"meshchatx/src/frontend/components/map/internal/MapMarkerPanel.vue",
|
||||
|
|
@ -161,12 +158,7 @@
|
|||
"meshchatx/src/frontend/components/layout/AppIdentitySwitchOverlay.vue",
|
||||
"meshchatx/src/frontend/components/call/CallOverlay.vue"
|
||||
],
|
||||
"required_shell_imports": [
|
||||
"AppShellBanners",
|
||||
"AppIdentitySwitchOverlay",
|
||||
"CallOverlay",
|
||||
"listNavItems"
|
||||
]
|
||||
"required_shell_imports": ["AppShellBanners", "AppIdentitySwitchOverlay", "CallOverlay", "listNavItems"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
22
tests/frontend/networkRecovery.test.js
Normal file
22
tests/frontend/networkRecovery.test.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
isLikelyInterfaceRecoveryError,
|
||||
recoveryRouteForNetworkError,
|
||||
} from "../../meshchatx/src/frontend/js/networkRecovery.js";
|
||||
|
||||
describe("networkRecovery", () => {
|
||||
it("does not treat telephony or generic errors as interface recovery", () => {
|
||||
expect(isLikelyInterfaceRecoveryError("type object 'Profiles' has no attribute 'available_modes'")).toBe(false);
|
||||
expect(recoveryRouteForNetworkError("Telephone init failed")).toBeNull();
|
||||
expect(recoveryRouteForNetworkError("")).toBeNull();
|
||||
});
|
||||
|
||||
it("routes interface-shaped errors to the interfaces page", () => {
|
||||
expect(isLikelyInterfaceRecoveryError("RNode interface failed to open")).toBe(true);
|
||||
expect(recoveryRouteForNetworkError("AutoInterface bind error")).toBe("interfaces");
|
||||
expect(recoveryRouteForNetworkError("I2P interface unavailable")).toBe("interfaces");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue