The prose of each docstring and comment in the core package now follows ASD-STE100 Simplified Technical English, with Canadian spelling: short sentences, simple verb tenses, no dashes or semicolons, and one word for one concept (docs/development/writing-style.md has the rules and the glossary). The meaning and each reason stay. The code does not change: the AST of each file, without its docstrings, is the same as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
137 lines
5.7 KiB
Python
137 lines
5.7 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""The shared transmit clock: how long ago our node last transmitted.
|
|
|
|
MeshTerm always paced its own bursts: a trace waits between samples, and a TX sweep waits
|
|
between levels. But each feature did it privately, inside its own loop. No feature knew
|
|
what another feature transmitted a moment before. Thus a trace could finish and an advert
|
|
could go out a tenth of a second later, and no screen could tell why it made you wait.
|
|
|
|
This module is that missing clock. Each real transmission marks it (refer to the send
|
|
methods in :mod:`meshterm.core.connection`, which are the only place that speaks to the
|
|
radio). Code that is about to transmit can ask :func:`remaining` how long it must wait to
|
|
transmit politely. The wait itself is the job of the UI. Refer to
|
|
:func:`meshterm.ui.cooldown.wait_for_cooldown`, which changes a wait that is long enough
|
|
to notice into a countdown that you can cancel.
|
|
|
|
There are two clocks, because two types of transmission cost the mesh very different
|
|
amounts:
|
|
|
|
* **Each transmission**, spaced by the ``trace_cooldown_s`` preference. This is duty-cycle
|
|
courtesy: our own airtime, spread out.
|
|
* **Flood adverts**, spaced by the longer ``flood_advert_cooldown_s``. A flood advert is
|
|
the most expensive thing that a node can ask for, because each repeater in range relays
|
|
it. Thus a flood advert waits for both clocks, and the minimum of its own clock is much
|
|
higher than the minimum of the general clock.
|
|
|
|
The timing uses :func:`time.monotonic`, not the wall clock, because this module measures
|
|
an elapsed interval. A clock sync during a session (which MeshTerm itself can start) must
|
|
not make a cooldown look finished, or look as if it has years left.
|
|
|
|
The gate is a singleton for the full process, the same as
|
|
:func:`meshterm.core.preferences.current`. Code does not pass it down. The reason: the
|
|
layer that transmits and the layer that waits are far apart. To pass a clock down between
|
|
them changes each call between them, and gives no benefit.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
|
|
class TransmitGate:
|
|
"""The clock of the last transmission, and the wait that it causes.
|
|
|
|
:meth:`mark` stores that something went out. :meth:`remaining` tells how long a
|
|
caller must wait. A new gate transmitted nothing, so no wait is necessary.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
"""Start an idle gate: nothing sent, and no wait."""
|
|
self._last_sent: float | None = None
|
|
self._last_flood_advert: float | None = None
|
|
|
|
def mark(self, *, flood_advert: bool = False) -> None:
|
|
"""Store a transmission that finished at this moment.
|
|
|
|
MeshTerm calls this method after the send returns. Thus the cooldown counts from
|
|
the moment when the air was free again, not from when our node started to
|
|
transmit.
|
|
|
|
Args:
|
|
flood_advert: Whether the transmission was a flood advert. A flood advert
|
|
starts its own longer clock, in addition to the general clock.
|
|
"""
|
|
now = time.monotonic()
|
|
self._last_sent = now
|
|
if flood_advert:
|
|
self._last_flood_advert = now
|
|
|
|
@property
|
|
def last_sent(self) -> float | None:
|
|
"""When something last went out, on the :func:`time.monotonic` clock (or ``None``).
|
|
|
|
This is for a listener that reads our own transmissions as an end of the silence
|
|
on the air (the weekly advert waits for a quiet period), not as a cooldown to wait
|
|
for. ``None`` means that nothing ever went out.
|
|
"""
|
|
return self._last_sent
|
|
|
|
def remaining(self, *, flood_advert: bool = False) -> float:
|
|
"""The seconds that a caller must wait before it transmits (``0.0`` to go now).
|
|
|
|
Args:
|
|
flood_advert: Whether the planned transmission is a flood advert. A flood
|
|
advert waits for both clocks: the general cooldown since the last
|
|
transmission, and the flood cooldown since the last flood advert. Thus
|
|
the method returns the longer of the two waits.
|
|
|
|
Returns:
|
|
The wait in seconds, never negative.
|
|
"""
|
|
from .preferences import current as current_preferences
|
|
|
|
preferences = current_preferences()
|
|
wait = _left(self._last_sent, preferences.trace_cooldown_s)
|
|
if flood_advert:
|
|
wait = max(wait, _left(self._last_flood_advert, preferences.flood_advert_cooldown_s))
|
|
return wait
|
|
|
|
def reset(self) -> None:
|
|
"""Forget both clocks, as if nothing was ever sent.
|
|
|
|
This is for a new session and for tests. The user has no way to clear a cooldown,
|
|
because such a way makes the cooldown useless.
|
|
"""
|
|
self._last_sent = None
|
|
self._last_flood_advert = None
|
|
|
|
|
|
def _left(last: float | None, cooldown: float) -> float:
|
|
"""The seconds of ``cooldown`` that remain after ``last``, or ``0.0`` if it never occurred."""
|
|
if last is None or cooldown <= 0:
|
|
return 0.0
|
|
return max(0.0, cooldown - (time.monotonic() - last))
|
|
|
|
|
|
#: The clock for this process. It is a module-level singleton, for the reason that the
|
|
#: module docstring gives: the layer that transmits and the layer that waits never meet.
|
|
_gate = TransmitGate()
|
|
|
|
|
|
def current() -> TransmitGate:
|
|
"""The transmit clock that applies to this process."""
|
|
return _gate
|
|
|
|
|
|
def mark(*, flood_advert: bool = False) -> None:
|
|
"""Store a transmission on the clock of the process (refer to :meth:`TransmitGate.mark`)."""
|
|
_gate.mark(flood_advert=flood_advert)
|
|
|
|
|
|
def remaining(*, flood_advert: bool = False) -> float:
|
|
"""The necessary wait on the process clock (refer to :meth:`TransmitGate.remaining`)."""
|
|
return _gate.remaining(flood_advert=flood_advert)
|
|
|
|
|
|
__all__ = ["TransmitGate", "current", "mark", "remaining"]
|