Old-School Reliability: How an Old Yaesu FT-90R saved my peace of mind (and why I am stripping myself of my chinese rigs)

It was supposed to be a my first Parks on the Air (POTA) activation. I struggled to find a parking slot inside the Park because streets sides where very narrow, until I found one barred from sea view by a small debris hill. I had my gear set up, my portable antenna ready. Full of ‘some animal’ shit. I packed two rigs: my hamgeek USDX + DIY Magloop and one chinese dual-band VHF/UHF mobile transceiver with car antenna. The VHF propagation was good and this park (unless you find a parking spot without sea view like me) is high on Tirrenian Sea. It was hot and sweaty – but I had a two galoons of water and a lot of faith.

Then, right in the middle of setup , catastrophe struck—or rather, cheap electronics struck.

My cheap dual-band Chinese clone completely went haywire. First, the PTT button locked up on transmit, putting me into an accidental continuous carrier loop. Moments later, after a frantic power cycle, the radio wiped its own memory channels entirely. Just like that, sitting in a hot car in the middle of a roadside, my primary car transceiver turned into a useless, unprogrammed brick (that just went to TX on poweron).
The POTA spot on 20m didn’t end well, too. My magloop isn’t rugged enough to stand a breeze of wind and went out of resonance frequently. The USDX managed better, I did a german FT8 QSO, but 5W on POTA is not enough. Frustration.

That was the exact moment I decided: never again. Just the day before an ham fellow did a POTA with tens of contacts all around the tirrenian, 5W, a similar park and a Japanese Radio. I was with 25W and with my mic in the hands!! Right now I am selling my car chinese VHF/UHF transceivers and I am buying japanese. That is all. Can’t they break? Yes. They are old and dizzy. Are they solid? Yes.


Returning to the Classics: The Yaesu FT-60R

So I put aside my QRP ethics, shelled out some bucks and bought some japanese classics from the FT series. 5W-50W-100W. Not saying more – I am not one to spend money easily, that’s why I loved cheap SDR mobile transceivers but imagine my frustration. One of the three rig I pulled out from the shop in the cloud is an old Yaesu FT-90R. This isn’t a modern Software Defined Radio (SDR) with a bright color waterfall display, built-in GPS, or digital voice modes. It is a classic, rugged, dual-band superheterodyne transceiver. Built like a brick, heavy in the hand, and utterly dependable.

The auctions said ‘perfect‘. However, mine showed a bit of its age. Restoring it wasn’t just a basic maintenance job—it was a little restoration project:

  • The Car mount: The metal mounting clip was re-painted hastly. It requires a complete strip-down, chemical paint stripping, sanding, and a fresh coat of protective enamel.
  • The Microphone Cable: The original hand mic cable was frayed and exposed near the connector strain relief, with tape and heat shrink tubing in place. Now, I don’t like to see an hastily made paint job and a shrink tube…if I didn’t place it myself. Watcha hiding?

Bringing 20-year-old hardware back to life: stripping down mounts and patching original mic cables. We have a very different definition of ‘perfect’, my friend.


The mic isn’t available anymore but…

The main body of the rig seems fine. The mic works, but its cable – ugly. Yaesu retailer says mic (I understand is MH-36 A6J) is not available anymore. I cry. But spare cable is…around. I buy and I connect and then…I remember the horror stories of fried FT891 heads by reversed cables and I stop. I check. I dutifully check and – te cable is pinout indeed reversed. I hate everyone -I hate everyone and myself. check the auction – says MH-36 A6J too. Everytime I get into fixing something – frustration. At least didn’t fry anything. Horror stories help fellow hams – disclose your error and help someone else avoiding getting down that road.

Recognize the connector (this is the original cable) it is not hard to swap the wires and the new cable

So by lifting those flimsy plastic tab and sweating and suffering I managed to swap the wires accordingly. To avoid meltdowns and expensive mishaps, you need to document well beforehand. Find a clear image of the rj12 connector.

FT90R mic connection from flqelettronica.it Understand well the wiring before connecting! Image says RJ11m but is it is 6 contacts, RJ12!

I used this criteria: I placed the both connector, the internal one and the RJ12 choosing a side. I checked wiring on the original cable with a multimeter and two small wires on the probes to be sure to make contact with the tiny contacts of RJ12 and internal connecton. I traced the wires position. Then I placed the new cable connector in the same position I used for the old cable and rewired the internal connector using a small blade to lift the flimsy plastic tabs that keeps the metal connectors in place, and swapped them. Basically in my case I had to invert every wire with its twin (like 1-6, 2-5, 3-4). I double checked the wiring on the new cable, tested, and voilà…new cable. No shipping back, no refunds, no waiting.

Much better look, same functionality. Loving it (source: napalmpiri)

I will strip the mouting bracket as soon as I am outside with free access to open air.
The FT90R looks fine now…but it is empty. No memories. I test it with a fellow ham … no issues on VHF and UHF and the modulation is clear. Now I just need to program it.

The programming cable

I have too many programming cables and I spent too much life programming stuff by hand. But I also care for this FT90R. No reason to fry it. I decided that the other mic cable will be a programming interface. I did some regrettable things in the past with programming cables and chinese radios…not on this one. DIY, but tidy.

No dangling wires this time. The other cable (also new, and rewired) was not harmed by the judicious use of tiny copper wires.
Hope I got them right. In my setup, you can see the pin 2 and 4 of internal connector are involved…I won’t tell you which one, it is too dangerous (spewing FUD) check and double check yourself!

Classic FTDI is set on 5V. Diode between TX and RX (check direction), 10k pull-up resistor, easy money. The diode shouldn’t be critical here, I used the first rectifier that came handy.

More information see also: https://www.vr2xkp.org/2018/03/04/a-simple-usb-programming-cable-for-yaesu-ft-90r and https://kc1sle.com/yaesu-ft-90-programming-cable-diy-instructions-make-it-yourself/.
Always double check everything – in these difficult and exciting times of images made with AI by lazy people like me it is a possible to have a reversed diode in a schematic or worse. The first iteration of the one above, made by a lazy guy with the help of a lazy AI, had the diode reversed. I caught the error – but maybe there ‘s more.

Decoding the Legacy Interface: Python & CHIRP to the Rescue

While the FT-90R’s analog hardware is sturdy, unlike modern radios, the FT-90R relies on an older serial transfer protocol routed through its RJ12 mic jack.. When attempting to upload a fresh multi-band repeater layout via standard CHIRP, I ran into a roadblock: standard CSV exports from modern repeater databases (like other CHIRP .csv/.img templates I got) contain fields that the FT-90R’s formats differently, resulting in error messages, and many entries being refused because of ‘reasons’ (‘wrong tuning step’,’HI power not supported’, ‘humidity rate wrong’…)

To bridge this gap without manually editing two hundreds channels through the FT-90R keypad or CHIRP, I wrote a custom Python script to pre-process and sanitize the famous IK2ANE repeater list data before feeding it to CHIRP. The script expects a CSV, not IK2ANE xlsx. I converted with Libreoffice. If invoked without arguments search for file input.csv (IK2ANE converted XLSX) and produces out_chirp_ft90r. Some frequencies are not accepted, I have to see why. It worked, somehow, so I fixed the rest by hand.
The script, (which is vibe-coded in part, so don’t blame me if it sets your world on fire) does several tasks:

  • Field Normalization: It parses modern repeater CSV export headers and maps them specifically to the legacy FT-90R CHIRP format (e.g., standardizing Tone, Power level), checks for valid tone. I do not know of any active repeaters without CTCSS tone in my area, so I filtered out the ones without tone in the list, 60% chance they are leftovers. So this script would filter out an entry with no tone, even if it is active.
    If tone in ‘tone’ is something other than CTCSS frequencies (e.g. CC2, or whatever) the script filters out the entry, too.
  • Memory optimization: FT90R has only 180 memories – you have to choose. I am accustomed in having 500 memories (I used at most 30) but again, who needs 500 memories unless they travel a lot? I added a control to add only entries from zones (Italian ham zones) I reach/ visit. The script matches the County to a zone (so if an entries on Ik2ane says AO- Aosta, it matches it with zone 1) and adds the line. It skips it otherwise. Comment out ‘if not is_in_zone_of_interest(zone_str): continue’ to skip this check and process the whole list.
#Copyright 2026 napalmpiri.info
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and #associated documentation files (the “Software”), to deal in the Software without restriction, including #without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to #the following conditions:
#The above copyright notice and this permission notice shall be included in all copies or substantial #portions of the Software.
#
#THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, #INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A #PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR #COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN #ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH #THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import csv
import re

# Elenco dei 50 toni CTCSS standard (in Hz)
CTCSS_TONES = {
    67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5,
    94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123.0, 127.3,
    131.8, 136.5, 141.3, 146.2, 151.4, 156.7, 159.8, 162.2, 165.5, 167.9,
    171.3, 173.8, 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5,
    203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, 250.3, 254.1
}

# Zone di interesse. Cambiare con le proprie.
ZONES_OF_INTEREST ={1,2,3}

# Lookup Table: Sigla Provincia (2 lettere) -> Zona Callsign Radioamatoriale (0-9). Da verificare.
PROV_TO_ZONE = {
    # Zona 1: Piemonte, Liguria, Valle d'Aosta
    'AL': 1, 'AT': 1, 'BI': 1, 'CN': 1, 'NO': 1, 'TO': 1, 'VB': 1, 'VC': 1,
    'GE': 1, 'IM': 1, 'SP': 1, 'SV': 1, 'AO': 1,
    # Zona 2: Lombardia
    'BG': 2, 'BS': 2, 'CO': 2, 'CR': 2, 'LC': 2, 'LO': 2, 'MN': 2, 'MI': 2, 'MB': 2, 'PV': 2, 'SO': 2, 'VA': 2,
    # Zona 3: Veneto, Trentino-Alto Adige, Friuli-Venezia Giulia
    'BL': 3, 'PD': 3, 'RO': 3, 'TV': 3, 'VE': 3, 'VR': 3, 'VI': 3,
    'BZ': 3, 'TN': 3, 'GO': 3, 'PN': 3, 'TS': 3, 'UD': 3,
    # Zona 4: Emilia-Romagna
    'BO': 4, 'FE': 4, 'FC': 4, 'MO': 4, 'PR': 4, 'PC': 4, 'RA': 4, 'RE': 4, 'RN': 4,
    # Zona 5: Toscana
    'AR': 5, 'FI': 5, 'GR': 5, 'LI': 5, 'LU': 5, 'MS': 5, 'PI': 5, 'PT': 5, 'PO': 5, 'SI': 5,
    # Zona 6: Marche, Abruzzo
    'AN': 6, 'AP': 6, 'FM': 6, 'MC': 6, 'PU': 6, 'AQ': 6, 'CH': 6, 'PE': 6, 'TE': 6,
    # Zona 7: Puglia, Basilicata
    'BA': 7, 'BT': 7, 'BR': 7, 'FG': 7, 'LE': 7, 'TA': 7, 'MT': 7, 'PZ': 7,
    # Zona 8: Campania, Calabria
    'AV': 8, 'BN': 8, 'CE': 8, 'NA': 8, 'SA': 8, 'CZ': 8, 'CS': 8, 'KR': 8, 'VV': 8, 'RC': 8,
    # Zona 9: Sicilia
    'AG': 9, 'CL': 9, 'CT': 9, 'EN': 9, 'ME': 9, 'PA': 9, 'RG': 9, 'SR': 9, 'TP': 9,
    # Zona 0: Lazio, Umbria, Sardegna
    'FR': 0, 'LT': 0, 'RI': 0, 'RM': 0, 'VT': 0, 'PG': 0, 'TR': 0,
    'CA': 0, 'SU': 0, 'NU': 0, 'OR': 0, 'SS': 0
}

# Mappa ausiliaria per convertire eventuali nomi di provincia per esteso in sigle a 2 lettere
NOMI_A_SIGLA = {
    'PALERMO': 'PA', 'ROMA': 'RM', 'MILANO': 'MI', 'TORINO': 'TO', 'NAPOLI': 'NA',
    'GENOVA': 'GE', 'BOLOGNA': 'BO', 'FIRENZE': 'FI', 'CATANIA': 'CT', 'CAGLIARI': 'CA'
}

# def parse_float(val_str):
#     """Estrae un valore numerico float da una stringa."""
#     if not val_str:
#         return None
#     match = re.search(r"(\d+(?:\.\d+)?)", val_str)
#     return float(match.group(1)) if match else None

# --- FIX BUG 1: Parsing Float con gestione della virgola ---
def parse_float(val_str):
    """Estrae un valore numerico float da una stringa gestendo sia punto che virgola."""
    if not val_str:
        return None
    val_clean = str(val_str).replace(',', '.')
    match = re.search(r"(\d+(?:\.\d+)?)", val_clean)
    return float(match.group(1)) if match else None

def is_valid_tone(tone_str):
    """Verifica se il tono è presente e valido come CTCSS standard."""
    val = parse_float(tone_str)
    return val in CTCSS_TONES if val is not None else False

def is_valid_frequency(freq_str):
    """Verifica se la frequenza ricade nella banda VHF (136-174) o UHF (400-480)."""
    freq = parse_float(freq_str)
    if freq is None:
        return False
    return (136.0 <= freq <= 174.0) or (400.0 <= freq <= 480.0)

# def parse_shift_direction(shift_str):
#     """Mappa la direzione dello shift/duplex per CHIRP (+, -, o vuoto)."""
#     if not shift_str:
#         return ""
#     s = shift_str.strip().lower()
#     if '+' in s or 'pos' in s:
#         return "+"
#     elif '-' in s or 'neg' in s:
#         return "-"
#     return ""

# --- FIX BUG 2: Direzione Shift / Duplex ---
def parse_shift_direction(shift_str):
    """Mappa la direzione dello shift per CHIRP (+, -, o vuoto/off)."""
    if not shift_str:
        return ""
    s = shift_str.strip().lower()
    if '+' in s or 'pos' in s:
        return "+"
    elif '-' in s or 'neg' in s:
        return "-"
    return ""

def parse_shift(shift_str: str) -> float:
    """
    Parsa una stringa rappresentante uno shift/offset e restituisce 
    il valore assoluto in MHz (float) per il campo Offset di CHIRP.
    
    Esempi:
    - "-600 kHz" -> 0.6
    - "+1.6 MHz" -> 1.6
    - "+5.0 MHz" -> 5.0
    - "diretto" / "0" -> 0.0
    """
    if not shift_str or not isinstance(shift_str, str):
        return 0.0

    # Lookup table per termini o formati frequenti (valore assoluto in MHz)
    lookup_table = {
        "0": 0.0,
        "diretto": 0.0,
        "simplex": 0.0,
        "nessuno": 0.0,
        "none": 0.0,
        "standard vhf": 0.6,
        "-600": 0.6,
        "+600": 0.6,
        "standard uhf": 1.6,
        "+1.6": 1.6,
        "+5.0": 5.0,
        "+5": 5.0,
    }

    # Pulizia preliminare: minuscolo, rimozione spazi esterni, gestione virgola
    valore_pulito = shift_str.strip().lower().replace(',', '.')

    # 1. Controllo diretto nella Lookup Table
    if valore_pulito in lookup_table:
        return lookup_table[valore_pulito]

    # 2. Parsing con Regex per numeri ed eventuale unità di misura (kHz/MHz)
    match = re.search(r'([+-]?\d+(?:\.\d+)?)\s*(khz|mhz)?', valore_pulito)
    if match:
        valore_num = abs(float(match.group(1)))  # CHIRP richiede offset sempre positivi
        unita = match.group(2)

        if unita == 'khz':
            return valore_num / 1000.0
        elif unita == 'mhz':
            return valore_num
        else:
            # Se l'unità è omessa: valori >= 100 sono considerati kHz, altrimenti MHz
            return valore_num / 1000.0 if valore_num >= 100 else valore_num

    return 0.0



def get_prov_sigla(raw_prov):
    """Normalizza la provincia ricavando la sigla a 2 lettere."""
    if not raw_prov:
        return ""
    clean = re.sub(r'[^a-zA-Z]', '', raw_prov.strip()).upper()
    if clean in NOMI_A_SIGLA:
        return NOMI_A_SIGLA[clean]
    return clean[:2]

def get_zone_provincia(raw_prov):
    prov_sigla = get_prov_sigla(raw_prov)

    # Lookup della zona radioamatoriale
    zone_str = str(PROV_TO_ZONE[prov_sigla]) if prov_sigla in PROV_TO_ZONE else ""
    return zone_str,prov_sigla

def is_in_zone_of_interest(zone):
    if zone:
        return (int(zone)) in ZONES_OF_INTEREST
    else:
        return False

def format_chirp_name(prov_sigla, raw_nome, default_idx,zone_str):
    """
    Crea un nome formattato nel formato <Zona><Provincia><Nome> per FT-90R (max 6 caratteri).
    Esempio: Prov 'PA', Nome 'RPT' -> '9PARPT'
    """
    #prov_sigla = get_prov_sigla(raw_prov)
    
    # Lookup della zona radioamatoriale
    #zone_str = str(PROV_TO_ZONE[prov_sigla]) if prov_sigla in PROV_TO_ZONE else ""
    # Prefisso geografico (es. '9PA' o 'PA' se la zona non viene trovata)
    geo_prefix = f"{zone_str}{prov_sigla}"
    
    # Pulizia del nome del ripetitore
    nome_clean = re.sub(r'[^a-zA-Z0-9]', '', raw_nome.strip()).upper() if raw_nome else ""
    
    # Composizione con troncamento a max 6 caratteri (limite FT-90R)
    if geo_prefix:
        remaining_len = max(0, 6 - len(geo_prefix))
        full_name = f"{geo_prefix}{nome_clean[:remaining_len]}"
    else:
        full_name = nome_clean if nome_clean else f"CH{default_idx}"

    return full_name[:6]

# def processa_csv(file_ingresso, file_uscita):
#     righe_valide = []

#     with open(file_ingresso, mode='r', encoding='utf-8') as f:
#         reader = csv.DictReader(f)
#         colonne = [c.strip() for c in (reader.fieldnames or [])]

#         # Ricerca specifica dell'intestazione '(P)rov.'
#         col_prov = next((c for c in colonne if (c.lower() == '(p)rov') or (c.lower() == '(p)rov.') ), None)
#         if not col_prov:
#             # Fallback se non trova l'esatto '(P)rov'
#             col_prov = next((c for c in colonne if 'prov' in c.lower()), None)

#         col_freq = next((c for c in colonne if c.lower() in ['(f)req', 'f', 'freq', 'frequenza']), None)
#         col_tono = next((c for c in colonne if 'tono' in c.lower() or 'ctcss' in c.lower()), None)
#         col_nome = next((c for c in colonne if c.lower() in ['(n)ome', 'nome', 'name', 'label', 'tag']), None)
#         col_shift = next((c for c in colonne if 'shift' in c.lower() or 'duplex' in c.lower()), None)
#         col_offset = next((c for c in colonne if 'offset' in c.lower()), None)

#         if not col_freq or not col_tono:
#             print("Errore: colonne Frequenza o Tono non trovate nel file sorgente.")
#             return

#         for row in reader:
#             # Normalizzazione delle chiavi di riga rimuovendo spazi bianchi
#             row_clean = {k.strip(): v for k, v in row.items() if k}

#             val_tono_str = row_clean.get(col_tono, '').strip() if col_tono else ''
#             val_freq_str = row_clean.get(col_freq, '').strip() if col_freq else ''

#             if not is_valid_tone(val_tono_str) or not is_valid_frequency(val_freq_str):
#                 continue

#             freq = parse_float(val_freq_str)
#             tone_val = f"{parse_float(val_tono_str):.1f}"
            
#             val_nome_raw = row_clean.get(col_nome, '') if col_nome else ''
#             val_prov_raw = row_clean.get(col_prov, '') if col_prov else ''
#             # recupera provincia e zona 
#             zone_str,prov_sigla= get_zone_provincia(val_prov_raw)
#             #controllo se zona di interesse, altrimenti salta riga
#             if not is_in_zone_of_interest(zone_str):
#                 continue
#             nome_chirp = format_chirp_name(prov_sigla, val_nome_raw, len(righe_valide) + 1,zone_str)
#             duplex = parse_shift_direction(row_clean.get(col_shift, '')) if col_shift else ''
#             offset_val=parse_shift(row_clean.get(col_shift, '')) if col_shift else ''
#             #offset_val = parse_float(row_clean.get(col_offset, '')) if col_offset else None
#             #if offset_val is None:
#             #    offset_val = 0.600000 if freq < 300.0 else 1.600000

#             righe_valide.append({
#                 'prov_orig': val_prov_raw,
#                 'nome_orig': val_nome_raw,
#                 'nome': nome_chirp,
#                 'freq': freq,
#                 'tone_freq': tone_val,
#                 'duplex': duplex,
#                 'offset': f"{offset_val:.6f}"
#             })

#     # Scrittura file CSV per CHIRP
#     chirp_headers = [
#         "Location", "Name", "Frequency", "Duplex", "Offset", 
#         "Tone", "rToneFreq", "cToneFreq", "DtcsCode", "DtcsPolarity", 
#         "Mode", "TStep", "Skip", "Comment", "URCALL", "RPT1CALL", "RPT2CALL"
#     ]

#     with open(file_uscita, mode='w', newline='', encoding='utf-8') as f:
#         writer = csv.writer(f)
#         writer.writerow(chirp_headers)

#         for idx, item in enumerate(righe_valide, start=1):
#             writer.writerow([
#                 idx, item['nome'], f"{item['freq']:.6f}", item['duplex'], item['offset'],
#                 "Tone", item['tone_freq'], item['tone_freq'], "023", "NN", "FM", "10.00",
#                 "", "", "", "", ""
#             ])
# --- FIX BUG 3 & 4: Processa CSV Corretto per CHIRP ---
def processa_csv(file_ingresso, file_uscita):
    righe_valide = []

    # Uso utf-8-sig per rimuovere automaticamente eventuale BOM UTF-8 iniziale
    with open(file_ingresso, mode='r', encoding='utf-8-sig') as f:
        reader = csv.DictReader(f)
        colonne = [c.strip() for c in (reader.fieldnames or [])]

        col_prov = next((c for c in colonne if '(p)rov' in c.lower() or 'prov' in c.lower()), None)
        col_freq = next((c for c in colonne if c.lower() in ['(f)req', 'f', 'freq', 'frequenza']), None)
        col_tono = next((c for c in colonne if 'tono' in c.lower() or 'ctcss' in c.lower()), None)
        col_nome = next((c for c in colonne if c.lower() in ['(n)ome', 'nome', 'name', 'label', 'tag']), None)
        col_shift = next((c for c in colonne if 'shift' in c.lower() or 'duplex' in c.lower()), None)

        if not col_freq or not col_tono:
            print("Errore: colonne Frequenza o Tono non trovate nel file sorgente.")
            return

        for row in reader:
            row_clean = {k.strip(): v for k, v in row.items() if k}

            val_tono_str = row_clean.get(col_tono, '').strip() if col_tono else ''
            val_freq_str = row_clean.get(col_freq, '').strip() if col_freq else ''

            if not is_valid_tone(val_tono_str) or not is_valid_frequency(val_freq_str):
                continue

            freq = parse_float(val_freq_str)
            tone_val = f"{parse_float(val_tono_str):.1f}"
            
            val_nome_raw = row_clean.get(col_nome, '') if col_nome else ''
            val_prov_raw = row_clean.get(col_prov, '') if col_prov else ''
            
            zone_str, prov_sigla = get_zone_provincia(val_prov_raw)
            
            if not is_in_zone_of_interest(zone_str):
                continue
                
            nome_chirp = format_chirp_name(prov_sigla, val_nome_raw, len(righe_valide) + 1, zone_str)
            
            raw_shift = row_clean.get(col_shift, '') if col_shift else ''
            duplex = parse_shift_direction(raw_shift)
            offset_val = parse_shift(raw_shift)

            # Impostazione corretta della modalità Tono per CHIRP
            # Se duplex è vuoto (simplex), l'offset deve essere 0
            if not duplex:
                offset_val = 0.0

            righe_valide.append({
                'prov_orig': val_prov_raw,
                'nome_orig': val_nome_raw,
                'nome': nome_chirp,
                'freq': freq,
                'tone_freq': tone_val,
                'duplex': duplex,
                'offset': f"{offset_val:.6f}"
            })

    chirp_headers = [
        "Location", "Name", "Frequency", "Duplex", "Offset", 
        "Tone", "rToneFreq", "cToneFreq", "DtcsCode", "DtcsPolarity", 
        "Mode", "TStep", "Skip", "Comment", "URCALL", "RPT1CALL", "RPT2CALL"
    ]

    with open(file_uscita, mode='w', newline='', encoding='utf-8') as f:
        writer = csv.writer(f)
        writer.writerow(chirp_headers)

        for idx, item in enumerate(righe_valide, start=1):
            writer.writerow([
                idx, item['nome'], f"{item['freq']:.6f}", item['duplex'], item['offset'],
                "Tone", item['tone_freq'], item['tone_freq'], "023", "NN", "FM", "10.00",
                "", "", "", "", ""
            ])
            
    # Anteprima a schermo
    print("=" * 80)
    print(" ANTEPRIMA PRIMI 5 CANALI CONVERTITI PER CHIRP (FT-90R)")
    print("=" * 80)
    print(f"{'CH':<4} | {'(P)rov':<8} | {'(N)ome Orig.':<12} | {'CHIRP Name':<10} | {'Freq (MHz)':<10} | {'Tono':<6}")
    print("-" * 80)
    
    num_anteprima = min(5, len(righe_valide))
    for idx in range(num_anteprima):
        item = righe_valide[idx]
        p_orig = item['prov_orig'][:8] if item['prov_orig'] else "<vuoto>"
        n_orig = item['nome_orig'][:12] if item['nome_orig'] else "<vuoto>"
        print(f"{idx+1:<4} | {p_orig:<8} | {n_orig:<12} | {item['nome']:<10} | {item['freq']:<10.4f} | {item['tone_freq']:<6}")

    print("=" * 80)
    print(f"Elaborazione completata: {len(righe_valide)} canali scritti in '{file_uscita}'.\n")

if __name__ == "__main__":
    processa_csv("input.csv", "out_chirp_ft90r.csv")

After filtering the dataset with Python and putting the FT-90R into manual CLONE mode (CHIRPS says how, read the instructions on screen ) .

Bringing a 20-year-old rig up to speed required a mix of vintage hardware patience and modern coding—but once loaded, the channels locked in .


Aged Steel Over Disposable Tech

The FT-90R has no SDR tricks, no 2.5k tuning, but its front end doesn’t overload when a nearby high-power station keys up, and its PTT won’t lock up halfway through a POTA activation.

Since we have something more valuable than China Export, I revamped the power conditioner.

A few days ago this was the chance to add an overvoltage protection to the input of my power conditioner. This and a few ferrites stays below the rigs and helps filtering some EMI and spikes.

It took sweat,I will require some paint remover, a soldering iron, and some custom code to bring it back, but carrying a bulletproof legacy rig into the field brings a peace of mind that no disposable clone can ever replicate.


Lascia un commento

Questo sito utilizza Akismet per ridurre lo spam. Scopri come vengono elaborati i dati derivati dai commenti.