View difference between Paste ID: PaMrFU7u and SbEwRbzX
SHOW: | | - or go back to the newest paste.
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
13+
14
}
15-
    # w przyszłości możesz dodać np.:
15+
16-
    # "KAMERA2": "rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif",
16+
PRE_SECONDS = 10      # ile sekund PRZED triggerem ma być w nagraniu
17
POST_SECONDS = 15     # ile sekund PO triggerze ma być w nagraniu
18
FPS_DEFAULT = 25      # stały fps do pliku
19-
PRE_SECONDS = 20        # ile sekund wstecz ma być w nagraniu (dla pierwszego nagrania po przerwie)
19+
20-
POST_SECONDS = 60       # ile sekund po triggerze nagrywać
20+
OUTPUT_DIR = r"D:\wideo"
21-
FPS_DEFAULT = 25        # używane gdy kamera nie zwraca sensownego FPS
21+
RETENTION_DAYS = 1    # na testy 1 dzień, potem możesz dać 30
22
23-
# ŚCIEŻKA NA DYSK USB – ZMIEŃ LITERĘ DYSKU JEŚLI POTRZEBA:
23+
TCP_HOST = "0.0.0.0"
24-
OUTPUT_DIR = r"E:\Nagrania"
24+
TCP_PORT = 5000
25
26-
RETENTION_DAYS = 30     # ile dni trzymać nagrania, starsze są kasowane
26+
27
# ================== POMOCNICZE ==================
28-
TCP_HOST = "0.0.0.0"    # nasłuch na wszystkich interfejsach
28+
29-
TCP_PORT = 5000         # port, na który HMI wysyła kod części
29+
30
    os.makedirs(OUTPUT_DIR, exist_ok=True)
31
32
33
def cleanup_old_files():
34
    now = time.time()
35
    cutoff = now - RETENTION_DAYS * 24 * 3600
36
37
    for filename in os.listdir(OUTPUT_DIR):
38
        filepath = os.path.join(OUTPUT_DIR, filename)
39-
    """
39+
        if os.path.isfile(filepath):
40-
    Usuwa pliki starsze niż RETENTION_DAYS w OUTPUT_DIR.
40+
            if os.path.getmtime(filepath) < cutoff:
41-
    """
41+
                print(f"[CLEANUP] Usuwam stary plik: {filename}")
42
                os.remove(filepath)
43
44
45-
    try:
45+
46-
        for filename in os.listdir(OUTPUT_DIR):
46+
47-
            filepath = os.path.join(OUTPUT_DIR, filename)
47+
48-
            if os.path.isfile(filepath):
48+
        time.sleep(24 * 3600)
49-
                mtime = os.path.getmtime(filepath)
49+
50-
                if mtime < cutoff:
50+
51-
                    print(f"[CLEANUP] Usuwam stary plik: {filename}")
51+
52-
                    os.remove(filepath)
52+
53-
    except Exception as e:
53+
54-
        print(f"[CLEANUP] Błąd podczas czyszczenia: {e}")
54+
55
    return re.sub(r"[^0-9A-Za-z_-]", "_", part_code)
56
57
58-
    """
58+
# ================== KAMERA ==================
59-
    Wątek, który co 24h uruchamia cleanup_old_files().
59+
60-
    """
60+
61
    def __init__(self, name, url):
62
        super().__init__(daemon=True)
63-
        time.sleep(24 * 3600)  # 24 godziny
63+
64
        self.url = url
65
        self.lock = threading.Lock()
66
67-
    """
67+
68-
    Czyści kod części tak, by nadawał się do nazwy pliku.
68+
69-
    Zostawiamy tylko cyfry, litery, _, -.
69+
70-
    """
70+
        self.fps = FPS_DEFAULT
71
        self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 640)
72
        self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 480)
73
74-
    safe = re.sub(r"[^0-9A-Za-z_-]", "_", part_code)
74+
        # bufor: (timestamp, frame)
75-
    return safe
75+
        self.buffer = deque()
76
        self.buffer_max_seconds = PRE_SECONDS + 2  # mały zapas
77
78-
# ================== KLASA DO OBSŁUGI KAMERY ==================
78+
        self.recording = False
79
        self.record_end_time = 0.0
80
        self.writer = None
81
82
        print(f"[{self.name}] Start. W={self.width} H={self.height}, fps={self.fps}")
83
84
    def _open_stream(self):
85
        if self.cap:
86
            self.cap.release()
87
        print(f"[{self.name}] Otwieram RTSP...")
88
        self.cap = cv2.VideoCapture(self.url)
89-
        fps = self.cap.get(cv2.CAP_PROP_FPS) if self.cap else 0
89+
90-
        if fps <= 0 or fps > 120:
90+
            print(f"[{self.name}] ❌ Nie można otworzyć strumienia RTSP!")
91-
            fps = FPS_DEFAULT
91+
92-
        self.fps = fps
92+
    def _trim_buffer(self, now: float):
93
        cutoff = now - self.buffer_max_seconds
94
        while self.buffer and self.buffer[0][0] < cutoff:
95
            self.buffer.popleft()
96
97-
        maxlen = int(self.fps * PRE_SECONDS)
97+
98-
        self.buffer = deque(maxlen=maxlen)
98+
99
        Tworzy nowy plik i:
100-
        self.recording = False       # czy aktualnie nagrywamy post-trigger
100+
        - jeśli use_prebuffer=True -> zapisuje dokładnie PRE_SECONDS wstecz,
101
          a POTEM ustawia record_end_time = teraz + POST_SECONDS
102
        - jeśli use_prebuffer=False -> od razu ustawia record_end_time = teraz + POST_SECONDS
103
        """
104
        # czas wyzwolenia do nazwy pliku
105-
        print(f"[{self.name}] Start. FPS={self.fps}, {self.width}x{self.height}, bufor={maxlen} klatek")
105+
        trigger_time_for_name = time.time()
106
        ts_name = datetime.fromtimestamp(trigger_time_for_name).strftime("%Y_%m_%d_%H_%M_%S")
107
        code = sanitize_part_code(part_code)
108-
        if self.cap is not None:
108+
        filename = f"{ts_name}_{code}_{self.name}.mp4"
109
        filepath = os.path.join(OUTPUT_DIR, filename)
110-
        print(f"[{self.name}] Otwieram strumień RTSP: {self.url}")
110+
111
        print(f"[{self.name}] Start nagrania: {filename} | pre={use_prebuffer}")
112
113-
            print(f"[{self.name}] ❌ Nie można otworzyć strumienia!")
113+
114
            filepath,
115
            cv2.VideoWriter_fourcc(*"mp4v"),
116
            self.fps,
117-
        Tworzy nowy plik wideo:
117+
118-
        - use_prebuffer = True  -> zapisuje bufor (-20 s)
118+
119-
        - use_prebuffer = False -> nagrywa tylko od triggera (bez cofania)
119+
120-
        Nazwa pliku: RRRR_MM_DD_GG_MM_SS_KODCZESCI_KAMERA.mp4
120+
121
            print(f"[{self.name}] ❌ Błąd otwierania pliku!")
122-
        now = time.time()
122+
123-
        timestamp_str = datetime.fromtimestamp(now).strftime("%Y_%m_%d_%H_%M_%S")
123+
124-
        safe_code = sanitize_part_code(part_code)
124+
        # najpierw zapisujemy pre-buffer (10s wstecz)
125
        if use_prebuffer:
126-
        filename = f"{timestamp_str}_{safe_code}_{self.name}.mp4"
126+
            # cutoff liczymy od momentu triggera (sprzed chwili)
127
            cutoff = trigger_time_for_name - PRE_SECONDS
128
            frames_to_write = [frame for (t, frame) in list(self.buffer) if t >= cutoff]
129-
        print(f"[{self.name}] Nowe nagranie: część={part_code}, plik={filename}, prebuffer={use_prebuffer}")
129+
            print(f"[{self.name}] Zapisuję pre-buffer: {len(frames_to_write)} klatek (~{PRE_SECONDS}s)")
130
            for frame in frames_to_write:
131
                writer.write(frame)
132
133
        # UWAGA: koniec nagrywania liczymy OD TEGO MOMENTU,
134
        # czyli po zapisaniu pre-buffer (żeby nie ucinało po 5s).
135
        self.recording = True
136
        self.record_end_time = time.time() + POST_SECONDS
137
138
        return writer
139-
            print(f"[{self.name}] ❌ Nie można otworzyć pliku do zapisu!")
139+
140
    def trigger(self, part_code: str):
141
        with self.lock:
142-
        # jeśli mamy użyć bufora wstecznego (-20 s)
142+
143
            if self.recording and self.writer:
144-
            print(f"[{self.name}] Zapis bufora (~{PRE_SECONDS} s wstecz, {len(self.buffer)} klatek)...")
144+
                print(f"[{self.name}] Kończę poprzednie nagranie (nowy trigger).")
145-
            for (_t_frame, frame) in self.buffer:
145+
146
                    self.writer.release()
147
                except Exception:
148-
        # ustaw parametry nagrywania post-trigger
148+
149
                self.writer = None
150-
        self.record_end_time = now + POST_SECONDS
150+
151
                # nowe nagranie BEZ pre-buffer
152
                self.writer = self._start_new_writer(part_code, use_prebuffer=False)
153
            else:
154
                # pierwsze / po przerwie – z pre-buffer
155
                self.writer = self._start_new_writer(part_code, use_prebuffer=True)
156-
        Wywoływane, gdy przychodzi trigger z kodem części.
156+
157
            if self.writer:
158-
        Logika:
158+
159-
        - jeżeli NIE nagrywamy -> nagranie z buforem (-20 s) + 60 s w przód
159+
160-
        - jeżeli JUŻ nagrywamy -> kończymy poprzednie, nowe nagranie bez bufora,
160+
                print(f"[{self.name}] ❌ Nie udało się rozpocząć nagrania.")
161-
          60 s w przód od TEGO momentu.
161+
162
    def run(self):
163-
        Dzięki temu pliki czasowo NIE nachodzą na siebie.
163+
164
            if not self.cap.isOpened():
165
                time.sleep(1)
166-
            if self.recording and self.writer is not None:
166+
167-
                # trwa nagranie poprzedniej części -> przerywamy je w tym momencie
167+
168-
                print(f"[{self.name}] Przerywam poprzednie nagranie i zamykam plik.")
168+
169
            ret, frame = self.cap.read()
170
            if not ret:
171
                time.sleep(0.5)
172
                self._open_stream()
173
                continue
174
175
            now = time.time()
176-
                # nowe nagranie -> BEZ bufora (-20), tylko od TEGO momentu
176+
177
            with self.lock:
178
                # aktualizacja bufora
179
                self.buffer.append((now, frame.copy()))
180-
                # nie nagrywaliśmy nic -> pierwsze nagranie po przerwie
180+
                self._trim_buffer(now)
181-
                # nagranie z buforem (-20 s)
181+
182
                # zapis post-trigger
183
                if self.recording and self.writer:
184-
            if self.writer is None:
184+
185-
                # nie udało się stworzyć pliku
185+
186
                        print(f"[{self.name}] Koniec nagrania (post {POST_SECONDS}s).")
187-
                self.record_end_time = 0.0
187+
188
                            self.writer.release()
189
                        except Exception:
190
                            pass
191
                        self.writer = None
192
                        self.recording = False
193-
            if self.cap is None or not self.cap.isOpened():
193+
194-
                print(f"[{self.name}] Próba ponownego połączenia za 1 s...")
194+
195
# ================== TCP SERVER ==================
196
197
def tcp_server(cameras):
198
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
199
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
200
    srv.bind((TCP_HOST, TCP_PORT))
201-
                print(f"[{self.name}] Brak klatki, reconnect...")
201+
202
203
    print(f"[TCP] Nasłuch na {TCP_HOST}:{TCP_PORT}")
204
205
    while True:
206
        conn, addr = srv.accept()
207-
            # zapis do bufora (dla potencjalnego pre-triggera)
207+
208-
            self.buffer.append((now, frame.copy()))
208+
209
                data = conn.recv(1024)
210-
            # jeśli trwa nagrywanie post-trigger -> zapisuj bieżące klatki
210+
211
                    continue
212-
                if self.recording and self.writer is not None:
212+
                code = data.decode(errors="ignore").strip()
213
                print(f"[TCP] Kod części: {code!r}")
214
                if not code:
215-
                        print(f"[{self.name}] Koniec nagrania post-trigger, zamykam plik.")
215+
216
                for cam in cameras:
217
                    cam.trigger(code)
218
            except Exception as e:
219
                print("[TCP] Błąd:", e)
220
221
222
# ================== MAIN ==================
223
224-
# ================== SERWER TCP DO ODBIORU KODU CZĘŚCI ==================
224+
225
    ensure_output_dir()
226-
def tcp_server(camera_objects):
226+
227-
    """
227+
228-
    Prosty serwer TCP:
228+
229-
    - HMI łączy się z IP mini-komputera:TCP_PORT
229+
230-
    - wysyła tekst z kodem części + np. \n
230+
231-
    - serwer wywołuje trigger() dla wszystkich kamer
231+
232-
    """
232+
233
    threading.Thread(target=cleanup_worker, daemon=True).start()
234
    threading.Thread(target=tcp_server, args=(cameras,), daemon=True).start()
235
236
    print("=== SYSTEM NAGRYWANIA URUCHOMIONY ===")
237
    print("Katalog:", OUTPUT_DIR)
238
    print(f"Pre: {PRE_SECONDS} s, Post: {POST_SECONDS} s")
239
    print(f"Retencja: {RETENTION_DAYS} dni\n")
240
241-
        print(f"[TCP] Połączenie od: {addr}")
241+
242
        time.sleep(1)
243
244
245
if __name__ == "__main__":
246
    main()
247-
                part_code = data.decode("utf-8", errors="ignore").strip()
247+
248-
                print(f"[TCP] Otrzymano kod części: '{part_code}'")
248+