Add keyboard-wedge typing and LED event support
- keyboard.py: emulate a commercial USB RFID reader by typing card ids into the focused window via a /dev/uinput virtual keyboard - led.py / bridge.py: drive the status LED from card events - cli.py, card.py: wire up the new options and event flow - deploy: harden install.sh and the systemd unit - README: document keyboard mode, LED support, and deployment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -75,6 +75,12 @@ DEFAULT_HISTORY = 50
|
||||
#: replaced.
|
||||
DEFAULT_TAP_TTL = 10.0
|
||||
|
||||
#: How long a *claimed* tap stays readable afterwards. The page that claimed it
|
||||
#: needs a moment to confirm which card logged in, but an id that stays
|
||||
#: readable forever is indistinguishable from a tap still waiting to be spent -
|
||||
#: so it is cleared once this window passes. Set to 0 to keep it indefinitely.
|
||||
DEFAULT_CLAIMED_TTL = 30.0
|
||||
|
||||
|
||||
class EventHub:
|
||||
"""Tracks reader state and fans card events out to waiting subscribers.
|
||||
@@ -96,6 +102,7 @@ class EventHub:
|
||||
history: int = DEFAULT_HISTORY,
|
||||
mode: str = MODE_LOGIN,
|
||||
tap_ttl: float = DEFAULT_TAP_TTL,
|
||||
claimed_ttl: float = DEFAULT_CLAIMED_TTL,
|
||||
):
|
||||
self._lock = threading.Lock()
|
||||
self._subscribers: List["deque"] = []
|
||||
@@ -107,6 +114,7 @@ class EventHub:
|
||||
self._counts = {EVENT_INSERTED: 0, EVENT_REMOVED: 0, "invalid_card": 0}
|
||||
self._port: Optional[str] = None
|
||||
self._tap_ttl = float(tap_ttl if tap_ttl else 0.0)
|
||||
self._claimed_ttl = float(claimed_ttl if claimed_ttl else 0.0)
|
||||
|
||||
# The latched tap. It deliberately outlives the card leaving the
|
||||
# reader, so a page that loads after the tap can still claim it.
|
||||
@@ -177,8 +185,16 @@ class EventHub:
|
||||
|
||||
``present`` is whether the card is physically on the reader right now;
|
||||
``claimable`` is whether this tap can still be spent on a login.
|
||||
|
||||
A claimed tap is reported for ``claimed_ttl`` seconds after it was
|
||||
spent, so the page that claimed it can confirm which card logged in. An
|
||||
expired unclaimed tap is cleared too: keeping it would leave an id that
|
||||
can never be claimed again, which is indistinguishable from a tap that
|
||||
is still waiting to be used.
|
||||
"""
|
||||
with self._lock:
|
||||
self._drop_dead_tap()
|
||||
|
||||
if self._tap is None:
|
||||
return {
|
||||
"present": False,
|
||||
@@ -200,6 +216,32 @@ class EventHub:
|
||||
"card": self._tap.to_dict(),
|
||||
}
|
||||
|
||||
def _drop_dead_tap(self) -> None:
|
||||
"""Clear the latched tap once it can no longer be acted on.
|
||||
|
||||
Called with the lock held. Two independent deadlines: an unclaimed tap
|
||||
is dead after ``tap_ttl``, a claimed one after ``claimed_ttl``. A tap of
|
||||
zero TTL means "keep until replaced", which the claimed case uses to
|
||||
keep the id around indefinitely.
|
||||
|
||||
Crucially this drops only the *tap*, never the presence flag: whether a
|
||||
card is on the reader is a physical fact learnt from insert/remove
|
||||
events, and a tap ageing out says nothing about it. Clearing presence
|
||||
here reported "no card" while a card was sitting on the antenna.
|
||||
"""
|
||||
if self._tap is None:
|
||||
return
|
||||
now = time.time()
|
||||
if self._consumed_at is not None:
|
||||
if self._claimed_ttl and now - self._consumed_at > self._claimed_ttl:
|
||||
self._tap = None
|
||||
self._tap_at = None
|
||||
self._consumed_at = None
|
||||
return
|
||||
if self._tap_ttl and now - (self._tap_at or 0.0) > self._tap_ttl:
|
||||
self._tap = None
|
||||
self._tap_at = None
|
||||
|
||||
def consume_card(self):
|
||||
"""Atomically claim the latched tap for one authentication.
|
||||
|
||||
@@ -207,6 +249,7 @@ class EventHub:
|
||||
this call succeeds, so two concurrent requests cannot both win.
|
||||
"""
|
||||
with self._lock:
|
||||
self._drop_dead_tap()
|
||||
if self._tap is None:
|
||||
return None, "no card read yet", 409
|
||||
|
||||
@@ -245,6 +288,7 @@ class EventHub:
|
||||
|
||||
def health(self) -> Dict[str, Any]:
|
||||
with self._lock:
|
||||
self._drop_dead_tap()
|
||||
last_age = (
|
||||
round(time.time() - self._last_at, 1) if self._last_at else None
|
||||
)
|
||||
@@ -260,6 +304,7 @@ class EventHub:
|
||||
"card_present": self._on_reader,
|
||||
"card_consumed": self._consumed_at is not None,
|
||||
"tap_ttl_seconds": self._tap_ttl or None,
|
||||
"claimed_ttl_seconds": self._claimed_ttl or None,
|
||||
}
|
||||
|
||||
def last(self) -> Optional[Dict[str, Any]]:
|
||||
|
||||
Reference in New Issue
Block a user