rocket-welder-sdk 0.0.1__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rocket_welder_sdk/__init__.py +8 -0
- rocket_welder_sdk/client.py +183 -0
- rocket_welder_sdk-1.0.1.dist-info/METADATA +36 -0
- rocket_welder_sdk-1.0.1.dist-info/RECORD +6 -0
- {rocket_welder_sdk-0.0.1.dist-info → rocket_welder_sdk-1.0.1.dist-info}/WHEEL +1 -1
- rocket_welder_sdk-1.0.1.dist-info/top_level.txt +1 -0
- rocket_welder_camera/__init__.py +0 -0
- rocket_welder_camera/camera.py +0 -76
- rocket_welder_sdk-0.0.1.dist-info/METADATA +0 -51
- rocket_welder_sdk-0.0.1.dist-info/RECORD +0 -8
- rocket_welder_sdk-0.0.1.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- tests/test_camera.py +0 -36
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RocketWelder client implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Optional, Callable, Dict, Any, Iterator
|
|
10
|
+
import numpy as np
|
|
11
|
+
import cv2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Client:
|
|
15
|
+
"""Client for RocketWelder video streaming services."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, connection_string: str):
|
|
18
|
+
"""
|
|
19
|
+
Initialize client with connection string.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
connection_string: Connection string (e.g., "shm://buffer_name")
|
|
23
|
+
"""
|
|
24
|
+
self.connection_string = connection_string
|
|
25
|
+
self._callback: Optional[Callable[[np.ndarray], None]] = None
|
|
26
|
+
self._callback_with_metadata: Optional[Callable[[np.ndarray, Dict[str, Any]], None]] = None
|
|
27
|
+
self._running = False
|
|
28
|
+
self._thread: Optional[threading.Thread] = None
|
|
29
|
+
|
|
30
|
+
# TODO: Parse connection string
|
|
31
|
+
# TODO: Initialize based on protocol (shm://, mjpeg+http://, etc.)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_args(cls, argv: list) -> 'Client':
|
|
35
|
+
"""
|
|
36
|
+
Create client from command line arguments and environment variables.
|
|
37
|
+
Environment variable CONNECTION_STRING is checked first, then overridden by args.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
argv: Command line arguments (typically sys.argv)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Client instance
|
|
44
|
+
"""
|
|
45
|
+
# Check environment variable first
|
|
46
|
+
connection_string = os.environ.get('CONNECTION_STRING')
|
|
47
|
+
|
|
48
|
+
# Override with command line args if present
|
|
49
|
+
if argv:
|
|
50
|
+
for arg in argv[1:]: # Skip program name
|
|
51
|
+
if (arg.startswith('shm://') or
|
|
52
|
+
arg.startswith('mjpeg+http://') or
|
|
53
|
+
arg.startswith('mjpeg+tcp://')):
|
|
54
|
+
connection_string = arg
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
return cls(connection_string or 'shm://default')
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_env(cls) -> 'Client':
|
|
61
|
+
"""
|
|
62
|
+
Create client from environment variable CONNECTION_STRING.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Client instance
|
|
66
|
+
"""
|
|
67
|
+
connection_string = os.environ.get('CONNECTION_STRING', 'shm://default')
|
|
68
|
+
return cls(connection_string)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_connection_string(cls, connection_string: str) -> 'Client':
|
|
72
|
+
"""
|
|
73
|
+
Create client from a specific connection string.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
connection_string: Connection string
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Client instance
|
|
80
|
+
"""
|
|
81
|
+
return cls(connection_string)
|
|
82
|
+
|
|
83
|
+
def on_frame(self, callback: Callable) -> Callable:
|
|
84
|
+
"""
|
|
85
|
+
Decorator/method to set frame processing callback.
|
|
86
|
+
|
|
87
|
+
The callback can have one of these signatures:
|
|
88
|
+
- callback(frame: np.ndarray) -> None
|
|
89
|
+
- callback(frame: np.ndarray, metadata: dict) -> None
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
callback: Function to process frames
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The callback function (for decorator usage)
|
|
96
|
+
"""
|
|
97
|
+
import inspect
|
|
98
|
+
sig = inspect.signature(callback)
|
|
99
|
+
param_count = len(sig.parameters)
|
|
100
|
+
|
|
101
|
+
if param_count == 1:
|
|
102
|
+
self._callback = callback
|
|
103
|
+
elif param_count == 2:
|
|
104
|
+
self._callback_with_metadata = callback
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError(f"Callback must have 1 or 2 parameters, got {param_count}")
|
|
107
|
+
|
|
108
|
+
return callback
|
|
109
|
+
|
|
110
|
+
def start(self):
|
|
111
|
+
"""Start frame processing."""
|
|
112
|
+
if self._running:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if not self._callback and not self._callback_with_metadata:
|
|
116
|
+
raise RuntimeError("Frame callback must be set before starting")
|
|
117
|
+
|
|
118
|
+
self._running = True
|
|
119
|
+
self._thread = threading.Thread(target=self._process_frames)
|
|
120
|
+
self._thread.daemon = True
|
|
121
|
+
self._thread.start()
|
|
122
|
+
|
|
123
|
+
def stop(self):
|
|
124
|
+
"""Stop frame processing."""
|
|
125
|
+
if not self._running:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
self._running = False
|
|
129
|
+
if self._thread:
|
|
130
|
+
self._thread.join(timeout=5.0)
|
|
131
|
+
self._thread = None
|
|
132
|
+
|
|
133
|
+
def is_running(self) -> bool:
|
|
134
|
+
"""Check if client is running."""
|
|
135
|
+
return self._running
|
|
136
|
+
|
|
137
|
+
def frames(self) -> Iterator[np.ndarray]:
|
|
138
|
+
"""
|
|
139
|
+
Iterator interface for frame processing.
|
|
140
|
+
|
|
141
|
+
Yields:
|
|
142
|
+
Frame as numpy array
|
|
143
|
+
"""
|
|
144
|
+
# TODO: Implement actual frame reading
|
|
145
|
+
# For now, generate dummy frames for testing
|
|
146
|
+
while True:
|
|
147
|
+
# Create dummy frame
|
|
148
|
+
frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
|
149
|
+
yield frame
|
|
150
|
+
time.sleep(0.033) # ~30 FPS
|
|
151
|
+
|
|
152
|
+
def _process_frames(self):
|
|
153
|
+
"""Internal method to process frames in thread."""
|
|
154
|
+
# TODO: Implement actual frame processing
|
|
155
|
+
# For now, generate dummy frames for testing
|
|
156
|
+
while self._running:
|
|
157
|
+
# Create dummy frame
|
|
158
|
+
frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
|
159
|
+
|
|
160
|
+
# Create dummy metadata
|
|
161
|
+
metadata = {
|
|
162
|
+
'timestamp': time.time(),
|
|
163
|
+
'format': 'BGR',
|
|
164
|
+
'width': 640,
|
|
165
|
+
'height': 480
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Call appropriate callback
|
|
169
|
+
if self._callback:
|
|
170
|
+
self._callback(frame)
|
|
171
|
+
elif self._callback_with_metadata:
|
|
172
|
+
self._callback_with_metadata(frame, metadata)
|
|
173
|
+
|
|
174
|
+
time.sleep(0.033) # ~30 FPS
|
|
175
|
+
|
|
176
|
+
def __enter__(self):
|
|
177
|
+
"""Context manager entry."""
|
|
178
|
+
self.start()
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
182
|
+
"""Context manager exit."""
|
|
183
|
+
self.stop()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rocket-welder-sdk
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Client library for RocketWelder video streaming services
|
|
5
|
+
Home-page: https://github.com/modelingevolution/rocket-welder-sdk
|
|
6
|
+
Author: RocketWelder
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Topic :: Multimedia :: Video
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: numpy>=1.20.0
|
|
20
|
+
Requires-Dist: opencv-python>=4.5.0
|
|
21
|
+
Requires-Dist: zerobuffer-ipc>=1.0.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
|
+
Requires-Dist: black>=22.0; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: classifier
|
|
28
|
+
Dynamic: description
|
|
29
|
+
Dynamic: description-content-type
|
|
30
|
+
Dynamic: home-page
|
|
31
|
+
Dynamic: provides-extra
|
|
32
|
+
Dynamic: requires-dist
|
|
33
|
+
Dynamic: requires-python
|
|
34
|
+
Dynamic: summary
|
|
35
|
+
|
|
36
|
+
Client library for RocketWelder video streaming services
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
rocket_welder_sdk/__init__.py,sha256=LT2gnntM29r0IaokaJYOBOIccEcIYZm6FU_t9Eg1pzY,164
|
|
2
|
+
rocket_welder_sdk/client.py,sha256=W4WZdzAJ5eHuDUxQZyR_2rI9i1w2Zxr-UfwcFzKOwDw,5686
|
|
3
|
+
rocket_welder_sdk-1.0.1.dist-info/METADATA,sha256=k9I5YiqYGlMBQu02e06DfWSHvzk0YLFs0dNNEIonIrM,1264
|
|
4
|
+
rocket_welder_sdk-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
rocket_welder_sdk-1.0.1.dist-info/top_level.txt,sha256=2iZvBjnwVCUW-uDE23-eJld5PZ9-mlPI69QiXM5IrTA,18
|
|
6
|
+
rocket_welder_sdk-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rocket_welder_sdk
|
rocket_welder_camera/__init__.py
DELETED
|
File without changes
|
rocket_welder_camera/camera.py
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# rocket_welder_camera/camera.py
|
|
2
|
-
import cv2
|
|
3
|
-
import threading
|
|
4
|
-
import socket
|
|
5
|
-
import struct
|
|
6
|
-
import numpy as np
|
|
7
|
-
from urllib.parse import urlparse
|
|
8
|
-
|
|
9
|
-
class RocketWelderCamera:
|
|
10
|
-
def __init__(self, url):
|
|
11
|
-
self.url = url
|
|
12
|
-
parsed_url = urlparse(url)
|
|
13
|
-
self.host = parsed_url.hostname
|
|
14
|
-
self.port = parsed_url.port
|
|
15
|
-
self.stream_name = parsed_url.path[1:]
|
|
16
|
-
self.buffer_size = 10
|
|
17
|
-
self.circular_buffer = [None] * self.buffer_size
|
|
18
|
-
self.current_frame = -1
|
|
19
|
-
self.lock = threading.Lock()
|
|
20
|
-
self.thread = threading.Thread(target=self.receive_frames)
|
|
21
|
-
self.thread.daemon = True
|
|
22
|
-
self.thread.start()
|
|
23
|
-
|
|
24
|
-
@staticmethod
|
|
25
|
-
def write_prefixed_ascii_string(socket, value):
|
|
26
|
-
# Encode the string to ASCII bytes
|
|
27
|
-
name = value.encode('ascii')
|
|
28
|
-
|
|
29
|
-
# Send the length of the string as a single byte
|
|
30
|
-
length_byte = len(name).to_bytes(1, 'little')
|
|
31
|
-
socket.send(length_byte)
|
|
32
|
-
|
|
33
|
-
# Send the actual ASCII bytes
|
|
34
|
-
socket.send(name)
|
|
35
|
-
|
|
36
|
-
def receive_frames(self):
|
|
37
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
38
|
-
sock.connect((self.host, self.port))
|
|
39
|
-
self.write_prefixed_ascii_string(sock,self.stream_name)
|
|
40
|
-
|
|
41
|
-
while True:
|
|
42
|
-
header_data = sock.recv(32)
|
|
43
|
-
if len(header_data) != 32:
|
|
44
|
-
continue
|
|
45
|
-
|
|
46
|
-
frame_number, frame_size, stream_position, xor = struct.unpack('<QQQQ', header_data)
|
|
47
|
-
xor_check = frame_number ^ frame_size ^ stream_position
|
|
48
|
-
if frame_size == 0 or xor_check != xor:
|
|
49
|
-
continue
|
|
50
|
-
|
|
51
|
-
frame_size = int(frame_size)
|
|
52
|
-
frame_data = b''
|
|
53
|
-
|
|
54
|
-
while len(frame_data) < frame_size:
|
|
55
|
-
packet = sock.recv(frame_size - len(frame_data))
|
|
56
|
-
if not packet:
|
|
57
|
-
break
|
|
58
|
-
frame_data += packet
|
|
59
|
-
|
|
60
|
-
if len(frame_data) != frame_size:
|
|
61
|
-
continue
|
|
62
|
-
|
|
63
|
-
frame = np.frombuffer(frame_data, dtype=np.uint8)
|
|
64
|
-
image = cv2.imdecode(frame, cv2.IMREAD_COLOR)
|
|
65
|
-
|
|
66
|
-
with self.lock:
|
|
67
|
-
self.current_frame = (self.current_frame + 1) % self.buffer_size
|
|
68
|
-
self.circular_buffer[self.current_frame] = image
|
|
69
|
-
|
|
70
|
-
def get_frame(self):
|
|
71
|
-
with self.lock:
|
|
72
|
-
frame = self.circular_buffer[self.current_frame]
|
|
73
|
-
if frame is not None:
|
|
74
|
-
return frame
|
|
75
|
-
else:
|
|
76
|
-
return None
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: rocket-welder-sdk
|
|
3
|
-
Version: 0.0.1
|
|
4
|
-
Summary: Supporting sdk for RocketWelder
|
|
5
|
-
Home-page: https://github.com/rocket-welder-sdk/rocket-welder-sdk
|
|
6
|
-
Author: Rafal Maciag
|
|
7
|
-
Author-email: rafal.maciag@modelingevolution.com
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.6
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: numpy
|
|
14
|
-
Requires-Dist: opencv-python
|
|
15
|
-
Requires-Dist: requests
|
|
16
|
-
|
|
17
|
-
# SDK for Rocket Welder
|
|
18
|
-
|
|
19
|
-
Example
|
|
20
|
-
|
|
21
|
-
```python
|
|
22
|
-
# main.py
|
|
23
|
-
import cv2
|
|
24
|
-
from rocket_welder_camera.camera import RocketWelderCamera
|
|
25
|
-
|
|
26
|
-
def main():
|
|
27
|
-
url = 'tcp://{YOUR-HOST}:{YOUR-PORT}/{STREAM-NAME}'
|
|
28
|
-
cam = RocketWelderCamera(url)
|
|
29
|
-
|
|
30
|
-
cv2.namedWindow('Frame', cv2.WINDOW_NORMAL)
|
|
31
|
-
|
|
32
|
-
while True:
|
|
33
|
-
frame = cam.get_frame()
|
|
34
|
-
if frame is not None:
|
|
35
|
-
# Display the frame
|
|
36
|
-
cv2.imshow('Frame', frame)
|
|
37
|
-
|
|
38
|
-
# Print the current frame number
|
|
39
|
-
# print(f'Frame number: {cam.current_frame}')
|
|
40
|
-
|
|
41
|
-
# Exit loop if 'q' is pressed
|
|
42
|
-
if cv2.waitKey(1) & 0xFF == ord('q'):
|
|
43
|
-
break
|
|
44
|
-
|
|
45
|
-
cv2.destroyAllWindows()
|
|
46
|
-
|
|
47
|
-
if __name__ == '__main__':
|
|
48
|
-
main()
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Usually, YOUR-PORT is 7000 and YOUR-STREAM is "default".
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
rocket_welder_camera/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
rocket_welder_camera/camera.py,sha256=IGyz6g6arKkc9-_JyK9GWa_iT5tE_t6qGk4EEw1kZcI,2629
|
|
3
|
-
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
tests/test_camera.py,sha256=8oHvTbQRhiCp8cYrEuTkLUpzpTROMd7m3paNLLAGg0c,1255
|
|
5
|
-
rocket_welder_sdk-0.0.1.dist-info/METADATA,sha256=T2gcVRA0M91XbN7RzPpIc42-Fx69kJhYidvjE1mpFm0,1364
|
|
6
|
-
rocket_welder_sdk-0.0.1.dist-info/WHEEL,sha256=rWxmBtp7hEUqVLOnTaDOPpR-cZpCDkzhhcBce-Zyd5k,91
|
|
7
|
-
rocket_welder_sdk-0.0.1.dist-info/top_level.txt,sha256=qQCTu4qgoQUYLOGduATIONjktVBNsnWiYFNVB5_bEyY,27
|
|
8
|
-
rocket_welder_sdk-0.0.1.dist-info/RECORD,,
|
tests/__init__.py
DELETED
|
File without changes
|
tests/test_camera.py
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# tests/test_camera.py
|
|
2
|
-
import unittest
|
|
3
|
-
from unittest.mock import patch, MagicMock
|
|
4
|
-
import numpy as np
|
|
5
|
-
from rocket_welder_camera.camera import RocketWelderCamera
|
|
6
|
-
import struct
|
|
7
|
-
|
|
8
|
-
class TestRocketWelderCamera(unittest.TestCase):
|
|
9
|
-
@patch('rocket_welder_camera.camera.socket.socket')
|
|
10
|
-
@patch('rocket_welder_camera.camera.cv2.imdecode')
|
|
11
|
-
def test_receive_frames(self, mock_imdecode, mock_socket):
|
|
12
|
-
mock_sock_instance = MagicMock()
|
|
13
|
-
mock_socket.return_value = mock_sock_instance
|
|
14
|
-
mock_sock_instance.recv.side_effect = [
|
|
15
|
-
struct.pack('>QQQ', 1, 5, 1), # Mock header
|
|
16
|
-
b'\x00\x01\x02\x03\x04' # Mock frame data
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
mock_imdecode.return_value = np.zeros((480, 640, 3), dtype=np.uint8)
|
|
20
|
-
|
|
21
|
-
camera = RocketWelderCamera('tcp://pi-51:8082/a')
|
|
22
|
-
camera.receive_frames()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
frame = camera.get_frame()
|
|
26
|
-
|
|
27
|
-
self.assertIsNotNone(frame)
|
|
28
|
-
self.assertEqual(frame.shape, (480, 640, 3))
|
|
29
|
-
|
|
30
|
-
def test_get_frame_empty_buffer(self):
|
|
31
|
-
camera = RocketWelderCamera('tcp://pi-51:8082/a')
|
|
32
|
-
frame = camera.get_frame()
|
|
33
|
-
self.assertIsNone(frame)
|
|
34
|
-
|
|
35
|
-
if __name__ == '__main__':
|
|
36
|
-
unittest.main()
|