feat: Enhance elapsed time handling in messages

- Updated the message handling logic to format elapsed time more accurately, displaying "Nms" when the device clock is valid or "Sync Device Clock" when invalid.
- Introduced a new utility function `format_elapsed_display` to centralize elapsed time formatting, improving code maintainability.
- Modified configuration examples to clarify the usage of the {elapsed} placeholder.
- Added translations for the "Sync Device Clock" message in multiple languages to support internationalization.
This commit is contained in:
agessaman
2026-01-19 09:55:31 -08:00
parent abe8e57ae3
commit c53bfce4b7
14 changed files with 86 additions and 36 deletions
+1 -1
View File
@@ -245,7 +245,7 @@ long_jokes = false
# {path} - Message routing path (e.g., "01,5f (2 hops)")
# {path_distance} - Total distance between all hops in path with locations (e.g., "123.4km (3 segs, 1 no-loc)")
# {firstlast_distance} - Distance between first and last repeater in path (e.g., "45.6km" or empty if locations missing)
# {elapsed} - Message elapsed time
# {elapsed} - Elapsed time (e.g. 1234ms) or "Sync Device Clock" when device clock is invalid (use {elapsed} only; do not append ms)
#
# To add newlines in responses, use \n (single backslash + n):
# Example: test = "Line 1\nLine 2\nLine 3"
+1 -1
View File
@@ -183,7 +183,7 @@ admin_commands = repeater,webviewer
# {path} - Message routing path (e.g., "01,5f (2 hops)")
# {path_distance} - Total distance between all hops in path with locations (e.g., "123.4km (3 segs, 1 no-loc)")
# {firstlast_distance} - Distance between first and last repeater in path (e.g., "45.6km" or empty if locations missing)
# {elapsed} - Message elapsed time
# {elapsed} - Elapsed time (e.g. 1234ms) or "Sync Device Clock" when device clock is invalid (use {elapsed} only; do not append ms)
test = "ack @[{sender}]{phrase_part} | {connection_info} | Received at: {timestamp}"
ping = "Pong!"
+4 -21
View File
@@ -11,6 +11,7 @@ import pytz
import re
from ..models import MeshMessage
from ..security_utils import validate_pubkey_format
from ..utils import format_elapsed_display
class BaseCommand(ABC):
@@ -642,27 +643,9 @@ class BaseCommand(ABC):
return "Unknown"
def format_elapsed(self, message: MeshMessage) -> str:
"""Format message timestamp for display"""
if message.timestamp and message.timestamp != 'unknown':
try:
from datetime import datetime,UTC
el = round((datetime.now(UTC).timestamp()-message.timestamp)*1000)
return f"{el}ms"
except:
return str(message.timestamp)
else:
return "Unknown"
def format_elapsed(self, message: MeshMessage) -> str:
"""Format message timestamp for display"""
if message.timestamp and message.timestamp != 'unknown':
try:
from datetime import datetime,UTC
el = round((datetime.now(UTC).timestamp()-message.timestamp)*1000)
return f"{el}ms"
except:
return str(message.timestamp)
else:
return "Unknown"
"""Format message elapsed for display. Uses 'Sync Device Clock' when device clock is invalid."""
translator = getattr(self.bot, 'translator', None)
return format_elapsed_display(message.timestamp, translator)
def format_response(self, message: MeshMessage, response_format: str) -> str:
"""Format a response string with message data"""
+11 -12
View File
@@ -14,7 +14,7 @@ from meshcore import EventType
from .models import MeshMessage
from .enums import PayloadType, PayloadVersion, RouteType, AdvertFlags, DeviceRole
from .utils import calculate_packet_hash
from .utils import calculate_packet_hash, format_elapsed_display
from .security_utils import sanitize_input
@@ -303,17 +303,11 @@ class MessageHandler:
message_content = payload.get('text', '')
message_content = sanitize_input(message_content, max_length=None, strip_controls=True)
# Format timestamp
if timestamp and timestamp != 'unknown':
try:
from datetime import datetime,UTC
dt = datetime.fromtimestamp(message.timestamp)
elapsed_str = round((datetime.now(UTC).timestamp()-message.timestamp)*1000)
except:
elapsed_str = "Unknown"
else:
elapsed_str = "Unknown"
# Elapsed: "Nms" when device clock is valid, or "Sync Device Clock" when
# invalid (e.g. T-Deck before GPS sync: 0, future, or far in the past).
translator = getattr(self.bot, 'translator', None)
elapsed_str = format_elapsed_display(timestamp, translator)
# Convert to our message format
message = MeshMessage(
content=message_content,
@@ -1611,6 +1605,10 @@ class MessageHandler:
self.logger.debug(f"Found full public key for {sender_id}: {sender_pubkey[:16]}...")
break
# Elapsed: "Nms" when device clock is valid, or "Sync Device Clock" when invalid.
_translator = getattr(self.bot, 'translator', None)
_elapsed = format_elapsed_display(payload.get('sender_timestamp'), _translator)
# Convert to our message format
message = MeshMessage(
content=message_content, # Use the extracted message content
@@ -1622,6 +1620,7 @@ class MessageHandler:
rssi=rssi,
hops=hops,
path=path_string, # Use the path information extracted from RF data
elapsed=_elapsed,
is_dm=False
)
+42 -1
View File
@@ -1698,6 +1698,42 @@ def _get_node_location_from_db(bot: Any, node_id: str) -> Optional[Tuple[float,
return None
# Maximum plausible elapsed ms (5 minutes) for device clock validation.
# Values above indicate device time is far in the past (e.g. epoch); negative = in the future.
_ELAPSED_MS_MAX = 5 * 60 * 1000 # 5 minutes in milliseconds
def format_elapsed_display(ts: Any, translator: Any = None) -> str:
"""Format elapsed time from sender timestamp for {elapsed} placeholder.
Returns "Nms" when valid, or the i18n "Sync Device Clock" when the device
clock is invalid (e.g. T-Deck before GPS sync: 0, future, or far in the past).
Args:
ts: Sender timestamp (int, float, None, or 'unknown').
translator: Bot translator for i18n; uses "Sync Device Clock" if None.
Returns:
str: e.g. "1234ms" or translated "Sync Device Clock".
"""
def _sync_str() -> str:
if translator:
return translator.translate('elapsed.sync_device_clock')
return "Sync Device Clock"
if ts is None or ts == 'unknown':
return _sync_str()
try:
ts_f = float(ts)
except (TypeError, ValueError):
return _sync_str()
from datetime import UTC, datetime
elapsed_ms = (datetime.now(UTC).timestamp() - ts_f) * 1000
if elapsed_ms < 0 or elapsed_ms > _ELAPSED_MS_MAX:
return _sync_str()
return f"{round(elapsed_ms)}ms"
def format_keyword_response_with_placeholders(
response_format: str,
message: Any,
@@ -1728,7 +1764,12 @@ def format_keyword_response_with_placeholders(
replacements['path'] = message.path or "Unknown"
replacements['snr'] = message.snr or "Unknown"
replacements['rssi'] = message.rssi or "Unknown"
replacements['elapsed'] = message.elapsed or "Unknown"
# Compute elapsed from message.timestamp (same as TestCommand) so it's available
# for all keywords. Using message.elapsed would miss when it's unset on some paths.
_translator = getattr(bot, 'translator', None)
replacements['elapsed'] = format_elapsed_display(
getattr(message, 'timestamp', None), _translator
)
# Build connection_info
routing_info = message.path or "Unknown routing"
+3
View File
@@ -962,6 +962,9 @@
"description": "Verfügbare Befehle auflisten"
}
},
"elapsed": {
"sync_device_clock": "Geräteuhr synchronisieren"
},
"errors": {
"rate_limited": "Rate-limitiert. Warte {seconds:.1f} Sekunden",
"cooldown": "Befehl '{command}' hat Wartezeit. Warte {seconds} Sekunden.",
+3
View File
@@ -942,6 +942,9 @@
"description": "List available commands"
}
},
"elapsed": {
"sync_device_clock": "Sync Device Clock"
},
"errors": {
"rate_limited": "Rate limited. Wait {seconds:.1f} seconds",
"cooldown": "Command '{command}' is on cooldown. Wait {seconds} seconds.",
+3
View File
@@ -992,6 +992,9 @@
"description": "List available commands"
}
},
"elapsed": {
"sync_device_clock": "Sync Device Clock"
},
"errors": {
"rate_limited": "Rate limited. Wait {seconds:.1f} seconds",
"cooldown": "Command '{command}' is on cooldown. Wait {seconds} seconds.",
+3
View File
@@ -814,6 +814,9 @@
"description": "Listar comandos disponibles"
}
},
"elapsed": {
"sync_device_clock": "Sincronizar reloj del dispositivo"
},
"errors": {
"rate_limited": "Límite de solicitudes. Espera {seconds:.1f} segundos",
"cooldown": "El comando '{command}' está en espera. Espera {seconds} segundos.",
+3
View File
@@ -1016,6 +1016,9 @@
"description": "Lister les commandes disponibles"
}
},
"elapsed": {
"sync_device_clock": "Synchroniser l'horloge du dispositif"
},
"errors": {
"rate_limited": "Limite de débit atteinte. Attendez {seconds:.1f} secondes",
"cooldown": "La commande '{command}' est en délai d'attente. Attendez {seconds} secondes.",
+3
View File
@@ -933,6 +933,9 @@
"description": "Lister commandes disponibles"
}
},
"elapsed": {
"sync_device_clock": "Synchroniser l'horloge du dispositif"
},
"errors": {
"rate_limited": "Limite atteinte. Attendez {seconds:.1f} secondes",
"cooldown": "Commande '{command}' en attente. Attendez {seconds} secondes.",
+3
View File
@@ -942,6 +942,9 @@
"description": "Lijst beschikbare commando's"
}
},
"elapsed": {
"sync_device_clock": "Synchroniseer apparaatklok"
},
"errors": {
"rate_limited": "Rate limited. Wacht {seconds:.1f} seconden",
"cooldown": "Commando '{command}' is in cooldown. Wacht {seconds} seconden.",
+3
View File
@@ -939,6 +939,9 @@
"description": "Listar comandos disponíveis"
}
},
"elapsed": {
"sync_device_clock": "Sincronizar relógio do dispositivo"
},
"errors": {
"rate_limited": "Limite de taxa atingido. Aguarda {seconds:.1f} segundos",
"cooldown": "Comando '{command}' em espera. Aguarda {seconds} segundos.",
+3
View File
@@ -938,6 +938,9 @@
"description": "Listar comandos disponíveis"
}
},
"elapsed": {
"sync_device_clock": "Sincronizar relógio do dispositivo"
},
"errors": {
"rate_limited": "Limitado por taxa. Aguarde {seconds:.1f} segundos",
"cooldown": "Comando '{command}' em espera. Aguarde {seconds} segundos.",