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.
Files changed (92) hide show
  1. nedo_vision_worker/__init__.py +10 -0
  2. nedo_vision_worker/cli.py +195 -0
  3. nedo_vision_worker/config/ConfigurationManager.py +196 -0
  4. nedo_vision_worker/config/__init__.py +1 -0
  5. nedo_vision_worker/database/DatabaseManager.py +219 -0
  6. nedo_vision_worker/database/__init__.py +1 -0
  7. nedo_vision_worker/doctor.py +453 -0
  8. nedo_vision_worker/initializer/AppInitializer.py +78 -0
  9. nedo_vision_worker/initializer/__init__.py +1 -0
  10. nedo_vision_worker/models/__init__.py +15 -0
  11. nedo_vision_worker/models/ai_model.py +29 -0
  12. nedo_vision_worker/models/auth.py +14 -0
  13. nedo_vision_worker/models/config.py +9 -0
  14. nedo_vision_worker/models/dataset_source.py +30 -0
  15. nedo_vision_worker/models/logs.py +9 -0
  16. nedo_vision_worker/models/ppe_detection.py +39 -0
  17. nedo_vision_worker/models/ppe_detection_label.py +20 -0
  18. nedo_vision_worker/models/restricted_area_violation.py +20 -0
  19. nedo_vision_worker/models/user.py +10 -0
  20. nedo_vision_worker/models/worker_source.py +19 -0
  21. nedo_vision_worker/models/worker_source_pipeline.py +21 -0
  22. nedo_vision_worker/models/worker_source_pipeline_config.py +24 -0
  23. nedo_vision_worker/models/worker_source_pipeline_debug.py +15 -0
  24. nedo_vision_worker/models/worker_source_pipeline_detection.py +14 -0
  25. nedo_vision_worker/protos/AIModelService_pb2.py +46 -0
  26. nedo_vision_worker/protos/AIModelService_pb2_grpc.py +140 -0
  27. nedo_vision_worker/protos/DatasetSourceService_pb2.py +46 -0
  28. nedo_vision_worker/protos/DatasetSourceService_pb2_grpc.py +140 -0
  29. nedo_vision_worker/protos/HumanDetectionService_pb2.py +44 -0
  30. nedo_vision_worker/protos/HumanDetectionService_pb2_grpc.py +140 -0
  31. nedo_vision_worker/protos/PPEDetectionService_pb2.py +46 -0
  32. nedo_vision_worker/protos/PPEDetectionService_pb2_grpc.py +140 -0
  33. nedo_vision_worker/protos/VisionWorkerService_pb2.py +72 -0
  34. nedo_vision_worker/protos/VisionWorkerService_pb2_grpc.py +471 -0
  35. nedo_vision_worker/protos/WorkerSourcePipelineService_pb2.py +64 -0
  36. nedo_vision_worker/protos/WorkerSourcePipelineService_pb2_grpc.py +312 -0
  37. nedo_vision_worker/protos/WorkerSourceService_pb2.py +50 -0
  38. nedo_vision_worker/protos/WorkerSourceService_pb2_grpc.py +183 -0
  39. nedo_vision_worker/protos/__init__.py +1 -0
  40. nedo_vision_worker/repositories/AIModelRepository.py +44 -0
  41. nedo_vision_worker/repositories/DatasetSourceRepository.py +150 -0
  42. nedo_vision_worker/repositories/PPEDetectionRepository.py +112 -0
  43. nedo_vision_worker/repositories/RestrictedAreaRepository.py +88 -0
  44. nedo_vision_worker/repositories/WorkerSourcePipelineDebugRepository.py +90 -0
  45. nedo_vision_worker/repositories/WorkerSourcePipelineDetectionRepository.py +48 -0
  46. nedo_vision_worker/repositories/WorkerSourcePipelineRepository.py +174 -0
  47. nedo_vision_worker/repositories/WorkerSourceRepository.py +46 -0
  48. nedo_vision_worker/repositories/__init__.py +1 -0
  49. nedo_vision_worker/services/AIModelClient.py +362 -0
  50. nedo_vision_worker/services/ConnectionInfoClient.py +57 -0
  51. nedo_vision_worker/services/DatasetSourceClient.py +88 -0
  52. nedo_vision_worker/services/FileToRTMPServer.py +78 -0
  53. nedo_vision_worker/services/GrpcClientBase.py +155 -0
  54. nedo_vision_worker/services/GrpcClientManager.py +141 -0
  55. nedo_vision_worker/services/ImageUploadClient.py +82 -0
  56. nedo_vision_worker/services/PPEDetectionClient.py +108 -0
  57. nedo_vision_worker/services/RTSPtoRTMPStreamer.py +98 -0
  58. nedo_vision_worker/services/RestrictedAreaClient.py +100 -0
  59. nedo_vision_worker/services/SystemUsageClient.py +77 -0
  60. nedo_vision_worker/services/VideoStreamClient.py +161 -0
  61. nedo_vision_worker/services/WorkerSourceClient.py +215 -0
  62. nedo_vision_worker/services/WorkerSourcePipelineClient.py +393 -0
  63. nedo_vision_worker/services/WorkerSourceUpdater.py +134 -0
  64. nedo_vision_worker/services/WorkerStatusClient.py +65 -0
  65. nedo_vision_worker/services/__init__.py +1 -0
  66. nedo_vision_worker/util/HardwareID.py +104 -0
  67. nedo_vision_worker/util/ImageUploader.py +92 -0
  68. nedo_vision_worker/util/Networking.py +94 -0
  69. nedo_vision_worker/util/PlatformDetector.py +50 -0
  70. nedo_vision_worker/util/SystemMonitor.py +299 -0
  71. nedo_vision_worker/util/VideoProbeUtil.py +120 -0
  72. nedo_vision_worker/util/__init__.py +1 -0
  73. nedo_vision_worker/worker/CoreActionWorker.py +125 -0
  74. nedo_vision_worker/worker/DataSenderWorker.py +168 -0
  75. nedo_vision_worker/worker/DataSyncWorker.py +143 -0
  76. nedo_vision_worker/worker/DatasetFrameSender.py +208 -0
  77. nedo_vision_worker/worker/DatasetFrameWorker.py +412 -0
  78. nedo_vision_worker/worker/PPEDetectionManager.py +86 -0
  79. nedo_vision_worker/worker/PipelineActionWorker.py +129 -0
  80. nedo_vision_worker/worker/PipelineImageWorker.py +116 -0
  81. nedo_vision_worker/worker/RabbitMQListener.py +170 -0
  82. nedo_vision_worker/worker/RestrictedAreaManager.py +85 -0
  83. nedo_vision_worker/worker/SystemUsageManager.py +111 -0
  84. nedo_vision_worker/worker/VideoStreamWorker.py +139 -0
  85. nedo_vision_worker/worker/WorkerManager.py +155 -0
  86. nedo_vision_worker/worker/__init__.py +1 -0
  87. nedo_vision_worker/worker_service.py +264 -0
  88. nedo_vision_worker-1.0.0.dist-info/METADATA +563 -0
  89. nedo_vision_worker-1.0.0.dist-info/RECORD +92 -0
  90. nedo_vision_worker-1.0.0.dist-info/WHEEL +5 -0
  91. nedo_vision_worker-1.0.0.dist-info/entry_points.txt +2 -0
  92. 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.")