rocket-welder-sdk 1.1.28__py3-none-any.whl → 1.1.29__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/controllers.py +156 -40
- rocket_welder_sdk-1.1.29.dist-info/METADATA +819 -0
- {rocket_welder_sdk-1.1.28.dist-info → rocket_welder_sdk-1.1.29.dist-info}/RECORD +5 -5
- rocket_welder_sdk-1.1.28.dist-info/METADATA +0 -627
- {rocket_welder_sdk-1.1.28.dist-info → rocket_welder_sdk-1.1.29.dist-info}/WHEEL +0 -0
- {rocket_welder_sdk-1.1.28.dist-info → rocket_welder_sdk-1.1.29.dist-info}/top_level.txt +0 -0
rocket_welder_sdk/controllers.py
CHANGED
|
@@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Callable, Optional
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
from zerobuffer import BufferConfig, Frame, Reader, Writer
|
|
16
16
|
from zerobuffer.duplex import DuplexChannelFactory, IImmutableDuplexServer
|
|
17
|
+
from zerobuffer.duplex.server import ImmutableDuplexServer
|
|
17
18
|
from zerobuffer.exceptions import WriterDeadException
|
|
18
19
|
|
|
19
20
|
from .connection_string import ConnectionMode, ConnectionString, Protocol
|
|
@@ -276,36 +277,14 @@ class OneWayShmController(IController):
|
|
|
276
277
|
bytes(metadata_bytes[: min(100, len(metadata_bytes))]),
|
|
277
278
|
)
|
|
278
279
|
|
|
279
|
-
#
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
# Decode UTF-8
|
|
284
|
-
metadata_str = metadata_bytes.decode("utf-8")
|
|
285
|
-
|
|
286
|
-
# Check if metadata is empty or all zeros
|
|
287
|
-
if not metadata_str or metadata_str == "\x00" * len(metadata_str):
|
|
288
|
-
logger.warning("Metadata is empty or all zeros, skipping")
|
|
280
|
+
# Use helper method to parse metadata
|
|
281
|
+
metadata = self._parse_metadata_json(metadata_bytes)
|
|
282
|
+
if not metadata:
|
|
283
|
+
logger.warning("Failed to parse metadata, skipping")
|
|
289
284
|
continue
|
|
290
285
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if json_start == -1:
|
|
294
|
-
logger.warning("No JSON found in metadata: %r", metadata_str[:100])
|
|
295
|
-
continue
|
|
296
|
-
|
|
297
|
-
if json_start > 0:
|
|
298
|
-
logger.debug("Skipping %d bytes before JSON", json_start)
|
|
299
|
-
metadata_str = metadata_str[json_start:]
|
|
300
|
-
|
|
301
|
-
# Find the end of JSON (handle null padding)
|
|
302
|
-
json_end = metadata_str.rfind("}")
|
|
303
|
-
if json_end != -1 and json_end < len(metadata_str) - 1:
|
|
304
|
-
metadata_str = metadata_str[: json_end + 1]
|
|
305
|
-
|
|
306
|
-
metadata_json = json.loads(metadata_str)
|
|
307
|
-
self._metadata = GstMetadata.from_json(metadata_json)
|
|
308
|
-
self._gst_caps = self._metadata.caps
|
|
286
|
+
self._metadata = metadata
|
|
287
|
+
self._gst_caps = metadata.caps
|
|
309
288
|
logger.info(
|
|
310
289
|
"Received metadata from buffer '%s': %s",
|
|
311
290
|
self._connection.buffer_name,
|
|
@@ -414,6 +393,40 @@ class OneWayShmController(IController):
|
|
|
414
393
|
|
|
415
394
|
# Try common resolutions
|
|
416
395
|
frame_size = len(frame.data)
|
|
396
|
+
|
|
397
|
+
# First, check if it's a perfect square (square frame)
|
|
398
|
+
import math
|
|
399
|
+
|
|
400
|
+
sqrt_size = math.sqrt(frame_size)
|
|
401
|
+
if sqrt_size == int(sqrt_size):
|
|
402
|
+
# Perfect square - assume square grayscale image
|
|
403
|
+
dimension = int(sqrt_size)
|
|
404
|
+
logger.info(
|
|
405
|
+
f"Frame size {frame_size} is a perfect square, assuming {dimension}x{dimension} grayscale"
|
|
406
|
+
)
|
|
407
|
+
data = np.frombuffer(frame.data, dtype=np.uint8)
|
|
408
|
+
return data.reshape((dimension, dimension)) # type: ignore[no-any-return]
|
|
409
|
+
|
|
410
|
+
# Also check for square RGB (size = width * height * 3)
|
|
411
|
+
if frame_size % 3 == 0:
|
|
412
|
+
pixels = frame_size // 3
|
|
413
|
+
sqrt_pixels = math.sqrt(pixels)
|
|
414
|
+
if sqrt_pixels == int(sqrt_pixels):
|
|
415
|
+
dimension = int(sqrt_pixels)
|
|
416
|
+
logger.info(f"Frame size {frame_size} suggests {dimension}x{dimension} RGB")
|
|
417
|
+
data = np.frombuffer(frame.data, dtype=np.uint8)
|
|
418
|
+
return data.reshape((dimension, dimension, 3)) # type: ignore[no-any-return]
|
|
419
|
+
|
|
420
|
+
# Check for square RGBA (size = width * height * 4)
|
|
421
|
+
if frame_size % 4 == 0:
|
|
422
|
+
pixels = frame_size // 4
|
|
423
|
+
sqrt_pixels = math.sqrt(pixels)
|
|
424
|
+
if sqrt_pixels == int(sqrt_pixels):
|
|
425
|
+
dimension = int(sqrt_pixels)
|
|
426
|
+
logger.info(f"Frame size {frame_size} suggests {dimension}x{dimension} RGBA")
|
|
427
|
+
data = np.frombuffer(frame.data, dtype=np.uint8)
|
|
428
|
+
return data.reshape((dimension, dimension, 4)) # type: ignore[no-any-return]
|
|
429
|
+
|
|
417
430
|
common_resolutions = [
|
|
418
431
|
(640, 480, 3), # VGA RGB
|
|
419
432
|
(640, 480, 4), # VGA RGBA
|
|
@@ -448,6 +461,45 @@ class OneWayShmController(IController):
|
|
|
448
461
|
logger.error("Failed to convert frame to Mat: %s", e)
|
|
449
462
|
return None
|
|
450
463
|
|
|
464
|
+
def _parse_metadata_json(self, metadata_bytes: bytes | memoryview) -> GstMetadata | None:
|
|
465
|
+
"""
|
|
466
|
+
Parse metadata JSON from bytes, handling null padding and boundaries.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
metadata_bytes: Raw metadata bytes or memoryview
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
GstMetadata object or None if parsing fails
|
|
473
|
+
"""
|
|
474
|
+
try:
|
|
475
|
+
# Convert to string
|
|
476
|
+
if isinstance(metadata_bytes, memoryview):
|
|
477
|
+
metadata_bytes = bytes(metadata_bytes)
|
|
478
|
+
metadata_str = metadata_bytes.decode("utf-8")
|
|
479
|
+
|
|
480
|
+
# Find JSON boundaries (handle null padding)
|
|
481
|
+
json_start = metadata_str.find("{")
|
|
482
|
+
if json_start < 0:
|
|
483
|
+
logger.debug("No JSON found in metadata")
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
json_end = metadata_str.rfind("}")
|
|
487
|
+
if json_end <= json_start:
|
|
488
|
+
logger.debug("Invalid JSON boundaries in metadata")
|
|
489
|
+
return None
|
|
490
|
+
|
|
491
|
+
# Extract JSON
|
|
492
|
+
metadata_str = metadata_str[json_start : json_end + 1]
|
|
493
|
+
|
|
494
|
+
# Parse JSON
|
|
495
|
+
metadata_json = json.loads(metadata_str)
|
|
496
|
+
metadata = GstMetadata.from_json(metadata_json)
|
|
497
|
+
return metadata
|
|
498
|
+
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.debug("Failed to parse metadata JSON: %s", e)
|
|
501
|
+
return None
|
|
502
|
+
|
|
451
503
|
def _infer_caps_from_frame(self, mat: Mat) -> None: # type: ignore[valid-type]
|
|
452
504
|
"""
|
|
453
505
|
Infer GStreamer caps from OpenCV Mat.
|
|
@@ -495,7 +547,7 @@ class DuplexShmController(IController):
|
|
|
495
547
|
)
|
|
496
548
|
|
|
497
549
|
self._connection = connection
|
|
498
|
-
self._duplex_server: Optional[
|
|
550
|
+
self._duplex_server: Optional[ImmutableDuplexServer] = None
|
|
499
551
|
self._gst_caps: Optional[GstCaps] = None
|
|
500
552
|
self._metadata: Optional[GstMetadata] = None
|
|
501
553
|
self._is_running = False
|
|
@@ -540,6 +592,11 @@ class DuplexShmController(IController):
|
|
|
540
592
|
if not self._connection.buffer_name:
|
|
541
593
|
raise ValueError("Buffer name is required for shared memory connection")
|
|
542
594
|
timeout_seconds = self._connection.timeout_ms / 1000.0
|
|
595
|
+
logger.debug(
|
|
596
|
+
"Creating duplex server with timeout: %d ms (%.1f seconds)",
|
|
597
|
+
self._connection.timeout_ms,
|
|
598
|
+
timeout_seconds,
|
|
599
|
+
)
|
|
543
600
|
factory = DuplexChannelFactory()
|
|
544
601
|
self._duplex_server = factory.create_immutable_server(
|
|
545
602
|
self._connection.buffer_name, config, timeout_seconds
|
|
@@ -571,6 +628,44 @@ class DuplexShmController(IController):
|
|
|
571
628
|
|
|
572
629
|
logger.info("DuplexShmController stopped")
|
|
573
630
|
|
|
631
|
+
def _parse_metadata_json(self, metadata_bytes: bytes | memoryview) -> GstMetadata | None:
|
|
632
|
+
"""
|
|
633
|
+
Parse metadata JSON from bytes, handling null padding and boundaries.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
metadata_bytes: Raw metadata bytes or memoryview
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
GstMetadata object or None if parsing fails
|
|
640
|
+
"""
|
|
641
|
+
try:
|
|
642
|
+
# Convert to string
|
|
643
|
+
if isinstance(metadata_bytes, memoryview):
|
|
644
|
+
metadata_bytes = bytes(metadata_bytes)
|
|
645
|
+
metadata_str = metadata_bytes.decode("utf-8")
|
|
646
|
+
|
|
647
|
+
# Find JSON boundaries (handle null padding)
|
|
648
|
+
json_start = metadata_str.find("{")
|
|
649
|
+
if json_start < 0:
|
|
650
|
+
logger.debug("No JSON found in metadata")
|
|
651
|
+
return None
|
|
652
|
+
|
|
653
|
+
json_end = metadata_str.rfind("}")
|
|
654
|
+
if json_end <= json_start:
|
|
655
|
+
logger.debug("Invalid JSON boundaries in metadata")
|
|
656
|
+
return None
|
|
657
|
+
|
|
658
|
+
# Extract JSON
|
|
659
|
+
metadata_str = metadata_str[json_start : json_end + 1]
|
|
660
|
+
|
|
661
|
+
# Parse JSON
|
|
662
|
+
metadata_json = json.loads(metadata_str)
|
|
663
|
+
metadata = GstMetadata.from_json(metadata_json)
|
|
664
|
+
return metadata
|
|
665
|
+
except Exception as e:
|
|
666
|
+
logger.debug("Failed to parse metadata JSON: %s", e)
|
|
667
|
+
return None
|
|
668
|
+
|
|
574
669
|
def _on_metadata(self, metadata_bytes: bytes | memoryview) -> None:
|
|
575
670
|
"""
|
|
576
671
|
Handle metadata from duplex channel.
|
|
@@ -582,23 +677,20 @@ class DuplexShmController(IController):
|
|
|
582
677
|
"_on_metadata called with %d bytes", len(metadata_bytes) if metadata_bytes else 0
|
|
583
678
|
)
|
|
584
679
|
try:
|
|
585
|
-
# Convert memoryview to bytes if needed
|
|
586
|
-
if isinstance(metadata_bytes, memoryview):
|
|
587
|
-
metadata_bytes = bytes(metadata_bytes)
|
|
588
|
-
|
|
589
680
|
# Log raw bytes for debugging
|
|
590
681
|
logger.debug(
|
|
591
682
|
"Raw metadata bytes (first 100): %r",
|
|
592
683
|
metadata_bytes[: min(100, len(metadata_bytes))],
|
|
593
684
|
)
|
|
594
685
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
686
|
+
# Use helper method to parse metadata
|
|
687
|
+
metadata = self._parse_metadata_json(metadata_bytes)
|
|
688
|
+
if metadata:
|
|
689
|
+
self._metadata = metadata
|
|
690
|
+
self._gst_caps = metadata.caps
|
|
691
|
+
logger.info("Received metadata: %s", self._metadata)
|
|
692
|
+
else:
|
|
693
|
+
logger.warning("Failed to parse metadata from buffer initialization")
|
|
602
694
|
except Exception as e:
|
|
603
695
|
logger.error("Failed to parse metadata: %s", e, exc_info=True)
|
|
604
696
|
|
|
@@ -622,6 +714,30 @@ class DuplexShmController(IController):
|
|
|
622
714
|
|
|
623
715
|
self._frame_count += 1
|
|
624
716
|
|
|
717
|
+
# Try to read metadata if we don't have it yet
|
|
718
|
+
if (
|
|
719
|
+
self._metadata is None
|
|
720
|
+
and self._duplex_server
|
|
721
|
+
and self._duplex_server.request_reader
|
|
722
|
+
):
|
|
723
|
+
try:
|
|
724
|
+
metadata_bytes = self._duplex_server.request_reader.get_metadata()
|
|
725
|
+
if metadata_bytes:
|
|
726
|
+
# Use helper method to parse metadata
|
|
727
|
+
metadata = self._parse_metadata_json(metadata_bytes)
|
|
728
|
+
if metadata:
|
|
729
|
+
self._metadata = metadata
|
|
730
|
+
self._gst_caps = metadata.caps
|
|
731
|
+
logger.info(
|
|
732
|
+
"Successfully read metadata from buffer '%s': %s",
|
|
733
|
+
self._connection.buffer_name,
|
|
734
|
+
self._gst_caps,
|
|
735
|
+
)
|
|
736
|
+
else:
|
|
737
|
+
logger.debug("Failed to parse metadata in frame processing")
|
|
738
|
+
except Exception as e:
|
|
739
|
+
logger.debug("Failed to read metadata in frame processing: %s", e)
|
|
740
|
+
|
|
625
741
|
# Convert input frame to Mat
|
|
626
742
|
input_mat = self._frame_to_mat(request_frame)
|
|
627
743
|
if input_mat is None:
|