2025-04-15 20:19:33 +02:00
|
|
|
|
# Reticulum License
|
2022-04-01 17:18:18 +02:00
|
|
|
|
#
|
2025-04-15 20:19:33 +02:00
|
|
|
|
# Copyright (c) 2016-2025 Mark Qvist
|
2022-04-01 17:18:18 +02:00
|
|
|
|
#
|
|
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
|
|
|
|
# in the Software without restriction, including without limitation the rights
|
|
|
|
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
|
|
|
|
# furnished to do so, subject to the following conditions:
|
|
|
|
|
|
#
|
2025-04-15 20:19:33 +02:00
|
|
|
|
# - The Software shall not be used in any kind of system which includes amongst
|
|
|
|
|
|
# its functions the ability to purposefully do harm to human beings.
|
|
|
|
|
|
#
|
|
|
|
|
|
# - The Software shall not be used, directly or indirectly, in the creation of
|
|
|
|
|
|
# an artificial intelligence, machine learning or language model training
|
|
|
|
|
|
# dataset, including but not limited to any use that contributes to the
|
|
|
|
|
|
# training or development of such a model or algorithm.
|
|
|
|
|
|
#
|
|
|
|
|
|
# - The above copyright notice and this permission notice shall be included in
|
|
|
|
|
|
# all copies or substantial portions of the Software.
|
2022-04-01 17:18:18 +02:00
|
|
|
|
#
|
|
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
|
|
# SOFTWARE.
|
|
|
|
|
|
|
2016-06-03 19:02:02 +02:00
|
|
|
|
import os
|
2018-04-16 22:04:28 +02:00
|
|
|
|
import sys
|
2016-06-03 19:02:02 +02:00
|
|
|
|
import glob
|
2018-03-19 16:39:08 +01:00
|
|
|
|
import time
|
2025-01-12 18:51:27 +01:00
|
|
|
|
import datetime
|
2019-11-10 13:56:04 +01:00
|
|
|
|
import random
|
2021-05-03 20:24:44 +02:00
|
|
|
|
import threading
|
2026-08-19 23:16:52 -06:00
|
|
|
|
import math
|
|
|
|
|
|
import bisect
|
2016-06-03 19:02:02 +02:00
|
|
|
|
|
2026-08-24 10:52:47 +02:00
|
|
|
|
from ._version import __version__
|
2026-08-19 17:34:35 -06:00
|
|
|
|
from collections import deque
|
2026-08-23 02:27:53 -06:00
|
|
|
|
from functools import partial, wraps
|
2026-08-19 17:34:35 -06:00
|
|
|
|
|
2024-12-02 14:20:34 +01:00
|
|
|
|
py_modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
|
|
|
|
|
pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
|
2026-08-27 14:56:47 +02:00
|
|
|
|
so_modules = glob.glob(os.path.dirname(__file__)+"/*.so")
|
|
|
|
|
|
pyd_modules = glob.glob(os.path.dirname(__file__)+"/*.pyd")
|
|
|
|
|
|
modules = py_modules+pyc_modules+so_modules+pyd_modules
|
|
|
|
|
|
__all__ = list(set([os.path.basename(f).split(".")[0] for f in modules if not os.path.basename(f).split(".")[0] == "__init__"]))
|
2018-03-19 16:39:08 +01:00
|
|
|
|
|
2025-04-08 15:23:44 +02:00
|
|
|
|
import importlib.util
|
2026-08-27 23:37:43 +02:00
|
|
|
|
try: from ._buildinfo import compiled
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
if importlib.util.find_spec("cython"): import cython; compiled = cython.compiled
|
|
|
|
|
|
else: compiled = False
|
2025-02-23 14:45:24 +01:00
|
|
|
|
|
2025-02-23 15:18:12 +01:00
|
|
|
|
LOG_NONE = -1
|
2018-03-19 16:39:08 +01:00
|
|
|
|
LOG_CRITICAL = 0
|
|
|
|
|
|
LOG_ERROR = 1
|
|
|
|
|
|
LOG_WARNING = 2
|
|
|
|
|
|
LOG_NOTICE = 3
|
|
|
|
|
|
LOG_INFO = 4
|
|
|
|
|
|
LOG_VERBOSE = 5
|
|
|
|
|
|
LOG_DEBUG = 6
|
2026-07-18 15:07:06 +02:00
|
|
|
|
LOG_PATHING = 7
|
|
|
|
|
|
LOG_EXTREME = 8
|
2018-03-19 16:39:08 +01:00
|
|
|
|
|
2020-08-13 12:15:56 +02:00
|
|
|
|
LOG_STDOUT = 0x91
|
2018-03-19 16:39:08 +01:00
|
|
|
|
LOG_FILE = 0x92
|
2024-12-08 14:27:17 +01:00
|
|
|
|
LOG_CALLBACK = 0x93
|
2018-03-19 16:39:08 +01:00
|
|
|
|
|
2026-08-24 10:52:47 +02:00
|
|
|
|
LOG_MAXSIZE = 5*1024*1024
|
2021-10-08 19:23:10 +02:00
|
|
|
|
|
2022-11-01 20:26:55 +01:00
|
|
|
|
loglevel = LOG_NOTICE
|
|
|
|
|
|
logfile = None
|
|
|
|
|
|
logdest = LOG_STDOUT
|
2024-12-08 14:27:17 +01:00
|
|
|
|
logcall = None
|
2026-05-28 16:52:39 +02:00
|
|
|
|
logtimestamps = True
|
2022-11-01 20:26:55 +01:00
|
|
|
|
logtimefmt = "%Y-%m-%d %H:%M:%S"
|
2025-01-12 18:51:27 +01:00
|
|
|
|
logtimefmt_p = "%H:%M:%S.%f"
|
2022-11-01 20:26:55 +01:00
|
|
|
|
compact_log_fmt = False
|
2018-03-19 16:39:08 +01:00
|
|
|
|
|
2022-04-27 19:00:09 +02:00
|
|
|
|
instance_random = random.Random()
|
|
|
|
|
|
instance_random.seed(os.urandom(10))
|
2019-11-10 13:56:04 +01:00
|
|
|
|
|
2021-05-03 20:24:44 +02:00
|
|
|
|
_always_override_destination = False
|
|
|
|
|
|
|
2026-08-24 10:52:47 +02:00
|
|
|
|
logging_lock = threading.Lock()
|
|
|
|
|
|
|
2018-03-19 16:39:08 +01:00
|
|
|
|
def loglevelname(level):
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if (level == LOG_CRITICAL): return "[Critical]"
|
|
|
|
|
|
if (level == LOG_ERROR): return "[Error] "
|
|
|
|
|
|
if (level == LOG_WARNING): return "[Warning] "
|
|
|
|
|
|
if (level == LOG_NOTICE): return "[Notice] "
|
|
|
|
|
|
if (level == LOG_INFO): return "[Info] "
|
|
|
|
|
|
if (level == LOG_VERBOSE): return "[Verbose] "
|
|
|
|
|
|
if (level == LOG_DEBUG): return "[Debug] "
|
2026-07-18 15:07:06 +02:00
|
|
|
|
if (level == LOG_PATHING): return "[Pathing] "
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if (level == LOG_EXTREME): return "[Extra] "
|
2020-08-13 12:15:56 +02:00
|
|
|
|
|
|
|
|
|
|
return "Unknown"
|
2018-03-19 16:39:08 +01:00
|
|
|
|
|
2021-08-19 14:10:37 +02:00
|
|
|
|
def version():
|
|
|
|
|
|
return __version__
|
|
|
|
|
|
|
2021-12-01 11:40:44 +01:00
|
|
|
|
def host_os():
|
|
|
|
|
|
from .vendor.platformutils import get_platform
|
|
|
|
|
|
return get_platform()
|
|
|
|
|
|
|
2022-04-20 10:40:51 +02:00
|
|
|
|
def timestamp_str(time_s):
|
|
|
|
|
|
timestamp = time.localtime(time_s)
|
|
|
|
|
|
return time.strftime(logtimefmt, timestamp)
|
|
|
|
|
|
|
2025-01-12 18:51:27 +01:00
|
|
|
|
def precise_timestamp_str(time_s):
|
|
|
|
|
|
return datetime.datetime.now().strftime(logtimefmt_p)[:-3]
|
|
|
|
|
|
|
2026-05-06 01:03:43 +02:00
|
|
|
|
def sl(level=3): return loglevel >= level
|
2025-01-12 18:51:27 +01:00
|
|
|
|
def log(msg, level=3, _override_destination = False, pt=False):
|
2025-02-23 15:18:12 +01:00
|
|
|
|
if loglevel == LOG_NONE: return
|
2026-08-24 10:52:47 +02:00
|
|
|
|
global _always_override_destination, compact_log_fmt
|
2023-06-05 17:31:52 -04:00
|
|
|
|
msg = str(msg)
|
2020-08-13 12:15:56 +02:00
|
|
|
|
if loglevel >= level:
|
2026-08-24 10:52:47 +02:00
|
|
|
|
if pt: logstring = "["+precise_timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
|
|
|
|
|
|
else:
|
|
|
|
|
|
if not compact_log_fmt: logstring = ("["+timestamp_str(time.time())+"] " if logtimestamps else "")+loglevelname(level)+" "+msg
|
|
|
|
|
|
else: logstring = ("["+timestamp_str(time.time())+"] " if logtimestamps else "")+msg
|
|
|
|
|
|
|
|
|
|
|
|
with logging_lock:
|
|
|
|
|
|
if (logdest == LOG_STDOUT or _always_override_destination or _override_destination):
|
|
|
|
|
|
if not threading.main_thread().is_alive(): return
|
|
|
|
|
|
else:
|
|
|
|
|
|
try: print(logstring)
|
|
|
|
|
|
except: pass
|
|
|
|
|
|
|
|
|
|
|
|
elif (logdest == LOG_FILE and logfile != None):
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(logfile, "a") as file: file.write(logstring+"\n")
|
|
|
|
|
|
if os.path.getsize(logfile) > LOG_MAXSIZE:
|
|
|
|
|
|
prevfile = logfile+".1"
|
|
|
|
|
|
if os.path.isfile(prevfile): os.unlink(prevfile)
|
|
|
|
|
|
os.rename(logfile, prevfile)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
_always_override_destination = True
|
|
|
|
|
|
log("Exception occurred while writing log message to log file: "+str(e), LOG_CRITICAL)
|
|
|
|
|
|
log("Dumping future log events to console!", LOG_CRITICAL)
|
|
|
|
|
|
log(msg, level)
|
|
|
|
|
|
|
|
|
|
|
|
elif logdest == LOG_CALLBACK:
|
|
|
|
|
|
try: logcall(logstring)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
_always_override_destination = True
|
|
|
|
|
|
log("Exception occurred while calling external log handler: "+str(e), LOG_CRITICAL)
|
|
|
|
|
|
log("Dumping future log events to console!", LOG_CRITICAL)
|
|
|
|
|
|
log(msg, level)
|
2018-03-19 18:11:50 +01:00
|
|
|
|
|
2019-11-10 13:56:04 +01:00
|
|
|
|
def rand():
|
2022-04-27 19:00:09 +02:00
|
|
|
|
result = instance_random.random()
|
2020-08-13 12:15:56 +02:00
|
|
|
|
return result
|
2019-11-10 13:56:04 +01:00
|
|
|
|
|
2024-01-14 18:56:20 +01:00
|
|
|
|
def trace_exception(e):
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
exception_info = "".join(traceback.TracebackException.from_exception(e).format())
|
|
|
|
|
|
log(f"An unhandled {str(type(e))} exception occurred: {str(e)}", LOG_ERROR)
|
|
|
|
|
|
log(exception_info, LOG_ERROR)
|
|
|
|
|
|
|
2018-03-19 18:11:50 +01:00
|
|
|
|
def hexrep(data, delimit=True):
|
2026-05-11 23:31:25 +02:00
|
|
|
|
try: iter(data)
|
|
|
|
|
|
except TypeError: data = [data]
|
2022-01-22 21:36:49 +01:00
|
|
|
|
|
2020-08-13 12:15:56 +02:00
|
|
|
|
delimiter = ":"
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if not delimit: delimiter = ""
|
2020-08-13 12:15:56 +02:00
|
|
|
|
hexrep = delimiter.join("{:02x}".format(c) for c in data)
|
|
|
|
|
|
return hexrep
|
2018-03-20 12:32:41 +01:00
|
|
|
|
|
|
|
|
|
|
def prettyhexrep(data):
|
2020-08-13 12:15:56 +02:00
|
|
|
|
delimiter = ""
|
|
|
|
|
|
hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">"
|
|
|
|
|
|
return hexrep
|
2018-04-16 22:04:28 +02:00
|
|
|
|
|
2023-10-27 18:16:52 +02:00
|
|
|
|
def prettyspeed(num, suffix="b"):
|
|
|
|
|
|
return prettysize(num/8, suffix=suffix)+"ps"
|
|
|
|
|
|
|
2022-06-09 14:46:36 +02:00
|
|
|
|
def prettysize(num, suffix='B'):
|
|
|
|
|
|
units = ['','K','M','G','T','P','E','Z']
|
|
|
|
|
|
last_unit = 'Y'
|
|
|
|
|
|
|
|
|
|
|
|
if suffix == 'b':
|
|
|
|
|
|
num *= 8
|
|
|
|
|
|
units = ['','K','M','G','T','P','E','Z']
|
|
|
|
|
|
last_unit = 'Y'
|
|
|
|
|
|
|
|
|
|
|
|
for unit in units:
|
|
|
|
|
|
if abs(num) < 1000.0:
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if unit == "": return "%.0f %s%s" % (num, unit, suffix)
|
|
|
|
|
|
else: return "%.2f %s%s" % (num, unit, suffix)
|
2022-06-09 14:46:36 +02:00
|
|
|
|
num /= 1000.0
|
|
|
|
|
|
|
|
|
|
|
|
return "%.2f%s%s" % (num, last_unit, suffix)
|
|
|
|
|
|
|
2026-05-09 00:05:39 +02:00
|
|
|
|
def prettyfrequency(hz, suffix="Hz", d=2, lpf=False):
|
2026-04-12 18:39:06 +02:00
|
|
|
|
if hz == 0: return "0 Hz"
|
2026-05-09 00:05:39 +02:00
|
|
|
|
if not lpf: num = hz*1e6
|
|
|
|
|
|
else: num = hz
|
|
|
|
|
|
if not lpf: units = ["µ", "m", "", "K","M","G","T","P","E","Z"]
|
|
|
|
|
|
else: units = ["", "K","M","G","T","P","E","Z"]
|
2023-09-30 19:09:26 +02:00
|
|
|
|
last_unit = "Y"
|
|
|
|
|
|
|
|
|
|
|
|
for unit in units:
|
|
|
|
|
|
if abs(num) < 1000.0:
|
2026-05-09 00:50:19 +02:00
|
|
|
|
if d == 2: return "%.2f %s%s" % (num, unit, suffix)
|
|
|
|
|
|
else: return "%s %s%s" % (str(round(num,d)), unit, suffix)
|
2023-09-30 19:09:26 +02:00
|
|
|
|
num /= 1000.0
|
|
|
|
|
|
|
|
|
|
|
|
return "%.2f%s%s" % (num, last_unit, suffix)
|
|
|
|
|
|
|
2023-10-24 13:24:40 +02:00
|
|
|
|
def prettydistance(m, suffix="m"):
|
|
|
|
|
|
num = m*1e6
|
|
|
|
|
|
units = ["µ", "m", "c", ""]
|
|
|
|
|
|
last_unit = "K"
|
2023-10-24 01:41:49 +02:00
|
|
|
|
|
|
|
|
|
|
for unit in units:
|
2023-10-24 13:24:40 +02:00
|
|
|
|
divisor = 1000.0
|
|
|
|
|
|
if unit == "m": divisor = 10
|
|
|
|
|
|
if unit == "c": divisor = 100
|
|
|
|
|
|
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if abs(num) < divisor: return "%.2f %s%s" % (num, unit, suffix)
|
2023-10-24 13:24:40 +02:00
|
|
|
|
num /= divisor
|
2023-10-24 01:41:49 +02:00
|
|
|
|
|
2023-10-24 13:24:40 +02:00
|
|
|
|
return "%.2f %s%s" % (num, last_unit, suffix)
|
2023-10-24 01:41:49 +02:00
|
|
|
|
|
|
|
|
|
|
def prettytime(time, verbose=False, compact=False):
|
2025-01-14 12:45:17 +01:00
|
|
|
|
neg = False
|
|
|
|
|
|
if time < 0:
|
|
|
|
|
|
time = abs(time)
|
|
|
|
|
|
neg = True
|
|
|
|
|
|
|
2022-07-02 13:12:54 +02:00
|
|
|
|
days = int(time // (24 * 3600))
|
|
|
|
|
|
time = time % (24 * 3600)
|
|
|
|
|
|
hours = int(time // 3600)
|
|
|
|
|
|
time %= 3600
|
|
|
|
|
|
minutes = int(time // 60)
|
|
|
|
|
|
time %= 60
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if compact: seconds = int(time)
|
|
|
|
|
|
else: seconds = round(time, 2)
|
2022-07-02 13:12:54 +02:00
|
|
|
|
|
|
|
|
|
|
ss = "" if seconds == 1 else "s"
|
|
|
|
|
|
sm = "" if minutes == 1 else "s"
|
|
|
|
|
|
sh = "" if hours == 1 else "s"
|
|
|
|
|
|
sd = "" if days == 1 else "s"
|
|
|
|
|
|
|
2023-10-24 01:41:49 +02:00
|
|
|
|
displayed = 0
|
2022-07-02 13:12:54 +02:00
|
|
|
|
components = []
|
2023-10-24 01:41:49 +02:00
|
|
|
|
if days > 0 and ((not compact) or displayed < 2):
|
2022-07-02 13:12:54 +02:00
|
|
|
|
components.append(str(days)+" day"+sd if verbose else str(days)+"d")
|
2023-10-24 01:41:49 +02:00
|
|
|
|
displayed += 1
|
2022-07-02 13:12:54 +02:00
|
|
|
|
|
2023-10-24 01:41:49 +02:00
|
|
|
|
if hours > 0 and ((not compact) or displayed < 2):
|
2022-07-02 13:12:54 +02:00
|
|
|
|
components.append(str(hours)+" hour"+sh if verbose else str(hours)+"h")
|
2023-10-24 01:41:49 +02:00
|
|
|
|
displayed += 1
|
2022-07-02 13:12:54 +02:00
|
|
|
|
|
2023-10-24 01:41:49 +02:00
|
|
|
|
if minutes > 0 and ((not compact) or displayed < 2):
|
2022-07-02 13:12:54 +02:00
|
|
|
|
components.append(str(minutes)+" minute"+sm if verbose else str(minutes)+"m")
|
2023-10-24 01:41:49 +02:00
|
|
|
|
displayed += 1
|
2022-07-02 13:12:54 +02:00
|
|
|
|
|
2023-10-24 01:41:49 +02:00
|
|
|
|
if seconds > 0 and ((not compact) or displayed < 2):
|
2022-07-02 13:12:54 +02:00
|
|
|
|
components.append(str(seconds)+" second"+ss if verbose else str(seconds)+"s")
|
2023-10-24 01:41:49 +02:00
|
|
|
|
displayed += 1
|
2022-07-02 13:12:54 +02:00
|
|
|
|
|
|
|
|
|
|
i = 0
|
|
|
|
|
|
tstr = ""
|
|
|
|
|
|
for c in components:
|
|
|
|
|
|
i += 1
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if i == 1: pass
|
|
|
|
|
|
elif i < len(components): tstr += ", "
|
|
|
|
|
|
elif i == len(components): tstr += " and "
|
2022-07-02 13:12:54 +02:00
|
|
|
|
|
|
|
|
|
|
tstr += c
|
|
|
|
|
|
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if tstr == "": return "0s"
|
2022-11-23 17:15:46 +01:00
|
|
|
|
else:
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if not neg: return tstr
|
|
|
|
|
|
else: return f"-{tstr}"
|
2022-07-02 13:12:54 +02:00
|
|
|
|
|
2026-08-19 23:16:52 -06:00
|
|
|
|
def prettyshorttime(time, verbose=False, compact=False, tight=False):
|
2025-01-14 12:45:17 +01:00
|
|
|
|
neg = False
|
2024-09-15 15:12:53 +02:00
|
|
|
|
time = time*1e6
|
2025-01-14 12:45:17 +01:00
|
|
|
|
if time < 0:
|
|
|
|
|
|
time = abs(time)
|
|
|
|
|
|
neg = True
|
2024-09-15 15:12:53 +02:00
|
|
|
|
|
|
|
|
|
|
seconds = int(time // 1e6); time %= 1e6
|
|
|
|
|
|
milliseconds = int(time // 1e3); time %= 1e3
|
|
|
|
|
|
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if compact: microseconds = int(time)
|
|
|
|
|
|
else: microseconds = round(time, 2)
|
2024-09-15 15:12:53 +02:00
|
|
|
|
|
|
|
|
|
|
ss = "" if seconds == 1 else "s"
|
|
|
|
|
|
sms = "" if milliseconds == 1 else "s"
|
|
|
|
|
|
sus = "" if microseconds == 1 else "s"
|
|
|
|
|
|
|
|
|
|
|
|
displayed = 0
|
|
|
|
|
|
components = []
|
|
|
|
|
|
if seconds > 0 and ((not compact) or displayed < 2):
|
|
|
|
|
|
components.append(str(seconds)+" second"+ss if verbose else str(seconds)+"s")
|
|
|
|
|
|
displayed += 1
|
|
|
|
|
|
|
|
|
|
|
|
if milliseconds > 0 and ((not compact) or displayed < 2):
|
|
|
|
|
|
components.append(str(milliseconds)+" millisecond"+sms if verbose else str(milliseconds)+"ms")
|
|
|
|
|
|
displayed += 1
|
|
|
|
|
|
|
|
|
|
|
|
if microseconds > 0 and ((not compact) or displayed < 2):
|
|
|
|
|
|
components.append(str(microseconds)+" microsecond"+sus if verbose else str(microseconds)+"µs")
|
|
|
|
|
|
displayed += 1
|
|
|
|
|
|
|
|
|
|
|
|
i = 0
|
|
|
|
|
|
tstr = ""
|
|
|
|
|
|
for c in components:
|
|
|
|
|
|
i += 1
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if i == 1: pass
|
2026-08-19 23:16:52 -06:00
|
|
|
|
elif i < len(components): tstr += ", " if not tight else " "
|
|
|
|
|
|
elif i == len(components): tstr += " and " if not tight else " "
|
2024-09-15 15:12:53 +02:00
|
|
|
|
|
|
|
|
|
|
tstr += c
|
|
|
|
|
|
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if tstr == "": return "0us"
|
2024-09-15 15:12:53 +02:00
|
|
|
|
else:
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if not neg: return tstr
|
|
|
|
|
|
else: return f"-{tstr}"
|
2024-09-15 15:12:53 +02:00
|
|
|
|
|
2022-06-30 14:02:57 +02:00
|
|
|
|
def phyparams():
|
|
|
|
|
|
print("Required Physical Layer MTU : "+str(Reticulum.MTU)+" bytes")
|
|
|
|
|
|
print("Plaintext Packet MDU : "+str(Packet.PLAIN_MDU)+" bytes")
|
|
|
|
|
|
print("Encrypted Packet MDU : "+str(Packet.ENCRYPTED_MDU)+" bytes")
|
|
|
|
|
|
print("Link Curve : "+str(Link.CURVE))
|
2023-03-04 18:37:28 +01:00
|
|
|
|
print("Link Packet MDU : "+str(Link.MDU)+" bytes")
|
2022-06-30 14:02:57 +02:00
|
|
|
|
print("Link Public Key Size : "+str(Link.ECPUBSIZE*8)+" bits")
|
|
|
|
|
|
print("Link Private Key Size : "+str(Link.KEYSIZE*8)+" bits")
|
|
|
|
|
|
|
2026-05-11 23:31:25 +02:00
|
|
|
|
def panic(): os._exit(255)
|
2021-09-24 16:09:07 +02:00
|
|
|
|
|
2025-02-09 17:52:49 +01:00
|
|
|
|
exit_called = False
|
2025-02-23 15:18:12 +01:00
|
|
|
|
def exit(code=0):
|
2025-02-09 17:52:49 +01:00
|
|
|
|
global exit_called
|
|
|
|
|
|
if not exit_called:
|
|
|
|
|
|
exit_called = True
|
|
|
|
|
|
Reticulum.exit_handler()
|
2025-02-23 15:18:12 +01:00
|
|
|
|
os._exit(code)
|
2024-09-15 15:12:53 +02:00
|
|
|
|
|
2026-05-05 23:42:00 +02:00
|
|
|
|
def _detach_stdout():
|
|
|
|
|
|
sys.stdout = open(os.devnull, "w")
|
|
|
|
|
|
sys.stderr = open(os.devnull, "w")
|
|
|
|
|
|
|
2025-01-10 20:07:17 +01:00
|
|
|
|
class Profiler:
|
2025-01-12 17:51:02 +01:00
|
|
|
|
_ran = False
|
2025-01-10 21:37:45 +01:00
|
|
|
|
profilers = {}
|
2025-01-10 20:07:17 +01:00
|
|
|
|
tags = {}
|
2024-09-15 15:12:53 +02:00
|
|
|
|
|
2026-08-23 21:43:49 -06:00
|
|
|
|
# Samples per tag
|
2026-08-19 23:16:52 -06:00
|
|
|
|
MAX_CAPTURES = 10000
|
|
|
|
|
|
|
2025-01-10 21:37:45 +01:00
|
|
|
|
@staticmethod
|
2026-08-19 23:16:52 -06:00
|
|
|
|
def get_profiler(tag=None, super_tag=None, max_captures=None):
|
2026-05-11 23:31:25 +02:00
|
|
|
|
if tag in Profiler.profilers: return Profiler.profilers[tag]
|
2025-01-10 21:37:45 +01:00
|
|
|
|
else:
|
2026-08-19 23:16:52 -06:00
|
|
|
|
if max_captures is None: max_captures = Profiler.MAX_CAPTURES
|
|
|
|
|
|
profiler = Profiler(tag, super_tag, max_captures)
|
2025-01-10 21:37:45 +01:00
|
|
|
|
Profiler.profilers[tag] = profiler
|
|
|
|
|
|
return profiler
|
|
|
|
|
|
|
2026-08-23 02:27:53 -06:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def profile(func=None, *, tag=None, max_captures=None):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Decorator to profile a function. With no arguments, it can be
|
|
|
|
|
|
used as `@RNS.Profiler.profile`, in which case it will use the
|
|
|
|
|
|
qualified name of the function as the profiler tag. It can
|
|
|
|
|
|
also be used as `@RNS.Profiler.profile(tag="prof_tag")` to use
|
|
|
|
|
|
a specific profiler tag.
|
|
|
|
|
|
|
|
|
|
|
|
:param tag: Either a profiler tag as passed to `Profiler.get_profiler(...)` or a `Profiler` object.
|
2026-08-23 21:43:49 -06:00
|
|
|
|
:param max_captures: Maximum samples to capture for the specified profiler tag.
|
2026-08-23 02:27:53 -06:00
|
|
|
|
"""
|
|
|
|
|
|
if func is None:
|
|
|
|
|
|
return partial(Profiler.profile, tag=tag, max_captures=max_captures)
|
|
|
|
|
|
|
|
|
|
|
|
tag = func.__qualname__ if tag is None else tag
|
|
|
|
|
|
if isinstance(tag, Profiler): profiler = tag
|
|
|
|
|
|
else: profiler = Profiler.get_profiler(tag=tag, max_captures=max_captures)
|
|
|
|
|
|
|
|
|
|
|
|
@wraps(func)
|
|
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
|
|
with profiler:
|
|
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
2026-08-19 23:16:52 -06:00
|
|
|
|
def __init__(self, tag=None, super_tag=None, max_captures=None):
|
2025-01-10 21:37:45 +01:00
|
|
|
|
self.paused = False
|
|
|
|
|
|
self.pause_time = 0
|
|
|
|
|
|
self.pause_started = None
|
2025-01-10 20:07:17 +01:00
|
|
|
|
self.tag = tag
|
|
|
|
|
|
self.super_tag = super_tag
|
2026-08-19 23:16:52 -06:00
|
|
|
|
self.max_captures = max_captures if max_captures is not None else Profiler.MAX_CAPTURES
|
2026-05-11 23:31:25 +02:00
|
|
|
|
|
2025-01-10 21:37:45 +01:00
|
|
|
|
if self.super_tag in Profiler.profilers:
|
|
|
|
|
|
self.super_profiler = Profiler.profilers[self.super_tag]
|
|
|
|
|
|
self.pause_super = self.super_profiler.pause
|
|
|
|
|
|
self.resume_super = self.super_profiler.resume
|
2026-05-11 23:31:25 +02:00
|
|
|
|
|
2025-01-10 21:37:45 +01:00
|
|
|
|
else:
|
2026-05-11 23:31:25 +02:00
|
|
|
|
def noop(self=None): pass
|
2025-01-10 21:37:45 +01:00
|
|
|
|
self.super_profiler = None
|
|
|
|
|
|
self.pause_super = noop
|
|
|
|
|
|
self.resume_super = noop
|
2024-09-15 15:12:53 +02:00
|
|
|
|
|
2025-01-10 20:07:17 +01:00
|
|
|
|
def __enter__(self):
|
2025-01-10 21:37:45 +01:00
|
|
|
|
self.pause_super()
|
2025-01-10 20:07:17 +01:00
|
|
|
|
tag = self.tag
|
|
|
|
|
|
super_tag = self.super_tag
|
|
|
|
|
|
thread_ident = threading.get_ident()
|
2026-08-23 21:43:49 -06:00
|
|
|
|
if not tag in Profiler.tags: Profiler.tags[tag] = {"threads": {}, "super": super_tag, "captures": deque(maxlen=self.max_captures)}
|
2025-01-10 20:07:17 +01:00
|
|
|
|
if not thread_ident in Profiler.tags[tag]["threads"]:
|
2026-08-23 21:43:49 -06:00
|
|
|
|
Profiler.tags[tag]["threads"][thread_ident] = {"running": deque()}
|
|
|
|
|
|
|
|
|
|
|
|
# The tag deque stores a shared reference to the capture, and the end
|
|
|
|
|
|
# time isn't updated until the context manager exits. This leaves the
|
|
|
|
|
|
# deque sorted by start time, except for cases where the thread is
|
|
|
|
|
|
# preemted between getting the current time and appending. We also store
|
|
|
|
|
|
# a stack of start times per thread to support reentrancy.
|
|
|
|
|
|
capture = {"start": time.perf_counter(), "end": None, "thread_ident": thread_ident}
|
|
|
|
|
|
Profiler.tags[tag]["captures"].append(capture)
|
|
|
|
|
|
Profiler.tags[tag]["threads"][thread_ident]["running"].append(capture)
|
2025-01-10 21:37:45 +01:00
|
|
|
|
self.resume_super()
|
2024-09-15 15:12:53 +02:00
|
|
|
|
|
2025-01-10 20:07:17 +01:00
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
2025-01-10 21:37:45 +01:00
|
|
|
|
self.pause_super()
|
2025-01-10 20:07:17 +01:00
|
|
|
|
tag = self.tag
|
|
|
|
|
|
super_tag = self.super_tag
|
2025-01-10 21:37:45 +01:00
|
|
|
|
end = time.perf_counter() - self.pause_time
|
|
|
|
|
|
self.pause_time = 0
|
2025-01-10 20:07:17 +01:00
|
|
|
|
thread_ident = threading.get_ident()
|
2026-08-23 21:43:49 -06:00
|
|
|
|
try:
|
|
|
|
|
|
if tag in Profiler.tags and thread_ident in Profiler.tags[tag]["threads"]:
|
|
|
|
|
|
try: capture = Profiler.tags[tag]["threads"][thread_ident]["running"].pop()
|
|
|
|
|
|
except IndexError as e: return
|
|
|
|
|
|
capture["end"] = end
|
2025-01-12 17:51:02 +01:00
|
|
|
|
if not Profiler._ran:
|
|
|
|
|
|
Profiler._ran = True
|
2026-08-23 21:43:49 -06:00
|
|
|
|
finally:
|
|
|
|
|
|
self.resume_super()
|
2025-01-10 21:37:45 +01:00
|
|
|
|
|
|
|
|
|
|
def pause(self, pause_started=None):
|
|
|
|
|
|
if not self.paused:
|
|
|
|
|
|
self.paused = True
|
|
|
|
|
|
self.pause_started = pause_started or time.perf_counter()
|
|
|
|
|
|
self.pause_super(self.pause_started)
|
|
|
|
|
|
|
|
|
|
|
|
def resume(self):
|
|
|
|
|
|
if self.paused:
|
|
|
|
|
|
self.pause_time += time.perf_counter() - self.pause_started
|
|
|
|
|
|
self.paused = False
|
|
|
|
|
|
self.resume_super()
|
2025-01-10 20:07:17 +01:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2026-05-11 23:31:25 +02:00
|
|
|
|
def ran(): return Profiler._ran
|
2025-01-10 20:07:17 +01:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def results():
|
2026-08-19 23:16:52 -06:00
|
|
|
|
def find_window_start(captures, start_time, hi=None):
|
|
|
|
|
|
hi = len(captures) if hi is None else hi
|
2026-08-23 21:43:49 -06:00
|
|
|
|
idx = bisect.bisect_left(captures, start_time, hi=hi, key=lambda c: c["start"])
|
2026-08-19 23:16:52 -06:00
|
|
|
|
return idx if len(captures) - idx > 1 else None
|
|
|
|
|
|
|
|
|
|
|
|
# Fast one-pass calculation of summary statistics
|
2026-08-23 21:43:49 -06:00
|
|
|
|
def calc_stats(captures, start=0, end=None, key=lambda c: c, threads_key=None):
|
2026-08-19 23:16:52 -06:00
|
|
|
|
if end is None: end = len(captures)
|
2026-08-23 21:43:49 -06:00
|
|
|
|
while start > 0 and start < len(captures) and start < end and key(captures[start]) is None: start += 1
|
|
|
|
|
|
while end > 0 and end <= len(captures) and start < end and key(captures[end-1]) is None: end -= 1
|
2026-08-19 23:16:52 -06:00
|
|
|
|
count = end - start
|
|
|
|
|
|
|
|
|
|
|
|
if count <= 0: return None
|
|
|
|
|
|
elif count == 1:
|
2026-08-23 21:43:49 -06:00
|
|
|
|
capture = key(captures[start])
|
|
|
|
|
|
return { "count": 1,
|
|
|
|
|
|
"mean": capture,
|
|
|
|
|
|
"median": capture,
|
|
|
|
|
|
"min": capture,
|
|
|
|
|
|
"max": capture,
|
|
|
|
|
|
"stdev": None,
|
|
|
|
|
|
"sum": capture,
|
|
|
|
|
|
"threads": 1 if threads_key else None}
|
|
|
|
|
|
|
|
|
|
|
|
# Median is approximate if there are any incomplete captures in the
|
|
|
|
|
|
# middle of the range.
|
2026-08-19 23:16:52 -06:00
|
|
|
|
med_even = count % 2 == 0
|
|
|
|
|
|
med_idx = start + (count // 2 if med_even else (count - 1) // 2)
|
2026-08-23 21:43:49 -06:00
|
|
|
|
while key(captures[med_idx]) is None and med_idx < end: med_idx += 1
|
|
|
|
|
|
if med_idx == end: c_median = None
|
|
|
|
|
|
elif med_even: c_median = key(captures[med_idx])
|
|
|
|
|
|
else:
|
|
|
|
|
|
med_idx2 = med_idx + 1
|
|
|
|
|
|
while key(captures[med_idx2]) is None and med_idx2 < end: med_idx2 += 1
|
|
|
|
|
|
if med_idx2 == end: c_median = None
|
|
|
|
|
|
else:
|
|
|
|
|
|
med_cap1 = key(captures[med_idx])
|
|
|
|
|
|
med_cap2 = key(captures[med_idx+1])
|
|
|
|
|
|
if med_cap1 is None or med_cap2 is None: c_median = None
|
|
|
|
|
|
else: c_median = (med_cap1 + med_cap2) / 2
|
|
|
|
|
|
|
|
|
|
|
|
c_count = 0; c_sum = 0; c_min = key(captures[start]); c_max = key(captures[start]); ck = 0; ck2 = 0
|
|
|
|
|
|
uniq_threads = set()
|
2026-08-19 23:16:52 -06:00
|
|
|
|
for idx in range(start, end):
|
|
|
|
|
|
c = key(captures[idx])
|
2026-08-23 21:43:49 -06:00
|
|
|
|
if c is None: continue
|
|
|
|
|
|
else: c_count += 1
|
|
|
|
|
|
c_sum += c
|
2026-08-19 23:16:52 -06:00
|
|
|
|
if c < c_min: c_min = c
|
|
|
|
|
|
if c > c_max: c_max = c
|
|
|
|
|
|
ck += c - c_median
|
|
|
|
|
|
ck2 += (c - c_median) ** 2
|
2026-08-23 21:43:49 -06:00
|
|
|
|
if threads_key: uniq_threads.add(threads_key(captures[idx]))
|
|
|
|
|
|
c_mean = c_sum / c_count if c_count > 0 else None
|
|
|
|
|
|
c_std = math.sqrt((ck2 - (ck ** 2)/c_count) / (c_count - 1)) if c_count > 1 else None
|
|
|
|
|
|
|
|
|
|
|
|
return { "count": c_count,
|
|
|
|
|
|
"mean": c_mean,
|
|
|
|
|
|
"median": c_median,
|
|
|
|
|
|
"min": c_min,
|
|
|
|
|
|
"max": c_max,
|
|
|
|
|
|
"stdev": c_std,
|
|
|
|
|
|
"sum": c_sum,
|
|
|
|
|
|
"threads": len(uniq_threads) if threads_key else None }
|
|
|
|
|
|
|
|
|
|
|
|
def stats_key(capture):
|
|
|
|
|
|
end = capture["end"]
|
|
|
|
|
|
if end is None: return None
|
|
|
|
|
|
else: return end - capture["start"]
|
2026-08-19 23:16:52 -06:00
|
|
|
|
|
2026-08-23 21:43:49 -06:00
|
|
|
|
results = {}
|
2026-08-19 23:16:52 -06:00
|
|
|
|
now = time.perf_counter()
|
2026-08-19 19:57:42 -06:00
|
|
|
|
for tag in sorted(Profiler.tags):
|
2025-01-10 20:07:17 +01:00
|
|
|
|
tag_entry = Profiler.tags[tag]
|
2026-08-23 21:43:49 -06:00
|
|
|
|
tag_captures = sorted(tag_entry["captures"], key=lambda c: c["start"])
|
2026-08-19 23:16:52 -06:00
|
|
|
|
|
|
|
|
|
|
if len(tag_captures):
|
|
|
|
|
|
captures_1m = None; captures_5m = None; captures_30m = None; captures_60m = None
|
|
|
|
|
|
stats_1m = None; stats_5m = None; stats_30m = None; stats_60m = None
|
|
|
|
|
|
|
|
|
|
|
|
captures_1m = find_window_start(tag_captures, now - 1*60)
|
|
|
|
|
|
if captures_1m: captures_5m = find_window_start(tag_captures, now - 5*60, hi=captures_1m)
|
|
|
|
|
|
if captures_5m: captures_30m = find_window_start(tag_captures, now - 30*60, hi=captures_5m)
|
|
|
|
|
|
if captures_30m: captures_60m = find_window_start(tag_captures, now - 60*60, hi=captures_30m)
|
|
|
|
|
|
|
2026-08-23 21:43:49 -06:00
|
|
|
|
stats_all = calc_stats(tag_captures, 0, None, key=stats_key, threads_key=lambda c: c["thread_ident"])
|
|
|
|
|
|
if captures_1m: stats_1m = calc_stats(tag_captures, captures_1m, None, key=stats_key)
|
|
|
|
|
|
if captures_5m: stats_5m = calc_stats(tag_captures, captures_5m, captures_1m, key=stats_key)
|
|
|
|
|
|
if captures_30m: stats_30m = calc_stats(tag_captures, captures_30m, captures_5m, key=stats_key)
|
|
|
|
|
|
if captures_60m: stats_60m = calc_stats(tag_captures, captures_60m, captures_30m, key=stats_key)
|
2026-08-19 23:16:52 -06:00
|
|
|
|
|
2026-08-23 21:43:49 -06:00
|
|
|
|
if stats_all["count"]:
|
|
|
|
|
|
results[tag] = { "name": tag,
|
|
|
|
|
|
"super": tag_entry["super"],
|
|
|
|
|
|
"stats_all": stats_all,
|
|
|
|
|
|
"stats_1m": stats_1m,
|
|
|
|
|
|
"stats_5m": stats_5m,
|
|
|
|
|
|
"stats_30m": stats_30m,
|
|
|
|
|
|
"stats_60m": stats_60m }
|
2026-08-19 23:16:52 -06:00
|
|
|
|
|
2026-08-23 21:43:49 -06:00
|
|
|
|
# Yield to avoid bogging down the instance
|
|
|
|
|
|
time.sleep(0.001)
|
2025-01-10 20:07:17 +01:00
|
|
|
|
|
2026-08-19 19:57:42 -06:00
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def format_results(results):
|
2026-08-19 23:16:52 -06:00
|
|
|
|
def pst(time):
|
|
|
|
|
|
if time is not None: return prettyshorttime(time, tight=True)
|
|
|
|
|
|
else: return "-----"
|
|
|
|
|
|
|
2025-01-10 20:07:17 +01:00
|
|
|
|
def print_results_recursive(tag, results, level=0):
|
2026-08-19 19:57:42 -06:00
|
|
|
|
results_str = print_tag_results(tag, level+1) + "\n"
|
2024-09-15 15:12:53 +02:00
|
|
|
|
|
2025-01-10 20:07:17 +01:00
|
|
|
|
for tag_name in results:
|
|
|
|
|
|
sub_tag = results[tag_name]
|
|
|
|
|
|
if sub_tag["super"] == tag["name"]:
|
2026-08-19 19:57:42 -06:00
|
|
|
|
results_str += print_results_recursive(sub_tag, results, level=level+1)
|
|
|
|
|
|
|
|
|
|
|
|
return results_str
|
2024-09-15 15:12:53 +02:00
|
|
|
|
|
2025-01-10 20:07:17 +01:00
|
|
|
|
def print_tag_results(tag, level):
|
|
|
|
|
|
ind = " "*level
|
2026-08-23 21:43:49 -06:00
|
|
|
|
name = tag["name"]
|
2026-08-19 23:16:52 -06:00
|
|
|
|
stats_all = tag["stats_all"]; stats_1m = tag["stats_1m"]; stats_5m = tag["stats_5m"]; stats_30m = tag["stats_30m"]; stats_60m = tag["stats_60m"]
|
2026-08-19 19:57:42 -06:00
|
|
|
|
results_str = f" {ind}{name}\n"
|
2026-08-25 02:18:23 +02:00
|
|
|
|
results_str += f" {ind} Samples : {stats_all['count']} from {stats_all['threads']} thread{'s' if stats_all['threads'] > 1 else ''}\n"
|
2026-08-19 23:16:52 -06:00
|
|
|
|
if stats_all != None:
|
2026-08-23 21:43:49 -06:00
|
|
|
|
results_str += f" {ind} {'Mean':^15} | {'Median':^15} | {'Min':^15} | {'Max':^15} | {'St. dev':^15} | {'Total':^15}\n"
|
2026-08-25 02:18:23 +02:00
|
|
|
|
results_str += f" {ind} Stats : ({pst(stats_all['mean']):^15} | {pst(stats_all['median']):^15} | {pst(stats_all['min']):^15} | {pst(stats_all['max']):^15} | {pst(stats_all['stdev']):^15} | {pst(stats_all['sum']):^15})\n"
|
2026-08-19 23:16:52 -06:00
|
|
|
|
if stats_1m != None:
|
2026-08-25 02:18:23 +02:00
|
|
|
|
results_str += f" {ind} 0-1m : ({pst(stats_1m['mean']):^15} | {pst(stats_1m['median']):^15} | {pst(stats_1m['min']):^15} | {pst(stats_1m['max']):^15} | {pst(stats_1m['stdev']):^15} | {pst(stats_1m['sum']):^15})\n"
|
2026-08-19 23:16:52 -06:00
|
|
|
|
if stats_5m != None:
|
2026-08-25 02:18:23 +02:00
|
|
|
|
results_str += f" {ind} 1-5m : ({pst(stats_5m['mean']):^15} | {pst(stats_5m['median']):^15} | {pst(stats_5m['min']):^15} | {pst(stats_5m['max']):^15} | {pst(stats_5m['stdev']):^15} | {pst(stats_5m['sum']):^15})\n"
|
2026-08-19 23:16:52 -06:00
|
|
|
|
if stats_30m != None:
|
2026-08-25 02:18:23 +02:00
|
|
|
|
results_str += f" {ind} 5-30m : ({pst(stats_30m['mean']):^15} | {pst(stats_30m['median']):^15} | {pst(stats_30m['min']):^15} | {pst(stats_30m['max']):^15} | {pst(stats_30m['stdev']):^15} | {pst(stats_30m['sum']):^15})\n"
|
2026-08-19 23:16:52 -06:00
|
|
|
|
if stats_60m != None:
|
2026-08-25 02:18:23 +02:00
|
|
|
|
results_str += f" {ind} 30-60m : ({pst(stats_60m['mean']):^15} | {pst(stats_60m['median']):^15} | {pst(stats_60m['min']):^15} | {pst(stats_60m['max']):^15} | {pst(stats_60m['stdev']):^15} | {pst(stats_60m['sum']):^15})\n"
|
2026-08-19 19:57:42 -06:00
|
|
|
|
return results_str
|
2024-09-15 16:46:52 +02:00
|
|
|
|
|
2026-08-19 19:57:42 -06:00
|
|
|
|
results_str = ""
|
2025-01-11 19:30:00 +01:00
|
|
|
|
for tag_name in results:
|
|
|
|
|
|
tag = results[tag_name]
|
|
|
|
|
|
if tag["super"] == None:
|
2026-08-19 19:57:42 -06:00
|
|
|
|
results_str += print_results_recursive(tag, results)
|
|
|
|
|
|
|
|
|
|
|
|
return results_str
|
2025-01-10 21:37:45 +01:00
|
|
|
|
|
2026-04-28 14:38:32 +02:00
|
|
|
|
profile = Profiler.get_profiler
|
|
|
|
|
|
|
2026-05-11 23:22:13 +02:00
|
|
|
|
# The base-256 table is likely to change. Currently, it is just
|
|
|
|
|
|
# experimental, so don't count on it too much just yet.
|
2026-04-28 14:38:32 +02:00
|
|
|
|
b256 = [
|
|
|
|
|
|
# 0 1 2 3 4 5 6 7 8 9 A B C D F F
|
|
|
|
|
|
"a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p", # 0x0 Latin & numerals
|
|
|
|
|
|
"q","r","s","t","u","v","x","y","z","æ","ø","0","1","2","3","4", # 0x1 Latin & numerals
|
|
|
|
|
|
"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P", # 0x2 Latin & numerals
|
|
|
|
|
|
"Q","R","S","T","U","W","X","Y","Z","Æ","Ø","5","6","7","8","9", # 0x3 Latin & numerals
|
|
|
|
|
|
"α","β","γ","δ","ε","ζ","η","θ","ι","κ","λ","μ","ν","ξ","π","ρ", # 0x4 Greek
|
|
|
|
|
|
"σ","τ","φ","χ","ψ","ω","Γ","Δ","Θ","Λ","Ξ","Π","Σ","Φ","Ψ","Ω", # 0x5 Greek
|
|
|
|
|
|
"Б","Д","Ж","З","И","Л","П","Ц","Ч","Ш","Щ","Ъ","Ы","Э","Ю","Я", # 0x6 Cyrillic
|
|
|
|
|
|
"б","д","ж","з","и","л","п","ц","ч","ш","щ","ъ","ы","э","ю","я", # 0x7 Cyrillic
|
|
|
|
|
|
"Ա","Բ","Գ","Դ","Ե","Զ","Է","Ը","Թ","Ժ","Ի","Խ","Ծ","Կ","Հ","Ձ", # 0x8 Armenian Capitals
|
|
|
|
|
|
"Ղ","Ճ","Մ","Յ","Ն","Շ","Ո","Չ","Պ","Ջ","Վ","Ր","Ց","Ւ","Ք","Ֆ", # 0x9 Armenian Captials
|
|
|
|
|
|
"ᚠ","ᚢ","ᚦ","ᚱ","ᚹ","ᚺ","ᚾ","ᛈ","ᛇ","ᛉ","ᛊ","ᛏ","ᛒ","ᛖ","ᛗ","ᛟ", # 0xA Elder Futhark
|
|
|
|
|
|
"イ","ウ","オ","カ","キ","ケ","サ","シ","ス","セ","タ","チ","テ","ト","ナ","ヌ", # 0xB Katakana
|
|
|
|
|
|
"ネ","ヒ","フ","ヘ","ホ","マ","ミ","ム","メ","モ","ヤ","ラ","リ","ル","レ","ワ", # 0xC Katakana
|
|
|
|
|
|
"𐑐","𐑑","𐑒","𐑔","𐑕","𐑗","𐑙","𐑳","𐑶","𐑸","𐑹","𐑺","𐑻","𐑽","𐑾","𐑿", # 0xD Shavian
|
|
|
|
|
|
"᱑","᱕","᱘","᱙","ᱚ","ᱝ","ᱟ","ᱣ","ᱦ","ᱨ","ᱬ","ᱭ","ᱰ","ᱳ","ᱶ","ᱷ", # 0xE Ol Chiki
|
|
|
|
|
|
"𐌳","𐌸","𐌾","𐐀","𐐁","𐐂","𐐆","𐐇","𐐈","𐐉","𐐊","𐐋","𐐌","𐐍","𐐎","𐐏", # 0xF Gothic & Deseret
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-05-11 23:22:13 +02:00
|
|
|
|
def b256rep(data): return "".join(bytes_to_b256(data))
|
|
|
|
|
|
def prettyb256rep(data): return f"<{b256rep(data)}>"
|
|
|
|
|
|
|
|
|
|
|
|
def b256_to_byte(point):
|
|
|
|
|
|
if not type(point) == str or not len(point) == 1: raise TypeError("Invalid input data for base256 byte decode")
|
|
|
|
|
|
try: return b256.index(point)
|
|
|
|
|
|
except Exception as e: raise ValueError(f"Could not decode base256 byte: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def b256_to_bytes(b256rep):
|
|
|
|
|
|
if not type(b256rep) == str: raise TypeError("Invalid input data for base256 decode")
|
|
|
|
|
|
try: return bytes([b256.index(c) for c in b256rep])
|
|
|
|
|
|
except Exception as e: raise ValueError(f"Could not decode base256: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def byte_to_b256(input_byte):
|
|
|
|
|
|
if type(input_byte) == bytes and not len(input_byte) == 1: TypeError("Invalid input data for base256 byte encode")
|
|
|
|
|
|
if type(input_byte) == bytes and len(input_byte) == 1: input_byte = ord(input_byte)
|
|
|
|
|
|
if not type(input_byte) == int: raise TypeError("Invalid input data for base256 byte encode")
|
|
|
|
|
|
try: return b256[int(input_byte)]
|
|
|
|
|
|
except Exception as e: raise TypeError(f"Could not encode byte to base256: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def bytes_to_b256(data):
|
|
|
|
|
|
if not type(data) == bytes: raise TypeError("Invalid input data for base256 encode")
|
|
|
|
|
|
try: return [byte_to_b256(c) for c in data]
|
|
|
|
|
|
except Exception as e: raise TypeError(f"Could not encode to base256: {e}")
|
2026-08-24 11:29:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from .Reticulum import Reticulum
|
|
|
|
|
|
from .Identity import Identity
|
|
|
|
|
|
from .Link import Link, RequestReceipt
|
|
|
|
|
|
from .Channel import MessageBase
|
|
|
|
|
|
from .Buffer import Buffer, RawChannelReader, RawChannelWriter
|
|
|
|
|
|
from .Transport import Transport
|
|
|
|
|
|
from .Discovery import InterfaceAnnouncer
|
|
|
|
|
|
from .Destination import Destination
|
|
|
|
|
|
from .Packet import Packet
|
|
|
|
|
|
from .Packet import PacketReceipt
|
|
|
|
|
|
from .Resolver import Resolver
|
|
|
|
|
|
from .Resource import Resource, ResourceAdvertisement
|
|
|
|
|
|
from .Cryptography import HKDF
|
|
|
|
|
|
from .Cryptography import Hashes
|