Crypto: - Replace broken ChaCha20 (static nonce) with ChaCha20-Poly1305 AEAD - HKDF-SHA256 key derivation from per-device factory NVS master keys - Random 12-byte nonce per message (ESP32 hardware RNG) - crypto_init/encrypt/decrypt API with mbedtls legacy (ESP-IDF v5.3.2) - Custom partition table with factory NVS (fctry at 0x10000) Firmware: - crypto.c full rewrite, messages.c device_id prefix + AEAD encrypt - crypto_init() at boot with esp_restart() on failure - Fix command_t initializations across all modules (sub/help fields) - Clean CMakeLists dependencies for ESP-IDF v5.3.2 C3PO (C2): - Rename tools/c2 + tools/c3po -> tools/C3PO - Per-device CryptoContext with HKDF key derivation - KeyStore (keys.json) for master key management - Transport parses device_id:base64(...) wire format Tools: - New tools/provisioning/provision.py for factory NVS key generation - Updated flasher with mbedtls config for v5.3.2 Docs: - Update all READMEs for new crypto, C3PO paths, provisioning - Update roadmap, architecture diagrams, security sections - Update CONTRIBUTING.md project structure
160 lines
4.9 KiB
Python
160 lines
4.9 KiB
Python
"""
|
|
Dynamic device tabs widget.
|
|
"""
|
|
from textual.widgets import Static, Button
|
|
from textual.containers import Horizontal
|
|
from textual.message import Message
|
|
from textual.reactive import reactive
|
|
|
|
|
|
class DeviceTabs(Horizontal):
|
|
"""Tab bar for device switching with dynamic updates."""
|
|
|
|
DEFAULT_CSS = """
|
|
DeviceTabs {
|
|
height: 1;
|
|
width: 100%;
|
|
background: $surface;
|
|
padding: 0;
|
|
}
|
|
|
|
DeviceTabs .tab-label {
|
|
padding: 0 1;
|
|
height: 1;
|
|
min-width: 8;
|
|
}
|
|
|
|
DeviceTabs .tab-label.active {
|
|
background: $primary;
|
|
color: $text;
|
|
text-style: bold;
|
|
}
|
|
|
|
DeviceTabs .tab-label:hover {
|
|
background: $primary-darken-1;
|
|
}
|
|
|
|
DeviceTabs .header-label {
|
|
padding: 0 1;
|
|
height: 1;
|
|
color: $text-muted;
|
|
}
|
|
|
|
DeviceTabs .separator {
|
|
padding: 0;
|
|
height: 1;
|
|
color: $text-muted;
|
|
}
|
|
|
|
DeviceTabs .device-count {
|
|
dock: right;
|
|
padding: 0 1;
|
|
height: 1;
|
|
color: $text-muted;
|
|
}
|
|
"""
|
|
|
|
active_tab: reactive[str] = reactive("global")
|
|
devices_hidden: reactive[bool] = reactive(False)
|
|
|
|
class TabSelected(Message):
|
|
"""Posted when a tab is selected."""
|
|
def __init__(self, tab_id: str, device_id: str | None = None):
|
|
self.tab_id = tab_id
|
|
self.device_id = device_id
|
|
super().__init__()
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self._devices: list[str] = []
|
|
|
|
def compose(self):
|
|
yield Static("C3PO", classes="header-label", id="c3po-label")
|
|
yield Static(" \u2500 ", classes="separator")
|
|
yield Static("[G]lobal", classes="tab-label active", id="tab-global")
|
|
yield Static(" [H]ide", classes="tab-label", id="tab-hide")
|
|
yield Static("", classes="device-count", id="device-count")
|
|
|
|
def add_device(self, device_id: str):
|
|
"""Add a device tab."""
|
|
if device_id not in self._devices:
|
|
self._devices.append(device_id)
|
|
self._rebuild_tabs()
|
|
|
|
def remove_device(self, device_id: str):
|
|
"""Remove a device tab."""
|
|
if device_id in self._devices:
|
|
self._devices.remove(device_id)
|
|
if self.active_tab == device_id:
|
|
self.active_tab = "global"
|
|
self._rebuild_tabs()
|
|
|
|
def _rebuild_tabs(self):
|
|
"""Rebuild all tabs."""
|
|
for widget in list(self.children):
|
|
if hasattr(widget, 'id') and widget.id and widget.id.startswith("tab-device-"):
|
|
widget.remove()
|
|
|
|
hide_tab = self.query_one("#tab-hide", Static)
|
|
|
|
for i, device_id in enumerate(self._devices):
|
|
if i < 9:
|
|
label = f"[{i+1}]{device_id}"
|
|
tab = Static(
|
|
label,
|
|
classes="tab-label" + (" active" if self.active_tab == device_id else ""),
|
|
id=f"tab-device-{device_id}"
|
|
)
|
|
self.mount(tab, before=hide_tab)
|
|
|
|
count_label = self.query_one("#device-count", Static)
|
|
count_label.update(f"{len(self._devices)} device{'s' if len(self._devices) != 1 else ''}")
|
|
|
|
def select_tab(self, tab_id: str):
|
|
"""Select a tab by ID."""
|
|
if tab_id == "global":
|
|
self.active_tab = "global"
|
|
self.post_message(self.TabSelected("global"))
|
|
elif tab_id in self._devices:
|
|
self.active_tab = tab_id
|
|
self.post_message(self.TabSelected(tab_id, tab_id))
|
|
|
|
self._update_active_styles()
|
|
|
|
def select_by_index(self, index: int):
|
|
"""Select device tab by numeric index (1-9)."""
|
|
if 0 < index <= len(self._devices):
|
|
device_id = self._devices[index - 1]
|
|
self.select_tab(device_id)
|
|
|
|
def toggle_hide(self):
|
|
"""Toggle device panes visibility."""
|
|
self.devices_hidden = not self.devices_hidden
|
|
hide_tab = self.query_one("#tab-hide", Static)
|
|
hide_tab.update("[H]ide" if not self.devices_hidden else "[H]show")
|
|
|
|
def _update_active_styles(self):
|
|
"""Update tab styles to show active state."""
|
|
for tab in self.query(".tab-label"):
|
|
tab.remove_class("active")
|
|
|
|
if self.active_tab == "global":
|
|
self.query_one("#tab-global", Static).add_class("active")
|
|
else:
|
|
try:
|
|
self.query_one(f"#tab-device-{self.active_tab}", Static).add_class("active")
|
|
except Exception:
|
|
pass
|
|
|
|
def on_click(self, event) -> None:
|
|
"""Handle tab clicks."""
|
|
target = event.target
|
|
if hasattr(target, 'id') and target.id:
|
|
if target.id == "tab-global":
|
|
self.select_tab("global")
|
|
elif target.id == "tab-hide":
|
|
self.toggle_hide()
|
|
elif target.id.startswith("tab-device-"):
|
|
device_id = target.id.replace("tab-device-", "")
|
|
self.select_tab(device_id)
|