---
name: sdr-project
description: Kontekst i architektura projektu RTL-SDR na Raspberry Pi (centralny scheduler, skaner naziemny, APRS, rtl_433, satelity/NOAA/ISS/FUNcube, GRAVES, radiosondy) z wysylka na Slack. Uzyj gdy pracujesz nad tym projektem SDR - zawiera dostep SSH, konfiguracje Slack, liste kanalow RF, stan architektury i liste TODO na nastepna sesje.
---

# sdrscanner - RTL-SDR

Nazwa aplikacji: **sdrscanner**.

Raspberry Pi z dwoma dongle RTL-SDR odbiera i skanuje rozne pasma radiowe,
przetwarza audio (denoise/EQ), i wysyla wyniki (audio, obraz lub tekst) na
dedykowane kanaly Slack. Centralny scheduler (`~/sdr-scanner/scheduler.py`,
uruchamiany jako systemd) koordynuje dostep obu kart do wszystkich modulow.

## Dostep

- SSH: `ssh rpi` (alias w `~/.ssh/config` na maszynie kontrolujacej, host 192.168.101.100, user `def`, klucz ed25519)
- Skrypty/config na Pi: `~/sdr-scanner/` (scheduler + skaner naziemny + satellites/ + funcube/ + graves/ + radiosonde/ + adsb/), `~/aprs/` (APRS + map/), `~/iss/`, `~/rtl433/`, `~/.config/slack-audio/`

## Sprzet

Dwa dongle RTL-SDR, obie karty widoczne rownoczesnie (`rtl_test -d 999`):

| Device index | Model | Tuner | SN |
|---|---|---|---|
| 0 | RTL-SDR Blog V4 | R828D | 00000001 |
| 1 | Generic RTL2832U OEM | R820T | 00000002 |

Dwie rozne anteny (2m i 70cm - rezonansowe, nie szerokopasmowe). **Ktora
antena wisi na ktorym dongle NIE jest jeszcze potwierdzone fizycznie** -
podejrzenie (z zachowania kalibracji, patrz sekcja lekcji) ze device 0 ma
anteny 2m, co zle pasuje do PMR446 (446MHz/UHF) skanowanego teraz na
device 0 - do zweryfikowania fizycznie, patrz TODO.

**Znane problemy sprzetowe:**

1. **"Wedge"** - dongle tunuje sie poprawnie ale streamuje 0 bajtow (rozne od
   fizycznego odlaczenia, `rtl_test -d 999` go nadal widzi). Naprawa:
   `sudo usbreset "<Product>"` (np. `"Blog V4"` / `"RTL2838UHIDIR"`) - UWAGA:
   forma `usbreset 0bda:2838` (VID:PID) jest NIEJEDNOZNACZNA bo oba dongle
   maja identyczny VID:PID, resetowaloby losowo pierwsze dopasowanie. Health
   monitor auto-naprawia to (patrz sekcja modulow) - ALE ma znany false-
   positive: jego weryfikacja-po-naprawie czasem koliduje z legalnym
   procesem ktory juz z powrotem zaczal uzywac karty (usb_claim_interface
   error -6 = "zajete", nie "nadal zawieszone") i wysyla mylny alert "naprawa
   NIEUDANA" mimo ze karta faktycznie dziala - NIE FIX-NIETE jeszcze
   (dev1: rozroznic "busy" vs "wedge" w kroku weryfikacji tak samo jak juz
   robi to w glownym wedge-check).

2. **Calkowite zniknięcie z USB** (gorsze niz wedge) - dongle w ogole nie
   enumeruje sie (`dmesg`: "unable to enumerate USB device" / "device
   descriptor read error"), `rtl_test`/`lsusb` go NIE widza wcale. `usbreset`
   NIE POMOZE (brak uchwytu USB do zresetowania - "No such device"). Widziane
   realnie na device 1 (SN 00000002, port USB 1-1.2) - prawdopodobnie luzny
   kabel/port/zasilanie, nie software.

3. **Padniecie calego kontrolera USB (xhci_hcd)** - obserwowane gdy flaky
   dongle probowal sie wielokrotnie re-enumerowac (power-cycle attempts w
   dmesg) - `xhci_hcd: HC died; cleaning up`, ZABIERA ZE SOBA WSZYSTKO na tej
   magistrali (takze stabilny wczesniej device 0!). Naprawa BEZ pelnego
   rebootu Pi: reset samego kontrolera PCI przez sysfs:
   ```
   echo -n '0000:01:00.0' | sudo tee /sys/bus/pci/drivers/xhci_hcd/unbind
   sleep 2
   echo -n '0000:01:00.0' | sudo tee /sys/bus/pci/drivers/xhci_hcd/bind
   ```
   (adres PCI sprawdzic przez `ls /sys/bus/pci/drivers/xhci_hcd/`). Po tym
   dongle re-enumeruja sie normalnie. Ratuje dzialajacy dongiel bez utraty
   SSH sesji.

4. **Przesluch/wyciek LO miedzy dwoma dongle'ami** (fizycznie blisko na tym
   samym hubie USB) - jeden dongiel tunowany na czestotliwosc X potrafi
   wygenerowac falszywy, silny, STALY sygnal na TEJ SAMEJ czestotliwosci X
   widoczny przez DRUGI dongiel. Potwierdzone empirycznie (`rtl_power`,
   PMR446 CH4/446.04375MHz): pik -5dB (14dB nad podloga szumow) gdy device 0
   byl tam nastrojony, calkowicie plaskie gdy device 0 byl gdzie indziej.
   Diagnostyka: `rtl_power -d <wolna_karta> -f <freq-10k>:<freq+10k>:1k -i 5
   -1 out.csv` w momencie potwierdzonym w logu ze DRUGA karta jest (lub NIE
   jest) na tej czestotliwosci - porownac wartosc dokladnie na docelowej
   czestotliwosci: wyrazny pik (+8dB i wiecej nad mediana) = przeslu ch/
   birdie, plasko = prawdziwy problem gdzie indziej (np. zbyt czuly squelch).
   Namierzone tak jednoznacznie tylko dla PMR CH4 jak dotad - inne "szumiace"
   kanaly (PKP R5, PSP Poznan) w kontrolnym tescie na WLASNEJ antenie
   device'u 0 wyszly PLASKIE (brak piku) = to byl inny problem, patrz #5.

5. **Squelch NFM "utyka" gdy bazowa kalibracja jest degenerowana** (patrz
   sekcja lekcji - bug w `ground_scanner.py` juz naprawiony).

6. **Wedge W TRAKCIE kalibracji squelcha = zatruta kalibracja closing_level=1**
   (incydent 2026-08-11 ~15:2x): dongiel streamujacy 0 bajtow wyglada w
   bisekcji -l identycznie jak "cisza przy -l 1" -> wszystkie kanaly dostaja
   closing_level=1 (operacyjny -l 2-4) na 6h TTL -> po ozywieniu karty
   wieczne nagrania szumu / squelch otwarty non-stop. Objaw w logu: seria
   "squelch: otwarty przy -l<=1, zamyka sie od -l 1" na WIELU kanalach pod
   rzad (zdrowe wartosci to -l 10-35). Naprawa: usbreset + restart instancji
   skanera (kalibracje sa w pamieci procesu). Guard w kodzie (lokalna wersja
   z hopperem, do wdrozenia): przy closing==1 dodatkowa proba BEZ squelcha -
   brak danych = RuntimeError/failed-cooldown zamiast falszywej kalibracji.

## Architektura schedulera (ZBUDOWANA I WDROZONA - `~/sdr-scanner/scheduler.py`)

Centralny koordynator zarzadza obiema kartami dynamicznie wg priorytetow.
Dziala jako systemd (`sdrscheduler.service`), enabled, przetrwa reboot.

1. **Satelity/NOAA/ISS/FUNcube** (najwyzszy) - TYLKO w oknie przewidzianego
   przelotu (AOS-90s .. LOS+60s, na bazie `predict_passes.get_upcoming_passes()`
   + `funcube_predict`). Na czas okna przejmuja device 1 na wylacznosc
   (istniejace moduly satelitarne odmawiaja pracy na device 0 - wbudowany
   guard). Wlasciwe powiadomienia/nagrania odpalaja ISTNIEJACE skrypty-ticki
   (`predict_passes.py` / `iss_scheduler.py` / `funcube_predict.py`), ktore
   maja wlasny dedup - scheduler tylko wywoluje je cyklicznie w poblizu
   zdarzen AOS/LOS (co 45s) i pilnuje zeby karta byla wtedy wolna.
2. **Skaner naziemny** (`ground_scanner.py`) - trzyma device 0 CIAGLE, a w
   swoim slocie rotacji (najwiekszy = wyzszy priorytet) dostaje DODATKOWO
   device 1 ("tryb dwoch kart"): scheduler dzieli `channels.csv` na dwa
   ROZLACZNE podzbiory pasmowe (VHF < `DEV1_BAND_MIN_MHZ`=300MHz <= UHF),
   zapisuje jako osobne pliki CSV, odpala druga instancje z `--device 1`.
   Przejscie w tryb dwoch kart uzywa "handshake" (zawez CSV dev0, czekaj na
   potwierdzenie hot-reloadu, dopiero wtedy start dev1) - PIERWSZY handshake
   po zimnym starcie moze trwac do ~10 min (caly cykl kalibracji squelchy).
3. **APRS / rtl_433 / GRAVES / radiosondy** (najnizszy) - odbiorniki ciagle,
   wiec "dzielenie wolnego czasu" = rotacja slotami czasowymi na device 1
   (scanner_dev1 25min > aprs 15 > radiosonde 15 > rtl433 10 > graves 10 -
   sam skaner tez uczestniczy w tej rotacji z najwiekszym slotem).

**Rejestr czestotliwosc->karta** (`FreqRegistry`): pilnuje zeby dwie karty
NIGDY nie stroily jednoczesnie na te sama/nakladajaca sie czestotliwosc -
konflikt = modul NIE startuje (log ERROR), nie krysza kolizja fizyczna.

**Guard obecnosci device'u** (`device_available()`, `enumerated_device_count()`
w scheduler.py) - PRZED startem czegokolwiek na dzielonej karcie sprawdza
realna liczbe zenumerowanych dongli (`rtl_test -d 999`, cache 5s). Dodany po
incydencie: gdy device 1 fizycznie zniknal, `rtl_fm -d 1` NIE zawiodl
bezpiecznie (librtlsdr nie failuje na out-of-range indeksie przy 1 urzadzeniu)
tylko po cichu podpial sie pod TEN SAM fizyczny dongiel co device 0,
blokujac skaner (wyzszy priorytet) na kilka minut. Guard to teraz uniemozliwia.

**Config-driven enable/disable** (`~/sdr-scanner/modules.yaml`) - czytany
przy starcie i HOT-RELOAD co tick (mtime, jak `channels.csv`). Sekcje:
`modules:` (scanner_dev0/scanner_dev1/aprs/rtl433/graves/radiosonde per
modul true/false) i `satellites:` (`enabled` = cala warstwa, `ticks:` per
skrypt). CLI `--disable nazwa1,nazwa2` zawsze wygrywa nad plikiem (tez po
hot-reloadzie). Wylaczenie modulu rotacji KONCZY biezacy slot, nie zabija w
polowie. Wylaczenie satelitow w trakcie AKTYWNEGO okna jest ODLOZONE do LOS
(`sat_disable_pending`) - chroni przed przerwaniem nagrania w polowie, ALE
jesli okno satelitarne ciagle sie przedluza (kolejne nakladajace sie
przeloty), disable moze czekac dlugo - w takim wypadku RESTART calego
`sdrscheduler.service` dziala natychmiast i bezpiecznie JESLI w danym
momencie nic aktywnie nie nagrywa (sprawdz `ps aux` na satelitarne procesy
najpierw).

**Systemd** (`sdrscheduler.service` + `sdr-health-monitor.service`, oba
`enabled`): `Type=simple`, `Restart=on-failure`, `TimeoutStopSec` z zapasem
nad najdluzszym `stop_grace_s` (radiosondy 15s). `health_monitor.py` mimo
wczesniejszego statusu "gotowe" NIGDY nie mial zadnego mechanizmu
uruchamiania (ani cron, ani proces) do czasu tej sesji - teraz ma wlasny
systemd unit.

**Obserwowalnosc:** `~/sdr-scanner/scheduler_run/state.json` (co tick,
atomowo: wlasciciele kart, rejestr czestotliwosci, PID-y, przeloty), logi per
modul w `scheduler_run/logs/*.log`.

**Znane ograniczenia:**
- Satelity zawsze biora device 1 (guardy w modulach odmawiaja dev0).
- Restart schedulera w trakcie trwajacego przelotu moze nie odtworzyc
  rezerwacji od razu (`get_upcoming_passes` patrzy w przod).
- Pierwszy handshake dwoch kart po zimnym starcie: do ~10 min.
- **celestrak.org bywa nieosiagalny** (TCP timeout mimo dzialajacego ogolnego
  lacza - potwierdzone `ping 8.8.8.8` OK, DNS resolve OK, ale `connect()` na
  port 443/80 wisi) - kazda nieudana proba fetch TLE zjada czas w GLOWNEJ
  PETLI schedulera (blokujaca, ~120s timeout per tick satelitarny), co
  spowalnia CALY scheduler (nie tylko satelity). Fallback na cache TLE juz
  dziala (`uzywam cache sprzed Xh`), ale nie ratuje czasu ticku. Obejscie
  zastosowane w tej sesji: `127.0.0.1 celestrak.org www.celestrak.org` w
  `/etc/hosts` na Pi - wymusza natychmiastowy fail zamiast timeoutu. USUNAC
  ten wpis kiedy celestrak wroci/jesli satelity znow beda potrzebne na
  biezaco (obecnie i tak wylaczone).

## Odbiornik wielokanalowy (GLOWNA ZMIANA 2026-08-11, WDROZONY)

`rtl_multichannel.py` - dongiel trzyma okno 2.4MHz, jedno FFT (65536, scipy
complex64) rozdziela je na WSZYSTKIE kanaly banku naraz. Kazdy kanal ma
wlasny squelch (moc RF liczona wprost z binow FFT - prawdziwe RSSI), wlasny
demodulator (NFM = dyskryminator fazy + DE-EMFAZA 50us, AM = obwiednia) i
wlasne nagrywanie. Pokrycie czasu = 100% na kazdym kanale banku.

Kluczowe fakty (zmierzone na Pi 4 @1.8GHz):
- koszt CPU prawie NIE ROSNIE z liczba kanalow: 1 kan. 26%, 63 kan. 29%,
  100 kan. 33% jednego rdzenia (dominuje jedno FFT bloku)
- scipy.fft (complex64) zamiast numpy.fft (promuje do complex128) = 2x szybciej
- moc kanalu liczona z ~25 binow FFT, IFFT+demodulacja TYLKO dla kanalow
  ktore faktycznie nadaja (bez tego 63 kanaly kosztowaly 67% zamiast 29%)
- ODCZYT IQ MUSI BYC W OSOBNYM WATKU: rtlsdr_read_sync pobiera dane dopiero
  na zadanie, wiec przerwa na przetwarzanie = DZIURA w strumieniu. Zmierzone:
  pokrycie 64-71% bez watku, 100% z watkiem (`BankReceiver.start_stream`)
- rate audio = fs*W/N = 18750 Sa/s (NIE 22050!) - przekazywany do
  `process_and_send(rate=...)` i dalej do ffmpeg
- czas trwania/hangover liczony w PROBKACH, nie zegarem sciennym (petla
  potrafi isc szybciej niz realny czas - wtedy transmisja nie zamykala sie)

Podzial kanalow miedzy karty jest BANKOWY (`split_rows` w scheduler.py):
dev0 = najliczniejszy bank na wylacznosc, dev1 = reszta w rotacji z postojem
`--mc-dwell` (8s). Wazenie postoju historia z freq_stats jest WYLACZONE na
prosbe uzytkownika (`USE_ACTIVITY_SCORE=False`, `--no-weight-from-stats`).

## Bezpieczniki dodane 2026-08-11 (kazdy po realnej awarii)

1. **Watchdog zaciecia (`--mc-stall-timeout`, 12s)** - `rtlsdr_read_sync` NIE
   MA TIMEOUTU. Gdy dongiel sie zawiesi albo ktos go zresetuje pod spodem
   (health_monitor robi to automatycznie!), watek glowny zostaje w libusb NA
   ZAWSZE: proces zyje, CPU ~1%, zero nagran. Osobny watek pilnuje kiedy
   przyszedl ostatni blok i robi `sudo usbreset <produkt>`; zablokowany read
   konczy sie wtedy bledem, a petla otwiera dongla na nowo.
2. **Anty-livelock w sciezce rtl_fm** (`--max-visit-seconds` 90,
   `--noise-abort` 2) - PKP R6 z kalibracja closing_level=3 (operacyjny -l 4)
   trzymal dev0 przez 22 MINUTY: 45s nagrania szumu -> linger 10s -> squelch
   znowu otwarty -> ... Linger przedluzal deadline wizyty bez gornego limitu.
   Teraz: twardy budzet wizyty, wyjscie po 2 nagraniach szumu z rzedu,
   podniesienie marginesu o 4dB, po 3 utknieciach kanal na 15 min do kata.
3. **Auto-odrzucanie kanalow z ciaglym zakloceniem** (`note_channel_verdict`)
   - piszczaca nosna/birdie nie zamyka squelcha, wiec nagranie leci do limitu
   i wychodzi "szum": wchlaniamy sygnal w tlo + margines +5dB, potem
   wyciszenie kanalu na 30 min, przy nawrocie na 6h.
4. **Watchdog wedge w sciezce rtl_fm** (`note_rtl_fm_data`) - seria odczytow
   bez danych -> `usbreset` wlasnej karty + czystka kalibracji.

## Obrobka audio (przepisana 2026-08-11)

- **przycinanie ciszy PRZED klasyfikacja** (`trim_silence_pcm`) - hangover
  squelcha bylo slychac jako 2-3s pustki. UWAGA: w trybie wielokanalowym ogon
  jest ciety po MOCY RF, nie po glosnosci audio - w FM brak nosnej daje
  GLOSNY szum (capture effect), wiec obwiednia audio go nie odroznia od mowy
- **odrzut krotkich trzaskow** (`--min-voice-duration` 0.8s) liczony PO
  przycieciu - "puknięcie + sekunda ciszy" nie idzie juz na Slack
- **lancuch ffmpeg** (`build_audio_filter`, `--denoise off|normal|strong`):
  highpass 250 -> lowpass 3400 -> afftdn ze sledzeniem szumu (tn=1) ->
  [anlmdn przy strong] -> equalizer 1.8k -> acompressor -> loudnorm
- **wiadomosc na Slacku okrojona** do jednej linijki (nazwa, freq, dlugosc) -
  blok techniczny usuniety na prosbe uzytkownika
- **de-emfaza 50us** w demodulatorze NFM (rtl_fm robil to tylko z -E deemp,
  ktorej skaner nie uzywal) - bez niej szum rosnie z czestotliwoscia i myli
  klasyfikator szum/mowa

## Bramka OpenSky dla kategorii "air" - USUNIETA (2026-08-11)

Na wyrazna prosbe uzytkownika. Powod: samolot na wysokosci przelotowej jest w
zasiegu optycznym z setek kilometrow, wiec bramka wokol JEDNEGO lotniska
wycinala caly ruch trasowy i z innych lotnisk, a sygnaly lotnicze sa mocne.
Usuniete: `check_air_active`, `--air-poll-interval`, `--disable-air-gating`,
stale OPENSKY/EPPO_BBOX/AIR_LOW_ALT.

## STAN NA KONIEC SESJI 2026-08-11, wieczor (czytaj to najpierw)

Kontynuacja tej samej daty - druga polowa sesji, po wdrozeniu odbiornika
wielokanalowego (opis nizej w tym pliku jest z pierwszej polowy i wciaz
aktualny architektonicznie). To co ponizej NADPISUJE/aktualizuje konkretne
liczby i stan z tamtej czesci.

**WAZNE OSTRZEZENIE (potwierdzone empirycznie ta sesja): ten plik DRYFUJE od
zywego stanu Pi szybciej niz sesje** - liczby progow, liczba kanalow, co jest
wlaczone/wylaczone - WERYFIKUJ NA `ssh rpi` PRZED dzialaniem, nie ufaj samym
liczbom w SKILL.md (patrz "Wazne lekcje" na koncu tej sekcji).

### Anteny - fizycznie zweryfikowane i PRZEPIETE

Uzytkownik potwierdzil fizycznie: RTL-SDR V4 (device 0, R828D) mial podpieta
antene 11m/CB, generyczny R820T (device 1) mial 2m/70cm. To bylo dokladnie
podejrzenie z TODO #3 (nizej w pliku, teraz NIEAKTUALNE). **Zamienione**:
teraz device 0 (V4) ma 2m/70cm, device 1 ma 11m/CB - bo device 0 na stale
trzyma PSP+PKP (148.66-150.575MHz, blisko pasma 2m), a device 1 obsluguje w
rotacji glownie PMR446(446MHz)+2m/70cm-repeaters/simplex (blisko rezonansu)
plus mniejszosciowo CB/10m (ktore i tak nie pasowalyby dobrze do zadnej z
dwoch anten). Nie zweryfikowano jeszcze ilosciowo (peak_db przed/po w
capture_log.csv) - **TODO na nastepna sesje: sprawdzic czy PSP/PKP maja
teraz wyzsze peak_db niz przed zamiana**.

### TODO #1 (przeciek na sasiednie kanaly) - ZROBIONE

`_is_adjacent_leakage()` w `rtl_multichannel.py`: przed otwarciem squelcha,
jesli inny kanal w promieniu 50kHz ma moc >=15dB wyzsza w TYM SAMYM bloku,
to przeciek, nie wlasna transmisja - kanal NIE otwiera squelcha. Test
`test_adjacent_leakage_suppressed()` (izolowany, monkeypatch
`Channelizer.analyze()` zeby narzucic dokladne dB, bez zaleznosci od fizyki
FFT). Wdrozone na Pi, `.bak-przed-adjacent-leakage-2124`.

### Nowa funkcja: wyciszanie przerw w dwustronnej korespondencji

`BankReceiver._mute_interior_gaps()`: gdy w JEDNYM nagraniu sa dwie serie
transmisji z cisza/szumem miedzy nimi (hangover trzyma squelch otwarty przez
krotka przerwe), fragment MIEDZY nimi jest przyciszany o -30dB (nie wycinany
calkiem - trzask). Koncowy "oddech" po OSTATNIM gorącym bloku zostaje
nietkniety (juz istniejacy, celowy fade-out). Test
`test_interior_gap_muted()`. Wdrozone, `.bak-przed-interior-gap-mute-2130`.

### Klasyfikator - duzy postep, 92 etykiety (bylo 45)

- **BUG ZNALEZIONY I NAPRAWIONY**: `NOISE_FLATNESS_NFM` w `signal_classify.py`
  bylo **0.72** na Pi (nie 0.88 jak w pierwszej czesci tego pliku - drift!).
  Podniesione na **0.87** wg rekomendacji `label_recordings.py` (50 etykiet w
  momencie zmiany): przy 0.72 gubilo 19 realnych glosow, przy 0.87 gubi 1,
  przepuszcza 3 szumy - najlepszy kompromis (rozklady CZYTELNE/NIECZYTELNE
  fizycznie sie NAKLADAJA, zaden prog nie da 0 bledow).
- `NOISE_FLATNESS_AM` analogicznie: 0.85 -> **0.94** (dowod cienszy - tylko 2
  etykiety AM na razie, CB 0.854 i EPPO Approach 2 0.927, obie przekraczaly
  stary prog mimo ze czytelne - do doprecyzowania jak przybedzie etykiet).
- Per-kanal `noise_flatness` w channels.csv podniesione (zawsze TYLKO w gore,
  nigdy w dol) na podstawie swiezych etykiet: PKP R1 0.86, PKP R3 0.94, PKP R6
  0.96, PSP B031 0.88, PSP B045 0.89, CB Radio 27.180 0.86 (nowa kolumna).
  **Te liczby beda znow stale przy kolejnych etykietach** - zawsze odpalic
  `label_recordings.py` bez argumentow i sprawdzic PER KANAL sekcje przed
  zalozeniem ze cokolwiek jest nadal aktualne.
- **NOWY GATE: cyfrowy sygnal/DMR** (`DIGITAL_BURST_ENV_MOD_LIMIT=0.18`,
  `DIGITAL_BURST_MIN_DURATION_S=10.0` w `signal_classify.py`). Odkryte na
  LPR/Sluzby medyczne 169.000 (potem tez Pogotowie K33 168.925, K33
  USUNIETY z channels.csv jako trwale DMR): DMR zdemodulowany analogowym NFM
  ma NISKA flatness (wyglada jak mowa!) bo jest strukturalny, ale env_mod
  (rytm sylabowy 2-8Hz) jest tak samo niski jak flatness - i to jest klucz.
  Prawdziwa mowa z niskim env_mod (<0.20) w etykietach zawsze byla KROTKA
  (1.1-3.8s) - DMR trwal 17.6-36.3s. Gate: env_mod<0.18 ORAZ duration>=10s ->
  noise, niezaleznie od flatness. Zwalidowane na pelnym zbiorze 92 etykiet:
  0 falszywych trafien. **Whisper/VAD by to tez zapewne zlapal, ale to
  gate byl szybszy do wdrozenia i juz dziala.**
- **VAD PONOWNIE SPRAWDZONY na 92 etykietach** (`compare_vad.py`, nowy plik w
  repo) - POTWIERDZONY wczesniejszy wniosek z pierwszej czesci tej sesji
  (tam na 21 etykietach, 19 dobre/2 zle - za malo zeby cokolwiek orzec).
  Teraz 45 dobre/47 zle: webrtcvad 42-50% bledu na wszystkich poziomach
  agresywnosci (praktycznie oznacza WSZYSTKO jako mowe), flatness 22% bledu.
  To NIE artefakt malej proby - webrtcvad genuinely nie nadaje sie do tego
  toru (trenowany na szumie telefonicznym). Szczegoly w README.md. Dalszy
  postep w klasyfikacji wymaga Whisper.cpp (patrz TODO nizej), nie VAD-a.

### Squelch i konfiguracja

- `--mc-margin-db` (flaga w scheduler.py, oba skanery): 6 -> **4dB** na
  wyrazna prosbe uzytkownika, PO ustaleniu ze wiekszosc zlych klasyfikacji
  bylo winne progu klasyfikatora, nie squelcha. Obserwowac czy nie zalewa
  szumem teraz gdy klasyfikator jest lepszy.
- APRS 144.800 (ground, decoder=aprs) WYLACZONY (`direct_sampling=true`) na
  prosbe uzytkownika - modul `aprs` w rotacji byl juz i tak wylaczony
  wczesniej, to byl osobny, wciaz aktywny kanal w banku dev1.
- CB dodane z powrotem na **27.180 MHz** (NIE zweryfikowane 27.185 - kanal
  19 standardowego planu 40-kanalowego, ktore bylo w starych backupach -
  uzytkownik swiadomie wybral 27.180 mimo mojej weryfikacji WebSearch ze to
  niestandardowa czestotliwosc; POTWIERDZONE realnym czytelnym ruchem CB na
  tej czestotliwosci, wiec dziala). Kategoria `cb`, priorytet 2.
- **10m dodane**: "10m FM wywolawcza" 29.600 MHz (potwierdzone WebSearch -
  ogolnoswiatowa czestotliwosc wywolawcza FM simplex 10m). Kategoria `10m`,
  priorytet 3. Uzytkownik pod koniec sesji zglosil ze slychac stacje z 10m
  "obok" CB i chce **ROZBUDOWAC pasmo 10m** - NIEDOKONCZONE, do zrobienia
  nastepna sesja (patrz TODO).
- Pogotowie K33 (168.925) USUNIETY z channels.csv - potwierdzone DMR
  (uzytkownik: "dmr nawala"), tej samej klasy problem co LPR 169.000.
  Pogotowie K5 (168.575) ma juz `direct_sampling=true` z wczesniej w sesji
  (przyczyna nieudokumentowana, do sprawdzenia jesli ktos zapyta).

### Wedge na device 1 podczas przelotu NOAA-18

Incydent: NOAA-18 (21:53-22:03) nagral 78 bajtow (pusty plik) - `rtl_fm`
stal 686s bez ani jednego bajtu. Przyczyna najpewniej fizyczne przepiecie
anten chwile wczesniej. `sudo usbreset RTL2838UHIDIR` + bezposredni test
`rtl_sdr` potwierdzily naprawe. Recznie odpalony testowy przechwyt na
pozostaly czas przelotu (270s) potwierdzil ze dongiel juz dziala (25.9MB
realnego nagrania) - ale sygnalu satelity i tak nie bylo (SNR -0.0dB, znany
juz problem antenowy na 137MHz, nie zwiazany z wedge'em). NOAA-19 (nastepny
przelot, elew. 75°, najlepszy tego dnia) automatycznie zlapany przez
scheduler poprawnie - tez bez sygnalu (ta sama przyczyna antenowa). AO-91
(satelita glosowy) - przechwyt OK technicznie, ale caly przelot to byl szum
(min_flatness=0.996) - `any_signal_detected()` poprawnie zablokowal wysylke,
zero spamu. **Wniosek: naprawa wedge'a dziala, karta 1 jest zdrowa - problem
z obrazami NOAA to WYLACZNIE antena, nie oprogramowanie.**

### Etykiety: 45 -> 93

Duzy przyrost z live-feedback uzytkownika sluchajacego Slacka w czasie
rzeczywistym. Wzorzec wspolpracy: uzytkownik wkleja nazwe pliku + werdykt
("czytelny"/"nieczytelny"/"na granicy") - kazde takie zdanie to etykieta,
oznaczyc NATYCHMIAST przez `label_recordings.py dobre|zle <fragment>`, nie
czekac na wiekszy batch. Zobacz `[[feedback-live-labeling-workflow]]` w
pamieci auto-memory (`/Users/def/.claude/projects/-Users-def-sdrscanner/memory/`).

### TODO na nastepna sesje (nowe, w kolejnosci sensownosci)

1. **Rozbudowac pasmo 10m** - uzytkownik zglosil aktywnosc obok CB, obecnie
   tylko jedna czestotliwosc wywolawcza (29.600). Sprawdzic realne pasmo
   aktywnosci (28.000-29.700, SSB/CW/FM segmenty) i dodac wiecej kanalow -
   ale SSB nie jest wspierane przez obecny demodulator (tylko am/nfm w
   `ChannelState.demod()`), wiec praktycznie to dalej segment FM
   (29.520-29.700 wg wczesniejszego WebSearch).
2. **Zweryfikowac ilosciowo zamiane anten** - `peak_db`/`floor_db` na
   PSP/PKP w `capture_log.csv` przed (dane sprzed ~22:00) vs po zamianie.
3. Whisper.cpp (tiny) na 5-sekundowych klipach - teraz z mocniejszym
   uzasadnieniem niz kiedykolwiek (VAD ostatecznie odrzucony na 92
   etykietach, flatness ma twardy sufit ~22% bledu, DMR-gate to tylko
   punktowa lata na jeden konkretny problem).
4. Zbierac etykiety AM (mamy tylko 2) - prog 0.94 stoi na cienkich danych.
5. Sprawdzic czy inne kanaly w 168-169MHz tez przeszly na DMR (systemowy
   problem calego pasma w tej okolicy, czy pojedyncze przypadki).
6. Reszta starych TODO nizej w tym pliku (hopper na Pi, health_monitor
   false-positive, GitHub push) - nietkniete ta sesje, wciaz aktualne.

### Przelacznik kanalu "trash" (debug)

`ground_scanner.py --no-trash` (nowa flaga, `SEND_TO_TRASH` global) wylacza
wysylanie GRANICZNYCH nagran (flatness w pasmie [prog, prog+0.18]) na kanal
Slack "trash" - reszta logiki (klasyfikacja, zapis do capture_log.csv,
labels.csv) dziala bez zmian, tylko sam Slack-send jest pominiety. Na koniec
tej sesji WYLACZONE na prosbe uzytkownika - `--no-trash` dodane do cmd obu
skanerow w `scheduler.py` (linie ~172, ~181). **Zeby PONOWNIE WLACZYC**:
usunac `"--no-trash"` z tych dwoch list i zrestartowac
`sdrscheduler.service`. `.bak-przed-trash-toggle-2320` /
`.bak-przed-no-trash-2321`.

### Wazne lekcje z tej czesci sesji

- **Zawsze weryfikuj live stan Pi, nie licz na liczby w SKILL.md** - ten
  dokument sam z siebie zdrifowal w PIERWSZEJ czesci tej samej sesji (0.88
  vs faktyczne 0.72), zanim jeszcze zdazyl "postarzec sie" miedzy sesjami.
  Zobacz `[[sdrscanner-skill-doc-drift]]` w auto-memory.
- **Backticki w heredoc wewnatrz `ssh rpi "..."` (podwojny cudzyslow) sa
  NIEBEZPIECZNE** - lokalny bash interpretuje je jako podstawienie polecen
  ZANIM `ssh` w ogole zobaczy tekst, mimo ze heredoc uzywa `<<'EOF'`
  (chronioną forme). Skutek: cichy, nie-fatalny blad lokalny + wyciety
  fragment tekstu w pliku docelowym (nie crash, wiec latwo przeoczyc).
  Fix zastosowany: unikac backtickow w tekscie wewnatrz takich konstrukcji,
  albo pisac plik lokalnie i `scp`, albo uzyc pojedynczych cudzyslowow na
  zewnatrz gdziekolwiek to mozliwe.
- **Zero-crossing rate na GOTOWYM (post-ffmpeg) mp3 nie dziala jako cecha**
  do wykrywania DMR - proba na finalnym, przetworzonym audio (highpass/
  lowpass/denoise/compressor/loudnorm) zatarla ewentualna sygnature cyfrowa
  ktora mogla byc widoczna w SUROWYM PCM (ktorego nie zapisujemy). Gdyby
  ktos chcial to sprawdzic porzadnie, trzeba by dodac logowanie surowego PCM
  albo dodatkowej cechy PRZY nagrywaniu (w `classify_raw_pcm`), nie
  rekonstruowac z finalnego mp3.
- **env_mod jako STANDALONE gate byl niebezpieczny** (7 confirmed-dobre
  etykiet ma env_mod<0.20) - dopiero POLACZENIE z dlugoscia (>=10s) dalo
  czysty rozdzial bez falszywych trafien. Nauka ogolna: gdy jedna cecha ma
  overlap z prawdziwymi pozytywami, sprawdz czy INNA cecha (tu: duration)
  rozdziela wlasnie TE konkretne przypadki brzegowe, zamiast od razu
  odrzucac cecha jako bezuzyteczna.


## Moduly GOTOWE i przetestowane

### Centralny scheduler + systemd + config-driven

Patrz sekcja "Architektura schedulera" wyzej - w pelni zbudowane, wdrozone,
przetestowane na zywym sprzecie (w tym wykryte i naprawione 2 realne bugi
produkcyjne, patrz "Wazne lekcje").

### Pipeline audio (fundament, referencyjny)

`~/sdr-scanner/fm_test.py` na Pi: `rtl_fm` -> `ffmpeg` (afftdn denoise,
highpass/lowpass, equalizer, loudnorm) -> mp3 -> Slack (`files.getUploadURLExternal`
+ upload + `files.completeUploadExternal`). Flagi: `--freq`, `--duration`,
`--device`, `--gain`, `--resend`.

### Skaner naziemny (`~/sdr-scanner/ground_scanner.py`)

- Kanaly z `channels.csv` (hot-reload - mtime check co cykl, bez restartu),
  priorytety z `category_priorities.csv` (klucz "category" w channels.csv).
  Wazony round-robin (smooth weighted, priorytet 1=waga5 ... 5=waga1).
- Squelch: dla NFM uzywa wbudowanego `-l` w `rtl_fm` (NIE RMS post-demod -
  FM ma "capture effect", szum jest GLOSNIEJSZY bez nosnej, RMS-threshold
  logika byla odwrotna). Kalibracja per-kanal przez bisekcje logarytmiczna
  (najmniejszy `-l` co w pelni wycisza tlo). Margines w dB nakladany PRZY
  UZYCIU (nie przy kalibracji) przez `operational_level = closing_level *
  10**(margin/20)`, z PODLOGA `>= round(margin)` (naprawiony bug, patrz
  lekcje). Zamkniety squelch = rtl_fm pisze 0 bajtow (nie cisza) - odczyt
  przez `select()` z timeoutem, nie blocking read (NAPRAWIONY W CALYM
  PLIKU, patrz lekcje - `read_chunk()` tez teraz uzywa select). AM (CB,
  EPPO) uzywa RMS post-demod (dziala dobrze, bez capture effect).
- Gain: agresywny, RECZNY, domyslnie **48.0dB** (jeden stopien ponizej max
  49.6dB dla R828D), NIE auto-AGC. Nadpisywalny kolumna `gain_db` per-kanal.
  Zmiana gainu automatycznie uniewaznia/przelicza kalibracje squelch (cache
  kluczowany po name+freq+mode+gain - BEZ device index, patrz TODO #6).
- "air" (EPPO) kategoria: bramkowana przez OpenSky Network API (darmowe,
  ADS-B) - skanowana TYLKO gdy realny ruch nisko nad lotniskiem, inaczej
  pomijana w cyklu.
- Umie dzialac na obu kartach rownolegle (load-balancer) - zarzadzane przez
  scheduler (`scanner_dev1` w rotacji), patrz architektura.
- `--device 0/1` flaga dodana przez scheduler (wczesniej device 0 byl
  zaszyty na sztywno).

**Tryb IQ hopper (DOMYSLNY od 2026-08-11; kod lokalnie, wdrozenie na Pi -
patrz TODO #1).** Motywacja: klasyczny tryb odpala rtl_fm na KAZDA wizyte
kanalu (start procesu + 0.4s settle + 0.8s probe ~= 1.5-2s/kanal = ~30-40
kanalo-odwiedzin/min). Hopper (styl Uniden): dongle otwarty NA STALE przez
librtlsdr/ctypes (`rtl_iq_hopper.py`), kanaly pogrupowane w BANKI mieszczace
sie w jednym pasmie probkowania 2.4Msps (np. PSP+PKP R1-R7, rozpietosc
1.54MHz = JEDEN bank) - jeden retune + jeden odczyt IQ 32ms daje moc
WSZYSTKICH kanalow banku naraz (Welch PSD, moc sumowana +/-6kHz wokol
kanalu, srodek banku dobierany z dala od DC-spike'a). Obecne 15 kanalow = 5
bankow = caly sweep w ulamek sekundy (tysiace kanalo-pomiarow/min,
zmierzone na syntetyku; realny sweep ~0.3-0.5s przez settle PLL i USB).
Detekcja: tlo per kanal uczone na biezaco (mediana z 5 sweepow warmup,
potem EMA 0.2 gdy cicho / 0.02 gdy podniesione - staly birdie/przesluch
typu PMR CH4 jest powoli WCHLANIANY w tlo zamiast triggerowac w kolko);
trafienie = tlo + `--hop-margin-db` (6dB) przekroczone w 2 kolejnych
sweepach (`--hop-confirm`) -> hopper ZAMYKA dongla i odpala DOTYCHCZASOWA
sciezke rtl_fm (squelch/nagrywanie/linger/klasyfikacja/Slack, scan_channel
bez zmian), po nagraniu wraca do sweepowania. Falszywe trafienie (rtl_fm
nie potwierdzil) = cooldown 15s, po 3 z rzedu +2dB lokalnego marginesu (max
+10, reset po udanym nagraniu). Kalibracja squelchy rtl_fm robiona
PROAKTYWNIE w chwilach ciszy (bisekcja -l w trakcie transmisji znalazlaby
prog domykajacy transmisje, nie szum) - max jedna na `--hop-calib-interval`
(20s). Wazony round-robin jest w tym trybie zbedny (kazdy sweep odwiedza
wszystkie kanaly); priorytet rozstrzyga tylko kolejnosc przy kilku
trafieniach w jednym sweepie. Bramka air/OpenSky i hot-reload channels.csv
dzialaja jak w klasyce. Degradacja: brak librtlsdr / karta nie do otwarcia
/ 5 pustych sweepow z rzedu -> automatyczny fallback do trybu klasycznego
(zero ryzyka wdrozeniowego). `--no-hopper` = wymus klasyke. Refaktor przy
okazji: kalibracja wydzielona do `ensure_calibrated_nfm/rms()` (uzywana i
przez klasyke, i proaktywnie przez hopper).

### Adaptacyjny squelch+gain (`~/sdr-scanner/adaptive_calibrate.py`) - GOTOWE

Log kazdego przechwycenia (`~/sdr-scanner/capture_log.csv`, w tym SUROWE
metryki PCM sprzed ffmpeg: `raw_rms_db`, `raw_peak_db`, `raw_clip_pct`,
`raw_flatness`). Kluczowy dyskryminator szum-vs-sygnal: **plaskosc widmowa
(Wiener entropy, pasmo mowy 300-3000Hz) na SUROWYM PCM** - liczyc PRZED
ffmpeg (afftdn/loudnorm zacieraja te cechy). Zmierzone empirycznie na tym
sprzecie: szum AM ~0.95, szum NFM ~0.60 (parabola f² demodu FM), realna
mowa/muzyka ~0.35-0.5. Progi w kodzie: `NOISE_FLATNESS_AM=0.75`,
`NOISE_FLATNESS_NFM=0.55`.

Cron `~/sdr-scanner/adaptive_calibrate.py` co 10 min (zmienione z 1h na
prosbe uzytkownika, `*/10 * * * *`) koryguje `squelch_margin_db`/`gain_db`
w `channels.csv` (kolumna `squelch_margin_db` istnieje w CSV). Wymaga ≥3
zdarzen FP od ostatniej korekty (watermark per kanal) zanim podniesie
margines (NFM +1.5dB krok, max 12dB) - celowo ostrozne, zeby nie
przekorygowac na podstawie przypadku. Mozna odpalic recznie
(`python3 adaptive_calibrate.py`) do przyspieszenia bez zmiany progu
ostroznosci.

**WAZNE - UWAGA na roznice miedzy typami sygnalow:** ten sam dyskryminator
(flatness < prog = sygnal) dziala dobrze dla MOWY (satelity glosowe,
ground scanner), ale KATASTROFALNIE zle dla NOAA APT (subcarrier, nie mowa)
- zmierzone na 3 prawdziwych, potwierdzonych nagraniach NOAA: flatness
~0.997 (nieodroznialne od szumu ta metoda!). Gdyby wdrozone dla APT, KAZDY
prawdziwy obraz zostalby po cichu odrzucony. NIE UZYWAC flatness-audio dla
APT - zamiast tego `noaa-apt` (binarka) sama juz loguje "Found N sync
frames" (grep w jej stdout), to wlasciwy dyskryminator dla tego typu
sygnalu (patrz `noaa_capture.py`, `MIN_SYNC_FRAMES=10`).

### ADS-B lokalny (`~/sdr-scanner/adsb/adsb_scanner.py`) - ZBUDOWANE, praktycznie bezuzyteczne bez dedykowanej anteny

`readsb` (apt) jako silnik - `dump1090-fa` NIEDOSTEPNY na Debian trixie
arm64. Pakietowy `readsb` bez wbudowanej obslugi RTL-SDR (licencja) -
obejscie: `rtl_sdr` -> pipe surowych I/Q -> `readsb --device-type ifile
--ifile - --iformat UC8`. Jednorazowy skrypt (NIE daemon, nie w cronie),
wzbogaca ICAO24 przez OpenSky (tanio kredytowo), dedup 60min, Slack `air`.

**Zmierzony wynik**: 8 min nasluchu na obecnych antenach 2m/70cm = 11
zaakceptowanych wiadomosci z 18.6 mln preambul, 1 pozycja lotnicza. Praktycznie
bezuzyteczne bez dedykowanej anteny 1090MHz. `readsb.service` (auto-startujacy
sie z instalacji apt) swiadomie zatrzymany/wylaczony.

### Satelity (`~/sdr-scanner/satellites/`) - WYLACZONE w modules.yaml (na prosbe uzytkownika)

`predict_passes.py` - TLE z celestrak (per-satelita po NORAD ID, nie po
grupie - `GROUP=weather` juz nie zawiera NOAA-15/18/19). Trigger: powiadomienie
Slack TYLKO gdy AOS/LOS w oknie ±90s od "teraz". Interfejs
`get_upcoming_passes()` uzywany przez scheduler.

`noaa_capture.py` - na AOS NOAA z max elewacja >=30° (konfigurowalne),
nagrywa (plain FM BEZ deemfazy - APT to subcarrier AM na FM) -> `noaa-apt`
(binarka Rust z GitHub releases) dekoduje z korektami: `--contrast telemetry`
(fallback `98_percent`), `--rotate auto`, `--false-color --palette
noaa-apt-daylight.png` -> PNG na Slack **TYLKO gdy `sync_frames >=
MIN_SYNC_FRAMES(10)`** (patrz adaptive squelch sekcja - NIE flatness-audio
dla tego typu sygnalu).

`voice_sat_capture.py` - audio FM dla satelitow glosowych (SO-50, AO-91,
AO-123, CAS-3H, PO-101, RS95S, TEVEL2 x6, wszystkie NORAD ID zweryfikowane
bezposrednio przez celestrak). Nagrywa CALY przelot do pamieci, ale wysyla
na Slack **TYLKO gdy `any_signal_detected()`** wykryje flatness < 0.55 w
ktoryms 8s kawalku (male nanosatelity typu TEVEL2 czesto nie nadaja na
kazdym przelocie - bez tej bramki kanal zalewalo samym szumem).

### ISS (`~/iss/`)

`iss_aprs_capture.py` (145.825 MHz, digipeater ISS, osobno od naziemnego
APRS na 144.800) + `iss_sstv_capture.py` (145.800 MHz, dekoder Open-SSTV,
wysyla obraz TYLKO gdy realnie wykryje naglowek VIS - inaczej cicho konczy,
zero spamu, ten sam duch co pozniejsze bramkowanie satelitow glosowych/NOAA)
+ `iss_scheduler.py` (trigger AOS/LOS jak NOAA). Wpiete w scheduler jako
sat_tick_iss.

### GRAVES (`~/sdr-scanner/graves/graves_meteor_detector.py`) - WYLACZONE w modules.yaml

143.050 MHz, strojenie 1.5kHz PONIZEJ nosnej (143.0485 MHz, tryb USB) -
standardowa technika zeby uniknac "gluchego" punktu 0Hz w SSB. Detekcja: RMS
w oknach 100ms, adaptacyjny szum tla (EMA), trigger 2x200ms powyzej
noise_floor+9dB, hangover 600ms.

### FUNcube/AMSAT (`~/sdr-scanner/funcube/`)

AO-73/FUNcube-1 (145.935 MHz BPSK, zweryfikowany aktywny) + JO-97 (145.840,
telemetria niepewna). Dekoder: `gr-satellites`+`gnuradio`. Kanal Slack:
`funcube`. Wpiete w scheduler jako sat_tick_funcube.

### Radiosondy (`~/sdr-scanner/radiosonde/`) - WYLACZONE w modules.yaml

`radiosonde_auto_rx` (projecthorus, skompilowany ze zrodel, wlasny venv -
systemowy python3 NIE ma `semver`/`flask`, ~400-406MHz). **Upload do
SondeHub WYLACZONY** (`sondehub_enabled = False` w `station.cfg`) na wyrazna
prosbe uzytkownika - tylko lokalna detekcja + Slack (`sonde_to_slack.py`).

### rtl_433 (`~/rtl433/rtl433_to_slack.py`)

`-F json`, dedup per (model,id,kanal) 10 min. Wykrywa realne czujniki
sasiadow (Prologue-TH, inFactory-TH, Nexus-TH, Fineoffset-WH5). Wpiete w
rotacje schedulera na device 1.

### APRS + mapa (`~/aprs/`)

`aprs_to_slack.py` + `direwolf.conf`: `rtl_fm -M fm -s 22050` -> `direwolf`
(stdin, `ADEVICE null null`) -> `aprslib.parse` -> Slack tekstem z emoji wg
tabeli symboli APRS. Kanal Slack rozwiazywany przez `channels.json`
(NAPRAWIONY bug w tej sesji - wczesniej sztywno slal na kanal "main"
zamiast na wlasciwy klucz "aprs"). Znane ograniczenie: `aprslib` 0.7.2
nie wspiera wszystkich typow pakietow (np. "item report" `)`), logowane i
pomijane. Wpiete w rotacje schedulera na device 1.

Mapa: `map/index.html` (Leaflet+OSM, serwer na porcie 8080, LAN-only) +
`map/send_map_to_slack.py` (generuje PNG przez biblioteke `staticmap`,
wysyla na Slack). Trigger: co 10 nowych unikatowych stacji.

### Monitoring zdrowia dongli (`~/sdr-scanner/health_monitor.py`) - GOTOWE, ma known bug (patrz TODO #2)

`rtl_test -d 999` co 45s (presence check, nieinwazyjne, ~0.15s). Alert TYLKO
na zmiane stanu (trigger/recovery), nie co cykl. Kanal: `alerts`.
Teraz jako systemd `sdr-health-monitor.service` (wczesniej
nie mial ZADNEGO mechanizmu uruchamiania mimo statusu "gotowe").

Dodatkowo co 300s: wedge-check (`rtl_sdr -n 50000` per SN, ~0.2s, liczy
bajty - >=50% oczekiwanych = healthy). Rozroznia "busy" (szybki fail <1.2s,
inny proces trzyma urzadzenie) od prawdziwego "wedge" (0 bajtow, wisi do
timeoutu) - W GLOWNYM checku. Auto-naprawa przez `sudo usbreset "<Product>"`.
**Znany bug**: weryfikacja PO naprawie nie ma tego samego busy-vs-wedge
rozroznienia, wiec czasem falszywie zglasza "naprawa nieudana" (patrz
sekcja Sprzet #1) - NIE NAPRAWIONE jeszcze.

### Auto-czyszczenie (cron)

`0 4 * * * find ~/sdr-recordings -type f -mtime +7 -delete`

## Konfiguracja Slack (na Pi)

- Token + kanal glowny: `~/.config/slack-audio/env`
  (`SLACK_BOT_TOKEN`, `SLACK_CHANNEL`, `SLACK_CHANNEL_ID`)
- Mapa wszystkich kanalow: `~/.config/slack-audio/channels.json` (prywatny,
  poza tym repo - patrz `config/slack_channels.example.json` w repo za wzor)
- Bot: wlasny Slack app, scopes: `chat:write`, `files:write`
  (BRAK `channels:read` - ID kanalow rozwiazywane przez wyslanie testowej
  wiadomosci po nazwie i odczytanie `channel` z odpowiedzi)

### Mapa kanalow -> typ danych

Klucze ponizej to te same klucze co w `config/slack_channels.example.json` -
realne nazwy/ID kanalow Slack sa prywatne, poza tym repo.

| Klucz | Przeznaczenie |
|---|---|
| main | (stary, nieuzywany - alerty przeniesione do "alerts") |
| alerts | Alerty zdrowia dongli (trigger/recovery) |
| fm | Ogolny / straz pozarna |
| cb | CB Radio kanal 19 |
| noaa | Przeloty NOAA (obrazy APT) |
| aprs | Pakiety APRS + mapa pozycji |
| air | Skaner lotniczy (lokalne lotnisko) + ADS-B lokalny |
| sat | Satelity glosowe FM (SO-50, AO-91, +10 nowych) |
| iss | ISS (przeloty, digipeater, SSTV) |
| rtl433 | Czujniki ISM 433MHz |
| pmr | PMR446 |
| train | Radiolacznosc pociagowa PKP |
| repeaters | Lokalne przemienniki |
| 2m | 2m FM wywolawcza |
| 70cm | 70cm FM wywolawcza |
| radars | GRAVES - echa meteorow |
| funcube | Telemetria FUNcube/AMSAT |
| radiosonde | Radiosondy meteorologiczne |
| 10m, 40m, 20m, 80m | istnieja, NIEUZYWANE - usuniete z channels.csv na prosbe uzytkownika |

## Lista kanalow RF skanera naziemnego (STAN BIEZACY - okrojony na prosbe uzytkownika)

`~/sdr-scanner/channels.csv` (kolumny: name, freq_mhz, mode, category,
direct_sampling, priority_override, gain_db, squelch_margin_db) +
`category_priorities.csv` (category, priority 1-5, 1=waga5...5=waga1).

**Obecnie aktywne (16 kanalow, stan na koniec sesji 2026-08-11)** - w
trakcie sesji master CSV okazal sie podmieniony (14:29, bez PMR - przyczyna
NIEZNANA, zagadka); PMR przywrocone z backupu, dodany LPR 169.000, po czym
PMR ponownie WYLACZONE na prosbe uzytkownika (backup
`channels.csv.bak-przed-wylaczeniem-pmr-1550`). Watch mode zostal COFNIETY
pod koniec sesji - normalny podzial dwoch kart: dev0 = fm+train (PSP, PKP
R1-7, LPR), dev1 = air+cb (EPPO x6, CB); mechanizm watch dostepny na
przyszlosc w scheduler.py (patrz TODO #12). Kategorie repeaters/2m/70cm
wywolawcze nadal poza CSV, pelniejsze zestawy w backupach
`channels.csv.bak-*`:

- PMR446: CALA kategoria tymczasowo WYLACZONA (2026-08-11, prosba
  uzytkownika). Gdy wroci: CH1, CH3-CH8 (analogowe, 12.5kHz spacing,
  446.00625-446.09375MHz); **CH2 trwale wylaczony** (przesluch/
  samo-zaklocenie); **CH4 ma potwierdzony przesluch/birdie z drugiego
  dongla** (patrz sekcja Sprzet #4) - prawdopodobnie zawsze bedzie szumial
- LPR / Sluzby medyczne: 169.000 MHz NFM, priority_override=1, kategoria fm
  (dodany 2026-08-11 na prosbe uzytkownika; jakosc nagran potwierdzona
  "idealna"; followup_wait_s=15)
- PKP R1-R7: 150.100/150.150/150.225/150.325/150.375/150.450/150.575 MHz NFM
- Lotnisko (EPPO): 6 czestotliwosci ATC (tower/atis/ground/delivery/approach x2) AM -
  DZIALA DOBRZE (niska flatness, czyste przechwycenia)
- Straz pozarna (PSP Poznan): 149.0375 MHz NFM

**16 cyfrowych kanalow PMR446** (CH9-CH24, DMR, 446.103125-446.196875MHz,
6.25kHz spacing, dodane 2018 przez ETSI) byly krotko dodane i USUNIETE na
prosbe uzytkownika - skaner robi analogowy NFM demod, cyfrowa transmisja
brzmi jak niezrozumialy warkot, nie czytelna mowa.

## Wazne lekcje / pulapki

- **Nie zgaduj czestotliwosci/danych technicznych** - zle zgadniete FM dalo
  szum wzmocniony przez denoise/EQ, wygladajace na dzialajacy pipeline ale
  bezuzyteczne. WebSearch tez potrafi zmyslac (fake NORAD ID dla TEVEL2) -
  ZAWSZE weryfikuj krzyzowo w zrodle pierwotnym (celestrak, PKP, OurAirports,
  ETSI/Ofcom dla PMR446) zanim dodasz do configu. PMR446 digital extension
  (CH9-24) to dobrze udokumentowany publiczny standard, bezpiecznie dodany
  bez weryfikacji zewnetrznej - inaczej niz satelitarne NORAD ID.
- **NFM ma capture effect** - szum jest GLOSNIEJSZY bez sygnalu, cichszy z
  sygnalem (odwrotnie niz AM). Squelch dla NFM musi byc RSSI/pre-demod
  (`rtl_fm -l`), nie RMS post-demod.
- **Plaskosc widmowa (flatness) jako dyskryminator szum/sygnal DZIALA TYLKO
  dla mowy/muzyki, NIE dla wszystkich typow sygnalu** - katastrofalnie zle
  dla NOAA APT (subcarrier, nie mowa - realny sygnal ma flatness ~0.997,
  tyle co szum). ZAWSZE testuj nowy dyskryminator na prawdziwych,
  potwierdzonych przykladach obu klas PRZED wdrozeniem, nie zakladaj ze
  metoda dziala dla innego typu sygnalu tylko dlatego ze dziala dla jednego.
- **Diagnozowanie "kanal caly czas szumi" - trzyetapowy playbook**: (1)
  sprawdz `capture_log.csv` czy `duration_s` uderza w `max_recording` (limit
  czasowy, nie naturalne zamkniecie squelcha) - silny sygnal ze to squelch
  utyka otwarty, nie losowy szum; (2) `rtl_power` na WOLNEJ karcie w
  potwierdzonym momencie ze DRUGA karta jest/nie jest tam nastrojona -
  wyrazny pik = przesluch miedzy dongle'ami; (3) jesli spektrum plaskie,
  sprawdz sam kod kalibracji - degenerowana baza (np. `closing_level=1`,
  patrz nizej) moze uniemozliwic adaptacyjnej korekcie kiedykolwiek
  zadzialac, niezaleznie od liczby iteracji.
- **BUG (naprawiony): `operational_level = closing_level * 10**(margin/20)`
  bezsilne przy bardzo malej bazie** - gdy bisekcja squelcha znajdzie
  `closing_level=1` (kanal zamyka sie juz przy minimalnym testowanym `-l`),
  nawet margines 12dB (limit systemu) daje `round(1 * 10**0.6) = round(3.98)
  = 4` - dalece za malo (zdrowe kanaly potrzebuja `-l` 10-20+). Adaptacyjna
  korekta marginesu NIGDY nie mogla naprawic takich kanalow bez wzgledu na
  liczbe iteracji cronowych. Fix: dodano podloge `>= round(margin)`, nie
  zmienia zachowania dla kanalow ze zdrowym `closing_level` (tam mnoznik i
  tak dominuje).
- **BUG (naprawiony): `read_chunk()` w `ground_scanner.py` mial prawdziwy
  blocking `proc.stdout.read()`** sprawdzajac deadline TYLKO przed
  wywolaniem, nie w jego trakcie - gdy rtl_fm/USB "zawiesil sie" w trakcie
  strumienia (obserwowane po cyklu resetow USB), caly proces skanera czekal
  w nieskonczonosc mimo dawno minietego deadline'u. Zaobserwowany incydent:
  kanal zawieszony >2h, ZERO przechwycen przez cala sesje mimo ze reszta
  systemu dzialala normalnie. Fix: kazdy pojedynczy odczyt teraz przez
  `select()` z timeoutem (jak juz bezpieczny `select_read_for()` w tym samym
  pliku) - nie moze sie juz zawiesic bez wzgledu na przyczyne.
- Gain domyslnie **48dB reczny** (agresywny), NIE auto-AGC - zmiana z
  wczesniejszej zasady, bo skaner potrzebuje czulosci na slabe sygnaly.
- **Nie pisz nic o Poznaniu w dokumentacji** (README, SKILL.md) - explicite
  usuniete na prosbe uzytkownika. SondeHub upload tez WYLACZONY z tego powodu
  (publiczny callsign zdradzalby lokalizacje).
- Zwykle stacje radiowe FM (broadcast) NIE sa juz skanowane - byly tylko do
  testu pipeline'u.
- Satelity/NOAA/ISS: powiadomienia TYLKO w oknie AOS/LOS (±90s), NIE lista
  przyszlych przelotow z wyprzedzeniem.
- **usbreset dziala tylko gdy dongiel ma jakikolwiek uchwyt USB** - jesli
  calkowicie zniknal z enumeracji ("No such device"), potrzeba fizycznej
  interwencji (przepiecie) lub reset calego kontrolera (patrz Sprzet #3),
  nie samego dongla.
- **Testowanie zmian na zywym, dzielonym sprzecie**: kilka razy w tej sesji
  rownolegle dzialajace agenty/edycje kolidowaly na tym samym pliku
  (`ground_scanner.py` edytowany jednoczesnie przez agenta schedulera i
  agenta adaptive-squelch) - zawsze sprawdzaj `ps aux` na dany proces PRZED
  edycja/restartem, i uzywaj `.bak-<opis>` przy kazdej podmianie pliku na
  Pi zeby latwo cofnac.

