Skip to content

Commit e3a07c0

Browse files
committedNov 13, 2022
Add video streaming support, changes in UI layout
1 parent 9c00a65 commit e3a07c0

File tree

5 files changed

+109
-21
lines changed

5 files changed

+109
-21
lines changed
 

‎requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ click
55
RPi.GPIO
66
PySimpleGUI
77
thrift
8-
busio
8+
busio
9+
opencv-python

‎steam_deck/controller.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def __init__(self, thrift_client: Client) -> None:
5858
self.monitor_dev()
5959
self.log_queue = queue.Queue()
6060

61+
6162
def await_controller(self) -> str:
6263
logger.info("Waiting for controller")
6364
while True:
@@ -75,8 +76,6 @@ def monitor_dev(self) -> None:
7576
self.monitor_thread.start()
7677

7778

78-
79-
8079
def _monitor_dev(self, vibrate: bool = False) -> None: # TODO vibrate not implemented
8180
while True:
8281
if self._terminate_monitor:
@@ -112,4 +111,5 @@ def _monitor_dev(self, vibrate: bool = False) -> None: # TODO vibrate not impl
112111
def terminate(self) -> None:
113112
self._terminate_monitor = True
114113
self.monitor_thread.join()
114+
self.thrift_client = None
115115
return

‎steam_deck/network.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ def connect(host: str, port: int):
4646
client = LockedClient(protocol)
4747
transport.open()
4848

49-
return client
49+
return socket, client

‎steam_deck/ui.py

+56-17
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import logging
22
import time
33
import PySimpleGUI as sg
4+
45
from controller import SteamDeckController
5-
from network import connect, Client
6+
from video_stream import VideoStream
7+
from network import connect, LockedClient
68
from typing import Any, Dict, List, Optional
79
from spec.ttypes import ARR_status, Mode, Giat
810
import queue
@@ -16,8 +18,11 @@ class SteamDeckUI:
1618
def __init__(self):
1719
self.connected = False
1820
self.network_client: Client = None
19-
self._connect_layout = self.get_connect_layout()
20-
self._op_layout = self.get_op_layout()
21+
self.network_socket = None
22+
self.video_stream: VideoStream = None
23+
self.logs_view = True
24+
self._connect_layout = None
25+
self._op_layout = None
2126

2227
self.ui_state: Dict[str, Any] = {}
2328
self.robot_state = ARR_status(
@@ -33,13 +38,12 @@ def __init__(self):
3338
self.event_routes = {
3439
"_button_connect": self._button_connect,
3540
"_button_disconnect": self._button_disconnect,
36-
"_button_ping": self._button_ping
41+
"_button_ping": self._button_ping,
42+
"_button_view": self._button_view
3743
}
3844

39-
self._multiline_log = None
4045
self.controller: Optional[SteamDeckController] = None
4146

42-
4347
def init_connect_window(self):
4448
self._connect_layout = self.get_connect_layout()
4549
self.window = sg.Window("Connect", self._connect_layout, finalize=True, font=self.FONT)
@@ -52,16 +56,20 @@ def init_op_window(self):
5256
def get_connect_layout(self) -> List[List[Any]]:
5357
return [
5458
[
59+
sg.Text("RC: "),
5560
sg.Input('localhost', key="_input_ip_addr", size=32),
5661
sg.Input('9090', key="_input_port", size=5),
5762
],
63+
[
64+
sg.Text("VD: "),
65+
sg.Input('http://192.168.0.6:9999/stream', key='_input_video_addr', size=38)
66+
],
5867
[
5968
sg.Button("Connect", key="_button_connect"),
6069
],
6170
]
6271

6372
def get_op_layout(self) -> List[List[Any]]:
64-
self._multiline_log = sg.Multiline('', size=(64,18), key="_multiline_log", autoscroll=True)
6573
return [
6674
[
6775
sg.Text('M: N/A', key="_text_mode"),
@@ -72,14 +80,13 @@ def get_op_layout(self) -> List[List[Any]]:
7280
sg.Text('Battery: N/A', key="_text_battery"),
7381
],
7482
[
75-
# TODO insert image here
76-
self._multiline_log
83+
sg.Image(key="_image_stream", visible =(not self.logs_view)),
84+
sg.Multiline('', size=(64,18), key="_multiline_log", autoscroll=True, visible=self.logs_view)
7785
],
7886
[
7987
sg.Text("Connection quality: TBD"),
8088
sg.Button("Ping", key="_button_ping"),
81-
sg.Button("Video: OFF"),
82-
sg.Button("Logs: OFF"),
89+
sg.Button("View: LOGS", key="_button_view"),
8390
sg.Button("Disconnect", key="_button_disconnect")
8491
]
8592
]
@@ -96,33 +103,63 @@ def _button_connect(self):
96103
if not self.connected:
97104
host = self.ui_state["_input_ip_addr"]
98105
port = int(self.ui_state["_input_port"])
99-
self.network_client = connect(host, port)
106+
video_addr =self.ui_state["_input_video_addr"]
107+
self.network_socket, self.network_client = connect(host, port)
100108
self.connected = True
101109
logger.info("Connected to %s:%s", host, port)
102110
self.window.close()
103111
self.init_op_window()
104112
self.controller = SteamDeckController(self.network_client)
113+
self.video_stream = VideoStream(video_addr)
114+
if not self.logs_view:
115+
self.video_stream.enable()
105116
else:
106117
logger.warning("Already connected!")
107118

119+
108120
def _button_disconnect(self):
109121
if self.connected:
110-
# TODO do disconnect logic here! Close Socket/etc
122+
self.controller.terminate()
123+
self.controller = None
124+
self.network_socket.close()
125+
self.network_client = None
126+
self.video_stream.disable()
111127
self.window.close()
112128
self.init_connect_window()
113129
self.connected = False
114130
logger.info("Disconnected")
115131
else:
116132
logger.warning("Already disconnected!")
117133

134+
118135
def _button_ping(self):
119136
if self.connected:
120137
self.network_client.ping()
121138

139+
140+
def _button_view(self):
141+
txt_map = {
142+
True: "LOGS",
143+
False: "VIDEO"
144+
}
145+
146+
self.logs_view = not self.logs_view
147+
self.window["_multiline_log"].update(visible=self.logs_view)
148+
self.window['_image_stream'].update(visible=not self.logs_view)
149+
self.window["_button_view"].update("View: " + txt_map[self.logs_view])
150+
if self.logs_view:
151+
self.video_stream.disable()
152+
else:
153+
self.video_stream.enable()
154+
122155
def update_op_values(self):
123-
while not self.controller.log_queue.empty():
124-
record = self.controller.log_queue.get()
125-
self.window['_multiline_log'].update(record+"\n", append=True)
156+
if self.logs_view:
157+
while not self.controller.log_queue.empty():
158+
record = self.controller.log_queue.get()
159+
self.window['_multiline_log'].update(record+"\n", append=True)
160+
else:
161+
self.window['_image_stream'].update(data=self.video_stream.get_frame())
162+
126163
self.robot_state = self.network_client.get_status()
127164
self.window["_text_mode"].update("M: " + Mode._VALUES_TO_NAMES[self.robot_state.mode])
128165
if self.robot_state.mode == Mode.WALK:
@@ -142,14 +179,16 @@ def run(self):
142179
if self.connected:
143180
self.update_op_values()
144181
self.window.refresh()
145-
time.sleep(0.05)
182+
time.sleep(0.01)
146183

147184

148185
def refresh(self):
149186
self.window.refresh()
150187

188+
151189
def read(self):
152190
return window.read()
153191

192+
154193
def close(self):
155194
window.close()

‎steam_deck/video_stream.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import cv2
2+
import urllib.request
3+
import numpy as np
4+
import threading
5+
import logging
6+
7+
logger = logging.getLogger(__name__)
8+
9+
class VideoStream:
10+
11+
def __init__(self, addr: str): # 'http://192.168.0.6:9999/stream'
12+
self.addr = addr
13+
self.enabled = False
14+
self._frame = b''
15+
16+
17+
def _receive_video(self):
18+
stream = urllib.request.urlopen(self.addr)
19+
bin_data = b''
20+
timeout = 1
21+
while self.enabled:
22+
bin_data += stream.read(1024)
23+
a = bin_data.find(b'\xff\xd8')
24+
b = bin_data.find(b'\xff\xd9')
25+
if a != -1 and b != -1:
26+
jpg = bin_data[a:b+2]
27+
bin_data = bin_data[b+2:]
28+
img_data = cv2.imdecode(np.fromstring(jpg, dtype=np.uint8), cv2.IMREAD_COLOR)
29+
self._frame = cv2.imencode('.ppm', img_data)[1].tobytes()
30+
31+
def enable(self):
32+
if self.enabled:
33+
logger.warning("Unable to enable video stream. Already enabled")
34+
else:
35+
self.video_thread = threading.Thread(target=self._receive_video)
36+
self.video_thread.start()
37+
self.enabled = True
38+
39+
def disable(self):
40+
if not self.enabled:
41+
logger.warning("Unable to disable video stream. Already disabled")
42+
else:
43+
self.enabled = False
44+
self.video_thread.join() # TODO what if it hangs?
45+
46+
def get_frame(self):
47+
return self._frame
48+

0 commit comments

Comments
 (0)
Please sign in to comment.