Ctrl K
ComunidadGuías…

rigs/civ_profiles.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""
civ_profiles.py — profile CI-V dla modeli ze wbudowanym scope (SCOPE_MODELS).

Kazdy profil zawiera parametry specyficzne dla danego modelu radia:
  - default_addr   : domyslny adres CI-V (hex int) — uzywany gdy uzytkownik
                      nie poda wlasnego w ustawieniach
  - default_baud   : domyslna predkosc portu szeregowego
  - mode_map       : mapowanie bajtu trybu (0x04/0x01) -> nazwa trybu
                      (wiekszosc Icomow uzywa tej samej tabeli, ale niektore
                      modele maja dodatkowe/inne tryby — np. D-STAR w IC-9100)
  - scope_max      : maksymalna wartosc amplitudy w danych scope (do
                      normalizacji 0-255 w UI)
  - scope_header_len: dlugosc naglowka pierwszej ramki scope (przed danymi)
  - notes          : uwagi/zrodlo dla osoby aktualizujacej profil

UWAGA: Wartosci dla modeli innych niz IC-7300 (referencyjny, przetestowany)
NIE byly zweryfikowane na sprzecie. Jezeli cos nie dziala dla danego modelu —
sprawdz oficjalna instrukcje "CI-V Reference Guide" dla tego radia
(dostepna na stronie icomeurope.com/icomamerica.com) i zaktualizuj profil.
"""

# Wspolna baza trybow — wiekszosc Icomow CI-V uzywa tego mapowania
_BASE_MODE_MAP = {
    0: "LSB", 1: "USB", 2: "AM", 3: "CW", 4: "RTTY",
    5: "FM", 6: "WFM", 7: "CW-R", 8: "RTTY-R", 17: "DV",
}

# Standardowy zestaw capabilities dla radia CI-V z bezposrednim sterowaniem.
# Kazdy klucz odpowiada jednej funkcji widocznej w panelu (przyciski pod waterfallem).
# Wartosc = True/False oznacza czy radio TECHNICZNIE wspiera te funkcje
# (czy CivRig potrafi ja wykonac). To NIE jest whitelist dla uzytkownikow —
# to jest zrodlo prawdy "co radio umie", a admin decyduje co WLACZYC w config.json.
_BASE_CAPS_CIV = {
    "freq_set":     True,   # zmiana czestotliwosci (0x05)
    "mode_set":     True,   # zmiana trybu pracy (0x06)
    "ptt":          True,   # PTT przez CI-V (0x1C 00)
    "split":        True,   # split VFO A/B (0x0F)
    "smeter":       True,   # odczyt S-metru (0x15 02)
    "scope":        True,   # waterfall/scope (0x27 00)
    "agc":          True,   # tryb AGC (16 12) — zaimplementowane, potwierdzone zywo 2026-10-18
    # REMOVED (2026-10-18): "rit" capability flag. RIT/ΔTX byl krotko
    # zaimplementowany w civ.py (CI-V 0x21), ale ustawianie Hz dawalo NG na
    # tym radiu nawet po poprawce kodowania BCD - przyczyny nie znaleziono,
    # user zdecydowal usunac funkcje calkowicie zamiast dalej szukac. Patrz
    # webapp.py's removed /api/rig/rit comment po pelna liste usunietych miejsc.
    "memory":       False,  # kanaly pamieci — niezaimplementowane
    "vfo_ab":       True,   # Select VFO A/B (0x07 00/01)
    "power":        True,   # Power ON/OFF radia (0x18 01/00)
    "dstar":        False,  # D-STAR (tylko niektore modele)
    # Dodane 2026-09-03: TUNER i AUTOTUNE faktycznie byly zaimplementowane
    # w civ.py (set_tuner/start_tuner_autotune) od dawna, ale nigdy nie
    # mialy wlasnego wpisu tutaj ani w rigs/features.py - ich widocznosc dla
    # zwyklego usera byla przypadkowo podpieta pod mode_set/ptt (patrz
    # webapp.py, elif t == "tuner"/"tuner_autotune"). Teraz wlasne klucze,
    # zeby admin mogl je wlaczac/wylaczac niezaleznie w KONFIGURACJA ->
    # FUNKCJE RADIA, tak jak split.
    "tuner":        True,   # Tuner ON/OFF (CI-V 1C 01 00/01)
    "autotune":     True,   # Autotune - cykl dopasowania, generuje TX (CI-V 1C 01 02)
}


# ── Set Level (CI-V cmd 0x14) — IC-7300 ──────────────────────────────────────
# Kazdy wpis: nazwa -> {sub: subcommand (0x14 XX), label, min/max (jednostki
# w UI), civ_max: max wartosc CI-V (0..255 lub 0..120 — zalezy od parametru)
# Wiekszosc poziomow IC-7300 uzywa zakresu CI-V 0000-0255 (BCD, 2 bajty).
# KEYSPD: 0000-0060 (6-48 WPM mapowane na 0-255 przez Hamlib, ale natywnie
#         IC-7300 uzywa wlasnej skali 0000-0060 dla 6-48 WPM).
_IC7300_LEVELS = {
    "AF":       {"sub": 0x01, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "AF"},
    "RF":       {"sub": 0x02, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "RF GAIN"},
    "SQL":      {"sub": 0x03, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "SQL"},
    "NR":       {"sub": 0x06, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "NR"},
    "PBT_IN":   {"sub": 0x07, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "PBT IN"},
    "PBT_OUT":  {"sub": 0x08, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "PBT OUT"},
    "CWPITCH":  {"sub": 0x09, "min": 300, "max": 900,  "civ_max": 255, "label": "CW PITCH"},
    "RFPOWER":  {"sub": 0x0A, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "RF PWR"},
    "MICGAIN":  {"sub": 0x0B, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "MIC GAIN"},
    "KEYSPD":   {"sub": 0x0C, "min": 6,   "max": 48,   "civ_max": 255, "label": "KEYSPD"},
    "NOTCHF":   {"sub": 0x0D, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "NOTCH"},
    "COMP":     {"sub": 0x0E, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "COMP"},
    # POPRAWKA: Break-IN Delay to CI-V 14 0F (00 00=2.0d .. 02 55=13.0d),
    # NIE 14 12 (to jest NB level, patrz NB_LEVEL ponizej). Wczesniejsza
    # wersja mapowala BKINDL na 0x12 — kolidowalo to z NB_LEVEL i bylo
    # niezgodne z dokumentacja CI-V (str.19-3).
    "BKINDL":   {"sub": 0x0F, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "BK-IN DELAY"},
    "NB_LEVEL": {"sub": 0x12, "min": 0,   "max": 1.0,  "civ_max": 255, "label": "NB LEVEL"},
    # USUNIETO: AGC_TIME (poprzednio blednie 14 37 — taki subcommand nie
    # istnieje w tabeli 0x14). Faktyczny AGC time constant to CI-V 1A 04
    # (00=OFF, AM:01-13=0.3-8.0s, SSB/CW/RTTY:01-13=0.1-6.0s) — INNA
    # struktura komendy (1A, nie 14), niezaimplementowana w set_level().
    # Wymaga osobnej obslugi w civ.py (set_agc_time) przed przywroceniem
    # tego slidera.
}

# ── Set Func (CI-V cmd 0x16) — IC-7300 ───────────────────────────────────────
# Kazdy wpis: nazwa -> subcommand (0x16 XX 00/01)
_IC7300_FUNCS = {
    "NB":     0x22,  # Noise Blanker
    "NR":     0x40,  # Noise Reduction
    "ANF":    0x41,  # Auto Notch Filter
    "COMP":   0x44,  # Compressor
    "VOX":    0x46,  # VOX
    "TONE":   0x42,  # Tone (repeater tone)
    "TSQL":   0x43,  # Tone Squelch
    # POPRAWKA: 0x47 to "BK-IN function" (00=OFF,01=Semi,02=Full) wg
    # dokumentacji CI-V — NIE "Twin PBT" jak bylo wczesniej oznaczone.
    # Twin PBT to regulacja ciagla (sliders PBT_IN/PBT_OUT, cmd 14 07/08),
    # nie prosty toggle 0x16. Przemianowano na BKIN zgodnie z faktyczna
    # funkcja CI-V.
    "BKIN":   0x47,  # BK-IN function (semi/full break-in)
    # POPRAWKA: 0x55 nie istnieje w tabeli 0x16 (Set Func). "Monitor
    # function" to faktycznie 0x45.
    "MON":    0x45,  # Monitor (sidetone TX)
}

# Etykiety dla funkcji (zgodne z hamlib_caps.FUNC_LABELS gdzie mozliwe).
# Format "SKROT (opis)": frontend (radiofunctions.js) tnie po '(' -> na
# przycisku widac tylko uniwersalny skrot (SKROT), pelny string (z polskim
# opisem) trafia do title (tooltip po najechaniu) — tak przyciski dzialaja
# identycznie w PL/EN bez osobnego tlumaczenia backendu.
_FUNC_LABELS = {
    "NB":   "NB (Noise Blanker)",
    "NR":   "NR (Redukcja szumow)",
    "ANF":  "ANF (Auto Notch Filter)",
    "COMP": "COMP (Kompresor)",
    "VOX":  "VOX",
    "TONE": "TONE (Tone CTCSS TX)",
    "TSQL": "TSQL (Tone Squelch)",
    "BKIN": "BKIN (Break-In)",
    "MON":  "MON (Monitor sidetone TX)",
}

# ── Advanced ("admin-only") radio settings — IC-7300 ────────────────────────
# Deeper CI-V commands beyond the generic Set Level (0x14)/Set Func (0x16)
# tables above - decided 2026-10-15 (user request) to gate these behind the
# admin role (webapp.py checks this on every endpoint that reads/writes one
# of these, not per-rig-whitelist like the generic levels/funcs above):
# modulation input source/level, AGC behavior, VOX fine-tuning, noise
# blanker depth/width, and TX sequencing delay (relevant for amplifier/
# antenna-relay keying, see the GSK fork's Sosny PA-controller work).
# Source: official Icom "CI-V REFERENCE GUIDE HF/50MHz TRANSCEIVER 7300MK2"
# (command table, cmd 0x1A sub 0x05 "SET > ..." menu-item area + cmds 0x14/
# 0x16) - verified against the PDF directly, not guessed.
#
# Each entry's "cmd" is the FULL byte sequence up to (not including) the
# data bytes - civ.py's get_adv/set_adv send this as-is for a read, or with
# the encoded value appended for a write, and validate a read reply's
# echoed address bytes (cmd[1:]) before trusting the data that follows.
#
#   kind "enum":      1 data byte, the raw value IS the UI value (0-based
#                      index into "labels"); "n" = how many valid values.
#   kind "range":      1 data byte, raw clamped to "rawmax" (default 255,
#                      set explicitly below for every entry whose real menu
#                      range is smaller - the radio's own menu limit, not
#                      just a UI nicety: an out-of-range raw byte is a
#                      command the radio would reject anyway), UI value =
#                      raw + "offset" (default 0), scaled by "scale"
#                      (default 1) if given.
#   kind "bcd2pct":    2-byte BCD 0000-0255, UI value = fraction 0.0-1.0.
#   kind "bcd2range":  2-byte BCD 0000-0255, UI value = "min".."max" (linear).
_IC7300_ADV = {
    # FIX (2026-10-18, live NG): the AGC time constant's DATA byte is
    # 1-indexed on the wire (01=FAST, 02=MID, 03=SLOW - 00 is not a valid
    # value for this specific sub-command), unlike most other enum-kind
    # commands which start at 00. "wire_base" shifts the UI's 0-based
    # dropdown index onto the radio's actual 1-based wire value - see
    # civ.py's _decode_adv/_encode_adv.
    # "feature": "agc" (2026-10-18, live request: "userzy nie widza agc...
    # trzeba dac to gdzies w konfiguracji") - opts this ONE entry out of the
    # blanket "settings"-only gate every other adv entry still uses. Gated
    # instead by the per-rig FEATURES whitelist (rigs/features.py's "agc"
    # entry, KONFIGURACJA -> FUNKCJE RADIA) - ONE switch turns it on for
    # every regular operator at once (no per-user granularity needed for a
    # small station - see webapp.py's /api/rig/adv handlers, which check
    # _feature_allowed("agc", role) for this key instead of _has_perm(...,
    # "settings")). An earlier per-user "perm" field approach was tried and
    # reverted the same day per user preference for the simpler whitelist.
    "AGC_MODE":     {"cmd": [0x16, 0x12],             "kind": "enum", "n": 3, "wire_base": 1,
                      "feature": "agc",
                      "labels": ["FAST", "MID", "SLOW"]},
    # TX Delay per band - relevant for amp/antenna-relay PTT sequencing.
    # 00~05 = OFF/10/15/20/25/30ms. Confirmed working live 2026-10-18.
    "TX_DELAY_HF":  {"cmd": [0x1A, 0x05, 0x00, 0x29], "kind": "enum", "n": 6,
                      "labels": ["OFF", "10ms", "15ms", "20ms", "25ms", "30ms"]},
    "TX_DELAY_50M": {"cmd": [0x1A, 0x05, 0x00, 0x30], "kind": "enum", "n": 6,
                      "labels": ["OFF", "10ms", "15ms", "20ms", "25ms", "30ms"]},
    "TX_DELAY_70M": {"cmd": [0x1A, 0x05, 0x00, 0x31], "kind": "enum", "n": 6,
                      "labels": ["OFF", "10ms", "15ms", "20ms", "25ms", "30ms"]},

    # REMOVED (2026-10-18, live NG on all of them): VOX_GAIN (14 16),
    # ANTI_VOX (14 17), VOX_DELAY (1A 05 02 67), NB_DEPTH (1A 05 02 65),
    # NB_WIDTH (1A 05 02 66), MOD_USB/ACC/LAN (1A 05 00 81/82/83),
    # DATA_OFF_MOD/DATA_MOD (1A 05 00 84/85) - part of the same
    # 2026-10-15 "approved" batch as the entries above, but unlike
    # AGC_MODE (fixed, see wire_base above) no concrete encoding bug was
    # found for these - every one of them was rejected (NG) live. Possible
    # causes: wrong addresses for this non-MK2 unit/firmware, sourced from
    # material that doesn't apply here. Don't re-add without a confirmed
    # (not guessed) address for this exact radio/firmware.
    #
    # Also REMOVED same day: TX_BASS_SSB/TX_TREBLE_SSB (1A 05 00 12/13),
    # TBW_ACTIVE (16 58), TBW_WIDE/MID/NAR (1A 05 00 14/15/16) - added during
    # a "czy HiFiSSB jest wykonalne programowo" investigation, sourced from
    # the IC-7300MK2 CI-V Reference Guide on the (wrong, in hindsight)
    # reasoning that since TBW/Tone Control predate MK2 as FRONT-PANEL
    # features, their CI-V REMOTE exposure must predate MK2 too. Live-tested
    # on the user's actual (non-MK2) IC-7300: ALL SIX gave "NG" (rejected) -
    # so this specific 1A 05 00 1x address range is MK2-only after all,
    # same failure class as the earlier APF mistake, just for a feature
    # that (unlike APF) genuinely isn't MK2-exclusive on the front panel -
    # only its CI-V remote control is. Don't re-add these addresses without
    # a real (non-MK2) IC-7300 CI-V reference confirming them - TBW is still
    # changeable manually on the radio's own FUNCTION button, just not
    # remotely through HAMCTRL.
}


# ── Pasma per model radia ────────────────────────────────────────────────────
# Kazde radio obejmuje inny zakres pasm. IC-7300 to HF + 6m (bez VHF/UHF),
# IC-9700 to czysto VHF/UHF/SHF (bez HF). Zeby UI pokazywalo tylko pasma,
# ktore dane radio FAKTYCZNIE ma, wiazemy liste pasm z profilem modelu.
#
# Format: nazwa -> (min_hz, max_hz, default_hz). Transwertery (13cm/23cm na
# radiach bez natywnego SHF) obsluguje sie osobno przez offset — patrz uwaga
# przy pasmach SHF.
_BANDS_HF = {
    "160m": (1810000,   2000000,   1850000),
    "80m":  (3500000,   3800000,   3650000),
    "60m":  (5351500,   5366500,   5357000),
    "40m":  (7000000,   7200000,   7100000),
    "30m":  (10100000,  10150000,  10125000),
    "20m":  (14000000,  14350000,  14200000),
    "17m":  (18068000,  18168000,  18100000),
    "15m":  (21000000,  21450000,  21200000),
    "12m":  (24890000,  24990000,  24930000),
    "10m":  (28000000,  29700000,  28400000),
}
_BAND_6M   = {"6m":   (50000000,   52000000,   50150000)}
_BAND_4M   = {"4m":   (70000000,   70500000,   70150000)}
_BAND_2M   = {"2m":   (144000000,  146000000,  144300000)}
_BAND_70CM = {"70cm": (430000000,  440000000,  432100000)}
_BAND_23CM = {"23cm": (1240000000, 1300000000, 1296200000)}
# 13cm: na Icomach zwykle przez transwerter (radio pokazuje IF, np. 144 lub
# 432 MHz, a nadaje na 2320/2400). Podajemy pasmo docelowe; obsluga offsetu
# transwertera to osobne ustawienie admina (transverter_offset w config).
_BAND_13CM = {"13cm": (2320000000, 2450000000, 2320200000)}

# Zlozone zestawy pasm per typ radia
_BANDS_HF_6M       = {**_BANDS_HF, **_BAND_6M}
_BANDS_HF_6M_4M    = {**_BANDS_HF, **_BAND_6M, **_BAND_4M}
_BANDS_HF_6M_2M    = {**_BANDS_HF, **_BAND_6M, **_BAND_2M}   # IC-746: HF+50+144MHz, BEZ 70cm
_BANDS_HF_VU       = {**_BANDS_HF, **_BAND_6M, **_BAND_2M, **_BAND_70CM}
_BANDS_HF_VU_23    = {**_BANDS_HF, **_BAND_6M, **_BAND_2M, **_BAND_70CM, **_BAND_23CM}
_BANDS_VUSHF       = {**_BAND_2M, **_BAND_70CM, **_BAND_23CM}   # IC-9700


CIV_PROFILES = {

    # ── IC-7300 — profil REFERENCYJNY, przetestowany ────────────────────────
    "3073": {
        "name":             "IC-7300",
        "default_addr":     0x94,
        "default_baud":     115200,
        "mode_map":         dict(_BASE_MODE_MAP),
        "bands":            dict(_BANDS_HF_6M_4M),   # HF + 6m + 4m (70 MHz)
        "scope_max":        160,   # 0x00..0xA0
        "scope_header_len": 15,
        "capabilities":     dict(_BASE_CAPS_CIV),
        "levels":           dict(_IC7300_LEVELS),
        "funcs":            dict(_IC7300_FUNCS),
        "func_labels":      dict(_FUNC_LABELS),
        # Admin-only advanced settings (1A 05 menu tree + 14/16 extras) -
        # NOT copied to IC-746 below: that's a much older radio generation
        # (no scope, no DATA/USB-D mode per its own profile notes) and this
        # modern per-menu-item CI-V addressing almost certainly doesn't
        # exist on it at all.
        "adv":              dict(_IC7300_ADV),
        "notes":            "Profil referencyjny — przetestowany na sprzecie.",
    },

    # ── IC-746 — stary CI-V (BEZ scope, BEZ trybu DATA/USB-D) ────────────────
    # Dodane 2026-09-02, zeby ominac rigctld/Hamlib (padal w trakcie sesji,
    # patrz _respawn_rigctld w rigcat.py) - IC-746 jest CI-V, wiec civ.py
    # moze mowic z nim bezposrednio, TRZEBA jednak innej ramki set_mode.
    # Zrodlo: oficjalny CI-V reference (adres domyslny 0x56, 19200bd, pasma
    # HF+50+144MHz bez 70cm - universal-radio.com/qsl.net specs) +
    # Hamlib rigs/icom/icom.c (RIG_IS_IC746 jest jawnie na liscie radii ze
    # "icmode_ext = -1" w icom_set_mode_without_data() - komenda 06 <mode>
    # BEZ trzeciego bajtu filtra, w przeciwienstwie do IC-7300's
    # 06 <mode> <fil>). Poziomy (0x14 xx) i funkcje (0x16 xx) to ten sam,
    # stary, uniwersalny zestaw Icoma co IC-7300 (potwierdzone w
    # icom_defs.h) - stad "levels"/"funcs" ponizej to bezposrednia kopia
    # IC-7300, NIE zgadywanie.
    "3023": {
        "name":             "IC-746",
        "default_addr":     0x56,
        "default_baud":     19200,
        "mode_map":         dict(_BASE_MODE_MAP),
        "bands":            dict(_BANDS_HF_6M_2M),   # HF + 6m + 2m, BEZ 70cm
        "scope_max":        160,   # nieuzywane (capabilities.scope=False), placeholder
        "scope_header_len": 15,    # jw.
        # power=False: Hamlib's own IC-746 backend (rigs/icom/ic746.c) never
        # assigns a set_powerstat function at all - CI-V remote power-ON
        # needs the radio's standby circuit to listen on the bus while
        # "off", a feature Icom only added starting with later designs.
        # Leaving power=True here would show a PWR ON/OFF button that's
        # unverified and likely does nothing on this radio.
        "capabilities":     {**_BASE_CAPS_CIV, "scope": False, "power": False},
        "levels":           dict(_IC7300_LEVELS),
        "funcs":            dict(_IC7300_FUNCS),
        "func_labels":      dict(_FUNC_LABELS),
        # Radio-specific quirks czytane przez civ.py::set_mode() - patrz
        # komentarz tam. Domyslnie (brak klucza) = True, jak IC-7300.
        "mode_has_filter_byte": False,   # 06 <mode> TYLKO, bez bajtu filtra
        "data_mode_supported":  False,   # brak 1A 06 (USB-D/DATA) - starsze radio
        # has_vfo_b_read=False: CI-V 25/26 (odczyt VFO-B/nieaktywnego VFO)
        # to komenda NOWEJ generacji - Hamlib sam to wykrywa i loguje przy
        # starcie: "icom_set_x25x26_ability: Hamlib thinks rig does not
        # support x25/x26 command" (potwierdzone live na tym IC-746, log
        # 2026-09-02). Bez tej flagi self.freq_b zostaje na starej/domyslnej
        # wartosci NA STALE (0x25 zawsze NG/timeout) - realna luka
        # bezpieczenstwa: zabezpieczenie cross-band-split (webapp.py
        # _is_split_cross_band) porownywalo pasmo na podstawie tej
        # zamrozonej, falszywej wartosci i nigdy nie wykrywalo prawdziwego
        # cross-band. Flaga wylacza zarowno zbedny poll 0x25 (radio i tak
        # go odrzuci) jak i każe cross-band-checkowi failować BEZPIECZNIE
        # (blokować TX na SPLIT) zamiast ufać nieznanej wartosci.
        "has_vfo_b_read":       False,
        "notes":            ("NIEZWERYFIKOWANE na sprzecie (dopiero po tym "
                              "profilu) - ale komendy sprawdzone zrodlowo w "
                              "Hamlibie, nie zgadywane. Jesli mode/levels nie "
                              "dzialaja: sprawdz najpierw czy radio faktycznie "
                              "ma adres CI-V 0x56 (MENU > SET > CI-V), "
                              "19200bd."),
    },

    # ── IC-7610 ──────────────────────────────────────────────────────────────
    "3078": {
        "name":             "IC-7610",
        "default_addr":     0x98,
        "default_baud":     115200,
        "mode_map":         dict(_BASE_MODE_MAP),
        "bands":            dict(_BANDS_HF_6M_4M),   # HF + 6m + 4m
        "scope_max":        160,
        "scope_header_len": 15,
        "capabilities":     dict(_BASE_CAPS_CIV),
        "notes":            ("NIEZWERYFIKOWANE na sprzecie. IC-7610 ma dual-RX "
                              "(Main/Sub) — naglowek scope zawiera dodatkowy bajt "
                              "wyboru scope'u (Main=0x00/Sub=0x01) na pozycji "
                              "zaraz po seq/total. Jezeli scope nie dziala "
                              "poprawnie, sprawdz CI-V Reference Guide IC-7610 "
                              "sekcja 'Scope waveform data' (cmd 27 00)."),
    },

    # ── IC-705 ───────────────────────────────────────────────────────────────
    "3085": {
        "name":             "IC-705",
        "default_addr":     0xA4,
        "default_baud":     115200,
        "mode_map":         dict(_BASE_MODE_MAP),
        "bands":            dict(_BANDS_HF_VU),   # HF, 6m, 2m, 70cm
        "scope_max":        160,
        "scope_header_len": 15,
        "capabilities":     dict(_BASE_CAPS_CIV),
        "notes":            ("NIEZWERYFIKOWANE na sprzecie. IC-705 czesto "
                              "laczy sie przez Bluetooth/WLAN zamiast USB-serial "
                              "— jesli uzywasz USB, sprawdz baudrate w "
                              "MENU > SET > Connectors > CI-V (domyslnie 115200 "
                              "dla USB)."),
    },

    # ── IC-9100 ──────────────────────────────────────────────────────────────
    "3068": {
        "name":             "IC-9100",
        "default_addr":     0x7C,
        "default_baud":     115200,
        # IC-9100 dodaje tryb DV (D-STAR) — juz w _BASE_MODE_MAP (17)
        "mode_map":         dict(_BASE_MODE_MAP),
        "bands":            dict(_BANDS_HF_VU_23),   # HF, 6m, 2m, 70cm, 23cm (z opcja UX-9100)
        "scope_max":        160,
        "scope_header_len": 15,
        "capabilities":     {**_BASE_CAPS_CIV, "dstar": True},
        "notes":            ("NIEZWERYFIKOWANE na sprzecie. IC-9100 to radio "
                              "VHF/UHF/HF z dual-band — moze wymagac dodatkowego "
                              "parametru wyboru pasma (Main/Sub = HF czy V/UHF) "
                              "przed komendami scope. Sprawdz CI-V Reference "
                              "Guide IC-9100, sekcja 'Scope waveform data'."),
    },

    # ── IC-7100 ──────────────────────────────────────────────────────────────
    "3070": {
        "name":             "IC-7100",
        "default_addr":     0x88,
        "default_baud":     115200,
        "mode_map":         dict(_BASE_MODE_MAP),
        "bands":            dict(_BANDS_HF_VU),   # HF, 6m, 2m, 70cm
        "scope_max":        160,
        "scope_header_len": 15,
        "capabilities":     {**_BASE_CAPS_CIV, "dstar": True},
        "notes":            ("NIEZWERYFIKOWANE na sprzecie. IC-7100 ma scope "
                              "tylko w trybie 'Center' (nie Fixed) wedlug "
                              "niektorych zrodel — jesli scope nie wysyla "
                              "danych, sprawdz ustawienie SCOPE > SPAN/MODE "
                              "na radiu."),
    },

    # ── IC-9700 — czysto VHF/UHF/SHF (2m/70cm/23cm, BEZ HF) ─────────────────
    "3081": {
        "name":             "IC-9700",
        "default_addr":     0xA2,
        "default_baud":     115200,
        "mode_map":         dict(_BASE_MODE_MAP),   # ma DV (D-STAR), juz w bazie
        "bands":            dict(_BANDS_VUSHF),      # 2m, 70cm, 23cm — bez HF
        "scope_max":        160,
        "scope_header_len": 15,
        "capabilities":     {**_BASE_CAPS_CIV, "dstar": True},
        "notes":            ("NIEZWERYFIKOWANE na sprzecie. IC-9700 to radio "
                              "czysto VHF/UHF/SHF (2m/70cm/23cm) — NIE ma HF. "
                              "Ma satelitarny dual-watch (Main/Sub) — scope moze "
                              "wymagac wyboru pasma przed komenda 27 00, podobnie "
                              "jak IC-9100. 23cm (1296) natywne, bez transwertera. "
                              "Adres CI-V domyslny 0xA2. Sprawdz CI-V Reference "
                              "Guide IC-9700 sekcja 'Scope waveform data' jesli "
                              "scope nie dziala."),
    },
}


# Profil domyslny — uzywany gdy model nie jest w CIV_PROFILES
# (nie powinno sie zdarzyc jesli SCOPE_MODELS i CIV_PROFILES sa zsynchronizowane,
#  ale zabezpiecza przed KeyError przy literowce/nowym modelu)
DEFAULT_PROFILE = {
    "name":             "Nieznany model CI-V",
    "default_addr":     0x94,
    "default_baud":     115200,
    "mode_map":         dict(_BASE_MODE_MAP),
    "bands":            dict(_BANDS_HF_6M_4M),   # fallback: zakres jak IC-7300
    "scope_max":        160,
    "scope_header_len": 15,
    "capabilities":     dict(_BASE_CAPS_CIV),
    "levels":           {},
    "funcs":            {},
    "func_labels":      {},
    "notes":            "Profil domyslny (fallback) — model nie ma dedykowanego profilu.",
}


def get_civ_profile(model_id: str) -> dict:
    """
    Zwroc profil CI-V dla danego model_id (np. '3073').
    Jesli model nie ma profilu, zwroc DEFAULT_PROFILE i wypisz ostrzezenie.
    Brakujace klucze 'levels'/'funcs'/'func_labels' sa dopelniane pustymi
    dict — pozwala to civ.py bezpiecznie wolac .get('levels', {}) bez
    sprawdzania kazdego profilu osobno.
    """
    model_id = str(model_id)
    profile = CIV_PROFILES.get(model_id)
    if profile is None:
        print(f"[civ_profiles] UWAGA: brak profilu dla model={model_id}, "
              f"uzywam DEFAULT_PROFILE")
        return DEFAULT_PROFILE
    # Dopelnij brakujace klucze (modele bez levels/funcs/bands zdefiniowanych jawnie)
    for k in ("levels", "funcs", "func_labels"):
        profile.setdefault(k, {})
    # bands: gdyby jakis profil go nie mial, daj zakres jak IC-7300 (bezpieczny
    # fallback HF+6m+4m) zamiast pustej listy, ktora ukrylaby wszystkie pasma
    profile.setdefault("bands", dict(_BANDS_HF_6M_4M))
    return profile