384 lines
14 KiB
Python
384 lines
14 KiB
Python
#!/bin/python
|
||
|
||
import io
|
||
import time
|
||
import traceback
|
||
from threading import Thread, Event, Lock
|
||
import gi
|
||
from PIL import Image
|
||
|
||
from streamer_utils import SocketBlocksWrapper, read_json_config
|
||
|
||
gi.require_version("Gtk", "3.0")
|
||
from gi.repository import Gtk, GLib, GdkPixbuf, Gdk
|
||
|
||
CLASSES = read_json_config('classes.json')
|
||
CONFIG = read_json_config('client-config.json')
|
||
|
||
|
||
class ConnectionDaemon:
|
||
def __init__(self, address, port, object_receive_callback: callable):
|
||
self._address = address
|
||
self._port = port
|
||
self._object_receive_callback = object_receive_callback
|
||
|
||
self._sock = None
|
||
self._lock = Lock()
|
||
|
||
self.__login = None
|
||
self.__password = None
|
||
|
||
# объект события, только для ожидания авторизации
|
||
self.__auth_event = Event()
|
||
self.__auth_callback = None
|
||
|
||
self.__auth_callback_is_done = False
|
||
self.__auth_success = False
|
||
|
||
self._conn_thread = Thread(target=self.__run, daemon=True)
|
||
self._conn_thread.start()
|
||
|
||
def auth(self, login, password, callback: callable):
|
||
with self._lock:
|
||
self.__login = login
|
||
self.__password = password
|
||
self.__auth_callback = callback
|
||
self.__auth_event.set()
|
||
|
||
def __do_session(self, login, password, auth_callback: callable):
|
||
try:
|
||
with SocketBlocksWrapper.connect(CONFIG["server-address"], CONFIG["server-port"]) as sock:
|
||
with self._lock:
|
||
self._sock = sock
|
||
sock.write_object(
|
||
{
|
||
'type': 'auth',
|
||
'client-type': 'client',
|
||
'target': login,
|
||
'password': password
|
||
})
|
||
res = sock.read_object()
|
||
print(res)
|
||
if 'status' in res:
|
||
if res['status'] == 'success':
|
||
self.__auth_success = True
|
||
if auth_callback is not None:
|
||
auth_callback(True, "success")
|
||
self.__auth_callback_is_done = True
|
||
while True:
|
||
res = sock.read_object()
|
||
if res is None:
|
||
break
|
||
with self._lock:
|
||
callback = self._object_receive_callback
|
||
callback(res)
|
||
else:
|
||
self.__auth_callback_is_done = True
|
||
if auth_callback is not None:
|
||
auth_callback(False, res['description'])
|
||
else:
|
||
raise Exception("'status' is not defined in response")
|
||
except Exception:
|
||
traceback.print_exc()
|
||
finally:
|
||
with self._lock:
|
||
self._sock = None
|
||
|
||
def __run(self):
|
||
# нужно ждать данных авторизации
|
||
while True:
|
||
if not self.__auth_success:
|
||
self.__auth_event.wait()
|
||
with self._lock:
|
||
login, password, callback = self.__login, self.__password, self.__auth_callback
|
||
self.__auth_callback_is_done = False
|
||
self.__auth_event.clear()
|
||
|
||
if login is None or password is None:
|
||
continue
|
||
|
||
# делаем сессию
|
||
self.__do_session(login, password, callback)
|
||
|
||
if not self.__auth_callback_is_done and callback is not None and callable(callback):
|
||
callback(False, "Ошибка ввода-вывода")
|
||
|
||
if not self.__auth_success:
|
||
with self._lock:
|
||
if not self.__auth_event.is_set():
|
||
self.__login = None
|
||
self.__password = None
|
||
else:
|
||
time.sleep(5)
|
||
print("Try to reconnect...")
|
||
|
||
def send_object(self, obj: dict):
|
||
try:
|
||
self._sock.write_object(obj)
|
||
except Exception as e:
|
||
# traceback.print_exc()
|
||
print(f"Failed to send command {obj} ({e})")
|
||
|
||
|
||
class MainWindow(Gtk.Window):
|
||
def __create_main_widget(self):
|
||
# основная "коробка"
|
||
root_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||
root_box.connect("key-press-event", self.on_key_press)
|
||
|
||
self.image = Gtk.Image()
|
||
self.image.set_vexpand(True)
|
||
self.image.set_can_focus(True)
|
||
|
||
no_connection_label = Gtk.Label(label="Нет подключения к плате")
|
||
no_connection_label.set_vexpand(True)
|
||
no_connection_label.set_hexpand(True)
|
||
|
||
no_video_label = Gtk.Label(label="Плата подключена\nОжидание действия пользователя")
|
||
no_video_label.set_vexpand(True)
|
||
no_video_label.set_hexpand(True)
|
||
|
||
self._image_stack = Gtk.Stack()
|
||
self._image_stack.set_transition_type(Gtk.StackTransitionType.SLIDE_DOWN)
|
||
self._image_stack.add_named(no_connection_label, "no-connection")
|
||
self._image_stack.add_named(no_video_label, "no-video")
|
||
self._image_stack.add_named(self.image, "video")
|
||
|
||
root_box.pack_start(self._image_stack, expand=True, fill=True, padding=0)
|
||
|
||
# нижняя сторона окна, панелька с управлением
|
||
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
|
||
toolbar.set_halign(Gtk.Align.CENTER)
|
||
|
||
button_start = Gtk.Button(label="Старт")
|
||
button_start.connect("clicked", lambda widget: self.__send_action('start'))
|
||
|
||
button_stop = Gtk.Button(label="Стоп")
|
||
button_stop.connect("clicked", lambda widget: self.__send_action('stop'))
|
||
|
||
button_lazer_start = Gtk.Button(label="Старт лазер")
|
||
button_lazer_start.connect("clicked", lambda widget: self.__send_action('lazerOn'))
|
||
|
||
button_lazer_stop = Gtk.Button(label="Стоп лазер")
|
||
button_lazer_stop.connect("clicked", lambda widget: self.__send_action('lazerOff'))
|
||
|
||
self.select_class = Gtk.ComboBoxText()
|
||
self.select_class.connect("changed", self.on_class_select)
|
||
|
||
sorted_list = [c for c in CLASSES]
|
||
self.select_class.append_text("<__ (все) __>")
|
||
for c in sorted_list:
|
||
self.select_class.append_text(c["label"])
|
||
|
||
toolbar.pack_start(button_start, expand=False, fill=False, padding=0)
|
||
toolbar.pack_start(button_stop, expand=False, fill=False, padding=0)
|
||
toolbar.pack_start(self.select_class, expand=False, fill=False, padding=0)
|
||
toolbar.pack_start(button_lazer_start, expand=False, fill=False, padding=0)
|
||
toolbar.pack_start(button_lazer_stop, expand=False, fill=False, padding=0)
|
||
|
||
root_box.pack_start(toolbar, expand=False, fill=True, padding=0)
|
||
|
||
return root_box
|
||
|
||
def __connection_auth_handler(self, result, message):
|
||
def gui_function():
|
||
self._spinner.stop()
|
||
self.__password_entry.set_editable(True)
|
||
self.__login_entry.set_editable(True)
|
||
self.__button_auth.set_sensitive(True)
|
||
if result:
|
||
self.root.set_visible_child_name("main")
|
||
else:
|
||
dialog = Gtk.MessageDialog(
|
||
transient_for=self,
|
||
flags=0,
|
||
message_type=Gtk.MessageType.ERROR,
|
||
buttons=Gtk.ButtonsType.OK,
|
||
text=message,
|
||
title="Ошибка аутентификации"
|
||
)
|
||
dialog.run()
|
||
dialog.destroy()
|
||
|
||
GLib.idle_add(lambda: gui_function())
|
||
|
||
def __auth_handler(self, widget):
|
||
self._spinner.start()
|
||
self.__password_entry.set_editable(False)
|
||
self.__login_entry.set_editable(False)
|
||
self.__button_auth.set_sensitive(False)
|
||
self._daemon.auth(self.__login_entry.get_text(),
|
||
self.__password_entry.get_text(),
|
||
self.__connection_auth_handler)
|
||
|
||
def __create_auth_widget(self):
|
||
hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||
hbox.set_halign(Gtk.Align.CENTER)
|
||
hbox.set_valign(Gtk.Align.CENTER)
|
||
|
||
logo = Gtk.Image.new_from_file("logo.png")
|
||
logo_label = Gtk.Label(label="Ведапроект")
|
||
self.__button_auth = Gtk.Button(label="Вход")
|
||
self.__button_auth.connect("clicked", self.__auth_handler)
|
||
|
||
label_login = Gtk.Label(label="Логин")
|
||
self.__login_entry = Gtk.Entry(can_default=True)
|
||
|
||
label_password = Gtk.Label(label="Пароль")
|
||
self.__password_entry = Gtk.Entry()
|
||
self.__password_entry.set_visibility(False)
|
||
|
||
self.__login_entry.connect("activate", lambda widget: self.__password_entry.grab_focus())
|
||
self.__password_entry.connect("activate", self.__auth_handler)
|
||
|
||
hbox.pack_start(logo, expand=False, fill=False, padding=0)
|
||
hbox.pack_start(logo_label, expand=False, fill=False, padding=10)
|
||
|
||
hbox.pack_start(label_login, expand=False, fill=False, padding=10)
|
||
hbox.pack_start(self.__login_entry, expand=False, fill=False, padding=0)
|
||
|
||
hbox.pack_start(label_password, expand=False, fill=False, padding=10)
|
||
hbox.pack_start(self.__password_entry, expand=False, fill=False, padding=0)
|
||
|
||
hbox.pack_start(self.__button_auth, expand=False, fill=False, padding=10)
|
||
|
||
# спинер, хрень которая будет показывать что идет процесс подключения
|
||
self._spinner = Gtk.Spinner()
|
||
hbox.pack_start(self._spinner, True, True, 0)
|
||
|
||
return hbox
|
||
|
||
def __init__(self, **kwargs):
|
||
super().__init__(**kwargs)
|
||
|
||
self._remote_selected_class = False
|
||
|
||
self._visible_image_child = "no-connection"
|
||
|
||
self._daemon = ConnectionDaemon(CONFIG["server-address"], CONFIG["server-port"], self.__on_object_received)
|
||
|
||
self.set_default_size(600, 400)
|
||
self.set_title("Ведапроект")
|
||
|
||
# основная "коробка"
|
||
self.root = Gtk.Stack()
|
||
self.root.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
|
||
self.root.add_named(self.__create_auth_widget(), "auth")
|
||
self.root.add_named(self.__create_main_widget(), "main")
|
||
self.add(self.root)
|
||
|
||
# ---------------------- Handlers --------------------------
|
||
|
||
def _send_command(self, cmd):
|
||
self._daemon.send_object({'type': 'command', 'data': cmd})
|
||
|
||
def __send_action(self, act):
|
||
self._send_command({'action': act})
|
||
|
||
def __get_selected_class(self):
|
||
label = self.select_class.get_active_text()
|
||
for i in range(0, len(CLASSES)):
|
||
if CLASSES[i]["label"] == label:
|
||
# теперь метка "не выбрано" имеет индекс 0, поэтому все остальные смещены на +1
|
||
return i
|
||
|
||
return None
|
||
|
||
def on_class_select(self, widget):
|
||
c = self.__get_selected_class()
|
||
if self._remote_selected_class:
|
||
self._remote_selected_class = False
|
||
else:
|
||
if c is None:
|
||
cl = "__all__"
|
||
else:
|
||
cl = CLASSES[c]["class"]
|
||
print(f'select class: id={c}, {cl}')
|
||
self._send_command({'action': 'set-class', 'class': cl})
|
||
|
||
def on_key_press(self, window, event_key: Gdk.EventKey):
|
||
commands = {
|
||
"up": [Gdk.KEY_w, Gdk.KEY_W], # Gdk.KEY_Up
|
||
"left": [Gdk.KEY_a, Gdk.KEY_A], # Gdk.KEY_Left
|
||
"down": [Gdk.KEY_s, Gdk.KEY_S], # Gdk.KEY_Down
|
||
"right": [Gdk.KEY_d, Gdk.KEY_D], # Gdk.KEY_Right
|
||
"start": Gdk.KEY_z,
|
||
"stop": Gdk.KEY_x,
|
||
}
|
||
keyval = event_key.get_keyval()
|
||
action = None
|
||
if keyval[0]:
|
||
keyval = keyval[1]
|
||
# прошерстим команды
|
||
for key in commands:
|
||
if type(commands[key]) == list:
|
||
if keyval in commands[key]:
|
||
action = key
|
||
break
|
||
else:
|
||
if commands[key] == keyval:
|
||
action = key
|
||
break
|
||
|
||
if action is not None:
|
||
self._send_command({'action': action})
|
||
|
||
def __update_image(self, image):
|
||
need_frame = "no-video"
|
||
if image is not None:
|
||
need_frame = "video"
|
||
data = image.tobytes()
|
||
w, h = image.size
|
||
data = GLib.Bytes.new(data)
|
||
pix = GdkPixbuf.Pixbuf.new_from_bytes(data, GdkPixbuf.Colorspace.RGB, False, 8, w, h, w * 3)
|
||
self.image.set_from_pixbuf(pix)
|
||
|
||
if self._visible_image_child is not need_frame:
|
||
self._have_image = need_frame
|
||
self._image_stack.set_visible_child_name(need_frame)
|
||
|
||
# print(f"{datetime.datetime.now()} MyWindow: image received")
|
||
|
||
def __update_selected_class(self, cl):
|
||
c = self.__get_selected_class()
|
||
|
||
if c is None:
|
||
c = 0
|
||
else:
|
||
c += 1
|
||
|
||
if cl < -1:
|
||
cl = 0
|
||
else:
|
||
cl += 1
|
||
|
||
if c != cl:
|
||
if cl != c:
|
||
self._remote_selected_class = True
|
||
# то же самое, из-за метки все классы уехали на +1
|
||
self.select_class.set_active(cl)
|
||
|
||
def __on_object_received(self, res):
|
||
if res["type"] == "video":
|
||
if res["data"] is None:
|
||
GLib.idle_add(lambda: self.__update_image(None))
|
||
else:
|
||
data = io.BytesIO(res["data"])
|
||
img = Image.open(data)
|
||
GLib.idle_add(lambda: self.__update_image(img))
|
||
|
||
if 'selected-class' in res:
|
||
GLib.idle_add(lambda: self.__update_selected_class(res['selected-class']))
|
||
|
||
|
||
def main():
|
||
""" Run the main application"""
|
||
win = MainWindow()
|
||
win.connect("destroy", Gtk.main_quit)
|
||
win.show_all()
|
||
Gtk.main()
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|