wiktortokumpel

chuh

Nov 12th, 2025 (edited)
30
0
Never
3
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.99 KB | None | 0 0
  1. import cv2
  2. import time
  3. import threading
  4. from collections import deque
  5. from datetime import datetime
  6. import socket
  7. import os
  8. import re
  9.  
  10. # ================== KONFIGURACJA ==================
  11.  
  12. CAMERAS = {
  13. # nazwa_kamery : URL RTSP
  14. "KAMERA1": "rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif",
  15. # w przyszłości możesz dodać np.:
  16. # "KAMERA2": "rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif",
  17. }
  18.  
  19. PRE_SECONDS = 20 # ile sekund wstecz ma być w nagraniu (dla pierwszego nagrania po przerwie)
  20. POST_SECONDS = 60 # ile sekund po triggerze nagrywać
  21. FPS_DEFAULT = 25 # używane gdy kamera nie zwraca sensownego FPS
  22.  
  23. # ŚCIEŻKA NA DYSK USB – ZMIEŃ LITERĘ DYSKU JEŚLI POTRZEBA:
  24. OUTPUT_DIR = r"E:\Nagrania"
  25.  
  26. RETENTION_DAYS = 30 # ile dni trzymać nagrania, starsze są kasowane
  27.  
  28. TCP_HOST = "0.0.0.0" # nasłuch na wszystkich interfejsach
  29. TCP_PORT = 5000 # port, na który HMI wysyła kod części
  30.  
  31.  
  32. # ================== POMOCNICZE ==================
  33.  
  34. def ensure_output_dir():
  35. os.makedirs(OUTPUT_DIR, exist_ok=True)
  36.  
  37.  
  38. def cleanup_old_files():
  39. """
  40. Usuwa pliki starsze niż RETENTION_DAYS w OUTPUT_DIR.
  41. """
  42. now = time.time()
  43. cutoff = now - RETENTION_DAYS * 24 * 3600
  44.  
  45. try:
  46. for filename in os.listdir(OUTPUT_DIR):
  47. filepath = os.path.join(OUTPUT_DIR, filename)
  48. if os.path.isfile(filepath):
  49. mtime = os.path.getmtime(filepath)
  50. if mtime < cutoff:
  51. print(f"[CLEANUP] Usuwam stary plik: {filename}")
  52. os.remove(filepath)
  53. except Exception as e:
  54. print(f"[CLEANUP] Błąd podczas czyszczenia: {e}")
  55.  
  56.  
  57. def cleanup_worker():
  58. """
  59. Wątek, który co 24h uruchamia cleanup_old_files().
  60. """
  61. while True:
  62. cleanup_old_files()
  63. time.sleep(24 * 3600) # 24 godziny
  64.  
  65.  
  66. def sanitize_part_code(part_code: str) -> str:
  67. """
  68. Czyści kod części tak, by nadawał się do nazwy pliku.
  69. Zostawiamy tylko cyfry, litery, _, -.
  70. """
  71. part_code = part_code.strip()
  72. if not part_code:
  73. part_code = "NO_CODE"
  74. safe = re.sub(r"[^0-9A-Za-z_-]", "_", part_code)
  75. return safe
  76.  
  77.  
  78. # ================== KLASA DO OBSŁUGI KAMERY ==================
  79.  
  80. class CameraRecorder(threading.Thread):
  81. def __init__(self, name, url):
  82. super().__init__(daemon=True)
  83. self.name = name
  84. self.url = url
  85.  
  86. self.cap = None
  87. self._open_stream()
  88.  
  89. fps = self.cap.get(cv2.CAP_PROP_FPS) if self.cap else 0
  90. if fps <= 0 or fps > 120:
  91. fps = FPS_DEFAULT
  92. self.fps = fps
  93.  
  94. self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 640)
  95. self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 480)
  96.  
  97. maxlen = int(self.fps * PRE_SECONDS)
  98. self.buffer = deque(maxlen=maxlen)
  99.  
  100. self.recording = False # czy aktualnie nagrywamy post-trigger
  101. self.record_end_time = 0.0
  102. self.writer = None
  103. self.lock = threading.Lock()
  104.  
  105. print(f"[{self.name}] Start. FPS={self.fps}, {self.width}x{self.height}, bufor={maxlen} klatek")
  106.  
  107. def _open_stream(self):
  108. if self.cap is not None:
  109. self.cap.release()
  110. print(f"[{self.name}] Otwieram strumień RTSP: {self.url}")
  111. self.cap = cv2.VideoCapture(self.url)
  112. if not self.cap.isOpened():
  113. print(f"[{self.name}] ❌ Nie można otworzyć strumienia!")
  114.  
  115. def _start_new_writer(self, part_code: str, use_prebuffer: bool):
  116. """
  117. Tworzy nowy plik wideo:
  118. - use_prebuffer = True -> zapisuje bufor (-20 s)
  119. - use_prebuffer = False -> nagrywa tylko od triggera (bez cofania)
  120. Nazwa pliku: RRRR_MM_DD_GG_MM_SS_KODCZESCI_KAMERA.mp4
  121. """
  122. now = time.time()
  123. timestamp_str = datetime.fromtimestamp(now).strftime("%Y_%m_%d_%H_%M_%S")
  124. safe_code = sanitize_part_code(part_code)
  125.  
  126. filename = f"{timestamp_str}_{safe_code}_{self.name}.mp4"
  127. filepath = os.path.join(OUTPUT_DIR, filename)
  128.  
  129. print(f"[{self.name}] Nowe nagranie: część={part_code}, plik={filename}, prebuffer={use_prebuffer}")
  130.  
  131. writer = cv2.VideoWriter(
  132. filepath,
  133. cv2.VideoWriter_fourcc(*"mp4v"),
  134. self.fps,
  135. (self.width, self.height)
  136. )
  137.  
  138. if not writer.isOpened():
  139. print(f"[{self.name}] ❌ Nie można otworzyć pliku do zapisu!")
  140. return None
  141.  
  142. # jeśli mamy użyć bufora wstecznego (-20 s)
  143. if use_prebuffer:
  144. print(f"[{self.name}] Zapis bufora (~{PRE_SECONDS} s wstecz, {len(self.buffer)} klatek)...")
  145. for (_t_frame, frame) in self.buffer:
  146. writer.write(frame)
  147.  
  148. # ustaw parametry nagrywania post-trigger
  149. self.recording = True
  150. self.record_end_time = now + POST_SECONDS
  151.  
  152. return writer
  153.  
  154. def trigger(self, part_code: str):
  155. """
  156. Wywoływane, gdy przychodzi trigger z kodem części.
  157.  
  158. Logika:
  159. - jeżeli NIE nagrywamy -> nagranie z buforem (-20 s) + 60 s w przód
  160. - jeżeli JUŻ nagrywamy -> kończymy poprzednie, nowe nagranie bez bufora,
  161. 60 s w przód od TEGO momentu.
  162.  
  163. Dzięki temu pliki czasowo NIE nachodzą na siebie.
  164. """
  165. with self.lock:
  166. if self.recording and self.writer is not None:
  167. # trwa nagranie poprzedniej części -> przerywamy je w tym momencie
  168. print(f"[{self.name}] Przerywam poprzednie nagranie i zamykam plik.")
  169. try:
  170. self.writer.release()
  171. except Exception:
  172. pass
  173. self.writer = None
  174. self.recording = False
  175.  
  176. # nowe nagranie -> BEZ bufora (-20), tylko od TEGO momentu
  177. self.writer = self._start_new_writer(part_code, use_prebuffer=False)
  178.  
  179. else:
  180. # nie nagrywaliśmy nic -> pierwsze nagranie po przerwie
  181. # nagranie z buforem (-20 s)
  182. self.writer = self._start_new_writer(part_code, use_prebuffer=True)
  183.  
  184. if self.writer is None:
  185. # nie udało się stworzyć pliku
  186. self.recording = False
  187. self.record_end_time = 0.0
  188. else:
  189. print(f"[{self.name}] Nagrywam do: {time.ctime(self.record_end_time)}")
  190.  
  191. def run(self):
  192. while True:
  193. if self.cap is None or not self.cap.isOpened():
  194. print(f"[{self.name}] Próba ponownego połączenia za 1 s...")
  195. time.sleep(1)
  196. self._open_stream()
  197. continue
  198.  
  199. ret, frame = self.cap.read()
  200. if not ret:
  201. print(f"[{self.name}] Brak klatki, reconnect...")
  202. time.sleep(1)
  203. self._open_stream()
  204. continue
  205.  
  206. now = time.time()
  207. # zapis do bufora (dla potencjalnego pre-triggera)
  208. self.buffer.append((now, frame.copy()))
  209.  
  210. # jeśli trwa nagrywanie post-trigger -> zapisuj bieżące klatki
  211. with self.lock:
  212. if self.recording and self.writer is not None:
  213. self.writer.write(frame)
  214. if now >= self.record_end_time:
  215. print(f"[{self.name}] Koniec nagrania post-trigger, zamykam plik.")
  216. self.recording = False
  217. try:
  218. self.writer.release()
  219. except Exception:
  220. pass
  221. self.writer = None
  222.  
  223.  
  224. # ================== SERWER TCP DO ODBIORU KODU CZĘŚCI ==================
  225.  
  226. def tcp_server(camera_objects):
  227. """
  228. Prosty serwer TCP:
  229. - HMI łączy się z IP mini-komputera:TCP_PORT
  230. - wysyła tekst z kodem części + np. \n
  231. - serwer wywołuje trigger() dla wszystkich kamer
  232. """
  233. srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  234. srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  235. srv.bind((TCP_HOST, TCP_PORT))
  236. srv.listen(5)
  237. print(f"[TCP] Nasłuch na {TCP_HOST}:{TCP_PORT}")
  238.  
  239. while True:
  240. conn, addr = srv.accept()
  241. print(f"[TCP] Połączenie od: {addr}")
  242. with conn:
  243. try:
  244. data = conn.recv(1024)
  245. if not data:
  246. continue
  247. part_code = data.decode("utf-8", errors="ignore").strip()
  248. print(f"[TCP] Otrzymano kod części: '{part_code}'")
  249.  
  250. if part_code:
  251. for cam in camera_objects:
  252. cam.trigger(part_code)
  253.  
  254. except Exception as e:
  255. print(f"[TCP] Błąd przy obsłudze połączenia: {e}")
  256.  
  257.  
  258. # ================== MAIN ==================
  259.  
  260. def main():
  261. ensure_output_dir()
  262.  
  263. # uruchom wątki kamer
  264. cameras = []
  265. for name, url in CAMERAS.items():
  266. cam = CameraRecorder(name, url)
  267. cam.start()
  268. cameras.append(cam)
  269.  
  270. # wątek czyszczenia starych plików (30 dni)
  271. cleaner = threading.Thread(target=cleanup_worker, daemon=True)
  272. cleaner.start()
  273.  
  274. # wątek serwera TCP do odbioru kodów części
  275. server_thread = threading.Thread(target=tcp_server, args=(cameras,), daemon=True)
  276. server_thread.start()
  277.  
  278. print("=== SYSTEM NAGRYWANIA URUCHOMIONY ===")
  279. print(f"Katalog nagrań: {OUTPUT_DIR}")
  280. print(f"Retencja: {RETENTION_DAYS} dni")
  281. print(f"Pre-trigger: {PRE_SECONDS} s, Post-trigger: {POST_SECONDS} s")
  282. print(f"Nasłuch TCP na porcie: {TCP_PORT}\n")
  283.  
  284. # główna pętla utrzymuje proces przy życiu
  285. try:
  286. while True:
  287. time.sleep(1)
  288. except KeyboardInterrupt:
  289. print("Zamykanie...")
  290.  
  291.  
  292. if __name__ == "__main__":
  293. main()
  294.  
Advertisement
Comments
  • wiktortokumpel
    63 days (edited)
    Comment was deleted
  • wiktortokumpel
    63 days
    Comment was deleted
  • wiktortokumpel
    62 days
    # text 0.27 KB | 0 0
    1. import socket
    2.  
    3. IP = "192.168.1.100" # IP komputera z recorder.py
    4. PORT = 5000
    5.  
    6. code = input("Podaj kod części: ")
    7.  
    8. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    9. s.connect((IP, PORT))
    10. s.sendall((code + "\n").encode("utf-8"))
    11.  
    12. print("Wysłano.")
Add Comment
Please, Sign In to add comment