Add Camera Capture overlay (F6) with multi-trigger capture pipeline
Camera backend abstraction wrapping fswebcam/ffmpeg/libcamera-still via subprocess, with DemoCamera fallback generating valid JPEG from raw bytes. Capture pipeline writes JPEG + JSON sidecar always, optional FITS (astropy) and EXIF (Pillow) when available. Thread-safe orchestrator with session tracking, sequence numbering, and date-based output directories. Trigger system: manual capture, configurable interval timer, and pass event detection (AOS/TCA/LOS) with 0.5-degree TCA hysteresis. PassEventDetector runs in the Craft tracking loop, fires callbacks to the camera overlay. F6 overlay follows the F5 ConsoleOverlay pattern — ModalScreen with install_screen persistence. Status panel, scrollable capture log, interval controls, and AOS/TCA/LOS toggle buttons. Tagline: "a generic AZ/EL positioner that doesn't care about wavelength" added to TUI header subtitle. 51 new tests (77 total passing).
This commit is contained in:
parent
6c1e9da773
commit
7035d814a1
13 changed files with 2576 additions and 2 deletions
|
|
@ -16,6 +16,7 @@ from textual.binding import Binding
|
|||
from textual.containers import Horizontal
|
||||
from textual.widgets import Button, ContentSwitcher, Footer, Header
|
||||
|
||||
from birdcage_tui.screens.camera import CameraOverlay
|
||||
from birdcage_tui.screens.console import ConsoleOverlay
|
||||
from birdcage_tui.screens.control import ControlScreen
|
||||
from birdcage_tui.screens.dashboard import DashboardScreen
|
||||
|
|
@ -45,6 +46,7 @@ class BirdcageApp(App):
|
|||
Binding("f3", "switch_tab('signal')", "Signal"),
|
||||
Binding("f4", "switch_tab('system')", "System"),
|
||||
Binding("f5", "toggle_console", "Console"),
|
||||
Binding("f6", "toggle_camera", "Camera"),
|
||||
Binding("q", "quit", "Quit"),
|
||||
Binding("d", "toggle_dark", "Dark"),
|
||||
]
|
||||
|
|
@ -55,6 +57,8 @@ class BirdcageApp(App):
|
|||
firmware_name: str = "g2"
|
||||
skip_init: bool = False
|
||||
craft_url: str = "https://space.warehack.ing"
|
||||
capture_dir: str = "captures"
|
||||
camera_device: str = "auto"
|
||||
device: object = None
|
||||
shutdown_event: threading.Event = threading.Event()
|
||||
|
||||
|
|
@ -64,12 +68,15 @@ class BirdcageApp(App):
|
|||
_prev_az: float = 0.0
|
||||
_prev_el: float = 0.0
|
||||
_console_visible: bool = False
|
||||
_camera_visible: bool = False
|
||||
_pass_detector: object = None # PassEventDetector, set by CameraOverlay
|
||||
|
||||
@property
|
||||
def SUB_TITLE(self) -> str: # noqa: N802
|
||||
tag = "a generic AZ/EL positioner that doesn't care about wavelength"
|
||||
if self.demo_mode:
|
||||
return "DEMO"
|
||||
return self.serial_port
|
||||
return f"{tag} · DEMO"
|
||||
return f"{tag} · {self.serial_port}"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
|
|
@ -114,6 +121,7 @@ class BirdcageApp(App):
|
|||
self._setup_craft_client()
|
||||
self._update_status_strip_connection()
|
||||
self._install_console()
|
||||
self._install_camera()
|
||||
self._start_position_poll()
|
||||
|
||||
async def _initialize_device(self) -> None:
|
||||
|
|
@ -159,6 +167,10 @@ class BirdcageApp(App):
|
|||
"""Pre-install the console overlay so it persists across open/close."""
|
||||
self.install_screen(ConsoleOverlay(), name="console-overlay")
|
||||
|
||||
def _install_camera(self) -> None:
|
||||
"""Pre-install the camera overlay so it persists across open/close."""
|
||||
self.install_screen(CameraOverlay(), name="camera-overlay")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# App-level position poll
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -246,6 +258,25 @@ class BirdcageApp(App):
|
|||
"""Called when the console overlay is dismissed."""
|
||||
self._console_visible = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Camera overlay
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def action_toggle_camera(self) -> None:
|
||||
"""Push or pop the camera capture overlay."""
|
||||
if self._camera_visible:
|
||||
try:
|
||||
self.pop_screen()
|
||||
except Exception:
|
||||
self._camera_visible = False
|
||||
else:
|
||||
self.push_screen("camera-overlay", callback=self._on_camera_dismissed)
|
||||
self._camera_visible = True
|
||||
|
||||
def _on_camera_dismissed(self, _result=None) -> None:
|
||||
"""Called when the camera overlay is dismissed."""
|
||||
self._camera_visible = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tab bar button handling
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -329,6 +360,16 @@ def main() -> None:
|
|||
default="https://space.warehack.ing",
|
||||
help="Craft API base URL",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--capture-dir",
|
||||
default="captures",
|
||||
help="Output directory for camera captures",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--camera-device",
|
||||
default="auto",
|
||||
help="Camera device (e.g., /dev/video0) or 'auto'",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
app = BirdcageApp()
|
||||
|
|
@ -337,6 +378,8 @@ def main() -> None:
|
|||
app.firmware_name = args.firmware
|
||||
app.skip_init = args.skip_init
|
||||
app.craft_url = args.craft_url
|
||||
app.capture_dir = args.capture_dir
|
||||
app.camera_device = args.camera_device
|
||||
try:
|
||||
app.run()
|
||||
except KeyboardInterrupt:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue