diff --git a/README.md b/README.md index 9837af1..a10d4e0 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,37 @@ # SIP-DMR Radio Gateway -Un gateway bidirezionale scritto in Python che permette di collegare centralini VoIP SIP (come FreePBX) a reti radio DMR (tramite protocollo OpenBridge). +Un gateway bidirezionale scritto in Python che permette di collegare centralini VoIP SIP (come FreePBX) a reti radio DMR (tramite protocollo OpenBridge)[cite: 2]. -Il sistema gestisce l'handshake SIP, la transcodifica audio in tempo reale tra PCM (G.711a) e AMBE, il controllo PTT tramite toni DTMF e il cambio dinamico del Talkgroup direttamente dal tastierino del telefono, simulando il comportamento di una vera radio. +Il sistema gestisce l'handshake SIP, la transcodifica audio in tempo reale tra PCM (G.711a) e AMBE, il controllo PTT tramite toni DTMF e il cambio dinamico del Talkgroup direttamente dal tastierino del telefono, simulando il comportamento di una vera radio[cite: 2]. ## 🚀 Caratteristiche Principali -* **Integrazione Trasparente:** Dialoga nativamente con FreePBX gestendo i ping `OPTIONS` (Trunk PJSIP). -* **Cambio Canale al Volo:** Digita il numero del Talkgroup sul telefono e il gateway ti sposterà di stanza senza far cadere la chiamata. -* **Sicurezza IP:** Rifiuta nativamente le connessioni SIP da IP non autorizzati. -* **Aggiornamento Display:** Usa il protocollo `re-INVITE` per mostrare Caller ID (DMR ID) e Talkgroup attivi sui telefoni hardware compatibili. +* **Integrazione Trasparente:** Dialoga nativamente con FreePBX gestendo i ping `OPTIONS` (Trunk PJSIP)[cite: 2]. +* **Cambio Canale al Volo:** Digita il numero del Talkgroup sul telefono e il gateway ti sposterà di stanza senza far cadere la chiamata[cite: 2]. +* **Sicurezza IP:** Rifiuta nativamente le connessioni SIP da IP non autorizzati[cite: 2]. +* **Aggiornamento Display:** Usa il protocollo `re-INVITE` per mostrare Caller ID (DMR ID) e Talkgroup attivi sui telefoni hardware compatibili[cite: 2]. +* **Transcoding 100% Software:** Nessun hardware dedicato richiesto (es. DVStick). Il sistema usa l'emulatore nativo `md380-emu` elaborando l'audio crudo (RAW) direttamente tramite CPU. +* **Multi-Tenancy Dinamica:** Gestisce chiamate simultanee multiple. Il motore alloca e dealloca dinamicamente le porte UDP instradando gli stream audio in tempo reale sui vocoder liberi. +* **Audio Bidirezionale G.711 / AMBE:** Converte al volo i pacchetti SIP A-Law in PCM lineare 16-bit (tramite modulo `audioop`) per la rete DMR, e viceversa. --- ## 🛠️ 1. Prerequisiti di Sistema (Debian / Ubuntu) -Prima di installare il gateway, assicurati di avere gli strumenti di compilazione e l'ambiente Python installati sul server: +Prima di installare il gateway, assicurati di avere gli strumenti di compilazione e l'ambiente Python installati sul server[cite: 2]: ```bash sudo apt update && sudo apt upgrade -y sudo apt install -y git build-essential python3 python3-venv python3-pip ``` -*(Nota: su architetture a 64-bit potrebbe essere necessario installare `libc6-dev-i386` per compilare correttamente l'emulatore AMBE).* +*(Nota: su architetture a 64-bit potrebbe essere necessario installare `libc6-dev-i386` per compilare correttamente l'emulatore AMBE)[cite: 2].* --- -## 📻 2. Installazione Motore Audio (md380-emu) +## 📻 2. Installazione Motore Audio Scalabile (md380-emu) -Il cuore della conversione audio è l'emulatore open-source `md380-emu`. Va scaricato, compilato e lasciato in esecuzione in background. +Il cuore della conversione audio è l'emulatore open-source `md380-emu`[cite: 2]. Va scaricato, compilato e configurato come "template" Systemd per supportare un pool di porte[cite: 2]. ```bash cd /opt @@ -37,20 +40,21 @@ cd md380-emu sudo make ``` -**Creazione del demone Systemd per l'emulatore:** +**Creazione del demone Systemd Template per le istanze multiple:** -Crea il file `/etc/systemd/system/md380-emu.service`: +Crea il file `/etc/systemd/system/md380-emu@.service`: ```ini [Unit] -Description=MD380 AMBE Emulator +Description=MD380 Emulator Vocoder (Port %i) After=network.target [Service] Type=simple User=root WorkingDirectory=/opt/md380-emu -ExecStart=/opt/md380-emu/emulator -p 2470 +# Il %i verrà sostituito automaticamente con il numero di porta +ExecStart=/opt/md380-emu/emulator -p %i Restart=always RestartSec=3 @@ -58,17 +62,24 @@ RestartSec=3 WantedBy=multi-user.target ``` -Abilita e avvia il servizio: +**Attivazione delle istanze (es. per 10 canali simultanei):** + +Ricarica i demoni e avvia il loop di porte (es. dalla 2470 alla 2479): ```bash -sudo systemctl enable md380-emu --now +sudo systemctl daemon-reload +for port in {2470..2479}; do + sudo systemctl enable --now md380-emu@$port.service +done ``` +*(Per aumentare la capacità futura, ti basterà avviare nuovi servizi systemctl ed espandere il range di porte generate in `vocoder_engine.py`).* + --- ## 💻 3. Installazione del Gateway Python -Clona questo repository e configura l'ambiente virtuale: +Clona questo repository e configura l'ambiente virtuale[cite: 2]: ```bash cd /opt @@ -83,50 +94,50 @@ source venv/bin/activate pip install -r requirements.txt ``` -Copia i file di esempio per creare la tua configurazione reale: +Copia i file di esempio per creare la tua configurazione reale[cite: 2]: ```bash cp config.example.json config.json cp utenti.example.json utenti.json ``` -**Modifica `config.json`:** Inserisci le credenziali del tuo server HBlink/OpenBridge, le porte e, soprattutto, l'IP autorizzato del tuo server FreePBX per la sicurezza. +**Modifica `config.json`:** Inserisci le credenziali del tuo server HBlink/OpenBridge, le porte e, soprattutto, l'IP autorizzato del tuo server FreePBX per la sicurezza[cite: 2]. --- ## ⚙️ 4. Configurazione FreePBX -Affinché il centralino dialoghi correttamente con il Gateway, crea un Trunk PJSIP con queste regole: +Affinché il centralino dialoghi correttamente con il Gateway, crea un Trunk PJSIP con queste regole[cite: 2]: -* **Autenticazione:** Nessuna (Identity via IP Match). L'IP match deve puntare all'IP del server dove gira questo script Python. -* **Qualify Frequency:** Lascialo attivo (es. 60 secondi). Il gateway risponderà regolarmente con `200 OK`. -* **Codecs:** Disabilita tutto e lascia spuntato **SOLO `alaw`** (G.711a). -* **Advanced -> DTMF Mode:** Imposta su `RFC4733`. -* **Advanced -> Trust RPID/PAI:** Imposta su `Yes`. -* **Advanced -> Send RPID/PAI:** Imposta su `Both`. +* **Autenticazione:** Nessuna (Identity via IP Match)[cite: 2]. L'IP match deve puntare all'IP del server dove gira questo script Python[cite: 2]. +* **Qualify Frequency:** Lascialo attivo (es. 60 secondi)[cite: 2]. Il gateway risponderà regolarmente con `200 OK`[cite: 2]. +* **Codecs:** Disabilita tutto e lascia spuntato **SOLO `alaw`** (G.711a)[cite: 2]. +* **Advanced -> DTMF Mode:** Imposta su `RFC4733`[cite: 2]. +* **Advanced -> Trust RPID/PAI:** Imposta su `Yes`[cite: 2]. +* **Advanced -> Send RPID/PAI:** Imposta su `Both`[cite: 2]. -Crea poi una Rotta in Uscita (Outbound Route) che intercetti un prefisso a tua scelta (es. `99`) e che lo *strippi* (lo elimini), inviando al Trunk solo il numero del Talkgroup. +Crea poi una Rotta in Uscita (Outbound Route) che intercetti un prefisso a tua scelta (es. `99`) e che lo *strippi* (lo elimini), inviando al Trunk solo il numero del Talkgroup[cite: 2]. --- ## 📞 5. Utilizzo e Comandi DTMF (Tastierino) -Chiama la rotta configurata su FreePBX seguita dal Talkgroup desiderato (es. componi `99 222` per entrare sul TG 222). Una volta in linea, usa il tastierino del telefono per pilotare la radio: +Chiama la rotta configurata su FreePBX seguita dal Talkgroup desiderato (es. componi `99 222` per entrare sul TG 222)[cite: 2]. Una volta in linea, usa il tastierino del telefono per pilotare la radio[cite: 2]: -* **Tasto `*` (Asterisco):** Premi per abilitare il PTT e iniziare a trasmettere. -* **Tasto `#` (Cancelletto):** Premi per disabilitare il PTT e tornare in ascolto. -* **Tasti `0-9` (Cambio Canale):** Digita il numero di un nuovo Talkgroup (es. `9`, `1`) e rilascia i tasti. Dopo circa 2,5 secondi il sistema ti sposterà automaticamente sul nuovo TG (verrai avvisato da un aggiornamento sul display del telefono, se supportato). +* **Tasto `*` (Asterisco):** Premi per abilitare il PTT e iniziare a trasmettere[cite: 2]. +* **Tasto `#` (Cancelletto):** Premi per disabilitare il PTT e tornare in ascolto[cite: 2]. +* **Tasti `0-9` (Cambio Canale):** Digita il numero di un nuovo Talkgroup (es. `9`, `1`) e rilascia i tasti[cite: 2]. Dopo circa 2,5 secondi il sistema ti sposterà automaticamente sul nuovo TG (verrai avvisato da un aggiornamento sul display del telefono, se supportato)[cite: 2]. --- ## 🔄 6. Esecuzione come Servizio (Produzione) -Per far avviare il Gateway automaticamente al boot, crea il file `/etc/systemd/system/sipdmr.service`: +Per far avviare il Gateway automaticamente al boot, crea il file `/etc/systemd/system/sipdmr.service`[cite: 2]: ```ini [Unit] Description=Gateway SIP-DMR Core -After=network.target md380-emu.service +After=network.target [Service] Type=simple @@ -143,14 +154,14 @@ SyslogIdentifier=sipdmr WantedBy=multi-user.target ``` -Abilita e avvia il gateway: +Abilita e avvia il gateway[cite: 2]: ```bash sudo systemctl enable sipdmr --now ``` -Per visualizzare i log in tempo reale e monitorare il traffico radio: +Per visualizzare i log in tempo reale e monitorare il traffico radio[cite: 2]: ```bash journalctl -u sipdmr -f -``` \ No newline at end of file +``` diff --git a/core.py.con.bug.multiutenza b/core.py.con.bug.multiutenza deleted file mode 100644 index efedf8a..0000000 --- a/core.py.con.bug.multiutenza +++ /dev/null @@ -1,480 +0,0 @@ -import json -from bitstring import BitArray -import asyncio -import struct -import time -import os -import audioop -from vocoder_engine import VocoderEngine - -from openbridge_protocol import crea_pacchetto_ping, calcola_hmac -from dmr_utils3.ambe_utils import convert49BitTo72BitAMBE, convert72BitTo49BitAMBE - -# ==================================================================== -# CARICAMENTO CONFIGURAZIONI (JSON) -# ==================================================================== -def carica_config(): - try: - with open("config.json", "r") as f: - return json.load(f) - except Exception as e: - print(f"[ERRORE CRITICO] File config.json non trovato o errato ({e}). Chiusura.") - exit(1) - -CONFIG = carica_config() - -HBLINK_IP = CONFIG.get("hblink_ip", "127.0.0.1") -HBLINK_PORT = CONFIG.get("hblink_port", 62047) -HBLINK_PASSWORD = CONFIG.get("hblink_password", "TestVOIP") -PEER_ID = CONFIG.get("peer_id", 22241) -SIP_PORT = CONFIG.get("sip_port", 55060) -RTP_PORT = CONFIG.get("rtp_port", 10000) -VOCODER_HOST = CONFIG.get("vocoder_host", "127.0.0.1") -VOCODER_PORT = CONFIG.get("vocoder_port", 2470) -# --- NUOVA VARIABILE PER LA SICUREZZA IP --- -FREEPBX_IP = CONFIG.get("freepbx_ip", "127.0.0.1") - -def carica_rubrica(): - try: - with open("utenti.json", "r") as f: - return json.load(f) - except Exception as e: - return {} - -UTENTI_SIP_DMR = carica_rubrica() - -# ==================================================================== -# CLASSE SESSIONE SIP (Con Buffer DTMF per il cambio TG) -# ==================================================================== -class SessioneSIP: - def __init__(self, call_id, chiamante, destinatario_tg, rtp_addr, sip_addr, full_from, full_to, ip_vps): - self.call_id = call_id - self.chiamante = chiamante - self.tg_destinazione = str(destinatario_tg) - self.dmr_id = int(UTENTI_SIP_DMR.get(str(chiamante), 2223619)) - - self.rtp_client_addr = rtp_addr - self.sip_client_addr = sip_addr - self.ip_vps = ip_vps - self.rtp_seq = 0 - self.rtp_timestamp = 0 - - self.dialog_to = full_from - self.dialog_from = full_to.split(';')[0] + ";tag=987654" - self.local_cseq = 10 - - self.ptt_attivo = False - self.buffer_ambe = [] - self.seq_dmr = 0 - self.stream_id = os.urandom(4) - - self.ultimo_src_id = None - - # --- VARIABILI PER IL CAMBIO TG TRAMITE DTMF --- - self.dtmf_buffer = "" - self.dtmf_task = None - self.ultimo_tempo_rtp_dtmf = 0 - self.ultimo_evento_dtmf = None - -class OpenBridgeClient(asyncio.DatagramProtocol): - def __init__(self, manager): - self.transport = None - self.manager = manager - - def connection_made(self, transport): - self.transport = transport - print(f"[OpenBridge] Socket connesso a {HBLINK_IP}:{HBLINK_PORT}") - asyncio.create_task(self.keepalive_loop()) - - def datagram_received(self, data, addr): - if len(data) < 53: - return - - if data.startswith(b"DMRD") and len(data) >= 73: - src_id = int.from_bytes(data[5:8], 'big') - tg_arrivo = str(int.from_bytes(data[8:11], 'big')) - - # Il routing è dinamico: filtra in base al tg_destinazione attuale della sessione - sessioni_interessate = [s for s in self.manager.sessioni.values() if s.tg_destinazione == tg_arrivo] - if not sessioni_interessate: return - - print("D", end="", flush=True) - payload_33 = data[20:53] - - ambe1 = payload_33[0:9] - ambe2 = bytearray(9) - ambe2[0:4] = payload_33[9:13] - ambe2[4] = (payload_33[13] & 0xF0) | (payload_33[19] & 0x0F) - ambe2[5:9] = payload_33[20:24] - ambe3 = payload_33[24:33] - - frame_uniti = ambe1 + bytes(ambe2) + ambe3 - self.manager.ricevi_frame_dmr_multi(frame_uniti, sessioni_interessate, src_id) - - async def keepalive_loop(self): - while True: - if self.transport: - pacchetto_ping = crea_pacchetto_ping(PEER_ID, HBLINK_PASSWORD) - self.transport.sendto(pacchetto_ping) - await asyncio.sleep(10) - - def apri_flusso_chiamata(self, sess): - if not self.transport: return - flags = 0 - payload_base = struct.pack("!3sB I 4s B I I", b"OPB", 0x80, PEER_ID, sess.stream_id, flags, sess.dmr_id, int(sess.tg_destinazione)) - firma_hmac = calcola_hmac(HBLINK_PASSWORD, payload_base) - self.transport.sendto(payload_base + firma_hmac) - - def invia_dmrd_voce(self, sess, payload_ambe): - if not self.transport: return - try: - magic = b"DMRD" - seq_byte = struct.pack("B", sess.seq_dmr) - src_bytes = sess.dmr_id.to_bytes(3, 'big') - dst_bytes = int(sess.tg_destinazione).to_bytes(3, 'big') - peer_bytes = PEER_ID.to_bytes(4, 'big') - flags_byte = struct.pack("B", 0x00) - - header = magic + seq_byte + src_bytes + dst_bytes + peer_bytes + flags_byte + sess.stream_id - payload_ambe = payload_ambe.ljust(27, b'\x00') - - ambe1 = payload_ambe[0:9] - ambe2 = payload_ambe[9:18] - ambe3 = payload_ambe[18:27] - - payload_33 = bytearray(33) - payload_33[0:9] = ambe1 - payload_33[9:13] = ambe2[0:4] - payload_33[13] = ambe2[4] & 0xF0 - payload_33[19] = ambe2[4] & 0x0F - payload_33[20:24] = ambe2[5:9] - payload_33[24:33] = ambe3 - - pacchetto_base = header + bytes(payload_33) - firma_hmac = calcola_hmac(HBLINK_PASSWORD, pacchetto_base) - self.transport.sendto(pacchetto_base + firma_hmac) - except Exception: - pass - - def chiudi_flusso_chiamata(self, sess): - if not self.transport: return - magic = b"DMRD" - flags_byte = struct.pack("B", 0x02) - src_bytes = sess.dmr_id.to_bytes(3, 'big') - dst_bytes = int(sess.tg_destinazione).to_bytes(3, 'big') - peer_bytes = PEER_ID.to_bytes(4, 'big') - - for i in range(3): - seq_byte = struct.pack("B", (sess.seq_dmr + i) % 256) - header = magic + seq_byte + src_bytes + dst_bytes + peer_bytes + flags_byte + sess.stream_id - pacchetto_base = header + (b'\x00' * 33) - firma_hmac = calcola_hmac(HBLINK_PASSWORD, pacchetto_base) - self.transport.sendto(pacchetto_base + firma_hmac) - -# ==================================================================== -# GESTORE CENTRALE DELLE SESSIONI -# ==================================================================== -class SessionManager: - def __init__(self): - self.vocoder = VocoderEngine(mode="emulator", host=VOCODER_HOST, port=VOCODER_PORT) - self.openbridge = None - self.rtp_transport = None - self.sip_transport = None - self.sessioni = {} - - async def avvia(self): - await self.vocoder.avvia() - loop = asyncio.get_running_loop() - _, self.openbridge = await loop.create_datagram_endpoint( - lambda: OpenBridgeClient(self), - local_addr=('0.0.0.0', HBLINK_PORT), - remote_addr=(HBLINK_IP, HBLINK_PORT) - ) - print(f"[Core] PBX Multi-Utente avviato. SIP su porta {SIP_PORT}...\n") - print(f"[Security] Accetto connessioni SIP SOLO da: {FREEPBX_IP}\n") - - def trova_sessione_per_rtp(self, addr): - for sess in self.sessioni.values(): - if sess.rtp_client_addr and sess.rtp_client_addr[0] == addr[0]: - sess.rtp_client_addr = addr - return sess - return None - - def gestisci_nuova_chiamata(self, call_id, chiamante, destinatario, rtp_addr, sip_addr, full_from, full_to, ip_vps): - sess = SessioneSIP(call_id, chiamante, destinatario, rtp_addr, sip_addr, full_from, full_to, ip_vps) - self.sessioni[call_id] = sess - print(f"\n[Core] >>> SIP {chiamante} -> TG {destinatario} | ID: {sess.dmr_id}") - - def termina_chiamata(self, call_id): - if call_id in self.sessioni: - sess = self.sessioni[call_id] - if sess.ptt_attivo and self.openbridge: - self.openbridge.chiudi_flusso_chiamata(sess) - del self.sessioni[call_id] - - def gestisci_ptt(self, sess, stato): - if stato is True and not sess.ptt_attivo: - sess.ptt_attivo = True - sess.buffer_ambe = [] - sess.seq_dmr = 0 - sess.stream_id = os.urandom(4) - if self.openbridge: self.openbridge.apri_flusso_chiamata(sess) - elif stato is False and sess.ptt_attivo: - sess.ptt_attivo = False - if self.openbridge: self.openbridge.chiudi_flusso_chiamata(sess) - - # --- LOGICA CAMBIO TG (DTMF 0-9) --- - def gestisci_dtmf_digit(self, sess, digit): - if sess.ptt_attivo: return # Evita di cambiare TG mentre sei in TX - - sess.dtmf_buffer += str(digit) - - # Resetta il task esistente se si preme un altro numero velocemente - if sess.dtmf_task: - sess.dtmf_task.cancel() - - sess.dtmf_task = asyncio.create_task(self.attendi_cambio_tg(sess)) - - async def attendi_cambio_tg(self, sess): - try: - # Attende 2.5 secondi dalla pressione dell'ultimo tasto - await asyncio.sleep(2.5) - if sess.dtmf_buffer: - nuovo_tg = sess.dtmf_buffer - sess.dtmf_buffer = "" # Svuota il buffer - - print(f"\n[Core] *** CAMBIO TG: {sess.chiamante} è passato al Talkgroup {nuovo_tg} ***") - sess.tg_destinazione = nuovo_tg - - # Notifica il telefono del cambio avvenuto - self.notifica_display(sess, f"TG: {nuovo_tg}") - except asyncio.CancelledError: - pass # Il task è stato annullato perché l'utente ha premuto un altro tasto - - # --- FUNZIONE DISPLAY GENERICA (Ora accetta stringhe personalizzate) --- - def notifica_display(self, sess, testo_display): - if not self.sip_transport or not sess.sip_client_addr: - return - - sess.local_cseq += 1 - branch = f"z9hG4bK-{os.urandom(4).hex()}" - client_sip_ip, client_sip_port = sess.sip_client_addr - - sdp = (f"v=0\r\no=- 12345 12345 IN IP4 {sess.ip_vps}\r\ns=Talk\r\n" - f"c=IN IP4 {sess.ip_vps}\r\nt=0 0\r\nm=audio {RTP_PORT} RTP/AVP 8 101\r\n" - f"a=rtpmap:8 PCMA/8000\r\na=rtpmap:101 telephone-event/8000\r\na=sendrecv\r\n") - - sip_reinvite = ( - f"INVITE sip:{sess.chiamante}@{client_sip_ip}:{client_sip_port} SIP/2.0\r\n" - f"Via: SIP/2.0/UDP 127.0.0.1:{SIP_PORT};branch={branch}\r\n" - f"Max-Forwards: 70\r\n" - f"From: {sess.dialog_from}\r\n" - f"To: {sess.dialog_to}\r\n" - f"Call-ID: {sess.call_id}\r\n" - f"CSeq: {sess.local_cseq} INVITE\r\n" - f"Contact: \r\n" - f"Remote-Party-ID: \"{testo_display}\" ;party=called;screen=yes;privacy=off\r\n" - f"P-Asserted-Identity: \"{testo_display}\" \r\n" - f"Content-Type: application/sdp\r\n" - f"Content-Length: {len(sdp)}\r\n" - f"\r\n" - f"{sdp}" - ) - self.sip_transport.sendto(sip_reinvite.encode('utf-8'), sess.sip_client_addr) - - def ricevi_frame_dmr_multi(self, ambe_frames, sessioni_interessate, src_id): - ascoltatori = [s for s in sessioni_interessate if not s.ptt_attivo] - if not ascoltatori or not self.rtp_transport: return - - for sess in ascoltatori: - if sess.ultimo_src_id != src_id: - sess.ultimo_src_id = src_id - self.notifica_display(sess, f"RX: {src_id}") - - asyncio.create_task(self.processa_audio_dmr(ambe_frames, ascoltatori)) - - async def processa_audio_dmr(self, ambe_27_bytes, ascoltatori): - try: - for i in range(0, len(ambe_27_bytes), 9): - frame_9 = ambe_27_bytes[i:i+9] - if len(frame_9) < 9: continue - - try: - ambe_49_bits = convert72BitTo49BitAMBE(BitArray(bytes=frame_9)) - ba_49 = ambe_49_bits if isinstance(ambe_49_bits, BitArray) else BitArray(ambe_49_bits) - if len(ba_49) < 56: ba_49.append(BitArray(uint=0, length=56 - len(ba_49))) - frame_7 = ba_49.bytes[:7] - except Exception: continue - - pcm_data = await self.vocoder.converti_ambe_in_pcm(frame_7) - if not pcm_data or len(pcm_data) < 2: continue - if len(pcm_data) % 2 != 0: pcm_data = pcm_data[:-1] - pcma_data = audioop.lin2alaw(pcm_data, 2) - - for sess in ascoltatori: - header = bytearray(12) - header[0] = 0x80; header[1] = 0x08 - header[2:4] = struct.pack("!H", sess.rtp_seq) - header[4:8] = struct.pack("!I", sess.rtp_timestamp) - header[8:12] = struct.pack("!I", 0x12345678) - - rtp_packet = header + pcma_data - if sess.rtp_client_addr: - self.rtp_transport.sendto(rtp_packet, sess.rtp_client_addr) - - sess.rtp_seq = (sess.rtp_seq + 1) % 65536 - sess.rtp_timestamp = (sess.rtp_timestamp + 160) % 4294967296 - except Exception: - pass - - async def processa_audio_rtp(self, sess, rtp_payload): - if not sess.ptt_attivo: return - try: - ambe_bytes = await asyncio.wait_for(self.vocoder.converti_rtp_in_ambe(rtp_payload), timeout=0.1) - if not ambe_bytes: return - - dim_frame = 7 if len(ambe_bytes) % 7 == 0 else 9 - for i in range(0, len(ambe_bytes), dim_frame): - frame = ambe_bytes[i:i+dim_frame] - if len(frame) == 7: - stringa_bit = BitArray(bytes=frame).bin - ambe_49_bits = [int(b) for b in stringa_bit[:49]] - fec_9_byte = convert49BitTo72BitAMBE(ambe_49_bits) - sess.buffer_ambe.append(bytes(fec_9_byte)) - elif len(frame) == 9: - sess.buffer_ambe.append(frame) - - while len(sess.buffer_ambe) >= 3: - payload_vocale = b''.join(sess.buffer_ambe[:3]) - sess.buffer_ambe = sess.buffer_ambe[3:] - self.openbridge.invia_dmrd_voce(sess, payload_vocale) - sess.seq_dmr = (sess.seq_dmr + 1) % 256 - except Exception: - pass - -# ==================================================================== -# PROTOCOLLI DI RETE SIP/RTP -# ==================================================================== -class RtpProtocol(asyncio.DatagramProtocol): - def __init__(self, manager): self.manager = manager - def connection_made(self, transport): self.manager.rtp_transport = transport - def datagram_received(self, data, addr): - sess = self.manager.trova_sessione_per_rtp(addr) - if not sess or len(data) < 12: return - payload_type = data[1] & 0x7F - if payload_type == 8: - asyncio.create_task(self.manager.processa_audio_rtp(sess, data[12:])) - elif payload_type == 101: - evento_dtmf = data[12] - - # Meccanismo anti-rimbalzo (Debounce) per ignorare i frame duplicati - ora = time.time() - if ora - sess.ultimo_tempo_rtp_dtmf < 0.2 and sess.ultimo_evento_dtmf == evento_dtmf: - return - sess.ultimo_tempo_rtp_dtmf = ora - sess.ultimo_evento_dtmf = evento_dtmf - - # Esecuzione dei Comandi - if evento_dtmf == 10: - self.manager.gestisci_ptt(sess, True) - elif evento_dtmf == 11: - self.manager.gestisci_ptt(sess, False) - elif 0 <= evento_dtmf <= 9: - self.manager.gestisci_dtmf_digit(sess, evento_dtmf) - -class SipProtocol(asyncio.DatagramProtocol): - def __init__(self, manager): self.manager = manager - def connection_made(self, transport): - self.transport = transport - self.manager.sip_transport = transport - - def datagram_received(self, data, addr): - # --- SICUREZZA: BLOCCO IP --- - if addr[0] != FREEPBX_IP and FREEPBX_IP != "0.0.0.0": - # Decommenta la riga sotto se vuoi vedere i log dei tentativi bloccati - # print(f"[SECURITY] Rifiutato pacchetto SIP da IP non autorizzato: {addr[0]}") - return - # ---------------------------- - - msg = data.decode('utf-8', errors='ignore') - headers = {line.split(":", 1)[0].strip().upper(): line.split(":", 1)[1].strip() for line in msg.split('\r\n')[1:] if ":" in line} - call_id = headers.get('CALL-ID', '') - - if msg.startswith("OPTIONS"): - resp = (f"SIP/2.0 200 OK\r\nVia: {headers.get('VIA', '')}\r\n" - f"From: {headers.get('FROM', '')}\r\nTo: {headers.get('TO', '')}\r\n" - f"Call-ID: {call_id}\r\nCSeq: {headers.get('CSEQ', '')}\r\n" - f"Contact: \r\n" - f"Content-Length: 0\r\n\r\n") - self.transport.sendto(resp.encode('utf-8'), addr) - - elif msg.startswith("INVITE"): - client_rtp_port = 4000 - for line in msg.split('\n'): - line = line.strip() - if line.startswith("m=audio "): client_rtp_port = int(line.split()[1]) - - ip_vps = "127.0.0.1" - if "@" in headers.get("TO", ""): ip_vps = headers.get("TO", "").split("@")[1].split(">")[0] - - chiamante = headers.get("FROM", "").split("sip:")[1].split("@")[0] - destinatario = headers.get("TO", "").split("sip:")[1].split("@")[0] - - full_from = headers.get('FROM', '') - full_to = headers.get('TO', '') - - self.manager.gestisci_nuova_chiamata(call_id, chiamante, destinatario, (addr[0], client_rtp_port), addr, full_from, full_to, ip_vps) - - sdp = (f"v=0\r\no=- 12345 12345 IN IP4 {ip_vps}\r\ns=Talk\r\n" - f"c=IN IP4 {ip_vps}\r\nt=0 0\r\nm=audio {RTP_PORT} RTP/AVP 8 101\r\n" - f"a=rtpmap:8 PCMA/8000\r\na=rtpmap:101 telephone-event/8000\r\na=sendrecv\r\n") - - resp = (f"SIP/2.0 200 OK\r\nVia: {headers.get('VIA', '')}\r\n" - f"From: {full_from}\r\nTo: {full_to};tag=987654\r\n" - f"Call-ID: {call_id}\r\nCSeq: {headers.get('CSEQ', '')}\r\n" - f"Contact: \r\nContent-Type: application/sdp\r\n" - f"Content-Length: {len(sdp)}\r\n\r\n{sdp}") - self.transport.sendto(resp.encode('utf-8'), addr) - - elif msg.startswith("SIP/2.0 200 OK"): - if "INVITE" in headers.get('CSEQ', ''): - cseq_num = headers.get('CSEQ', '').split()[0] - ack_uri = f"sip:{addr[0]}:{addr[1]}" - if "CONTACT" in headers: - contact_hdr = headers["CONTACT"] - if "<" in contact_hdr: - ack_uri = contact_hdr.split("<")[1].split(">")[0] - - ack = (f"ACK {ack_uri} SIP/2.0\r\n" - f"Via: SIP/2.0/UDP 127.0.0.1:{SIP_PORT};branch=z9hG4bK-{os.urandom(4).hex()}\r\n" - f"Max-Forwards: 70\r\n" - f"From: {headers.get('FROM', '')}\r\n" - f"To: {headers.get('TO', '')}\r\n" - f"Call-ID: {call_id}\r\n" - f"CSeq: {cseq_num} ACK\r\n" - f"Content-Length: 0\r\n\r\n") - self.transport.sendto(ack.encode('utf-8'), addr) - - elif msg.startswith("BYE"): - self.manager.termina_chiamata(call_id) - resp = (f"SIP/2.0 200 OK\r\nVia: {headers.get('VIA', '')}\r\n" - f"From: {headers.get('FROM', '')}\r\nTo: {headers.get('TO', '')}\r\n" - f"Call-ID: {call_id}\r\nCSeq: {headers.get('CSEQ', '')}\r\n" - f"Content-Length: 0\r\n\r\n") - self.transport.sendto(resp.encode('utf-8'), addr) - -async def main(): - loop = asyncio.get_running_loop() - manager = SessionManager() - await manager.avvia() - - _, _ = await loop.create_datagram_endpoint(lambda: SipProtocol(manager), local_addr=('0.0.0.0', SIP_PORT)) - _, _ = await loop.create_datagram_endpoint(lambda: RtpProtocol(manager), local_addr=('0.0.0.0', RTP_PORT)) - - try: - await asyncio.Event().wait() - except KeyboardInterrupt: - pass - -if __name__ == "__main__": - asyncio.run(main())