nedo-vision-worker 1.0.0__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.
- nedo_vision_worker/__init__.py +10 -0
- nedo_vision_worker/cli.py +195 -0
- nedo_vision_worker/config/ConfigurationManager.py +196 -0
- nedo_vision_worker/config/__init__.py +1 -0
- nedo_vision_worker/database/DatabaseManager.py +219 -0
- nedo_vision_worker/database/__init__.py +1 -0
- nedo_vision_worker/doctor.py +453 -0
- nedo_vision_worker/initializer/AppInitializer.py +78 -0
- nedo_vision_worker/initializer/__init__.py +1 -0
- nedo_vision_worker/models/__init__.py +15 -0
- nedo_vision_worker/models/ai_model.py +29 -0
- nedo_vision_worker/models/auth.py +14 -0
- nedo_vision_worker/models/config.py +9 -0
- nedo_vision_worker/models/dataset_source.py +30 -0
- nedo_vision_worker/models/logs.py +9 -0
- nedo_vision_worker/models/ppe_detection.py +39 -0
- nedo_vision_worker/models/ppe_detection_label.py +20 -0
- nedo_vision_worker/models/restricted_area_violation.py +20 -0
- nedo_vision_worker/models/user.py +10 -0
- nedo_vision_worker/models/worker_source.py +19 -0
- nedo_vision_worker/models/worker_source_pipeline.py +21 -0
- nedo_vision_worker/models/worker_source_pipeline_config.py +24 -0
- nedo_vision_worker/models/worker_source_pipeline_debug.py +15 -0
- nedo_vision_worker/models/worker_source_pipeline_detection.py +14 -0
- nedo_vision_worker/protos/AIModelService_pb2.py +46 -0
- nedo_vision_worker/protos/AIModelService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/DatasetSourceService_pb2.py +46 -0
- nedo_vision_worker/protos/DatasetSourceService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/HumanDetectionService_pb2.py +44 -0
- nedo_vision_worker/protos/HumanDetectionService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/PPEDetectionService_pb2.py +46 -0
- nedo_vision_worker/protos/PPEDetectionService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/VisionWorkerService_pb2.py +72 -0
- nedo_vision_worker/protos/VisionWorkerService_pb2_grpc.py +471 -0
- nedo_vision_worker/protos/WorkerSourcePipelineService_pb2.py +64 -0
- nedo_vision_worker/protos/WorkerSourcePipelineService_pb2_grpc.py +312 -0
- nedo_vision_worker/protos/WorkerSourceService_pb2.py +50 -0
- nedo_vision_worker/protos/WorkerSourceService_pb2_grpc.py +183 -0
- nedo_vision_worker/protos/__init__.py +1 -0
- nedo_vision_worker/repositories/AIModelRepository.py +44 -0
- nedo_vision_worker/repositories/DatasetSourceRepository.py +150 -0
- nedo_vision_worker/repositories/PPEDetectionRepository.py +112 -0
- nedo_vision_worker/repositories/RestrictedAreaRepository.py +88 -0
- nedo_vision_worker/repositories/WorkerSourcePipelineDebugRepository.py +90 -0
- nedo_vision_worker/repositories/WorkerSourcePipelineDetectionRepository.py +48 -0
- nedo_vision_worker/repositories/WorkerSourcePipelineRepository.py +174 -0
- nedo_vision_worker/repositories/WorkerSourceRepository.py +46 -0
- nedo_vision_worker/repositories/__init__.py +1 -0
- nedo_vision_worker/services/AIModelClient.py +362 -0
- nedo_vision_worker/services/ConnectionInfoClient.py +57 -0
- nedo_vision_worker/services/DatasetSourceClient.py +88 -0
- nedo_vision_worker/services/FileToRTMPServer.py +78 -0
- nedo_vision_worker/services/GrpcClientBase.py +155 -0
- nedo_vision_worker/services/GrpcClientManager.py +141 -0
- nedo_vision_worker/services/ImageUploadClient.py +82 -0
- nedo_vision_worker/services/PPEDetectionClient.py +108 -0
- nedo_vision_worker/services/RTSPtoRTMPStreamer.py +98 -0
- nedo_vision_worker/services/RestrictedAreaClient.py +100 -0
- nedo_vision_worker/services/SystemUsageClient.py +77 -0
- nedo_vision_worker/services/VideoStreamClient.py +161 -0
- nedo_vision_worker/services/WorkerSourceClient.py +215 -0
- nedo_vision_worker/services/WorkerSourcePipelineClient.py +393 -0
- nedo_vision_worker/services/WorkerSourceUpdater.py +134 -0
- nedo_vision_worker/services/WorkerStatusClient.py +65 -0
- nedo_vision_worker/services/__init__.py +1 -0
- nedo_vision_worker/util/HardwareID.py +104 -0
- nedo_vision_worker/util/ImageUploader.py +92 -0
- nedo_vision_worker/util/Networking.py +94 -0
- nedo_vision_worker/util/PlatformDetector.py +50 -0
- nedo_vision_worker/util/SystemMonitor.py +299 -0
- nedo_vision_worker/util/VideoProbeUtil.py +120 -0
- nedo_vision_worker/util/__init__.py +1 -0
- nedo_vision_worker/worker/CoreActionWorker.py +125 -0
- nedo_vision_worker/worker/DataSenderWorker.py +168 -0
- nedo_vision_worker/worker/DataSyncWorker.py +143 -0
- nedo_vision_worker/worker/DatasetFrameSender.py +208 -0
- nedo_vision_worker/worker/DatasetFrameWorker.py +412 -0
- nedo_vision_worker/worker/PPEDetectionManager.py +86 -0
- nedo_vision_worker/worker/PipelineActionWorker.py +129 -0
- nedo_vision_worker/worker/PipelineImageWorker.py +116 -0
- nedo_vision_worker/worker/RabbitMQListener.py +170 -0
- nedo_vision_worker/worker/RestrictedAreaManager.py +85 -0
- nedo_vision_worker/worker/SystemUsageManager.py +111 -0
- nedo_vision_worker/worker/VideoStreamWorker.py +139 -0
- nedo_vision_worker/worker/WorkerManager.py +155 -0
- nedo_vision_worker/worker/__init__.py +1 -0
- nedo_vision_worker/worker_service.py +264 -0
- nedo_vision_worker-1.0.0.dist-info/METADATA +563 -0
- nedo_vision_worker-1.0.0.dist-info/RECORD +92 -0
- nedo_vision_worker-1.0.0.dist-info/WHEEL +5 -0
- nedo_vision_worker-1.0.0.dist-info/entry_points.txt +2 -0
- nedo_vision_worker-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from ..services.PPEDetectionClient import PPEDetectionClient
|
|
5
|
+
from ..repositories.PPEDetectionRepository import PPEDetectionRepository
|
|
6
|
+
from ..util.Networking import Networking
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
def safe_join_thread(thread, timeout=5):
|
|
11
|
+
"""Safely join a thread, avoiding RuntimeError when joining current thread."""
|
|
12
|
+
if thread and thread != threading.current_thread():
|
|
13
|
+
thread.join(timeout=timeout)
|
|
14
|
+
elif thread == threading.current_thread():
|
|
15
|
+
logging.info("🛑 [APP] Thread stopping from within itself, skipping join.")
|
|
16
|
+
|
|
17
|
+
class PPEDetectionManager:
|
|
18
|
+
def __init__(self, server_host: str, worker_id: str, worker_source_id: str, token: str):
|
|
19
|
+
"""
|
|
20
|
+
Handles PPE detection monitoring and reporting.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
server_host (str): The gRPC server host.
|
|
24
|
+
worker_id (str): Unique worker ID (passed externally).
|
|
25
|
+
worker_source_id (str): Unique worker source ID (passed externally).
|
|
26
|
+
token (str): Authentication token for the worker.
|
|
27
|
+
"""
|
|
28
|
+
if not worker_id or not worker_source_id:
|
|
29
|
+
raise ValueError("⚠️ [APP] 'worker_id' and 'worker_source_id' cannot be empty.")
|
|
30
|
+
if not token:
|
|
31
|
+
raise ValueError("⚠️ [APP] 'token' cannot be empty.")
|
|
32
|
+
|
|
33
|
+
self.ppe_detection_client = PPEDetectionClient(server_host)
|
|
34
|
+
self.server_host = server_host
|
|
35
|
+
self.worker_id = worker_id
|
|
36
|
+
self.worker_source_id = worker_source_id
|
|
37
|
+
self.token = token
|
|
38
|
+
self.ppe_detection_data = []
|
|
39
|
+
self.stop_event = threading.Event()
|
|
40
|
+
self.ppe_detection_thread = None
|
|
41
|
+
self.ppe_detection_repo = PPEDetectionRepository()
|
|
42
|
+
|
|
43
|
+
self._start_ppe_detection_monitoring()
|
|
44
|
+
|
|
45
|
+
def _start_ppe_detection_monitoring(self):
|
|
46
|
+
"""Starts a background thread to monitor and collect PPE detection data."""
|
|
47
|
+
if self.ppe_detection_thread and self.ppe_detection_thread.is_alive():
|
|
48
|
+
logger.warning("⚠️ [APP] PPE detection monitoring thread is already running.")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
logger.info("📡 [APP] PPE detection monitoring started.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def send_ppe_detection_batch(self):
|
|
55
|
+
"""Sends a batch of collected PPE detection data to the server."""
|
|
56
|
+
try:
|
|
57
|
+
self.ppe_detection_data = self.ppe_detection_repo.get_latest_5_detections()
|
|
58
|
+
if not self.ppe_detection_data:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
response = self.ppe_detection_client.send_upsert_batch(
|
|
62
|
+
worker_id=self.worker_id,
|
|
63
|
+
worker_source_id=self.worker_source_id,
|
|
64
|
+
detection_data=self.ppe_detection_data,
|
|
65
|
+
token=self.token
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if response.get("success"):
|
|
69
|
+
logger.info("✅ [APP] Successfully sent PPE detection batch.")
|
|
70
|
+
self.ppe_detection_data.clear()
|
|
71
|
+
else:
|
|
72
|
+
logger.error(f"❌ [APP] Failed to send PPE detection batch: {response.get('message')}")
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error("🚨 [APP] Error sending PPE detection batch.", exc_info=True)
|
|
76
|
+
|
|
77
|
+
def close(self):
|
|
78
|
+
"""Closes the PPE detection client and stops the monitoring thread."""
|
|
79
|
+
self.stop_event.set()
|
|
80
|
+
|
|
81
|
+
if self.ppe_detection_thread and self.ppe_detection_thread.is_alive():
|
|
82
|
+
safe_join_thread(self.ppe_detection_thread)
|
|
83
|
+
logger.info("🔌 [APP] PPE detection monitoring thread stopped.")
|
|
84
|
+
|
|
85
|
+
if self.ppe_detection_client:
|
|
86
|
+
logger.info("✅ [APP] PPE Detection Client closed.")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import logging
|
|
3
|
+
import json
|
|
4
|
+
from ..repositories.WorkerSourcePipelineDebugRepository import WorkerSourcePipelineDebugRepository
|
|
5
|
+
from ..repositories.WorkerSourcePipelineRepository import WorkerSourcePipelineRepository
|
|
6
|
+
from .RabbitMQListener import RabbitMQListener
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
def safe_join_thread(thread, timeout=5):
|
|
11
|
+
"""Safely join a thread, avoiding RuntimeError when joining current thread."""
|
|
12
|
+
if thread and thread != threading.current_thread():
|
|
13
|
+
thread.join(timeout=timeout)
|
|
14
|
+
elif thread == threading.current_thread():
|
|
15
|
+
logging.info("🛑 [APP] Thread stopping from within itself, skipping join.")
|
|
16
|
+
|
|
17
|
+
class PipelineActionWorker:
|
|
18
|
+
def __init__(self, config: dict):
|
|
19
|
+
"""
|
|
20
|
+
Initialize Pipeline Action Worker.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config (dict): Configuration object containing settings.
|
|
24
|
+
"""
|
|
25
|
+
if not isinstance(config, dict):
|
|
26
|
+
raise ValueError("⚠️ [APP] config must be a dictionary.")
|
|
27
|
+
|
|
28
|
+
self.config = config
|
|
29
|
+
self.worker_id = self.config.get("worker_id")
|
|
30
|
+
|
|
31
|
+
if not self.worker_id:
|
|
32
|
+
raise ValueError("⚠️ [APP] Configuration is missing 'worker_id'.")
|
|
33
|
+
|
|
34
|
+
self.thread = None
|
|
35
|
+
self.stop_event = threading.Event()
|
|
36
|
+
self.lock = threading.Lock()
|
|
37
|
+
|
|
38
|
+
self.repo = WorkerSourcePipelineRepository()
|
|
39
|
+
self.debug_repo = WorkerSourcePipelineDebugRepository()
|
|
40
|
+
|
|
41
|
+
# Initialize RabbitMQ listener
|
|
42
|
+
self.listener = RabbitMQListener(
|
|
43
|
+
self.config, self.worker_id, self.stop_event, self._process_pipeline_action_message
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def start(self):
|
|
47
|
+
"""Start the Pipeline Action Worker."""
|
|
48
|
+
with self.lock:
|
|
49
|
+
if self.thread and self.thread.is_alive():
|
|
50
|
+
logger.warning("⚠️ [APP] Pipeline Action Worker is already running.")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
self.stop_event.clear()
|
|
54
|
+
self.thread = threading.Thread(target=self._run, daemon=True)
|
|
55
|
+
self.thread.start()
|
|
56
|
+
logger.info(f"🚀 [APP] Pipeline Action Worker started (Device: {self.worker_id}).")
|
|
57
|
+
|
|
58
|
+
def stop(self):
|
|
59
|
+
"""Stop the Pipeline Action Worker."""
|
|
60
|
+
with self.lock:
|
|
61
|
+
if not self.thread or not self.thread.is_alive():
|
|
62
|
+
logger.warning("⚠️ [APP] Pipeline Action Worker is not running.")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
self.stop_event.set()
|
|
66
|
+
self.listener.stop_listening()
|
|
67
|
+
|
|
68
|
+
safe_join_thread(self.thread)
|
|
69
|
+
self.thread = None
|
|
70
|
+
logger.info(f"🛑 [APP] Pipeline Action Worker stopped (Device: {self.worker_id}).")
|
|
71
|
+
|
|
72
|
+
def _run(self):
|
|
73
|
+
"""Main loop to manage RabbitMQ listener."""
|
|
74
|
+
try:
|
|
75
|
+
while not self.stop_event.is_set():
|
|
76
|
+
logger.info("📡 [APP] Waiting for Pipeline action messages...")
|
|
77
|
+
self.listener.start_listening(
|
|
78
|
+
exchange_name="nedo.worker.pipeline.action",
|
|
79
|
+
queue_name=f"nedo.worker.pipeline.{self.worker_id}"
|
|
80
|
+
)
|
|
81
|
+
safe_join_thread(self.listener.listener_thread)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error("🚨 [APP] Unexpected error in Pipeline Action Worker loop.", exc_info=True)
|
|
84
|
+
|
|
85
|
+
def _process_pipeline_action_message(self, message):
|
|
86
|
+
"""
|
|
87
|
+
Process received Pipeline action messages.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
message (str): JSON message containing action and timestamp
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
data = json.loads(message)
|
|
94
|
+
uuid = data.get('uuid')
|
|
95
|
+
pipeline_id = data.get('workerSourcePipelineId')
|
|
96
|
+
action = data.get('action')
|
|
97
|
+
timestamp = data.get('timestamp')
|
|
98
|
+
|
|
99
|
+
logger.info(f"📥 [APP] Received Pipeline action: {pipeline_id}:{action} at {timestamp}")
|
|
100
|
+
|
|
101
|
+
pipeline = self.repo.get_worker_source_pipeline(pipeline_id)
|
|
102
|
+
|
|
103
|
+
if not pipeline:
|
|
104
|
+
logger.warning(f"⚠️ [APP] Pipeline not found: {pipeline_id}")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
if action == "start":
|
|
108
|
+
pipeline.pipeline_status_code = "run"
|
|
109
|
+
|
|
110
|
+
elif action == "stop":
|
|
111
|
+
pipeline.pipeline_status_code = "stop"
|
|
112
|
+
|
|
113
|
+
elif action == "restart":
|
|
114
|
+
pipeline.pipeline_status_code = "restart"
|
|
115
|
+
|
|
116
|
+
elif action == "debug":
|
|
117
|
+
self.debug_repo.create_debug_entry(uuid, pipeline_id)
|
|
118
|
+
|
|
119
|
+
else:
|
|
120
|
+
logger.warning(f"⚠️ [APP] Unknown Pipeline action received: {action}")
|
|
121
|
+
|
|
122
|
+
self.repo.session.commit()
|
|
123
|
+
logger.info(f"✅ [APP] Pipeline action processed: {pipeline_id}:{action}")
|
|
124
|
+
|
|
125
|
+
except json.JSONDecodeError:
|
|
126
|
+
logger.error("🚨 [APP] Failed to parse Pipeline action message JSON")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"🚨 [APP] Error processing Pipeline action: {str(e)}")
|
|
129
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import logging
|
|
3
|
+
import json
|
|
4
|
+
from ..repositories.WorkerSourceRepository import WorkerSourceRepository
|
|
5
|
+
from ..services.WorkerSourcePipelineClient import WorkerSourcePipelineClient
|
|
6
|
+
from .RabbitMQListener import RabbitMQListener
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
def safe_join_thread(thread, timeout=5):
|
|
11
|
+
"""Safely join a thread, avoiding RuntimeError when joining current thread."""
|
|
12
|
+
if thread and thread != threading.current_thread():
|
|
13
|
+
thread.join(timeout=timeout)
|
|
14
|
+
elif thread == threading.current_thread():
|
|
15
|
+
logging.info("🛑 [APP] Thread stopping from within itself, skipping join.")
|
|
16
|
+
|
|
17
|
+
class PipelineImageWorker:
|
|
18
|
+
def __init__(self, config: dict):
|
|
19
|
+
"""
|
|
20
|
+
Initialize Pipeline Image Worker.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config (dict): Configuration object containing settings.
|
|
24
|
+
"""
|
|
25
|
+
if not isinstance(config, dict):
|
|
26
|
+
raise ValueError("⚠️ [APP] config must be a dictionary.")
|
|
27
|
+
|
|
28
|
+
self.config = config
|
|
29
|
+
self.worker_id = self.config.get("worker_id")
|
|
30
|
+
self.server_host = self.config.get("server_host")
|
|
31
|
+
self.token = self.config.get("token")
|
|
32
|
+
|
|
33
|
+
if not self.worker_id:
|
|
34
|
+
raise ValueError("⚠️ [APP] Configuration is missing 'worker_id'.")
|
|
35
|
+
if not self.token:
|
|
36
|
+
raise ValueError("⚠️ [APP] Configuration is missing 'token'.")
|
|
37
|
+
|
|
38
|
+
self.worker_source_pipeline_client = WorkerSourcePipelineClient(self.server_host)
|
|
39
|
+
|
|
40
|
+
self.thread = None
|
|
41
|
+
self.stop_event = threading.Event()
|
|
42
|
+
self.lock = threading.Lock()
|
|
43
|
+
|
|
44
|
+
self.worker_source_repo = WorkerSourceRepository()
|
|
45
|
+
|
|
46
|
+
# Initialize RabbitMQ listener
|
|
47
|
+
self.listener = RabbitMQListener(
|
|
48
|
+
self.config, self.worker_id, self.stop_event, self._process_image_request_message
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def start(self):
|
|
52
|
+
"""Start the Pipeline Image Worker."""
|
|
53
|
+
with self.lock:
|
|
54
|
+
if self.thread and self.thread.is_alive():
|
|
55
|
+
logger.warning("⚠️ [APP] Pipeline Image Worker is already running.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
self.stop_event.clear()
|
|
59
|
+
self.thread = threading.Thread(target=self._run, daemon=True) # ✅ Run as daemon
|
|
60
|
+
self.thread.start()
|
|
61
|
+
logger.info(f"🚀 [APP] Pipeline Image Worker started (Device: {self.worker_id}).")
|
|
62
|
+
|
|
63
|
+
def stop(self):
|
|
64
|
+
"""Stop the Pipeline Image Worker."""
|
|
65
|
+
with self.lock:
|
|
66
|
+
if not self.thread or not self.thread.is_alive():
|
|
67
|
+
logger.warning("⚠️ [APP] Pipeline Image Worker is not running.")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
self.stop_event.set()
|
|
71
|
+
self.listener.stop_listening()
|
|
72
|
+
|
|
73
|
+
safe_join_thread(self.thread) # Ensures the thread stops gracefully
|
|
74
|
+
self.thread = None
|
|
75
|
+
logger.info(f"🛑 [APP] Pipeline Image Worker stopped (Device: {self.worker_id}).")
|
|
76
|
+
|
|
77
|
+
def _run(self):
|
|
78
|
+
"""Main loop to manage RabbitMQ listener."""
|
|
79
|
+
try:
|
|
80
|
+
while not self.stop_event.is_set():
|
|
81
|
+
logger.info("📡 [APP] Waiting for image request messages...")
|
|
82
|
+
self.listener.start_listening(exchange_name="nedo.pipeline.image.request", queue_name=f"nedo.pipeline.request.{self.worker_id}")
|
|
83
|
+
safe_join_thread(self.listener.listener_thread)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error("🚨 [APP] Unexpected error in Pipeline Image Worker loop.", exc_info=True)
|
|
86
|
+
|
|
87
|
+
def _process_image_request_message(self, message):
|
|
88
|
+
"""Process messages related to video preview streaming."""
|
|
89
|
+
try:
|
|
90
|
+
data = json.loads(message)
|
|
91
|
+
worker_source_pipeline_id = data.get("workerSourcePipelineId")
|
|
92
|
+
worker_source_id = data.get("workerSourceId")
|
|
93
|
+
uuid = data.get("uuid")
|
|
94
|
+
|
|
95
|
+
worker_source = self.worker_source_repo.get_worker_source_by_id(worker_source_id)
|
|
96
|
+
if not worker_source:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
logger.info(f"📡 [APP] Sending Pipeline Image Preview to Worker Source Pipeline: {worker_source_pipeline_id}")
|
|
100
|
+
|
|
101
|
+
response = self.worker_source_pipeline_client.send_pipeline_image(
|
|
102
|
+
worker_source_pipeline_id=worker_source_pipeline_id,
|
|
103
|
+
uuid=uuid,
|
|
104
|
+
url=worker_source.url if worker_source.type_code == "live" else worker_source.file_path,
|
|
105
|
+
token=self.token
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if response.get("success"):
|
|
109
|
+
logger.info("✅ [APP] Successfully sent Pipeline Image Preview.")
|
|
110
|
+
else:
|
|
111
|
+
logger.error(f"❌ [APP] Failed to send Pipeline Image Preview: {response.get('message')}")
|
|
112
|
+
|
|
113
|
+
except json.JSONDecodeError:
|
|
114
|
+
logger.error("⚠️ [APP] Invalid JSON message format.")
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.error("🚨 [APP] Error processing video preview message.", exc_info=True)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
import pika
|
|
5
|
+
import pika.exceptions
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class RabbitMQListener:
|
|
10
|
+
def __init__(self, config, device_id, stop_event, message_callback):
|
|
11
|
+
self.config = config
|
|
12
|
+
self.device_id = device_id.lower()
|
|
13
|
+
self.stop_event = stop_event
|
|
14
|
+
self.message_callback = message_callback
|
|
15
|
+
self.connection = None
|
|
16
|
+
self.channel = None
|
|
17
|
+
self.exchange_name = None
|
|
18
|
+
self.queue_name = None
|
|
19
|
+
self.listener_thread = None
|
|
20
|
+
self.reconnect_delay = 5 # Exponential backoff (max 60s)
|
|
21
|
+
|
|
22
|
+
def _connect(self):
|
|
23
|
+
"""Establish a new RabbitMQ connection."""
|
|
24
|
+
rabbitmq_host = self.config.get("rabbitmq_host")
|
|
25
|
+
rabbitmq_port = int(self.config.get("rabbitmq_port", 5672))
|
|
26
|
+
rabbitmq_username = self.config.get("rabbitmq_username")
|
|
27
|
+
rabbitmq_password = self.config.get("rabbitmq_password")
|
|
28
|
+
|
|
29
|
+
credentials = pika.PlainCredentials(rabbitmq_username, rabbitmq_password)
|
|
30
|
+
parameters = pika.ConnectionParameters(
|
|
31
|
+
host=rabbitmq_host,
|
|
32
|
+
port=rabbitmq_port,
|
|
33
|
+
virtual_host='/',
|
|
34
|
+
credentials=credentials,
|
|
35
|
+
heartbeat=30,
|
|
36
|
+
blocked_connection_timeout=10
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
self.connection = pika.SelectConnection(
|
|
41
|
+
parameters,
|
|
42
|
+
on_open_callback=self._on_connected,
|
|
43
|
+
on_close_callback=self._on_closed
|
|
44
|
+
)
|
|
45
|
+
except pika.exceptions.AMQPConnectionError as e:
|
|
46
|
+
logger.error(f"⚠️ [APP] Connection failed: {e}")
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
def _on_connected(self, connection):
|
|
52
|
+
"""Callback for successful RabbitMQ connection."""
|
|
53
|
+
logger.info("🔌 [APP] Connected to RabbitMQ")
|
|
54
|
+
self.connection = connection
|
|
55
|
+
self.connection.channel(on_open_callback=self._on_channel_open)
|
|
56
|
+
|
|
57
|
+
def _on_channel_open(self, channel):
|
|
58
|
+
"""Callback for successfully opened channel."""
|
|
59
|
+
self.channel = channel
|
|
60
|
+
routing_key = self.device_id
|
|
61
|
+
|
|
62
|
+
self.channel.exchange_declare(exchange=self.exchange_name, exchange_type='direct', durable=True)
|
|
63
|
+
self.channel.queue_declare(queue=self.queue_name, durable=False, auto_delete=True, exclusive=True)
|
|
64
|
+
self.channel.queue_bind(exchange=self.exchange_name, queue=self.queue_name, routing_key=routing_key)
|
|
65
|
+
self.channel.basic_qos(prefetch_count=1)
|
|
66
|
+
|
|
67
|
+
self.channel.basic_consume(queue=self.queue_name, on_message_callback=self._on_message_received, auto_ack=True)
|
|
68
|
+
|
|
69
|
+
logger.info(f"📡 [APP] Listening for RabbitMQ messages on '{self.exchange_name}' with routing key '{routing_key}'.")
|
|
70
|
+
|
|
71
|
+
def _on_closed(self, connection, reason):
|
|
72
|
+
"""Handle unexpected connection closure."""
|
|
73
|
+
logger.error(f"🚨 [APP] RabbitMQ connection closed unexpectedly: {reason}")
|
|
74
|
+
self._cleanup_connection()
|
|
75
|
+
time.sleep(self.reconnect_delay)
|
|
76
|
+
self.reconnect_delay = min(self.reconnect_delay * 2, 60)
|
|
77
|
+
self.start_listening(self.exchange_name, self.queue_name) # Restart listener
|
|
78
|
+
|
|
79
|
+
def start_listening(self, exchange_name, queue_name):
|
|
80
|
+
"""
|
|
81
|
+
Establish a RabbitMQ connection and listen for messages.
|
|
82
|
+
Automatically reconnects if the connection is lost.
|
|
83
|
+
"""
|
|
84
|
+
if not self.stop_event:
|
|
85
|
+
logger.error("🚨 [APP] Stop event is not initialized")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
self.exchange_name = exchange_name
|
|
89
|
+
self.queue_name = queue_name
|
|
90
|
+
|
|
91
|
+
if self.listener_thread and self.listener_thread.is_alive():
|
|
92
|
+
logger.warning("⚠️ [APP] RabbitMQ listener is already running.")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
def run():
|
|
96
|
+
while not self.stop_event.is_set():
|
|
97
|
+
if self._connect():
|
|
98
|
+
try:
|
|
99
|
+
self.connection.ioloop.start() # Start pika's event loop
|
|
100
|
+
except KeyboardInterrupt:
|
|
101
|
+
self.stop_listening()
|
|
102
|
+
break
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"🚨 [APP] Unexpected RabbitMQ error: {e}")
|
|
105
|
+
time.sleep(self.reconnect_delay)
|
|
106
|
+
self.reconnect_delay = min(self.reconnect_delay * 2, 60)
|
|
107
|
+
else:
|
|
108
|
+
logger.error(f"⚠️ [APP] Connection failed. Retrying in {self.reconnect_delay}s...")
|
|
109
|
+
time.sleep(self.reconnect_delay)
|
|
110
|
+
|
|
111
|
+
# Start RabbitMQ listener in a new thread
|
|
112
|
+
self.listener_thread = threading.Thread(target=run, daemon=True)
|
|
113
|
+
self.listener_thread.start()
|
|
114
|
+
|
|
115
|
+
def stop_listening(self):
|
|
116
|
+
"""
|
|
117
|
+
Stop RabbitMQ listening and close the connection.
|
|
118
|
+
"""
|
|
119
|
+
logger.info("🛑 [APP] Stopping RabbitMQ listener...")
|
|
120
|
+
if not self.stop_event:
|
|
121
|
+
logger.error("🚨 [APP] Stop event is not initialized")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
self.stop_event.set()
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
if self.channel and self.channel.is_open:
|
|
128
|
+
self.channel.close()
|
|
129
|
+
|
|
130
|
+
if self.connection and self.connection.is_open:
|
|
131
|
+
self.connection.close()
|
|
132
|
+
|
|
133
|
+
if self.connection:
|
|
134
|
+
self.connection.ioloop.stop() # Stop pika event loop
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f"🚨 [APP] Error during RabbitMQ shutdown: {e}")
|
|
138
|
+
|
|
139
|
+
self._cleanup_connection()
|
|
140
|
+
logger.info("🔌 [APP] RabbitMQ listener stopped.")
|
|
141
|
+
|
|
142
|
+
def _cleanup_connection(self):
|
|
143
|
+
"""Safely close RabbitMQ connection and channel."""
|
|
144
|
+
if self.channel:
|
|
145
|
+
try:
|
|
146
|
+
if self.channel.is_open:
|
|
147
|
+
self.channel.close()
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
self.channel = None
|
|
151
|
+
|
|
152
|
+
if self.connection:
|
|
153
|
+
try:
|
|
154
|
+
if self.connection.is_open:
|
|
155
|
+
self.connection.close()
|
|
156
|
+
self.connection.ioloop.stop()
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
self.connection = None
|
|
160
|
+
|
|
161
|
+
def _on_message_received(self, ch, method, properties, body):
|
|
162
|
+
"""
|
|
163
|
+
Callback function triggered when a message is received.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
message = body.decode("utf-8")
|
|
167
|
+
logger.info(f"📩 [APP] Received RabbitMQ message: {message}")
|
|
168
|
+
self.message_callback(message)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"🚨 [APP] Error processing RabbitMQ message: {e}")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from ..repositories.RestrictedAreaRepository import RestrictedAreaRepository
|
|
6
|
+
from ..services.RestrictedAreaClient import RestrictedAreaClient
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
def safe_join_thread(thread, timeout=5):
|
|
11
|
+
"""Safely join a thread, avoiding RuntimeError when joining current thread."""
|
|
12
|
+
if thread and thread != threading.current_thread():
|
|
13
|
+
thread.join(timeout=timeout)
|
|
14
|
+
elif thread == threading.current_thread():
|
|
15
|
+
logging.info("🛑 [APP] Thread stopping from within itself, skipping join.")
|
|
16
|
+
|
|
17
|
+
class RestrictedAreaManager:
|
|
18
|
+
def __init__(self, server_host: str, worker_id: str, worker_source_id: str, token: str):
|
|
19
|
+
"""
|
|
20
|
+
Handles restricted area violation monitoring and reporting.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
server_host (str): The gRPC server host.
|
|
24
|
+
worker_id (str): Unique worker ID (passed externally).
|
|
25
|
+
worker_source_id (str): Unique worker source ID (passed externally).
|
|
26
|
+
token (str): Authentication token for the worker.
|
|
27
|
+
"""
|
|
28
|
+
if not worker_id or not worker_source_id:
|
|
29
|
+
raise ValueError("⚠️ [APP] 'worker_id' and 'worker_source_id' cannot be empty.")
|
|
30
|
+
if not token:
|
|
31
|
+
raise ValueError("⚠️ [APP] 'token' cannot be empty.")
|
|
32
|
+
|
|
33
|
+
self.client = RestrictedAreaClient(server_host)
|
|
34
|
+
self.server_host = server_host
|
|
35
|
+
self.worker_id = worker_id
|
|
36
|
+
self.worker_source_id = worker_source_id
|
|
37
|
+
self.token = token
|
|
38
|
+
self.violations_data = []
|
|
39
|
+
self.stop_event = threading.Event()
|
|
40
|
+
self.violation_thread = None
|
|
41
|
+
self.repo = RestrictedAreaRepository()
|
|
42
|
+
|
|
43
|
+
self._start_violation_monitoring()
|
|
44
|
+
|
|
45
|
+
def _start_violation_monitoring(self):
|
|
46
|
+
"""Starts a background thread to monitor and collect restricted area violations."""
|
|
47
|
+
if self.violation_thread and self.violation_thread.is_alive():
|
|
48
|
+
logger.warning("⚠️ [APP] Restricted area violation thread already running.")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
logger.info("📡 [APP] Restricted area violation monitoring started.")
|
|
52
|
+
|
|
53
|
+
def send_violation_batch(self):
|
|
54
|
+
"""Sends a batch of collected violation data to the server."""
|
|
55
|
+
try:
|
|
56
|
+
self.violations_data = self.repo.get_latest_5_violations()
|
|
57
|
+
if not self.violations_data:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
response = self.client.send_upsert_batch(
|
|
61
|
+
worker_id=self.worker_id,
|
|
62
|
+
worker_source_id=self.worker_source_id,
|
|
63
|
+
violation_data=self.violations_data,
|
|
64
|
+
token=self.token
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if response.get("success"):
|
|
68
|
+
logger.info("✅ [APP] Successfully sent restricted area violation batch.")
|
|
69
|
+
self.violations_data.clear()
|
|
70
|
+
else:
|
|
71
|
+
logger.error(f"❌ [APP] Failed to send restricted area violation batch: {response.get('message')}")
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error("🚨 [APP] Error sending restricted area violation batch.", exc_info=True)
|
|
75
|
+
|
|
76
|
+
def close(self):
|
|
77
|
+
"""Closes the violation client and stops the monitoring thread."""
|
|
78
|
+
self.stop_event.set()
|
|
79
|
+
|
|
80
|
+
if self.violation_thread and self.violation_thread.is_alive():
|
|
81
|
+
safe_join_thread(self.violation_thread)
|
|
82
|
+
logger.info("🔌 [APP] Restricted area violation monitoring thread stopped.")
|
|
83
|
+
|
|
84
|
+
if self.client:
|
|
85
|
+
logger.info("✅ [APP] Restricted Area Violation Client closed.")
|