Quoi de mieux qu’écrire soit même ce dont on a besoin et de pouvoir le faire évoluer à l’envie.

La quasi totalité des transceivers Icom disposent d’un protocole appelé CI-V qui permet de les commander par une liaison série ou bien en USB (ce qui est finalement la même chose puisque l’interface USB émule un (ou deux) ports série).

L’intérêt de passer par ce protocole est entre autre de se fabriquer des « macro-commandes » permettant une configuration immédiate du transceiver sans passer par les multiples (et compliqués) menus.

Pour ce qui est du pilotage à distance, après avoir testé une interface de déport de liaison série hardware fonctionnant sur toutes plateformes, je suis revenu à l’utilisation toute simple d’un Raspberry PI 4 connecté en réseau et accessible très simplement via le bien connu logicile VNC.

La partie voix étant traitée par le logiciel multi plateformes VOIP Mumble, cela fera l’objet d’un autre article.

Passons au code proprement dit :

Toutes les procédures sont basées sur les documentations CI-V d’Icom, bien sûr il faut aussi mettre la main à la patte pour les interpréter en lecture ou les instancier en écriture, mais bon ça nettoie les méninges…

Je ne suis (pas du tout) un expert en Python et je maitrise encore moins la bibliothèque Tkinter qui permet de réaliser des fenêtres interactives. Si vous avez des suggestions et des conseils ils seront les bienvenus.

La première partie du code est suffisamment explicite pour ne pas avoir à revenir sur les principes d’échanges de messages depuis la liaison série.

# CAT Icom V5.4
# 25/08/2021
# Version intermédiaire
# Programme Python d'envoi de télégrammes de contrôle et de commandes par protocole Icom IC-V
# Christian Thomas (F1CIY)
# Ces procédures et parties de programme sont librement réutilisable à condition de citer leur(s) source(s)
# Les routines de calcul bcd4 & bcd2 sont de Martin Ewing AA6E (https://aa6e.net/nw/)
# Merci à DF4OR pour ses précieuses explications sur le protocole CI-V (http://www.plicht.de/ekki/civ/civ-p31.html)
# Fonctionne sur PC sous Windows (compatible Linux, Raspberry, etc)
# Les télégrammes échangés peuvent être différents selon les appareils utilisés, toutes ces procédures ne sont pas implémentées sur
# tous les transceivers,
# Celles-ci sont applicables aux aux IC-9700, IC-7100, IC-7410 qui sont les appareils dont je dispose (voir commentaires pour chaque procédure)
# beaucoup sont probablement fonctionnelles sur d'autres transceivers
# La signification des échanges est indiquée dans les différentes documentations des transceivers Icom
# Pour faire simple la forme d'un télégramme est la suivante
# $FE,$FE,adresse du transceiver destinataire, adresse du PC origine (par défaut $00), message à envoyer, $FD (fin de la commande)
# Les octets $FE $FE de début et $FD de fin de télégramme sont fixés dans le protocole de tous les échanges (telegrammes envoyés ou reçus)
# Exemple :
# Pour passer le transceiver en émission (procédure TxOn())
# $FE,$FE,(adresse du transceiver),$E0,$1C,$00,$01,$FD
# Pour passer le transceiver en réception (procédure TxOff())
# $FE,$FE,(adresse du transceiver),$E0,$1C,$00,$00,$FD
# La partie active du télégramme est $1C,$00,$01 ou $00 
# De la même façon le transceiver réponds sous la forme suivante :
# $FE,$FE,$E0,(adresse du transceiver),$FB,$FD si la commande est correcte ($FB) ou bien
# $FE,$FE,$E0,(adresse du transceiver),$FA,$FD si la commande est incorrecte ($FA)
# Attention certains transceivers (IC-7410, IC-7100 et certainement d'autres) renvoient en écho la commande qu'ils ont reçue
# ce qui doit être pris en compte pour la lecture de la trame en retour car celle-ci comprendra aussi la trame originale envoyée
# Une des procédure se nomme "LireTrame()" et peut être utilisée pour le débogage des échanges 
# Les télégrammes échangés peuvent contenir :
# Des ordres auxquels il sera seulement répondu OK ou non ($FB) ou ($FA) voir plus haut
# Des valeurs renvoyées par le transceiver (consigne puissance, valeur S mètre, etc..) qu'il faudra capturer
# sous forme de chaine de caractères et analyser
# Les valeurs échangées sont et doivent être exprimées en BCD (Binaire Codé Décimal)
# entre 0 et 255 et non en héxadécimal entre $00 et $FF
# Car dans cet intervalle héxadécimal on trouve les signaux de contrôle des télégrammes ($FB, $FC, $FE, etc..)
# qui pertuberaient le protocole d'échange
# Ce programme est destiné à l'Icom IC-7100 mais est aisément paramétrable au niveau des deux menus pour appeler les procédures non
# employées actuellement
# Toutes les procédures ne sont pas implémentées (il y en a des centaines possibles pour chaque transceiver)
# seules les plus utiles (?) ont été développées

import serial
import time
import datetime
from tkinter import *

# Initialisation 
debut   = 0xFE                  # Preambule
adresse = 0x88                  # Adresse du transceiver                          
depuis  = 0x00                  # Adresse du PC ou du Raspberry                                
fin     = 0xFD                  # Fin de message                                
#serial_dev  = "/dev/ttyUSB0"   # Raspberry mode
serial_dev  = "COM5"            # Port COM du PC ou du Raspberry          
serial_baud = 19200             # Vitesse port COM

# Initialisation liaison série : un timeout est nécéssaire pour la fonction "ser.read_until()"
ser = serial.Serial(serial_dev, serial_baud, bytesize=8, parity='N', stopbits=1, timeout=0.5, xonxoff=0, rtscts=0) 

def clear(): # Efface console
    print('\n' * 50)

def LireTrame():                 # Petit utilitaire de débogage 
    ser.flushInput()
    ser.flushOutput()
    command = (debut,debut,adresse,depuis,0X03,fin)
    ser.write(command)
    s = ser.read_until('')
    a = len(s)
    print ("Longueur de la trame reçue : ",a," octets")
    print ("Trame reçue en hexadecimal :  ", end="")
    for i in range(a):
        print((hex(s[i])),"", end="")
    print()
    print("Trame reçue en décimal     :  ",end="")
    for i in range(a):
        print((s[i]),"", end="")
        
def ReadFrequencyEcho():          # Version pour transceiver avec echo de retour de commande (IC-7410, IC-7100)
    ser.flushInput()              # Corrigée le 06-08-2021 mais ne fonctionne pas correctement (432 MHz)
    ser.flushOutput()
    command = (debut,debut,adresse,depuis,0x03,fin)
    ser.write(command)
    s = ser.read_until('')
    freq = []
    for i in range (0,17):
        freq.append(hex(s[i])[2:]) 
    print(freq)
    Frequence = (freq[15]+freq[14]+freq[13]+freq[12]+freq[11])
    return(Frequence)
        
def ReadPowerEcho():              # Version pour transceiver avec echo de retour de commande (IC-7410, IC-7100)
    ser.flushInput()               
    ser.flushOutput()
    command = (debut,debut,adresse,depuis,0x14,0x0A,fin)
    ser.write(command)
    s = ser.read_until('')
    octet_1 = str(hex((s[13]))[2]) 
    if len(str(hex(s[14]))) == 3 : # A puissance proche de 0 % cette chaine de caractères ne fait plus que 3 caractères, il faut donc en différentier la lecture
        octet_2 = "0"
    else:    
        octet_2 = str(hex((s[14]))[2]) + str(hex((s[14]))[3])
    valeur = (octet_1 + octet_2)
    puissance = round((100/255)*int(valeur))
    #print(puissance)
    return(puissance)

def ReadPower():                   # Version pour transceiver sans echo (IC-9700, IC-7610, etc)
    ser.flushInput()
    ser.flushOutput()
    command = (debut,debut,adresse,depuis,0x14,0x0A,fin)
    ser.write(command)
    s = ser.read_until('')
    octet_1 = str(hex((s[6]))[2]) 
    if len(str(hex(s[7]))) == 3 :  # A puissance proche de 0 % cette chaine de caractères ne fait plus que 3 caractères, il faut donc en différentier la lecture
        octet_2 = "0"
    else:    
        octet_2 = str(hex((s[7]))[2]) + str(hex((s[7]))[3])
    valeur = (octet_1 + octet_2)
    puissance = round((100/255)*int(valeur))
    print(puissance)  

"""  Routines de calcul des 5 octets de programmation de la fréquence (bcd4 & BCD2) """

def bcd4(d1,d2,d3,d4):
    return (16*d1+d2, 16*d3+d4)

def bcd2(d1,d2):
    return ((16*d1+d2),)

def SaisieFrequenceManuelle():
    # Saisie de la fréquence à programmer
    freq = str(input("Fréquence désirée sur 9 digits ? "))
    # Construction des 5 octets
    fs = "%010d" % int(freq)
    print ('Fréquence :',fs,"KHz")
    out  = bcd4(int(fs[8]),int(fs[9]),int(fs[6]),int(fs[7]))
    out += bcd4(int(fs[4]),int(fs[5]),int(fs[2]),int(fs[3]))
    out += bcd2(int(fs[0]),int(fs[1]))
    octet0 = out[0]
    octet1 = out[1]
    octet2 = out[2]
    octet3 = out[3]
    octet4 = out[4]
    command = (debut,debut,adresse,depuis,0x00,octet0,octet1,octet2,octet3,octet4,fin)
    ser.write(command)
    
def Frequence(freq):
    fs = "%010d" % int(freq)
    # Construction des 5 octets
    fs = "%010d" % int(freq)
    print ('Fréquence :',fs,"KHz")
    out  = bcd4(int(fs[8]),int(fs[9]),int(fs[6]),int(fs[7]))
    out += bcd4(int(fs[4]),int(fs[5]),int(fs[2]),int(fs[3]))
    out += bcd2(int(fs[0]),int(fs[1]))
    octet0 = out[0]
    octet1 = out[1]
    octet2 = out[2]
    octet3 = out[3]
    octet4 = out[4]
    command = (debut,debut,adresse,depuis,0x00,octet0,octet1,octet2,octet3,octet4,fin)
    ser.write(command)
     
def VFO():
    command = (debut,debut,adresse,depuis,0x07,fin)
    ser.write(command)
    
def VFOA():
    command = (debut,debut,adresse,depuis,0x07,0x00,fin)
    ser.write(command)

def VFOB():
    command = (debut,debut,adresse,depuis,0x07,0x01,fin)
    ser.write(command)
    
def VFOAB():
    command = (debut,debut,adresse,depuis,0x07,0xA0,fin)
    ser.write(command)

def MainBand():             # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x07,0xD0,fin)
    ser.write(command)
    
def SubBand():              # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x07,0xD1,fin)
    ser.write(command)
    
def ChangBand():            # Pour IC-9700, commande Flip Flop
    command = (debut,debut,adresse,depuis,0x07,0xB0,fin)
    ser.write(command)   

def Memory(): 
    command = (debut,debut,adresse,depuis,0x08,fin)
    ser.write(command)

def MemoryBankA():          # Pour IC-7100 
    command = (debut,debut,adresse,depuis,0x08,0xA0,0x01,fin)
    ser.write(command)
    
def MemoryBankB():          # Pour IC-7100
    command = (debut,debut,adresse,depuis,0x08,0xA0,0x02,fin)
    ser.write(command)
    
def MemoryBankC():          # Pour IC-7100
    command = (debut,debut,adresse,depuis,0x08,0xA0,0x03,fin)
    ser.write(command)
    
def MemoryBankD():          # Pour IC-7100
    command = (debut,debut,adresse,depuis,0x08,0xA0,0x04,fin)
    ser.write(command)    

def MemoryBankE():          # Pour IC-7100
    command = (debut,debut,adresse,depuis,0x08,0xA0,0x05,fin)
    ser.write(command)    

def MemoryScanChannel_1():  # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x0E,0xB2,0x01,fin)
    ser.write(command)   
    
def MemoryScanChannel_2():  # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x0E,0xB2,0x02,fin)
    ser.write(command)
    
def MemoryScanChannel_3():  # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x0E,0xB2,0x03,fin)
    ser.write(command)   
    
def MemoryScanChannel_123():# Pour IC-9700
    command = (debut,debut,adresse,depuis,0x0E,0xB2,0x00,fin)
    ser.write(command)    
    
def MemoryScanStart():
    command = (debut,debut,adresse,depuis,0x0E,0x22,fin)
    ser.write(command)
    
def MemoryScanStop():
    command = (debut,debut,adresse,depuis,0x0E,0x00,fin)
    ser.write(command)

def ScanResumeOn():
    command = (debut,debut,adresse,depuis,0x0E,0xD3,fin)
    ser.write(command)

def ScanResumeOff():
    command = (debut,debut,adresse,depuis,0x0E,0xD0,fin)
    ser.write(command)

def Simplex():
    command = (debut,debut,adresse,depuis,0x0F,0x10,fin)
    ser.write(command)
    
def DuplexMoins():
    command = (debut,debut,adresse,depuis,0x0F,0x11,fin)
    ser.write(command)
    
def DuplexPlus():
    command = (debut,debut,adresse,depuis,0x0F,0x12,fin)
    ser.write(command)
    
def AnnonceFrequence():
    command = (debut,debut,adresse,depuis,0x13,0x00,fin)
    ser.write(command)    
        
def SubtoneOff():
    command = (debut,debut,adresse,depuis,0x16,0x42,0X00,fin)
    ser.write(command)    

def SubtoneOn():
    command = (debut,debut,adresse,depuis,0x16,0x42,0X01,fin)
    ser.write(command)   
    
def TxOn():
    command = (debut,debut,adresse,depuis,0x1C,0x0,0x1,fin)
    ser.write(command)
    
def TxOff():
    command = (debut,debut,adresse,depuis,0x1C,0x0,0x0,fin)
    ser.write(command)

def Mod_Lsb():
    command = (debut,debut,adresse,depuis,0x1,0x0,fin)
    ser.write(command)

def Mod_Usb():
    command = (debut,debut,adresse,depuis,0x1,0x1,fin)
    ser.write(command)
    
def Mod_Am():
    command = (debut,debut,adresse,depuis,0x1,0x2,fin)
    ser.write(command)

def Mod_Cw():
    command = (debut,debut,adresse,depuis,0x1,0x3,fin)
    ser.write(command)

def Mod_Rtty():
    command = (debut,debut,adresse,depuis,0x1,0x4,fin)
    ser.write(command)
 
def Mod_Fm():
    command = (debut,debut,adresse,depuis,0x1,0x5,fin)
    ser.write(command) 
    
def Mod_Dv():
    command = (debut,debut,adresse,depuis,0x1,0x17,fin)
    ser.write(command)
    
def Message_1():           # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x28,0x00,0x01,fin)
    ser.write(command)

def Message_2():           # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x28,0x00,0x02,fin)
    ser.write(command)
    
def Message_3():           # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x28,0x00,0x03,fin)
    ser.write(command)

def Message_4():           # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x28,0x00,0x04,fin)
    ser.write(command)
    
def Message_5():           # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x28,0x00,0x05,fin)
    ser.write(command)    

def Message_6():           # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x28,0x00,0x06,fin)
    ser.write(command)

def Message_7():           # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x28,0x00,0x07,fin)
    ser.write(command)
    
def Message_8():           # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x28,0x00,0x08,fin)
    ser.write(command)
    
def Puissance(x):
    # Routine de saisie de la valeur entre 0 & 100%
    # x = int(input("Valeur entre 0 et 100% ? "))
    valeur = round(255/100*x)
    str_valeur = str(valeur)
    if valeur < 10:
        str_valeur = '00' + str_valeur    
    if valeur < 100:
        str_valeur = '0' + str_valeur
    # Création des 2 octets de définition du % de la valeur
    Octet_1 = int((str_valeur[0]),base=16)
    Octet_2 = int((str_valeur[1]) + (str_valeur[2]),base=16)
    # Envoi de la commande au transceiver avec les 2 octets calculés
    command = (debut,debut,adresse,depuis,0x14,0x0A,Octet_1,Octet_2,fin)
    ser.write(command)    
       
def TurnOn():               # Ne fonctionne pas sur IC-7100, (à développer)
    command = (debut,debut,adresse,depuis,0x18,0x01,fin) 
    ser.write(command)    
    
def TurnOff():    
    command = (debut,debut,adresse,depuis,0x18,0x00,fin) 
    ser.write(command)
    
def PreampOn():    
    command = (debut,debut,adresse,depuis,0x16,0x02,0x01,fin) 
    ser.write(command)
    
def PreampOff():    
    command = (debut,debut,adresse,depuis,0x16,0x02,0x00,fin)
    ser.write(command)
    
def TsqlOn():    
    command = (debut,debut,adresse,depuis,0x16,0x43,0x01,fin)
    ser.write(command)

def TsqlOff():    
    command = (debut,debut,adresse,depuis,0x16,0x43,0x00,fin)
    ser.write(command) 
        
def ToneSquelsh():
    tone = str(input("Tonalité désirée sur 4 digits ? "))
    tone_1 = ("0x" + (tone[0]) + (tone[1]))
    tone_2 = ("0x" + (tone[2]) + (tone[3]))
    Octet_1 = (int(tone_1, 16))  
    Octet_2 = (int(tone_2, 16))
    command = (debut,debut,adresse,depuis,0x1B,0x01,Octet_1,Octet_2,fin)
    ser.write(command) 

def ToneSquelsh_1928(): 
    command = (debut,debut,adresse,depuis,0X1B,0X01,0X19,0X28,fin)     # Commande Tone squelsh 192.8
    ser.write(command)
    
def ToneSquelsh_1188(): 
    command = (debut,debut,adresse,depuis,0X1B,0X01,0X11,0X88,fin)     # Commande Tone squelsh 118.8
    ser.write(command)      

def ToneSelect():
    tone = str(input("Tonalité désirée sur 4 digits ? "))
    tone_1 = ("0x" + (tone[0]) + (tone[1]))
    tone_2 = ("0x" + (tone[2]) + (tone[3]))
    Octet_1 = (int(tone_1, 16))  
    Octet_2 = (int(tone_2, 16))
    command = (debut,debut,adresse,depuis,0x1B,0x00,Octet_1,Octet_2,fin)
    ser.write(command)

def Tone_1928(): 
    command = (debut,debut,adresse,depuis,0X1B,0X00,0X19,0X28,fin)     # Commande Tone 192.8
    ser.write(command)

def Tone_1188(): 
    command = (debut,debut,adresse,depuis,0X1B,0X00,0X11,0X88,fin)     # Commande Tone 118.8
    ser.write(command)    
    
def MonOn():    
    command = (debut,debut,adresse,depuis,0x16,0x45,0x01,fin)
    ser.write(command)
    
def MonOff():    
    command = (debut,debut,adresse,depuis,0x16,0x45,0x00,fin)
    ser.write(command)
    
def Data_Off_Mod_Mic():         # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x1A,0X05,0x01,0x15,0x00,fin)
    ser.write(command)
    
def Data_Off_Mod_Usb():         # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x1A,0X05,0x01,0x15,0x03,fin)
    ser.write(command)
    
def Usb_Af_Af():                # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x1A,0X05,0x01,0x05,0x00,fin) 
    ser.write(command)
    
def Usb_Af_If():                # Pour IC-9700
    command = (debut,debut,adresse,depuis,0x1A,0X05,0x01,0x05,0x01,fin)
    ser.write(command)    

def Date():                     # Pour IC-7100
    now      = datetime.datetime.now()
    annee_1  = int(str("0x" + now.strftime("%Y")[0] + now.strftime("%Y")[1]),base=16)
    annee_2  = int(str("0x" + now.strftime("%Y")[2] + now.strftime("%Y")[3]),base=16) 
    mois     = int(str("0x" + now.strftime("%m")[0] + now.strftime("%m")[1]),base=16)
    jour     = int(str("0x" + now.strftime("%d")[0] + now.strftime("%d")[1]),base=16)
    command  = (debut,debut,adresse,depuis,0x1A,0X05,0x01,0x20,annee_1,annee_2,mois,jour,fin)
    ser.write(command)    

def Heure():                    # Pour IC-7100
    now    = datetime.datetime.now()
    heure  = int(str("0x" + now.strftime("%H")[0] + now.strftime("%H")[1]),base=16)
    minute = int(str("0x" + now.strftime("%M")[0] + now.strftime("%M")[1]),base=16)
    command = (debut,debut,adresse,depuis,0x1A,0X05,0x01,0x21,heure,minute,fin)
    ser.write(command)

def Date_Heure():               # Pour IC-7100
    Heure()
    time.sleep(.1)
    Date()
    
# Fin de définition des procédures

clear()                         # Effacement console
Date_Heure()                    # Mise à l'heure et date (pour IC-7100)

# Configuration Menu_1
Menu_1 = Tk()
Menu_1.title('Commandes pour IC-7100')
Menu_1.geometry('1400x50+0+100')     # Largeur X Hauteur, position X, position Y
Menu_1.configure(background="green") # Couleur de fond de la fenetre
Menu_1.configure(borderwidth =5)     # Valeur Décalage gauche

# Personnalisation du menu : chaque commande est identifiée par le couple "texte du bouton" et nom de la procédure correspondante
# Les commandes avec paramètres doivent être précédées de l'argument "lambda"
Contenu_Menu_1 = (['VFO',VFO],['VFOA',VFOA],['VFOB',VFOB],['VFO A=B',VFOAB],['Simplex',Simplex],
['Dup -',DuplexMoins],['Dup +',DuplexPlus],['MEM',Memory],['MemBank A',MemoryBankA],
['MemBank B',MemoryBankB],['MemBank C',MemoryBankC],['MemBank D',MemoryBankD],
['MemBank E',MemoryBankE],['MemScan',MemoryScanStart],['MemScan Stop',MemoryScanStop],['Resume On',ScanResumeOn],
['Resume Off',ScanResumeOff],['Pwr 10%',lambda:Puissance(10)],['Pwr 50%',lambda:Puissance(50)],['145.500',lambda:Frequence(145500000)],
['145.550',lambda:Frequence(145550000)],['432.550',lambda:Frequence(432550000)],
['446.11875',lambda:Frequence(446118750)])

# Construction dynamique du menu
for i in range(len(Contenu_Menu_1)):
    Bouton= Button(Menu_1, text = Contenu_Menu_1 [i][0], command = Contenu_Menu_1 [i][1])
    Bouton.pack(side = LEFT)
#Menu_1.mainloop()

# Configuration Menu_2
Menu_2 = Tk()
Menu_2.title('PTT')
Menu_2.geometry('190x100+600+500') # Largeur X Hauteur, position X, position Y
Menu_2.configure(background="red") # couleur de fond de la fenetre
Menu_2.configure(borderwidth =15)

# Personnalisation du menu : chaque commande est identifiée par le couple "texte du bouton" et nom de la procédure correspondante
Contenu_Menu_2 = (['TX',TxOn],['RX',TxOff])

# Construction dynamique du menu
for i in range(len(Contenu_Menu_2)):
    Bouton= Button(Menu_2, text = Contenu_Menu_2 [i][0], command = Contenu_Menu_2 [i][1],
    background ='yellow', fg='red',width = 10,height = 5)
    Bouton.pack(side = LEFT)

# Menu_2.mainloop()
Menu_1.mainloop()
# Le Menu_2 est imbriqué avec le Menu_1