Camera connection added
This commit is contained in:
3
global_state.py
Normal file
3
global_state.py
Normal file
@ -0,0 +1,3 @@
|
||||
class GlobalState:
|
||||
def __init__(self):
|
||||
pass
|
1
main.py → negstation.py
Normal file → Executable file
1
main.py → negstation.py
Normal file → Executable file
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
import ui
|
||||
|
||||
if __name__=="__main__":
|
37
negstation_layout.ini
Normal file
37
negstation_layout.ini
Normal file
@ -0,0 +1,37 @@
|
||||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,19
|
||||
Size=1280,701
|
||||
Collapsed=0
|
||||
|
||||
[Window][###28]
|
||||
Pos=321,420
|
||||
Size=959,300
|
||||
Collapsed=0
|
||||
DockId=0x00000002,0
|
||||
|
||||
[Window][###30]
|
||||
Pos=0,19
|
||||
Size=319,701
|
||||
Collapsed=0
|
||||
DockId=0x0000000B,0
|
||||
|
||||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Size=400,400
|
||||
Collapsed=0
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=1280,701 Split=X
|
||||
DockNode ID=0x0000000B Parent=0x7C6B3D9B SizeRef=319,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x0000000C Parent=0x7C6B3D9B SizeRef=959,701 Split=X
|
||||
DockNode ID=0x00000009 Parent=0x0000000C SizeRef=319,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x0000000A Parent=0x0000000C SizeRef=959,701 Split=X
|
||||
DockNode ID=0x00000007 Parent=0x0000000A SizeRef=340,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x00000008 Parent=0x0000000A SizeRef=938,701 Split=X
|
||||
DockNode ID=0x00000005 Parent=0x00000008 SizeRef=300,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x00000006 Parent=0x00000008 SizeRef=978,701 Split=X
|
||||
DockNode ID=0x00000003 Parent=0x00000006 SizeRef=318,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x00000004 Parent=0x00000006 SizeRef=960,701 Split=Y
|
||||
DockNode ID=0x00000001 Parent=0x00000004 SizeRef=979,399 CentralNode=1
|
||||
DockNode ID=0x00000002 Parent=0x00000004 SizeRef=979,300 Selected=0xA4B861D9
|
||||
|
14
negstation_widgets.json
Normal file
14
negstation_widgets.json
Normal file
@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"widget_type": "LogWidget",
|
||||
"config": {
|
||||
"label": "LogWidget"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget_type": "CamControlWidget",
|
||||
"config": {
|
||||
"label": "CamControlWidget"
|
||||
}
|
||||
}
|
||||
]
|
8
ui.py
8
ui.py
@ -6,6 +6,7 @@ import importlib
|
||||
import inspect
|
||||
from collections import deque
|
||||
|
||||
import global_state
|
||||
from widgets.base_widget import BaseWidget
|
||||
|
||||
class DpgLogHandler(logging.Handler):
|
||||
@ -29,6 +30,8 @@ class LayoutManager:
|
||||
def __init__(self):
|
||||
self.active_widgets = {}
|
||||
self.widget_classes = {}
|
||||
self.updating_widgets = []
|
||||
self.global_state = global_state.GlobalState()
|
||||
|
||||
def discover_and_register_widgets(self, directory="widgets"):
|
||||
"""Dynamically discovers and registers widgets from a given directory."""
|
||||
@ -63,7 +66,7 @@ class LayoutManager:
|
||||
return
|
||||
config = {"label": widget_type}
|
||||
WidgetClass = self.widget_classes[widget_type]
|
||||
widget_instance = WidgetClass(widget_type, config, self)
|
||||
widget_instance = WidgetClass(widget_type, config, self, self.global_state)
|
||||
logging.info(f"Creating new widget of type: {widget_type}")
|
||||
self.active_widgets[widget_type] = widget_instance
|
||||
widget_instance.create()
|
||||
@ -87,6 +90,9 @@ class LayoutManager:
|
||||
if "LogWidget" in self.active_widgets:
|
||||
# We need to pass the handler to the update method
|
||||
self.active_widgets["LogWidget"].update_logs(log_handler)
|
||||
for w in self.updating_widgets:
|
||||
if w in self.active_widgets:
|
||||
self.active_widgets[w].update()
|
||||
|
||||
@staticmethod
|
||||
def run():
|
||||
|
@ -3,10 +3,11 @@ import logging
|
||||
|
||||
class BaseWidget:
|
||||
"""A base class to handle common functionality for all widgets."""
|
||||
def __init__(self, widget_type: str, config: dict, layout_manager):
|
||||
def __init__(self, widget_type: str, config: dict, layout_manager, global_state):
|
||||
self.widget_type = widget_type
|
||||
self.config = config
|
||||
self.layout_manager = layout_manager
|
||||
self.global_state = global_state
|
||||
self.window_tag = f"widget_win_{self.widget_type}"
|
||||
|
||||
def create(self):
|
||||
@ -15,6 +16,9 @@ class BaseWidget:
|
||||
def get_config(self) -> dict:
|
||||
return self.config
|
||||
|
||||
def update(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _on_window_close(self, sender, app_data, user_data):
|
||||
logging.info(f"Hiding widget: {self.widget_type}")
|
||||
dpg.configure_item(self.window_tag, show=False)
|
157
widgets/camcontrol_widget.py
Normal file
157
widgets/camcontrol_widget.py
Normal file
@ -0,0 +1,157 @@
|
||||
# in widgets/camera_widget.py
|
||||
import dearpygui.dearpygui as dpg
|
||||
import gphoto2 as gp
|
||||
import logging
|
||||
import threading
|
||||
import queue
|
||||
|
||||
from .base_widget import BaseWidget
|
||||
|
||||
# Set up a logger specific to this widget for clear debugging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel("DEBUG")
|
||||
|
||||
class CamControlWidget(BaseWidget):
|
||||
def __init__(self, widget_type: str, config: dict, layout_manager, app_state):
|
||||
super().__init__(widget_type, config, layout_manager, app_state)
|
||||
layout_manager.updating_widgets.append("CamControlWidget")
|
||||
|
||||
# A dictionary to map user-friendly camera names to technical port strings
|
||||
self.camera_map = {}
|
||||
|
||||
# Queues for communicating with the worker thread
|
||||
self.command_queue = queue.Queue()
|
||||
self.result_queue = queue.Queue()
|
||||
|
||||
# Start the background worker thread. It's a daemon so it exits with the app.
|
||||
self.worker_thread = threading.Thread(target=self._camera_worker, daemon=True)
|
||||
self.worker_thread.start()
|
||||
|
||||
def create(self):
|
||||
if dpg.does_item_exist(self.window_tag): return
|
||||
|
||||
with dpg.window(label="Camera Control", tag=self.window_tag, on_close=self._on_window_close, width=320, height=160):
|
||||
|
||||
# This group contains all controls visible when disconnected
|
||||
with dpg.group(tag=f"connection_group_{self.widget_type}") as self.connection_group_tag:
|
||||
dpg.add_button(label="Detect Cameras", callback=lambda: self.command_queue.put(('DETECT', None)), width=-1)
|
||||
with dpg.group(horizontal=True):
|
||||
self.camera_combo_tag = dpg.add_combo(label="Camera", items=[], width=-115)
|
||||
|
||||
def connect_callback():
|
||||
selected_name = dpg.get_value(self.camera_combo_tag)
|
||||
if selected_name and selected_name in self.camera_map:
|
||||
port = self.camera_map[selected_name]
|
||||
self.command_queue.put(('CONNECT', port))
|
||||
else:
|
||||
logger.warning("No camera selected to connect to.")
|
||||
|
||||
self.connect_button = dpg.add_button(label="Connect", callback=connect_callback)
|
||||
|
||||
# This button is only visible when connected
|
||||
self.disconnect_button_tag = dpg.add_button(
|
||||
label="Disconnect",
|
||||
callback=lambda: self.command_queue.put(('DISCONNECT', None)),
|
||||
show=False,
|
||||
width=-1
|
||||
)
|
||||
|
||||
self.status_text_tag = dpg.add_text("Status: Disconnected")
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
# Check for a result from the worker thread without blocking
|
||||
result_type, data = self.result_queue.get_nowait()
|
||||
|
||||
if result_type == 'STATUS':
|
||||
dpg.set_value(self.status_text_tag, f"Status: {data}")
|
||||
|
||||
elif result_type == 'CAMERAS_DETECTED':
|
||||
# data is a list of (name, port) tuples
|
||||
self.camera_map = {name: port for name, port in data}
|
||||
camera_names = list(self.camera_map.keys())
|
||||
dpg.configure_item(self.camera_combo_tag, items=camera_names)
|
||||
if camera_names:
|
||||
dpg.set_value(self.camera_combo_tag, camera_names[0])
|
||||
|
||||
elif result_type == 'CONNECTED' and data is True:
|
||||
# Hide connection controls and show the disconnect button
|
||||
dpg.configure_item(self.connection_group_tag, show=False)
|
||||
dpg.configure_item(self.disconnect_button_tag, show=True)
|
||||
|
||||
elif result_type == 'DISCONNECTED' and data is True:
|
||||
# Hide disconnect button and show connection controls
|
||||
dpg.configure_item(self.connection_group_tag, show=True)
|
||||
dpg.configure_item(self.disconnect_button_tag, show=False)
|
||||
|
||||
except queue.Empty:
|
||||
# This is expected when there are no new results from the worker
|
||||
pass
|
||||
|
||||
def _camera_worker(self):
|
||||
camera = None
|
||||
logger.info("Camera worker thread started.")
|
||||
|
||||
while True:
|
||||
try:
|
||||
command, args = self.command_queue.get()
|
||||
logger.info(f"Camera worker received command: {command}")
|
||||
|
||||
if command == 'DETECT':
|
||||
try:
|
||||
# Autodetect returns a list of (name, port) tuples
|
||||
camera_list = list(gp.Camera.autodetect())
|
||||
logger.debug(f"Cameras found: {str(camera_list)}")
|
||||
if not camera_list:
|
||||
self.result_queue.put(('STATUS', 'No cameras found.'))
|
||||
self.result_queue.put(('CAMERAS_DETECTED', []))
|
||||
continue
|
||||
|
||||
self.result_queue.put(('CAMERAS_DETECTED', camera_list))
|
||||
self.result_queue.put(('STATUS', f'Detected {len(camera_list)} camera(s).'))
|
||||
except Exception as e:
|
||||
logger.error(f"Error during camera detection: {e}")
|
||||
self.result_queue.put(('STATUS', 'Error during detection.'))
|
||||
|
||||
elif command == 'CONNECT':
|
||||
if camera is not None:
|
||||
self.result_queue.put(('STATUS', 'A camera is already connected.'))
|
||||
continue
|
||||
|
||||
port = args
|
||||
if not port:
|
||||
self.result_queue.put(('STATUS', 'No camera port selected.'))
|
||||
continue
|
||||
|
||||
try:
|
||||
camera = gp.Camera()
|
||||
port_info_list = gp.PortInfoList(); port_info_list.load()
|
||||
port_index = port_info_list.lookup_path(port)
|
||||
camera.set_port_info(port_info_list[port_index])
|
||||
|
||||
camera.init()
|
||||
self.result_queue.put(('STATUS', f"Connected successfully!"))
|
||||
self.result_queue.put(('CONNECTED', True))
|
||||
except gp.GPhoto2Error as ex:
|
||||
camera = None # Ensure camera is None on failure
|
||||
logger.error(f"GPhoto2 Error on connect: {ex}")
|
||||
self.result_queue.put(('STATUS', f"Error: {ex}"))
|
||||
self.result_queue.put(('CONNECTED', False))
|
||||
|
||||
elif command == 'DISCONNECT':
|
||||
if camera:
|
||||
try:
|
||||
camera.exit()
|
||||
camera = None # Critical to update state
|
||||
self.result_queue.put(('STATUS', 'Disconnected.'))
|
||||
self.result_queue.put(('DISCONNECTED', True))
|
||||
except gp.GPhoto2Error as ex:
|
||||
logger.error(f"Error on disconnect: {ex}")
|
||||
self.result_queue.put(('STATUS', f"Error on disconnect: {ex}"))
|
||||
else:
|
||||
self.result_queue.put(('STATUS', 'Already disconnected.'))
|
||||
|
||||
except Exception as e:
|
||||
# Broad exception to catch any other errors in the worker loop
|
||||
logger.error(f"An unexpected exception occurred in the camera worker: {e}")
|
||||
self.result_queue.put(('STATUS', f'Worker Error: {e}'))
|
@ -1,10 +0,0 @@
|
||||
import dearpygui.dearpygui as dpg
|
||||
from .base_widget import BaseWidget
|
||||
|
||||
class SimpleWidget(BaseWidget):
|
||||
"""A basic text widget to demonstrate dynamic loading."""
|
||||
def create(self):
|
||||
if dpg.does_item_exist(self.window_tag): return
|
||||
with dpg.window(label="Simple Widget", tag=self.window_tag, on_close=self._on_window_close, width=300, height=120):
|
||||
dpg.add_text("This widget was loaded dynamically!")
|
||||
dpg.add_button(label="A Button")
|
Reference in New Issue
Block a user