From bc791577e79b27ea57ff775133233b4d7e3ebf5e Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 7 May 2020 16:08:58 +0100 Subject: [PATCH 001/163] Beginnings of FM networking. --- Conf.cpp | 60 +++++++++++++++++ Conf.h | 17 +++++ FMControl.h | 55 ++++++++++++++++ FMNetwork.h | 55 ++++++++++++++++ MMDVM.ini | 9 +++ MMDVMHost.cpp | 131 ++++++++++++++++++++++++++++++++++---- MMDVMHost.h | 6 ++ MMDVMHost.vcxproj | 2 + MMDVMHost.vcxproj.filters | 6 ++ Modem.cpp | 83 +++++++++++++++++++++++- Modem.h | 6 ++ 11 files changed, 416 insertions(+), 14 deletions(-) create mode 100644 FMControl.h create mode 100644 FMNetwork.h diff --git a/Conf.cpp b/Conf.cpp index 8c07bc6..071c717 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -51,6 +51,7 @@ enum SECTION { SECTION_P25_NETWORK, SECTION_NXDN_NETWORK, SECTION_POCSAG_NETWORK, + SECTION_FM_NETWORK, SECTION_TFTSERIAL, SECTION_HD44780, SECTION_NEXTION, @@ -246,6 +247,13 @@ m_pocsagLocalAddress(), m_pocsagLocalPort(0U), m_pocsagNetworkModeHang(3U), m_pocsagNetworkDebug(false), +m_fmNetworkEnabled(false), +m_fmGatewayAddress(), +m_fmGatewayPort(0U), +m_fmLocalAddress(), +m_fmLocalPort(0U), +m_fmNetworkModeHang(3U), +m_fmNetworkDebug(false), m_tftSerialPort("/dev/ttyAMA0"), m_tftSerialBrightness(50U), m_hd44780Rows(2U), @@ -351,6 +359,8 @@ bool CConf::read() section = SECTION_NXDN_NETWORK; else if (::strncmp(buffer, "[POCSAG Network]", 16U) == 0) section = SECTION_POCSAG_NETWORK; + else if (::strncmp(buffer, "[FM Network]", 12U) == 0) + section = SECTION_FM_NETWORK; else if (::strncmp(buffer, "[TFT Serial]", 12U) == 0) section = SECTION_TFTSERIAL; else if (::strncmp(buffer, "[HD44780]", 9U) == 0) @@ -858,6 +868,21 @@ bool CConf::read() m_pocsagNetworkModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "Debug") == 0) m_pocsagNetworkDebug = ::atoi(value) == 1; + } else if (section == SECTION_POCSAG_NETWORK) { + if (::strcmp(key, "Enable") == 0) + m_fmNetworkEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "LocalAddress") == 0) + m_fmLocalAddress = value; + else if (::strcmp(key, "LocalPort") == 0) + m_fmLocalPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "GatewayAddress") == 0) + m_fmGatewayAddress = value; + else if (::strcmp(key, "GatewayPort") == 0) + m_fmGatewayPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_fmNetworkModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "Debug") == 0) + m_fmNetworkDebug = ::atoi(value) == 1; } else if (section == SECTION_TFTSERIAL) { if (::strcmp(key, "Port") == 0) m_tftSerialPort = value; @@ -1871,6 +1896,41 @@ bool CConf::getPOCSAGNetworkDebug() const return m_pocsagNetworkDebug; } +bool CConf::getFMNetworkEnabled() const +{ + return m_fmNetworkEnabled; +} + +std::string CConf::getFMGatewayAddress() const +{ + return m_fmGatewayAddress; +} + +unsigned int CConf::getFMGatewayPort() const +{ + return m_fmGatewayPort; +} + +std::string CConf::getFMLocalAddress() const +{ + return m_fmLocalAddress; +} + +unsigned int CConf::getFMLocalPort() const +{ + return m_fmLocalPort; +} + +unsigned int CConf::getFMNetworkModeHang() const +{ + return m_fmNetworkModeHang; +} + +bool CConf::getFMNetworkDebug() const +{ + return m_fmNetworkDebug; +} + std::string CConf::getTFTSerialPort() const { return m_tftSerialPort; diff --git a/Conf.h b/Conf.h index f5a40bd..915a21c 100644 --- a/Conf.h +++ b/Conf.h @@ -257,6 +257,15 @@ public: unsigned int getPOCSAGNetworkModeHang() const; bool getPOCSAGNetworkDebug() const; + // The FM Network section + bool getFMNetworkEnabled() const; + std::string getFMGatewayAddress() const; + unsigned int getFMGatewayPort() const; + std::string getFMLocalAddress() const; + unsigned int getFMLocalPort() const; + unsigned int getFMNetworkModeHang() const; + bool getFMNetworkDebug() const; + // The TFTSERIAL section std::string getTFTSerialPort() const; unsigned int getTFTSerialBrightness() const; @@ -518,6 +527,14 @@ private: unsigned int m_pocsagNetworkModeHang; bool m_pocsagNetworkDebug; + bool m_fmNetworkEnabled; + std::string m_fmGatewayAddress; + unsigned int m_fmGatewayPort; + std::string m_fmLocalAddress; + unsigned int m_fmLocalPort; + unsigned int m_fmNetworkModeHang; + bool m_fmNetworkDebug; + std::string m_tftSerialPort; unsigned int m_tftSerialBrightness; diff --git a/FMControl.h b/FMControl.h new file mode 100644 index 0000000..57dfd0b --- /dev/null +++ b/FMControl.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015-2019 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(FMControl_H) +#define FMControl_H + +#include "FMNetwork.h" +#include "RingBuffer.h" +#include "StopWatch.h" +#include "Defines.h" +#include "Timer.h" +#include "Modem.h" + +#include + +class CFMControl { +public: + CFMControl(CFMNetwork* network); + ~CFMControl(); + + bool writeModem(unsigned char* data, unsigned int len); + + unsigned int readModem(unsigned char* data, unsigned int space); + + void clock(unsigned int ms); + + void enable(bool enabled); + +private: + CFMNetwork* m_network; + CRingBuffer m_queue; + bool m_enabled; + FILE* m_fp; + + bool openFile(); + bool writeFile(const unsigned char* data); + void closeFile(); +}; + +#endif diff --git a/FMNetwork.h b/FMNetwork.h new file mode 100644 index 0000000..5229098 --- /dev/null +++ b/FMNetwork.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#ifndef FMNetwork_H +#define FMNetwork_H + +#include "RingBuffer.h" +#include "UDPSocket.h" +#include "Timer.h" + +#include +#include + +class CFMNetwork { +public: + CFMNetwork(const std::string& myAddress, unsigned int myPort, const std::string& gatewayAddress, unsigned int gatewayPort, bool debug); + ~CFMNetwork(); + + bool open(); + + void enable(bool enabled); + + unsigned int read(unsigned char* data); + + void reset(); + + void close(); + + void clock(unsigned int ms); + +private: + CUDPSocket m_socket; + in_addr m_address; + unsigned int m_port; + bool m_debug; + bool m_enabled; + CRingBuffer m_buffer; +}; + +#endif diff --git a/MMDVM.ini b/MMDVM.ini index 5e71a84..a86bdd3 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -227,6 +227,15 @@ GatewayPort=4800 # ModeHang=3 Debug=0 +[FM Network] +Enable=1 +LocalAddress=127.0.0.1 +LocalPort=3810 +GatewayAddress=127.0.0.1 +GatewayPort=4810 +# ModeHang=3 +Debug=0 + [TFT Serial] # Port=modem Port=/dev/ttyAMA0 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index e2a7766..b9f866e 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -119,12 +119,14 @@ m_ysf(NULL), m_p25(NULL), m_nxdn(NULL), m_pocsag(NULL), +m_fm(NULL), m_dstarNetwork(NULL), m_dmrNetwork(NULL), m_ysfNetwork(NULL), m_p25Network(NULL), m_nxdnNetwork(NULL), m_pocsagNetwork(NULL), +m_fmNetwork(NULL), m_display(NULL), m_ump(NULL), m_mode(MODE_IDLE), @@ -139,6 +141,7 @@ m_ysfNetModeHang(3U), m_p25NetModeHang(3U), m_nxdnNetModeHang(3U), m_pocsagNetModeHang(3U), +m_fmNetModeHang(3U), m_modeTimer(1000U), m_dmrTXTimer(1000U), m_cwIdTimer(1000U), @@ -317,6 +320,12 @@ int CMMDVMHost::run() return 1; } + if (m_fmEnabled && m_conf.getFMNetworkEnabled()) { + ret = createFMNetwork(); + if (!ret) + return 1; + } + in_addr transparentAddress; unsigned int transparentPort = 0U; CUDPSocket* transparentSocket = NULL; @@ -606,6 +615,9 @@ int CMMDVMHost::run() pocsagTimer.start(); } + if (m_fmEnabled) + m_fm = new CFMControl(m_fmNetwork); + bool remoteControlEnabled = m_conf.getRemoteControlEnabled(); if (remoteControlEnabled) { unsigned int port = m_conf.getRemoteControlPort(); @@ -798,6 +810,22 @@ int CMMDVMHost::run() } } + len = m_modem->readFMData(data); + if (m_nxdn != NULL && len > 0U) { + if (m_mode == MODE_IDLE) { + bool ret = m_fm->writeModem(data, len); + if (ret) { + m_modeTimer.setTimeout(m_nxdnRFModeHang); + setMode(MODE_FM); + } + } else if (m_mode == MODE_FM) { + m_fm->writeModem(data, len); + m_modeTimer.start(); + } else if (m_mode != MODE_LOCKOUT) { + LogWarning("FM modem data received when in mode %u", m_mode); + } + } + len = m_modem->readTransparentData(data); if (transparentSocket != NULL && len > 0U) transparentSocket->write(data, len, transparentAddress, transparentPort); @@ -948,6 +976,25 @@ int CMMDVMHost::run() } } + if (m_fm != NULL) { + unsigned int space = m_modem->getFMSpace(); + if (space > 0U) { + len = m_fm->readModem(data, space); + if (len > 0U) { + if (m_mode == MODE_IDLE) { + m_modeTimer.setTimeout(m_fmNetModeHang); + setMode(MODE_FM); + } + if (m_mode == MODE_FM) { + m_modem->writeFMData(data, len); + m_modeTimer.start(); + } else if (m_mode != MODE_LOCKOUT) { + LogWarning("FM data received when in mode %u", m_mode); + } + } + } + } + if (transparentSocket != NULL) { in_addr address; unsigned int port = 0U; @@ -980,6 +1027,8 @@ int CMMDVMHost::run() m_nxdn->clock(ms); if (m_pocsag != NULL) m_pocsag->clock(ms); + if (m_fm != NULL) + m_fm->clock(ms); if (m_dstarNetwork != NULL) m_dstarNetwork->clock(ms); @@ -993,6 +1042,8 @@ int CMMDVMHost::run() m_nxdnNetwork->clock(ms); if (m_pocsagNetwork != NULL) m_pocsagNetwork->clock(ms); + if (m_fmNetwork != NULL) + m_fmNetwork->clock(ms); if (m_mobileGPS != NULL) m_mobileGPS->clock(ms); @@ -1118,6 +1169,11 @@ int CMMDVMHost::run() delete m_pocsagNetwork; } + if (m_fmNetwork != NULL) { + m_fmNetwork->close(); + delete m_fmNetwork; + } + if (transparentSocket != NULL) { transparentSocket->close(); delete transparentSocket; @@ -1134,6 +1190,7 @@ int CMMDVMHost::run() delete m_p25; delete m_nxdn; delete m_pocsag; + delete m_fm; return 0; } @@ -1517,6 +1574,36 @@ bool CMMDVMHost::createPOCSAGNetwork() return true; } +bool CMMDVMHost::createFMNetwork() +{ + std::string gatewayAddress = m_conf.getFMGatewayAddress(); + unsigned int gatewayPort = m_conf.getFMGatewayPort(); + std::string localAddress = m_conf.getFMLocalAddress(); + unsigned int localPort = m_conf.getFMLocalPort(); + m_fmNetModeHang = m_conf.getFMNetworkModeHang(); + bool debug = m_conf.getFMNetworkDebug(); + + LogInfo("FM Network Parameters"); + LogInfo(" Gateway Address: %s", gatewayAddress.c_str()); + LogInfo(" Gateway Port: %u", gatewayPort); + LogInfo(" Local Address: %s", localAddress.c_str()); + LogInfo(" Local Port: %u", localPort); + LogInfo(" Mode Hang: %us", m_fmNetModeHang); + + m_fmNetwork = new CFMNetwork(localAddress, localPort, gatewayAddress, gatewayPort, debug); + + bool ret = m_fmNetwork->open(); + if (!ret) { + delete m_fmNetwork; + m_fmNetwork = NULL; + return false; + } + + m_fmNetwork->enable(true); + + return true; +} + void CMMDVMHost::readParams() { m_dstarEnabled = m_conf.getDStarEnabled(); @@ -1564,6 +1651,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_fmNetwork != NULL) + m_fmNetwork->enable(false); if (m_dstar != NULL) m_dstar->enable(true); if (m_dmr != NULL) @@ -1598,6 +1687,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_fmNetwork != NULL) + m_fmNetwork->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1636,6 +1727,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_fmNetwork != NULL) + m_fmNetwork->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1670,6 +1763,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_fmNetwork != NULL) + m_fmNetwork->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1704,6 +1799,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(true); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_fmNetwork != NULL) + m_fmNetwork->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1738,6 +1835,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(true); + if (m_fmNetwork != NULL) + m_fmNetwork->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1812,6 +1911,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_fmNetwork != NULL) + m_fmNetwork->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1852,6 +1953,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_fmNetwork != NULL) + m_fmNetwork->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1890,6 +1993,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(true); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(true); + if (m_fmNetwork != NULL) + m_fmNetwork->enable(true); if (m_dstar != NULL) m_dstar->enable(true); if (m_dmr != NULL) @@ -1980,55 +2085,55 @@ void CMMDVMHost::remoteControl() processModeCommand(MODE_NXDN, m_nxdnRFModeHang); break; case RCD_MODE_FM: - if (m_fmEnabled != false) + if (m_fmEnabled) processModeCommand(MODE_FM, 0); break; case RCD_ENABLE_DSTAR: - if (m_dstar != NULL && m_dstarEnabled==false) + if (m_dstar != NULL && !m_dstarEnabled) processEnableCommand(m_dstarEnabled, true); break; case RCD_ENABLE_DMR: - if (m_dmr != NULL && m_dmrEnabled==false) + if (m_dmr != NULL && !m_dmrEnabled) processEnableCommand(m_dmrEnabled, true); break; case RCD_ENABLE_YSF: - if (m_ysf != NULL && m_ysfEnabled==false) + if (m_ysf != NULL && !m_ysfEnabled) processEnableCommand(m_ysfEnabled, true); break; case RCD_ENABLE_P25: - if (m_p25 != NULL && m_p25Enabled==false) + if (m_p25 != NULL && !m_p25Enabled) processEnableCommand(m_p25Enabled, true); break; case RCD_ENABLE_NXDN: - if (m_nxdn != NULL && m_nxdnEnabled==false) + if (m_nxdn != NULL && !m_nxdnEnabled) processEnableCommand(m_nxdnEnabled, true); break; case RCD_ENABLE_FM: - if (m_fmEnabled==false) + if (!m_fmEnabled) processEnableCommand(m_fmEnabled, true); break; case RCD_DISABLE_DSTAR: - if (m_dstar != NULL && m_dstarEnabled==true) + if (m_dstar != NULL && m_dstarEnabled) processEnableCommand(m_dstarEnabled, false); break; case RCD_DISABLE_DMR: - if (m_dmr != NULL && m_dmrEnabled==true) + if (m_dmr != NULL && m_dmrEnabled) processEnableCommand(m_dmrEnabled, false); break; case RCD_DISABLE_YSF: - if (m_ysf != NULL && m_ysfEnabled==true) + if (m_ysf != NULL && m_ysfEnabled) processEnableCommand(m_ysfEnabled, false); break; case RCD_DISABLE_P25: - if (m_p25 != NULL && m_p25Enabled==true) + if (m_p25 != NULL && m_p25Enabled) processEnableCommand(m_p25Enabled, false); break; case RCD_DISABLE_NXDN: - if (m_nxdn != NULL && m_nxdnEnabled==true) + if (m_nxdn != NULL && m_nxdnEnabled) processEnableCommand(m_nxdnEnabled, false); break; case RCD_DISABLE_FM: - if (m_fmEnabled == true) + if (m_fmEnabled) processEnableCommand(m_fmEnabled, false); break; case RCD_PAGE: diff --git a/MMDVMHost.h b/MMDVMHost.h index 17d6786..ea268c6 100644 --- a/MMDVMHost.h +++ b/MMDVMHost.h @@ -33,8 +33,10 @@ #include "YSFNetwork.h" #include "P25Network.h" #include "DMRNetwork.h" +#include "FMNetwork.h" #include "DMRLookup.h" #include "MobileGPS.h" +#include "FMControl.h" #include "Display.h" #include "Timer.h" #include "Modem.h" @@ -62,12 +64,14 @@ private: CP25Control* m_p25; CNXDNControl* m_nxdn; CPOCSAGControl* m_pocsag; + CFMControl* m_fm; CDStarNetwork* m_dstarNetwork; CDMRNetwork* m_dmrNetwork; CYSFNetwork* m_ysfNetwork; CP25Network* m_p25Network; CNXDNNetwork* m_nxdnNetwork; CPOCSAGNetwork* m_pocsagNetwork; + CFMNetwork* m_fmNetwork; CDisplay* m_display; CUMP* m_ump; unsigned char m_mode; @@ -82,6 +86,7 @@ private: unsigned int m_p25NetModeHang; unsigned int m_nxdnNetModeHang; unsigned int m_pocsagNetModeHang; + unsigned int m_fmNetModeHang; CTimer m_modeTimer; CTimer m_dmrTXTimer; CTimer m_cwIdTimer; @@ -114,6 +119,7 @@ private: bool createP25Network(); bool createNXDNNetwork(); bool createPOCSAGNetwork(); + bool createFMNetwork(); void remoteControl(); void processModeCommand(unsigned char mode, unsigned int timeout); diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index 812c7a4..6aee2cb 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -181,6 +181,8 @@ + + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index fb8a362..55fda11 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -299,6 +299,12 @@ Header Files + + Header Files + + + Header Files + diff --git a/Modem.cpp b/Modem.cpp index edbebec..02276f2 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -79,6 +79,7 @@ const unsigned char MMDVM_POCSAG_DATA = 0x50U; const unsigned char MMDVM_FM_PARAMS1 = 0x60U; const unsigned char MMDVM_FM_PARAMS2 = 0x61U; const unsigned char MMDVM_FM_PARAMS3 = 0x62U; +const unsigned char MMDVM_FM_DATA = 0x65U; const unsigned char MMDVM_ACK = 0x70U; const unsigned char MMDVM_NAK = 0x7FU; @@ -150,6 +151,8 @@ m_txP25Data(1000U, "Modem TX P25"), m_rxNXDNData(1000U, "Modem RX NXDN"), m_txNXDNData(1000U, "Modem TX NXDN"), m_txPOCSAGData(1000U, "Modem TX POCSAG"), +m_rxFMData(1000U, "Modem RX FM"), +m_txFMData(1000U, "Modem TX FM"), m_rxTransparentData(1000U, "Modem RX Transparent"), m_txTransparentData(1000U, "Modem TX Transparent"), m_sendTransparentDataFrameType(0U), @@ -163,6 +166,7 @@ m_ysfSpace(0U), m_p25Space(0U), m_nxdnSpace(0U), m_pocsagSpace(0U), +m_fmSpace(0U), m_tx(false), m_cd(false), m_lockout(false), @@ -572,6 +576,20 @@ void CModem::clock(unsigned int ms) } break; + case MMDVM_FM_DATA: { + if (m_trace) + CUtils::dump(1U, "RX FM Data", m_buffer, m_length); + + unsigned char data = m_length - 2U; + m_rxFMData.addData(&data, 1U); + + data = TAG_DATA; + m_rxFMData.addData(&data, 1U); + + m_rxFMData.addData(m_buffer + 3U, m_length - 3U); + } + break; + case MMDVM_GET_STATUS: { // if (m_trace) // CUtils::dump(1U, "GET_STATUS", m_buffer, m_length); @@ -615,9 +633,11 @@ void CModem::clock(unsigned int ms) m_nxdnSpace = m_buffer[11U]; if (m_length > 12U) m_pocsagSpace = m_buffer[12U]; + if (m_length > 13U) + m_fmSpace = m_buffer[13U]; m_inactivityTimer.start(); - // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[5U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_pocsagSpace, int(m_lockout), int(m_cd)); + // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[5U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_pocsagSpace, m_fmSpace, int(m_lockout), int(m_cd)); } break; @@ -821,6 +841,23 @@ void CModem::clock(unsigned int ms) m_pocsagSpace--; } + if (m_fmSpace > 1U && !m_txFMData.isEmpty()) { + unsigned char len = 0U; + m_txFMData.getData(&len, 1U); + m_txFMData.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX FM Data", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing FM data to the MMDVM"); + + m_playoutTimer.start(); + + m_fmSpace--; + } + if (!m_txTransparentData.isEmpty()) { unsigned char len = 0U; m_txTransparentData.getData(&len, 1U); @@ -928,6 +965,20 @@ unsigned int CModem::readNXDNData(unsigned char* data) return len; } +unsigned int CModem::readFMData(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxFMData.isEmpty()) + return 0U; + + unsigned char len = 0U; + m_rxFMData.getData(&len, 1U); + m_rxFMData.getData(data, len); + + return len; +} + unsigned int CModem::readTransparentData(unsigned char* data) { assert(data != NULL); @@ -1169,6 +1220,36 @@ bool CModem::writePOCSAGData(const unsigned char* data, unsigned int length) return true; } +unsigned int CModem::getFMSpace() const +{ + return (m_txFMData.freeSpace() * 2U) / 3U; +} + +bool CModem::writeFMData(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + length = (length * 2U) / 3U; + + if (length > 252U) + return false; + + unsigned char buffer[255U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 3U; + buffer[2U] = MMDVM_FM_DATA; + + ::memcpy(buffer + 3U, data, length); + + unsigned char len = length + 3U; + m_txFMData.addData(&len, 1U); + m_txFMData.addData(buffer, len); + + return true; +} + bool CModem::writeTransparentData(const unsigned char* data, unsigned int length) { assert(data != NULL); diff --git a/Modem.h b/Modem.h index 6404d89..7605225 100644 --- a/Modem.h +++ b/Modem.h @@ -57,6 +57,7 @@ public: virtual unsigned int readYSFData(unsigned char* data); virtual unsigned int readP25Data(unsigned char* data); virtual unsigned int readNXDNData(unsigned char* data); + virtual unsigned int readFMData(unsigned char* data); virtual unsigned int readTransparentData(unsigned char* data); virtual unsigned int readSerial(unsigned char* data, unsigned int length); @@ -68,6 +69,7 @@ public: virtual bool hasP25Space() const; virtual bool hasNXDNSpace() const; virtual bool hasPOCSAGSpace() const; + virtual unsigned int getFMSpace() const; virtual bool hasTX() const; virtual bool hasCD() const; @@ -83,6 +85,7 @@ public: virtual bool writeP25Data(const unsigned char* data, unsigned int length); virtual bool writeNXDNData(const unsigned char* data, unsigned int length); virtual bool writePOCSAGData(const unsigned char* data, unsigned int length); + virtual bool writeFMData(const unsigned char* data, unsigned int length); virtual bool writeTransparentData(const unsigned char* data, unsigned int length); @@ -165,6 +168,8 @@ private: CRingBuffer m_rxNXDNData; CRingBuffer m_txNXDNData; CRingBuffer m_txPOCSAGData; + CRingBuffer m_rxFMData; + CRingBuffer m_txFMData; CRingBuffer m_rxTransparentData; CRingBuffer m_txTransparentData; unsigned int m_sendTransparentDataFrameType; @@ -178,6 +183,7 @@ private: unsigned int m_p25Space; unsigned int m_nxdnSpace; unsigned int m_pocsagSpace; + unsigned int m_fmSpace; bool m_tx; bool m_cd; bool m_lockout; From b3d287965f65a5710bf7084991fd911aa3878f18 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 7 May 2020 21:49:18 +0100 Subject: [PATCH 002/163] Create a dummy FM Control class. --- FMControl.cpp | 113 ++++++++++++++++++++++++++++++ FMControl.h | 14 ++-- FMNetwork.cpp | 141 ++++++++++++++++++++++++++++++++++++++ FMNetwork.h | 4 +- MMDVMHost.cpp | 36 ++++++++-- MMDVMHost.vcxproj | 2 + MMDVMHost.vcxproj.filters | 6 ++ Makefile | 11 +-- Makefile.Pi | 11 +-- Makefile.Pi.Adafruit | 11 +-- Makefile.Pi.HD44780 | 11 +-- Makefile.Pi.OLED | 11 +-- Makefile.Pi.PCF8574 | 11 +-- Makefile.Solaris | 10 +-- Modem.cpp | 70 ++++++++++++++++++- Modem.h | 5 ++ 16 files changed, 419 insertions(+), 48 deletions(-) create mode 100644 FMControl.cpp create mode 100644 FMNetwork.cpp diff --git a/FMControl.cpp b/FMControl.cpp new file mode 100644 index 0000000..f6d0df4 --- /dev/null +++ b/FMControl.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "FMControl.h" + +#include +#include + +CFMControl::CFMControl(CFMNetwork* network) : +m_network(network), +m_queue(3000U, "FM Control"), +m_enabled(false), +m_fp(NULL) +{ + assert(network != NULL); +} + +CFMControl::~CFMControl() +{ +} + +bool CFMControl::writeModem(unsigned char* data, unsigned int len) +{ + assert(data != NULL); + assert(len > 0U); + + // Unpack serial data (12-bit unsigned values) + + // De-emphasise the data + + // Repack the data (16-bit unsigned values) + + return true; +} + +unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) +{ + assert(data != NULL); + assert(space > 0U); + + // Unpack serial data (16-bit unsigned values) + + // Pre-emphasise the data + + // Repack the data (12-bit unsigned values) + + return 0U; +} + +void CFMControl::clock(unsigned int ms) +{ + // May not be needed +} + +void CFMControl::enable(bool enabled) +{ + // May not be needed +} + +bool CFMControl::openFile() +{ + if (m_fp != NULL) + return true; + + time_t t; + ::time(&t); + + struct tm* tm = ::localtime(&t); + + char name[100U]; + ::sprintf(name, "FM_%04d%02d%02d_%02d%02d%02d.ambe", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); + + m_fp = ::fopen(name, "wb"); + if (m_fp == NULL) + return false; + + ::fwrite("FM", 1U, 2U, m_fp); + + return true; +} + +bool CFMControl::writeFile(const unsigned char* data, unsigned int length) +{ + if (m_fp == NULL) + return false; + + ::fwrite(data, 1U, length, m_fp); + + return true; +} + +void CFMControl::closeFile() +{ + if (m_fp != NULL) { + ::fclose(m_fp); + m_fp = NULL; + } +} diff --git a/FMControl.h b/FMControl.h index 57dfd0b..177638e 100644 --- a/FMControl.h +++ b/FMControl.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2019 by Jonathan Naylor G4KLX + * Copyright (C) 2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,7 +33,7 @@ public: CFMControl(CFMNetwork* network); ~CFMControl(); - bool writeModem(unsigned char* data, unsigned int len); + bool writeModem(unsigned char* data, unsigned int length); unsigned int readModem(unsigned char* data, unsigned int space); @@ -42,13 +42,13 @@ public: void enable(bool enabled); private: - CFMNetwork* m_network; - CRingBuffer m_queue; - bool m_enabled; - FILE* m_fp; + CFMNetwork* m_network; + CRingBuffer m_queue; + bool m_enabled; + FILE* m_fp; bool openFile(); - bool writeFile(const unsigned char* data); + bool writeFile(const unsigned char* data, unsigned int length); void closeFile(); }; diff --git a/FMNetwork.cpp b/FMNetwork.cpp new file mode 100644 index 0000000..91cd1c9 --- /dev/null +++ b/FMNetwork.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "FMNetwork.h" +#include "Defines.h" +#include "Utils.h" +#include "Log.h" + +#include +#include +#include + +const unsigned int BUFFER_LENGTH = 500U; + +CFMNetwork::CFMNetwork(const std::string& localAddress, unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, bool debug) : +m_socket(localAddress, localPort), +m_address(), +m_port(gatewayPort), +m_debug(debug), +m_enabled(false), +m_buffer(2000U, "FM Network") +{ + assert(gatewayPort > 0U); + assert(!gatewayAddress.empty()); + + m_address = CUDPSocket::lookup(gatewayAddress); +} + +CFMNetwork::~CFMNetwork() +{ +} + +bool CFMNetwork::open() +{ + LogMessage("Opening FM network connection"); + + if (m_address.s_addr == INADDR_NONE) + return false; + + return m_socket.open(); +} + +bool CFMNetwork::write(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + + unsigned char buffer[500U]; + ::memset(buffer, 0x00U, 500U); + + buffer[0U] = 'F'; + buffer[1U] = 'M'; + buffer[2U] = 'D'; + + ::memcpy(buffer + 3U, data, length); + + if (m_debug) + CUtils::dump(1U, "FM Network Data Sent", buffer, length + 3U); + + return m_socket.write(buffer, length + 3U, m_address, m_port); +} + +void CFMNetwork::clock(unsigned int ms) +{ + unsigned char buffer[BUFFER_LENGTH]; + + in_addr address; + unsigned int port; + int length = m_socket.read(buffer, BUFFER_LENGTH, address, port); + if (length <= 0) + return; + + // Check if the data is for us + if (m_address.s_addr != address.s_addr || port != m_port) { + LogMessage("FM packet received from an invalid source, %08X != %08X and/or %u != %u", m_address.s_addr, address.s_addr, m_port, port); + return; + } + + // Invalid packet type? + if (::memcmp(buffer, "FMD", 3U) != 0) + return; + + if (!m_enabled) + return; + + if (m_debug) + CUtils::dump(1U, "FM Network Data Received", buffer, length); + + m_buffer.addData(buffer + 3U, length - 3U); +} + +unsigned int CFMNetwork::read(unsigned char* data, unsigned int space) +{ + assert(data != NULL); + + unsigned int bytes = m_buffer.dataSize(); + if (bytes == 0U) + return 0U; + + if (bytes < space) + space = bytes; + + m_buffer.getData(data, space); + + return space; +} + +void CFMNetwork::reset() +{ +} + +void CFMNetwork::close() +{ + m_socket.close(); + + LogMessage("Closing FM network connection"); +} + +void CFMNetwork::enable(bool enabled) +{ + if (enabled && !m_enabled) + reset(); + else if (!enabled && m_enabled) + m_buffer.clear(); + + m_enabled = enabled; +} diff --git a/FMNetwork.h b/FMNetwork.h index 5229098..e4c01ae 100644 --- a/FMNetwork.h +++ b/FMNetwork.h @@ -35,7 +35,9 @@ public: void enable(bool enabled); - unsigned int read(unsigned char* data); + bool write(const unsigned char* data, unsigned int length); + + unsigned int read(unsigned char* data, unsigned int space); void reset(); diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index b9f866e..bac53e0 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -1274,7 +1274,6 @@ bool CMMDVMHost::createModem() bool callsignAtEnd = m_conf.getFMCallsignAtEnd(); bool callsignAtLatch = m_conf.getFMCallsignAtLatch(); std::string rfAck = m_conf.getFMRFAck(); - std::string extAck = m_conf.getFMExtAck(); unsigned int ackSpeed = m_conf.getFMAckSpeed(); unsigned int ackFrequency = m_conf.getFMAckFrequency(); unsigned int ackMinTime = m_conf.getFMAckMinTime(); @@ -1291,7 +1290,6 @@ bool CMMDVMHost::createModem() bool cosInvert = m_conf.getFMCOSInvert(); unsigned int rfAudioBoost = m_conf.getFMRFAudioBoost(); float maxDevLevel = m_conf.getFMMaxDevLevel(); - unsigned int extAudioBoost = m_conf.getFMExtAudioBoost(); LogInfo("FM Parameters"); LogInfo(" Callsign: %s", callsign.c_str()); @@ -1305,7 +1303,6 @@ bool CMMDVMHost::createModem() LogInfo(" Callsign At End: %s", callsignAtEnd ? "yes" : "no"); LogInfo(" Callsign At Latch: %s", callsignAtLatch ? "yes" : "no"); LogInfo(" RF Ack: %s", rfAck.c_str()); - // LogInfo(" Ext. Ack: %s", extAck.c_str()); LogInfo(" Ack Speed: %uWPM", ackSpeed); LogInfo(" Ack Frequency: %uHz", ackFrequency); LogInfo(" Ack Min Time: %us", ackMinTime); @@ -1322,11 +1319,20 @@ bool CMMDVMHost::createModem() LogInfo(" COS Invert: %s", cosInvert ? "yes" : "no"); LogInfo(" RF Audio Boost: x%u", rfAudioBoost); LogInfo(" Max. Deviation Level: %.1f%%", maxDevLevel); - // LogInfo(" Ext. Audio Boost: x%u", extAudioBoost); m_modem->setFMCallsignParams(callsign, callsignSpeed, callsignFrequency, callsignTime, callsignHoldoff, callsignHighLevel, callsignLowLevel, callsignAtStart, callsignAtEnd, callsignAtLatch); m_modem->setFMAckParams(rfAck, ackSpeed, ackFrequency, ackMinTime, ackDelay, ackLevel); m_modem->setFMMiscParams(timeout, timeoutLevel, ctcssFrequency, ctcssThreshold, ctcssLevel, kerchunkTime, hangTime, useCOS, cosInvert, rfAudioBoost, maxDevLevel); + + if (m_conf.getFMNetworkEnabled()) { + std::string extAck = m_conf.getFMExtAck(); + unsigned int extAudioBoost = m_conf.getFMExtAudioBoost(); + + LogInfo(" Ext. Ack: %s", extAck.c_str()); + LogInfo(" Ext. Audio Boost: x%u", extAudioBoost); + + m_modem->setFMExtParams(extAck, extAudioBoost); + } } bool ret = m_modem->open(); @@ -1665,6 +1671,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_fm != NULL) + m_fm->enable(false); m_modem->setMode(MODE_DSTAR); if (m_ump != NULL) m_ump->setMode(MODE_DSTAR); @@ -1701,6 +1709,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_fm != NULL) + m_fm->enable(false); m_modem->setMode(MODE_DMR); if (m_ump != NULL) m_ump->setMode(MODE_DMR); @@ -1741,6 +1751,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_fm != NULL) + m_fm->enable(false); m_modem->setMode(MODE_YSF); if (m_ump != NULL) m_ump->setMode(MODE_YSF); @@ -1777,6 +1789,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_fm != NULL) + m_fm->enable(false); m_modem->setMode(MODE_P25); if (m_ump != NULL) m_ump->setMode(MODE_P25); @@ -1813,6 +1827,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(true); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_fm != NULL) + m_fm->enable(false); m_modem->setMode(MODE_NXDN); if (m_ump != NULL) m_ump->setMode(MODE_NXDN); @@ -1849,6 +1865,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(true); + if (m_fm != NULL) + m_fm->enable(false); m_modem->setMode(MODE_POCSAG); if (m_ump != NULL) m_ump->setMode(MODE_POCSAG); @@ -1872,6 +1890,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_fmNetwork != NULL) + m_fmNetwork->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1884,6 +1904,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_fm != NULL) + m_fm->enable(true); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); @@ -1925,6 +1947,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_fm != NULL) + m_fm->enable(false); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); @@ -1967,6 +1991,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_fm != NULL) + m_fm->enable(false); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); @@ -2007,6 +2033,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(true); if (m_pocsag != NULL) m_pocsag->enable(true); + if (m_fm != NULL) + m_fm->enable(true); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index 6aee2cb..cb65730 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -279,6 +279,8 @@ + + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index 55fda11..4c0b48f 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -568,5 +568,11 @@ Source Files + + Source Files + + + Source Files + \ No newline at end of file diff --git a/Makefile b/Makefile index efdaba6..716a8f6 100644 --- a/Makefile +++ b/Makefile @@ -9,11 +9,12 @@ LDFLAGS = -g OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNLICH.o \ - NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ - POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o \ - TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ + NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ + P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ + YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi b/Makefile.Pi index 640c07f..7d7e1f1 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -9,11 +9,12 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o \ - NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ - POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o \ - UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ + NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ + P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ + YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 24c2907..5009315 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -10,11 +10,12 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o \ - NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ - POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o \ - Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ + NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ + P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ + YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index c2faad2..5c875f6 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -9,11 +9,12 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o \ - NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ - POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o \ - Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ + NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ + P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ + YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index d2db40c..d4288cc 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -9,11 +9,12 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o OLED.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o \ - NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ - POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o \ - Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o OLED.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ + NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ + P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ + YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index 4c5d3a3..53ecc80 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -10,11 +10,12 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o \ - NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ - POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o \ - Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ + NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ + P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ + YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Solaris b/Makefile.Solaris index c50d41e..8c6ac68 100644 --- a/Makefile.Solaris +++ b/Makefile.Solaris @@ -9,11 +9,11 @@ LDFLAGS = -g OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o \ - NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o \ - NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o \ - QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ - UMP.o UserDB.o UserDBebtry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ + NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o \ + NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ + POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o \ + TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBebtry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Modem.cpp b/Modem.cpp index 02276f2..7c8ec97 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -79,6 +79,7 @@ const unsigned char MMDVM_POCSAG_DATA = 0x50U; const unsigned char MMDVM_FM_PARAMS1 = 0x60U; const unsigned char MMDVM_FM_PARAMS2 = 0x61U; const unsigned char MMDVM_FM_PARAMS3 = 0x62U; +const unsigned char MMDVM_FM_PARAMS4 = 0x63U; const unsigned char MMDVM_FM_DATA = 0x65U; const unsigned char MMDVM_ACK = 0x70U; @@ -184,6 +185,7 @@ m_fmCallsignAtStart(true), m_fmCallsignAtEnd(true), m_fmCallsignAtLatch(true), m_fmRfAck("K"), +m_fmExtAck("N"), m_fmAckSpeed(20U), m_fmAckFrequency(1750U), m_fmAckMinTime(4U), @@ -199,7 +201,9 @@ m_fmHangTime(5U), m_fmUseCOS(true), m_fmCOSInvert(false), m_fmRFAudioBoost(1U), -m_fmMaxDevLevel(90.0F) +m_fmExtAudioBoost(1U), +m_fmMaxDevLevel(90.0F), +m_fmExtEnable(false) { m_buffer = new unsigned char[BUFFER_LENGTH]; @@ -333,6 +337,16 @@ bool CModem::open() m_serial = NULL; return false; } + + if (m_fmExtEnable) { + ret = setFMExtParams(); + if (!ret) { + m_serial->close(); + delete m_serial; + m_serial = NULL; + return false; + } + } } m_statusTimer.start(); @@ -2013,6 +2027,13 @@ void CModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctc m_fmMaxDevLevel = maxDevLevel; } +void CModem::setFMExtParams(const std::string& ack, unsigned int audioBoost) +{ + m_fmExtAck = ack; + m_fmExtAudioBoost = audioBoost; + m_fmExtEnable = true; +} + bool CModem::setFMCallsignParams() { assert(m_serial != NULL); @@ -2189,6 +2210,53 @@ bool CModem::setFMMiscParams() return true; } +bool CModem::setFMExtParams() +{ + assert(m_serial != NULL); + + unsigned char buffer[80U]; + unsigned char len = 4U + m_fmExtAck.size(); + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = len; + buffer[2U] = MMDVM_FM_PARAMS4; + + buffer[3U] = m_fmExtAudioBoost; + + for (unsigned int i = 0U; i < m_fmExtAck.size(); i++) + buffer[4U + i] = m_fmExtAck.at(i); + + // CUtils::dump(1U, "Written", buffer, len); + + int ret = m_serial->write(buffer, len); + if (ret != len) + return false; + + unsigned int count = 0U; + RESP_TYPE_MMDVM resp; + do { + CThread::sleep(10U); + + resp = getResponse(); + if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { + count++; + if (count >= MAX_RESPONSES) { + LogError("The MMDVM is not responding to the SET_FM_PARAMS4 command"); + return false; + } + } + } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); + + // CUtils::dump(1U, "Response", m_buffer, m_length); + + if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { + LogError("Received a NAK to the SET_FM_PARAMS4 command from the modem"); + return false; + } + + return true; +} + void CModem::printDebug() { if (m_buffer[2U] == MMDVM_DEBUG1) { diff --git a/Modem.h b/Modem.h index 7605225..81b4a21 100644 --- a/Modem.h +++ b/Modem.h @@ -48,6 +48,7 @@ public: virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch); virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel); virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel); + virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost); virtual bool open(); @@ -202,6 +203,7 @@ private: bool m_fmCallsignAtEnd; bool m_fmCallsignAtLatch; std::string m_fmRfAck; + std::string m_fmExtAck; unsigned int m_fmAckSpeed; unsigned int m_fmAckFrequency; unsigned int m_fmAckMinTime; @@ -217,7 +219,9 @@ private: bool m_fmUseCOS; bool m_fmCOSInvert; unsigned int m_fmRFAudioBoost; + unsigned int m_fmExtAudioBoost; float m_fmMaxDevLevel; + bool m_fmExtEnable; bool readVersion(); bool readStatus(); @@ -226,6 +230,7 @@ private: bool setFMCallsignParams(); bool setFMAckParams(); bool setFMMiscParams(); + bool setFMExtParams(); void printDebug(); From c2ca65f8893df02e73bb831236d201694dcf0430 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 7 May 2020 22:03:57 +0100 Subject: [PATCH 003/163] Add more parameters to the external ack data. --- Modem.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Modem.cpp b/Modem.cpp index 7c8ec97..ec6bbd3 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -2215,16 +2215,20 @@ bool CModem::setFMExtParams() assert(m_serial != NULL); unsigned char buffer[80U]; - unsigned char len = 4U + m_fmExtAck.size(); + unsigned char len = 7U + m_fmExtAck.size(); buffer[0U] = MMDVM_FRAME_START; buffer[1U] = len; buffer[2U] = MMDVM_FM_PARAMS4; buffer[3U] = m_fmExtAudioBoost; + buffer[4U] = m_fmAckSpeed; + buffer[5U] = m_fmAckFrequency / 10U; + + buffer[6U] = (unsigned char)(m_fmAckLevel * 2.55F + 0.5F); for (unsigned int i = 0U; i < m_fmExtAck.size(); i++) - buffer[4U + i] = m_fmExtAck.at(i); + buffer[7U + i] = m_fmExtAck.at(i); // CUtils::dump(1U, "Written", buffer, len); From 51a1e2bb9cca182a115038576d34f469961d4597 Mon Sep 17 00:00:00 2001 From: phl0 Date: Fri, 8 May 2020 13:17:29 +0200 Subject: [PATCH 004/163] Fix FM network section detection in Conf.cpp --- Conf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Conf.cpp b/Conf.cpp index 071c717..ae77dd2 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -868,7 +868,7 @@ bool CConf::read() m_pocsagNetworkModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "Debug") == 0) m_pocsagNetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_POCSAG_NETWORK) { + } else if (section == SECTION_FM_NETWORK) { if (::strcmp(key, "Enable") == 0) m_fmNetworkEnabled = ::atoi(value) == 1; else if (::strcmp(key, "LocalAddress") == 0) From 7ae77a01a944a64aa35bc0e842d5c7c07f29b368 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Fri, 8 May 2020 13:12:37 +0100 Subject: [PATCH 005/163] Add the data packing and unpacking code. --- Conf.cpp | 2 +- FMControl.cpp | 128 +++++++++++++++++++++++++++++--------------------- FMControl.h | 18 ++----- MMDVMHost.cpp | 4 +- Version.h | 2 +- 5 files changed, 81 insertions(+), 73 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index 071c717..ae77dd2 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -868,7 +868,7 @@ bool CConf::read() m_pocsagNetworkModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "Debug") == 0) m_pocsagNetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_POCSAG_NETWORK) { + } else if (section == SECTION_FM_NETWORK) { if (::strcmp(key, "Enable") == 0) m_fmNetworkEnabled = ::atoi(value) == 1; else if (::strcmp(key, "LocalAddress") == 0) diff --git a/FMControl.cpp b/FMControl.cpp index f6d0df4..8e1a10c 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -19,13 +19,15 @@ #include "FMControl.h" #include -#include + +const uint8_t BIT_MASK_TABLE[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U }; + +#define WRITE_BIT(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) +#define READ_BIT(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) CFMControl::CFMControl(CFMNetwork* network) : m_network(network), -m_queue(3000U, "FM Control"), -m_enabled(false), -m_fp(NULL) +m_enabled(false) { assert(network != NULL); } @@ -34,32 +36,90 @@ CFMControl::~CFMControl() { } -bool CFMControl::writeModem(unsigned char* data, unsigned int len) +bool CFMControl::writeModem(const unsigned char* data, unsigned int length) { assert(data != NULL); - assert(len > 0U); + assert(length > 0U); + assert(m_network != NULL); - // Unpack serial data (12-bit unsigned values) + float samples[170U]; + unsigned int nSamples = 0U; - // De-emphasise the data + // Unpack the serial data into float values. + for (unsigned int i = 0U; i < length; i += 3U) { + unsigned short sample1 = 0U; + unsigned short sample2 = 0U; + unsigned short MASK = 0x0001U; - // Repack the data (16-bit unsigned values) + const unsigned char* base = data + i; + for (unsigned int j = 0U; j < 12U; j++, MASK <<= 1) { + unsigned int pos1 = j; + unsigned int pos2 = j + 12U; - return true; + bool b1 = READ_BIT(base, pos1) != 0U; + bool b2 = READ_BIT(base, pos2) != 0U; + + if (b1) + sample1 |= MASK; + if (b2) + sample2 |= MASK; + } + + // Convert from unsigned short (0 - +4095) to float (-1.0 - +1.0) + samples[nSamples++] = (float(sample1) - 2048.0F) / 2048.0F; + samples[nSamples++] = (float(sample2) - 2048.0F) / 2048.0F; + } + + // De-emphasise the data and any other processing needed (maybe a low-pass filter to remove the CTCSS) + + unsigned char out[350U]; + unsigned int nOut = 0U; + + // Repack the data (8-bit unsigned values containing unsigned 16-bit data) + for (unsigned int i = 0U; i < nSamples; i++) { + unsigned short sample = (unsigned short)((samples[i] + 1.0F) * 32767.0F + 0.5F); + out[nOut++] = (sample >> 8) & 0xFFU; + out[nOut++] = (sample >> 0) & 0xFFU; + } + + return m_network->write(out, nOut); } unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) { assert(data != NULL); assert(space > 0U); + assert(m_network != NULL); - // Unpack serial data (16-bit unsigned values) + unsigned char netData[300U]; + unsigned int length = m_network->read(netData, 270U); + if (length == 0U) + return 0U; - // Pre-emphasise the data + float samples[170U]; + unsigned int nSamples = 0U; - // Repack the data (12-bit unsigned values) + // Convert the unsigned 16-bit data (+65535 - 0) to float (+1.0 - -1.0) + for (unsigned int i = 0U; i < length; i += 2U) { + unsigned short sample = (netData[i + 0U] << 8) | netData[i + 1U]; + samples[nSamples++] = (float(sample) / 32767.0F) - 1.0F; + } - return 0U; + // Pre-emphasise the data and other stuff. + + // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) + unsigned int offset = 0U; + for (unsigned int i = 0U; i < nSamples; i++) { + unsigned short sample = (unsigned short)((samples[i] + 1.0F) * 2048.0F + 0.5F); + unsigned short MASK = 0x0001U; + for (unsigned int j = 0U; j < 12U; j++, MASK <<= 1) { + bool b = (sample & MASK) != 0U; + WRITE_BIT(data, offset, b); + offset++; + } + } + + return nSamples; } void CFMControl::clock(unsigned int ms) @@ -71,43 +131,3 @@ void CFMControl::enable(bool enabled) { // May not be needed } - -bool CFMControl::openFile() -{ - if (m_fp != NULL) - return true; - - time_t t; - ::time(&t); - - struct tm* tm = ::localtime(&t); - - char name[100U]; - ::sprintf(name, "FM_%04d%02d%02d_%02d%02d%02d.ambe", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); - - m_fp = ::fopen(name, "wb"); - if (m_fp == NULL) - return false; - - ::fwrite("FM", 1U, 2U, m_fp); - - return true; -} - -bool CFMControl::writeFile(const unsigned char* data, unsigned int length) -{ - if (m_fp == NULL) - return false; - - ::fwrite(data, 1U, length, m_fp); - - return true; -} - -void CFMControl::closeFile() -{ - if (m_fp != NULL) { - ::fclose(m_fp); - m_fp = NULL; - } -} diff --git a/FMControl.h b/FMControl.h index 177638e..bcae8a3 100644 --- a/FMControl.h +++ b/FMControl.h @@ -20,20 +20,14 @@ #define FMControl_H #include "FMNetwork.h" -#include "RingBuffer.h" -#include "StopWatch.h" #include "Defines.h" -#include "Timer.h" -#include "Modem.h" - -#include class CFMControl { public: CFMControl(CFMNetwork* network); ~CFMControl(); - bool writeModem(unsigned char* data, unsigned int length); + bool writeModem(const unsigned char* data, unsigned int length); unsigned int readModem(unsigned char* data, unsigned int space); @@ -42,14 +36,8 @@ public: void enable(bool enabled); private: - CFMNetwork* m_network; - CRingBuffer m_queue; - bool m_enabled; - FILE* m_fp; - - bool openFile(); - bool writeFile(const unsigned char* data, unsigned int length); - void closeFile(); + CFMNetwork* m_network; + bool m_enabled; }; #endif diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index bac53e0..c6f5137 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -811,11 +811,11 @@ int CMMDVMHost::run() } len = m_modem->readFMData(data); - if (m_nxdn != NULL && len > 0U) { + if (m_fm != NULL && len > 0U) { if (m_mode == MODE_IDLE) { bool ret = m_fm->writeModem(data, len); if (ret) { - m_modeTimer.setTimeout(m_nxdnRFModeHang); + m_modeTimer.setTimeout(m_nxdnRFModeHang); // XXX setMode(MODE_FM); } } else if (m_mode == MODE_FM) { diff --git a/Version.h b/Version.h index 7bf2d5a..b663e2a 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200506"; +const char* VERSION = "20200507"; #endif From 8cdc1ffe7f9a935efbfbe647f1c8f6c8e1b7a98a Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Fri, 8 May 2020 16:41:06 +0200 Subject: [PATCH 006/163] Pack and unpack using shifting and masking --- FMControl.cpp | 46 ++++++++++++++++++++++++---------------------- FMControl.h | 11 +++++++++++ 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index 8e1a10c..d5e7a14 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -49,21 +49,17 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) for (unsigned int i = 0U; i < length; i += 3U) { unsigned short sample1 = 0U; unsigned short sample2 = 0U; - unsigned short MASK = 0x0001U; + unsigned short MASK = ~(0xFFFFF000); - const unsigned char* base = data + i; - for (unsigned int j = 0U; j < 12U; j++, MASK <<= 1) { - unsigned int pos1 = j; - unsigned int pos2 = j + 12U; + int pack = 0; + char* packPointer = (char*)&pack; - bool b1 = READ_BIT(base, pos1) != 0U; - bool b2 = READ_BIT(base, pos2) != 0U; + packPointer[1] = data[i]; + packPointer[2] = data[i + 1]; + packPointer[3] = data[i + 2]; - if (b1) - sample1 |= MASK; - if (b2) - sample2 |= MASK; - } + sample2 = (short)(pack & MASK); + sample1 = (short)(pack >> 12); // Convert from unsigned short (0 - +4095) to float (-1.0 - +1.0) samples[nSamples++] = (float(sample1) - 2048.0F) / 2048.0F; @@ -108,18 +104,24 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) // Pre-emphasise the data and other stuff. // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) - unsigned int offset = 0U; - for (unsigned int i = 0U; i < nSamples; i++) { - unsigned short sample = (unsigned short)((samples[i] + 1.0F) * 2048.0F + 0.5F); - unsigned short MASK = 0x0001U; - for (unsigned int j = 0U; j < 12U; j++, MASK <<= 1) { - bool b = (sample & MASK) != 0U; - WRITE_BIT(data, offset, b); - offset++; - } + int pack = 0; + char* packPointer = (char*)&pack; + unsigned int j = 0U; + unsigned int i = 0U; + for (; i < nSamples && j < space; i += 2, j += 3) { + unsigned short sample1 = (unsigned short)((samples[i] + 1.0F) * 2048.0F + 0.5F); + unsigned short sample2 = (unsigned short)((samples[i + 1] + 1.0F) * 2048.0F + 0.5F); + + pack = 0; + pack = ((int)sample1) << 12; + pack |= sample2; + + data[j] = packPointer[1]; + data[j + 1] = packPointer[2]; + data[j + 2] = packPointer[3]; } - return nSamples; + return j;//return the number of bytes written } void CFMControl::clock(unsigned int ms) diff --git a/FMControl.h b/FMControl.h index bcae8a3..2d184e2 100644 --- a/FMControl.h +++ b/FMControl.h @@ -22,6 +22,17 @@ #include "FMNetwork.h" #include "Defines.h" +typedef struct +{ + union + { + int pack; + char packBytes[4]; + }; + +} SamplePack; + + class CFMControl { public: CFMControl(CFMNetwork* network); From 93652c764e928afd09cb6a7262dffd65ea0bc935 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Fri, 8 May 2020 17:05:04 +0200 Subject: [PATCH 007/163] Using correct types --- FMControl.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index d5e7a14..b2e6652 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -49,14 +49,14 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) for (unsigned int i = 0U; i < length; i += 3U) { unsigned short sample1 = 0U; unsigned short sample2 = 0U; - unsigned short MASK = ~(0xFFFFF000); + unsigned int MASK = 0x00000FFFU; - int pack = 0; - char* packPointer = (char*)&pack; + unsigned int pack = 0U; + unsigned char* packPointer = (unsigned char*)&pack; - packPointer[1] = data[i]; - packPointer[2] = data[i + 1]; - packPointer[3] = data[i + 2]; + packPointer[1U] = data[i]; + packPointer[2U] = data[i + 1U]; + packPointer[3U] = data[i + 2U]; sample2 = (short)(pack & MASK); sample1 = (short)(pack >> 12); @@ -104,21 +104,21 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) // Pre-emphasise the data and other stuff. // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) - int pack = 0; - char* packPointer = (char*)&pack; + unsigned int pack = 0U; + unsigned char* packPointer = (unsigned char*)&pack; unsigned int j = 0U; unsigned int i = 0U; - for (; i < nSamples && j < space; i += 2, j += 3) { + for (; i < nSamples && j < space; i += 2U, j += 3U) { unsigned short sample1 = (unsigned short)((samples[i] + 1.0F) * 2048.0F + 0.5F); unsigned short sample2 = (unsigned short)((samples[i + 1] + 1.0F) * 2048.0F + 0.5F); pack = 0; - pack = ((int)sample1) << 12; + pack = ((unsigned int)sample1) << 12; pack |= sample2; - data[j] = packPointer[1]; - data[j + 1] = packPointer[2]; - data[j + 2] = packPointer[3]; + data[j] = packPointer[1U]; + data[j + 1U] = packPointer[2U]; + data[j + 2U] = packPointer[3U]; } return j;//return the number of bytes written From c48f45d2da712d7742e14096daeec7ca206d9b5a Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Fri, 8 May 2020 17:30:22 +0200 Subject: [PATCH 008/163] remove union --- FMControl.h | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/FMControl.h b/FMControl.h index 2d184e2..bcae8a3 100644 --- a/FMControl.h +++ b/FMControl.h @@ -22,17 +22,6 @@ #include "FMNetwork.h" #include "Defines.h" -typedef struct -{ - union - { - int pack; - char packBytes[4]; - }; - -} SamplePack; - - class CFMControl { public: CFMControl(CFMNetwork* network); From dc9e6ade235a32f665ffc455e7c80fcaca5c84c6 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Fri, 8 May 2020 20:41:57 +0200 Subject: [PATCH 009/163] Add filter --- IIDirectForm1Filter.cpp | 56 +++++++++++++++++++++++++++++++++++++++++ IIRDirectForm1Filter.h | 50 ++++++++++++++++++++++++++++++++++++ Makefile.Pi | 2 +- Makefile.Pi.HD44780 | 2 +- Makefile.Pi.OLED | 2 +- Makefile.Pi.PCF8574 | 2 +- Makefile.Solaris | 2 +- 7 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 IIDirectForm1Filter.cpp create mode 100644 IIRDirectForm1Filter.h diff --git a/IIDirectForm1Filter.cpp b/IIDirectForm1Filter.cpp new file mode 100644 index 0000000..51e4e13 --- /dev/null +++ b/IIDirectForm1Filter.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015-2020 by Jonathan Naylor G4KLX + * Copyright (C) 2020 by Geoffrey Merck - F4FXL KC3FRA + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "IIRDirectForm1Filter.h" +#include "math.h" + +CIIRDirectForm1Filter::CIIRDirectForm1Filter(float b0, float b1, float b2, float , float a1, float a2, float addtionalGaindB) : +m_b0(b0), +m_b1(b1), +m_b2(b2), +m_a1(a1), +m_a2(a2), +m_additionalGainLin(::powf(10.0F, addtionalGaindB / 20.0F)) +{ + +} + +float CIIRDirectForm1Filter::filter(float sample) +{ + float output = m_b0 * sample + + m_b1 * m_x1 + + m_b2 * m_x2 + - m_a1 * m_y1 + - m_a2 * m_y2; + + m_x2 = m_x1; + m_y2 = m_y1; + m_x1 = sample; + m_y1 = output; + + return output * m_additionalGainLin; +} + +void CIIRDirectForm1Filter::reset() +{ + m_x1 = 0.0f; + m_x2 = 0.0f; + m_y1 = 0.0f; + m_y2 = 0.0f; +} diff --git a/IIRDirectForm1Filter.h b/IIRDirectForm1Filter.h new file mode 100644 index 0000000..f575f7f --- /dev/null +++ b/IIRDirectForm1Filter.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015-2020 by Jonathan Naylor G4KLX + * Copyright (C) 2020 by Geoffrey Merck - F4FXL KC3FRA + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(IIRDIRECTFORM1FILTER_H) +#define IIRDIRECTFORM1FILTER_H + +class CIIRDirectForm1Filter +{ +public: + CIIRDirectForm1Filter(float b0, float b1, float b2, float, float a1, float a2, float additionalGaindB); + float filter(float sample); + void reset(); + +private: +// delay line + float m_x2; // x[n-2] + float m_y2; // y[n-2] + float m_x1; // x[n-1] + float m_y1; // y[n-1] + + // coefficients + // FIR + float m_b0; + float m_b1; + float m_b2; + // IIR + float m_a1; + float m_a2; + + float m_additionalGainLin; +}; + + +#endif \ No newline at end of file diff --git a/Makefile.Pi b/Makefile.Pi index 7d7e1f1..06ef859 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -9,7 +9,7 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index 5c875f6..9c42ca8 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -9,7 +9,7 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o IIDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index d4288cc..63be413 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -9,7 +9,7 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o OLED.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIDirectForm1Filter.o OLED.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index 53ecc80..4c89696 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -10,7 +10,7 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o IIDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ diff --git a/Makefile.Solaris b/Makefile.Solaris index 8c6ac68..ffe9564 100644 --- a/Makefile.Solaris +++ b/Makefile.Solaris @@ -9,7 +9,7 @@ LDFLAGS = -g OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o IIDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o \ NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o \ From bc22709abb3b3131a7484a9d4afd4765105a537c Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Fri, 8 May 2020 21:16:02 +0200 Subject: [PATCH 010/163] Add preemphasis and deemphasis --- FMControl.cpp | 15 ++++++++++----- FMControl.h | 3 +++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index b2e6652..e3836d7 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -20,14 +20,13 @@ #include -const uint8_t BIT_MASK_TABLE[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U }; - -#define WRITE_BIT(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) -#define READ_BIT(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) +#define EMPHASIS_GAIN_DB 0 //Gain needs to be the same for pre an deeemphasis CFMControl::CFMControl(CFMNetwork* network) : m_network(network), -m_enabled(false) +m_enabled(false), +m_preemphasis(0.3889703087993727F, -0.3290005228984741F, 0.0F, 1.0F, 0.282029168302153F, 0.0F, EMPHASIS_GAIN_DB), +m_deemphasis(1.0F, 0.282029168302153F, 0.0F, 0.3889703087993727F, -0.3290005228984741F, 0.0F, EMPHASIS_GAIN_DB) { assert(network != NULL); } @@ -67,6 +66,9 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) } // De-emphasise the data and any other processing needed (maybe a low-pass filter to remove the CTCSS) + for(unsigned int i = 0U; i < nSamples; i++) { + samples[i] = m_deemphasis.filter(samples[i]); + } unsigned char out[350U]; unsigned int nOut = 0U; @@ -102,6 +104,9 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) } // Pre-emphasise the data and other stuff. + for(unsigned int i = 0U; i < nSamples; i++) { + samples[i] = m_preemphasis.filter(samples[i]); + } // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) unsigned int pack = 0U; diff --git a/FMControl.h b/FMControl.h index bcae8a3..941b347 100644 --- a/FMControl.h +++ b/FMControl.h @@ -21,6 +21,7 @@ #include "FMNetwork.h" #include "Defines.h" +#include "IIRDirectForm1Filter.h" class CFMControl { public: @@ -38,6 +39,8 @@ public: private: CFMNetwork* m_network; bool m_enabled; + CIIRDirectForm1Filter m_preemphasis; + CIIRDirectForm1Filter m_deemphasis; }; #endif From e28dfe79dd27f672e3d1d82adb673e95846a06ff Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Fri, 8 May 2020 21:23:32 +0200 Subject: [PATCH 011/163] Fix typo in file name --- IIDirectForm1Filter.cpp => IIRDirectForm1Filter.cpp | 0 Makefile | 2 +- Makefile.Pi | 2 +- Makefile.Pi.Adafruit | 2 +- Makefile.Pi.OLED | 2 +- Makefile.Pi.PCF8574 | 2 +- Makefile.Solaris | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename IIDirectForm1Filter.cpp => IIRDirectForm1Filter.cpp (100%) diff --git a/IIDirectForm1Filter.cpp b/IIRDirectForm1Filter.cpp similarity index 100% rename from IIDirectForm1Filter.cpp rename to IIRDirectForm1Filter.cpp diff --git a/Makefile b/Makefile index 716a8f6..2091355 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ LDFLAGS = -g OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ diff --git a/Makefile.Pi b/Makefile.Pi index 06ef859..a7d416c 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -9,7 +9,7 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 5009315..09125e3 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -10,7 +10,7 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index 63be413..2ea260c 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -9,7 +9,7 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIDirectForm1Filter.o OLED.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIRDirectForm1Filter.o OLED.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index 4c89696..1513f45 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -10,7 +10,7 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o IIDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ diff --git a/Makefile.Solaris b/Makefile.Solaris index ffe9564..ec34917 100644 --- a/Makefile.Solaris +++ b/Makefile.Solaris @@ -9,7 +9,7 @@ LDFLAGS = -g OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o IIDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ + DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o \ NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o \ From 467140dbf863fc37fa1779705206cfc11c1c49f8 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 9 May 2020 12:41:37 +0100 Subject: [PATCH 012/163] VS2019 fixes. --- FMControl.cpp | 6 ++---- MMDVMHost.vcxproj | 2 ++ MMDVMHost.vcxproj.filters | 6 ++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index e3836d7..68aeab8 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -66,9 +66,8 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) } // De-emphasise the data and any other processing needed (maybe a low-pass filter to remove the CTCSS) - for(unsigned int i = 0U; i < nSamples; i++) { + for (unsigned int i = 0U; i < nSamples; i++) samples[i] = m_deemphasis.filter(samples[i]); - } unsigned char out[350U]; unsigned int nOut = 0U; @@ -104,9 +103,8 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) } // Pre-emphasise the data and other stuff. - for(unsigned int i = 0U; i < nSamples; i++) { + for (unsigned int i = 0U; i < nSamples; i++) samples[i] = m_preemphasis.filter(samples[i]); - } // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) unsigned int pack = 0U; diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index cb65730..63ba164 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -188,6 +188,7 @@ + @@ -285,6 +286,7 @@ + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index 4c0b48f..8dc6f12 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -305,6 +305,9 @@ Header Files + + Header Files + @@ -574,5 +577,8 @@ Source Files + + Source Files + \ No newline at end of file From 33fedb781d909efa581fed6b96f84dcf3e34d9af Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 10 May 2020 10:31:53 +0200 Subject: [PATCH 013/163] Handle no network --- FMControl.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index 68aeab8..fa81728 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -28,7 +28,6 @@ m_enabled(false), m_preemphasis(0.3889703087993727F, -0.3290005228984741F, 0.0F, 1.0F, 0.282029168302153F, 0.0F, EMPHASIS_GAIN_DB), m_deemphasis(1.0F, 0.282029168302153F, 0.0F, 0.3889703087993727F, -0.3290005228984741F, 0.0F, EMPHASIS_GAIN_DB) { - assert(network != NULL); } CFMControl::~CFMControl() @@ -39,7 +38,9 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) { assert(data != NULL); assert(length > 0U); - assert(m_network != NULL); + + if(m_network == NULL) + return false; float samples[170U]; unsigned int nSamples = 0U; @@ -86,7 +87,8 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) { assert(data != NULL); assert(space > 0U); - assert(m_network != NULL); + if(m_network == NULL) + return 0; unsigned char netData[300U]; unsigned int length = m_network->read(netData, 270U); From 0178ba0aba9cfe72138a0fd8de2a046ddf86db93 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 10 May 2020 21:49:11 +0200 Subject: [PATCH 014/163] Buffer incoming modem audio --- FMControl.cpp | 82 ++++++++++++++++++++++++++++++--------------------- FMControl.h | 5 ++-- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index fa81728..45310ce 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -21,12 +21,14 @@ #include #define EMPHASIS_GAIN_DB 0 //Gain needs to be the same for pre an deeemphasis +#define RF_AUDIO_SAMP_RATE 8000 CFMControl::CFMControl(CFMNetwork* network) : m_network(network), m_enabled(false), -m_preemphasis(0.3889703087993727F, -0.3290005228984741F, 0.0F, 1.0F, 0.282029168302153F, 0.0F, EMPHASIS_GAIN_DB), -m_deemphasis(1.0F, 0.282029168302153F, 0.0F, 0.3889703087993727F, -0.3290005228984741F, 0.0F, EMPHASIS_GAIN_DB) +// m_preemphasis(0.3889703087993727F, -0.3290005228984741F, 0.0F, 1.0F, 0.282029168302153F, 0.0F, EMPHASIS_GAIN_DB), +// m_deemphasis(1.0F, 0.282029168302153F, 0.0F, 0.3889703087993727F, -0.3290005228984741F, 0.0F, EMPHASIS_GAIN_DB), +m_incomingRFAudio(1000U, "Incoming RF FM Audio") { } @@ -45,42 +47,54 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) float samples[170U]; unsigned int nSamples = 0U; - // Unpack the serial data into float values. - for (unsigned int i = 0U; i < length; i += 3U) { - unsigned short sample1 = 0U; - unsigned short sample2 = 0U; - unsigned int MASK = 0x00000FFFU; + + m_incomingRFAudio.addData(data, length); + unsigned int bufferLength = m_incomingRFAudio.dataSize(); - unsigned int pack = 0U; - unsigned char* packPointer = (unsigned char*)&pack; + if(bufferLength >= 3) { + bufferLength = bufferLength - bufferLength % 3; //round down to nearest multiple of 3 + unsigned char bufferData[bufferLength]; + m_incomingRFAudio.getData(bufferData, bufferLength); - packPointer[1U] = data[i]; - packPointer[2U] = data[i + 1U]; - packPointer[3U] = data[i + 2U]; + // Unpack the serial data into float values. + for (unsigned int i = 0U; i < length; i += 3U) { + unsigned short sample1 = 0U; + unsigned short sample2 = 0U; + unsigned int MASK = 0x00000FFFU; - sample2 = (short)(pack & MASK); - sample1 = (short)(pack >> 12); + unsigned int pack = 0U; + unsigned char* packPointer = (unsigned char*)&pack; - // Convert from unsigned short (0 - +4095) to float (-1.0 - +1.0) - samples[nSamples++] = (float(sample1) - 2048.0F) / 2048.0F; - samples[nSamples++] = (float(sample2) - 2048.0F) / 2048.0F; + packPointer[1U] = bufferData[i]; + packPointer[2U] = bufferData[i + 1U]; + packPointer[3U] = bufferData[i + 2U]; + + sample2 = (short)(pack & MASK); + sample1 = (short)(pack >> 12); + + // Convert from unsigned short (0 - +4095) to float (-1.0 - +1.0) + samples[nSamples++] = (float(sample1) - 2048.0F) / 2048.0F; + samples[nSamples++] = (float(sample2) - 2048.0F) / 2048.0F; + } + + // De-emphasise the data and any other processing needed (maybe a low-pass filter to remove the CTCSS) + // for (unsigned int i = 0U; i < nSamples; i++) + // samples[i] = m_deemphasis.filter(samples[i]); + + unsigned char out[350U]; + unsigned int nOut = 0U; + + // Repack the data (8-bit unsigned values containing unsigned 16-bit data) + for (unsigned int i = 0U; i < nSamples; i++) { + unsigned short sample = (unsigned short)((samples[i] + 1.0F) * 32767.0F + 0.5F); + out[nOut++] = (sample >> 8) & 0xFFU; + out[nOut++] = (sample >> 0) & 0xFFU; + } + + return m_network->write(out, nOut); } - // De-emphasise the data and any other processing needed (maybe a low-pass filter to remove the CTCSS) - for (unsigned int i = 0U; i < nSamples; i++) - samples[i] = m_deemphasis.filter(samples[i]); - - unsigned char out[350U]; - unsigned int nOut = 0U; - - // Repack the data (8-bit unsigned values containing unsigned 16-bit data) - for (unsigned int i = 0U; i < nSamples; i++) { - unsigned short sample = (unsigned short)((samples[i] + 1.0F) * 32767.0F + 0.5F); - out[nOut++] = (sample >> 8) & 0xFFU; - out[nOut++] = (sample >> 0) & 0xFFU; - } - - return m_network->write(out, nOut); + return 0U; } unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) @@ -105,8 +119,8 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) } // Pre-emphasise the data and other stuff. - for (unsigned int i = 0U; i < nSamples; i++) - samples[i] = m_preemphasis.filter(samples[i]); + // for (unsigned int i = 0U; i < nSamples; i++) + // samples[i] = m_preemphasis.filter(samples[i]); // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) unsigned int pack = 0U; diff --git a/FMControl.h b/FMControl.h index 941b347..3a95cc4 100644 --- a/FMControl.h +++ b/FMControl.h @@ -39,8 +39,9 @@ public: private: CFMNetwork* m_network; bool m_enabled; - CIIRDirectForm1Filter m_preemphasis; - CIIRDirectForm1Filter m_deemphasis; + // CIIRDirectForm1Filter m_preemphasis; + // CIIRDirectForm1Filter m_deemphasis; + CRingBuffer m_incomingRFAudio; }; #endif From 6b4fe7dd3350d3ba9857372a469d20bc029ba07a Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 10 May 2020 22:08:36 +0200 Subject: [PATCH 015/163] Loop using correct length --- FMControl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FMControl.cpp b/FMControl.cpp index 45310ce..4a04e72 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -57,7 +57,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) m_incomingRFAudio.getData(bufferData, bufferLength); // Unpack the serial data into float values. - for (unsigned int i = 0U; i < length; i += 3U) { + for (unsigned int i = 0U; i < bufferLength; i += 3U) { unsigned short sample1 = 0U; unsigned short sample2 = 0U; unsigned int MASK = 0x00000FFFU; From 29b36a66f869d11bd2eea5ef4ba39cd8fb2e06ec Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 11 May 2020 12:59:28 +0100 Subject: [PATCH 016/163] Use more conventional handling of FM mode timing. --- Conf.cpp | 16 ++++++++++++---- Conf.h | 2 ++ FMControl.cpp | 26 ++++++++++++++++---------- MMDVM.ini | 1 + MMDVMHost.cpp | 16 ++++++++-------- MMDVMHost.h | 1 + Modem.cpp | 19 ++++++++++++++++--- 7 files changed, 56 insertions(+), 25 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index ae77dd2..fd0f12d 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -203,6 +203,7 @@ m_fmCOSInvert(false), m_fmRFAudioBoost(1U), m_fmMaxDevLevel(90.0F), m_fmExtAudioBoost(1U), +m_fmModeHang(10U), m_dstarNetworkEnabled(false), m_dstarGatewayAddress(), m_dstarGatewayPort(0U), @@ -411,12 +412,12 @@ bool CConf::read() else if (::strcmp(key, "Duplex") == 0) m_duplex = ::atoi(value) == 1; else if (::strcmp(key, "ModeHang") == 0) - m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = - m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = (unsigned int)::atoi(value); + m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = m_nxdnNetworkModeHang = m_pocsagNetworkModeHang = m_fmNetworkModeHang = + m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = m_nxdnModeHang = m_fmModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "RFModeHang") == 0) - m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = (unsigned int)::atoi(value); + m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = m_nxdnModeHang = m_fmModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "NetModeHang") == 0) - m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = (unsigned int)::atoi(value); + m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = m_nxdnNetworkModeHang = m_pocsagNetworkModeHang = m_fmNetworkModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "Display") == 0) m_display = value; else if (::strcmp(key, "Daemon") == 0) @@ -774,6 +775,8 @@ bool CConf::read() m_fmMaxDevLevel = float(::atof(value)); else if (::strcmp(key, "ExtAudioBoost") == 0) m_fmExtAudioBoost = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_fmModeHang = (unsigned int)::atoi(value); } else if (section == SECTION_DSTAR_NETWORK) { if (::strcmp(key, "Enable") == 0) m_dstarNetworkEnabled = ::atoi(value) == 1; @@ -1676,6 +1679,11 @@ unsigned int CConf::getFMExtAudioBoost() const return m_fmExtAudioBoost; } +unsigned int CConf::getFMModeHang() const +{ + return m_fmModeHang; +} + bool CConf::getDStarNetworkEnabled() const { return m_dstarNetworkEnabled; diff --git a/Conf.h b/Conf.h index 915a21c..5c03afd 100644 --- a/Conf.h +++ b/Conf.h @@ -200,6 +200,7 @@ public: unsigned int getFMRFAudioBoost() const; float getFMMaxDevLevel() const; unsigned int getFMExtAudioBoost() const; + unsigned int getFMModeHang() const; // The D-Star Network section bool getDStarNetworkEnabled() const; @@ -476,6 +477,7 @@ private: unsigned int m_fmRFAudioBoost; float m_fmMaxDevLevel; unsigned int m_fmExtAudioBoost; + unsigned int m_fmModeHang; bool m_dstarNetworkEnabled; std::string m_dstarGatewayAddress; diff --git a/FMControl.cpp b/FMControl.cpp index 45310ce..c1d3fd0 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -41,18 +41,23 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) assert(data != NULL); assert(length > 0U); - if(m_network == NULL) + if (data[0U] == TAG_HEADER) + return true; + + if (data[0U] != TAG_DATA) return false; + if (m_network == NULL) + return true; + float samples[170U]; unsigned int nSamples = 0U; - - m_incomingRFAudio.addData(data, length); + m_incomingRFAudio.addData(data + 1U, length - 1U); unsigned int bufferLength = m_incomingRFAudio.dataSize(); - if(bufferLength >= 3) { - bufferLength = bufferLength - bufferLength % 3; //round down to nearest multiple of 3 + if (bufferLength >= 3U) { + bufferLength = bufferLength - bufferLength % 3U; //round down to nearest multiple of 3 unsigned char bufferData[bufferLength]; m_incomingRFAudio.getData(bufferData, bufferLength); @@ -69,8 +74,8 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) packPointer[2U] = bufferData[i + 1U]; packPointer[3U] = bufferData[i + 2U]; - sample2 = (short)(pack & MASK); - sample1 = (short)(pack >> 12); + sample2 = short(pack & MASK); + sample1 = short(pack >> 12); // Convert from unsigned short (0 - +4095) to float (-1.0 - +1.0) samples[nSamples++] = (float(sample1) - 2048.0F) / 2048.0F; @@ -94,15 +99,16 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) return m_network->write(out, nOut); } - return 0U; + return true; } unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) { assert(data != NULL); assert(space > 0U); - if(m_network == NULL) - return 0; + + if (m_network == NULL) + return 0U; unsigned char netData[300U]; unsigned int length = m_network->read(netData, 270U); diff --git a/MMDVM.ini b/MMDVM.ini index a86bdd3..f55ad04 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -170,6 +170,7 @@ COSInvert=0 RFAudioBoost=1 MaxDevLevel=90 ExtAudioBoost=1 +# ModeHang=10 [D-Star Network] Enable=1 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 7740d1b..c8914ba 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -135,6 +135,7 @@ m_dmrRFModeHang(10U), m_ysfRFModeHang(10U), m_p25RFModeHang(10U), m_nxdnRFModeHang(10U), +m_fmRFModeHang(10U), m_dstarNetModeHang(3U), m_dmrNetModeHang(3U), m_ysfNetModeHang(3U), @@ -618,8 +619,11 @@ int CMMDVMHost::run() pocsagTimer.start(); } - if (m_fmEnabled) + if (m_fmEnabled) { + m_fmRFModeHang = m_conf.getFMModeHang(); + m_fm = new CFMControl(m_fmNetwork); + } bool remoteControlEnabled = m_conf.getRemoteControlEnabled(); if (remoteControlEnabled) { @@ -658,12 +662,6 @@ int CMMDVMHost::run() else if (!error && m_mode == MODE_ERROR) setMode(MODE_IDLE); - unsigned char mode = m_modem->getMode(); - if (mode == MODE_FM && m_mode != MODE_FM) - setMode(mode); - else if (mode != MODE_FM && m_mode == MODE_FM) - setMode(mode); - if (m_ump != NULL) { bool tx = m_modem->hasTX(); m_ump->setTX(tx); @@ -818,7 +816,7 @@ int CMMDVMHost::run() if (m_mode == MODE_IDLE) { bool ret = m_fm->writeModem(data, len); if (ret) { - m_modeTimer.setTimeout(m_nxdnRFModeHang); // XXX + m_modeTimer.setTimeout(m_fmRFModeHang); setMode(MODE_FM); } } else if (m_mode == MODE_FM) { @@ -1293,6 +1291,7 @@ bool CMMDVMHost::createModem() bool cosInvert = m_conf.getFMCOSInvert(); unsigned int rfAudioBoost = m_conf.getFMRFAudioBoost(); float maxDevLevel = m_conf.getFMMaxDevLevel(); + unsigned int modeHangTime = m_conf.getFMModeHang(); LogInfo("FM Parameters"); LogInfo(" Callsign: %s", callsign.c_str()); @@ -1322,6 +1321,7 @@ bool CMMDVMHost::createModem() LogInfo(" COS Invert: %s", cosInvert ? "yes" : "no"); LogInfo(" RF Audio Boost: x%u", rfAudioBoost); LogInfo(" Max. Deviation Level: %.1f%%", maxDevLevel); + LogInfo(" Mode Hang: %us", modeHangTime); m_modem->setFMCallsignParams(callsign, callsignSpeed, callsignFrequency, callsignTime, callsignHoldoff, callsignHighLevel, callsignLowLevel, callsignAtStart, callsignAtEnd, callsignAtLatch); m_modem->setFMAckParams(rfAck, ackSpeed, ackFrequency, ackMinTime, ackDelay, ackLevel); diff --git a/MMDVMHost.h b/MMDVMHost.h index ea268c6..110f268 100644 --- a/MMDVMHost.h +++ b/MMDVMHost.h @@ -80,6 +80,7 @@ private: unsigned int m_ysfRFModeHang; unsigned int m_p25RFModeHang; unsigned int m_nxdnRFModeHang; + unsigned int m_fmRFModeHang; unsigned int m_dstarNetModeHang; unsigned int m_dmrNetModeHang; unsigned int m_ysfNetModeHang; diff --git a/Modem.cpp b/Modem.cpp index ec6bbd3..a65c4fa 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -81,6 +81,7 @@ const unsigned char MMDVM_FM_PARAMS2 = 0x61U; const unsigned char MMDVM_FM_PARAMS3 = 0x62U; const unsigned char MMDVM_FM_PARAMS4 = 0x63U; const unsigned char MMDVM_FM_DATA = 0x65U; +const unsigned char MMDVM_FM_CONTROL = 0x66U; const unsigned char MMDVM_ACK = 0x70U; const unsigned char MMDVM_NAK = 0x7FU; @@ -604,6 +605,20 @@ void CModem::clock(unsigned int ms) } break; + case MMDVM_FM_CONTROL: { + if (m_trace) + CUtils::dump(1U, "RX FM Control", m_buffer, m_length); + + unsigned char data = m_length - 2U; + m_rxFMData.addData(&data, 1U); + + data = TAG_HEADER; + m_rxFMData.addData(&data, 1U); + + m_rxFMData.addData(m_buffer + 3U, m_length - 3U); + } + break; + case MMDVM_GET_STATUS: { // if (m_trace) // CUtils::dump(1U, "GET_STATUS", m_buffer, m_length); @@ -1236,7 +1251,7 @@ bool CModem::writePOCSAGData(const unsigned char* data, unsigned int length) unsigned int CModem::getFMSpace() const { - return (m_txFMData.freeSpace() * 2U) / 3U; + return m_txFMData.freeSpace(); } bool CModem::writeFMData(const unsigned char* data, unsigned int length) @@ -1244,8 +1259,6 @@ bool CModem::writeFMData(const unsigned char* data, unsigned int length) assert(data != NULL); assert(length > 0U); - length = (length * 2U) / 3U; - if (length > 252U) return false; From 7bca8578521db2aa39961e7240397b37d2f7d259 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 10 May 2020 22:08:36 +0200 Subject: [PATCH 017/163] Loop using correct length --- FMControl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FMControl.cpp b/FMControl.cpp index c1d3fd0..a3a7528 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -62,7 +62,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) m_incomingRFAudio.getData(bufferData, bufferLength); // Unpack the serial data into float values. - for (unsigned int i = 0U; i < length; i += 3U) { + for (unsigned int i = 0U; i < bufferLength; i += 3U) { unsigned short sample1 = 0U; unsigned short sample2 = 0U; unsigned int MASK = 0x00000FFFU; From 039ef44a635956865eac8166ae51633f3267a439 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 11 May 2020 15:32:39 +0100 Subject: [PATCH 018/163] Fix the setMode(MODE_FM) command. --- MMDVMHost.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index c8914ba..15e12f5 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -1894,7 +1894,7 @@ void CMMDVMHost::setMode(unsigned char mode) if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); if (m_fmNetwork != NULL) - m_fmNetwork->enable(false); + m_fmNetwork->enable(true); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1909,15 +1909,12 @@ void CMMDVMHost::setMode(unsigned char mode) m_pocsag->enable(false); if (m_fm != NULL) m_fm->enable(true); - if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { - m_modem->writeDMRStart(false); - m_dmrTXTimer.stop(); - } + m_modem->setMode(MODE_FM); if (m_ump != NULL) m_ump->setMode(MODE_FM); m_display->setFM(); m_mode = MODE_FM; - m_modeTimer.stop(); + m_modeTimer.start(); m_cwIdTimer.stop(); createLockFile("FM"); break; From bc6f832b7d4fd5a4ac228f974ea7a02880301157 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 10 May 2020 22:08:36 +0200 Subject: [PATCH 019/163] Loop using correct length --- FMControl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FMControl.cpp b/FMControl.cpp index c1d3fd0..a3a7528 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -62,7 +62,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) m_incomingRFAudio.getData(bufferData, bufferLength); // Unpack the serial data into float values. - for (unsigned int i = 0U; i < length; i += 3U) { + for (unsigned int i = 0U; i < bufferLength; i += 3U) { unsigned short sample1 = 0U; unsigned short sample2 = 0U; unsigned int MASK = 0x00000FFFU; From cbcbe4c56a4579039fe705dd75d1c91e467f9771 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Mon, 11 May 2020 18:38:07 +0200 Subject: [PATCH 020/163] Used fixed length array --- FMControl.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/FMControl.cpp b/FMControl.cpp index a3a7528..bc1e0b0 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -22,6 +22,7 @@ #define EMPHASIS_GAIN_DB 0 //Gain needs to be the same for pre an deeemphasis #define RF_AUDIO_SAMP_RATE 8000 +#define FM_AUDIO_BLOCK_SIZE 240 CFMControl::CFMControl(CFMNetwork* network) : m_network(network), @@ -55,10 +56,12 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) m_incomingRFAudio.addData(data + 1U, length - 1U); unsigned int bufferLength = m_incomingRFAudio.dataSize(); + if(bufferLength > 255U) + bufferLength = 255U; if (bufferLength >= 3U) { bufferLength = bufferLength - bufferLength % 3U; //round down to nearest multiple of 3 - unsigned char bufferData[bufferLength]; + unsigned char bufferData[255]; m_incomingRFAudio.getData(bufferData, bufferLength); // Unpack the serial data into float values. From c413c3a855343112261494b60f35ef2a222907b1 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Thu, 14 May 2020 22:01:10 +0200 Subject: [PATCH 021/163] Tighten code, reactivate emphasis --- FMControl.cpp | 48 ++++++++++++++++++++++++------------------------ FMControl.h | 4 ++-- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index bc1e0b0..0041797 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -20,16 +20,15 @@ #include -#define EMPHASIS_GAIN_DB 0 //Gain needs to be the same for pre an deeemphasis -#define RF_AUDIO_SAMP_RATE 8000 -#define FM_AUDIO_BLOCK_SIZE 240 +const float EMPHASIS_GAIN_DB = 0.0F; //Gain needs to be the same for pre an deeemphasis +const unsigned int FM_MASK = 0x00000FFFU; CFMControl::CFMControl(CFMNetwork* network) : m_network(network), m_enabled(false), -// m_preemphasis(0.3889703087993727F, -0.3290005228984741F, 0.0F, 1.0F, 0.282029168302153F, 0.0F, EMPHASIS_GAIN_DB), -// m_deemphasis(1.0F, 0.282029168302153F, 0.0F, 0.3889703087993727F, -0.3290005228984741F, 0.0F, EMPHASIS_GAIN_DB), -m_incomingRFAudio(1000U, "Incoming RF FM Audio") +m_incomingRFAudio(1600U, "Incoming RF FM Audio"), +m_preemphasis(0.3889703155F, -0.32900055326F, 0.0F, 1.0F, 0.2820291817F, 0.0F, EMPHASIS_GAIN_DB), +m_deemphasis(1.0F, 0.2820291817F, 0.0F, 0.3889703155F, -0.32900055326F, 0.0F, EMPHASIS_GAIN_DB) { } @@ -50,9 +49,6 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) if (m_network == NULL) return true; - - float samples[170U]; - unsigned int nSamples = 0U; m_incomingRFAudio.addData(data + 1U, length - 1U); unsigned int bufferLength = m_incomingRFAudio.dataSize(); @@ -61,14 +57,16 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) if (bufferLength >= 3U) { bufferLength = bufferLength - bufferLength % 3U; //round down to nearest multiple of 3 - unsigned char bufferData[255]; + unsigned char bufferData[255U]; m_incomingRFAudio.getData(bufferData, bufferLength); + + unsigned int nSamples = 0; + float samples[85U]; // 255 / 3; // Unpack the serial data into float values. for (unsigned int i = 0U; i < bufferLength; i += 3U) { unsigned short sample1 = 0U; unsigned short sample2 = 0U; - unsigned int MASK = 0x00000FFFU; unsigned int pack = 0U; unsigned char* packPointer = (unsigned char*)&pack; @@ -77,7 +75,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) packPointer[2U] = bufferData[i + 1U]; packPointer[3U] = bufferData[i + 2U]; - sample2 = short(pack & MASK); + sample2 = short(pack & FM_MASK); sample1 = short(pack >> 12); // Convert from unsigned short (0 - +4095) to float (-1.0 - +1.0) @@ -85,11 +83,11 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) samples[nSamples++] = (float(sample2) - 2048.0F) / 2048.0F; } - // De-emphasise the data and any other processing needed (maybe a low-pass filter to remove the CTCSS) - // for (unsigned int i = 0U; i < nSamples; i++) - // samples[i] = m_deemphasis.filter(samples[i]); + //De-emphasise the data and any other processing needed (maybe a low-pass filter to remove the CTCSS) + for (unsigned int i = 0U; i < nSamples; i++) + samples[i] = m_deemphasis.filter(samples[i]); - unsigned char out[350U]; + unsigned short out[170U]; // 85 * 2 unsigned int nOut = 0U; // Repack the data (8-bit unsigned values containing unsigned 16-bit data) @@ -99,7 +97,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) out[nOut++] = (sample >> 0) & 0xFFU; } - return m_network->write(out, nOut); + return m_network->write((unsigned char*)out, nOut); } return true; @@ -113,23 +111,25 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) if (m_network == NULL) return 0U; - unsigned char netData[300U]; - unsigned int length = m_network->read(netData, 270U); + if(space > 252U) + space = 252U; + + unsigned char netData[168U];//84 * 2 modem can handle up to 84 samples (252 bytes) at a time + unsigned int length = m_network->read(netData, 168U); if (length == 0U) return 0U; - float samples[170U]; + float samples[84U]; unsigned int nSamples = 0U; - // Convert the unsigned 16-bit data (+65535 - 0) to float (+1.0 - -1.0) for (unsigned int i = 0U; i < length; i += 2U) { unsigned short sample = (netData[i + 0U] << 8) | netData[i + 1U]; samples[nSamples++] = (float(sample) / 32767.0F) - 1.0F; } - // Pre-emphasise the data and other stuff. - // for (unsigned int i = 0U; i < nSamples; i++) - // samples[i] = m_preemphasis.filter(samples[i]); + //Pre-emphasise the data and other stuff. + for (unsigned int i = 0U; i < nSamples; i++) + samples[i] = m_preemphasis.filter(samples[i]); // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) unsigned int pack = 0U; diff --git a/FMControl.h b/FMControl.h index 3a95cc4..5a4a8cd 100644 --- a/FMControl.h +++ b/FMControl.h @@ -39,9 +39,9 @@ public: private: CFMNetwork* m_network; bool m_enabled; - // CIIRDirectForm1Filter m_preemphasis; - // CIIRDirectForm1Filter m_deemphasis; CRingBuffer m_incomingRFAudio; + CIIRDirectForm1Filter m_preemphasis; + CIIRDirectForm1Filter m_deemphasis; }; #endif From 2165b38379315b2ec55308d32a1e86352f814683 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Thu, 14 May 2020 22:01:38 +0200 Subject: [PATCH 022/163] Make sur we always return even length --- FMNetwork.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/FMNetwork.cpp b/FMNetwork.cpp index 91cd1c9..cfe6105 100644 --- a/FMNetwork.cpp +++ b/FMNetwork.cpp @@ -107,6 +107,7 @@ unsigned int CFMNetwork::read(unsigned char* data, unsigned int space) { assert(data != NULL); + unsigned int bytes = m_buffer.dataSize(); if (bytes == 0U) return 0U; @@ -114,6 +115,10 @@ unsigned int CFMNetwork::read(unsigned char* data, unsigned int space) if (bytes < space) space = bytes; + //we store usignedshorts, therefore ensure we always return and even number of data + if(space > 0 && space % 2 != 0) + space--;//round down to multiple of 2 + m_buffer.getData(data, space); return space; From d96e2204bf0571c6860a7fe01f5a4a115886cb8d Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Thu, 14 May 2020 22:02:05 +0200 Subject: [PATCH 023/163] Initialize all members --- IIRDirectForm1Filter.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/IIRDirectForm1Filter.cpp b/IIRDirectForm1Filter.cpp index 51e4e13..946acdd 100644 --- a/IIRDirectForm1Filter.cpp +++ b/IIRDirectForm1Filter.cpp @@ -21,14 +21,18 @@ #include "math.h" CIIRDirectForm1Filter::CIIRDirectForm1Filter(float b0, float b1, float b2, float , float a1, float a2, float addtionalGaindB) : +m_x2(0.0F), +m_y2(0.0F), +m_x1(0.0F), +m_y1(0.0F), m_b0(b0), m_b1(b1), m_b2(b2), m_a1(a1), m_a2(a2), -m_additionalGainLin(::powf(10.0F, addtionalGaindB / 20.0F)) +m_additionalGainLin(0.0F) { - + m_additionalGainLin = ::powf(10.0F, addtionalGaindB / 20.0F); } float CIIRDirectForm1Filter::filter(float sample) From b5316907ad1f02947fd1d68c21eec6c2337b8be8 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Fri, 15 May 2020 20:19:57 +0200 Subject: [PATCH 024/163] Handle FM EOT --- FMControl.cpp | 3 +++ Modem.cpp | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/FMControl.cpp b/FMControl.cpp index 0041797..f4a0f27 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -44,6 +44,9 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) if (data[0U] == TAG_HEADER) return true; + if (data[0U] == TAG_EOT) + return m_network->write(data, 1U); + if (data[0U] != TAG_DATA) return false; diff --git a/Modem.cpp b/Modem.cpp index b73f54c..4d5aaab 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -82,6 +82,7 @@ const unsigned char MMDVM_FM_PARAMS3 = 0x62U; const unsigned char MMDVM_FM_PARAMS4 = 0x63U; const unsigned char MMDVM_FM_DATA = 0x65U; const unsigned char MMDVM_FM_CONTROL = 0x66U; +const unsigned char MMDVM_FM_EOT = 0x67U; const unsigned char MMDVM_ACK = 0x70U; const unsigned char MMDVM_NAK = 0x7FU; @@ -632,6 +633,19 @@ void CModem::clock(unsigned int ms) } break; + case MMDVM_FM_EOT: { + if(m_trace) + CUtils::dump(1U, "RX FM End of transmission", m_buffer, m_length); + + unsigned char data = m_length - 2U; + m_rxFMData.addData(&data, 1U); + + data = TAG_EOT; + m_rxFMData.addData(&data, 1U); + + m_rxFMData.addData(m_buffer + 3U, m_length - 3U); + } + case MMDVM_GET_STATUS: { // if (m_trace) // CUtils::dump(1U, "GET_STATUS", m_buffer, m_length); From a66f4c6188787ec6749c9374d7affb9a8ed89770 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 16 May 2020 13:09:28 +0100 Subject: [PATCH 025/163] Add an FM EOT network message. --- FMControl.cpp | 6 +++--- FMNetwork.cpp | 17 ++++++++++++++++- FMNetwork.h | 4 +++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index f4a0f27..18d90c5 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -45,7 +45,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) return true; if (data[0U] == TAG_EOT) - return m_network->write(data, 1U); + return m_network->writeEOT(); if (data[0U] != TAG_DATA) return false; @@ -55,7 +55,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) m_incomingRFAudio.addData(data + 1U, length - 1U); unsigned int bufferLength = m_incomingRFAudio.dataSize(); - if(bufferLength > 255U) + if (bufferLength > 255U) bufferLength = 255U; if (bufferLength >= 3U) { @@ -100,7 +100,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) out[nOut++] = (sample >> 0) & 0xFFU; } - return m_network->write((unsigned char*)out, nOut); + return m_network->writeData((unsigned char*)out, nOut); } return true; diff --git a/FMNetwork.cpp b/FMNetwork.cpp index cfe6105..4e3df87 100644 --- a/FMNetwork.cpp +++ b/FMNetwork.cpp @@ -55,7 +55,7 @@ bool CFMNetwork::open() return m_socket.open(); } -bool CFMNetwork::write(const unsigned char* data, unsigned int length) +bool CFMNetwork::writeData(const unsigned char* data, unsigned int length) { assert(data != NULL); @@ -74,6 +74,21 @@ bool CFMNetwork::write(const unsigned char* data, unsigned int length) return m_socket.write(buffer, length + 3U, m_address, m_port); } +bool CFMNetwork::writeEOT() +{ + unsigned char buffer[10U]; + ::memset(buffer, 0x00U, 10U); + + buffer[0U] = 'F'; + buffer[1U] = 'M'; + buffer[2U] = 'E'; + + if (m_debug) + CUtils::dump(1U, "FM Network Data Sent", buffer, 3U); + + return m_socket.write(buffer, 3U, m_address, m_port); +} + void CFMNetwork::clock(unsigned int ms) { unsigned char buffer[BUFFER_LENGTH]; diff --git a/FMNetwork.h b/FMNetwork.h index e4c01ae..fba36d2 100644 --- a/FMNetwork.h +++ b/FMNetwork.h @@ -35,7 +35,9 @@ public: void enable(bool enabled); - bool write(const unsigned char* data, unsigned int length); + bool writeData(const unsigned char* data, unsigned int length); + + bool writeEOT(); unsigned int read(unsigned char* data, unsigned int space); From 734af9453ca53aaf22565876003388357f4e3871 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 16 May 2020 17:16:05 +0200 Subject: [PATCH 026/163] Fix message for FM EOT --- FMNetwork.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FMNetwork.cpp b/FMNetwork.cpp index 4e3df87..57095c5 100644 --- a/FMNetwork.cpp +++ b/FMNetwork.cpp @@ -84,7 +84,7 @@ bool CFMNetwork::writeEOT() buffer[2U] = 'E'; if (m_debug) - CUtils::dump(1U, "FM Network Data Sent", buffer, 3U); + CUtils::dump(1U, "FM Network End of Transmission Sent", buffer, 3U); return m_socket.write(buffer, 3U, m_address, m_port); } From 23e92af6faa7430dc8afe8e60153c9c9d26c5d00 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 16 May 2020 17:40:42 +0200 Subject: [PATCH 027/163] Add Poll message --- FMNetwork.cpp | 25 ++++++++++++++++++++++++- FMNetwork.h | 3 +++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/FMNetwork.cpp b/FMNetwork.cpp index 57095c5..10078b5 100644 --- a/FMNetwork.cpp +++ b/FMNetwork.cpp @@ -33,7 +33,8 @@ m_address(), m_port(gatewayPort), m_debug(debug), m_enabled(false), -m_buffer(2000U, "FM Network") +m_buffer(2000U, "FM Network"), +m_pollTimer(1000U, 5U) { assert(gatewayPort > 0U); assert(!gatewayAddress.empty()); @@ -52,6 +53,8 @@ bool CFMNetwork::open() if (m_address.s_addr == INADDR_NONE) return false; + m_pollTimer.start(); + return m_socket.open(); } @@ -91,6 +94,12 @@ bool CFMNetwork::writeEOT() void CFMNetwork::clock(unsigned int ms) { + m_pollTimer.clock(ms); + if (m_pollTimer.hasExpired()) { + writePoll(); + m_pollTimer.start(); + } + unsigned char buffer[BUFFER_LENGTH]; in_addr address; @@ -159,3 +168,17 @@ void CFMNetwork::enable(bool enabled) m_enabled = enabled; } + +bool CFMNetwork::writePoll() +{ + unsigned char buffer[3U]; + + buffer[0U] = 'F'; + buffer[1U] = 'M'; + buffer[2U] = 'P'; + + if (m_debug) + CUtils::dump(1U, "FM Network Poll Sent", buffer, 3U); + + return m_socket.write(buffer, 3U, m_address, m_port); +} diff --git a/FMNetwork.h b/FMNetwork.h index fba36d2..453e776 100644 --- a/FMNetwork.h +++ b/FMNetwork.h @@ -54,6 +54,9 @@ private: bool m_debug; bool m_enabled; CRingBuffer m_buffer; + CTimer m_pollTimer; + + bool writePoll(); }; #endif From 578ef5a3d41021cacf937c5366077a9cf15ee872 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 16 May 2020 17:45:24 +0200 Subject: [PATCH 028/163] Handle poll message --- FMNetwork.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FMNetwork.cpp b/FMNetwork.cpp index 10078b5..67d0f2e 100644 --- a/FMNetwork.cpp +++ b/FMNetwork.cpp @@ -114,6 +114,10 @@ void CFMNetwork::clock(unsigned int ms) return; } + // Ignore incoming polls + if (::memcmp(buffer, "FMP", 3U) == 0) + return; + // Invalid packet type? if (::memcmp(buffer, "FMD", 3U) != 0) return; From 1290f9c49eaea3a69b09408b1e09f5ccc536a548 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Wed, 20 May 2020 18:07:57 +0200 Subject: [PATCH 029/163] do not write EOT when network is not set --- FMControl.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index 18d90c5..0867914 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -41,6 +41,9 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) assert(data != NULL); assert(length > 0U); + if (m_network == NULL) + return true; + if (data[0U] == TAG_HEADER) return true; @@ -50,8 +53,6 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) if (data[0U] != TAG_DATA) return false; - if (m_network == NULL) - return true; m_incomingRFAudio.addData(data + 1U, length - 1U); unsigned int bufferLength = m_incomingRFAudio.dataSize(); From 303a0163d37d0fb3ae758e174354f012bc1995d2 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 23 May 2020 13:03:55 +0100 Subject: [PATCH 030/163] Make the modem speed dynamic with a default of 115200. --- Conf.cpp | 1400 +++++++++++++++++++++--------------------- Conf.h | 2 + Display.cpp | 10 +- I2CController.cpp | 10 +- I2CController.h | 4 +- MMDVM.ini | 1 + MMDVMHost.cpp | 6 +- Modem.cpp | 6 +- Modem.h | 2 +- SerialController.cpp | 27 +- SerialController.h | 18 +- UMP.cpp | 4 +- Version.h | 2 +- 13 files changed, 746 insertions(+), 746 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index ca2a347..eae0db6 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -28,38 +28,38 @@ const int BUFFER_SIZE = 500; enum SECTION { - SECTION_NONE, - SECTION_GENERAL, - SECTION_INFO, - SECTION_LOG, - SECTION_CWID, - SECTION_DMRID_LOOKUP, - SECTION_NXDNID_LOOKUP, - SECTION_MODEM, - SECTION_TRANSPARENT, - SECTION_UMP, - SECTION_DSTAR, - SECTION_DMR, - SECTION_FUSION, - SECTION_P25, - SECTION_NXDN, - SECTION_POCSAG, - SECTION_FM, - SECTION_DSTAR_NETWORK, - SECTION_DMR_NETWORK, - SECTION_FUSION_NETWORK, - SECTION_P25_NETWORK, - SECTION_NXDN_NETWORK, - SECTION_POCSAG_NETWORK, - SECTION_FM_NETWORK, - SECTION_TFTSERIAL, - SECTION_HD44780, - SECTION_NEXTION, - SECTION_OLED, - SECTION_LCDPROC, - SECTION_LOCK_FILE, - SECTION_MOBILE_GPS, - SECTION_REMOTE_CONTROL + SECTION_NONE, + SECTION_GENERAL, + SECTION_INFO, + SECTION_LOG, + SECTION_CWID, + SECTION_DMRID_LOOKUP, + SECTION_NXDNID_LOOKUP, + SECTION_MODEM, + SECTION_TRANSPARENT, + SECTION_UMP, + SECTION_DSTAR, + SECTION_DMR, + SECTION_FUSION, + SECTION_P25, + SECTION_NXDN, + SECTION_POCSAG, + SECTION_FM, + SECTION_DSTAR_NETWORK, + SECTION_DMR_NETWORK, + SECTION_FUSION_NETWORK, + SECTION_P25_NETWORK, + SECTION_NXDN_NETWORK, + SECTION_POCSAG_NETWORK, + SECTION_FM_NETWORK, + SECTION_TFTSERIAL, + SECTION_HD44780, + SECTION_NEXTION, + SECTION_OLED, + SECTION_LCDPROC, + SECTION_LOCK_FILE, + SECTION_MOBILE_GPS, + SECTION_REMOTE_CONTROL }; CConf::CConf(const std::string& file) : @@ -92,6 +92,7 @@ m_nxdnIdLookupFile(), m_nxdnIdLookupTime(0U), m_modemPort(), m_modemProtocol("uart"), +m_modemSpeed(115200U), m_modemAddress(0x22), m_modemRXInvert(false), m_modemTXInvert(false), @@ -305,346 +306,348 @@ CConf::~CConf() bool CConf::read() { - FILE* fp = ::fopen(m_file.c_str(), "rt"); - if (fp == NULL) { - ::fprintf(stderr, "Couldn't open the .ini file - %s\n", m_file.c_str()); - return false; - } + FILE* fp = ::fopen(m_file.c_str(), "rt"); + if (fp == NULL) { + ::fprintf(stderr, "Couldn't open the .ini file - %s\n", m_file.c_str()); + return false; + } - SECTION section = SECTION_NONE; + SECTION section = SECTION_NONE; - char buffer[BUFFER_SIZE]; - while (::fgets(buffer, BUFFER_SIZE, fp) != NULL) { - if (buffer[0U] == '#') - continue; + char buffer[BUFFER_SIZE]; + while (::fgets(buffer, BUFFER_SIZE, fp) != NULL) { + if (buffer[0U] == '#') + continue; - if (buffer[0U] == '[') { - if (::strncmp(buffer, "[General]", 9U) == 0) - section = SECTION_GENERAL; - else if (::strncmp(buffer, "[Info]", 6U) == 0) - section = SECTION_INFO; - else if (::strncmp(buffer, "[Log]", 5U) == 0) - section = SECTION_LOG; - else if (::strncmp(buffer, "[CW Id]", 7U) == 0) - section = SECTION_CWID; - else if (::strncmp(buffer, "[DMR Id Lookup]", 15U) == 0) - section = SECTION_DMRID_LOOKUP; - else if (::strncmp(buffer, "[NXDN Id Lookup]", 16U) == 0) - section = SECTION_NXDNID_LOOKUP; - else if (::strncmp(buffer, "[Modem]", 7U) == 0) - section = SECTION_MODEM; - else if (::strncmp(buffer, "[Transparent Data]", 18U) == 0) - section = SECTION_TRANSPARENT; - else if (::strncmp(buffer, "[UMP]", 5U) == 0) - section = SECTION_UMP; - else if (::strncmp(buffer, "[D-Star]", 8U) == 0) - section = SECTION_DSTAR; - else if (::strncmp(buffer, "[DMR]", 5U) == 0) - section = SECTION_DMR; - else if (::strncmp(buffer, "[System Fusion]", 15U) == 0) - section = SECTION_FUSION; - else if (::strncmp(buffer, "[P25]", 5U) == 0) - section = SECTION_P25; - else if (::strncmp(buffer, "[NXDN]", 6U) == 0) - section = SECTION_NXDN; - else if (::strncmp(buffer, "[POCSAG]", 8U) == 0) - section = SECTION_POCSAG; - else if (::strncmp(buffer, "[FM]", 4U) == 0) - section = SECTION_FM; - else if (::strncmp(buffer, "[D-Star Network]", 16U) == 0) - section = SECTION_DSTAR_NETWORK; - else if (::strncmp(buffer, "[DMR Network]", 13U) == 0) - section = SECTION_DMR_NETWORK; - else if (::strncmp(buffer, "[System Fusion Network]", 23U) == 0) - section = SECTION_FUSION_NETWORK; - else if (::strncmp(buffer, "[P25 Network]", 13U) == 0) - section = SECTION_P25_NETWORK; - else if (::strncmp(buffer, "[NXDN Network]", 14U) == 0) - section = SECTION_NXDN_NETWORK; - else if (::strncmp(buffer, "[POCSAG Network]", 16U) == 0) - section = SECTION_POCSAG_NETWORK; - else if (::strncmp(buffer, "[FM Network]", 12U) == 0) - section = SECTION_FM_NETWORK; - else if (::strncmp(buffer, "[TFT Serial]", 12U) == 0) - section = SECTION_TFTSERIAL; - else if (::strncmp(buffer, "[HD44780]", 9U) == 0) - section = SECTION_HD44780; - else if (::strncmp(buffer, "[Nextion]", 9U) == 0) - section = SECTION_NEXTION; - else if (::strncmp(buffer, "[OLED]", 6U) == 0) - section = SECTION_OLED; - else if (::strncmp(buffer, "[LCDproc]", 9U) == 0) - section = SECTION_LCDPROC; - else if (::strncmp(buffer, "[Lock File]", 11U) == 0) - section = SECTION_LOCK_FILE; - else if (::strncmp(buffer, "[Mobile GPS]", 12U) == 0) - section = SECTION_MOBILE_GPS; - else if (::strncmp(buffer, "[Remote Control]", 16U) == 0) - section = SECTION_REMOTE_CONTROL; - else - section = SECTION_NONE; + if (buffer[0U] == '[') { + if (::strncmp(buffer, "[General]", 9U) == 0) + section = SECTION_GENERAL; + else if (::strncmp(buffer, "[Info]", 6U) == 0) + section = SECTION_INFO; + else if (::strncmp(buffer, "[Log]", 5U) == 0) + section = SECTION_LOG; + else if (::strncmp(buffer, "[CW Id]", 7U) == 0) + section = SECTION_CWID; + else if (::strncmp(buffer, "[DMR Id Lookup]", 15U) == 0) + section = SECTION_DMRID_LOOKUP; + else if (::strncmp(buffer, "[NXDN Id Lookup]", 16U) == 0) + section = SECTION_NXDNID_LOOKUP; + else if (::strncmp(buffer, "[Modem]", 7U) == 0) + section = SECTION_MODEM; + else if (::strncmp(buffer, "[Transparent Data]", 18U) == 0) + section = SECTION_TRANSPARENT; + else if (::strncmp(buffer, "[UMP]", 5U) == 0) + section = SECTION_UMP; + else if (::strncmp(buffer, "[D-Star]", 8U) == 0) + section = SECTION_DSTAR; + else if (::strncmp(buffer, "[DMR]", 5U) == 0) + section = SECTION_DMR; + else if (::strncmp(buffer, "[System Fusion]", 15U) == 0) + section = SECTION_FUSION; + else if (::strncmp(buffer, "[P25]", 5U) == 0) + section = SECTION_P25; + else if (::strncmp(buffer, "[NXDN]", 6U) == 0) + section = SECTION_NXDN; + else if (::strncmp(buffer, "[POCSAG]", 8U) == 0) + section = SECTION_POCSAG; + else if (::strncmp(buffer, "[FM]", 4U) == 0) + section = SECTION_FM; + else if (::strncmp(buffer, "[D-Star Network]", 16U) == 0) + section = SECTION_DSTAR_NETWORK; + else if (::strncmp(buffer, "[DMR Network]", 13U) == 0) + section = SECTION_DMR_NETWORK; + else if (::strncmp(buffer, "[System Fusion Network]", 23U) == 0) + section = SECTION_FUSION_NETWORK; + else if (::strncmp(buffer, "[P25 Network]", 13U) == 0) + section = SECTION_P25_NETWORK; + else if (::strncmp(buffer, "[NXDN Network]", 14U) == 0) + section = SECTION_NXDN_NETWORK; + else if (::strncmp(buffer, "[POCSAG Network]", 16U) == 0) + section = SECTION_POCSAG_NETWORK; + else if (::strncmp(buffer, "[FM Network]", 12U) == 0) + section = SECTION_FM_NETWORK; + else if (::strncmp(buffer, "[TFT Serial]", 12U) == 0) + section = SECTION_TFTSERIAL; + else if (::strncmp(buffer, "[HD44780]", 9U) == 0) + section = SECTION_HD44780; + else if (::strncmp(buffer, "[Nextion]", 9U) == 0) + section = SECTION_NEXTION; + else if (::strncmp(buffer, "[OLED]", 6U) == 0) + section = SECTION_OLED; + else if (::strncmp(buffer, "[LCDproc]", 9U) == 0) + section = SECTION_LCDPROC; + else if (::strncmp(buffer, "[Lock File]", 11U) == 0) + section = SECTION_LOCK_FILE; + else if (::strncmp(buffer, "[Mobile GPS]", 12U) == 0) + section = SECTION_MOBILE_GPS; + else if (::strncmp(buffer, "[Remote Control]", 16U) == 0) + section = SECTION_REMOTE_CONTROL; + else + section = SECTION_NONE; - continue; - } - - char* key = ::strtok(buffer, " \t=\r\n"); - if (key == NULL) - continue; - - char* value = ::strtok(NULL, "\r\n"); - if (value == NULL) - continue; - - // Remove quotes from the value - size_t len = ::strlen(value); - if (len > 1U && *value == '"' && value[len - 1U] == '"') { - value[len - 1U] = '\0'; - value++; - } - - if (section == SECTION_GENERAL) { - if (::strcmp(key, "Callsign") == 0) { - // Convert the callsign to upper case - for (unsigned int i = 0U; value[i] != 0; i++) - value[i] = ::toupper(value[i]); - m_fmCallsign = m_cwIdCallsign = m_callsign = value; - } else if (::strcmp(key, "Id") == 0) - m_id = m_p25Id = m_dmrId = (unsigned int)::atoi(value); - else if (::strcmp(key, "Timeout") == 0) - m_fmTimeout = m_timeout = (unsigned int)::atoi(value); - else if (::strcmp(key, "Duplex") == 0) - m_duplex = ::atoi(value) == 1; - else if (::strcmp(key, "ModeHang") == 0) - m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = m_nxdnNetworkModeHang = m_pocsagNetworkModeHang = m_fmNetworkModeHang = - m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = m_nxdnModeHang = m_fmModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "RFModeHang") == 0) - m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = m_nxdnModeHang = m_fmModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "NetModeHang") == 0) - m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = m_nxdnNetworkModeHang = m_pocsagNetworkModeHang = m_fmNetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Display") == 0) - m_display = value; - else if (::strcmp(key, "Daemon") == 0) - m_daemon = ::atoi(value) == 1; - } else if (section == SECTION_INFO) { - if (::strcmp(key, "TXFrequency") == 0) - m_pocsagFrequency = m_txFrequency = (unsigned int)::atoi(value); - else if (::strcmp(key, "RXFrequency") == 0) - m_rxFrequency = (unsigned int)::atoi(value); - else if (::strcmp(key, "Power") == 0) - m_power = (unsigned int)::atoi(value); - else if (::strcmp(key, "Latitude") == 0) - m_latitude = float(::atof(value)); - else if (::strcmp(key, "Longitude") == 0) - m_longitude = float(::atof(value)); - else if (::strcmp(key, "Height") == 0) - m_height = ::atoi(value); - else if (::strcmp(key, "Location") == 0) - m_location = value; - else if (::strcmp(key, "Description") == 0) - m_description = value; - else if (::strcmp(key, "URL") == 0) - m_url = value; - } else if (section == SECTION_LOG) { - if (::strcmp(key, "FilePath") == 0) - m_logFilePath = value; - else if (::strcmp(key, "FileRoot") == 0) - m_logFileRoot = value; - else if (::strcmp(key, "FileLevel") == 0) - m_logFileLevel = (unsigned int)::atoi(value); - else if (::strcmp(key, "DisplayLevel") == 0) - m_logDisplayLevel = (unsigned int)::atoi(value); - } else if (section == SECTION_CWID) { - if (::strcmp(key, "Enable") == 0) - m_cwIdEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Time") == 0) - m_cwIdTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "Callsign") == 0) { - // Convert the callsign to upper case - for (unsigned int i = 0U; value[i] != 0; i++) - value[i] = ::toupper(value[i]); - m_cwIdCallsign = value; + continue; } - } else if (section == SECTION_DMRID_LOOKUP) { - if (::strcmp(key, "File") == 0) - m_dmrIdLookupFile = value; - else if (::strcmp(key, "Time") == 0) - m_dmrIdLookupTime = (unsigned int)::atoi(value); - } else if (section == SECTION_NXDNID_LOOKUP) { - if (::strcmp(key, "File") == 0) - m_nxdnIdLookupFile = value; - else if (::strcmp(key, "Time") == 0) - m_nxdnIdLookupTime = (unsigned int)::atoi(value); - } else if (section == SECTION_MODEM) { - if (::strcmp(key, "Port") == 0) - m_modemPort = value; - else if (::strcmp(key, "Protocol") == 0) - m_modemProtocol = value; - else if (::strcmp(key, "Address") == 0) - m_modemAddress = (unsigned int)::strtoul(value, NULL, 16); - else if (::strcmp(key, "RXInvert") == 0) - m_modemRXInvert = ::atoi(value) == 1; - else if (::strcmp(key, "TXInvert") == 0) - m_modemTXInvert = ::atoi(value) == 1; - else if (::strcmp(key, "PTTInvert") == 0) - m_modemPTTInvert = ::atoi(value) == 1; - else if (::strcmp(key, "TXDelay") == 0) - m_modemTXDelay = (unsigned int)::atoi(value); - else if (::strcmp(key, "DMRDelay") == 0) - m_modemDMRDelay = (unsigned int)::atoi(value); - else if (::strcmp(key, "RXOffset") == 0) - m_modemRXOffset = ::atoi(value); - else if (::strcmp(key, "TXOffset") == 0) - m_modemTXOffset = ::atoi(value); - else if (::strcmp(key, "RXDCOffset") == 0) - m_modemRXDCOffset = ::atoi(value); - else if (::strcmp(key, "TXDCOffset") == 0) - m_modemTXDCOffset = ::atoi(value); - else if (::strcmp(key, "RFLevel") == 0) - m_modemRFLevel = float(::atof(value)); - else if (::strcmp(key, "RXLevel") == 0) - m_modemRXLevel = float(::atof(value)); - else if (::strcmp(key, "TXLevel") == 0) - m_modemFMTXLevel = m_modemCWIdTXLevel = m_modemDStarTXLevel = m_modemDMRTXLevel = m_modemYSFTXLevel = m_modemP25TXLevel = m_modemNXDNTXLevel = float(::atof(value)); - else if (::strcmp(key, "CWIdTXLevel") == 0) - m_modemCWIdTXLevel = float(::atof(value)); - else if (::strcmp(key, "D-StarTXLevel") == 0) - m_modemDStarTXLevel = float(::atof(value)); - else if (::strcmp(key, "DMRTXLevel") == 0) - m_modemDMRTXLevel = float(::atof(value)); - else if (::strcmp(key, "YSFTXLevel") == 0) - m_modemYSFTXLevel = float(::atof(value)); - else if (::strcmp(key, "P25TXLevel") == 0) - m_modemP25TXLevel = float(::atof(value)); - else if (::strcmp(key, "NXDNTXLevel") == 0) - m_modemNXDNTXLevel = float(::atof(value)); - else if (::strcmp(key, "POCSAGTXLevel") == 0) - m_modemPOCSAGTXLevel = float(::atof(value)); - else if (::strcmp(key, "FMTXLevel") == 0) - m_modemFMTXLevel = float(::atof(value)); - else if (::strcmp(key, "RSSIMappingFile") == 0) - m_modemRSSIMappingFile = value; - else if (::strcmp(key, "Trace") == 0) - m_modemTrace = ::atoi(value) == 1; - else if (::strcmp(key, "Debug") == 0) - m_modemDebug = ::atoi(value) == 1; - } else if (section == SECTION_TRANSPARENT) { - if (::strcmp(key, "Enable") == 0) - m_transparentEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "RemoteAddress") == 0) - m_transparentRemoteAddress = value; - else if (::strcmp(key, "RemotePort") == 0) - m_transparentRemotePort = (unsigned int)::atoi(value); - else if (::strcmp(key, "LocalPort") == 0) - m_transparentLocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "SendFrameType") == 0) - m_transparentSendFrameType = (unsigned int)::atoi(value); - } else if (section == SECTION_UMP) { - if (::strcmp(key, "Enable") == 0) - m_umpEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Port") == 0) - m_umpPort = value; - } else if (section == SECTION_DSTAR) { - if (::strcmp(key, "Enable") == 0) - m_dstarEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Module") == 0) { - // Convert the module to upper case - for (unsigned int i = 0U; value[i] != 0; i++) - value[i] = ::toupper(value[i]); - m_dstarModule = value; - } else if (::strcmp(key, "SelfOnly") == 0) - m_dstarSelfOnly = ::atoi(value) == 1; - else if (::strcmp(key, "BlackList") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - if (::strlen(p) > 0U) { - for (unsigned int i = 0U; p[i] != 0; i++) - p[i] = ::toupper(p[i]); - std::string callsign = std::string(p); - callsign.resize(DSTAR_LONG_CALLSIGN_LENGTH, ' '); - m_dstarBlackList.push_back(callsign); + + char* key = ::strtok(buffer, " \t=\r\n"); + if (key == NULL) + continue; + + char* value = ::strtok(NULL, "\r\n"); + if (value == NULL) + continue; + + // Remove quotes from the value + size_t len = ::strlen(value); + if (len > 1U && *value == '"' && value[len - 1U] == '"') { + value[len - 1U] = '\0'; + value++; + } + + if (section == SECTION_GENERAL) { + if (::strcmp(key, "Callsign") == 0) { + // Convert the callsign to upper case + for (unsigned int i = 0U; value[i] != 0; i++) + value[i] = ::toupper(value[i]); + m_fmCallsign = m_cwIdCallsign = m_callsign = value; + } else if (::strcmp(key, "Id") == 0) + m_id = m_p25Id = m_dmrId = (unsigned int)::atoi(value); + else if (::strcmp(key, "Timeout") == 0) + m_fmTimeout = m_timeout = (unsigned int)::atoi(value); + else if (::strcmp(key, "Duplex") == 0) + m_duplex = ::atoi(value) == 1; + else if (::strcmp(key, "ModeHang") == 0) + m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = m_nxdnNetworkModeHang = m_pocsagNetworkModeHang = m_fmNetworkModeHang = + m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = m_nxdnModeHang = m_fmModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "RFModeHang") == 0) + m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = m_nxdnModeHang = m_fmModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "NetModeHang") == 0) + m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = m_nxdnNetworkModeHang = m_pocsagNetworkModeHang = m_fmNetworkModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "Display") == 0) + m_display = value; + else if (::strcmp(key, "Daemon") == 0) + m_daemon = ::atoi(value) == 1; + } else if (section == SECTION_INFO) { + if (::strcmp(key, "TXFrequency") == 0) + m_pocsagFrequency = m_txFrequency = (unsigned int)::atoi(value); + else if (::strcmp(key, "RXFrequency") == 0) + m_rxFrequency = (unsigned int)::atoi(value); + else if (::strcmp(key, "Power") == 0) + m_power = (unsigned int)::atoi(value); + else if (::strcmp(key, "Latitude") == 0) + m_latitude = float(::atof(value)); + else if (::strcmp(key, "Longitude") == 0) + m_longitude = float(::atof(value)); + else if (::strcmp(key, "Height") == 0) + m_height = ::atoi(value); + else if (::strcmp(key, "Location") == 0) + m_location = value; + else if (::strcmp(key, "Description") == 0) + m_description = value; + else if (::strcmp(key, "URL") == 0) + m_url = value; + } else if (section == SECTION_LOG) { + if (::strcmp(key, "FilePath") == 0) + m_logFilePath = value; + else if (::strcmp(key, "FileRoot") == 0) + m_logFileRoot = value; + else if (::strcmp(key, "FileLevel") == 0) + m_logFileLevel = (unsigned int)::atoi(value); + else if (::strcmp(key, "DisplayLevel") == 0) + m_logDisplayLevel = (unsigned int)::atoi(value); + } else if (section == SECTION_CWID) { + if (::strcmp(key, "Enable") == 0) + m_cwIdEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Time") == 0) + m_cwIdTime = (unsigned int)::atoi(value); + else if (::strcmp(key, "Callsign") == 0) { + // Convert the callsign to upper case + for (unsigned int i = 0U; value[i] != 0; i++) + value[i] = ::toupper(value[i]); + m_cwIdCallsign = value; + } + } else if (section == SECTION_DMRID_LOOKUP) { + if (::strcmp(key, "File") == 0) + m_dmrIdLookupFile = value; + else if (::strcmp(key, "Time") == 0) + m_dmrIdLookupTime = (unsigned int)::atoi(value); + } else if (section == SECTION_NXDNID_LOOKUP) { + if (::strcmp(key, "File") == 0) + m_nxdnIdLookupFile = value; + else if (::strcmp(key, "Time") == 0) + m_nxdnIdLookupTime = (unsigned int)::atoi(value); + } else if (section == SECTION_MODEM) { + if (::strcmp(key, "Port") == 0) + m_modemPort = value; + else if (::strcmp(key, "Protocol") == 0) + m_modemProtocol = value; + else if (::strcmp(key, "Speed") == 0) + m_modemSpeed = (unsigned int)::atoi(value); + else if (::strcmp(key, "Address") == 0) + m_modemAddress = (unsigned int)::strtoul(value, NULL, 16); + else if (::strcmp(key, "RXInvert") == 0) + m_modemRXInvert = ::atoi(value) == 1; + else if (::strcmp(key, "TXInvert") == 0) + m_modemTXInvert = ::atoi(value) == 1; + else if (::strcmp(key, "PTTInvert") == 0) + m_modemPTTInvert = ::atoi(value) == 1; + else if (::strcmp(key, "TXDelay") == 0) + m_modemTXDelay = (unsigned int)::atoi(value); + else if (::strcmp(key, "DMRDelay") == 0) + m_modemDMRDelay = (unsigned int)::atoi(value); + else if (::strcmp(key, "RXOffset") == 0) + m_modemRXOffset = ::atoi(value); + else if (::strcmp(key, "TXOffset") == 0) + m_modemTXOffset = ::atoi(value); + else if (::strcmp(key, "RXDCOffset") == 0) + m_modemRXDCOffset = ::atoi(value); + else if (::strcmp(key, "TXDCOffset") == 0) + m_modemTXDCOffset = ::atoi(value); + else if (::strcmp(key, "RFLevel") == 0) + m_modemRFLevel = float(::atof(value)); + else if (::strcmp(key, "RXLevel") == 0) + m_modemRXLevel = float(::atof(value)); + else if (::strcmp(key, "TXLevel") == 0) + m_modemFMTXLevel = m_modemCWIdTXLevel = m_modemDStarTXLevel = m_modemDMRTXLevel = m_modemYSFTXLevel = m_modemP25TXLevel = m_modemNXDNTXLevel = float(::atof(value)); + else if (::strcmp(key, "CWIdTXLevel") == 0) + m_modemCWIdTXLevel = float(::atof(value)); + else if (::strcmp(key, "D-StarTXLevel") == 0) + m_modemDStarTXLevel = float(::atof(value)); + else if (::strcmp(key, "DMRTXLevel") == 0) + m_modemDMRTXLevel = float(::atof(value)); + else if (::strcmp(key, "YSFTXLevel") == 0) + m_modemYSFTXLevel = float(::atof(value)); + else if (::strcmp(key, "P25TXLevel") == 0) + m_modemP25TXLevel = float(::atof(value)); + else if (::strcmp(key, "NXDNTXLevel") == 0) + m_modemNXDNTXLevel = float(::atof(value)); + else if (::strcmp(key, "POCSAGTXLevel") == 0) + m_modemPOCSAGTXLevel = float(::atof(value)); + else if (::strcmp(key, "FMTXLevel") == 0) + m_modemFMTXLevel = float(::atof(value)); + else if (::strcmp(key, "RSSIMappingFile") == 0) + m_modemRSSIMappingFile = value; + else if (::strcmp(key, "Trace") == 0) + m_modemTrace = ::atoi(value) == 1; + else if (::strcmp(key, "Debug") == 0) + m_modemDebug = ::atoi(value) == 1; + } else if (section == SECTION_TRANSPARENT) { + if (::strcmp(key, "Enable") == 0) + m_transparentEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "RemoteAddress") == 0) + m_transparentRemoteAddress = value; + else if (::strcmp(key, "RemotePort") == 0) + m_transparentRemotePort = (unsigned int)::atoi(value); + else if (::strcmp(key, "LocalPort") == 0) + m_transparentLocalPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "SendFrameType") == 0) + m_transparentSendFrameType = (unsigned int)::atoi(value); + } else if (section == SECTION_UMP) { + if (::strcmp(key, "Enable") == 0) + m_umpEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Port") == 0) + m_umpPort = value; + } else if (section == SECTION_DSTAR) { + if (::strcmp(key, "Enable") == 0) + m_dstarEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Module") == 0) { + // Convert the module to upper case + for (unsigned int i = 0U; value[i] != 0; i++) + value[i] = ::toupper(value[i]); + m_dstarModule = value; + } else if (::strcmp(key, "SelfOnly") == 0) + m_dstarSelfOnly = ::atoi(value) == 1; + else if (::strcmp(key, "BlackList") == 0) { + char* p = ::strtok(value, ",\r\n"); + while (p != NULL) { + if (::strlen(p) > 0U) { + for (unsigned int i = 0U; p[i] != 0; i++) + p[i] = ::toupper(p[i]); + std::string callsign = std::string(p); + callsign.resize(DSTAR_LONG_CALLSIGN_LENGTH, ' '); + m_dstarBlackList.push_back(callsign); + } + p = ::strtok(NULL, ",\r\n"); } - p = ::strtok(NULL, ",\r\n"); - } - } else if (::strcmp(key, "AckReply") == 0) - m_dstarAckReply = ::atoi(value) == 1; - else if (::strcmp(key, "AckTime") == 0) - m_dstarAckTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "AckMessage") == 0) - m_dstarAckMessage = ::atoi(value) == 1; - else if (::strcmp(key, "ErrorReply") == 0) - m_dstarErrorReply = ::atoi(value) == 1; - else if (::strcmp(key, "RemoteGateway") == 0) - m_dstarRemoteGateway = ::atoi(value) == 1; - else if (::strcmp(key, "ModeHang") == 0) - m_dstarModeHang = (unsigned int)::atoi(value); - } else if (section == SECTION_DMR) { - if (::strcmp(key, "Enable") == 0) - m_dmrEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Beacons") == 0) - m_dmrBeacons = ::atoi(value) == 1 ? DMR_BEACONS_NETWORK : DMR_BEACONS_OFF; - else if (::strcmp(key, "BeaconInterval") == 0) { - m_dmrBeacons = m_dmrBeacons != DMR_BEACONS_OFF ? DMR_BEACONS_TIMED : DMR_BEACONS_OFF; - m_dmrBeaconInterval = (unsigned int)::atoi(value); - } else if (::strcmp(key, "BeaconDuration") == 0) - m_dmrBeaconDuration = (unsigned int)::atoi(value); - else if (::strcmp(key, "Id") == 0) - m_dmrId = (unsigned int)::atoi(value); - else if (::strcmp(key, "ColorCode") == 0) - m_dmrColorCode = (unsigned int)::atoi(value); - else if (::strcmp(key, "SelfOnly") == 0) - m_dmrSelfOnly = ::atoi(value) == 1; - else if (::strcmp(key, "EmbeddedLCOnly") == 0) - m_dmrEmbeddedLCOnly = ::atoi(value) == 1; - else if (::strcmp(key, "DumpTAData") == 0) - m_dmrDumpTAData = ::atoi(value) == 1; - else if (::strcmp(key, "Prefixes") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int prefix = (unsigned int)::atoi(p); - if (prefix > 0U && prefix <= 999U) - m_dmrPrefixes.push_back(prefix); - p = ::strtok(NULL, ",\r\n"); - } - } else if (::strcmp(key, "BlackList") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int id = (unsigned int)::atoi(p); - if (id > 0U) - m_dmrBlackList.push_back(id); - p = ::strtok(NULL, ",\r\n"); - } - } else if (::strcmp(key, "WhiteList") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int id = (unsigned int)::atoi(p); - if (id > 0U) - m_dmrWhiteList.push_back(id); - p = ::strtok(NULL, ",\r\n"); - } - } else if (::strcmp(key, "Slot1TGWhiteList") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int id = (unsigned int)::atoi(p); - if (id > 0U) - m_dmrSlot1TGWhiteList.push_back(id); - p = ::strtok(NULL, ",\r\n"); - } - } else if (::strcmp(key, "Slot2TGWhiteList") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int id = (unsigned int)::atoi(p); - if (id > 0U) - m_dmrSlot2TGWhiteList.push_back(id); - p = ::strtok(NULL, ",\r\n"); - } - } else if (::strcmp(key, "TXHang") == 0) - m_dmrTXHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "CallHang") == 0) - m_dmrCallHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_dmrModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "OVCM") == 0) - switch(::atoi(value)) { + } else if (::strcmp(key, "AckReply") == 0) + m_dstarAckReply = ::atoi(value) == 1; + else if (::strcmp(key, "AckTime") == 0) + m_dstarAckTime = (unsigned int)::atoi(value); + else if (::strcmp(key, "AckMessage") == 0) + m_dstarAckMessage = ::atoi(value) == 1; + else if (::strcmp(key, "ErrorReply") == 0) + m_dstarErrorReply = ::atoi(value) == 1; + else if (::strcmp(key, "RemoteGateway") == 0) + m_dstarRemoteGateway = ::atoi(value) == 1; + else if (::strcmp(key, "ModeHang") == 0) + m_dstarModeHang = (unsigned int)::atoi(value); + } else if (section == SECTION_DMR) { + if (::strcmp(key, "Enable") == 0) + m_dmrEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Beacons") == 0) + m_dmrBeacons = ::atoi(value) == 1 ? DMR_BEACONS_NETWORK : DMR_BEACONS_OFF; + else if (::strcmp(key, "BeaconInterval") == 0) { + m_dmrBeacons = m_dmrBeacons != DMR_BEACONS_OFF ? DMR_BEACONS_TIMED : DMR_BEACONS_OFF; + m_dmrBeaconInterval = (unsigned int)::atoi(value); + } else if (::strcmp(key, "BeaconDuration") == 0) + m_dmrBeaconDuration = (unsigned int)::atoi(value); + else if (::strcmp(key, "Id") == 0) + m_dmrId = (unsigned int)::atoi(value); + else if (::strcmp(key, "ColorCode") == 0) + m_dmrColorCode = (unsigned int)::atoi(value); + else if (::strcmp(key, "SelfOnly") == 0) + m_dmrSelfOnly = ::atoi(value) == 1; + else if (::strcmp(key, "EmbeddedLCOnly") == 0) + m_dmrEmbeddedLCOnly = ::atoi(value) == 1; + else if (::strcmp(key, "DumpTAData") == 0) + m_dmrDumpTAData = ::atoi(value) == 1; + else if (::strcmp(key, "Prefixes") == 0) { + char* p = ::strtok(value, ",\r\n"); + while (p != NULL) { + unsigned int prefix = (unsigned int)::atoi(p); + if (prefix > 0U && prefix <= 999U) + m_dmrPrefixes.push_back(prefix); + p = ::strtok(NULL, ",\r\n"); + } + } else if (::strcmp(key, "BlackList") == 0) { + char* p = ::strtok(value, ",\r\n"); + while (p != NULL) { + unsigned int id = (unsigned int)::atoi(p); + if (id > 0U) + m_dmrBlackList.push_back(id); + p = ::strtok(NULL, ",\r\n"); + } + } else if (::strcmp(key, "WhiteList") == 0) { + char* p = ::strtok(value, ",\r\n"); + while (p != NULL) { + unsigned int id = (unsigned int)::atoi(p); + if (id > 0U) + m_dmrWhiteList.push_back(id); + p = ::strtok(NULL, ",\r\n"); + } + } else if (::strcmp(key, "Slot1TGWhiteList") == 0) { + char* p = ::strtok(value, ",\r\n"); + while (p != NULL) { + unsigned int id = (unsigned int)::atoi(p); + if (id > 0U) + m_dmrSlot1TGWhiteList.push_back(id); + p = ::strtok(NULL, ",\r\n"); + } + } else if (::strcmp(key, "Slot2TGWhiteList") == 0) { + char* p = ::strtok(value, ",\r\n"); + while (p != NULL) { + unsigned int id = (unsigned int)::atoi(p); + if (id > 0U) + m_dmrSlot2TGWhiteList.push_back(id); + p = ::strtok(NULL, ",\r\n"); + } + } else if (::strcmp(key, "TXHang") == 0) + m_dmrTXHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "CallHang") == 0) + m_dmrCallHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_dmrModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "OVCM") == 0) { + switch (::atoi(value)) { case 1: m_dmrOVCM = DMR_OVCM_RX_ON; break; @@ -657,342 +660,342 @@ bool CConf::read() default: m_dmrOVCM = DMR_OVCM_OFF; break; + } } - } else if (section == SECTION_FUSION) { - if (::strcmp(key, "Enable") == 0) - m_fusionEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "LowDeviation") == 0) - m_fusionLowDeviation = ::atoi(value) == 1; - else if (::strcmp(key, "DGID") == 0) { - m_fusionDGIdEnabled = true; - m_fusionDGId = (unsigned int)::atoi(value); - } else if (::strcmp(key, "RemoteGateway") == 0) - m_fusionRemoteGateway = ::atoi(value) == 1; - else if (::strcmp(key, "SelfOnly") == 0) - m_fusionSelfOnly = ::atoi(value) == 1; - else if (::strcmp(key, "TXHang") == 0) - m_fusionTXHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_fusionModeHang = (unsigned int)::atoi(value); - } else if (section == SECTION_P25) { - if (::strcmp(key, "Enable") == 0) - m_p25Enabled = ::atoi(value) == 1; - else if (::strcmp(key, "Id") == 0) - m_p25Id = (unsigned int)::atoi(value); - else if (::strcmp(key, "NAC") == 0) - m_p25NAC = (unsigned int)::strtoul(value, NULL, 16); - else if (::strcmp(key, "OverrideUIDCheck") == 0) - m_p25OverrideUID = ::atoi(value) == 1; - else if (::strcmp(key, "SelfOnly") == 0) - m_p25SelfOnly = ::atoi(value) == 1; - else if (::strcmp(key, "RemoteGateway") == 0) - m_p25RemoteGateway = ::atoi(value) == 1; - else if (::strcmp(key, "TXHang") == 0) - m_p25TXHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_p25ModeHang = (unsigned int)::atoi(value); - } else if (section == SECTION_NXDN) { - if (::strcmp(key, "Enable") == 0) - m_nxdnEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Id") == 0) - m_nxdnId = (unsigned int)::atoi(value); - else if (::strcmp(key, "RAN") == 0) - m_nxdnRAN = (unsigned int)::atoi(value); - else if (::strcmp(key, "SelfOnly") == 0) - m_nxdnSelfOnly = ::atoi(value) == 1; - else if (::strcmp(key, "RemoteGateway") == 0) - m_nxdnRemoteGateway = ::atoi(value) == 1; - else if (::strcmp(key, "TXHang") == 0) - m_nxdnTXHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_nxdnModeHang = (unsigned int)::atoi(value); - } else if (section == SECTION_POCSAG) { - if (::strcmp(key, "Enable") == 0) - m_pocsagEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Frequency") == 0) - m_pocsagFrequency = (unsigned int)::atoi(value); - } - else if (section == SECTION_FM) { - if (::strcmp(key, "Enable") == 0) - m_fmEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Callsign") == 0) { - // Convert the callsign to upper case - for (unsigned int i = 0U; value[i] != 0; i++) - value[i] = ::toupper(value[i]); - m_fmCallsign = value; - } else if (::strcmp(key, "CallsignSpeed") == 0) - m_fmCallsignSpeed = (unsigned int)::atoi(value); - else if (::strcmp(key, "CallsignFrequency") == 0) - m_fmCallsignFrequency = (unsigned int)::atoi(value); - else if (::strcmp(key, "CallsignTime") == 0) - m_fmCallsignTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "CallsignHoldoff") == 0) - m_fmCallsignHoldoff = (unsigned int)::atoi(value); - else if (::strcmp(key, "CallsignHighLevel") == 0) - m_fmCallsignHighLevel = float(::atof(value)); - else if (::strcmp(key, "CallsignLowLevel") == 0) - m_fmCallsignLowLevel = float(::atof(value)); - else if (::strcmp(key, "CallsignAtStart") == 0) - m_fmCallsignAtStart = ::atoi(value) == 1; - else if (::strcmp(key, "CallsignAtEnd") == 0) - m_fmCallsignAtEnd = ::atoi(value) == 1; - else if (::strcmp(key, "CallsignAtLatch") == 0) - m_fmCallsignAtLatch = ::atoi(value) == 1; - else if (::strcmp(key, "RFAck") == 0) { - // Convert the ack to upper case - for (unsigned int i = 0U; value[i] != 0; i++) - value[i] = ::toupper(value[i]); - m_fmRFAck = value; - } else if (::strcmp(key, "ExtAck") == 0) { - // Convert the ack to upper case - for (unsigned int i = 0U; value[i] != 0; i++) - value[i] = ::toupper(value[i]); - m_fmExtAck = value; - } else if (::strcmp(key, "AckSpeed") == 0) - m_fmAckSpeed = (unsigned int)::atoi(value); - else if (::strcmp(key, "AckFrequency") == 0) - m_fmAckFrequency = (unsigned int)::atoi(value); - else if (::strcmp(key, "AckMinTime") == 0) - m_fmAckMinTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "AckDelay") == 0) - m_fmAckDelay = (unsigned int)::atoi(value); - else if (::strcmp(key, "AckLevel") == 0) - m_fmAckLevel = float(::atof(value)); - else if (::strcmp(key, "Timeout") == 0) - m_fmTimeout = (unsigned int)::atoi(value); - else if (::strcmp(key, "TimeoutLevel") == 0) - m_fmTimeoutLevel = float(::atof(value)); - else if (::strcmp(key, "CTCSSFrequency") == 0) - m_fmCTCSSFrequency = float(::atof(value)); - else if (::strcmp(key, "CTCSSThreshold") == 0) - m_fmCTCSSHighThreshold = m_fmCTCSSLowThreshold = (unsigned int)::atoi(value); - else if (::strcmp(key, "CTCSSHighThreshold") == 0) - m_fmCTCSSHighThreshold = (unsigned int)::atoi(value); - else if (::strcmp(key, "CTCSSLowThreshold") == 0) - m_fmCTCSSLowThreshold = (unsigned int)::atoi(value); - else if (::strcmp(key, "CTCSSLevel") == 0) - m_fmCTCSSLevel = float(::atof(value)); - else if (::strcmp(key, "KerchunkTime") == 0) - m_fmKerchunkTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "HangTime") == 0) - m_fmHangTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "UseCOS") == 0) - m_fmUseCOS = ::atoi(value) == 1; - else if (::strcmp(key, "COSInvert") == 0) - m_fmCOSInvert = ::atoi(value) == 1; - else if (::strcmp(key, "RFAudioBoost") == 0) - m_fmRFAudioBoost = (unsigned int)::atoi(value); - else if (::strcmp(key, "MaxDevLevel") == 0) - m_fmMaxDevLevel = float(::atof(value)); - else if (::strcmp(key, "ExtAudioBoost") == 0) - m_fmExtAudioBoost = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_fmModeHang = (unsigned int)::atoi(value); - } else if (section == SECTION_DSTAR_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_dstarNetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "GatewayAddress") == 0) - m_dstarGatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_dstarGatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "LocalPort") == 0) - m_dstarLocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_dstarNetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_dstarNetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_DMR_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_dmrNetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Address") == 0) - m_dmrNetworkAddress = value; - else if (::strcmp(key, "Port") == 0) - m_dmrNetworkPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "Local") == 0) - m_dmrNetworkLocal = (unsigned int)::atoi(value); - else if (::strcmp(key, "Password") == 0) - m_dmrNetworkPassword = value; - else if (::strcmp(key, "Options") == 0) - m_dmrNetworkOptions = value; - else if (::strcmp(key, "Debug") == 0) - m_dmrNetworkDebug = ::atoi(value) == 1; - else if (::strcmp(key, "Jitter") == 0) - m_dmrNetworkJitter = (unsigned int)::atoi(value); - else if (::strcmp(key, "Slot1") == 0) - m_dmrNetworkSlot1 = ::atoi(value) == 1; - else if (::strcmp(key, "Slot2") == 0) - m_dmrNetworkSlot2 = ::atoi(value) == 1; - else if (::strcmp(key, "ModeHang") == 0) - m_dmrNetworkModeHang = (unsigned int)::atoi(value); - } else if (section == SECTION_FUSION_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_fusionNetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "LocalAddress") == 0) - m_fusionNetworkMyAddress = value; - else if (::strcmp(key, "LocalPort") == 0) - m_fusionNetworkMyPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "GatewayAddress") == 0) - m_fusionNetworkGatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_fusionNetworkGatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_fusionNetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_fusionNetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_P25_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_p25NetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "GatewayAddress") == 0) - m_p25GatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_p25GatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "LocalPort") == 0) - m_p25LocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_p25NetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_p25NetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_NXDN_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_nxdnNetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "LocalAddress") == 0) - m_nxdnLocalAddress = value; - else if (::strcmp(key, "LocalPort") == 0) - m_nxdnLocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "GatewayAddress") == 0) - m_nxdnGatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_nxdnGatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_nxdnNetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_nxdnNetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_POCSAG_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_pocsagNetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "LocalAddress") == 0) - m_pocsagLocalAddress = value; - else if (::strcmp(key, "LocalPort") == 0) - m_pocsagLocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "GatewayAddress") == 0) - m_pocsagGatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_pocsagGatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_pocsagNetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_pocsagNetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_FM_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_fmNetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "LocalAddress") == 0) - m_fmLocalAddress = value; - else if (::strcmp(key, "LocalPort") == 0) - m_fmLocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "GatewayAddress") == 0) - m_fmGatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_fmGatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_fmNetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_fmNetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_TFTSERIAL) { - if (::strcmp(key, "Port") == 0) - m_tftSerialPort = value; - else if (::strcmp(key, "Brightness") == 0) - m_tftSerialBrightness = (unsigned int)::atoi(value); - } else if (section == SECTION_HD44780) { - if (::strcmp(key, "Rows") == 0) - m_hd44780Rows = (unsigned int)::atoi(value); - else if (::strcmp(key, "Columns") == 0) - m_hd44780Columns = (unsigned int)::atoi(value); - else if (::strcmp(key, "I2CAddress") == 0) - m_hd44780i2cAddress = (unsigned int)::strtoul(value, NULL, 16); - else if (::strcmp(key, "PWM") == 0) - m_hd44780PWM = ::atoi(value) == 1; - else if (::strcmp(key, "PWMPin") == 0) - m_hd44780PWMPin = (unsigned int)::atoi(value); - else if (::strcmp(key, "PWMBright") == 0) - m_hd44780PWMBright = (unsigned int)::atoi(value); - else if (::strcmp(key, "PWMDim") == 0) - m_hd44780PWMDim = (unsigned int)::atoi(value); - else if (::strcmp(key, "DisplayClock") == 0) - m_hd44780DisplayClock = ::atoi(value) == 1; - else if (::strcmp(key, "UTC") == 0) - m_hd44780UTC = ::atoi(value) == 1; - else if (::strcmp(key, "Pins") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int pin = (unsigned int)::atoi(p); - m_hd44780Pins.push_back(pin); - p = ::strtok(NULL, ",\r\n"); + } else if (section == SECTION_FUSION) { + if (::strcmp(key, "Enable") == 0) + m_fusionEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "LowDeviation") == 0) + m_fusionLowDeviation = ::atoi(value) == 1; + else if (::strcmp(key, "DGID") == 0) { + m_fusionDGIdEnabled = true; + m_fusionDGId = (unsigned int)::atoi(value); + } else if (::strcmp(key, "RemoteGateway") == 0) + m_fusionRemoteGateway = ::atoi(value) == 1; + else if (::strcmp(key, "SelfOnly") == 0) + m_fusionSelfOnly = ::atoi(value) == 1; + else if (::strcmp(key, "TXHang") == 0) + m_fusionTXHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_fusionModeHang = (unsigned int)::atoi(value); + } else if (section == SECTION_P25) { + if (::strcmp(key, "Enable") == 0) + m_p25Enabled = ::atoi(value) == 1; + else if (::strcmp(key, "Id") == 0) + m_p25Id = (unsigned int)::atoi(value); + else if (::strcmp(key, "NAC") == 0) + m_p25NAC = (unsigned int)::strtoul(value, NULL, 16); + else if (::strcmp(key, "OverrideUIDCheck") == 0) + m_p25OverrideUID = ::atoi(value) == 1; + else if (::strcmp(key, "SelfOnly") == 0) + m_p25SelfOnly = ::atoi(value) == 1; + else if (::strcmp(key, "RemoteGateway") == 0) + m_p25RemoteGateway = ::atoi(value) == 1; + else if (::strcmp(key, "TXHang") == 0) + m_p25TXHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_p25ModeHang = (unsigned int)::atoi(value); + } else if (section == SECTION_NXDN) { + if (::strcmp(key, "Enable") == 0) + m_nxdnEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Id") == 0) + m_nxdnId = (unsigned int)::atoi(value); + else if (::strcmp(key, "RAN") == 0) + m_nxdnRAN = (unsigned int)::atoi(value); + else if (::strcmp(key, "SelfOnly") == 0) + m_nxdnSelfOnly = ::atoi(value) == 1; + else if (::strcmp(key, "RemoteGateway") == 0) + m_nxdnRemoteGateway = ::atoi(value) == 1; + else if (::strcmp(key, "TXHang") == 0) + m_nxdnTXHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_nxdnModeHang = (unsigned int)::atoi(value); + } else if (section == SECTION_POCSAG) { + if (::strcmp(key, "Enable") == 0) + m_pocsagEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Frequency") == 0) + m_pocsagFrequency = (unsigned int)::atoi(value); + } else if (section == SECTION_FM) { + if (::strcmp(key, "Enable") == 0) + m_fmEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Callsign") == 0) { + // Convert the callsign to upper case + for (unsigned int i = 0U; value[i] != 0; i++) + value[i] = ::toupper(value[i]); + m_fmCallsign = value; + } else if (::strcmp(key, "CallsignSpeed") == 0) + m_fmCallsignSpeed = (unsigned int)::atoi(value); + else if (::strcmp(key, "CallsignFrequency") == 0) + m_fmCallsignFrequency = (unsigned int)::atoi(value); + else if (::strcmp(key, "CallsignTime") == 0) + m_fmCallsignTime = (unsigned int)::atoi(value); + else if (::strcmp(key, "CallsignHoldoff") == 0) + m_fmCallsignHoldoff = (unsigned int)::atoi(value); + else if (::strcmp(key, "CallsignHighLevel") == 0) + m_fmCallsignHighLevel = float(::atof(value)); + else if (::strcmp(key, "CallsignLowLevel") == 0) + m_fmCallsignLowLevel = float(::atof(value)); + else if (::strcmp(key, "CallsignAtStart") == 0) + m_fmCallsignAtStart = ::atoi(value) == 1; + else if (::strcmp(key, "CallsignAtEnd") == 0) + m_fmCallsignAtEnd = ::atoi(value) == 1; + else if (::strcmp(key, "CallsignAtLatch") == 0) + m_fmCallsignAtLatch = ::atoi(value) == 1; + else if (::strcmp(key, "RFAck") == 0) { + // Convert the ack to upper case + for (unsigned int i = 0U; value[i] != 0; i++) + value[i] = ::toupper(value[i]); + m_fmRFAck = value; + } else if (::strcmp(key, "ExtAck") == 0) { + // Convert the ack to upper case + for (unsigned int i = 0U; value[i] != 0; i++) + value[i] = ::toupper(value[i]); + m_fmExtAck = value; + } else if (::strcmp(key, "AckSpeed") == 0) + m_fmAckSpeed = (unsigned int)::atoi(value); + else if (::strcmp(key, "AckFrequency") == 0) + m_fmAckFrequency = (unsigned int)::atoi(value); + else if (::strcmp(key, "AckMinTime") == 0) + m_fmAckMinTime = (unsigned int)::atoi(value); + else if (::strcmp(key, "AckDelay") == 0) + m_fmAckDelay = (unsigned int)::atoi(value); + else if (::strcmp(key, "AckLevel") == 0) + m_fmAckLevel = float(::atof(value)); + else if (::strcmp(key, "Timeout") == 0) + m_fmTimeout = (unsigned int)::atoi(value); + else if (::strcmp(key, "TimeoutLevel") == 0) + m_fmTimeoutLevel = float(::atof(value)); + else if (::strcmp(key, "CTCSSFrequency") == 0) + m_fmCTCSSFrequency = float(::atof(value)); + else if (::strcmp(key, "CTCSSThreshold") == 0) + m_fmCTCSSHighThreshold = m_fmCTCSSLowThreshold = (unsigned int)::atoi(value); + else if (::strcmp(key, "CTCSSHighThreshold") == 0) + m_fmCTCSSHighThreshold = (unsigned int)::atoi(value); + else if (::strcmp(key, "CTCSSLowThreshold") == 0) + m_fmCTCSSLowThreshold = (unsigned int)::atoi(value); + else if (::strcmp(key, "CTCSSLevel") == 0) + m_fmCTCSSLevel = float(::atof(value)); + else if (::strcmp(key, "KerchunkTime") == 0) + m_fmKerchunkTime = (unsigned int)::atoi(value); + else if (::strcmp(key, "HangTime") == 0) + m_fmHangTime = (unsigned int)::atoi(value); + else if (::strcmp(key, "UseCOS") == 0) + m_fmUseCOS = ::atoi(value) == 1; + else if (::strcmp(key, "COSInvert") == 0) + m_fmCOSInvert = ::atoi(value) == 1; + else if (::strcmp(key, "RFAudioBoost") == 0) + m_fmRFAudioBoost = (unsigned int)::atoi(value); + else if (::strcmp(key, "MaxDevLevel") == 0) + m_fmMaxDevLevel = float(::atof(value)); + else if (::strcmp(key, "ExtAudioBoost") == 0) + m_fmExtAudioBoost = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_fmModeHang = (unsigned int)::atoi(value); + } else if (section == SECTION_DSTAR_NETWORK) { + if (::strcmp(key, "Enable") == 0) + m_dstarNetworkEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "GatewayAddress") == 0) + m_dstarGatewayAddress = value; + else if (::strcmp(key, "GatewayPort") == 0) + m_dstarGatewayPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "LocalPort") == 0) + m_dstarLocalPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_dstarNetworkModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "Debug") == 0) + m_dstarNetworkDebug = ::atoi(value) == 1; + } else if (section == SECTION_DMR_NETWORK) { + if (::strcmp(key, "Enable") == 0) + m_dmrNetworkEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Address") == 0) + m_dmrNetworkAddress = value; + else if (::strcmp(key, "Port") == 0) + m_dmrNetworkPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "Local") == 0) + m_dmrNetworkLocal = (unsigned int)::atoi(value); + else if (::strcmp(key, "Password") == 0) + m_dmrNetworkPassword = value; + else if (::strcmp(key, "Options") == 0) + m_dmrNetworkOptions = value; + else if (::strcmp(key, "Debug") == 0) + m_dmrNetworkDebug = ::atoi(value) == 1; + else if (::strcmp(key, "Jitter") == 0) + m_dmrNetworkJitter = (unsigned int)::atoi(value); + else if (::strcmp(key, "Slot1") == 0) + m_dmrNetworkSlot1 = ::atoi(value) == 1; + else if (::strcmp(key, "Slot2") == 0) + m_dmrNetworkSlot2 = ::atoi(value) == 1; + else if (::strcmp(key, "ModeHang") == 0) + m_dmrNetworkModeHang = (unsigned int)::atoi(value); + } else if (section == SECTION_FUSION_NETWORK) { + if (::strcmp(key, "Enable") == 0) + m_fusionNetworkEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "LocalAddress") == 0) + m_fusionNetworkMyAddress = value; + else if (::strcmp(key, "LocalPort") == 0) + m_fusionNetworkMyPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "GatewayAddress") == 0) + m_fusionNetworkGatewayAddress = value; + else if (::strcmp(key, "GatewayPort") == 0) + m_fusionNetworkGatewayPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_fusionNetworkModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "Debug") == 0) + m_fusionNetworkDebug = ::atoi(value) == 1; + } else if (section == SECTION_P25_NETWORK) { + if (::strcmp(key, "Enable") == 0) + m_p25NetworkEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "GatewayAddress") == 0) + m_p25GatewayAddress = value; + else if (::strcmp(key, "GatewayPort") == 0) + m_p25GatewayPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "LocalPort") == 0) + m_p25LocalPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_p25NetworkModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "Debug") == 0) + m_p25NetworkDebug = ::atoi(value) == 1; + } else if (section == SECTION_NXDN_NETWORK) { + if (::strcmp(key, "Enable") == 0) + m_nxdnNetworkEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "LocalAddress") == 0) + m_nxdnLocalAddress = value; + else if (::strcmp(key, "LocalPort") == 0) + m_nxdnLocalPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "GatewayAddress") == 0) + m_nxdnGatewayAddress = value; + else if (::strcmp(key, "GatewayPort") == 0) + m_nxdnGatewayPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_nxdnNetworkModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "Debug") == 0) + m_nxdnNetworkDebug = ::atoi(value) == 1; + } else if (section == SECTION_POCSAG_NETWORK) { + if (::strcmp(key, "Enable") == 0) + m_pocsagNetworkEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "LocalAddress") == 0) + m_pocsagLocalAddress = value; + else if (::strcmp(key, "LocalPort") == 0) + m_pocsagLocalPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "GatewayAddress") == 0) + m_pocsagGatewayAddress = value; + else if (::strcmp(key, "GatewayPort") == 0) + m_pocsagGatewayPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_pocsagNetworkModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "Debug") == 0) + m_pocsagNetworkDebug = ::atoi(value) == 1; + } else if (section == SECTION_FM_NETWORK) { + if (::strcmp(key, "Enable") == 0) + m_fmNetworkEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "LocalAddress") == 0) + m_fmLocalAddress = value; + else if (::strcmp(key, "LocalPort") == 0) + m_fmLocalPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "GatewayAddress") == 0) + m_fmGatewayAddress = value; + else if (::strcmp(key, "GatewayPort") == 0) + m_fmGatewayPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_fmNetworkModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "Debug") == 0) + m_fmNetworkDebug = ::atoi(value) == 1; + } else if (section == SECTION_TFTSERIAL) { + if (::strcmp(key, "Port") == 0) + m_tftSerialPort = value; + else if (::strcmp(key, "Brightness") == 0) + m_tftSerialBrightness = (unsigned int)::atoi(value); + } else if (section == SECTION_HD44780) { + if (::strcmp(key, "Rows") == 0) + m_hd44780Rows = (unsigned int)::atoi(value); + else if (::strcmp(key, "Columns") == 0) + m_hd44780Columns = (unsigned int)::atoi(value); + else if (::strcmp(key, "I2CAddress") == 0) + m_hd44780i2cAddress = (unsigned int)::strtoul(value, NULL, 16); + else if (::strcmp(key, "PWM") == 0) + m_hd44780PWM = ::atoi(value) == 1; + else if (::strcmp(key, "PWMPin") == 0) + m_hd44780PWMPin = (unsigned int)::atoi(value); + else if (::strcmp(key, "PWMBright") == 0) + m_hd44780PWMBright = (unsigned int)::atoi(value); + else if (::strcmp(key, "PWMDim") == 0) + m_hd44780PWMDim = (unsigned int)::atoi(value); + else if (::strcmp(key, "DisplayClock") == 0) + m_hd44780DisplayClock = ::atoi(value) == 1; + else if (::strcmp(key, "UTC") == 0) + m_hd44780UTC = ::atoi(value) == 1; + else if (::strcmp(key, "Pins") == 0) { + char* p = ::strtok(value, ",\r\n"); + while (p != NULL) { + unsigned int pin = (unsigned int)::atoi(p); + m_hd44780Pins.push_back(pin); + p = ::strtok(NULL, ",\r\n"); + } } + } else if (section == SECTION_NEXTION) { + if (::strcmp(key, "Port") == 0) + m_nextionPort = value; + else if (::strcmp(key, "Brightness") == 0) + m_nextionIdleBrightness = m_nextionBrightness = (unsigned int)::atoi(value); + else if (::strcmp(key, "DisplayClock") == 0) + m_nextionDisplayClock = ::atoi(value) == 1; + else if (::strcmp(key, "UTC") == 0) + m_nextionUTC = ::atoi(value) == 1; + else if (::strcmp(key, "IdleBrightness") == 0) + m_nextionIdleBrightness = (unsigned int)::atoi(value); + else if (::strcmp(key, "ScreenLayout") == 0) + m_nextionScreenLayout = (unsigned int)::atoi(value); + else if (::strcmp(key, "DisplayTempInFahrenheit") == 0) + m_nextionTempInFahrenheit = ::atoi(value) == 1; + } else if (section == SECTION_OLED) { + if (::strcmp(key, "Type") == 0) + m_oledType = (unsigned char)::atoi(value); + else if (::strcmp(key, "Brightness") == 0) + m_oledBrightness = (unsigned char)::atoi(value); + else if (::strcmp(key, "Invert") == 0) + m_oledInvert = ::atoi(value) == 1; + else if (::strcmp(key, "Scroll") == 0) + m_oledScroll = ::atoi(value) == 1; + else if (::strcmp(key, "Rotate") == 0) + m_oledRotate = ::atoi(value) == 1; + else if (::strcmp(key, "LogoScreensaver") == 0) + m_oledLogoScreensaver = ::atoi(value) == 1; + } else if (section == SECTION_LCDPROC) { + if (::strcmp(key, "Address") == 0) + m_lcdprocAddress = value; + else if (::strcmp(key, "Port") == 0) + m_lcdprocPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "LocalPort") == 0) + m_lcdprocLocalPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "DisplayClock") == 0) + m_lcdprocDisplayClock = ::atoi(value) == 1; + else if (::strcmp(key, "UTC") == 0) + m_lcdprocUTC = ::atoi(value) == 1; + else if (::strcmp(key, "DimOnIdle") == 0) + m_lcdprocDimOnIdle = ::atoi(value) == 1; + } else if (section == SECTION_LOCK_FILE) { + if (::strcmp(key, "Enable") == 0) + m_lockFileEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "File") == 0) + m_lockFileName = value; + } else if (section == SECTION_MOBILE_GPS) { + if (::strcmp(key, "Enable") == 0) + m_mobileGPSEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Address") == 0) + m_mobileGPSAddress = value; + else if (::strcmp(key, "Port") == 0) + m_mobileGPSPort = (unsigned int)::atoi(value); + } else if (section == SECTION_REMOTE_CONTROL) { + if (::strcmp(key, "Enable") == 0) + m_remoteControlEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Port") == 0) + m_remoteControlPort = (unsigned int)::atoi(value); } - } else if (section == SECTION_NEXTION) { - if (::strcmp(key, "Port") == 0) - m_nextionPort = value; - else if (::strcmp(key, "Brightness") == 0) - m_nextionIdleBrightness = m_nextionBrightness = (unsigned int)::atoi(value); - else if (::strcmp(key, "DisplayClock") == 0) - m_nextionDisplayClock = ::atoi(value) == 1; - else if (::strcmp(key, "UTC") == 0) - m_nextionUTC = ::atoi(value) == 1; - else if (::strcmp(key, "IdleBrightness") == 0) - m_nextionIdleBrightness = (unsigned int)::atoi(value); - else if (::strcmp(key, "ScreenLayout") == 0) - m_nextionScreenLayout = (unsigned int)::atoi(value); - else if (::strcmp(key, "DisplayTempInFahrenheit") == 0) - m_nextionTempInFahrenheit = ::atoi(value) == 1; - } else if (section == SECTION_OLED) { - if (::strcmp(key, "Type") == 0) - m_oledType = (unsigned char)::atoi(value); - else if (::strcmp(key, "Brightness") == 0) - m_oledBrightness = (unsigned char)::atoi(value); - else if (::strcmp(key, "Invert") == 0) - m_oledInvert = ::atoi(value) == 1; - else if (::strcmp(key, "Scroll") == 0) - m_oledScroll = ::atoi(value) == 1; - else if (::strcmp(key, "Rotate") == 0) - m_oledRotate = ::atoi(value) == 1; - else if (::strcmp(key, "LogoScreensaver") == 0) - m_oledLogoScreensaver = ::atoi(value) == 1; - } else if (section == SECTION_LCDPROC) { - if (::strcmp(key, "Address") == 0) - m_lcdprocAddress = value; - else if (::strcmp(key, "Port") == 0) - m_lcdprocPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "LocalPort") == 0) - m_lcdprocLocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "DisplayClock") == 0) - m_lcdprocDisplayClock = ::atoi(value) == 1; - else if (::strcmp(key, "UTC") == 0) - m_lcdprocUTC = ::atoi(value) == 1; - else if (::strcmp(key, "DimOnIdle") == 0) - m_lcdprocDimOnIdle = ::atoi(value) == 1; - } else if (section == SECTION_LOCK_FILE) { - if (::strcmp(key, "Enable") == 0) - m_lockFileEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "File") == 0) - m_lockFileName = value; - } else if (section == SECTION_MOBILE_GPS) { - if (::strcmp(key, "Enable") == 0) - m_mobileGPSEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Address") == 0) - m_mobileGPSAddress = value; - else if (::strcmp(key, "Port") == 0) - m_mobileGPSPort = (unsigned int)::atoi(value); - } else if (section == SECTION_REMOTE_CONTROL) { - if (::strcmp(key, "Enable") == 0) - m_remoteControlEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Port") == 0) - m_remoteControlPort = (unsigned int)::atoi(value); } - } - ::fclose(fp); + ::fclose(fp); - return true; + return true; } std::string CConf::getCallsign() const @@ -1135,6 +1138,11 @@ std::string CConf::getModemProtocol() const return m_modemProtocol; } +unsigned int CConf::getModemSpeed() const +{ + return m_modemSpeed; +} + unsigned int CConf::getModemAddress() const { return m_modemAddress; diff --git a/Conf.h b/Conf.h index da8cdfc..b455a55 100644 --- a/Conf.h +++ b/Conf.h @@ -71,6 +71,7 @@ public: // The Modem section std::string getModemPort() const; std::string getModemProtocol() const; + unsigned int getModemSpeed() const; unsigned int getModemAddress() const; bool getModemRXInvert() const; bool getModemTXInvert() const; @@ -360,6 +361,7 @@ private: std::string m_modemPort; std::string m_modemProtocol; + unsigned int m_modemSpeed; unsigned int m_modemAddress; bool m_modemRXInvert; bool m_modemTXInvert; diff --git a/Display.cpp b/Display.cpp index 0b47b64..22bc953 100644 --- a/Display.cpp +++ b/Display.cpp @@ -511,7 +511,7 @@ CDisplay* CDisplay::createDisplay(const CConf& conf, CUMP* ump, CModem* modem) if (port == "modem") serial = new CModemSerialPort(modem); else - serial = new CSerialController(port, (type == "TFT Serial") ? SERIAL_9600 : SERIAL_115200); + serial = new CSerialController(port, (type == "TFT Serial") ? 9600U : 115200U); if (type == "TFT Surenoo") display = new CTFTSurenoo(conf.getCallsign(), dmrid, serial, brightness, conf.getDuplex()); @@ -565,11 +565,11 @@ CDisplay* CDisplay::createDisplay(const CConf& conf, CUMP* ump, CModem* modem) display = new CNullDisplay; } } else { - SERIAL_SPEED baudrate = SERIAL_9600; - if (screenLayout==4U) - baudrate = SERIAL_115200; + unsigned int baudrate = 9600U; + if (screenLayout == 4U) + baudrate = 115200U; - LogInfo(" Display baudrate: %u ",baudrate); + LogInfo(" Display baudrate: %u ", baudrate); ISerialPort* serial = new CSerialController(port, baudrate); display = new CNextion(conf.getCallsign(), dmrid, serial, brightness, displayClock, utc, idleBrightness, screenLayout, txFrequency, rxFrequency, displayTempInF, conf.getLocation()); } diff --git a/I2CController.cpp b/I2CController.cpp index 247129c..c6b7c77 100644 --- a/I2CController.cpp +++ b/I2CController.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2002-2004,2007-2011,2013,2014-2017 by Jonathan Naylor G4KLX + * Copyright (C) 2002-2004,2007-2011,2013,2014-2017,2020 by Jonathan Naylor G4KLX * Copyright (C) 1999-2001 by Thomas Sailor HB9JNX * * This program is free software; you can redistribute it and/or modify @@ -30,7 +30,7 @@ #include #include -CI2CController::CI2CController(const std::string& device, SERIAL_SPEED speed, unsigned int address, bool assertRTS) : +CI2CController::CI2CController(const std::string& device, unsigned int speed, unsigned int address, bool assertRTS) : CSerialController(device, speed, assertRTS), m_address(address) { @@ -67,7 +67,7 @@ int CI2CController::write(const unsigned char* buffer, unsigned int length) #include #endif -CI2CController::CI2CController(const std::string& device, SERIAL_SPEED speed, unsigned int address, bool assertRTS) : +CI2CController::CI2CController(const std::string& device, unsigned int speed, unsigned int address, bool assertRTS) : CSerialController(device, speed, assertRTS), m_address(address) { @@ -89,13 +89,13 @@ bool CI2CController::open() } if (::ioctl(m_fd, I2C_TENBIT, 0) < 0) { - LogError("CI2C: failed to set 7bitaddress"); + LogError("I2C: failed to set 7bitaddress"); ::close(m_fd); return false; } if (::ioctl(m_fd, I2C_SLAVE, m_address) < 0) { - LogError("CI2C: Failed to acquire bus access/talk to slave 0x%02X", m_address); + LogError("I2C: Failed to acquire bus access/talk to slave 0x%02X", m_address); ::close(m_fd); return false; } diff --git a/I2CController.h b/I2CController.h index 6e59672..a67db85 100644 --- a/I2CController.h +++ b/I2CController.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2002-2004,2007-2009,2011-2013,2015-2017 by Jonathan Naylor G4KLX + * Copyright (C) 2002-2004,2007-2009,2011-2013,2015-2017,2020 by Jonathan Naylor G4KLX * Copyright (C) 1999-2001 by Thomas Sailor HB9JNX * * This program is free software; you can redistribute it and/or modify @@ -24,7 +24,7 @@ class CI2CController : public CSerialController { public: - CI2CController(const std::string& device, SERIAL_SPEED speed, unsigned int address = 0x22U, bool assertRTS = false); + CI2CController(const std::string& device, unsigned int speed, unsigned int address = 0x22U, bool assertRTS = false); virtual ~CI2CController(); virtual bool open(); diff --git a/MMDVM.ini b/MMDVM.ini index 60bdff4..c031c49 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -45,6 +45,7 @@ Time=24 # Port=/dev/ttyAMA0 Port=\\.\COM4 Protocol=uart +Speed=115200 # Address=0x22 TXInvert=1 RXInvert=0 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 8957c29..9d9a01e 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -1204,6 +1204,7 @@ bool CMMDVMHost::createModem() { std::string port = m_conf.getModemPort(); std::string protocol = m_conf.getModemProtocol(); + unsigned int speed = m_conf.getModemSpeed(); unsigned int address = m_conf.getModemAddress(); bool rxInvert = m_conf.getModemRXInvert(); bool txInvert = m_conf.getModemTXInvert(); @@ -1239,7 +1240,8 @@ bool CMMDVMHost::createModem() LogInfo(" Port: %s", port.c_str()); LogInfo(" Protocol: %s", protocol.c_str()); if (protocol == "i2c") - LogInfo(" i2c Address: %02X", address); + LogInfo(" I2C Address: %02X", address); + LogInfo(" Speed: %u", speed); LogInfo(" RX Invert: %s", rxInvert ? "yes" : "no"); LogInfo(" TX Invert: %s", txInvert ? "yes" : "no"); LogInfo(" PTT Invert: %s", pttInvert ? "yes" : "no"); @@ -1262,7 +1264,7 @@ bool CMMDVMHost::createModem() LogInfo(" TX Frequency: %uHz (%uHz)", txFrequency, txFrequency + txOffset); m_modem = CModem::createModem(port, m_duplex, rxInvert, txInvert, pttInvert, txDelay, dmrDelay, trace, debug); - m_modem->setSerialParams(protocol,address); + m_modem->setSerialParams(protocol, address, speed); m_modem->setModeParams(m_dstarEnabled, m_dmrEnabled, m_ysfEnabled, m_p25Enabled, m_nxdnEnabled, m_pocsagEnabled, m_fmEnabled); m_modem->setLevels(rxLevel, cwIdTXLevel, dstarTXLevel, dmrTXLevel, ysfTXLevel, p25TXLevel, nxdnTXLevel, pocsagTXLevel, fmTXLevel); m_modem->setRFParams(rxFrequency, rxOffset, txFrequency, txOffset, txDCOffset, rxDCOffset, rfLevel, pocsagFrequency); diff --git a/Modem.cpp b/Modem.cpp index 4d5aaab..f648d22 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -221,13 +221,13 @@ CModem::~CModem() delete[] m_buffer; } -void CModem::setSerialParams(const std::string& protocol, unsigned int address) +void CModem::setSerialParams(const std::string& protocol, unsigned int address, unsigned int speed) { // Create the serial controller instance according the protocol specified in conf. if (protocol == "i2c") - m_serial = new CI2CController(m_port, SERIAL_115200, address, true); + m_serial = new CI2CController(m_port, speed, address, true); else - m_serial = new CSerialController(m_port, SERIAL_115200, true); + m_serial = new CSerialController(m_port, speed, true); } void CModem::setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) diff --git a/Modem.h b/Modem.h index c6bd0c6..48d59b4 100644 --- a/Modem.h +++ b/Modem.h @@ -37,7 +37,7 @@ public: CModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool trace, bool debug); virtual ~CModem(); - virtual void setSerialParams(const std::string& protocol, unsigned int address); + virtual void setSerialParams(const std::string& protocol, unsigned int address, unsigned int speed); virtual void setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency); virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled); virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel); diff --git a/SerialController.cpp b/SerialController.cpp index cfb448d..aacc6c6 100644 --- a/SerialController.cpp +++ b/SerialController.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2002-2004,2007-2011,2013,2014-2017,2019 by Jonathan Naylor G4KLX + * Copyright (C) 2002-2004,2007-2011,2013,2014-2017,2019,2020 by Jonathan Naylor G4KLX * Copyright (C) 1999-2001 by Thomas Sailor HB9JNX * * This program is free software; you can redistribute it and/or modify @@ -23,12 +23,11 @@ #include #include -#include - #if defined(_WIN32) || defined(_WIN64) #include #include #else +#include #include #include #include @@ -40,7 +39,7 @@ #if defined(_WIN32) || defined(_WIN64) -CSerialController::CSerialController(const std::string& device, SERIAL_SPEED speed, bool assertRTS) : +CSerialController::CSerialController(const std::string& device, unsigned int speed, bool assertRTS) : m_device(device), m_speed(speed), m_assertRTS(assertRTS), @@ -221,7 +220,7 @@ void CSerialController::close() #else -CSerialController::CSerialController(const std::string& device, SERIAL_SPEED speed, bool assertRTS) : +CSerialController::CSerialController(const std::string& device, unsigned int speed, bool assertRTS) : m_device(device), m_speed(speed), m_assertRTS(assertRTS), @@ -273,40 +272,40 @@ bool CSerialController::open() #endif switch (m_speed) { - case SERIAL_1200: + case 1200U: ::cfsetospeed(&termios, B1200); ::cfsetispeed(&termios, B1200); break; - case SERIAL_2400: + case 2400U: ::cfsetospeed(&termios, B2400); ::cfsetispeed(&termios, B2400); break; - case SERIAL_4800: + case 4800U: ::cfsetospeed(&termios, B4800); ::cfsetispeed(&termios, B4800); break; - case SERIAL_9600: + case 9600U: ::cfsetospeed(&termios, B9600); ::cfsetispeed(&termios, B9600); break; - case SERIAL_19200: + case 19200U: ::cfsetospeed(&termios, B19200); ::cfsetispeed(&termios, B19200); break; - case SERIAL_38400: + case 38400U: ::cfsetospeed(&termios, B38400); ::cfsetispeed(&termios, B38400); break; - case SERIAL_115200: + case 115200U: ::cfsetospeed(&termios, B115200); ::cfsetispeed(&termios, B115200); break; - case SERIAL_230400: + case 230400U: ::cfsetospeed(&termios, B230400); ::cfsetispeed(&termios, B230400); break; default: - LogError("Unsupported serial port speed - %d", int(m_speed)); + LogError("Unsupported serial port speed - %u", m_speed); ::close(m_fd); return false; } diff --git a/SerialController.h b/SerialController.h index 6dfc461..c7f97eb 100644 --- a/SerialController.h +++ b/SerialController.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2002-2004,2007-2009,2011-2013,2015-2017 by Jonathan Naylor G4KLX + * Copyright (C) 2002-2004,2007-2009,2011-2013,2015-2017,2020 by Jonathan Naylor G4KLX * Copyright (C) 1999-2001 by Thomas Sailor HB9JNX * * This program is free software; you can redistribute it and/or modify @@ -28,21 +28,9 @@ #include #endif -enum SERIAL_SPEED { - SERIAL_1200 = 1200, - SERIAL_2400 = 2400, - SERIAL_4800 = 4800, - SERIAL_9600 = 9600, - SERIAL_19200 = 19200, - SERIAL_38400 = 38400, - SERIAL_76800 = 76800, - SERIAL_115200 = 115200, - SERIAL_230400 = 230400 -}; - class CSerialController : public ISerialPort { public: - CSerialController(const std::string& device, SERIAL_SPEED speed, bool assertRTS = false); + CSerialController(const std::string& device, unsigned int speed, bool assertRTS = false); virtual ~CSerialController(); virtual bool open(); @@ -59,7 +47,7 @@ public: protected: std::string m_device; - SERIAL_SPEED m_speed; + unsigned int m_speed; bool m_assertRTS; #if defined(_WIN32) || defined(_WIN64) HANDLE m_handle; diff --git a/UMP.cpp b/UMP.cpp index f843322..4aca606 100644 --- a/UMP.cpp +++ b/UMP.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2016 by Jonathan Naylor G4KLX +* Copyright (C) 2016,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -41,7 +41,7 @@ const unsigned char UMP_STATUS = 0x50U; const unsigned int BUFFER_LENGTH = 255U; CUMP::CUMP(const std::string& port) : -m_serial(port, SERIAL_115200), +m_serial(port, 115200U), m_open(false), m_buffer(NULL), m_length(0U), diff --git a/Version.h b/Version.h index c48a90e..46f5c4f 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200512"; +const char* VERSION = "20200523"; #endif From 0b185a0900e40b1ac8686551bb1667cc1c63815d Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 23 May 2020 16:49:21 +0100 Subject: [PATCH 031/163] Clean up the I2C controller code. --- I2CController.cpp | 63 ++++++++++----------------------------- I2CController.h | 14 +++++++-- MMDVMHost.cpp | 5 +++- MMDVMHost.vcxproj | 2 -- MMDVMHost.vcxproj.filters | 6 ---- Modem.cpp | 7 ++++- Modem.h | 4 +-- 7 files changed, 38 insertions(+), 63 deletions(-) diff --git a/I2CController.cpp b/I2CController.cpp index c6b7c77..9886443 100644 --- a/I2CController.cpp +++ b/I2CController.cpp @@ -17,6 +17,8 @@ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ +#if defined(__linux__) + #include "I2CController.h" #include "Log.h" @@ -24,52 +26,18 @@ #include #include - -#if defined(_WIN32) || defined(_WIN64) - -#include -#include - -CI2CController::CI2CController(const std::string& device, unsigned int speed, unsigned int address, bool assertRTS) : -CSerialController(device, speed, assertRTS), -m_address(address) -{ -} - -CI2CController::~CI2CController() -{ -} - -bool CI2CController::open() -{ - return CSerialController::open(); -} - -int CI2CController::read(unsigned char* buffer, unsigned int length) -{ - return CSerialController::read(buffer, length); -} - -int CI2CController::write(const unsigned char* buffer, unsigned int length) -{ - return CSerialController::write(buffer, length); -} - -#else - #include #include #include #include #include #include -#if defined(__linux__) #include -#endif -CI2CController::CI2CController(const std::string& device, unsigned int speed, unsigned int address, bool assertRTS) : -CSerialController(device, speed, assertRTS), -m_address(address) +CI2CController::CI2CController(const std::string& device, unsigned int address) : +m_device(device), +m_address(address), +m_fd(-1) { } @@ -81,7 +49,6 @@ bool CI2CController::open() { assert(m_fd == -1); -#if defined(__linux__) m_fd = ::open(m_device.c_str(), O_RDWR); if (m_fd < 0) { LogError("Cannot open device - %s", m_device.c_str()); @@ -99,9 +66,6 @@ bool CI2CController::open() ::close(m_fd); return false; } -#else - #warning "I2C controller supports Linux only" -#endif return true; } @@ -117,7 +81,6 @@ int CI2CController::read(unsigned char* buffer, unsigned int length) unsigned int offset = 0U; while (offset < length) { -#if defined(__linux__) ssize_t n = ::read(m_fd, buffer + offset, 1U); if (n < 0) { if (errno != EAGAIN) { @@ -128,7 +91,6 @@ int CI2CController::read(unsigned char* buffer, unsigned int length) if (n > 0) offset += n; -#endif } return length; @@ -144,10 +106,7 @@ int CI2CController::write(const unsigned char* buffer, unsigned int length) unsigned int ptr = 0U; while (ptr < length) { - ssize_t n = 0U; -#if defined(__linux__) - n = ::write(m_fd, buffer + ptr, 1U); -#endif + ssize_t n = ::write(m_fd, buffer + ptr, 1U); if (n < 0) { if (errno != EAGAIN) { LogError("Error returned from write(), errno=%d", errno); @@ -162,4 +121,12 @@ int CI2CController::write(const unsigned char* buffer, unsigned int length) return length; } +void CI2CController::close() +{ + assert(m_fd != -1); + + ::close(m_fd); + m_fd = -1; +} + #endif diff --git a/I2CController.h b/I2CController.h index a67db85..14bc81e 100644 --- a/I2CController.h +++ b/I2CController.h @@ -20,11 +20,13 @@ #ifndef I2CController_H #define I2CController_H -#include "SerialController.h" +#if defined(__linux__) -class CI2CController : public CSerialController { +#include "SerialPort.h" + +class CI2CController : public ISerialPort { public: - CI2CController(const std::string& device, unsigned int speed, unsigned int address = 0x22U, bool assertRTS = false); + CI2CController(const std::string& device, unsigned int address = 0x22U); virtual ~CI2CController(); virtual bool open(); @@ -33,8 +35,14 @@ public: virtual int write(const unsigned char* buffer, unsigned int length); + virtual void close(); + private: + std::string m_device; unsigned int m_address; + int m_fd; }; #endif + +#endif diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 9d9a01e..c28f17e 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -1238,10 +1238,13 @@ bool CMMDVMHost::createModem() LogInfo("Modem Parameters"); LogInfo(" Port: %s", port.c_str()); +#if defined(__linux__) LogInfo(" Protocol: %s", protocol.c_str()); if (protocol == "i2c") LogInfo(" I2C Address: %02X", address); - LogInfo(" Speed: %u", speed); + else +#endif + LogInfo(" Speed: %u", speed); LogInfo(" RX Invert: %s", rxInvert ? "yes" : "no"); LogInfo(" TX Invert: %s", txInvert ? "yes" : "no"); LogInfo(" PTT Invert: %s", pttInvert ? "yes" : "no"); diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index 63ba164..3ed61f9 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -187,7 +187,6 @@ - @@ -285,7 +284,6 @@ - diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index 8dc6f12..704bf0b 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -275,9 +275,6 @@ Header Files - - Header Files - Header Files @@ -547,9 +544,6 @@ Source Files - - Source Files - Source Files diff --git a/Modem.cpp b/Modem.cpp index f648d22..cb2c18c 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -16,7 +16,10 @@ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ +#include "SerialController.h" +#if defined(__linux__) #include "I2CController.h" +#endif #include "DStarDefines.h" #include "DMRDefines.h" #include "YSFDefines.h" @@ -224,9 +227,11 @@ CModem::~CModem() void CModem::setSerialParams(const std::string& protocol, unsigned int address, unsigned int speed) { // Create the serial controller instance according the protocol specified in conf. +#if defined(__linux__) if (protocol == "i2c") - m_serial = new CI2CController(m_port, speed, address, true); + m_serial = new CI2CController(m_port, address); else +#endif m_serial = new CSerialController(m_port, speed, true); } diff --git a/Modem.h b/Modem.h index 48d59b4..4c77d9f 100644 --- a/Modem.h +++ b/Modem.h @@ -19,7 +19,7 @@ #ifndef MODEM_H #define MODEM_H -#include "SerialController.h" +#include "SerialPort.h" #include "RingBuffer.h" #include "Defines.h" #include "Timer.h" @@ -156,7 +156,7 @@ private: bool m_fmEnabled; int m_rxDCOffset; int m_txDCOffset; - CSerialController* m_serial; + ISerialPort* m_serial; unsigned char* m_buffer; unsigned int m_length; unsigned int m_offset; From af34d9abc3de1cae6f19fe5934bbca12bb91d5cb Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 23 May 2020 16:54:11 +0100 Subject: [PATCH 032/163] Linux compile fix. --- I2CController.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/I2CController.h b/I2CController.h index 14bc81e..281c587 100644 --- a/I2CController.h +++ b/I2CController.h @@ -24,6 +24,8 @@ #include "SerialPort.h" +#include + class CI2CController : public ISerialPort { public: CI2CController(const std::string& device, unsigned int address = 0x22U); From ad843e72835e05f2a2b6b60ad42c7ec21770cd6b Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Wed, 20 May 2020 18:07:57 +0200 Subject: [PATCH 033/163] do not write EOT when network is not set --- FMControl.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index 18d90c5..0867914 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -41,6 +41,9 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) assert(data != NULL); assert(length > 0U); + if (m_network == NULL) + return true; + if (data[0U] == TAG_HEADER) return true; @@ -50,8 +53,6 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) if (data[0U] != TAG_DATA) return false; - if (m_network == NULL) - return true; m_incomingRFAudio.addData(data + 1U, length - 1U); unsigned int bufferLength = m_incomingRFAudio.dataSize(); From ed3299a51376a46f5c9c8442c534ff19ea1437fc Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 24 May 2020 07:43:22 +0200 Subject: [PATCH 034/163] Fix unpack, add audio dump --- FMControl.cpp | 37 ++++++++++++++++++++++++++++--------- FMControl.h | 5 +++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index 0867914..5405a11 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -20,6 +20,11 @@ #include +#if defined(DUMP_RF_AUDIO) +#include +#include +#endif + const float EMPHASIS_GAIN_DB = 0.0F; //Gain needs to be the same for pre an deeemphasis const unsigned int FM_MASK = 0x00000FFFU; @@ -38,6 +43,11 @@ CFMControl::~CFMControl() bool CFMControl::writeModem(const unsigned char* data, unsigned int length) { +#if defined(DUMP_RF_AUDIO) + std::ofstream audiofile; + audiofile.open("audiodump.bin", std::ios::out | std::ios::app | std::ios::binary); +#endif + assert(data != NULL); assert(length > 0U); @@ -69,28 +79,33 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) // Unpack the serial data into float values. for (unsigned int i = 0U; i < bufferLength; i += 3U) { - unsigned short sample1 = 0U; - unsigned short sample2 = 0U; + short sample1 = 0U; + short sample2 = 0U; unsigned int pack = 0U; unsigned char* packPointer = (unsigned char*)&pack; - packPointer[1U] = bufferData[i]; - packPointer[2U] = bufferData[i + 1U]; - packPointer[3U] = bufferData[i + 2U]; + packPointer[0U] = bufferData[i]; + packPointer[1U] = bufferData[i + 1U]; + packPointer[2U] = bufferData[i + 2U]; - sample2 = short(pack & FM_MASK); - sample1 = short(pack >> 12); + //extract unsigned 12 bit samples to 16 bit signed + sample2 = short(int(pack & FM_MASK) - 2048); + sample1 = short(int(pack >> 12) - 2048); // Convert from unsigned short (0 - +4095) to float (-1.0 - +1.0) - samples[nSamples++] = (float(sample1) - 2048.0F) / 2048.0F; - samples[nSamples++] = (float(sample2) - 2048.0F) / 2048.0F; + samples[nSamples++] = float(sample1) / 2048.0F; + samples[nSamples++] = float(sample2) / 2048.0F; } //De-emphasise the data and any other processing needed (maybe a low-pass filter to remove the CTCSS) for (unsigned int i = 0U; i < nSamples; i++) samples[i] = m_deemphasis.filter(samples[i]); +#if defined(DUMP_RF_AUDIO) + audiofile.write((char*)(void*)samples, nSamples * sizeof(float)); +#endif + unsigned short out[170U]; // 85 * 2 unsigned int nOut = 0U; @@ -104,6 +119,10 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) return m_network->writeData((unsigned char*)out, nOut); } +#if defined(DUMP_RF_AUDIO) + audiofile.close(); +#endif + return true; } diff --git a/FMControl.h b/FMControl.h index 5a4a8cd..9067d89 100644 --- a/FMControl.h +++ b/FMControl.h @@ -23,6 +23,11 @@ #include "Defines.h" #include "IIRDirectForm1Filter.h" +// Uncomment this to dump audio to a raw audio file +// The file will be written in same folder as executable +// Toplay the file : aplay -f FLOAT_LE -c1 -r8000 -t raw audiodump.bin +//#define DUMP_RF_AUDIO + class CFMControl { public: CFMControl(CFMNetwork* network); From c0a9bb81a30063c046ec80e5c76721094882785c Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 24 May 2020 08:00:27 +0200 Subject: [PATCH 035/163] Fix sample packing --- FMControl.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index 5405a11..79dbdcb 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -167,9 +167,9 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) pack = ((unsigned int)sample1) << 12; pack |= sample2; - data[j] = packPointer[1U]; - data[j + 1U] = packPointer[2U]; - data[j + 2U] = packPointer[3U]; + data[j] = packPointer[0U]; + data[j + 1U] = packPointer[1U]; + data[j + 2U] = packPointer[2U]; } return j;//return the number of bytes written From 8b31cb34fff4134f919919165e7b2c8bb7be58b8 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 24 May 2020 10:47:11 +0200 Subject: [PATCH 036/163] Use stdio instead of iostream, fix file not properly closed --- FMControl.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index 79dbdcb..baa429c 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -21,8 +21,7 @@ #include #if defined(DUMP_RF_AUDIO) -#include -#include +#include #endif const float EMPHASIS_GAIN_DB = 0.0F; //Gain needs to be the same for pre an deeemphasis @@ -43,11 +42,6 @@ CFMControl::~CFMControl() bool CFMControl::writeModem(const unsigned char* data, unsigned int length) { -#if defined(DUMP_RF_AUDIO) - std::ofstream audiofile; - audiofile.open("audiodump.bin", std::ios::out | std::ios::app | std::ios::binary); -#endif - assert(data != NULL); assert(length > 0U); @@ -70,6 +64,10 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) bufferLength = 255U; if (bufferLength >= 3U) { +#if defined(DUMP_RF_AUDIO) + FILE * audiofile = fopen("./audiodump.bin", "ab"); +#endif + bufferLength = bufferLength - bufferLength % 3U; //round down to nearest multiple of 3 unsigned char bufferData[255U]; m_incomingRFAudio.getData(bufferData, bufferLength); @@ -103,7 +101,8 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) samples[i] = m_deemphasis.filter(samples[i]); #if defined(DUMP_RF_AUDIO) - audiofile.write((char*)(void*)samples, nSamples * sizeof(float)); + if(audiofile != NULL) + fwrite(samples, sizeof(float), nSamples, audiofile); #endif unsigned short out[170U]; // 85 * 2 @@ -116,13 +115,14 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) out[nOut++] = (sample >> 0) & 0xFFU; } +#if defined(DUMP_RF_AUDIO) + if(audiofile != NULL) + fclose(audiofile); +#endif + return m_network->writeData((unsigned char*)out, nOut); } -#if defined(DUMP_RF_AUDIO) - audiofile.close(); -#endif - return true; } From 1caffc1dad5702a5d1ecdbc418349383fe4a718c Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 24 May 2020 14:26:24 +0100 Subject: [PATCH 037/163] Small cleanups. --- FMControl.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index baa429c..55477cd 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -21,7 +21,7 @@ #include #if defined(DUMP_RF_AUDIO) -#include +#include #endif const float EMPHASIS_GAIN_DB = 0.0F; //Gain needs to be the same for pre an deeemphasis @@ -47,7 +47,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) if (m_network == NULL) return true; - + if (data[0U] == TAG_HEADER) return true; @@ -57,7 +57,6 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) if (data[0U] != TAG_DATA) return false; - m_incomingRFAudio.addData(data + 1U, length - 1U); unsigned int bufferLength = m_incomingRFAudio.dataSize(); if (bufferLength > 255U) @@ -65,9 +64,8 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) if (bufferLength >= 3U) { #if defined(DUMP_RF_AUDIO) - FILE * audiofile = fopen("./audiodump.bin", "ab"); + FILE* audiofile = ::fopen("./audiodump.bin", "ab"); #endif - bufferLength = bufferLength - bufferLength % 3U; //round down to nearest multiple of 3 unsigned char bufferData[255U]; m_incomingRFAudio.getData(bufferData, bufferLength); @@ -101,10 +99,9 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) samples[i] = m_deemphasis.filter(samples[i]); #if defined(DUMP_RF_AUDIO) - if(audiofile != NULL) - fwrite(samples, sizeof(float), nSamples, audiofile); + if (audiofile != NULL) + ::fwrite(samples, sizeof(float), nSamples, audiofile); #endif - unsigned short out[170U]; // 85 * 2 unsigned int nOut = 0U; @@ -116,10 +113,11 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) } #if defined(DUMP_RF_AUDIO) - if(audiofile != NULL) - fclose(audiofile); + if (audiofile != NULL) { + ::fclose(audiofile); + audiofile = NULL; + } #endif - return m_network->writeData((unsigned char*)out, nOut); } @@ -134,7 +132,7 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) if (m_network == NULL) return 0U; - if(space > 252U) + if (space > 252U) space = 252U; unsigned char netData[168U];//84 * 2 modem can handle up to 84 samples (252 bytes) at a time @@ -150,7 +148,7 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) samples[nSamples++] = (float(sample) / 32767.0F) - 1.0F; } - //Pre-emphasise the data and other stuff. + // Pre-emphasise the data and other stuff. for (unsigned int i = 0U; i < nSamples; i++) samples[i] = m_preemphasis.filter(samples[i]); From 02d1e2f0ef89f7c9031a93c274ed599b6b3158f7 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 24 May 2020 19:07:59 +0200 Subject: [PATCH 038/163] Add CTCSS removal --- FMControl.cpp | 33 +++++++++++++++++++++++++-------- FMControl.h | 9 ++++++--- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index baa429c..72188dd 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -24,20 +24,36 @@ #include #endif -const float EMPHASIS_GAIN_DB = 0.0F; //Gain needs to be the same for pre an deeemphasis -const unsigned int FM_MASK = 0x00000FFFU; +const float EMPHASIS_GAIN_DB = 0.0F; //Gain needs to be the same for pre an deeemphasis +const float FILTER_GAIN_DB = 0.0F; +const unsigned int FM_MASK = 0x00000FFFU; CFMControl::CFMControl(CFMNetwork* network) : m_network(network), m_enabled(false), m_incomingRFAudio(1600U, "Incoming RF FM Audio"), -m_preemphasis(0.3889703155F, -0.32900055326F, 0.0F, 1.0F, 0.2820291817F, 0.0F, EMPHASIS_GAIN_DB), -m_deemphasis(1.0F, 0.2820291817F, 0.0F, 0.3889703155F, -0.32900055326F, 0.0F, EMPHASIS_GAIN_DB) +m_preemphasis (NULL), +m_deemphasis (NULL), +m_filterStage1(NULL), +m_filterStage2(NULL), +m_filterStage3(NULL) { + m_preemphasis = new CIIRDirectForm1Filter(0.38897032f, -0.32900053f, 0.0f, 1.0f, 0.28202918f, 0.0f, EMPHASIS_GAIN_DB); + m_deemphasis = new CIIRDirectForm1Filter(1.0f,0.28202918f, 0.0f, 0.38897032f, -0.32900053f, 0.0f, EMPHASIS_GAIN_DB); + + m_filterStage1 = new CIIRDirectForm1Filter(0.29495028f, 0.0f, -0.29495028f, 1.0f, -0.61384624f, -0.057158668f, FILTER_GAIN_DB); + m_filterStage2 = new CIIRDirectForm1Filter(1.0f, 2.0f, 1.0f, 1.0f, 0.9946123f, 0.6050482f, FILTER_GAIN_DB); + m_filterStage3 = new CIIRDirectForm1Filter(1.0f, -2.0f, 1.0f, 1.0f, -1.8414584f, 0.8804949f, FILTER_GAIN_DB); } CFMControl::~CFMControl() { + delete m_preemphasis ; + delete m_deemphasis ; + + delete m_filterStage1; + delete m_filterStage2; + delete m_filterStage3; } bool CFMControl::writeModem(const unsigned char* data, unsigned int length) @@ -57,7 +73,6 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) if (data[0U] != TAG_DATA) return false; - m_incomingRFAudio.addData(data + 1U, length - 1U); unsigned int bufferLength = m_incomingRFAudio.dataSize(); if (bufferLength > 255U) @@ -97,8 +112,10 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) } //De-emphasise the data and any other processing needed (maybe a low-pass filter to remove the CTCSS) - for (unsigned int i = 0U; i < nSamples; i++) - samples[i] = m_deemphasis.filter(samples[i]); + for (unsigned int i = 0U; i < nSamples; i++) { + samples[i] = m_deemphasis->filter(samples[i]); + samples[i] = m_filterStage3->filter(m_filterStage2->filter(m_filterStage1->filter(samples[i]))); + } #if defined(DUMP_RF_AUDIO) if(audiofile != NULL) @@ -152,7 +169,7 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) //Pre-emphasise the data and other stuff. for (unsigned int i = 0U; i < nSamples; i++) - samples[i] = m_preemphasis.filter(samples[i]); + samples[i] = m_preemphasis->filter(samples[i]); // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) unsigned int pack = 0U; diff --git a/FMControl.h b/FMControl.h index 9067d89..c7af709 100644 --- a/FMControl.h +++ b/FMControl.h @@ -26,7 +26,7 @@ // Uncomment this to dump audio to a raw audio file // The file will be written in same folder as executable // Toplay the file : aplay -f FLOAT_LE -c1 -r8000 -t raw audiodump.bin -//#define DUMP_RF_AUDIO +// #define DUMP_RF_AUDIO class CFMControl { public: @@ -45,8 +45,11 @@ private: CFMNetwork* m_network; bool m_enabled; CRingBuffer m_incomingRFAudio; - CIIRDirectForm1Filter m_preemphasis; - CIIRDirectForm1Filter m_deemphasis; + CIIRDirectForm1Filter * m_preemphasis; + CIIRDirectForm1Filter * m_deemphasis; + CIIRDirectForm1Filter * m_filterStage1; + CIIRDirectForm1Filter * m_filterStage2; + CIIRDirectForm1Filter * m_filterStage3; }; #endif From a28aa7792749ecf49550e8f4ad5f0ed7514e25ae Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 30 May 2020 07:31:48 +0200 Subject: [PATCH 039/163] Add deemphasis and remove CTCSS, emphasis temp deactivated --- FMControl.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index 72188dd..fadf005 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -41,6 +41,7 @@ m_filterStage3(NULL) m_preemphasis = new CIIRDirectForm1Filter(0.38897032f, -0.32900053f, 0.0f, 1.0f, 0.28202918f, 0.0f, EMPHASIS_GAIN_DB); m_deemphasis = new CIIRDirectForm1Filter(1.0f,0.28202918f, 0.0f, 0.38897032f, -0.32900053f, 0.0f, EMPHASIS_GAIN_DB); + //cheby type 1 0.2dB cheby type 1 3rd order 300-2700Hz fs=8000 m_filterStage1 = new CIIRDirectForm1Filter(0.29495028f, 0.0f, -0.29495028f, 1.0f, -0.61384624f, -0.057158668f, FILTER_GAIN_DB); m_filterStage2 = new CIIRDirectForm1Filter(1.0f, 2.0f, 1.0f, 1.0f, 0.9946123f, 0.6050482f, FILTER_GAIN_DB); m_filterStage3 = new CIIRDirectForm1Filter(1.0f, -2.0f, 1.0f, 1.0f, -1.8414584f, 0.8804949f, FILTER_GAIN_DB); @@ -111,7 +112,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) samples[nSamples++] = float(sample2) / 2048.0F; } - //De-emphasise the data and any other processing needed (maybe a low-pass filter to remove the CTCSS) + //De-emphasise the data and remove CTCSS for (unsigned int i = 0U; i < nSamples; i++) { samples[i] = m_deemphasis->filter(samples[i]); samples[i] = m_filterStage3->filter(m_filterStage2->filter(m_filterStage1->filter(samples[i]))); @@ -164,12 +165,12 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) // Convert the unsigned 16-bit data (+65535 - 0) to float (+1.0 - -1.0) for (unsigned int i = 0U; i < length; i += 2U) { unsigned short sample = (netData[i + 0U] << 8) | netData[i + 1U]; - samples[nSamples++] = (float(sample) / 32767.0F) - 1.0F; + samples[nSamples++] = (float(sample) / 32768.0F) - 1.0F; } //Pre-emphasise the data and other stuff. - for (unsigned int i = 0U; i < nSamples; i++) - samples[i] = m_preemphasis->filter(samples[i]); + //for (unsigned int i = 0U; i < nSamples; i++) + // samples[i] = m_preemphasis->filter(samples[i]); // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) unsigned int pack = 0U; @@ -180,7 +181,7 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) unsigned short sample1 = (unsigned short)((samples[i] + 1.0F) * 2048.0F + 0.5F); unsigned short sample2 = (unsigned short)((samples[i + 1] + 1.0F) * 2048.0F + 0.5F); - pack = 0; + pack = 0U; pack = ((unsigned int)sample1) << 12; pack |= sample2; From 0b8a9a1a4cef43202c42eb1f88c31f51d96f7569 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 30 May 2020 07:32:09 +0200 Subject: [PATCH 040/163] Increase FM TX Buffer size --- Modem.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modem.cpp b/Modem.cpp index cb2c18c..3a6974d 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -160,7 +160,7 @@ m_rxNXDNData(1000U, "Modem RX NXDN"), m_txNXDNData(1000U, "Modem TX NXDN"), m_txPOCSAGData(1000U, "Modem TX POCSAG"), m_rxFMData(1000U, "Modem RX FM"), -m_txFMData(1000U, "Modem TX FM"), +m_txFMData(5000U, "Modem TX FM"), m_rxTransparentData(1000U, "Modem RX Transparent"), m_txTransparentData(1000U, "Modem TX Transparent"), m_sendTransparentDataFrameType(0U), @@ -232,7 +232,7 @@ void CModem::setSerialParams(const std::string& protocol, unsigned int address, m_serial = new CI2CController(m_port, address); else #endif - m_serial = new CSerialController(m_port, speed, true); + m_serial = new CSerialController(m_port, speed, false); } void CModem::setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) From 75d5083f8e20ad32a7e52c720a6c0fa86e0f3ef3 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 30 May 2020 19:59:15 +0200 Subject: [PATCH 041/163] fixe network byte ordering, change audio dump to dumep 16 bit samples --- FMControl.cpp | 13 ++++++------- FMControl.h | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index fadf005..1e460b7 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -118,21 +118,20 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) samples[i] = m_filterStage3->filter(m_filterStage2->filter(m_filterStage1->filter(samples[i]))); } -#if defined(DUMP_RF_AUDIO) - if(audiofile != NULL) - fwrite(samples, sizeof(float), nSamples, audiofile); -#endif - unsigned short out[170U]; // 85 * 2 unsigned int nOut = 0U; // Repack the data (8-bit unsigned values containing unsigned 16-bit data) for (unsigned int i = 0U; i < nSamples; i++) { unsigned short sample = (unsigned short)((samples[i] + 1.0F) * 32767.0F + 0.5F); - out[nOut++] = (sample >> 8) & 0xFFU; - out[nOut++] = (sample >> 0) & 0xFFU; + out[nOut++] = ((sample >> 8) & 0x00FFU) | ((sample << 8) & 0xFF00U);//change endianess to network order, transmit MSB first } +#if defined(DUMP_RF_AUDIO) + if(audiofile != NULL) + fwrite(out, sizeof(unsigned short), nOut, audiofile); +#endif + #if defined(DUMP_RF_AUDIO) if(audiofile != NULL) fclose(audiofile); diff --git a/FMControl.h b/FMControl.h index c7af709..1647010 100644 --- a/FMControl.h +++ b/FMControl.h @@ -25,7 +25,7 @@ // Uncomment this to dump audio to a raw audio file // The file will be written in same folder as executable -// Toplay the file : aplay -f FLOAT_LE -c1 -r8000 -t raw audiodump.bin +// Toplay the file : ffplay -autoexit -f u16be -ar 8000 audiodump.bin // #define DUMP_RF_AUDIO class CFMControl { From c2187fd6243683110f96161a993ac2fb6bbc2935 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 30 May 2020 20:19:46 +0200 Subject: [PATCH 042/163] Add 460800 serial speed --- SerialController.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SerialController.cpp b/SerialController.cpp index aacc6c6..970501a 100644 --- a/SerialController.cpp +++ b/SerialController.cpp @@ -304,6 +304,10 @@ bool CSerialController::open() ::cfsetospeed(&termios, B230400); ::cfsetispeed(&termios, B230400); break; + case 460800U: + ::cfsetospeed(&termios, B460800); + ::cfsetispeed(&termios, B460800); + break; default: LogError("Unsupported serial port speed - %u", m_speed); ::close(m_fd); From 409e0de721c1ba615019aa3058cb1fde8c1b421e Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 31 May 2020 07:03:01 +0200 Subject: [PATCH 043/163] Send correct number of bytes --- FMControl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FMControl.cpp b/FMControl.cpp index 1e460b7..b07206a 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -137,7 +137,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) fclose(audiofile); #endif - return m_network->writeData((unsigned char*)out, nOut); + return m_network->writeData((unsigned char*)out, nOut * sizeof(unsigned short)); } return true; From 8d4241d154e5e4d308d2f5087c04e7753c7ec437 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 31 May 2020 08:20:39 +0200 Subject: [PATCH 044/163] Reenable RTS --- Modem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modem.cpp b/Modem.cpp index 3a6974d..e4dcb79 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -232,7 +232,7 @@ void CModem::setSerialParams(const std::string& protocol, unsigned int address, m_serial = new CI2CController(m_port, address); else #endif - m_serial = new CSerialController(m_port, speed, false); + m_serial = new CSerialController(m_port, speed, true); } void CModem::setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) From eef364b1a97aec7749a9fcb32f35d66349cf092d Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 31 May 2020 08:38:17 +0200 Subject: [PATCH 045/163] Enable Pre emphasis --- FMControl.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index b07206a..f1dc045 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -24,9 +24,10 @@ #include #endif -const float EMPHASIS_GAIN_DB = 0.0F; //Gain needs to be the same for pre an deeemphasis -const float FILTER_GAIN_DB = 0.0F; -const unsigned int FM_MASK = 0x00000FFFU; +const float DEEMPHASIS_GAIN_DB = 0.0F; +const float PREEMPHASIS_GAIN_DB = 30.0F; +const float FILTER_GAIN_DB = 0.0F; +const unsigned int FM_MASK = 0x00000FFFU; CFMControl::CFMControl(CFMNetwork* network) : m_network(network), @@ -38,8 +39,8 @@ m_filterStage1(NULL), m_filterStage2(NULL), m_filterStage3(NULL) { - m_preemphasis = new CIIRDirectForm1Filter(0.38897032f, -0.32900053f, 0.0f, 1.0f, 0.28202918f, 0.0f, EMPHASIS_GAIN_DB); - m_deemphasis = new CIIRDirectForm1Filter(1.0f,0.28202918f, 0.0f, 0.38897032f, -0.32900053f, 0.0f, EMPHASIS_GAIN_DB); + m_preemphasis = new CIIRDirectForm1Filter(0.38897032f, -0.32900053f, 0.0f, 1.0f, 0.28202918f, 0.0f, PREEMPHASIS_GAIN_DB); + m_deemphasis = new CIIRDirectForm1Filter(1.0f,0.28202918f, 0.0f, 0.38897032f, -0.32900053f, 0.0f, DEEMPHASIS_GAIN_DB); //cheby type 1 0.2dB cheby type 1 3rd order 300-2700Hz fs=8000 m_filterStage1 = new CIIRDirectForm1Filter(0.29495028f, 0.0f, -0.29495028f, 1.0f, -0.61384624f, -0.057158668f, FILTER_GAIN_DB); @@ -168,8 +169,8 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) } //Pre-emphasise the data and other stuff. - //for (unsigned int i = 0U; i < nSamples; i++) - // samples[i] = m_preemphasis->filter(samples[i]); + for (unsigned int i = 0U; i < nSamples; i++) + samples[i] = m_preemphasis->filter(samples[i]); // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) unsigned int pack = 0U; From 98cd2404f56575c74975d9e7d770fe68671f3b04 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 31 May 2020 09:17:45 +0200 Subject: [PATCH 046/163] Use GNU Radio emphasis filters, add filters generation scripts --- FMControl.cpp | 6 +++--- Tools/DeEmphasis.py | 43 +++++++++++++++++++++++++++++++++++++ Tools/PreEmphasis.py | 51 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 Tools/DeEmphasis.py create mode 100644 Tools/PreEmphasis.py diff --git a/FMControl.cpp b/FMControl.cpp index f1dc045..d558ac7 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -25,7 +25,7 @@ #endif const float DEEMPHASIS_GAIN_DB = 0.0F; -const float PREEMPHASIS_GAIN_DB = 30.0F; +const float PREEMPHASIS_GAIN_DB = 0.0F; const float FILTER_GAIN_DB = 0.0F; const unsigned int FM_MASK = 0x00000FFFU; @@ -39,8 +39,8 @@ m_filterStage1(NULL), m_filterStage2(NULL), m_filterStage3(NULL) { - m_preemphasis = new CIIRDirectForm1Filter(0.38897032f, -0.32900053f, 0.0f, 1.0f, 0.28202918f, 0.0f, PREEMPHASIS_GAIN_DB); - m_deemphasis = new CIIRDirectForm1Filter(1.0f,0.28202918f, 0.0f, 0.38897032f, -0.32900053f, 0.0f, DEEMPHASIS_GAIN_DB); + m_preemphasis = new CIIRDirectForm1Filter(8.315375384336983F,-7.03334621603483F,0.0F,1.0F,0.282029168302153F,0.0F, PREEMPHASIS_GAIN_DB); + m_deemphasis = new CIIRDirectForm1Filter(0.07708787090460224F,0.07708787090460224F,0.0F,1.0F,-0.8458242581907955F,0.0F, DEEMPHASIS_GAIN_DB); //cheby type 1 0.2dB cheby type 1 3rd order 300-2700Hz fs=8000 m_filterStage1 = new CIIRDirectForm1Filter(0.29495028f, 0.0f, -0.29495028f, 1.0f, -0.61384624f, -0.057158668f, FILTER_GAIN_DB); diff --git a/Tools/DeEmphasis.py b/Tools/DeEmphasis.py new file mode 100644 index 0000000..44320c9 --- /dev/null +++ b/Tools/DeEmphasis.py @@ -0,0 +1,43 @@ +#based on https://github.com/gnuradio/gnuradio/blob/master/gr-analog/python/analog/fm_emph.py + +import math +import cmath +import numpy as np +import scipy.signal as signal +import pylab as pl + +tau = 750e-6 +fs = 8000 +fh = 2700 + +# Digital corner frequency +w_c = 1.0 / tau + +# Prewarped analog corner frequency +w_ca = 2.0 * fs * math.tan(w_c / (2.0 * fs)) + +# Resulting digital pole, zero, and gain term from the bilinear +# transformation of H(s) = w_ca / (s + w_ca) to +# H(z) = b0 (1 - z1 z^-1)/(1 - p1 z^-1) +k = -w_ca / (2.0 * fs) +z1 = -1.0 +p1 = (1.0 + k) / (1.0 - k) +b0 = -k / (1.0 - k) + +btaps = [ b0 * 1.0, b0 * -z1, 0 ] +ataps = [ 1.0, -p1, 0 ] + +# Since H(s = 0) = 1.0, then H(z = 1) = 1.0 and has 0 dB gain at DC + + +taps = np.concatenate((btaps, ataps), axis=0) +print("Taps") +print(*taps, "", sep=",", end="\n") + +f,h = signal.freqz(btaps,ataps, fs=fs) +pl.plot(f, 20*np.log10(np.abs(h))) +pl.xlabel('frequency/Hz') +pl.ylabel('gain/dB') +pl.ylim(top=0,bottom=-30) +pl.xlim(left=0, right=fh*2.5) +pl.show() \ No newline at end of file diff --git a/Tools/PreEmphasis.py b/Tools/PreEmphasis.py new file mode 100644 index 0000000..22c81f6 --- /dev/null +++ b/Tools/PreEmphasis.py @@ -0,0 +1,51 @@ +#based on https://github.com/gnuradio/gnuradio/blob/master/gr-analog/python/analog/fm_emph.py + +import math +import cmath +import numpy as np +import scipy.signal as signal +import pylab as pl + +tau = 750e-6 +fs = 8000 +fh = 2700 + +# Digital corner frequencies +w_cl = 1.0 / tau +w_ch = 2.0 * math.pi * fh + +# Prewarped analog corner frequencies +w_cla = 2.0 * fs * math.tan(w_cl / (2.0 * fs)) +w_cha = 2.0 * fs * math.tan(w_ch / (2.0 * fs)) + +# Resulting digital pole, zero, and gain term from the bilinear +# transformation of H(s) = (s + w_cla) / (s + w_cha) to +# H(z) = b0 (1 - z1 z^-1)/(1 - p1 z^-1) +kl = -w_cla / (2.0 * fs) +kh = -w_cha / (2.0 * fs) +z1 = (1.0 + kl) / (1.0 - kl) +p1 = (1.0 + kh) / (1.0 - kh) +b0 = (1.0 - kl) / (1.0 - kh) + +# Since H(s = infinity) = 1.0, then H(z = -1) = 1.0 and +# this filter has 0 dB gain at fs/2.0. +# That isn't what users are going to expect, so adjust with a +# gain, g, so that H(z = 1) = 1.0 for 0 dB gain at DC. +w_0dB = 2.0 * math.pi * 0.0 +g = abs(1.0 - p1 * cmath.rect(1.0, -w_0dB)) \ +/ (b0 * abs(1.0 - z1 * cmath.rect(1.0, -w_0dB))) + +btaps = [ g * b0 * 1.0, g * b0 * -z1, 0] +ataps = [ 1.0, -p1, 0] + +taps = np.concatenate((btaps, ataps), axis=0) +print("Taps") +print(*taps, "", sep=",", end="\n") + +f,h = signal.freqz(btaps,ataps, fs=fs) +pl.plot(f, 20*np.log10(np.abs(h))) +pl.xlabel('frequency/Hz') +pl.ylabel('gain/dB') +pl.ylim(top=30,bottom=0) +pl.xlim(left=0, right=fh*2.5) +pl.show() \ No newline at end of file From 015edf9b81c196ebbf7784db2ee2782932aafca7 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 31 May 2020 11:01:53 +0200 Subject: [PATCH 047/163] Simplify readmodem, only one loop --- FMControl.cpp | 90 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index d558ac7..74b3537 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -24,6 +24,8 @@ #include #endif +#define SWAP_BYTES_16(a) (((a >> 8) & 0x00FFU) | ((a << 8) & 0xFF00U)) + const float DEEMPHASIS_GAIN_DB = 0.0F; const float PREEMPHASIS_GAIN_DB = 0.0F; const float FILTER_GAIN_DB = 0.0F; @@ -125,7 +127,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) // Repack the data (8-bit unsigned values containing unsigned 16-bit data) for (unsigned int i = 0U; i < nSamples; i++) { unsigned short sample = (unsigned short)((samples[i] + 1.0F) * 32767.0F + 0.5F); - out[nOut++] = ((sample >> 8) & 0x00FFU) | ((sample << 8) & 0xFF00U);//change endianess to network order, transmit MSB first + out[nOut++] = SWAP_BYTES_16(sample);//change endianess to network order, transmit MSB first } #if defined(DUMP_RF_AUDIO) @@ -155,42 +157,74 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) if(space > 252U) space = 252U; - unsigned char netData[168U];//84 * 2 modem can handle up to 84 samples (252 bytes) at a time - unsigned int length = m_network->read(netData, 168U); + unsigned short netData[84U];//modem can handle up to 84 samples (252 bytes) at a time + unsigned int length = m_network->read((unsigned char*)netData, 84U * sizeof(unsigned short)); + length /= sizeof(unsigned short); if (length == 0U) return 0U; - float samples[84U]; - unsigned int nSamples = 0U; - // Convert the unsigned 16-bit data (+65535 - 0) to float (+1.0 - -1.0) - for (unsigned int i = 0U; i < length; i += 2U) { - unsigned short sample = (netData[i + 0U] << 8) | netData[i + 1U]; - samples[nSamples++] = (float(sample) / 32768.0F) - 1.0F; - } - - //Pre-emphasise the data and other stuff. - for (unsigned int i = 0U; i < nSamples; i++) - samples[i] = m_preemphasis->filter(samples[i]); - - // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) unsigned int pack = 0U; unsigned char* packPointer = (unsigned char*)&pack; - unsigned int j = 0U; - unsigned int i = 0U; - for (; i < nSamples && j < space; i += 2U, j += 3U) { - unsigned short sample1 = (unsigned short)((samples[i] + 1.0F) * 2048.0F + 0.5F); - unsigned short sample2 = (unsigned short)((samples[i + 1] + 1.0F) * 2048.0F + 0.5F); + unsigned int nData = 0U; - pack = 0U; - pack = ((unsigned int)sample1) << 12; - pack |= sample2; + for(unsigned int i = 0; i < length; i++) { + unsigned short netSample = SWAP_BYTES_16(netData[i]);//((netData[i] << 8) & 0xFF00U)| ((netData[i] >> 8) & 0x00FFU); + // Convert the unsigned 16-bit data (+65535 - 0) to float (+1.0 - -1.0) + float sampleFloat = (float(netSample) / 32768.0F) - 1.0F; - data[j] = packPointer[0U]; - data[j + 1U] = packPointer[1U]; - data[j + 2U] = packPointer[2U]; + //preemphasis + sampleFloat = m_preemphasis->filter(sampleFloat); + + // Convert float to 12-bit samples (0 to 4095) + unsigned int sample12bit = (unsigned int)((sampleFloat + 1.0F) * 2048.0F + 0.5F); + + // pack 2 samples onto 3 bytes + if((i & 1U) == 0) { + pack = 0U; + pack = sample12bit << 12; + } else { + pack |= sample12bit; + + data[nData++] = packPointer[0U]; + data[nData++] = packPointer[1U]; + data[nData++] = packPointer[2U]; + } } - return j;//return the number of bytes written + return nData; + + + // float samples[84U]; + // unsigned int nSamples = 0U; + // // Convert the unsigned 16-bit data (+65535 - 0) to float (+1.0 - -1.0) + // for (unsigned int i = 0U; i < length; i += 2U) { + // unsigned short sample = (netData[i + 0U] << 8) | netData[i + 1U]; + // samples[nSamples++] = (float(sample) / 32768.0F) - 1.0F; + // } + + // //Pre-emphasise the data and other stuff. + // for (unsigned int i = 0U; i < nSamples; i++) + // samples[i] = m_preemphasis->filter(samples[i]); + + // // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) + // unsigned int pack = 0U; + // unsigned char* packPointer = (unsigned char*)&pack; + // unsigned int j = 0U; + // unsigned int i = 0U; + // for (; i < nSamples && j < space; i += 2U, j += 3U) { + // unsigned short sample1 = (unsigned short)((samples[i] + 1.0F) * 2048.0F + 0.5F); + // unsigned short sample2 = (unsigned short)((samples[i + 1] + 1.0F) * 2048.0F + 0.5F); + + // pack = 0U; + // pack = ((unsigned int)sample1) << 12; + // pack |= sample2; + + // data[j] = packPointer[0U]; + // data[j + 1U] = packPointer[1U]; + // data[j + 2U] = packPointer[2U]; + // } + + // return j;//return the number of bytes written } void CFMControl::clock(unsigned int ms) From bd1aa20803daebfbcfae32b45d3140ec2fe7cca8 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 31 May 2020 11:37:18 +0200 Subject: [PATCH 048/163] Simplify writeModem, only 2 loops --- FMControl.cpp | 69 +++++++++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 43 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index 74b3537..72e79a1 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -83,63 +83,46 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) bufferLength = 255U; if (bufferLength >= 3U) { -#if defined(DUMP_RF_AUDIO) - FILE * audiofile = fopen("./audiodump.bin", "ab"); -#endif - bufferLength = bufferLength - bufferLength % 3U; //round down to nearest multiple of 3 unsigned char bufferData[255U]; m_incomingRFAudio.getData(bufferData, bufferLength); - - unsigned int nSamples = 0; - float samples[85U]; // 255 / 3; - // Unpack the serial data into float values. + unsigned int pack = 0U; + unsigned char* packPointer = (unsigned char*)&pack; + unsigned short out[168U]; // 84 * 2 + unsigned int nOut = 0U; + short unpackedSamples[2U]; + for (unsigned int i = 0U; i < bufferLength; i += 3U) { - short sample1 = 0U; - short sample2 = 0U; - - unsigned int pack = 0U; - unsigned char* packPointer = (unsigned char*)&pack; - + //extract unsigned 12 bit unsigned sample pairs pack into 3 bytes to 16 bit signed packPointer[0U] = bufferData[i]; packPointer[1U] = bufferData[i + 1U]; packPointer[2U] = bufferData[i + 2U]; + unpackedSamples[1U] = short(int(pack & FM_MASK) - 2048); + unpackedSamples[0U] = short(int(pack >> 12) - 2048); - //extract unsigned 12 bit samples to 16 bit signed - sample2 = short(int(pack & FM_MASK) - 2048); - sample1 = short(int(pack >> 12) - 2048); + //process unpacked sample pair + for(unsigned char j = 0U; j < 2U; j++) { + //Convert to float (-1.0 to +1.0) + float sampleFloat = float(unpackedSamples[j]) / 2048.0F; - // Convert from unsigned short (0 - +4095) to float (-1.0 - +1.0) - samples[nSamples++] = float(sample1) / 2048.0F; - samples[nSamples++] = float(sample2) / 2048.0F; + //De-emphasise and remove CTCSS + sampleFloat = m_deemphasis->filter(sampleFloat); + sampleFloat = m_filterStage3->filter(m_filterStage2->filter(m_filterStage1->filter(sampleFloat))); + + // Repack the float data to 16 bit unsigned + unsigned short sampleUShort = (unsigned short)((sampleFloat + 1.0F) * 32767.0F + 0.5F); + out[nOut++] = SWAP_BYTES_16(sampleUShort); + } } - //De-emphasise the data and remove CTCSS - for (unsigned int i = 0U; i < nSamples; i++) { - samples[i] = m_deemphasis->filter(samples[i]); - samples[i] = m_filterStage3->filter(m_filterStage2->filter(m_filterStage1->filter(samples[i]))); - } - - unsigned short out[170U]; // 85 * 2 - unsigned int nOut = 0U; - - // Repack the data (8-bit unsigned values containing unsigned 16-bit data) - for (unsigned int i = 0U; i < nSamples; i++) { - unsigned short sample = (unsigned short)((samples[i] + 1.0F) * 32767.0F + 0.5F); - out[nOut++] = SWAP_BYTES_16(sample);//change endianess to network order, transmit MSB first - } - -#if defined(DUMP_RF_AUDIO) - if(audiofile != NULL) - fwrite(out, sizeof(unsigned short), nOut, audiofile); -#endif - #if defined(DUMP_RF_AUDIO) - if(audiofile != NULL) - fclose(audiofile); + FILE * audiofile = fopen("./audiodump.bin", "ab"); + if(audiofile != NULL) { + fwrite(out, sizeof(unsigned short), nOut, audiofile); + fclose(audiofile); + } #endif - return m_network->writeData((unsigned char*)out, nOut * sizeof(unsigned short)); } From af6b7d79da18f8c7f4f71f32f1fbc11e86ba70d8 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 31 May 2020 11:43:35 +0200 Subject: [PATCH 049/163] Adjust emphasis gains --- FMControl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FMControl.cpp b/FMControl.cpp index 72e79a1..3f58cb4 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -27,7 +27,7 @@ #define SWAP_BYTES_16(a) (((a >> 8) & 0x00FFU) | ((a << 8) & 0xFF00U)) const float DEEMPHASIS_GAIN_DB = 0.0F; -const float PREEMPHASIS_GAIN_DB = 0.0F; +const float PREEMPHASIS_GAIN_DB = 13.0F; const float FILTER_GAIN_DB = 0.0F; const unsigned int FM_MASK = 0x00000FFFU; From 97a35e69a4215ee2ebefe39f1428f40a7136c4e8 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 31 May 2020 12:14:16 +0200 Subject: [PATCH 050/163] clean up --- FMControl.cpp | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index 3f58cb4..8adc9c4 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -175,39 +175,6 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) } return nData; - - - // float samples[84U]; - // unsigned int nSamples = 0U; - // // Convert the unsigned 16-bit data (+65535 - 0) to float (+1.0 - -1.0) - // for (unsigned int i = 0U; i < length; i += 2U) { - // unsigned short sample = (netData[i + 0U] << 8) | netData[i + 1U]; - // samples[nSamples++] = (float(sample) / 32768.0F) - 1.0F; - // } - - // //Pre-emphasise the data and other stuff. - // for (unsigned int i = 0U; i < nSamples; i++) - // samples[i] = m_preemphasis->filter(samples[i]); - - // // Pack the floating point data (+1.0 to -1.0) to packed 12-bit samples (+2047 - -2048) - // unsigned int pack = 0U; - // unsigned char* packPointer = (unsigned char*)&pack; - // unsigned int j = 0U; - // unsigned int i = 0U; - // for (; i < nSamples && j < space; i += 2U, j += 3U) { - // unsigned short sample1 = (unsigned short)((samples[i] + 1.0F) * 2048.0F + 0.5F); - // unsigned short sample2 = (unsigned short)((samples[i + 1] + 1.0F) * 2048.0F + 0.5F); - - // pack = 0U; - // pack = ((unsigned int)sample1) << 12; - // pack |= sample2; - - // data[j] = packPointer[0U]; - // data[j + 1U] = packPointer[1U]; - // data[j + 2U] = packPointer[2U]; - // } - - // return j;//return the number of bytes written } void CFMControl::clock(unsigned int ms) From 17b49fde883ed2ac79a5720c4e8f9f20396b8e31 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 31 May 2020 12:19:20 +0200 Subject: [PATCH 051/163] Typo --- FMControl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FMControl.cpp b/FMControl.cpp index e8b4230..13c4132 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -94,7 +94,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) short unpackedSamples[2U]; for (unsigned int i = 0U; i < bufferLength; i += 3U) { - //extract unsigned 12 bit unsigned sample pairs pack into 3 bytes to 16 bit signed + //extract unsigned 12 bit unsigned sample pairs packed into 3 bytes to 16 bit signed packPointer[0U] = bufferData[i]; packPointer[1U] = bufferData[i + 1U]; packPointer[2U] = bufferData[i + 2U]; From 82c6f717cf706194a87bd77a2f6aad6ad83d7293 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 31 May 2020 20:35:04 +0200 Subject: [PATCH 052/163] Remove 250 length check --- Modem.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Modem.cpp b/Modem.cpp index e4dcb79..3350a5c 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -1853,12 +1853,6 @@ RESP_TYPE_MMDVM CModem::getResponse() if (ret == 0) return RTM_TIMEOUT; - if (m_buffer[1U] >= 250U) { - LogError("Invalid length received from the modem - %u", m_buffer[1U]); - m_offset = 0U; - return RTM_ERROR; - } - m_length = m_buffer[1U]; m_offset = 2U; } From f01fc3e9fcc5fb2f0588c26eb04a272b3a02b7d1 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 31 May 2020 21:42:35 +0200 Subject: [PATCH 053/163] Ensure Modem FM frames are always 168 samples --- FMControl.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index 13c4132..8a8f133 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -79,17 +79,17 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) m_incomingRFAudio.addData(data + 1U, length - 1U); unsigned int bufferLength = m_incomingRFAudio.dataSize(); - if (bufferLength > 255U) - bufferLength = 255U; + if (bufferLength > 252U)//168 samples 12-bit + bufferLength = 252U; if (bufferLength >= 3U) { bufferLength = bufferLength - bufferLength % 3U; //round down to nearest multiple of 3 - unsigned char bufferData[255U]; + unsigned char bufferData[252U]; m_incomingRFAudio.getData(bufferData, bufferLength); unsigned int pack = 0U; unsigned char* packPointer = (unsigned char*)&pack; - unsigned short out[168U]; // 84 * 2 + unsigned short out[168U]; unsigned int nOut = 0U; short unpackedSamples[2U]; @@ -140,8 +140,8 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) if (space > 252U) space = 252U; - unsigned short netData[84U];//modem can handle up to 84 samples (252 bytes) at a time - unsigned int length = m_network->read((unsigned char*)netData, 84U * sizeof(unsigned short)); + unsigned short netData[168U];//modem can handle up to 168 samples at a time + unsigned int length = m_network->read((unsigned char*)netData, 168U * sizeof(unsigned short)); length /= sizeof(unsigned short); if (length == 0U) return 0U; @@ -151,7 +151,8 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) unsigned int nData = 0U; for(unsigned int i = 0; i < length; i++) { - unsigned short netSample = SWAP_BYTES_16(netData[i]);//((netData[i] << 8) & 0xFF00U)| ((netData[i] >> 8) & 0x00FFU); + unsigned short netSample = SWAP_BYTES_16(netData[i]); + // Convert the unsigned 16-bit data (+65535 - 0) to float (+1.0 - -1.0) float sampleFloat = (float(netSample) / 32768.0F) - 1.0F; From dbe04c3c2f48ae5933014afb713c7b349af52696 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 8 Jun 2020 16:33:20 +0100 Subject: [PATCH 054/163] Initial code for AX.25 support. --- AX25Control.cpp | 97 +++++++++++++++++++++++++++++++++ AX25Control.h | 45 ++++++++++++++++ AX25Network.cpp | 111 ++++++++++++++++++++++++++++++++++++++ AX25Network.h | 51 ++++++++++++++++++ Conf.cpp | 67 ++++++++++++++++++++++- Conf.h | 20 +++++++ MMDVM.ini | 11 ++++ MMDVMHost.cpp | 76 ++++++++++++++++++++++++-- MMDVMHost.h | 6 +++ MMDVMHost.vcxproj | 4 ++ MMDVMHost.vcxproj.filters | 12 +++++ Makefile | 2 +- Makefile.Pi | 2 +- Makefile.Pi.Adafruit | 2 +- Makefile.Pi.HD44780 | 2 +- Makefile.Pi.OLED | 2 +- Makefile.Pi.PCF8574 | 2 +- Makefile.Solaris | 2 +- Modem.cpp | 37 ++++++++++++- Modem.h | 5 +- RemoteControl.cpp | 6 ++- RemoteControl.h | 4 +- 22 files changed, 549 insertions(+), 17 deletions(-) create mode 100644 AX25Control.cpp create mode 100644 AX25Control.h create mode 100644 AX25Network.cpp create mode 100644 AX25Network.h diff --git a/AX25Control.cpp b/AX25Control.cpp new file mode 100644 index 0000000..1f1b590 --- /dev/null +++ b/AX25Control.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2020 Jonathan Naylor, G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "AX25Control.h" +#include "Utils.h" +#include "Log.h" + +#include +#include +#include +#include + +// #define DUMP_AX25 + +const unsigned char BIT_MASK_TABLE[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U }; + +#define WRITE_BIT1(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) +#define READ_BIT1(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) + +CAX25Control::CAX25Control(CAX25Network* network) : +m_network(network), +m_enabled(true), +m_fp(NULL) +{ +} + +CAX25Control::~CAX25Control() +{ +} + +bool CAX25Control::writeModem(unsigned char *data, unsigned int len) +{ + assert(data != NULL); + + if (!m_enabled) + return false; + + unsigned char type = data[0U]; + + return true; +} + +bool CAX25Control::openFile() +{ + if (m_fp != NULL) + return true; + + time_t t; + ::time(&t); + + struct tm* tm = ::localtime(&t); + + char name[100U]; + ::sprintf(name, "AX25_%04d%02d%02d_%02d%02d%02d.ambe", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); + + m_fp = ::fopen(name, "wb"); + if (m_fp == NULL) + return false; + + ::fwrite("AX25", 1U, 4U, m_fp); + + return true; +} + +bool CAX25Control::writeFile(const unsigned char* data, unsigned int length) +{ + if (m_fp == NULL) + return false; + + ::fwrite(&length, 1U, sizeof(unsigned int), m_fp); + ::fwrite(data, 1U, length, m_fp); + + return true; +} + +void CAX25Control::closeFile() +{ + if (m_fp != NULL) { + ::fclose(m_fp); + m_fp = NULL; + } +} + +void CAX25Control::enable(bool enabled) +{ + m_enabled = enabled; +} diff --git a/AX25Control.h b/AX25Control.h new file mode 100644 index 0000000..2f73874 --- /dev/null +++ b/AX25Control.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(AX25Control_H) +#define AX25Control_H + +#include "AX25Network.h" + +#include + +class CAX25Control { +public: + CAX25Control(CAX25Network* network); + ~CAX25Control(); + + bool writeModem(unsigned char* data, unsigned int len); + + void enable(bool enabled); + +private: + CAX25Network* m_network; + bool m_enabled; + FILE* m_fp; + + bool openFile(); + bool writeFile(const unsigned char* data, unsigned int length); + void closeFile(); +}; + +#endif diff --git a/AX25Network.cpp b/AX25Network.cpp new file mode 100644 index 0000000..8123c51 --- /dev/null +++ b/AX25Network.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "AX25Network.h" +#include "Defines.h" +#include "Utils.h" +#include "Log.h" + +#include +#include +#include + +const unsigned int BUFFER_LENGTH = 200U; + +CAX25Network::CAX25Network(const std::string& localAddress, unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, bool debug) : +m_socket(localAddress, localPort), +m_address(), +m_port(gatewayPort), +m_debug(debug), +m_enabled(false) +{ + assert(gatewayPort > 0U); + assert(!gatewayAddress.empty()); + + m_address = CUDPSocket::lookup(gatewayAddress); +} + +CAX25Network::~CAX25Network() +{ +} + +bool CAX25Network::open() +{ + LogMessage("Opening AX25 network connection"); + + if (m_address.s_addr == INADDR_NONE) + return false; + + return m_socket.open(); +} + +bool CAX25Network::writeAX25(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + + unsigned char buffer[110U]; + ::memset(buffer, 0x00U, 110U); + + buffer[0U] = 'A'; + buffer[1U] = 'X'; + buffer[2U] = '2'; + buffer[3U] = '5'; + + ::memcpy(buffer + 4U, data, length); + + if (m_debug) + CUtils::dump(1U, "AX25 Network Data Sent", buffer, length + 4U); + + return m_socket.write(buffer, length + 4U, m_address, m_port); +} + +bool CAX25Network::writeMICE(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + + unsigned char buffer[110U]; + ::memset(buffer, 0x00U, 110U); + + buffer[0U] = 'M'; + buffer[1U] = 'I'; + buffer[2U] = 'C'; + buffer[3U] = 'E'; + + ::memcpy(buffer + 4U, data, length); + + if (m_debug) + CUtils::dump(1U, "AX25 Network Data Sent", buffer, length + 4U); + + return m_socket.write(buffer, length + 4U, m_address, m_port); +} + +void CAX25Network::reset() +{ +} + +void CAX25Network::close() +{ + m_socket.close(); + + LogMessage("Closing AX25 network connection"); +} + +void CAX25Network::enable(bool enabled) +{ + m_enabled = enabled; +} diff --git a/AX25Network.h b/AX25Network.h new file mode 100644 index 0000000..b171e6a --- /dev/null +++ b/AX25Network.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#ifndef AX25Network_H +#define AX25Network_H + +#include "UDPSocket.h" + +#include +#include + +class CAX25Network { +public: + CAX25Network(const std::string& localAddress, unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, bool debug); + ~CAX25Network(); + + bool open(); + + void enable(bool enabled); + + bool writeAX25(const unsigned char* data, unsigned int length); + bool writeMICE(const unsigned char* data, unsigned int length); + + void reset(); + + void close(); + +private: + CUDPSocket m_socket; + in_addr m_address; + unsigned int m_port; + bool m_debug; + bool m_enabled; +}; + +#endif diff --git a/Conf.cpp b/Conf.cpp index ee4034b..47fe532 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -45,12 +45,14 @@ enum SECTION { SECTION_NXDN, SECTION_POCSAG, SECTION_FM, + SECTION_AX25, SECTION_DSTAR_NETWORK, SECTION_DMR_NETWORK, SECTION_FUSION_NETWORK, SECTION_P25_NETWORK, SECTION_NXDN_NETWORK, SECTION_POCSAG_NETWORK, + SECTION_AX25_NETWORK, SECTION_TFTSERIAL, SECTION_HD44780, SECTION_NEXTION, @@ -205,6 +207,7 @@ m_fmCOSInvert(false), m_fmRFAudioBoost(1U), m_fmMaxDevLevel(90.0F), m_fmExtAudioBoost(1U), +m_ax25Enabled(false), m_dstarNetworkEnabled(false), m_dstarGatewayAddress(), m_dstarGatewayPort(0U), @@ -249,6 +252,12 @@ m_pocsagLocalAddress(), m_pocsagLocalPort(0U), m_pocsagNetworkModeHang(3U), m_pocsagNetworkDebug(false), +m_ax25NetworkEnabled(false), +m_ax25GatewayAddress(), +m_ax25GatewayPort(0U), +m_ax25LocalAddress(), +m_ax25LocalPort(0U), +m_ax25NetworkDebug(false), m_tftSerialPort("/dev/ttyAMA0"), m_tftSerialBrightness(50U), m_hd44780Rows(2U), @@ -342,6 +351,8 @@ bool CConf::read() section = SECTION_POCSAG; else if (::strncmp(buffer, "[FM]", 4U) == 0) section = SECTION_FM; + else if (::strncmp(buffer, "[AX.25]", 7U) == 0) + section = SECTION_AX25; else if (::strncmp(buffer, "[D-Star Network]", 16U) == 0) section = SECTION_DSTAR_NETWORK; else if (::strncmp(buffer, "[DMR Network]", 13U) == 0) @@ -354,6 +365,8 @@ bool CConf::read() section = SECTION_NXDN_NETWORK; else if (::strncmp(buffer, "[POCSAG Network]", 16U) == 0) section = SECTION_POCSAG_NETWORK; + else if (::strncmp(buffer, "[AX.25 Network]", 15U) == 0) + section = SECTION_AX25_NETWORK; else if (::strncmp(buffer, "[TFT Serial]", 12U) == 0) section = SECTION_TFTSERIAL; else if (::strncmp(buffer, "[HD44780]", 9U) == 0) @@ -700,8 +713,7 @@ bool CConf::read() m_pocsagEnabled = ::atoi(value) == 1; else if (::strcmp(key, "Frequency") == 0) m_pocsagFrequency = (unsigned int)::atoi(value); - } - else if (section == SECTION_FM) { + } else if (section == SECTION_FM) { if (::strcmp(key, "Enable") == 0) m_fmEnabled = ::atoi(value) == 1; else if (::strcmp(key, "Callsign") == 0) { @@ -775,6 +787,9 @@ bool CConf::read() m_fmMaxDevLevel = float(::atof(value)); else if (::strcmp(key, "ExtAudioBoost") == 0) m_fmExtAudioBoost = (unsigned int)::atoi(value); + } else if (section == SECTION_AX25) { + if (::strcmp(key, "Enable") == 0) + m_ax25Enabled = ::atoi(value) == 1; } else if (section == SECTION_DSTAR_NETWORK) { if (::strcmp(key, "Enable") == 0) m_dstarNetworkEnabled = ::atoi(value) == 1; @@ -869,6 +884,19 @@ bool CConf::read() m_pocsagNetworkModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "Debug") == 0) m_pocsagNetworkDebug = ::atoi(value) == 1; + } else if (section == SECTION_AX25_NETWORK) { + if (::strcmp(key, "Enable") == 0) + m_ax25NetworkEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "LocalAddress") == 0) + m_ax25LocalAddress = value; + else if (::strcmp(key, "LocalPort") == 0) + m_ax25LocalPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "GatewayAddress") == 0) + m_ax25GatewayAddress = value; + else if (::strcmp(key, "GatewayPort") == 0) + m_ax25GatewayPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "Debug") == 0) + m_ax25NetworkDebug = ::atoi(value) == 1; } else if (section == SECTION_TFTSERIAL) { if (::strcmp(key, "Port") == 0) m_tftSerialPort = value; @@ -1677,6 +1705,11 @@ unsigned int CConf::getFMExtAudioBoost() const return m_fmExtAudioBoost; } +bool CConf::getAX25Enabled() const +{ + return m_ax25Enabled; +} + bool CConf::getDStarNetworkEnabled() const { return m_dstarNetworkEnabled; @@ -1897,6 +1930,36 @@ bool CConf::getPOCSAGNetworkDebug() const return m_pocsagNetworkDebug; } +bool CConf::getAX25NetworkEnabled() const +{ + return m_ax25NetworkEnabled; +} + +std::string CConf::getAX25GatewayAddress() const +{ + return m_ax25GatewayAddress; +} + +unsigned int CConf::getAX25GatewayPort() const +{ + return m_ax25GatewayPort; +} + +std::string CConf::getAX25LocalAddress() const +{ + return m_ax25LocalAddress; +} + +unsigned int CConf::getAX25LocalPort() const +{ + return m_ax25LocalPort; +} + +bool CConf::getAX25NetworkDebug() const +{ + return m_ax25NetworkDebug; +} + std::string CConf::getTFTSerialPort() const { return m_tftSerialPort; diff --git a/Conf.h b/Conf.h index 40e81a0..91d7118 100644 --- a/Conf.h +++ b/Conf.h @@ -171,6 +171,9 @@ public: bool getPOCSAGEnabled() const; unsigned int getPOCSAGFrequency() const; + // The AX.25 section + bool getAX25Enabled() const; + // The FM Section bool getFMEnabled() const; std::string getFMCallsign() const; @@ -260,6 +263,14 @@ public: unsigned int getPOCSAGNetworkModeHang() const; bool getPOCSAGNetworkDebug() const; + // The AX.25 Network section + bool getAX25NetworkEnabled() const; + std::string getAX25GatewayAddress() const; + unsigned int getAX25GatewayPort() const; + std::string getAX25LocalAddress() const; + unsigned int getAX25LocalPort() const; + bool getAX25NetworkDebug() const; + // The TFTSERIAL section std::string getTFTSerialPort() const; unsigned int getTFTSerialBrightness() const; @@ -442,6 +453,8 @@ private: bool m_pocsagEnabled; unsigned int m_pocsagFrequency; + bool m_ax25Enabled; + bool m_fmEnabled; std::string m_fmCallsign; unsigned int m_fmCallsignSpeed; @@ -524,6 +537,13 @@ private: unsigned int m_pocsagNetworkModeHang; bool m_pocsagNetworkDebug; + bool m_ax25NetworkEnabled; + std::string m_ax25GatewayAddress; + unsigned int m_ax25GatewayPort; + std::string m_ax25LocalAddress; + unsigned int m_ax25LocalPort; + bool m_ax25NetworkDebug; + std::string m_tftSerialPort; unsigned int m_tftSerialBrightness; diff --git a/MMDVM.ini b/MMDVM.ini index 12f2b73..6f2b910 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -175,6 +175,9 @@ RFAudioBoost=1 MaxDevLevel=90 ExtAudioBoost=1 +[AX.25] +Enable=1 + [D-Star Network] Enable=1 GatewayAddress=127.0.0.1 @@ -231,6 +234,14 @@ GatewayPort=4800 # ModeHang=3 Debug=0 +[AX.25 Network] +Enable=1 +LocalAddress=127.0.0.1 +LocalPort=47325 +GatewayAddress=127.0.0.1 +GatewayPort=47326 +Debug=0 + [TFT Serial] # Port=modem Port=/dev/ttyAMA0 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 924d4a9..b1f8353 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -119,12 +119,14 @@ m_ysf(NULL), m_p25(NULL), m_nxdn(NULL), m_pocsag(NULL), +m_ax25(NULL), m_dstarNetwork(NULL), m_dmrNetwork(NULL), m_ysfNetwork(NULL), m_p25Network(NULL), m_nxdnNetwork(NULL), m_pocsagNetwork(NULL), +m_ax25Network(NULL), m_display(NULL), m_ump(NULL), m_mode(MODE_IDLE), @@ -151,6 +153,7 @@ m_p25Enabled(false), m_nxdnEnabled(false), m_pocsagEnabled(false), m_fmEnabled(false), +m_ax25Enabled(false), m_cwIdTime(0U), m_dmrLookup(NULL), m_nxdnLookup(NULL), @@ -320,6 +323,12 @@ int CMMDVMHost::run() return 1; } + if (m_ax25Enabled && m_conf.getAX25NetworkEnabled()) { + ret = createAX25Network(); + if (!ret) + return 1; + } + in_addr transparentAddress; unsigned int transparentPort = 0U; CUDPSocket* transparentSocket = NULL; @@ -614,6 +623,9 @@ int CMMDVMHost::run() pocsagTimer.start(); } + if (m_ax25Enabled) + m_ax25 = new CAX25Control(m_ax25Network); + bool remoteControlEnabled = m_conf.getRemoteControlEnabled(); if (remoteControlEnabled) { unsigned int port = m_conf.getRemoteControlPort(); @@ -806,6 +818,15 @@ int CMMDVMHost::run() } } + len = m_modem->readAX25Data(data); + if (m_ax25 != NULL && len > 0U) { + if (m_mode == MODE_IDLE || m_mode == MODE_FM) { + m_ax25->writeModem(data, len); + } else if (m_mode != MODE_LOCKOUT) { + LogWarning("NXDN modem data received when in mode %u", m_mode); + } + } + len = m_modem->readTransparentData(data); if (transparentSocket != NULL && len > 0U) transparentSocket->write(data, len, transparentAddress, transparentPort); @@ -1126,6 +1147,11 @@ int CMMDVMHost::run() delete m_pocsagNetwork; } + if (m_ax25Network != NULL) { + m_ax25Network->close(); + delete m_ax25Network; + } + if (transparentSocket != NULL) { transparentSocket->close(); delete transparentSocket; @@ -1142,6 +1168,7 @@ int CMMDVMHost::run() delete m_p25; delete m_nxdn; delete m_pocsag; + delete m_ax25; return 0; } @@ -1209,7 +1236,7 @@ bool CMMDVMHost::createModem() m_modem = CModem::createModem(port, m_duplex, rxInvert, txInvert, pttInvert, txDelay, dmrDelay, trace, debug); m_modem->setSerialParams(protocol,address); - m_modem->setModeParams(m_dstarEnabled, m_dmrEnabled, m_ysfEnabled, m_p25Enabled, m_nxdnEnabled, m_pocsagEnabled, m_fmEnabled); + m_modem->setModeParams(m_dstarEnabled, m_dmrEnabled, m_ysfEnabled, m_p25Enabled, m_nxdnEnabled, m_pocsagEnabled, m_fmEnabled, m_ax25Enabled); m_modem->setLevels(rxLevel, cwIdTXLevel, dstarTXLevel, dmrTXLevel, ysfTXLevel, p25TXLevel, nxdnTXLevel, pocsagTXLevel, fmTXLevel); m_modem->setRFParams(rxFrequency, rxOffset, txFrequency, txOffset, txDCOffset, rxDCOffset, rfLevel, pocsagFrequency); m_modem->setDMRParams(colorCode); @@ -1531,6 +1558,34 @@ bool CMMDVMHost::createPOCSAGNetwork() return true; } +bool CMMDVMHost::createAX25Network() +{ + std::string gatewayAddress = m_conf.getAX25GatewayAddress(); + unsigned int gatewayPort = m_conf.getAX25GatewayPort(); + std::string localAddress = m_conf.getAX25LocalAddress(); + unsigned int localPort = m_conf.getAX25LocalPort(); + bool debug = m_conf.getAX25NetworkDebug(); + + LogInfo("AX.25 Network Parameters"); + LogInfo(" Gateway Address: %s", gatewayAddress.c_str()); + LogInfo(" Gateway Port: %u", gatewayPort); + LogInfo(" Local Address: %s", localAddress.c_str()); + LogInfo(" Local Port: %u", localPort); + + m_ax25Network = new CAX25Network(localAddress, localPort, gatewayAddress, gatewayPort, debug); + + bool ret = m_ax25Network->open(); + if (!ret) { + delete m_ax25Network; + m_ax25Network = NULL; + return false; + } + + m_ax25Network->enable(true); + + return true; +} + void CMMDVMHost::readParams() { m_dstarEnabled = m_conf.getDStarEnabled(); @@ -1540,6 +1595,7 @@ void CMMDVMHost::readParams() m_nxdnEnabled = m_conf.getNXDNEnabled(); m_pocsagEnabled = m_conf.getPOCSAGEnabled(); m_fmEnabled = m_conf.getFMEnabled(); + m_ax25Enabled = m_conf.getAX25Enabled(); m_duplex = m_conf.getDuplex(); m_callsign = m_conf.getCallsign(); m_id = m_conf.getId(); @@ -1557,8 +1613,10 @@ void CMMDVMHost::readParams() LogInfo(" NXDN: %s", m_nxdnEnabled ? "enabled" : "disabled"); LogInfo(" POCSAG: %s", m_pocsagEnabled ? "enabled" : "disabled"); LogInfo(" FM: %s", m_fmEnabled ? "enabled" : "disabled"); + LogInfo(" AX.25: %s", m_ax25Enabled ? "enabled" : "disabled"); } +// XXX AX.25 enabled/disabled void CMMDVMHost::setMode(unsigned char mode) { assert(m_modem != NULL); @@ -2016,9 +2074,13 @@ void CMMDVMHost::remoteControl() processEnableCommand(m_nxdnEnabled, true); break; case RCD_ENABLE_FM: - if (m_fmEnabled==false) + if (!m_fmEnabled) processEnableCommand(m_fmEnabled, true); break; + case RCD_ENABLE_AX25: + if (!m_ax25Enabled) + processEnableCommand(m_ax25Enabled, true); + break; case RCD_DISABLE_DSTAR: if (m_dstar != NULL && m_dstarEnabled==true) processEnableCommand(m_dstarEnabled, false); @@ -2043,6 +2105,10 @@ void CMMDVMHost::remoteControl() if (m_fmEnabled == true) processEnableCommand(m_fmEnabled, false); break; + case RCD_DISABLE_AX25: + if (m_ax25Enabled == true) + processEnableCommand(m_ax25Enabled, false); + break; case RCD_PAGE: if (m_pocsag != NULL) { unsigned int ric = m_remoteControl->getArgUInt(0U); @@ -2092,8 +2158,10 @@ void CMMDVMHost::processModeCommand(unsigned char mode, unsigned int timeout) void CMMDVMHost::processEnableCommand(bool& mode, bool enabled) { LogDebug("Setting mode current=%s new=%s",mode ? "true" : "false",enabled ? "true" : "false"); - mode=enabled; - m_modem->setModeParams(m_dstarEnabled, m_dmrEnabled, m_ysfEnabled, m_p25Enabled, m_nxdnEnabled, m_pocsagEnabled, m_fmEnabled); + + mode = enabled; + + m_modem->setModeParams(m_dstarEnabled, m_dmrEnabled, m_ysfEnabled, m_p25Enabled, m_nxdnEnabled, m_pocsagEnabled, m_fmEnabled, m_ax25Enabled); if (!m_modem->writeConfig()) LogError("Cannot write Config to MMDVM"); } diff --git a/MMDVMHost.h b/MMDVMHost.h index 17d6786..1a3f791 100644 --- a/MMDVMHost.h +++ b/MMDVMHost.h @@ -23,8 +23,10 @@ #include "POCSAGNetwork.h" #include "POCSAGControl.h" #include "DStarNetwork.h" +#include "AX25Network.h" #include "NXDNNetwork.h" #include "DStarControl.h" +#include "AX25Control.h" #include "DMRControl.h" #include "YSFControl.h" #include "P25Control.h" @@ -62,12 +64,14 @@ private: CP25Control* m_p25; CNXDNControl* m_nxdn; CPOCSAGControl* m_pocsag; + CAX25Control* m_ax25; CDStarNetwork* m_dstarNetwork; CDMRNetwork* m_dmrNetwork; CYSFNetwork* m_ysfNetwork; CP25Network* m_p25Network; CNXDNNetwork* m_nxdnNetwork; CPOCSAGNetwork* m_pocsagNetwork; + CAX25Network* m_ax25Network; CDisplay* m_display; CUMP* m_ump; unsigned char m_mode; @@ -94,6 +98,7 @@ private: bool m_nxdnEnabled; bool m_pocsagEnabled; bool m_fmEnabled; + bool m_ax25Enabled; unsigned int m_cwIdTime; CDMRLookup* m_dmrLookup; CNXDNLookup* m_nxdnLookup; @@ -114,6 +119,7 @@ private: bool createP25Network(); bool createNXDNNetwork(); bool createPOCSAGNetwork(); + bool createAX25Network(); void remoteControl(); void processModeCommand(unsigned char mode, unsigned int timeout); diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index 812c7a4..63b8aa6 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -153,6 +153,8 @@ + + @@ -251,6 +253,8 @@ + + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index fb8a362..b333302 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -299,6 +299,12 @@ Header Files + + Header Files + + + Header Files + @@ -562,5 +568,11 @@ Source Files + + Source Files + + + Source Files + \ No newline at end of file diff --git a/Makefile b/Makefile index efdaba6..2ac2552 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ CFLAGS = -g -O3 -Wall -std=c++0x -pthread LIBS = -lpthread LDFLAGS = -g -OBJECTS = \ +OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ diff --git a/Makefile.Pi b/Makefile.Pi index 640c07f..3a4c2c1 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -6,7 +6,7 @@ CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DRASPBERRY_PI -I/usr/local/include LIBS = -lwiringPi -lwiringPiDev -lpthread LDFLAGS = -g -L/usr/local/lib -OBJECTS = \ +OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 24c2907..16775bb 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -7,7 +7,7 @@ CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHD44780 -DADAFRUIT_DISPLAY -I/usr/l LIBS = -lwiringPi -lwiringPiDev -lpthread LDFLAGS = -g -L/usr/local/lib -OBJECTS = \ +OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index c2faad2..3d02d71 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -6,7 +6,7 @@ CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHD44780 -I/usr/local/include LIBS = -lwiringPi -lwiringPiDev -lpthread LDFLAGS = -g -L/usr/local/lib -OBJECTS = \ +OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index d2db40c..f4f2024 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -6,7 +6,7 @@ CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DOLED -I/usr/local/include LIBS = -lArduiPi_OLED -lwiringPi -lpthread LDFLAGS = -g -L/usr/local/lib -OBJECTS = \ +OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o OLED.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index 4c5d3a3..3a70077 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -7,7 +7,7 @@ CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHD44780 -DPCF8574_DISPLAY -I/usr/lo LIBS = -lwiringPi -lwiringPiDev -lpthread LDFLAGS = -g -L/usr/local/lib -OBJECTS = \ +OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o \ diff --git a/Makefile.Solaris b/Makefile.Solaris index c50d41e..d469fad 100644 --- a/Makefile.Solaris +++ b/Makefile.Solaris @@ -6,7 +6,7 @@ CFLAGS = -g -O3 -Wall -std=c++0x -pthread LIBS = -lpthread -lsocket LDFLAGS = -g -OBJECTS = \ +OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o Golay2087.o Golay24128.o Hamming.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o \ diff --git a/Modem.cpp b/Modem.cpp index 93319be..0f0b0c1 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -76,6 +76,8 @@ const unsigned char MMDVM_NXDN_LOST = 0x41U; const unsigned char MMDVM_POCSAG_DATA = 0x50U; +const unsigned char MMDVM_AX25_DATA = 0x55U; + const unsigned char MMDVM_FM_PARAMS1 = 0x60U; const unsigned char MMDVM_FM_PARAMS2 = 0x61U; const unsigned char MMDVM_FM_PARAMS3 = 0x62U; @@ -133,6 +135,7 @@ m_p25Enabled(false), m_nxdnEnabled(false), m_pocsagEnabled(false), m_fmEnabled(false), +m_ax25Enabled(false), m_rxDCOffset(0), m_txDCOffset(0), m_serial(NULL), @@ -152,6 +155,7 @@ m_txP25Data(1000U, "Modem TX P25"), m_rxNXDNData(1000U, "Modem RX NXDN"), m_txNXDNData(1000U, "Modem TX NXDN"), m_txPOCSAGData(1000U, "Modem TX POCSAG"), +m_rxAX25Data(1000U, "Modem RX AX.25"), m_rxTransparentData(1000U, "Modem RX Transparent"), m_txTransparentData(1000U, "Modem TX Transparent"), m_sendTransparentDataFrameType(0U), @@ -230,7 +234,7 @@ void CModem::setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int tx m_pocsagFrequency = pocsagFrequency + txOffset; } -void CModem::setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled) +void CModem::setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled, bool ax25Enabled) { m_dstarEnabled = dstarEnabled; m_dmrEnabled = dmrEnabled; @@ -239,6 +243,7 @@ void CModem::setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, m_nxdnEnabled = nxdnEnabled; m_pocsagEnabled = pocsagEnabled; m_fmEnabled = fmEnabled; + m_ax25Enabled = ax25Enabled; } void CModem::setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagTXLevel, float fmTXLevel) @@ -585,6 +590,20 @@ void CModem::clock(unsigned int ms) } break; + case MMDVM_AX25_DATA: { + if (m_trace) + CUtils::dump(1U, "RX AX.25 Data", m_buffer, m_length); + + unsigned char data = m_length - 2U; + m_rxAX25Data.addData(&data, 1U); + + data = TAG_DATA; + m_rxAX25Data.addData(&data, 1U); + + m_rxAX25Data.addData(m_buffer + 3U, m_length - 3U); + } + break; + case MMDVM_GET_STATUS: { // if (m_trace) // CUtils::dump(1U, "GET_STATUS", m_buffer, m_length); @@ -941,6 +960,20 @@ unsigned int CModem::readNXDNData(unsigned char* data) return len; } +unsigned int CModem::readAX25Data(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxAX25Data.isEmpty()) + return 0U; + + unsigned char len = 0U; + m_rxAX25Data.getData(&len, 1U); + m_rxAX25Data.getData(data, len); + + return len; +} + unsigned int CModem::readTransparentData(unsigned char* data) { assert(data != NULL); @@ -1552,6 +1585,8 @@ bool CModem::setConfig() buffer[4U] |= 0x20U; if (m_fmEnabled && m_duplex) buffer[4U] |= 0x40U; + if (m_ax25Enabled) + buffer[4U] |= 0x80U; buffer[5U] = m_txDelay / 10U; // In 10ms units diff --git a/Modem.h b/Modem.h index 069fe46..69228bb 100644 --- a/Modem.h +++ b/Modem.h @@ -39,7 +39,7 @@ public: virtual void setSerialParams(const std::string& protocol, unsigned int address); virtual void setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency); - virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled); + virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled, bool ax25Enabled); virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel); virtual void setDMRParams(unsigned int colorCode); virtual void setYSFParams(bool loDev, unsigned int txHang); @@ -59,6 +59,7 @@ public: virtual unsigned int readYSFData(unsigned char* data); virtual unsigned int readP25Data(unsigned char* data); virtual unsigned int readNXDNData(unsigned char* data); + virtual unsigned int readAX25Data(unsigned char* data); virtual unsigned int readTransparentData(unsigned char* data); virtual unsigned int readSerial(unsigned char* data, unsigned int length); @@ -150,6 +151,7 @@ private: bool m_nxdnEnabled; bool m_pocsagEnabled; bool m_fmEnabled; + bool m_ax25Enabled; int m_rxDCOffset; int m_txDCOffset; CSerialController* m_serial; @@ -169,6 +171,7 @@ private: CRingBuffer m_rxNXDNData; CRingBuffer m_txNXDNData; CRingBuffer m_txPOCSAGData; + CRingBuffer m_rxAX25Data; CRingBuffer m_rxTransparentData; CRingBuffer m_txTransparentData; unsigned int m_sendTransparentDataFrameType; diff --git a/RemoteControl.cpp b/RemoteControl.cpp index 15ac46c..bf400ad 100644 --- a/RemoteControl.cpp +++ b/RemoteControl.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 by Jonathan Naylor G4KLX + * Copyright (C) 2019,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -102,6 +102,8 @@ REMOTE_COMMAND CRemoteControl::getCommand() m_command = RCD_ENABLE_NXDN; else if (m_args.at(1U) == "fm") m_command = RCD_ENABLE_FM; + else if (m_args.at(1U) == "ax25") + m_command = RCD_ENABLE_AX25; } else if (m_args.at(0U) == "disable" && m_args.size() >= DISABLE_ARGS) { if (m_args.at(1U) == "dstar") m_command = RCD_DISABLE_DSTAR; @@ -115,6 +117,8 @@ REMOTE_COMMAND CRemoteControl::getCommand() m_command = RCD_DISABLE_NXDN; else if (m_args.at(1U) == "fm") m_command = RCD_DISABLE_FM; + else if (m_args.at(1U) == "ax25") + m_command = RCD_DISABLE_AX25; } else if (m_args.at(0U) == "page" && m_args.size() >= PAGE_ARGS) { // Page command is in the form of "page " m_command = RCD_PAGE; diff --git a/RemoteControl.h b/RemoteControl.h index 53b9820..3825dee 100644 --- a/RemoteControl.h +++ b/RemoteControl.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 by Jonathan Naylor G4KLX + * Copyright (C) 2019,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -40,12 +40,14 @@ enum REMOTE_COMMAND { RCD_ENABLE_P25, RCD_ENABLE_NXDN, RCD_ENABLE_FM, + RCD_ENABLE_AX25, RCD_DISABLE_DSTAR, RCD_DISABLE_DMR, RCD_DISABLE_YSF, RCD_DISABLE_P25, RCD_DISABLE_NXDN, RCD_DISABLE_FM, + RCD_DISABLE_AX25, RCD_PAGE, RCD_CW }; From d409700a313bcddafed901bf6522d351c573f1e3 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 9 Jun 2020 14:32:05 +0100 Subject: [PATCH 055/163] X25 cleanups. --- AX25Control.cpp | 2 +- MMDVMHost.cpp | 69 ++++++++++++++++++++++++++++++++++++++++--------- Modem.cpp | 5 +--- Version.h | 2 +- 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/AX25Control.cpp b/AX25Control.cpp index 1f1b590..d9f9a2f 100644 --- a/AX25Control.cpp +++ b/AX25Control.cpp @@ -45,7 +45,7 @@ bool CAX25Control::writeModem(unsigned char *data, unsigned int len) if (!m_enabled) return false; - unsigned char type = data[0U]; + CUtils::dump(1U, "AX.25 raw packet", data, len); return true; } diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index b1f8353..3cf60cb 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -623,8 +623,12 @@ int CMMDVMHost::run() pocsagTimer.start(); } - if (m_ax25Enabled) + if (m_ax25Enabled) { + LogInfo("AX.25 RF Parameters"); + LogInfo(" RXOnly: yes"); + m_ax25 = new CAX25Control(m_ax25Network); + } bool remoteControlEnabled = m_conf.getRemoteControlEnabled(); if (remoteControlEnabled) { @@ -1616,7 +1620,6 @@ void CMMDVMHost::readParams() LogInfo(" AX.25: %s", m_ax25Enabled ? "enabled" : "disabled"); } -// XXX AX.25 enabled/disabled void CMMDVMHost::setMode(unsigned char mode) { assert(m_modem != NULL); @@ -1636,6 +1639,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_ax25Network != NULL) + m_ax25Network->enable(false); if (m_dstar != NULL) m_dstar->enable(true); if (m_dmr != NULL) @@ -1648,6 +1653,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_ax25 != NULL) + m_ax25->enable(false); m_modem->setMode(MODE_DSTAR); if (m_ump != NULL) m_ump->setMode(MODE_DSTAR); @@ -1670,6 +1677,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_ax25Network != NULL) + m_ax25Network->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1682,6 +1691,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_ax25 != NULL) + m_ax25->enable(false); m_modem->setMode(MODE_DMR); if (m_ump != NULL) m_ump->setMode(MODE_DMR); @@ -1708,6 +1719,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_ax25Network != NULL) + m_ax25Network->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1720,6 +1733,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_ax25 != NULL) + m_ax25->enable(false); m_modem->setMode(MODE_YSF); if (m_ump != NULL) m_ump->setMode(MODE_YSF); @@ -1742,6 +1757,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_ax25Network != NULL) + m_ax25Network->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1754,6 +1771,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_ax25 != NULL) + m_ax25->enable(false); m_modem->setMode(MODE_P25); if (m_ump != NULL) m_ump->setMode(MODE_P25); @@ -1776,6 +1795,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(true); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_ax25Network != NULL) + m_ax25Network->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1788,6 +1809,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(true); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_ax25 != NULL) + m_ax25->enable(false); m_modem->setMode(MODE_NXDN); if (m_ump != NULL) m_ump->setMode(MODE_NXDN); @@ -1810,6 +1833,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(true); + if (m_ax25Network != NULL) + m_ax25Network->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1822,6 +1847,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(true); + if (m_ax25 != NULL) + m_ax25->enable(false); m_modem->setMode(MODE_POCSAG); if (m_ump != NULL) m_ump->setMode(MODE_POCSAG); @@ -1844,6 +1871,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_ax25Network != NULL) + m_ax25Network->enable(true); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1856,6 +1885,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_ax25 != NULL) + m_ax25->enable(true); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); @@ -1882,6 +1913,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_ax25Network != NULL) + m_ax25Network->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1894,6 +1927,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_ax25 != NULL) + m_ax25->enable(false); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); @@ -1922,6 +1957,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); + if (m_ax25Network != NULL) + m_ax25Network->enable(false); if (m_dstar != NULL) m_dstar->enable(false); if (m_dmr != NULL) @@ -1934,6 +1971,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); + if (m_ax25 != NULL) + m_ax25->enable(false); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); @@ -1960,6 +1999,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdnNetwork->enable(true); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(true); + if (m_ax25Network != NULL) + m_ax25Network->enable(true); if (m_dstar != NULL) m_dstar->enable(true); if (m_dmr != NULL) @@ -1972,6 +2013,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_nxdn->enable(true); if (m_pocsag != NULL) m_pocsag->enable(true); + if (m_ax25 != NULL) + m_ax25->enable(true); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); @@ -2120,18 +2163,20 @@ void CMMDVMHost::remoteControl() } m_pocsag->sendPage(ric, text); } + break; case RCD_CW: setMode(MODE_IDLE); // Force the modem to go idle so that we can send the CW text. - if (!m_modem->hasTX()){ - std::string cwtext; - for (unsigned int i = 0U; i < m_remoteControl->getArgCount(); i++) { - if (i > 0U) - cwtext += " "; - cwtext += m_remoteControl->getArgString(i); - } - m_display->writeCW(); - m_modem->sendCWId(cwtext); - } + if (!m_modem->hasTX()) { + std::string cwtext; + for (unsigned int i = 0U; i < m_remoteControl->getArgCount(); i++) { + if (i > 0U) + cwtext += " "; + cwtext += m_remoteControl->getArgString(i); + } + m_display->writeCW(); + m_modem->sendCWId(cwtext); + } + break; default: break; } diff --git a/Modem.cpp b/Modem.cpp index 0f0b0c1..3a59fca 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -594,10 +594,7 @@ void CModem::clock(unsigned int ms) if (m_trace) CUtils::dump(1U, "RX AX.25 Data", m_buffer, m_length); - unsigned char data = m_length - 2U; - m_rxAX25Data.addData(&data, 1U); - - data = TAG_DATA; + unsigned char data = m_length - 3U; m_rxAX25Data.addData(&data, 1U); m_rxAX25Data.addData(m_buffer + 3U, m_length - 3U); diff --git a/Version.h b/Version.h index 101ca94..6b25bcc 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200608"; +const char* VERSION = "20200609"; #endif From d9e2940f08550cc8ed8000dfa27ff031f3cf5e69 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 18 Jun 2020 12:51:13 +0100 Subject: [PATCH 056/163] Add AX.25 packet tracking. --- AX25Control.cpp | 154 +++++++++++++++++++++++++++++++++++++++++++++++- AX25Control.h | 6 +- AX25Network.cpp | 20 ------- AX25Network.h | 1 - Conf.cpp | 12 +++- Conf.h | 2 + MMDVM.ini | 1 + MMDVMHost.cpp | 5 +- Version.h | 2 +- 9 files changed, 175 insertions(+), 28 deletions(-) diff --git a/AX25Control.cpp b/AX25Control.cpp index d9f9a2f..a6b89ea 100644 --- a/AX25Control.cpp +++ b/AX25Control.cpp @@ -27,8 +27,9 @@ const unsigned char BIT_MASK_TABLE[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04 #define WRITE_BIT1(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) #define READ_BIT1(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) -CAX25Control::CAX25Control(CAX25Network* network) : +CAX25Control::CAX25Control(CAX25Network* network, bool trace) : m_network(network), +m_trace(trace), m_enabled(true), m_fp(NULL) { @@ -45,7 +46,15 @@ bool CAX25Control::writeModem(unsigned char *data, unsigned int len) if (!m_enabled) return false; - CUtils::dump(1U, "AX.25 raw packet", data, len); + if (m_trace) + decode(data, len); + + CUtils::dump(1U, "AX.25 raw packet", data, len); + + if (m_network != NULL) { + if (isUI(data, len)) + m_network->writeAX25(data, len); + } return true; } @@ -95,3 +104,144 @@ void CAX25Control::enable(bool enabled) { m_enabled = enabled; } + +void CAX25Control::decode(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length >= 15U); + + std::string text; + + bool more = decodeAddress(data + 7U, text); + + text += '>'; + + decodeAddress(data + 0U, text); + + unsigned int n = 14U; + while (more && n < length) { + text += ','; + more = decodeAddress(data + n, text, true); + n += 7U; + } + + text += ' '; + + if ((data[n] & 0x01U) == 0x00U) { + // I frame + char t[20U]; + ::sprintf(t, "", (data[n] >> 1) & 0x07U, (data[n] >> 5) & 0x07U); + text += t; + } else { + if ((data[n] & 0x02U) == 0x00U) { + // S frame + char t[20U]; + switch (data[n] & 0x0FU) { + case 0x01U: + sprintf(t, "", (data[n] >> 5) & 0x07U); + break; + case 0x05U: + sprintf(t, "", (data[n] >> 5) & 0x07U); + break; + case 0x09U: + sprintf(t, "", (data[n] >> 5) & 0x07U); + break; + case 0x0DU: + sprintf(t, "", (data[n] >> 5) & 0x07U); + break; + default: + sprintf(t, "", (data[n] >> 5) & 0x07U); + break; + } + + text += t; + LogMessage("AX.25, %s", text.c_str()); + return; + } else { + // U frame + switch (data[n] & 0xEFU) { + case 0x6FU: + text += ""; + break; + case 0x2FU: + text += ""; + break; + case 0x43U: + text += ""; + break; + case 0x0FU: + text += ""; + break; + case 0x63U: + text += ""; + break; + case 0x87U: + text += ""; + break; + case 0x03U: + text += ""; + break; + case 0xAFU: + text += ""; + break; + case 0xE3U: + text += ""; + break; + default: + text += ""; + break; + } + + if ((data[n] & 0xEFU) != 0x03U) { + LogMessage("AX.25, %s", text.c_str()); + return; + } + } + } + + n += 2U; + + LogMessage("AX.25, %s %.*s", text.c_str(), length - n, data + n); +} + +bool CAX25Control::decodeAddress(const unsigned char* data, std::string& text, bool isDigi) const +{ + assert(data != NULL); + + for (unsigned int i = 0U; i < 6U; i++) { + char c = data[i] >> 1; + if (c != ' ') + text += c; + } + + unsigned char ssid = (data[6U] >> 1) & 0x0FU; + if (ssid > 0U) { + text += '-'; + if (ssid >= 10U) { + text += '1'; + text += '0' + ssid - 10U; + } + else { + text += '0' + ssid; + } + } + + if (isDigi) { + if ((data[6U] & 0x80U) == 0x80U) + text += '*'; + } + + return (data[6U] & 0x01U) == 0x00U; +} + +bool CAX25Control::isUI(const unsigned char* data, unsigned int length) const +{ + assert(data != NULL); + assert(length >= 15U); + + unsigned int n = 13U; + while ((data[n] & 0x01U) == 0x00U && n < length) + n += 7U; + + return (data[n + 1U] & 0xEFU) == 0x03U; +} diff --git a/AX25Control.h b/AX25Control.h index 2f73874..59b5688 100644 --- a/AX25Control.h +++ b/AX25Control.h @@ -25,7 +25,7 @@ class CAX25Control { public: - CAX25Control(CAX25Network* network); + CAX25Control(CAX25Network* network, bool trace); ~CAX25Control(); bool writeModem(unsigned char* data, unsigned int len); @@ -34,9 +34,13 @@ public: private: CAX25Network* m_network; + bool m_trace; bool m_enabled; FILE* m_fp; + bool isUI(const unsigned char* data, unsigned int length) const; + void decode(const unsigned char* data, unsigned int length); + bool decodeAddress(const unsigned char* data, std::string& text, bool isDigi = false) const; bool openFile(); bool writeFile(const unsigned char* data, unsigned int length); void closeFile(); diff --git a/AX25Network.cpp b/AX25Network.cpp index 8123c51..3f5ac49 100644 --- a/AX25Network.cpp +++ b/AX25Network.cpp @@ -74,26 +74,6 @@ bool CAX25Network::writeAX25(const unsigned char* data, unsigned int length) return m_socket.write(buffer, length + 4U, m_address, m_port); } -bool CAX25Network::writeMICE(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - - unsigned char buffer[110U]; - ::memset(buffer, 0x00U, 110U); - - buffer[0U] = 'M'; - buffer[1U] = 'I'; - buffer[2U] = 'C'; - buffer[3U] = 'E'; - - ::memcpy(buffer + 4U, data, length); - - if (m_debug) - CUtils::dump(1U, "AX25 Network Data Sent", buffer, length + 4U); - - return m_socket.write(buffer, length + 4U, m_address, m_port); -} - void CAX25Network::reset() { } diff --git a/AX25Network.h b/AX25Network.h index b171e6a..866ae7e 100644 --- a/AX25Network.h +++ b/AX25Network.h @@ -34,7 +34,6 @@ public: void enable(bool enabled); bool writeAX25(const unsigned char* data, unsigned int length); - bool writeMICE(const unsigned char* data, unsigned int length); void reset(); diff --git a/Conf.cpp b/Conf.cpp index 5e10a9f..1e14523 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -208,6 +208,7 @@ m_fmRFAudioBoost(1U), m_fmMaxDevLevel(90.0F), m_fmExtAudioBoost(1U), m_ax25Enabled(false), +m_ax25Trace(false), m_dstarNetworkEnabled(false), m_dstarGatewayAddress(), m_dstarGatewayPort(0U), @@ -789,8 +790,10 @@ bool CConf::read() else if (::strcmp(key, "ExtAudioBoost") == 0) m_fmExtAudioBoost = (unsigned int)::atoi(value); } else if (section == SECTION_AX25) { - if (::strcmp(key, "Enable") == 0) - m_ax25Enabled = ::atoi(value) == 1; + if (::strcmp(key, "Enable") == 0) + m_ax25Enabled = ::atoi(value) == 1; + else if (::strcmp(key, "Trace") == 0) + m_ax25Trace = ::atoi(value) == 1; } else if (section == SECTION_DSTAR_NETWORK) { if (::strcmp(key, "Enable") == 0) m_dstarNetworkEnabled = ::atoi(value) == 1; @@ -1713,6 +1716,11 @@ bool CConf::getAX25Enabled() const return m_ax25Enabled; } +bool CConf::getAX25Trace() const +{ + return m_ax25Trace; +} + bool CConf::getDStarNetworkEnabled() const { return m_dstarNetworkEnabled; diff --git a/Conf.h b/Conf.h index ef584c7..0d340f9 100644 --- a/Conf.h +++ b/Conf.h @@ -173,6 +173,7 @@ public: // The AX.25 section bool getAX25Enabled() const; + bool getAX25Trace() const; // The FM Section bool getFMEnabled() const; @@ -455,6 +456,7 @@ private: unsigned int m_pocsagFrequency; bool m_ax25Enabled; + bool m_ax25Trace; bool m_fmEnabled; std::string m_fmCallsign; diff --git a/MMDVM.ini b/MMDVM.ini index e6de0fb..de6408d 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -177,6 +177,7 @@ ExtAudioBoost=1 [AX.25] Enable=1 +Trace=1 [D-Star Network] Enable=1 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index ebc5613..9c452dc 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -626,10 +626,13 @@ int CMMDVMHost::run() } if (m_ax25Enabled) { + bool trace = m_conf.getAX25Trace(); + LogInfo("AX.25 RF Parameters"); LogInfo(" RXOnly: yes"); + LogInfo(" Trace: %s", trace ? "yes" : "no"); - m_ax25 = new CAX25Control(m_ax25Network); + m_ax25 = new CAX25Control(m_ax25Network, trace); } bool remoteControlEnabled = m_conf.getRemoteControlEnabled(); diff --git a/Version.h b/Version.h index 6b25bcc..279ecfd 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200609"; +const char* VERSION = "20200618"; #endif From e0f800ba98f575d137192fd2c1e977ddfb827e53 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Fri, 19 Jun 2020 15:09:21 +0100 Subject: [PATCH 057/163] Handle the new larger frame type. --- Modem.cpp | 150 ++++++++++++++++++++++++++++-------------------------- Modem.h | 10 ++++ Version.h | 2 +- 3 files changed, 89 insertions(+), 73 deletions(-) diff --git a/Modem.cpp b/Modem.cpp index 3a59fca..bcdde83 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -142,6 +142,8 @@ m_serial(NULL), m_buffer(NULL), m_length(0U), m_offset(0U), +m_state(SS_START), +m_type(0U), m_rxDStarData(1000U, "Modem RX D-Star"), m_txDStarData(1000U, "Modem TX D-Star"), m_rxDMRData1(1000U, "Modem RX DMR1"), @@ -387,7 +389,7 @@ void CModem::clock(unsigned int ms) // Nothing to do } else { // type == RTM_OK - switch (m_buffer[2U]) { + switch (m_type) { case MMDVM_DSTAR_HEADER: { if (m_trace) CUtils::dump(1U, "RX D-Star Header", m_buffer, m_length); @@ -398,7 +400,7 @@ void CModem::clock(unsigned int ms) data = TAG_HEADER; m_rxDStarData.addData(&data, 1U); - m_rxDStarData.addData(m_buffer + 3U, m_length - 3U); + m_rxDStarData.addData(m_buffer + m_offset, m_length - m_offset); } break; @@ -412,7 +414,7 @@ void CModem::clock(unsigned int ms) data = TAG_DATA; m_rxDStarData.addData(&data, 1U); - m_rxDStarData.addData(m_buffer + 3U, m_length - 3U); + m_rxDStarData.addData(m_buffer + m_offset, m_length - m_offset); } break; @@ -453,7 +455,7 @@ void CModem::clock(unsigned int ms) data = TAG_DATA; m_rxDMRData1.addData(&data, 1U); - m_rxDMRData1.addData(m_buffer + 3U, m_length - 3U); + m_rxDMRData1.addData(m_buffer + m_offset, m_length - m_offset); } break; @@ -470,7 +472,7 @@ void CModem::clock(unsigned int ms) data = TAG_DATA; m_rxDMRData2.addData(&data, 1U); - m_rxDMRData2.addData(m_buffer + 3U, m_length - 3U); + m_rxDMRData2.addData(m_buffer + m_offset, m_length - m_offset); } break; @@ -508,7 +510,7 @@ void CModem::clock(unsigned int ms) data = TAG_DATA; m_rxYSFData.addData(&data, 1U); - m_rxYSFData.addData(m_buffer + 3U, m_length - 3U); + m_rxYSFData.addData(m_buffer + m_offset, m_length - m_offset); } break; @@ -534,7 +536,7 @@ void CModem::clock(unsigned int ms) data = TAG_HEADER; m_rxP25Data.addData(&data, 1U); - m_rxP25Data.addData(m_buffer + 3U, m_length - 3U); + m_rxP25Data.addData(m_buffer + m_offset, m_length - m_offset); } break; @@ -548,7 +550,7 @@ void CModem::clock(unsigned int ms) data = TAG_DATA; m_rxP25Data.addData(&data, 1U); - m_rxP25Data.addData(m_buffer + 3U, m_length - 3U); + m_rxP25Data.addData(m_buffer + m_offset, m_length - m_offset); } break; @@ -574,7 +576,7 @@ void CModem::clock(unsigned int ms) data = TAG_DATA; m_rxNXDNData.addData(&data, 1U); - m_rxNXDNData.addData(m_buffer + 3U, m_length - 3U); + m_rxNXDNData.addData(m_buffer + m_offset, m_length - m_offset); } break; @@ -597,7 +599,7 @@ void CModem::clock(unsigned int ms) unsigned char data = m_length - 3U; m_rxAX25Data.addData(&data, 1U); - m_rxAX25Data.addData(m_buffer + 3U, m_length - 3U); + m_rxAX25Data.addData(m_buffer + m_offset, m_length - m_offset); } break; @@ -609,44 +611,44 @@ void CModem::clock(unsigned int ms) m_nxdnSpace = 0U; m_pocsagSpace = 0U; - m_mode = m_buffer[4U]; + m_mode = m_buffer[m_offset + 1U]; - m_tx = (m_buffer[5U] & 0x01U) == 0x01U; + m_tx = (m_buffer[m_offset + 2U] & 0x01U) == 0x01U; - bool adcOverflow = (m_buffer[5U] & 0x02U) == 0x02U; + bool adcOverflow = (m_buffer[m_offset + 2U] & 0x02U) == 0x02U; if (adcOverflow) LogError("MMDVM ADC levels have overflowed"); - bool rxOverflow = (m_buffer[5U] & 0x04U) == 0x04U; + bool rxOverflow = (m_buffer[m_offset + 2U] & 0x04U) == 0x04U; if (rxOverflow) LogError("MMDVM RX buffer has overflowed"); - bool txOverflow = (m_buffer[5U] & 0x08U) == 0x08U; + bool txOverflow = (m_buffer[m_offset + 2U] & 0x08U) == 0x08U; if (txOverflow) LogError("MMDVM TX buffer has overflowed"); - m_lockout = (m_buffer[5U] & 0x10U) == 0x10U; + m_lockout = (m_buffer[m_offset + 2U] & 0x10U) == 0x10U; - bool dacOverflow = (m_buffer[5U] & 0x20U) == 0x20U; + bool dacOverflow = (m_buffer[m_offset + 2U] & 0x20U) == 0x20U; if (dacOverflow) LogError("MMDVM DAC levels have overflowed"); - m_cd = (m_buffer[5U] & 0x40U) == 0x40U; + m_cd = (m_buffer[m_offset + 2U] & 0x40U) == 0x40U; - m_dstarSpace = m_buffer[6U]; - m_dmrSpace1 = m_buffer[7U]; - m_dmrSpace2 = m_buffer[8U]; - m_ysfSpace = m_buffer[9U]; + m_dstarSpace = m_buffer[m_offset + 3U]; + m_dmrSpace1 = m_buffer[m_offset + 4U]; + m_dmrSpace2 = m_buffer[m_offset + 5U]; + m_ysfSpace = m_buffer[m_offset + 6U]; - if (m_length > 10U) - m_p25Space = m_buffer[10U]; - if (m_length > 11U) - m_nxdnSpace = m_buffer[11U]; - if (m_length > 12U) - m_pocsagSpace = m_buffer[12U]; + if (m_length > (m_offset + 7U)) + m_p25Space = m_buffer[m_offset + 7U]; + if (m_length > (m_offset + 8U)) + m_nxdnSpace = m_buffer[m_offset + 8U]; + if (m_length > (m_offset + 9U)) + m_pocsagSpace = m_buffer[m_offset + 9U]; m_inactivityTimer.start(); - // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[5U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_pocsagSpace, int(m_lockout), int(m_cd)); + // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[m_offset + 2U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_pocsagSpace, int(m_lockout), int(m_cd)); } break; @@ -656,10 +658,10 @@ void CModem::clock(unsigned int ms) unsigned char offset = m_sendTransparentDataFrameType; if (offset > 1U) offset = 1U; - unsigned char data = m_length - 3U + offset; + unsigned char data = m_length - m_offset + offset; m_rxTransparentData.addData(&data, 1U); - m_rxTransparentData.addData(m_buffer + 3U - offset, m_length - 3U + offset); + m_rxTransparentData.addData(m_buffer + m_offset - offset, m_length - m_offset + offset); } break; @@ -669,7 +671,7 @@ void CModem::clock(unsigned int ms) break; case MMDVM_NAK: - LogWarning("Received a NAK from the MMDVM, command = 0x%02X, reason = %u", m_buffer[3U], m_buffer[4U]); + LogWarning("Received a NAK from the MMDVM, command = 0x%02X, reason = %u", m_buffer[m_offset], m_buffer[m_offset + 1U]); break; case MMDVM_DEBUG1: @@ -689,14 +691,14 @@ void CModem::clock(unsigned int ms) unsigned char offset = m_sendTransparentDataFrameType; if (offset > 1U) offset = 1U; - unsigned char data = m_length - 3U + offset; + unsigned char data = m_length - m_offset + offset; m_rxTransparentData.addData(&data, 1U); - m_rxTransparentData.addData(m_buffer + 3U - offset, m_length - 3U + offset); + m_rxTransparentData.addData(m_buffer + m_offset - offset, m_length - m_offset + offset); break; //only break when sendFrameType>0, else message is unknown } default: - LogMessage("Unknown message, type: %02X", m_buffer[2U]); + LogMessage("Unknown message, type: %02X", m_type); CUtils::dump("Buffer dump", m_buffer, m_length); break; } @@ -1729,7 +1731,7 @@ RESP_TYPE_MMDVM CModem::getResponse() { assert(m_serial != NULL); - if (m_offset == 0U) { + if (m_state == SS_START) { // Get the start of the frame or nothing at all int ret = m_serial->read(m_buffer + 0U, 1U); if (ret < 0) { @@ -1743,68 +1745,71 @@ RESP_TYPE_MMDVM CModem::getResponse() if (m_buffer[0U] != MMDVM_FRAME_START) return RTM_TIMEOUT; - m_offset = 1U; + m_state = SS_LENGTH1; + m_length = 1U; } - if (m_offset == 1U) { - // Get the length of the frame + if (m_state == SS_LENGTH1) { + // Get the length of the frame, 1/2 int ret = m_serial->read(m_buffer + 1U, 1U); if (ret < 0) { LogError("Error when reading from the modem"); - m_offset = 0U; + m_state = SS_START; return RTM_ERROR; } if (ret == 0) return RTM_TIMEOUT; - if (m_buffer[1U] >= 250U) { - LogError("Invalid length received from the modem - %u", m_buffer[1U]); - m_offset = 0U; - return RTM_ERROR; - } - m_length = m_buffer[1U]; m_offset = 2U; + + if (m_length == 0U) + m_state = SS_LENGTH2; + else + m_state = SS_TYPE; } - if (m_offset == 2U) { - // Get the frame type + if (m_state == SS_LENGTH2) { + // Get the length of the frane, 2/2 int ret = m_serial->read(m_buffer + 2U, 1U); if (ret < 0) { LogError("Error when reading from the modem"); - m_offset = 0U; + m_state = SS_START; return RTM_ERROR; } if (ret == 0) return RTM_TIMEOUT; + m_length = m_buffer[2U] + 255U; m_offset = 3U; + m_state = SS_TYPE; } - if (m_offset >= 3U) { - // Use later two byte length field - if (m_length == 0U) { - int ret = m_serial->read(m_buffer + 3U, 2U); - if (ret < 0) { - LogError("Error when reading from the modem"); - m_offset = 0U; - return RTM_ERROR; - } - - if (ret == 0) - return RTM_TIMEOUT; - - m_length = (m_buffer[3U] << 8) | m_buffer[4U]; - m_offset = 5U; + if (m_state == SS_TYPE) { + // Get the frame type + int ret = m_serial->read(&m_type, 1U); + if (ret < 0) { + LogError("Error when reading from the modem"); + m_state = SS_START; + return RTM_ERROR; } + if (ret == 0) + return RTM_TIMEOUT; + + m_buffer[m_offset++] = m_type; + + m_state = SS_DATA; + } + + if (m_state == SS_DATA) { while (m_offset < m_length) { int ret = m_serial->read(m_buffer + m_offset, m_length - m_offset); if (ret < 0) { LogError("Error when reading from the modem"); - m_offset = 0U; + m_state = SS_START; return RTM_ERROR; } @@ -1816,10 +1821,11 @@ RESP_TYPE_MMDVM CModem::getResponse() } } - m_offset = 0U; - // CUtils::dump(1U, "Received", m_buffer, m_length); + m_offset = m_length > 255U ? 4U : 3U; + m_state = SS_START; + return RTM_OK; } @@ -2162,25 +2168,25 @@ bool CModem::setFMMiscParams() void CModem::printDebug() { if (m_buffer[2U] == MMDVM_DEBUG1) { - LogMessage("Debug: %.*s", m_length - 3U, m_buffer + 3U); + LogMessage("Debug: %.*s", m_length - m_offset - 0U, m_buffer + m_offset); } else if (m_buffer[2U] == MMDVM_DEBUG2) { short val1 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; - LogMessage("Debug: %.*s %d", m_length - 5U, m_buffer + 3U, val1); + LogMessage("Debug: %.*s %d", m_length - m_offset - 2U, m_buffer + m_offset, val1); } else if (m_buffer[2U] == MMDVM_DEBUG3) { short val1 = (m_buffer[m_length - 4U] << 8) | m_buffer[m_length - 3U]; short val2 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; - LogMessage("Debug: %.*s %d %d", m_length - 7U, m_buffer + 3U, val1, val2); + LogMessage("Debug: %.*s %d %d", m_length - m_offset - 4U, m_buffer + m_offset, val1, val2); } else if (m_buffer[2U] == MMDVM_DEBUG4) { short val1 = (m_buffer[m_length - 6U] << 8) | m_buffer[m_length - 5U]; short val2 = (m_buffer[m_length - 4U] << 8) | m_buffer[m_length - 3U]; short val3 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; - LogMessage("Debug: %.*s %d %d %d", m_length - 9U, m_buffer + 3U, val1, val2, val3); + LogMessage("Debug: %.*s %d %d %d", m_length - m_offset - 6U, m_buffer + m_offset, val1, val2, val3); } else if (m_buffer[2U] == MMDVM_DEBUG5) { short val1 = (m_buffer[m_length - 8U] << 8) | m_buffer[m_length - 7U]; short val2 = (m_buffer[m_length - 6U] << 8) | m_buffer[m_length - 5U]; short val3 = (m_buffer[m_length - 4U] << 8) | m_buffer[m_length - 3U]; short val4 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; - LogMessage("Debug: %.*s %d %d %d %d", m_length - 11U, m_buffer + 3U, val1, val2, val3, val4); + LogMessage("Debug: %.*s %d %d %d %d", m_length - m_offset - 8U, m_buffer + m_offset, val1, val2, val3, val4); } } diff --git a/Modem.h b/Modem.h index 69228bb..ea01945 100644 --- a/Modem.h +++ b/Modem.h @@ -32,6 +32,14 @@ enum RESP_TYPE_MMDVM { RTM_ERROR }; +enum SERIAL_STATE { + SS_START, + SS_LENGTH1, + SS_LENGTH2, + SS_TYPE, + SS_DATA +}; + class CModem { public: CModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool trace, bool debug); @@ -158,6 +166,8 @@ private: unsigned char* m_buffer; unsigned int m_length; unsigned int m_offset; + SERIAL_STATE m_state; + unsigned char m_type; CRingBuffer m_rxDStarData; CRingBuffer m_txDStarData; CRingBuffer m_rxDMRData1; diff --git a/Version.h b/Version.h index 279ecfd..ffc27e8 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200618"; +const char* VERSION = "20200619"; #endif From d0878bbf7e3eb3b32dd8338d2482be8272a3e175 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 20 Jun 2020 20:06:48 +0100 Subject: [PATCH 058/163] Add new AX.25 parameters. --- MMDVM.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MMDVM.ini b/MMDVM.ini index de6408d..dcbb0b8 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -66,6 +66,7 @@ RFLevel=100 # NXDNTXLevel=50 # POCSAGTXLevel=50 # FMTXLevel=50 +# AX25TXLevel=50 RSSIMappingFile=RSSI.dat Trace=0 Debug=0 @@ -177,6 +178,9 @@ ExtAudioBoost=1 [AX.25] Enable=1 +TXTwist=6 +RXTwist=6 +Digipeat=1 Trace=1 [D-Star Network] From f936a6c5b640514954ca06cd12ec93f3e80c78ca Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 20 Jun 2020 20:44:02 +0100 Subject: [PATCH 059/163] Add the Kerchunk TX parameter. --- Conf.cpp | 8 ++++++++ Conf.h | 2 ++ MMDVM.ini | 1 + MMDVMHost.cpp | 4 +++- Modem.cpp | 7 ++++++- Modem.h | 3 ++- Version.h | 2 +- 7 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index eae0db6..5799323 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -201,6 +201,7 @@ m_fmCTCSSHighThreshold(30U), m_fmCTCSSLowThreshold(20U), m_fmCTCSSLevel(2.0F), m_fmKerchunkTime(0U), +m_fmKerchunkTX(true), m_fmHangTime(7U), m_fmUseCOS(true), m_fmCOSInvert(false), @@ -777,6 +778,8 @@ bool CConf::read() m_fmCTCSSLevel = float(::atof(value)); else if (::strcmp(key, "KerchunkTime") == 0) m_fmKerchunkTime = (unsigned int)::atoi(value); + else if (::strcmp(key, "KerchunkTX") == 0) + m_fmKerchunkTX = ::atoi(value) == 1; else if (::strcmp(key, "HangTime") == 0) m_fmHangTime = (unsigned int)::atoi(value); else if (::strcmp(key, "UseCOS") == 0) @@ -1683,6 +1686,11 @@ unsigned int CConf::getFMKerchunkTime() const return m_fmKerchunkTime; } +bool CConf::getFMKerchunkTX() const +{ + return m_fmKerchunkTX; +} + unsigned int CConf::getFMHangTime() const { return m_fmHangTime; diff --git a/Conf.h b/Conf.h index b455a55..01e1f0b 100644 --- a/Conf.h +++ b/Conf.h @@ -198,6 +198,7 @@ public: unsigned int getFMCTCSSLowThreshold() const; float getFMCTCSSLevel() const; unsigned int getFMKerchunkTime() const; + bool getFMKerchunkTX() const; unsigned int getFMHangTime() const; bool getFMUseCOS() const; bool getFMCOSInvert() const; @@ -479,6 +480,7 @@ private: unsigned int m_fmCTCSSLowThreshold; float m_fmCTCSSLevel; unsigned int m_fmKerchunkTime; + bool m_fmKerchunkTX; unsigned int m_fmHangTime; bool m_fmUseCOS; bool m_fmCOSInvert; diff --git a/MMDVM.ini b/MMDVM.ini index c031c49..a323267 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -169,6 +169,7 @@ CTCSSThreshold=30 # CTCSSLowThreshold=20 CTCSSLevel=20 KerchunkTime=0 +KerchunkTX=1 HangTime=7 UseCOS=1 COSInvert=0 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index c28f17e..5c4a926 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -1300,6 +1300,7 @@ bool CMMDVMHost::createModem() unsigned int ctcssLowThreshold = m_conf.getFMCTCSSLowThreshold(); float ctcssLevel = m_conf.getFMCTCSSLevel(); unsigned int kerchunkTime = m_conf.getFMKerchunkTime(); + bool kerchunkTX = m_conf.getFMKerchunkTX(); unsigned int hangTime = m_conf.getFMHangTime(); bool useCOS = m_conf.getFMUseCOS(); bool cosInvert = m_conf.getFMCOSInvert(); @@ -1331,6 +1332,7 @@ bool CMMDVMHost::createModem() LogInfo(" CTCSS Low Threshold: %u", ctcssLowThreshold); LogInfo(" CTCSS Level: %.1f%%", ctcssLevel); LogInfo(" Kerchunk Time: %us", kerchunkTime); + LogInfo(" Kerchunk TX: %s", kerchunkTX ? "yes" : "no"); LogInfo(" Hang Time: %us", hangTime); LogInfo(" Use COS: %s", useCOS ? "yes" : "no"); LogInfo(" COS Invert: %s", cosInvert ? "yes" : "no"); @@ -1340,7 +1342,7 @@ bool CMMDVMHost::createModem() m_modem->setFMCallsignParams(callsign, callsignSpeed, callsignFrequency, callsignTime, callsignHoldoff, callsignHighLevel, callsignLowLevel, callsignAtStart, callsignAtEnd, callsignAtLatch); m_modem->setFMAckParams(rfAck, ackSpeed, ackFrequency, ackMinTime, ackDelay, ackLevel); - m_modem->setFMMiscParams(timeout, timeoutLevel, ctcssFrequency, ctcssHighThreshold, ctcssLowThreshold, ctcssLevel, kerchunkTime, hangTime, useCOS, cosInvert, rfAudioBoost, maxDevLevel); + m_modem->setFMMiscParams(timeout, timeoutLevel, ctcssFrequency, ctcssHighThreshold, ctcssLowThreshold, ctcssLevel, kerchunkTime, kerchunkTX, hangTime, useCOS, cosInvert, rfAudioBoost, maxDevLevel); if (m_conf.getFMNetworkEnabled()) { std::string extAck = m_conf.getFMExtAck(); diff --git a/Modem.cpp b/Modem.cpp index 3350a5c..8dfb235 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -205,6 +205,7 @@ m_fmCtcssHighThreshold(30U), m_fmCtcssLowThreshold(20U), m_fmCtcssLevel(10.0F), m_fmKerchunkTime(0U), +m_fmKerchunkTX(true), m_fmHangTime(5U), m_fmUseCOS(true), m_fmCOSInvert(false), @@ -2051,7 +2052,7 @@ void CModem::setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, uns m_fmAckLevel = ackLevel; } -void CModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) +void CModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) { m_fmTimeout = timeout; m_fmTimeoutLevel = timeoutLevel; @@ -2062,6 +2063,8 @@ void CModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctc m_fmCtcssLevel = ctcssLevel; m_fmKerchunkTime = kerchunkTime; + m_fmKerchunkTX = kerchunkTX; + m_fmHangTime = hangTime; m_fmUseCOS = useCOS; @@ -2217,6 +2220,8 @@ bool CModem::setFMMiscParams() buffer[11U] |= 0x01U; if (m_fmCOSInvert) buffer[11U] |= 0x02U; + if (m_fmKerchunkTX) + buffer[11U] |= 0x04U; buffer[12U] = m_fmRFAudioBoost; diff --git a/Modem.h b/Modem.h index 4c77d9f..6cbf683 100644 --- a/Modem.h +++ b/Modem.h @@ -49,7 +49,7 @@ public: virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch); virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel); - virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel); + virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel); virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost); virtual bool open(); @@ -220,6 +220,7 @@ private: unsigned int m_fmCtcssLowThreshold; float m_fmCtcssLevel; unsigned int m_fmKerchunkTime; + bool m_fmKerchunkTX; unsigned int m_fmHangTime; bool m_fmUseCOS; bool m_fmCOSInvert; diff --git a/Version.h b/Version.h index 46f5c4f..7cfe66a 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200523"; +const char* VERSION = "20200620"; #endif From fe77cbd68c7831a6f5f61d0cfd9926c1b46c5fcb Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 20 Jun 2020 21:28:39 +0100 Subject: [PATCH 060/163] Add the extra AX.25 parameters. --- Conf.cpp | 34 +++++++++++++++++++++++++++++++++- Conf.h | 8 ++++++++ MMDVMHost.cpp | 18 ++++++++++++++---- Version.h | 2 +- 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index 1e14523..85972c1 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -113,6 +113,7 @@ m_modemP25TXLevel(50.0F), m_modemNXDNTXLevel(50.0F), m_modemPOCSAGTXLevel(50.0F), m_modemFMTXLevel(50.0F), +m_modemAX25TXLevel(50.0F), m_modemRSSIMappingFile(), m_modemTrace(false), m_modemDebug(false), @@ -208,6 +209,9 @@ m_fmRFAudioBoost(1U), m_fmMaxDevLevel(90.0F), m_fmExtAudioBoost(1U), m_ax25Enabled(false), +m_ax25RXTwist(6), +m_ax25TXTwist(6), +m_ax25Digipeat(true), m_ax25Trace(false), m_dstarNetworkEnabled(false), m_dstarGatewayAddress(), @@ -508,7 +512,7 @@ bool CConf::read() else if (::strcmp(key, "RXLevel") == 0) m_modemRXLevel = float(::atof(value)); else if (::strcmp(key, "TXLevel") == 0) - m_modemFMTXLevel = m_modemCWIdTXLevel = m_modemDStarTXLevel = m_modemDMRTXLevel = m_modemYSFTXLevel = m_modemP25TXLevel = m_modemNXDNTXLevel = float(::atof(value)); + m_modemAX25TXLevel = m_modemFMTXLevel = m_modemCWIdTXLevel = m_modemDStarTXLevel = m_modemDMRTXLevel = m_modemYSFTXLevel = m_modemP25TXLevel = m_modemNXDNTXLevel = float(::atof(value)); else if (::strcmp(key, "CWIdTXLevel") == 0) m_modemCWIdTXLevel = float(::atof(value)); else if (::strcmp(key, "D-StarTXLevel") == 0) @@ -525,6 +529,8 @@ bool CConf::read() m_modemPOCSAGTXLevel = float(::atof(value)); else if (::strcmp(key, "FMTXLevel") == 0) m_modemFMTXLevel = float(::atof(value)); + else if (::strcmp(key, "AX25TXLevel") == 0) + m_modemAX25TXLevel = float(::atof(value)); else if (::strcmp(key, "RSSIMappingFile") == 0) m_modemRSSIMappingFile = value; else if (::strcmp(key, "Trace") == 0) @@ -792,6 +798,12 @@ bool CConf::read() } else if (section == SECTION_AX25) { if (::strcmp(key, "Enable") == 0) m_ax25Enabled = ::atoi(value) == 1; + else if (::strcmp(key, "RXTwist") == 0) + m_ax25RXTwist = ::atoi(value); + else if (::strcmp(key, "TXTwist") == 0) + m_ax25TXTwist = ::atoi(value); + else if (::strcmp(key, "Digipeat") == 0) + m_ax25Digipeat = ::atoi(value) == 1; else if (::strcmp(key, "Trace") == 0) m_ax25Trace = ::atoi(value) == 1; } else if (section == SECTION_DSTAR_NETWORK) { @@ -1241,6 +1253,11 @@ float CConf::getModemFMTXLevel() const return m_modemFMTXLevel; } +float CConf::getModemAX25TXLevel() const +{ + return m_modemAX25TXLevel; +} + std::string CConf::getModemRSSIMappingFile () const { return m_modemRSSIMappingFile; @@ -1716,6 +1733,21 @@ bool CConf::getAX25Enabled() const return m_ax25Enabled; } +int CConf::getAX25RXTwist() const +{ + return m_ax25RXTwist; +} + +int CConf::getAX25TXTwist() const +{ + return m_ax25TXTwist; +} + +bool CConf::getAX25Digipeat() const +{ + return m_ax25Digipeat; +} + bool CConf::getAX25Trace() const { return m_ax25Trace; diff --git a/Conf.h b/Conf.h index 0d340f9..8379061 100644 --- a/Conf.h +++ b/Conf.h @@ -91,6 +91,7 @@ public: float getModemNXDNTXLevel() const; float getModemPOCSAGTXLevel() const; float getModemFMTXLevel() const; + float getModemAX25TXLevel() const; std::string getModemRSSIMappingFile() const; bool getModemTrace() const; bool getModemDebug() const; @@ -173,6 +174,9 @@ public: // The AX.25 section bool getAX25Enabled() const; + int getAX25RXTwist() const; + int getAX25TXTwist() const; + bool getAX25Digipeat() const; bool getAX25Trace() const; // The FM Section @@ -383,6 +387,7 @@ private: float m_modemNXDNTXLevel; float m_modemPOCSAGTXLevel; float m_modemFMTXLevel; + float m_modemAX25TXLevel; std::string m_modemRSSIMappingFile; bool m_modemTrace; bool m_modemDebug; @@ -456,6 +461,9 @@ private: unsigned int m_pocsagFrequency; bool m_ax25Enabled; + int m_ax25RXTwist; + int m_ax25TXTwist; + bool m_ax25Digipeat; bool m_ax25Trace; bool m_fmEnabled; diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 9c452dc..db24632 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -626,13 +626,18 @@ int CMMDVMHost::run() } if (m_ax25Enabled) { - bool trace = m_conf.getAX25Trace(); + int rxTwist = m_conf.getAX25RXTwist(); + int txTwist = m_conf.getAX25TXTwist(); + bool digipeat = m_conf.getAX25Digipeat(); + bool trace = m_conf.getAX25Trace(); LogInfo("AX.25 RF Parameters"); - LogInfo(" RXOnly: yes"); + LogInfo(" RXTwist: %d", rxTwist); + LogInfo(" TXTwist: %d", txTwist); + LogInfo(" Digipeat: %s", digipeat ? "yes" : "no"); LogInfo(" Trace: %s", trace ? "yes" : "no"); - m_ax25 = new CAX25Control(m_ax25Network, trace); + m_ax25 = new CAX25Control(m_ax25Network, digipeat, trace); } bool remoteControlEnabled = m_conf.getRemoteControlEnabled(); @@ -1201,6 +1206,7 @@ bool CMMDVMHost::createModem() float nxdnTXLevel = m_conf.getModemNXDNTXLevel(); float pocsagTXLevel = m_conf.getModemPOCSAGTXLevel(); float fmTXLevel = m_conf.getModemFMTXLevel(); + float ax25TXLevel = m_conf.getModemAX25TXLevel(); bool trace = m_conf.getModemTrace(); bool debug = m_conf.getModemDebug(); unsigned int colorCode = m_conf.getDMRColorCode(); @@ -1216,6 +1222,8 @@ bool CMMDVMHost::createModem() int rxDCOffset = m_conf.getModemRXDCOffset(); int txDCOffset = m_conf.getModemTXDCOffset(); float rfLevel = m_conf.getModemRFLevel(); + int rxTwist = m_conf.getAX25RXTwist(); + int txTwist = m_conf.getAX25TXTwist(); LogInfo("Modem Parameters"); LogInfo(" Port: %s", port.c_str()); @@ -1241,17 +1249,19 @@ bool CMMDVMHost::createModem() LogInfo(" NXDN TX Level: %.1f%%", nxdnTXLevel); LogInfo(" POCSAG TX Level: %.1f%%", pocsagTXLevel); LogInfo(" FM TX Level: %.1f%%", fmTXLevel); + LogInfo(" AX.25 TX Level: %.1f%%", ax25TXLevel); LogInfo(" TX Frequency: %uHz (%uHz)", txFrequency, txFrequency + txOffset); m_modem = CModem::createModem(port, m_duplex, rxInvert, txInvert, pttInvert, txDelay, dmrDelay, trace, debug); m_modem->setSerialParams(protocol,address); m_modem->setModeParams(m_dstarEnabled, m_dmrEnabled, m_ysfEnabled, m_p25Enabled, m_nxdnEnabled, m_pocsagEnabled, m_fmEnabled, m_ax25Enabled); - m_modem->setLevels(rxLevel, cwIdTXLevel, dstarTXLevel, dmrTXLevel, ysfTXLevel, p25TXLevel, nxdnTXLevel, pocsagTXLevel, fmTXLevel); + m_modem->setLevels(rxLevel, cwIdTXLevel, dstarTXLevel, dmrTXLevel, ysfTXLevel, p25TXLevel, nxdnTXLevel, pocsagTXLevel, fmTXLevel, ax25TXLevel); m_modem->setRFParams(rxFrequency, rxOffset, txFrequency, txOffset, txDCOffset, rxDCOffset, rfLevel, pocsagFrequency); m_modem->setDMRParams(colorCode); m_modem->setYSFParams(lowDeviation, ysfTXHang); m_modem->setP25Params(p25TXHang); m_modem->setNXDNParams(nxdnTXHang); + m_modem->setAX25Params(rxTwist, txTwist); if (m_fmEnabled) { std::string callsign = m_conf.getFMCallsign(); diff --git a/Version.h b/Version.h index ffc27e8..7cfe66a 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200619"; +const char* VERSION = "20200620"; #endif From 692d6519db4dd9644cb430bda117efdaeea59da5 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 21 Jun 2020 14:15:37 +0100 Subject: [PATCH 061/163] Add the AX.25 modem parameters and start reworking the AX,25 RX processing. --- AX25Control.cpp | 20 +++---------------- AX25Control.h | 1 - AX25Network.cpp | 30 +++++++++++++++------------- AX25Network.h | 22 +++++++++++++-------- Conf.cpp | 52 +++++++++++++------------------------------------ Conf.h | 14 ++++--------- MMDVM.ini | 7 ++----- MMDVMHost.cpp | 26 ++++++++++--------------- Modem.cpp | 45 +++++++++++++++++++++++++++++------------- Modem.h | 6 +++++- Version.h | 2 +- 11 files changed, 101 insertions(+), 124 deletions(-) diff --git a/AX25Control.cpp b/AX25Control.cpp index a6b89ea..563fa42 100644 --- a/AX25Control.cpp +++ b/AX25Control.cpp @@ -51,12 +51,10 @@ bool CAX25Control::writeModem(unsigned char *data, unsigned int len) CUtils::dump(1U, "AX.25 raw packet", data, len); - if (m_network != NULL) { - if (isUI(data, len)) - m_network->writeAX25(data, len); - } + if (m_network == NULL) + return true; - return true; + return m_network->write(data, len); } bool CAX25Control::openFile() @@ -233,15 +231,3 @@ bool CAX25Control::decodeAddress(const unsigned char* data, std::string& text, b return (data[6U] & 0x01U) == 0x00U; } - -bool CAX25Control::isUI(const unsigned char* data, unsigned int length) const -{ - assert(data != NULL); - assert(length >= 15U); - - unsigned int n = 13U; - while ((data[n] & 0x01U) == 0x00U && n < length) - n += 7U; - - return (data[n + 1U] & 0xEFU) == 0x03U; -} diff --git a/AX25Control.h b/AX25Control.h index 59b5688..b0217df 100644 --- a/AX25Control.h +++ b/AX25Control.h @@ -38,7 +38,6 @@ private: bool m_enabled; FILE* m_fp; - bool isUI(const unsigned char* data, unsigned int length) const; void decode(const unsigned char* data, unsigned int length); bool decodeAddress(const unsigned char* data, std::string& text, bool isDigi = false) const; bool openFile(); diff --git a/AX25Network.cpp b/AX25Network.cpp index 3f5ac49..5330ec8 100644 --- a/AX25Network.cpp +++ b/AX25Network.cpp @@ -27,34 +27,38 @@ const unsigned int BUFFER_LENGTH = 200U; -CAX25Network::CAX25Network(const std::string& localAddress, unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, bool debug) : -m_socket(localAddress, localPort), -m_address(), -m_port(gatewayPort), +CAX25Network::CAX25Network(const std::string& port, unsigned int speed, bool debug) : +m_serial(port, SERIAL_SPEED(speed), false), // XXX +m_txData(NULL), +m_txLength(0U), +m_txOffset(0U), +m_rxData(NULL), +m_rxLength(0U), +m_rxComplete(false), m_debug(debug), m_enabled(false) { - assert(gatewayPort > 0U); - assert(!gatewayAddress.empty()); + assert(!port.empty()); + assert(speed > 0U); - m_address = CUDPSocket::lookup(gatewayAddress); + m_txData = new unsigned char[BUFFER_LENGTH]; + m_rxData = new unsigned char[BUFFER_LENGTH]; } CAX25Network::~CAX25Network() { + delete[] m_txData; + delete[] m_rxData; } bool CAX25Network::open() { LogMessage("Opening AX25 network connection"); - if (m_address.s_addr == INADDR_NONE) - return false; - - return m_socket.open(); + return m_serial.open(); } -bool CAX25Network::writeAX25(const unsigned char* data, unsigned int length) +bool CAX25Network::write(const unsigned char* data, unsigned int length) { assert(data != NULL); @@ -80,7 +84,7 @@ void CAX25Network::reset() void CAX25Network::close() { - m_socket.close(); + m_serial.close(); LogMessage("Closing AX25 network connection"); } diff --git a/AX25Network.h b/AX25Network.h index 866ae7e..9c9cecb 100644 --- a/AX25Network.h +++ b/AX25Network.h @@ -19,32 +19,38 @@ #ifndef AX25Network_H #define AX25Network_H -#include "UDPSocket.h" +#include "SerialController.h" #include #include class CAX25Network { public: - CAX25Network(const std::string& localAddress, unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, bool debug); + CAX25Network(const std::string& port, unsigned int speed, bool debug); ~CAX25Network(); bool open(); void enable(bool enabled); - bool writeAX25(const unsigned char* data, unsigned int length); + bool write(const unsigned char* data, unsigned int length); + + unsigned int read(unsigned char* data, unsigned int length); void reset(); void close(); private: - CUDPSocket m_socket; - in_addr m_address; - unsigned int m_port; - bool m_debug; - bool m_enabled; + CSerialController m_serial; + unsigned char* m_txData; + unsigned int m_txLength; + unsigned int m_txOffset; + unsigned char* m_rxData; + unsigned int m_rxLength; + bool m_rxComplete; + bool m_debug; + bool m_enabled; }; #endif diff --git a/Conf.cpp b/Conf.cpp index 85972c1..0ef321f 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -211,7 +211,6 @@ m_fmExtAudioBoost(1U), m_ax25Enabled(false), m_ax25RXTwist(6), m_ax25TXTwist(6), -m_ax25Digipeat(true), m_ax25Trace(false), m_dstarNetworkEnabled(false), m_dstarGatewayAddress(), @@ -259,10 +258,8 @@ m_pocsagLocalPort(0U), m_pocsagNetworkModeHang(3U), m_pocsagNetworkDebug(false), m_ax25NetworkEnabled(false), -m_ax25GatewayAddress(), -m_ax25GatewayPort(0U), -m_ax25LocalAddress(), -m_ax25LocalPort(0U), +m_ax25NetworkPort(), +m_ax25NetworkSpeed(9600U), m_ax25NetworkDebug(false), m_tftSerialPort("/dev/ttyAMA0"), m_tftSerialBrightness(50U), @@ -802,8 +799,6 @@ bool CConf::read() m_ax25RXTwist = ::atoi(value); else if (::strcmp(key, "TXTwist") == 0) m_ax25TXTwist = ::atoi(value); - else if (::strcmp(key, "Digipeat") == 0) - m_ax25Digipeat = ::atoi(value) == 1; else if (::strcmp(key, "Trace") == 0) m_ax25Trace = ::atoi(value) == 1; } else if (section == SECTION_DSTAR_NETWORK) { @@ -903,18 +898,14 @@ bool CConf::read() else if (::strcmp(key, "Debug") == 0) m_pocsagNetworkDebug = ::atoi(value) == 1; } else if (section == SECTION_AX25_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_ax25NetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "LocalAddress") == 0) - m_ax25LocalAddress = value; - else if (::strcmp(key, "LocalPort") == 0) - m_ax25LocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "GatewayAddress") == 0) - m_ax25GatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_ax25GatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_ax25NetworkDebug = ::atoi(value) == 1; + if (::strcmp(key, "Enable") == 0) + m_ax25NetworkEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Port") == 0) + m_ax25NetworkPort = value; + else if (::strcmp(key, "Speed") == 0) + m_ax25NetworkSpeed = (unsigned int)::atoi(value); + else if (::strcmp(key, "Debug") == 0) + m_ax25NetworkDebug = ::atoi(value) == 1; } else if (section == SECTION_TFTSERIAL) { if (::strcmp(key, "Port") == 0) m_tftSerialPort = value; @@ -1743,11 +1734,6 @@ int CConf::getAX25TXTwist() const return m_ax25TXTwist; } -bool CConf::getAX25Digipeat() const -{ - return m_ax25Digipeat; -} - bool CConf::getAX25Trace() const { return m_ax25Trace; @@ -1983,24 +1969,14 @@ bool CConf::getAX25NetworkEnabled() const return m_ax25NetworkEnabled; } -std::string CConf::getAX25GatewayAddress() const +std::string CConf::getAX25NetworkPort() const { - return m_ax25GatewayAddress; + return m_ax25NetworkPort; } -unsigned int CConf::getAX25GatewayPort() const +unsigned int CConf::getAX25NetworkSpeed() const { - return m_ax25GatewayPort; -} - -std::string CConf::getAX25LocalAddress() const -{ - return m_ax25LocalAddress; -} - -unsigned int CConf::getAX25LocalPort() const -{ - return m_ax25LocalPort; + return m_ax25NetworkSpeed; } bool CConf::getAX25NetworkDebug() const diff --git a/Conf.h b/Conf.h index 8379061..335c1e3 100644 --- a/Conf.h +++ b/Conf.h @@ -176,7 +176,6 @@ public: bool getAX25Enabled() const; int getAX25RXTwist() const; int getAX25TXTwist() const; - bool getAX25Digipeat() const; bool getAX25Trace() const; // The FM Section @@ -271,10 +270,8 @@ public: // The AX.25 Network section bool getAX25NetworkEnabled() const; - std::string getAX25GatewayAddress() const; - unsigned int getAX25GatewayPort() const; - std::string getAX25LocalAddress() const; - unsigned int getAX25LocalPort() const; + std::string getAX25NetworkPort() const; + unsigned int getAX25NetworkSpeed() const; bool getAX25NetworkDebug() const; // The TFTSERIAL section @@ -463,7 +460,6 @@ private: bool m_ax25Enabled; int m_ax25RXTwist; int m_ax25TXTwist; - bool m_ax25Digipeat; bool m_ax25Trace; bool m_fmEnabled; @@ -550,10 +546,8 @@ private: bool m_pocsagNetworkDebug; bool m_ax25NetworkEnabled; - std::string m_ax25GatewayAddress; - unsigned int m_ax25GatewayPort; - std::string m_ax25LocalAddress; - unsigned int m_ax25LocalPort; + std::string m_ax25NetworkPort; + unsigned int m_ax25NetworkSpeed; bool m_ax25NetworkDebug; std::string m_tftSerialPort; diff --git a/MMDVM.ini b/MMDVM.ini index dcbb0b8..6b12fff 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -180,7 +180,6 @@ ExtAudioBoost=1 Enable=1 TXTwist=6 RXTwist=6 -Digipeat=1 Trace=1 [D-Star Network] @@ -242,10 +241,8 @@ Debug=0 [AX.25 Network] Enable=1 -LocalAddress=127.0.0.1 -LocalPort=47325 -GatewayAddress=127.0.0.1 -GatewayPort=47326 +Port=/dev/ttyp7 +Speed=9600 Debug=0 [TFT Serial] diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index db24632..12e71b5 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -626,18 +626,16 @@ int CMMDVMHost::run() } if (m_ax25Enabled) { - int rxTwist = m_conf.getAX25RXTwist(); - int txTwist = m_conf.getAX25TXTwist(); - bool digipeat = m_conf.getAX25Digipeat(); - bool trace = m_conf.getAX25Trace(); + int rxTwist = m_conf.getAX25RXTwist(); + int txTwist = m_conf.getAX25TXTwist(); + bool trace = m_conf.getAX25Trace(); LogInfo("AX.25 RF Parameters"); LogInfo(" RXTwist: %d", rxTwist); LogInfo(" TXTwist: %d", txTwist); - LogInfo(" Digipeat: %s", digipeat ? "yes" : "no"); LogInfo(" Trace: %s", trace ? "yes" : "no"); - m_ax25 = new CAX25Control(m_ax25Network, digipeat, trace); + m_ax25 = new CAX25Control(m_ax25Network, trace); } bool remoteControlEnabled = m_conf.getRemoteControlEnabled(); @@ -1584,19 +1582,15 @@ bool CMMDVMHost::createPOCSAGNetwork() bool CMMDVMHost::createAX25Network() { - std::string gatewayAddress = m_conf.getAX25GatewayAddress(); - unsigned int gatewayPort = m_conf.getAX25GatewayPort(); - std::string localAddress = m_conf.getAX25LocalAddress(); - unsigned int localPort = m_conf.getAX25LocalPort(); - bool debug = m_conf.getAX25NetworkDebug(); + std::string port = m_conf.getAX25NetworkPort(); + unsigned int speed = m_conf.getAX25NetworkSpeed(); + bool debug = m_conf.getAX25NetworkDebug(); LogInfo("AX.25 Network Parameters"); - LogInfo(" Gateway Address: %s", gatewayAddress.c_str()); - LogInfo(" Gateway Port: %u", gatewayPort); - LogInfo(" Local Address: %s", localAddress.c_str()); - LogInfo(" Local Port: %u", localPort); + LogInfo(" Port: %s", port.c_str()); + LogInfo(" Speed: %u", speed); - m_ax25Network = new CAX25Network(localAddress, localPort, gatewayAddress, gatewayPort, debug); + m_ax25Network = new CAX25Network(port, speed, debug); bool ret = m_ax25Network->open(); if (!ret) { diff --git a/Modem.cpp b/Modem.cpp index bcdde83..f1c6449 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -114,15 +114,17 @@ m_txInvert(txInvert), m_pttInvert(pttInvert), m_txDelay(txDelay), m_dmrDelay(dmrDelay), -m_rxLevel(0U), -m_cwIdTXLevel(0U), -m_dstarTXLevel(0U), -m_dmrTXLevel(0U), -m_ysfTXLevel(0U), -m_p25TXLevel(0U), -m_nxdnTXLevel(0U), -m_pocsagTXLevel(0U), -m_fmTXLevel(0U), +m_rxLevel(0.0F), +m_cwIdTXLevel(0.0F), +m_dstarTXLevel(0.0F), +m_dmrTXLevel(0.0F), +m_ysfTXLevel(0.0F), +m_p25TXLevel(0.0F), +m_nxdnTXLevel(0.0F), +m_pocsagTXLevel(0.0F), +m_fmTXLevel(0.0F), +m_ax25TXLevel(0.0F), +m_rfLevel(0.0F), m_trace(trace), m_debug(debug), m_rxFrequency(0U), @@ -177,6 +179,8 @@ m_lockout(false), m_error(false), m_mode(MODE_IDLE), m_hwType(HWT_UNKNOWN), +m_ax25RXTwist(0), +m_ax25TXTwist(0), m_fmCallsign(), m_fmCallsignSpeed(20U), m_fmCallsignFrequency(1000U), @@ -248,7 +252,7 @@ void CModem::setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, m_ax25Enabled = ax25Enabled; } -void CModem::setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagTXLevel, float fmTXLevel) +void CModem::setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagTXLevel, float fmTXLevel, float ax25TXLevel) { m_rxLevel = rxLevel; m_cwIdTXLevel = cwIdTXLevel; @@ -259,6 +263,7 @@ void CModem::setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, flo m_nxdnTXLevel = nxdnTXLevel; m_pocsagTXLevel = pocsagTXLevel; m_fmTXLevel = fmTXLevel; + m_ax25TXLevel = ax25TXLevel; } void CModem::setDMRParams(unsigned int colorCode) @@ -284,6 +289,12 @@ void CModem::setNXDNParams(unsigned int txHang) m_nxdnTXHang = txHang; } +void CModem::setAX25Params(int rxTwist, int txTwist) +{ + m_ax25RXTwist = rxTwist; + m_ax25TXTwist = txTwist; +} + void CModem::setTransparentDataParams(unsigned int sendFrameType) { m_sendTransparentDataFrameType = sendFrameType; @@ -1551,7 +1562,7 @@ bool CModem::setConfig() buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 24U; + buffer[1U] = 27U; buffer[2U] = MMDVM_SET_CONFIG; @@ -1621,10 +1632,16 @@ bool CModem::setConfig() buffer[23U] = (unsigned char)m_nxdnTXHang; - // CUtils::dump(1U, "Written", buffer, 24U); + buffer[24U] = (unsigned char)(m_ax25TXLevel * 2.55F + 0.5F); - int ret = m_serial->write(buffer, 24U); - if (ret != 24) + buffer[25U] = (unsigned char)(m_ax25RXTwist + 128); + + buffer[26U] = (unsigned char)(m_ax25TXTwist + 128); + + // CUtils::dump(1U, "Written", buffer, 27U); + + int ret = m_serial->write(buffer, 27U); + if (ret != 27) return false; unsigned int count = 0U; diff --git a/Modem.h b/Modem.h index ea01945..1f8b291 100644 --- a/Modem.h +++ b/Modem.h @@ -48,11 +48,12 @@ public: virtual void setSerialParams(const std::string& protocol, unsigned int address); virtual void setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency); virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled, bool ax25Enabled); - virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel); + virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel, float ax25TXLevel); virtual void setDMRParams(unsigned int colorCode); virtual void setYSFParams(bool loDev, unsigned int txHang); virtual void setP25Params(unsigned int txHang); virtual void setNXDNParams(unsigned int txHang); + virtual void setAX25Params(int rxTwist, int txTwist); virtual void setTransparentDataParams(unsigned int sendFrameType); virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch); @@ -146,6 +147,7 @@ private: float m_nxdnTXLevel; float m_pocsagTXLevel; float m_fmTXLevel; + float m_ax25TXLevel; float m_rfLevel; bool m_trace; bool m_debug; @@ -201,6 +203,8 @@ private: bool m_error; unsigned char m_mode; HW_TYPE m_hwType; + int m_ax25RXTwist; + int m_ax25TXTwist; std::string m_fmCallsign; unsigned int m_fmCallsignSpeed; diff --git a/Version.h b/Version.h index 7cfe66a..5f48e8f 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200620"; +const char* VERSION = "20200621"; #endif From 38f59236aeebac5415d01f28325cb43773966693 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 21 Jun 2020 14:28:40 +0100 Subject: [PATCH 062/163] Add the KISS encoding. --- AX25Network.cpp | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/AX25Network.cpp b/AX25Network.cpp index 5330ec8..82c341b 100644 --- a/AX25Network.cpp +++ b/AX25Network.cpp @@ -25,7 +25,14 @@ #include #include -const unsigned int BUFFER_LENGTH = 200U; +const unsigned int BUFFER_LENGTH = 500U; + +const unsigned char AX25_KISS_DATA = 0x00U; + +const unsigned char AX25_FEND = 0xC0U; +const unsigned char AX25_FESC = 0xDBU; +const unsigned char AX25_TFEND = 0xDCU; +const unsigned char AX25_TFESC = 0xDDU; CAX25Network::CAX25Network(const std::string& port, unsigned int speed, bool debug) : m_serial(port, SERIAL_SPEED(speed), false), // XXX @@ -62,20 +69,36 @@ bool CAX25Network::write(const unsigned char* data, unsigned int length) { assert(data != NULL); - unsigned char buffer[110U]; - ::memset(buffer, 0x00U, 110U); + m_txLength = 0U; + m_txOffset = 0U; - buffer[0U] = 'A'; - buffer[1U] = 'X'; - buffer[2U] = '2'; - buffer[3U] = '5'; + m_txData[m_txLength++] = AX25_FEND; + m_txData[m_txLength++] = AX25_KISS_DATA; - ::memcpy(buffer + 4U, data, length); + for (unsigned int i = 0U; i < length; i++) { + unsigned char c = data[i]; + + switch (c) { + case AX25_FEND: + m_txData[m_txLength++] = AX25_FESC; + m_txData[m_txLength++] = AX25_TFEND; + break; + case AX25_FESC: + m_txData[m_txLength++] = AX25_FESC; + m_txData[m_txLength++] = AX25_TFESC; + break; + default: + m_txData[m_txLength++] = c; + break; + } + } + + m_txData[m_txLength++] = AX25_FEND; if (m_debug) - CUtils::dump(1U, "AX25 Network Data Sent", buffer, length + 4U); + CUtils::dump(1U, "AX25 Network Data Sent", m_txData, m_txLength); - return m_socket.write(buffer, length + 4U, m_address, m_port); + return m_serial.write(m_txData, m_txLength); } void CAX25Network::reset() From 27c9ad43eccead2773013b8691789d49cb4170ff Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 21 Jun 2020 17:22:27 +0100 Subject: [PATCH 063/163] Add KISS receive. --- AX25Network.cpp | 100 ++++++++++++++++++++++++++++++++++++++++-------- AX25Network.h | 4 +- 2 files changed, 86 insertions(+), 18 deletions(-) diff --git a/AX25Network.cpp b/AX25Network.cpp index 82c341b..7da9a4c 100644 --- a/AX25Network.cpp +++ b/AX25Network.cpp @@ -37,11 +37,9 @@ const unsigned char AX25_TFESC = 0xDDU; CAX25Network::CAX25Network(const std::string& port, unsigned int speed, bool debug) : m_serial(port, SERIAL_SPEED(speed), false), // XXX m_txData(NULL), -m_txLength(0U), -m_txOffset(0U), m_rxData(NULL), m_rxLength(0U), -m_rxComplete(false), +m_rxLastChar(0U), m_debug(debug), m_enabled(false) { @@ -69,36 +67,103 @@ bool CAX25Network::write(const unsigned char* data, unsigned int length) { assert(data != NULL); - m_txLength = 0U; - m_txOffset = 0U; + if (!m_enabled) + return true; - m_txData[m_txLength++] = AX25_FEND; - m_txData[m_txLength++] = AX25_KISS_DATA; + unsigned int txLength = 0U; + + m_txData[txLength++] = AX25_FEND; + m_txData[txLength++] = AX25_KISS_DATA; for (unsigned int i = 0U; i < length; i++) { unsigned char c = data[i]; switch (c) { case AX25_FEND: - m_txData[m_txLength++] = AX25_FESC; - m_txData[m_txLength++] = AX25_TFEND; + m_txData[txLength++] = AX25_FESC; + m_txData[txLength++] = AX25_TFEND; break; case AX25_FESC: - m_txData[m_txLength++] = AX25_FESC; - m_txData[m_txLength++] = AX25_TFESC; + m_txData[txLength++] = AX25_FESC; + m_txData[txLength++] = AX25_TFESC; break; default: - m_txData[m_txLength++] = c; + m_txData[txLength++] = c; break; } } - m_txData[m_txLength++] = AX25_FEND; + m_txData[txLength++] = AX25_FEND; if (m_debug) - CUtils::dump(1U, "AX25 Network Data Sent", m_txData, m_txLength); + CUtils::dump(1U, "AX25 Network Data Sent", m_txData, txLength); - return m_serial.write(m_txData, m_txLength); + return m_serial.write(m_txData, txLength); +} + +unsigned int CAX25Network::read(unsigned char* data, unsigned int length) +{ + assert(data != NULL); + + bool complete = false; + + unsigned char c; + while (m_serial.read(&c, 1U) > 0U) { + if (m_rxLength == 0U && c == AX25_FEND) + m_rxData[m_rxLength++] = c; + else if (m_rxLength > 0U) + m_rxData[m_rxLength++] = c; + + if (m_rxLength > 1U && c == AX25_FEND) { + complete = true; + break; + } + } + + if (!m_enabled) + return 0U; + + if (!complete) + return 0U; + + if (m_rxLength == 0U) + return 0U; + + if (m_rxData[1U] != AX25_KISS_DATA) { + m_rxLength = 0U; + return 0U; + } + + complete = false; + + unsigned int dataLen = 0U; + for (unsigned int i = 2U; i < m_rxLength; i++) { + unsigned char c = m_rxData[i]; + + if (c == AX25_FEND) { + complete = true; + break; + } else if (c == AX25_TFEND && m_rxLastChar == AX25_FESC) { + data[dataLen++] = AX25_FEND; + } else if (c == AX25_TFESC && m_rxLastChar == AX25_FESC) { + data[dataLen++] = AX25_FESC; + } else if (c != AX25_FESC) { + data[dataLen++] = c; + } + + m_rxLastChar = c; + } + + if (!complete) + return 0U; + + if (m_debug) + CUtils::dump(1U, "AX25 Network Data Received", m_rxData, m_rxLength); + + m_rxLength = 0U; + m_rxLastChar = 0U; + + return dataLen; } void CAX25Network::reset() @@ -115,4 +180,9 @@ void CAX25Network::close() void CAX25Network::enable(bool enabled) { m_enabled = enabled; + + if (enabled && !m_enabled) { + m_rxLastChar = 0U; + m_rxLength = 0U; + } } diff --git a/AX25Network.h b/AX25Network.h index 9c9cecb..37804d6 100644 --- a/AX25Network.h +++ b/AX25Network.h @@ -44,11 +44,9 @@ public: private: CSerialController m_serial; unsigned char* m_txData; - unsigned int m_txLength; - unsigned int m_txOffset; unsigned char* m_rxData; unsigned int m_rxLength; - bool m_rxComplete; + unsigned char m_rxLastChar; bool m_debug; bool m_enabled; }; From c026471aec8b5618b806a1f3c1a1c907200b2c7a Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 21 Jun 2020 18:53:38 +0100 Subject: [PATCH 064/163] Start the AX.25 transmit path development. --- AX25Control.cpp | 20 +++++++++++++++++++- AX25Control.h | 2 ++ AX25Network.cpp | 2 +- MMDVMHost.cpp | 16 +++++++++++++++- Modem.h | 2 ++ 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/AX25Control.cpp b/AX25Control.cpp index 563fa42..00cfc0d 100644 --- a/AX25Control.cpp +++ b/AX25Control.cpp @@ -49,7 +49,7 @@ bool CAX25Control::writeModem(unsigned char *data, unsigned int len) if (m_trace) decode(data, len); - CUtils::dump(1U, "AX.25 raw packet", data, len); + CUtils::dump(1U, "AX.25 received packet", data, len); if (m_network == NULL) return true; @@ -57,6 +57,24 @@ bool CAX25Control::writeModem(unsigned char *data, unsigned int len) return m_network->write(data, len); } +unsigned int CAX25Control::readModem(unsigned char* data) +{ + assert(data != NULL); + + if (m_network == NULL) + return 0U; + + if (!m_enabled) + return 0U; + + unsigned int length = m_network->read(data, 500U); + + if (length > 0U) + CUtils::dump(1U, "AX.25 transmitted packet", data, length); + + return length; +} + bool CAX25Control::openFile() { if (m_fp != NULL) diff --git a/AX25Control.h b/AX25Control.h index b0217df..b4fed2a 100644 --- a/AX25Control.h +++ b/AX25Control.h @@ -30,6 +30,8 @@ public: bool writeModem(unsigned char* data, unsigned int len); + unsigned int readModem(unsigned char* data); + void enable(bool enabled); private: diff --git a/AX25Network.cpp b/AX25Network.cpp index 7da9a4c..806844f 100644 --- a/AX25Network.cpp +++ b/AX25Network.cpp @@ -181,7 +181,7 @@ void CAX25Network::enable(bool enabled) { m_enabled = enabled; - if (enabled && !m_enabled) { + if (enabled != m_enabled) { m_rxLastChar = 0U; m_rxLength = 0U; } diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 12e71b5..6c10567 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -688,7 +688,7 @@ int CMMDVMHost::run() m_ump->setCD(cd); } - unsigned char data[220U]; + unsigned char data[500U]; unsigned int len; bool ret; @@ -989,6 +989,20 @@ int CMMDVMHost::run() } } + if (m_ax25 != NULL) { + ret = m_modem->hasAX25Space(); + if (ret) { + len = m_ax25->readModem(data); + if (len > 0U) { + if (m_mode == MODE_IDLE || m_mode == MODE_FM) { + m_modem->writeAX25Data(data, len); + } else if (m_mode != MODE_LOCKOUT) { + LogWarning("AX.25 data received when in mode %u", m_mode); + } + } + } + } + if (transparentSocket != NULL) { in_addr address; unsigned int port = 0U; diff --git a/Modem.h b/Modem.h index 1f8b291..29ecbe4 100644 --- a/Modem.h +++ b/Modem.h @@ -80,6 +80,7 @@ public: virtual bool hasP25Space() const; virtual bool hasNXDNSpace() const; virtual bool hasPOCSAGSpace() const; + virtual bool hasAX25Space() const; virtual bool hasTX() const; virtual bool hasCD() const; @@ -95,6 +96,7 @@ public: virtual bool writeP25Data(const unsigned char* data, unsigned int length); virtual bool writeNXDNData(const unsigned char* data, unsigned int length); virtual bool writePOCSAGData(const unsigned char* data, unsigned int length); + virtual bool writeAX25Data(const unsigned char* data, unsigned int length); virtual bool writeTransparentData(const unsigned char* data, unsigned int length); From 92ceba052a996b7925b28c17ee28447939744324 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 21 Jun 2020 21:16:31 +0100 Subject: [PATCH 065/163] Add AX.25 transmit functionality to the modem handler. --- AX25Control.cpp | 1 + AX25Defines.h | 39 +++++++++++++++++++++++++ AX25Network.cpp | 7 +---- MMDVMHost.vcxproj | 1 + MMDVMHost.vcxproj.filters | 3 ++ Modem.cpp | 61 ++++++++++++++++++++++++++++++++++++++- Modem.h | 2 ++ 7 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 AX25Defines.h diff --git a/AX25Control.cpp b/AX25Control.cpp index 00cfc0d..3b108fc 100644 --- a/AX25Control.cpp +++ b/AX25Control.cpp @@ -12,6 +12,7 @@ */ #include "AX25Control.h" +#include "AX25Defines.h" #include "Utils.h" #include "Log.h" diff --git a/AX25Defines.h b/AX25Defines.h new file mode 100644 index 0000000..1cb145e --- /dev/null +++ b/AX25Defines.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(AX25Defines_H) +#define AX25Defines_H + +const unsigned int AX25_CALLSIGN_TEXT_LENGTH = 6U; +const unsigned int AX25_SSID_LENGTH = 1U; +const unsigned int AX25_CALLSIGN_LENGTH = 7U; + +const unsigned int AX25_MAX_DIGIPEATERS = 6U; + +const unsigned char AX25_PID_NOL3 = 0xF0U; + +const unsigned int AX25_MAX_FRAME_LENGTH_BYTES = 300U; + +const unsigned char AX25_KISS_DATA = 0x00U; + +const unsigned char AX25_FEND = 0xC0U; +const unsigned char AX25_FESC = 0xDBU; +const unsigned char AX25_TFEND = 0xDCU; +const unsigned char AX25_TFESC = 0xDDU; + +#endif diff --git a/AX25Network.cpp b/AX25Network.cpp index 806844f..2f25a47 100644 --- a/AX25Network.cpp +++ b/AX25Network.cpp @@ -17,6 +17,7 @@ */ #include "AX25Network.h" +#include "AX25Defines.h" #include "Defines.h" #include "Utils.h" #include "Log.h" @@ -27,12 +28,6 @@ const unsigned int BUFFER_LENGTH = 500U; -const unsigned char AX25_KISS_DATA = 0x00U; - -const unsigned char AX25_FEND = 0xC0U; -const unsigned char AX25_FESC = 0xDBU; -const unsigned char AX25_TFEND = 0xDCU; -const unsigned char AX25_TFESC = 0xDDU; CAX25Network::CAX25Network(const std::string& port, unsigned int speed, bool debug) : m_serial(port, SERIAL_SPEED(speed), false), // XXX diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index 693adb1..535e686 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -154,6 +154,7 @@ + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index b4bee02..d5c8464 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -311,6 +311,9 @@ Header Files + + Header Files + diff --git a/Modem.cpp b/Modem.cpp index f1c6449..1ed2dee 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -22,6 +22,7 @@ #include "YSFDefines.h" #include "P25Defines.h" #include "NXDNDefines.h" +#include "AX25Defines.h" #include "POCSAGDefines.h" #include "Thread.h" #include "Modem.h" @@ -160,6 +161,7 @@ m_rxNXDNData(1000U, "Modem RX NXDN"), m_txNXDNData(1000U, "Modem TX NXDN"), m_txPOCSAGData(1000U, "Modem TX POCSAG"), m_rxAX25Data(1000U, "Modem RX AX.25"), +m_txAX25Data(1000U, "Modem TX AX.25"), m_rxTransparentData(1000U, "Modem RX Transparent"), m_txTransparentData(1000U, "Modem TX Transparent"), m_sendTransparentDataFrameType(0U), @@ -173,6 +175,7 @@ m_ysfSpace(0U), m_p25Space(0U), m_nxdnSpace(0U), m_pocsagSpace(0U), +m_ax25Space(0U), m_tx(false), m_cd(false), m_lockout(false), @@ -621,6 +624,7 @@ void CModem::clock(unsigned int ms) m_p25Space = 0U; m_nxdnSpace = 0U; m_pocsagSpace = 0U; + m_ax25Space = 0U; m_mode = m_buffer[m_offset + 1U]; @@ -657,9 +661,11 @@ void CModem::clock(unsigned int ms) m_nxdnSpace = m_buffer[m_offset + 8U]; if (m_length > (m_offset + 9U)) m_pocsagSpace = m_buffer[m_offset + 9U]; + if (m_length > (m_offset + 10U)) + m_ax25Space = m_buffer[m_offset + 10U] * 8U; m_inactivityTimer.start(); - // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[m_offset + 2U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_pocsagSpace, int(m_lockout), int(m_cd)); + // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[m_offset + 2U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_pocsagSpace, m_ax25Space, int(m_lockout), int(m_cd)); } break; @@ -863,6 +869,23 @@ void CModem::clock(unsigned int ms) m_pocsagSpace--; } + if (m_ax25Space > 1U && !m_txAX25Data.isEmpty()) { + unsigned char len = 0U; + m_txAX25Data.getData((unsigned char*)&len, sizeof(unsigned int)); + m_txAX25Data.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX AX.25 Data", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing AX.25 data to the MMDVM"); + + m_playoutTimer.start(); + + m_ax25Space -= len; + } + if (!m_txTransparentData.isEmpty()) { unsigned char len = 0U; m_txTransparentData.getData(&len, 1U); @@ -1225,6 +1248,42 @@ bool CModem::writePOCSAGData(const unsigned char* data, unsigned int length) return true; } +bool CModem::hasAX25Space() const +{ + unsigned int space = m_txAX25Data.freeSpace() / (AX25_MAX_FRAME_LENGTH_BYTES + 5U); + + return space > 1U; +} + +bool CModem::writeAX25Data(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + unsigned char buffer[500U]; + + unsigned int len; + if (length > 252U) { + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 0U; + buffer[2U] = (length + 4U) - 255U; + buffer[3U] = MMDVM_AX25_DATA; + ::memcpy(buffer + 4U, data, length); + len = length + 4U; + } else { + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 3U; + buffer[2U] = MMDVM_AX25_DATA; + ::memcpy(buffer + 3U, data, length); + len = length + 3U; + } + + m_txAX25Data.addData((unsigned char*)&len, sizeof(unsigned int)); + m_txAX25Data.addData(buffer, len); + + return true; +} + bool CModem::writeTransparentData(const unsigned char* data, unsigned int length) { assert(data != NULL); diff --git a/Modem.h b/Modem.h index 29ecbe4..a6e711e 100644 --- a/Modem.h +++ b/Modem.h @@ -186,6 +186,7 @@ private: CRingBuffer m_txNXDNData; CRingBuffer m_txPOCSAGData; CRingBuffer m_rxAX25Data; + CRingBuffer m_txAX25Data; CRingBuffer m_rxTransparentData; CRingBuffer m_txTransparentData; unsigned int m_sendTransparentDataFrameType; @@ -199,6 +200,7 @@ private: unsigned int m_p25Space; unsigned int m_nxdnSpace; unsigned int m_pocsagSpace; + unsigned int m_ax25Space; bool m_tx; bool m_cd; bool m_lockout; From d8716adc434f3a254d62439aac677ca02584d276 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Wed, 24 Jun 2020 11:18:12 +0100 Subject: [PATCH 066/163] Change the free space handling for AX25. --- Modem.cpp | 4 ++-- Version.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modem.cpp b/Modem.cpp index 1ed2dee..daee6c3 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -869,7 +869,7 @@ void CModem::clock(unsigned int ms) m_pocsagSpace--; } - if (m_ax25Space > 1U && !m_txAX25Data.isEmpty()) { + if (m_ax25Space > 0U && !m_txAX25Data.isEmpty()) { unsigned char len = 0U; m_txAX25Data.getData((unsigned char*)&len, sizeof(unsigned int)); m_txAX25Data.getData(m_buffer, len); @@ -883,7 +883,7 @@ void CModem::clock(unsigned int ms) m_playoutTimer.start(); - m_ax25Space -= len; + m_ax25Space = 0U; } if (!m_txTransparentData.isEmpty()) { diff --git a/Version.h b/Version.h index 5f48e8f..9bb9c4a 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200621"; +const char* VERSION = "20200624"; #endif From 96a8ec0e354d1844648ce1f0900759e9bd651b89 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Wed, 24 Jun 2020 13:22:55 +0100 Subject: [PATCH 067/163] Change the maximum packet length. --- AX25Defines.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AX25Defines.h b/AX25Defines.h index 1cb145e..a0f4c80 100644 --- a/AX25Defines.h +++ b/AX25Defines.h @@ -27,8 +27,8 @@ const unsigned int AX25_MAX_DIGIPEATERS = 6U; const unsigned char AX25_PID_NOL3 = 0xF0U; -const unsigned int AX25_MAX_FRAME_LENGTH_BYTES = 300U; - +const unsigned int AX25_MAX_FRAME_LENGTH_BYTES = 330U; // Callsign (7) + Callsign (7) + 8 Digipeaters (56) + + // Control (1) + PID (1) + Data (256) + Checksum (2) const unsigned char AX25_KISS_DATA = 0x00U; const unsigned char AX25_FEND = 0xC0U; From aec1ae74f799aad24d771ad1b2e9951252cc4532 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Thu, 25 Jun 2020 20:40:13 +0200 Subject: [PATCH 068/163] add missing files in makefile --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 8b7136f..985ab3c 100644 --- a/Makefile +++ b/Makefile @@ -10,9 +10,9 @@ OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ - ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ - NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ + NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o \ + P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o From 63215634968f2fae813b4b2f4e6924254f338489 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Thu, 25 Jun 2020 21:13:28 +0200 Subject: [PATCH 069/163] add missing files in makefiles --- Makefile.Pi | 6 +++--- Makefile.Pi.Adafruit | 4 ++-- Makefile.Pi.HD44780 | 4 ++-- Makefile.Pi.OLED | 4 ++-- Makefile.Pi.PCF8574 | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Makefile.Pi b/Makefile.Pi index c4aea75..5631c0b 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -10,9 +10,9 @@ OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ - ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ - NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ + NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o \ + P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 1c98ef2..4a28051 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -11,8 +11,8 @@ OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ - ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ - NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ + NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index 3ade129..f5b6dc0 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -10,8 +10,8 @@ OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o IIDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ - ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ - NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ + NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index 3de00ec..d49d135 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -11,8 +11,8 @@ OBJECTS = AX25Control.o AX25Network.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIRDirectForm1Filter.o OLED.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ - NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + NXDNLayer3.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ + P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index c58b728..8e9518a 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -11,8 +11,8 @@ OBJECTS = AX25Control.o AX25Network.o \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ - ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ - NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ + NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o From cb51a145753870699a2c725dc711f2233e47ed73 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 25 Jun 2020 21:17:38 +0100 Subject: [PATCH 070/163] Create a pseudo TTY for the AX25 Network. --- AX25Network.h | 16 ++-- MMDVMHost.vcxproj | 2 + MMDVMHost.vcxproj.filters | 6 ++ Makefile | 2 +- PseudoTTYController.cpp | 161 ++++++++++++++++++++++++++++++++++++++ PseudoTTYController.h | 40 ++++++++++ 6 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 PseudoTTYController.cpp create mode 100644 PseudoTTYController.h diff --git a/AX25Network.h b/AX25Network.h index 37804d6..34aa43b 100644 --- a/AX25Network.h +++ b/AX25Network.h @@ -19,7 +19,7 @@ #ifndef AX25Network_H #define AX25Network_H -#include "SerialController.h" +#include "PseudoTTYController.h" #include #include @@ -42,13 +42,13 @@ public: void close(); private: - CSerialController m_serial; - unsigned char* m_txData; - unsigned char* m_rxData; - unsigned int m_rxLength; - unsigned char m_rxLastChar; - bool m_debug; - bool m_enabled; + CPseudoTTYController m_serial; + unsigned char* m_txData; + unsigned char* m_rxData; + unsigned int m_rxLength; + unsigned char m_rxLastChar; + bool m_debug; + bool m_enabled; }; #endif diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index 8c3375b..7f64db6 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -227,6 +227,7 @@ + @@ -326,6 +327,7 @@ + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index a144fce..00ed473 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -320,6 +320,9 @@ Header Files + + Header Files + @@ -601,5 +604,8 @@ Source Files + + Source Files + \ No newline at end of file diff --git a/Makefile b/Makefile index 985ab3c..3f34ccd 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ OBJECTS = AX25Control.o AX25Network.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o \ - P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o diff --git a/PseudoTTYController.cpp b/PseudoTTYController.cpp new file mode 100644 index 0000000..a21fb3d --- /dev/null +++ b/PseudoTTYController.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2002-2004,2007-2011,2013,2014-2017,2019,2020 by Jonathan Naylor G4KLX + * Copyright (C) 1999-2001 by Thomas Sailor HB9JNX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(_WIN32) && !defined(_WIN64) + +#include "SerialController.h" +#include "Log.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + + +CPseudoTTYController::CPseudoTTYController(const std::string& device, unsigned int speed, bool assertRTS) : +CSerialController(device, speed, assertRTS) +{ +} + +CPseudoTTYController::~CPseudoTTYController() +{ +} + +bool CPseudoTTYController::open() +{ + assert(m_fd == -1); + + m_fd = ::posix_openpt(O_RDWR | O_NOCTTY | O_NDELAY); + if (m_fd < 0) { + LogError("Cannot open device - %s", m_device.c_str()); + return false; + } + + std::string slave = std::string(::ptsname(m_fd)); + + // Remove any previous stale symlink + ::unlink(m_device.c_str()); + + int ret = ::symlink(slave.c_str(), m_device.c_str()); + if (ret != 0) { + LogError("Cannot make symlink to %s with %s", slave.c_str(), m_device.c_str()); + close(); + return false; + } + + m_device = std::string(::ttyname(m_fd)); + + if (::isatty(m_fd)) { + termios termios; + if (::tcgetattr(m_fd, &termios) < 0) { + LogError("Cannot get the attributes for %s", m_device.c_str()); + ::close(m_fd); + return false; + } + + termios.c_iflag &= ~(IGNBRK | BRKINT | IGNPAR | PARMRK | INPCK); + termios.c_iflag &= ~(ISTRIP | INLCR | IGNCR | ICRNL); + termios.c_iflag &= ~(IXON | IXOFF | IXANY); + termios.c_oflag &= ~(OPOST); + termios.c_cflag &= ~(CSIZE | CSTOPB | PARENB | CRTSCTS); + termios.c_cflag |= (CS8 | CLOCAL | CREAD); + termios.c_lflag &= ~(ISIG | ICANON | IEXTEN); + termios.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL); + termios.c_cc[VMIN] = 0; + termios.c_cc[VTIME] = 10; + + switch (m_speed) { + case 1200U: + ::cfsetospeed(&termios, B1200); + ::cfsetispeed(&termios, B1200); + break; + case 2400U: + ::cfsetospeed(&termios, B2400); + ::cfsetispeed(&termios, B2400); + break; + case 4800U: + ::cfsetospeed(&termios, B4800); + ::cfsetispeed(&termios, B4800); + break; + case 9600U: + ::cfsetospeed(&termios, B9600); + ::cfsetispeed(&termios, B9600); + break; + case 19200U: + ::cfsetospeed(&termios, B19200); + ::cfsetispeed(&termios, B19200); + break; + case 38400U: + ::cfsetospeed(&termios, B38400); + ::cfsetispeed(&termios, B38400); + break; + case 115200U: + ::cfsetospeed(&termios, B115200); + ::cfsetispeed(&termios, B115200); + break; + case 230400U: + ::cfsetospeed(&termios, B230400); + ::cfsetispeed(&termios, B230400); + break; + case 460800U: + ::cfsetospeed(&termios, B460800); + ::cfsetispeed(&termios, B460800); + break; + default: + LogError("Unsupported serial port speed - %u", m_speed); + ::close(m_fd); + return false; + } + + if (::tcsetattr(m_fd, TCSANOW, &termios) < 0) { + LogError("Cannot set the attributes for %s", m_device.c_str()); + ::close(m_fd); + return false; + } + + if (m_assertRTS) { + unsigned int y; + if (::ioctl(m_fd, TIOCMGET, &y) < 0) { + LogError("Cannot get the control attributes for %s", m_device.c_str()); + ::close(m_fd); + return false; + } + + y |= TIOCM_RTS; + + if (::ioctl(m_fd, TIOCMSET, &y) < 0) { + LogError("Cannot set the control attributes for %s", m_device.c_str()); + ::close(m_fd); + return false; + } + } + } + + return true; +} + +#endif + diff --git a/PseudoTTYController.h b/PseudoTTYController.h new file mode 100644 index 0000000..9f9a921 --- /dev/null +++ b/PseudoTTYController.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#ifndef PseudoTTYController_H +#define PseudoTTYController_H + +#if !defined(_WIN32) && !defined(_WIN64) + +#include + +#include "SerialController.h" + +class CPseudoTTYController : public CSerialController { +public: + CPseudoTTYController(const std::string& device, unsigned int speed, bool assertRTS = false); + virtual ~CPseudoTTYController(); + + virtual bool open(); + +protected: +}; + +#endif + +#endif From e164538b09ef4bcd4dc86fdaf208ebda8a412271 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 25 Jun 2020 21:23:43 +0100 Subject: [PATCH 071/163] Fix compilation bugs. --- AX25Network.cpp | 2 +- Conf.h | 10 +++++----- PseudoTTYController.cpp | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/AX25Network.cpp b/AX25Network.cpp index 192b6df..dec4ea3 100644 --- a/AX25Network.cpp +++ b/AX25Network.cpp @@ -103,7 +103,7 @@ unsigned int CAX25Network::read(unsigned char* data, unsigned int length) bool complete = false; unsigned char c; - while (m_serial.read(&c, 1U) > 0U) { + while (m_serial.read(&c, 1U) > 0) { if (m_rxLength == 0U && c == AX25_FEND) m_rxData[m_rxLength++] = c; else if (m_rxLength > 0U) diff --git a/Conf.h b/Conf.h index c3c0c16..04c2be8 100644 --- a/Conf.h +++ b/Conf.h @@ -470,11 +470,6 @@ private: bool m_pocsagEnabled; unsigned int m_pocsagFrequency; - bool m_ax25Enabled; - int m_ax25RXTwist; - int m_ax25TXTwist; - bool m_ax25Trace; - bool m_fmEnabled; std::string m_fmCallsign; unsigned int m_fmCallsignSpeed; @@ -509,6 +504,11 @@ private: unsigned int m_fmExtAudioBoost; unsigned int m_fmModeHang; + bool m_ax25Enabled; + int m_ax25RXTwist; + int m_ax25TXTwist; + bool m_ax25Trace; + bool m_dstarNetworkEnabled; std::string m_dstarGatewayAddress; unsigned int m_dstarGatewayPort; diff --git a/PseudoTTYController.cpp b/PseudoTTYController.cpp index a21fb3d..e853c52 100644 --- a/PseudoTTYController.cpp +++ b/PseudoTTYController.cpp @@ -19,7 +19,7 @@ #if !defined(_WIN32) && !defined(_WIN64) -#include "SerialController.h" +#include "PseudoTTYController.h" #include "Log.h" #include From f151ca6c1ae21e0ea8c6dbc37294b4f56c461fab Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Fri, 26 Jun 2020 21:17:14 +0200 Subject: [PATCH 072/163] Make pty work --- Makefile | 2 +- PseudoTTYController.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 3f34ccd..bd07c05 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CC = cc CXX = c++ CFLAGS = -g -O3 -Wall -std=c++0x -pthread -LIBS = -lpthread +LIBS = -lpthread -lutil LDFLAGS = -g OBJECTS = AX25Control.o AX25Network.o \ diff --git a/PseudoTTYController.cpp b/PseudoTTYController.cpp index e853c52..71196bd 100644 --- a/PseudoTTYController.cpp +++ b/PseudoTTYController.cpp @@ -33,6 +33,7 @@ #include #include #include +#include CPseudoTTYController::CPseudoTTYController(const std::string& device, unsigned int speed, bool assertRTS) : @@ -48,9 +49,11 @@ bool CPseudoTTYController::open() { assert(m_fd == -1); - m_fd = ::posix_openpt(O_RDWR | O_NOCTTY | O_NDELAY); - if (m_fd < 0) { - LogError("Cannot open device - %s", m_device.c_str()); + int slavefd; + char buf[300]; + int result = ::openpty(&m_fd, &slavefd, buf, NULL,NULL); + if (result < 0) { + LogError("Cannot open device - %s - Errno : %d", m_device.c_str(), errno); return false; } From d1a809808b5c686e708c47d8e9a660eb9f928edd Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 27 Jun 2020 22:33:24 +0100 Subject: [PATCH 073/163] Rationalise the pseudo tty handling. --- PseudoTTYController.cpp | 112 ++++------------------- PseudoTTYController.h | 5 +- SerialController.cpp | 190 +++++++++++++++++++++------------------- SerialController.h | 1 + 4 files changed, 120 insertions(+), 188 deletions(-) diff --git a/PseudoTTYController.cpp b/PseudoTTYController.cpp index 71196bd..67df4a4 100644 --- a/PseudoTTYController.cpp +++ b/PseudoTTYController.cpp @@ -36,8 +36,9 @@ #include -CPseudoTTYController::CPseudoTTYController(const std::string& device, unsigned int speed, bool assertRTS) : -CSerialController(device, speed, assertRTS) +CPseudoTTYController::CPseudoTTYController(const std::string& symlink, unsigned int speed, bool assertRTS) : +CSerialController("", speed, assertRTS), +m_symlink(symlink) { } @@ -50,114 +51,35 @@ bool CPseudoTTYController::open() assert(m_fd == -1); int slavefd; - char buf[300]; - int result = ::openpty(&m_fd, &slavefd, buf, NULL,NULL); + char slave[300]; + int result = ::openpty(&m_fd, &slavefd, slave, NULL, NULL); if (result < 0) { - LogError("Cannot open device - %s - Errno : %d", m_device.c_str(), errno); + LogError("Cannot open the pseudo tty - errno : %d", errno); return false; } - std::string slave = std::string(::ptsname(m_fd)); - // Remove any previous stale symlink - ::unlink(m_device.c_str()); + ::unlink(m_symlink.c_str()); - int ret = ::symlink(slave.c_str(), m_device.c_str()); + int ret = ::symlink(slave, m_symlink.c_str()); if (ret != 0) { - LogError("Cannot make symlink to %s with %s", slave.c_str(), m_device.c_str()); + LogError("Cannot make symlink to %s with %s", slave, m_symlink.c_str()); close(); return false; } + LogMessage("Made symbolic link from %s to %s", slave, m_symlink.c_str()); + m_device = std::string(::ttyname(m_fd)); - if (::isatty(m_fd)) { - termios termios; - if (::tcgetattr(m_fd, &termios) < 0) { - LogError("Cannot get the attributes for %s", m_device.c_str()); - ::close(m_fd); - return false; - } + return setRaw(); +} - termios.c_iflag &= ~(IGNBRK | BRKINT | IGNPAR | PARMRK | INPCK); - termios.c_iflag &= ~(ISTRIP | INLCR | IGNCR | ICRNL); - termios.c_iflag &= ~(IXON | IXOFF | IXANY); - termios.c_oflag &= ~(OPOST); - termios.c_cflag &= ~(CSIZE | CSTOPB | PARENB | CRTSCTS); - termios.c_cflag |= (CS8 | CLOCAL | CREAD); - termios.c_lflag &= ~(ISIG | ICANON | IEXTEN); - termios.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL); - termios.c_cc[VMIN] = 0; - termios.c_cc[VTIME] = 10; +void CPseudoTTYController::close() +{ + CSerialController::close(); - switch (m_speed) { - case 1200U: - ::cfsetospeed(&termios, B1200); - ::cfsetispeed(&termios, B1200); - break; - case 2400U: - ::cfsetospeed(&termios, B2400); - ::cfsetispeed(&termios, B2400); - break; - case 4800U: - ::cfsetospeed(&termios, B4800); - ::cfsetispeed(&termios, B4800); - break; - case 9600U: - ::cfsetospeed(&termios, B9600); - ::cfsetispeed(&termios, B9600); - break; - case 19200U: - ::cfsetospeed(&termios, B19200); - ::cfsetispeed(&termios, B19200); - break; - case 38400U: - ::cfsetospeed(&termios, B38400); - ::cfsetispeed(&termios, B38400); - break; - case 115200U: - ::cfsetospeed(&termios, B115200); - ::cfsetispeed(&termios, B115200); - break; - case 230400U: - ::cfsetospeed(&termios, B230400); - ::cfsetispeed(&termios, B230400); - break; - case 460800U: - ::cfsetospeed(&termios, B460800); - ::cfsetispeed(&termios, B460800); - break; - default: - LogError("Unsupported serial port speed - %u", m_speed); - ::close(m_fd); - return false; - } - - if (::tcsetattr(m_fd, TCSANOW, &termios) < 0) { - LogError("Cannot set the attributes for %s", m_device.c_str()); - ::close(m_fd); - return false; - } - - if (m_assertRTS) { - unsigned int y; - if (::ioctl(m_fd, TIOCMGET, &y) < 0) { - LogError("Cannot get the control attributes for %s", m_device.c_str()); - ::close(m_fd); - return false; - } - - y |= TIOCM_RTS; - - if (::ioctl(m_fd, TIOCMSET, &y) < 0) { - LogError("Cannot set the control attributes for %s", m_device.c_str()); - ::close(m_fd); - return false; - } - } - } - - return true; + ::unlink(m_symlink.c_str()); } #endif diff --git a/PseudoTTYController.h b/PseudoTTYController.h index 9f9a921..ccf6ba1 100644 --- a/PseudoTTYController.h +++ b/PseudoTTYController.h @@ -27,12 +27,15 @@ class CPseudoTTYController : public CSerialController { public: - CPseudoTTYController(const std::string& device, unsigned int speed, bool assertRTS = false); + CPseudoTTYController(const std::string& symlink, unsigned int speed, bool assertRTS = false); virtual ~CPseudoTTYController(); virtual bool open(); + virtual void close(); + protected: + std::string m_symlink; }; #endif diff --git a/SerialController.cpp b/SerialController.cpp index 970501a..10c634a 100644 --- a/SerialController.cpp +++ b/SerialController.cpp @@ -247,101 +247,107 @@ bool CSerialController::open() return false; } - if (::isatty(m_fd)) { - termios termios; - if (::tcgetattr(m_fd, &termios) < 0) { - LogError("Cannot get the attributes for %s", m_device.c_str()); - ::close(m_fd); - return false; - } + if (::isatty(m_fd)) + return setRaw(); + + return true; +} - termios.c_iflag &= ~(IGNBRK | BRKINT | IGNPAR | PARMRK | INPCK); - termios.c_iflag &= ~(ISTRIP | INLCR | IGNCR | ICRNL); - termios.c_iflag &= ~(IXON | IXOFF | IXANY); - termios.c_oflag &= ~(OPOST); - termios.c_cflag &= ~(CSIZE | CSTOPB | PARENB | CRTSCTS); - termios.c_cflag |= (CS8 | CLOCAL | CREAD); - termios.c_lflag &= ~(ISIG | ICANON | IEXTEN); - termios.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL); -#if defined(__APPLE__) - termios.c_cc[VMIN] = 1; - termios.c_cc[VTIME] = 1; -#else - termios.c_cc[VMIN] = 0; - termios.c_cc[VTIME] = 10; -#endif - - switch (m_speed) { - case 1200U: - ::cfsetospeed(&termios, B1200); - ::cfsetispeed(&termios, B1200); - break; - case 2400U: - ::cfsetospeed(&termios, B2400); - ::cfsetispeed(&termios, B2400); - break; - case 4800U: - ::cfsetospeed(&termios, B4800); - ::cfsetispeed(&termios, B4800); - break; - case 9600U: - ::cfsetospeed(&termios, B9600); - ::cfsetispeed(&termios, B9600); - break; - case 19200U: - ::cfsetospeed(&termios, B19200); - ::cfsetispeed(&termios, B19200); - break; - case 38400U: - ::cfsetospeed(&termios, B38400); - ::cfsetispeed(&termios, B38400); - break; - case 115200U: - ::cfsetospeed(&termios, B115200); - ::cfsetispeed(&termios, B115200); - break; - case 230400U: - ::cfsetospeed(&termios, B230400); - ::cfsetispeed(&termios, B230400); - break; - case 460800U: - ::cfsetospeed(&termios, B460800); - ::cfsetispeed(&termios, B460800); - break; - default: - LogError("Unsupported serial port speed - %u", m_speed); - ::close(m_fd); - return false; - } - - if (::tcsetattr(m_fd, TCSANOW, &termios) < 0) { - LogError("Cannot set the attributes for %s", m_device.c_str()); - ::close(m_fd); - return false; - } - - if (m_assertRTS) { - unsigned int y; - if (::ioctl(m_fd, TIOCMGET, &y) < 0) { - LogError("Cannot get the control attributes for %s", m_device.c_str()); - ::close(m_fd); - return false; - } - - y |= TIOCM_RTS; - - if (::ioctl(m_fd, TIOCMSET, &y) < 0) { - LogError("Cannot set the control attributes for %s", m_device.c_str()); - ::close(m_fd); - return false; - } - } - -#if defined(__APPLE__) - setNonblock(false); -#endif +bool CSerialController::setRaw() +{ + termios termios; + if (::tcgetattr(m_fd, &termios) < 0) { + LogError("Cannot get the attributes for %s", m_device.c_str()); + ::close(m_fd); + return false; } + termios.c_iflag &= ~(IGNBRK | BRKINT | IGNPAR | PARMRK | INPCK); + termios.c_iflag &= ~(ISTRIP | INLCR | IGNCR | ICRNL); + termios.c_iflag &= ~(IXON | IXOFF | IXANY); + termios.c_oflag &= ~(OPOST); + termios.c_cflag &= ~(CSIZE | CSTOPB | PARENB | CRTSCTS); + termios.c_cflag |= (CS8 | CLOCAL | CREAD); + termios.c_lflag &= ~(ISIG | ICANON | IEXTEN); + termios.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL); +#if defined(__APPLE__) + termios.c_cc[VMIN] = 1; + termios.c_cc[VTIME] = 1; +#else + termios.c_cc[VMIN] = 0; + termios.c_cc[VTIME] = 10; +#endif + + switch (m_speed) { + case 1200U: + ::cfsetospeed(&termios, B1200); + ::cfsetispeed(&termios, B1200); + break; + case 2400U: + ::cfsetospeed(&termios, B2400); + ::cfsetispeed(&termios, B2400); + break; + case 4800U: + ::cfsetospeed(&termios, B4800); + ::cfsetispeed(&termios, B4800); + break; + case 9600U: + ::cfsetospeed(&termios, B9600); + ::cfsetispeed(&termios, B9600); + break; + case 19200U: + ::cfsetospeed(&termios, B19200); + ::cfsetispeed(&termios, B19200); + break; + case 38400U: + ::cfsetospeed(&termios, B38400); + ::cfsetispeed(&termios, B38400); + break; + case 115200U: + ::cfsetospeed(&termios, B115200); + ::cfsetispeed(&termios, B115200); + break; + case 230400U: + ::cfsetospeed(&termios, B230400); + ::cfsetispeed(&termios, B230400); + break; + case 460800U: + ::cfsetospeed(&termios, B460800); + ::cfsetispeed(&termios, B460800); + break; + default: + LogError("Unsupported serial port speed - %u", m_speed); + ::close(m_fd); + return false; + } + + if (::tcsetattr(m_fd, TCSANOW, &termios) < 0) { + LogError("Cannot set the attributes for %s", m_device.c_str()); + ::close(m_fd); + return false; + } + + if (m_assertRTS) { + unsigned int y; + if (::ioctl(m_fd, TIOCMGET, &y) < 0) { + LogError("Cannot get the control attributes for %s", m_device.c_str()); + ::close(m_fd); + return false; + } + + y |= TIOCM_RTS; + + if (::ioctl(m_fd, TIOCMSET, &y) < 0) { + LogError("Cannot set the control attributes for %s", m_device.c_str()); + ::close(m_fd); + return false; + } + } + +#if defined(__APPLE__) + setNonblock(false); +#endif + return true; } diff --git a/SerialController.h b/SerialController.h index c7f97eb..d4cb519 100644 --- a/SerialController.h +++ b/SerialController.h @@ -59,6 +59,7 @@ protected: int readNonblock(unsigned char* buffer, unsigned int length); #else bool canWrite(); + bool setRaw(); #endif }; From 97f1ebb25fb69ff7c9bff817b10dee5f18f6ae67 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 27 Jun 2020 22:47:54 +0100 Subject: [PATCH 074/163] Convert TXTwist to TXDelay. --- Conf.cpp | 12 ++++++------ Conf.h | 4 ++-- MMDVM.ini | 2 +- MMDVMHost.cpp | 14 +++++++------- Modem.cpp | 8 ++++---- Modem.h | 4 ++-- NullModem.h | 1 + Version.h | 2 +- 8 files changed, 24 insertions(+), 23 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index fc6dad7..fae8b95 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -214,7 +214,7 @@ m_fmExtAudioBoost(1U), m_fmModeHang(10U), m_ax25Enabled(false), m_ax25RXTwist(6), -m_ax25TXTwist(6), +m_ax25TXDelay(300U), m_ax25Trace(false), m_dstarNetworkEnabled(false), m_dstarGatewayAddress(), @@ -508,7 +508,7 @@ bool CConf::read() else if (::strcmp(key, "PTTInvert") == 0) m_modemPTTInvert = ::atoi(value) == 1; else if (::strcmp(key, "TXDelay") == 0) - m_modemTXDelay = (unsigned int)::atoi(value); + m_ax25TXDelay = m_modemTXDelay = (unsigned int)::atoi(value); else if (::strcmp(key, "DMRDelay") == 0) m_modemDMRDelay = (unsigned int)::atoi(value); else if (::strcmp(key, "RXOffset") == 0) @@ -817,8 +817,8 @@ bool CConf::read() m_ax25Enabled = ::atoi(value) == 1; else if (::strcmp(key, "RXTwist") == 0) m_ax25RXTwist = ::atoi(value); - else if (::strcmp(key, "TXTwist") == 0) - m_ax25TXTwist = ::atoi(value); + else if (::strcmp(key, "TXDelay") == 0) + m_ax25TXDelay = (unsigned int)::atoi(value); else if (::strcmp(key, "Trace") == 0) m_ax25Trace = ::atoi(value) == 1; } else if (section == SECTION_DSTAR_NETWORK) { @@ -1777,9 +1777,9 @@ int CConf::getAX25RXTwist() const return m_ax25RXTwist; } -int CConf::getAX25TXTwist() const +unsigned int CConf::getAX25TXDelay() const { - return m_ax25TXTwist; + return m_ax25TXDelay; } bool CConf::getAX25Trace() const diff --git a/Conf.h b/Conf.h index 04c2be8..c87c730 100644 --- a/Conf.h +++ b/Conf.h @@ -176,7 +176,7 @@ public: // The AX.25 section bool getAX25Enabled() const; int getAX25RXTwist() const; - int getAX25TXTwist() const; + unsigned int getAX25TXDelay() const; bool getAX25Trace() const; // The FM Section @@ -506,7 +506,7 @@ private: bool m_ax25Enabled; int m_ax25RXTwist; - int m_ax25TXTwist; + unsigned int m_ax25TXDelay; bool m_ax25Trace; bool m_dstarNetworkEnabled; diff --git a/MMDVM.ini b/MMDVM.ini index 0ae670f..6cb0a9c 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -181,7 +181,7 @@ ExtAudioBoost=1 [AX.25] Enable=1 -TXTwist=6 +TXDelay=300 RXTwist=6 Trace=1 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 8e95fec..b8d94a2 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -636,13 +636,13 @@ int CMMDVMHost::run() } if (m_ax25Enabled) { - int rxTwist = m_conf.getAX25RXTwist(); - int txTwist = m_conf.getAX25TXTwist(); - bool trace = m_conf.getAX25Trace(); + int rxTwist = m_conf.getAX25RXTwist(); + unsigned int txDelay = m_conf.getAX25TXDelay(); + bool trace = m_conf.getAX25Trace(); LogInfo("AX.25 RF Parameters"); - LogInfo(" RXTwist: %d", rxTwist); - LogInfo(" TXTwist: %d", txTwist); + LogInfo(" RX Twist: %d", rxTwist); + LogInfo(" TX Delay: %ums", txDelay); LogInfo(" Trace: %s", trace ? "yes" : "no"); m_ax25 = new CAX25Control(m_ax25Network, trace); @@ -1293,7 +1293,7 @@ bool CMMDVMHost::createModem() int txDCOffset = m_conf.getModemTXDCOffset(); float rfLevel = m_conf.getModemRFLevel(); int rxTwist = m_conf.getAX25RXTwist(); - int txTwist = m_conf.getAX25TXTwist(); + unsigned int ax25TXDelay = m_conf.getAX25TXDelay(); LogInfo("Modem Parameters"); LogInfo(" Port: %s", port.c_str()); @@ -1335,7 +1335,7 @@ bool CMMDVMHost::createModem() m_modem->setYSFParams(lowDeviation, ysfTXHang); m_modem->setP25Params(p25TXHang); m_modem->setNXDNParams(nxdnTXHang); - m_modem->setAX25Params(rxTwist, txTwist); + m_modem->setAX25Params(rxTwist, ax25TXDelay); if (m_fmEnabled) { std::string callsign = m_conf.getFMCallsign(); diff --git a/Modem.cpp b/Modem.cpp index 3b412b9..9591b0f 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -193,7 +193,7 @@ m_error(false), m_mode(MODE_IDLE), m_hwType(HWT_UNKNOWN), m_ax25RXTwist(0), -m_ax25TXTwist(0), +m_ax25TXDelay(300U), m_fmCallsign(), m_fmCallsignSpeed(20U), m_fmCallsignFrequency(1000U), @@ -308,10 +308,10 @@ void CModem::setNXDNParams(unsigned int txHang) m_nxdnTXHang = txHang; } -void CModem::setAX25Params(int rxTwist, int txTwist) +void CModem::setAX25Params(int rxTwist, unsigned int txDelay) { m_ax25RXTwist = rxTwist; - m_ax25TXTwist = txTwist; + m_ax25TXDelay = txDelay; } void CModem::setTransparentDataParams(unsigned int sendFrameType) @@ -1831,7 +1831,7 @@ bool CModem::setConfig() buffer[25U] = (unsigned char)(m_ax25RXTwist + 128); - buffer[26U] = (unsigned char)(m_ax25TXTwist + 128); + buffer[26U] = m_ax25TXDelay / 10U; // In 10ms units // CUtils::dump(1U, "Written", buffer, 27U); diff --git a/Modem.h b/Modem.h index c794477..822d50f 100644 --- a/Modem.h +++ b/Modem.h @@ -53,7 +53,7 @@ public: virtual void setYSFParams(bool loDev, unsigned int txHang); virtual void setP25Params(unsigned int txHang); virtual void setNXDNParams(unsigned int txHang); - virtual void setAX25Params(int rxTwist, int txTwist); + virtual void setAX25Params(int rxTwist, unsigned int txDelay); virtual void setTransparentDataParams(unsigned int sendFrameType); virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch); @@ -215,7 +215,7 @@ private: unsigned char m_mode; HW_TYPE m_hwType; int m_ax25RXTwist; - int m_ax25TXTwist; + unsigned int m_ax25TXDelay; std::string m_fmCallsign; unsigned int m_fmCallsignSpeed; diff --git a/NullModem.h b/NullModem.h index a7ec68f..ec8757a 100644 --- a/NullModem.h +++ b/NullModem.h @@ -36,6 +36,7 @@ public: virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel){}; virtual void setDMRParams(unsigned int colorCode){}; virtual void setYSFParams(bool loDev, unsigned int txHang){}; + virtual void setAX25Params(int rxTwist, unsigned int txDelay){}; virtual void setTransparentDataParams(unsigned int sendFrameType){}; virtual bool open(); diff --git a/Version.h b/Version.h index 4559871..dc5e793 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200625"; +const char* VERSION = "20200627"; #endif From 648426f44ce076d9d8ea8bdbd96c442103bba2fc Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 27 Jun 2020 22:58:37 +0100 Subject: [PATCH 075/163] Allow Windows compilation. --- AX25Network.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AX25Network.h b/AX25Network.h index 34aa43b..f46de15 100644 --- a/AX25Network.h +++ b/AX25Network.h @@ -19,7 +19,11 @@ #ifndef AX25Network_H #define AX25Network_H +#if defined(_WIN32) || defined(_WIN64) +#include "SerialController.h" +#else #include "PseudoTTYController.h" +#endif #include #include @@ -42,7 +46,11 @@ public: void close(); private: +#if defined(_WIN32) || defined(_WIN64) + CSerialController m_serial; +#else CPseudoTTYController m_serial; +#endif unsigned char* m_txData; unsigned char* m_rxData; unsigned int m_rxLength; From 93a0b9793bf82a518cb18637966c418822711954 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 27 Jun 2020 23:01:00 +0100 Subject: [PATCH 076/163] Update the other Makefiles. --- Makefile.Pi | 2 +- Makefile.Pi.Adafruit | 2 +- Makefile.Pi.HD44780 | 2 +- Makefile.Pi.OLED | 2 +- Makefile.Pi.PCF8574 | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile.Pi b/Makefile.Pi index 5631c0b..2e7fb1c 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -12,7 +12,7 @@ OBJECTS = AX25Control.o AX25Network.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o \ - P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 4a28051..1f274ef 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -13,7 +13,7 @@ OBJECTS = AX25Control.o AX25Network.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index f5b6dc0..3adaeab 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -12,7 +12,7 @@ OBJECTS = AX25Control.o AX25Network.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o IIDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index d49d135..79d1f6e 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -12,7 +12,7 @@ OBJECTS = AX25Control.o AX25Network.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o I2CController.o IIRDirectForm1Filter.o OLED.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ NXDNLayer3.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ - P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index 8e9518a..81871cc 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -13,7 +13,7 @@ OBJECTS = AX25Control.o AX25Network.o \ DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o MobileGPS.o Modem.o \ ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ + P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ YSFFICH.o YSFNetwork.o YSFPayload.o From d4c7f20696e9a70e7b2924b7da84cd046308280d Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sun, 28 Jun 2020 11:22:54 +0200 Subject: [PATCH 077/163] Fix assertion in PseudoTTY --- SerialController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialController.cpp b/SerialController.cpp index 10c634a..f48cf70 100644 --- a/SerialController.cpp +++ b/SerialController.cpp @@ -226,7 +226,6 @@ m_speed(speed), m_assertRTS(assertRTS), m_fd(-1) { - assert(!device.empty()); } CSerialController::~CSerialController() @@ -235,6 +234,7 @@ CSerialController::~CSerialController() bool CSerialController::open() { + assert(!m_device.empty()); assert(m_fd == -1); #if defined(__APPLE__) From 30098a5d3897167c70f1a7a54095d1cefa263970 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 28 Jun 2020 15:23:56 +0100 Subject: [PATCH 078/163] Fix SerialController being used by PseudoTTYController. --- PseudoTTYController.cpp | 2 +- SerialController.cpp | 16 ++++++++++++++++ SerialController.h | 2 ++ Version.h | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/PseudoTTYController.cpp b/PseudoTTYController.cpp index 67df4a4..52a4ac9 100644 --- a/PseudoTTYController.cpp +++ b/PseudoTTYController.cpp @@ -37,7 +37,7 @@ CPseudoTTYController::CPseudoTTYController(const std::string& symlink, unsigned int speed, bool assertRTS) : -CSerialController("", speed, assertRTS), +CSerialController(speed, assertRTS), m_symlink(symlink) { } diff --git a/SerialController.cpp b/SerialController.cpp index 10c634a..99fe576 100644 --- a/SerialController.cpp +++ b/SerialController.cpp @@ -48,6 +48,14 @@ m_handle(INVALID_HANDLE_VALUE) assert(!device.empty()); } +CSerialController::CSerialController(unsigned int speed, bool assertRTS) : +m_device(), +m_speed(speed), +m_assertRTS(assertRTS), +m_handle(INVALID_HANDLE_VALUE) +{ +} + CSerialController::~CSerialController() { } @@ -229,6 +237,14 @@ m_fd(-1) assert(!device.empty()); } +CSerialController::CSerialController(unsigned int speed, bool assertRTS) : +m_device(), +m_speed(speed), +m_assertRTS(assertRTS), +m_fd(-1) +{ +} + CSerialController::~CSerialController() { } diff --git a/SerialController.h b/SerialController.h index d4cb519..e4adb7d 100644 --- a/SerialController.h +++ b/SerialController.h @@ -46,6 +46,8 @@ public: #endif protected: + CSerialController(unsigned int speed, bool assertRTS = false); + std::string m_device; unsigned int m_speed; bool m_assertRTS; diff --git a/Version.h b/Version.h index dc5e793..d3f9fbf 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200627"; +const char* VERSION = "20200628"; #endif From fab4dc227eaa96f1cf1437a1c1e029a271dc5d5d Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 28 Jun 2020 15:41:41 +0100 Subject: [PATCH 079/163] Fix NullModem.h --- NullModem.h | 74 +++++++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/NullModem.h b/NullModem.h index ec8757a..e44a352 100644 --- a/NullModem.h +++ b/NullModem.h @@ -30,48 +30,56 @@ public: CNullModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool trace, bool debug); virtual ~CNullModem(); - virtual void setSerialParams(const std::string& protocol, unsigned int address){}; - virtual void setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency){}; - virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled){}; - virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel){}; - virtual void setDMRParams(unsigned int colorCode){}; - virtual void setYSFParams(bool loDev, unsigned int txHang){}; - virtual void setAX25Params(int rxTwist, unsigned int txDelay){}; - virtual void setTransparentDataParams(unsigned int sendFrameType){}; + virtual void setSerialParams(const std::string& protocol, unsigned int address) {}; + virtual void setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) {}; + virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled) {}; + virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel) {}; + virtual void setDMRParams(unsigned int colorCode) {}; + virtual void setYSFParams(bool loDev, unsigned int txHang) {}; + virtual void setP25Params(unsigned int txHang) {}; + virtual void setNXDNParams(unsigned int txHang) {}; + virtual void setAX25Params(int rxTwist, unsigned int txDelay) {}; + virtual void setTransparentDataParams(unsigned int sendFrameType) {}; virtual bool open(); - virtual unsigned int readDStarData(unsigned char* data){return 0;}; - virtual unsigned int readDMRData1(unsigned char* data){return 0;}; - virtual unsigned int readDMRData2(unsigned char* data){return 0;}; - virtual unsigned int readYSFData(unsigned char* data){return 0;}; - virtual unsigned int readP25Data(unsigned char* data){return 0;}; - virtual unsigned int readNXDNData(unsigned char* data){return 0;}; - virtual unsigned int readTransparentData(unsigned char* data){return 0;}; + virtual unsigned int readDStarData(unsigned char* data) { return 0U; }; + virtual unsigned int readDMRData1(unsigned char* data) { return 0U; }; + virtual unsigned int readDMRData2(unsigned char* data) { return 0U; }; + virtual unsigned int readYSFData(unsigned char* data) { return 0U; }; + virtual unsigned int readP25Data(unsigned char* data) { return 0U; }; + virtual unsigned int readNXDNData(unsigned char* data) { return 0U; }; + virtual unsigned int readFMData(unsigned char* data) { return 0U; }; + virtual unsigned int readAX25Data(unsigned char* data) { return 0U; }; + virtual unsigned int readTransparentData(unsigned char* data) { return 0U; }; virtual unsigned int readSerial(unsigned char* data, unsigned int length){return 0;}; - virtual bool hasDStarSpace()const {return true;}; - virtual bool hasDMRSpace1() const {return true;}; - virtual bool hasDMRSpace2() const {return true;}; - virtual bool hasYSFSpace() const {return true;}; - virtual bool hasP25Space() const {return true;}; - virtual bool hasNXDNSpace() const {return true;}; - virtual bool hasPOCSAGSpace() const{return true;}; + virtual bool hasDStarSpace()const { return true; }; + virtual bool hasDMRSpace1() const { return true; }; + virtual bool hasDMRSpace2() const { return true; }; + virtual bool hasYSFSpace() const { return true; }; + virtual bool hasP25Space() const { return true; }; + virtual bool hasNXDNSpace() const { return true; }; + virtual bool hasPOCSAGSpace() const { return true; }; + virtual unsigned int getFMSpace() const { return true; }; + virtual bool hasAX25Space() const { return true; }; - virtual bool hasTX() const {return false;}; - virtual bool hasCD() const {return false;}; + virtual bool hasTX() const { return false; }; + virtual bool hasCD() const { return false; }; - virtual bool hasLockout() const {return false;}; - virtual bool hasError() const {return false;}; + virtual bool hasLockout() const { return false; }; + virtual bool hasError() const { return false; }; - virtual bool writeDStarData(const unsigned char* data, unsigned int length){return true;}; - virtual bool writeDMRData1(const unsigned char* data, unsigned int length){return true;}; - virtual bool writeDMRData2(const unsigned char* data, unsigned int length){return true;}; - virtual bool writeYSFData(const unsigned char* data, unsigned int length){return true;}; - virtual bool writeP25Data(const unsigned char* data, unsigned int length){return true;}; - virtual bool writeNXDNData(const unsigned char* data, unsigned int length){return true;}; - virtual bool writePOCSAGData(const unsigned char* data, unsigned int length){return true;}; + virtual bool writeDStarData(const unsigned char* data, unsigned int length) { return true; }; + virtual bool writeDMRData1(const unsigned char* data, unsigned int length) { return true; }; + virtual bool writeDMRData2(const unsigned char* data, unsigned int length) { return true; }; + virtual bool writeYSFData(const unsigned char* data, unsigned int length) {return true; }; + virtual bool writeP25Data(const unsigned char* data, unsigned int length) { return true; }; + virtual bool writeNXDNData(const unsigned char* data, unsigned int length) { return true; }; + virtual bool writePOCSAGData(const unsigned char* data, unsigned int length) { return true; }; + virtual bool writeFMData(const unsigned char* data, unsigned int length) { return true; }; + virtual bool writeAX25Data(const unsigned char* data, unsigned int length) { return true; }; virtual bool writeTransparentData(const unsigned char* data, unsigned int length){return true;}; From 4ee32a506f3656595ef2047b2a56a0484b88ffde Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 29 Jun 2020 11:46:30 +0100 Subject: [PATCH 080/163] Fix the NullModem. --- NullModem.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NullModem.h b/NullModem.h index e44a352..49dd3ec 100644 --- a/NullModem.h +++ b/NullModem.h @@ -41,6 +41,11 @@ public: virtual void setAX25Params(int rxTwist, unsigned int txDelay) {}; virtual void setTransparentDataParams(unsigned int sendFrameType) {}; + virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch) {}; + virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel) {}; + virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) {}; + virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost) {}; + virtual bool open(); virtual unsigned int readDStarData(unsigned char* data) { return 0U; }; From 5dbf916db039d686f234b8047862ac6f48883825 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 29 Jun 2020 11:53:54 +0100 Subject: [PATCH 081/163] Fix Windows compilation. --- MMDVMHost.vcxproj | 4 ++-- MMDVMHost.vcxproj.filters | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index 7f64db6..befb54e 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -188,13 +188,13 @@ + - @@ -291,12 +291,12 @@ + - diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index 00ed473..ecb892d 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -275,9 +275,6 @@ Header Files - - Header Files - Header Files @@ -323,6 +320,9 @@ Header Files + + Header Files + @@ -562,9 +562,6 @@ Source Files - - Source Files - Source Files @@ -607,5 +604,8 @@ Source Files + + Source Files + \ No newline at end of file From 8383c7d320e53ad6863e6538b0a118e63e9a6bec Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 29 Jun 2020 12:11:01 +0100 Subject: [PATCH 082/163] Clean up the Makefiles. --- Makefile | 19 ++++++++++--------- Makefile.Pi | 19 ++++++++++--------- Makefile.Pi.Adafruit | 19 ++++++++++--------- Makefile.Pi.HD44780 | 19 ++++++++++--------- Makefile.Pi.OLED | 19 ++++++++++--------- Makefile.Pi.PCF8574 | 19 ++++++++++--------- 6 files changed, 60 insertions(+), 54 deletions(-) diff --git a/Makefile b/Makefile index f9699c9..6af3151 100644 --- a/Makefile +++ b/Makefile @@ -13,15 +13,16 @@ LIBS = -lpthread -lutil LDFLAGS = -g -OBJECTS = AX25Control.o AX25Network.o \ - AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o GPSD.o Hamming.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o Modem.o \ - ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ - NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o \ - P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ - SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ - YSFFICH.o YSFNetwork.o YSFPayload.o +OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ + DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o \ + DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o DStarSlowData.o FMControl.o FMNetwork.o \ + Golay2087.o Golay24128.o GPSD.o Hamming.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o \ + NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ + NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ + PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ + SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ + UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi b/Makefile.Pi index f46272d..9e6fe90 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -12,15 +12,16 @@ LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil LDFLAGS = -g -L/usr/local/lib -OBJECTS = AX25Control.o AX25Network.o \ - AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o GPSD.o Hamming.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o Modem.o \ - ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ - NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o \ - P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ - SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ - YSFFICH.o YSFNetwork.o YSFPayload.o +OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ + DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o \ + DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o DStarSlowData.o FMControl.o FMNetwork.o \ + Golay2087.o Golay24128.o GPSD.o Hamming.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o \ + NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ + NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ + PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ + SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ + UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 84bd1fc..ab3279b 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -13,15 +13,16 @@ LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil LDFLAGS = -g -L/usr/local/lib -OBJECTS = AX25Control.o AX25Network.o \ - AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o GPSD.o Hamming.o HD44780.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o Modem.o \ - ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ - NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ - SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ - YSFFICH.o YSFNetwork.o YSFPayload.o +OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ + DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o \ + DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o DStarSlowData.o FMControl.o FMNetwork.o \ + Golay2087.o Golay24128.o GPSD.o Hamming.o HD44780.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o \ + NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ + NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ + PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ + SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ + UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index a1e86cd..659b6fd 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -13,15 +13,16 @@ LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil LDFLAGS = -g -L/usr/local/lib -OBJECTS = AX25Control.o AX25Network.o \ - AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o GPSD.o Hamming.o HD44780.o I2CController.o IIDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o Modem.o \ - ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ - NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ - SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ - YSFFICH.o YSFNetwork.o YSFPayload.o +OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ + DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o \ + DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o DStarSlowData.o FMControl.o FMNetwork.o \ + Golay2087.o Golay24128.o GPSD.o Hamming.o HD44780.o I2CController.o IIDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o \ + NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ + NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ + PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ + SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ + UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index af780eb..61b2626 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -13,15 +13,16 @@ LIBS = -lArduiPi_OLED -lwiringPi -lpthread -lutil LDFLAGS = -g -L/usr/local/lib -OBJECTS = AX25Control.o AX25Network.o \ - AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o GPSD.o Hamming.o I2CController.o IIRDirectForm1Filter.o OLED.o LCDproc.o Log.o MMDVMHost.o Modem.o \ - ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o \ - NXDNLayer3.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ - P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ - SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ - YSFFICH.o YSFNetwork.o YSFPayload.o +OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ + DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o \ + DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o DStarSlowData.o FMControl.o FMNetwork.o \ + Golay2087.o Golay24128.o GPSD.o Hamming.o I2CController.o IIRDirectForm1Filter.o OLED.o LCDproc.o Log.o MMDVMHost.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o \ + NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ + NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ + PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ + SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ + UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index 94d3aaf..d037b95 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -14,15 +14,16 @@ LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil LDFLAGS = -g -L/usr/local/lib -OBJECTS = AX25Control.o AX25Network.o \ - AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o FMControl.o FMNetwork.o Golay2087.o Golay24128.o GPSD.o Hamming.o HD44780.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o Modem.o \ - ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ - NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ - SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ - YSFFICH.o YSFNetwork.o YSFPayload.o +OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ + DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o \ + DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o DStarSlowData.o FMControl.o FMNetwork.o \ + Golay2087.o Golay24128.o GPSD.o Hamming.o HD44780.o I2CController.o IIRDirectForm1Filter.o LCDproc.o Log.o MMDVMHost.o Modem.o \ + ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o \ + NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ + NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ + PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ + SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o \ + UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand From aa8b78a31125869c46a035e0658fe7746c38f25d Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 30 Jun 2020 12:35:03 +0100 Subject: [PATCH 083/163] Refactor the Modem class hierarchy. --- CASTInfo.cpp | 2 +- CASTInfo.h | 4 +- DMRControl.cpp | 2 +- DMRControl.h | 4 +- DMRSlot.cpp | 4 +- DMRSlot.h | 6 +- Display.cpp | 6 +- Display.h | 5 +- MMDVMHost.cpp | 8 +- MMDVMHost.h | 2 +- MMDVMHost.vcxproj | 2 + MMDVMHost.vcxproj.filters | 12 +- Makefile | 4 +- Makefile.Pi | 4 +- Makefile.Pi.Adafruit | 4 +- Makefile.Pi.HD44780 | 4 +- Makefile.Pi.OLED | 4 +- Makefile.Pi.PCF8574 | 4 +- Modem.cpp | 2453 +----------------------------------- Modem.h | 286 +---- ModemSerialPort.cpp | 14 +- ModemSerialPort.h | 10 +- NullModem.cpp | 12 +- NullModem.h | 54 +- SerialModem.cpp | 2465 +++++++++++++++++++++++++++++++++++++ SerialModem.h | 267 ++++ 26 files changed, 2891 insertions(+), 2751 deletions(-) create mode 100644 SerialModem.cpp create mode 100644 SerialModem.h diff --git a/CASTInfo.cpp b/CASTInfo.cpp index cce34d8..4821251 100644 --- a/CASTInfo.cpp +++ b/CASTInfo.cpp @@ -21,7 +21,7 @@ static bool networkInfoInitialized = false; static unsigned char passCounter = 0; -CCASTInfo::CCASTInfo(CModem* modem) : +CCASTInfo::CCASTInfo(IModem* modem) : CDisplay(), m_modem(modem), m_ipaddress() diff --git a/CASTInfo.h b/CASTInfo.h index 9254995..4651cf0 100644 --- a/CASTInfo.h +++ b/CASTInfo.h @@ -28,7 +28,7 @@ class CCASTInfo : public CDisplay { public: - CCASTInfo(CModem* modem); + CCASTInfo(IModem* modem); virtual ~CCASTInfo(); virtual bool open(); @@ -64,7 +64,7 @@ protected: virtual void clearCWInt(); private: - CModem* m_modem; + IModem* m_modem; std::string m_ipaddress; }; diff --git a/DMRControl.cpp b/DMRControl.cpp index bd07082..4ec1a4c 100644 --- a/DMRControl.cpp +++ b/DMRControl.cpp @@ -21,7 +21,7 @@ #include #include -CDMRControl::CDMRControl(unsigned int id, unsigned int colorCode, unsigned int callHang, bool selfOnly, bool embeddedLCOnly, bool dumpTAData, const std::vector& prefixes, const std::vector& blacklist, const std::vector& whitelist, const std::vector& slot1TGWhitelist, const std::vector& slot2TGWhitelist, unsigned int timeout, CModem* modem, CDMRNetwork* network, CDisplay* display, bool duplex, CDMRLookup* lookup, CRSSIInterpolator* rssi, unsigned int jitter, DMR_OVCM_TYPES ovcm) : +CDMRControl::CDMRControl(unsigned int id, unsigned int colorCode, unsigned int callHang, bool selfOnly, bool embeddedLCOnly, bool dumpTAData, const std::vector& prefixes, const std::vector& blacklist, const std::vector& whitelist, const std::vector& slot1TGWhitelist, const std::vector& slot2TGWhitelist, unsigned int timeout, IModem* modem, CDMRNetwork* network, CDisplay* display, bool duplex, CDMRLookup* lookup, CRSSIInterpolator* rssi, unsigned int jitter, DMR_OVCM_TYPES ovcm) : m_colorCode(colorCode), m_modem(modem), m_network(network), diff --git a/DMRControl.h b/DMRControl.h index 4d2597e..5a8d7de 100644 --- a/DMRControl.h +++ b/DMRControl.h @@ -31,7 +31,7 @@ class CDMRControl { public: - CDMRControl(unsigned int id, unsigned int colorCode, unsigned int callHang, bool selfOnly, bool embeddedLCOnly, bool dumpTAData, const std::vector& prefixes, const std::vector& blacklist, const std::vector& whitelist, const std::vector& slot1TGWhitelist, const std::vector& slot2TGWhitelist, unsigned int timeout, CModem* modem, CDMRNetwork* network, CDisplay* display, bool duplex, CDMRLookup* lookup, CRSSIInterpolator* rssi, unsigned int jitter, DMR_OVCM_TYPES ovcm); + CDMRControl(unsigned int id, unsigned int colorCode, unsigned int callHang, bool selfOnly, bool embeddedLCOnly, bool dumpTAData, const std::vector& prefixes, const std::vector& blacklist, const std::vector& whitelist, const std::vector& slot1TGWhitelist, const std::vector& slot2TGWhitelist, unsigned int timeout, IModem* modem, CDMRNetwork* network, CDisplay* display, bool duplex, CDMRLookup* lookup, CRSSIInterpolator* rssi, unsigned int jitter, DMR_OVCM_TYPES ovcm); ~CDMRControl(); bool processWakeup(const unsigned char* data); @@ -50,7 +50,7 @@ public: private: unsigned int m_colorCode; - CModem* m_modem; + IModem* m_modem; CDMRNetwork* m_network; CDMRSlot m_slot1; CDMRSlot m_slot2; diff --git a/DMRSlot.cpp b/DMRSlot.cpp index 1cfecab..5554328 100644 --- a/DMRSlot.cpp +++ b/DMRSlot.cpp @@ -37,7 +37,7 @@ unsigned int CDMRSlot::m_colorCode = 0U; bool CDMRSlot::m_embeddedLCOnly = false; bool CDMRSlot::m_dumpTAData = true; -CModem* CDMRSlot::m_modem = NULL; +IModem* CDMRSlot::m_modem = NULL; CDMRNetwork* CDMRSlot::m_network = NULL; CDisplay* CDMRSlot::m_display = NULL; bool CDMRSlot::m_duplex = true; @@ -1896,7 +1896,7 @@ void CDMRSlot::writeQueueNet(const unsigned char *data) m_queue.addData(data, len); } -void CDMRSlot::init(unsigned int colorCode, bool embeddedLCOnly, bool dumpTAData, unsigned int callHang, CModem* modem, CDMRNetwork* network, CDisplay* display, bool duplex, CDMRLookup* lookup, CRSSIInterpolator* rssiMapper, unsigned int jitter, DMR_OVCM_TYPES ovcm) +void CDMRSlot::init(unsigned int colorCode, bool embeddedLCOnly, bool dumpTAData, unsigned int callHang, IModem* modem, CDMRNetwork* network, CDisplay* display, bool duplex, CDMRLookup* lookup, CRSSIInterpolator* rssiMapper, unsigned int jitter, DMR_OVCM_TYPES ovcm) { assert(modem != NULL); assert(display != NULL); diff --git a/DMRSlot.h b/DMRSlot.h index 1e95dc2..f50596b 100644 --- a/DMRSlot.h +++ b/DMRSlot.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2019 by Jonathan Naylor G4KLX + * Copyright (C) 2015-2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -62,7 +62,7 @@ public: void enable(bool enabled); - static void init(unsigned int colorCode, bool embeddedLCOnly, bool dumpTAData, unsigned int callHang, CModem* modem, CDMRNetwork* network, CDisplay* display, bool duplex, CDMRLookup* lookup, CRSSIInterpolator* rssiMapper, unsigned int jitter, DMR_OVCM_TYPES ovcm); + static void init(unsigned int colorCode, bool embeddedLCOnly, bool dumpTAData, unsigned int callHang, IModem* modem, CDMRNetwork* network, CDisplay* display, bool duplex, CDMRLookup* lookup, CRSSIInterpolator* rssiMapper, unsigned int jitter, DMR_OVCM_TYPES ovcm); private: unsigned int m_slotNo; @@ -117,7 +117,7 @@ private: static bool m_embeddedLCOnly; static bool m_dumpTAData; - static CModem* m_modem; + static IModem* m_modem; static CDMRNetwork* m_network; static CDisplay* m_display; static bool m_duplex; diff --git a/Display.cpp b/Display.cpp index 22bc953..ce19ce5 100644 --- a/Display.cpp +++ b/Display.cpp @@ -490,7 +490,7 @@ int CDisplay::writeNXDNIntEx(const class CUserDBentry& source, bool group, unsig /* Factory method extracted from MMDVMHost.cpp - BG5HHP */ -CDisplay* CDisplay::createDisplay(const CConf& conf, CUMP* ump, CModem* modem) +CDisplay* CDisplay::createDisplay(const CConf& conf, CUMP* ump, IModem* modem) { CDisplay *display = NULL; @@ -509,7 +509,7 @@ CDisplay* CDisplay::createDisplay(const CConf& conf, CUMP* ump, CModem* modem) ISerialPort* serial = NULL; if (port == "modem") - serial = new CModemSerialPort(modem); + serial = new IModemSerialPort(modem); else serial = new CSerialController(port, (type == "TFT Serial") ? 9600U : 115200U); @@ -555,7 +555,7 @@ CDisplay* CDisplay::createDisplay(const CConf& conf, CUMP* ump, CModem* modem) } if (port == "modem") { - ISerialPort* serial = new CModemSerialPort(modem); + ISerialPort* serial = new IModemSerialPort(modem); display = new CNextion(conf.getCallsign(), dmrid, serial, brightness, displayClock, utc, idleBrightness, screenLayout, txFrequency, rxFrequency, displayTempInF, conf.getLocation()); } else if (port == "ump") { if (ump != NULL) { diff --git a/Display.h b/Display.h index 17caa41..c4937d2 100644 --- a/Display.h +++ b/Display.h @@ -21,13 +21,14 @@ #include "Timer.h" #include "UserDBentry.h" +#include "Modem.h" #include #include class CConf; -class CModem; +class IModem; class CUMP; class CDisplay @@ -81,7 +82,7 @@ public: void clock(unsigned int ms); - static CDisplay* createDisplay(const CConf& conf, CUMP* ump, CModem* modem); + static CDisplay* createDisplay(const CConf& conf, CUMP* ump, IModem* modem); protected: virtual void setIdleInt() = 0; diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 5e6940b..93ee9f7 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -21,6 +21,8 @@ #include "NXDNIcomNetwork.h" #include "RSSIInterpolator.h" #include "SerialController.h" +#include "SerialModem.h" +#include "NullModem.h" #include "Version.h" #include "StopWatch.h" #include "Defines.h" @@ -514,7 +516,6 @@ int CMMDVMHost::run() else if (ovcm == DMR_OVCM_ON) LogInfo(" OVCM: on"); - switch (dmrBeacons) { case DMR_BEACONS_NETWORK: { unsigned int dmrBeaconDuration = m_conf.getDMRBeaconDuration(); @@ -1332,7 +1333,10 @@ bool CMMDVMHost::createModem() LogInfo(" AX.25 TX Level: %.1f%%", ax25TXLevel); LogInfo(" TX Frequency: %uHz (%uHz)", txFrequency, txFrequency + txOffset); - m_modem = CModem::createModem(port, m_duplex, rxInvert, txInvert, pttInvert, txDelay, dmrDelay, trace, debug); + if (port == "NullModem") + m_modem = new CNullModem; + else + m_modem = new CSerialModem(port, m_duplex, rxInvert, txInvert, pttInvert, txDelay, dmrDelay, trace, debug); m_modem->setSerialParams(protocol, address, speed); m_modem->setModeParams(m_dstarEnabled, m_dmrEnabled, m_ysfEnabled, m_p25Enabled, m_nxdnEnabled, m_pocsagEnabled, m_fmEnabled, m_ax25Enabled); m_modem->setLevels(rxLevel, cwIdTXLevel, dstarTXLevel, dmrTXLevel, ysfTXLevel, p25TXLevel, nxdnTXLevel, pocsagTXLevel, fmTXLevel, ax25TXLevel); diff --git a/MMDVMHost.h b/MMDVMHost.h index d7f3e84..9cb28cb 100644 --- a/MMDVMHost.h +++ b/MMDVMHost.h @@ -59,7 +59,7 @@ public: private: CConf m_conf; - CModem* m_modem; + IModem* m_modem; CDStarControl* m_dstar; CDMRControl* m_dmr; CYSFControl* m_ysf; diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index befb54e..96f4c96 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -236,6 +236,7 @@ + @@ -334,6 +335,7 @@ + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index 32cca98..0757e74 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -275,9 +275,6 @@ Header Files - - Header Files - Header Files @@ -326,6 +323,9 @@ Header Files + + Header Files + @@ -565,9 +565,6 @@ Source Files - - Source Files - Source Files @@ -613,5 +610,8 @@ Source Files + + Source Files + \ No newline at end of file diff --git a/Makefile b/Makefile index 5ab488b..91fcf2d 100644 --- a/Makefile +++ b/Makefile @@ -21,8 +21,8 @@ OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Con NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ - SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ - UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + SerialController.o SerialModem.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ + UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi b/Makefile.Pi index f936cad..62e27aa 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -20,8 +20,8 @@ OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Con NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ - SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ - UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + SerialController.o SerialModem.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ + UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 928937e..2915c69 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -21,8 +21,8 @@ OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Con NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ - SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ - UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + SerialController.o SerialModem.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ + UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index d6a7241..c00e58f 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -21,8 +21,8 @@ OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Con NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ - SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ - UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + SerialController.o SerialModem.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ + UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index 61b2626..41e837e 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -21,8 +21,8 @@ OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Con NXDNCRC.o NXDNFACCH1.o NXDNLayer3.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ - SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ - UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + SerialController.o SerialModem.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ + UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index f06d5c4..37de0b1 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -22,8 +22,8 @@ OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Con NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o \ PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ - SerialController.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o \ - UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + SerialController.o SerialModem.o SerialPort.o SHA256.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ + UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Modem.cpp b/Modem.cpp index 9591b0f..75c65dc 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2018,2020 by Jonathan Naylor G4KLX + * Copyright (C) 2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,2457 +16,8 @@ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ -#include "SerialController.h" -#if defined(__linux__) -#include "I2CController.h" -#endif -#include "DStarDefines.h" -#include "DMRDefines.h" -#include "YSFDefines.h" -#include "P25Defines.h" -#include "NXDNDefines.h" -#include "AX25Defines.h" -#include "POCSAGDefines.h" -#include "Thread.h" #include "Modem.h" -#include "NullModem.h" -#include "Utils.h" -#include "Log.h" -#include -#include -#include -#include -#include - -#if defined(_WIN32) || defined(_WIN64) -#include -#else -#include -#endif - -const unsigned char MMDVM_FRAME_START = 0xE0U; - -const unsigned char MMDVM_GET_VERSION = 0x00U; -const unsigned char MMDVM_GET_STATUS = 0x01U; -const unsigned char MMDVM_SET_CONFIG = 0x02U; -const unsigned char MMDVM_SET_MODE = 0x03U; -const unsigned char MMDVM_SET_FREQ = 0x04U; - -const unsigned char MMDVM_SEND_CWID = 0x0AU; - -const unsigned char MMDVM_DSTAR_HEADER = 0x10U; -const unsigned char MMDVM_DSTAR_DATA = 0x11U; -const unsigned char MMDVM_DSTAR_LOST = 0x12U; -const unsigned char MMDVM_DSTAR_EOT = 0x13U; - -const unsigned char MMDVM_DMR_DATA1 = 0x18U; -const unsigned char MMDVM_DMR_LOST1 = 0x19U; -const unsigned char MMDVM_DMR_DATA2 = 0x1AU; -const unsigned char MMDVM_DMR_LOST2 = 0x1BU; -const unsigned char MMDVM_DMR_SHORTLC = 0x1CU; -const unsigned char MMDVM_DMR_START = 0x1DU; -const unsigned char MMDVM_DMR_ABORT = 0x1EU; - -const unsigned char MMDVM_YSF_DATA = 0x20U; -const unsigned char MMDVM_YSF_LOST = 0x21U; - -const unsigned char MMDVM_P25_HDR = 0x30U; -const unsigned char MMDVM_P25_LDU = 0x31U; -const unsigned char MMDVM_P25_LOST = 0x32U; - -const unsigned char MMDVM_NXDN_DATA = 0x40U; -const unsigned char MMDVM_NXDN_LOST = 0x41U; - -const unsigned char MMDVM_POCSAG_DATA = 0x50U; - -const unsigned char MMDVM_AX25_DATA = 0x55U; - -const unsigned char MMDVM_FM_PARAMS1 = 0x60U; -const unsigned char MMDVM_FM_PARAMS2 = 0x61U; -const unsigned char MMDVM_FM_PARAMS3 = 0x62U; -const unsigned char MMDVM_FM_PARAMS4 = 0x63U; -const unsigned char MMDVM_FM_DATA = 0x65U; -const unsigned char MMDVM_FM_CONTROL = 0x66U; -const unsigned char MMDVM_FM_EOT = 0x67U; - -const unsigned char MMDVM_ACK = 0x70U; -const unsigned char MMDVM_NAK = 0x7FU; - -const unsigned char MMDVM_SERIAL = 0x80U; - -const unsigned char MMDVM_TRANSPARENT = 0x90U; -const unsigned char MMDVM_QSO_INFO = 0x91U; - -const unsigned char MMDVM_DEBUG1 = 0xF1U; -const unsigned char MMDVM_DEBUG2 = 0xF2U; -const unsigned char MMDVM_DEBUG3 = 0xF3U; -const unsigned char MMDVM_DEBUG4 = 0xF4U; -const unsigned char MMDVM_DEBUG5 = 0xF5U; - -const unsigned int MAX_RESPONSES = 30U; - -const unsigned int BUFFER_LENGTH = 2000U; - - -CModem::CModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool trace, bool debug) : -m_port(port), -m_dmrColorCode(0U), -m_ysfLoDev(false), -m_ysfTXHang(4U), -m_p25TXHang(5U), -m_nxdnTXHang(5U), -m_duplex(duplex), -m_rxInvert(rxInvert), -m_txInvert(txInvert), -m_pttInvert(pttInvert), -m_txDelay(txDelay), -m_dmrDelay(dmrDelay), -m_rxLevel(0.0F), -m_cwIdTXLevel(0.0F), -m_dstarTXLevel(0.0F), -m_dmrTXLevel(0.0F), -m_ysfTXLevel(0.0F), -m_p25TXLevel(0.0F), -m_nxdnTXLevel(0.0F), -m_pocsagTXLevel(0.0F), -m_fmTXLevel(0.0F), -m_ax25TXLevel(0.0F), -m_rfLevel(0.0F), -m_trace(trace), -m_debug(debug), -m_rxFrequency(0U), -m_txFrequency(0U), -m_pocsagFrequency(0U), -m_dstarEnabled(false), -m_dmrEnabled(false), -m_ysfEnabled(false), -m_p25Enabled(false), -m_nxdnEnabled(false), -m_pocsagEnabled(false), -m_fmEnabled(false), -m_ax25Enabled(false), -m_rxDCOffset(0), -m_txDCOffset(0), -m_serial(NULL), -m_buffer(NULL), -m_length(0U), -m_offset(0U), -m_state(SS_START), -m_type(0U), -m_rxDStarData(1000U, "Modem RX D-Star"), -m_txDStarData(1000U, "Modem TX D-Star"), -m_rxDMRData1(1000U, "Modem RX DMR1"), -m_rxDMRData2(1000U, "Modem RX DMR2"), -m_txDMRData1(1000U, "Modem TX DMR1"), -m_txDMRData2(1000U, "Modem TX DMR2"), -m_rxYSFData(1000U, "Modem RX YSF"), -m_txYSFData(1000U, "Modem TX YSF"), -m_rxP25Data(1000U, "Modem RX P25"), -m_txP25Data(1000U, "Modem TX P25"), -m_rxNXDNData(1000U, "Modem RX NXDN"), -m_txNXDNData(1000U, "Modem TX NXDN"), -m_txPOCSAGData(1000U, "Modem TX POCSAG"), -m_rxFMData(5000U, "Modem RX FM"), -m_txFMData(5000U, "Modem TX FM"), -m_rxAX25Data(1000U, "Modem RX AX.25"), -m_txAX25Data(1000U, "Modem TX AX.25"), -m_rxTransparentData(1000U, "Modem RX Transparent"), -m_txTransparentData(1000U, "Modem TX Transparent"), -m_sendTransparentDataFrameType(0U), -m_statusTimer(1000U, 0U, 250U), -m_inactivityTimer(1000U, 2U), -m_playoutTimer(1000U, 0U, 10U), -m_dstarSpace(0U), -m_dmrSpace1(0U), -m_dmrSpace2(0U), -m_ysfSpace(0U), -m_p25Space(0U), -m_nxdnSpace(0U), -m_pocsagSpace(0U), -m_fmSpace(0U), -m_ax25Space(0U), -m_tx(false), -m_cd(false), -m_lockout(false), -m_error(false), -m_mode(MODE_IDLE), -m_hwType(HWT_UNKNOWN), -m_ax25RXTwist(0), -m_ax25TXDelay(300U), -m_fmCallsign(), -m_fmCallsignSpeed(20U), -m_fmCallsignFrequency(1000U), -m_fmCallsignTime(600U), -m_fmCallsignHoldoff(0U), -m_fmCallsignHighLevel(35.0F), -m_fmCallsignLowLevel(15.0F), -m_fmCallsignAtStart(true), -m_fmCallsignAtEnd(true), -m_fmCallsignAtLatch(true), -m_fmRfAck("K"), -m_fmExtAck("N"), -m_fmAckSpeed(20U), -m_fmAckFrequency(1750U), -m_fmAckMinTime(4U), -m_fmAckDelay(1000U), -m_fmAckLevel(80.0F), -m_fmTimeout(120U), -m_fmTimeoutLevel(80.0F), -m_fmCtcssFrequency(88.4F), -m_fmCtcssHighThreshold(30U), -m_fmCtcssLowThreshold(20U), -m_fmCtcssLevel(10.0F), -m_fmKerchunkTime(0U), -m_fmKerchunkTX(true), -m_fmHangTime(5U), -m_fmUseCOS(true), -m_fmCOSInvert(false), -m_fmRFAudioBoost(1U), -m_fmExtAudioBoost(1U), -m_fmMaxDevLevel(90.0F), -m_fmExtEnable(false) -{ - m_buffer = new unsigned char[BUFFER_LENGTH]; - - assert(!port.empty()); -} - -CModem::~CModem() -{ - delete m_serial; - delete[] m_buffer; -} - -void CModem::setSerialParams(const std::string& protocol, unsigned int address, unsigned int speed) -{ - // Create the serial controller instance according the protocol specified in conf. -#if defined(__linux__) - if (protocol == "i2c") - m_serial = new CI2CController(m_port, address); - else -#endif - m_serial = new CSerialController(m_port, speed, true); -} - -void CModem::setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) -{ - m_rxFrequency = rxFrequency + rxOffset; - m_txFrequency = txFrequency + txOffset; - m_txDCOffset = txDCOffset; - m_rxDCOffset = rxDCOffset; - m_rfLevel = rfLevel; - m_pocsagFrequency = pocsagFrequency + txOffset; -} - -void CModem::setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled, bool ax25Enabled) -{ - m_dstarEnabled = dstarEnabled; - m_dmrEnabled = dmrEnabled; - m_ysfEnabled = ysfEnabled; - m_p25Enabled = p25Enabled; - m_nxdnEnabled = nxdnEnabled; - m_pocsagEnabled = pocsagEnabled; - m_fmEnabled = fmEnabled; - m_ax25Enabled = ax25Enabled; -} - -void CModem::setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagTXLevel, float fmTXLevel, float ax25TXLevel) -{ - m_rxLevel = rxLevel; - m_cwIdTXLevel = cwIdTXLevel; - m_dstarTXLevel = dstarTXLevel; - m_dmrTXLevel = dmrTXLevel; - m_ysfTXLevel = ysfTXLevel; - m_p25TXLevel = p25TXLevel; - m_nxdnTXLevel = nxdnTXLevel; - m_pocsagTXLevel = pocsagTXLevel; - m_fmTXLevel = fmTXLevel; - m_ax25TXLevel = ax25TXLevel; -} - -void CModem::setDMRParams(unsigned int colorCode) -{ - assert(colorCode < 16U); - - m_dmrColorCode = colorCode; -} - -void CModem::setYSFParams(bool loDev, unsigned int txHang) -{ - m_ysfLoDev = loDev; - m_ysfTXHang = txHang; -} - -void CModem::setP25Params(unsigned int txHang) -{ - m_p25TXHang = txHang; -} - -void CModem::setNXDNParams(unsigned int txHang) -{ - m_nxdnTXHang = txHang; -} - -void CModem::setAX25Params(int rxTwist, unsigned int txDelay) -{ - m_ax25RXTwist = rxTwist; - m_ax25TXDelay = txDelay; -} - -void CModem::setTransparentDataParams(unsigned int sendFrameType) -{ - m_sendTransparentDataFrameType = sendFrameType; -} - -bool CModem::open() -{ - ::LogMessage("Opening the MMDVM"); - - bool ret = m_serial->open(); - if (!ret) - return false; - - ret = readVersion(); - if (!ret) { - m_serial->close(); - delete m_serial; - m_serial = NULL; - return false; - } else { - /* Stopping the inactivity timer here when a firmware version has been - successfuly read prevents the death spiral of "no reply from modem..." */ - m_inactivityTimer.stop(); - } - - ret = setFrequency(); - if (!ret) { - m_serial->close(); - delete m_serial; - m_serial = NULL; - return false; - } - - ret = setConfig(); - if (!ret) { - m_serial->close(); - delete m_serial; - m_serial = NULL; - return false; - } - - if (m_fmEnabled && m_duplex) { - ret = setFMCallsignParams(); - if (!ret) { - m_serial->close(); - delete m_serial; - m_serial = NULL; - return false; - } - - ret = setFMAckParams(); - if (!ret) { - m_serial->close(); - delete m_serial; - m_serial = NULL; - return false; - } - - ret = setFMMiscParams(); - if (!ret) { - m_serial->close(); - delete m_serial; - m_serial = NULL; - return false; - } - - if (m_fmExtEnable) { - ret = setFMExtParams(); - if (!ret) { - m_serial->close(); - delete m_serial; - m_serial = NULL; - return false; - } - } - } - - m_statusTimer.start(); - - m_error = false; - m_offset = 0U; - - return true; -} - -void CModem::clock(unsigned int ms) -{ - assert(m_serial != NULL); - - // Poll the modem status every 250ms - m_statusTimer.clock(ms); - if (m_statusTimer.hasExpired()) { - readStatus(); - m_statusTimer.start(); - } - - m_inactivityTimer.clock(ms); - if (m_inactivityTimer.hasExpired()) { - LogError("No reply from the modem for some time, resetting it"); - m_error = true; - close(); - - CThread::sleep(2000U); // 2s - while (!open()) - CThread::sleep(5000U); // 5s - } - - RESP_TYPE_MMDVM type = getResponse(); - - if (type == RTM_TIMEOUT) { - // Nothing to do - } else if (type == RTM_ERROR) { - // Nothing to do - } else { - // type == RTM_OK - switch (m_type) { - case MMDVM_DSTAR_HEADER: { - if (m_trace) - CUtils::dump(1U, "RX D-Star Header", m_buffer, m_length); - - unsigned char data = m_length - m_offset + 1U; - m_rxDStarData.addData(&data, 1U); - - data = TAG_HEADER; - m_rxDStarData.addData(&data, 1U); - - m_rxDStarData.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_DSTAR_DATA: { - if (m_trace) - CUtils::dump(1U, "RX D-Star Data", m_buffer, m_length); - - unsigned char data = m_length - m_offset + 1U; - m_rxDStarData.addData(&data, 1U); - - data = TAG_DATA; - m_rxDStarData.addData(&data, 1U); - - m_rxDStarData.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_DSTAR_LOST: { - if (m_trace) - CUtils::dump(1U, "RX D-Star Lost", m_buffer, m_length); - - unsigned char data = 1U; - m_rxDStarData.addData(&data, 1U); - - data = TAG_LOST; - m_rxDStarData.addData(&data, 1U); - } - break; - - case MMDVM_DSTAR_EOT: { - if (m_trace) - CUtils::dump(1U, "RX D-Star EOT", m_buffer, m_length); - - unsigned char data = 1U; - m_rxDStarData.addData(&data, 1U); - - data = TAG_EOT; - m_rxDStarData.addData(&data, 1U); - } - break; - - case MMDVM_DMR_DATA1: { - if (m_trace) - CUtils::dump(1U, "RX DMR Data 1", m_buffer, m_length); - - unsigned char data = m_length - m_offset + 1U; - m_rxDMRData1.addData(&data, 1U); - - if (m_buffer[3U] == (DMR_SYNC_DATA | DT_TERMINATOR_WITH_LC)) - data = TAG_EOT; - else - data = TAG_DATA; - m_rxDMRData1.addData(&data, 1U); - - m_rxDMRData1.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_DMR_DATA2: { - if (m_trace) - CUtils::dump(1U, "RX DMR Data 2", m_buffer, m_length); - - unsigned char data = m_length - m_offset + 1U; - m_rxDMRData2.addData(&data, 1U); - - if (m_buffer[3U] == (DMR_SYNC_DATA | DT_TERMINATOR_WITH_LC)) - data = TAG_EOT; - else - data = TAG_DATA; - m_rxDMRData2.addData(&data, 1U); - - m_rxDMRData2.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_DMR_LOST1: { - if (m_trace) - CUtils::dump(1U, "RX DMR Lost 1", m_buffer, m_length); - - unsigned char data = 1U; - m_rxDMRData1.addData(&data, 1U); - - data = TAG_LOST; - m_rxDMRData1.addData(&data, 1U); - } - break; - - case MMDVM_DMR_LOST2: { - if (m_trace) - CUtils::dump(1U, "RX DMR Lost 2", m_buffer, m_length); - - unsigned char data = 1U; - m_rxDMRData2.addData(&data, 1U); - - data = TAG_LOST; - m_rxDMRData2.addData(&data, 1U); - } - break; - - case MMDVM_YSF_DATA: { - if (m_trace) - CUtils::dump(1U, "RX YSF Data", m_buffer, m_length); - - unsigned char data = m_length - m_offset + 1U; - m_rxYSFData.addData(&data, 1U); - - data = TAG_DATA; - m_rxYSFData.addData(&data, 1U); - - m_rxYSFData.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_YSF_LOST: { - if (m_trace) - CUtils::dump(1U, "RX YSF Lost", m_buffer, m_length); - - unsigned char data = 1U; - m_rxYSFData.addData(&data, 1U); - - data = TAG_LOST; - m_rxYSFData.addData(&data, 1U); - } - break; - - case MMDVM_P25_HDR: { - if (m_trace) - CUtils::dump(1U, "RX P25 Header", m_buffer, m_length); - - unsigned char data = m_length - m_offset + 1U; - m_rxP25Data.addData(&data, 1U); - - data = TAG_HEADER; - m_rxP25Data.addData(&data, 1U); - - m_rxP25Data.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_P25_LDU: { - if (m_trace) - CUtils::dump(1U, "RX P25 LDU", m_buffer, m_length); - - unsigned char data = m_length - m_offset + 1U; - m_rxP25Data.addData(&data, 1U); - - data = TAG_DATA; - m_rxP25Data.addData(&data, 1U); - - m_rxP25Data.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_P25_LOST: { - if (m_trace) - CUtils::dump(1U, "RX P25 Lost", m_buffer, m_length); - - unsigned char data = 1U; - m_rxP25Data.addData(&data, 1U); - - data = TAG_LOST; - m_rxP25Data.addData(&data, 1U); - } - break; - - case MMDVM_NXDN_DATA: { - if (m_trace) - CUtils::dump(1U, "RX NXDN Data", m_buffer, m_length); - - unsigned char data = m_length - m_offset + 1U; - m_rxNXDNData.addData(&data, 1U); - - data = TAG_DATA; - m_rxNXDNData.addData(&data, 1U); - - m_rxNXDNData.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_NXDN_LOST: { - if (m_trace) - CUtils::dump(1U, "RX NXDN Lost", m_buffer, m_length); - - unsigned char data = 1U; - m_rxNXDNData.addData(&data, 1U); - - data = TAG_LOST; - m_rxNXDNData.addData(&data, 1U); - } - break; - - case MMDVM_FM_DATA: { - if (m_trace) - CUtils::dump(1U, "RX FM Data", m_buffer, m_length); - - unsigned int data1 = m_length - m_offset + 1U; - m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); - - unsigned char data2 = TAG_DATA; - m_rxFMData.addData(&data2, 1U); - - m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_FM_CONTROL: { - if (m_trace) - CUtils::dump(1U, "RX FM Control", m_buffer, m_length); - - unsigned char data = m_length - m_offset + 1U; - m_rxFMData.addData(&data, 1U); - - data = TAG_HEADER; - m_rxFMData.addData(&data, 1U); - - m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_FM_EOT: { - if(m_trace) - CUtils::dump(1U, "RX FM End of transmission", m_buffer, m_length); - - unsigned char data = m_length - m_offset + 1U; - m_rxFMData.addData(&data, 1U); - - data = TAG_EOT; - m_rxFMData.addData(&data, 1U); - - m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_AX25_DATA: { - if (m_trace) - CUtils::dump(1U, "RX AX.25 Data", m_buffer, m_length); - - unsigned int data = m_length - m_offset; - m_rxAX25Data.addData((unsigned char*)&data, sizeof(unsigned int)); - - m_rxAX25Data.addData(m_buffer + m_offset, m_length - m_offset); - } - break; - - case MMDVM_GET_STATUS: { - // if (m_trace) - // CUtils::dump(1U, "GET_STATUS", m_buffer, m_length); - - m_p25Space = 0U; - m_nxdnSpace = 0U; - m_pocsagSpace = 0U; - m_fmSpace = 0U; - m_ax25Space = 0U; - - m_mode = m_buffer[m_offset + 1U]; - - m_tx = (m_buffer[m_offset + 2U] & 0x01U) == 0x01U; - - bool adcOverflow = (m_buffer[m_offset + 2U] & 0x02U) == 0x02U; - if (adcOverflow) - LogError("MMDVM ADC levels have overflowed"); - - bool rxOverflow = (m_buffer[m_offset + 2U] & 0x04U) == 0x04U; - if (rxOverflow) - LogError("MMDVM RX buffer has overflowed"); - - bool txOverflow = (m_buffer[m_offset + 2U] & 0x08U) == 0x08U; - if (txOverflow) - LogError("MMDVM TX buffer has overflowed"); - - m_lockout = (m_buffer[m_offset + 2U] & 0x10U) == 0x10U; - - bool dacOverflow = (m_buffer[m_offset + 2U] & 0x20U) == 0x20U; - if (dacOverflow) - LogError("MMDVM DAC levels have overflowed"); - - m_cd = (m_buffer[m_offset + 2U] & 0x40U) == 0x40U; - - m_dstarSpace = m_buffer[m_offset + 3U]; - m_dmrSpace1 = m_buffer[m_offset + 4U]; - m_dmrSpace2 = m_buffer[m_offset + 5U]; - m_ysfSpace = m_buffer[m_offset + 6U]; - - if (m_length > (m_offset + 7U)) - m_p25Space = m_buffer[m_offset + 7U]; - if (m_length > (m_offset + 8U)) - m_nxdnSpace = m_buffer[m_offset + 8U]; - if (m_length > (m_offset + 9U)) - m_pocsagSpace = m_buffer[m_offset + 9U]; - if (m_length > (m_offset + 10U)) - m_fmSpace = m_buffer[m_offset + 10U]; - if (m_length > (m_offset + 11U)) - m_ax25Space = m_buffer[m_offset + 11U]; - - m_inactivityTimer.start(); - // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[m_offset + 2U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_pocsagSpace, m_fmSpace, m_ax25Space, int(m_lockout), int(m_cd)); - } - break; - - case MMDVM_TRANSPARENT: { - if (m_trace) - CUtils::dump(1U, "RX Transparent Data", m_buffer, m_length); - - unsigned char offset = m_sendTransparentDataFrameType; - if (offset > 1U) offset = 1U; - unsigned char data = m_length - m_offset + offset; - m_rxTransparentData.addData(&data, 1U); - - m_rxTransparentData.addData(m_buffer + m_offset - offset, m_length - m_offset + offset); - } - break; - - // These should not be received, but don't complain if we do - case MMDVM_GET_VERSION: - case MMDVM_ACK: - break; - - case MMDVM_NAK: - LogWarning("Received a NAK from the MMDVM, command = 0x%02X, reason = %u", m_buffer[m_offset], m_buffer[m_offset + 1U]); - break; - - case MMDVM_DEBUG1: - case MMDVM_DEBUG2: - case MMDVM_DEBUG3: - case MMDVM_DEBUG4: - case MMDVM_DEBUG5: - printDebug(); - break; - - case MMDVM_SERIAL: - //MMDVMHost does not process serial data from the display, - // so we send it to the transparent port if sendFrameType==1 - if (m_sendTransparentDataFrameType > 0U) { - if (m_trace) - CUtils::dump(1U, "RX Serial Data", m_buffer, m_length); - - unsigned char offset = m_sendTransparentDataFrameType; - if (offset > 1U) offset = 1U; - unsigned char data = m_length - m_offset + offset; - m_rxTransparentData.addData(&data, 1U); - - m_rxTransparentData.addData(m_buffer + m_offset - offset, m_length - m_offset + offset); - break; //only break when sendFrameType>0, else message is unknown - } - default: - LogMessage("Unknown message, type: %02X", m_type); - CUtils::dump("Buffer dump", m_buffer, m_length); - break; - } - } - - // Only feed data to the modem if the playout timer has expired - m_playoutTimer.clock(ms); - if (!m_playoutTimer.hasExpired()) - return; - - if (m_dstarSpace > 1U && !m_txDStarData.isEmpty()) { - unsigned char buffer[4U]; - m_txDStarData.peek(buffer, 4U); - - if ((buffer[3U] == MMDVM_DSTAR_HEADER && m_dstarSpace > 4U) || - (buffer[3U] == MMDVM_DSTAR_DATA && m_dstarSpace > 1U) || - (buffer[3U] == MMDVM_DSTAR_EOT && m_dstarSpace > 1U)) { - unsigned char len = 0U; - m_txDStarData.getData(&len, 1U); - m_txDStarData.getData(m_buffer, len); - - switch (buffer[3U]) { - case MMDVM_DSTAR_HEADER: - if (m_trace) - CUtils::dump(1U, "TX D-Star Header", m_buffer, len); - m_dstarSpace -= 4U; - break; - case MMDVM_DSTAR_DATA: - if (m_trace) - CUtils::dump(1U, "TX D-Star Data", m_buffer, len); - m_dstarSpace -= 1U; - break; - default: - if (m_trace) - CUtils::dump(1U, "TX D-Star EOT", m_buffer, len); - m_dstarSpace -= 1U; - break; - } - - int ret = m_serial->write(m_buffer, len); - if (ret != int(len)) - LogWarning("Error when writing D-Star data to the MMDVM"); - - m_playoutTimer.start(); - } - } - - if (m_dmrSpace1 > 1U && !m_txDMRData1.isEmpty()) { - unsigned char len = 0U; - m_txDMRData1.getData(&len, 1U); - m_txDMRData1.getData(m_buffer, len); - - if (m_trace) - CUtils::dump(1U, "TX DMR Data 1", m_buffer, len); - - int ret = m_serial->write(m_buffer, len); - if (ret != int(len)) - LogWarning("Error when writing DMR data to the MMDVM"); - - m_playoutTimer.start(); - - m_dmrSpace1--; - } - - if (m_dmrSpace2 > 1U && !m_txDMRData2.isEmpty()) { - unsigned char len = 0U; - m_txDMRData2.getData(&len, 1U); - m_txDMRData2.getData(m_buffer, len); - - if (m_trace) - CUtils::dump(1U, "TX DMR Data 2", m_buffer, len); - - int ret = m_serial->write(m_buffer, len); - if (ret != int(len)) - LogWarning("Error when writing DMR data to the MMDVM"); - - m_playoutTimer.start(); - - m_dmrSpace2--; - } - - if (m_ysfSpace > 1U && !m_txYSFData.isEmpty()) { - unsigned char len = 0U; - m_txYSFData.getData(&len, 1U); - m_txYSFData.getData(m_buffer, len); - - if (m_trace) - CUtils::dump(1U, "TX YSF Data", m_buffer, len); - - int ret = m_serial->write(m_buffer, len); - if (ret != int(len)) - LogWarning("Error when writing YSF data to the MMDVM"); - - m_playoutTimer.start(); - - m_ysfSpace--; - } - - if (m_p25Space > 1U && !m_txP25Data.isEmpty()) { - unsigned char len = 0U; - m_txP25Data.getData(&len, 1U); - m_txP25Data.getData(m_buffer, len); - - if (m_trace) { - if (m_buffer[2U] == MMDVM_P25_HDR) - CUtils::dump(1U, "TX P25 HDR", m_buffer, len); - else - CUtils::dump(1U, "TX P25 LDU", m_buffer, len); - } - - int ret = m_serial->write(m_buffer, len); - if (ret != int(len)) - LogWarning("Error when writing P25 data to the MMDVM"); - - m_playoutTimer.start(); - - m_p25Space--; - } - - if (m_nxdnSpace > 1U && !m_txNXDNData.isEmpty()) { - unsigned char len = 0U; - m_txNXDNData.getData(&len, 1U); - m_txNXDNData.getData(m_buffer, len); - - if (m_trace) - CUtils::dump(1U, "TX NXDN Data", m_buffer, len); - - int ret = m_serial->write(m_buffer, len); - if (ret != int(len)) - LogWarning("Error when writing NXDN data to the MMDVM"); - - m_playoutTimer.start(); - - m_nxdnSpace--; - } - - if (m_pocsagSpace > 1U && !m_txPOCSAGData.isEmpty()) { - unsigned char len = 0U; - m_txPOCSAGData.getData(&len, 1U); - m_txPOCSAGData.getData(m_buffer, len); - - if (m_trace) - CUtils::dump(1U, "TX POCSAG Data", m_buffer, len); - - int ret = m_serial->write(m_buffer, len); - if (ret != int(len)) - LogWarning("Error when writing POCSAG data to the MMDVM"); - - m_playoutTimer.start(); - - m_pocsagSpace--; - } - - if (m_fmSpace > 1U && !m_txFMData.isEmpty()) { - unsigned char len = 0U; - m_txFMData.getData(&len, 1U); - m_txFMData.getData(m_buffer, len); - - if (m_trace) - CUtils::dump(1U, "TX FM Data", m_buffer, len); - - int ret = m_serial->write(m_buffer, len); - if (ret != int(len)) - LogWarning("Error when writing FM data to the MMDVM"); - - m_playoutTimer.start(); - - m_fmSpace--; - } - - if (m_ax25Space > 0U && !m_txAX25Data.isEmpty()) { - unsigned char len = 0U; - m_txAX25Data.getData((unsigned char*)&len, sizeof(unsigned int)); - m_txAX25Data.getData(m_buffer, len); - - if (m_trace) - CUtils::dump(1U, "TX AX.25 Data", m_buffer, len); - - int ret = m_serial->write(m_buffer, len); - if (ret != int(len)) - LogWarning("Error when writing AX.25 data to the MMDVM"); - - m_playoutTimer.start(); - - m_ax25Space = 0U; - } - - if (!m_txTransparentData.isEmpty()) { - unsigned char len = 0U; - m_txTransparentData.getData(&len, 1U); - m_txTransparentData.getData(m_buffer, len); - - if (m_trace) - CUtils::dump(1U, "TX Transparent Data", m_buffer, len); - - int ret = m_serial->write(m_buffer, len); - if (ret != int(len)) - LogWarning("Error when writing Transparent data to the MMDVM"); - } -} - -void CModem::close() -{ - assert(m_serial != NULL); - - ::LogMessage("Closing the MMDVM"); - - m_serial->close(); -} - -unsigned int CModem::readDStarData(unsigned char* data) -{ - assert(data != NULL); - - if (m_rxDStarData.isEmpty()) - return 0U; - - unsigned char len = 0U; - m_rxDStarData.getData(&len, 1U); - m_rxDStarData.getData(data, len); - - return len; -} - -unsigned int CModem::readDMRData1(unsigned char* data) -{ - assert(data != NULL); - - if (m_rxDMRData1.isEmpty()) - return 0U; - - unsigned char len = 0U; - m_rxDMRData1.getData(&len, 1U); - m_rxDMRData1.getData(data, len); - - return len; -} - -unsigned int CModem::readDMRData2(unsigned char* data) -{ - assert(data != NULL); - - if (m_rxDMRData2.isEmpty()) - return 0U; - - unsigned char len = 0U; - m_rxDMRData2.getData(&len, 1U); - m_rxDMRData2.getData(data, len); - - return len; -} - -unsigned int CModem::readYSFData(unsigned char* data) -{ - assert(data != NULL); - - if (m_rxYSFData.isEmpty()) - return 0U; - - unsigned char len = 0U; - m_rxYSFData.getData(&len, 1U); - m_rxYSFData.getData(data, len); - - return len; -} - -unsigned int CModem::readP25Data(unsigned char* data) -{ - assert(data != NULL); - - if (m_rxP25Data.isEmpty()) - return 0U; - - unsigned char len = 0U; - m_rxP25Data.getData(&len, 1U); - m_rxP25Data.getData(data, len); - - return len; -} - -unsigned int CModem::readNXDNData(unsigned char* data) -{ - assert(data != NULL); - - if (m_rxNXDNData.isEmpty()) - return 0U; - - unsigned char len = 0U; - m_rxNXDNData.getData(&len, 1U); - m_rxNXDNData.getData(data, len); - - return len; -} - -unsigned int CModem::readFMData(unsigned char* data) -{ - assert(data != NULL); - - if (m_rxFMData.isEmpty()) - return 0U; - - unsigned int len = 0U; - m_rxFMData.getData((unsigned char*)&len, sizeof(unsigned int)); - m_rxFMData.getData(data, len); - - return len; -} - -unsigned int CModem::readAX25Data(unsigned char* data) -{ - assert(data != NULL); - - if (m_rxAX25Data.isEmpty()) - return 0U; - - unsigned int len = 0U; - m_rxAX25Data.getData((unsigned char*)&len, sizeof(unsigned int)); - m_rxAX25Data.getData(data, len); - - return len; -} - -unsigned int CModem::readTransparentData(unsigned char* data) -{ - assert(data != NULL); - - if (m_rxTransparentData.isEmpty()) - return 0U; - - unsigned char len = 0U; - m_rxTransparentData.getData(&len, 1U); - m_rxTransparentData.getData(data, len); - - return len; -} - -// To be implemented later if needed -unsigned int CModem::readSerial(unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - return 0U; -} - -bool CModem::hasDStarSpace() const -{ - unsigned int space = m_txDStarData.freeSpace() / (DSTAR_FRAME_LENGTH_BYTES + 4U); - - return space > 1U; -} - -bool CModem::writeDStarData(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - unsigned char buffer[50U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 2U; - - switch (data[0U]) { - case TAG_HEADER: - buffer[2U] = MMDVM_DSTAR_HEADER; - break; - case TAG_DATA: - buffer[2U] = MMDVM_DSTAR_DATA; - break; - case TAG_EOT: - buffer[2U] = MMDVM_DSTAR_EOT; - break; - default: - CUtils::dump(2U, "Unknown D-Star packet type", data, length); - return false; - } - - ::memcpy(buffer + 3U, data + 1U, length - 1U); - - unsigned char len = length + 2U; - m_txDStarData.addData(&len, 1U); - m_txDStarData.addData(buffer, len); - - return true; -} - -bool CModem::hasDMRSpace1() const -{ - unsigned int space = m_txDMRData1.freeSpace() / (DMR_FRAME_LENGTH_BYTES + 4U); - - return space > 1U; -} - -bool CModem::hasDMRSpace2() const -{ - unsigned int space = m_txDMRData2.freeSpace() / (DMR_FRAME_LENGTH_BYTES + 4U); - - return space > 1U; -} - -bool CModem::writeDMRData1(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) - return false; - - unsigned char buffer[40U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 2U; - buffer[2U] = MMDVM_DMR_DATA1; - - ::memcpy(buffer + 3U, data + 1U, length - 1U); - - unsigned char len = length + 2U; - m_txDMRData1.addData(&len, 1U); - m_txDMRData1.addData(buffer, len); - - return true; -} - -bool CModem::writeDMRData2(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) - return false; - - unsigned char buffer[40U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 2U; - buffer[2U] = MMDVM_DMR_DATA2; - - ::memcpy(buffer + 3U, data + 1U, length - 1U); - - unsigned char len = length + 2U; - m_txDMRData2.addData(&len, 1U); - m_txDMRData2.addData(buffer, len); - - return true; -} - -bool CModem::hasYSFSpace() const -{ - unsigned int space = m_txYSFData.freeSpace() / (YSF_FRAME_LENGTH_BYTES + 4U); - - return space > 1U; -} - -bool CModem::writeYSFData(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) - return false; - - unsigned char buffer[130U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 2U; - buffer[2U] = MMDVM_YSF_DATA; - - ::memcpy(buffer + 3U, data + 1U, length - 1U); - - unsigned char len = length + 2U; - m_txYSFData.addData(&len, 1U); - m_txYSFData.addData(buffer, len); - - return true; -} - -bool CModem::hasP25Space() const -{ - unsigned int space = m_txP25Data.freeSpace() / (P25_LDU_FRAME_LENGTH_BYTES + 4U); - - return space > 1U; -} - -bool CModem::writeP25Data(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - if (data[0U] != TAG_HEADER && data[0U] != TAG_DATA && data[0U] != TAG_EOT) - return false; - - unsigned char buffer[250U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 2U; - buffer[2U] = (data[0U] == TAG_HEADER) ? MMDVM_P25_HDR : MMDVM_P25_LDU; - - ::memcpy(buffer + 3U, data + 1U, length - 1U); - - unsigned char len = length + 2U; - m_txP25Data.addData(&len, 1U); - m_txP25Data.addData(buffer, len); - - return true; -} - -bool CModem::hasNXDNSpace() const -{ - unsigned int space = m_txNXDNData.freeSpace() / (NXDN_FRAME_LENGTH_BYTES + 4U); - - return space > 1U; -} - -bool CModem::writeNXDNData(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) - return false; - - unsigned char buffer[130U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 2U; - buffer[2U] = MMDVM_NXDN_DATA; - - ::memcpy(buffer + 3U, data + 1U, length - 1U); - - unsigned char len = length + 2U; - m_txNXDNData.addData(&len, 1U); - m_txNXDNData.addData(buffer, len); - - return true; -} - -bool CModem::hasPOCSAGSpace() const -{ - unsigned int space = m_txPOCSAGData.freeSpace() / (POCSAG_FRAME_LENGTH_BYTES + 4U); - - return space > 1U; -} - -bool CModem::writePOCSAGData(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - unsigned char buffer[130U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 3U; - buffer[2U] = MMDVM_POCSAG_DATA; - - ::memcpy(buffer + 3U, data, length); - - unsigned char len = length + 3U; - m_txPOCSAGData.addData(&len, 1U); - m_txPOCSAGData.addData(buffer, len); - - return true; -} - -unsigned int CModem::getFMSpace() const -{ - return m_txFMData.freeSpace(); -} - -bool CModem::writeFMData(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - unsigned char buffer[500U]; - - unsigned int len; - if (length > 252U) { - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 0U; - buffer[2U] = (length + 4U) - 255U; - buffer[3U] = MMDVM_FM_DATA; - ::memcpy(buffer + 4U, data, length); - len = length + 4U; - } else { - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 3U; - buffer[2U] = MMDVM_FM_DATA; - ::memcpy(buffer + 3U, data, length); - len = length + 3U; - } - - m_txFMData.addData((unsigned char*)&len, sizeof(unsigned int)); - m_txFMData.addData(buffer, len); - - return true; -} - -bool CModem::hasAX25Space() const -{ - unsigned int space = m_txAX25Data.freeSpace() / (AX25_MAX_FRAME_LENGTH_BYTES + 5U); - - return space > 1U; -} - -bool CModem::writeAX25Data(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - unsigned char buffer[500U]; - - unsigned int len; - if (length > 252U) { - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 0U; - buffer[2U] = (length + 4U) - 255U; - buffer[3U] = MMDVM_AX25_DATA; - ::memcpy(buffer + 4U, data, length); - len = length + 4U; - } else { - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 3U; - buffer[2U] = MMDVM_AX25_DATA; - ::memcpy(buffer + 3U, data, length); - len = length + 3U; - } - - m_txAX25Data.addData((unsigned char*)&len, sizeof(unsigned int)); - m_txAX25Data.addData(buffer, len); - - return true; -} - -bool CModem::writeTransparentData(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - unsigned char buffer[250U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 3U; - buffer[2U] = MMDVM_TRANSPARENT; - - if (m_sendTransparentDataFrameType > 0U) { - ::memcpy(buffer + 2U, data, length); - length--; - buffer[1U]--; - - //when sendFrameType==1 , only 0x80 and 0x90 (MMDVM_SERIAL and MMDVM_TRANSPARENT) are allowed - // and reverted to default (MMDVM_TRANSPARENT) for any other value - //when >1, frame type is not checked - if (m_sendTransparentDataFrameType == 1U) { - if ((buffer[2U] & 0xE0) != 0x80) - buffer[2U] = MMDVM_TRANSPARENT; - } - } else { - ::memcpy(buffer + 3U, data, length); - } - - unsigned char len = length + 3U; - m_txTransparentData.addData(&len, 1U); - m_txTransparentData.addData(buffer, len); - - return true; -} - -bool CModem::writeDStarInfo(const char* my1, const char* my2, const char* your, const char* type, const char* reflector) -{ - assert(m_serial != NULL); - assert(my1 != NULL); - assert(my2 != NULL); - assert(your != NULL); - assert(type != NULL); - assert(reflector != NULL); - - unsigned char buffer[50U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 33U; - buffer[2U] = MMDVM_QSO_INFO; - - buffer[3U] = MODE_DSTAR; - - ::memcpy(buffer + 4U, my1, DSTAR_LONG_CALLSIGN_LENGTH); - ::memcpy(buffer + 12U, my2, DSTAR_SHORT_CALLSIGN_LENGTH); - - ::memcpy(buffer + 16U, your, DSTAR_LONG_CALLSIGN_LENGTH); - - ::memcpy(buffer + 24U, type, 1U); - - ::memcpy(buffer + 25U, reflector, DSTAR_LONG_CALLSIGN_LENGTH); - - return m_serial->write(buffer, 33U) != 33; -} - -bool CModem::writeDMRInfo(unsigned int slotNo, const std::string& src, bool group, const std::string& dest, const char* type) -{ - assert(m_serial != NULL); - assert(type != NULL); - - unsigned char buffer[50U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 47U; - buffer[2U] = MMDVM_QSO_INFO; - - buffer[3U] = MODE_DMR; - - buffer[4U] = slotNo; - - ::sprintf((char*)(buffer + 5U), "%20.20s", src.c_str()); - - buffer[25U] = group ? 'G' : 'I'; - - ::sprintf((char*)(buffer + 26U), "%20.20s", dest.c_str()); - - ::memcpy(buffer + 46U, type, 1U); - - return m_serial->write(buffer, 47U) != 47; -} - -bool CModem::writeYSFInfo(const char* source, const char* dest, const char* type, const char* origin) -{ - assert(m_serial != NULL); - assert(source != NULL); - assert(dest != NULL); - assert(type != NULL); - assert(origin != NULL); - - unsigned char buffer[50U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 35U; - buffer[2U] = MMDVM_QSO_INFO; - - buffer[3U] = MODE_YSF; - - ::memcpy(buffer + 4U, source, YSF_CALLSIGN_LENGTH); - ::memcpy(buffer + 14U, dest, YSF_CALLSIGN_LENGTH); - - ::memcpy(buffer + 24U, type, 1U); - - ::memcpy(buffer + 25U, origin, YSF_CALLSIGN_LENGTH); - - return m_serial->write(buffer, 35U) != 35; -} - -bool CModem::writeP25Info(const char* source, bool group, unsigned int dest, const char* type) -{ - assert(m_serial != NULL); - assert(source != NULL); - assert(type != NULL); - - unsigned char buffer[40U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 31U; - buffer[2U] = MMDVM_QSO_INFO; - - buffer[3U] = MODE_DMR; - - ::sprintf((char*)(buffer + 4U), "%20.20s", source); - - buffer[24U] = group ? 'G' : 'I'; - - ::sprintf((char*)(buffer + 25U), "%05u", dest); // 16-bits - - ::memcpy(buffer + 30U, type, 1U); - - return m_serial->write(buffer, 31U) != 31; -} - -bool CModem::writeNXDNInfo(const char* source, bool group, unsigned int dest, const char* type) -{ - assert(m_serial != NULL); - assert(source != NULL); - assert(type != NULL); - - unsigned char buffer[40U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 31U; - buffer[2U] = MMDVM_QSO_INFO; - - buffer[3U] = MODE_NXDN; - - ::sprintf((char*)(buffer + 4U), "%20.20s", source); - - buffer[24U] = group ? 'G' : 'I'; - - ::sprintf((char*)(buffer + 25U), "%05u", dest); // 16-bits - - ::memcpy(buffer + 30U, type, 1U); - - return m_serial->write(buffer, 31U) != 31; -} - -bool CModem::writePOCSAGInfo(unsigned int ric, const std::string& message) -{ - assert(m_serial != NULL); - - size_t length = message.size(); - - unsigned char buffer[250U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 11U; - buffer[2U] = MMDVM_QSO_INFO; - - buffer[3U] = MODE_POCSAG; - - ::sprintf((char*)(buffer + 4U), "%07u", ric); // 21-bits - - ::memcpy(buffer + 11U, message.c_str(), length); - - int ret = m_serial->write(buffer, length + 11U); - - return ret != int(length + 11U); -} - -bool CModem::writeIPInfo(const std::string& address) -{ - assert(m_serial != NULL); - - size_t length = address.size(); - - unsigned char buffer[25U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 4U; - buffer[2U] = MMDVM_QSO_INFO; - - buffer[3U] = 250U; - - ::memcpy(buffer + 4U, address.c_str(), length); - - int ret = m_serial->write(buffer, length + 4U); - - return ret != int(length + 4U); -} - -bool CModem::writeSerial(const unsigned char* data, unsigned int length) -{ - assert(m_serial != NULL); - assert(data != NULL); - assert(length > 0U); - - unsigned char buffer[250U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 3U; - buffer[2U] = MMDVM_SERIAL; - - ::memcpy(buffer + 3U, data, length); - - int ret = m_serial->write(buffer, length + 3U); - - return ret != int(length + 3U); -} - -bool CModem::hasTX() const -{ - return m_tx; -} - -bool CModem::hasCD() const -{ - return m_cd; -} - -bool CModem::hasLockout() const -{ - return m_lockout; -} - -bool CModem::hasError() const -{ - return m_error; -} - -bool CModem::readVersion() -{ - assert(m_serial != NULL); - - CThread::sleep(2000U); // 2s - - for (unsigned int i = 0U; i < 6U; i++) { - unsigned char buffer[3U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 3U; - buffer[2U] = MMDVM_GET_VERSION; - - // CUtils::dump(1U, "Written", buffer, 3U); - - int ret = m_serial->write(buffer, 3U); - if (ret != 3) - return false; - -#if defined(__APPLE__) - m_serial->setNonblock(true); -#endif - - for (unsigned int count = 0U; count < MAX_RESPONSES; count++) { - CThread::sleep(10U); - RESP_TYPE_MMDVM resp = getResponse(); - if (resp == RTM_OK && m_buffer[2U] == MMDVM_GET_VERSION) { - if (::memcmp(m_buffer + 4U, "MMDVM ", 6U) == 0) - m_hwType = HWT_MMDVM; - else if (::memcmp(m_buffer + 4U, "DVMEGA", 6U) == 0) - m_hwType = HWT_DVMEGA; - else if (::memcmp(m_buffer + 4U, "ZUMspot", 7U) == 0) - m_hwType = HWT_MMDVM_ZUMSPOT; - else if (::memcmp(m_buffer + 4U, "MMDVM_HS_Hat", 12U) == 0) - m_hwType = HWT_MMDVM_HS_HAT; - else if (::memcmp(m_buffer + 4U, "MMDVM_HS_Dual_Hat", 17U) == 0) - m_hwType = HWT_MMDVM_HS_DUAL_HAT; - else if (::memcmp(m_buffer + 4U, "Nano_hotSPOT", 12U) == 0) - m_hwType = HWT_NANO_HOTSPOT; - else if (::memcmp(m_buffer + 4U, "Nano_DV", 7U) == 0) - m_hwType = HWT_NANO_DV; - else if (::memcmp(m_buffer + 4U, "D2RG_MMDVM_HS", 13U) == 0) - m_hwType = HWT_D2RG_MMDVM_HS; - else if (::memcmp(m_buffer + 4U, "MMDVM_HS-", 9U) == 0) - m_hwType = HWT_MMDVM_HS; - else if (::memcmp(m_buffer + 4U, "OpenGD77_HS", 11U) == 0) - m_hwType = HWT_OPENGD77_HS; - - LogInfo("MMDVM protocol version: %u, description: %.*s", m_buffer[3U], m_length - 4U, m_buffer + 4U); - return true; - } - } - - CThread::sleep(1500U); - } - - LogError("Unable to read the firmware version after six attempts"); - - return false; -} - -bool CModem::readStatus() -{ - assert(m_serial != NULL); - - unsigned char buffer[3U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 3U; - buffer[2U] = MMDVM_GET_STATUS; - - // CUtils::dump(1U, "Written", buffer, 3U); - - return m_serial->write(buffer, 3U) == 3; -} - -bool CModem::writeConfig() -{ - return setConfig(); -} - -bool CModem::setConfig() -{ - assert(m_serial != NULL); - - unsigned char buffer[30U]; - - buffer[0U] = MMDVM_FRAME_START; - - buffer[1U] = 27U; - - buffer[2U] = MMDVM_SET_CONFIG; - - buffer[3U] = 0x00U; - if (m_rxInvert) - buffer[3U] |= 0x01U; - if (m_txInvert) - buffer[3U] |= 0x02U; - if (m_pttInvert) - buffer[3U] |= 0x04U; - if (m_ysfLoDev) - buffer[3U] |= 0x08U; - if (m_debug) - buffer[3U] |= 0x10U; - if (!m_duplex) - buffer[3U] |= 0x80U; - - buffer[4U] = 0x00U; - if (m_dstarEnabled) - buffer[4U] |= 0x01U; - if (m_dmrEnabled) - buffer[4U] |= 0x02U; - if (m_ysfEnabled) - buffer[4U] |= 0x04U; - if (m_p25Enabled) - buffer[4U] |= 0x08U; - if (m_nxdnEnabled) - buffer[4U] |= 0x10U; - if (m_pocsagEnabled) - buffer[4U] |= 0x20U; - if (m_fmEnabled && m_duplex) - buffer[4U] |= 0x40U; - if (m_ax25Enabled) - buffer[4U] |= 0x80U; - - buffer[5U] = m_txDelay / 10U; // In 10ms units - - buffer[6U] = MODE_IDLE; - - buffer[7U] = (unsigned char)(m_rxLevel * 2.55F + 0.5F); - - buffer[8U] = (unsigned char)(m_cwIdTXLevel * 2.55F + 0.5F); - - buffer[9U] = m_dmrColorCode; - - buffer[10U] = m_dmrDelay; - - buffer[11U] = 128U; // Was OscOffset - - buffer[12U] = (unsigned char)(m_dstarTXLevel * 2.55F + 0.5F); - buffer[13U] = (unsigned char)(m_dmrTXLevel * 2.55F + 0.5F); - buffer[14U] = (unsigned char)(m_ysfTXLevel * 2.55F + 0.5F); - buffer[15U] = (unsigned char)(m_p25TXLevel * 2.55F + 0.5F); - - buffer[16U] = (unsigned char)(m_txDCOffset + 128); - buffer[17U] = (unsigned char)(m_rxDCOffset + 128); - - buffer[18U] = (unsigned char)(m_nxdnTXLevel * 2.55F + 0.5F); - - buffer[19U] = (unsigned char)m_ysfTXHang; - - buffer[20U] = (unsigned char)(m_pocsagTXLevel * 2.55F + 0.5F); - - buffer[21U] = (unsigned char)(m_fmTXLevel * 2.55F + 0.5F); - - buffer[22U] = (unsigned char)m_p25TXHang; - - buffer[23U] = (unsigned char)m_nxdnTXHang; - - buffer[24U] = (unsigned char)(m_ax25TXLevel * 2.55F + 0.5F); - - buffer[25U] = (unsigned char)(m_ax25RXTwist + 128); - - buffer[26U] = m_ax25TXDelay / 10U; // In 10ms units - - // CUtils::dump(1U, "Written", buffer, 27U); - - int ret = m_serial->write(buffer, 27U); - if (ret != 27) - return false; - - unsigned int count = 0U; - RESP_TYPE_MMDVM resp; - do { - CThread::sleep(10U); - - resp = getResponse(); - if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { - count++; - if (count >= MAX_RESPONSES) { - LogError("The MMDVM is not responding to the SET_CONFIG command"); - return false; - } - } - } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); - - // CUtils::dump(1U, "Response", m_buffer, m_length); - - if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { - LogError("Received a NAK to the SET_CONFIG command from the modem"); - return false; - } - - m_playoutTimer.start(); - - return true; -} - -bool CModem::setFrequency() -{ - assert(m_serial != NULL); - - unsigned char buffer[20U]; - unsigned char len; - unsigned int pocsagFrequency = 433000000U; - - if (m_pocsagEnabled) - pocsagFrequency = m_pocsagFrequency; - - if (m_hwType == HWT_DVMEGA) - len = 12U; - else { - buffer[12U] = (unsigned char)(m_rfLevel * 2.55F + 0.5F); - - buffer[13U] = (pocsagFrequency >> 0) & 0xFFU; - buffer[14U] = (pocsagFrequency >> 8) & 0xFFU; - buffer[15U] = (pocsagFrequency >> 16) & 0xFFU; - buffer[16U] = (pocsagFrequency >> 24) & 0xFFU; - - len = 17U; - } - - buffer[0U] = MMDVM_FRAME_START; - - buffer[1U] = len; - - buffer[2U] = MMDVM_SET_FREQ; - - buffer[3U] = 0x00U; - - buffer[4U] = (m_rxFrequency >> 0) & 0xFFU; - buffer[5U] = (m_rxFrequency >> 8) & 0xFFU; - buffer[6U] = (m_rxFrequency >> 16) & 0xFFU; - buffer[7U] = (m_rxFrequency >> 24) & 0xFFU; - - buffer[8U] = (m_txFrequency >> 0) & 0xFFU; - buffer[9U] = (m_txFrequency >> 8) & 0xFFU; - buffer[10U] = (m_txFrequency >> 16) & 0xFFU; - buffer[11U] = (m_txFrequency >> 24) & 0xFFU; - - // CUtils::dump(1U, "Written", buffer, len); - - int ret = m_serial->write(buffer, len); - if (ret != len) - return false; - - unsigned int count = 0U; - RESP_TYPE_MMDVM resp; - do { - CThread::sleep(10U); - - resp = getResponse(); - if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { - count++; - if (count >= MAX_RESPONSES) { - LogError("The MMDVM is not responding to the SET_FREQ command"); - return false; - } - } - } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); - - // CUtils::dump(1U, "Response", m_buffer, m_length); - - if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { - LogError("Received a NAK to the SET_FREQ command from the modem"); - return false; - } - - return true; -} - -RESP_TYPE_MMDVM CModem::getResponse() -{ - assert(m_serial != NULL); - - if (m_state == SS_START) { - // Get the start of the frame or nothing at all - int ret = m_serial->read(m_buffer + 0U, 1U); - if (ret < 0) { - LogError("Error when reading from the modem"); - return RTM_ERROR; - } - - if (ret == 0) - return RTM_TIMEOUT; - - if (m_buffer[0U] != MMDVM_FRAME_START) - return RTM_TIMEOUT; - - m_state = SS_LENGTH1; - m_length = 1U; - } - - if (m_state == SS_LENGTH1) { - // Get the length of the frame, 1/2 - int ret = m_serial->read(m_buffer + 1U, 1U); - if (ret < 0) { - LogError("Error when reading from the modem"); - m_state = SS_START; - return RTM_ERROR; - } - - if (ret == 0) - return RTM_TIMEOUT; - - m_length = m_buffer[1U]; - m_offset = 2U; - - if (m_length == 0U) - m_state = SS_LENGTH2; - else - m_state = SS_TYPE; - } - - if (m_state == SS_LENGTH2) { - // Get the length of the frane, 2/2 - int ret = m_serial->read(m_buffer + 2U, 1U); - if (ret < 0) { - LogError("Error when reading from the modem"); - m_state = SS_START; - return RTM_ERROR; - } - - if (ret == 0) - return RTM_TIMEOUT; - - m_length = m_buffer[2U] + 255U; - m_offset = 3U; - m_state = SS_TYPE; - } - - if (m_state == SS_TYPE) { - // Get the frame type - int ret = m_serial->read(&m_type, 1U); - if (ret < 0) { - LogError("Error when reading from the modem"); - m_state = SS_START; - return RTM_ERROR; - } - - if (ret == 0) - return RTM_TIMEOUT; - - m_buffer[m_offset++] = m_type; - - m_state = SS_DATA; - } - - if (m_state == SS_DATA) { - while (m_offset < m_length) { - int ret = m_serial->read(m_buffer + m_offset, m_length - m_offset); - if (ret < 0) { - LogError("Error when reading from the modem"); - m_state = SS_START; - return RTM_ERROR; - } - - if (ret == 0) - return RTM_TIMEOUT; - - if (ret > 0) - m_offset += ret; - } - } - - // CUtils::dump(1U, "Received", m_buffer, m_length); - - m_offset = m_length > 255U ? 4U : 3U; - m_state = SS_START; - - return RTM_OK; -} - -HW_TYPE CModem::getHWType() const -{ - return m_hwType; -} - -unsigned char CModem::getMode() const -{ - return m_mode; -} - -bool CModem::setMode(unsigned char mode) -{ - assert(m_serial != NULL); - - unsigned char buffer[4U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 4U; - buffer[2U] = MMDVM_SET_MODE; - buffer[3U] = mode; - - // CUtils::dump(1U, "Written", buffer, 4U); - - return m_serial->write(buffer, 4U) == 4; -} - -bool CModem::sendCWId(const std::string& callsign) -{ - assert(m_serial != NULL); - - unsigned int length = callsign.length(); - if (length > 200U) - length = 200U; - - unsigned char buffer[205U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = length + 3U; - buffer[2U] = MMDVM_SEND_CWID; - - for (unsigned int i = 0U; i < length; i++) - buffer[i + 3U] = callsign.at(i); - - // CUtils::dump(1U, "Written", buffer, length + 3U); - - return m_serial->write(buffer, length + 3U) == int(length + 3U); -} - -bool CModem::writeDMRStart(bool tx) -{ - assert(m_serial != NULL); - - if (tx && m_tx) - return true; - if (!tx && !m_tx) - return true; - - unsigned char buffer[4U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 4U; - buffer[2U] = MMDVM_DMR_START; - buffer[3U] = tx ? 0x01U : 0x00U; - - // CUtils::dump(1U, "Written", buffer, 4U); - - return m_serial->write(buffer, 4U) == 4; -} - -bool CModem::writeDMRAbort(unsigned int slotNo) -{ - assert(m_serial != NULL); - - if (slotNo == 1U) - m_txDMRData1.clear(); - else - m_txDMRData2.clear(); - - unsigned char buffer[4U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 4U; - buffer[2U] = MMDVM_DMR_ABORT; - buffer[3U] = slotNo; - - // CUtils::dump(1U, "Written", buffer, 4U); - - return m_serial->write(buffer, 4U) == 4; -} - -bool CModem::writeDMRShortLC(const unsigned char* lc) -{ - assert(m_serial != NULL); - assert(lc != NULL); - - unsigned char buffer[12U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 12U; - buffer[2U] = MMDVM_DMR_SHORTLC; - buffer[3U] = lc[0U]; - buffer[4U] = lc[1U]; - buffer[5U] = lc[2U]; - buffer[6U] = lc[3U]; - buffer[7U] = lc[4U]; - buffer[8U] = lc[5U]; - buffer[9U] = lc[6U]; - buffer[10U] = lc[7U]; - buffer[11U] = lc[8U]; - - // CUtils::dump(1U, "Written", buffer, 12U); - - return m_serial->write(buffer, 12U) == 12; -} - -void CModem::setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch) -{ - m_fmCallsign = callsign; - m_fmCallsignSpeed = callsignSpeed; - m_fmCallsignFrequency = callsignFrequency; - m_fmCallsignTime = callsignTime; - m_fmCallsignHoldoff = callsignHoldoff; - m_fmCallsignHighLevel = callsignHighLevel; - m_fmCallsignLowLevel = callsignLowLevel; - m_fmCallsignAtStart = callsignAtStart; - m_fmCallsignAtEnd = callsignAtEnd; - m_fmCallsignAtLatch = callsignAtLatch; -} - -void CModem::setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel) -{ - m_fmRfAck = rfAck; - m_fmAckSpeed = ackSpeed; - m_fmAckFrequency = ackFrequency; - m_fmAckMinTime = ackMinTime; - m_fmAckDelay = ackDelay; - m_fmAckLevel = ackLevel; -} - -void CModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) -{ - m_fmTimeout = timeout; - m_fmTimeoutLevel = timeoutLevel; - - m_fmCtcssFrequency = ctcssFrequency; - m_fmCtcssHighThreshold = ctcssHighThreshold; - m_fmCtcssLowThreshold = ctcssLowThreshold; - m_fmCtcssLevel = ctcssLevel; - - m_fmKerchunkTime = kerchunkTime; - m_fmKerchunkTX = kerchunkTX; - - m_fmHangTime = hangTime; - - m_fmUseCOS = useCOS; - m_fmCOSInvert = cosInvert; - - m_fmRFAudioBoost = rfAudioBoost; - m_fmMaxDevLevel = maxDevLevel; -} - -void CModem::setFMExtParams(const std::string& ack, unsigned int audioBoost) -{ - m_fmExtAck = ack; - m_fmExtAudioBoost = audioBoost; - m_fmExtEnable = true; -} - -bool CModem::setFMCallsignParams() -{ - assert(m_serial != NULL); - - unsigned char buffer[80U]; - unsigned char len = 10U + m_fmCallsign.size(); - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = len; - buffer[2U] = MMDVM_FM_PARAMS1; - - buffer[3U] = m_fmCallsignSpeed; - buffer[4U] = m_fmCallsignFrequency / 10U; - buffer[5U] = m_fmCallsignTime; - buffer[6U] = m_fmCallsignHoldoff; - - buffer[7U] = (unsigned char)(m_fmCallsignHighLevel * 2.55F + 0.5F); - buffer[8U] = (unsigned char)(m_fmCallsignLowLevel * 2.55F + 0.5F); - - buffer[9U] = 0x00U; - if (m_fmCallsignAtStart) - buffer[9U] |= 0x01U; - if (m_fmCallsignAtEnd) - buffer[9U] |= 0x02U; - if (m_fmCallsignAtLatch) - buffer[9U] |= 0x04U; - - for (unsigned int i = 0U; i < m_fmCallsign.size(); i++) - buffer[10U + i] = m_fmCallsign.at(i); - - // CUtils::dump(1U, "Written", buffer, len); - - int ret = m_serial->write(buffer, len); - if (ret != len) - return false; - - unsigned int count = 0U; - RESP_TYPE_MMDVM resp; - do { - CThread::sleep(10U); - - resp = getResponse(); - if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { - count++; - if (count >= MAX_RESPONSES) { - LogError("The MMDVM is not responding to the SET_FM_PARAMS1 command"); - return false; - } - } - } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); - - // CUtils::dump(1U, "Response", m_buffer, m_length); - - if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { - LogError("Received a NAK to the SET_FM_PARAMS1 command from the modem"); - return false; - } - - return true; -} - -bool CModem::setFMAckParams() -{ - assert(m_serial != NULL); - - unsigned char buffer[80U]; - unsigned char len = 8U + m_fmRfAck.size(); - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = len; - buffer[2U] = MMDVM_FM_PARAMS2; - - buffer[3U] = m_fmAckSpeed; - buffer[4U] = m_fmAckFrequency / 10U; - buffer[5U] = m_fmAckMinTime; - buffer[6U] = m_fmAckDelay / 10U; - - buffer[7U] = (unsigned char)(m_fmAckLevel * 2.55F + 0.5F); - - for (unsigned int i = 0U; i < m_fmRfAck.size(); i++) - buffer[8U + i] = m_fmRfAck.at(i); - - // CUtils::dump(1U, "Written", buffer, len); - - int ret = m_serial->write(buffer, len); - if (ret != len) - return false; - - unsigned int count = 0U; - RESP_TYPE_MMDVM resp; - do { - CThread::sleep(10U); - - resp = getResponse(); - if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { - count++; - if (count >= MAX_RESPONSES) { - LogError("The MMDVM is not responding to the SET_FM_PARAMS2 command"); - return false; - } - } - } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); - - // CUtils::dump(1U, "Response", m_buffer, m_length); - - if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { - LogError("Received a NAK to the SET_FM_PARAMS2 command from the modem"); - return false; - } - - return true; -} - -bool CModem::setFMMiscParams() -{ - assert(m_serial != NULL); - - unsigned char buffer[20U]; - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 15U; - buffer[2U] = MMDVM_FM_PARAMS3; - - buffer[3U] = m_fmTimeout / 5U; - buffer[4U] = (unsigned char)(m_fmTimeoutLevel * 2.55F + 0.5F); - - buffer[5U] = (unsigned char)m_fmCtcssFrequency; - buffer[6U] = m_fmCtcssHighThreshold; - buffer[7U] = m_fmCtcssLowThreshold; - buffer[8U] = (unsigned char)(m_fmCtcssLevel * 2.55F + 0.5F); - - buffer[9U] = m_fmKerchunkTime; - buffer[10U] = m_fmHangTime; - - buffer[11U] = 0x00U; - if (m_fmUseCOS) - buffer[11U] |= 0x01U; - if (m_fmCOSInvert) - buffer[11U] |= 0x02U; - if (m_fmKerchunkTX) - buffer[11U] |= 0x04U; - - buffer[12U] = m_fmRFAudioBoost; - - buffer[13U] = (unsigned char)(m_fmMaxDevLevel * 2.55F + 0.5F); - - buffer[14U] = (unsigned char)(m_rxLevel * 2.55F + 0.5F); - - // CUtils::dump(1U, "Written", buffer, 15U); - - int ret = m_serial->write(buffer, 15U); - if (ret != 15) - return false; - - unsigned int count = 0U; - RESP_TYPE_MMDVM resp; - do { - CThread::sleep(10U); - - resp = getResponse(); - if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { - count++; - if (count >= MAX_RESPONSES) { - LogError("The MMDVM is not responding to the SET_FM_PARAMS3 command"); - return false; - } - } - } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); - - // CUtils::dump(1U, "Response", m_buffer, m_length); - - if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { - LogError("Received a NAK to the SET_FM_PARAMS3 command from the modem"); - return false; - } - - return true; -} - -bool CModem::setFMExtParams() -{ - assert(m_serial != NULL); - - unsigned char buffer[80U]; - unsigned char len = 7U + m_fmExtAck.size(); - - buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = len; - buffer[2U] = MMDVM_FM_PARAMS4; - - buffer[3U] = m_fmExtAudioBoost; - buffer[4U] = m_fmAckSpeed; - buffer[5U] = m_fmAckFrequency / 10U; - - buffer[6U] = (unsigned char)(m_fmAckLevel * 2.55F + 0.5F); - - for (unsigned int i = 0U; i < m_fmExtAck.size(); i++) - buffer[7U + i] = m_fmExtAck.at(i); - - // CUtils::dump(1U, "Written", buffer, len); - - int ret = m_serial->write(buffer, len); - if (ret != len) - return false; - - unsigned int count = 0U; - RESP_TYPE_MMDVM resp; - do { - CThread::sleep(10U); - - resp = getResponse(); - if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { - count++; - if (count >= MAX_RESPONSES) { - LogError("The MMDVM is not responding to the SET_FM_PARAMS4 command"); - return false; - } - } - } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); - - // CUtils::dump(1U, "Response", m_buffer, m_length); - - if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { - LogError("Received a NAK to the SET_FM_PARAMS4 command from the modem"); - return false; - } - - return true; -} - -void CModem::printDebug() +IModem::~IModem() { - if (m_buffer[2U] == MMDVM_DEBUG1) { - LogMessage("Debug: %.*s", m_length - m_offset - 0U, m_buffer + m_offset); - } else if (m_buffer[2U] == MMDVM_DEBUG2) { - short val1 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; - LogMessage("Debug: %.*s %d", m_length - m_offset - 2U, m_buffer + m_offset, val1); - } else if (m_buffer[2U] == MMDVM_DEBUG3) { - short val1 = (m_buffer[m_length - 4U] << 8) | m_buffer[m_length - 3U]; - short val2 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; - LogMessage("Debug: %.*s %d %d", m_length - m_offset - 4U, m_buffer + m_offset, val1, val2); - } else if (m_buffer[2U] == MMDVM_DEBUG4) { - short val1 = (m_buffer[m_length - 6U] << 8) | m_buffer[m_length - 5U]; - short val2 = (m_buffer[m_length - 4U] << 8) | m_buffer[m_length - 3U]; - short val3 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; - LogMessage("Debug: %.*s %d %d %d", m_length - m_offset - 6U, m_buffer + m_offset, val1, val2, val3); - } else if (m_buffer[2U] == MMDVM_DEBUG5) { - short val1 = (m_buffer[m_length - 8U] << 8) | m_buffer[m_length - 7U]; - short val2 = (m_buffer[m_length - 6U] << 8) | m_buffer[m_length - 5U]; - short val3 = (m_buffer[m_length - 4U] << 8) | m_buffer[m_length - 3U]; - short val4 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; - LogMessage("Debug: %.*s %d %d %d %d", m_length - m_offset - 8U, m_buffer + m_offset, val1, val2, val3, val4); - } -} - -CModem* CModem::createModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool trace, bool debug){ - if (port == "NullModem") - return new CNullModem(port, duplex, rxInvert, txInvert, pttInvert, txDelay, dmrDelay, trace, debug); - else - return new CModem(port, duplex, rxInvert, txInvert, pttInvert, txDelay, dmrDelay, trace, debug); } diff --git a/Modem.h b/Modem.h index 822d50f..c408f01 100644 --- a/Modem.h +++ b/Modem.h @@ -19,249 +19,99 @@ #ifndef MODEM_H #define MODEM_H -#include "SerialPort.h" -#include "RingBuffer.h" #include "Defines.h" -#include "Timer.h" #include -enum RESP_TYPE_MMDVM { - RTM_OK, - RTM_TIMEOUT, - RTM_ERROR -}; - -enum SERIAL_STATE { - SS_START, - SS_LENGTH1, - SS_LENGTH2, - SS_TYPE, - SS_DATA -}; - -class CModem { +class IModem { public: - CModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool trace, bool debug); - virtual ~CModem(); + virtual ~IModem() = 0; - virtual void setSerialParams(const std::string& protocol, unsigned int address, unsigned int speed); - virtual void setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency); - virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled, bool ax25Enabled); - virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel, float ax25TXLevel); - virtual void setDMRParams(unsigned int colorCode); - virtual void setYSFParams(bool loDev, unsigned int txHang); - virtual void setP25Params(unsigned int txHang); - virtual void setNXDNParams(unsigned int txHang); - virtual void setAX25Params(int rxTwist, unsigned int txDelay); - virtual void setTransparentDataParams(unsigned int sendFrameType); + virtual void setSerialParams(const std::string& protocol, unsigned int address, unsigned int speed) = 0; + virtual void setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) = 0; + virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled, bool ax25Enabled) = 0; + virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel, float ax25TXLevel) = 0; + virtual void setDMRParams(unsigned int colorCode) = 0; + virtual void setYSFParams(bool loDev, unsigned int txHang) = 0; + virtual void setP25Params(unsigned int txHang) = 0; + virtual void setNXDNParams(unsigned int txHang) = 0; + virtual void setAX25Params(int rxTwist, unsigned int txDelay) = 0; + virtual void setTransparentDataParams(unsigned int sendFrameType) = 0; - virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch); - virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel); - virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel); - virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost); + virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch) = 0; + virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel) = 0; + virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) = 0; + virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost) = 0; - virtual bool open(); + virtual bool open() = 0; - virtual unsigned int readDStarData(unsigned char* data); - virtual unsigned int readDMRData1(unsigned char* data); - virtual unsigned int readDMRData2(unsigned char* data); - virtual unsigned int readYSFData(unsigned char* data); - virtual unsigned int readP25Data(unsigned char* data); - virtual unsigned int readNXDNData(unsigned char* data); - virtual unsigned int readFMData(unsigned char* data); - virtual unsigned int readAX25Data(unsigned char* data); - virtual unsigned int readTransparentData(unsigned char* data); + virtual unsigned int readDStarData(unsigned char* data) = 0; + virtual unsigned int readDMRData1(unsigned char* data) = 0; + virtual unsigned int readDMRData2(unsigned char* data) = 0; + virtual unsigned int readYSFData(unsigned char* data) = 0; + virtual unsigned int readP25Data(unsigned char* data) = 0; + virtual unsigned int readNXDNData(unsigned char* data) = 0; + virtual unsigned int readFMData(unsigned char* data) = 0; + virtual unsigned int readAX25Data(unsigned char* data) = 0; + virtual unsigned int readTransparentData(unsigned char* data) = 0; - virtual unsigned int readSerial(unsigned char* data, unsigned int length); + virtual unsigned int readSerial(unsigned char* data, unsigned int length) = 0; - virtual bool hasDStarSpace() const; - virtual bool hasDMRSpace1() const; - virtual bool hasDMRSpace2() const; - virtual bool hasYSFSpace() const; - virtual bool hasP25Space() const; - virtual bool hasNXDNSpace() const; - virtual bool hasPOCSAGSpace() const; - virtual unsigned int getFMSpace() const; - virtual bool hasAX25Space() const; + virtual bool hasDStarSpace() const = 0; + virtual bool hasDMRSpace1() const = 0; + virtual bool hasDMRSpace2() const = 0; + virtual bool hasYSFSpace() const = 0; + virtual bool hasP25Space() const = 0; + virtual bool hasNXDNSpace() const = 0; + virtual bool hasPOCSAGSpace() const = 0; + virtual unsigned int getFMSpace() const = 0; + virtual bool hasAX25Space() const = 0; - virtual bool hasTX() const; - virtual bool hasCD() const; + virtual bool hasTX() const = 0; + virtual bool hasCD() const = 0; - virtual bool hasLockout() const; - virtual bool hasError() const; + virtual bool hasLockout() const = 0; + virtual bool hasError() const = 0; - virtual bool writeConfig(); - virtual bool writeDStarData(const unsigned char* data, unsigned int length); - virtual bool writeDMRData1(const unsigned char* data, unsigned int length); - virtual bool writeDMRData2(const unsigned char* data, unsigned int length); - virtual bool writeYSFData(const unsigned char* data, unsigned int length); - virtual bool writeP25Data(const unsigned char* data, unsigned int length); - virtual bool writeNXDNData(const unsigned char* data, unsigned int length); - virtual bool writePOCSAGData(const unsigned char* data, unsigned int length); - virtual bool writeFMData(const unsigned char* data, unsigned int length); - virtual bool writeAX25Data(const unsigned char* data, unsigned int length); + virtual bool writeConfig() = 0; + virtual bool writeDStarData(const unsigned char* data, unsigned int length) = 0; + virtual bool writeDMRData1(const unsigned char* data, unsigned int length) = 0; + virtual bool writeDMRData2(const unsigned char* data, unsigned int length) = 0; + virtual bool writeYSFData(const unsigned char* data, unsigned int length) = 0; + virtual bool writeP25Data(const unsigned char* data, unsigned int length) = 0; + virtual bool writeNXDNData(const unsigned char* data, unsigned int length) = 0; + virtual bool writePOCSAGData(const unsigned char* data, unsigned int length) = 0; + virtual bool writeFMData(const unsigned char* data, unsigned int length) = 0; + virtual bool writeAX25Data(const unsigned char* data, unsigned int length) = 0; - virtual bool writeTransparentData(const unsigned char* data, unsigned int length); + virtual bool writeTransparentData(const unsigned char* data, unsigned int length) = 0; - virtual bool writeDStarInfo(const char* my1, const char* my2, const char* your, const char* type, const char* reflector); - virtual bool writeDMRInfo(unsigned int slotNo, const std::string& src, bool group, const std::string& dst, const char* type); - virtual bool writeYSFInfo(const char* source, const char* dest, const char* type, const char* origin); - virtual bool writeP25Info(const char* source, bool group, unsigned int dest, const char* type); - virtual bool writeNXDNInfo(const char* source, bool group, unsigned int dest, const char* type); - virtual bool writePOCSAGInfo(unsigned int ric, const std::string& message); - virtual bool writeIPInfo(const std::string& address); + virtual bool writeDStarInfo(const char* my1, const char* my2, const char* your, const char* type, const char* reflector) = 0; + virtual bool writeDMRInfo(unsigned int slotNo, const std::string& src, bool group, const std::string& dst, const char* type) = 0; + virtual bool writeYSFInfo(const char* source, const char* dest, const char* type, const char* origin) = 0; + virtual bool writeP25Info(const char* source, bool group, unsigned int dest, const char* type) = 0; + virtual bool writeNXDNInfo(const char* source, bool group, unsigned int dest, const char* type) = 0; + virtual bool writePOCSAGInfo(unsigned int ric, const std::string& message) = 0; + virtual bool writeIPInfo(const std::string& address) = 0; - virtual bool writeDMRStart(bool tx); - virtual bool writeDMRShortLC(const unsigned char* lc); - virtual bool writeDMRAbort(unsigned int slotNo); + virtual bool writeDMRStart(bool tx) = 0; + virtual bool writeDMRShortLC(const unsigned char* lc) = 0; + virtual bool writeDMRAbort(unsigned int slotNo) = 0; - virtual bool writeSerial(const unsigned char* data, unsigned int length); + virtual bool writeSerial(const unsigned char* data, unsigned int length) = 0; - virtual unsigned char getMode() const; - virtual bool setMode(unsigned char mode); + virtual unsigned char getMode() const = 0; + virtual bool setMode(unsigned char mode) = 0; - virtual bool sendCWId(const std::string& callsign); + virtual bool sendCWId(const std::string& callsign) = 0; - virtual HW_TYPE getHWType() const; + virtual HW_TYPE getHWType() const = 0; - virtual void clock(unsigned int ms); + virtual void clock(unsigned int ms) = 0; - virtual void close(); - - static CModem* createModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool trace, bool debug); + virtual void close() = 0; private: - std::string m_port; - unsigned int m_dmrColorCode; - bool m_ysfLoDev; - unsigned int m_ysfTXHang; - unsigned int m_p25TXHang; - unsigned int m_nxdnTXHang; - bool m_duplex; - bool m_rxInvert; - bool m_txInvert; - bool m_pttInvert; - unsigned int m_txDelay; - unsigned int m_dmrDelay; - float m_rxLevel; - float m_cwIdTXLevel; - float m_dstarTXLevel; - float m_dmrTXLevel; - float m_ysfTXLevel; - float m_p25TXLevel; - float m_nxdnTXLevel; - float m_pocsagTXLevel; - float m_fmTXLevel; - float m_ax25TXLevel; - float m_rfLevel; - bool m_trace; - bool m_debug; - unsigned int m_rxFrequency; - unsigned int m_txFrequency; - unsigned int m_pocsagFrequency; - bool m_dstarEnabled; - bool m_dmrEnabled; - bool m_ysfEnabled; - bool m_p25Enabled; - bool m_nxdnEnabled; - bool m_pocsagEnabled; - bool m_fmEnabled; - bool m_ax25Enabled; - int m_rxDCOffset; - int m_txDCOffset; - ISerialPort* m_serial; - unsigned char* m_buffer; - unsigned int m_length; - unsigned int m_offset; - SERIAL_STATE m_state; - unsigned char m_type; - CRingBuffer m_rxDStarData; - CRingBuffer m_txDStarData; - CRingBuffer m_rxDMRData1; - CRingBuffer m_rxDMRData2; - CRingBuffer m_txDMRData1; - CRingBuffer m_txDMRData2; - CRingBuffer m_rxYSFData; - CRingBuffer m_txYSFData; - CRingBuffer m_rxP25Data; - CRingBuffer m_txP25Data; - CRingBuffer m_rxNXDNData; - CRingBuffer m_txNXDNData; - CRingBuffer m_txPOCSAGData; - CRingBuffer m_rxFMData; - CRingBuffer m_txFMData; - CRingBuffer m_rxAX25Data; - CRingBuffer m_txAX25Data; - CRingBuffer m_rxTransparentData; - CRingBuffer m_txTransparentData; - unsigned int m_sendTransparentDataFrameType; - CTimer m_statusTimer; - CTimer m_inactivityTimer; - CTimer m_playoutTimer; - unsigned int m_dstarSpace; - unsigned int m_dmrSpace1; - unsigned int m_dmrSpace2; - unsigned int m_ysfSpace; - unsigned int m_p25Space; - unsigned int m_nxdnSpace; - unsigned int m_pocsagSpace; - unsigned int m_fmSpace; - unsigned int m_ax25Space; - bool m_tx; - bool m_cd; - bool m_lockout; - bool m_error; - unsigned char m_mode; - HW_TYPE m_hwType; - int m_ax25RXTwist; - unsigned int m_ax25TXDelay; - - std::string m_fmCallsign; - unsigned int m_fmCallsignSpeed; - unsigned int m_fmCallsignFrequency; - unsigned int m_fmCallsignTime; - unsigned int m_fmCallsignHoldoff; - float m_fmCallsignHighLevel; - float m_fmCallsignLowLevel; - bool m_fmCallsignAtStart; - bool m_fmCallsignAtEnd; - bool m_fmCallsignAtLatch; - std::string m_fmRfAck; - std::string m_fmExtAck; - unsigned int m_fmAckSpeed; - unsigned int m_fmAckFrequency; - unsigned int m_fmAckMinTime; - unsigned int m_fmAckDelay; - float m_fmAckLevel; - unsigned int m_fmTimeout; - float m_fmTimeoutLevel; - float m_fmCtcssFrequency; - unsigned int m_fmCtcssHighThreshold; - unsigned int m_fmCtcssLowThreshold; - float m_fmCtcssLevel; - unsigned int m_fmKerchunkTime; - bool m_fmKerchunkTX; - unsigned int m_fmHangTime; - bool m_fmUseCOS; - bool m_fmCOSInvert; - unsigned int m_fmRFAudioBoost; - unsigned int m_fmExtAudioBoost; - float m_fmMaxDevLevel; - bool m_fmExtEnable; - - bool readVersion(); - bool readStatus(); - bool setConfig(); - bool setFrequency(); - bool setFMCallsignParams(); - bool setFMAckParams(); - bool setFMMiscParams(); - bool setFMExtParams(); - - void printDebug(); - - RESP_TYPE_MMDVM getResponse(); }; #endif diff --git a/ModemSerialPort.cpp b/ModemSerialPort.cpp index 51ba01d..c34cb3d 100644 --- a/ModemSerialPort.cpp +++ b/ModemSerialPort.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2016 by Jonathan Naylor G4KLX +* Copyright (C) 2016,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,22 +21,22 @@ #include #include -CModemSerialPort::CModemSerialPort(CModem* modem) : +IModemSerialPort::IModemSerialPort(IModem* modem) : m_modem(modem) { assert(modem != NULL); } -CModemSerialPort::~CModemSerialPort() +IModemSerialPort::~IModemSerialPort() { } -bool CModemSerialPort::open() +bool IModemSerialPort::open() { return true; } -int CModemSerialPort::write(const unsigned char* data, unsigned int length) +int IModemSerialPort::write(const unsigned char* data, unsigned int length) { assert(data != NULL); assert(length > 0U); @@ -46,7 +46,7 @@ int CModemSerialPort::write(const unsigned char* data, unsigned int length) return ret ? int(length) : -1; } -int CModemSerialPort::read(unsigned char* data, unsigned int length) +int IModemSerialPort::read(unsigned char* data, unsigned int length) { assert(data != NULL); assert(length > 0U); @@ -54,6 +54,6 @@ int CModemSerialPort::read(unsigned char* data, unsigned int length) return m_modem->readSerial(data, length); } -void CModemSerialPort::close() +void IModemSerialPort::close() { } diff --git a/ModemSerialPort.h b/ModemSerialPort.h index d6761ce..6de56a7 100644 --- a/ModemSerialPort.h +++ b/ModemSerialPort.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2016 by Jonathan Naylor G4KLX +* Copyright (C) 2016,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,10 +22,10 @@ #include "SerialPort.h" #include "Modem.h" -class CModemSerialPort : public ISerialPort { +class IModemSerialPort : public ISerialPort { public: - CModemSerialPort(CModem* modem); - virtual ~CModemSerialPort(); + IModemSerialPort(IModem* modem); + virtual ~IModemSerialPort(); virtual bool open(); @@ -36,7 +36,7 @@ public: virtual void close(); private: - CModem* m_modem; + IModem* m_modem; }; #endif diff --git a/NullModem.cpp b/NullModem.cpp index 8539f1b..40e3fdd 100644 --- a/NullModem.cpp +++ b/NullModem.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2018 by Jonathan Naylor G4KLX + * Copyright (C) 2011-2018,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,9 +19,7 @@ #include "NullModem.h" #include "Log.h" -CNullModem::CNullModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool trace, bool debug) : -CModem(port, duplex,rxInvert, txInvert,pttInvert,txDelay, dmrDelay, trace, debug), -m_hwType(HWT_MMDVM) +CNullModem::CNullModem() { } @@ -29,7 +27,9 @@ CNullModem::~CNullModem() { } -bool CNullModem::open(){ +bool CNullModem::open() +{ ::LogMessage("Opening the MMDVM Null Modem"); + return true; -} \ No newline at end of file +} diff --git a/NullModem.h b/NullModem.h index 49dd3ec..12ae460 100644 --- a/NullModem.h +++ b/NullModem.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2018 by Jonathan Naylor G4KLX + * Copyright (C) 2011-2018,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,16 +24,15 @@ #include - -class CNullModem : public CModem { +class CNullModem : public IModem { public: - CNullModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool trace, bool debug); + CNullModem(); virtual ~CNullModem(); - virtual void setSerialParams(const std::string& protocol, unsigned int address) {}; + virtual void setSerialParams(const std::string& protocol, unsigned int address, unsigned int speed) {}; virtual void setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) {}; - virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled) {}; - virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel) {}; + virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled, bool ax25Enabled) {}; + virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel, float ax25TXLevel) {}; virtual void setDMRParams(unsigned int colorCode) {}; virtual void setYSFParams(bool loDev, unsigned int txHang) {}; virtual void setP25Params(unsigned int txHang) {}; @@ -58,7 +57,7 @@ public: virtual unsigned int readAX25Data(unsigned char* data) { return 0U; }; virtual unsigned int readTransparentData(unsigned char* data) { return 0U; }; - virtual unsigned int readSerial(unsigned char* data, unsigned int length){return 0;}; + virtual unsigned int readSerial(unsigned char* data, unsigned int length) { return 0; }; virtual bool hasDStarSpace()const { return true; }; virtual bool hasDMRSpace1() const { return true; }; @@ -79,41 +78,42 @@ public: virtual bool writeDStarData(const unsigned char* data, unsigned int length) { return true; }; virtual bool writeDMRData1(const unsigned char* data, unsigned int length) { return true; }; virtual bool writeDMRData2(const unsigned char* data, unsigned int length) { return true; }; - virtual bool writeYSFData(const unsigned char* data, unsigned int length) {return true; }; + virtual bool writeYSFData(const unsigned char* data, unsigned int length) { return true; }; virtual bool writeP25Data(const unsigned char* data, unsigned int length) { return true; }; virtual bool writeNXDNData(const unsigned char* data, unsigned int length) { return true; }; virtual bool writePOCSAGData(const unsigned char* data, unsigned int length) { return true; }; virtual bool writeFMData(const unsigned char* data, unsigned int length) { return true; }; virtual bool writeAX25Data(const unsigned char* data, unsigned int length) { return true; }; - virtual bool writeTransparentData(const unsigned char* data, unsigned int length){return true;}; + virtual bool writeTransparentData(const unsigned char* data, unsigned int length) { return true; }; - virtual bool writeDStarInfo(const char* my1, const char* my2, const char* your, const char* type, const char* reflector){return true;}; - virtual bool writeDMRInfo(unsigned int slotNo, const std::string& src, bool group, const std::string& dst, const char* type){return true;}; - virtual bool writeYSFInfo(const char* source, const char* dest, const char* type, const char* origin){return true;}; - virtual bool writeP25Info(const char* source, bool group, unsigned int dest, const char* type){return true;}; - virtual bool writeNXDNInfo(const char* source, bool group, unsigned int dest, const char* type){return true;}; - virtual bool writePOCSAGInfo(unsigned int ric, const std::string& message){return true;}; - virtual bool writeIPInfo(const std::string& address){return true;}; + virtual bool writeConfig() { return true; }; + virtual bool writeDStarInfo(const char* my1, const char* my2, const char* your, const char* type, const char* reflector) { return true; }; + virtual bool writeDMRInfo(unsigned int slotNo, const std::string& src, bool group, const std::string& dst, const char* type) { return true; }; + virtual bool writeYSFInfo(const char* source, const char* dest, const char* type, const char* origin) { return true; }; + virtual bool writeP25Info(const char* source, bool group, unsigned int dest, const char* type) { return true; }; + virtual bool writeNXDNInfo(const char* source, bool group, unsigned int dest, const char* type) { return true; }; + virtual bool writePOCSAGInfo(unsigned int ric, const std::string& message) { return true; }; + virtual bool writeIPInfo(const std::string& address) { return true; }; - virtual bool writeDMRStart(bool tx){return true;}; - virtual bool writeDMRShortLC(const unsigned char* lc){return true;}; - virtual bool writeDMRAbort(unsigned int slotNo){return true;}; + virtual bool writeDMRStart(bool tx) { return true; }; + virtual bool writeDMRShortLC(const unsigned char* lc) { return true; }; + virtual bool writeDMRAbort(unsigned int slotNo) { return true; }; - virtual bool writeSerial(const unsigned char* data, unsigned int length){return true;}; + virtual bool writeSerial(const unsigned char* data, unsigned int length) { return true; }; - virtual bool setMode(unsigned char mode){return true;}; + virtual unsigned char getMode() const { return MODE_IDLE; }; + virtual bool setMode(unsigned char mode) { return true; }; - virtual bool sendCWId(const std::string& callsign){return true;}; + virtual bool sendCWId(const std::string& callsign) { return true; }; - virtual HW_TYPE getHWType() const {return m_hwType;}; + virtual HW_TYPE getHWType() const { return HWT_MMDVM; }; - virtual void clock(unsigned int ms){}; + virtual void clock(unsigned int ms) {}; - virtual void close(){}; + virtual void close() {}; private: - HW_TYPE m_hwType; }; #endif diff --git a/SerialModem.cpp b/SerialModem.cpp new file mode 100644 index 0000000..10ddefa --- /dev/null +++ b/SerialModem.cpp @@ -0,0 +1,2465 @@ +/* + * Copyright (C) 2011-2018,2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "SerialController.h" +#if defined(__linux__) +#include "I2CController.h" +#endif +#include "DStarDefines.h" +#include "DMRDefines.h" +#include "YSFDefines.h" +#include "P25Defines.h" +#include "NXDNDefines.h" +#include "AX25Defines.h" +#include "POCSAGDefines.h" +#include "Thread.h" +#include "SerialModem.h" +#include "NullModem.h" +#include "Utils.h" +#include "Log.h" + +#include +#include +#include +#include +#include + +#if defined(_WIN32) || defined(_WIN64) +#include +#else +#include +#endif + +const unsigned char MMDVM_FRAME_START = 0xE0U; + +const unsigned char MMDVM_GET_VERSION = 0x00U; +const unsigned char MMDVM_GET_STATUS = 0x01U; +const unsigned char MMDVM_SET_CONFIG = 0x02U; +const unsigned char MMDVM_SET_MODE = 0x03U; +const unsigned char MMDVM_SET_FREQ = 0x04U; + +const unsigned char MMDVM_SEND_CWID = 0x0AU; + +const unsigned char MMDVM_DSTAR_HEADER = 0x10U; +const unsigned char MMDVM_DSTAR_DATA = 0x11U; +const unsigned char MMDVM_DSTAR_LOST = 0x12U; +const unsigned char MMDVM_DSTAR_EOT = 0x13U; + +const unsigned char MMDVM_DMR_DATA1 = 0x18U; +const unsigned char MMDVM_DMR_LOST1 = 0x19U; +const unsigned char MMDVM_DMR_DATA2 = 0x1AU; +const unsigned char MMDVM_DMR_LOST2 = 0x1BU; +const unsigned char MMDVM_DMR_SHORTLC = 0x1CU; +const unsigned char MMDVM_DMR_START = 0x1DU; +const unsigned char MMDVM_DMR_ABORT = 0x1EU; + +const unsigned char MMDVM_YSF_DATA = 0x20U; +const unsigned char MMDVM_YSF_LOST = 0x21U; + +const unsigned char MMDVM_P25_HDR = 0x30U; +const unsigned char MMDVM_P25_LDU = 0x31U; +const unsigned char MMDVM_P25_LOST = 0x32U; + +const unsigned char MMDVM_NXDN_DATA = 0x40U; +const unsigned char MMDVM_NXDN_LOST = 0x41U; + +const unsigned char MMDVM_POCSAG_DATA = 0x50U; + +const unsigned char MMDVM_AX25_DATA = 0x55U; + +const unsigned char MMDVM_FM_PARAMS1 = 0x60U; +const unsigned char MMDVM_FM_PARAMS2 = 0x61U; +const unsigned char MMDVM_FM_PARAMS3 = 0x62U; +const unsigned char MMDVM_FM_PARAMS4 = 0x63U; +const unsigned char MMDVM_FM_DATA = 0x65U; +const unsigned char MMDVM_FM_CONTROL = 0x66U; +const unsigned char MMDVM_FM_EOT = 0x67U; + +const unsigned char MMDVM_ACK = 0x70U; +const unsigned char MMDVM_NAK = 0x7FU; + +const unsigned char MMDVM_SERIAL = 0x80U; + +const unsigned char MMDVM_TRANSPARENT = 0x90U; +const unsigned char MMDVM_QSO_INFO = 0x91U; + +const unsigned char MMDVM_DEBUG1 = 0xF1U; +const unsigned char MMDVM_DEBUG2 = 0xF2U; +const unsigned char MMDVM_DEBUG3 = 0xF3U; +const unsigned char MMDVM_DEBUG4 = 0xF4U; +const unsigned char MMDVM_DEBUG5 = 0xF5U; + +const unsigned int MAX_RESPONSES = 30U; + +const unsigned int BUFFER_LENGTH = 2000U; + + +CSerialModem::CSerialModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool trace, bool debug) : +m_port(port), +m_dmrColorCode(0U), +m_ysfLoDev(false), +m_ysfTXHang(4U), +m_p25TXHang(5U), +m_nxdnTXHang(5U), +m_duplex(duplex), +m_rxInvert(rxInvert), +m_txInvert(txInvert), +m_pttInvert(pttInvert), +m_txDelay(txDelay), +m_dmrDelay(dmrDelay), +m_rxLevel(0.0F), +m_cwIdTXLevel(0.0F), +m_dstarTXLevel(0.0F), +m_dmrTXLevel(0.0F), +m_ysfTXLevel(0.0F), +m_p25TXLevel(0.0F), +m_nxdnTXLevel(0.0F), +m_pocsagTXLevel(0.0F), +m_fmTXLevel(0.0F), +m_ax25TXLevel(0.0F), +m_rfLevel(0.0F), +m_trace(trace), +m_debug(debug), +m_rxFrequency(0U), +m_txFrequency(0U), +m_pocsagFrequency(0U), +m_dstarEnabled(false), +m_dmrEnabled(false), +m_ysfEnabled(false), +m_p25Enabled(false), +m_nxdnEnabled(false), +m_pocsagEnabled(false), +m_fmEnabled(false), +m_ax25Enabled(false), +m_rxDCOffset(0), +m_txDCOffset(0), +m_serial(NULL), +m_buffer(NULL), +m_length(0U), +m_offset(0U), +m_state(SS_START), +m_type(0U), +m_rxDStarData(1000U, "Modem RX D-Star"), +m_txDStarData(1000U, "Modem TX D-Star"), +m_rxDMRData1(1000U, "Modem RX DMR1"), +m_rxDMRData2(1000U, "Modem RX DMR2"), +m_txDMRData1(1000U, "Modem TX DMR1"), +m_txDMRData2(1000U, "Modem TX DMR2"), +m_rxYSFData(1000U, "Modem RX YSF"), +m_txYSFData(1000U, "Modem TX YSF"), +m_rxP25Data(1000U, "Modem RX P25"), +m_txP25Data(1000U, "Modem TX P25"), +m_rxNXDNData(1000U, "Modem RX NXDN"), +m_txNXDNData(1000U, "Modem TX NXDN"), +m_txPOCSAGData(1000U, "Modem TX POCSAG"), +m_rxFMData(5000U, "Modem RX FM"), +m_txFMData(5000U, "Modem TX FM"), +m_rxAX25Data(1000U, "Modem RX AX.25"), +m_txAX25Data(1000U, "Modem TX AX.25"), +m_rxTransparentData(1000U, "Modem RX Transparent"), +m_txTransparentData(1000U, "Modem TX Transparent"), +m_sendTransparentDataFrameType(0U), +m_statusTimer(1000U, 0U, 250U), +m_inactivityTimer(1000U, 2U), +m_playoutTimer(1000U, 0U, 10U), +m_dstarSpace(0U), +m_dmrSpace1(0U), +m_dmrSpace2(0U), +m_ysfSpace(0U), +m_p25Space(0U), +m_nxdnSpace(0U), +m_pocsagSpace(0U), +m_fmSpace(0U), +m_ax25Space(0U), +m_tx(false), +m_cd(false), +m_lockout(false), +m_error(false), +m_mode(MODE_IDLE), +m_hwType(HWT_UNKNOWN), +m_ax25RXTwist(0), +m_ax25TXDelay(300U), +m_fmCallsign(), +m_fmCallsignSpeed(20U), +m_fmCallsignFrequency(1000U), +m_fmCallsignTime(600U), +m_fmCallsignHoldoff(0U), +m_fmCallsignHighLevel(35.0F), +m_fmCallsignLowLevel(15.0F), +m_fmCallsignAtStart(true), +m_fmCallsignAtEnd(true), +m_fmCallsignAtLatch(true), +m_fmRfAck("K"), +m_fmExtAck("N"), +m_fmAckSpeed(20U), +m_fmAckFrequency(1750U), +m_fmAckMinTime(4U), +m_fmAckDelay(1000U), +m_fmAckLevel(80.0F), +m_fmTimeout(120U), +m_fmTimeoutLevel(80.0F), +m_fmCtcssFrequency(88.4F), +m_fmCtcssHighThreshold(30U), +m_fmCtcssLowThreshold(20U), +m_fmCtcssLevel(10.0F), +m_fmKerchunkTime(0U), +m_fmKerchunkTX(true), +m_fmHangTime(5U), +m_fmUseCOS(true), +m_fmCOSInvert(false), +m_fmRFAudioBoost(1U), +m_fmExtAudioBoost(1U), +m_fmMaxDevLevel(90.0F), +m_fmExtEnable(false) +{ + m_buffer = new unsigned char[BUFFER_LENGTH]; + + assert(!port.empty()); +} + +CSerialModem::~CSerialModem() +{ + delete m_serial; + delete[] m_buffer; +} + +void CSerialModem::setSerialParams(const std::string& protocol, unsigned int address, unsigned int speed) +{ + // Create the serial controller instance according the protocol specified in conf. +#if defined(__linux__) + if (protocol == "i2c") + m_serial = new CI2CController(m_port, address); + else +#endif + m_serial = new CSerialController(m_port, speed, true); +} + +void CSerialModem::setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) +{ + m_rxFrequency = rxFrequency + rxOffset; + m_txFrequency = txFrequency + txOffset; + m_txDCOffset = txDCOffset; + m_rxDCOffset = rxDCOffset; + m_rfLevel = rfLevel; + m_pocsagFrequency = pocsagFrequency + txOffset; +} + +void CSerialModem::setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled, bool ax25Enabled) +{ + m_dstarEnabled = dstarEnabled; + m_dmrEnabled = dmrEnabled; + m_ysfEnabled = ysfEnabled; + m_p25Enabled = p25Enabled; + m_nxdnEnabled = nxdnEnabled; + m_pocsagEnabled = pocsagEnabled; + m_fmEnabled = fmEnabled; + m_ax25Enabled = ax25Enabled; +} + +void CSerialModem::setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagTXLevel, float fmTXLevel, float ax25TXLevel) +{ + m_rxLevel = rxLevel; + m_cwIdTXLevel = cwIdTXLevel; + m_dstarTXLevel = dstarTXLevel; + m_dmrTXLevel = dmrTXLevel; + m_ysfTXLevel = ysfTXLevel; + m_p25TXLevel = p25TXLevel; + m_nxdnTXLevel = nxdnTXLevel; + m_pocsagTXLevel = pocsagTXLevel; + m_fmTXLevel = fmTXLevel; + m_ax25TXLevel = ax25TXLevel; +} + +void CSerialModem::setDMRParams(unsigned int colorCode) +{ + assert(colorCode < 16U); + + m_dmrColorCode = colorCode; +} + +void CSerialModem::setYSFParams(bool loDev, unsigned int txHang) +{ + m_ysfLoDev = loDev; + m_ysfTXHang = txHang; +} + +void CSerialModem::setP25Params(unsigned int txHang) +{ + m_p25TXHang = txHang; +} + +void CSerialModem::setNXDNParams(unsigned int txHang) +{ + m_nxdnTXHang = txHang; +} + +void CSerialModem::setAX25Params(int rxTwist, unsigned int txDelay) +{ + m_ax25RXTwist = rxTwist; + m_ax25TXDelay = txDelay; +} + +void CSerialModem::setTransparentDataParams(unsigned int sendFrameType) +{ + m_sendTransparentDataFrameType = sendFrameType; +} + +bool CSerialModem::open() +{ + ::LogMessage("Opening the MMDVM"); + + bool ret = m_serial->open(); + if (!ret) + return false; + + ret = readVersion(); + if (!ret) { + m_serial->close(); + delete m_serial; + m_serial = NULL; + return false; + } else { + /* Stopping the inactivity timer here when a firmware version has been + successfuly read prevents the death spiral of "no reply from modem..." */ + m_inactivityTimer.stop(); + } + + ret = setFrequency(); + if (!ret) { + m_serial->close(); + delete m_serial; + m_serial = NULL; + return false; + } + + ret = setConfig(); + if (!ret) { + m_serial->close(); + delete m_serial; + m_serial = NULL; + return false; + } + + if (m_fmEnabled && m_duplex) { + ret = setFMCallsignParams(); + if (!ret) { + m_serial->close(); + delete m_serial; + m_serial = NULL; + return false; + } + + ret = setFMAckParams(); + if (!ret) { + m_serial->close(); + delete m_serial; + m_serial = NULL; + return false; + } + + ret = setFMMiscParams(); + if (!ret) { + m_serial->close(); + delete m_serial; + m_serial = NULL; + return false; + } + + if (m_fmExtEnable) { + ret = setFMExtParams(); + if (!ret) { + m_serial->close(); + delete m_serial; + m_serial = NULL; + return false; + } + } + } + + m_statusTimer.start(); + + m_error = false; + m_offset = 0U; + + return true; +} + +void CSerialModem::clock(unsigned int ms) +{ + assert(m_serial != NULL); + + // Poll the modem status every 250ms + m_statusTimer.clock(ms); + if (m_statusTimer.hasExpired()) { + readStatus(); + m_statusTimer.start(); + } + + m_inactivityTimer.clock(ms); + if (m_inactivityTimer.hasExpired()) { + LogError("No reply from the modem for some time, resetting it"); + m_error = true; + close(); + + CThread::sleep(2000U); // 2s + while (!open()) + CThread::sleep(5000U); // 5s + } + + RESP_TYPE_MMDVM type = getResponse(); + + if (type == RTM_TIMEOUT) { + // Nothing to do + } else if (type == RTM_ERROR) { + // Nothing to do + } else { + // type == RTM_OK + switch (m_type) { + case MMDVM_DSTAR_HEADER: { + if (m_trace) + CUtils::dump(1U, "RX D-Star Header", m_buffer, m_length); + + unsigned char data = m_length - m_offset + 1U; + m_rxDStarData.addData(&data, 1U); + + data = TAG_HEADER; + m_rxDStarData.addData(&data, 1U); + + m_rxDStarData.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_DSTAR_DATA: { + if (m_trace) + CUtils::dump(1U, "RX D-Star Data", m_buffer, m_length); + + unsigned char data = m_length - m_offset + 1U; + m_rxDStarData.addData(&data, 1U); + + data = TAG_DATA; + m_rxDStarData.addData(&data, 1U); + + m_rxDStarData.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_DSTAR_LOST: { + if (m_trace) + CUtils::dump(1U, "RX D-Star Lost", m_buffer, m_length); + + unsigned char data = 1U; + m_rxDStarData.addData(&data, 1U); + + data = TAG_LOST; + m_rxDStarData.addData(&data, 1U); + } + break; + + case MMDVM_DSTAR_EOT: { + if (m_trace) + CUtils::dump(1U, "RX D-Star EOT", m_buffer, m_length); + + unsigned char data = 1U; + m_rxDStarData.addData(&data, 1U); + + data = TAG_EOT; + m_rxDStarData.addData(&data, 1U); + } + break; + + case MMDVM_DMR_DATA1: { + if (m_trace) + CUtils::dump(1U, "RX DMR Data 1", m_buffer, m_length); + + unsigned char data = m_length - m_offset + 1U; + m_rxDMRData1.addData(&data, 1U); + + if (m_buffer[3U] == (DMR_SYNC_DATA | DT_TERMINATOR_WITH_LC)) + data = TAG_EOT; + else + data = TAG_DATA; + m_rxDMRData1.addData(&data, 1U); + + m_rxDMRData1.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_DMR_DATA2: { + if (m_trace) + CUtils::dump(1U, "RX DMR Data 2", m_buffer, m_length); + + unsigned char data = m_length - m_offset + 1U; + m_rxDMRData2.addData(&data, 1U); + + if (m_buffer[3U] == (DMR_SYNC_DATA | DT_TERMINATOR_WITH_LC)) + data = TAG_EOT; + else + data = TAG_DATA; + m_rxDMRData2.addData(&data, 1U); + + m_rxDMRData2.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_DMR_LOST1: { + if (m_trace) + CUtils::dump(1U, "RX DMR Lost 1", m_buffer, m_length); + + unsigned char data = 1U; + m_rxDMRData1.addData(&data, 1U); + + data = TAG_LOST; + m_rxDMRData1.addData(&data, 1U); + } + break; + + case MMDVM_DMR_LOST2: { + if (m_trace) + CUtils::dump(1U, "RX DMR Lost 2", m_buffer, m_length); + + unsigned char data = 1U; + m_rxDMRData2.addData(&data, 1U); + + data = TAG_LOST; + m_rxDMRData2.addData(&data, 1U); + } + break; + + case MMDVM_YSF_DATA: { + if (m_trace) + CUtils::dump(1U, "RX YSF Data", m_buffer, m_length); + + unsigned char data = m_length - m_offset + 1U; + m_rxYSFData.addData(&data, 1U); + + data = TAG_DATA; + m_rxYSFData.addData(&data, 1U); + + m_rxYSFData.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_YSF_LOST: { + if (m_trace) + CUtils::dump(1U, "RX YSF Lost", m_buffer, m_length); + + unsigned char data = 1U; + m_rxYSFData.addData(&data, 1U); + + data = TAG_LOST; + m_rxYSFData.addData(&data, 1U); + } + break; + + case MMDVM_P25_HDR: { + if (m_trace) + CUtils::dump(1U, "RX P25 Header", m_buffer, m_length); + + unsigned char data = m_length - m_offset + 1U; + m_rxP25Data.addData(&data, 1U); + + data = TAG_HEADER; + m_rxP25Data.addData(&data, 1U); + + m_rxP25Data.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_P25_LDU: { + if (m_trace) + CUtils::dump(1U, "RX P25 LDU", m_buffer, m_length); + + unsigned char data = m_length - m_offset + 1U; + m_rxP25Data.addData(&data, 1U); + + data = TAG_DATA; + m_rxP25Data.addData(&data, 1U); + + m_rxP25Data.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_P25_LOST: { + if (m_trace) + CUtils::dump(1U, "RX P25 Lost", m_buffer, m_length); + + unsigned char data = 1U; + m_rxP25Data.addData(&data, 1U); + + data = TAG_LOST; + m_rxP25Data.addData(&data, 1U); + } + break; + + case MMDVM_NXDN_DATA: { + if (m_trace) + CUtils::dump(1U, "RX NXDN Data", m_buffer, m_length); + + unsigned char data = m_length - m_offset + 1U; + m_rxNXDNData.addData(&data, 1U); + + data = TAG_DATA; + m_rxNXDNData.addData(&data, 1U); + + m_rxNXDNData.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_NXDN_LOST: { + if (m_trace) + CUtils::dump(1U, "RX NXDN Lost", m_buffer, m_length); + + unsigned char data = 1U; + m_rxNXDNData.addData(&data, 1U); + + data = TAG_LOST; + m_rxNXDNData.addData(&data, 1U); + } + break; + + case MMDVM_FM_DATA: { + if (m_trace) + CUtils::dump(1U, "RX FM Data", m_buffer, m_length); + + unsigned int data1 = m_length - m_offset + 1U; + m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); + + unsigned char data2 = TAG_DATA; + m_rxFMData.addData(&data2, 1U); + + m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_FM_CONTROL: { + if (m_trace) + CUtils::dump(1U, "RX FM Control", m_buffer, m_length); + + unsigned char data = m_length - m_offset + 1U; + m_rxFMData.addData(&data, 1U); + + data = TAG_HEADER; + m_rxFMData.addData(&data, 1U); + + m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_FM_EOT: { + if(m_trace) + CUtils::dump(1U, "RX FM End of transmission", m_buffer, m_length); + + unsigned char data = m_length - m_offset + 1U; + m_rxFMData.addData(&data, 1U); + + data = TAG_EOT; + m_rxFMData.addData(&data, 1U); + + m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_AX25_DATA: { + if (m_trace) + CUtils::dump(1U, "RX AX.25 Data", m_buffer, m_length); + + unsigned int data = m_length - m_offset; + m_rxAX25Data.addData((unsigned char*)&data, sizeof(unsigned int)); + + m_rxAX25Data.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_GET_STATUS: { + // if (m_trace) + // CUtils::dump(1U, "GET_STATUS", m_buffer, m_length); + + m_p25Space = 0U; + m_nxdnSpace = 0U; + m_pocsagSpace = 0U; + m_fmSpace = 0U; + m_ax25Space = 0U; + + m_mode = m_buffer[m_offset + 1U]; + + m_tx = (m_buffer[m_offset + 2U] & 0x01U) == 0x01U; + + bool adcOverflow = (m_buffer[m_offset + 2U] & 0x02U) == 0x02U; + if (adcOverflow) + LogError("MMDVM ADC levels have overflowed"); + + bool rxOverflow = (m_buffer[m_offset + 2U] & 0x04U) == 0x04U; + if (rxOverflow) + LogError("MMDVM RX buffer has overflowed"); + + bool txOverflow = (m_buffer[m_offset + 2U] & 0x08U) == 0x08U; + if (txOverflow) + LogError("MMDVM TX buffer has overflowed"); + + m_lockout = (m_buffer[m_offset + 2U] & 0x10U) == 0x10U; + + bool dacOverflow = (m_buffer[m_offset + 2U] & 0x20U) == 0x20U; + if (dacOverflow) + LogError("MMDVM DAC levels have overflowed"); + + m_cd = (m_buffer[m_offset + 2U] & 0x40U) == 0x40U; + + m_dstarSpace = m_buffer[m_offset + 3U]; + m_dmrSpace1 = m_buffer[m_offset + 4U]; + m_dmrSpace2 = m_buffer[m_offset + 5U]; + m_ysfSpace = m_buffer[m_offset + 6U]; + + if (m_length > (m_offset + 7U)) + m_p25Space = m_buffer[m_offset + 7U]; + if (m_length > (m_offset + 8U)) + m_nxdnSpace = m_buffer[m_offset + 8U]; + if (m_length > (m_offset + 9U)) + m_pocsagSpace = m_buffer[m_offset + 9U]; + if (m_length > (m_offset + 10U)) + m_fmSpace = m_buffer[m_offset + 10U]; + if (m_length > (m_offset + 11U)) + m_ax25Space = m_buffer[m_offset + 11U]; + + m_inactivityTimer.start(); + // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[m_offset + 2U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_pocsagSpace, m_fmSpace, m_ax25Space, int(m_lockout), int(m_cd)); + } + break; + + case MMDVM_TRANSPARENT: { + if (m_trace) + CUtils::dump(1U, "RX Transparent Data", m_buffer, m_length); + + unsigned char offset = m_sendTransparentDataFrameType; + if (offset > 1U) offset = 1U; + unsigned char data = m_length - m_offset + offset; + m_rxTransparentData.addData(&data, 1U); + + m_rxTransparentData.addData(m_buffer + m_offset - offset, m_length - m_offset + offset); + } + break; + + // These should not be received, but don't complain if we do + case MMDVM_GET_VERSION: + case MMDVM_ACK: + break; + + case MMDVM_NAK: + LogWarning("Received a NAK from the MMDVM, command = 0x%02X, reason = %u", m_buffer[m_offset], m_buffer[m_offset + 1U]); + break; + + case MMDVM_DEBUG1: + case MMDVM_DEBUG2: + case MMDVM_DEBUG3: + case MMDVM_DEBUG4: + case MMDVM_DEBUG5: + printDebug(); + break; + + case MMDVM_SERIAL: + //MMDVMHost does not process serial data from the display, + // so we send it to the transparent port if sendFrameType==1 + if (m_sendTransparentDataFrameType > 0U) { + if (m_trace) + CUtils::dump(1U, "RX Serial Data", m_buffer, m_length); + + unsigned char offset = m_sendTransparentDataFrameType; + if (offset > 1U) offset = 1U; + unsigned char data = m_length - m_offset + offset; + m_rxTransparentData.addData(&data, 1U); + + m_rxTransparentData.addData(m_buffer + m_offset - offset, m_length - m_offset + offset); + break; //only break when sendFrameType>0, else message is unknown + } + default: + LogMessage("Unknown message, type: %02X", m_type); + CUtils::dump("Buffer dump", m_buffer, m_length); + break; + } + } + + // Only feed data to the modem if the playout timer has expired + m_playoutTimer.clock(ms); + if (!m_playoutTimer.hasExpired()) + return; + + if (m_dstarSpace > 1U && !m_txDStarData.isEmpty()) { + unsigned char buffer[4U]; + m_txDStarData.peek(buffer, 4U); + + if ((buffer[3U] == MMDVM_DSTAR_HEADER && m_dstarSpace > 4U) || + (buffer[3U] == MMDVM_DSTAR_DATA && m_dstarSpace > 1U) || + (buffer[3U] == MMDVM_DSTAR_EOT && m_dstarSpace > 1U)) { + unsigned char len = 0U; + m_txDStarData.getData(&len, 1U); + m_txDStarData.getData(m_buffer, len); + + switch (buffer[3U]) { + case MMDVM_DSTAR_HEADER: + if (m_trace) + CUtils::dump(1U, "TX D-Star Header", m_buffer, len); + m_dstarSpace -= 4U; + break; + case MMDVM_DSTAR_DATA: + if (m_trace) + CUtils::dump(1U, "TX D-Star Data", m_buffer, len); + m_dstarSpace -= 1U; + break; + default: + if (m_trace) + CUtils::dump(1U, "TX D-Star EOT", m_buffer, len); + m_dstarSpace -= 1U; + break; + } + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing D-Star data to the MMDVM"); + + m_playoutTimer.start(); + } + } + + if (m_dmrSpace1 > 1U && !m_txDMRData1.isEmpty()) { + unsigned char len = 0U; + m_txDMRData1.getData(&len, 1U); + m_txDMRData1.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX DMR Data 1", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing DMR data to the MMDVM"); + + m_playoutTimer.start(); + + m_dmrSpace1--; + } + + if (m_dmrSpace2 > 1U && !m_txDMRData2.isEmpty()) { + unsigned char len = 0U; + m_txDMRData2.getData(&len, 1U); + m_txDMRData2.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX DMR Data 2", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing DMR data to the MMDVM"); + + m_playoutTimer.start(); + + m_dmrSpace2--; + } + + if (m_ysfSpace > 1U && !m_txYSFData.isEmpty()) { + unsigned char len = 0U; + m_txYSFData.getData(&len, 1U); + m_txYSFData.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX YSF Data", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing YSF data to the MMDVM"); + + m_playoutTimer.start(); + + m_ysfSpace--; + } + + if (m_p25Space > 1U && !m_txP25Data.isEmpty()) { + unsigned char len = 0U; + m_txP25Data.getData(&len, 1U); + m_txP25Data.getData(m_buffer, len); + + if (m_trace) { + if (m_buffer[2U] == MMDVM_P25_HDR) + CUtils::dump(1U, "TX P25 HDR", m_buffer, len); + else + CUtils::dump(1U, "TX P25 LDU", m_buffer, len); + } + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing P25 data to the MMDVM"); + + m_playoutTimer.start(); + + m_p25Space--; + } + + if (m_nxdnSpace > 1U && !m_txNXDNData.isEmpty()) { + unsigned char len = 0U; + m_txNXDNData.getData(&len, 1U); + m_txNXDNData.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX NXDN Data", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing NXDN data to the MMDVM"); + + m_playoutTimer.start(); + + m_nxdnSpace--; + } + + if (m_pocsagSpace > 1U && !m_txPOCSAGData.isEmpty()) { + unsigned char len = 0U; + m_txPOCSAGData.getData(&len, 1U); + m_txPOCSAGData.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX POCSAG Data", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing POCSAG data to the MMDVM"); + + m_playoutTimer.start(); + + m_pocsagSpace--; + } + + if (m_fmSpace > 1U && !m_txFMData.isEmpty()) { + unsigned char len = 0U; + m_txFMData.getData(&len, 1U); + m_txFMData.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX FM Data", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing FM data to the MMDVM"); + + m_playoutTimer.start(); + + m_fmSpace--; + } + + if (m_ax25Space > 0U && !m_txAX25Data.isEmpty()) { + unsigned char len = 0U; + m_txAX25Data.getData((unsigned char*)&len, sizeof(unsigned int)); + m_txAX25Data.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX AX.25 Data", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing AX.25 data to the MMDVM"); + + m_playoutTimer.start(); + + m_ax25Space = 0U; + } + + if (!m_txTransparentData.isEmpty()) { + unsigned char len = 0U; + m_txTransparentData.getData(&len, 1U); + m_txTransparentData.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX Transparent Data", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing Transparent data to the MMDVM"); + } +} + +void CSerialModem::close() +{ + assert(m_serial != NULL); + + ::LogMessage("Closing the MMDVM"); + + m_serial->close(); +} + +unsigned int CSerialModem::readDStarData(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxDStarData.isEmpty()) + return 0U; + + unsigned char len = 0U; + m_rxDStarData.getData(&len, 1U); + m_rxDStarData.getData(data, len); + + return len; +} + +unsigned int CSerialModem::readDMRData1(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxDMRData1.isEmpty()) + return 0U; + + unsigned char len = 0U; + m_rxDMRData1.getData(&len, 1U); + m_rxDMRData1.getData(data, len); + + return len; +} + +unsigned int CSerialModem::readDMRData2(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxDMRData2.isEmpty()) + return 0U; + + unsigned char len = 0U; + m_rxDMRData2.getData(&len, 1U); + m_rxDMRData2.getData(data, len); + + return len; +} + +unsigned int CSerialModem::readYSFData(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxYSFData.isEmpty()) + return 0U; + + unsigned char len = 0U; + m_rxYSFData.getData(&len, 1U); + m_rxYSFData.getData(data, len); + + return len; +} + +unsigned int CSerialModem::readP25Data(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxP25Data.isEmpty()) + return 0U; + + unsigned char len = 0U; + m_rxP25Data.getData(&len, 1U); + m_rxP25Data.getData(data, len); + + return len; +} + +unsigned int CSerialModem::readNXDNData(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxNXDNData.isEmpty()) + return 0U; + + unsigned char len = 0U; + m_rxNXDNData.getData(&len, 1U); + m_rxNXDNData.getData(data, len); + + return len; +} + +unsigned int CSerialModem::readFMData(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxFMData.isEmpty()) + return 0U; + + unsigned int len = 0U; + m_rxFMData.getData((unsigned char*)&len, sizeof(unsigned int)); + m_rxFMData.getData(data, len); + + return len; +} + +unsigned int CSerialModem::readAX25Data(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxAX25Data.isEmpty()) + return 0U; + + unsigned int len = 0U; + m_rxAX25Data.getData((unsigned char*)&len, sizeof(unsigned int)); + m_rxAX25Data.getData(data, len); + + return len; +} + +unsigned int CSerialModem::readTransparentData(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxTransparentData.isEmpty()) + return 0U; + + unsigned char len = 0U; + m_rxTransparentData.getData(&len, 1U); + m_rxTransparentData.getData(data, len); + + return len; +} + +// To be implemented later if needed +unsigned int CSerialModem::readSerial(unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + return 0U; +} + +bool CSerialModem::hasDStarSpace() const +{ + unsigned int space = m_txDStarData.freeSpace() / (DSTAR_FRAME_LENGTH_BYTES + 4U); + + return space > 1U; +} + +bool CSerialModem::writeDStarData(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + unsigned char buffer[50U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 2U; + + switch (data[0U]) { + case TAG_HEADER: + buffer[2U] = MMDVM_DSTAR_HEADER; + break; + case TAG_DATA: + buffer[2U] = MMDVM_DSTAR_DATA; + break; + case TAG_EOT: + buffer[2U] = MMDVM_DSTAR_EOT; + break; + default: + CUtils::dump(2U, "Unknown D-Star packet type", data, length); + return false; + } + + ::memcpy(buffer + 3U, data + 1U, length - 1U); + + unsigned char len = length + 2U; + m_txDStarData.addData(&len, 1U); + m_txDStarData.addData(buffer, len); + + return true; +} + +bool CSerialModem::hasDMRSpace1() const +{ + unsigned int space = m_txDMRData1.freeSpace() / (DMR_FRAME_LENGTH_BYTES + 4U); + + return space > 1U; +} + +bool CSerialModem::hasDMRSpace2() const +{ + unsigned int space = m_txDMRData2.freeSpace() / (DMR_FRAME_LENGTH_BYTES + 4U); + + return space > 1U; +} + +bool CSerialModem::writeDMRData1(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) + return false; + + unsigned char buffer[40U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 2U; + buffer[2U] = MMDVM_DMR_DATA1; + + ::memcpy(buffer + 3U, data + 1U, length - 1U); + + unsigned char len = length + 2U; + m_txDMRData1.addData(&len, 1U); + m_txDMRData1.addData(buffer, len); + + return true; +} + +bool CSerialModem::writeDMRData2(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) + return false; + + unsigned char buffer[40U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 2U; + buffer[2U] = MMDVM_DMR_DATA2; + + ::memcpy(buffer + 3U, data + 1U, length - 1U); + + unsigned char len = length + 2U; + m_txDMRData2.addData(&len, 1U); + m_txDMRData2.addData(buffer, len); + + return true; +} + +bool CSerialModem::hasYSFSpace() const +{ + unsigned int space = m_txYSFData.freeSpace() / (YSF_FRAME_LENGTH_BYTES + 4U); + + return space > 1U; +} + +bool CSerialModem::writeYSFData(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) + return false; + + unsigned char buffer[130U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 2U; + buffer[2U] = MMDVM_YSF_DATA; + + ::memcpy(buffer + 3U, data + 1U, length - 1U); + + unsigned char len = length + 2U; + m_txYSFData.addData(&len, 1U); + m_txYSFData.addData(buffer, len); + + return true; +} + +bool CSerialModem::hasP25Space() const +{ + unsigned int space = m_txP25Data.freeSpace() / (P25_LDU_FRAME_LENGTH_BYTES + 4U); + + return space > 1U; +} + +bool CSerialModem::writeP25Data(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + if (data[0U] != TAG_HEADER && data[0U] != TAG_DATA && data[0U] != TAG_EOT) + return false; + + unsigned char buffer[250U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 2U; + buffer[2U] = (data[0U] == TAG_HEADER) ? MMDVM_P25_HDR : MMDVM_P25_LDU; + + ::memcpy(buffer + 3U, data + 1U, length - 1U); + + unsigned char len = length + 2U; + m_txP25Data.addData(&len, 1U); + m_txP25Data.addData(buffer, len); + + return true; +} + +bool CSerialModem::hasNXDNSpace() const +{ + unsigned int space = m_txNXDNData.freeSpace() / (NXDN_FRAME_LENGTH_BYTES + 4U); + + return space > 1U; +} + +bool CSerialModem::writeNXDNData(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) + return false; + + unsigned char buffer[130U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 2U; + buffer[2U] = MMDVM_NXDN_DATA; + + ::memcpy(buffer + 3U, data + 1U, length - 1U); + + unsigned char len = length + 2U; + m_txNXDNData.addData(&len, 1U); + m_txNXDNData.addData(buffer, len); + + return true; +} + +bool CSerialModem::hasPOCSAGSpace() const +{ + unsigned int space = m_txPOCSAGData.freeSpace() / (POCSAG_FRAME_LENGTH_BYTES + 4U); + + return space > 1U; +} + +bool CSerialModem::writePOCSAGData(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + unsigned char buffer[130U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 3U; + buffer[2U] = MMDVM_POCSAG_DATA; + + ::memcpy(buffer + 3U, data, length); + + unsigned char len = length + 3U; + m_txPOCSAGData.addData(&len, 1U); + m_txPOCSAGData.addData(buffer, len); + + return true; +} + +unsigned int CSerialModem::getFMSpace() const +{ + return m_txFMData.freeSpace(); +} + +bool CSerialModem::writeFMData(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + unsigned char buffer[500U]; + + unsigned int len; + if (length > 252U) { + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 0U; + buffer[2U] = (length + 4U) - 255U; + buffer[3U] = MMDVM_FM_DATA; + ::memcpy(buffer + 4U, data, length); + len = length + 4U; + } else { + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 3U; + buffer[2U] = MMDVM_FM_DATA; + ::memcpy(buffer + 3U, data, length); + len = length + 3U; + } + + m_txFMData.addData((unsigned char*)&len, sizeof(unsigned int)); + m_txFMData.addData(buffer, len); + + return true; +} + +bool CSerialModem::hasAX25Space() const +{ + unsigned int space = m_txAX25Data.freeSpace() / (AX25_MAX_FRAME_LENGTH_BYTES + 5U); + + return space > 1U; +} + +bool CSerialModem::writeAX25Data(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + unsigned char buffer[500U]; + + unsigned int len; + if (length > 252U) { + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 0U; + buffer[2U] = (length + 4U) - 255U; + buffer[3U] = MMDVM_AX25_DATA; + ::memcpy(buffer + 4U, data, length); + len = length + 4U; + } else { + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 3U; + buffer[2U] = MMDVM_AX25_DATA; + ::memcpy(buffer + 3U, data, length); + len = length + 3U; + } + + m_txAX25Data.addData((unsigned char*)&len, sizeof(unsigned int)); + m_txAX25Data.addData(buffer, len); + + return true; +} + +bool CSerialModem::writeTransparentData(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + unsigned char buffer[250U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 3U; + buffer[2U] = MMDVM_TRANSPARENT; + + if (m_sendTransparentDataFrameType > 0U) { + ::memcpy(buffer + 2U, data, length); + length--; + buffer[1U]--; + + //when sendFrameType==1 , only 0x80 and 0x90 (MMDVM_SERIAL and MMDVM_TRANSPARENT) are allowed + // and reverted to default (MMDVM_TRANSPARENT) for any other value + //when >1, frame type is not checked + if (m_sendTransparentDataFrameType == 1U) { + if ((buffer[2U] & 0xE0) != 0x80) + buffer[2U] = MMDVM_TRANSPARENT; + } + } else { + ::memcpy(buffer + 3U, data, length); + } + + unsigned char len = length + 3U; + m_txTransparentData.addData(&len, 1U); + m_txTransparentData.addData(buffer, len); + + return true; +} + +bool CSerialModem::writeDStarInfo(const char* my1, const char* my2, const char* your, const char* type, const char* reflector) +{ + assert(m_serial != NULL); + assert(my1 != NULL); + assert(my2 != NULL); + assert(your != NULL); + assert(type != NULL); + assert(reflector != NULL); + + unsigned char buffer[50U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 33U; + buffer[2U] = MMDVM_QSO_INFO; + + buffer[3U] = MODE_DSTAR; + + ::memcpy(buffer + 4U, my1, DSTAR_LONG_CALLSIGN_LENGTH); + ::memcpy(buffer + 12U, my2, DSTAR_SHORT_CALLSIGN_LENGTH); + + ::memcpy(buffer + 16U, your, DSTAR_LONG_CALLSIGN_LENGTH); + + ::memcpy(buffer + 24U, type, 1U); + + ::memcpy(buffer + 25U, reflector, DSTAR_LONG_CALLSIGN_LENGTH); + + return m_serial->write(buffer, 33U) != 33; +} + +bool CSerialModem::writeDMRInfo(unsigned int slotNo, const std::string& src, bool group, const std::string& dest, const char* type) +{ + assert(m_serial != NULL); + assert(type != NULL); + + unsigned char buffer[50U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 47U; + buffer[2U] = MMDVM_QSO_INFO; + + buffer[3U] = MODE_DMR; + + buffer[4U] = slotNo; + + ::sprintf((char*)(buffer + 5U), "%20.20s", src.c_str()); + + buffer[25U] = group ? 'G' : 'I'; + + ::sprintf((char*)(buffer + 26U), "%20.20s", dest.c_str()); + + ::memcpy(buffer + 46U, type, 1U); + + return m_serial->write(buffer, 47U) != 47; +} + +bool CSerialModem::writeYSFInfo(const char* source, const char* dest, const char* type, const char* origin) +{ + assert(m_serial != NULL); + assert(source != NULL); + assert(dest != NULL); + assert(type != NULL); + assert(origin != NULL); + + unsigned char buffer[50U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 35U; + buffer[2U] = MMDVM_QSO_INFO; + + buffer[3U] = MODE_YSF; + + ::memcpy(buffer + 4U, source, YSF_CALLSIGN_LENGTH); + ::memcpy(buffer + 14U, dest, YSF_CALLSIGN_LENGTH); + + ::memcpy(buffer + 24U, type, 1U); + + ::memcpy(buffer + 25U, origin, YSF_CALLSIGN_LENGTH); + + return m_serial->write(buffer, 35U) != 35; +} + +bool CSerialModem::writeP25Info(const char* source, bool group, unsigned int dest, const char* type) +{ + assert(m_serial != NULL); + assert(source != NULL); + assert(type != NULL); + + unsigned char buffer[40U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 31U; + buffer[2U] = MMDVM_QSO_INFO; + + buffer[3U] = MODE_DMR; + + ::sprintf((char*)(buffer + 4U), "%20.20s", source); + + buffer[24U] = group ? 'G' : 'I'; + + ::sprintf((char*)(buffer + 25U), "%05u", dest); // 16-bits + + ::memcpy(buffer + 30U, type, 1U); + + return m_serial->write(buffer, 31U) != 31; +} + +bool CSerialModem::writeNXDNInfo(const char* source, bool group, unsigned int dest, const char* type) +{ + assert(m_serial != NULL); + assert(source != NULL); + assert(type != NULL); + + unsigned char buffer[40U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 31U; + buffer[2U] = MMDVM_QSO_INFO; + + buffer[3U] = MODE_NXDN; + + ::sprintf((char*)(buffer + 4U), "%20.20s", source); + + buffer[24U] = group ? 'G' : 'I'; + + ::sprintf((char*)(buffer + 25U), "%05u", dest); // 16-bits + + ::memcpy(buffer + 30U, type, 1U); + + return m_serial->write(buffer, 31U) != 31; +} + +bool CSerialModem::writePOCSAGInfo(unsigned int ric, const std::string& message) +{ + assert(m_serial != NULL); + + size_t length = message.size(); + + unsigned char buffer[250U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 11U; + buffer[2U] = MMDVM_QSO_INFO; + + buffer[3U] = MODE_POCSAG; + + ::sprintf((char*)(buffer + 4U), "%07u", ric); // 21-bits + + ::memcpy(buffer + 11U, message.c_str(), length); + + int ret = m_serial->write(buffer, length + 11U); + + return ret != int(length + 11U); +} + +bool CSerialModem::writeIPInfo(const std::string& address) +{ + assert(m_serial != NULL); + + size_t length = address.size(); + + unsigned char buffer[25U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 4U; + buffer[2U] = MMDVM_QSO_INFO; + + buffer[3U] = 250U; + + ::memcpy(buffer + 4U, address.c_str(), length); + + int ret = m_serial->write(buffer, length + 4U); + + return ret != int(length + 4U); +} + +bool CSerialModem::writeSerial(const unsigned char* data, unsigned int length) +{ + assert(m_serial != NULL); + assert(data != NULL); + assert(length > 0U); + + unsigned char buffer[250U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 3U; + buffer[2U] = MMDVM_SERIAL; + + ::memcpy(buffer + 3U, data, length); + + int ret = m_serial->write(buffer, length + 3U); + + return ret != int(length + 3U); +} + +bool CSerialModem::hasTX() const +{ + return m_tx; +} + +bool CSerialModem::hasCD() const +{ + return m_cd; +} + +bool CSerialModem::hasLockout() const +{ + return m_lockout; +} + +bool CSerialModem::hasError() const +{ + return m_error; +} + +bool CSerialModem::readVersion() +{ + assert(m_serial != NULL); + + CThread::sleep(2000U); // 2s + + for (unsigned int i = 0U; i < 6U; i++) { + unsigned char buffer[3U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 3U; + buffer[2U] = MMDVM_GET_VERSION; + + // CUtils::dump(1U, "Written", buffer, 3U); + + int ret = m_serial->write(buffer, 3U); + if (ret != 3) + return false; + +#if defined(__APPLE__) + m_serial->setNonblock(true); +#endif + + for (unsigned int count = 0U; count < MAX_RESPONSES; count++) { + CThread::sleep(10U); + RESP_TYPE_MMDVM resp = getResponse(); + if (resp == RTM_OK && m_buffer[2U] == MMDVM_GET_VERSION) { + if (::memcmp(m_buffer + 4U, "MMDVM ", 6U) == 0) + m_hwType = HWT_MMDVM; + else if (::memcmp(m_buffer + 4U, "DVMEGA", 6U) == 0) + m_hwType = HWT_DVMEGA; + else if (::memcmp(m_buffer + 4U, "ZUMspot", 7U) == 0) + m_hwType = HWT_MMDVM_ZUMSPOT; + else if (::memcmp(m_buffer + 4U, "MMDVM_HS_Hat", 12U) == 0) + m_hwType = HWT_MMDVM_HS_HAT; + else if (::memcmp(m_buffer + 4U, "MMDVM_HS_Dual_Hat", 17U) == 0) + m_hwType = HWT_MMDVM_HS_DUAL_HAT; + else if (::memcmp(m_buffer + 4U, "Nano_hotSPOT", 12U) == 0) + m_hwType = HWT_NANO_HOTSPOT; + else if (::memcmp(m_buffer + 4U, "Nano_DV", 7U) == 0) + m_hwType = HWT_NANO_DV; + else if (::memcmp(m_buffer + 4U, "D2RG_MMDVM_HS", 13U) == 0) + m_hwType = HWT_D2RG_MMDVM_HS; + else if (::memcmp(m_buffer + 4U, "MMDVM_HS-", 9U) == 0) + m_hwType = HWT_MMDVM_HS; + else if (::memcmp(m_buffer + 4U, "OpenGD77_HS", 11U) == 0) + m_hwType = HWT_OPENGD77_HS; + + LogInfo("MMDVM protocol version: %u, description: %.*s", m_buffer[3U], m_length - 4U, m_buffer + 4U); + return true; + } + } + + CThread::sleep(1500U); + } + + LogError("Unable to read the firmware version after six attempts"); + + return false; +} + +bool CSerialModem::readStatus() +{ + assert(m_serial != NULL); + + unsigned char buffer[3U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 3U; + buffer[2U] = MMDVM_GET_STATUS; + + // CUtils::dump(1U, "Written", buffer, 3U); + + return m_serial->write(buffer, 3U) == 3; +} + +bool CSerialModem::writeConfig() +{ + return setConfig(); +} + +bool CSerialModem::setConfig() +{ + assert(m_serial != NULL); + + unsigned char buffer[30U]; + + buffer[0U] = MMDVM_FRAME_START; + + buffer[1U] = 27U; + + buffer[2U] = MMDVM_SET_CONFIG; + + buffer[3U] = 0x00U; + if (m_rxInvert) + buffer[3U] |= 0x01U; + if (m_txInvert) + buffer[3U] |= 0x02U; + if (m_pttInvert) + buffer[3U] |= 0x04U; + if (m_ysfLoDev) + buffer[3U] |= 0x08U; + if (m_debug) + buffer[3U] |= 0x10U; + if (!m_duplex) + buffer[3U] |= 0x80U; + + buffer[4U] = 0x00U; + if (m_dstarEnabled) + buffer[4U] |= 0x01U; + if (m_dmrEnabled) + buffer[4U] |= 0x02U; + if (m_ysfEnabled) + buffer[4U] |= 0x04U; + if (m_p25Enabled) + buffer[4U] |= 0x08U; + if (m_nxdnEnabled) + buffer[4U] |= 0x10U; + if (m_pocsagEnabled) + buffer[4U] |= 0x20U; + if (m_fmEnabled && m_duplex) + buffer[4U] |= 0x40U; + if (m_ax25Enabled) + buffer[4U] |= 0x80U; + + buffer[5U] = m_txDelay / 10U; // In 10ms units + + buffer[6U] = MODE_IDLE; + + buffer[7U] = (unsigned char)(m_rxLevel * 2.55F + 0.5F); + + buffer[8U] = (unsigned char)(m_cwIdTXLevel * 2.55F + 0.5F); + + buffer[9U] = m_dmrColorCode; + + buffer[10U] = m_dmrDelay; + + buffer[11U] = 128U; // Was OscOffset + + buffer[12U] = (unsigned char)(m_dstarTXLevel * 2.55F + 0.5F); + buffer[13U] = (unsigned char)(m_dmrTXLevel * 2.55F + 0.5F); + buffer[14U] = (unsigned char)(m_ysfTXLevel * 2.55F + 0.5F); + buffer[15U] = (unsigned char)(m_p25TXLevel * 2.55F + 0.5F); + + buffer[16U] = (unsigned char)(m_txDCOffset + 128); + buffer[17U] = (unsigned char)(m_rxDCOffset + 128); + + buffer[18U] = (unsigned char)(m_nxdnTXLevel * 2.55F + 0.5F); + + buffer[19U] = (unsigned char)m_ysfTXHang; + + buffer[20U] = (unsigned char)(m_pocsagTXLevel * 2.55F + 0.5F); + + buffer[21U] = (unsigned char)(m_fmTXLevel * 2.55F + 0.5F); + + buffer[22U] = (unsigned char)m_p25TXHang; + + buffer[23U] = (unsigned char)m_nxdnTXHang; + + buffer[24U] = (unsigned char)(m_ax25TXLevel * 2.55F + 0.5F); + + buffer[25U] = (unsigned char)(m_ax25RXTwist + 128); + + buffer[26U] = m_ax25TXDelay / 10U; // In 10ms units + + // CUtils::dump(1U, "Written", buffer, 27U); + + int ret = m_serial->write(buffer, 27U); + if (ret != 27) + return false; + + unsigned int count = 0U; + RESP_TYPE_MMDVM resp; + do { + CThread::sleep(10U); + + resp = getResponse(); + if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { + count++; + if (count >= MAX_RESPONSES) { + LogError("The MMDVM is not responding to the SET_CONFIG command"); + return false; + } + } + } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); + + // CUtils::dump(1U, "Response", m_buffer, m_length); + + if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { + LogError("Received a NAK to the SET_CONFIG command from the modem"); + return false; + } + + m_playoutTimer.start(); + + return true; +} + +bool CSerialModem::setFrequency() +{ + assert(m_serial != NULL); + + unsigned char buffer[20U]; + unsigned char len; + unsigned int pocsagFrequency = 433000000U; + + if (m_pocsagEnabled) + pocsagFrequency = m_pocsagFrequency; + + if (m_hwType == HWT_DVMEGA) + len = 12U; + else { + buffer[12U] = (unsigned char)(m_rfLevel * 2.55F + 0.5F); + + buffer[13U] = (pocsagFrequency >> 0) & 0xFFU; + buffer[14U] = (pocsagFrequency >> 8) & 0xFFU; + buffer[15U] = (pocsagFrequency >> 16) & 0xFFU; + buffer[16U] = (pocsagFrequency >> 24) & 0xFFU; + + len = 17U; + } + + buffer[0U] = MMDVM_FRAME_START; + + buffer[1U] = len; + + buffer[2U] = MMDVM_SET_FREQ; + + buffer[3U] = 0x00U; + + buffer[4U] = (m_rxFrequency >> 0) & 0xFFU; + buffer[5U] = (m_rxFrequency >> 8) & 0xFFU; + buffer[6U] = (m_rxFrequency >> 16) & 0xFFU; + buffer[7U] = (m_rxFrequency >> 24) & 0xFFU; + + buffer[8U] = (m_txFrequency >> 0) & 0xFFU; + buffer[9U] = (m_txFrequency >> 8) & 0xFFU; + buffer[10U] = (m_txFrequency >> 16) & 0xFFU; + buffer[11U] = (m_txFrequency >> 24) & 0xFFU; + + // CUtils::dump(1U, "Written", buffer, len); + + int ret = m_serial->write(buffer, len); + if (ret != len) + return false; + + unsigned int count = 0U; + RESP_TYPE_MMDVM resp; + do { + CThread::sleep(10U); + + resp = getResponse(); + if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { + count++; + if (count >= MAX_RESPONSES) { + LogError("The MMDVM is not responding to the SET_FREQ command"); + return false; + } + } + } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); + + // CUtils::dump(1U, "Response", m_buffer, m_length); + + if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { + LogError("Received a NAK to the SET_FREQ command from the modem"); + return false; + } + + return true; +} + +RESP_TYPE_MMDVM CSerialModem::getResponse() +{ + assert(m_serial != NULL); + + if (m_state == SS_START) { + // Get the start of the frame or nothing at all + int ret = m_serial->read(m_buffer + 0U, 1U); + if (ret < 0) { + LogError("Error when reading from the modem"); + return RTM_ERROR; + } + + if (ret == 0) + return RTM_TIMEOUT; + + if (m_buffer[0U] != MMDVM_FRAME_START) + return RTM_TIMEOUT; + + m_state = SS_LENGTH1; + m_length = 1U; + } + + if (m_state == SS_LENGTH1) { + // Get the length of the frame, 1/2 + int ret = m_serial->read(m_buffer + 1U, 1U); + if (ret < 0) { + LogError("Error when reading from the modem"); + m_state = SS_START; + return RTM_ERROR; + } + + if (ret == 0) + return RTM_TIMEOUT; + + m_length = m_buffer[1U]; + m_offset = 2U; + + if (m_length == 0U) + m_state = SS_LENGTH2; + else + m_state = SS_TYPE; + } + + if (m_state == SS_LENGTH2) { + // Get the length of the frane, 2/2 + int ret = m_serial->read(m_buffer + 2U, 1U); + if (ret < 0) { + LogError("Error when reading from the modem"); + m_state = SS_START; + return RTM_ERROR; + } + + if (ret == 0) + return RTM_TIMEOUT; + + m_length = m_buffer[2U] + 255U; + m_offset = 3U; + m_state = SS_TYPE; + } + + if (m_state == SS_TYPE) { + // Get the frame type + int ret = m_serial->read(&m_type, 1U); + if (ret < 0) { + LogError("Error when reading from the modem"); + m_state = SS_START; + return RTM_ERROR; + } + + if (ret == 0) + return RTM_TIMEOUT; + + m_buffer[m_offset++] = m_type; + + m_state = SS_DATA; + } + + if (m_state == SS_DATA) { + while (m_offset < m_length) { + int ret = m_serial->read(m_buffer + m_offset, m_length - m_offset); + if (ret < 0) { + LogError("Error when reading from the modem"); + m_state = SS_START; + return RTM_ERROR; + } + + if (ret == 0) + return RTM_TIMEOUT; + + if (ret > 0) + m_offset += ret; + } + } + + // CUtils::dump(1U, "Received", m_buffer, m_length); + + m_offset = m_length > 255U ? 4U : 3U; + m_state = SS_START; + + return RTM_OK; +} + +HW_TYPE CSerialModem::getHWType() const +{ + return m_hwType; +} + +unsigned char CSerialModem::getMode() const +{ + return m_mode; +} + +bool CSerialModem::setMode(unsigned char mode) +{ + assert(m_serial != NULL); + + unsigned char buffer[4U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 4U; + buffer[2U] = MMDVM_SET_MODE; + buffer[3U] = mode; + + // CUtils::dump(1U, "Written", buffer, 4U); + + return m_serial->write(buffer, 4U) == 4; +} + +bool CSerialModem::sendCWId(const std::string& callsign) +{ + assert(m_serial != NULL); + + unsigned int length = callsign.length(); + if (length > 200U) + length = 200U; + + unsigned char buffer[205U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 3U; + buffer[2U] = MMDVM_SEND_CWID; + + for (unsigned int i = 0U; i < length; i++) + buffer[i + 3U] = callsign.at(i); + + // CUtils::dump(1U, "Written", buffer, length + 3U); + + return m_serial->write(buffer, length + 3U) == int(length + 3U); +} + +bool CSerialModem::writeDMRStart(bool tx) +{ + assert(m_serial != NULL); + + if (tx && m_tx) + return true; + if (!tx && !m_tx) + return true; + + unsigned char buffer[4U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 4U; + buffer[2U] = MMDVM_DMR_START; + buffer[3U] = tx ? 0x01U : 0x00U; + + // CUtils::dump(1U, "Written", buffer, 4U); + + return m_serial->write(buffer, 4U) == 4; +} + +bool CSerialModem::writeDMRAbort(unsigned int slotNo) +{ + assert(m_serial != NULL); + + if (slotNo == 1U) + m_txDMRData1.clear(); + else + m_txDMRData2.clear(); + + unsigned char buffer[4U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 4U; + buffer[2U] = MMDVM_DMR_ABORT; + buffer[3U] = slotNo; + + // CUtils::dump(1U, "Written", buffer, 4U); + + return m_serial->write(buffer, 4U) == 4; +} + +bool CSerialModem::writeDMRShortLC(const unsigned char* lc) +{ + assert(m_serial != NULL); + assert(lc != NULL); + + unsigned char buffer[12U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 12U; + buffer[2U] = MMDVM_DMR_SHORTLC; + buffer[3U] = lc[0U]; + buffer[4U] = lc[1U]; + buffer[5U] = lc[2U]; + buffer[6U] = lc[3U]; + buffer[7U] = lc[4U]; + buffer[8U] = lc[5U]; + buffer[9U] = lc[6U]; + buffer[10U] = lc[7U]; + buffer[11U] = lc[8U]; + + // CUtils::dump(1U, "Written", buffer, 12U); + + return m_serial->write(buffer, 12U) == 12; +} + +void CSerialModem::setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch) +{ + m_fmCallsign = callsign; + m_fmCallsignSpeed = callsignSpeed; + m_fmCallsignFrequency = callsignFrequency; + m_fmCallsignTime = callsignTime; + m_fmCallsignHoldoff = callsignHoldoff; + m_fmCallsignHighLevel = callsignHighLevel; + m_fmCallsignLowLevel = callsignLowLevel; + m_fmCallsignAtStart = callsignAtStart; + m_fmCallsignAtEnd = callsignAtEnd; + m_fmCallsignAtLatch = callsignAtLatch; +} + +void CSerialModem::setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel) +{ + m_fmRfAck = rfAck; + m_fmAckSpeed = ackSpeed; + m_fmAckFrequency = ackFrequency; + m_fmAckMinTime = ackMinTime; + m_fmAckDelay = ackDelay; + m_fmAckLevel = ackLevel; +} + +void CSerialModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) +{ + m_fmTimeout = timeout; + m_fmTimeoutLevel = timeoutLevel; + + m_fmCtcssFrequency = ctcssFrequency; + m_fmCtcssHighThreshold = ctcssHighThreshold; + m_fmCtcssLowThreshold = ctcssLowThreshold; + m_fmCtcssLevel = ctcssLevel; + + m_fmKerchunkTime = kerchunkTime; + m_fmKerchunkTX = kerchunkTX; + + m_fmHangTime = hangTime; + + m_fmUseCOS = useCOS; + m_fmCOSInvert = cosInvert; + + m_fmRFAudioBoost = rfAudioBoost; + m_fmMaxDevLevel = maxDevLevel; +} + +void CSerialModem::setFMExtParams(const std::string& ack, unsigned int audioBoost) +{ + m_fmExtAck = ack; + m_fmExtAudioBoost = audioBoost; + m_fmExtEnable = true; +} + +bool CSerialModem::setFMCallsignParams() +{ + assert(m_serial != NULL); + + unsigned char buffer[80U]; + unsigned char len = 10U + m_fmCallsign.size(); + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = len; + buffer[2U] = MMDVM_FM_PARAMS1; + + buffer[3U] = m_fmCallsignSpeed; + buffer[4U] = m_fmCallsignFrequency / 10U; + buffer[5U] = m_fmCallsignTime; + buffer[6U] = m_fmCallsignHoldoff; + + buffer[7U] = (unsigned char)(m_fmCallsignHighLevel * 2.55F + 0.5F); + buffer[8U] = (unsigned char)(m_fmCallsignLowLevel * 2.55F + 0.5F); + + buffer[9U] = 0x00U; + if (m_fmCallsignAtStart) + buffer[9U] |= 0x01U; + if (m_fmCallsignAtEnd) + buffer[9U] |= 0x02U; + if (m_fmCallsignAtLatch) + buffer[9U] |= 0x04U; + + for (unsigned int i = 0U; i < m_fmCallsign.size(); i++) + buffer[10U + i] = m_fmCallsign.at(i); + + // CUtils::dump(1U, "Written", buffer, len); + + int ret = m_serial->write(buffer, len); + if (ret != len) + return false; + + unsigned int count = 0U; + RESP_TYPE_MMDVM resp; + do { + CThread::sleep(10U); + + resp = getResponse(); + if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { + count++; + if (count >= MAX_RESPONSES) { + LogError("The MMDVM is not responding to the SET_FM_PARAMS1 command"); + return false; + } + } + } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); + + // CUtils::dump(1U, "Response", m_buffer, m_length); + + if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { + LogError("Received a NAK to the SET_FM_PARAMS1 command from the modem"); + return false; + } + + return true; +} + +bool CSerialModem::setFMAckParams() +{ + assert(m_serial != NULL); + + unsigned char buffer[80U]; + unsigned char len = 8U + m_fmRfAck.size(); + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = len; + buffer[2U] = MMDVM_FM_PARAMS2; + + buffer[3U] = m_fmAckSpeed; + buffer[4U] = m_fmAckFrequency / 10U; + buffer[5U] = m_fmAckMinTime; + buffer[6U] = m_fmAckDelay / 10U; + + buffer[7U] = (unsigned char)(m_fmAckLevel * 2.55F + 0.5F); + + for (unsigned int i = 0U; i < m_fmRfAck.size(); i++) + buffer[8U + i] = m_fmRfAck.at(i); + + // CUtils::dump(1U, "Written", buffer, len); + + int ret = m_serial->write(buffer, len); + if (ret != len) + return false; + + unsigned int count = 0U; + RESP_TYPE_MMDVM resp; + do { + CThread::sleep(10U); + + resp = getResponse(); + if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { + count++; + if (count >= MAX_RESPONSES) { + LogError("The MMDVM is not responding to the SET_FM_PARAMS2 command"); + return false; + } + } + } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); + + // CUtils::dump(1U, "Response", m_buffer, m_length); + + if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { + LogError("Received a NAK to the SET_FM_PARAMS2 command from the modem"); + return false; + } + + return true; +} + +bool CSerialModem::setFMMiscParams() +{ + assert(m_serial != NULL); + + unsigned char buffer[20U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 15U; + buffer[2U] = MMDVM_FM_PARAMS3; + + buffer[3U] = m_fmTimeout / 5U; + buffer[4U] = (unsigned char)(m_fmTimeoutLevel * 2.55F + 0.5F); + + buffer[5U] = (unsigned char)m_fmCtcssFrequency; + buffer[6U] = m_fmCtcssHighThreshold; + buffer[7U] = m_fmCtcssLowThreshold; + buffer[8U] = (unsigned char)(m_fmCtcssLevel * 2.55F + 0.5F); + + buffer[9U] = m_fmKerchunkTime; + buffer[10U] = m_fmHangTime; + + buffer[11U] = 0x00U; + if (m_fmUseCOS) + buffer[11U] |= 0x01U; + if (m_fmCOSInvert) + buffer[11U] |= 0x02U; + if (m_fmKerchunkTX) + buffer[11U] |= 0x04U; + + buffer[12U] = m_fmRFAudioBoost; + + buffer[13U] = (unsigned char)(m_fmMaxDevLevel * 2.55F + 0.5F); + + buffer[14U] = (unsigned char)(m_rxLevel * 2.55F + 0.5F); + + // CUtils::dump(1U, "Written", buffer, 15U); + + int ret = m_serial->write(buffer, 15U); + if (ret != 15) + return false; + + unsigned int count = 0U; + RESP_TYPE_MMDVM resp; + do { + CThread::sleep(10U); + + resp = getResponse(); + if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { + count++; + if (count >= MAX_RESPONSES) { + LogError("The MMDVM is not responding to the SET_FM_PARAMS3 command"); + return false; + } + } + } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); + + // CUtils::dump(1U, "Response", m_buffer, m_length); + + if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { + LogError("Received a NAK to the SET_FM_PARAMS3 command from the modem"); + return false; + } + + return true; +} + +bool CSerialModem::setFMExtParams() +{ + assert(m_serial != NULL); + + unsigned char buffer[80U]; + unsigned char len = 7U + m_fmExtAck.size(); + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = len; + buffer[2U] = MMDVM_FM_PARAMS4; + + buffer[3U] = m_fmExtAudioBoost; + buffer[4U] = m_fmAckSpeed; + buffer[5U] = m_fmAckFrequency / 10U; + + buffer[6U] = (unsigned char)(m_fmAckLevel * 2.55F + 0.5F); + + for (unsigned int i = 0U; i < m_fmExtAck.size(); i++) + buffer[7U + i] = m_fmExtAck.at(i); + + // CUtils::dump(1U, "Written", buffer, len); + + int ret = m_serial->write(buffer, len); + if (ret != len) + return false; + + unsigned int count = 0U; + RESP_TYPE_MMDVM resp; + do { + CThread::sleep(10U); + + resp = getResponse(); + if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { + count++; + if (count >= MAX_RESPONSES) { + LogError("The MMDVM is not responding to the SET_FM_PARAMS4 command"); + return false; + } + } + } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); + + // CUtils::dump(1U, "Response", m_buffer, m_length); + + if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { + LogError("Received a NAK to the SET_FM_PARAMS4 command from the modem"); + return false; + } + + return true; +} + +void CSerialModem::printDebug() +{ + if (m_buffer[2U] == MMDVM_DEBUG1) { + LogMessage("Debug: %.*s", m_length - m_offset - 0U, m_buffer + m_offset); + } else if (m_buffer[2U] == MMDVM_DEBUG2) { + short val1 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; + LogMessage("Debug: %.*s %d", m_length - m_offset - 2U, m_buffer + m_offset, val1); + } else if (m_buffer[2U] == MMDVM_DEBUG3) { + short val1 = (m_buffer[m_length - 4U] << 8) | m_buffer[m_length - 3U]; + short val2 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; + LogMessage("Debug: %.*s %d %d", m_length - m_offset - 4U, m_buffer + m_offset, val1, val2); + } else if (m_buffer[2U] == MMDVM_DEBUG4) { + short val1 = (m_buffer[m_length - 6U] << 8) | m_buffer[m_length - 5U]; + short val2 = (m_buffer[m_length - 4U] << 8) | m_buffer[m_length - 3U]; + short val3 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; + LogMessage("Debug: %.*s %d %d %d", m_length - m_offset - 6U, m_buffer + m_offset, val1, val2, val3); + } else if (m_buffer[2U] == MMDVM_DEBUG5) { + short val1 = (m_buffer[m_length - 8U] << 8) | m_buffer[m_length - 7U]; + short val2 = (m_buffer[m_length - 6U] << 8) | m_buffer[m_length - 5U]; + short val3 = (m_buffer[m_length - 4U] << 8) | m_buffer[m_length - 3U]; + short val4 = (m_buffer[m_length - 2U] << 8) | m_buffer[m_length - 1U]; + LogMessage("Debug: %.*s %d %d %d %d", m_length - m_offset - 8U, m_buffer + m_offset, val1, val2, val3, val4); + } +} diff --git a/SerialModem.h b/SerialModem.h new file mode 100644 index 0000000..e1ef578 --- /dev/null +++ b/SerialModem.h @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2011-2018,2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#ifndef SERIALMODEM_H +#define SERIALMODEM_H + +#include "Modem.h" + +#include "SerialPort.h" +#include "RingBuffer.h" +#include "Defines.h" +#include "Timer.h" + +#include + +enum RESP_TYPE_MMDVM { + RTM_OK, + RTM_TIMEOUT, + RTM_ERROR +}; + +enum SERIAL_STATE { + SS_START, + SS_LENGTH1, + SS_LENGTH2, + SS_TYPE, + SS_DATA +}; + +class CSerialModem : public IModem { +public: + CSerialModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool trace, bool debug); + virtual ~CSerialModem(); + + virtual void setSerialParams(const std::string& protocol, unsigned int address, unsigned int speed); + virtual void setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency); + virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled, bool ax25Enabled); + virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel, float ax25TXLevel); + virtual void setDMRParams(unsigned int colorCode); + virtual void setYSFParams(bool loDev, unsigned int txHang); + virtual void setP25Params(unsigned int txHang); + virtual void setNXDNParams(unsigned int txHang); + virtual void setAX25Params(int rxTwist, unsigned int txDelay); + virtual void setTransparentDataParams(unsigned int sendFrameType); + + virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch); + virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel); + virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel); + virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost); + + virtual bool open(); + + virtual unsigned int readDStarData(unsigned char* data); + virtual unsigned int readDMRData1(unsigned char* data); + virtual unsigned int readDMRData2(unsigned char* data); + virtual unsigned int readYSFData(unsigned char* data); + virtual unsigned int readP25Data(unsigned char* data); + virtual unsigned int readNXDNData(unsigned char* data); + virtual unsigned int readFMData(unsigned char* data); + virtual unsigned int readAX25Data(unsigned char* data); + virtual unsigned int readTransparentData(unsigned char* data); + + virtual unsigned int readSerial(unsigned char* data, unsigned int length); + + virtual bool hasDStarSpace() const; + virtual bool hasDMRSpace1() const; + virtual bool hasDMRSpace2() const; + virtual bool hasYSFSpace() const; + virtual bool hasP25Space() const; + virtual bool hasNXDNSpace() const; + virtual bool hasPOCSAGSpace() const; + virtual unsigned int getFMSpace() const; + virtual bool hasAX25Space() const; + + virtual bool hasTX() const; + virtual bool hasCD() const; + + virtual bool hasLockout() const; + virtual bool hasError() const; + + virtual bool writeConfig(); + virtual bool writeDStarData(const unsigned char* data, unsigned int length); + virtual bool writeDMRData1(const unsigned char* data, unsigned int length); + virtual bool writeDMRData2(const unsigned char* data, unsigned int length); + virtual bool writeYSFData(const unsigned char* data, unsigned int length); + virtual bool writeP25Data(const unsigned char* data, unsigned int length); + virtual bool writeNXDNData(const unsigned char* data, unsigned int length); + virtual bool writePOCSAGData(const unsigned char* data, unsigned int length); + virtual bool writeFMData(const unsigned char* data, unsigned int length); + virtual bool writeAX25Data(const unsigned char* data, unsigned int length); + + virtual bool writeTransparentData(const unsigned char* data, unsigned int length); + + virtual bool writeDStarInfo(const char* my1, const char* my2, const char* your, const char* type, const char* reflector); + virtual bool writeDMRInfo(unsigned int slotNo, const std::string& src, bool group, const std::string& dst, const char* type); + virtual bool writeYSFInfo(const char* source, const char* dest, const char* type, const char* origin); + virtual bool writeP25Info(const char* source, bool group, unsigned int dest, const char* type); + virtual bool writeNXDNInfo(const char* source, bool group, unsigned int dest, const char* type); + virtual bool writePOCSAGInfo(unsigned int ric, const std::string& message); + virtual bool writeIPInfo(const std::string& address); + + virtual bool writeDMRStart(bool tx); + virtual bool writeDMRShortLC(const unsigned char* lc); + virtual bool writeDMRAbort(unsigned int slotNo); + + virtual bool writeSerial(const unsigned char* data, unsigned int length); + + virtual unsigned char getMode() const; + virtual bool setMode(unsigned char mode); + + virtual bool sendCWId(const std::string& callsign); + + virtual HW_TYPE getHWType() const; + + virtual void clock(unsigned int ms); + + virtual void close(); + +private: + std::string m_port; + unsigned int m_dmrColorCode; + bool m_ysfLoDev; + unsigned int m_ysfTXHang; + unsigned int m_p25TXHang; + unsigned int m_nxdnTXHang; + bool m_duplex; + bool m_rxInvert; + bool m_txInvert; + bool m_pttInvert; + unsigned int m_txDelay; + unsigned int m_dmrDelay; + float m_rxLevel; + float m_cwIdTXLevel; + float m_dstarTXLevel; + float m_dmrTXLevel; + float m_ysfTXLevel; + float m_p25TXLevel; + float m_nxdnTXLevel; + float m_pocsagTXLevel; + float m_fmTXLevel; + float m_ax25TXLevel; + float m_rfLevel; + bool m_trace; + bool m_debug; + unsigned int m_rxFrequency; + unsigned int m_txFrequency; + unsigned int m_pocsagFrequency; + bool m_dstarEnabled; + bool m_dmrEnabled; + bool m_ysfEnabled; + bool m_p25Enabled; + bool m_nxdnEnabled; + bool m_pocsagEnabled; + bool m_fmEnabled; + bool m_ax25Enabled; + int m_rxDCOffset; + int m_txDCOffset; + ISerialPort* m_serial; + unsigned char* m_buffer; + unsigned int m_length; + unsigned int m_offset; + SERIAL_STATE m_state; + unsigned char m_type; + CRingBuffer m_rxDStarData; + CRingBuffer m_txDStarData; + CRingBuffer m_rxDMRData1; + CRingBuffer m_rxDMRData2; + CRingBuffer m_txDMRData1; + CRingBuffer m_txDMRData2; + CRingBuffer m_rxYSFData; + CRingBuffer m_txYSFData; + CRingBuffer m_rxP25Data; + CRingBuffer m_txP25Data; + CRingBuffer m_rxNXDNData; + CRingBuffer m_txNXDNData; + CRingBuffer m_txPOCSAGData; + CRingBuffer m_rxFMData; + CRingBuffer m_txFMData; + CRingBuffer m_rxAX25Data; + CRingBuffer m_txAX25Data; + CRingBuffer m_rxTransparentData; + CRingBuffer m_txTransparentData; + unsigned int m_sendTransparentDataFrameType; + CTimer m_statusTimer; + CTimer m_inactivityTimer; + CTimer m_playoutTimer; + unsigned int m_dstarSpace; + unsigned int m_dmrSpace1; + unsigned int m_dmrSpace2; + unsigned int m_ysfSpace; + unsigned int m_p25Space; + unsigned int m_nxdnSpace; + unsigned int m_pocsagSpace; + unsigned int m_fmSpace; + unsigned int m_ax25Space; + bool m_tx; + bool m_cd; + bool m_lockout; + bool m_error; + unsigned char m_mode; + HW_TYPE m_hwType; + int m_ax25RXTwist; + unsigned int m_ax25TXDelay; + + std::string m_fmCallsign; + unsigned int m_fmCallsignSpeed; + unsigned int m_fmCallsignFrequency; + unsigned int m_fmCallsignTime; + unsigned int m_fmCallsignHoldoff; + float m_fmCallsignHighLevel; + float m_fmCallsignLowLevel; + bool m_fmCallsignAtStart; + bool m_fmCallsignAtEnd; + bool m_fmCallsignAtLatch; + std::string m_fmRfAck; + std::string m_fmExtAck; + unsigned int m_fmAckSpeed; + unsigned int m_fmAckFrequency; + unsigned int m_fmAckMinTime; + unsigned int m_fmAckDelay; + float m_fmAckLevel; + unsigned int m_fmTimeout; + float m_fmTimeoutLevel; + float m_fmCtcssFrequency; + unsigned int m_fmCtcssHighThreshold; + unsigned int m_fmCtcssLowThreshold; + float m_fmCtcssLevel; + unsigned int m_fmKerchunkTime; + bool m_fmKerchunkTX; + unsigned int m_fmHangTime; + bool m_fmUseCOS; + bool m_fmCOSInvert; + unsigned int m_fmRFAudioBoost; + unsigned int m_fmExtAudioBoost; + float m_fmMaxDevLevel; + bool m_fmExtEnable; + + bool readVersion(); + bool readStatus(); + bool setConfig(); + bool setFrequency(); + bool setFMCallsignParams(); + bool setFMAckParams(); + bool setFMMiscParams(); + bool setFMExtParams(); + + void printDebug(); + + RESP_TYPE_MMDVM getResponse(); +}; + +#endif From 4ae141f59463f8a59d555ed44dd5d2f1bc782f90 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Tue, 30 Jun 2020 23:11:00 +0200 Subject: [PATCH 084/163] Fix KISS --- AX25Network.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AX25Network.cpp b/AX25Network.cpp index dec4ea3..d7f876d 100644 --- a/AX25Network.cpp +++ b/AX25Network.cpp @@ -142,7 +142,7 @@ unsigned int CAX25Network::read(unsigned char* data, unsigned int length) data[dataLen++] = AX25_FEND; } else if (c == AX25_TFESC && m_rxLastChar == AX25_FESC) { data[dataLen++] = AX25_FESC; - } else if (c != AX25_FESC) { + } else { data[dataLen++] = c; } From 5f0ee739e134772b0b1e0201a23a9dbc3e67a266 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Tue, 30 Jun 2020 23:19:26 +0200 Subject: [PATCH 085/163] Use correct file --- SerialController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialController.cpp b/SerialController.cpp index 6352743..99fe576 100644 --- a/SerialController.cpp +++ b/SerialController.cpp @@ -234,6 +234,7 @@ m_speed(speed), m_assertRTS(assertRTS), m_fd(-1) { + assert(!device.empty()); } CSerialController::CSerialController(unsigned int speed, bool assertRTS) : @@ -250,7 +251,6 @@ CSerialController::~CSerialController() bool CSerialController::open() { - assert(!m_device.empty()); assert(m_fd == -1); #if defined(__APPLE__) From d2be9202546e28fa316dcba7f8ca7f0c414ff487 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Wed, 1 Jul 2020 10:59:46 +0100 Subject: [PATCH 086/163] Add AX25 SlotTime and P-Persist parameters. --- Conf.cpp | 26 +++++++++++++++++++++----- Conf.h | 8 ++++++-- MMDVM.ini | 2 ++ MMDVMHost.cpp | 16 +++++++++++----- Modem.h | 2 +- NullModem.h | 2 +- SerialModem.cpp | 24 ++++++++++++++---------- SerialModem.h | 4 +++- Version.h | 2 +- 9 files changed, 60 insertions(+), 26 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index 36c8ce3..ae7037a 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -213,8 +213,10 @@ m_fmMaxDevLevel(90.0F), m_fmExtAudioBoost(1U), m_fmModeHang(10U), m_ax25Enabled(false), -m_ax25RXTwist(6), m_ax25TXDelay(300U), +m_ax25RXTwist(6), +m_ax25SlotTime(30U), +m_ax25PPersist(128U), m_ax25Trace(false), m_dstarNetworkEnabled(false), m_dstarGatewayAddress(), @@ -815,10 +817,14 @@ bool CConf::read() } else if (section == SECTION_AX25) { if (::strcmp(key, "Enable") == 0) m_ax25Enabled = ::atoi(value) == 1; - else if (::strcmp(key, "RXTwist") == 0) - m_ax25RXTwist = ::atoi(value); else if (::strcmp(key, "TXDelay") == 0) m_ax25TXDelay = (unsigned int)::atoi(value); + else if (::strcmp(key, "RXTwist") == 0) + m_ax25RXTwist = ::atoi(value); + else if (::strcmp(key, "SlotTime") == 0) + m_ax25SlotTime = (unsigned int)::atoi(value); + else if (::strcmp(key, "PPersist") == 0) + m_ax25PPersist = (unsigned int)::atoi(value); else if (::strcmp(key, "Trace") == 0) m_ax25Trace = ::atoi(value) == 1; } else if (section == SECTION_DSTAR_NETWORK) { @@ -1772,14 +1778,24 @@ bool CConf::getAX25Enabled() const return m_ax25Enabled; } +unsigned int CConf::getAX25TXDelay() const +{ + return m_ax25TXDelay; +} + int CConf::getAX25RXTwist() const { return m_ax25RXTwist; } -unsigned int CConf::getAX25TXDelay() const +unsigned int CConf::getAX25SlotTime() const { - return m_ax25TXDelay; + return m_ax25SlotTime; +} + +unsigned int CConf::getAX25PPersist() const +{ + return m_ax25PPersist; } bool CConf::getAX25Trace() const diff --git a/Conf.h b/Conf.h index cbae96c..dd830c3 100644 --- a/Conf.h +++ b/Conf.h @@ -175,8 +175,10 @@ public: // The AX.25 section bool getAX25Enabled() const; - int getAX25RXTwist() const; unsigned int getAX25TXDelay() const; + int getAX25RXTwist() const; + unsigned int getAX25SlotTime() const; + unsigned int getAX25PPersist() const; bool getAX25Trace() const; // The FM Section @@ -505,8 +507,10 @@ private: unsigned int m_fmModeHang; bool m_ax25Enabled; - int m_ax25RXTwist; unsigned int m_ax25TXDelay; + int m_ax25RXTwist; + unsigned int m_ax25SlotTime; + unsigned int m_ax25PPersist; bool m_ax25Trace; bool m_dstarNetworkEnabled; diff --git a/MMDVM.ini b/MMDVM.ini index 474bcbb..1435956 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -183,6 +183,8 @@ ExtAudioBoost=1 Enable=1 TXDelay=300 RXTwist=6 +SlotTime=30 +PPersist=128 Trace=1 [D-Star Network] diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 93ee9f7..6703f90 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -639,13 +639,17 @@ int CMMDVMHost::run() } if (m_ax25Enabled) { - int rxTwist = m_conf.getAX25RXTwist(); - unsigned int txDelay = m_conf.getAX25TXDelay(); - bool trace = m_conf.getAX25Trace(); + unsigned int txDelay = m_conf.getAX25TXDelay(); + int rxTwist = m_conf.getAX25RXTwist(); + unsigned int slotTime = m_conf.getAX25SlotTime(); + unsigned int pPersist = m_conf.getAX25PPersist(); + bool trace = m_conf.getAX25Trace(); LogInfo("AX.25 RF Parameters"); - LogInfo(" RX Twist: %d", rxTwist); LogInfo(" TX Delay: %ums", txDelay); + LogInfo(" RX Twist: %d", rxTwist); + LogInfo(" Slot Time: %ums", slotTime); + LogInfo(" P-Persist: %u", pPersist); LogInfo(" Trace: %s", trace ? "yes" : "no"); m_ax25 = new CAX25Control(m_ax25Network, trace); @@ -1301,6 +1305,8 @@ bool CMMDVMHost::createModem() float rfLevel = m_conf.getModemRFLevel(); int rxTwist = m_conf.getAX25RXTwist(); unsigned int ax25TXDelay = m_conf.getAX25TXDelay(); + unsigned int ax25SlotTime = m_conf.getAX25SlotTime(); + unsigned int ax25PPersist = m_conf.getAX25PPersist(); LogInfo("Modem Parameters"); LogInfo(" Port: %s", port.c_str()); @@ -1345,7 +1351,7 @@ bool CMMDVMHost::createModem() m_modem->setYSFParams(lowDeviation, ysfTXHang); m_modem->setP25Params(p25TXHang); m_modem->setNXDNParams(nxdnTXHang); - m_modem->setAX25Params(rxTwist, ax25TXDelay); + m_modem->setAX25Params(rxTwist, ax25TXDelay, ax25SlotTime, ax25PPersist); if (m_fmEnabled) { std::string callsign = m_conf.getFMCallsign(); diff --git a/Modem.h b/Modem.h index c408f01..d5fc4b3 100644 --- a/Modem.h +++ b/Modem.h @@ -35,7 +35,7 @@ public: virtual void setYSFParams(bool loDev, unsigned int txHang) = 0; virtual void setP25Params(unsigned int txHang) = 0; virtual void setNXDNParams(unsigned int txHang) = 0; - virtual void setAX25Params(int rxTwist, unsigned int txDelay) = 0; + virtual void setAX25Params(int rxTwist, unsigned int txDelay, unsigned int slotTime, unsigned int pPersist) = 0; virtual void setTransparentDataParams(unsigned int sendFrameType) = 0; virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch) = 0; diff --git a/NullModem.h b/NullModem.h index 12ae460..5ab0dd0 100644 --- a/NullModem.h +++ b/NullModem.h @@ -37,7 +37,7 @@ public: virtual void setYSFParams(bool loDev, unsigned int txHang) {}; virtual void setP25Params(unsigned int txHang) {}; virtual void setNXDNParams(unsigned int txHang) {}; - virtual void setAX25Params(int rxTwist, unsigned int txDelay) {}; + virtual void setAX25Params(int rxTwist, unsigned int txDelay, unsigned int slotTime, unsigned int pPersist) {}; virtual void setTransparentDataParams(unsigned int sendFrameType) {}; virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch) {}; diff --git a/SerialModem.cpp b/SerialModem.cpp index 10ddefa..95f5765 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -194,6 +194,8 @@ m_mode(MODE_IDLE), m_hwType(HWT_UNKNOWN), m_ax25RXTwist(0), m_ax25TXDelay(300U), +m_ax25SlotTime(30U), +m_ax25PPersist(128U), m_fmCallsign(), m_fmCallsignSpeed(20U), m_fmCallsignFrequency(1000U), @@ -308,10 +310,12 @@ void CSerialModem::setNXDNParams(unsigned int txHang) m_nxdnTXHang = txHang; } -void CSerialModem::setAX25Params(int rxTwist, unsigned int txDelay) +void CSerialModem::setAX25Params(int rxTwist, unsigned int txDelay, unsigned int slotTime, unsigned int pPersist) { - m_ax25RXTwist = rxTwist; - m_ax25TXDelay = txDelay; + m_ax25RXTwist = rxTwist; + m_ax25TXDelay = txDelay; + m_ax25SlotTime = slotTime; + m_ax25PPersist = pPersist; } void CSerialModem::setTransparentDataParams(unsigned int sendFrameType) @@ -1757,7 +1761,7 @@ bool CSerialModem::setConfig() buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 27U; + buffer[1U] = 29U; buffer[2U] = MMDVM_SET_CONFIG; @@ -1788,7 +1792,7 @@ bool CSerialModem::setConfig() buffer[4U] |= 0x10U; if (m_pocsagEnabled) buffer[4U] |= 0x20U; - if (m_fmEnabled && m_duplex) + if (m_fmEnabled) buffer[4U] |= 0x40U; if (m_ax25Enabled) buffer[4U] |= 0x80U; @@ -1828,15 +1832,15 @@ bool CSerialModem::setConfig() buffer[23U] = (unsigned char)m_nxdnTXHang; buffer[24U] = (unsigned char)(m_ax25TXLevel * 2.55F + 0.5F); - buffer[25U] = (unsigned char)(m_ax25RXTwist + 128); - buffer[26U] = m_ax25TXDelay / 10U; // In 10ms units + buffer[27U] = m_ax25SlotTime / 10U; // In 10ms units + buffer[28U] = m_ax25PPersist; - // CUtils::dump(1U, "Written", buffer, 27U); + // CUtils::dump(1U, "Written", buffer, 29U); - int ret = m_serial->write(buffer, 27U); - if (ret != 27) + int ret = m_serial->write(buffer, 29U); + if (ret != 29) return false; unsigned int count = 0U; diff --git a/SerialModem.h b/SerialModem.h index e1ef578..9ec4d85 100644 --- a/SerialModem.h +++ b/SerialModem.h @@ -55,7 +55,7 @@ public: virtual void setYSFParams(bool loDev, unsigned int txHang); virtual void setP25Params(unsigned int txHang); virtual void setNXDNParams(unsigned int txHang); - virtual void setAX25Params(int rxTwist, unsigned int txDelay); + virtual void setAX25Params(int rxTwist, unsigned int txDelay, unsigned int slotTime, unsigned int pPersist); virtual void setTransparentDataParams(unsigned int sendFrameType); virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch); @@ -216,6 +216,8 @@ private: HW_TYPE m_hwType; int m_ax25RXTwist; unsigned int m_ax25TXDelay; + unsigned int m_ax25SlotTime; + unsigned int m_ax25PPersist; std::string m_fmCallsign; unsigned int m_fmCallsignSpeed; diff --git a/Version.h b/Version.h index bb444d2..9a9d6e3 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200630"; +const char* VERSION = "20200701"; #endif From 192e8e24356cdd247ca4ca11416a9df37a62603f Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 4 Jul 2020 13:04:24 +0200 Subject: [PATCH 087/163] Fix buffer underrun in FM --- SerialModem.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index 95f5765..25a4ed5 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -248,7 +248,7 @@ void CSerialModem::setSerialParams(const std::string& protocol, unsigned int add m_serial = new CI2CController(m_port, address); else #endif - m_serial = new CSerialController(m_port, speed, true); + m_serial = new CSerialController(m_port, speed, false); } void CSerialModem::setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) @@ -640,7 +640,7 @@ void CSerialModem::clock(unsigned int ms) if (m_trace) CUtils::dump(1U, "RX FM Data", m_buffer, m_length); - unsigned int data1 = m_length - m_offset + 1U; + unsigned int data1 = m_length - m_offset; m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); unsigned char data2 = TAG_DATA; @@ -654,11 +654,11 @@ void CSerialModem::clock(unsigned int ms) if (m_trace) CUtils::dump(1U, "RX FM Control", m_buffer, m_length); - unsigned char data = m_length - m_offset + 1U; - m_rxFMData.addData(&data, 1U); + unsigned int data1 = m_length - m_offset; + m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); - data = TAG_HEADER; - m_rxFMData.addData(&data, 1U); + unsigned char data2 = TAG_DATA; + m_rxFMData.addData(&data2, 1U); m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); } @@ -668,7 +668,7 @@ void CSerialModem::clock(unsigned int ms) if(m_trace) CUtils::dump(1U, "RX FM End of transmission", m_buffer, m_length); - unsigned char data = m_length - m_offset + 1U; + unsigned char data = m_length - m_offset; m_rxFMData.addData(&data, 1U); data = TAG_EOT; From 183430593f085c643f0b168609914c74e45b8a68 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 4 Jul 2020 14:21:11 +0200 Subject: [PATCH 088/163] Revert "Fix buffer underrun in FM" This reverts commit 192e8e24356cdd247ca4ca11416a9df37a62603f. --- SerialModem.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index 25a4ed5..95f5765 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -248,7 +248,7 @@ void CSerialModem::setSerialParams(const std::string& protocol, unsigned int add m_serial = new CI2CController(m_port, address); else #endif - m_serial = new CSerialController(m_port, speed, false); + m_serial = new CSerialController(m_port, speed, true); } void CSerialModem::setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) @@ -640,7 +640,7 @@ void CSerialModem::clock(unsigned int ms) if (m_trace) CUtils::dump(1U, "RX FM Data", m_buffer, m_length); - unsigned int data1 = m_length - m_offset; + unsigned int data1 = m_length - m_offset + 1U; m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); unsigned char data2 = TAG_DATA; @@ -654,11 +654,11 @@ void CSerialModem::clock(unsigned int ms) if (m_trace) CUtils::dump(1U, "RX FM Control", m_buffer, m_length); - unsigned int data1 = m_length - m_offset; - m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); + unsigned char data = m_length - m_offset + 1U; + m_rxFMData.addData(&data, 1U); - unsigned char data2 = TAG_DATA; - m_rxFMData.addData(&data2, 1U); + data = TAG_HEADER; + m_rxFMData.addData(&data, 1U); m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); } @@ -668,7 +668,7 @@ void CSerialModem::clock(unsigned int ms) if(m_trace) CUtils::dump(1U, "RX FM End of transmission", m_buffer, m_length); - unsigned char data = m_length - m_offset; + unsigned char data = m_length - m_offset + 1U; m_rxFMData.addData(&data, 1U); data = TAG_EOT; From f42f6f07c432efc470044443756cbc03854f192c Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 6 Jul 2020 10:16:06 +0100 Subject: [PATCH 089/163] Fix bug. --- SerialModem.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index 95f5765..d745e0a 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -945,7 +945,7 @@ void CSerialModem::clock(unsigned int ms) } if (m_fmSpace > 1U && !m_txFMData.isEmpty()) { - unsigned char len = 0U; + unsigned int len = 0U; m_txFMData.getData(&len, 1U); m_txFMData.getData(m_buffer, len); @@ -962,7 +962,7 @@ void CSerialModem::clock(unsigned int ms) } if (m_ax25Space > 0U && !m_txAX25Data.isEmpty()) { - unsigned char len = 0U; + unsigned int len = 0U; m_txAX25Data.getData((unsigned char*)&len, sizeof(unsigned int)); m_txAX25Data.getData(m_buffer, len); From 3cc77c5c577fe994373cf33ce029021e1a16d6fd Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 6 Jul 2020 10:20:40 +0100 Subject: [PATCH 090/163] Fix bug in bug. --- SerialModem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index d745e0a..6cc873b 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -946,7 +946,7 @@ void CSerialModem::clock(unsigned int ms) if (m_fmSpace > 1U && !m_txFMData.isEmpty()) { unsigned int len = 0U; - m_txFMData.getData(&len, 1U); + m_txFMData.getData((unsigned char*)&len, sizeof(unsigned int)); m_txFMData.getData(m_buffer, len); if (m_trace) From 954231ccbc331c8b9659cdfe9491ae2d2041e8ed Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 6 Jul 2020 10:22:00 +0100 Subject: [PATCH 091/163] Bump the version date. --- Version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version.h b/Version.h index 9a9d6e3..c634cfc 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200701"; +const char* VERSION = "20200706"; #endif From 41dc96affeb03ba6086f6f51571b4e0e3c311916 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 4 Jul 2020 13:04:24 +0200 Subject: [PATCH 092/163] Fix buffer underrun in FM --- SerialModem.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index 6cc873b..3d0cce9 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -248,7 +248,7 @@ void CSerialModem::setSerialParams(const std::string& protocol, unsigned int add m_serial = new CI2CController(m_port, address); else #endif - m_serial = new CSerialController(m_port, speed, true); + m_serial = new CSerialController(m_port, speed, false); } void CSerialModem::setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) @@ -640,7 +640,7 @@ void CSerialModem::clock(unsigned int ms) if (m_trace) CUtils::dump(1U, "RX FM Data", m_buffer, m_length); - unsigned int data1 = m_length - m_offset + 1U; + unsigned int data1 = m_length - m_offset; m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); unsigned char data2 = TAG_DATA; @@ -654,11 +654,11 @@ void CSerialModem::clock(unsigned int ms) if (m_trace) CUtils::dump(1U, "RX FM Control", m_buffer, m_length); - unsigned char data = m_length - m_offset + 1U; - m_rxFMData.addData(&data, 1U); + unsigned int data1 = m_length - m_offset; + m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); - data = TAG_HEADER; - m_rxFMData.addData(&data, 1U); + unsigned char data2 = TAG_DATA; + m_rxFMData.addData(&data2, 1U); m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); } @@ -668,7 +668,7 @@ void CSerialModem::clock(unsigned int ms) if(m_trace) CUtils::dump(1U, "RX FM End of transmission", m_buffer, m_length); - unsigned char data = m_length - m_offset + 1U; + unsigned char data = m_length - m_offset; m_rxFMData.addData(&data, 1U); data = TAG_EOT; From 40a974d7cbf8b880900ca8f08f8c0c4b5a403d27 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 4 Jul 2020 14:21:11 +0200 Subject: [PATCH 093/163] Revert "Fix buffer underrun in FM" This reverts commit 192e8e24356cdd247ca4ca11416a9df37a62603f. --- SerialModem.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index 3d0cce9..6cc873b 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -248,7 +248,7 @@ void CSerialModem::setSerialParams(const std::string& protocol, unsigned int add m_serial = new CI2CController(m_port, address); else #endif - m_serial = new CSerialController(m_port, speed, false); + m_serial = new CSerialController(m_port, speed, true); } void CSerialModem::setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency) @@ -640,7 +640,7 @@ void CSerialModem::clock(unsigned int ms) if (m_trace) CUtils::dump(1U, "RX FM Data", m_buffer, m_length); - unsigned int data1 = m_length - m_offset; + unsigned int data1 = m_length - m_offset + 1U; m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); unsigned char data2 = TAG_DATA; @@ -654,11 +654,11 @@ void CSerialModem::clock(unsigned int ms) if (m_trace) CUtils::dump(1U, "RX FM Control", m_buffer, m_length); - unsigned int data1 = m_length - m_offset; - m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); + unsigned char data = m_length - m_offset + 1U; + m_rxFMData.addData(&data, 1U); - unsigned char data2 = TAG_DATA; - m_rxFMData.addData(&data2, 1U); + data = TAG_HEADER; + m_rxFMData.addData(&data, 1U); m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); } @@ -668,7 +668,7 @@ void CSerialModem::clock(unsigned int ms) if(m_trace) CUtils::dump(1U, "RX FM End of transmission", m_buffer, m_length); - unsigned char data = m_length - m_offset; + unsigned char data = m_length - m_offset + 1U; m_rxFMData.addData(&data, 1U); data = TAG_EOT; From 4d6f64a51eacc60c831051800cf2163313503f9b Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Mon, 6 Jul 2020 19:44:55 +0200 Subject: [PATCH 094/163] Write FM control length as integer, since we always read it as int --- SerialModem.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index 6cc873b..ef29423 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -654,11 +654,11 @@ void CSerialModem::clock(unsigned int ms) if (m_trace) CUtils::dump(1U, "RX FM Control", m_buffer, m_length); - unsigned char data = m_length - m_offset + 1U; - m_rxFMData.addData(&data, 1U); + unsigned int data1 = m_length - m_offset + 1U; + m_rxFMData.addData((unsigned char *)&data1, sizeof(unsigned int)); - data = TAG_HEADER; - m_rxFMData.addData(&data, 1U); + unsigned char data2= TAG_HEADER; + m_rxFMData.addData(&data2, 1U); m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); } From 009f50a9138f3f6e478d1e59c73380787009e620 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Mon, 6 Jul 2020 19:52:16 +0200 Subject: [PATCH 095/163] Write FM_EOT as uint --- SerialModem.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index ef29423..5fe164b 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -655,7 +655,7 @@ void CSerialModem::clock(unsigned int ms) CUtils::dump(1U, "RX FM Control", m_buffer, m_length); unsigned int data1 = m_length - m_offset + 1U; - m_rxFMData.addData((unsigned char *)&data1, sizeof(unsigned int)); + m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); unsigned char data2= TAG_HEADER; m_rxFMData.addData(&data2, 1U); @@ -668,11 +668,11 @@ void CSerialModem::clock(unsigned int ms) if(m_trace) CUtils::dump(1U, "RX FM End of transmission", m_buffer, m_length); - unsigned char data = m_length - m_offset + 1U; - m_rxFMData.addData(&data, 1U); + unsigned int data1 = m_length - m_offset + 1U; + m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); - data = TAG_EOT; - m_rxFMData.addData(&data, 1U); + unsigned char data2 = TAG_EOT; + m_rxFMData.addData(&data2, 1U); m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); } From 1e833d78cfd6dfc227ed7cf5ce7c8f2997f57a10 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 7 Jul 2020 17:54:46 +0100 Subject: [PATCH 096/163] Remove KerchunkTX. --- Conf.cpp | 8 -------- Conf.h | 2 -- MMDVM.ini | 1 - MMDVMHost.cpp | 4 +--- Modem.h | 2 +- NullModem.h | 2 +- SerialModem.cpp | 6 +----- SerialModem.h | 3 +-- Version.h | 2 +- 9 files changed, 6 insertions(+), 24 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index 71976d8..961959e 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -204,7 +204,6 @@ m_fmCTCSSHighThreshold(30U), m_fmCTCSSLowThreshold(20U), m_fmCTCSSLevel(2.0F), m_fmKerchunkTime(0U), -m_fmKerchunkTX(true), m_fmHangTime(7U), m_fmUseCOS(true), m_fmCOSInvert(false), @@ -801,8 +800,6 @@ bool CConf::read() m_fmCTCSSLevel = float(::atof(value)); else if (::strcmp(key, "KerchunkTime") == 0) m_fmKerchunkTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "KerchunkTX") == 0) - m_fmKerchunkTX = ::atoi(value) == 1; else if (::strcmp(key, "HangTime") == 0) m_fmHangTime = (unsigned int)::atoi(value); else if (::strcmp(key, "UseCOS") == 0) @@ -1736,11 +1733,6 @@ unsigned int CConf::getFMKerchunkTime() const return m_fmKerchunkTime; } -bool CConf::getFMKerchunkTX() const -{ - return m_fmKerchunkTX; -} - unsigned int CConf::getFMHangTime() const { return m_fmHangTime; diff --git a/Conf.h b/Conf.h index dd830c3..49ec140 100644 --- a/Conf.h +++ b/Conf.h @@ -207,7 +207,6 @@ public: unsigned int getFMCTCSSLowThreshold() const; float getFMCTCSSLevel() const; unsigned int getFMKerchunkTime() const; - bool getFMKerchunkTX() const; unsigned int getFMHangTime() const; bool getFMUseCOS() const; bool getFMCOSInvert() const; @@ -497,7 +496,6 @@ private: unsigned int m_fmCTCSSLowThreshold; float m_fmCTCSSLevel; unsigned int m_fmKerchunkTime; - bool m_fmKerchunkTX; unsigned int m_fmHangTime; bool m_fmUseCOS; bool m_fmCOSInvert; diff --git a/MMDVM.ini b/MMDVM.ini index 1435956..0439646 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -170,7 +170,6 @@ CTCSSThreshold=30 # CTCSSLowThreshold=20 CTCSSLevel=20 KerchunkTime=0 -KerchunkTX=1 HangTime=7 UseCOS=1 COSInvert=0 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 6703f90..714699a 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -1377,7 +1377,6 @@ bool CMMDVMHost::createModem() unsigned int ctcssLowThreshold = m_conf.getFMCTCSSLowThreshold(); float ctcssLevel = m_conf.getFMCTCSSLevel(); unsigned int kerchunkTime = m_conf.getFMKerchunkTime(); - bool kerchunkTX = m_conf.getFMKerchunkTX(); unsigned int hangTime = m_conf.getFMHangTime(); bool useCOS = m_conf.getFMUseCOS(); bool cosInvert = m_conf.getFMCOSInvert(); @@ -1409,7 +1408,6 @@ bool CMMDVMHost::createModem() LogInfo(" CTCSS Low Threshold: %u", ctcssLowThreshold); LogInfo(" CTCSS Level: %.1f%%", ctcssLevel); LogInfo(" Kerchunk Time: %us", kerchunkTime); - LogInfo(" Kerchunk TX: %s", kerchunkTX ? "yes" : "no"); LogInfo(" Hang Time: %us", hangTime); LogInfo(" Use COS: %s", useCOS ? "yes" : "no"); LogInfo(" COS Invert: %s", cosInvert ? "yes" : "no"); @@ -1419,7 +1417,7 @@ bool CMMDVMHost::createModem() m_modem->setFMCallsignParams(callsign, callsignSpeed, callsignFrequency, callsignTime, callsignHoldoff, callsignHighLevel, callsignLowLevel, callsignAtStart, callsignAtEnd, callsignAtLatch); m_modem->setFMAckParams(rfAck, ackSpeed, ackFrequency, ackMinTime, ackDelay, ackLevel); - m_modem->setFMMiscParams(timeout, timeoutLevel, ctcssFrequency, ctcssHighThreshold, ctcssLowThreshold, ctcssLevel, kerchunkTime, kerchunkTX, hangTime, useCOS, cosInvert, rfAudioBoost, maxDevLevel); + m_modem->setFMMiscParams(timeout, timeoutLevel, ctcssFrequency, ctcssHighThreshold, ctcssLowThreshold, ctcssLevel, kerchunkTime, hangTime, useCOS, cosInvert, rfAudioBoost, maxDevLevel); if (m_conf.getFMNetworkEnabled()) { std::string extAck = m_conf.getFMExtAck(); diff --git a/Modem.h b/Modem.h index d5fc4b3..4fba648 100644 --- a/Modem.h +++ b/Modem.h @@ -40,7 +40,7 @@ public: virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch) = 0; virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel) = 0; - virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) = 0; + virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) = 0; virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost) = 0; virtual bool open() = 0; diff --git a/NullModem.h b/NullModem.h index 5ab0dd0..3f73083 100644 --- a/NullModem.h +++ b/NullModem.h @@ -42,7 +42,7 @@ public: virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch) {}; virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel) {}; - virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) {}; + virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) {}; virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost) {}; virtual bool open(); diff --git a/SerialModem.cpp b/SerialModem.cpp index 5fe164b..37e87dc 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -220,7 +220,6 @@ m_fmCtcssHighThreshold(30U), m_fmCtcssLowThreshold(20U), m_fmCtcssLevel(10.0F), m_fmKerchunkTime(0U), -m_fmKerchunkTX(true), m_fmHangTime(5U), m_fmUseCOS(true), m_fmCOSInvert(false), @@ -2184,7 +2183,7 @@ void CSerialModem::setFMAckParams(const std::string& rfAck, unsigned int ackSpee m_fmAckLevel = ackLevel; } -void CSerialModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) +void CSerialModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) { m_fmTimeout = timeout; m_fmTimeoutLevel = timeoutLevel; @@ -2195,7 +2194,6 @@ void CSerialModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, flo m_fmCtcssLevel = ctcssLevel; m_fmKerchunkTime = kerchunkTime; - m_fmKerchunkTX = kerchunkTX; m_fmHangTime = hangTime; @@ -2352,8 +2350,6 @@ bool CSerialModem::setFMMiscParams() buffer[11U] |= 0x01U; if (m_fmCOSInvert) buffer[11U] |= 0x02U; - if (m_fmKerchunkTX) - buffer[11U] |= 0x04U; buffer[12U] = m_fmRFAudioBoost; diff --git a/SerialModem.h b/SerialModem.h index 9ec4d85..9befe26 100644 --- a/SerialModem.h +++ b/SerialModem.h @@ -60,7 +60,7 @@ public: virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch); virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel); - virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, bool kerchunkTX, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel); + virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, bool useCOS, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel); virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost); virtual bool open(); @@ -243,7 +243,6 @@ private: unsigned int m_fmCtcssLowThreshold; float m_fmCtcssLevel; unsigned int m_fmKerchunkTime; - bool m_fmKerchunkTX; unsigned int m_fmHangTime; bool m_fmUseCOS; bool m_fmCOSInvert; diff --git a/Version.h b/Version.h index c634cfc..a235cfe 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200706"; +const char* VERSION = "20200707"; #endif From c80894ddaa4a75481d8ba5df7519c15de801b5a8 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Wed, 15 Jul 2020 12:31:48 +0100 Subject: [PATCH 097/163] Fix the configuration source file. --- Conf.cpp | 431 ------------------------------------------------------- 1 file changed, 431 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index e19f3e6..380dcea 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -486,181 +486,6 @@ bool CConf::read() for (unsigned int i = 0U; value[i] != 0; i++) value[i] = ::toupper(value[i]); m_cwIdCallsign = value; - } - } else if (section == SECTION_DMRID_LOOKUP) { - if (::strcmp(key, "File") == 0) - m_dmrIdLookupFile = value; - else if (::strcmp(key, "Time") == 0) - m_dmrIdLookupTime = (unsigned int)::atoi(value); - } else if (section == SECTION_NXDNID_LOOKUP) { - if (::strcmp(key, "File") == 0) - m_nxdnIdLookupFile = value; - else if (::strcmp(key, "Time") == 0) - m_nxdnIdLookupTime = (unsigned int)::atoi(value); - } else if (section == SECTION_MODEM) { - if (::strcmp(key, "Port") == 0) - m_modemPort = value; - else if (::strcmp(key, "Protocol") == 0) - m_modemProtocol = value; - else if (::strcmp(key, "Address") == 0) - m_modemAddress = (unsigned int)::strtoul(value, NULL, 16); - else if (::strcmp(key, "RXInvert") == 0) - m_modemRXInvert = ::atoi(value) == 1; - else if (::strcmp(key, "TXInvert") == 0) - m_modemTXInvert = ::atoi(value) == 1; - else if (::strcmp(key, "PTTInvert") == 0) - m_modemPTTInvert = ::atoi(value) == 1; - else if (::strcmp(key, "TXDelay") == 0) - m_modemTXDelay = (unsigned int)::atoi(value); - else if (::strcmp(key, "DMRDelay") == 0) - m_modemDMRDelay = (unsigned int)::atoi(value); - else if (::strcmp(key, "RXOffset") == 0) - m_modemRXOffset = ::atoi(value); - else if (::strcmp(key, "TXOffset") == 0) - m_modemTXOffset = ::atoi(value); - else if (::strcmp(key, "RXDCOffset") == 0) - m_modemRXDCOffset = ::atoi(value); - else if (::strcmp(key, "TXDCOffset") == 0) - m_modemTXDCOffset = ::atoi(value); - else if (::strcmp(key, "RFLevel") == 0) - m_modemRFLevel = float(::atof(value)); - else if (::strcmp(key, "RXLevel") == 0) - m_modemRXLevel = float(::atof(value)); - else if (::strcmp(key, "TXLevel") == 0) - m_modemAX25TXLevel = m_modemFMTXLevel = m_modemCWIdTXLevel = m_modemDStarTXLevel = m_modemDMRTXLevel = m_modemYSFTXLevel = m_modemP25TXLevel = m_modemNXDNTXLevel = float(::atof(value)); - else if (::strcmp(key, "CWIdTXLevel") == 0) - m_modemCWIdTXLevel = float(::atof(value)); - else if (::strcmp(key, "D-StarTXLevel") == 0) - m_modemDStarTXLevel = float(::atof(value)); - else if (::strcmp(key, "DMRTXLevel") == 0) - m_modemDMRTXLevel = float(::atof(value)); - else if (::strcmp(key, "YSFTXLevel") == 0) - m_modemYSFTXLevel = float(::atof(value)); - else if (::strcmp(key, "P25TXLevel") == 0) - m_modemP25TXLevel = float(::atof(value)); - else if (::strcmp(key, "NXDNTXLevel") == 0) - m_modemNXDNTXLevel = float(::atof(value)); - else if (::strcmp(key, "POCSAGTXLevel") == 0) - m_modemPOCSAGTXLevel = float(::atof(value)); - else if (::strcmp(key, "FMTXLevel") == 0) - m_modemFMTXLevel = float(::atof(value)); - else if (::strcmp(key, "RSSIMappingFile") == 0) - m_modemRSSIMappingFile = value; - else if (::strcmp(key, "UseCOSAsLockout") == 0) - m_modemUseCOSAsLockout = ::atoi(value) == 1; - else if (::strcmp(key, "Trace") == 0) - m_modemTrace = ::atoi(value) == 1; - else if (::strcmp(key, "Debug") == 0) - m_modemDebug = ::atoi(value) == 1; - } else if (section == SECTION_TRANSPARENT) { - if (::strcmp(key, "Enable") == 0) - m_transparentEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "RemoteAddress") == 0) - m_transparentRemoteAddress = value; - else if (::strcmp(key, "RemotePort") == 0) - m_transparentRemotePort = (unsigned int)::atoi(value); - else if (::strcmp(key, "LocalPort") == 0) - m_transparentLocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "SendFrameType") == 0) - m_transparentSendFrameType = (unsigned int)::atoi(value); - } else if (section == SECTION_UMP) { - if (::strcmp(key, "Enable") == 0) - m_umpEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Port") == 0) - m_umpPort = value; - } else if (section == SECTION_DSTAR) { - if (::strcmp(key, "Enable") == 0) - m_dstarEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Module") == 0) { - // Convert the module to upper case - for (unsigned int i = 0U; value[i] != 0; i++) - value[i] = ::toupper(value[i]); - m_dstarModule = value; - } else if (::strcmp(key, "SelfOnly") == 0) - m_dstarSelfOnly = ::atoi(value) == 1; - else if (::strcmp(key, "BlackList") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - if (::strlen(p) > 0U) { - for (unsigned int i = 0U; p[i] != 0; i++) - p[i] = ::toupper(p[i]); - std::string callsign = std::string(p); - callsign.resize(DSTAR_LONG_CALLSIGN_LENGTH, ' '); - m_dstarBlackList.push_back(callsign); - } - p = ::strtok(NULL, ",\r\n"); - } - } else if (::strcmp(key, "AckReply") == 0) - m_dstarAckReply = ::atoi(value) == 1; - else if (::strcmp(key, "AckTime") == 0) - m_dstarAckTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "AckMessage") == 0) - m_dstarAckMessage = ::atoi(value) == 1; - else if (::strcmp(key, "ErrorReply") == 0) - m_dstarErrorReply = ::atoi(value) == 1; - else if (::strcmp(key, "RemoteGateway") == 0) - m_dstarRemoteGateway = ::atoi(value) == 1; - else if (::strcmp(key, "ModeHang") == 0) - m_dstarModeHang = (unsigned int)::atoi(value); - } else if (section == SECTION_DMR) { - if (::strcmp(key, "Enable") == 0) - m_dmrEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Beacons") == 0) - m_dmrBeacons = ::atoi(value) == 1 ? DMR_BEACONS_NETWORK : DMR_BEACONS_OFF; - else if (::strcmp(key, "BeaconInterval") == 0) { - m_dmrBeacons = m_dmrBeacons != DMR_BEACONS_OFF ? DMR_BEACONS_TIMED : DMR_BEACONS_OFF; - m_dmrBeaconInterval = (unsigned int)::atoi(value); - } else if (::strcmp(key, "BeaconDuration") == 0) - m_dmrBeaconDuration = (unsigned int)::atoi(value); - else if (::strcmp(key, "Id") == 0) - m_dmrId = (unsigned int)::atoi(value); - else if (::strcmp(key, "ColorCode") == 0) - m_dmrColorCode = (unsigned int)::atoi(value); - else if (::strcmp(key, "SelfOnly") == 0) - m_dmrSelfOnly = ::atoi(value) == 1; - else if (::strcmp(key, "EmbeddedLCOnly") == 0) - m_dmrEmbeddedLCOnly = ::atoi(value) == 1; - else if (::strcmp(key, "DumpTAData") == 0) - m_dmrDumpTAData = ::atoi(value) == 1; - else if (::strcmp(key, "Prefixes") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int prefix = (unsigned int)::atoi(p); - if (prefix > 0U && prefix <= 999U) - m_dmrPrefixes.push_back(prefix); - p = ::strtok(NULL, ",\r\n"); - } - } else if (::strcmp(key, "BlackList") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int id = (unsigned int)::atoi(p); - if (id > 0U) - m_dmrBlackList.push_back(id); - p = ::strtok(NULL, ",\r\n"); - } - } else if (::strcmp(key, "WhiteList") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int id = (unsigned int)::atoi(p); - if (id > 0U) - m_dmrWhiteList.push_back(id); - p = ::strtok(NULL, ",\r\n"); - } - } else if (::strcmp(key, "Slot1TGWhiteList") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int id = (unsigned int)::atoi(p); - if (id > 0U) - m_dmrSlot1TGWhiteList.push_back(id); - p = ::strtok(NULL, ",\r\n"); - } - } else if (::strcmp(key, "Slot2TGWhiteList") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int id = (unsigned int)::atoi(p); - if (id > 0U) - m_dmrSlot2TGWhiteList.push_back(id); - p = ::strtok(NULL, ",\r\n"); } } else if (section == SECTION_DMRID_LOOKUP) { if (::strcmp(key, "File") == 0) @@ -1152,262 +977,6 @@ bool CConf::read() m_hd44780Pins.push_back(pin); p = ::strtok(NULL, ",\r\n"); } - - } else if (section == SECTION_FUSION) { - if (::strcmp(key, "Enable") == 0) - m_fusionEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "LowDeviation") == 0) - m_fusionLowDeviation = ::atoi(value) == 1; - else if (::strcmp(key, "DGID") == 0) { - m_fusionDGIdEnabled = true; - m_fusionDGId = (unsigned int)::atoi(value); - } else if (::strcmp(key, "RemoteGateway") == 0) - m_fusionRemoteGateway = ::atoi(value) == 1; - else if (::strcmp(key, "SelfOnly") == 0) - m_fusionSelfOnly = ::atoi(value) == 1; - else if (::strcmp(key, "TXHang") == 0) - m_fusionTXHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_fusionModeHang = (unsigned int)::atoi(value); - } else if (section == SECTION_P25) { - if (::strcmp(key, "Enable") == 0) - m_p25Enabled = ::atoi(value) == 1; - else if (::strcmp(key, "Id") == 0) - m_p25Id = (unsigned int)::atoi(value); - else if (::strcmp(key, "NAC") == 0) - m_p25NAC = (unsigned int)::strtoul(value, NULL, 16); - else if (::strcmp(key, "OverrideUIDCheck") == 0) - m_p25OverrideUID = ::atoi(value) == 1; - else if (::strcmp(key, "SelfOnly") == 0) - m_p25SelfOnly = ::atoi(value) == 1; - else if (::strcmp(key, "RemoteGateway") == 0) - m_p25RemoteGateway = ::atoi(value) == 1; - else if (::strcmp(key, "TXHang") == 0) - m_p25TXHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_p25ModeHang = (unsigned int)::atoi(value); - } else if (section == SECTION_NXDN) { - if (::strcmp(key, "Enable") == 0) - m_nxdnEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Id") == 0) - m_nxdnId = (unsigned int)::atoi(value); - else if (::strcmp(key, "RAN") == 0) - m_nxdnRAN = (unsigned int)::atoi(value); - else if (::strcmp(key, "SelfOnly") == 0) - m_nxdnSelfOnly = ::atoi(value) == 1; - else if (::strcmp(key, "RemoteGateway") == 0) - m_nxdnRemoteGateway = ::atoi(value) == 1; - else if (::strcmp(key, "TXHang") == 0) - m_nxdnTXHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_nxdnModeHang = (unsigned int)::atoi(value); - } else if (section == SECTION_POCSAG) { - if (::strcmp(key, "Enable") == 0) - m_pocsagEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Frequency") == 0) - m_pocsagFrequency = (unsigned int)::atoi(value); - } - else if (section == SECTION_FM) { - if (::strcmp(key, "Enable") == 0) - m_fmEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Callsign") == 0) { - // Convert the callsign to upper case - for (unsigned int i = 0U; value[i] != 0; i++) - value[i] = ::toupper(value[i]); - m_fmCallsign = value; - } else if (::strcmp(key, "CallsignSpeed") == 0) - m_fmCallsignSpeed = (unsigned int)::atoi(value); - else if (::strcmp(key, "CallsignFrequency") == 0) - m_fmCallsignFrequency = (unsigned int)::atoi(value); - else if (::strcmp(key, "CallsignTime") == 0) - m_fmCallsignTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "CallsignHoldoff") == 0) - m_fmCallsignHoldoff = (unsigned int)::atoi(value); - else if (::strcmp(key, "CallsignHighLevel") == 0) - m_fmCallsignHighLevel = float(::atof(value)); - else if (::strcmp(key, "CallsignLowLevel") == 0) - m_fmCallsignLowLevel = float(::atof(value)); - else if (::strcmp(key, "CallsignAtStart") == 0) - m_fmCallsignAtStart = ::atoi(value) == 1; - else if (::strcmp(key, "CallsignAtEnd") == 0) - m_fmCallsignAtEnd = ::atoi(value) == 1; - else if (::strcmp(key, "CallsignAtLatch") == 0) - m_fmCallsignAtLatch = ::atoi(value) == 1; - else if (::strcmp(key, "RFAck") == 0) { - // Convert the ack to upper case - for (unsigned int i = 0U; value[i] != 0; i++) - value[i] = ::toupper(value[i]); - m_fmRFAck = value; - } else if (::strcmp(key, "ExtAck") == 0) { - // Convert the ack to upper case - for (unsigned int i = 0U; value[i] != 0; i++) - value[i] = ::toupper(value[i]); - m_fmExtAck = value; - } else if (::strcmp(key, "AckSpeed") == 0) - m_fmAckSpeed = (unsigned int)::atoi(value); - else if (::strcmp(key, "AckFrequency") == 0) - m_fmAckFrequency = (unsigned int)::atoi(value); - else if (::strcmp(key, "AckMinTime") == 0) - m_fmAckMinTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "AckDelay") == 0) - m_fmAckDelay = (unsigned int)::atoi(value); - else if (::strcmp(key, "AckLevel") == 0) - m_fmAckLevel = float(::atof(value)); - else if (::strcmp(key, "Timeout") == 0) - m_fmTimeout = (unsigned int)::atoi(value); - else if (::strcmp(key, "TimeoutLevel") == 0) - m_fmTimeoutLevel = float(::atof(value)); - else if (::strcmp(key, "CTCSSFrequency") == 0) - m_fmCTCSSFrequency = float(::atof(value)); - else if (::strcmp(key, "CTCSSThreshold") == 0) - m_fmCTCSSHighThreshold = m_fmCTCSSLowThreshold = (unsigned int)::atoi(value); - else if (::strcmp(key, "CTCSSHighThreshold") == 0) - m_fmCTCSSHighThreshold = (unsigned int)::atoi(value); - else if (::strcmp(key, "CTCSSLowThreshold") == 0) - m_fmCTCSSLowThreshold = (unsigned int)::atoi(value); - else if (::strcmp(key, "CTCSSLevel") == 0) - m_fmCTCSSLevel = float(::atof(value)); - else if (::strcmp(key, "KerchunkTime") == 0) - m_fmKerchunkTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "HangTime") == 0) - m_fmHangTime = (unsigned int)::atoi(value); - else if (::strcmp(key, "AccessMode") == 0) - m_fmAccessMode = (unsigned int)::atoi(value); - else if (::strcmp(key, "COSInvert") == 0) - m_fmCOSInvert = ::atoi(value) == 1; - else if (::strcmp(key, "RFAudioBoost") == 0) - m_fmRFAudioBoost = (unsigned int)::atoi(value); - else if (::strcmp(key, "MaxDevLevel") == 0) - m_fmMaxDevLevel = float(::atof(value)); - else if (::strcmp(key, "ExtAudioBoost") == 0) - m_fmExtAudioBoost = (unsigned int)::atoi(value); - } else if (section == SECTION_DSTAR_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_dstarNetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "GatewayAddress") == 0) - m_dstarGatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_dstarGatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "LocalPort") == 0) - m_dstarLocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_dstarNetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_dstarNetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_DMR_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_dmrNetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Address") == 0) - m_dmrNetworkAddress = value; - else if (::strcmp(key, "Port") == 0) - m_dmrNetworkPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "Local") == 0) - m_dmrNetworkLocal = (unsigned int)::atoi(value); - else if (::strcmp(key, "Password") == 0) - m_dmrNetworkPassword = value; - else if (::strcmp(key, "Options") == 0) - m_dmrNetworkOptions = value; - else if (::strcmp(key, "Debug") == 0) - m_dmrNetworkDebug = ::atoi(value) == 1; - else if (::strcmp(key, "Jitter") == 0) - m_dmrNetworkJitter = (unsigned int)::atoi(value); - else if (::strcmp(key, "Slot1") == 0) - m_dmrNetworkSlot1 = ::atoi(value) == 1; - else if (::strcmp(key, "Slot2") == 0) - m_dmrNetworkSlot2 = ::atoi(value) == 1; - else if (::strcmp(key, "ModeHang") == 0) - m_dmrNetworkModeHang = (unsigned int)::atoi(value); - } else if (section == SECTION_FUSION_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_fusionNetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "LocalAddress") == 0) - m_fusionNetworkMyAddress = value; - else if (::strcmp(key, "LocalPort") == 0) - m_fusionNetworkMyPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "GatewayAddress") == 0) - m_fusionNetworkGatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_fusionNetworkGatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_fusionNetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_fusionNetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_P25_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_p25NetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "GatewayAddress") == 0) - m_p25GatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_p25GatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "LocalPort") == 0) - m_p25LocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_p25NetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_p25NetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_NXDN_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_nxdnNetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Protocol") == 0) - m_nxdnNetworkProtocol = value; - else if (::strcmp(key, "LocalAddress") == 0) - m_nxdnLocalAddress = value; - else if (::strcmp(key, "LocalPort") == 0) - m_nxdnLocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "GatewayAddress") == 0) - m_nxdnGatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_nxdnGatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_nxdnNetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_nxdnNetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_POCSAG_NETWORK) { - if (::strcmp(key, "Enable") == 0) - m_pocsagNetworkEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "LocalAddress") == 0) - m_pocsagLocalAddress = value; - else if (::strcmp(key, "LocalPort") == 0) - m_pocsagLocalPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "GatewayAddress") == 0) - m_pocsagGatewayAddress = value; - else if (::strcmp(key, "GatewayPort") == 0) - m_pocsagGatewayPort = (unsigned int)::atoi(value); - else if (::strcmp(key, "ModeHang") == 0) - m_pocsagNetworkModeHang = (unsigned int)::atoi(value); - else if (::strcmp(key, "Debug") == 0) - m_pocsagNetworkDebug = ::atoi(value) == 1; - } else if (section == SECTION_TFTSERIAL) { - if (::strcmp(key, "Port") == 0) - m_tftSerialPort = value; - else if (::strcmp(key, "Brightness") == 0) - m_tftSerialBrightness = (unsigned int)::atoi(value); - } else if (section == SECTION_HD44780) { - if (::strcmp(key, "Rows") == 0) - m_hd44780Rows = (unsigned int)::atoi(value); - else if (::strcmp(key, "Columns") == 0) - m_hd44780Columns = (unsigned int)::atoi(value); - else if (::strcmp(key, "I2CAddress") == 0) - m_hd44780i2cAddress = (unsigned int)::strtoul(value, NULL, 16); - else if (::strcmp(key, "PWM") == 0) - m_hd44780PWM = ::atoi(value) == 1; - else if (::strcmp(key, "PWMPin") == 0) - m_hd44780PWMPin = (unsigned int)::atoi(value); - else if (::strcmp(key, "PWMBright") == 0) - m_hd44780PWMBright = (unsigned int)::atoi(value); - else if (::strcmp(key, "PWMDim") == 0) - m_hd44780PWMDim = (unsigned int)::atoi(value); - else if (::strcmp(key, "DisplayClock") == 0) - m_hd44780DisplayClock = ::atoi(value) == 1; - else if (::strcmp(key, "UTC") == 0) - m_hd44780UTC = ::atoi(value) == 1; - else if (::strcmp(key, "Pins") == 0) { - char* p = ::strtok(value, ",\r\n"); - while (p != NULL) { - unsigned int pin = (unsigned int)::atoi(p); - m_hd44780Pins.push_back(pin); - p = ::strtok(NULL, ",\r\n"); - } } else if (section == SECTION_NEXTION) { if (::strcmp(key, "Port") == 0) From 87b7a7c48a7b8b8942debf6fc4bdf84ef43cc62b Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 18 Jul 2020 17:36:54 +0200 Subject: [PATCH 098/163] Moved existing scripts to pi-star folder --- linux/{ => pi-star}/init/README.md | 0 linux/{ => pi-star}/init/mmdvmhost | 0 linux/{ => pi-star}/systemd/README.md | 0 linux/{ => pi-star}/systemd/mmdvmhost.service | 0 linux/{ => pi-star}/systemd/mmdvmhost.timer | 0 linux/{ => pi-star}/systemd/mmdvmhost_service | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename linux/{ => pi-star}/init/README.md (100%) rename linux/{ => pi-star}/init/mmdvmhost (100%) rename linux/{ => pi-star}/systemd/README.md (100%) rename linux/{ => pi-star}/systemd/mmdvmhost.service (100%) rename linux/{ => pi-star}/systemd/mmdvmhost.timer (100%) rename linux/{ => pi-star}/systemd/mmdvmhost_service (100%) diff --git a/linux/init/README.md b/linux/pi-star/init/README.md similarity index 100% rename from linux/init/README.md rename to linux/pi-star/init/README.md diff --git a/linux/init/mmdvmhost b/linux/pi-star/init/mmdvmhost similarity index 100% rename from linux/init/mmdvmhost rename to linux/pi-star/init/mmdvmhost diff --git a/linux/systemd/README.md b/linux/pi-star/systemd/README.md similarity index 100% rename from linux/systemd/README.md rename to linux/pi-star/systemd/README.md diff --git a/linux/systemd/mmdvmhost.service b/linux/pi-star/systemd/mmdvmhost.service similarity index 100% rename from linux/systemd/mmdvmhost.service rename to linux/pi-star/systemd/mmdvmhost.service diff --git a/linux/systemd/mmdvmhost.timer b/linux/pi-star/systemd/mmdvmhost.timer similarity index 100% rename from linux/systemd/mmdvmhost.timer rename to linux/pi-star/systemd/mmdvmhost.timer diff --git a/linux/systemd/mmdvmhost_service b/linux/pi-star/systemd/mmdvmhost_service similarity index 100% rename from linux/systemd/mmdvmhost_service rename to linux/pi-star/systemd/mmdvmhost_service From 76033dd30174f8c7ab3f4378ca1eecf6556bf287 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 18 Jul 2020 18:07:10 +0200 Subject: [PATCH 099/163] Add systemd unit --- linux/systemd/mmdvmhost.service | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 linux/systemd/mmdvmhost.service diff --git a/linux/systemd/mmdvmhost.service b/linux/systemd/mmdvmhost.service new file mode 100644 index 0000000..0587683 --- /dev/null +++ b/linux/systemd/mmdvmhost.service @@ -0,0 +1,12 @@ +[Unit] +Description=MMDVMHost Radio Servce +After=syslog.target network.target + +[Service] +User=mmdvm +Type=forking +ExecStart=/usr/bin/local/MMDVMHost +Restart=on-abnormal + +[Install] +WantedBy=multi-user.target From ba9c97289fbb9b4b800971543bc44be2088dd573 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 18 Jul 2020 18:07:21 +0200 Subject: [PATCH 100/163] add install targets --- Makefile | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 91fcf2d..ccc4eef 100644 --- a/Makefile +++ b/Makefile @@ -35,10 +35,31 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h From bb1a542fe1ba0372f1fc7c2a9425cb76c432b52d Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Mon, 20 Jul 2020 08:06:29 +0200 Subject: [PATCH 101/163] delete unit file on uninstall --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index ccc4eef..96a6b3e 100644 --- a/Makefile +++ b/Makefile @@ -55,10 +55,12 @@ install-service: install /etc/MMDVM.ini @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini @chown mmdvm:mmdvm /etc/MMDVM.ini +.PHONY uninstall-service: uninstall-service: @systemctl stop mmdvmhost.service || true @systemctl disable mmdvmhost.service || true @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h From 49a4e2ad0d5880cced428289ecec45017adf3702 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Mon, 20 Jul 2020 08:13:27 +0200 Subject: [PATCH 102/163] fix typo in executable path --- linux/systemd/mmdvmhost.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/systemd/mmdvmhost.service b/linux/systemd/mmdvmhost.service index 0587683..7ab6aa1 100644 --- a/linux/systemd/mmdvmhost.service +++ b/linux/systemd/mmdvmhost.service @@ -5,7 +5,7 @@ After=syslog.target network.target [Service] User=mmdvm Type=forking -ExecStart=/usr/bin/local/MMDVMHost +ExecStart=/usr/local/bin/MMDVMHost Restart=on-abnormal [Install] From fa7be67d9c920aa62f8499b8fe3f7f41cef04c5a Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Mon, 20 Jul 2020 08:36:22 +0200 Subject: [PATCH 103/163] Add install service to othe rmakefiles --- Makefile.Pi | 25 ++++++++++++++++++++++++- Makefile.Pi.Adafruit | 25 ++++++++++++++++++++++++- Makefile.Pi.HD44780 | 25 ++++++++++++++++++++++++- Makefile.Pi.OLED | 25 ++++++++++++++++++++++++- Makefile.Pi.PCF8574 | 25 ++++++++++++++++++++++++- 5 files changed, 120 insertions(+), 5 deletions(-) diff --git a/Makefile.Pi b/Makefile.Pi index 62e27aa..7909ef4 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -34,10 +34,33 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +.PHONY uninstall-service: +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 2915c69..9af13af 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -35,10 +35,33 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +.PHONY uninstall-service: +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index c00e58f..eae303b 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -35,10 +35,33 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +.PHONY uninstall-service: +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index 41e837e..08ec63f 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -35,10 +35,33 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +.PHONY uninstall-service: +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index 37de0b1..4ebf3fc 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -36,10 +36,33 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +.PHONY uninstall-service: +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h From 801ba04e0d7a01ac440142d614eed0e51876ddf3 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Mon, 20 Jul 2020 09:08:58 +0200 Subject: [PATCH 104/163] Correct typo --- linux/systemd/mmdvmhost.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/systemd/mmdvmhost.service b/linux/systemd/mmdvmhost.service index 7ab6aa1..27ca4ee 100644 --- a/linux/systemd/mmdvmhost.service +++ b/linux/systemd/mmdvmhost.service @@ -1,5 +1,5 @@ [Unit] -Description=MMDVMHost Radio Servce +Description=MMDVMHost Radio Service After=syslog.target network.target [Service] From da077c73f8ef929eb6e7616a723687e1cd93a808 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 27 Jul 2020 10:38:07 +0100 Subject: [PATCH 105/163] Allow the FM network sample rate to be specified. --- Conf.cpp | 8 ++++ Conf.h | 2 + FMControl.cpp | 22 ++++------ FMControl.h | 10 ++--- FMNetwork.cpp | 115 ++++++++++++++++++++++++++++++++++++++++++-------- FMNetwork.h | 11 +++-- MMDVM.ini | 1 + MMDVMHost.cpp | 4 +- Version.h | 2 +- 9 files changed, 134 insertions(+), 41 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index 380dcea..1ea5cc5 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -268,6 +268,7 @@ m_fmGatewayAddress(), m_fmGatewayPort(0U), m_fmLocalAddress(), m_fmLocalPort(0U), +m_fmSampleRate(8000U), m_fmNetworkModeHang(3U), m_fmNetworkDebug(false), m_ax25NetworkEnabled(false), @@ -933,6 +934,8 @@ bool CConf::read() m_fmGatewayAddress = value; else if (::strcmp(key, "GatewayPort") == 0) m_fmGatewayPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "SampleRate") == 0) + m_fmSampleRate = (unsigned int)::atoi(value); else if (::strcmp(key, "ModeHang") == 0) m_fmNetworkModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "Debug") == 0) @@ -2054,6 +2057,11 @@ unsigned int CConf::getFMLocalPort() const return m_fmLocalPort; } +unsigned int CConf::getFMSampleRate() const +{ + return m_fmSampleRate; +} + unsigned int CConf::getFMNetworkModeHang() const { return m_fmNetworkModeHang; diff --git a/Conf.h b/Conf.h index 095915d..01ef9f5 100644 --- a/Conf.h +++ b/Conf.h @@ -279,6 +279,7 @@ public: unsigned int getFMGatewayPort() const; std::string getFMLocalAddress() const; unsigned int getFMLocalPort() const; + unsigned int getFMSampleRate() const; unsigned int getFMNetworkModeHang() const; bool getFMNetworkDebug() const; @@ -569,6 +570,7 @@ private: unsigned int m_fmGatewayPort; std::string m_fmLocalAddress; unsigned int m_fmLocalPort; + unsigned int m_fmSampleRate; unsigned int m_fmNetworkModeHang; bool m_fmNetworkDebug; diff --git a/FMControl.cpp b/FMControl.cpp index 8a8f133..c07c8d4 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -89,7 +89,7 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) unsigned int pack = 0U; unsigned char* packPointer = (unsigned char*)&pack; - unsigned short out[168U]; + float out[168U]; unsigned int nOut = 0U; short unpackedSamples[2U]; @@ -102,28 +102,24 @@ bool CFMControl::writeModem(const unsigned char* data, unsigned int length) unpackedSamples[0U] = short(int(pack >> 12) - 2048); //process unpacked sample pair - for(unsigned char j = 0U; j < 2U; j++) { - //Convert to float (-1.0 to +1.0) + for (unsigned char j = 0U; j < 2U; j++) { + // Convert to float (-1.0 to +1.0) float sampleFloat = float(unpackedSamples[j]) / 2048.0F; - //De-emphasise and remove CTCSS + // De-emphasise and remove CTCSS sampleFloat = m_deemphasis->filter(sampleFloat); - sampleFloat = m_filterStage3->filter(m_filterStage2->filter(m_filterStage1->filter(sampleFloat))); - - // Repack the float data to 16 bit unsigned - unsigned short sampleUShort = (unsigned short)((sampleFloat + 1.0F) * 32767.0F + 0.5F); - out[nOut++] = SWAP_BYTES_16(sampleUShort); + out[nOut++] = m_filterStage3->filter(m_filterStage2->filter(m_filterStage1->filter(sampleFloat))); } } #if defined(DUMP_RF_AUDIO) FILE * audiofile = fopen("./audiodump.bin", "ab"); if(audiofile != NULL) { - fwrite(out, sizeof(unsigned short), nOut, audiofile); + fwrite(out, sizeof(float), nOut, audiofile); fclose(audiofile); } #endif - return m_network->writeData((unsigned char*)out, nOut * sizeof(unsigned short)); + return m_network->writeData(out, nOut); } return true; @@ -140,7 +136,7 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) if (space > 252U) space = 252U; - unsigned short netData[168U];//modem can handle up to 168 samples at a time + unsigned short netData[168U]; // Modem can handle up to 168 samples at a time unsigned int length = m_network->read((unsigned char*)netData, 168U * sizeof(unsigned short)); length /= sizeof(unsigned short); if (length == 0U) @@ -150,7 +146,7 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) unsigned char* packPointer = (unsigned char*)&pack; unsigned int nData = 0U; - for(unsigned int i = 0; i < length; i++) { + for (unsigned int i = 0; i < length; i++) { unsigned short netSample = SWAP_BYTES_16(netData[i]); // Convert the unsigned 16-bit data (+65535 - 0) to float (+1.0 - -1.0) diff --git a/FMControl.h b/FMControl.h index 1647010..ac2142e 100644 --- a/FMControl.h +++ b/FMControl.h @@ -45,11 +45,11 @@ private: CFMNetwork* m_network; bool m_enabled; CRingBuffer m_incomingRFAudio; - CIIRDirectForm1Filter * m_preemphasis; - CIIRDirectForm1Filter * m_deemphasis; - CIIRDirectForm1Filter * m_filterStage1; - CIIRDirectForm1Filter * m_filterStage2; - CIIRDirectForm1Filter * m_filterStage3; + CIIRDirectForm1Filter* m_preemphasis; + CIIRDirectForm1Filter* m_deemphasis; + CIIRDirectForm1Filter* m_filterStage1; + CIIRDirectForm1Filter* m_filterStage2; + CIIRDirectForm1Filter* m_filterStage3; }; #endif diff --git a/FMNetwork.cpp b/FMNetwork.cpp index 67d0f2e..afc2014 100644 --- a/FMNetwork.cpp +++ b/FMNetwork.cpp @@ -27,10 +27,11 @@ const unsigned int BUFFER_LENGTH = 500U; -CFMNetwork::CFMNetwork(const std::string& localAddress, unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, bool debug) : +CFMNetwork::CFMNetwork(const std::string& localAddress, unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, unsigned int sampleRate, bool debug) : m_socket(localAddress, localPort), m_address(), m_port(gatewayPort), +m_sampleRate(sampleRate), m_debug(debug), m_enabled(false), m_buffer(2000U, "FM Network"), @@ -38,12 +39,22 @@ m_pollTimer(1000U, 5U) { assert(gatewayPort > 0U); assert(!gatewayAddress.empty()); + assert(sampleRate > 0U); m_address = CUDPSocket::lookup(gatewayAddress); + + int error; + m_incoming = ::src_new(SRC_SINC_FASTEST, 1, &error); + m_outgoing = ::src_new(SRC_SINC_FASTEST, 1, &error); + + assert(m_incoming != NULL); + assert(m_outgoing != NULL); } CFMNetwork::~CFMNetwork() { + ::src_delete(m_incoming); + ::src_delete(m_outgoing); } bool CFMNetwork::open() @@ -58,23 +69,53 @@ bool CFMNetwork::open() return m_socket.open(); } -bool CFMNetwork::writeData(const unsigned char* data, unsigned int length) +bool CFMNetwork::writeData(const float* data, unsigned int nSamples) { + assert(m_outgoing != NULL); assert(data != NULL); + assert(nSamples > 0U); - unsigned char buffer[500U]; - ::memset(buffer, 0x00U, 500U); + float out[1000U]; + SRC_DATA src; + + if (m_sampleRate != 8000U) { + src.data_in = data; + src.data_out = out; + src.input_frames = nSamples; + src.output_frames = 1000; + src.end_of_input = 0; + src.src_ratio = double(m_sampleRate) / 8000.0; + + int ret = ::src_process(m_outgoing, &src); + if (ret != 0) { + LogError("Error up/downsampling of the output audio has an error - %s", src_strerror(ret)); + return false; + } + } else { + src.data_out = data; + src.output_frames_gen = nSamples; + } + + unsigned int length = 3U; + + unsigned char buffer[1500U]; + ::memset(buffer, 0x00U, 1500U); buffer[0U] = 'F'; buffer[1U] = 'M'; buffer[2U] = 'D'; - ::memcpy(buffer + 3U, data, length); + for (long i = 0L; i < src.output_frames_gen; i++) { + unsigned short val = (unsigned short)((src.data_out[i] + 1.0F) * 32767.0F + 0.5F); + + buffer[length++] = (val >> 8) & 0xFFU; + buffer[length++] = (val >> 0) & 0xFFU; + } if (m_debug) - CUtils::dump(1U, "FM Network Data Sent", buffer, length + 3U); + CUtils::dump(1U, "FM Network Data Sent", buffer, length); - return m_socket.write(buffer, length + 3U, m_address, m_port); + return m_socket.write(buffer, length, m_address, m_port); } bool CFMNetwork::writeEOT() @@ -131,29 +172,67 @@ void CFMNetwork::clock(unsigned int ms) m_buffer.addData(buffer + 3U, length - 3U); } -unsigned int CFMNetwork::read(unsigned char* data, unsigned int space) +unsigned int CFMNetwork::read(float* data, unsigned int nSamples) { + assert(m_incoming != NULL); assert(data != NULL); + assert(nSamples > 0U); - - unsigned int bytes = m_buffer.dataSize(); + unsigned int bytes = m_buffer.dataSize() / sizeof(unsigned short); if (bytes == 0U) return 0U; - if (bytes < space) - space = bytes; + if (bytes < nSamples) + nSamples = bytes; - //we store usignedshorts, therefore ensure we always return and even number of data - if(space > 0 && space % 2 != 0) - space--;//round down to multiple of 2 + unsigned char buffer[1500U]; + m_buffer.getData(buffer, nSamples * sizeof(unsigned short)); - m_buffer.getData(data, space); + SRC_DATA src; - return space; + if (m_sampleRate != 8000U) { + float in[750U]; + + unsigned int j = 0U; + for (unsigned int i = 0U; i < nSamples; i++) { + unsigned short val = ((buffer[j++] & 0xFFU) << 8) + ((buffer[j++] & 0xFFU) << 0); + in[i] = (float(val) - 32768.0F) / 32768.0F; + } + + src.data_in = in; + src.data_out = data; + src.input_frames = nSamples; + src.output_frames = 750; + src.end_of_input = 0; + src.src_ratio = 8000.0 / double(m_sampleRate); + + int ret = ::src_process(m_incoming, &src); + if (ret != 0) { + LogError("Error up/downsampling of the input audio has an error - %s", src_strerror(ret)); + return 0U; + } + + return src.output_frames_gen; + } else { + unsigned int j = 0U; + for (unsigned int i = 0U; i < nSamples; i++) { + unsigned short val = ((buffer[j++] & 0xFFU) << 8) + ((buffer[j++] & 0xFFU) << 0); + data[i] = (float(val) - 32768.0F) / 32768.0F; + } + + return nSamples; + } } void CFMNetwork::reset() { + assert(m_incoming != NULL); + assert(m_outgoing != NULL); + + m_buffer.clear(); + + ::src_reset(m_incoming); + ::src_reset(m_outgoing); } void CFMNetwork::close() @@ -168,7 +247,7 @@ void CFMNetwork::enable(bool enabled) if (enabled && !m_enabled) reset(); else if (!enabled && m_enabled) - m_buffer.clear(); + reset(); m_enabled = enabled; } diff --git a/FMNetwork.h b/FMNetwork.h index 453e776..6dcdaac 100644 --- a/FMNetwork.h +++ b/FMNetwork.h @@ -23,23 +23,25 @@ #include "UDPSocket.h" #include "Timer.h" +#include + #include #include class CFMNetwork { public: - CFMNetwork(const std::string& myAddress, unsigned int myPort, const std::string& gatewayAddress, unsigned int gatewayPort, bool debug); + CFMNetwork(const std::string& myAddress, unsigned int myPort, const std::string& gatewayAddress, unsigned int gatewayPort, unsigned int sampleRate, bool debug); ~CFMNetwork(); bool open(); void enable(bool enabled); - bool writeData(const unsigned char* data, unsigned int length); + bool writeData(const float* data, unsigned int nSamples); bool writeEOT(); - unsigned int read(unsigned char* data, unsigned int space); + unsigned int read(float* data, unsigned int nSamples); void reset(); @@ -51,10 +53,13 @@ private: CUDPSocket m_socket; in_addr m_address; unsigned int m_port; + unsigned int m_sampleRate; bool m_debug; bool m_enabled; CRingBuffer m_buffer; CTimer m_pollTimer; + SRC_STATE* m_incoming; + SRC_STATE* m_outgoing; bool writePoll(); }; diff --git a/MMDVM.ini b/MMDVM.ini index 911cb29..6c35909 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -250,6 +250,7 @@ LocalAddress=127.0.0.1 LocalPort=3810 GatewayAddress=127.0.0.1 GatewayPort=4810 +SampleRate=8000 # ModeHang=3 Debug=0 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 2a04430..7066ad1 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -1690,6 +1690,7 @@ bool CMMDVMHost::createFMNetwork() unsigned int gatewayPort = m_conf.getFMGatewayPort(); std::string localAddress = m_conf.getFMLocalAddress(); unsigned int localPort = m_conf.getFMLocalPort(); + unsigned int sampleRate = m_conf.getFMSampleRate(); m_fmNetModeHang = m_conf.getFMNetworkModeHang(); bool debug = m_conf.getFMNetworkDebug(); @@ -1698,9 +1699,10 @@ bool CMMDVMHost::createFMNetwork() LogInfo(" Gateway Port: %u", gatewayPort); LogInfo(" Local Address: %s", localAddress.c_str()); LogInfo(" Local Port: %u", localPort); + LogInfo(" Sample Rate: %u", sampleRate); LogInfo(" Mode Hang: %us", m_fmNetModeHang); - m_fmNetwork = new CFMNetwork(localAddress, localPort, gatewayAddress, gatewayPort, debug); + m_fmNetwork = new CFMNetwork(localAddress, localPort, gatewayAddress, gatewayPort, sampleRate, debug); bool ret = m_fmNetwork->open(); if (!ret) { diff --git a/Version.h b/Version.h index 874077c..d5b3f85 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200713"; +const char* VERSION = "20200727"; #endif From 15c21de416f2597b27505f491d13ff89ae6ee524 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 27 Jul 2020 10:58:32 +0100 Subject: [PATCH 106/163] Fix complilation on Linux. --- FMControl.cpp | 14 ++++---------- FMNetwork.cpp | 10 +++++----- FMNetwork.h | 6 +++--- Makefile | 4 ++-- Makefile.Pi | 4 ++-- Makefile.Pi.Adafruit | 4 ++-- Makefile.Pi.HD44780 | 4 ++-- Makefile.Pi.OLED | 4 ++-- Makefile.Pi.PCF8574 | 4 ++-- 9 files changed, 24 insertions(+), 30 deletions(-) diff --git a/FMControl.cpp b/FMControl.cpp index c07c8d4..9a2a78a 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -136,9 +136,8 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) if (space > 252U) space = 252U; - unsigned short netData[168U]; // Modem can handle up to 168 samples at a time - unsigned int length = m_network->read((unsigned char*)netData, 168U * sizeof(unsigned short)); - length /= sizeof(unsigned short); + float netData[168U]; // Modem can handle up to 168 samples at a time + unsigned int length = m_network->read(netData, 168U); if (length == 0U) return 0U; @@ -147,13 +146,8 @@ unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) unsigned int nData = 0U; for (unsigned int i = 0; i < length; i++) { - unsigned short netSample = SWAP_BYTES_16(netData[i]); - - // Convert the unsigned 16-bit data (+65535 - 0) to float (+1.0 - -1.0) - float sampleFloat = (float(netSample) / 32768.0F) - 1.0F; - - //preemphasis - sampleFloat = m_preemphasis->filter(sampleFloat); + // Pre-emphasis + float sampleFloat = m_preemphasis->filter(netData[i]); // Convert float to 12-bit samples (0 to 4095) unsigned int sample12bit = (unsigned int)((sampleFloat + 1.0F) * 2048.0F + 0.5F); diff --git a/FMNetwork.cpp b/FMNetwork.cpp index afc2014..fd13df4 100644 --- a/FMNetwork.cpp +++ b/FMNetwork.cpp @@ -69,7 +69,7 @@ bool CFMNetwork::open() return m_socket.open(); } -bool CFMNetwork::writeData(const float* data, unsigned int nSamples) +bool CFMNetwork::writeData(float* data, unsigned int nSamples) { assert(m_outgoing != NULL); assert(data != NULL); @@ -193,9 +193,9 @@ unsigned int CFMNetwork::read(float* data, unsigned int nSamples) if (m_sampleRate != 8000U) { float in[750U]; - unsigned int j = 0U; for (unsigned int i = 0U; i < nSamples; i++) { - unsigned short val = ((buffer[j++] & 0xFFU) << 8) + ((buffer[j++] & 0xFFU) << 0); + unsigned short val = ((buffer[i * 2U + 0U] & 0xFFU) << 8) + + ((buffer[i * 2U + 1U] & 0xFFU) << 0); in[i] = (float(val) - 32768.0F) / 32768.0F; } @@ -214,9 +214,9 @@ unsigned int CFMNetwork::read(float* data, unsigned int nSamples) return src.output_frames_gen; } else { - unsigned int j = 0U; for (unsigned int i = 0U; i < nSamples; i++) { - unsigned short val = ((buffer[j++] & 0xFFU) << 8) + ((buffer[j++] & 0xFFU) << 0); + unsigned short val = ((buffer[i * 2U + 0U] & 0xFFU) << 8) + + ((buffer[i * 2U + 1U] & 0xFFU) << 0); data[i] = (float(val) - 32768.0F) / 32768.0F; } diff --git a/FMNetwork.h b/FMNetwork.h index 6dcdaac..f0d5d2a 100644 --- a/FMNetwork.h +++ b/FMNetwork.h @@ -37,11 +37,11 @@ public: void enable(bool enabled); - bool writeData(const float* data, unsigned int nSamples); + bool writeData(float* data, unsigned int nSamples); - bool writeEOT(); + bool writeEOT(); - unsigned int read(float* data, unsigned int nSamples); + unsigned int read(float* data, unsigned int nSamples); void reset(); diff --git a/Makefile b/Makefile index 91fcf2d..2181178 100644 --- a/Makefile +++ b/Makefile @@ -5,11 +5,11 @@ CXX = c++ # Use the following CFLAGS and LIBS if you don't want to use gpsd. CFLAGS = -g -O3 -Wall -std=c++0x -pthread -LIBS = -lpthread -lutil +LIBS = -lpthread -lutil -lsamplerate # Use the following CFLAGS and LIBS if you do want to use gpsd. #CFLAGS = -g -O3 -Wall -DUSE_GPSD -std=c++0x -pthread -#LIBS = -lpthread -lgps -lutil +#LIBS = -lpthread -lgps -lutil -lsamplerate LDFLAGS = -g diff --git a/Makefile.Pi b/Makefile.Pi index 62e27aa..7123530 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -4,11 +4,11 @@ CC = gcc CXX = g++ # Use the following CFLAGS and LIBS if you don't want to use gpsd. CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DRASPBERRY_PI -I/usr/local/include -LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil +LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil -lsamplerate # Use the following CFLAGS and LIBS if you do want to use gpsd. #CFLAGS = -g -O3 -Wall -DUSE_GPSD -std=c++0x -pthread -DRASPBERRY_PI -I/usr/local/include -#LIBS = -lwiringPi -lwiringPiDev -lpthread -lgps -lutil +#LIBS = -lwiringPi -lwiringPiDev -lpthread -lgps -lutil -lsamplerate LDFLAGS = -g -L/usr/local/lib diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 2915c69..4c5e9f5 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -5,11 +5,11 @@ CC = gcc CXX = g++ # Use the following CFLAGS and LIBS if you don't want to use gpsd. CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHD44780 -DADAFRUIT_DISPLAY -I/usr/local/include -LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil +LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil -lsamplerate # Use the following CFLAGS and LIBS if you do want to use gpsd. #CFLAGS = -g -O3 -Wall -DUSE_GPSD -std=c++0x -pthread -DHD44780 -DADAFRUIT_DISPLAY -I/usr/local/include -#LIBS = -lwiringPi -lwiringPiDev -lpthread -lgps -lutil +#LIBS = -lwiringPi -lwiringPiDev -lpthread -lgps -lutil -lsamplerate LDFLAGS = -g -L/usr/local/lib diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index c00e58f..186b22f 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -5,11 +5,11 @@ CXX = g++ # Use the following CFLAGS and LIBS if you don't want to use gpsd. CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHD44780 -I/usr/local/include -LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil +LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil -lsamplerate # Use the following CFLAGS and LIBS if you do want to use gpsd. #CFLAGS = -g -O3 -Wall -DUSE_GPSD -std=c++0x -pthread -DHD44780 -I/usr/local/include -#LIBS = -lwiringPi -lwiringPiDev -lpthread -lgps -lutil +#LIBS = -lwiringPi -lwiringPiDev -lpthread -lgps -lutil -lsamplerate LDFLAGS = -g -L/usr/local/lib diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index 41e837e..34b2a9e 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -5,11 +5,11 @@ CXX = g++ # Use the following CFLAGS and LIBS if you don't want to use gpsd. CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DOLED -I/usr/local/include -LIBS = -lArduiPi_OLED -lwiringPi -lpthread -lutil +LIBS = -lArduiPi_OLED -lwiringPi -lpthread -lutil -lsamplerate # Use the following CFLAGS and LIBS if you do want to use gpsd. #CFLAGS = -g -O3 -Wall -DUSE_GPS -std=c++0x -pthread -DOLED -I/usr/local/include -#LIBS = -lArduiPi_OLED -lwiringPi -lpthread -lgps -lutil +#LIBS = -lArduiPi_OLED -lwiringPi -lpthread -lgps -lutil -lsamplerate LDFLAGS = -g -L/usr/local/lib diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index 37de0b1..bda859b 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -6,11 +6,11 @@ CXX = g++ # Use the following CFLAGS and LIBS if you don't want to use gpsd. CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHD44780 -DPCF8574_DISPLAY -I/usr/local/include -LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil +LIBS = -lwiringPi -lwiringPiDev -lpthread -lutil -lsamplerate # Use the following CFLAGS and LIBS if you do want to use gpsd. #CFLAGS = -g -O3 -Wall -DUSE_GPSD -std=c++0x -pthread -DHD44780 -DPCF8574_DISPLAY -I/usr/local/include -#LIBS = -lwiringPi -lwiringPiDev -lpthread -lgps -lutil +#LIBS = -lwiringPi -lwiringPiDev -lpthread -lgps -lutil -lsamplerate LDFLAGS = -g -L/usr/local/lib From 8f59691738c2e732925d425b09271b80d32c0550 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 18 Jul 2020 17:36:54 +0200 Subject: [PATCH 107/163] Moved existing scripts to pi-star folder --- linux/{ => pi-star}/init/README.md | 0 linux/{ => pi-star}/init/mmdvmhost | 0 linux/{ => pi-star}/systemd/README.md | 0 linux/{ => pi-star}/systemd/mmdvmhost.service | 0 linux/{ => pi-star}/systemd/mmdvmhost.timer | 0 linux/{ => pi-star}/systemd/mmdvmhost_service | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename linux/{ => pi-star}/init/README.md (100%) rename linux/{ => pi-star}/init/mmdvmhost (100%) rename linux/{ => pi-star}/systemd/README.md (100%) rename linux/{ => pi-star}/systemd/mmdvmhost.service (100%) rename linux/{ => pi-star}/systemd/mmdvmhost.timer (100%) rename linux/{ => pi-star}/systemd/mmdvmhost_service (100%) diff --git a/linux/init/README.md b/linux/pi-star/init/README.md similarity index 100% rename from linux/init/README.md rename to linux/pi-star/init/README.md diff --git a/linux/init/mmdvmhost b/linux/pi-star/init/mmdvmhost similarity index 100% rename from linux/init/mmdvmhost rename to linux/pi-star/init/mmdvmhost diff --git a/linux/systemd/README.md b/linux/pi-star/systemd/README.md similarity index 100% rename from linux/systemd/README.md rename to linux/pi-star/systemd/README.md diff --git a/linux/systemd/mmdvmhost.service b/linux/pi-star/systemd/mmdvmhost.service similarity index 100% rename from linux/systemd/mmdvmhost.service rename to linux/pi-star/systemd/mmdvmhost.service diff --git a/linux/systemd/mmdvmhost.timer b/linux/pi-star/systemd/mmdvmhost.timer similarity index 100% rename from linux/systemd/mmdvmhost.timer rename to linux/pi-star/systemd/mmdvmhost.timer diff --git a/linux/systemd/mmdvmhost_service b/linux/pi-star/systemd/mmdvmhost_service similarity index 100% rename from linux/systemd/mmdvmhost_service rename to linux/pi-star/systemd/mmdvmhost_service From 354e808ccb1a6ea1283a3719c86cc25bc5cb2803 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 18 Jul 2020 18:07:10 +0200 Subject: [PATCH 108/163] Add systemd unit --- linux/systemd/mmdvmhost.service | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 linux/systemd/mmdvmhost.service diff --git a/linux/systemd/mmdvmhost.service b/linux/systemd/mmdvmhost.service new file mode 100644 index 0000000..0587683 --- /dev/null +++ b/linux/systemd/mmdvmhost.service @@ -0,0 +1,12 @@ +[Unit] +Description=MMDVMHost Radio Servce +After=syslog.target network.target + +[Service] +User=mmdvm +Type=forking +ExecStart=/usr/bin/local/MMDVMHost +Restart=on-abnormal + +[Install] +WantedBy=multi-user.target From d4e6f2cb5ef3eebc9844d9fb96a61f4935d8e3b8 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Sat, 18 Jul 2020 18:07:21 +0200 Subject: [PATCH 109/163] add install targets --- Makefile | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2181178..3f298dc 100644 --- a/Makefile +++ b/Makefile @@ -35,10 +35,31 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h From f64371b4c8e112850c2af0329c56e6742a859d99 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Mon, 20 Jul 2020 08:06:29 +0200 Subject: [PATCH 110/163] delete unit file on uninstall --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 3f298dc..df54350 100644 --- a/Makefile +++ b/Makefile @@ -55,10 +55,12 @@ install-service: install /etc/MMDVM.ini @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini @chown mmdvm:mmdvm /etc/MMDVM.ini +.PHONY uninstall-service: uninstall-service: @systemctl stop mmdvmhost.service || true @systemctl disable mmdvmhost.service || true @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h From 869edd33fd17f0421e3ef9cc9e2a2f6c32561fd9 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Mon, 20 Jul 2020 08:13:27 +0200 Subject: [PATCH 111/163] fix typo in executable path --- linux/systemd/mmdvmhost.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/systemd/mmdvmhost.service b/linux/systemd/mmdvmhost.service index 0587683..7ab6aa1 100644 --- a/linux/systemd/mmdvmhost.service +++ b/linux/systemd/mmdvmhost.service @@ -5,7 +5,7 @@ After=syslog.target network.target [Service] User=mmdvm Type=forking -ExecStart=/usr/bin/local/MMDVMHost +ExecStart=/usr/local/bin/MMDVMHost Restart=on-abnormal [Install] From 7aec7eefcb08f98fbe6d7a71e621bcb06aa37641 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Mon, 20 Jul 2020 08:36:22 +0200 Subject: [PATCH 112/163] Add install service to othe rmakefiles --- Makefile.Pi | 25 ++++++++++++++++++++++++- Makefile.Pi.Adafruit | 25 ++++++++++++++++++++++++- Makefile.Pi.HD44780 | 25 ++++++++++++++++++++++++- Makefile.Pi.OLED | 25 ++++++++++++++++++++++++- Makefile.Pi.PCF8574 | 25 ++++++++++++++++++++++++- 5 files changed, 120 insertions(+), 5 deletions(-) diff --git a/Makefile.Pi b/Makefile.Pi index 7123530..3b14a13 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -34,10 +34,33 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +.PHONY uninstall-service: +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 4c5e9f5..149d9ba 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -35,10 +35,33 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +.PHONY uninstall-service: +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index 186b22f..85044ef 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -35,10 +35,33 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +.PHONY uninstall-service: +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index 34b2a9e..0a3d7bb 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -35,10 +35,33 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +.PHONY uninstall-service: +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index bda859b..deda9e3 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -36,10 +36,33 @@ RemoteCommand: Log.o RemoteCommand.o UDPSocket.o %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< -install: +.PHONY install: +install: all install -m 755 MMDVMHost /usr/local/bin/ install -m 755 RemoteCommand /usr/local/bin/ +.PHONY install-service: +install-service: install /etc/MMDVM.ini + @useradd --user-group -M --system mmdvm --shell /bin/false || true + @usermod --groups dialout --append mmdvm || true + @mkdir /var/log/mmdvm || true + @chown mmdvm:mmdvm /var/log/mmdvm + @cp ./linux/systemd/mmdvmhost.service /lib/systemd/system/ + @systemctl enable mmdvmhost.service + +/etc/MMDVM.ini: + @cp -n MMDVM.ini /etc/MMDVM.ini + @sed -i 's/FilePath=./FilePath=\/var\/log\/mmdvm\//' /etc/MMDVM.ini + @sed -i 's/Daemon=0/Daemon=1/' /etc/MMDVM.ini + @chown mmdvm:mmdvm /etc/MMDVM.ini + +.PHONY uninstall-service: +uninstall-service: + @systemctl stop mmdvmhost.service || true + @systemctl disable mmdvmhost.service || true + @rm -f /usr/local/bin/MMDVMHost || true + @rm -f /lib/systemd/system/mmdvmhost.service || true + clean: $(RM) MMDVMHost RemoteCommand *.o *.d *.bak *~ GitVersion.h From c2c1266233cf4b8b5655b386c78ffe23a4037d01 Mon Sep 17 00:00:00 2001 From: Geoffrey Merck Date: Mon, 20 Jul 2020 09:08:58 +0200 Subject: [PATCH 113/163] Correct typo --- linux/systemd/mmdvmhost.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/systemd/mmdvmhost.service b/linux/systemd/mmdvmhost.service index 7ab6aa1..27ca4ee 100644 --- a/linux/systemd/mmdvmhost.service +++ b/linux/systemd/mmdvmhost.service @@ -1,5 +1,5 @@ [Unit] -Description=MMDVMHost Radio Servce +Description=MMDVMHost Radio Service After=syslog.target network.target [Service] From 0579b4387a7bf77d92db09c3e26158fe5ed53cf5 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 28 Jul 2020 14:22:26 +0100 Subject: [PATCH 114/163] Add the noise squelch configuration items. --- Conf.cpp | 26 ++++++++++++++++++ Conf.h | 6 +++++ MMDVM.ini | 4 +++ MMDVMHost.cpp | 70 ++++++++++++++++++++++++++++--------------------- Modem.h | 2 +- NullModem.h | 2 +- SerialModem.cpp | 24 ++++++++++++----- SerialModem.h | 5 +++- Version.h | 2 +- 9 files changed, 101 insertions(+), 40 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index 1ea5cc5..150b258 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -208,6 +208,9 @@ m_fmKerchunkTime(0U), m_fmHangTime(7U), m_fmAccessMode(1U), m_fmCOSInvert(false), +m_fmNoiseSquelch(false), +m_fmSquelchHighThreshold(30U), +m_fmSquelchLowThreshold(20U), m_fmRFAudioBoost(1U), m_fmMaxDevLevel(90.0F), m_fmExtAudioBoost(1U), @@ -808,6 +811,14 @@ bool CConf::read() m_fmAccessMode = ::atoi(value); else if (::strcmp(key, "COSInvert") == 0) m_fmCOSInvert = ::atoi(value) == 1; + else if (::strcmp(key, "NoiseSquelch") == 0) + m_fmNoiseSquelch = ::atoi(value) == 1; + else if (::strcmp(key, "SquelchThreshold") == 0) + m_fmSquelchHighThreshold = m_fmSquelchLowThreshold = (unsigned int)::atoi(value); + else if (::strcmp(key, "SquelchHighThreshold") == 0) + m_fmSquelchHighThreshold = (unsigned int)::atoi(value); + else if (::strcmp(key, "SquelchLowThreshold") == 0) + m_fmSquelchLowThreshold = (unsigned int)::atoi(value); else if (::strcmp(key, "RFAudioBoost") == 0) m_fmRFAudioBoost = (unsigned int)::atoi(value); else if (::strcmp(key, "MaxDevLevel") == 0) @@ -1757,6 +1768,21 @@ bool CConf::getFMCOSInvert() const return m_fmCOSInvert; } +bool CConf::getFMNoiseSquelch() const +{ + return m_fmNoiseSquelch; +} + +unsigned int CConf::getFMSquelchHighThreshold() const +{ + return m_fmSquelchHighThreshold; +} + +unsigned int CConf::getFMSquelchLowThreshold() const +{ + return m_fmSquelchLowThreshold; +} + unsigned int CConf::getFMRFAudioBoost() const { return m_fmRFAudioBoost; diff --git a/Conf.h b/Conf.h index 01ef9f5..f80ad87 100644 --- a/Conf.h +++ b/Conf.h @@ -211,6 +211,9 @@ public: unsigned int getFMHangTime() const; unsigned int getFMAccessMode() const; bool getFMCOSInvert() const; + bool getFMNoiseSquelch() const; + unsigned int getFMSquelchHighThreshold() const; + unsigned int getFMSquelchLowThreshold() const; unsigned int getFMRFAudioBoost() const; float getFMMaxDevLevel() const; unsigned int getFMExtAudioBoost() const; @@ -502,6 +505,9 @@ private: unsigned int m_fmHangTime; unsigned int m_fmAccessMode; bool m_fmCOSInvert; + bool m_fmNoiseSquelch; + unsigned int m_fmSquelchHighThreshold; + unsigned int m_fmSquelchLowThreshold; unsigned int m_fmRFAudioBoost; float m_fmMaxDevLevel; unsigned int m_fmExtAudioBoost; diff --git a/MMDVM.ini b/MMDVM.ini index 6c35909..2b5d8f1 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -174,6 +174,10 @@ KerchunkTime=0 HangTime=7 AccessMode=1 COSInvert=0 +NoiseSquelch=0 +SquelchThreshold=30 +# SquelchHighThreshold=30 +# SquelchLowThreshold=20 RFAudioBoost=1 MaxDevLevel=90 ExtAudioBoost=1 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 7066ad1..2c712a6 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -1356,35 +1356,38 @@ bool CMMDVMHost::createModem() m_modem->setAX25Params(rxTwist, ax25TXDelay, ax25SlotTime, ax25PPersist); if (m_fmEnabled) { - std::string callsign = m_conf.getFMCallsign(); - unsigned int callsignSpeed = m_conf.getFMCallsignSpeed(); - unsigned int callsignFrequency = m_conf.getFMCallsignFrequency(); - unsigned int callsignTime = m_conf.getFMCallsignTime(); - unsigned int callsignHoldoff = m_conf.getFMCallsignHoldoff(); - float callsignHighLevel = m_conf.getFMCallsignHighLevel(); - float callsignLowLevel = m_conf.getFMCallsignLowLevel(); - bool callsignAtStart = m_conf.getFMCallsignAtStart(); - bool callsignAtEnd = m_conf.getFMCallsignAtEnd(); - bool callsignAtLatch = m_conf.getFMCallsignAtLatch(); - std::string rfAck = m_conf.getFMRFAck(); - unsigned int ackSpeed = m_conf.getFMAckSpeed(); - unsigned int ackFrequency = m_conf.getFMAckFrequency(); - unsigned int ackMinTime = m_conf.getFMAckMinTime(); - unsigned int ackDelay = m_conf.getFMAckDelay(); - float ackLevel = m_conf.getFMAckLevel(); - unsigned int timeout = m_conf.getFMTimeout(); - float timeoutLevel = m_conf.getFMTimeoutLevel(); - float ctcssFrequency = m_conf.getFMCTCSSFrequency(); - unsigned int ctcssHighThreshold = m_conf.getFMCTCSSHighThreshold(); - unsigned int ctcssLowThreshold = m_conf.getFMCTCSSLowThreshold(); - float ctcssLevel = m_conf.getFMCTCSSLevel(); - unsigned int kerchunkTime = m_conf.getFMKerchunkTime(); - unsigned int hangTime = m_conf.getFMHangTime(); - unsigned int accessMode = m_conf.getFMAccessMode(); - bool cosInvert = m_conf.getFMCOSInvert(); - unsigned int rfAudioBoost = m_conf.getFMRFAudioBoost(); - float maxDevLevel = m_conf.getFMMaxDevLevel(); - unsigned int modeHangTime = m_conf.getFMModeHang(); + std::string callsign = m_conf.getFMCallsign(); + unsigned int callsignSpeed = m_conf.getFMCallsignSpeed(); + unsigned int callsignFrequency = m_conf.getFMCallsignFrequency(); + unsigned int callsignTime = m_conf.getFMCallsignTime(); + unsigned int callsignHoldoff = m_conf.getFMCallsignHoldoff(); + float callsignHighLevel = m_conf.getFMCallsignHighLevel(); + float callsignLowLevel = m_conf.getFMCallsignLowLevel(); + bool callsignAtStart = m_conf.getFMCallsignAtStart(); + bool callsignAtEnd = m_conf.getFMCallsignAtEnd(); + bool callsignAtLatch = m_conf.getFMCallsignAtLatch(); + std::string rfAck = m_conf.getFMRFAck(); + unsigned int ackSpeed = m_conf.getFMAckSpeed(); + unsigned int ackFrequency = m_conf.getFMAckFrequency(); + unsigned int ackMinTime = m_conf.getFMAckMinTime(); + unsigned int ackDelay = m_conf.getFMAckDelay(); + float ackLevel = m_conf.getFMAckLevel(); + unsigned int timeout = m_conf.getFMTimeout(); + float timeoutLevel = m_conf.getFMTimeoutLevel(); + float ctcssFrequency = m_conf.getFMCTCSSFrequency(); + unsigned int ctcssHighThreshold = m_conf.getFMCTCSSHighThreshold(); + unsigned int ctcssLowThreshold = m_conf.getFMCTCSSLowThreshold(); + float ctcssLevel = m_conf.getFMCTCSSLevel(); + unsigned int kerchunkTime = m_conf.getFMKerchunkTime(); + unsigned int hangTime = m_conf.getFMHangTime(); + unsigned int accessMode = m_conf.getFMAccessMode(); + bool cosInvert = m_conf.getFMCOSInvert(); + bool noiseSquelch = m_conf.getFMNoiseSquelch(); + unsigned int squelchHighThreshold = m_conf.getFMSquelchHighThreshold(); + unsigned int squelchLowThreshold = m_conf.getFMSquelchLowThreshold(); + unsigned int rfAudioBoost = m_conf.getFMRFAudioBoost(); + float maxDevLevel = m_conf.getFMMaxDevLevel(); + unsigned int modeHangTime = m_conf.getFMModeHang(); LogInfo("FM Parameters"); LogInfo(" Callsign: %s", callsign.c_str()); @@ -1413,13 +1416,20 @@ bool CMMDVMHost::createModem() LogInfo(" Hang Time: %us", hangTime); LogInfo(" Access Mode: %u", accessMode); LogInfo(" COS Invert: %s", cosInvert ? "yes" : "no"); + + LogInfo(" Noise Squelch: %s", noiseSquelch ? "yes" : "no"); + if (noiseSquelch) { + LogInfo(" Squelch High Threshold: %u", squelchHighThreshold); + LogInfo(" Squelch Low Threshold: %u", squelchLowThreshold); + } + LogInfo(" RF Audio Boost: x%u", rfAudioBoost); LogInfo(" Max. Deviation Level: %.1f%%", maxDevLevel); LogInfo(" Mode Hang: %us", modeHangTime); m_modem->setFMCallsignParams(callsign, callsignSpeed, callsignFrequency, callsignTime, callsignHoldoff, callsignHighLevel, callsignLowLevel, callsignAtStart, callsignAtEnd, callsignAtLatch); m_modem->setFMAckParams(rfAck, ackSpeed, ackFrequency, ackMinTime, ackDelay, ackLevel); - m_modem->setFMMiscParams(timeout, timeoutLevel, ctcssFrequency, ctcssHighThreshold, ctcssLowThreshold, ctcssLevel, kerchunkTime, hangTime, accessMode, cosInvert, rfAudioBoost, maxDevLevel); + m_modem->setFMMiscParams(timeout, timeoutLevel, ctcssFrequency, ctcssHighThreshold, ctcssLowThreshold, ctcssLevel, kerchunkTime, hangTime, accessMode, cosInvert, noiseSquelch, squelchHighThreshold, squelchLowThreshold, rfAudioBoost, maxDevLevel); if (m_conf.getFMNetworkEnabled()) { std::string extAck = m_conf.getFMExtAck(); diff --git a/Modem.h b/Modem.h index 45a5860..bbddc4d 100644 --- a/Modem.h +++ b/Modem.h @@ -40,7 +40,7 @@ public: virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch) = 0; virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel) = 0; - virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, unsigned int accessMode, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) = 0; + virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, unsigned int accessMode, bool cosInvert, bool noiseSquelch, unsigned int squelchHighThreshold, unsigned int squelchLowThreshold, unsigned int rfAudioBoost, float maxDevLevel) = 0; virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost) = 0; virtual bool open() = 0; diff --git a/NullModem.h b/NullModem.h index 41930c8..a99424a 100644 --- a/NullModem.h +++ b/NullModem.h @@ -42,7 +42,7 @@ public: virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch) {}; virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel) {}; - virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, unsigned int accessMode, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) {}; + virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, unsigned int accessMode, bool cosInvert, bool noiseSquelch, unsigned int squelchHighThreshold, unsigned int squelchLowThreshold, unsigned int rfAudioBoost, float maxDevLevel) {}; virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost) {}; virtual bool open(); diff --git a/SerialModem.cpp b/SerialModem.cpp index 0282850..cc0416d 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -224,6 +224,9 @@ m_fmKerchunkTime(0U), m_fmHangTime(5U), m_fmAccessMode(1U), m_fmCOSInvert(false), +m_fmNoiseSquelch(false), +m_fmSquelchHighThreshold(30U), +m_fmSquelchLowThreshold(20U), m_fmRFAudioBoost(1U), m_fmExtAudioBoost(1U), m_fmMaxDevLevel(90.0F), @@ -2186,7 +2189,7 @@ void CSerialModem::setFMAckParams(const std::string& rfAck, unsigned int ackSpee m_fmAckLevel = ackLevel; } -void CSerialModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, unsigned int accessMode, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel) +void CSerialModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, unsigned int accessMode, bool cosInvert, bool noiseSquelch, unsigned int squelchHighThreshold, unsigned int squelchLowThreshold, unsigned int rfAudioBoost, float maxDevLevel) { m_fmTimeout = timeout; m_fmTimeoutLevel = timeoutLevel; @@ -2203,6 +2206,10 @@ void CSerialModem::setFMMiscParams(unsigned int timeout, float timeoutLevel, flo m_fmAccessMode = accessMode; m_fmCOSInvert = cosInvert; + m_fmNoiseSquelch = noiseSquelch; + m_fmSquelchHighThreshold = squelchHighThreshold; + m_fmSquelchLowThreshold = squelchLowThreshold; + m_fmRFAudioBoost = rfAudioBoost; m_fmMaxDevLevel = maxDevLevel; } @@ -2334,7 +2341,7 @@ bool CSerialModem::setFMMiscParams() unsigned char buffer[20U]; buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 15U; + buffer[1U] = 17U; buffer[2U] = MMDVM_FM_PARAMS3; buffer[3U] = m_fmTimeout / 5U; @@ -2348,7 +2355,9 @@ bool CSerialModem::setFMMiscParams() buffer[9U] = m_fmKerchunkTime; buffer[10U] = m_fmHangTime; - buffer[11U] = m_fmAccessMode; + buffer[11U] = m_fmAccessMode & 0x0FU; + if (m_fmNoiseSquelch) + buffer[11U] |= 0x40U; if (m_fmCOSInvert) buffer[11U] |= 0x80U; @@ -2358,10 +2367,13 @@ bool CSerialModem::setFMMiscParams() buffer[14U] = (unsigned char)(m_rxLevel * 2.55F + 0.5F); - // CUtils::dump(1U, "Written", buffer, 15U); + buffer[15U] = m_fmSquelchHighThreshold; + buffer[16U] = m_fmSquelchLowThreshold; - int ret = m_serial->write(buffer, 15U); - if (ret != 15) + // CUtils::dump(1U, "Written", buffer, 17U); + + int ret = m_serial->write(buffer, 17U); + if (ret != 17) return false; unsigned int count = 0U; diff --git a/SerialModem.h b/SerialModem.h index 365e311..223774b 100644 --- a/SerialModem.h +++ b/SerialModem.h @@ -60,7 +60,7 @@ public: virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch); virtual void setFMAckParams(const std::string& rfAck, unsigned int ackSpeed, unsigned int ackFrequency, unsigned int ackMinTime, unsigned int ackDelay, float ackLevel); - virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, unsigned int accessMode, bool cosInvert, unsigned int rfAudioBoost, float maxDevLevel); + virtual void setFMMiscParams(unsigned int timeout, float timeoutLevel, float ctcssFrequency, unsigned int ctcssHighThreshold, unsigned int ctcssLowThreshold, float ctcssLevel, unsigned int kerchunkTime, unsigned int hangTime, unsigned int accessMode, bool cosInvert, bool noiseSquelch, unsigned int squelchHighThreshold, unsigned int squelchLowThreshold, unsigned int rfAudioBoost, float maxDevLevel); virtual void setFMExtParams(const std::string& ack, unsigned int audioBoost); virtual bool open(); @@ -247,6 +247,9 @@ private: unsigned int m_fmHangTime; unsigned int m_fmAccessMode; bool m_fmCOSInvert; + bool m_fmNoiseSquelch; + unsigned int m_fmSquelchHighThreshold; + unsigned int m_fmSquelchLowThreshold; unsigned int m_fmRFAudioBoost; unsigned int m_fmExtAudioBoost; float m_fmMaxDevLevel; diff --git a/Version.h b/Version.h index d5b3f85..342e069 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20200727"; +const char* VERSION = "20200728"; #endif From 5a3c61644c9091b580dee6454c041a73155ee127 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 6 Sep 2020 14:49:52 +0100 Subject: [PATCH 115/163] Fix compilation. --- FMNetwork.cpp | 12 +++++++++--- Makefile | 6 +++--- Makefile.Pi | 2 +- Makefile.Pi.Adafruit | 2 +- Makefile.Pi.HD44780 | 2 +- Makefile.Pi.OLED | 2 +- Makefile.Pi.PCF8574 | 2 +- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/FMNetwork.cpp b/FMNetwork.cpp index 590a793..bc394c2 100644 --- a/FMNetwork.cpp +++ b/FMNetwork.cpp @@ -30,7 +30,7 @@ const unsigned int BUFFER_LENGTH = 500U; CFMNetwork::CFMNetwork(const std::string& localAddress, unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, unsigned int sampleRate, bool debug) : m_socket(localAddress, localPort), m_addr(), -m_addrLen(), +m_addrLen(0U), m_sampleRate(sampleRate), m_debug(debug), m_enabled(false), @@ -41,7 +41,8 @@ m_pollTimer(1000U, 5U) assert(!gatewayAddress.empty()); assert(sampleRate > 0U); - CUDPSocket::lookup(gatewayAddress, gatewayPort, m_addr, m_addrLen); + if (CUDPSocket::lookup(gatewayAddress, gatewayPort, m_addr, m_addrLen) != 0) + m_addrLen = 0U; int error; m_incoming = ::src_new(SRC_SINC_FASTEST, 1, &error); @@ -59,11 +60,16 @@ CFMNetwork::~CFMNetwork() bool CFMNetwork::open() { + if (m_addrLen == 0U) { + LogError("Unable to resolve the address of the FM Gateway"); + return false; + } + LogMessage("Opening FM network connection"); m_pollTimer.start(); - return m_socket.open(); + return m_socket.open(m_addr); } bool CFMNetwork::writeData(float* data, unsigned int nSamples) diff --git a/Makefile b/Makefile index 6582ae6..adf0143 100644 --- a/Makefile +++ b/Makefile @@ -2,9 +2,9 @@ CC = cc CXX = c++ -CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHAVE_LOG_H -LIBS = -lpthread -LDFLAGS = -g +CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHAVE_LOG_H -I/usr/local/include +LIBS = -lpthread -lsamplerate -lutil +LDFLAGS = -g -L/usr/local/lib OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o \ diff --git a/Makefile.Pi b/Makefile.Pi index 4a757ad..055d450 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -3,7 +3,7 @@ CC = cc CXX = c++ CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHAVE_LOG_H -DRASPBERRY_PI -I/usr/local/include -LIBS = -lwiringPi -lwiringPiDev -lpthread +LIBS = -lwiringPi -lwiringPiDev -lpthread -lsamplerate -lutil LDFLAGS = -g -L/usr/local/lib OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 19ddbae..5301a2a 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -4,7 +4,7 @@ CC = cc CXX = c++ CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHAVE_LOG_H -DHD44780 -DADAFRUIT_DISPLAY -I/usr/local/include -LIBS = -lwiringPi -lwiringPiDev -lpthread +LIBS = -lwiringPi -lwiringPiDev -lpthread -lsamplerate -lutil LDFLAGS = -g -L/usr/local/lib OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index f05b28d..906fc66 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -3,7 +3,7 @@ CC = cc CXX = c++ CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHAVE_LOG_H -DHD44780 -I/usr/local/include -LIBS = -lwiringPi -lwiringPiDev -lpthread +LIBS = -lwiringPi -lwiringPiDev -lpthread -lsamplerate -lutil LDFLAGS = -g -L/usr/local/lib OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index a4a2e66..45b22ef 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -3,7 +3,7 @@ CC = cc CXX = c++ CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHAVE_LOG_H -DOLED -I/usr/local/include -LIBS = -lArduiPi_OLED -lwiringPi -lpthread +LIBS = -lArduiPi_OLED -lwiringPi -lpthread -lsamplerate -lutil LDFLAGS = -g -L/usr/local/lib OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index 7d8afa5..13291e4 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -4,7 +4,7 @@ CC = cc CXX = c++ CFLAGS = -g -O3 -Wall -std=c++0x -pthread -DHAVE_LOG_H -DHD44780 -DPCF8574_DISPLAY -I/usr/local/include -LIBS = -lwiringPi -lwiringPiDev -lpthread +LIBS = -lwiringPi -lwiringPiDev -lpthread -lsamplerate -lutil LDFLAGS = -g -L/usr/local/lib OBJECTS = AMBEFEC.o AX25Control.o AX25Network.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o \ From 8536294b7666cbbef9102a6284b207a560947a5a Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Wed, 14 Oct 2020 16:16:54 +0100 Subject: [PATCH 116/163] Initial work on the M17 protocol. --- Conf.cpp | 117 +++- Conf.h | 28 + Defines.h | 3 +- M17Control.cpp | 1130 +++++++++++++++++++++++++++++++++++++ M17Control.h | 98 ++++ M17Defines.h | 34 ++ M17Network.cpp | 133 +++++ M17Network.h | 56 ++ MMDVM.ini | 15 + MMDVMHost.cpp | 231 +++++++- MMDVMHost.h | 8 + MMDVMHost.vcxproj | 5 + MMDVMHost.vcxproj.filters | 15 + Modem.cpp | 150 ++++- Modem.h | 15 +- RemoteControl.cpp | 14 +- RemoteControl.h | 5 +- Sync.cpp | 10 +- Sync.h | 4 +- Version.h | 2 +- 20 files changed, 2029 insertions(+), 44 deletions(-) create mode 100644 M17Control.cpp create mode 100644 M17Control.h create mode 100644 M17Defines.h create mode 100644 M17Network.cpp create mode 100644 M17Network.h diff --git a/Conf.cpp b/Conf.cpp index f594a18..9038b28 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -43,6 +43,7 @@ enum SECTION { SECTION_FUSION, SECTION_P25, SECTION_NXDN, + SECTION_M17, SECTION_POCSAG, SECTION_FM, SECTION_DSTAR_NETWORK, @@ -50,6 +51,7 @@ enum SECTION { SECTION_FUSION_NETWORK, SECTION_P25_NETWORK, SECTION_NXDN_NETWORK, + SECTION_M17_NETWORK, SECTION_POCSAG_NETWORK, SECTION_TFTSERIAL, SECTION_HD44780, @@ -102,6 +104,7 @@ m_modemDMRTXLevel(50.0F), m_modemYSFTXLevel(50.0F), m_modemP25TXLevel(50.0F), m_modemNXDNTXLevel(50.0F), +m_modemM17TXLevel(50.0F), m_modemPOCSAGTXLevel(50.0F), m_modemFMTXLevel(50.0F), m_modemRSSIMappingFile(), @@ -164,6 +167,10 @@ m_nxdnSelfOnly(false), m_nxdnRemoteGateway(false), m_nxdnTXHang(5U), m_nxdnModeHang(10U), +m_m17Enabled(false), +m_m17SelfOnly(false), +m_m17TXHang(5U), +m_m17ModeHang(10U), m_pocsagEnabled(false), m_pocsagFrequency(0U), m_fmEnabled(false), @@ -235,6 +242,12 @@ m_nxdnLocalAddress(), m_nxdnLocalPort(0U), m_nxdnNetworkModeHang(3U), m_nxdnNetworkDebug(false), +m_m17NetworkEnabled(false), +m_m17GatewayAddress(), +m_m17GatewayPort(0U), +m_m17LocalPort(0U), +m_m17NetworkModeHang(3U), +m_m17NetworkDebug(false), m_pocsagNetworkEnabled(false), m_pocsagGatewayAddress(), m_pocsagGatewayPort(0U), @@ -329,6 +342,8 @@ bool CConf::read() section = SECTION_P25; else if (::strncmp(buffer, "[NXDN]", 6U) == 0) section = SECTION_NXDN; + else if (::strncmp(buffer, "[M17]", 5U) == 0) + section = SECTION_M17; else if (::strncmp(buffer, "[POCSAG]", 8U) == 0) section = SECTION_POCSAG; else if (::strncmp(buffer, "[FM]", 4U) == 0) @@ -343,6 +358,8 @@ bool CConf::read() section = SECTION_P25_NETWORK; else if (::strncmp(buffer, "[NXDN Network]", 14U) == 0) section = SECTION_NXDN_NETWORK; + else if (::strncmp(buffer, "[M17 Network]", 13U) == 0) + section = SECTION_M17_NETWORK; else if (::strncmp(buffer, "[POCSAG Network]", 16U) == 0) section = SECTION_POCSAG_NETWORK; else if (::strncmp(buffer, "[TFT Serial]", 12U) == 0) @@ -404,12 +421,12 @@ bool CConf::read() else if (::strcmp(key, "Duplex") == 0) m_duplex = ::atoi(value) == 1; else if (::strcmp(key, "ModeHang") == 0) - m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = - m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = (unsigned int)::atoi(value); + m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = m_nxdnNetworkModeHang = m_m17NetworkModeHang = + m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = m_nxdnModeHang = m_m17ModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "RFModeHang") == 0) - m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = (unsigned int)::atoi(value); + m_dstarModeHang = m_dmrModeHang = m_fusionModeHang = m_p25ModeHang = m_nxdnModeHang = m_m17ModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "NetModeHang") == 0) - m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = (unsigned int)::atoi(value); + m_dstarNetworkModeHang = m_dmrNetworkModeHang = m_fusionNetworkModeHang = m_p25NetworkModeHang = m_nxdnNetworkModeHang = m_m17NetworkModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "Display") == 0) m_display = value; else if (::strcmp(key, "Daemon") == 0) @@ -481,7 +498,7 @@ bool CConf::read() else if (::strcmp(key, "RXLevel") == 0) m_modemRXLevel = float(::atof(value)); else if (::strcmp(key, "TXLevel") == 0) - m_modemFMTXLevel = m_modemCWIdTXLevel = m_modemDStarTXLevel = m_modemDMRTXLevel = m_modemYSFTXLevel = m_modemP25TXLevel = m_modemNXDNTXLevel = float(::atof(value)); + m_modemFMTXLevel = m_modemCWIdTXLevel = m_modemDStarTXLevel = m_modemDMRTXLevel = m_modemYSFTXLevel = m_modemP25TXLevel = m_modemNXDNTXLevel = m_modemM17TXLevel = float(::atof(value)); else if (::strcmp(key, "CWIdTXLevel") == 0) m_modemCWIdTXLevel = float(::atof(value)); else if (::strcmp(key, "D-StarTXLevel") == 0) @@ -494,6 +511,8 @@ bool CConf::read() m_modemP25TXLevel = float(::atof(value)); else if (::strcmp(key, "NXDNTXLevel") == 0) m_modemNXDNTXLevel = float(::atof(value)); + else if (::strcmp(key, "M17TXLevel") == 0) + m_modemM17TXLevel = float(::atof(value)); else if (::strcmp(key, "POCSAGTXLevel") == 0) m_modemPOCSAGTXLevel = float(::atof(value)); else if (::strcmp(key, "FMTXLevel") == 0) @@ -682,13 +701,21 @@ bool CConf::read() m_nxdnTXHang = (unsigned int)::atoi(value); else if (::strcmp(key, "ModeHang") == 0) m_nxdnModeHang = (unsigned int)::atoi(value); + } else if (section == SECTION_M17) { + if (::strcmp(key, "Enable") == 0) + m_m17Enabled = ::atoi(value) == 1; + else if (::strcmp(key, "SelfOnly") == 0) + m_m17SelfOnly = ::atoi(value) == 1; + else if (::strcmp(key, "TXHang") == 0) + m_m17TXHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_m17ModeHang = (unsigned int)::atoi(value); } else if (section == SECTION_POCSAG) { - if (::strcmp(key, "Enable") == 0) - m_pocsagEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Frequency") == 0) - m_pocsagFrequency = (unsigned int)::atoi(value); - } - else if (section == SECTION_FM) { + if (::strcmp(key, "Enable") == 0) + m_pocsagEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Frequency") == 0) + m_pocsagFrequency = (unsigned int)::atoi(value); + } else if (section == SECTION_FM) { if (::strcmp(key, "Enable") == 0) m_fmEnabled = ::atoi(value) == 1; else if (::strcmp(key, "Callsign") == 0) { @@ -843,6 +870,19 @@ bool CConf::read() m_nxdnNetworkModeHang = (unsigned int)::atoi(value); else if (::strcmp(key, "Debug") == 0) m_nxdnNetworkDebug = ::atoi(value) == 1; + } else if (section == SECTION_M17_NETWORK) { + if (::strcmp(key, "Enable") == 0) + m_m17NetworkEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "LocalPort") == 0) + m_m17LocalPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "GatewayAddress") == 0) + m_m17GatewayAddress = value; + else if (::strcmp(key, "GatewayPort") == 0) + m_m17GatewayPort = (unsigned int)::atoi(value); + else if (::strcmp(key, "ModeHang") == 0) + m_m17NetworkModeHang = (unsigned int)::atoi(value); + else if (::strcmp(key, "Debug") == 0) + m_m17NetworkDebug = ::atoi(value) == 1; } else if (section == SECTION_POCSAG_NETWORK) { if (::strcmp(key, "Enable") == 0) m_pocsagNetworkEnabled = ::atoi(value) == 1; @@ -1151,6 +1191,11 @@ float CConf::getModemNXDNTXLevel() const return m_modemNXDNTXLevel; } +float CConf::getModemM17TXLevel() const +{ + return m_modemM17TXLevel; +} + float CConf::getModemPOCSAGTXLevel() const { return m_modemPOCSAGTXLevel; @@ -1461,6 +1506,26 @@ unsigned int CConf::getNXDNModeHang() const return m_nxdnModeHang; } +bool CConf::getM17Enabled() const +{ + return m_m17Enabled; +} + +bool CConf::getM17SelfOnly() const +{ + return m_m17SelfOnly; +} + +unsigned int CConf::getM17TXHang() const +{ + return m_m17TXHang; +} + +unsigned int CConf::getM17ModeHang() const +{ + return m_m17ModeHang; +} + bool CConf::getPOCSAGEnabled() const { return m_pocsagEnabled; @@ -1816,6 +1881,36 @@ bool CConf::getNXDNNetworkDebug() const return m_nxdnNetworkDebug; } +bool CConf::getM17NetworkEnabled() const +{ + return m_m17NetworkEnabled; +} + +std::string CConf::getM17GatewayAddress() const +{ + return m_m17GatewayAddress; +} + +unsigned int CConf::getM17GatewayPort() const +{ + return m_m17GatewayPort; +} + +unsigned int CConf::getM17LocalPort() const +{ + return m_m17LocalPort; +} + +unsigned int CConf::getM17NetworkModeHang() const +{ + return m_m17NetworkModeHang; +} + +bool CConf::getM17NetworkDebug() const +{ + return m_m17NetworkDebug; +} + bool CConf::getPOCSAGNetworkEnabled() const { return m_pocsagNetworkEnabled; diff --git a/Conf.h b/Conf.h index 2b04fa5..9470746 100644 --- a/Conf.h +++ b/Conf.h @@ -83,6 +83,7 @@ public: float getModemYSFTXLevel() const; float getModemP25TXLevel() const; float getModemNXDNTXLevel() const; + float getModemM17TXLevel() const; float getModemPOCSAGTXLevel() const; float getModemFMTXLevel() const; std::string getModemRSSIMappingFile() const; @@ -160,6 +161,12 @@ public: unsigned int getNXDNTXHang() const; unsigned int getNXDNModeHang() const; + // The M17 section + bool getM17Enabled() const; + bool getM17SelfOnly() const; + unsigned int getM17TXHang() const; + unsigned int getM17ModeHang() const; + // The POCSAG section bool getPOCSAGEnabled() const; unsigned int getPOCSAGFrequency() const; @@ -245,6 +252,14 @@ public: unsigned int getNXDNNetworkModeHang() const; bool getNXDNNetworkDebug() const; + // The M17 Network section + bool getM17NetworkEnabled() const; + std::string getM17GatewayAddress() const; + unsigned int getM17GatewayPort() const; + unsigned int getM17LocalPort() const; + unsigned int getM17NetworkModeHang() const; + bool getM17NetworkDebug() const; + // The POCSAG Network section bool getPOCSAGNetworkEnabled() const; std::string getPOCSAGGatewayAddress() const; @@ -352,6 +367,7 @@ private: float m_modemYSFTXLevel; float m_modemP25TXLevel; float m_modemNXDNTXLevel; + float m_modemM17TXLevel; float m_modemPOCSAGTXLevel; float m_modemFMTXLevel; std::string m_modemRSSIMappingFile; @@ -422,6 +438,11 @@ private: unsigned int m_nxdnTXHang; unsigned int m_nxdnModeHang; + bool m_m17Enabled; + bool m_m17SelfOnly; + unsigned int m_m17TXHang; + unsigned int m_m17ModeHang; + bool m_pocsagEnabled; unsigned int m_pocsagFrequency; @@ -500,6 +521,13 @@ private: unsigned int m_nxdnNetworkModeHang; bool m_nxdnNetworkDebug; + bool m_m17NetworkEnabled; + std::string m_m17GatewayAddress; + unsigned int m_m17GatewayPort; + unsigned int m_m17LocalPort; + unsigned int m_m17NetworkModeHang; + bool m_m17NetworkDebug; + bool m_pocsagNetworkEnabled; std::string m_pocsagGatewayAddress; unsigned int m_pocsagGatewayPort; diff --git a/Defines.h b/Defines.h index 18c103e..81973ff 100644 --- a/Defines.h +++ b/Defines.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2017,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2017,2018,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,6 +26,7 @@ const unsigned char MODE_YSF = 3U; const unsigned char MODE_P25 = 4U; const unsigned char MODE_NXDN = 5U; const unsigned char MODE_POCSAG = 6U; +const unsigned char MODE_M17 = 7U; const unsigned char MODE_FM = 10U; diff --git a/M17Control.cpp b/M17Control.cpp new file mode 100644 index 0000000..a2495f6 --- /dev/null +++ b/M17Control.cpp @@ -0,0 +1,1130 @@ +/* + * Copyright (C) 2015-2020 Jonathan Naylor, G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "M17Control.h" +#include "Utils.h" +#include "Sync.h" +#include "Log.h" + +#include +#include +#include +#include + +const unsigned char SCRAMBLER[] = { + 0x00U, 0x00U, 0x00U, 0x82U, 0xA0U, 0x88U, 0x8AU, 0x00U, 0xA2U, 0xA8U, 0x82U, 0x8AU, 0x82U, 0x02U, + 0x20U, 0x08U, 0x8AU, 0x20U, 0xAAU, 0xA2U, 0x82U, 0x08U, 0x22U, 0x8AU, 0xAAU, 0x08U, 0x28U, 0x88U, + 0x28U, 0x28U, 0x00U, 0x0AU, 0x02U, 0x82U, 0x20U, 0x28U, 0x82U, 0x2AU, 0xAAU, 0x20U, 0x22U, 0x80U, + 0xA8U, 0x8AU, 0x08U, 0xA0U, 0xAAU, 0x02U }; + +// #define DUMP_M17 + +const unsigned char BIT_MASK_TABLE[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U }; + +#define WRITE_BIT1(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) +#define READ_BIT1(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) + +CM17Control::CM17Control(const std::string& callsign, bool selfOnly, CM17Network* network, CDisplay* display, unsigned int timeout, bool duplex, CRSSIInterpolator* rssiMapper) : +m_callsign(callsign), +m_selfOnly(selfOnly), +m_network(network), +m_display(display), +m_duplex(duplex), +m_queue(5000U, "M17 Control"), +m_rfState(RS_RF_LISTENING), +m_netState(RS_NET_IDLE), +m_rfTimeoutTimer(1000U, timeout), +m_netTimeoutTimer(1000U, timeout), +m_packetTimer(1000U, 0U, 200U), +m_networkWatchdog(1000U, 0U, 1500U), +m_elapsed(), +m_rfFrames(0U), +m_netFrames(0U), +m_rfErrs(0U), +m_rfBits(1U), +m_rfLastLICH(), +m_rfLayer3(), +m_netLayer3(), +m_rfMask(0x00U), +m_netMask(0x00U), +m_rssiMapper(rssiMapper), +m_rssi(0U), +m_maxRSSI(0U), +m_minRSSI(0U), +m_aveRSSI(0U), +m_rssiCount(0U), +m_enabled(true), +m_fp(NULL) +{ + assert(display != NULL); + assert(rssiMapper != NULL); +} + +CM17Control::~CM17Control() +{ +} + +bool CM17Control::writeModem(unsigned char *data, unsigned int len) +{ + assert(data != NULL); + + if (!m_enabled) + return false; + + unsigned char type = data[0U]; + + if (type == TAG_LOST && m_rfState == RS_RF_AUDIO) { + unsigned short dstId = m_rfLayer3.getDestinationGroupId(); + bool grp = m_rfLayer3.getIsGroup(); + std::string source = m_lookup->find(m_rfLayer3.getSourceUnitId()); + + if (m_rssi != 0U) + LogMessage("M17, transmission lost from %s to %s%u, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 12.5F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); + else + LogMessage("M17, transmission lost from %s to %s%u, %.1f seconds, BER: %.1f%%", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 12.5F, float(m_rfErrs * 100U) / float(m_rfBits)); + writeEndRF(); + return false; + } + + if (type == TAG_LOST && m_rfState == RS_RF_DATA) { + writeEndRF(); + return false; + } + + if (type == TAG_LOST) { + m_rfState = RS_RF_LISTENING; + m_rfMask = 0x00U; + m_rfLayer3.reset(); + return false; + } + + // Have we got RSSI bytes on the end? + if (len == (M17_FRAME_LENGTH_BYTES + 4U)) { + uint16_t raw = 0U; + raw |= (data[50U] << 8) & 0xFF00U; + raw |= (data[51U] << 0) & 0x00FFU; + + // Convert the raw RSSI to dBm + int rssi = m_rssiMapper->interpolate(raw); + if (rssi != 0) + LogDebug("M17, raw RSSI: %u, reported RSSI: %d dBm", raw, rssi); + + // RSSI is always reported as positive + m_rssi = (rssi >= 0) ? rssi : -rssi; + + if (m_rssi > m_minRSSI) + m_minRSSI = m_rssi; + if (m_rssi < m_maxRSSI) + m_maxRSSI = m_rssi; + + m_aveRSSI += m_rssi; + m_rssiCount++; + } + + scrambler(data + 2U); + + CM17LICH lich; + bool valid = lich.decode(data + 2U); + + if (valid) + m_rfLastLICH = lich; + + // Stop repeater packets coming through, unless we're acting as a remote gateway + if (m_remoteGateway) { + unsigned char direction = m_rfLastLICH.getDirection(); + if (direction == M17_LICH_DIRECTION_INBOUND) + return false; + } else { + unsigned char direction = m_rfLastLICH.getDirection(); + if (direction == M17_LICH_DIRECTION_OUTBOUND) + return false; + } + + unsigned char usc = m_rfLastLICH.getFCT(); + unsigned char option = m_rfLastLICH.getOption(); + + bool ret; + if (usc == M17_LICH_USC_UDCH) + ret = processData(option, data); + else + ret = processVoice(usc, option, data); + + return ret; +} + +bool CM17Control::processVoice(unsigned char usc, unsigned char option, unsigned char *data) +{ + CM17SACCH sacch; + bool valid = sacch.decode(data + 2U); + if (valid) { + unsigned char ran = sacch.getRAN(); + if (ran != m_ran && ran != 0U) + return false; + } else if (m_rfState == RS_RF_LISTENING) { + return false; + } + + unsigned char netData[40U]; + ::memset(netData, 0x00U, 40U); + + if (usc == M17_LICH_USC_SACCH_NS) { + // The SACCH on a non-superblock frame is usually an idle and not interesting apart from the RAN. + CM17FACCH1 facch; + bool valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + if (!valid) + valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + if (!valid) + return false; + + unsigned char buffer[10U]; + facch.getData(buffer); + + CM17Layer3 layer3; + layer3.decode(buffer, M17_FACCH1_LENGTH_BITS); + + unsigned char type = layer3.getMessageType(); + if (type == M17_MESSAGE_TYPE_TX_REL) { + if (m_rfState != RS_RF_AUDIO) { + m_rfState = RS_RF_LISTENING; + m_rfMask = 0x00U; + m_rfLayer3.reset(); + return false; + } + } else if (type == M17_MESSAGE_TYPE_VCALL) { + if (m_rfState == RS_RF_LISTENING && m_selfOnly) { + unsigned short srcId = layer3.getSourceUnitId(); + if (srcId != m_id) { + m_rfState = RS_RF_REJECTED; + return false; + } + } + } else { + return false; + } + + m_rfLayer3 = layer3; + + data[0U] = type == M17_MESSAGE_TYPE_TX_REL ? TAG_EOT : TAG_DATA; + data[1U] = 0x00U; + + CSync::addM17Sync(data + 2U); + + CM17LICH lich; + lich.setRFCT(M17_LICH_RFCT_RDCH); + lich.setFCT(M17_LICH_USC_SACCH_NS); + lich.setOption(M17_LICH_STEAL_FACCH); + lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); + lich.encode(data + 2U); + + lich.setDirection(M17_LICH_DIRECTION_INBOUND); + netData[0U] = lich.getRaw(); + + CM17SACCH sacch; + sacch.setRAN(m_ran); + sacch.setStructure(M17_SR_SINGLE); + sacch.setData(SACCH_IDLE); + sacch.encode(data + 2U); + + sacch.getRaw(netData + 1U); + + facch.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + facch.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + + facch.getRaw(netData + 5U + 0U); + facch.getRaw(netData + 5U + 14U); + + scrambler(data + 2U); + + writeNetwork(netData, data[0U] == TAG_EOT ? NNMT_VOICE_TRAILER : NNMT_VOICE_HEADER); + +#if defined(DUMP_M17) + writeFile(data + 2U); +#endif + if (m_duplex) + writeQueueRF(data); + + if (data[0U] == TAG_EOT) { + unsigned short dstId = m_rfLayer3.getDestinationGroupId(); + bool grp = m_rfLayer3.getIsGroup(); + std::string source = m_lookup->find(m_rfLayer3.getSourceUnitId()); + + m_rfFrames++; + if (m_rssi != 0U) + LogMessage("M17, received RF end of transmission from %s to %s%u, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 12.5F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); + else + LogMessage("M17, received RF end of transmission from %s to %s%u, %.1f seconds, BER: %.1f%%", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 12.5F, float(m_rfErrs * 100U) / float(m_rfBits)); + writeEndRF(); + } else { + m_rfFrames = 0U; + m_rfErrs = 0U; + m_rfBits = 1U; + m_rfTimeoutTimer.start(); + m_rfState = RS_RF_AUDIO; + m_minRSSI = m_rssi; + m_maxRSSI = m_rssi; + m_aveRSSI = m_rssi; + m_rssiCount = 1U; +#if defined(DUMP_M17) + openFile(); +#endif + unsigned short srcId = m_rfLayer3.getSourceUnitId(); + unsigned short dstId = m_rfLayer3.getDestinationGroupId(); + bool grp = m_rfLayer3.getIsGroup(); + + std::string source = m_lookup->find(srcId); + LogMessage("M17, received RF header from %s to %s%u", source.c_str(), grp ? "TG " : "", dstId); + m_display->writeM17(source.c_str(), grp, dstId, "R"); + } + + return true; + } else { + if (m_rfState == RS_RF_LISTENING) { + CM17FACCH1 facch; + bool valid = false; + switch (option) { + case M17_LICH_STEAL_FACCH: + valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + if (!valid) + valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + break; + case M17_LICH_STEAL_FACCH1_1: + valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + break; + case M17_LICH_STEAL_FACCH1_2: + valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + break; + default: + break; + } + + bool hasInfo = false; + if (valid) { + unsigned char buffer[10U]; + facch.getData(buffer); + + CM17Layer3 layer3; + layer3.decode(buffer, M17_FACCH1_LENGTH_BITS); + + hasInfo = layer3.getMessageType() == M17_MESSAGE_TYPE_VCALL; + if (!hasInfo) + return false; + + m_rfLayer3 = layer3; + } + + if (!hasInfo) { + unsigned char message[3U]; + sacch.getData(message); + + unsigned char structure = sacch.getStructure(); + switch (structure) { + case M17_SR_1_4: + m_rfLayer3.decode(message, 18U, 0U); + if(m_rfLayer3.getMessageType() == M17_MESSAGE_TYPE_VCALL) + m_rfMask = 0x01U; + else + m_rfMask = 0x00U; + break; + case M17_SR_2_4: + m_rfMask |= 0x02U; + m_rfLayer3.decode(message, 18U, 18U); + break; + case M17_SR_3_4: + m_rfMask |= 0x04U; + m_rfLayer3.decode(message, 18U, 36U); + break; + case M17_SR_4_4: + m_rfMask |= 0x08U; + m_rfLayer3.decode(message, 18U, 54U); + break; + default: + break; + } + + if (m_rfMask != 0x0FU) + return false; + + unsigned char type = m_rfLayer3.getMessageType(); + if (type != M17_MESSAGE_TYPE_VCALL) + return false; + } + + unsigned short srcId = m_rfLayer3.getSourceUnitId(); + unsigned short dstId = m_rfLayer3.getDestinationGroupId(); + bool grp = m_rfLayer3.getIsGroup(); + + if (m_selfOnly) { + if (srcId != m_id) { + m_rfState = RS_RF_REJECTED; + return false; + } + } + + m_rfFrames = 0U; + m_rfErrs = 0U; + m_rfBits = 1U; + m_rfTimeoutTimer.start(); + m_rfState = RS_RF_AUDIO; + + m_minRSSI = m_rssi; + m_maxRSSI = m_rssi; + m_aveRSSI = m_rssi; + m_rssiCount = 1U; +#if defined(DUMP_M17) + openFile(); +#endif + std::string source = m_lookup->find(srcId); + LogMessage("M17, received RF late entry from %s to %s%u", source.c_str(), grp ? "TG " : "", dstId); + m_display->writeM17(source.c_str(), grp, dstId, "R"); + + m_rfState = RS_RF_AUDIO; + + // Create a dummy start message + unsigned char start[M17_FRAME_LENGTH_BYTES + 2U]; + + start[0U] = TAG_DATA; + start[1U] = 0x00U; + + // Generate the sync + CSync::addM17Sync(start + 2U); + + // Generate the LICH + CM17LICH lich; + lich.setRFCT(M17_LICH_RFCT_RDCH); + lich.setFCT(M17_LICH_USC_SACCH_NS); + lich.setOption(M17_LICH_STEAL_FACCH); + lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); + lich.encode(start + 2U); + + lich.setDirection(M17_LICH_DIRECTION_INBOUND); + netData[0U] = lich.getRaw(); + + CM17SACCH sacch; + sacch.setRAN(m_ran); + sacch.setStructure(M17_SR_SINGLE); + sacch.setData(SACCH_IDLE); + sacch.encode(start + 2U); + + sacch.getRaw(netData + 1U); + + unsigned char message[22U]; + m_rfLayer3.getData(message); + + facch.setData(message); + facch.encode(start + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + facch.encode(start + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + + facch.getRaw(netData + 5U + 0U); + facch.getRaw(netData + 5U + 14U); + + scrambler(start + 2U); + + writeNetwork(netData, NNMT_VOICE_HEADER); + +#if defined(DUMP_M17) + writeFile(start + 2U); +#endif + if (m_duplex) + writeQueueRF(start); + } + } + + if (m_rfState == RS_RF_AUDIO) { + // Regenerate the sync + CSync::addM17Sync(data + 2U); + + // Regenerate the LICH + CM17LICH lich; + lich.setRFCT(M17_LICH_RFCT_RDCH); + lich.setFCT(M17_LICH_USC_SACCH_SS); + lich.setOption(option); + lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); + lich.encode(data + 2U); + + lich.setDirection(M17_LICH_DIRECTION_INBOUND); + netData[0U] = lich.getRaw(); + + // Regenerate SACCH if it's valid + CM17SACCH sacch; + bool validSACCH = sacch.decode(data + 2U); + if (validSACCH) { + sacch.setRAN(m_ran); + sacch.encode(data + 2U); + } + + sacch.getRaw(netData + 1U); + + // Regenerate the audio and interpret the FACCH1 data + if (option == M17_LICH_STEAL_NONE) { + CAMBEFEC ambe; + unsigned int errors = 0U; + errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U); + errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 9U); + errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U); + errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 27U); + m_rfErrs += errors; + m_rfBits += 188U; + m_display->writeM17BER(float(errors) / 1.88F); + LogDebug("M17, AMBE FEC %u/188 (%.1f%%)", errors, float(errors) / 1.88F); + + CM17Audio audio; + audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U, netData + 5U + 0U); + audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U, netData + 5U + 14U); + } else if (option == M17_LICH_STEAL_FACCH1_1) { + CM17FACCH1 facch1; + bool valid = facch1.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + if (valid) + facch1.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + facch1.getRaw(netData + 5U + 0U); + + CAMBEFEC ambe; + unsigned int errors = 0U; + errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U); + errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 27U); + m_rfErrs += errors; + m_rfBits += 94U; + m_display->writeM17BER(float(errors) / 0.94F); + LogDebug("M17, AMBE FEC %u/94 (%.1f%%)", errors, float(errors) / 0.94F); + + CM17Audio audio; + audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U, netData + 5U + 14U); + } else if (option == M17_LICH_STEAL_FACCH1_2) { + CAMBEFEC ambe; + unsigned int errors = 0U; + errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES); + errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 9U); + m_rfErrs += errors; + m_rfBits += 94U; + m_display->writeM17BER(float(errors) / 0.94F); + LogDebug("M17, AMBE FEC %u/94 (%.1f%%)", errors, float(errors) / 0.94F); + + CM17Audio audio; + audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U, netData + 5U + 0U); + + CM17FACCH1 facch1; + bool valid = facch1.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + if (valid) + facch1.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + facch1.getRaw(netData + 5U + 14U); + } else { + CM17FACCH1 facch11; + bool valid1 = facch11.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + if (valid1) + facch11.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + facch11.getRaw(netData + 5U + 0U); + + CM17FACCH1 facch12; + bool valid2 = facch12.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + if (valid2) + facch12.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + facch12.getRaw(netData + 5U + 14U); + } + + data[0U] = TAG_DATA; + data[1U] = 0x00U; + + scrambler(data + 2U); + + writeNetwork(netData, NNMT_VOICE_BODY); + +#if defined(DUMP_M17) + writeFile(data + 2U); +#endif + + if (m_duplex) + writeQueueRF(data); + + m_rfFrames++; + + m_display->writeM17RSSI(m_rssi); + } + + return true; +} + +bool CM17Control::processData(unsigned char option, unsigned char *data) +{ + CM17UDCH udch; + bool validUDCH = udch.decode(data + 2U); + if (m_rfState == RS_RF_LISTENING && !validUDCH) + return false; + + if (validUDCH) { + unsigned char ran = udch.getRAN(); + if (ran != m_ran && ran != 0U) + return false; + } + + unsigned char netData[40U]; + ::memset(netData, 0x00U, 40U); + + // The layer3 data will only be correct if valid is true + unsigned char buffer[23U]; + udch.getData(buffer); + + CM17Layer3 layer3; + layer3.decode(buffer, 184U); + + if (m_rfState == RS_RF_LISTENING) { + unsigned char type = layer3.getMessageType(); + if (type != M17_MESSAGE_TYPE_DCALL_HDR) + return false; + + unsigned short srcId = layer3.getSourceUnitId(); + unsigned short dstId = layer3.getDestinationGroupId(); + bool grp = layer3.getIsGroup(); + + if (m_selfOnly) { + if (srcId != m_id) + return false; + } + + unsigned char frames = layer3.getDataBlocks(); + + std::string source = m_lookup->find(srcId); + + m_display->writeM17(source.c_str(), grp, dstId, "R"); + m_display->writeM17RSSI(m_rssi); + + LogMessage("M17, received RF data header from %s to %s%u, %u blocks", source.c_str(), grp ? "TG " : "", dstId, frames); + + m_rfLayer3 = layer3; + m_rfFrames = 0U; + + m_rfState = RS_RF_DATA; + +#if defined(DUMP_M17) + openFile(); +#endif + } + + if (m_rfState != RS_RF_DATA) + return false; + + CSync::addM17Sync(data + 2U); + + CM17LICH lich; + lich.setRFCT(M17_LICH_RFCT_RDCH); + lich.setFCT(M17_LICH_USC_UDCH); + lich.setOption(option); + lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); + lich.encode(data + 2U); + + lich.setDirection(M17_LICH_DIRECTION_INBOUND); + netData[0U] = lich.getRaw(); + + udch.getRaw(netData + 1U); + + unsigned char type = M17_MESSAGE_TYPE_DCALL_DATA; + + if (validUDCH) { + type = layer3.getMessageType(); + data[0U] = type == M17_MESSAGE_TYPE_TX_REL ? TAG_EOT : TAG_DATA; + + udch.setRAN(m_ran); + udch.encode(data + 2U); + } else { + data[0U] = TAG_DATA; + data[1U] = 0x00U; + } + + scrambler(data + 2U); + + switch (type) { + case M17_MESSAGE_TYPE_DCALL_HDR: + writeNetwork(netData, NNMT_DATA_HEADER); + break; + case M17_MESSAGE_TYPE_TX_REL: + writeNetwork(netData, NNMT_DATA_TRAILER); + break; + default: + writeNetwork(netData, NNMT_DATA_BODY); + break; + } + + if (m_duplex) + writeQueueRF(data); + + m_rfFrames++; + +#if defined(DUMP_M17) + writeFile(data + 2U); +#endif + + if (data[0U] == TAG_EOT) { + unsigned short dstId = m_rfLayer3.getDestinationGroupId(); + bool grp = m_rfLayer3.getIsGroup(); + std::string source = m_lookup->find(m_rfLayer3.getSourceUnitId()); + + LogMessage("M17, ended RF data transmission from %s to %s%u", source.c_str(), grp ? "TG " : "", dstId); + writeEndRF(); + } + + return true; +} + +unsigned int CM17Control::readModem(unsigned char* data) +{ + assert(data != NULL); + + if (m_queue.isEmpty()) + return 0U; + + unsigned char len = 0U; + m_queue.getData(&len, 1U); + + m_queue.getData(data, len); + + return len; +} + +void CM17Control::writeEndRF() +{ + m_rfState = RS_RF_LISTENING; + + m_rfMask = 0x00U; + m_rfLayer3.reset(); + + m_rfTimeoutTimer.stop(); + + if (m_netState == RS_NET_IDLE) { + m_display->clearM17(); + + if (m_network != NULL) + m_network->reset(); + } + +#if defined(DUMP_M17) + closeFile(); +#endif +} + +void CM17Control::writeEndNet() +{ + m_netState = RS_NET_IDLE; + + m_netMask = 0x00U; + m_netLayer3.reset(); + + m_netTimeoutTimer.stop(); + m_networkWatchdog.stop(); + m_packetTimer.stop(); + + m_display->clearM17(); + + if (m_network != NULL) + m_network->reset(); +} + +void CM17Control::writeNetwork() +{ + unsigned char netData[40U]; + bool exists = m_network->read(netData); + if (!exists) + return; + + if (!m_enabled) + return; + + if (m_rfState != RS_RF_LISTENING && m_netState == RS_NET_IDLE) + return; + + m_networkWatchdog.start(); + + unsigned char data[M17_FRAME_LENGTH_BYTES + 2U]; + + CSync::addM17Sync(data + 2U); + + CM17LICH lich; + lich.setRaw(netData[0U]); + unsigned char usc = lich.getFCT(); + unsigned char option = lich.getOption(); + lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); + lich.encode(data + 2U); + + if (usc == M17_LICH_USC_UDCH) { + CM17Layer3 layer3; + layer3.setData(netData + 2U, 23U); + unsigned char type = layer3.getMessageType(); + + if (m_netState == RS_NET_IDLE) { + if (type == M17_MESSAGE_TYPE_DCALL_HDR) { + unsigned short srcId = layer3.getSourceUnitId(); + unsigned short dstId = layer3.getDestinationGroupId(); + bool grp = layer3.getIsGroup(); + + unsigned char frames = layer3.getDataBlocks(); + + std::string source = m_lookup->find(srcId); + m_display->writeM17(source.c_str(), grp, dstId, "N"); + LogMessage("M17, received network data header from %s to %s%u, %u blocks", source.c_str(), grp ? "TG " : "", dstId, frames); + + m_netState = RS_NET_DATA; + } else { + return; + } + } + + if (m_netState == RS_NET_DATA) { + data[0U] = type == M17_MESSAGE_TYPE_TX_REL ? TAG_EOT : TAG_DATA; + data[1U] = 0x00U; + + CM17UDCH udch; + udch.setRAN(m_ran); + udch.setData(netData + 2U); + udch.encode(data + 2U); + + scrambler(data + 2U); + + writeQueueNet(data); + + if (type == M17_MESSAGE_TYPE_TX_REL) { + unsigned short dstId = m_netLayer3.getDestinationGroupId(); + bool grp = m_netLayer3.getIsGroup(); + std::string source = m_lookup->find(m_netLayer3.getSourceUnitId()); + + LogMessage("M17, ended network data transmission from %s to %s%u", source.c_str(), grp ? "TG " : "", dstId); + writeEndNet(); + } + } + } else if (usc == M17_LICH_USC_SACCH_NS) { + m_netLayer3.setData(netData + 5U + 0U, 10U); + + unsigned char type = m_netLayer3.getMessageType(); + if (type == M17_MESSAGE_TYPE_TX_REL && m_netState == RS_NET_IDLE) + return; + if (type == M17_MESSAGE_TYPE_VCALL && m_netState != RS_NET_IDLE) + return; + + CM17SACCH sacch; + sacch.setRAN(m_ran); + sacch.setStructure(M17_SR_SINGLE); + sacch.setData(SACCH_IDLE); + sacch.encode(data + 2U); + + CM17FACCH1 facch; + facch.setRaw(netData + 5U + 0U); + facch.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + facch.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + + data[0U] = type == M17_MESSAGE_TYPE_TX_REL ? TAG_EOT : TAG_DATA; + data[1U] = 0x00U; + + scrambler(data + 2U); + + writeQueueNet(data); + + unsigned short dstId = m_netLayer3.getDestinationGroupId(); + bool grp = m_netLayer3.getIsGroup(); + class CUserDBentry source; + m_lookup->findWithName(m_netLayer3.getSourceUnitId(), &source); + + if (type == M17_MESSAGE_TYPE_TX_REL) { + m_netFrames++; + LogMessage("M17, received network end of transmission from %s to %s%u, %.1f seconds", source.get(keyCALLSIGN).c_str(), grp ? "TG " : "", dstId, float(m_netFrames) / 12.5F); + writeEndNet(); + } else if (type == M17_MESSAGE_TYPE_VCALL) { + LogMessage("M17, received network transmission from %s to %s%u", source.get(keyCALLSIGN).c_str(), grp ? "TG " : "", dstId); + m_display->writeM17(source, grp, dstId, "N"); + + m_netTimeoutTimer.start(); + m_packetTimer.start(); + m_elapsed.start(); + m_netState = RS_NET_AUDIO; + m_netFrames = 1U; + } else { + CUtils::dump(2U, "M17, interesting non superblock network frame", netData, 33U); + return; + } + } else { + if (m_netState == RS_NET_IDLE) { + unsigned char structure = (netData[1U] >> 6) & 0x03U; + switch (structure) { + case M17_SR_1_4: + m_netLayer3.decode(netData + 2U, 18U, 0U); + if (m_netLayer3.getMessageType() == M17_MESSAGE_TYPE_VCALL) + m_netMask = 0x01U; + else + m_netMask = 0x00U; + break; + case M17_SR_2_4: + m_netMask |= 0x02U; + m_netLayer3.decode(netData + 2U, 18U, 18U); + break; + case M17_SR_3_4: + m_netMask |= 0x04U; + m_netLayer3.decode(netData + 2U, 18U, 36U); + break; + case M17_SR_4_4: + m_netMask |= 0x08U; + m_netLayer3.decode(netData + 2U, 18U, 54U); + break; + default: + break; + } + + if (m_netMask != 0x0FU) + return; + + unsigned char type = m_netLayer3.getMessageType(); + if (type != M17_MESSAGE_TYPE_VCALL) + return; + + unsigned short srcId = m_netLayer3.getSourceUnitId(); + unsigned short dstId = m_netLayer3.getDestinationGroupId(); + bool grp = m_netLayer3.getIsGroup(); + + class CUserDBentry source; + m_lookup->findWithName(srcId, &source); + LogMessage("M17, received network transmission from %s to %s%u", source.get(keyCALLSIGN).c_str(), grp ? "TG " : "", dstId); + m_display->writeM17(source, grp, dstId, "N"); + + m_netTimeoutTimer.start(); + m_packetTimer.start(); + m_elapsed.start(); + m_netState = RS_NET_AUDIO; + m_netFrames = 1U; + + // Create a dummy start message + unsigned char start[M17_FRAME_LENGTH_BYTES + 2U]; + + start[0U] = TAG_DATA; + start[1U] = 0x00U; + + // Generate the sync + CSync::addM17Sync(start + 2U); + + // Generate the LICH + CM17LICH lich; + lich.setRFCT(M17_LICH_RFCT_RDCH); + lich.setFCT(M17_LICH_USC_SACCH_NS); + lich.setOption(M17_LICH_STEAL_FACCH); + lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); + lich.encode(start + 2U); + + CM17SACCH sacch; + sacch.setRAN(m_ran); + sacch.setStructure(M17_SR_SINGLE); + sacch.setData(SACCH_IDLE); + sacch.encode(start + 2U); + + unsigned char message[22U]; + m_netLayer3.getData(message); + + CM17FACCH1 facch; + facch.setData(message); + facch.encode(start + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + facch.encode(start + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + + scrambler(start + 2U); + + writeQueueNet(start); + } + + m_netFrames++; + + data[0U] = TAG_DATA; + data[1U] = 0x00U; + + CM17SACCH sacch; + sacch.setRaw(netData + 1U); + sacch.setRAN(m_ran); + sacch.encode(data + 2U); + + if (option == M17_LICH_STEAL_NONE) { + CM17Audio audio; + audio.encode(netData + 5U + 0U, data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U); + audio.encode(netData + 5U + 14U, data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U); + } else if (option == M17_LICH_STEAL_FACCH1_1) { + CM17FACCH1 facch1; + facch1.setRaw(netData + 5U + 0U); + facch1.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + + CM17Audio audio; + audio.encode(netData + 5U + 14U, data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U); + } else if (option == M17_LICH_STEAL_FACCH1_2) { + CM17Audio audio; + audio.encode(netData + 5U + 0U, data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U); + + CM17FACCH1 facch1; + facch1.setRaw(netData + 5U + 14U); + facch1.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + } else { + CM17FACCH1 facch11; + facch11.setRaw(netData + 5U + 0U); + facch11.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); + + CM17FACCH1 facch12; + facch12.setRaw(netData + 5U + 14U); + facch12.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + } + + scrambler(data + 2U); + + writeQueueNet(data); + } +} + +void CM17Control::clock(unsigned int ms) +{ + if (m_network != NULL) + writeNetwork(); + + m_rfTimeoutTimer.clock(ms); + m_netTimeoutTimer.clock(ms); + + if (m_netState == RS_NET_AUDIO) { + m_networkWatchdog.clock(ms); + + if (m_networkWatchdog.hasExpired()) { + LogMessage("M17, network watchdog has expired, %.1f seconds", float(m_netFrames) / 12.5F); + writeEndNet(); + } + } +} + +void CM17Control::writeQueueRF(const unsigned char *data) +{ + assert(data != NULL); + + if (m_netState != RS_NET_IDLE) + return; + + if (m_rfTimeoutTimer.isRunning() && m_rfTimeoutTimer.hasExpired()) + return; + + unsigned char len = M17_FRAME_LENGTH_BYTES + 2U; + + unsigned int space = m_queue.freeSpace(); + if (space < (len + 1U)) { + LogError("M17, overflow in the M17 RF queue"); + return; + } + + m_queue.addData(&len, 1U); + + m_queue.addData(data, len); +} + +void CM17Control::writeQueueNet(const unsigned char *data) +{ + assert(data != NULL); + + if (m_netTimeoutTimer.isRunning() && m_netTimeoutTimer.hasExpired()) + return; + + unsigned char len = M17_FRAME_LENGTH_BYTES + 2U; + + unsigned int space = m_queue.freeSpace(); + if (space < (len + 1U)) { + LogError("M17, overflow in the M17 RF queue"); + return; + } + + m_queue.addData(&len, 1U); + + m_queue.addData(data, len); +} + +void CM17Control::writeNetwork(const unsigned char *data, M17_NETWORK_MESSAGE_TYPE type) +{ + assert(data != NULL); + + if (m_network == NULL) + return; + + if (m_rfTimeoutTimer.isRunning() && m_rfTimeoutTimer.hasExpired()) + return; + + m_network->write(data, type); +} + +void CM17Control::scrambler(unsigned char* data) const +{ + assert(data != NULL); + + for (unsigned int i = 0U; i < M17_FRAME_LENGTH_BYTES; i++) + data[i] ^= SCRAMBLER[i]; +} + +bool CM17Control::openFile() +{ + if (m_fp != NULL) + return true; + + time_t t; + ::time(&t); + + struct tm* tm = ::localtime(&t); + + char name[100U]; + ::sprintf(name, "M17_%04d%02d%02d_%02d%02d%02d.ambe", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); + + m_fp = ::fopen(name, "wb"); + if (m_fp == NULL) + return false; + + ::fwrite("M17", 1U, 3U, m_fp); + + return true; +} + +bool CM17Control::writeFile(const unsigned char* data) +{ + if (m_fp == NULL) + return false; + + ::fwrite(data, 1U, M17_FRAME_LENGTH_BYTES, m_fp); + + return true; +} + +void CM17Control::closeFile() +{ + if (m_fp != NULL) { + ::fclose(m_fp); + m_fp = NULL; + } +} + +bool CM17Control::isBusy() const +{ + return m_rfState != RS_RF_LISTENING || m_netState != RS_NET_IDLE; +} + +void CM17Control::enable(bool enabled) +{ + if (!enabled && m_enabled) { + m_queue.clear(); + + // Reset the RF section + m_rfState = RS_RF_LISTENING; + + m_rfMask = 0x00U; + m_rfLayer3.reset(); + + m_rfTimeoutTimer.stop(); + + // Reset the networking section + m_netState = RS_NET_IDLE; + + m_netMask = 0x00U; + m_netLayer3.reset(); + + m_netTimeoutTimer.stop(); + m_networkWatchdog.stop(); + m_packetTimer.stop(); + } + + m_enabled = enabled; +} diff --git a/M17Control.h b/M17Control.h new file mode 100644 index 0000000..de7bc45 --- /dev/null +++ b/M17Control.h @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2015-2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(M17Control_H) +#define M17Control_H + +#include "RSSIInterpolator.h" +#include "M17Network.h" +#include "M17Defines.h" +#include "RingBuffer.h" +#include "StopWatch.h" +#include "Display.h" +#include "Defines.h" +#include "Timer.h" +#include "Modem.h" + +#include + +class CM17Control { +public: + CM17Control(const std::string& callsign, bool selfOnly, CM17Network* network, CDisplay* display, unsigned int timeout, bool duplex, CRSSIInterpolator* rssiMapper); + ~CM17Control(); + + bool writeModem(unsigned char* data, unsigned int len); + + unsigned int readModem(unsigned char* data); + + void clock(unsigned int ms); + + bool isBusy() const; + + void enable(bool enabled); + +private: + std::string m_callsign; + bool m_selfOnly; + CM17Network* m_network; + CDisplay* m_display; + bool m_duplex; + CRingBuffer m_queue; + RPT_RF_STATE m_rfState; + RPT_NET_STATE m_netState; + CTimer m_rfTimeoutTimer; + CTimer m_netTimeoutTimer; + CTimer m_packetTimer; + CTimer m_networkWatchdog; + CStopWatch m_elapsed; + unsigned int m_rfFrames; + unsigned int m_netFrames; + unsigned int m_rfErrs; + unsigned int m_rfBits; + CNXDNLICH m_rfLastLICH; + CNXDNLayer3 m_rfLayer3; + CNXDNLayer3 m_netLayer3; + unsigned char m_rfMask; + unsigned char m_netMask; + CRSSIInterpolator* m_rssiMapper; + unsigned char m_rssi; + unsigned char m_maxRSSI; + unsigned char m_minRSSI; + unsigned int m_aveRSSI; + unsigned int m_rssiCount; + bool m_enabled; + FILE* m_fp; + + bool processVoice(unsigned char usc, unsigned char option, unsigned char* data); + + void writeQueueRF(const unsigned char* data); + void writeQueueNet(const unsigned char* data); + void writeNetwork(const unsigned char* data); + void writeNetwork(); + + void scrambler(unsigned char* data) const; + + void writeEndRF(); + void writeEndNet(); + + bool openFile(); + bool writeFile(const unsigned char* data); + void closeFile(); +}; + +#endif diff --git a/M17Defines.h b/M17Defines.h new file mode 100644 index 0000000..e9cb48f --- /dev/null +++ b/M17Defines.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016,2017,2018,2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(M17DEFINES_H) +#define M17DEFINES_H + +const unsigned int M17_RADIO_SYMBOL_LENGTH = 5U; // At 24 kHz sample rate + +const unsigned int M17_FRAME_LENGTH_BITS = 384U; +const unsigned int M17_FRAME_LENGTH_BYTES = M17_FRAME_LENGTH_BITS / 8U; +const unsigned int M17_FRAME_LENGTH_SYMBOLS = M17_FRAME_LENGTH_BITS / 2U; + +const unsigned int M17_SYNC_LENGTH_BITS = 16U; +const unsigned int M17_SYNC_LENGTH_SYMBOLS = M17_SYNC_LENGTH_BITS / 2U; + +const unsigned char M17_SYNC_BYTES[] = {0x32U, 0x43U}; +const unsigned int M17_SYNC_BYTES_LENGTH = 2U; + +#endif diff --git a/M17Network.cpp b/M17Network.cpp new file mode 100644 index 0000000..3a21373 --- /dev/null +++ b/M17Network.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2009-2014,2016,2019,2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "M17Network.h" +#include "M17Defines.h" +#include "Defines.h" +#include "Utils.h" +#include "Log.h" + +#include +#include +#include + +const unsigned int BUFFER_LENGTH = 200U; + +CM17Network::CM17Network(unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, bool debug) : +m_socket(localPort), +m_addr(), +m_addrLen(0U), +m_debug(debug), +m_enabled(false), +m_buffer(1000U, "M17 Network") +{ + if (CUDPSocket::lookup(gatewayAddress, gatewayPort, m_addr, m_addrLen) != 0) + m_addrLen = 0U; +} + +CM17Network::~CM17Network() +{ +} + +bool CM17Network::open() +{ + if (m_addrLen == 0U) { + LogError("Unable to resolve the address of the M17 Gateway"); + return false; + } + + LogMessage("Opening M17 network connection"); + + return m_socket.open(m_addr); +} + +bool CM17Network::write(const unsigned char* data) +{ + assert(data != NULL); + + unsigned char buffer[100U]; + + buffer[0U] = 'M'; + buffer[1U] = '1'; + buffer[2U] = '7'; + + if (m_debug) + CUtils::dump(1U, "M17 data transmitted", buffer, 36U); + + return m_socket.write(buffer, 36U, m_addr, m_addrLen); +} + +void CM17Network::clock(unsigned int ms) +{ + unsigned char buffer[BUFFER_LENGTH]; + + sockaddr_storage address; + unsigned int addrLen; + int length = m_socket.read(buffer, BUFFER_LENGTH, address, addrLen); + if (length <= 0) + return; + + if (!CUDPSocket::match(m_addr, address)) { + LogMessage("M17, packet received from an invalid source"); + return; + } + + if (!m_enabled) + return; + + if (m_debug) + CUtils::dump(1U, "M17 Network Data Received", buffer, length); + + if (::memcmp(buffer + 0U, "M17", 3U) != 0) + return; + + unsigned char c = length; + m_buffer.addData(&c, 1U); + + m_buffer.addData(buffer, length); +} + +bool CM17Network::read(unsigned char* data) +{ + assert(data != NULL); + + if (m_buffer.isEmpty()) + return false; + + unsigned char c = 0U; + m_buffer.getData(&c, 1U); + + m_buffer.getData(data, c); + + return true; +} + +void CM17Network::close() +{ + m_socket.close(); + + LogMessage("Closing M17 network connection"); +} + +void CM17Network::enable(bool enabled) +{ + if (!enabled && m_enabled) + m_buffer.clear(); + + m_enabled = enabled; +} diff --git a/M17Network.h b/M17Network.h new file mode 100644 index 0000000..d0ef7b5 --- /dev/null +++ b/M17Network.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2009-2014,2016,2018,2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#ifndef M17Network_H +#define M17Network_H + +#include "M17Defines.h" +#include "RingBuffer.h" +#include "UDPSocket.h" + +#include + +class CM17Network { +public: + CM17Network(unsigned int localPort, const std::string& gwyAddress, unsigned int gwyPort, bool debug); + ~CM17Network(); + + bool open(); + + void enable(bool enabled); + + bool write(const unsigned char* data); + + bool read(unsigned char* data); + + void reset(); + + void close(); + + void clock(unsigned int ms); + +private: + CUDPSocket m_socket; + sockaddr_storage m_addr; + unsigned int m_addrLen; + bool m_debug; + bool m_enabled; + CRingBuffer m_buffer; +}; + +#endif diff --git a/MMDVM.ini b/MMDVM.ini index 408e79b..6f51621 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -58,6 +58,7 @@ RFLevel=100 # YSFTXLevel=50 # P25TXLevel=50 # NXDNTXLevel=50 +# M17TXLevel-50 # POCSAGTXLevel=50 # FMTXLevel=50 RSSIMappingFile=RSSI.dat @@ -131,6 +132,12 @@ RemoteGateway=0 TXHang=5 # ModeHang=10 +[M17] +Enable=1 +SelfOnly=0 +TXHang=5 +# ModeHang=10 + [POCSAG] Enable=1 Frequency=439987500 @@ -215,6 +222,14 @@ GatewayPort=14020 # ModeHang=3 Debug=0 +[M17 Network] +Enable=1 +GatewayAddress=127.0.0.1 +GatewayPort=11657 +LocalPort=11657 +# ModeHang=3 +Debug=0 + [POCSAG Network] Enable=1 LocalAddress=127.0.0.1 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index bfede16..6d8880a 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -120,12 +120,14 @@ m_dmr(NULL), m_ysf(NULL), m_p25(NULL), m_nxdn(NULL), +m_m17(NULL), m_pocsag(NULL), m_dstarNetwork(NULL), m_dmrNetwork(NULL), m_ysfNetwork(NULL), m_p25Network(NULL), m_nxdnNetwork(NULL), +m_m17Network(NULL), m_pocsagNetwork(NULL), m_display(NULL), m_ump(NULL), @@ -135,11 +137,13 @@ m_dmrRFModeHang(10U), m_ysfRFModeHang(10U), m_p25RFModeHang(10U), m_nxdnRFModeHang(10U), +m_m17RFModeHang(10U), m_dstarNetModeHang(3U), m_dmrNetModeHang(3U), m_ysfNetModeHang(3U), m_p25NetModeHang(3U), m_nxdnNetModeHang(3U), +m_m17NetModeHang(3U), m_pocsagNetModeHang(3U), m_modeTimer(1000U), m_dmrTXTimer(1000U), @@ -151,6 +155,7 @@ m_dmrEnabled(false), m_ysfEnabled(false), m_p25Enabled(false), m_nxdnEnabled(false), +m_m17Enabled(false), m_pocsagEnabled(false), m_fmEnabled(false), m_cwIdTime(0U), @@ -317,6 +322,12 @@ int CMMDVMHost::run() return 1; } + if (m_m17Enabled && m_conf.getM17NetworkEnabled()) { + ret = createM17Network(); + if (!ret) + return 1; + } + if (m_pocsagEnabled && m_conf.getPOCSAGNetworkEnabled()) { ret = createPOCSAGNetwork(); if (!ret) @@ -496,7 +507,6 @@ int CMMDVMHost::run() else if (ovcm == DMR_OVCM_ON) LogInfo(" OVCM: on"); - switch (dmrBeacons) { case DMR_BEACONS_NETWORK: { unsigned int dmrBeaconDuration = m_conf.getDMRBeaconDuration(); @@ -599,6 +609,19 @@ int CMMDVMHost::run() m_nxdn = new CNXDNControl(ran, id, selfOnly, m_nxdnNetwork, m_display, m_timeout, m_duplex, remoteGateway, m_nxdnLookup, rssi); } + if (m_m17Enabled) { + bool selfOnly = m_conf.getM17SelfOnly(); + unsigned int txHang = m_conf.getM17TXHang(); + m_m17RFModeHang = m_conf.getM17ModeHang(); + + LogInfo("M17 RF Parameters"); + LogInfo(" Self Only: %s", selfOnly ? "yes" : "no"); + LogInfo(" TX Hang: %us", txHang); + LogInfo(" Mode Hang: %us", m_m17RFModeHang); + + m_m17 = new CM17Control(m_callsign, selfOnly, m_m17Network, m_display, m_timeout, m_duplex, rssi); + } + CTimer pocsagTimer(1000U, 30U); if (m_pocsagEnabled) { @@ -807,6 +830,22 @@ int CMMDVMHost::run() } } + len = m_modem->readM17Data(data); + if (m_m17 != NULL && len > 0U) { + if (m_mode == MODE_IDLE) { + bool ret = m_m17->writeModem(data, len); + if (ret) { + m_modeTimer.setTimeout(m_m17RFModeHang); + setMode(MODE_M17); + } + } else if (m_mode == MODE_M17) { + m_m17->writeModem(data, len); + m_modeTimer.start(); + } else if (m_mode != MODE_LOCKOUT) { + LogWarning("M17 modem data received when in mode %u", m_mode); + } + } + len = m_modem->readTransparentData(data); if (transparentSocket != NULL && len > 0U) transparentSocket->write(data, len, transparentAddress, transparentAddrLen); @@ -938,6 +977,25 @@ int CMMDVMHost::run() } } + if (m_m17 != NULL) { + ret = m_modem->hasM17Space(); + if (ret) { + len = m_m17->readModem(data); + if (len > 0U) { + if (m_mode == MODE_IDLE) { + m_modeTimer.setTimeout(m_m17NetModeHang); + setMode(MODE_M17); + } + if (m_mode == MODE_M17) { + m_modem->writeM17Data(data, len); + m_modeTimer.start(); + } else if (m_mode != MODE_LOCKOUT) { + LogWarning("M17 data received when in mode %u", m_mode); + } + } + } + } + if (m_pocsag != NULL) { ret = m_modem->hasPOCSAGSpace(); if (ret) { @@ -987,6 +1045,8 @@ int CMMDVMHost::run() m_p25->clock(ms); if (m_nxdn != NULL) m_nxdn->clock(ms); + if (m_m17 != NULL) + m_m17->clock(ms); if (m_pocsag != NULL) m_pocsag->clock(ms); @@ -1000,6 +1060,8 @@ int CMMDVMHost::run() m_p25Network->clock(ms); if (m_nxdnNetwork != NULL) m_nxdnNetwork->clock(ms); + if (m_m17Network != NULL) + m_m17Network->clock(ms); if (m_pocsagNetwork != NULL) m_pocsagNetwork->clock(ms); @@ -1114,6 +1176,11 @@ int CMMDVMHost::run() delete m_nxdnNetwork; } + if (m_m17Network != NULL) { + m_m17Network->close(); + delete m_m17Network; + } + if (m_pocsagNetwork != NULL) { m_pocsagNetwork->close(); delete m_pocsagNetwork; @@ -1134,6 +1201,7 @@ int CMMDVMHost::run() delete m_ysf; delete m_p25; delete m_nxdn; + delete m_m17; delete m_pocsag; return 0; @@ -1156,6 +1224,7 @@ bool CMMDVMHost::createModem() float ysfTXLevel = m_conf.getModemYSFTXLevel(); float p25TXLevel = m_conf.getModemP25TXLevel(); float nxdnTXLevel = m_conf.getModemNXDNTXLevel(); + float m17TXLevel = m_conf.getModemM17TXLevel(); float pocsagTXLevel = m_conf.getModemPOCSAGTXLevel(); float fmTXLevel = m_conf.getModemFMTXLevel(); bool trace = m_conf.getModemTrace(); @@ -1165,6 +1234,7 @@ bool CMMDVMHost::createModem() unsigned int ysfTXHang = m_conf.getFusionTXHang(); unsigned int p25TXHang = m_conf.getP25TXHang(); unsigned int nxdnTXHang = m_conf.getNXDNTXHang(); + unsigned int m17TXHang = m_conf.getM17TXHang(); unsigned int rxFrequency = m_conf.getRXFrequency(); unsigned int txFrequency = m_conf.getTXFrequency(); unsigned int pocsagFrequency = m_conf.getPOCSAGFrequency(); @@ -1197,6 +1267,7 @@ bool CMMDVMHost::createModem() LogInfo(" YSF TX Level: %.1f%%", ysfTXLevel); LogInfo(" P25 TX Level: %.1f%%", p25TXLevel); LogInfo(" NXDN TX Level: %.1f%%", nxdnTXLevel); + LogInfo(" M17 TX Level: %.1f%%", m17TXLevel); LogInfo(" POCSAG TX Level: %.1f%%", pocsagTXLevel); LogInfo(" FM TX Level: %.1f%%", fmTXLevel); LogInfo(" TX Frequency: %uHz (%uHz)", txFrequency, txFrequency + txOffset); @@ -1204,13 +1275,14 @@ bool CMMDVMHost::createModem() m_modem = CModem::createModem(port, m_duplex, rxInvert, txInvert, pttInvert, txDelay, dmrDelay, useCOSAsLockout, trace, debug); m_modem->setSerialParams(protocol, address); - m_modem->setModeParams(m_dstarEnabled, m_dmrEnabled, m_ysfEnabled, m_p25Enabled, m_nxdnEnabled, m_pocsagEnabled, m_fmEnabled); - m_modem->setLevels(rxLevel, cwIdTXLevel, dstarTXLevel, dmrTXLevel, ysfTXLevel, p25TXLevel, nxdnTXLevel, pocsagTXLevel, fmTXLevel); + m_modem->setModeParams(m_dstarEnabled, m_dmrEnabled, m_ysfEnabled, m_p25Enabled, m_nxdnEnabled, m_m17Enabled, m_pocsagEnabled, m_fmEnabled); + m_modem->setLevels(rxLevel, cwIdTXLevel, dstarTXLevel, dmrTXLevel, ysfTXLevel, p25TXLevel, nxdnTXLevel, m17TXLevel, pocsagTXLevel, fmTXLevel); m_modem->setRFParams(rxFrequency, rxOffset, txFrequency, txOffset, txDCOffset, rxDCOffset, rfLevel, pocsagFrequency); m_modem->setDMRParams(colorCode); m_modem->setYSFParams(lowDeviation, ysfTXHang); m_modem->setP25Params(p25TXHang); m_modem->setNXDNParams(nxdnTXHang); + m_modem->setM17Params(m17TXHang); if (m_fmEnabled) { std::string callsign = m_conf.getFMCallsign(); @@ -1465,6 +1537,33 @@ bool CMMDVMHost::createNXDNNetwork() return true; } +bool CMMDVMHost::createM17Network() +{ + std::string gatewayAddress = m_conf.getM17GatewayAddress(); + unsigned int gatewayPort = m_conf.getM17GatewayPort(); + unsigned int localPort = m_conf.getM17LocalPort(); + m_m17NetModeHang = m_conf.getM17NetworkModeHang(); + bool debug = m_conf.getM17NetworkDebug(); + + LogInfo("M17 Network Parameters"); + LogInfo(" Gateway Address: %s", gatewayAddress.c_str()); + LogInfo(" Gateway Port: %u", gatewayPort); + LogInfo(" Local Port: %u", localPort); + LogInfo(" Mode Hang: %us", m_m17NetModeHang); + + m_m17Network = new CM17Network(localPort, gatewayAddress, gatewayPort, debug); + bool ret = m_m17Network->open(); + if (!ret) { + delete m_m17Network; + m_m17Network = NULL; + return false; + } + + m_m17Network->enable(true); + + return true; +} + bool CMMDVMHost::createPOCSAGNetwork() { std::string gatewayAddress = m_conf.getPOCSAGGatewayAddress(); @@ -1502,6 +1601,7 @@ void CMMDVMHost::readParams() m_ysfEnabled = m_conf.getFusionEnabled(); m_p25Enabled = m_conf.getP25Enabled(); m_nxdnEnabled = m_conf.getNXDNEnabled(); + m_m17Enabled = m_conf.getM17Enabled(); m_pocsagEnabled = m_conf.getPOCSAGEnabled(); m_fmEnabled = m_conf.getFMEnabled(); m_duplex = m_conf.getDuplex(); @@ -1519,6 +1619,7 @@ void CMMDVMHost::readParams() LogInfo(" YSF: %s", m_ysfEnabled ? "enabled" : "disabled"); LogInfo(" P25: %s", m_p25Enabled ? "enabled" : "disabled"); LogInfo(" NXDN: %s", m_nxdnEnabled ? "enabled" : "disabled"); + LogInfo(" M17: %s", m_m17Enabled ? "enabled" : "disabled"); LogInfo(" POCSAG: %s", m_pocsagEnabled ? "enabled" : "disabled"); LogInfo(" FM: %s", m_fmEnabled ? "enabled" : "disabled"); } @@ -1540,6 +1641,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25Network->enable(false); if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(false); + if (m_m17Network != NULL) + m_m17Network->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); if (m_dstar != NULL) @@ -1552,6 +1655,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25->enable(false); if (m_nxdn != NULL) m_nxdn->enable(false); + if (m_m17 != NULL) + m_m17->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); m_modem->setMode(MODE_DSTAR); @@ -1574,6 +1679,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25Network->enable(false); if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(false); + if (m_m17Network != NULL) + m_m17Network->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); if (m_dstar != NULL) @@ -1586,6 +1693,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25->enable(false); if (m_nxdn != NULL) m_nxdn->enable(false); + if (m_m17 != NULL) + m_m17->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); m_modem->setMode(MODE_DMR); @@ -1612,6 +1721,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25Network->enable(false); if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(false); + if (m_m17Network != NULL) + m_m17Network->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); if (m_dstar != NULL) @@ -1624,6 +1735,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25->enable(false); if (m_nxdn != NULL) m_nxdn->enable(false); + if (m_m17 != NULL) + m_m17->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); m_modem->setMode(MODE_YSF); @@ -1646,6 +1759,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25Network->enable(true); if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(false); + if (m_m17Network != NULL) + m_m17Network->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); if (m_dstar != NULL) @@ -1658,6 +1773,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25->enable(true); if (m_nxdn != NULL) m_nxdn->enable(false); + if (m_m17 != NULL) + m_m17->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); m_modem->setMode(MODE_P25); @@ -1680,6 +1797,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25Network->enable(false); if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(true); + if (m_m17Network != NULL) + m_m17Network->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); if (m_dstar != NULL) @@ -1692,6 +1811,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25->enable(false); if (m_nxdn != NULL) m_nxdn->enable(true); + if (m_m17 != NULL) + m_m17->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); m_modem->setMode(MODE_NXDN); @@ -1703,6 +1824,44 @@ void CMMDVMHost::setMode(unsigned char mode) createLockFile("NXDN"); break; + case MODE_M17: + if (m_dstarNetwork != NULL) + m_dstarNetwork->enable(false); + if (m_dmrNetwork != NULL) + m_dmrNetwork->enable(false); + if (m_ysfNetwork != NULL) + m_ysfNetwork->enable(false); + if (m_p25Network != NULL) + m_p25Network->enable(false); + if (m_nxdnNetwork != NULL) + m_nxdnNetwork->enable(false); + if (m_m17Network != NULL) + m_m17Network->enable(true); + if (m_pocsagNetwork != NULL) + m_pocsagNetwork->enable(false); + if (m_dstar != NULL) + m_dstar->enable(false); + if (m_dmr != NULL) + m_dmr->enable(false); + if (m_ysf != NULL) + m_ysf->enable(false); + if (m_p25 != NULL) + m_p25->enable(false); + if (m_nxdn != NULL) + m_nxdn->enable(false); + if (m_m17 != NULL) + m_m17->enable(true); + if (m_pocsag != NULL) + m_pocsag->enable(false); + m_modem->setMode(MODE_M17); + if (m_ump != NULL) + m_ump->setMode(MODE_M17); + m_mode = MODE_M17; + m_modeTimer.start(); + m_cwIdTimer.stop(); + createLockFile("M17"); + break; + case MODE_POCSAG: if (m_dstarNetwork != NULL) m_dstarNetwork->enable(false); @@ -1714,6 +1873,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25Network->enable(false); if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(false); + if (m_m17Network != NULL) + m_m17Network->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(true); if (m_dstar != NULL) @@ -1726,6 +1887,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25->enable(false); if (m_nxdn != NULL) m_nxdn->enable(false); + if (m_m17 != NULL) + m_m17->enable(false); if (m_pocsag != NULL) m_pocsag->enable(true); m_modem->setMode(MODE_POCSAG); @@ -1748,6 +1911,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25Network->enable(false); if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(false); + if (m_m17Network != NULL) + m_m17Network->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); if (m_dstar != NULL) @@ -1760,6 +1925,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25->enable(false); if (m_nxdn != NULL) m_nxdn->enable(false); + if (m_m17 != NULL) + m_m17->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { @@ -1786,6 +1953,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25Network->enable(false); if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(false); + if (m_m17Network != NULL) + m_m17Network->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); if (m_dstar != NULL) @@ -1798,6 +1967,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25->enable(false); if (m_nxdn != NULL) m_nxdn->enable(false); + if (m_m17 != NULL) + m_m17->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { @@ -1826,6 +1997,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25Network->enable(false); if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(false); + if (m_m17Network != NULL) + m_m17Network->enable(false); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(false); if (m_dstar != NULL) @@ -1838,6 +2011,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25->enable(false); if (m_nxdn != NULL) m_nxdn->enable(false); + if (m_m17 != NULL) + m_m17->enable(false); if (m_pocsag != NULL) m_pocsag->enable(false); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { @@ -1864,6 +2039,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25Network->enable(true); if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(true); + if (m_m17Network != NULL) + m_m17Network->enable(true); if (m_pocsagNetwork != NULL) m_pocsagNetwork->enable(true); if (m_dstar != NULL) @@ -1876,6 +2053,8 @@ void CMMDVMHost::setMode(unsigned char mode) m_p25->enable(true); if (m_nxdn != NULL) m_nxdn->enable(true); + if (m_m17 != NULL) + m_m17->enable(true); if (m_pocsag != NULL) m_pocsag->enable(true); if (m_mode == MODE_DMR && m_duplex && m_modem->hasTX()) { @@ -1955,6 +2134,10 @@ void CMMDVMHost::remoteControl() if (m_nxdn != NULL) processModeCommand(MODE_NXDN, m_nxdnRFModeHang); break; + case RCD_MODE_M17: + if (m_m17 != NULL) + processModeCommand(MODE_M17, m_m17RFModeHang); + break; case RCD_MODE_FM: if (m_fmEnabled != false) processModeCommand(MODE_FM, 0); @@ -1989,6 +2172,12 @@ void CMMDVMHost::remoteControl() if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(true); break; + case RCD_ENABLE_M17: + if (m_m17 != NULL && m_m17Enabled == false) + processEnableCommand(m_m17Enabled, true); + if (m_m17Network != NULL) + m_m17Network->enable(true); + break; case RCD_ENABLE_FM: if (m_fmEnabled==false) processEnableCommand(m_fmEnabled, true); @@ -2023,6 +2212,12 @@ void CMMDVMHost::remoteControl() if (m_nxdnNetwork != NULL) m_nxdnNetwork->enable(false); break; + case RCD_DISABLE_M17: + if (m_m17 != NULL && m_m17Enabled == true) + processEnableCommand(m_m17Enabled, false); + if (m_m17Network != NULL) + m_m17Network->enable(false); + break; case RCD_DISABLE_FM: if (m_fmEnabled == true) processEnableCommand(m_fmEnabled, false); @@ -2038,18 +2233,20 @@ void CMMDVMHost::remoteControl() } m_pocsag->sendPage(ric, text); } + break; case RCD_CW: setMode(MODE_IDLE); // Force the modem to go idle so that we can send the CW text. - if (!m_modem->hasTX()){ - std::string cwtext; - for (unsigned int i = 0U; i < m_remoteControl->getArgCount(); i++) { - if (i > 0U) - cwtext += " "; - cwtext += m_remoteControl->getArgString(i); - } - m_display->writeCW(); - m_modem->sendCWId(cwtext); - } + if (!m_modem->hasTX()){ + std::string cwtext; + for (unsigned int i = 0U; i < m_remoteControl->getArgCount(); i++) { + if (i > 0U) + cwtext += " "; + cwtext += m_remoteControl->getArgString(i); + } + m_display->writeCW(); + m_modem->sendCWId(cwtext); + } + break; default: break; } @@ -2075,9 +2272,11 @@ void CMMDVMHost::processModeCommand(unsigned char mode, unsigned int timeout) void CMMDVMHost::processEnableCommand(bool& mode, bool enabled) { - LogDebug("Setting mode current=%s new=%s",mode ? "true" : "false",enabled ? "true" : "false"); - mode=enabled; - m_modem->setModeParams(m_dstarEnabled, m_dmrEnabled, m_ysfEnabled, m_p25Enabled, m_nxdnEnabled, m_pocsagEnabled, m_fmEnabled); + LogDebug("Setting mode current=%s new=%s", mode ? "true" : "false", enabled ? "true" : "false"); + + mode = enabled; + + m_modem->setModeParams(m_dstarEnabled, m_dmrEnabled, m_ysfEnabled, m_p25Enabled, m_nxdnEnabled, m_m17Enabled, m_pocsagEnabled, m_fmEnabled); if (!m_modem->writeConfig()) LogError("Cannot write Config to MMDVM"); } diff --git a/MMDVMHost.h b/MMDVMHost.h index 4c58f98..9fadcb9 100644 --- a/MMDVMHost.h +++ b/MMDVMHost.h @@ -29,10 +29,12 @@ #include "YSFControl.h" #include "P25Control.h" #include "NXDNControl.h" +#include "M17Control.h" #include "NXDNLookup.h" #include "YSFNetwork.h" #include "P25Network.h" #include "DMRNetwork.h" +#include "M17Network.h" #include "DMRLookup.h" #include "Display.h" #include "Timer.h" @@ -59,12 +61,14 @@ private: CYSFControl* m_ysf; CP25Control* m_p25; CNXDNControl* m_nxdn; + CM17Control* m_m17; CPOCSAGControl* m_pocsag; CDStarNetwork* m_dstarNetwork; CDMRNetwork* m_dmrNetwork; CYSFNetwork* m_ysfNetwork; CP25Network* m_p25Network; INXDNNetwork* m_nxdnNetwork; + CM17Network* m_m17Network; CPOCSAGNetwork* m_pocsagNetwork; CDisplay* m_display; CUMP* m_ump; @@ -74,11 +78,13 @@ private: unsigned int m_ysfRFModeHang; unsigned int m_p25RFModeHang; unsigned int m_nxdnRFModeHang; + unsigned int m_m17RFModeHang; unsigned int m_dstarNetModeHang; unsigned int m_dmrNetModeHang; unsigned int m_ysfNetModeHang; unsigned int m_p25NetModeHang; unsigned int m_nxdnNetModeHang; + unsigned int m_m17NetModeHang; unsigned int m_pocsagNetModeHang; CTimer m_modeTimer; CTimer m_dmrTXTimer; @@ -90,6 +96,7 @@ private: bool m_ysfEnabled; bool m_p25Enabled; bool m_nxdnEnabled; + bool m_m17Enabled; bool m_pocsagEnabled; bool m_fmEnabled; unsigned int m_cwIdTime; @@ -110,6 +117,7 @@ private: bool createYSFNetwork(); bool createP25Network(); bool createNXDNNetwork(); + bool createM17Network(); bool createPOCSAGNetwork(); void remoteControl(); diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index ef7577c..317803a 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -188,6 +188,9 @@ + + + @@ -283,6 +286,8 @@ + + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index ee1ff1b..eec7ccb 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -299,6 +299,15 @@ Header Files + + Header Files + + + Header Files + + + Header Files + @@ -562,5 +571,11 @@ Source Files + + Source Files + + + Source Files + \ No newline at end of file diff --git a/Modem.cpp b/Modem.cpp index 52a497d..2451c9b 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -23,6 +23,7 @@ #include "P25Defines.h" #include "NXDNDefines.h" #include "POCSAGDefines.h" +#include "M17Defines.h" #include "Thread.h" #include "Modem.h" #include "NullModem.h" @@ -74,6 +75,9 @@ const unsigned char MMDVM_P25_LOST = 0x32U; const unsigned char MMDVM_NXDN_DATA = 0x40U; const unsigned char MMDVM_NXDN_LOST = 0x41U; +const unsigned char MMDVM_M17_DATA = 0x45U; +const unsigned char MMDVM_M17_LOST = 0x46U; + const unsigned char MMDVM_POCSAG_DATA = 0x50U; const unsigned char MMDVM_FM_PARAMS1 = 0x60U; @@ -106,6 +110,7 @@ m_ysfLoDev(false), m_ysfTXHang(4U), m_p25TXHang(5U), m_nxdnTXHang(5U), +m_m17TXHang(5U), m_duplex(duplex), m_rxInvert(rxInvert), m_txInvert(txInvert), @@ -119,6 +124,7 @@ m_dmrTXLevel(0.0F), m_ysfTXLevel(0.0F), m_p25TXLevel(0.0F), m_nxdnTXLevel(0.0F), +m_m17TXLevel(0.0F), m_pocsagTXLevel(0.0F), m_fmTXLevel(0.0F), m_rfLevel(0.0F), @@ -133,6 +139,7 @@ m_dmrEnabled(false), m_ysfEnabled(false), m_p25Enabled(false), m_nxdnEnabled(false), +m_m17Enabled(false), m_pocsagEnabled(false), m_fmEnabled(false), m_rxDCOffset(0), @@ -153,6 +160,8 @@ m_rxP25Data(1000U, "Modem RX P25"), m_txP25Data(1000U, "Modem TX P25"), m_rxNXDNData(1000U, "Modem RX NXDN"), m_txNXDNData(1000U, "Modem TX NXDN"), +m_rxM17Data(1000U, "Modem RX M17"), +m_txM17Data(1000U, "Modem TX M17"), m_txPOCSAGData(1000U, "Modem TX POCSAG"), m_rxTransparentData(1000U, "Modem RX Transparent"), m_txTransparentData(1000U, "Modem TX Transparent"), @@ -166,6 +175,7 @@ m_dmrSpace2(0U), m_ysfSpace(0U), m_p25Space(0U), m_nxdnSpace(0U), +m_m17Space(0U), m_pocsagSpace(0U), m_tx(false), m_cd(false), @@ -232,18 +242,19 @@ void CModem::setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int tx m_pocsagFrequency = pocsagFrequency + txOffset; } -void CModem::setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled) +void CModem::setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool m17Enabled, bool pocsagEnabled, bool fmEnabled) { m_dstarEnabled = dstarEnabled; m_dmrEnabled = dmrEnabled; m_ysfEnabled = ysfEnabled; m_p25Enabled = p25Enabled; m_nxdnEnabled = nxdnEnabled; + m_m17Enabled = m17Enabled; m_pocsagEnabled = pocsagEnabled; m_fmEnabled = fmEnabled; } -void CModem::setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagTXLevel, float fmTXLevel) +void CModem::setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float m17TXLevel, float pocsagTXLevel, float fmTXLevel) { m_rxLevel = rxLevel; m_cwIdTXLevel = cwIdTXLevel; @@ -252,6 +263,7 @@ void CModem::setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, flo m_ysfTXLevel = ysfTXLevel; m_p25TXLevel = p25TXLevel; m_nxdnTXLevel = nxdnTXLevel; + m_m17TXLevel = m17TXLevel; m_pocsagTXLevel = pocsagTXLevel; m_fmTXLevel = fmTXLevel; } @@ -279,6 +291,11 @@ void CModem::setNXDNParams(unsigned int txHang) m_nxdnTXHang = txHang; } +void CModem::setM17Params(unsigned int txHang) +{ + m_m17TXHang = txHang; +} + void CModem::setTransparentDataParams(unsigned int sendFrameType) { m_sendTransparentDataFrameType = sendFrameType; @@ -587,12 +604,39 @@ void CModem::clock(unsigned int ms) } break; + case MMDVM_M17_DATA: { + if (m_trace) + CUtils::dump(1U, "RX M17 Data", m_buffer, m_length); + + unsigned char data = m_length - 2U; + m_rxM17Data.addData(&data, 1U); + + data = TAG_DATA; + m_rxM17Data.addData(&data, 1U); + + m_rxM17Data.addData(m_buffer + 3U, m_length - 3U); + } + break; + + case MMDVM_M17_LOST: { + if (m_trace) + CUtils::dump(1U, "RX M17 Lost", m_buffer, m_length); + + unsigned char data = 1U; + m_rxM17Data.addData(&data, 1U); + + data = TAG_LOST; + m_rxM17Data.addData(&data, 1U); + } + break; + case MMDVM_GET_STATUS: { // if (m_trace) // CUtils::dump(1U, "GET_STATUS", m_buffer, m_length); m_p25Space = 0U; m_nxdnSpace = 0U; + m_m17Space = 0U; m_pocsagSpace = 0U; m_mode = m_buffer[4U]; @@ -630,9 +674,11 @@ void CModem::clock(unsigned int ms) m_nxdnSpace = m_buffer[11U]; if (m_length > 12U) m_pocsagSpace = m_buffer[12U]; + if (m_length > 13U) + m_m17Space = m_buffer[13U]; m_inactivityTimer.start(); - // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[5U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_pocsagSpace, int(m_lockout), int(m_cd)); + // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[5U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_m17Space, m_pocsagSpace, int(m_lockout), int(m_cd)); } break; @@ -819,6 +865,23 @@ void CModem::clock(unsigned int ms) m_nxdnSpace--; } + if (m_m17Space > 1U && !m_txM17Data.isEmpty()) { + unsigned char len = 0U; + m_txM17Data.getData(&len, 1U); + m_txM17Data.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX M17 Data", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing M17 data to the MMDVM"); + + m_playoutTimer.start(); + + m_m17Space--; + } + if (m_pocsagSpace > 1U && !m_txPOCSAGData.isEmpty()) { unsigned char len = 0U; m_txPOCSAGData.getData(&len, 1U); @@ -943,6 +1006,20 @@ unsigned int CModem::readNXDNData(unsigned char* data) return len; } +unsigned int CModem::readM17Data(unsigned char* data) +{ + assert(data != NULL); + + if (m_rxM17Data.isEmpty()) + return 0U; + + unsigned char len = 0U; + m_rxM17Data.getData(&len, 1U); + m_rxM17Data.getData(data, len); + + return len; +} + unsigned int CModem::readTransparentData(unsigned char* data) { assert(data != NULL); @@ -1157,6 +1234,36 @@ bool CModem::writeNXDNData(const unsigned char* data, unsigned int length) return true; } +bool CModem::hasM17Space() const +{ + unsigned int space = m_txM17Data.freeSpace() / (M17_FRAME_LENGTH_BYTES + 4U); + + return space > 1U; +} + +bool CModem::writeM17Data(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + assert(length > 0U); + + if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) + return false; + + unsigned char buffer[130U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = length + 2U; + buffer[2U] = MMDVM_M17_DATA; + + ::memcpy(buffer + 3U, data + 1U, length - 1U); + + unsigned char len = length + 2U; + m_txM17Data.addData(&len, 1U); + m_txM17Data.addData(buffer, len); + + return true; +} + bool CModem::hasPOCSAGSpace() const { unsigned int space = m_txPOCSAGData.freeSpace() / (POCSAG_FRAME_LENGTH_BYTES + 4U); @@ -1351,6 +1458,30 @@ bool CModem::writeNXDNInfo(const char* source, bool group, unsigned int dest, co return m_serial->write(buffer, 31U) != 31; } +bool CModem::writeM17Info(const char* source, const char* dest, const char* type) +{ + assert(m_serial != NULL); + assert(source != NULL); + assert(dest != NULL); + assert(type != NULL); + + unsigned char buffer[40U]; + + buffer[0U] = MMDVM_FRAME_START; + buffer[1U] = 31U; + buffer[2U] = MMDVM_QSO_INFO; + + buffer[3U] = MODE_M17; + + ::sprintf((char*)(buffer + 4U), "%9.9s", source); + + ::sprintf((char*)(buffer + 13U), "%9.9s", dest); + + ::memcpy(buffer + 22U, type, 1U); + + return m_serial->write(buffer, 23U) != 23; +} + bool CModem::writePOCSAGInfo(unsigned int ric, const std::string& message) { assert(m_serial != NULL); @@ -1523,7 +1654,7 @@ bool CModem::setConfig() buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 24U; + buffer[1U] = 26U; buffer[2U] = MMDVM_SET_CONFIG; @@ -1558,6 +1689,8 @@ bool CModem::setConfig() buffer[4U] |= 0x20U; if (m_fmEnabled && m_duplex) buffer[4U] |= 0x40U; + if (m_m17Enabled) + buffer[4U] |= 0x80U; buffer[5U] = m_txDelay / 10U; // In 10ms units @@ -1593,10 +1726,13 @@ bool CModem::setConfig() buffer[23U] = (unsigned char)m_nxdnTXHang; - // CUtils::dump(1U, "Written", buffer, 24U); + buffer[24U] = (unsigned char)(m_m17TXLevel * 2.55F + 0.5F); + buffer[25U] = (unsigned char)m_m17TXHang; - int ret = m_serial->write(buffer, 24U); - if (ret != 24) + // CUtils::dump(1U, "Written", buffer, 26U); + + int ret = m_serial->write(buffer, 26U); + if (ret != 26) return false; unsigned int count = 0U; diff --git a/Modem.h b/Modem.h index e28aad4..8c5e53a 100644 --- a/Modem.h +++ b/Modem.h @@ -39,12 +39,13 @@ public: virtual void setSerialParams(const std::string& protocol, unsigned int address); virtual void setRFParams(unsigned int rxFrequency, int rxOffset, unsigned int txFrequency, int txOffset, int txDCOffset, int rxDCOffset, float rfLevel, unsigned int pocsagFrequency); - virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool pocsagEnabled, bool fmEnabled); - virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float pocsagLevel, float fmTXLevel); + virtual void setModeParams(bool dstarEnabled, bool dmrEnabled, bool ysfEnabled, bool p25Enabled, bool nxdnEnabled, bool m17Enabled, bool pocsagEnabled, bool fmEnabled); + virtual void setLevels(float rxLevel, float cwIdTXLevel, float dstarTXLevel, float dmrTXLevel, float ysfTXLevel, float p25TXLevel, float nxdnTXLevel, float m17TXLevel, float pocsagLevel, float fmTXLevel); virtual void setDMRParams(unsigned int colorCode); virtual void setYSFParams(bool loDev, unsigned int txHang); virtual void setP25Params(unsigned int txHang); virtual void setNXDNParams(unsigned int txHang); + virtual void setM17Params(unsigned int txHang); virtual void setTransparentDataParams(unsigned int sendFrameType); virtual void setFMCallsignParams(const std::string& callsign, unsigned int callsignSpeed, unsigned int callsignFrequency, unsigned int callsignTime, unsigned int callsignHoldoff, float callsignHighLevel, float callsignLowLevel, bool callsignAtStart, bool callsignAtEnd, bool callsignAtLatch); @@ -59,6 +60,7 @@ public: virtual unsigned int readYSFData(unsigned char* data); virtual unsigned int readP25Data(unsigned char* data); virtual unsigned int readNXDNData(unsigned char* data); + virtual unsigned int readM17Data(unsigned char* data); virtual unsigned int readTransparentData(unsigned char* data); virtual unsigned int readSerial(unsigned char* data, unsigned int length); @@ -69,6 +71,7 @@ public: virtual bool hasYSFSpace() const; virtual bool hasP25Space() const; virtual bool hasNXDNSpace() const; + virtual bool hasM17Space() const; virtual bool hasPOCSAGSpace() const; virtual bool hasTX() const; @@ -84,6 +87,7 @@ public: virtual bool writeYSFData(const unsigned char* data, unsigned int length); virtual bool writeP25Data(const unsigned char* data, unsigned int length); virtual bool writeNXDNData(const unsigned char* data, unsigned int length); + virtual bool writeM17Data(const unsigned char* data, unsigned int length); virtual bool writePOCSAGData(const unsigned char* data, unsigned int length); virtual bool writeTransparentData(const unsigned char* data, unsigned int length); @@ -93,6 +97,7 @@ public: virtual bool writeYSFInfo(const char* source, const char* dest, unsigned char dgid, const char* type, const char* origin); virtual bool writeP25Info(const char* source, bool group, unsigned int dest, const char* type); virtual bool writeNXDNInfo(const char* source, bool group, unsigned int dest, const char* type); + virtual bool writeM17Info(const char* source, const char* dest, const char* type); virtual bool writePOCSAGInfo(unsigned int ric, const std::string& message); virtual bool writeIPInfo(const std::string& address); @@ -122,6 +127,7 @@ private: unsigned int m_ysfTXHang; unsigned int m_p25TXHang; unsigned int m_nxdnTXHang; + unsigned int m_m17TXHang; bool m_duplex; bool m_rxInvert; bool m_txInvert; @@ -135,6 +141,7 @@ private: float m_ysfTXLevel; float m_p25TXLevel; float m_nxdnTXLevel; + float m_m17TXLevel; float m_pocsagTXLevel; float m_fmTXLevel; float m_rfLevel; @@ -149,6 +156,7 @@ private: bool m_ysfEnabled; bool m_p25Enabled; bool m_nxdnEnabled; + bool m_m17Enabled; bool m_pocsagEnabled; bool m_fmEnabled; int m_rxDCOffset; @@ -169,6 +177,8 @@ private: CRingBuffer m_txP25Data; CRingBuffer m_rxNXDNData; CRingBuffer m_txNXDNData; + CRingBuffer m_rxM17Data; + CRingBuffer m_txM17Data; CRingBuffer m_txPOCSAGData; CRingBuffer m_rxTransparentData; CRingBuffer m_txTransparentData; @@ -182,6 +192,7 @@ private: unsigned int m_ysfSpace; unsigned int m_p25Space; unsigned int m_nxdnSpace; + unsigned int m_m17Space; unsigned int m_pocsagSpace; bool m_tx; bool m_cd; diff --git a/RemoteControl.cpp b/RemoteControl.cpp index 3c2d2a8..2e3bf8e 100644 --- a/RemoteControl.cpp +++ b/RemoteControl.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 by Jonathan Naylor G4KLX + * Copyright (C) 2019,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -89,6 +89,8 @@ REMOTE_COMMAND CRemoteControl::getCommand() m_command = RCD_MODE_P25; else if (m_args.at(1U) == "nxdn") m_command = RCD_MODE_NXDN; + else if (m_args.at(1U) == "m17") + m_command = RCD_MODE_M17; } else if (m_args.at(0U) == "enable" && m_args.size() >= ENABLE_ARGS) { if (m_args.at(1U) == "dstar") m_command = RCD_ENABLE_DSTAR; @@ -100,6 +102,8 @@ REMOTE_COMMAND CRemoteControl::getCommand() m_command = RCD_ENABLE_P25; else if (m_args.at(1U) == "nxdn") m_command = RCD_ENABLE_NXDN; + else if (m_args.at(1U) == "m17") + m_command = RCD_ENABLE_M17; else if (m_args.at(1U) == "fm") m_command = RCD_ENABLE_FM; } else if (m_args.at(0U) == "disable" && m_args.size() >= DISABLE_ARGS) { @@ -113,6 +117,8 @@ REMOTE_COMMAND CRemoteControl::getCommand() m_command = RCD_DISABLE_P25; else if (m_args.at(1U) == "nxdn") m_command = RCD_DISABLE_NXDN; + else if (m_args.at(1U) == "m17") + m_command = RCD_DISABLE_M17; else if (m_args.at(1U) == "fm") m_command = RCD_DISABLE_FM; } else if (m_args.at(0U) == "page" && m_args.size() >= PAGE_ARGS) { @@ -144,6 +150,7 @@ unsigned int CRemoteControl::getArgCount() const case RCD_MODE_YSF: case RCD_MODE_P25: case RCD_MODE_NXDN: + case RCD_MODE_M17: return m_args.size() - SET_MODE_ARGS; case RCD_PAGE: return m_args.size() - 1U; @@ -164,14 +171,15 @@ std::string CRemoteControl::getArgString(unsigned int n) const case RCD_MODE_YSF: case RCD_MODE_P25: case RCD_MODE_NXDN: + case RCD_MODE_M17: n += SET_MODE_ARGS; break; case RCD_PAGE: n += 1U; break; case RCD_CW: - n += 1U; - break; + n += 1U; + break; default: return ""; } diff --git a/RemoteControl.h b/RemoteControl.h index c8d060d..ace1425 100644 --- a/RemoteControl.h +++ b/RemoteControl.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 by Jonathan Naylor G4KLX + * Copyright (C) 2019,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,18 +33,21 @@ enum REMOTE_COMMAND { RCD_MODE_YSF, RCD_MODE_P25, RCD_MODE_NXDN, + RCD_MODE_M17, RCD_MODE_FM, RCD_ENABLE_DSTAR, RCD_ENABLE_DMR, RCD_ENABLE_YSF, RCD_ENABLE_P25, RCD_ENABLE_NXDN, + RCD_ENABLE_M17, RCD_ENABLE_FM, RCD_DISABLE_DSTAR, RCD_DISABLE_DMR, RCD_DISABLE_YSF, RCD_DISABLE_P25, RCD_DISABLE_NXDN, + RCD_DISABLE_M17, RCD_DISABLE_FM, RCD_PAGE, RCD_CW diff --git a/Sync.cpp b/Sync.cpp index 75156af..7144683 100644 --- a/Sync.cpp +++ b/Sync.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2018,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,6 +23,7 @@ #include "YSFDefines.h" #include "P25Defines.h" #include "NXDNDefines.h" +#include "M17Defines.h" #include #include @@ -83,3 +84,10 @@ void CSync::addNXDNSync(unsigned char* data) for (unsigned int i = 0U; i < NXDN_FSW_BYTES_LENGTH; i++) data[i] = (data[i] & ~NXDN_FSW_BYTES_MASK[i]) | NXDN_FSW_BYTES[i]; } + +void CSync::addM17Sync(unsigned char* data) +{ + assert(data != NULL); + + ::memcpy(data, M17_SYNC_BYTES, M17_SYNC_BYTES_LENGTH); +} diff --git a/Sync.h b/Sync.h index 3ff325c..2118c96 100644 --- a/Sync.h +++ b/Sync.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2018,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,6 +33,8 @@ public: static void addNXDNSync(unsigned char* data); + static void addM17Sync(unsigned char* data); + private: }; diff --git a/Version.h b/Version.h index d8d0145..dcda43b 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201013"; +const char* VERSION = "20201014"; #endif From 80a77bd12adf16a9b55d5c343f95c6a120a73777 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 15 Oct 2020 13:59:28 +0100 Subject: [PATCH 117/163] Add the M17 page to the Nextion displays. --- Images/M17.bmp | Bin 0 -> 58014 bytes Nextion_G4KLX/NX3224K024.HMI | Bin 544682 -> 593215 bytes Nextion_G4KLX/NX3224K024.tft | Bin 645650 -> 687714 bytes Nextion_G4KLX/NX3224K028.HMI | Bin 544682 -> 593215 bytes Nextion_G4KLX/NX3224K028.tft | Bin 645650 -> 687730 bytes Nextion_G4KLX/NX3224T024.HMI | Bin 544682 -> 593215 bytes Nextion_G4KLX/NX3224T024.tft | Bin 584944 -> 627024 bytes Nextion_G4KLX/NX3224T028.HMI | Bin 544682 -> 593215 bytes Nextion_G4KLX/NX3224T028.tft | Bin 584944 -> 627024 bytes Nextion_G4KLX/NX4024K032.HMI | Bin 565913 -> 614446 bytes Nextion_G4KLX/NX4024K032.tft | Bin 662308 -> 708495 bytes Nextion_G4KLX/NX4024T032.HMI | Bin 565913 -> 614446 bytes Nextion_G4KLX/NX4024T032.tft | Bin 601602 -> 647789 bytes Nextion_G4KLX/NX4832K035.HMI | Bin 1110771 -> 1159304 bytes Nextion_G4KLX/NX4832K035.tft | Bin 100698 -> 1217059 bytes Nextion_G4KLX/NX4832T035.HMI | Bin 1110771 -> 1159304 bytes Nextion_G4KLX/NX4832T035.tft | Bin 1114218 -> 1156321 bytes 17 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Images/M17.bmp diff --git a/Images/M17.bmp b/Images/M17.bmp new file mode 100644 index 0000000000000000000000000000000000000000..8d88e8e0f05499dbd1193a3a0b6f7d471dbc18e6 GIT binary patch literal 58014 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 literal 0 HcmV?d00001 diff --git a/Nextion_G4KLX/NX3224K024.HMI b/Nextion_G4KLX/NX3224K024.HMI index 61c8067a911ca9b0d891fb3cfcd6e9329f05d3c8..ef8698beddb7d7d51998c585cf5e558e600f6af7 100644 GIT binary patch delta 11828 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 diff --git a/Nextion_G4KLX/NX3224K024.tft b/Nextion_G4KLX/NX3224K024.tft index 417aefd74752335b2743392520ccf262581232d0..b1a66da313ec89e21039885509d27377c993c8dd 100644 GIT binary patch delta 14080 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 lZ`&a-s@hU zlWTvc#w53(1FnOLUJOEq!lnKx%TNg&`%R7@f``H>ZT*oRn;Dc@ua_RZ5*@yT>E8n5 zVoA@hWl;2P-%FdGUkl0%kV+=0*ZsM;Mq$mT)^_pOd!d5iB5xNmv|nnSEA=#9llZXP z*UR1V3fPwNSEEGa>Qs8Q!eez*zsvcwHSZpUv?{Gid>B%6&(-pZ!J{wfTQhdOyD7^2 zoPOIVChM}$Dz8>GAb9ZktRvOJSO>T|m^6x5{+_Y#^Y+-aYdtUCdZ=)kFHq`*K>LAZ zo5c@`6I;p`AJ2A=^32LgpL;#WP5j;O6wm9^m>YH06vkaFr8g#jGRD@s%=fLk zmAH-+W1CvnLMDH(-GDj$zVc?T8NNsJ=;6B?vuGVpU&uMqcm5dGTA9?|e5YrjDxjV2o0yltRGA9 zs6BgmL0Tj}n&##&QpaeZ*Wc`=g|CTuovfiKz>I13%k@x5PO^u4*nwxo&ZcymW(7A! z{OJMfSLWLU4ZkllGiGU7uZ2_rHhBwwU5k{FDrr4mIj#2k;y#6ip`K{N&+ zbNEQ};UlJuI0rsFqlv50x8I2?;3N1xQ5&IQNqn$8r-SH-0Xc=O#BHb zPp4Gim^hmHXe}(z*e?bUd%+Hr3kK_W# z1dL#332FRH{9es(DQz7B)dG5T|b*$mWGH0GVJmkh&Uh*aUL**cnB0joC{hZVn7>2Jm{daUp_#@ zf{*NS7epNBg(wds6VPtK5Z8eQh*m)J8UQL_Gek`g3{f9Efv5#Ai2zsw8;DxqBt!*} z0Z|rIK{Ns6>j10-P7n=16hwJY1W^wRU#FuTG?D<&1#S@a!8M2&kO&bEQrP7i5V0VQ zT~3FH16dH|K@G%1z#3yb#yQr{&%qz@PMcSA|Yym3W)kZAR8UP0HQSrVV6rFDu53VWq}wx-!TDp5LbfB z5Dh>bM0qd*Q4i=c(7sfNx*!pvK6nce1DYY?K^wc=4iO8me=(4q>qz?e)x9FQ{{!VF B12zBv delta 239 zcmdl#OJ&u3#R(G5YI+@^b$I9MVlZe!d$k?}f9b2?jlIvXPpGXXI(5VHU=D-g47PiJGFp|kzkHFovi(}SOL itlTd0fz^&D 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 C&>q(S diff --git a/Nextion_G4KLX/NX3224T024.HMI b/Nextion_G4KLX/NX3224T024.HMI index 44f1538bd82037bd9c7a10188b416271560896d2..ae1204dd396fe2073ca9996521dd732158397564 100644 GIT binary patch delta 11735 zcmc&)2|Scr*gvzGv8yCyGD3yyDK4UwD20nsh}$B`7_FoX3hl1-?Mjl6khPdn)@09 zIQG19F!GZ)tmph03iE$MF|G%q(L``r=~`oF7iZ|<_U7FIH!CqOH(i6}@ZRfoSl7eN z>4lfuoEk4T`-fg`8Vfz$W

A&Z;?@rsnsItK6 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

r0nF*OG(Vvy3o9aqLpH$ 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 literal 100698 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 r$H>>fdGYcE@c9_|>Nn3OU;5_Rfco!Oo2cRLDB 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 Date: Thu, 15 Oct 2020 14:32:56 +0100 Subject: [PATCH 118/163] Handle M17 in the display code. --- CASTInfo.cpp | 10 +++++++ CASTInfo.h | 3 ++ Display.cpp | 47 +++++++++++++++++++++++++++++++ Display.h | 10 +++++++ Nextion.cpp | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ Nextion.h | 5 ++++ NullDisplay.cpp | 14 +++++++++ NullDisplay.h | 3 ++ TFTSerial.cpp | 44 ++++++++++++++++++++++++++++- TFTSerial.h | 3 ++ TFTSurenoo.cpp | 24 ++++++++++++++++ TFTSurenoo.h | 3 ++ 12 files changed, 240 insertions(+), 1 deletion(-) diff --git a/CASTInfo.cpp b/CASTInfo.cpp index 603ee20..46178c0 100644 --- a/CASTInfo.cpp +++ b/CASTInfo.cpp @@ -130,6 +130,16 @@ void CCASTInfo::clearNXDNInt() { } +void CCASTInfo::writeM17Int(const char* source, const char* dest, const char* type) +{ + if (m_modem != NULL) + m_modem->writeM17Info(source, dest, type); +} + +void CCASTInfo::clearM17Int() +{ +} + void CCASTInfo::writePOCSAGInt(uint32_t ric, const std::string& message) { if (m_modem != NULL) diff --git a/CASTInfo.h b/CASTInfo.h index 8945dcd..a369aaa 100644 --- a/CASTInfo.h +++ b/CASTInfo.h @@ -57,6 +57,9 @@ protected: virtual void writeNXDNInt(const char* source, bool group, unsigned int dest, const char* type); virtual void clearNXDNInt(); + virtual void writeM17Int(const char* source, const char* dest, const char* type); + virtual void clearM17Int(); + virtual void writePOCSAGInt(uint32_t ric, const std::string& message); virtual void clearPOCSAGInt(); diff --git a/Display.cpp b/Display.cpp index 39b0a09..bacba26 100644 --- a/Display.cpp +++ b/Display.cpp @@ -335,6 +335,40 @@ void CDisplay::clearNXDN() } } +void CDisplay::writeM17(const char* source, const char* dest, const char* type) +{ + assert(source != NULL); + assert(dest != NULL); + assert(type != NULL); + + m_timer1.start(); + m_mode1 = MODE_IDLE; + + writeM17Int(source, dest, type); +} + +void CDisplay::writeM17RSSI(unsigned char rssi) +{ + if (rssi != 0U) + writeM17RSSIInt(rssi); +} + +void CDisplay::writeM17BER(float ber) +{ + writeM17BERInt(ber); +} + +void CDisplay::clearM17() +{ + if (m_timer1.hasExpired()) { + clearM17Int(); + m_timer1.stop(); + m_mode1 = MODE_IDLE; + } else { + m_mode1 = MODE_M17; + } +} + void CDisplay::writePOCSAG(uint32_t ric, const std::string& message) { m_timer1.start(); @@ -392,6 +426,11 @@ void CDisplay::clock(unsigned int ms) m_mode1 = MODE_IDLE; m_timer1.stop(); break; + case MODE_M17: + clearM17Int(); + m_mode1 = MODE_IDLE; + m_timer1.stop(); + break; case MODE_POCSAG: clearPOCSAGInt(); m_mode1 = MODE_IDLE; @@ -482,6 +521,14 @@ void CDisplay::writeNXDNBERInt(float ber) { } +void CDisplay::writeM17RSSIInt(unsigned char rssi) +{ +} + +void CDisplay::writeM17BERInt(float ber) +{ +} + int CDisplay::writeNXDNIntEx(const class CUserDBentry& source, bool group, unsigned int dest, const char* type) { /* return value definition is same as writeDMRIntEx() */ diff --git a/Display.h b/Display.h index e23b3ec..165d05c 100644 --- a/Display.h +++ b/Display.h @@ -72,6 +72,11 @@ public: void writeNXDNBER(float ber); void clearNXDN(); + void writeM17(const char* source, const char* dest, const char* type); + void writeM17RSSI(unsigned char rssi); + void writeM17BER(float ber); + void clearM17(); + void writePOCSAG(uint32_t ric, const std::string& message); void clearPOCSAG(); @@ -118,6 +123,11 @@ protected: virtual void writeNXDNBERInt(float ber); virtual void clearNXDNInt() = 0; + virtual void writeM17Int(const char* source, const char* dest, const char* type) = 0; + virtual void writeM17RSSIInt(unsigned char rssi); + virtual void writeM17BERInt(float ber); + virtual void clearM17Int() = 0; + virtual void writePOCSAGInt(uint32_t ric, const std::string& message) = 0; virtual void clearPOCSAGInt() = 0; diff --git a/Nextion.cpp b/Nextion.cpp index fa6851e..2029aeb 100644 --- a/Nextion.cpp +++ b/Nextion.cpp @@ -36,6 +36,8 @@ const unsigned int P25_RSSI_COUNT = 7U; // 7 * 180ms = 1260ms const unsigned int P25_BER_COUNT = 7U; // 7 * 180ms = 1260ms const unsigned int NXDN_RSSI_COUNT = 28U; // 28 * 40ms = 1120ms const unsigned int NXDN_BER_COUNT = 28U; // 28 * 40ms = 1120ms +const unsigned int M17_RSSI_COUNT = 28U; // 28 * 40ms = 1120ms +const unsigned int M17_BER_COUNT = 28U; // 28 * 40ms = 1120ms #define LAYOUT_COMPAT_MASK (7 << 0) // compatibility for old setting #define LAYOUT_TA_ENABLE (1 << 4) // enable Talker Alias (TA) display @@ -822,6 +824,79 @@ void CNextion::clearNXDNInt() sendCommand("t3.txt=\"\""); } +void CNextion::writeM17Int(const char* source, const char* dest, const char* type) +{ + assert(source != NULL); + assert(dest != NULL); + assert(type != NULL); + + if (m_mode != MODE_M17) { + sendCommand("page M17"); + sendCommandAction(6U); + } + + char text[30U]; + if (m_brightness > 0) { + ::sprintf(text, "dim=%u", m_brightness); + sendCommand(text); + } + + ::sprintf(text, "t0.txt=\"%s %.10s\"", type, source); + sendCommand(text); + sendCommandAction(122U); + + ::sprintf(text, "t1.txt=\"%s\"", dest); + sendCommand(text); + sendCommandAction(123U); + + m_clockDisplayTimer.stop(); + + m_mode = MODE_M17; + m_rssiAccum1 = 0U; + m_berAccum1 = 0.0F; + m_rssiCount1 = 0U; + m_berCount1 = 0U; +} + +void CNextion::writeM17RSSIInt(unsigned char rssi) +{ + m_rssiAccum1 += rssi; + m_rssiCount1++; + + if (m_rssiCount1 == M17_RSSI_COUNT) { + char text[25U]; + ::sprintf(text, "t2.txt=\"-%udBm\"", m_rssiAccum1 / M17_RSSI_COUNT); + sendCommand(text); + sendCommandAction(124U); + m_rssiAccum1 = 0U; + m_rssiCount1 = 0U; + } +} + +void CNextion::writeM17BERInt(float ber) +{ + m_berAccum1 += ber; + m_berCount1++; + + if (m_berCount1 == M17_BER_COUNT) { + char text[25U]; + ::sprintf(text, "t3.txt=\"%.1f%%\"", m_berAccum1 / float(M17_BER_COUNT)); + sendCommand(text); + sendCommandAction(125U); + m_berAccum1 = 0.0F; + m_berCount1 = 0U; + } +} + +void CNextion::clearM17Int() +{ + sendCommand("t0.txt=\"Listening\""); + sendCommandAction(121U); + sendCommand("t1.txt=\"\""); + sendCommand("t2.txt=\"\""); + sendCommand("t3.txt=\"\""); +} + void CNextion::writePOCSAGInt(uint32_t ric, const std::string& message) { if (m_mode != MODE_POCSAG) { diff --git a/Nextion.h b/Nextion.h index 488372f..713fc90 100644 --- a/Nextion.h +++ b/Nextion.h @@ -70,6 +70,11 @@ protected: virtual void writeNXDNBERInt(float ber); virtual void clearNXDNInt(); + virtual void writeM17Int(const char* source, const char* dest, const char* type); + virtual void writeM17RSSIInt(unsigned char rssi); + virtual void writeM17BERInt(float ber); + virtual void clearM17Int(); + virtual void writePOCSAGInt(uint32_t ric, const std::string& message); virtual void clearPOCSAGInt(); diff --git a/NullDisplay.cpp b/NullDisplay.cpp index 2471e93..5708028 100644 --- a/NullDisplay.cpp +++ b/NullDisplay.cpp @@ -134,6 +134,20 @@ void CNullDisplay::clearNXDNInt() #endif } +void CNullDisplay::writeM17Int(const char* source, const char* dest, const char* type) +{ +#if defined(RASPBERRY_PI) + ::digitalWrite(LED_STATUS, 1); +#endif +} + +void CNullDisplay::clearM17Int() +{ +#if defined(RASPBERRY_PI) + ::digitalWrite(LED_STATUS, 0); +#endif +} + void CNullDisplay::writePOCSAGInt(uint32_t ric, const std::string& message) { #if defined(RASPBERRY_PI) diff --git a/NullDisplay.h b/NullDisplay.h index a222959..c53dc08 100644 --- a/NullDisplay.h +++ b/NullDisplay.h @@ -55,6 +55,9 @@ protected: virtual void writeNXDNInt(const char* source, bool group, unsigned int dest, const char* type); virtual void clearNXDNInt(); + virtual void writeM17Int(const char* source, const char* dest, const char* type); + virtual void clearM17Int(); + virtual void writePOCSAGInt(uint32_t ric, const std::string& message); virtual void clearPOCSAGInt(); diff --git a/TFTSerial.cpp b/TFTSerial.cpp index dab532e..2d0e9dc 100644 --- a/TFTSerial.cpp +++ b/TFTSerial.cpp @@ -397,7 +397,7 @@ void CTFTSerial::writeNXDNInt(const char* source, bool group, unsigned int dest, setFontSize(FONT_MEDIUM); - // Draw the P25 insignia + // Draw the NXDN insignia displayBitmap(0U, 0U, "NXDN_sm.bmp"); } @@ -427,6 +427,48 @@ void CTFTSerial::clearNXDNInt() displayText(" "); } +void CTFTSerial::writeM17Int(const char* source, const char* dest, const char* type) +{ + assert(source != NULL); + assert(dest != NULL); + assert(type != NULL); + + if (m_mode != MODE_M17) { + // Clear the screen + clearScreen(); + + setFontSize(FONT_MEDIUM); + + // Draw the M17 insignia + displayBitmap(0U, 0U, "M17_sm.bmp"); + } + + char text[30U]; + ::sprintf(text, "%s %.10s", type, source); + + gotoPosPixel(5U, 70U); + displayText(text); + + ::sprintf(text, " %s", dest); + + gotoPosPixel(5U, 90U); + displayText(text); + + m_mode = MODE_M17; +} + +void CTFTSerial::clearM17Int() +{ + gotoPosPixel(5U, 70U); + displayText(" Listening "); + + gotoPosPixel(5U, 90U); + displayText(" "); + + gotoPosPixel(5U, 110U); + displayText(" "); +} + void CTFTSerial::writePOCSAGInt(uint32_t ric, const std::string& message) { gotoPosPixel(15U, 90U); diff --git a/TFTSerial.h b/TFTSerial.h index d34e9da..86d4130 100644 --- a/TFTSerial.h +++ b/TFTSerial.h @@ -57,6 +57,9 @@ protected: virtual void writeNXDNInt(const char* source, bool group, unsigned int dest, const char* type); virtual void clearNXDNInt(); + virtual void writeM17Int(const char* source, const char* dest, const char* type); + virtual void clearM17Int(); + virtual void writePOCSAGInt(uint32_t ric, const std::string& message); virtual void clearPOCSAGInt(); diff --git a/TFTSurenoo.cpp b/TFTSurenoo.cpp index fc61ca9..c9498b6 100644 --- a/TFTSurenoo.cpp +++ b/TFTSurenoo.cpp @@ -77,6 +77,7 @@ enum LcdColour { #define STR_DSTAR "D-STAR" #define STR_MMDVM "MMDVM" #define STR_NXDN "NXDN" +#define STR_M17 "M17" #define STR_P25 "P25" #define STR_YSF "SystemFusion" @@ -358,6 +359,29 @@ void CTFTSurenoo::clearNXDNInt() clearDStarInt(); } +void CTFTSurenoo::writeM17Int(const char* source, const char* dest, const char* type) +{ + assert(source != NULL); + assert(dest != NULL); + assert(type != NULL); + + if (m_mode != MODE_M17) + setModeLine(STR_M17); + + ::snprintf(m_temp, sizeof(m_temp), "%s %s", type, source); + setStatusLine(statusLineNo(0), m_temp); + + ::snprintf(m_temp, sizeof(m_temp), "%s", dest); + setStatusLine(statusLineNo(1), m_temp); + + m_mode = MODE_M17; +} + +void CTFTSurenoo::clearM17Int() +{ + clearDStarInt(); +} + void CTFTSurenoo::writePOCSAGInt(uint32_t ric, const std::string& message) { setStatusLine(statusLineNo(1), "POCSAG TX"); diff --git a/TFTSurenoo.h b/TFTSurenoo.h index e21a17e..2e9abdb 100644 --- a/TFTSurenoo.h +++ b/TFTSurenoo.h @@ -61,6 +61,9 @@ protected: virtual int writeNXDNIntEx(const class CUserDBentry& source, bool group, unsigned int dest, const char* type); virtual void clearNXDNInt(); + virtual void writeM17Int(const char* source, const char* dest, const char* type); + virtual void clearM17Int(); + virtual void writePOCSAGInt(uint32_t ric, const std::string& message); virtual void clearPOCSAGInt(); From 6e9174cf4c73b1f48ba977d3038e57d941508ef5 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 15 Oct 2020 15:44:59 +0100 Subject: [PATCH 119/163] Improve the M17 networking. --- M17Control.cpp | 158 +++--------------------------------------------- M17Control.h | 3 - M17Network.cpp | 40 +++++++++++- M17Network.h | 4 ++ MMDVM.ini | 4 +- NXDNControl.cpp | 2 +- 6 files changed, 52 insertions(+), 159 deletions(-) diff --git a/M17Control.cpp b/M17Control.cpp index a2495f6..74cd114 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -89,9 +89,9 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) std::string source = m_lookup->find(m_rfLayer3.getSourceUnitId()); if (m_rssi != 0U) - LogMessage("M17, transmission lost from %s to %s%u, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 12.5F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); + LogMessage("M17, transmission lost from %s to %s%u, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); else - LogMessage("M17, transmission lost from %s to %s%u, %.1f seconds, BER: %.1f%%", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 12.5F, float(m_rfErrs * 100U) / float(m_rfBits)); + LogMessage("M17, transmission lost from %s to %s%u, %.1f seconds, BER: %.1f%%", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); writeEndRF(); return false; } @@ -139,31 +139,9 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) if (valid) m_rfLastLICH = lich; - // Stop repeater packets coming through, unless we're acting as a remote gateway - if (m_remoteGateway) { - unsigned char direction = m_rfLastLICH.getDirection(); - if (direction == M17_LICH_DIRECTION_INBOUND) - return false; - } else { - unsigned char direction = m_rfLastLICH.getDirection(); - if (direction == M17_LICH_DIRECTION_OUTBOUND) - return false; - } - unsigned char usc = m_rfLastLICH.getFCT(); unsigned char option = m_rfLastLICH.getOption(); - bool ret; - if (usc == M17_LICH_USC_UDCH) - ret = processData(option, data); - else - ret = processVoice(usc, option, data); - - return ret; -} - -bool CM17Control::processVoice(unsigned char usc, unsigned char option, unsigned char *data) -{ CM17SACCH sacch; bool valid = sacch.decode(data + 2U); if (valid) { @@ -260,9 +238,9 @@ bool CM17Control::processVoice(unsigned char usc, unsigned char option, unsigned m_rfFrames++; if (m_rssi != 0U) - LogMessage("M17, received RF end of transmission from %s to %s%u, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 12.5F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); + LogMessage("M17, received RF end of transmission from %s to %s%u, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); else - LogMessage("M17, received RF end of transmission from %s to %s%u, %.1f seconds, BER: %.1f%%", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 12.5F, float(m_rfErrs * 100U) / float(m_rfBits)); + LogMessage("M17, received RF end of transmission from %s to %s%u, %.1f seconds, BER: %.1f%%", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); writeEndRF(); } else { m_rfFrames = 0U; @@ -552,127 +530,6 @@ bool CM17Control::processVoice(unsigned char usc, unsigned char option, unsigned return true; } -bool CM17Control::processData(unsigned char option, unsigned char *data) -{ - CM17UDCH udch; - bool validUDCH = udch.decode(data + 2U); - if (m_rfState == RS_RF_LISTENING && !validUDCH) - return false; - - if (validUDCH) { - unsigned char ran = udch.getRAN(); - if (ran != m_ran && ran != 0U) - return false; - } - - unsigned char netData[40U]; - ::memset(netData, 0x00U, 40U); - - // The layer3 data will only be correct if valid is true - unsigned char buffer[23U]; - udch.getData(buffer); - - CM17Layer3 layer3; - layer3.decode(buffer, 184U); - - if (m_rfState == RS_RF_LISTENING) { - unsigned char type = layer3.getMessageType(); - if (type != M17_MESSAGE_TYPE_DCALL_HDR) - return false; - - unsigned short srcId = layer3.getSourceUnitId(); - unsigned short dstId = layer3.getDestinationGroupId(); - bool grp = layer3.getIsGroup(); - - if (m_selfOnly) { - if (srcId != m_id) - return false; - } - - unsigned char frames = layer3.getDataBlocks(); - - std::string source = m_lookup->find(srcId); - - m_display->writeM17(source.c_str(), grp, dstId, "R"); - m_display->writeM17RSSI(m_rssi); - - LogMessage("M17, received RF data header from %s to %s%u, %u blocks", source.c_str(), grp ? "TG " : "", dstId, frames); - - m_rfLayer3 = layer3; - m_rfFrames = 0U; - - m_rfState = RS_RF_DATA; - -#if defined(DUMP_M17) - openFile(); -#endif - } - - if (m_rfState != RS_RF_DATA) - return false; - - CSync::addM17Sync(data + 2U); - - CM17LICH lich; - lich.setRFCT(M17_LICH_RFCT_RDCH); - lich.setFCT(M17_LICH_USC_UDCH); - lich.setOption(option); - lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); - lich.encode(data + 2U); - - lich.setDirection(M17_LICH_DIRECTION_INBOUND); - netData[0U] = lich.getRaw(); - - udch.getRaw(netData + 1U); - - unsigned char type = M17_MESSAGE_TYPE_DCALL_DATA; - - if (validUDCH) { - type = layer3.getMessageType(); - data[0U] = type == M17_MESSAGE_TYPE_TX_REL ? TAG_EOT : TAG_DATA; - - udch.setRAN(m_ran); - udch.encode(data + 2U); - } else { - data[0U] = TAG_DATA; - data[1U] = 0x00U; - } - - scrambler(data + 2U); - - switch (type) { - case M17_MESSAGE_TYPE_DCALL_HDR: - writeNetwork(netData, NNMT_DATA_HEADER); - break; - case M17_MESSAGE_TYPE_TX_REL: - writeNetwork(netData, NNMT_DATA_TRAILER); - break; - default: - writeNetwork(netData, NNMT_DATA_BODY); - break; - } - - if (m_duplex) - writeQueueRF(data); - - m_rfFrames++; - -#if defined(DUMP_M17) - writeFile(data + 2U); -#endif - - if (data[0U] == TAG_EOT) { - unsigned short dstId = m_rfLayer3.getDestinationGroupId(); - bool grp = m_rfLayer3.getIsGroup(); - std::string source = m_lookup->find(m_rfLayer3.getSourceUnitId()); - - LogMessage("M17, ended RF data transmission from %s to %s%u", source.c_str(), grp ? "TG " : "", dstId); - writeEndRF(); - } - - return true; -} - unsigned int CM17Control::readModem(unsigned char* data) { assert(data != NULL); @@ -728,7 +585,7 @@ void CM17Control::writeEndNet() void CM17Control::writeNetwork() { - unsigned char netData[40U]; + unsigned char netData[100U]; bool exists = m_network->read(netData); if (!exists) return; @@ -749,7 +606,6 @@ void CM17Control::writeNetwork() lich.setRaw(netData[0U]); unsigned char usc = lich.getFCT(); unsigned char option = lich.getOption(); - lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); lich.encode(data + 2U); if (usc == M17_LICH_USC_UDCH) { @@ -831,7 +687,7 @@ void CM17Control::writeNetwork() if (type == M17_MESSAGE_TYPE_TX_REL) { m_netFrames++; - LogMessage("M17, received network end of transmission from %s to %s%u, %.1f seconds", source.get(keyCALLSIGN).c_str(), grp ? "TG " : "", dstId, float(m_netFrames) / 12.5F); + LogMessage("M17, received network end of transmission from %s to %s%u, %.1f seconds", source.get(keyCALLSIGN).c_str(), grp ? "TG " : "", dstId, float(m_netFrames) / 25.0F); writeEndNet(); } else if (type == M17_MESSAGE_TYPE_VCALL) { LogMessage("M17, received network transmission from %s to %s%u", source.get(keyCALLSIGN).c_str(), grp ? "TG " : "", dstId); @@ -987,7 +843,7 @@ void CM17Control::clock(unsigned int ms) m_networkWatchdog.clock(ms); if (m_networkWatchdog.hasExpired()) { - LogMessage("M17, network watchdog has expired, %.1f seconds", float(m_netFrames) / 12.5F); + LogMessage("M17, network watchdog has expired, %.1f seconds", float(m_netFrames) / 25.0F); writeEndNet(); } } diff --git a/M17Control.h b/M17Control.h index de7bc45..97828a1 100644 --- a/M17Control.h +++ b/M17Control.h @@ -78,11 +78,8 @@ private: bool m_enabled; FILE* m_fp; - bool processVoice(unsigned char usc, unsigned char option, unsigned char* data); - void writeQueueRF(const unsigned char* data); void writeQueueNet(const unsigned char* data); - void writeNetwork(const unsigned char* data); void writeNetwork(); void scrambler(unsigned char* data) const; diff --git a/M17Network.cpp b/M17Network.cpp index 3a21373..20ee736 100644 --- a/M17Network.cpp +++ b/M17Network.cpp @@ -34,8 +34,15 @@ m_addr(), m_addrLen(0U), m_debug(debug), m_enabled(false), -m_buffer(1000U, "M17 Network") +m_outId(0U), +m_inId(0U), +m_buffer(1000U, "M17 Network"), +m_random() { + std::random_device rd; + std::mt19937 mt(rd()); + m_random = mt; + if (CUDPSocket::lookup(gatewayAddress, gatewayPort, m_addr, m_addrLen) != 0) m_addrLen = 0U; } @@ -65,6 +72,16 @@ bool CM17Network::write(const unsigned char* data) buffer[0U] = 'M'; buffer[1U] = '1'; buffer[2U] = '7'; + buffer[3U] = ' '; + + // Create a random id for this transmission if needed + if (m_outId == 0U) { + std::uniform_int_distribution dist(0x0001, 0xFFFE); + m_outId = dist(m_random); + } + + buffer[4U] = m_outId / 256U; // Unique session id + buffer[5U] = m_outId % 256U; if (m_debug) CUtils::dump(1U, "M17 data transmitted", buffer, 36U); @@ -93,9 +110,22 @@ void CM17Network::clock(unsigned int ms) if (m_debug) CUtils::dump(1U, "M17 Network Data Received", buffer, length); - if (::memcmp(buffer + 0U, "M17", 3U) != 0) + if (::memcmp(buffer + 0U, "M17 ", 4U) != 0) return; + uint16_t id = (buffer[4U] << 8) + (buffer[5U] << 0); + if (m_inId == 0U) { + m_inId = id; + } else { + if (id != m_inId) + return; + } + + // EOT? + uint16_t fn = (buffer[38U] << 8) + (buffer[39U] << 0); + if ((fn & 0x8000U) == 0x8000U) + m_inId = 0U; + unsigned char c = length; m_buffer.addData(&c, 1U); @@ -124,6 +154,12 @@ void CM17Network::close() LogMessage("Closing M17 network connection"); } +void CM17Network::reset() +{ + m_outId = 0U; + m_inId = 0U; +} + void CM17Network::enable(bool enabled) { if (!enabled && m_enabled) diff --git a/M17Network.h b/M17Network.h index d0ef7b5..697cb1a 100644 --- a/M17Network.h +++ b/M17Network.h @@ -23,6 +23,7 @@ #include "RingBuffer.h" #include "UDPSocket.h" +#include #include class CM17Network { @@ -50,7 +51,10 @@ private: unsigned int m_addrLen; bool m_debug; bool m_enabled; + uint16_t m_outId; + uint16_t m_inId; CRingBuffer m_buffer; + std::mt19937 m_random; }; #endif diff --git a/MMDVM.ini b/MMDVM.ini index 6f51621..83eb5fb 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -225,8 +225,8 @@ Debug=0 [M17 Network] Enable=1 GatewayAddress=127.0.0.1 -GatewayPort=11657 -LocalPort=11657 +GatewayPort=17000 +LocalPort=17000 # ModeHang=3 Debug=0 diff --git a/NXDNControl.cpp b/NXDNControl.cpp index 1e200c4..119fad6 100644 --- a/NXDNControl.cpp +++ b/NXDNControl.cpp @@ -737,7 +737,7 @@ void CNXDNControl::writeEndNet() void CNXDNControl::writeNetwork() { - unsigned char netData[40U]; + unsigned char netData[100U]; bool exists = m_network->read(netData); if (!exists) return; From b921f99d0a76712bb8d81953da85ada782095dbe Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Fri, 16 Oct 2020 10:35:19 +0100 Subject: [PATCH 120/163] WIP on M17. --- LCDproc.cpp | 67 +++++++ LCDproc.h | 4 + M17Control.cpp | 380 +++++++++++++------------------------- M17Control.h | 13 +- M17LICH.h | 45 +++++ MMDVMHost.vcxproj | 1 + MMDVMHost.vcxproj.filters | 3 + 7 files changed, 259 insertions(+), 254 deletions(-) create mode 100644 M17LICH.h diff --git a/LCDproc.cpp b/LCDproc.cpp index 19ac15e..0ef66eb 100644 --- a/LCDproc.cpp +++ b/LCDproc.cpp @@ -95,6 +95,7 @@ const unsigned int DMR_RSSI_COUNT = 4U; // 4 * 360ms = 1440ms const unsigned int YSF_RSSI_COUNT = 13U; // 13 * 100ms = 1300ms const unsigned int P25_RSSI_COUNT = 7U; // 7 * 180ms = 1260ms const unsigned int NXDN_RSSI_COUNT = 28U; // 28 * 40ms = 1120ms +const unsigned int M17_RSSI_COUNT = 28U; // 28 * 40ms = 1120ms CLCDproc::CLCDproc(std::string address, unsigned int port, unsigned int localPort, const std::string& callsign, unsigned int dmrid, bool displayClock, bool utc, bool duplex, bool dimOnIdle) : CDisplay(), @@ -188,6 +189,7 @@ void CLCDproc::setIdleInt() socketPrintf(m_socketfd, "screen_set YSF -priority hidden"); socketPrintf(m_socketfd, "screen_set P25 -priority hidden"); socketPrintf(m_socketfd, "screen_set NXDN -priority hidden"); + socketPrintf(m_socketfd, "screen_set M17 -priority hidden"); socketPrintf(m_socketfd, "widget_set Status Status %u %u Idle", m_cols - 3, m_rows); socketPrintf(m_socketfd, "output 0"); // Clear all LEDs } @@ -207,6 +209,7 @@ void CLCDproc::setErrorInt(const char* text) socketPrintf(m_socketfd, "screen_set YSF -priority hidden"); socketPrintf(m_socketfd, "screen_set P25 -priority hidden"); socketPrintf(m_socketfd, "screen_set NXDN -priority hidden"); + socketPrintf(m_socketfd, "screen_set M17 -priority hidden"); socketPrintf(m_socketfd, "widget_set Status Status %u %u Error", m_cols - 4, m_rows); socketPrintf(m_socketfd, "output 0"); // Clear all LEDs } @@ -224,6 +227,7 @@ void CLCDproc::setLockoutInt() socketPrintf(m_socketfd, "screen_set YSF -priority hidden"); socketPrintf(m_socketfd, "screen_set P25 -priority hidden"); socketPrintf(m_socketfd, "screen_set NXDN -priority hidden"); + socketPrintf(m_socketfd, "screen_set M17 -priority hidden"); socketPrintf(m_socketfd, "widget_set Status Status %u %u Lockout", m_cols - 6, m_rows); socketPrintf(m_socketfd, "output 0"); // Clear all LEDs } @@ -243,6 +247,7 @@ void CLCDproc::setQuitInt() socketPrintf(m_socketfd, "screen_set YSF -priority hidden"); socketPrintf(m_socketfd, "screen_set P25 -priority hidden"); socketPrintf(m_socketfd, "screen_set NXDN -priority hidden"); + socketPrintf(m_socketfd, "screen_set M17 -priority hidden"); socketPrintf(m_socketfd, "widget_set Status Status %u %u Stopped", m_cols - 6, m_rows); socketPrintf(m_socketfd, "output 0"); // Clear all LEDs } @@ -260,6 +265,7 @@ void CLCDproc::setFMInt() socketPrintf(m_socketfd, "screen_set YSF -priority hidden"); socketPrintf(m_socketfd, "screen_set P25 -priority hidden"); socketPrintf(m_socketfd, "screen_set NXDN -priority hidden"); + socketPrintf(m_socketfd, "screen_set M17 -priority hidden"); socketPrintf(m_socketfd, "widget_set Status Status %u %u FM", m_cols - 6, m_rows); socketPrintf(m_socketfd, "output 0"); // Clear all LEDs } @@ -556,6 +562,51 @@ void CLCDproc::clearNXDNInt() socketPrintf(m_socketfd, "output 16"); // Set LED5 color green } +void CLCDproc::writeM17Int(const char* source, const char* dest, const char* type) +{ + assert(source != NULL); + assert(dest != NULL); + assert(type != NULL); + + m_clockDisplayTimer.stop(); // Stop the clock display + + socketPrintf(m_socketfd, "screen_set M17 -priority foreground"); + socketPrintf(m_socketfd, "widget_set M17 Mode 1 1 M17"); + + if (m_rows == 2U) { + socketPrintf(m_socketfd, "widget_set M17 Line2 1 2 15 2 h 3 \"%.9s > %.9s\"", source, dest); + } + else { + socketPrintf(m_socketfd, "widget_set M17 Line2 1 2 15 2 h 3 \"%.9s >\"", source); + socketPrintf(m_socketfd, "widget_set M17 Line3 1 3 15 3 h 3 \"%.9ss\"", dest); + socketPrintf(m_socketfd, "output 255"); // Set LED5 color red + } + + m_dmr = false; + m_rssiCount1 = 0U; +} + +void CLCDproc::writeM17RSSIInt(unsigned char rssi) +{ + if (m_rssiCount1 == 0U) { + socketPrintf(m_socketfd, "widget_set M17 Line4 1 4 %u 4 h 3 \"-%3udBm\"", m_cols - 1, rssi); + } + + m_rssiCount1++; + if (m_rssiCount1 >= M17_RSSI_COUNT) + m_rssiCount1 = 0U; +} + +void CLCDproc::clearM17Int() +{ + m_clockDisplayTimer.stop(); // Stop the clock display + + socketPrintf(m_socketfd, "widget_set M17 Line2 1 2 15 2 h 3 \"Listening\""); + socketPrintf(m_socketfd, "widget_set M17 Line3 1 3 15 3 h 3 \"\""); + socketPrintf(m_socketfd, "widget_set M17 Line4 1 4 15 4 h 3 \"\""); + socketPrintf(m_socketfd, "output 16"); // Set LED5 color green +} + void CLCDproc::writePOCSAGInt(uint32_t ric, const std::string& message) { } @@ -850,5 +901,21 @@ void CLCDproc::defineScreens() socketPrintf(m_socketfd, "widget_set NXDN Line4 4 2 15 2 h 3 \" \""); */ +// The M17 Screen + + socketPrintf(m_socketfd, "screen_add M17"); + socketPrintf(m_socketfd, "screen_set M17 -name M17 -heartbeat on -priority hidden -backlight on"); + + socketPrintf(m_socketfd, "widget_add M17 Mode string"); + socketPrintf(m_socketfd, "widget_add M17 Line2 scroller"); + socketPrintf(m_socketfd, "widget_add M17 Line3 scroller"); + socketPrintf(m_socketfd, "widget_add M17 Line4 scroller"); + + /* Do we need to pre-populate the values?? + socketPrintf(m_socketfd, "widget_set M17 Line3 2 1 15 1 h 3 \"Listening\""); + socketPrintf(m_socketfd, "widget_set M17 Line3 3 1 15 1 h 3 \" \""); + socketPrintf(m_socketfd, "widget_set M17 Line4 4 2 15 2 h 3 \" \""); + */ + m_screensDefined = true; } diff --git a/LCDproc.h b/LCDproc.h index 896e8a6..553968a 100644 --- a/LCDproc.h +++ b/LCDproc.h @@ -62,6 +62,10 @@ protected: virtual void writeNXDNRSSIInt(unsigned char rssi); virtual void clearNXDNInt(); + virtual void writeM17Int(const char* source, const char* dest, const char* type); + virtual void writeM17RSSIInt(unsigned char rssi); + virtual void clearM17Int(); + virtual void writePOCSAGInt(uint32_t ric, const std::string& message); virtual void clearPOCSAGInt(); diff --git a/M17Control.cpp b/M17Control.cpp index 74cd114..fbf9b1e 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -21,18 +21,37 @@ #include #include +const unsigned int INTERLEAVER[] = { + 0U, 137U, 90U, 227U, 180U, 317U, 270U, 39U, 360U, 129U, 82U, 219U, 172U, 309U, 262U, 31U, 352U, 121U, 74U, 211U, 164U, + 301U, 254U, 23U, 344U, 113U, 66U, 203U, 156U, 293U, 246U, 15U, 336U, 105U, 58U, 195U, 148U, 285U, 238U, 7U, 328U, 97U, + 50U, 187U, 140U, 277U, 230U, 367U, 320U, 89U, 42U, 179U, 132U, 269U, 222U, 359U, 312U, 81U, 34U, 171U, 124U, 261U, 214U, + 351U, 304U, 73U, 26U, 163U, 116U, 253U, 206U, 343U, 296U, 65U, 18U, 155U, 108U, 245U, 198U, 335U, 288U, 57U, 10U, 147U, + 100U, 237U, 190U, 327U, 280U, 49U, 2U, 139U, 92U, 229U, 182U, 319U, 272U, 41U, 362U, 131U, 84U, 221U, 174U, 311U, 264U, + 33U, 354U, 123U, 76U, 213U, 166U, 303U, 256U, 25U, 346U, 115U, 68U, 205U, 158U, 295U, 248U, 17U, 338U, 107U, 60U, 197U, + 150U, 287U, 240U, 9U, 330U, 99U, 52U, 189U, 142U, 279U, 232U, 1U, 322U, 91U, 44U, 181U, 134U, 271U, 224U, 361U, 314U, 83U, + 36U, 173U, 126U, 263U, 216U, 353U, 306U, 75U, 28U, 165U, 118U, 255U, 208U, 345U, 298U, 67U, 20U, 157U, 110U, 247U, 200U, + 337U, 290U, 59U, 12U, 149U, 102U, 239U, 192U, 329U, 282U, 51U, 4U, 141U, 94U, 231U, 184U, 321U, 274U, 43U, 364U, 133U, 86U, + 223U, 176U, 313U, 266U, 35U, 356U, 125U, 78U, 215U, 168U, 305U, 258U, 27U, 348U, 117U, 70U, 207U, 160U, 297U, 250U, 19U, + 340U, 109U, 62U, 199U, 152U, 289U, 242U, 11U, 332U, 101U, 54U, 191U, 144U, 281U, 234U, 3U, 324U, 93U, 46U, 183U, 136U, 273U, + 226U, 363U, 316U, 85U, 38U, 175U, 128U, 265U, 218U, 355U, 308U, 77U, 30U, 167U, 120U, 257U, 210U, 347U, 300U, 69U, 22U, + 159U, 112U, 249U, 202U, 339U, 292U, 61U, 14U, 151U, 104U, 241U, 194U, 331U, 284U, 53U, 6U, 143U, 96U, 233U, 186U, 323U, + 276U, 45U, 366U, 135U, 88U, 225U, 178U, 315U, 268U, 37U, 358U, 127U, 80U, 217U, 170U, 307U, 260U, 29U, 350U, 119U, 72U, + 209U, 162U, 299U, 252U, 21U, 342U, 111U, 64U, 201U, 154U, 291U, 244U, 13U, 334U, 103U, 56U, 193U, 146U, 283U, 236U, 5U, + 326U, 95U, 48U, 185U, 138U, 275U, 228U, 365U, 318U, 87U, 40U, 177U, 130U, 267U, 220U, 357U, 310U, 79U, 32U, 169U, 122U, + 259U, 212U, 349U, 302U, 71U, 24U, 161U, 114U, 251U, 204U, 341U, 294U, 63U, 16U, 153U, 106U, 243U, 196U, 333U, 286U, 55U, + 8U, 145U, 98U, 235U, 188U, 325U, 278U, 47U}; + const unsigned char SCRAMBLER[] = { - 0x00U, 0x00U, 0x00U, 0x82U, 0xA0U, 0x88U, 0x8AU, 0x00U, 0xA2U, 0xA8U, 0x82U, 0x8AU, 0x82U, 0x02U, - 0x20U, 0x08U, 0x8AU, 0x20U, 0xAAU, 0xA2U, 0x82U, 0x08U, 0x22U, 0x8AU, 0xAAU, 0x08U, 0x28U, 0x88U, - 0x28U, 0x28U, 0x00U, 0x0AU, 0x02U, 0x82U, 0x20U, 0x28U, 0x82U, 0x2AU, 0xAAU, 0x20U, 0x22U, 0x80U, - 0xA8U, 0x8AU, 0x08U, 0xA0U, 0xAAU, 0x02U }; + 0xD6U, 0xB5U, 0xE2U, 0x30U, 0x82U, 0xFFU, 0x84U, 0x62U, 0xBAU, 0x4EU, 0x96U, 0x90U, 0xD8U, 0x98U, 0xDDU, 0x5DU, 0x0CU, + 0xC8U, 0x52U, 0x43U, 0x91U, 0x1DU, 0xF8U, 0x6EU, 0x68U, 0x2FU, 0x35U, 0xDAU, 0x14U, 0xEAU, 0xCDU, 0x76U, 0x19U, 0x8DU, + 0xD5U, 0x80U, 0xD1U, 0x33U, 0x87U, 0x13U, 0x57U, 0x18U, 0x2DU, 0x29U, 0x78U, 0xC3U}; // #define DUMP_M17 const unsigned char BIT_MASK_TABLE[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U }; -#define WRITE_BIT1(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) -#define READ_BIT1(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) +#define WRITE_BIT(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) +#define READ_BIT(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) CM17Control::CM17Control(const std::string& callsign, bool selfOnly, CM17Network* network, CDisplay* display, unsigned int timeout, bool duplex, CRSSIInterpolator* rssiMapper) : m_callsign(callsign), @@ -52,11 +71,9 @@ m_rfFrames(0U), m_netFrames(0U), m_rfErrs(0U), m_rfBits(1U), -m_rfLastLICH(), -m_rfLayer3(), -m_netLayer3(), +m_rfLICH(), m_rfMask(0x00U), -m_netMask(0x00U), +m_netLICH(), m_rssiMapper(rssiMapper), m_rssi(0U), m_maxRSSI(0U), @@ -84,14 +101,13 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) unsigned char type = data[0U]; if (type == TAG_LOST && m_rfState == RS_RF_AUDIO) { - unsigned short dstId = m_rfLayer3.getDestinationGroupId(); - bool grp = m_rfLayer3.getIsGroup(); - std::string source = m_lookup->find(m_rfLayer3.getSourceUnitId()); + std::string source = m_rfLICH.getSource(); + std::string dest = m_rfLICH.getDest(); if (m_rssi != 0U) - LogMessage("M17, transmission lost from %s to %s%u, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); + LogMessage("M17, transmission lost from %s to %s, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); else - LogMessage("M17, transmission lost from %s to %s%u, %.1f seconds, BER: %.1f%%", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); + LogMessage("M17, transmission lost from %s to %s, %.1f seconds, BER: %.1f%%", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); writeEndRF(); return false; } @@ -131,7 +147,9 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) m_rssiCount++; } - scrambler(data + 2U); + unsigned char temp[M17_FRAME_LENGTH_BYTES]; + decorrelator(data + 2U, temp); + interleaver(temp, data + 2U); CM17LICH lich; bool valid = lich.decode(data + 2U); @@ -405,9 +423,10 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) facch.getRaw(netData + 5U + 0U); facch.getRaw(netData + 5U + 14U); - scrambler(start + 2U); + interleaver(start + 2U); + decorrelator(start + 2U); - writeNetwork(netData, NNMT_VOICE_HEADER); + writeNetwork(netData); #if defined(DUMP_M17) writeFile(start + 2U); @@ -511,9 +530,11 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) data[0U] = TAG_DATA; data[1U] = 0x00U; - scrambler(data + 2U); + unsigned char temp[M17_FRAME_LENGTH_BYTES]; + interleaver(data + 2U, temp); + decorrelator(temp, data + 2U); - writeNetwork(netData, NNMT_VOICE_BODY); + writeNetwork(netData); #if defined(DUMP_M17) writeFile(data + 2U); @@ -570,7 +591,6 @@ void CM17Control::writeEndNet() { m_netState = RS_NET_IDLE; - m_netMask = 0x00U; m_netLayer3.reset(); m_netTimeoutTimer.stop(); @@ -598,236 +618,87 @@ void CM17Control::writeNetwork() m_networkWatchdog.start(); + if (m_netState == RS_NET_IDLE) { + m_netLICH.setNetworkData(netData); + + std::string source = m_netLICH.getSource(); + std::string dest = m_netLICH.getDest(); + + unsigned char dataType = m_netLICH.getDataType(); + switch (dataType) { + case 1U: + LogMessage("M17, received network data transmission from %s to %s", source.c_str(), dest.c_str()); + m_netState = RS_NET_DATA; + break; + case 2U: + LogMessage("M17, received network voice transmission from %s to %s", source.c_str(), dest.c_str()); + m_netState = RS_NET_AUDIO; + break; + case 3U: + LogMessage("M17, received network voice + data transmission from %s to %s", source.c_str(), dest.c_str()); + m_netState = RS_NET_AUDIO; + break; + default: + LogMessage("M17, received network unknown transmission from %s to %s", source.c_str(), dest.c_str()); + m_netState = RS_NET_DATA; + break; + } + + m_display->writeM17(source.c_str(), dest.c_str(), "N"); + + m_netTimeoutTimer.start(); + m_packetTimer.start(); + m_elapsed.start(); + m_netFrames = 1U; + + // Create a dummy start message + unsigned char start[M17_FRAME_LENGTH_BYTES + 2U]; + + start[0U] = TAG_DATA; + start[1U] = 0x00U; + + // Generate the sync + CSync::addM17Sync(start + 2U); + + m_netLICH.getLinkSetup(start + 2U); + + unsigned char temp[M17_FRAME_LENGTH_BYTES]; + interleaver(start + 2U, temp); + decorrelator(temp, start + 2U); + + writeQueueNet(start); + } + unsigned char data[M17_FRAME_LENGTH_BYTES + 2U]; + data[0U] = TAG_DATA; + data[1U] = 0x00U; + + // Generate the sync CSync::addM17Sync(data + 2U); - CM17LICH lich; - lich.setRaw(netData[0U]); - unsigned char usc = lich.getFCT(); - unsigned char option = lich.getOption(); - lich.encode(data + 2U); + m_netFrames++; - if (usc == M17_LICH_USC_UDCH) { - CM17Layer3 layer3; - layer3.setData(netData + 2U, 23U); - unsigned char type = layer3.getMessageType(); + // Add the fragment LICH + uint16_t fn = (netData[38U] << 8) + (netData[39U] << 0); + m_netLICH.getFragment(data + 2U, fn & 0x7FFFU); - if (m_netState == RS_NET_IDLE) { - if (type == M17_MESSAGE_TYPE_DCALL_HDR) { - unsigned short srcId = layer3.getSourceUnitId(); - unsigned short dstId = layer3.getDestinationGroupId(); - bool grp = layer3.getIsGroup(); + // Add the data/audio - unsigned char frames = layer3.getDataBlocks(); + // Add the FEC - std::string source = m_lookup->find(srcId); - m_display->writeM17(source.c_str(), grp, dstId, "N"); - LogMessage("M17, received network data header from %s to %s%u, %u blocks", source.c_str(), grp ? "TG " : "", dstId, frames); + unsigned char temp[M17_FRAME_LENGTH_BYTES]; + interleaver(data + 2U, temp); + decorrelator(temp, data + 2U); - m_netState = RS_NET_DATA; - } else { - return; - } - } + writeQueueNet(data); - if (m_netState == RS_NET_DATA) { - data[0U] = type == M17_MESSAGE_TYPE_TX_REL ? TAG_EOT : TAG_DATA; - data[1U] = 0x00U; - - CM17UDCH udch; - udch.setRAN(m_ran); - udch.setData(netData + 2U); - udch.encode(data + 2U); - - scrambler(data + 2U); - - writeQueueNet(data); - - if (type == M17_MESSAGE_TYPE_TX_REL) { - unsigned short dstId = m_netLayer3.getDestinationGroupId(); - bool grp = m_netLayer3.getIsGroup(); - std::string source = m_lookup->find(m_netLayer3.getSourceUnitId()); - - LogMessage("M17, ended network data transmission from %s to %s%u", source.c_str(), grp ? "TG " : "", dstId); - writeEndNet(); - } - } - } else if (usc == M17_LICH_USC_SACCH_NS) { - m_netLayer3.setData(netData + 5U + 0U, 10U); - - unsigned char type = m_netLayer3.getMessageType(); - if (type == M17_MESSAGE_TYPE_TX_REL && m_netState == RS_NET_IDLE) - return; - if (type == M17_MESSAGE_TYPE_VCALL && m_netState != RS_NET_IDLE) - return; - - CM17SACCH sacch; - sacch.setRAN(m_ran); - sacch.setStructure(M17_SR_SINGLE); - sacch.setData(SACCH_IDLE); - sacch.encode(data + 2U); - - CM17FACCH1 facch; - facch.setRaw(netData + 5U + 0U); - facch.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - facch.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - - data[0U] = type == M17_MESSAGE_TYPE_TX_REL ? TAG_EOT : TAG_DATA; - data[1U] = 0x00U; - - scrambler(data + 2U); - - writeQueueNet(data); - - unsigned short dstId = m_netLayer3.getDestinationGroupId(); - bool grp = m_netLayer3.getIsGroup(); - class CUserDBentry source; - m_lookup->findWithName(m_netLayer3.getSourceUnitId(), &source); - - if (type == M17_MESSAGE_TYPE_TX_REL) { - m_netFrames++; - LogMessage("M17, received network end of transmission from %s to %s%u, %.1f seconds", source.get(keyCALLSIGN).c_str(), grp ? "TG " : "", dstId, float(m_netFrames) / 25.0F); - writeEndNet(); - } else if (type == M17_MESSAGE_TYPE_VCALL) { - LogMessage("M17, received network transmission from %s to %s%u", source.get(keyCALLSIGN).c_str(), grp ? "TG " : "", dstId); - m_display->writeM17(source, grp, dstId, "N"); - - m_netTimeoutTimer.start(); - m_packetTimer.start(); - m_elapsed.start(); - m_netState = RS_NET_AUDIO; - m_netFrames = 1U; - } else { - CUtils::dump(2U, "M17, interesting non superblock network frame", netData, 33U); - return; - } - } else { - if (m_netState == RS_NET_IDLE) { - unsigned char structure = (netData[1U] >> 6) & 0x03U; - switch (structure) { - case M17_SR_1_4: - m_netLayer3.decode(netData + 2U, 18U, 0U); - if (m_netLayer3.getMessageType() == M17_MESSAGE_TYPE_VCALL) - m_netMask = 0x01U; - else - m_netMask = 0x00U; - break; - case M17_SR_2_4: - m_netMask |= 0x02U; - m_netLayer3.decode(netData + 2U, 18U, 18U); - break; - case M17_SR_3_4: - m_netMask |= 0x04U; - m_netLayer3.decode(netData + 2U, 18U, 36U); - break; - case M17_SR_4_4: - m_netMask |= 0x08U; - m_netLayer3.decode(netData + 2U, 18U, 54U); - break; - default: - break; - } - - if (m_netMask != 0x0FU) - return; - - unsigned char type = m_netLayer3.getMessageType(); - if (type != M17_MESSAGE_TYPE_VCALL) - return; - - unsigned short srcId = m_netLayer3.getSourceUnitId(); - unsigned short dstId = m_netLayer3.getDestinationGroupId(); - bool grp = m_netLayer3.getIsGroup(); - - class CUserDBentry source; - m_lookup->findWithName(srcId, &source); - LogMessage("M17, received network transmission from %s to %s%u", source.get(keyCALLSIGN).c_str(), grp ? "TG " : "", dstId); - m_display->writeM17(source, grp, dstId, "N"); - - m_netTimeoutTimer.start(); - m_packetTimer.start(); - m_elapsed.start(); - m_netState = RS_NET_AUDIO; - m_netFrames = 1U; - - // Create a dummy start message - unsigned char start[M17_FRAME_LENGTH_BYTES + 2U]; - - start[0U] = TAG_DATA; - start[1U] = 0x00U; - - // Generate the sync - CSync::addM17Sync(start + 2U); - - // Generate the LICH - CM17LICH lich; - lich.setRFCT(M17_LICH_RFCT_RDCH); - lich.setFCT(M17_LICH_USC_SACCH_NS); - lich.setOption(M17_LICH_STEAL_FACCH); - lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); - lich.encode(start + 2U); - - CM17SACCH sacch; - sacch.setRAN(m_ran); - sacch.setStructure(M17_SR_SINGLE); - sacch.setData(SACCH_IDLE); - sacch.encode(start + 2U); - - unsigned char message[22U]; - m_netLayer3.getData(message); - - CM17FACCH1 facch; - facch.setData(message); - facch.encode(start + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - facch.encode(start + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - - scrambler(start + 2U); - - writeQueueNet(start); - } - - m_netFrames++; - - data[0U] = TAG_DATA; - data[1U] = 0x00U; - - CM17SACCH sacch; - sacch.setRaw(netData + 1U); - sacch.setRAN(m_ran); - sacch.encode(data + 2U); - - if (option == M17_LICH_STEAL_NONE) { - CM17Audio audio; - audio.encode(netData + 5U + 0U, data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U); - audio.encode(netData + 5U + 14U, data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U); - } else if (option == M17_LICH_STEAL_FACCH1_1) { - CM17FACCH1 facch1; - facch1.setRaw(netData + 5U + 0U); - facch1.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - - CM17Audio audio; - audio.encode(netData + 5U + 14U, data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U); - } else if (option == M17_LICH_STEAL_FACCH1_2) { - CM17Audio audio; - audio.encode(netData + 5U + 0U, data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U); - - CM17FACCH1 facch1; - facch1.setRaw(netData + 5U + 14U); - facch1.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - } else { - CM17FACCH1 facch11; - facch11.setRaw(netData + 5U + 0U); - facch11.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - - CM17FACCH1 facch12; - facch12.setRaw(netData + 5U + 14U); - facch12.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - } - - scrambler(data + 2U); - - writeQueueNet(data); + // EOT handling + if ((fn & 0x8000U) == 0x8000U) { + std::string source = m_netLICH.getSource(); + std::string dest = m_netLICH.getDest(); + LogMessage("M17, received network end of transmission from %s to %s, %.1f seconds", source.c_str(), dest.c_str(), float(m_netFrames) / 25.0F); + writeEndNet(); } } @@ -892,7 +763,7 @@ void CM17Control::writeQueueNet(const unsigned char *data) m_queue.addData(data, len); } -void CM17Control::writeNetwork(const unsigned char *data, M17_NETWORK_MESSAGE_TYPE type) +void CM17Control::writeNetwork(const unsigned char *data) { assert(data != NULL); @@ -902,15 +773,29 @@ void CM17Control::writeNetwork(const unsigned char *data, M17_NETWORK_MESSAGE_TY if (m_rfTimeoutTimer.isRunning() && m_rfTimeoutTimer.hasExpired()) return; - m_network->write(data, type); + m_network->write(data); } -void CM17Control::scrambler(unsigned char* data) const +void CM17Control::interleaver(const unsigned char* in, unsigned char* out) const { - assert(data != NULL); + assert(in != NULL); + assert(out != NULL); - for (unsigned int i = 0U; i < M17_FRAME_LENGTH_BYTES; i++) - data[i] ^= SCRAMBLER[i]; + for (unsigned int i = 0U; i < (M17_FRAME_LENGTH_BITS - M17_SYNC_LENGTH_BITS); i++) { + unsigned int n1 = i + M17_SYNC_LENGTH_BITS; + bool b = READ_BIT(in, n1) != 0U; + unsigned int n2 = INTERLEAVER[i] + M17_SYNC_LENGTH_BITS; + WRITE_BIT(out, n2, b); + } +} + +void CM17Control::decorrelator(const unsigned char* in, unsigned char* out) const +{ + assert(in != NULL); + assert(out != NULL); + + for (unsigned int i = M17_SYNC_BYTES_LENGTH; i < M17_FRAME_LENGTH_BYTES; i++) + out[i] = in[i] ^ SCRAMBLER[i]; } bool CM17Control::openFile() @@ -974,7 +859,6 @@ void CM17Control::enable(bool enabled) // Reset the networking section m_netState = RS_NET_IDLE; - m_netMask = 0x00U; m_netLayer3.reset(); m_netTimeoutTimer.stop(); diff --git a/M17Control.h b/M17Control.h index 97828a1..371b729 100644 --- a/M17Control.h +++ b/M17Control.h @@ -24,6 +24,7 @@ #include "M17Defines.h" #include "RingBuffer.h" #include "StopWatch.h" +#include "M17LICH.h" #include "Display.h" #include "Defines.h" #include "Timer.h" @@ -64,12 +65,10 @@ private: unsigned int m_netFrames; unsigned int m_rfErrs; unsigned int m_rfBits; - CNXDNLICH m_rfLastLICH; - CNXDNLayer3 m_rfLayer3; - CNXDNLayer3 m_netLayer3; + CM17LICH m_rfLICH; unsigned char m_rfMask; - unsigned char m_netMask; - CRSSIInterpolator* m_rssiMapper; + CM17LICH m_netLICH; + CRSSIInterpolator* m_rssiMapper; unsigned char m_rssi; unsigned char m_maxRSSI; unsigned char m_minRSSI; @@ -80,9 +79,11 @@ private: void writeQueueRF(const unsigned char* data); void writeQueueNet(const unsigned char* data); + void writeNetwork(const unsigned char* data); void writeNetwork(); - void scrambler(unsigned char* data) const; + void interleaver(const unsigned char* in, unsigned char* out) const; + void decorrelator(const unsigned char* in, unsigned char* out) const; void writeEndRF(); void writeEndNet(); diff --git a/M17LICH.h b/M17LICH.h new file mode 100644 index 0000000..97215cd --- /dev/null +++ b/M17LICH.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(M17LICH_H) +#define M17LICH_H + +#include + +class CM17LICH { +public: + CM17LICH(const CM17LICH& lich); + CM17LICH(); + ~CM17LICH(); + + void setNetworkData(const unsigned char* data); + + std::string getSource() const; + std::string getDest() const; + unsigned char getDataType() const; + + void getLinkSetup(unsigned char* data) const; + void getFragment(unsigned char* data, unsigned short fn); + + CM17LICH& operator=(const CM17LICH& lich); + +private: + unsigned char* m_lich; +}; + +#endif diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index 317803a..87d40df 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -190,6 +190,7 @@ + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index eec7ccb..f013ec1 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -308,6 +308,9 @@ Header Files + + Header Files + From 8946038e0e619f7b3bdf7aa146d8667a77873199 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 17 Oct 2020 16:25:48 +0100 Subject: [PATCH 121/163] Add M17 reflector linking and start on the frame handling. --- M17CRC.h | 34 ++++ M17Control.cpp | 371 ++++++++++++++++++++++++++------------ M17Convolution.h | 44 +++++ M17Defines.h | 22 ++- M17LICH.h | 6 + M17Network.cpp | 177 ++++++++++++++++-- M17Network.h | 23 ++- M17Utils.h | 36 ++++ MMDVMHost.vcxproj | 3 + MMDVMHost.vcxproj.filters | 9 + 10 files changed, 594 insertions(+), 131 deletions(-) create mode 100644 M17CRC.h create mode 100644 M17Convolution.h create mode 100644 M17Utils.h diff --git a/M17CRC.h b/M17CRC.h new file mode 100644 index 0000000..af0f3d0 --- /dev/null +++ b/M17CRC.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(M17CRC_H) +#define M17CRC_H + +#include + +class CM17CRC +{ +public: + static bool checkCRC(const unsigned char* in, unsigned int length); + static void encodeCRC(unsigned char* in, unsigned int length); + +private: + static uint16_t createCRC(const unsigned char* in, unsigned int length); +}; + +#endif diff --git a/M17Control.cpp b/M17Control.cpp index fbf9b1e..24cc8c3 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -12,6 +12,9 @@ */ #include "M17Control.h" +#include "M17Convolution.h" +#include "M17CRC.h" +#include "Golay24128.h" #include "Utils.h" #include "Sync.h" #include "Log.h" @@ -91,7 +94,7 @@ CM17Control::~CM17Control() { } -bool CM17Control::writeModem(unsigned char *data, unsigned int len) +bool CM17Control::writeModem(unsigned char* data, unsigned int len) { assert(data != NULL); @@ -102,7 +105,7 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) if (type == TAG_LOST && m_rfState == RS_RF_AUDIO) { std::string source = m_rfLICH.getSource(); - std::string dest = m_rfLICH.getDest(); + std::string dest = m_rfLICH.getDest(); if (m_rssi != 0U) LogMessage("M17, transmission lost from %s to %s, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); @@ -119,8 +122,7 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) if (type == TAG_LOST) { m_rfState = RS_RF_LISTENING; - m_rfMask = 0x00U; - m_rfLayer3.reset(); + m_rfMask = 0x00U; return false; } @@ -151,98 +153,193 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) decorrelator(data + 2U, temp); interleaver(temp, data + 2U); - CM17LICH lich; - bool valid = lich.decode(data + 2U); + if (m_rfState == RS_RF_LISTENING) { + m_rfMask = 0x00U; - if (valid) - m_rfLastLICH = lich; + CM17Convolution conv; + conv.start(); - unsigned char usc = m_rfLastLICH.getFCT(); - unsigned char option = m_rfLastLICH.getOption(); + unsigned int n = 2U + M17_SYNC_LENGTH_BYTES; + for (unsigned int i = 0U; i < (M17_LICH_LENGTH_BYTES / 2U); i++) { + uint8_t s0 = data[n++]; + uint8_t s1 = data[n++]; - CM17SACCH sacch; - bool valid = sacch.decode(data + 2U); - if (valid) { - unsigned char ran = sacch.getRAN(); - if (ran != m_ran && ran != 0U) - return false; - } else if (m_rfState == RS_RF_LISTENING) { - return false; - } - - unsigned char netData[40U]; - ::memset(netData, 0x00U, 40U); - - if (usc == M17_LICH_USC_SACCH_NS) { - // The SACCH on a non-superblock frame is usually an idle and not interesting apart from the RAN. - CM17FACCH1 facch; - bool valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - if (!valid) - valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - if (!valid) - return false; - - unsigned char buffer[10U]; - facch.getData(buffer); - - CM17Layer3 layer3; - layer3.decode(buffer, M17_FACCH1_LENGTH_BITS); - - unsigned char type = layer3.getMessageType(); - if (type == M17_MESSAGE_TYPE_TX_REL) { - if (m_rfState != RS_RF_AUDIO) { - m_rfState = RS_RF_LISTENING; - m_rfMask = 0x00U; - m_rfLayer3.reset(); - return false; - } - } else if (type == M17_MESSAGE_TYPE_VCALL) { - if (m_rfState == RS_RF_LISTENING && m_selfOnly) { - unsigned short srcId = layer3.getSourceUnitId(); - if (srcId != m_id) { - m_rfState = RS_RF_REJECTED; - return false; - } - } - } else { - return false; + conv.decode(s0, s1); } - m_rfLayer3 = layer3; + unsigned char frame[M17_LICH_LENGTH_BYTES]; + conv.chainback(frame, M17_LICH_LENGTH_BITS); - data[0U] = type == M17_MESSAGE_TYPE_TX_REL ? TAG_EOT : TAG_DATA; - data[1U] = 0x00U; + bool valid = CM17CRC::checkCRC(frame, M17_LICH_LENGTH_BITS); + if (valid) { + m_rfFrames = 0U; + m_rfErrs = 0U; + m_rfBits = 1U; + m_rfTimeoutTimer.start(); + m_minRSSI = m_rssi; + m_maxRSSI = m_rssi; + m_aveRSSI = m_rssi; + m_rssiCount = 1U; - CSync::addM17Sync(data + 2U); +#if defined(DUMP_M17) + openFile(); +#endif + m_rfLICH.setLinkSetup(frame); - CM17LICH lich; - lich.setRFCT(M17_LICH_RFCT_RDCH); - lich.setFCT(M17_LICH_USC_SACCH_NS); - lich.setOption(M17_LICH_STEAL_FACCH); - lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); - lich.encode(data + 2U); + std::string source = m_rfLICH.getSource(); + std::string dest = m_rfLICH.getDest(); - lich.setDirection(M17_LICH_DIRECTION_INBOUND); - netData[0U] = lich.getRaw(); + unsigned char dataType = m_rfLICH.getDataType(); + switch (dataType) { + case 1U: + LogMessage("M17, received RF data transmission from %s to %s", source.c_str(), dest.c_str()); + m_rfState = RS_RF_DATA; + break; + case 2U: + LogMessage("M17, received RF voice transmission from %s to %s", source.c_str(), dest.c_str()); + m_rfState = RS_RF_AUDIO; + break; + case 3U: + LogMessage("M17, received RF voice + data transmission from %s to %s", source.c_str(), dest.c_str()); + m_rfState = RS_RF_AUDIO; + break; + default: + LogMessage("M17, received RF unknown transmission from %s to %s", source.c_str(), dest.c_str()); + m_rfState = RS_RF_DATA; + break; + } - CM17SACCH sacch; - sacch.setRAN(m_ran); - sacch.setStructure(M17_SR_SINGLE); - sacch.setData(SACCH_IDLE); - sacch.encode(data + 2U); + m_display->writeM17(source.c_str(), dest.c_str(), "R"); - sacch.getRaw(netData + 1U); +#if defined(DUMP_M17) + writeFile(data + 2U); +#endif + if (m_duplex) { + data[0U] = TAG_DATA; + data[1U] = 0x00U; - facch.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - facch.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); + // Generate the sync + CSync::addM17Sync(data + 2U); - facch.getRaw(netData + 5U + 0U); - facch.getRaw(netData + 5U + 14U); + unsigned char setup[M17_LICH_LENGTH_BYTES]; + m_rfLICH.getLinkSetup(data + 2U); - scrambler(data + 2U); + // Add the CRC + CM17CRC::encodeCRC(data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); - writeNetwork(netData, data[0U] == TAG_EOT ? NNMT_VOICE_TRAILER : NNMT_VOICE_HEADER); + // Add the convolution FEC + CM17Convolution conv; + conv.encode(setup, data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); + unsigned char temp[M17_FRAME_LENGTH_BYTES]; + interleaver(data + 2U, temp); + decorrelator(temp, data + 2U); + + writeQueueRF(data); + } + } else { + m_rfState = RS_RF_LATE_ENTRY; + } + } + + if (m_rfState == RS_RF_LATE_ENTRY) { + CM17Convolution conv; + conv.start(); + + unsigned int n = 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_LENGTH_BYTES; + for (unsigned int i = 0U; i < (M17_LICH_LENGTH_BYTES / 2U); i++) { + uint8_t s0 = data[n++]; + uint8_t s1 = data[n++]; + + conv.decode(s0, s1); + } + + unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; + conv.chainback(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + + bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + if (valid) { + unsigned int fn = (frame[0U] << 8) + (frame[1U] << 0); + + unsigned int frag1, frag2, frag3, frag4; + + // XXX TODO populate frag1-4 + + unsigned int lich1 = CGolay24128::decode24128(frag1); + unsigned int lich2 = CGolay24128::decode24128(frag2); + unsigned int lich3 = CGolay24128::decode24128(frag3); + unsigned int lich4 = CGolay24128::decode24128(frag4); + + m_rfLICH.setFragment(data + 2U + M17_SYNC_LENGTH_BYTES, fn & 0x7FFFU); + + valid = m_rfLICH.isValid(); + if (valid) { + m_rfFrames = 0U; + m_rfErrs = 0U; + m_rfBits = 1U; + m_rfTimeoutTimer.start(); + m_minRSSI = m_rssi; + m_maxRSSI = m_rssi; + m_aveRSSI = m_rssi; + m_rssiCount = 1U; + +#if defined(DUMP_M17) + openFile(); +#endif + std::string source = m_rfLICH.getSource(); + std::string dest = m_rfLICH.getDest(); + + unsigned char dataType = m_rfLICH.getDataType(); + switch (dataType) { + case 1U: + LogMessage("M17, received RF late entry data transmission from %s to %s", source.c_str(), dest.c_str()); + m_rfState = RS_RF_DATA; + break; + case 2U: + LogMessage("M17, received RF late entry voice transmission from %s to %s", source.c_str(), dest.c_str()); + m_rfState = RS_RF_AUDIO; + break; + case 3U: + LogMessage("M17, received RF late entry voice + data transmission from %s to %s", source.c_str(), dest.c_str()); + m_rfState = RS_RF_AUDIO; + break; + default: + LogMessage("M17, received RF late entry unknown transmission from %s to %s", source.c_str(), dest.c_str()); + m_rfState = RS_RF_DATA; + break; + } + + m_display->writeM17(source.c_str(), dest.c_str(), "R"); + + if (m_duplex) { + // Create a Link Setup frame + data[0U] = TAG_DATA; + data[1U] = 0x00U; + + // Generate the sync + CSync::addM17Sync(data + 2U); + + unsigned char setup[M17_LICH_LENGTH_BYTES]; + m_rfLICH.getLinkSetup(data + 2U); + + // Add the CRC + CM17CRC::encodeCRC(data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); + + // Add the convolution FEC + CM17Convolution conv; + conv.encode(setup, data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); + + unsigned char temp[M17_FRAME_LENGTH_BYTES]; + interleaver(data + 2U, temp); + decorrelator(temp, data + 2U); + + writeQueueRF(data); + } + } + } + } + + if (m_rfState == RS_RF_AUDIO || m_rfState == RS_RF_DATA) { #if defined(DUMP_M17) writeFile(data + 2U); #endif @@ -250,32 +347,32 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) writeQueueRF(data); if (data[0U] == TAG_EOT) { - unsigned short dstId = m_rfLayer3.getDestinationGroupId(); - bool grp = m_rfLayer3.getIsGroup(); - std::string source = m_lookup->find(m_rfLayer3.getSourceUnitId()); + std::string source = m_rfLICH.getSource(); + std::string dest = m_rfLICH.getDest(); m_rfFrames++; if (m_rssi != 0U) - LogMessage("M17, received RF end of transmission from %s to %s%u, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); + LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); else - LogMessage("M17, received RF end of transmission from %s to %s%u, %.1f seconds, BER: %.1f%%", source.c_str(), grp ? "TG " : "", dstId, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); + LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); writeEndRF(); - } else { - m_rfFrames = 0U; - m_rfErrs = 0U; - m_rfBits = 1U; + } + else { + m_rfFrames = 0U; + m_rfErrs = 0U; + m_rfBits = 1U; m_rfTimeoutTimer.start(); - m_rfState = RS_RF_AUDIO; - m_minRSSI = m_rssi; - m_maxRSSI = m_rssi; - m_aveRSSI = m_rssi; + m_rfState = RS_RF_AUDIO; + m_minRSSI = m_rssi; + m_maxRSSI = m_rssi; + m_aveRSSI = m_rssi; m_rssiCount = 1U; #if defined(DUMP_M17) openFile(); #endif unsigned short srcId = m_rfLayer3.getSourceUnitId(); unsigned short dstId = m_rfLayer3.getDestinationGroupId(); - bool grp = m_rfLayer3.getIsGroup(); + bool grp = m_rfLayer3.getIsGroup(); std::string source = m_lookup->find(srcId); LogMessage("M17, received RF header from %s to %s%u", source.c_str(), grp ? "TG " : "", dstId); @@ -283,7 +380,8 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) } return true; - } else { + } + else { if (m_rfState == RS_RF_LISTENING) { CM17FACCH1 facch; bool valid = false; @@ -326,7 +424,7 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) switch (structure) { case M17_SR_1_4: m_rfLayer3.decode(message, 18U, 0U); - if(m_rfLayer3.getMessageType() == M17_MESSAGE_TYPE_VCALL) + if (m_rfLayer3.getMessageType() == M17_MESSAGE_TYPE_VCALL) m_rfMask = 0x01U; else m_rfMask = 0x00U; @@ -475,9 +573,10 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) LogDebug("M17, AMBE FEC %u/188 (%.1f%%)", errors, float(errors) / 1.88F); CM17Audio audio; - audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U, netData + 5U + 0U); + audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U, netData + 5U + 0U); audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U, netData + 5U + 14U); - } else if (option == M17_LICH_STEAL_FACCH1_1) { + } + else if (option == M17_LICH_STEAL_FACCH1_1) { CM17FACCH1 facch1; bool valid = facch1.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); if (valid) @@ -495,7 +594,8 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) CM17Audio audio; audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U, netData + 5U + 14U); - } else if (option == M17_LICH_STEAL_FACCH1_2) { + } + else if (option == M17_LICH_STEAL_FACCH1_2) { CAMBEFEC ambe; unsigned int errors = 0U; errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES); @@ -513,7 +613,8 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) if (valid) facch1.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); facch1.getRaw(netData + 5U + 14U); - } else { + } + else { CM17FACCH1 facch11; bool valid1 = facch11.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); if (valid1) @@ -548,6 +649,17 @@ bool CM17Control::writeModem(unsigned char *data, unsigned int len) m_display->writeM17RSSI(m_rssi); } + m_rfFrames++; + + // EOT? + if ((fn & 0x8000U) == 0x8000U) { + if (m_rssi != 0U) + LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); + else + LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); + writeEndRF(); + } + return true; } @@ -571,7 +683,6 @@ void CM17Control::writeEndRF() m_rfState = RS_RF_LISTENING; m_rfMask = 0x00U; - m_rfLayer3.reset(); m_rfTimeoutTimer.stop(); @@ -591,8 +702,6 @@ void CM17Control::writeEndNet() { m_netState = RS_NET_IDLE; - m_netLayer3.reset(); - m_netTimeoutTimer.stop(); m_networkWatchdog.stop(); m_packetTimer.stop(); @@ -660,8 +769,16 @@ void CM17Control::writeNetwork() // Generate the sync CSync::addM17Sync(start + 2U); + unsigned char setup[M17_LICH_LENGTH_BYTES]; m_netLICH.getLinkSetup(start + 2U); + // Add the CRC + CM17CRC::encodeCRC(start + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); + + // Add the convolution FEC + CM17Convolution conv; + conv.encode(setup, start + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); + unsigned char temp[M17_FRAME_LENGTH_BYTES]; interleaver(start + 2U, temp); decorrelator(temp, start + 2U); @@ -674,18 +791,53 @@ void CM17Control::writeNetwork() data[0U] = TAG_DATA; data[1U] = 0x00U; + unsigned char* p = data + 2U; + // Generate the sync - CSync::addM17Sync(data + 2U); + CSync::addM17Sync(p); + p += M17_SYNC_LENGTH_BYTES; m_netFrames++; // Add the fragment LICH uint16_t fn = (netData[38U] << 8) + (netData[39U] << 0); - m_netLICH.getFragment(data + 2U, fn & 0x7FFFU); - // Add the data/audio + unsigned char lich[6U]; + m_netLICH.getFragment(lich, fn & 0x7FFFU); - // Add the FEC + unsigned int lich1, lich2, lich3, lich4; + + // XXX TODO + + // Add Golay to the LICH fragment here + CGolay24128::encode24128(lich1); + CGolay24128::encode24128(lich2); + CGolay24128::encode24128(lich3); + CGolay24128::encode24128(lich4); + + ::memcpy(p, &lich1, M17_LICH_FRAGMENT_LENGTH_BYTES / 4U); + p += M17_LICH_FRAGMENT_LENGTH_BYTES / 4U; + + ::memcpy(p, &lich2, M17_LICH_FRAGMENT_LENGTH_BYTES / 4U); + p += M17_LICH_FRAGMENT_LENGTH_BYTES / 4U; + + ::memcpy(p, &lich3, M17_LICH_FRAGMENT_LENGTH_BYTES / 4U); + p += M17_LICH_FRAGMENT_LENGTH_BYTES / 4U; + + ::memcpy(p, &lich4, M17_LICH_FRAGMENT_LENGTH_BYTES / 4U); + p += M17_LICH_FRAGMENT_LENGTH_BYTES / 4U; + + unsigned char payload[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; + + // Add the FN and the data/audio + ::memcpy(payload, netData + 38U, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES); + + // Add the CRC + CM17CRC::encodeCRC(payload, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + + // Add the Convolution FEC + CM17Convolution conv; + conv.encode(payload, p, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); unsigned char temp[M17_FRAME_LENGTH_BYTES]; interleaver(data + 2U, temp); @@ -794,7 +946,7 @@ void CM17Control::decorrelator(const unsigned char* in, unsigned char* out) cons assert(in != NULL); assert(out != NULL); - for (unsigned int i = M17_SYNC_BYTES_LENGTH; i < M17_FRAME_LENGTH_BYTES; i++) + for (unsigned int i = M17_SYNC_LENGTH_BYTES; i < M17_FRAME_LENGTH_BYTES; i++) out[i] = in[i] ^ SCRAMBLER[i]; } @@ -852,15 +1004,12 @@ void CM17Control::enable(bool enabled) m_rfState = RS_RF_LISTENING; m_rfMask = 0x00U; - m_rfLayer3.reset(); m_rfTimeoutTimer.stop(); // Reset the networking section m_netState = RS_NET_IDLE; - m_netLayer3.reset(); - m_netTimeoutTimer.stop(); m_networkWatchdog.stop(); m_packetTimer.stop(); diff --git a/M17Convolution.h b/M17Convolution.h new file mode 100644 index 0000000..9e7feb4 --- /dev/null +++ b/M17Convolution.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015,2016,2018,2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(M17Convolution_H) +#define M17Convolution_H + +#include + +class CM17Convolution { +public: + CM17Convolution(); + ~CM17Convolution(); + + void start(); + void decode(uint8_t s0, uint8_t s1); + void chainback(unsigned char* out, unsigned int nBits); + + void encode(const unsigned char* in, unsigned char* out, unsigned int nBits) const; + +private: + uint16_t* m_metrics1; + uint16_t* m_metrics2; + uint16_t* m_oldMetrics; + uint16_t* m_newMetrics; + uint64_t* m_decisions; + uint64_t* m_dp; +}; + +#endif diff --git a/M17Defines.h b/M17Defines.h index e9cb48f..9e061e8 100644 --- a/M17Defines.h +++ b/M17Defines.h @@ -23,12 +23,24 @@ const unsigned int M17_RADIO_SYMBOL_LENGTH = 5U; // At 24 kHz sample rate const unsigned int M17_FRAME_LENGTH_BITS = 384U; const unsigned int M17_FRAME_LENGTH_BYTES = M17_FRAME_LENGTH_BITS / 8U; -const unsigned int M17_FRAME_LENGTH_SYMBOLS = M17_FRAME_LENGTH_BITS / 2U; - -const unsigned int M17_SYNC_LENGTH_BITS = 16U; -const unsigned int M17_SYNC_LENGTH_SYMBOLS = M17_SYNC_LENGTH_BITS / 2U; const unsigned char M17_SYNC_BYTES[] = {0x32U, 0x43U}; -const unsigned int M17_SYNC_BYTES_LENGTH = 2U; +const unsigned int M17_SYNC_LENGTH_BITS = 16U; +const unsigned int M17_SYNC_LENGTH_BYTES = M17_SYNC_LENGTH_BITS / 8U; + +const unsigned int M17_LICH_LENGTH_BITS = 244U; +const unsigned int M17_LICH_LENGTH_BYTES = 31U; + +const unsigned int M17_LICH_FRAGMENT_LENGTH_BITS = 96U; +const unsigned int M17_LICH_FRAGMENT_LENGTH_BYTES = M17_LICH_FRAGMENT_LENGTH_BITS / 8U; + +const unsigned int M17_PAYLOAD_LENGTH_BITS = 128U; +const unsigned int M17_PAYLOAD_LENGTH_BYTES = M17_PAYLOAD_LENGTH_BITS / 8U; + +const unsigned int M17_FN_LENGTH_BITS = 16U; +const unsigned int M17_FN_LENGTH_BYTES = M17_FN_LENGTH_BITS / 8U; + +const unsigned int M17_CRC_LENGTH_BITS = 16U; +const unsigned int M17_CRC_LENGTH_BYTES = M17_CRC_LENGTH_BITS / 8U; #endif diff --git a/M17LICH.h b/M17LICH.h index 97215cd..c16a58f 100644 --- a/M17LICH.h +++ b/M17LICH.h @@ -27,14 +27,20 @@ public: CM17LICH(); ~CM17LICH(); + void getNetworkData(unsigned char* data); void setNetworkData(const unsigned char* data); std::string getSource() const; std::string getDest() const; unsigned char getDataType() const; + bool isValid() const; + void getLinkSetup(unsigned char* data) const; + void setLinkSetup(const unsigned char* data) const; + void getFragment(unsigned char* data, unsigned short fn); + void setFragment(const unsigned char* data, unsigned short fn); CM17LICH& operator=(const CM17LICH& lich); diff --git a/M17Network.cpp b/M17Network.cpp index 20ee736..6e4d66e 100644 --- a/M17Network.cpp +++ b/M17Network.cpp @@ -18,6 +18,7 @@ #include "M17Network.h" #include "M17Defines.h" +#include "M17Utils.h" #include "Defines.h" #include "Utils.h" #include "Log.h" @@ -28,8 +29,8 @@ const unsigned int BUFFER_LENGTH = 200U; -CM17Network::CM17Network(unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, bool debug) : -m_socket(localPort), +CM17Network::CM17Network(unsigned int port, bool debug) : +m_socket(port), m_addr(), m_addrLen(0U), m_debug(debug), @@ -37,34 +38,69 @@ m_enabled(false), m_outId(0U), m_inId(0U), m_buffer(1000U, "M17 Network"), -m_random() +m_random(), +m_state(M17N_NOTLINKED), +m_reflector(NULL), +m_module(' '), +m_timer(1000U, 5U) { std::random_device rd; std::mt19937 mt(rd()); m_random = mt; - if (CUDPSocket::lookup(gatewayAddress, gatewayPort, m_addr, m_addrLen) != 0) - m_addrLen = 0U; + m_encoded = new unsigned char[6U]; } CM17Network::~CM17Network() { + delete[] m_encoded; } bool CM17Network::open() { - if (m_addrLen == 0U) { - LogError("Unable to resolve the address of the M17 Gateway"); - return false; - } - LogMessage("Opening M17 network connection"); return m_socket.open(m_addr); } +bool CM17Network::link(const std::string& address, unsigned int port, const std::string& reflector, char module) +{ + if (CUDPSocket::lookup(address, port, m_addr, m_addrLen) != 0) { + m_state = M17N_NOTLINKED; + return false; + } + + m_reflector = reflector; + m_module = module; + + CM17Utils::encodeCallsign(m_reflector, m_encoded); + + m_state = M17N_LINKING; + + sendConnect(); + + m_timer.start(); + + return true; +} + +void CM17Network::unlink() +{ + if (m_state != M17N_LINKED) + return; + + m_state = M17N_UNLINKING; + + sendConnect(); + + m_timer.start(); +} + bool CM17Network::write(const unsigned char* data) { + if (m_state != M17N_LINKED) + return false; + assert(data != NULL); unsigned char buffer[100U]; @@ -91,6 +127,23 @@ bool CM17Network::write(const unsigned char* data) void CM17Network::clock(unsigned int ms) { + m_timer.clock(ms); + if (m_timer.isRunning() && m_timer.hasExpired()) { + switch (m_state) { + case M17N_LINKING: + sendConnect(); + m_timer.start(); + break; + case M17N_UNLINKING: + sendDisconnect(); + m_timer.start(); + break; + default: + m_timer.stop(); + break; + } + } + unsigned char buffer[BUFFER_LENGTH]; sockaddr_storage address; @@ -104,14 +157,42 @@ void CM17Network::clock(unsigned int ms) return; } - if (!m_enabled) - return; - if (m_debug) CUtils::dump(1U, "M17 Network Data Received", buffer, length); - if (::memcmp(buffer + 0U, "M17 ", 4U) != 0) + if (::memcmp(buffer + 0U, "ACKN", 4U) == 0) { + m_timer.stop(); + m_state = M17N_LINKED; + LogMessage("M17, linked to %s", m_reflector.c_str()); return; + } + + if (::memcmp(buffer + 0U, "NACK", 4U) == 0) { + m_timer.stop(); + m_state = M17N_NOTLINKED; + LogMessage("M17, link refused by %s", m_reflector.c_str()); + return; + } + + if (::memcmp(buffer + 0U, "PING", 4U) == 0) { + sendPong(); + return; + } + + if (::memcmp(buffer + 0U, "DISC", 4U) == 0) { + m_timer.stop(); + m_state = M17N_NOTLINKED; + LogMessage("M17, unlinked from %s", m_reflector.c_str()); + return; + } + + if (!m_enabled) + return; + + if (::memcmp(buffer + 0U, "M17 ", 4U) != 0) { + CUtils::dump(2U, "M17, received unknown packet", buffer, length); + return; + } uint16_t id = (buffer[4U] << 8) + (buffer[5U] << 0); if (m_inId == 0U) { @@ -167,3 +248,71 @@ void CM17Network::enable(bool enabled) m_enabled = enabled; } + +void CM17Network::sendConnect() +{ + unsigned char buffer[15U]; + + buffer[0U] = 'C'; + buffer[1U] = 'O'; + buffer[2U] = 'N'; + buffer[3U] = 'N'; + + buffer[4U] = m_encoded[0U]; + buffer[5U] = m_encoded[1U]; + buffer[6U] = m_encoded[2U]; + buffer[7U] = m_encoded[3U]; + buffer[8U] = m_encoded[4U]; + buffer[9U] = m_encoded[5U]; + + buffer[10U] = m_module; + + if (m_debug) + CUtils::dump(1U, "M17 data transmitted", buffer, 11U); + + m_socket.write(buffer, 11U, m_addr, m_addrLen); +} + +void CM17Network::sendPong() +{ + unsigned char buffer[15U]; + + buffer[0U] = 'P'; + buffer[1U] = 'O'; + buffer[2U] = 'N'; + buffer[3U] = 'G'; + + buffer[4U] = m_encoded[0U]; + buffer[5U] = m_encoded[1U]; + buffer[6U] = m_encoded[2U]; + buffer[7U] = m_encoded[3U]; + buffer[8U] = m_encoded[4U]; + buffer[9U] = m_encoded[5U]; + + if (m_debug) + CUtils::dump(1U, "M17 data transmitted", buffer, 10U); + + m_socket.write(buffer, 10U, m_addr, m_addrLen); +} + +void CM17Network::sendDisconnect() +{ + unsigned char buffer[15U]; + + buffer[0U] = 'D'; + buffer[1U] = 'I'; + buffer[2U] = 'S'; + buffer[3U] = 'C'; + + buffer[4U] = m_encoded[0U]; + buffer[5U] = m_encoded[1U]; + buffer[6U] = m_encoded[2U]; + buffer[7U] = m_encoded[3U]; + buffer[8U] = m_encoded[4U]; + buffer[9U] = m_encoded[5U]; + + if (m_debug) + CUtils::dump(1U, "M17 data transmitted", buffer, 10U); + + m_socket.write(buffer, 10U, m_addr, m_addrLen); +} diff --git a/M17Network.h b/M17Network.h index 697cb1a..b1d3710 100644 --- a/M17Network.h +++ b/M17Network.h @@ -22,17 +22,29 @@ #include "M17Defines.h" #include "RingBuffer.h" #include "UDPSocket.h" +#include "Timer.h" #include #include +enum M17NET_STATUS { + M17N_NOTLINKED, + M17N_LINKING, + M17N_LINKED, + M17N_UNLINKING +}; + class CM17Network { public: - CM17Network(unsigned int localPort, const std::string& gwyAddress, unsigned int gwyPort, bool debug); + CM17Network(unsigned int port, bool debug); ~CM17Network(); bool open(); + bool link(const std::string& address, unsigned int port, const std::string& reflector, char module); + + void unlink(); + void enable(bool enabled); bool write(const unsigned char* data); @@ -55,6 +67,15 @@ private: uint16_t m_inId; CRingBuffer m_buffer; std::mt19937 m_random; + M17NET_STATUS m_state; + std::string m_reflector; + unsigned char* m_encoded; + char m_module; + CTimer m_timer; + + void sendConnect(); + void sendDisconnect(); + void sendPong(); }; #endif diff --git a/M17Utils.h b/M17Utils.h new file mode 100644 index 0000000..025e65a --- /dev/null +++ b/M17Utils.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(M17Utils_H) +#define M17Utils_H + +#include + +class CM17Utils { +public: + CM17Utils(); + ~CM17Utils(); + + static void encodeCallsign(const std::string& callsign, unsigned char* data); + + static void decodeCallsign(const unsigned char* data, std::string& callsign); + +private: +}; + +#endif diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index 87d40df..a399c3a 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -189,9 +189,12 @@ + + + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index f013ec1..8f9a797 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -311,6 +311,15 @@ Header Files + + Header Files + + + Header Files + + + Header Files + From 9a72e95ec073ed82f5765c931f64fff25b97b4e3 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 17 Oct 2020 16:47:19 +0100 Subject: [PATCH 122/163] Clean compile(-ish), still more to do. --- M17Control.cpp | 338 +++++-------------------------------------------- M17Control.h | 2 +- M17LICH.h | 1 + M17Network.cpp | 11 +- MMDVMHost.cpp | 4 +- Sync.cpp | 2 +- 6 files changed, 41 insertions(+), 317 deletions(-) diff --git a/M17Control.cpp b/M17Control.cpp index 24cc8c3..1169a5c 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -72,10 +72,10 @@ m_networkWatchdog(1000U, 0U, 1500U), m_elapsed(), m_rfFrames(0U), m_netFrames(0U), +m_rfLastFN(0U), m_rfErrs(0U), m_rfBits(1U), m_rfLICH(), -m_rfMask(0x00U), m_netLICH(), m_rssiMapper(rssiMapper), m_rssi(0U), @@ -122,7 +122,6 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) if (type == TAG_LOST) { m_rfState = RS_RF_LISTENING; - m_rfMask = 0x00U; return false; } @@ -154,7 +153,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) interleaver(temp, data + 2U); if (m_rfState == RS_RF_LISTENING) { - m_rfMask = 0x00U; + m_rfLICH.reset(); CM17Convolution conv; conv.start(); @@ -180,6 +179,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) m_maxRSSI = m_rssi; m_aveRSSI = m_rssi; m_rssiCount = 1U; + m_rfLastFN = 0U; #if defined(DUMP_M17) openFile(); @@ -259,7 +259,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); if (valid) { - unsigned int fn = (frame[0U] << 8) + (frame[1U] << 0); + m_rfLastFN = (frame[0U] << 8) + (frame[1U] << 0); unsigned int frag1, frag2, frag3, frag4; @@ -270,7 +270,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) unsigned int lich3 = CGolay24128::decode24128(frag3); unsigned int lich4 = CGolay24128::decode24128(frag4); - m_rfLICH.setFragment(data + 2U + M17_SYNC_LENGTH_BYTES, fn & 0x7FFFU); + m_rfLICH.setFragment(data + 2U + M17_SYNC_LENGTH_BYTES, m_rfLastFN & 0x7FFFU); valid = m_rfLICH.isValid(); if (valid) { @@ -343,321 +343,45 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) #if defined(DUMP_M17) writeFile(data + 2U); #endif - if (m_duplex) - writeQueueRF(data); - if (data[0U] == TAG_EOT) { - std::string source = m_rfLICH.getSource(); - std::string dest = m_rfLICH.getDest(); + CM17Convolution conv; + conv.start(); - m_rfFrames++; - if (m_rssi != 0U) - LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); - else - LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); - writeEndRF(); - } - else { - m_rfFrames = 0U; - m_rfErrs = 0U; - m_rfBits = 1U; - m_rfTimeoutTimer.start(); - m_rfState = RS_RF_AUDIO; - m_minRSSI = m_rssi; - m_maxRSSI = m_rssi; - m_aveRSSI = m_rssi; - m_rssiCount = 1U; -#if defined(DUMP_M17) - openFile(); -#endif - unsigned short srcId = m_rfLayer3.getSourceUnitId(); - unsigned short dstId = m_rfLayer3.getDestinationGroupId(); - bool grp = m_rfLayer3.getIsGroup(); + unsigned int n = 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_LENGTH_BYTES; + for (unsigned int i = 0U; i < (M17_LICH_LENGTH_BYTES / 2U); i++) { + uint8_t s0 = data[n++]; + uint8_t s1 = data[n++]; - std::string source = m_lookup->find(srcId); - LogMessage("M17, received RF header from %s to %s%u", source.c_str(), grp ? "TG " : "", dstId); - m_display->writeM17(source.c_str(), grp, dstId, "R"); + conv.decode(s0, s1); } - return true; - } - else { - if (m_rfState == RS_RF_LISTENING) { - CM17FACCH1 facch; - bool valid = false; - switch (option) { - case M17_LICH_STEAL_FACCH: - valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - if (!valid) - valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - break; - case M17_LICH_STEAL_FACCH1_1: - valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - break; - case M17_LICH_STEAL_FACCH1_2: - valid = facch.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - break; - default: - break; - } + unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; + conv.chainback(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); - bool hasInfo = false; - if (valid) { - unsigned char buffer[10U]; - facch.getData(buffer); + bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + if (valid) { + m_rfLastFN = (frame[0U] << 8) + (frame[1U] << 0); - CM17Layer3 layer3; - layer3.decode(buffer, M17_FACCH1_LENGTH_BITS); - - hasInfo = layer3.getMessageType() == M17_MESSAGE_TYPE_VCALL; - if (!hasInfo) - return false; - - m_rfLayer3 = layer3; - } - - if (!hasInfo) { - unsigned char message[3U]; - sacch.getData(message); - - unsigned char structure = sacch.getStructure(); - switch (structure) { - case M17_SR_1_4: - m_rfLayer3.decode(message, 18U, 0U); - if (m_rfLayer3.getMessageType() == M17_MESSAGE_TYPE_VCALL) - m_rfMask = 0x01U; - else - m_rfMask = 0x00U; - break; - case M17_SR_2_4: - m_rfMask |= 0x02U; - m_rfLayer3.decode(message, 18U, 18U); - break; - case M17_SR_3_4: - m_rfMask |= 0x04U; - m_rfLayer3.decode(message, 18U, 36U); - break; - case M17_SR_4_4: - m_rfMask |= 0x08U; - m_rfLayer3.decode(message, 18U, 54U); - break; - default: - break; - } - - if (m_rfMask != 0x0FU) - return false; - - unsigned char type = m_rfLayer3.getMessageType(); - if (type != M17_MESSAGE_TYPE_VCALL) - return false; - } - - unsigned short srcId = m_rfLayer3.getSourceUnitId(); - unsigned short dstId = m_rfLayer3.getDestinationGroupId(); - bool grp = m_rfLayer3.getIsGroup(); - - if (m_selfOnly) { - if (srcId != m_id) { - m_rfState = RS_RF_REJECTED; - return false; - } - } - - m_rfFrames = 0U; - m_rfErrs = 0U; - m_rfBits = 1U; - m_rfTimeoutTimer.start(); - m_rfState = RS_RF_AUDIO; - - m_minRSSI = m_rssi; - m_maxRSSI = m_rssi; - m_aveRSSI = m_rssi; - m_rssiCount = 1U; -#if defined(DUMP_M17) - openFile(); -#endif - std::string source = m_lookup->find(srcId); - LogMessage("M17, received RF late entry from %s to %s%u", source.c_str(), grp ? "TG " : "", dstId); - m_display->writeM17(source.c_str(), grp, dstId, "R"); - - m_rfState = RS_RF_AUDIO; - - // Create a dummy start message - unsigned char start[M17_FRAME_LENGTH_BYTES + 2U]; - - start[0U] = TAG_DATA; - start[1U] = 0x00U; - - // Generate the sync - CSync::addM17Sync(start + 2U); - - // Generate the LICH - CM17LICH lich; - lich.setRFCT(M17_LICH_RFCT_RDCH); - lich.setFCT(M17_LICH_USC_SACCH_NS); - lich.setOption(M17_LICH_STEAL_FACCH); - lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); - lich.encode(start + 2U); - - lich.setDirection(M17_LICH_DIRECTION_INBOUND); - netData[0U] = lich.getRaw(); - - CM17SACCH sacch; - sacch.setRAN(m_ran); - sacch.setStructure(M17_SR_SINGLE); - sacch.setData(SACCH_IDLE); - sacch.encode(start + 2U); - - sacch.getRaw(netData + 1U); - - unsigned char message[22U]; - m_rfLayer3.getData(message); - - facch.setData(message); - facch.encode(start + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - facch.encode(start + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - - facch.getRaw(netData + 5U + 0U); - facch.getRaw(netData + 5U + 14U); - - interleaver(start + 2U); - decorrelator(start + 2U); - - writeNetwork(netData); - -#if defined(DUMP_M17) - writeFile(start + 2U); -#endif - if (m_duplex) - writeQueueRF(start); + } else { + m_rfLastFN++; } - } - - if (m_rfState == RS_RF_AUDIO) { - // Regenerate the sync - CSync::addM17Sync(data + 2U); - - // Regenerate the LICH - CM17LICH lich; - lich.setRFCT(M17_LICH_RFCT_RDCH); - lich.setFCT(M17_LICH_USC_SACCH_SS); - lich.setOption(option); - lich.setDirection(m_remoteGateway || !m_duplex ? M17_LICH_DIRECTION_INBOUND : M17_LICH_DIRECTION_OUTBOUND); - lich.encode(data + 2U); - - lich.setDirection(M17_LICH_DIRECTION_INBOUND); - netData[0U] = lich.getRaw(); - - // Regenerate SACCH if it's valid - CM17SACCH sacch; - bool validSACCH = sacch.decode(data + 2U); - if (validSACCH) { - sacch.setRAN(m_ran); - sacch.encode(data + 2U); - } - - sacch.getRaw(netData + 1U); - - // Regenerate the audio and interpret the FACCH1 data - if (option == M17_LICH_STEAL_NONE) { - CAMBEFEC ambe; - unsigned int errors = 0U; - errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U); - errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 9U); - errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U); - errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 27U); - m_rfErrs += errors; - m_rfBits += 188U; - m_display->writeM17BER(float(errors) / 1.88F); - LogDebug("M17, AMBE FEC %u/188 (%.1f%%)", errors, float(errors) / 1.88F); - - CM17Audio audio; - audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U, netData + 5U + 0U); - audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U, netData + 5U + 14U); - } - else if (option == M17_LICH_STEAL_FACCH1_1) { - CM17FACCH1 facch1; - bool valid = facch1.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - if (valid) - facch1.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - facch1.getRaw(netData + 5U + 0U); - - CAMBEFEC ambe; - unsigned int errors = 0U; - errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U); - errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 27U); - m_rfErrs += errors; - m_rfBits += 94U; - m_display->writeM17BER(float(errors) / 0.94F); - LogDebug("M17, AMBE FEC %u/94 (%.1f%%)", errors, float(errors) / 0.94F); - - CM17Audio audio; - audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 18U, netData + 5U + 14U); - } - else if (option == M17_LICH_STEAL_FACCH1_2) { - CAMBEFEC ambe; - unsigned int errors = 0U; - errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES); - errors += ambe.regenerateYSFDN(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 9U); - m_rfErrs += errors; - m_rfBits += 94U; - m_display->writeM17BER(float(errors) / 0.94F); - LogDebug("M17, AMBE FEC %u/94 (%.1f%%)", errors, float(errors) / 0.94F); - - CM17Audio audio; - audio.decode(data + 2U + M17_FSW_LICH_SACCH_LENGTH_BYTES + 0U, netData + 5U + 0U); - - CM17FACCH1 facch1; - bool valid = facch1.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - if (valid) - facch1.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - facch1.getRaw(netData + 5U + 14U); - } - else { - CM17FACCH1 facch11; - bool valid1 = facch11.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - if (valid1) - facch11.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS); - facch11.getRaw(netData + 5U + 0U); - - CM17FACCH1 facch12; - bool valid2 = facch12.decode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - if (valid2) - facch12.encode(data + 2U, M17_FSW_LENGTH_BITS + M17_LICH_LENGTH_BITS + M17_SACCH_LENGTH_BITS + M17_FACCH1_LENGTH_BITS); - facch12.getRaw(netData + 5U + 14U); - } - - data[0U] = TAG_DATA; - data[1U] = 0x00U; - - unsigned char temp[M17_FRAME_LENGTH_BYTES]; - interleaver(data + 2U, temp); - decorrelator(temp, data + 2U); - - writeNetwork(netData); - -#if defined(DUMP_M17) - writeFile(data + 2U); -#endif if (m_duplex) writeQueueRF(data); m_rfFrames++; - m_display->writeM17RSSI(m_rssi); - } + // EOT? + if ((m_rfLastFN & 0x8000U) == 0x8000U) { + std::string source = m_rfLICH.getSource(); + std::string dest = m_rfLICH.getDest(); - m_rfFrames++; - - // EOT? - if ((fn & 0x8000U) == 0x8000U) { - if (m_rssi != 0U) - LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); - else - LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); - writeEndRF(); + if (m_rssi != 0U) + LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); + else + LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); + writeEndRF(); + } } return true; @@ -682,8 +406,6 @@ void CM17Control::writeEndRF() { m_rfState = RS_RF_LISTENING; - m_rfMask = 0x00U; - m_rfTimeoutTimer.stop(); if (m_netState == RS_NET_IDLE) { @@ -1003,8 +725,6 @@ void CM17Control::enable(bool enabled) // Reset the RF section m_rfState = RS_RF_LISTENING; - m_rfMask = 0x00U; - m_rfTimeoutTimer.stop(); // Reset the networking section diff --git a/M17Control.h b/M17Control.h index 371b729..ed39f3a 100644 --- a/M17Control.h +++ b/M17Control.h @@ -63,10 +63,10 @@ private: CStopWatch m_elapsed; unsigned int m_rfFrames; unsigned int m_netFrames; + unsigned int m_rfLastFN; unsigned int m_rfErrs; unsigned int m_rfBits; CM17LICH m_rfLICH; - unsigned char m_rfMask; CM17LICH m_netLICH; CRSSIInterpolator* m_rssiMapper; unsigned char m_rssi; diff --git a/M17LICH.h b/M17LICH.h index c16a58f..4c4346c 100644 --- a/M17LICH.h +++ b/M17LICH.h @@ -34,6 +34,7 @@ public: std::string getDest() const; unsigned char getDataType() const; + void reset(); bool isValid() const; void getLinkSetup(unsigned char* data) const; diff --git a/M17Network.cpp b/M17Network.cpp index 6e4d66e..28314cd 100644 --- a/M17Network.cpp +++ b/M17Network.cpp @@ -174,11 +174,6 @@ void CM17Network::clock(unsigned int ms) return; } - if (::memcmp(buffer + 0U, "PING", 4U) == 0) { - sendPong(); - return; - } - if (::memcmp(buffer + 0U, "DISC", 4U) == 0) { m_timer.stop(); m_state = M17N_NOTLINKED; @@ -186,6 +181,12 @@ void CM17Network::clock(unsigned int ms) return; } + if (::memcmp(buffer + 0U, "PING", 4U) == 0) { + if (m_state == M17N_LINKED) + sendPong(); + return; + } + if (!m_enabled) return; diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 6d8880a..b30263f 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -1551,7 +1551,7 @@ bool CMMDVMHost::createM17Network() LogInfo(" Local Port: %u", localPort); LogInfo(" Mode Hang: %us", m_m17NetModeHang); - m_m17Network = new CM17Network(localPort, gatewayAddress, gatewayPort, debug); + m_m17Network = new CM17Network(localPort, debug); bool ret = m_m17Network->open(); if (!ret) { delete m_m17Network; @@ -1559,6 +1559,8 @@ bool CMMDVMHost::createM17Network() return false; } + m_m17Network->link(gatewayAddress, gatewayPort, "M17-USA", 'A'); + m_m17Network->enable(true); return true; diff --git a/Sync.cpp b/Sync.cpp index 7144683..34dec37 100644 --- a/Sync.cpp +++ b/Sync.cpp @@ -89,5 +89,5 @@ void CSync::addM17Sync(unsigned char* data) { assert(data != NULL); - ::memcpy(data, M17_SYNC_BYTES, M17_SYNC_BYTES_LENGTH); + ::memcpy(data, M17_SYNC_BYTES, M17_SYNC_LENGTH_BYTES); } From 48f95be982f30dee942179b454c4002943cedae1 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 17 Oct 2020 21:59:27 +0100 Subject: [PATCH 123/163] Mostly complete processing of RF frames. --- M17Control.cpp | 93 ++++++++++++++++++++++++++++++++++++++++++++------ M17Control.h | 4 ++- M17Defines.h | 7 ++-- M17LICH.h | 5 +++ M17Network.cpp | 32 +++++++++-------- M17Network.h | 6 ++-- MMDVM.ini | 2 +- MMDVMHost.cpp | 4 +-- 8 files changed, 118 insertions(+), 35 deletions(-) diff --git a/M17Control.cpp b/M17Control.cpp index 1169a5c..39b83d3 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -72,7 +72,7 @@ m_networkWatchdog(1000U, 0U, 1500U), m_elapsed(), m_rfFrames(0U), m_netFrames(0U), -m_rfLastFN(0U), +m_rfFN(0U), m_rfErrs(0U), m_rfBits(1U), m_rfLICH(), @@ -179,7 +179,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) m_maxRSSI = m_rssi; m_aveRSSI = m_rssi; m_rssiCount = 1U; - m_rfLastFN = 0U; + m_rfFN = 0U; #if defined(DUMP_M17) openFile(); @@ -259,7 +259,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); if (valid) { - m_rfLastFN = (frame[0U] << 8) + (frame[1U] << 0); + m_rfFN = (frame[0U] << 8) + (frame[1U] << 0); unsigned int frag1, frag2, frag3, frag4; @@ -270,7 +270,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) unsigned int lich3 = CGolay24128::decode24128(frag3); unsigned int lich4 = CGolay24128::decode24128(frag4); - m_rfLICH.setFragment(data + 2U + M17_SYNC_LENGTH_BYTES, m_rfLastFN & 0x7FFFU); + m_rfLICH.setFragment(data + 2U + M17_SYNC_LENGTH_BYTES, m_rfFN & 0x7FFFU); valid = m_rfLICH.isValid(); if (valid) { @@ -343,12 +343,11 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) #if defined(DUMP_M17) writeFile(data + 2U); #endif - CM17Convolution conv; conv.start(); unsigned int n = 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_LENGTH_BYTES; - for (unsigned int i = 0U; i < (M17_LICH_LENGTH_BYTES / 2U); i++) { + for (unsigned int i = 0U; i < ((M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES) / 2U); i++) { uint8_t s0 = data[n++]; uint8_t s1 = data[n++]; @@ -360,19 +359,77 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); if (valid) { - m_rfLastFN = (frame[0U] << 8) + (frame[1U] << 0); - + m_rfFN = (frame[0U] << 8) + (frame[1U] << 0); } else { - m_rfLastFN++; + // Create a silence frame + m_rfFN++; + + // The new FN + frame[0U] = m_rfFN >> 8; + frame[1U] = m_rfFN >> 0; + + // Add silent audio + unsigned char dataType = m_rfLICH.getDataType(); + switch (dataType) { + case 2U: + ::memcpy(frame + M17_FN_LENGTH_BYTES + 0U, M17_3200_SILENCE, 8U); + ::memcpy(frame + M17_FN_LENGTH_BYTES + 8U, M17_3200_SILENCE, 8U); + break; + case 3U: + ::memcpy(frame + M17_FN_LENGTH_BYTES + 0U, M17_1600_SILENCE, 8U); + break; + default: + break; + } + + // Add the CRC + CM17CRC::encodeCRC(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + } + + unsigned char rfData[2U + M17_FRAME_LENGTH_BYTES]; + + rfData[0U] = TAG_DATA; + rfData[1U] = 0x00U; + + CSync::addM17Sync(rfData + 2U); + + // Re-encode the LICH fragment + m_rfLICH.getFragment(rfData + 2U + M17_SYNC_LENGTH_BYTES, m_rfFN & 0x7FFFU); + + // XXX TODO Golay on LICH fragment + + // Re-encode the payload + conv.encode(frame, rfData + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_LENGTH_BYTES, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + + // Calculate the BER + if (valid) { + for (unsigned int i = 2U; i < 50U; i++) + m_rfErrs += countBits(rfData[i] ^ data[i]); + + m_rfBits += 272U; + + float ber = float(m_rfErrs) / float(m_rfBits); + m_display->writeM17BER(ber); } if (m_duplex) - writeQueueRF(data); + writeQueueRF(rfData); + + unsigned char netData[M17_LICH_LENGTH_BYTES + M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; + + m_rfLICH.getNetworkData(netData + 0U); + + // Copy the FN and payload from the frame + ::memcpy(netData + M17_LICH_LENGTH_BYTES, frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES); + + CM17CRC::encodeCRC(netData, M17_LICH_LENGTH_BITS + M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + + writeNetwork(netData); m_rfFrames++; // EOT? - if ((m_rfLastFN & 0x8000U) == 0x8000U) { + if ((m_rfFN & 0x8000U) == 0x8000U) { std::string source = m_rfLICH.getSource(); std::string dest = m_rfLICH.getDest(); @@ -737,3 +794,17 @@ void CM17Control::enable(bool enabled) m_enabled = enabled; } + +unsigned int CM17Control::countBits(unsigned char byte) +{ + unsigned int count = 0U; + + const unsigned char* p = &byte; + + for (unsigned int i = 0U; i < 8U; i++) { + if (READ_BIT(p, i) != 0U) + count++; + } + + return count; +} diff --git a/M17Control.h b/M17Control.h index ed39f3a..1c2a271 100644 --- a/M17Control.h +++ b/M17Control.h @@ -63,7 +63,7 @@ private: CStopWatch m_elapsed; unsigned int m_rfFrames; unsigned int m_netFrames; - unsigned int m_rfLastFN; + unsigned int m_rfFN; unsigned int m_rfErrs; unsigned int m_rfBits; CM17LICH m_rfLICH; @@ -85,6 +85,8 @@ private: void interleaver(const unsigned char* in, unsigned char* out) const; void decorrelator(const unsigned char* in, unsigned char* out) const; + unsigned int countBits(unsigned char byte); + void writeEndRF(); void writeEndNet(); diff --git a/M17Defines.h b/M17Defines.h index 9e061e8..201f049 100644 --- a/M17Defines.h +++ b/M17Defines.h @@ -28,8 +28,8 @@ const unsigned char M17_SYNC_BYTES[] = {0x32U, 0x43U}; const unsigned int M17_SYNC_LENGTH_BITS = 16U; const unsigned int M17_SYNC_LENGTH_BYTES = M17_SYNC_LENGTH_BITS / 8U; -const unsigned int M17_LICH_LENGTH_BITS = 244U; -const unsigned int M17_LICH_LENGTH_BYTES = 31U; +const unsigned int M17_LICH_LENGTH_BITS = 224U; +const unsigned int M17_LICH_LENGTH_BYTES = M17_LICH_LENGTH_BITS / 8U; const unsigned int M17_LICH_FRAGMENT_LENGTH_BITS = 96U; const unsigned int M17_LICH_FRAGMENT_LENGTH_BYTES = M17_LICH_FRAGMENT_LENGTH_BITS / 8U; @@ -43,4 +43,7 @@ const unsigned int M17_FN_LENGTH_BYTES = M17_FN_LENGTH_BITS / 8U; const unsigned int M17_CRC_LENGTH_BITS = 16U; const unsigned int M17_CRC_LENGTH_BYTES = M17_CRC_LENGTH_BITS / 8U; +const unsigned char M17_3200_SILENCE[] = {0x01U, 0x00U, 0x09U, 0x43U, 0x9CU, 0xE4U, 0x21U, 0x08U}; +const unsigned char M17_1600_SILENCE[] = {0x01U, 0x00U, 0x04U, 0x00U, 0x25U, 0x75U, 0xDDU, 0xF2U}; + #endif diff --git a/M17LICH.h b/M17LICH.h index 4c4346c..4ab48d0 100644 --- a/M17LICH.h +++ b/M17LICH.h @@ -31,8 +31,13 @@ public: void setNetworkData(const unsigned char* data); std::string getSource() const; + void setSource(const std::string& callsign); + std::string getDest() const; + void setDest(const std::string& callsign); + unsigned char getDataType() const; + void setDataType(unsigned char type); void reset(); bool isValid() const; diff --git a/M17Network.cpp b/M17Network.cpp index 28314cd..8c8d251 100644 --- a/M17Network.cpp +++ b/M17Network.cpp @@ -29,7 +29,8 @@ const unsigned int BUFFER_LENGTH = 200U; -CM17Network::CM17Network(unsigned int port, bool debug) : +CM17Network::CM17Network(const std::string& callsign, unsigned int port, bool debug) : +m_callsign(callsign), m_socket(port), m_addr(), m_addrLen(0U), @@ -40,7 +41,7 @@ m_inId(0U), m_buffer(1000U, "M17 Network"), m_random(), m_state(M17N_NOTLINKED), -m_reflector(NULL), +m_encoded(NULL), m_module(' '), m_timer(1000U, 5U) { @@ -49,6 +50,8 @@ m_timer(1000U, 5U) m_random = mt; m_encoded = new unsigned char[6U]; + + CM17Utils::encodeCallsign(m_callsign, m_encoded); } CM17Network::~CM17Network() @@ -63,17 +66,14 @@ bool CM17Network::open() return m_socket.open(m_addr); } -bool CM17Network::link(const std::string& address, unsigned int port, const std::string& reflector, char module) +bool CM17Network::link(const std::string& address, unsigned int port, char module) { if (CUDPSocket::lookup(address, port, m_addr, m_addrLen) != 0) { m_state = M17N_NOTLINKED; return false; } - m_reflector = reflector; - m_module = module; - - CM17Utils::encodeCallsign(m_reflector, m_encoded); + m_module = module; m_state = M17N_LINKING; @@ -119,10 +119,12 @@ bool CM17Network::write(const unsigned char* data) buffer[4U] = m_outId / 256U; // Unique session id buffer[5U] = m_outId % 256U; - if (m_debug) - CUtils::dump(1U, "M17 data transmitted", buffer, 36U); + ::memcpy(buffer + 6U, data, 48U); - return m_socket.write(buffer, 36U, m_addr, m_addrLen); + if (m_debug) + CUtils::dump(1U, "M17 data transmitted", buffer, 54U); + + return m_socket.write(buffer, 54U, m_addr, m_addrLen); } void CM17Network::clock(unsigned int ms) @@ -163,21 +165,21 @@ void CM17Network::clock(unsigned int ms) if (::memcmp(buffer + 0U, "ACKN", 4U) == 0) { m_timer.stop(); m_state = M17N_LINKED; - LogMessage("M17, linked to %s", m_reflector.c_str()); + LogMessage("M17, linked to reflector"); return; } if (::memcmp(buffer + 0U, "NACK", 4U) == 0) { m_timer.stop(); m_state = M17N_NOTLINKED; - LogMessage("M17, link refused by %s", m_reflector.c_str()); + LogMessage("M17, link refused by reflector"); return; } if (::memcmp(buffer + 0U, "DISC", 4U) == 0) { m_timer.stop(); m_state = M17N_NOTLINKED; - LogMessage("M17, unlinked from %s", m_reflector.c_str()); + LogMessage("M17, unlinked from reflector"); return; } @@ -208,10 +210,10 @@ void CM17Network::clock(unsigned int ms) if ((fn & 0x8000U) == 0x8000U) m_inId = 0U; - unsigned char c = length; + unsigned char c = length - 6U; m_buffer.addData(&c, 1U); - m_buffer.addData(buffer, length); + m_buffer.addData(buffer + 6U, length - 6U); } bool CM17Network::read(unsigned char* data) diff --git a/M17Network.h b/M17Network.h index b1d3710..ff357cf 100644 --- a/M17Network.h +++ b/M17Network.h @@ -36,12 +36,12 @@ enum M17NET_STATUS { class CM17Network { public: - CM17Network(unsigned int port, bool debug); + CM17Network(const std::string& callsign, unsigned int port, bool debug); ~CM17Network(); bool open(); - bool link(const std::string& address, unsigned int port, const std::string& reflector, char module); + bool link(const std::string& address, unsigned int port, char module); void unlink(); @@ -58,6 +58,7 @@ public: void clock(unsigned int ms); private: + std::string m_callsign; CUDPSocket m_socket; sockaddr_storage m_addr; unsigned int m_addrLen; @@ -68,7 +69,6 @@ private: CRingBuffer m_buffer; std::mt19937 m_random; M17NET_STATUS m_state; - std::string m_reflector; unsigned char* m_encoded; char m_module; CTimer m_timer; diff --git a/MMDVM.ini b/MMDVM.ini index 83eb5fb..7075501 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -224,7 +224,7 @@ Debug=0 [M17 Network] Enable=1 -GatewayAddress=127.0.0.1 +GatewayAddress=3.138.122.152 GatewayPort=17000 LocalPort=17000 # ModeHang=3 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index b30263f..b665905 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -1551,7 +1551,7 @@ bool CMMDVMHost::createM17Network() LogInfo(" Local Port: %u", localPort); LogInfo(" Mode Hang: %us", m_m17NetModeHang); - m_m17Network = new CM17Network(localPort, debug); + m_m17Network = new CM17Network(m_callsign, localPort, debug); bool ret = m_m17Network->open(); if (!ret) { delete m_m17Network; @@ -1559,7 +1559,7 @@ bool CMMDVMHost::createM17Network() return false; } - m_m17Network->link(gatewayAddress, gatewayPort, "M17-USA", 'A'); + m_m17Network->link(gatewayAddress, gatewayPort, 'A'); m_m17Network->enable(true); From 521da9b54d9ab8832841e1502df5e69a798de8c2 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 18 Oct 2020 14:51:52 +0100 Subject: [PATCH 124/163] Add the CRC and callsign encoding. --- M17CRC.cpp | 69 ++++++++++++++++++++++++++++++++ M17CRC.h | 6 +-- M17Control.cpp | 20 +++++----- M17Network.cpp | 13 +++++- M17Utils.cpp | 83 +++++++++++++++++++++++++++++++++++++++ M17Utils.h | 4 +- MMDVMHost.vcxproj | 2 + MMDVMHost.vcxproj.filters | 6 +++ 8 files changed, 187 insertions(+), 16 deletions(-) create mode 100644 M17CRC.cpp create mode 100644 M17Utils.cpp diff --git a/M17CRC.cpp b/M17CRC.cpp new file mode 100644 index 0000000..933f86f --- /dev/null +++ b/M17CRC.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018,2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "M17CRC.h" + +#include +#include + +const uint8_t BIT_MASK_TABLE1[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U }; + +#define READ_BIT(p,i) (p[(i)>>3] & BIT_MASK_TABLE1[(i)&7]) + +bool CM17CRC::checkCRC(const unsigned char* in, unsigned int nBytes) +{ + assert(in != NULL); + assert(nBytes > 2U); + + uint16_t crc = createCRC(in, nBytes); + + uint8_t temp[2U]; + temp[0U] = (crc >> 8) & 0xFFU; + temp[1U] = (crc >> 0) & 0xFFU; + + return temp[0U] == in[nBytes - 2U] && temp[1U] == in[nBytes - 1U]; +} + +void CM17CRC::encodeCRC(unsigned char* in, unsigned int nBytes) +{ + assert(in != NULL); + assert(nBytes > 2U); + + uint16_t crc = createCRC(in, nBytes); + + in[nBytes - 2U] = (crc >> 8) & 0xFFU; + in[nBytes - 1U] = (crc >> 0) & 0xFFU; +} +uint16_t CM17CRC::createCRC(const unsigned char* in, unsigned int nBytes) +{ + assert(in != NULL); + + uint16_t crc = 0xFFFFU; + + for (unsigned int i = 0U; i < nBytes; i++) { + bool bit1 = READ_BIT(in, i) != 0x00U; + bool bit2 = (crc & 0x8000U) == 0x8000U; + + crc <<= 1; + + if (bit1 ^ bit2) + crc ^= 0x5935U; + } + + return crc; +} diff --git a/M17CRC.h b/M17CRC.h index af0f3d0..22c8f31 100644 --- a/M17CRC.h +++ b/M17CRC.h @@ -24,11 +24,11 @@ class CM17CRC { public: - static bool checkCRC(const unsigned char* in, unsigned int length); - static void encodeCRC(unsigned char* in, unsigned int length); + static bool checkCRC(const unsigned char* in, unsigned int nBytes); + static void encodeCRC(unsigned char* in, unsigned int nBytes); private: - static uint16_t createCRC(const unsigned char* in, unsigned int length); + static uint16_t createCRC(const unsigned char* in, unsigned int nBytes); }; #endif diff --git a/M17Control.cpp b/M17Control.cpp index 39b83d3..3086c1b 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -169,7 +169,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) unsigned char frame[M17_LICH_LENGTH_BYTES]; conv.chainback(frame, M17_LICH_LENGTH_BITS); - bool valid = CM17CRC::checkCRC(frame, M17_LICH_LENGTH_BITS); + bool valid = CM17CRC::checkCRC(frame, M17_LICH_LENGTH_BYTES); if (valid) { m_rfFrames = 0U; m_rfErrs = 0U; @@ -225,7 +225,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) m_rfLICH.getLinkSetup(data + 2U); // Add the CRC - CM17CRC::encodeCRC(data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); + CM17CRC::encodeCRC(data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); // Add the convolution FEC CM17Convolution conv; @@ -257,7 +257,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; conv.chainback(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); - bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); if (valid) { m_rfFN = (frame[0U] << 8) + (frame[1U] << 0); @@ -323,7 +323,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) m_rfLICH.getLinkSetup(data + 2U); // Add the CRC - CM17CRC::encodeCRC(data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); + CM17CRC::encodeCRC(data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); // Add the convolution FEC CM17Convolution conv; @@ -357,7 +357,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; conv.chainback(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); - bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); if (valid) { m_rfFN = (frame[0U] << 8) + (frame[1U] << 0); } else { @@ -383,7 +383,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) } // Add the CRC - CM17CRC::encodeCRC(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + CM17CRC::encodeCRC(frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); } unsigned char rfData[2U + M17_FRAME_LENGTH_BYTES]; @@ -422,7 +422,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) // Copy the FN and payload from the frame ::memcpy(netData + M17_LICH_LENGTH_BYTES, frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES); - CM17CRC::encodeCRC(netData, M17_LICH_LENGTH_BITS + M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + // The CRC is added in the networking code writeNetwork(netData); @@ -552,7 +552,7 @@ void CM17Control::writeNetwork() m_netLICH.getLinkSetup(start + 2U); // Add the CRC - CM17CRC::encodeCRC(start + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); + CM17CRC::encodeCRC(start + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); // Add the convolution FEC CM17Convolution conv; @@ -612,7 +612,7 @@ void CM17Control::writeNetwork() ::memcpy(payload, netData + 38U, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES); // Add the CRC - CM17CRC::encodeCRC(payload, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + CM17CRC::encodeCRC(payload, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); // Add the Convolution FEC CM17Convolution conv; @@ -726,7 +726,7 @@ void CM17Control::decorrelator(const unsigned char* in, unsigned char* out) cons assert(out != NULL); for (unsigned int i = M17_SYNC_LENGTH_BYTES; i < M17_FRAME_LENGTH_BYTES; i++) - out[i] = in[i] ^ SCRAMBLER[i]; + out[i] = in[i] ^ SCRAMBLER[i - M17_SYNC_LENGTH_BYTES]; } bool CM17Control::openFile() diff --git a/M17Network.cpp b/M17Network.cpp index 8c8d251..7a3c6ff 100644 --- a/M17Network.cpp +++ b/M17Network.cpp @@ -20,6 +20,7 @@ #include "M17Defines.h" #include "M17Utils.h" #include "Defines.h" +#include "M17CRC.h" #include "Utils.h" #include "Log.h" @@ -119,7 +120,10 @@ bool CM17Network::write(const unsigned char* data) buffer[4U] = m_outId / 256U; // Unique session id buffer[5U] = m_outId % 256U; - ::memcpy(buffer + 6U, data, 48U); + ::memcpy(buffer + 6U, data, 46U); + + // Add the CRC + CM17CRC::encodeCRC(buffer + 6U, M17_LICH_LENGTH_BYTES + M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); if (m_debug) CUtils::dump(1U, "M17 data transmitted", buffer, 54U); @@ -205,6 +209,13 @@ void CM17Network::clock(unsigned int ms) return; } + // Check the CRC + bool valid = CM17CRC::checkCRC(buffer + 6U, M17_LICH_LENGTH_BYTES + M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); + if (!valid) { + LogMessage("M17, network packet received with an invalid CRC"); + return; + } + // EOT? uint16_t fn = (buffer[38U] << 8) + (buffer[39U] << 0); if ((fn & 0x8000U) == 0x8000U) diff --git a/M17Utils.cpp b/M17Utils.cpp new file mode 100644 index 0000000..406ebdb --- /dev/null +++ b/M17Utils.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "M17Utils.h" + +#include + +void CM17Utils::encodeCallsign(const std::string& callsign, unsigned char* encoded) +{ + assert(encoded != NULL); + + std::string call = callsign; + call.resize(8U, ' '); + + uint64_t enc = 0ULL; + for (int i = 7U; i >= 0; i--) { + char c = call.at(i); + + enc *= 40ULL; + + // If speed is more important than code space, you can replace this with a lookup into a 256 byte array. + if (c >= 'A' && c <= 'Z') // 1-26 + enc += c - 'A' + 1ULL; + else if (c >= '0' && c <= '9') // 27-36 + enc += c - '0' + 27ULL; + else if (c == '-') // 37 + enc += 37ULL; + + // These are just place holders. If other characters make more sense, change these. + // Be sure to change them in the decode array below too. + else if (c == '/') // 38 + enc += 38ULL; + else if (c == '.') // 39 + enc += 39ULL; + else + // Invalid character or space, represented by 0, decoded as a space. + enc += 0ULL; + } + + encoded[0U] = enc >> 40; + encoded[1U] = enc >> 32; + encoded[2U] = enc >> 24; + encoded[3U] = enc >> 16; + encoded[4U] = enc >> 8; + encoded[5U] = enc >> 0; +} + +void CM17Utils::decodeCallsign(const unsigned char* encoded, std::string& callsign) +{ + assert(encoded != NULL); + + callsign.empty(); + + uint64_t enc = (uint64_t(encoded[5U]) << 40) + + (uint64_t(encoded[4U]) << 32) + + (uint64_t(encoded[3U]) << 24) + + (uint64_t(encoded[2U]) << 16) + + (uint64_t(encoded[1U]) << 8) + + (uint64_t(encoded[0U]) << 0); + + if (enc >= 262144000000000ULL) // 40^9 + return; + + while (enc > 0ULL) { + callsign += " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-/."[enc % 40ULL]; + enc /= 40ULL; + } +} diff --git a/M17Utils.h b/M17Utils.h index 025e65a..c9a18df 100644 --- a/M17Utils.h +++ b/M17Utils.h @@ -26,9 +26,9 @@ public: CM17Utils(); ~CM17Utils(); - static void encodeCallsign(const std::string& callsign, unsigned char* data); + static void encodeCallsign(const std::string& callsign, unsigned char* encoded); - static void decodeCallsign(const unsigned char* data, std::string& callsign); + static void decodeCallsign(const unsigned char* encoded, std::string& callsign); private: }; diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index a399c3a..e1a78c7 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -291,6 +291,7 @@ + @@ -330,6 +331,7 @@ + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index 8f9a797..c1bd903 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -589,5 +589,11 @@ Source Files + + Source Files + + + Source Files + \ No newline at end of file From a00ecbe7d9bbaa6d9121631a20b7e2c22026ad69 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 18 Oct 2020 20:04:44 +0100 Subject: [PATCH 125/163] Get the CRC and callsign mangling working correctly. --- M17CRC.cpp | 44 +++++++++++++++++++---------- M17Utils.cpp | 79 ++++++++++++++++++++++------------------------------ 2 files changed, 64 insertions(+), 59 deletions(-) diff --git a/M17CRC.cpp b/M17CRC.cpp index 933f86f..7ba4da6 100644 --- a/M17CRC.cpp +++ b/M17CRC.cpp @@ -21,16 +21,37 @@ #include #include -const uint8_t BIT_MASK_TABLE1[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U }; - -#define READ_BIT(p,i) (p[(i)>>3] & BIT_MASK_TABLE1[(i)&7]) +const uint16_t CRC_TABLE[] = {0x0000U, 0x5935U, 0xB26AU, 0xEB5FU, 0x3DE1U, 0x64D4U, 0x8F8BU, 0xD6BEU, 0x7BC2U, 0x22F7U, 0xC9A8U, + 0x909DU, 0x4623U, 0x1F16U, 0xF449U, 0xAD7CU, 0xF784U, 0xAEB1U, 0x45EEU, 0x1CDBU, 0xCA65U, 0x9350U, + 0x780FU, 0x213AU, 0x8C46U, 0xD573U, 0x3E2CU, 0x6719U, 0xB1A7U, 0xE892U, 0x03CDU, 0x5AF8U, 0xB63DU, + 0xEF08U, 0x0457U, 0x5D62U, 0x8BDCU, 0xD2E9U, 0x39B6U, 0x6083U, 0xCDFFU, 0x94CAU, 0x7F95U, 0x26A0U, + 0xF01EU, 0xA92BU, 0x4274U, 0x1B41U, 0x41B9U, 0x188CU, 0xF3D3U, 0xAAE6U, 0x7C58U, 0x256DU, 0xCE32U, + 0x9707U, 0x3A7BU, 0x634EU, 0x8811U, 0xD124U, 0x079AU, 0x5EAFU, 0xB5F0U, 0xECC5U, 0x354FU, 0x6C7AU, + 0x8725U, 0xDE10U, 0x08AEU, 0x519BU, 0xBAC4U, 0xE3F1U, 0x4E8DU, 0x17B8U, 0xFCE7U, 0xA5D2U, 0x736CU, + 0x2A59U, 0xC106U, 0x9833U, 0xC2CBU, 0x9BFEU, 0x70A1U, 0x2994U, 0xFF2AU, 0xA61FU, 0x4D40U, 0x1475U, + 0xB909U, 0xE03CU, 0x0B63U, 0x5256U, 0x84E8U, 0xDDDDU, 0x3682U, 0x6FB7U, 0x8372U, 0xDA47U, 0x3118U, + 0x682DU, 0xBE93U, 0xE7A6U, 0x0CF9U, 0x55CCU, 0xF8B0U, 0xA185U, 0x4ADAU, 0x13EFU, 0xC551U, 0x9C64U, + 0x773BU, 0x2E0EU, 0x74F6U, 0x2DC3U, 0xC69CU, 0x9FA9U, 0x4917U, 0x1022U, 0xFB7DU, 0xA248U, 0x0F34U, + 0x5601U, 0xBD5EU, 0xE46BU, 0x32D5U, 0x6BE0U, 0x80BFU, 0xD98AU, 0x6A9EU, 0x33ABU, 0xD8F4U, 0x81C1U, + 0x577FU, 0x0E4AU, 0xE515U, 0xBC20U, 0x115CU, 0x4869U, 0xA336U, 0xFA03U, 0x2CBDU, 0x7588U, 0x9ED7U, + 0xC7E2U, 0x9D1AU, 0xC42FU, 0x2F70U, 0x7645U, 0xA0FBU, 0xF9CEU, 0x1291U, 0x4BA4U, 0xE6D8U, 0xBFEDU, + 0x54B2U, 0x0D87U, 0xDB39U, 0x820CU, 0x6953U, 0x3066U, 0xDCA3U, 0x8596U, 0x6EC9U, 0x37FCU, 0xE142U, + 0xB877U, 0x5328U, 0x0A1DU, 0xA761U, 0xFE54U, 0x150BU, 0x4C3EU, 0x9A80U, 0xC3B5U, 0x28EAU, 0x71DFU, + 0x2B27U, 0x7212U, 0x994DU, 0xC078U, 0x16C6U, 0x4FF3U, 0xA4ACU, 0xFD99U, 0x50E5U, 0x09D0U, 0xE28FU, + 0xBBBAU, 0x6D04U, 0x3431U, 0xDF6EU, 0x865BU, 0x5FD1U, 0x06E4U, 0xEDBBU, 0xB48EU, 0x6230U, 0x3B05U, + 0xD05AU, 0x896FU, 0x2413U, 0x7D26U, 0x9679U, 0xCF4CU, 0x19F2U, 0x40C7U, 0xAB98U, 0xF2ADU, 0xA855U, + 0xF160U, 0x1A3FU, 0x430AU, 0x95B4U, 0xCC81U, 0x27DEU, 0x7EEBU, 0xD397U, 0x8AA2U, 0x61FDU, 0x38C8U, + 0xEE76U, 0xB743U, 0x5C1CU, 0x0529U, 0xE9ECU, 0xB0D9U, 0x5B86U, 0x02B3U, 0xD40DU, 0x8D38U, 0x6667U, + 0x3F52U, 0x922EU, 0xCB1BU, 0x2044U, 0x7971U, 0xAFCFU, 0xF6FAU, 0x1DA5U, 0x4490U, 0x1E68U, 0x475DU, + 0xAC02U, 0xF537U, 0x2389U, 0x7ABCU, 0x91E3U, 0xC8D6U, 0x65AAU, 0x3C9FU, 0xD7C0U, 0x8EF5U, 0x584BU, + 0x017EU, 0xEA21U, 0xB314U}; bool CM17CRC::checkCRC(const unsigned char* in, unsigned int nBytes) { assert(in != NULL); assert(nBytes > 2U); - uint16_t crc = createCRC(in, nBytes); + uint16_t crc = createCRC(in, nBytes - 2U); uint8_t temp[2U]; temp[0U] = (crc >> 8) & 0xFFU; @@ -44,26 +65,21 @@ void CM17CRC::encodeCRC(unsigned char* in, unsigned int nBytes) assert(in != NULL); assert(nBytes > 2U); - uint16_t crc = createCRC(in, nBytes); + uint16_t crc = createCRC(in, nBytes - 2U); in[nBytes - 2U] = (crc >> 8) & 0xFFU; in[nBytes - 1U] = (crc >> 0) & 0xFFU; } + uint16_t CM17CRC::createCRC(const unsigned char* in, unsigned int nBytes) { assert(in != NULL); uint16_t crc = 0xFFFFU; - for (unsigned int i = 0U; i < nBytes; i++) { - bool bit1 = READ_BIT(in, i) != 0x00U; - bool bit2 = (crc & 0x8000U) == 0x8000U; - - crc <<= 1; - - if (bit1 ^ bit2) - crc ^= 0x5935U; - } + for (unsigned int i = 0U; i < nBytes; i++) + crc = (crc << 8) ^ CRC_TABLE[((crc >> 8) ^ uint16_t(in[i])) & 0x00FFU]; return crc; } + diff --git a/M17Utils.cpp b/M17Utils.cpp index 406ebdb..b3d0056 100644 --- a/M17Utils.cpp +++ b/M17Utils.cpp @@ -20,64 +20,53 @@ #include +const std::string M17_CHARS = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-/."; + void CM17Utils::encodeCallsign(const std::string& callsign, unsigned char* encoded) { assert(encoded != NULL); - std::string call = callsign; - call.resize(8U, ' '); + unsigned int len = callsign.size(); + if (len > 9U) + len = 9U; - uint64_t enc = 0ULL; - for (int i = 7U; i >= 0; i--) { - char c = call.at(i); + uint64_t enc = 0ULL; + for (int i = len - 1; i >= 0; i--) { + size_t pos = M17_CHARS.find(callsign[i]); + if (pos == std::string::npos) + pos = 0ULL; - enc *= 40ULL; + enc *= 40ULL; + enc += pos; + } - // If speed is more important than code space, you can replace this with a lookup into a 256 byte array. - if (c >= 'A' && c <= 'Z') // 1-26 - enc += c - 'A' + 1ULL; - else if (c >= '0' && c <= '9') // 27-36 - enc += c - '0' + 27ULL; - else if (c == '-') // 37 - enc += 37ULL; - - // These are just place holders. If other characters make more sense, change these. - // Be sure to change them in the decode array below too. - else if (c == '/') // 38 - enc += 38ULL; - else if (c == '.') // 39 - enc += 39ULL; - else - // Invalid character or space, represented by 0, decoded as a space. - enc += 0ULL; - } - - encoded[0U] = enc >> 40; - encoded[1U] = enc >> 32; - encoded[2U] = enc >> 24; - encoded[3U] = enc >> 16; - encoded[4U] = enc >> 8; - encoded[5U] = enc >> 0; + encoded[0U] = enc >> 40; + encoded[1U] = enc >> 32; + encoded[2U] = enc >> 24; + encoded[3U] = enc >> 16; + encoded[4U] = enc >> 8; + encoded[5U] = enc >> 0; } void CM17Utils::decodeCallsign(const unsigned char* encoded, std::string& callsign) { - assert(encoded != NULL); + assert(encoded != NULL); - callsign.empty(); + callsign.empty(); - uint64_t enc = (uint64_t(encoded[5U]) << 40) + - (uint64_t(encoded[4U]) << 32) + - (uint64_t(encoded[3U]) << 24) + - (uint64_t(encoded[2U]) << 16) + - (uint64_t(encoded[1U]) << 8) + - (uint64_t(encoded[0U]) << 0); + uint64_t enc = (uint64_t(encoded[0U]) << 40) + + (uint64_t(encoded[1U]) << 32) + + (uint64_t(encoded[2U]) << 24) + + (uint64_t(encoded[3U]) << 16) + + (uint64_t(encoded[4U]) << 8) + + (uint64_t(encoded[5U]) << 0); - if (enc >= 262144000000000ULL) // 40^9 - return; + if (enc >= 262144000000000ULL) // 40^9 + return; - while (enc > 0ULL) { - callsign += " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-/."[enc % 40ULL]; - enc /= 40ULL; - } + while (enc > 0ULL) { + callsign += " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-/."[enc % 40ULL]; + enc /= 40ULL; + } } + From c549cf3594d2e1b5676b1229f7c874f2d3baf3b2 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 18 Oct 2020 20:44:03 +0100 Subject: [PATCH 126/163] Add the LICH processing. --- M17Control.cpp | 15 ++-- M17LICH.cpp | 160 ++++++++++++++++++++++++++++++++++++++ M17LICH.h | 9 ++- M17Network.cpp | 7 +- M17Network.h | 1 - MMDVMHost.vcxproj | 1 + MMDVMHost.vcxproj.filters | 3 + 7 files changed, 182 insertions(+), 14 deletions(-) create mode 100644 M17LICH.cpp diff --git a/M17Control.cpp b/M17Control.cpp index 3086c1b..754ccc0 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -45,9 +45,9 @@ const unsigned int INTERLEAVER[] = { 8U, 145U, 98U, 235U, 188U, 325U, 278U, 47U}; const unsigned char SCRAMBLER[] = { - 0xD6U, 0xB5U, 0xE2U, 0x30U, 0x82U, 0xFFU, 0x84U, 0x62U, 0xBAU, 0x4EU, 0x96U, 0x90U, 0xD8U, 0x98U, 0xDDU, 0x5DU, 0x0CU, - 0xC8U, 0x52U, 0x43U, 0x91U, 0x1DU, 0xF8U, 0x6EU, 0x68U, 0x2FU, 0x35U, 0xDAU, 0x14U, 0xEAU, 0xCDU, 0x76U, 0x19U, 0x8DU, - 0xD5U, 0x80U, 0xD1U, 0x33U, 0x87U, 0x13U, 0x57U, 0x18U, 0x2DU, 0x29U, 0x78U, 0xC3U}; + 0x00U, 0x00U, 0xD6U, 0xB5U, 0xE2U, 0x30U, 0x82U, 0xFFU, 0x84U, 0x62U, 0xBAU, 0x4EU, 0x96U, 0x90U, 0xD8U, 0x98U, 0xDDU, + 0x5DU, 0x0CU, 0xC8U, 0x52U, 0x43U, 0x91U, 0x1DU, 0xF8U, 0x6EU, 0x68U, 0x2FU, 0x35U, 0xDAU, 0x14U, 0xEAU, 0xCDU, 0x76U, + 0x19U, 0x8DU, 0xD5U, 0x80U, 0xD1U, 0x33U, 0x87U, 0x13U, 0x57U, 0x18U, 0x2DU, 0x29U, 0x78U, 0xC3U}; // #define DUMP_M17 @@ -417,7 +417,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) unsigned char netData[M17_LICH_LENGTH_BYTES + M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; - m_rfLICH.getNetworkData(netData + 0U); + m_rfLICH.getNetwork(netData + 0U); // Copy the FN and payload from the frame ::memcpy(netData + M17_LICH_LENGTH_BYTES, frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES); @@ -507,7 +507,7 @@ void CM17Control::writeNetwork() m_networkWatchdog.start(); if (m_netState == RS_NET_IDLE) { - m_netLICH.setNetworkData(netData); + m_netLICH.setNetwork(netData); std::string source = m_netLICH.getSource(); std::string dest = m_netLICH.getDest(); @@ -725,8 +725,9 @@ void CM17Control::decorrelator(const unsigned char* in, unsigned char* out) cons assert(in != NULL); assert(out != NULL); - for (unsigned int i = M17_SYNC_LENGTH_BYTES; i < M17_FRAME_LENGTH_BYTES; i++) - out[i] = in[i] ^ SCRAMBLER[i - M17_SYNC_LENGTH_BYTES]; + for (unsigned int i = M17_SYNC_LENGTH_BYTES; i < M17_FRAME_LENGTH_BYTES; i++) { + out[i] = in[i] ^ SCRAMBLER[i]; + } } bool CM17Control::openFile() diff --git a/M17LICH.cpp b/M17LICH.cpp new file mode 100644 index 0000000..228f096 --- /dev/null +++ b/M17LICH.cpp @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "M17LICH.h" +#include "M17Utils.h" +#include "M17CRC.h" + +#include + +CM17LICH::CM17LICH(const CM17LICH& lich) : +m_lich(NULL), +m_valid(false) +{ + m_lich = new unsigned char[30U]; + + ::memcpy(m_lich, lich.m_lich, 30U); + + m_valid = lich.m_valid; +} + +CM17LICH::CM17LICH() : +m_lich(NULL), +m_valid(false) +{ + m_lich = new unsigned char[30U]; +} + +CM17LICH::~CM17LICH() +{ + delete[] m_lich; +} + +void CM17LICH::getNetwork(unsigned char* data) const +{ + assert(data != NULL); + + ::memcpy(data, m_lich, 28U); +} + +void CM17LICH::setNetwork(const unsigned char* data) +{ + assert(data != NULL); + + ::memcpy(m_lich, data, 28U); + + m_valid = true; +} + +std::string CM17LICH::getSource() const +{ + if (!m_valid) + return ""; + + std::string callsign; + CM17Utils::decodeCallsign(m_lich + 6U, callsign); + + return callsign; +} + +void CM17LICH::setSource(const std::string& callsign) +{ + CM17Utils::encodeCallsign(callsign, m_lich + 6U); +} + +std::string CM17LICH::getDest() const +{ + if (!m_valid) + return ""; + + std::string callsign; + CM17Utils::decodeCallsign(m_lich + 0U, callsign); + + return callsign; +} + +void CM17LICH::setDest(const std::string& callsign) +{ + CM17Utils::encodeCallsign(callsign, m_lich + 0U); +} + +unsigned char CM17LICH::getDataType() const +{ + if (!m_valid) + return 0U; + + return (m_lich[12U] >> 1) & 0x03U; +} + +void CM17LICH::setDataType(unsigned char type) +{ + m_lich[12U] &= 0xF9U; + m_lich[12U] |= (type << 1) & 0x06U; +} + +void CM17LICH::reset() +{ + ::memset(m_lich, 0x00U, 30U); + + m_valid = false; +} + +bool CM17LICH::isValid() const +{ + return m_valid; +} + +void CM17LICH::getLinkSetup(unsigned char* data) const +{ + assert(data != NULL); + + ::memcpy(data, m_lich, 30U); + + CM17CRC::encodeCRC(data, 30U); +} + +void CM17LICH::setLinkSetup(const unsigned char* data) +{ + assert(data != NULL); + + ::memcpy(m_lich, data, 30U); + + m_valid = CM17CRC::checkCRC(m_lich, 30U); +} + +void CM17LICH::getFragment(unsigned char* data, unsigned short fn) const +{ + assert(data != NULL); + + CM17CRC::encodeCRC(m_lich, 30U); + + unsigned int n = (fn & 0x7FFFU) % 5U; + + ::memcpy(data, m_lich + (n * 6U), 6U); +} + +void CM17LICH::setFragment(const unsigned char* data, unsigned short fn) +{ + assert(data != NULL); + + unsigned int n = (fn & 0x7FFFU) % 5U; + + ::memcpy(m_lich + (n * 6U), data, 6U); + + m_valid = CM17CRC::checkCRC(m_lich, 30U); +} diff --git a/M17LICH.h b/M17LICH.h index 4ab48d0..b4855f6 100644 --- a/M17LICH.h +++ b/M17LICH.h @@ -27,8 +27,8 @@ public: CM17LICH(); ~CM17LICH(); - void getNetworkData(unsigned char* data); - void setNetworkData(const unsigned char* data); + void getNetwork(unsigned char* data) const; + void setNetwork(const unsigned char* data); std::string getSource() const; void setSource(const std::string& callsign); @@ -43,15 +43,16 @@ public: bool isValid() const; void getLinkSetup(unsigned char* data) const; - void setLinkSetup(const unsigned char* data) const; + void setLinkSetup(const unsigned char* data); - void getFragment(unsigned char* data, unsigned short fn); + void getFragment(unsigned char* data, unsigned short fn) const; void setFragment(const unsigned char* data, unsigned short fn); CM17LICH& operator=(const CM17LICH& lich); private: unsigned char* m_lich; + bool m_valid; }; #endif diff --git a/M17Network.cpp b/M17Network.cpp index 7a3c6ff..d6b273e 100644 --- a/M17Network.cpp +++ b/M17Network.cpp @@ -31,7 +31,6 @@ const unsigned int BUFFER_LENGTH = 200U; CM17Network::CM17Network(const std::string& callsign, unsigned int port, bool debug) : -m_callsign(callsign), m_socket(port), m_addr(), m_addrLen(0U), @@ -52,7 +51,11 @@ m_timer(1000U, 5U) m_encoded = new unsigned char[6U]; - CM17Utils::encodeCallsign(m_callsign, m_encoded); + std::string call = callsign; + call.resize(8U, ' '); + call += "D"; + + CM17Utils::encodeCallsign(call, m_encoded); } CM17Network::~CM17Network() diff --git a/M17Network.h b/M17Network.h index ff357cf..ed6e81c 100644 --- a/M17Network.h +++ b/M17Network.h @@ -58,7 +58,6 @@ public: void clock(unsigned int ms); private: - std::string m_callsign; CUDPSocket m_socket; sockaddr_storage m_addr; unsigned int m_addrLen; diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index e1a78c7..15665a0 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -292,6 +292,7 @@ + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index c1bd903..d024110 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -595,5 +595,8 @@ Source Files + + Source Files + \ No newline at end of file From 5b57557a79eb4ff55a2a41070b83c56b4d262857 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 18 Oct 2020 22:23:18 +0100 Subject: [PATCH 127/163] Start on the convolution FEC. --- M17Control.cpp | 55 ++------ M17Convolution.cpp | 276 ++++++++++++++++++++++++++++++++++++++ M17Convolution.h | 14 +- M17LICH.cpp | 11 -- M17LICH.h | 3 - MMDVMHost.vcxproj | 1 + MMDVMHost.vcxproj.filters | 3 + 7 files changed, 298 insertions(+), 65 deletions(-) create mode 100644 M17Convolution.cpp diff --git a/M17Control.cpp b/M17Control.cpp index 754ccc0..839c954 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -156,18 +156,8 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) m_rfLICH.reset(); CM17Convolution conv; - conv.start(); - - unsigned int n = 2U + M17_SYNC_LENGTH_BYTES; - for (unsigned int i = 0U; i < (M17_LICH_LENGTH_BYTES / 2U); i++) { - uint8_t s0 = data[n++]; - uint8_t s1 = data[n++]; - - conv.decode(s0, s1); - } - unsigned char frame[M17_LICH_LENGTH_BYTES]; - conv.chainback(frame, M17_LICH_LENGTH_BITS); + conv.decodeLinkSetup(data, frame); bool valid = CM17CRC::checkCRC(frame, M17_LICH_LENGTH_BYTES); if (valid) { @@ -224,12 +214,9 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) unsigned char setup[M17_LICH_LENGTH_BYTES]; m_rfLICH.getLinkSetup(data + 2U); - // Add the CRC - CM17CRC::encodeCRC(data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); - // Add the convolution FEC CM17Convolution conv; - conv.encode(setup, data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); + conv.encodeLinkSetup(setup, data + 2U + M17_SYNC_LENGTH_BYTES); unsigned char temp[M17_FRAME_LENGTH_BYTES]; interleaver(data + 2U, temp); @@ -244,18 +231,8 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) if (m_rfState == RS_RF_LATE_ENTRY) { CM17Convolution conv; - conv.start(); - - unsigned int n = 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_LENGTH_BYTES; - for (unsigned int i = 0U; i < (M17_LICH_LENGTH_BYTES / 2U); i++) { - uint8_t s0 = data[n++]; - uint8_t s1 = data[n++]; - - conv.decode(s0, s1); - } - unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; - conv.chainback(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + conv.decodeData(data, frame); bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); if (valid) { @@ -322,12 +299,9 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) unsigned char setup[M17_LICH_LENGTH_BYTES]; m_rfLICH.getLinkSetup(data + 2U); - // Add the CRC - CM17CRC::encodeCRC(data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); - // Add the convolution FEC CM17Convolution conv; - conv.encode(setup, data + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); + conv.encodeLinkSetup(setup, data + 2U + M17_SYNC_LENGTH_BYTES); unsigned char temp[M17_FRAME_LENGTH_BYTES]; interleaver(data + 2U, temp); @@ -344,18 +318,8 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) writeFile(data + 2U); #endif CM17Convolution conv; - conv.start(); - - unsigned int n = 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_LENGTH_BYTES; - for (unsigned int i = 0U; i < ((M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES) / 2U); i++) { - uint8_t s0 = data[n++]; - uint8_t s1 = data[n++]; - - conv.decode(s0, s1); - } - unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; - conv.chainback(frame, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + conv.decodeData(data, frame); bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); if (valid) { @@ -399,7 +363,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) // XXX TODO Golay on LICH fragment // Re-encode the payload - conv.encode(frame, rfData + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_LENGTH_BYTES, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + conv.encodeData(frame, rfData + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_LENGTH_BYTES); // Calculate the BER if (valid) { @@ -551,12 +515,9 @@ void CM17Control::writeNetwork() unsigned char setup[M17_LICH_LENGTH_BYTES]; m_netLICH.getLinkSetup(start + 2U); - // Add the CRC - CM17CRC::encodeCRC(start + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); - // Add the convolution FEC CM17Convolution conv; - conv.encode(setup, start + 2U + M17_SYNC_LENGTH_BYTES, M17_LICH_LENGTH_BITS + M17_CRC_LENGTH_BITS); + conv.encodeLinkSetup(setup, start + 2U + M17_SYNC_LENGTH_BYTES); unsigned char temp[M17_FRAME_LENGTH_BYTES]; interleaver(start + 2U, temp); @@ -616,7 +577,7 @@ void CM17Control::writeNetwork() // Add the Convolution FEC CM17Convolution conv; - conv.encode(payload, p, M17_FN_LENGTH_BITS + M17_PAYLOAD_LENGTH_BITS + M17_CRC_LENGTH_BITS); + conv.encodeData(payload, p); unsigned char temp[M17_FRAME_LENGTH_BYTES]; interleaver(data + 2U, temp); diff --git a/M17Convolution.cpp b/M17Convolution.cpp new file mode 100644 index 0000000..166348d --- /dev/null +++ b/M17Convolution.cpp @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2009-2016,2018 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "M17Convolution.h" + +#include +#include +#include +#include + +const unsigned int PUNCTURE_LIST_LINK_SETUP[] = { 3U, 11U, 17U, 25U, 31U, 39U, 45U, 53U, 59U, 67U, + 73U, 81U, 87U, 95U, 101U, 109U, 115U, 123U, 129U, 137U, + 143U, 151U, 157U, 165U, 171U, 179U, 185U, 193U, 199U, 207U, + 213U, 221U, 227U, 235U, 241U, 249U, 255U, 263U, 269U, 277U, + 283U, 291U, 297U, 305U, 311U, 319U, 325U, 333U, 339U, 347U, + 353U, 361U, 367U, 375U, 381U, 389U, 395U, 403U }; + +const unsigned int PUNCTURE_LIST_DATA[] = { 3U, 11U, 17U, 25U, 31U, 39U, 45U, 53U, 59U, 67U, + 73U, 81U, 87U, 95U, 101U, 109U, 115U, 123U, 129U, 137U, + 143U, 151U, 157U, 165U, 171U, 179U, 185U, 193U, 199U, 207U, + 213U, 221U, 227U, 235U, 241U, 249U, 255U, 263U, 269U, 277U, + 283U, 291U, 297U, 305U, 311U, 319U, 325U, 333U, 339U, 347U, + 353U, 361U, 367U, 375U, 381U, 389U, 395U, 403U }; + +const unsigned char BIT_MASK_TABLE[] = {0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U}; + +#define WRITE_BIT1(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) +#define READ_BIT1(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) + +const uint8_t BRANCH_TABLE1[] = {0U, 0U, 0U, 0U, 2U, 2U, 2U, 2U}; +const uint8_t BRANCH_TABLE2[] = {0U, 2U, 2U, 0U, 0U, 2U, 2U, 0U}; + +const unsigned int NUM_OF_STATES_D2 = 8U; +const unsigned int NUM_OF_STATES = 16U; +const uint32_t M = 4U; +const unsigned int K = 5U; + +CM17Convolution::CM17Convolution() : +m_metrics1(NULL), +m_metrics2(NULL), +m_oldMetrics(NULL), +m_newMetrics(NULL), +m_decisions(NULL), +m_dp(NULL) +{ + m_metrics1 = new uint16_t[16U]; + m_metrics2 = new uint16_t[16U]; + m_decisions = new uint64_t[300U]; +} + +CM17Convolution::~CM17Convolution() +{ + delete[] m_metrics1; + delete[] m_metrics2; + delete[] m_decisions; +} + +void CM17Convolution::encodeLinkSetup(const unsigned char* in, unsigned char* out) const +{ + assert(in != NULL); + assert(out != NULL); + + unsigned char temp1[31U]; + ::memset(temp1, 0x00U, 31U); + ::memcpy(temp1, in, 30U); + + unsigned char temp2[61U]; + encode(temp1, temp2, 244U); + + unsigned int n = 0U; + unsigned int index = 0U; + for (unsigned int i = 0U; i < 488U; i++) { + if (i != PUNCTURE_LIST_LINK_SETUP[index]) { + bool b = READ_BIT1(temp2, i); + WRITE_BIT1(out, n, b); + n++; + } else { + index++; + } + } +} + +void CM17Convolution::encodeData(const unsigned char* in, unsigned char* out) const +{ + assert(in != NULL); + assert(out != NULL); + + unsigned char temp1[21U]; + ::memset(temp1, 0x00U, 21U); + ::memcpy(temp1, in, 20U); + + unsigned char temp2[41U]; + encode(temp1, temp2, 164U); + + unsigned int n = 0U; + unsigned int index = 0U; + for (unsigned int i = 0U; i < 328U; i++) { + if (i != PUNCTURE_LIST_DATA[index]) { + bool b = READ_BIT1(temp2, i); + WRITE_BIT1(out, n, b); + n++; + } else { + index++; + } + } +} + +void CM17Convolution::decodeLinkSetup(const unsigned char* in, unsigned char* out) +{ + assert(in != NULL); + assert(out != NULL); + + uint8_t temp[488U]; + + unsigned int n = 0U; + unsigned int index = 0U; + for (unsigned int i = 0U; i < 368U; i++) { + if (n == PUNCTURE_LIST_LINK_SETUP[index]) { + temp[n++] = 1U; + index++; + } + + bool b = READ_BIT1(in, i); + temp[n++] = b ? 2U : 0U; + } + + for (unsigned int i = 0U; i < 8U; i++) + temp[n++] = 0U; + + start(); + + n = 0U; + for (unsigned int i = 0U; i < 244U; i++) { + uint8_t s0 = temp[n++]; + uint8_t s1 = temp[n++]; + + decode(s0, s1); + } + + chainback(out, 244U); +} + +void CM17Convolution::decodeData(const unsigned char* in, unsigned char* out) +{ + assert(in != NULL); + assert(out != NULL); + + uint8_t temp[328U]; + + unsigned int n = 0U; + unsigned int index = 0U; + for (unsigned int i = 0U; i < 272U; i++) { + if (n == PUNCTURE_LIST_DATA[index]) { + temp[n++] = 1U; + index++; + } + + bool b = READ_BIT1(in, i); + temp[n++] = b ? 2U : 0U; + } + + for (unsigned int i = 0U; i < 8U; i++) + temp[n++] = 0U; + + start(); + + n = 0U; + for (unsigned int i = 0U; i < 164U; i++) { + uint8_t s0 = temp[n++]; + uint8_t s1 = temp[n++]; + + decode(s0, s1); + } + + chainback(out, 164U); +} + +void CM17Convolution::start() +{ + ::memset(m_metrics1, 0x00U, NUM_OF_STATES * sizeof(uint16_t)); + ::memset(m_metrics2, 0x00U, NUM_OF_STATES * sizeof(uint16_t)); + + m_oldMetrics = m_metrics1; + m_newMetrics = m_metrics2; + m_dp = m_decisions; +} + +void CM17Convolution::decode(uint8_t s0, uint8_t s1) +{ + *m_dp = 0U; + + for (uint8_t i = 0U; i < NUM_OF_STATES_D2; i++) { + uint8_t j = i * 2U; + + uint16_t metric = std::abs(BRANCH_TABLE1[i] - s0) + std::abs(BRANCH_TABLE2[i] - s1); + + uint16_t m0 = m_oldMetrics[i] + metric; + uint16_t m1 = m_oldMetrics[i + NUM_OF_STATES_D2] + (M - metric); + uint8_t decision0 = (m0 >= m1) ? 1U : 0U; + m_newMetrics[j + 0U] = decision0 != 0U ? m1 : m0; + + m0 = m_oldMetrics[i] + (M - metric); + m1 = m_oldMetrics[i + NUM_OF_STATES_D2] + metric; + uint8_t decision1 = (m0 >= m1) ? 1U : 0U; + m_newMetrics[j + 1U] = decision1 != 0U ? m1 : m0; + + *m_dp |= (uint64_t(decision1) << (j + 1U)) | (uint64_t(decision0) << (j + 0U)); + } + + ++m_dp; + + assert((m_dp - m_decisions) <= 300); + + uint16_t* tmp = m_oldMetrics; + m_oldMetrics = m_newMetrics; + m_newMetrics = tmp; +} + +void CM17Convolution::chainback(unsigned char* out, unsigned int nBits) +{ + assert(out != NULL); + + uint32_t state = 0U; + + while (nBits-- > 0) { + --m_dp; + + uint32_t i = state >> (9 - K); + uint8_t bit = uint8_t(*m_dp >> i) & 1; + state = (bit << 7) | (state >> 1); + + WRITE_BIT1(out, nBits, bit != 0U); + } +} + +void CM17Convolution::encode(const unsigned char* in, unsigned char* out, unsigned int nBits) const +{ + assert(in != NULL); + assert(out != NULL); + assert(nBits > 0U); + + uint8_t d1 = 0U, d2 = 0U, d3 = 0U, d4 = 0U; + uint32_t k = 0U; + for (unsigned int i = 0U; i < nBits; i++) { + uint8_t d = READ_BIT1(in, i) ? 1U : 0U; + + uint8_t g1 = (d + d3 + d4) & 1; + uint8_t g2 = (d + d1 + d2 + d4) & 1; + + d4 = d3; + d3 = d2; + d2 = d1; + d1 = d; + + WRITE_BIT1(out, k, g1 != 0U); + k++; + + WRITE_BIT1(out, k, g2 != 0U); + k++; + } +} diff --git a/M17Convolution.h b/M17Convolution.h index 9e7feb4..e3f5d82 100644 --- a/M17Convolution.h +++ b/M17Convolution.h @@ -26,11 +26,11 @@ public: CM17Convolution(); ~CM17Convolution(); - void start(); - void decode(uint8_t s0, uint8_t s1); - void chainback(unsigned char* out, unsigned int nBits); + void decodeLinkSetup(const unsigned char* in, unsigned char* out); + void decodeData(const unsigned char* in, unsigned char* out); - void encode(const unsigned char* in, unsigned char* out, unsigned int nBits) const; + void encodeLinkSetup(const unsigned char* in, unsigned char* out) const; + void encodeData(const unsigned char* in, unsigned char* out) const; private: uint16_t* m_metrics1; @@ -39,6 +39,12 @@ private: uint16_t* m_newMetrics; uint64_t* m_decisions; uint64_t* m_dp; + + void start(); + void decode(uint8_t s0, uint8_t s1); + void chainback(unsigned char* out, unsigned int nBits); + + void encode(const unsigned char* in, unsigned char* out, unsigned int nBits) const; }; #endif diff --git a/M17LICH.cpp b/M17LICH.cpp index 228f096..c48e5b5 100644 --- a/M17LICH.cpp +++ b/M17LICH.cpp @@ -22,17 +22,6 @@ #include -CM17LICH::CM17LICH(const CM17LICH& lich) : -m_lich(NULL), -m_valid(false) -{ - m_lich = new unsigned char[30U]; - - ::memcpy(m_lich, lich.m_lich, 30U); - - m_valid = lich.m_valid; -} - CM17LICH::CM17LICH() : m_lich(NULL), m_valid(false) diff --git a/M17LICH.h b/M17LICH.h index b4855f6..14e3a2d 100644 --- a/M17LICH.h +++ b/M17LICH.h @@ -23,7 +23,6 @@ class CM17LICH { public: - CM17LICH(const CM17LICH& lich); CM17LICH(); ~CM17LICH(); @@ -48,8 +47,6 @@ public: void getFragment(unsigned char* data, unsigned short fn) const; void setFragment(const unsigned char* data, unsigned short fn); - CM17LICH& operator=(const CM17LICH& lich); - private: unsigned char* m_lich; bool m_valid; diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index 15665a0..e190780 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -291,6 +291,7 @@ + diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index d024110..92f9aca 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -598,5 +598,8 @@ Source Files + + Source Files + \ No newline at end of file From ef07f3c035c766e05804b85f0344dee758237251 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 18 Oct 2020 23:00:16 +0100 Subject: [PATCH 128/163] Update the puncturing tables. --- M17Convolution.cpp | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/M17Convolution.cpp b/M17Convolution.cpp index 166348d..a3d06ba 100644 --- a/M17Convolution.cpp +++ b/M17Convolution.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2016,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2009-2016,2018,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,19 +23,18 @@ #include #include -const unsigned int PUNCTURE_LIST_LINK_SETUP[] = { 3U, 11U, 17U, 25U, 31U, 39U, 45U, 53U, 59U, 67U, - 73U, 81U, 87U, 95U, 101U, 109U, 115U, 123U, 129U, 137U, - 143U, 151U, 157U, 165U, 171U, 179U, 185U, 193U, 199U, 207U, - 213U, 221U, 227U, 235U, 241U, 249U, 255U, 263U, 269U, 277U, - 283U, 291U, 297U, 305U, 311U, 319U, 325U, 333U, 339U, 347U, - 353U, 361U, 367U, 375U, 381U, 389U, 395U, 403U }; +const unsigned int PUNCTURE_LIST_LINK_SETUP[] = { + 3U, 6U, 9U, 12U, 19U, 22U, 25U, 28U, 35U, 38U, 41U, 44U, 51U, 54U, 57U, 64U, 67U, 70U, 73U, 80U, 83U, 86U, 89U, 96U, 99U, 102U, + 105U, 112U, 115U, 118U, 125U, 128U, 131U, 134U, 141U, 144U, 147U, 150U, 157U, 160U, 163U, 166U, 173U, 176U, 179U, 186U, 189U, + 192U, 195U, 202U, 205U, 208U, 211U, 218U, 221U, 224U, 227U, 234U, 237U, 240U, 247U, 250U, 253U, 256U, 263U, 266U, 269U, 272U, + 279U, 282U, 285U, 288U, 295U, 298U, 301U, 308U, 311U, 314U, 317U, 324U, 327U, 330U, 333U, 340U, 343U, 346U, 349U, 356U, 359U, + 362U, 369U, 372U, 375U, 378U, 385U, 388U, 391U, 394U, 401U, 404U, 407U, 410U, 417U, 420U, 423U, 430U, 433U, 436U, 439U, 446U, + 449U, 452U, 455U, 462U, 465U, 468U, 471U, 478U, 481U, 484U}; -const unsigned int PUNCTURE_LIST_DATA[] = { 3U, 11U, 17U, 25U, 31U, 39U, 45U, 53U, 59U, 67U, - 73U, 81U, 87U, 95U, 101U, 109U, 115U, 123U, 129U, 137U, - 143U, 151U, 157U, 165U, 171U, 179U, 185U, 193U, 199U, 207U, - 213U, 221U, 227U, 235U, 241U, 249U, 255U, 263U, 269U, 277U, - 283U, 291U, 297U, 305U, 311U, 319U, 325U, 333U, 339U, 347U, - 353U, 361U, 367U, 375U, 381U, 389U, 395U, 403U }; +const unsigned int PUNCTURE_LIST_DATA[] = { + 5U, 11U, 17U, 20U, 23U, 29U, 35U, 46U, 52U, 58U, 61U, 64U, 70U, 76U, 87U, 93U, 99U, 102U, 105U, 111U, 117U, 128U, 134U, 140U, + 143U, 146U, 152U, 158U, 169U, 175U, 181U, 184U, 187U, 193U, 199U, 210U, 216U, 222U, 225U, 228U, 234U, 240U, 251U, 257U, 263U, + 266U, 269U, 275U, 281U, 292U, 298U, 304U, 307U, 310U, 316U, 322U}; const unsigned char BIT_MASK_TABLE[] = {0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U}; From b46d2f586cafe1ab48a5d2a774e1f03dac678a8d Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 18 Oct 2020 23:05:53 +0100 Subject: [PATCH 129/163] Add the M17 TFT Serial image. --- TFTSerial/M17_sm.bmp | Bin 0 -> 24054 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 TFTSerial/M17_sm.bmp diff --git a/TFTSerial/M17_sm.bmp b/TFTSerial/M17_sm.bmp new file mode 100644 index 0000000000000000000000000000000000000000..656e410fb6c27ccbb26cff2b49ed32d15b502bb0 GIT binary patch literal 24054 zcmeI4ZA=tL7{>u27)8w~(W)P`Xnb#z5&{8VVvNS5YLz3^5?|AXNU(_?O#E^Zp|lti zKqPt~7SStW1&Vr{L<}HUUjUC36l_#Z0l@?16gXT&j@>?Y%gxR1?Jc`-u_Vr9?{4})NQZRfkfzNsP1j5JD;|uuq@TeHxZd)1tVn`wi5Cw<=L;<3} z7!>&U@#7dulcM8VfzHm3TD`u#y?tCqNrD@#0?o}YLPJBf+H0e&fRr283cPvq8ixCc ziHXmiJssD9lHf+AKv&lr;JaYKg8cmaQB^<+jcWy1zJU1P!GpE6`f(jA2~NHOi0{;? zQ?s+PDeAraI!KBCQ-QYjwzqHJx{3htg(e&k5m8=N=E~FU_KuFG*PWeiulDn)aA06y z`}f;dty--sDspwyIA1`VoSZDKd3}BT{rmT;s;Vk0D?3H$^!JlI zZUFE0?Q0DR`umzTi)$xqQWf~~(k0->*J(JZzfNE>*x16EiH#(hFIs~>o8)6g@Gj@;Y`!B zMpu;(yDJ468yXNY$ShsDw4k6s(Am`0g?mC?rBXe8DtxW9b?bM?8Lpb!+S&x`1f#jv z!VYSZ<{WEyZpFdxt z(Kvg8B_hI6=gO5UMfmoaagLVs^vkII?%6Yhp=V-Z@@s1=1b0V_A_|zzW}L4=p~%U} z8TjylSHzhwl!}gyhLbGM6IR{Amtg=sjSwpoy=O90D9Y@-J7N)6;Mar%9C2i1I`uh5+)oO~Oc+PI(%lv+p(}b1ETcxGYfs-U#fr%3*BE+7ao@r@mU0q#xErNWZ z$d)Zzii-FHEqC!{7<%SRyaQ*Fl1_FQTk*h1UWx+c<>iPkWLB?UU3~pIUeFa^a6WM& zm3LpseSDc({UGY1ArltXke{E92S)M|680c1G8-FfQD7t^QGxe;oPkzAK)}U|a5Xxd@b)`p%9JHbtWTGr6dN0hkGkE< zx33TQItE&^XLp@Dcg!9a`IVu-&j$`5WRQu8iFr^{!?>;ckZmH zxW!f@jV}$(BG{%z_pUeWF2*n#FxsfUUSaE&s4hd<{YOUjWlH@8782F7KHD|sJ4UYspo%uo& z4ha!NER`=D#kzHz8dR#RCr<<(OOSiKpPRqI&?HFBnJ*{<&s91*3-M)MzU0ucH(?ok z85(ME^z-xP8UN6v()94NTb~=Iu^caNWW^E`QSo$jiS z4}}mT84M zLXva*#4@4+>({ThLvzeO3zmBE{5kRz=L=1E(hU|IBK&d)F@3btJxw#exP}e7=K&LoZtgjYf;~rLE1<{f^)or2?@k6-#YI zzOVy{e8JN`Db43Q$|y;(F(@$6*B8J^;d>?{13ckPG?DjH?19jJe(?8TQL7#Ar`R`< z-%qN*mBK=xcJ{2K`QB;MrUnNGx3-S_M7s8x^(>r`G5mhnCp{#xGp+)wR;?^4xxuvw zua%XRDZ|5!Mx*d%j+I`shAA#?py>gQnXG(r1)7?gjIUlv+^tUM@||&B4FvNSP5e0|>L;<1zQGh5w6d(!^1zaic9|%F7N&o-= literal 0 HcmV?d00001 From 551556b65d428e41fdf7c21a802f0739c10981be Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 18 Oct 2020 23:24:46 +0100 Subject: [PATCH 130/163] Update the Makefiles. --- Makefile | 14 +++++++------- Makefile.Pi | 14 +++++++------- Makefile.Pi.Adafruit | 14 +++++++------- Makefile.Pi.HD44780 | 14 +++++++------- Makefile.Pi.OLED | 14 +++++++------- Makefile.Pi.PCF8574 | 14 +++++++------- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/Makefile b/Makefile index ed7568e..24323fe 100644 --- a/Makefile +++ b/Makefile @@ -8,13 +8,13 @@ LDFLAGS = -g OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ - NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ - P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o \ - SerialController.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o \ - YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o \ + DStarNetwork.o DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o M17Control.o M17Convolution.o M17CRC.o M17LICH.o \ + M17Network.o M17Utils.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o \ + NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ + NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o \ + RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o \ + UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi b/Makefile.Pi index cd01588..22dca30 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -8,13 +8,13 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o \ - NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ - StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ - YSFFICH.o YSFNetwork.o YSFPayload.o + DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o \ + DStarNetwork.o DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o LCDproc.o Log.o M17Control.o M17Convolution.o M17CRC.o M17LICH.o \ + M17Network.o M17Utils.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o \ + NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o \ + NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o \ + RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o \ + UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index 54d2b28..cd37ada 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -9,13 +9,13 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o \ - NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ - StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ - YSFFICH.o YSFNetwork.o YSFPayload.o + DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o \ + DStarNetwork.o DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o M17Control.o M17Convolution.o M17CRC.o \ + M17LICH.o M17Network.o M17Utils.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o \ + NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o \ + NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ + POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o StopWatch.o Sync.o TFTSerial.o \ + TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index 10e5dd3..004aac0 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -8,13 +8,13 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o \ - NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ - StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ - YSFFICH.o YSFNetwork.o YSFPayload.o + DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o \ + DStarNetwork.o DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o M17Control.o M17Convolution.o M17CRC.o \ + M17LICH.o M17Network.o M17Utils.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o \ + NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o \ + NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ + POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o StopWatch.o Sync.o TFTSerial.o \ + TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index 8206dc7..8164647 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -12,13 +12,13 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o OLED.o LCDproc.o Log.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o \ - NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ - StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ - YSFFICH.o YSFNetwork.o YSFPayload.o + DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o \ + DStarNetwork.o DStarSlowData.o Golay2087.o Golay24128.o Hamming.o I2CController.o OLED.o LCDproc.o Log.o M17Control.o M17Convolution.o M17CRC.o \ + M17LICH.o M17Network.o M17Utils.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o \ + NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o \ + NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ + POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o StopWatch.o Sync.o TFTSerial.o \ + TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index 192c86e..c105a58 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -9,13 +9,13 @@ LDFLAGS = -g -L/usr/local/lib OBJECTS = \ AMBEFEC.o BCH.o BPTC19696.o CASTInfo.o Conf.o CRC.o Display.o DMRControl.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREMB.o DMREmbeddedData.o DMRFullLC.o \ - DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o DStarNetwork.o \ - DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o \ - NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o \ - NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o \ - P25Trellis.o P25Utils.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o \ - StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o \ - YSFFICH.o YSFNetwork.o YSFPayload.o + DMRLookup.o DMRLC.o DMRNetwork.o DMRShortLC.o DMRSlot.o DMRSlotType.o DMRAccessControl.o DMRTA.o DMRTrellis.o DStarControl.o DStarHeader.o \ + DStarNetwork.o DStarSlowData.o Golay2087.o Golay24128.o Hamming.o HD44780.o I2CController.o LCDproc.o Log.o M17Control.o M17Convolution.o M17CRC.o \ + M17LICH.o M17Network.o M17Utils.o MMDVMHost.o Modem.o ModemSerialPort.o Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o \ + NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o \ + NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o POCSAGControl.o \ + POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialPort.o StopWatch.o Sync.o TFTSerial.o \ + TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand From d2bf8eb744718ba615ab25af591cdb3a5acd8aa2 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 18 Oct 2020 23:28:29 +0100 Subject: [PATCH 131/163] Fix Linux compile. --- M17LICH.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/M17LICH.cpp b/M17LICH.cpp index c48e5b5..164549b 100644 --- a/M17LICH.cpp +++ b/M17LICH.cpp @@ -21,6 +21,7 @@ #include "M17CRC.h" #include +#include CM17LICH::CM17LICH() : m_lich(NULL), From 28e013acb7b12390a9cd93b759a75ad3c3c76560 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 19 Oct 2020 10:37:23 +0100 Subject: [PATCH 132/163] Clean up the frame processing for RF and network. --- M17Control.cpp | 80 +++++++++++++------------- M17Defines.h | 7 ++- M17Network.cpp | 5 -- M17Utils.cpp | 151 ++++++++++++++++++++++++++++++++++++++++++++++--- M17Utils.h | 7 ++- Version.h | 2 +- 6 files changed, 197 insertions(+), 55 deletions(-) diff --git a/M17Control.cpp b/M17Control.cpp index 839c954..3f98e5a 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -13,6 +13,7 @@ #include "M17Control.h" #include "M17Convolution.h" +#include "M17Utils.h" #include "M17CRC.h" #include "Golay24128.h" #include "Utils.h" @@ -157,7 +158,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) CM17Convolution conv; unsigned char frame[M17_LICH_LENGTH_BYTES]; - conv.decodeLinkSetup(data, frame); + conv.decodeLinkSetup(data + 2U + M17_SYNC_LENGTH_BYTES, frame); bool valid = CM17CRC::checkCRC(frame, M17_LICH_LENGTH_BYTES); if (valid) { @@ -212,7 +213,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) CSync::addM17Sync(data + 2U); unsigned char setup[M17_LICH_LENGTH_BYTES]; - m_rfLICH.getLinkSetup(data + 2U); + m_rfLICH.getLinkSetup(setup); // Add the convolution FEC CM17Convolution conv; @@ -232,22 +233,24 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) if (m_rfState == RS_RF_LATE_ENTRY) { CM17Convolution conv; unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; - conv.decodeData(data, frame); + conv.decodeData(data + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_FEC_LENGTH_BYTES, frame); bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); if (valid) { m_rfFN = (frame[0U] << 8) + (frame[1U] << 0); unsigned int frag1, frag2, frag3, frag4; - - // XXX TODO populate frag1-4 + CM17Utils::splitFragmentLICHFEC(data + 2U + M17_SYNC_LENGTH_BYTES, frag1, frag2, frag3, frag4); unsigned int lich1 = CGolay24128::decode24128(frag1); unsigned int lich2 = CGolay24128::decode24128(frag2); unsigned int lich3 = CGolay24128::decode24128(frag3); unsigned int lich4 = CGolay24128::decode24128(frag4); - m_rfLICH.setFragment(data + 2U + M17_SYNC_LENGTH_BYTES, m_rfFN & 0x7FFFU); + unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; + CM17Utils::combineFragmentLICH(lich1, lich2, lich3, lich4, lich); + + m_rfLICH.setFragment(lich, m_rfFN); valid = m_rfLICH.isValid(); if (valid) { @@ -297,7 +300,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) CSync::addM17Sync(data + 2U); unsigned char setup[M17_LICH_LENGTH_BYTES]; - m_rfLICH.getLinkSetup(data + 2U); + m_rfLICH.getLinkSetup(setup); // Add the convolution FEC CM17Convolution conv; @@ -319,7 +322,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) #endif CM17Convolution conv; unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; - conv.decodeData(data, frame); + conv.decodeData(data + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_FEC_LENGTH_BYTES, frame); bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); if (valid) { @@ -355,22 +358,36 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) rfData[0U] = TAG_DATA; rfData[1U] = 0x00U; + // Generate the sync CSync::addM17Sync(rfData + 2U); - // Re-encode the LICH fragment - m_rfLICH.getFragment(rfData + 2U + M17_SYNC_LENGTH_BYTES, m_rfFN & 0x7FFFU); + unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; + m_netLICH.getFragment(lich, m_rfFN); - // XXX TODO Golay on LICH fragment + unsigned int lich1, lich2, lich3, lich4; + CM17Utils::splitFragmentLICH(lich, lich1, lich2, lich3, lich4); - // Re-encode the payload - conv.encodeData(frame, rfData + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_LENGTH_BYTES); + // Add Golay to the LICH fragment here + CGolay24128::encode24128(lich1); + CGolay24128::encode24128(lich2); + CGolay24128::encode24128(lich3); + CGolay24128::encode24128(lich4); + + CM17Utils::combineFragmentLICHFEC(lich1, lich2, lich3, lich4, rfData + 2U + M17_SYNC_LENGTH_BYTES); + + // Add the Convolution FEC + conv.encodeData(frame, rfData + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_FEC_LENGTH_BYTES); + + unsigned char temp[M17_FRAME_LENGTH_BYTES]; + interleaver(rfData + 2U, temp); + decorrelator(rfData, data + 2U); // Calculate the BER if (valid) { - for (unsigned int i = 2U; i < 50U; i++) + for (unsigned int i = 2U; i < (M17_FRAME_LENGTH_BYTES + 2U); i++) m_rfErrs += countBits(rfData[i] ^ data[i]); - m_rfBits += 272U; + m_rfBits += M17_FRAME_LENGTH_BITS; float ber = float(m_rfErrs) / float(m_rfBits); m_display->writeM17BER(ber); @@ -501,7 +518,7 @@ void CM17Control::writeNetwork() m_netTimeoutTimer.start(); m_packetTimer.start(); m_elapsed.start(); - m_netFrames = 1U; + m_netFrames = 0U; // Create a dummy start message unsigned char start[M17_FRAME_LENGTH_BYTES + 2U]; @@ -513,7 +530,7 @@ void CM17Control::writeNetwork() CSync::addM17Sync(start + 2U); unsigned char setup[M17_LICH_LENGTH_BYTES]; - m_netLICH.getLinkSetup(start + 2U); + m_netLICH.getLinkSetup(setup); // Add the convolution FEC CM17Convolution conv; @@ -531,23 +548,19 @@ void CM17Control::writeNetwork() data[0U] = TAG_DATA; data[1U] = 0x00U; - unsigned char* p = data + 2U; - // Generate the sync - CSync::addM17Sync(p); - p += M17_SYNC_LENGTH_BYTES; + CSync::addM17Sync(data + 2U); m_netFrames++; // Add the fragment LICH uint16_t fn = (netData[38U] << 8) + (netData[39U] << 0); - unsigned char lich[6U]; - m_netLICH.getFragment(lich, fn & 0x7FFFU); + unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; + m_netLICH.getFragment(lich, fn); unsigned int lich1, lich2, lich3, lich4; - - // XXX TODO + CM17Utils::splitFragmentLICH(lich, lich1, lich2, lich3, lich4); // Add Golay to the LICH fragment here CGolay24128::encode24128(lich1); @@ -555,21 +568,10 @@ void CM17Control::writeNetwork() CGolay24128::encode24128(lich3); CGolay24128::encode24128(lich4); - ::memcpy(p, &lich1, M17_LICH_FRAGMENT_LENGTH_BYTES / 4U); - p += M17_LICH_FRAGMENT_LENGTH_BYTES / 4U; - - ::memcpy(p, &lich2, M17_LICH_FRAGMENT_LENGTH_BYTES / 4U); - p += M17_LICH_FRAGMENT_LENGTH_BYTES / 4U; - - ::memcpy(p, &lich3, M17_LICH_FRAGMENT_LENGTH_BYTES / 4U); - p += M17_LICH_FRAGMENT_LENGTH_BYTES / 4U; - - ::memcpy(p, &lich4, M17_LICH_FRAGMENT_LENGTH_BYTES / 4U); - p += M17_LICH_FRAGMENT_LENGTH_BYTES / 4U; - - unsigned char payload[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; + CM17Utils::combineFragmentLICHFEC(lich1, lich2, lich3, lich4, data + 2U + M17_SYNC_LENGTH_BYTES); // Add the FN and the data/audio + unsigned char payload[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; ::memcpy(payload, netData + 38U, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES); // Add the CRC @@ -577,7 +579,7 @@ void CM17Control::writeNetwork() // Add the Convolution FEC CM17Convolution conv; - conv.encodeData(payload, p); + conv.encodeData(payload, data + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_FEC_LENGTH_BYTES); unsigned char temp[M17_FRAME_LENGTH_BYTES]; interleaver(data + 2U, temp); diff --git a/M17Defines.h b/M17Defines.h index 201f049..ed530a4 100644 --- a/M17Defines.h +++ b/M17Defines.h @@ -28,12 +28,15 @@ const unsigned char M17_SYNC_BYTES[] = {0x32U, 0x43U}; const unsigned int M17_SYNC_LENGTH_BITS = 16U; const unsigned int M17_SYNC_LENGTH_BYTES = M17_SYNC_LENGTH_BITS / 8U; -const unsigned int M17_LICH_LENGTH_BITS = 224U; +const unsigned int M17_LICH_LENGTH_BITS = 240U; const unsigned int M17_LICH_LENGTH_BYTES = M17_LICH_LENGTH_BITS / 8U; -const unsigned int M17_LICH_FRAGMENT_LENGTH_BITS = 96U; +const unsigned int M17_LICH_FRAGMENT_LENGTH_BITS = M17_LICH_LENGTH_BITS / 5U; const unsigned int M17_LICH_FRAGMENT_LENGTH_BYTES = M17_LICH_FRAGMENT_LENGTH_BITS / 8U; +const unsigned int M17_LICH_FRAGMENT_FEC_LENGTH_BITS = M17_LICH_FRAGMENT_LENGTH_BITS * 2U; +const unsigned int M17_LICH_FRAGMENT_FEC_LENGTH_BYTES = M17_LICH_FRAGMENT_FEC_LENGTH_BITS / 8U; + const unsigned int M17_PAYLOAD_LENGTH_BITS = 128U; const unsigned int M17_PAYLOAD_LENGTH_BYTES = M17_PAYLOAD_LENGTH_BITS / 8U; diff --git a/M17Network.cpp b/M17Network.cpp index d6b273e..b020dfa 100644 --- a/M17Network.cpp +++ b/M17Network.cpp @@ -219,11 +219,6 @@ void CM17Network::clock(unsigned int ms) return; } - // EOT? - uint16_t fn = (buffer[38U] << 8) + (buffer[39U] << 0); - if ((fn & 0x8000U) == 0x8000U) - m_inId = 0U; - unsigned char c = length - 6U; m_buffer.addData(&c, 1U); diff --git a/M17Utils.cpp b/M17Utils.cpp index b3d0056..1ba33b1 100644 --- a/M17Utils.cpp +++ b/M17Utils.cpp @@ -17,11 +17,17 @@ */ #include "M17Utils.h" +#include "M17Defines.h" #include const std::string M17_CHARS = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-/."; +const unsigned char BIT_MASK_TABLE[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U }; + +#define WRITE_BIT1(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) +#define READ_BIT1(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) + void CM17Utils::encodeCallsign(const std::string& callsign, unsigned char* encoded) { assert(encoded != NULL); @@ -40,19 +46,19 @@ void CM17Utils::encodeCallsign(const std::string& callsign, unsigned char* encod enc += pos; } - encoded[0U] = enc >> 40; - encoded[1U] = enc >> 32; - encoded[2U] = enc >> 24; - encoded[3U] = enc >> 16; - encoded[4U] = enc >> 8; - encoded[5U] = enc >> 0; + encoded[0U] = (enc >> 40) & 0xFFU; + encoded[1U] = (enc >> 32) & 0xFFU; + encoded[2U] = (enc >> 24) & 0xFFU; + encoded[3U] = (enc >> 16) & 0xFFU; + encoded[4U] = (enc >> 8) & 0xFFU; + encoded[5U] = (enc >> 0) & 0xFFU; } void CM17Utils::decodeCallsign(const unsigned char* encoded, std::string& callsign) { assert(encoded != NULL); - callsign.empty(); + callsign.clear(); uint64_t enc = (uint64_t(encoded[0U]) << 40) + (uint64_t(encoded[1U]) << 32) + @@ -70,3 +76,134 @@ void CM17Utils::decodeCallsign(const unsigned char* encoded, std::string& callsi } } +void CM17Utils::splitFragmentLICH(const unsigned char* data, unsigned int& frag1, unsigned int& frag2, unsigned int& frag3, unsigned int& frag4) +{ + assert(data != NULL); + + frag1 = frag2 = frag3 = frag4 = 0x00U; + + unsigned int offset = 0U; + unsigned int MASK = 0x800U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = READ_BIT1(data, offset) != 0x00U; + if (b) + frag1 |= MASK; + } + + MASK = 0x800U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = READ_BIT1(data, offset) != 0x00U; + if (b) + frag2 |= MASK; + } + + MASK = 0x800U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = READ_BIT1(data, offset) != 0x00U; + if (b) + frag3 |= MASK; + } + + MASK = 0x800U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = READ_BIT1(data, offset) != 0x00U; + if (b) + frag4 |= MASK; + } +} + +void CM17Utils::splitFragmentLICHFEC(const unsigned char* data, unsigned int& frag1, unsigned int& frag2, unsigned int& frag3, unsigned int& frag4) +{ + assert(data != NULL); + + frag1 = frag2 = frag3 = frag4 = 0x00U; + + unsigned int offset = 0U; + unsigned int MASK = 0x800000U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_FEC_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = READ_BIT1(data, offset) != 0x00U; + if (b) + frag1 |= MASK; + } + + MASK = 0x800000U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_FEC_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = READ_BIT1(data, offset) != 0x00U; + if (b) + frag2 |= MASK; + } + + MASK = 0x800000U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_FEC_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = READ_BIT1(data, offset) != 0x00U; + if (b) + frag3 |= MASK; + } + + MASK = 0x800000U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_FEC_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = READ_BIT1(data, offset) != 0x00U; + if (b) + frag4 |= MASK; + } +} + +void CM17Utils::combineFragmentLICH(unsigned int frag1, unsigned int frag2, unsigned int frag3, unsigned int frag4, unsigned char* data) +{ + assert(data != NULL); + + unsigned int offset = 0U; + unsigned int MASK = 0x800U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = (frag1 & MASK) == MASK; + WRITE_BIT1(data, offset, b); + } + + MASK = 0x800U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = (frag2 & MASK) == MASK; + WRITE_BIT1(data, offset, b); + } + + MASK = 0x800U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = (frag3 & MASK) == MASK; + WRITE_BIT1(data, offset, b); + } + + MASK = 0x800U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = (frag4 & MASK) == MASK; + WRITE_BIT1(data, offset, b); + } +} + +void CM17Utils::combineFragmentLICHFEC(unsigned int frag1, unsigned int frag2, unsigned int frag3, unsigned int frag4, unsigned char* data) +{ + assert(data != NULL); + + unsigned int offset = 0U; + unsigned int MASK = 0x800000U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_FEC_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = (frag1 & MASK) == MASK; + WRITE_BIT1(data, offset, b); + } + + MASK = 0x800000U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_FEC_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = (frag2 & MASK) == MASK; + WRITE_BIT1(data, offset, b); + } + + MASK = 0x800000U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_FEC_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = (frag3 & MASK) == MASK; + WRITE_BIT1(data, offset, b); + } + + MASK = 0x800000U; + for (unsigned int i = 0U; i < (M17_LICH_FRAGMENT_FEC_LENGTH_BITS / 4U); i++, offset++, MASK >>= 1) { + bool b = (frag4 & MASK) == MASK; + WRITE_BIT1(data, offset, b); + } +} diff --git a/M17Utils.h b/M17Utils.h index c9a18df..c6e27ce 100644 --- a/M17Utils.h +++ b/M17Utils.h @@ -27,9 +27,14 @@ public: ~CM17Utils(); static void encodeCallsign(const std::string& callsign, unsigned char* encoded); - static void decodeCallsign(const unsigned char* encoded, std::string& callsign); + static void splitFragmentLICH(const unsigned char* data, unsigned int& frag1, unsigned int& frag2, unsigned int& frag3, unsigned int& frag4); + static void splitFragmentLICHFEC(const unsigned char* data, unsigned int& frag1, unsigned int& frag2, unsigned int& frag3, unsigned int& frag4); + + static void combineFragmentLICH(unsigned int frag1, unsigned int frag2, unsigned int frag3, unsigned int frag4, unsigned char* data); + static void combineFragmentLICHFEC(unsigned int frag1, unsigned int frag2, unsigned int frag3, unsigned int frag4, unsigned char* data); + private: }; diff --git a/Version.h b/Version.h index dcda43b..0117c94 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201014"; +const char* VERSION = "20201019"; #endif From 9567ac0b207876294d3d74b5bc405811c52fc83f Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 19 Oct 2020 11:00:45 +0100 Subject: [PATCH 133/163] Use defined lengths instead of magic numbers. --- M17LICH.cpp | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/M17LICH.cpp b/M17LICH.cpp index 164549b..2de383e 100644 --- a/M17LICH.cpp +++ b/M17LICH.cpp @@ -18,6 +18,7 @@ #include "M17LICH.h" #include "M17Utils.h" +#include "M17Defines.h" #include "M17CRC.h" #include @@ -27,7 +28,7 @@ CM17LICH::CM17LICH() : m_lich(NULL), m_valid(false) { - m_lich = new unsigned char[30U]; + m_lich = new unsigned char[M17_LICH_LENGTH_BYTES]; } CM17LICH::~CM17LICH() @@ -39,23 +40,20 @@ void CM17LICH::getNetwork(unsigned char* data) const { assert(data != NULL); - ::memcpy(data, m_lich, 28U); + ::memcpy(data, m_lich, M17_LICH_LENGTH_BYTES); } void CM17LICH::setNetwork(const unsigned char* data) { assert(data != NULL); - ::memcpy(m_lich, data, 28U); + ::memcpy(m_lich, data, M17_LICH_LENGTH_BYTES); m_valid = true; } std::string CM17LICH::getSource() const { - if (!m_valid) - return ""; - std::string callsign; CM17Utils::decodeCallsign(m_lich + 6U, callsign); @@ -69,9 +67,6 @@ void CM17LICH::setSource(const std::string& callsign) std::string CM17LICH::getDest() const { - if (!m_valid) - return ""; - std::string callsign; CM17Utils::decodeCallsign(m_lich + 0U, callsign); @@ -85,9 +80,6 @@ void CM17LICH::setDest(const std::string& callsign) unsigned char CM17LICH::getDataType() const { - if (!m_valid) - return 0U; - return (m_lich[12U] >> 1) & 0x03U; } @@ -113,29 +105,29 @@ void CM17LICH::getLinkSetup(unsigned char* data) const { assert(data != NULL); - ::memcpy(data, m_lich, 30U); + ::memcpy(data, m_lich, M17_LICH_LENGTH_BYTES); - CM17CRC::encodeCRC(data, 30U); + CM17CRC::encodeCRC(data, M17_LICH_LENGTH_BYTES); } void CM17LICH::setLinkSetup(const unsigned char* data) { assert(data != NULL); - ::memcpy(m_lich, data, 30U); + ::memcpy(m_lich, data, M17_LICH_LENGTH_BYTES); - m_valid = CM17CRC::checkCRC(m_lich, 30U); + m_valid = CM17CRC::checkCRC(m_lich, M17_LICH_LENGTH_BYTES); } void CM17LICH::getFragment(unsigned char* data, unsigned short fn) const { assert(data != NULL); - CM17CRC::encodeCRC(m_lich, 30U); + CM17CRC::encodeCRC(m_lich, M17_LICH_LENGTH_BYTES); unsigned int n = (fn & 0x7FFFU) % 5U; - ::memcpy(data, m_lich + (n * 6U), 6U); + ::memcpy(data, m_lich + (n * M17_LICH_FRAGMENT_LENGTH_BYTES), M17_LICH_FRAGMENT_LENGTH_BYTES); } void CM17LICH::setFragment(const unsigned char* data, unsigned short fn) @@ -144,7 +136,7 @@ void CM17LICH::setFragment(const unsigned char* data, unsigned short fn) unsigned int n = (fn & 0x7FFFU) % 5U; - ::memcpy(m_lich + (n * 6U), data, 6U); + ::memcpy(m_lich + (n * M17_LICH_FRAGMENT_LENGTH_BYTES), data, M17_LICH_FRAGMENT_LENGTH_BYTES); - m_valid = CM17CRC::checkCRC(m_lich, 30U); + m_valid = CM17CRC::checkCRC(m_lich, M17_LICH_LENGTH_BYTES); } From d58a0c53b58e66a63f31cdf9ff487e35eba76932 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 20 Oct 2020 09:40:29 +0100 Subject: [PATCH 134/163] Add the SelfOnly processing for M17. --- M17Control.cpp | 25 +++++++++++++++++++++++++ M17Control.h | 2 ++ Version.h | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/M17Control.cpp b/M17Control.cpp index 3f98e5a..59ef2de 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -180,6 +180,15 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) std::string source = m_rfLICH.getSource(); std::string dest = m_rfLICH.getDest(); + if (m_selfOnly) { + bool ret = checkCallsign(source); + if (!ret) { + LogMessage("M17, invalid access attempt from %s to %s", source.c_str(), dest.c_str()); + m_rfState = RS_RF_REJECTED; + return false; + } + } + unsigned char dataType = m_rfLICH.getDataType(); switch (dataType) { case 1U: @@ -269,6 +278,15 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) std::string source = m_rfLICH.getSource(); std::string dest = m_rfLICH.getDest(); + if (m_selfOnly) { + bool ret = checkCallsign(source); + if (!ret) { + LogMessage("M17, invalid access attempt from %s to %s", source.c_str(), dest.c_str()); + m_rfState = RS_RF_REJECTED; + return false; + } + } + unsigned char dataType = m_rfLICH.getDataType(); switch (dataType) { case 1U: @@ -693,6 +711,13 @@ void CM17Control::decorrelator(const unsigned char* in, unsigned char* out) cons } } +bool CM17Control::checkCallsign(const std::string& callsign) const +{ + size_t len = m_callsign.size(); + + return m_callsign.compare(0U, len, callsign, 0U, len) == 0; +} + bool CM17Control::openFile() { if (m_fp != NULL) diff --git a/M17Control.h b/M17Control.h index 1c2a271..71c402d 100644 --- a/M17Control.h +++ b/M17Control.h @@ -85,6 +85,8 @@ private: void interleaver(const unsigned char* in, unsigned char* out) const; void decorrelator(const unsigned char* in, unsigned char* out) const; + bool checkCallsign(const std::string& source) const; + unsigned int countBits(unsigned char byte); void writeEndRF(); diff --git a/Version.h b/Version.h index 0117c94..f1c220d 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201019"; +const char* VERSION = "20201020"; #endif From 1bb1e9da59647555121b28253ca09cf87e785317 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 20 Oct 2020 14:26:47 +0100 Subject: [PATCH 135/163] M17 bug fixing. --- M17Control.cpp | 16 +++++++++------- M17Convolution.cpp | 8 ++++---- M17LICH.cpp | 6 +++--- M17Network.cpp | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/M17Control.cpp b/M17Control.cpp index 59ef2de..0eda4f6 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -234,6 +234,8 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) writeQueueRF(data); } + + return true; } else { m_rfState = RS_RF_LATE_ENTRY; } @@ -396,10 +398,6 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) // Add the Convolution FEC conv.encodeData(frame, rfData + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_FEC_LENGTH_BYTES); - unsigned char temp[M17_FRAME_LENGTH_BYTES]; - interleaver(rfData + 2U, temp); - decorrelator(rfData, data + 2U); - // Calculate the BER if (valid) { for (unsigned int i = 2U; i < (M17_FRAME_LENGTH_BYTES + 2U); i++) @@ -411,6 +409,10 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) m_display->writeM17BER(ber); } + unsigned char temp[M17_FRAME_LENGTH_BYTES]; + interleaver(rfData + 2U, temp); + decorrelator(rfData, data + 2U); + if (m_duplex) writeQueueRF(rfData); @@ -419,7 +421,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) m_rfLICH.getNetwork(netData + 0U); // Copy the FN and payload from the frame - ::memcpy(netData + M17_LICH_LENGTH_BYTES, frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES); + ::memcpy(netData + M17_LICH_LENGTH_BYTES - M17_CRC_LENGTH_BYTES, frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES); // The CRC is added in the networking code @@ -572,7 +574,7 @@ void CM17Control::writeNetwork() m_netFrames++; // Add the fragment LICH - uint16_t fn = (netData[38U] << 8) + (netData[39U] << 0); + uint16_t fn = (netData[28U] << 8) + (netData[29U] << 0); unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; m_netLICH.getFragment(lich, fn); @@ -590,7 +592,7 @@ void CM17Control::writeNetwork() // Add the FN and the data/audio unsigned char payload[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; - ::memcpy(payload, netData + 38U, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES); + ::memcpy(payload, netData + 28U, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES); // Add the CRC CM17CRC::encodeCRC(payload, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); diff --git a/M17Convolution.cpp b/M17Convolution.cpp index a3d06ba..1f64a76 100644 --- a/M17Convolution.cpp +++ b/M17Convolution.cpp @@ -124,7 +124,7 @@ void CM17Convolution::decodeLinkSetup(const unsigned char* in, unsigned char* ou assert(in != NULL); assert(out != NULL); - uint8_t temp[488U]; + uint8_t temp[500U]; unsigned int n = 0U; unsigned int index = 0U; @@ -151,7 +151,7 @@ void CM17Convolution::decodeLinkSetup(const unsigned char* in, unsigned char* ou decode(s0, s1); } - chainback(out, 244U); + chainback(out, 240U); } void CM17Convolution::decodeData(const unsigned char* in, unsigned char* out) @@ -159,7 +159,7 @@ void CM17Convolution::decodeData(const unsigned char* in, unsigned char* out) assert(in != NULL); assert(out != NULL); - uint8_t temp[328U]; + uint8_t temp[350U]; unsigned int n = 0U; unsigned int index = 0U; @@ -186,7 +186,7 @@ void CM17Convolution::decodeData(const unsigned char* in, unsigned char* out) decode(s0, s1); } - chainback(out, 164U); + chainback(out, 160U); } void CM17Convolution::start() diff --git a/M17LICH.cpp b/M17LICH.cpp index 2de383e..ed2b870 100644 --- a/M17LICH.cpp +++ b/M17LICH.cpp @@ -80,13 +80,13 @@ void CM17LICH::setDest(const std::string& callsign) unsigned char CM17LICH::getDataType() const { - return (m_lich[12U] >> 1) & 0x03U; + return (m_lich[13U] >> 1) & 0x03U; } void CM17LICH::setDataType(unsigned char type) { - m_lich[12U] &= 0xF9U; - m_lich[12U] |= (type << 1) & 0x06U; + m_lich[13U] &= 0xF9U; + m_lich[13U] |= (type << 1) & 0x06U; } void CM17LICH::reset() diff --git a/M17Network.cpp b/M17Network.cpp index b020dfa..281413e 100644 --- a/M17Network.cpp +++ b/M17Network.cpp @@ -222,7 +222,7 @@ void CM17Network::clock(unsigned int ms) unsigned char c = length - 6U; m_buffer.addData(&c, 1U); - m_buffer.addData(buffer + 6U, length - 6U); + m_buffer.addData(buffer + 6U, length - 8U); } bool CM17Network::read(unsigned char* data) From bab4b50d3054fac58580d82008e46063935aa4cf Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Wed, 21 Oct 2020 10:17:52 +0100 Subject: [PATCH 136/163] Handle EOT on rejected frames correctly. --- M17Control.cpp | 16 ++++++++++++++++ Version.h | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/M17Control.cpp b/M17Control.cpp index 0eda4f6..661889b 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -442,6 +442,22 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) } } + if (m_rfState == RS_RF_REJECTED) { + CM17Convolution conv; + unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; + conv.decodeData(data + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_FEC_LENGTH_BYTES, frame); + + bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); + if (valid) { + // Handle the EOT for rejected frames + unsigned int fn = (frame[0U] << 8) + (frame[1U] << 0); + if ((fn & 0x8000U) == 0x8000U) + writeEndRF(); + } + + return false; + } + return true; } diff --git a/Version.h b/Version.h index f1c220d..7335272 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201020"; +const char* VERSION = "20201021"; #endif From 9688929c3d34f2925c5c2a7611ecc3df9240944a Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Wed, 21 Oct 2020 10:41:06 +0100 Subject: [PATCH 137/163] Fix the M17 LICH fragment encoding. --- M17Control.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/M17Control.cpp b/M17Control.cpp index 661889b..90dbfad 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -384,14 +384,14 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; m_netLICH.getFragment(lich, m_rfFN); - unsigned int lich1, lich2, lich3, lich4; - CM17Utils::splitFragmentLICH(lich, lich1, lich2, lich3, lich4); + unsigned int frag1, frag2, frag3, frag4; + CM17Utils::splitFragmentLICH(lich, frag1, frag2, frag3, frag4); // Add Golay to the LICH fragment here - CGolay24128::encode24128(lich1); - CGolay24128::encode24128(lich2); - CGolay24128::encode24128(lich3); - CGolay24128::encode24128(lich4); + unsigned int lich1 = CGolay24128::encode24128(frag1); + unsigned int lich2 = CGolay24128::encode24128(frag2); + unsigned int lich3 = CGolay24128::encode24128(frag3); + unsigned int lich4 = CGolay24128::encode24128(frag4); CM17Utils::combineFragmentLICHFEC(lich1, lich2, lich3, lich4, rfData + 2U + M17_SYNC_LENGTH_BYTES); @@ -595,14 +595,14 @@ void CM17Control::writeNetwork() unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; m_netLICH.getFragment(lich, fn); - unsigned int lich1, lich2, lich3, lich4; - CM17Utils::splitFragmentLICH(lich, lich1, lich2, lich3, lich4); + unsigned int frag1, frag2, frag3, frag4; + CM17Utils::splitFragmentLICH(lich, frag1, frag2, frag3, frag4); // Add Golay to the LICH fragment here - CGolay24128::encode24128(lich1); - CGolay24128::encode24128(lich2); - CGolay24128::encode24128(lich3); - CGolay24128::encode24128(lich4); + unsigned int lich1 = CGolay24128::encode24128(frag1); + unsigned int lich2 = CGolay24128::encode24128(frag2); + unsigned int lich3 = CGolay24128::encode24128(frag3); + unsigned int lich4 = CGolay24128::encode24128(frag4); CM17Utils::combineFragmentLICHFEC(lich1, lich2, lich3, lich4, data + 2U + M17_SYNC_LENGTH_BYTES); From 2ff0e52558d4a8a4288bc294b999dc0b3dbf162c Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 25 Oct 2020 12:48:23 +0000 Subject: [PATCH 138/163] Add M17 BER debug info. --- M17Control.cpp | 6 +++++- Version.h | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/M17Control.cpp b/M17Control.cpp index 90dbfad..058cb22 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -400,10 +400,14 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) // Calculate the BER if (valid) { + unsigned int errors = 0U; for (unsigned int i = 2U; i < (M17_FRAME_LENGTH_BYTES + 2U); i++) - m_rfErrs += countBits(rfData[i] ^ data[i]); + errors += countBits(rfData[i] ^ data[i]); + + LogDebug("M17, FN. %u, errs: %u/384 (%.1f%%)", m_rfFN, errors, float(errors) / 3.84F); m_rfBits += M17_FRAME_LENGTH_BITS; + m_rfErrs += errors; float ber = float(m_rfErrs) / float(m_rfBits); m_display->writeM17BER(ber); diff --git a/Version.h b/Version.h index 7335272..92f4910 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201021"; +const char* VERSION = "20201025"; #endif From 9c13baef828284e2044412540e61b39fb6f2c1c5 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 25 Oct 2020 13:15:51 +0000 Subject: [PATCH 139/163] Remove the CRC processing from the networking side. --- M17Network.cpp | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/M17Network.cpp b/M17Network.cpp index 281413e..3517355 100644 --- a/M17Network.cpp +++ b/M17Network.cpp @@ -20,7 +20,6 @@ #include "M17Defines.h" #include "M17Utils.h" #include "Defines.h" -#include "M17CRC.h" #include "Utils.h" #include "Log.h" @@ -125,8 +124,9 @@ bool CM17Network::write(const unsigned char* data) ::memcpy(buffer + 6U, data, 46U); - // Add the CRC - CM17CRC::encodeCRC(buffer + 6U, M17_LICH_LENGTH_BYTES + M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); + // Dummy CRC + buffer[52U] = 0x00U; + buffer[53U] = 0x00U; if (m_debug) CUtils::dump(1U, "M17 data transmitted", buffer, 54U); @@ -212,17 +212,10 @@ void CM17Network::clock(unsigned int ms) return; } - // Check the CRC - bool valid = CM17CRC::checkCRC(buffer + 6U, M17_LICH_LENGTH_BYTES + M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); - if (!valid) { - LogMessage("M17, network packet received with an invalid CRC"); - return; - } - unsigned char c = length - 6U; m_buffer.addData(&c, 1U); - m_buffer.addData(buffer + 6U, length - 8U); + m_buffer.addData(buffer + 6U, length - 6U); } bool CM17Network::read(unsigned char* data) From d0e85741875290213c405da7ebb72bb0598c0f86 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 26 Oct 2020 09:39:16 +0000 Subject: [PATCH 140/163] Handle the return value for M17 RF transmissions. --- M17Control.cpp | 6 +++++- Version.h | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/M17Control.cpp b/M17Control.cpp index 058cb22..bca351f 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -332,6 +332,8 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) writeQueueRF(data); } + + // Fall through to the next section } } } @@ -444,6 +446,8 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%", source.c_str(), dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); writeEndRF(); } + + return true; } if (m_rfState == RS_RF_REJECTED) { @@ -462,7 +466,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) return false; } - return true; + return false; } unsigned int CM17Control::readModem(unsigned char* data) diff --git a/Version.h b/Version.h index 92f4910..acc024f 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201025"; +const char* VERSION = "20201026"; #endif From 31002c275792d35e5648b32e6165bbc811ab48b4 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 26 Oct 2020 10:10:31 +0000 Subject: [PATCH 141/163] Allow encrypted M17 data to be rejected. --- Conf.cpp | 8 ++++++++ Conf.h | 2 ++ M17Control.cpp | 22 +++++++++++++++++++--- M17Control.h | 3 ++- M17Defines.h | 4 ++++ M17LICH.cpp | 5 +++++ M17LICH.h | 2 ++ MMDVMHost.cpp | 10 ++++++---- 8 files changed, 48 insertions(+), 8 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index 9038b28..f47a8a6 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -169,6 +169,7 @@ m_nxdnTXHang(5U), m_nxdnModeHang(10U), m_m17Enabled(false), m_m17SelfOnly(false), +m_m17AllowEncryption(false), m_m17TXHang(5U), m_m17ModeHang(10U), m_pocsagEnabled(false), @@ -706,6 +707,8 @@ bool CConf::read() m_m17Enabled = ::atoi(value) == 1; else if (::strcmp(key, "SelfOnly") == 0) m_m17SelfOnly = ::atoi(value) == 1; + else if (::strcmp(key, "AllowEncryption") == 0) + m_m17AllowEncryption = ::atoi(value) == 1; else if (::strcmp(key, "TXHang") == 0) m_m17TXHang = (unsigned int)::atoi(value); else if (::strcmp(key, "ModeHang") == 0) @@ -1516,6 +1519,11 @@ bool CConf::getM17SelfOnly() const return m_m17SelfOnly; } +bool CConf::getM17AllowEncryption() const +{ + return m_m17AllowEncryption; +} + unsigned int CConf::getM17TXHang() const { return m_m17TXHang; diff --git a/Conf.h b/Conf.h index 9470746..435c049 100644 --- a/Conf.h +++ b/Conf.h @@ -164,6 +164,7 @@ public: // The M17 section bool getM17Enabled() const; bool getM17SelfOnly() const; + bool getM17AllowEncryption() const; unsigned int getM17TXHang() const; unsigned int getM17ModeHang() const; @@ -440,6 +441,7 @@ private: bool m_m17Enabled; bool m_m17SelfOnly; + bool m_m17AllowEncryption; unsigned int m_m17TXHang; unsigned int m_m17ModeHang; diff --git a/M17Control.cpp b/M17Control.cpp index bca351f..060f4ef 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -57,9 +57,10 @@ const unsigned char BIT_MASK_TABLE[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04 #define WRITE_BIT(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) #define READ_BIT(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) -CM17Control::CM17Control(const std::string& callsign, bool selfOnly, CM17Network* network, CDisplay* display, unsigned int timeout, bool duplex, CRSSIInterpolator* rssiMapper) : +CM17Control::CM17Control(const std::string& callsign, bool selfOnly, bool allowEncryption, CM17Network* network, CDisplay* display, unsigned int timeout, bool duplex, CRSSIInterpolator* rssiMapper) : m_callsign(callsign), m_selfOnly(selfOnly), +m_allowEncryption(allowEncryption), m_network(network), m_display(display), m_duplex(duplex), @@ -189,6 +190,15 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) } } + if (!m_allowEncryption) { + bool ret = m_rfLICH.isNONCENull(); + if (!ret) { + LogMessage("M17, invalid access attempt from %s to %s", source.c_str(), dest.c_str()); + m_rfState = RS_RF_REJECTED; + return false; + } + } + unsigned char dataType = m_rfLICH.getDataType(); switch (dataType) { case 1U: @@ -531,9 +541,15 @@ void CM17Control::writeNetwork() m_networkWatchdog.start(); - if (m_netState == RS_NET_IDLE) { - m_netLICH.setNetwork(netData); + m_netLICH.setNetwork(netData); + if (!m_allowEncryption) { + bool ret = m_rfLICH.isNONCENull(); + if (!ret) + return; + } + + if (m_netState == RS_NET_IDLE) { std::string source = m_netLICH.getSource(); std::string dest = m_netLICH.getDest(); diff --git a/M17Control.h b/M17Control.h index 71c402d..bff2d19 100644 --- a/M17Control.h +++ b/M17Control.h @@ -34,7 +34,7 @@ class CM17Control { public: - CM17Control(const std::string& callsign, bool selfOnly, CM17Network* network, CDisplay* display, unsigned int timeout, bool duplex, CRSSIInterpolator* rssiMapper); + CM17Control(const std::string& callsign, bool selfOnly, bool allowEncryption, CM17Network* network, CDisplay* display, unsigned int timeout, bool duplex, CRSSIInterpolator* rssiMapper); ~CM17Control(); bool writeModem(unsigned char* data, unsigned int len); @@ -50,6 +50,7 @@ public: private: std::string m_callsign; bool m_selfOnly; + bool m_allowEncryption; CM17Network* m_network; CDisplay* m_display; bool m_duplex; diff --git a/M17Defines.h b/M17Defines.h index ed530a4..53b5f63 100644 --- a/M17Defines.h +++ b/M17Defines.h @@ -40,6 +40,10 @@ const unsigned int M17_LICH_FRAGMENT_FEC_LENGTH_BYTES = M17_LICH_FRAGMENT_FEC_LE const unsigned int M17_PAYLOAD_LENGTH_BITS = 128U; const unsigned int M17_PAYLOAD_LENGTH_BYTES = M17_PAYLOAD_LENGTH_BITS / 8U; +const unsigned char M17_NULL_NONCE[] = {0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; +const unsigned int M17_NONCE_LENGTH_BITS = 112U; +const unsigned int M17_NONCE_LENGTH_BYTES = M17_NONCE_LENGTH_BITS / 8U; + const unsigned int M17_FN_LENGTH_BITS = 16U; const unsigned int M17_FN_LENGTH_BYTES = M17_FN_LENGTH_BITS / 8U; diff --git a/M17LICH.cpp b/M17LICH.cpp index ed2b870..0c0469a 100644 --- a/M17LICH.cpp +++ b/M17LICH.cpp @@ -89,6 +89,11 @@ void CM17LICH::setDataType(unsigned char type) m_lich[13U] |= (type << 1) & 0x06U; } +bool CM17LICH::isNONCENull() const +{ + return ::memcmp(m_lich + 14U, M17_NULL_NONCE, M17_NONCE_LENGTH_BYTES) == 0; +} + void CM17LICH::reset() { ::memset(m_lich, 0x00U, 30U); diff --git a/M17LICH.h b/M17LICH.h index 14e3a2d..8842e22 100644 --- a/M17LICH.h +++ b/M17LICH.h @@ -38,6 +38,8 @@ public: unsigned char getDataType() const; void setDataType(unsigned char type); + bool isNONCENull() const; + void reset(); bool isValid() const; diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index b665905..2015516 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -610,16 +610,18 @@ int CMMDVMHost::run() } if (m_m17Enabled) { - bool selfOnly = m_conf.getM17SelfOnly(); - unsigned int txHang = m_conf.getM17TXHang(); - m_m17RFModeHang = m_conf.getM17ModeHang(); + bool selfOnly = m_conf.getM17SelfOnly(); + bool allowEncryption = m_conf.getM17AllowEncryption(); + unsigned int txHang = m_conf.getM17TXHang(); + m_m17RFModeHang = m_conf.getM17ModeHang(); LogInfo("M17 RF Parameters"); LogInfo(" Self Only: %s", selfOnly ? "yes" : "no"); + LogInfo(" Allow Encryption: %s", allowEncryption ? "yes" : "no"); LogInfo(" TX Hang: %us", txHang); LogInfo(" Mode Hang: %us", m_m17RFModeHang); - m_m17 = new CM17Control(m_callsign, selfOnly, m_m17Network, m_display, m_timeout, m_duplex, rssi); + m_m17 = new CM17Control(m_callsign, selfOnly, allowEncryption, m_m17Network, m_display, m_timeout, m_duplex, rssi); } CTimer pocsagTimer(1000U, 30U); From 18c8b958299c80310919c2d41def712c6a6af136 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sun, 1 Nov 2020 16:07:36 +0000 Subject: [PATCH 142/163] Convert M17 to use the new M17 Gateway. --- M17Network.cpp | 169 ++++++++----------------------------------------- M17Network.h | 20 +----- MMDVM.ini | 6 +- MMDVMHost.cpp | 4 +- Version.h | 2 +- 5 files changed, 35 insertions(+), 166 deletions(-) diff --git a/M17Network.cpp b/M17Network.cpp index 3517355..6fa65ae 100644 --- a/M17Network.cpp +++ b/M17Network.cpp @@ -29,8 +29,8 @@ const unsigned int BUFFER_LENGTH = 200U; -CM17Network::CM17Network(const std::string& callsign, unsigned int port, bool debug) : -m_socket(port), +CM17Network::CM17Network(unsigned int localPort, const std::string& gwyAddress, unsigned int gwyPort, bool debug) : +m_socket(localPort), m_addr(), m_addrLen(0U), m_debug(debug), @@ -39,69 +39,44 @@ m_outId(0U), m_inId(0U), m_buffer(1000U, "M17 Network"), m_random(), -m_state(M17N_NOTLINKED), -m_encoded(NULL), -m_module(' '), m_timer(1000U, 5U) { + if (CUDPSocket::lookup(gwyAddress, gwyPort, m_addr, m_addrLen) != 0) { + m_addrLen = 0U; + return; + } + std::random_device rd; std::mt19937 mt(rd()); m_random = mt; - - m_encoded = new unsigned char[6U]; - - std::string call = callsign; - call.resize(8U, ' '); - call += "D"; - - CM17Utils::encodeCallsign(call, m_encoded); } CM17Network::~CM17Network() { - delete[] m_encoded; } bool CM17Network::open() { - LogMessage("Opening M17 network connection"); - - return m_socket.open(m_addr); -} - -bool CM17Network::link(const std::string& address, unsigned int port, char module) -{ - if (CUDPSocket::lookup(address, port, m_addr, m_addrLen) != 0) { - m_state = M17N_NOTLINKED; + if (m_addrLen == 0U) { + LogError("M17, unable to resolve the gateway address"); return false; } - m_module = module; + LogMessage("Opening M17 network connection"); - m_state = M17N_LINKING; + bool ret = m_socket.open(m_addr); - sendConnect(); - - m_timer.start(); - - return true; -} - -void CM17Network::unlink() -{ - if (m_state != M17N_LINKED) - return; - - m_state = M17N_UNLINKING; - - sendConnect(); - - m_timer.start(); + if (ret) { + m_timer.start(); + return true; + } else { + return false; + } } bool CM17Network::write(const unsigned char* data) { - if (m_state != M17N_LINKED) + if (m_addrLen != 0U) return false; assert(data != NULL); @@ -138,19 +113,8 @@ void CM17Network::clock(unsigned int ms) { m_timer.clock(ms); if (m_timer.isRunning() && m_timer.hasExpired()) { - switch (m_state) { - case M17N_LINKING: - sendConnect(); - m_timer.start(); - break; - case M17N_UNLINKING: - sendDisconnect(); - m_timer.start(); - break; - default: - m_timer.stop(); - break; - } + sendPing(); + m_timer.start(); } unsigned char buffer[BUFFER_LENGTH]; @@ -169,36 +133,12 @@ void CM17Network::clock(unsigned int ms) if (m_debug) CUtils::dump(1U, "M17 Network Data Received", buffer, length); - if (::memcmp(buffer + 0U, "ACKN", 4U) == 0) { - m_timer.stop(); - m_state = M17N_LINKED; - LogMessage("M17, linked to reflector"); - return; - } - - if (::memcmp(buffer + 0U, "NACK", 4U) == 0) { - m_timer.stop(); - m_state = M17N_NOTLINKED; - LogMessage("M17, link refused by reflector"); - return; - } - - if (::memcmp(buffer + 0U, "DISC", 4U) == 0) { - m_timer.stop(); - m_state = M17N_NOTLINKED; - LogMessage("M17, unlinked from reflector"); - return; - } - - if (::memcmp(buffer + 0U, "PING", 4U) == 0) { - if (m_state == M17N_LINKED) - sendPong(); - return; - } - if (!m_enabled) return; + if (::memcmp(buffer + 0U, "PING", 4U) == 0) + return; + if (::memcmp(buffer + 0U, "M17 ", 4U) != 0) { CUtils::dump(2U, "M17, received unknown packet", buffer, length); return; @@ -254,70 +194,17 @@ void CM17Network::enable(bool enabled) m_enabled = enabled; } -void CM17Network::sendConnect() +void CM17Network::sendPing() { - unsigned char buffer[15U]; - - buffer[0U] = 'C'; - buffer[1U] = 'O'; - buffer[2U] = 'N'; - buffer[3U] = 'N'; - - buffer[4U] = m_encoded[0U]; - buffer[5U] = m_encoded[1U]; - buffer[6U] = m_encoded[2U]; - buffer[7U] = m_encoded[3U]; - buffer[8U] = m_encoded[4U]; - buffer[9U] = m_encoded[5U]; - - buffer[10U] = m_module; - - if (m_debug) - CUtils::dump(1U, "M17 data transmitted", buffer, 11U); - - m_socket.write(buffer, 11U, m_addr, m_addrLen); -} - -void CM17Network::sendPong() -{ - unsigned char buffer[15U]; + unsigned char buffer[5U]; buffer[0U] = 'P'; - buffer[1U] = 'O'; + buffer[1U] = 'I'; buffer[2U] = 'N'; buffer[3U] = 'G'; - buffer[4U] = m_encoded[0U]; - buffer[5U] = m_encoded[1U]; - buffer[6U] = m_encoded[2U]; - buffer[7U] = m_encoded[3U]; - buffer[8U] = m_encoded[4U]; - buffer[9U] = m_encoded[5U]; - if (m_debug) - CUtils::dump(1U, "M17 data transmitted", buffer, 10U); + CUtils::dump(1U, "M17 data transmitted", buffer, 4U); - m_socket.write(buffer, 10U, m_addr, m_addrLen); -} - -void CM17Network::sendDisconnect() -{ - unsigned char buffer[15U]; - - buffer[0U] = 'D'; - buffer[1U] = 'I'; - buffer[2U] = 'S'; - buffer[3U] = 'C'; - - buffer[4U] = m_encoded[0U]; - buffer[5U] = m_encoded[1U]; - buffer[6U] = m_encoded[2U]; - buffer[7U] = m_encoded[3U]; - buffer[8U] = m_encoded[4U]; - buffer[9U] = m_encoded[5U]; - - if (m_debug) - CUtils::dump(1U, "M17 data transmitted", buffer, 10U); - - m_socket.write(buffer, 10U, m_addr, m_addrLen); + m_socket.write(buffer, 4U, m_addr, m_addrLen); } diff --git a/M17Network.h b/M17Network.h index ed6e81c..e65e1f3 100644 --- a/M17Network.h +++ b/M17Network.h @@ -27,24 +27,13 @@ #include #include -enum M17NET_STATUS { - M17N_NOTLINKED, - M17N_LINKING, - M17N_LINKED, - M17N_UNLINKING -}; - class CM17Network { public: - CM17Network(const std::string& callsign, unsigned int port, bool debug); + CM17Network(unsigned int localPort, const std::string& gwyAddress, unsigned int gwyPort, bool debug); ~CM17Network(); bool open(); - bool link(const std::string& address, unsigned int port, char module); - - void unlink(); - void enable(bool enabled); bool write(const unsigned char* data); @@ -67,14 +56,9 @@ private: uint16_t m_inId; CRingBuffer m_buffer; std::mt19937 m_random; - M17NET_STATUS m_state; - unsigned char* m_encoded; - char m_module; CTimer m_timer; - void sendConnect(); - void sendDisconnect(); - void sendPong(); + void sendPing(); }; #endif diff --git a/MMDVM.ini b/MMDVM.ini index 9f3309a..3fb7a23 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -226,9 +226,9 @@ Debug=0 [M17 Network] Enable=1 -GatewayAddress=3.138.122.152 -GatewayPort=17000 -LocalPort=17000 +GatewayAddress=127.0.0.1 +GatewayPort=17010 +LocalPort=17011 # ModeHang=3 Debug=0 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 84ba69f..408e971 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -1556,7 +1556,7 @@ bool CMMDVMHost::createM17Network() LogInfo(" Local Port: %u", localPort); LogInfo(" Mode Hang: %us", m_m17NetModeHang); - m_m17Network = new CM17Network(m_callsign, localPort, debug); + m_m17Network = new CM17Network(localPort, gatewayAddress, gatewayPort, debug); bool ret = m_m17Network->open(); if (!ret) { delete m_m17Network; @@ -1564,8 +1564,6 @@ bool CMMDVMHost::createM17Network() return false; } - m_m17Network->link(gatewayAddress, gatewayPort, 'A'); - m_m17Network->enable(true); return true; diff --git a/Version.h b/Version.h index b85724b..8d5b383 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201031"; +const char* VERSION = "20201101"; #endif From 827a0cb72f3e0cee59e16a89e6c4c4e5861676c8 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 9 Nov 2020 12:17:06 +0000 Subject: [PATCH 143/163] Allow the FM Network code to build on Windows. --- FMNetwork.cpp | 28 +++++++++++++++++++++++----- FMNetwork.h | 4 ++++ Version.h | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/FMNetwork.cpp b/FMNetwork.cpp index bc394c2..bf76ccb 100644 --- a/FMNetwork.cpp +++ b/FMNetwork.cpp @@ -44,18 +44,22 @@ m_pollTimer(1000U, 5U) if (CUDPSocket::lookup(gatewayAddress, gatewayPort, m_addr, m_addrLen) != 0) m_addrLen = 0U; +#if !defined(_WIN32) && !defined(_WIN64) int error; m_incoming = ::src_new(SRC_SINC_FASTEST, 1, &error); m_outgoing = ::src_new(SRC_SINC_FASTEST, 1, &error); assert(m_incoming != NULL); assert(m_outgoing != NULL); +#endif } CFMNetwork::~CFMNetwork() { +#if !defined(_WIN32) && !defined(_WIN64) ::src_delete(m_incoming); ::src_delete(m_outgoing); +#endif } bool CFMNetwork::open() @@ -74,10 +78,12 @@ bool CFMNetwork::open() bool CFMNetwork::writeData(float* data, unsigned int nSamples) { - assert(m_outgoing != NULL); assert(data != NULL); assert(nSamples > 0U); +#if !defined(_WIN32) && !defined(_WIN64) + assert(m_outgoing != NULL); + float out[1000U]; SRC_DATA src; @@ -98,6 +104,7 @@ bool CFMNetwork::writeData(float* data, unsigned int nSamples) src.data_out = data; src.output_frames_gen = nSamples; } +#endif unsigned int length = 3U; @@ -108,8 +115,13 @@ bool CFMNetwork::writeData(float* data, unsigned int nSamples) buffer[1U] = 'M'; buffer[2U] = 'D'; +#if defined(_WIN32) || defined(_WIN64) + for (long i = 0L; i < nSamples; i++) { + unsigned short val = (unsigned short)((data[i] + 1.0F) * 32767.0F + 0.5F); +#else for (long i = 0L; i < src.output_frames_gen; i++) { unsigned short val = (unsigned short)((src.data_out[i] + 1.0F) * 32767.0F + 0.5F); +#endif buffer[length++] = (val >> 8) & 0xFFU; buffer[length++] = (val >> 0) & 0xFFU; @@ -177,7 +189,6 @@ void CFMNetwork::clock(unsigned int ms) unsigned int CFMNetwork::read(float* data, unsigned int nSamples) { - assert(m_incoming != NULL); assert(data != NULL); assert(nSamples > 0U); @@ -191,6 +202,9 @@ unsigned int CFMNetwork::read(float* data, unsigned int nSamples) unsigned char buffer[1500U]; m_buffer.getData(buffer, nSamples * sizeof(unsigned short)); +#if !defined(_WIN32) && !defined(_WIN64) + assert(m_incoming != NULL); + SRC_DATA src; if (m_sampleRate != 8000U) { @@ -217,6 +231,7 @@ unsigned int CFMNetwork::read(float* data, unsigned int nSamples) return src.output_frames_gen; } else { +#endif for (unsigned int i = 0U; i < nSamples; i++) { unsigned short val = ((buffer[i * 2U + 0U] & 0xFFU) << 8) + ((buffer[i * 2U + 1U] & 0xFFU) << 0); @@ -224,18 +239,22 @@ unsigned int CFMNetwork::read(float* data, unsigned int nSamples) } return nSamples; +#if !defined(_WIN32) && !defined(_WIN64) } +#endif } void CFMNetwork::reset() { +#if !defined(_WIN32) && !defined(_WIN64) assert(m_incoming != NULL); assert(m_outgoing != NULL); - m_buffer.clear(); - ::src_reset(m_incoming); ::src_reset(m_outgoing); +#endif + + m_buffer.clear(); } void CFMNetwork::close() @@ -268,4 +287,3 @@ bool CFMNetwork::writePoll() return m_socket.write(buffer, 3U, m_addr, m_addrLen); } - diff --git a/FMNetwork.h b/FMNetwork.h index c4314fd..aa99751 100644 --- a/FMNetwork.h +++ b/FMNetwork.h @@ -23,7 +23,9 @@ #include "UDPSocket.h" #include "Timer.h" +#if !defined(_WIN32) && !defined(_WIN64) #include +#endif #include #include @@ -58,8 +60,10 @@ private: bool m_enabled; CRingBuffer m_buffer; CTimer m_pollTimer; +#if !defined(_WIN32) && !defined(_WIN64) SRC_STATE* m_incoming; SRC_STATE* m_outgoing; +#endif bool writePoll(); }; diff --git a/Version.h b/Version.h index d4a354b..1331f35 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201106"; +const char* VERSION = "20201109"; #endif From 304143d4a64b33a229ac07cb1bd1ddf07432f2c8 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 9 Nov 2020 14:22:43 +0000 Subject: [PATCH 144/163] Handle protocol versions 1 and 2. --- SerialModem.cpp | 318 +++++++++++++++++++++++++++++++++++++++--------- SerialModem.h | 4 +- 2 files changed, 266 insertions(+), 56 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index 24d88e3..6ca7771 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -112,11 +112,10 @@ const unsigned int MAX_RESPONSES = 30U; const unsigned int BUFFER_LENGTH = 2000U; -const unsigned char PROTOCOL_VERSION = 2U; - CSerialModem::CSerialModem(const std::string& port, bool duplex, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int dmrDelay, bool useCOSAsLockout, bool trace, bool debug) : m_port(port), +m_protocolVersion(0U), m_dmrColorCode(0U), m_ysfLoDev(false), m_ysfTXHang(4U), @@ -373,7 +372,7 @@ bool CSerialModem::open() return false; } - ret = setConfig(); + ret = writeConfig(); if (!ret) { m_serial->close(); delete m_serial; @@ -737,48 +736,102 @@ void CSerialModem::clock(unsigned int ms) } break; - case MMDVM_GET_STATUS: { - // if (m_trace) - // CUtils::dump(1U, "GET_STATUS", m_buffer, m_length); + case MMDVM_GET_STATUS: + // if (m_trace) + // CUtils::dump(1U, "GET_STATUS", m_buffer, m_length); - m_mode = m_buffer[m_offset + 0U]; + switch (m_protocolVersion) { + case 1U: { + m_mode = m_buffer[m_offset + 1U]; - m_tx = (m_buffer[m_offset + 1U] & 0x01U) == 0x01U; + m_tx = (m_buffer[m_offset + 2U] & 0x01U) == 0x01U; + bool adcOverflow = (m_buffer[m_offset + 2U] & 0x02U) == 0x02U; + if (adcOverflow) + LogError("MMDVM ADC levels have overflowed"); + bool rxOverflow = (m_buffer[m_offset + 2U] & 0x04U) == 0x04U; + if (rxOverflow) + LogError("MMDVM RX buffer has overflowed"); + bool txOverflow = (m_buffer[m_offset + 2U] & 0x08U) == 0x08U; + if (txOverflow) + LogError("MMDVM TX buffer has overflowed"); + m_lockout = (m_buffer[m_offset + 2U] & 0x10U) == 0x10U; + bool dacOverflow = (m_buffer[m_offset + 2U] & 0x20U) == 0x20U; + if (dacOverflow) + LogError("MMDVM DAC levels have overflowed"); + m_cd = (m_buffer[m_offset + 2U] & 0x40U) == 0x40U; - bool adcOverflow = (m_buffer[m_offset + 1U] & 0x02U) == 0x02U; - if (adcOverflow) - LogError("MMDVM ADC levels have overflowed"); + m_p25Space = 0U; + m_nxdnSpace = 0U; + m_m17Space = 0U; + m_pocsagSpace = 0U; + m_fmSpace = 0U; + m_ax25Space = 0U; - bool rxOverflow = (m_buffer[m_offset + 1U] & 0x04U) == 0x04U; - if (rxOverflow) - LogError("MMDVM RX buffer has overflowed"); + m_dstarSpace = m_buffer[m_offset + 3U]; + m_dmrSpace1 = m_buffer[m_offset + 4U]; + m_dmrSpace2 = m_buffer[m_offset + 5U]; + m_ysfSpace = m_buffer[m_offset + 6U]; - bool txOverflow = (m_buffer[m_offset + 1U] & 0x08U) == 0x08U; - if (txOverflow) - LogError("MMDVM TX buffer has overflowed"); + // The following depend on the version of the firmware + if (m_length > (m_offset + 7U)) + m_p25Space = m_buffer[m_offset + 7U]; + if (m_length > (m_offset + 8U)) + m_nxdnSpace = m_buffer[m_offset + 8U]; + if (m_length > (m_offset + 9U)) + m_pocsagSpace = m_buffer[m_offset + 9U]; + if (m_length > (m_offset + 10U)) + m_m17Space = m_buffer[m_offset + 10U]; + } + break; - m_lockout = (m_buffer[m_offset + 1U] & 0x10U) == 0x10U; + case 2U: { + m_mode = m_buffer[m_offset + 0U]; - bool dacOverflow = (m_buffer[m_offset + 1U] & 0x20U) == 0x20U; - if (dacOverflow) - LogError("MMDVM DAC levels have overflowed"); + m_tx = (m_buffer[m_offset + 1U] & 0x01U) == 0x01U; + bool adcOverflow = (m_buffer[m_offset + 1U] & 0x02U) == 0x02U; + if (adcOverflow) + LogError("MMDVM ADC levels have overflowed"); + bool rxOverflow = (m_buffer[m_offset + 1U] & 0x04U) == 0x04U; + if (rxOverflow) + LogError("MMDVM RX buffer has overflowed"); + bool txOverflow = (m_buffer[m_offset + 1U] & 0x08U) == 0x08U; + if (txOverflow) + LogError("MMDVM TX buffer has overflowed"); + m_lockout = (m_buffer[m_offset + 1U] & 0x10U) == 0x10U; + bool dacOverflow = (m_buffer[m_offset + 1U] & 0x20U) == 0x20U; + if (dacOverflow) + LogError("MMDVM DAC levels have overflowed"); + m_cd = (m_buffer[m_offset + 1U] & 0x40U) == 0x40U; - m_cd = (m_buffer[m_offset + 1U] & 0x40U) == 0x40U; + m_dstarSpace = m_buffer[m_offset + 3U]; + m_dmrSpace1 = m_buffer[m_offset + 4U]; + m_dmrSpace2 = m_buffer[m_offset + 5U]; + m_ysfSpace = m_buffer[m_offset + 6U]; + m_p25Space = m_buffer[m_offset + 7U]; + m_nxdnSpace = m_buffer[m_offset + 8U]; + m_m17Space = m_buffer[m_offset + 9U]; + m_fmSpace = m_buffer[m_offset + 10U]; + m_pocsagSpace = m_buffer[m_offset + 11U]; + m_ax25Space = m_buffer[m_offset + 12U]; + } + break; - m_dstarSpace = m_buffer[m_offset + 2U]; - m_dmrSpace1 = m_buffer[m_offset + 3U]; - m_dmrSpace2 = m_buffer[m_offset + 4U]; - m_ysfSpace = m_buffer[m_offset + 5U]; - m_p25Space = m_buffer[m_offset + 6U]; - m_nxdnSpace = m_buffer[m_offset + 7U]; - m_m17Space = m_buffer[m_offset + 8U]; - m_pocsagSpace = m_buffer[m_offset + 9U]; - m_fmSpace = m_buffer[m_offset + 10U]; - m_ax25Space = m_buffer[m_offset + 11U]; - - m_inactivityTimer.start(); - // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[m_offset + 2U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_m17Space, m_pocsagSpace, m_fmSpace, m_ax25Space, int(m_lockout), int(m_cd)); + default: + m_dstarSpace = 0U; + m_dmrSpace1 = 0U; + m_dmrSpace2 = 0U; + m_ysfSpace = 0U; + m_p25Space = 0U; + m_nxdnSpace = 0U; + m_m17Space = 0U; + m_pocsagSpace = 0U; + m_fmSpace = 0U; + m_ax25Space = 0U; + break; } + + m_inactivityTimer.start(); + // LogMessage("status=%02X, tx=%d, space=%u,%u,%u,%u,%u,%u,%u,%u,%u,%u lockout=%d, cd=%d", m_buffer[m_offset + 2U], int(m_tx), m_dstarSpace, m_dmrSpace1, m_dmrSpace2, m_ysfSpace, m_p25Space, m_nxdnSpace, m_m17Space, m_pocsagSpace, m_fmSpace, m_ax25Space, int(m_lockout), int(m_cd)); break; case MMDVM_TRANSPARENT: { @@ -1846,10 +1899,33 @@ bool CSerialModem::readVersion() else if (::memcmp(m_buffer + 4U, "SkyBridge", 9U) == 0) m_hwType = HWT_SKYBRIDGE; - LogInfo("MMDVM protocol version: %u, description: %.*s", m_buffer[3U], m_length - 4U, m_buffer + 4U); + m_protocolVersion = m_buffer[3U]; - if (m_buffer[3U] != PROTOCOL_VERSION) { - LogError("Invalid protocol version for this MMDVM host"); + switch (m_protocolVersion) { + case 1U: + LogInfo("MMDVM protocol version: %u, description: %.*s", m_protocolVersion, m_length - 4U, m_buffer + 4U); + return true; + + case 2U: + LogInfo("MMDVM protocol version: %u, description: %.*s", m_protocolVersion, m_length - 21U, m_buffer + 21U); + switch (m_buffer[4U]) { + case 0U: + LogInfo("Atmel ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[5U], m_buffer[6U], m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U], m_buffer[19U], m_buffer[20U]); + break; + case 1U: + LogInfo("NXP ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[5U], m_buffer[6U], m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U], m_buffer[19U], m_buffer[20U]); + break; + case 2U: + LogInfo("ST-Micro ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[5U], m_buffer[6U], m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U]); + break; + default: + LogInfo("Unknown CPU type: %u", m_buffer[4U]); + break; + } + return true; + + default: + LogError("MMDVM protocol version: %u, unsupported by this version of the MMDVM Host", m_protocolVersion); return false; } @@ -1882,18 +1958,140 @@ bool CSerialModem::readStatus() bool CSerialModem::writeConfig() { - return setConfig(); + switch (m_protocolVersion) { + case 1U: + return setConfig1(); + case 2U: + return setConfig2(); + default: + return false; + } } -bool CSerialModem::setConfig() +bool CSerialModem::setConfig1() { assert(m_serial != NULL); - unsigned char buffer[40U]; + unsigned char buffer[30U]; buffer[0U] = MMDVM_FRAME_START; - buffer[1U] = 31U; + buffer[1U] = 26U; + + buffer[2U] = MMDVM_SET_CONFIG; + + buffer[3U] = 0x00U; + if (m_rxInvert) + buffer[3U] |= 0x01U; + if (m_txInvert) + buffer[3U] |= 0x02U; + if (m_pttInvert) + buffer[3U] |= 0x04U; + if (m_ysfLoDev) + buffer[3U] |= 0x08U; + if (m_debug) + buffer[3U] |= 0x10U; + if (m_useCOSAsLockout) + buffer[3U] |= 0x20U; + if (!m_duplex) + buffer[3U] |= 0x80U; + + buffer[4U] = 0x00U; + if (m_dstarEnabled) + buffer[4U] |= 0x01U; + if (m_dmrEnabled) + buffer[4U] |= 0x02U; + if (m_ysfEnabled) + buffer[4U] |= 0x04U; + if (m_p25Enabled) + buffer[4U] |= 0x08U; + if (m_nxdnEnabled) + buffer[4U] |= 0x10U; + if (m_pocsagEnabled) + buffer[4U] |= 0x20U; + if (m_m17Enabled) + buffer[4U] |= 0x40U; + + buffer[5U] = m_txDelay / 10U; // In 10ms units + + buffer[6U] = MODE_IDLE; + + buffer[7U] = (unsigned char)(m_rxLevel * 2.55F + 0.5F); + + buffer[8U] = (unsigned char)(m_cwIdTXLevel * 2.55F + 0.5F); + + buffer[9U] = m_dmrColorCode; + + buffer[10U] = m_dmrDelay; + + buffer[11U] = 128U; // Was OscOffset + + buffer[12U] = (unsigned char)(m_dstarTXLevel * 2.55F + 0.5F); + buffer[13U] = (unsigned char)(m_dmrTXLevel * 2.55F + 0.5F); + buffer[14U] = (unsigned char)(m_ysfTXLevel * 2.55F + 0.5F); + buffer[15U] = (unsigned char)(m_p25TXLevel * 2.55F + 0.5F); + + buffer[16U] = (unsigned char)(m_txDCOffset + 128); + buffer[17U] = (unsigned char)(m_rxDCOffset + 128); + + buffer[18U] = (unsigned char)(m_nxdnTXLevel * 2.55F + 0.5F); + + buffer[19U] = (unsigned char)m_ysfTXHang; + + buffer[20U] = (unsigned char)(m_pocsagTXLevel * 2.55F + 0.5F); + + buffer[21U] = (unsigned char)(m_fmTXLevel * 2.55F + 0.5F); + + buffer[22U] = (unsigned char)m_p25TXHang; + + buffer[23U] = (unsigned char)m_nxdnTXHang; + + buffer[24U] = (unsigned char)(m_m17TXLevel * 2.55F + 0.5F); + + buffer[25U] = (unsigned char)m_m17TXHang; + + // CUtils::dump(1U, "Written", buffer, 26U); + + int ret = m_serial->write(buffer, 26U); + if (ret != 26) + return false; + + unsigned int count = 0U; + RESP_TYPE_MMDVM resp; + do { + CThread::sleep(10U); + + resp = getResponse(); + if (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK) { + count++; + if (count >= MAX_RESPONSES) { + LogError("The MMDVM is not responding to the SET_CONFIG command"); + return false; + } + } + } while (resp == RTM_OK && m_buffer[2U] != MMDVM_ACK && m_buffer[2U] != MMDVM_NAK); + + // CUtils::dump(1U, "Response", m_buffer, m_length); + + if (resp == RTM_OK && m_buffer[2U] == MMDVM_NAK) { + LogError("Received a NAK to the SET_CONFIG command from the modem"); + return false; + } + + m_playoutTimer.start(); + + return true; +} + +bool CSerialModem::setConfig2() +{ + assert(m_serial != NULL); + + unsigned char buffer[50U]; + + buffer[0U] = MMDVM_FRAME_START; + + buffer[1U] = 40U; buffer[2U] = MMDVM_SET_CONFIG; @@ -1954,24 +2152,34 @@ bool CSerialModem::setConfig() buffer[18U] = (unsigned char)(m_pocsagTXLevel * 2.55F + 0.5F); buffer[19U] = (unsigned char)(m_fmTXLevel * 2.55F + 0.5F); buffer[20U] = (unsigned char)(m_ax25TXLevel * 2.55F + 0.5F); + buffer[21U] = 0x00U; + buffer[22U] = 0x00U; - buffer[21U] = (unsigned char)m_ysfTXHang; - buffer[22U] = (unsigned char)m_p25TXHang; - buffer[23U] = (unsigned char)m_nxdnTXHang; - buffer[24U] = (unsigned char)m_m17TXHang; + buffer[23U] = (unsigned char)m_ysfTXHang; + buffer[24U] = (unsigned char)m_p25TXHang; + buffer[25U] = (unsigned char)m_nxdnTXHang; + buffer[26U] = (unsigned char)m_m17TXHang; + buffer[27U] = 0x00U; + buffer[28U] = 0x00U; - buffer[25U] = m_dmrColorCode; - buffer[26U] = m_dmrDelay; + buffer[29U] = m_dmrColorCode; + buffer[30U] = m_dmrDelay; - buffer[27U] = (unsigned char)(m_ax25RXTwist + 128); - buffer[28U] = m_ax25TXDelay / 10U; // In 10ms units - buffer[29U] = m_ax25SlotTime / 10U; // In 10ms units - buffer[30U] = m_ax25PPersist; + buffer[31U] = (unsigned char)(m_ax25RXTwist + 128); + buffer[32U] = m_ax25TXDelay / 10U; // In 10ms units + buffer[33U] = m_ax25SlotTime / 10U; // In 10ms units + buffer[34U] = m_ax25PPersist; - // CUtils::dump(1U, "Written", buffer, 31U); + buffer[35U] = 0x00U; + buffer[36U] = 0x00U; + buffer[37U] = 0x00U; + buffer[38U] = 0x00U; + buffer[39U] = 0x00U; - int ret = m_serial->write(buffer, 31U); - if (ret != 31) + // CUtils::dump(1U, "Written", buffer, 40U); + + int ret = m_serial->write(buffer, 40U); + if (ret != 40) return false; unsigned int count = 0U; diff --git a/SerialModem.h b/SerialModem.h index 073cb16..45bfea7 100644 --- a/SerialModem.h +++ b/SerialModem.h @@ -138,6 +138,7 @@ public: private: std::string m_port; + unsigned int m_protocolVersion; unsigned int m_dmrColorCode; bool m_ysfLoDev; unsigned int m_ysfTXHang; @@ -268,7 +269,8 @@ private: bool readVersion(); bool readStatus(); - bool setConfig(); + bool setConfig1(); + bool setConfig2(); bool setFrequency(); bool setFMCallsignParams(); bool setFMAckParams(); From 3d489096cd7a5fa5ce770e3c9dc620cf028eb2cd Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 9 Nov 2020 16:23:33 +0000 Subject: [PATCH 145/163] Add the modem capabilities decoding. --- SerialModem.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index 6ca7771..4255284 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -1907,21 +1907,30 @@ bool CSerialModem::readVersion() return true; case 2U: - LogInfo("MMDVM protocol version: %u, description: %.*s", m_protocolVersion, m_length - 21U, m_buffer + 21U); - switch (m_buffer[4U]) { + LogInfo("MMDVM protocol version: %u, description: %.*s", m_protocolVersion, m_length - 23U, m_buffer + 23U); + switch (m_buffer[6U]) { case 0U: - LogInfo("Atmel ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[5U], m_buffer[6U], m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U], m_buffer[19U], m_buffer[20U]); + LogInfo("Atmel ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U], m_buffer[19U], m_buffer[20U], m_buffer[21U], m_buffer[22U]); break; case 1U: - LogInfo("NXP ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[5U], m_buffer[6U], m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U], m_buffer[19U], m_buffer[20U]); + LogInfo("NXP ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U], m_buffer[19U], m_buffer[20U], m_buffer[21U], m_buffer[22U]); break; case 2U: - LogInfo("ST-Micro ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[5U], m_buffer[6U], m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U]); + LogInfo("ST-Micro ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U]); break; default: - LogInfo("Unknown CPU type: %u", m_buffer[4U]); + LogInfo("Unknown CPU type: %u", m_buffer[6U]); break; } + LogInfo("D-Star: %s", (m_buffer[4U] & 0x01U) == 0x01U ? "yes" : "no"); + LogInfo("DMR: %s", (m_buffer[4U] & 0x02U) == 0x02U ? "yes" : "no"); + LogInfo("YSF: %s", (m_buffer[4U] & 0x04U) == 0x04U ? "yes" : "no"); + LogInfo("P25: %s", (m_buffer[4U] & 0x08U) == 0x08U ? "yes" : "no"); + LogInfo("NXDN: %s", (m_buffer[4U] & 0x10U) == 0x10U ? "yes" : "no"); + LogInfo("M17: %s", (m_buffer[4U] & 0x20U) == 0x20U ? "yes" : "no"); + LogInfo("FM: %s", (m_buffer[4U] & 0x40U) == 0x40U ? "yes" : "no"); + LogInfo("POCSAG: %s", (m_buffer[5U] & 0x01U) == 0x01U ? "yes" : "no"); + LogInfo("AX.25: %s", (m_buffer[5U] & 0x02U) == 0x02U ? "yes" : "no"); return true; default: From f1dda251c345da46cb4de3f4b2efbe5ba839df5f Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 10 Nov 2020 09:40:54 +0000 Subject: [PATCH 146/163] Display the modes in the log more nicely. --- SerialModem.cpp | 30 +++++++++++++++++++++--------- Version.h | 2 +- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index 4255284..3f91e97 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -1922,15 +1922,27 @@ bool CSerialModem::readVersion() LogInfo("Unknown CPU type: %u", m_buffer[6U]); break; } - LogInfo("D-Star: %s", (m_buffer[4U] & 0x01U) == 0x01U ? "yes" : "no"); - LogInfo("DMR: %s", (m_buffer[4U] & 0x02U) == 0x02U ? "yes" : "no"); - LogInfo("YSF: %s", (m_buffer[4U] & 0x04U) == 0x04U ? "yes" : "no"); - LogInfo("P25: %s", (m_buffer[4U] & 0x08U) == 0x08U ? "yes" : "no"); - LogInfo("NXDN: %s", (m_buffer[4U] & 0x10U) == 0x10U ? "yes" : "no"); - LogInfo("M17: %s", (m_buffer[4U] & 0x20U) == 0x20U ? "yes" : "no"); - LogInfo("FM: %s", (m_buffer[4U] & 0x40U) == 0x40U ? "yes" : "no"); - LogInfo("POCSAG: %s", (m_buffer[5U] & 0x01U) == 0x01U ? "yes" : "no"); - LogInfo("AX.25: %s", (m_buffer[5U] & 0x02U) == 0x02U ? "yes" : "no"); + char modeText[10U]; + ::strcpy(modeText, "Modes:"); + if ((m_buffer[4U] & 0x01U) == 0x01U) + ::strcat(modeText, " D-Star"); + if ((m_buffer[4U] & 0x02U) == 0x02U) + ::strcat(modeText, " DMR"); + if ((m_buffer[4U] & 0x04U) == 0x04U) + ::strcat(modeText, " YSF"); + if ((m_buffer[4U] & 0x08U) == 0x08U) + ::strcat(modeText, " P25"); + if ((m_buffer[4U] & 0x10U) == 0x10U) + ::strcat(modeText, " NXDN"); + if ((m_buffer[4U] & 0x20U) == 0x20U) + ::strcat(modeText, " M17"); + if ((m_buffer[4U] & 0x40U) == 0x40U) + ::strcat(modeText, " FM"); + if ((m_buffer[5U] & 0x01U) == 0x01U) + ::strcat(modeText, " POCSAG"); + if ((m_buffer[5U] & 0x02U) == 0x02U) + ::strcat(modeText, " AX.25"); + LogInfo(modeText); return true; default: diff --git a/Version.h b/Version.h index 1331f35..5fe7f6f 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201109"; +const char* VERSION = "20201110"; #endif From f89145b7720c8f8377a6f9d18e50045f64d1e363 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 10 Nov 2020 09:58:10 +0000 Subject: [PATCH 147/163] Remove support for the UMP (sob). --- Conf.cpp | 20 --- Conf.h | 7 - Display.cpp | 14 +- Display.h | 3 +- MMDVM.ini | 5 - MMDVMHost.cpp | 63 +-------- MMDVMHost.h | 2 - MMDVMHost.vcxproj | 2 - MMDVMHost.vcxproj.filters | 8 +- Makefile | 2 +- Makefile.Pi | 2 +- Makefile.Pi.Adafruit | 2 +- Makefile.Pi.HD44780 | 2 +- Makefile.Pi.OLED | 2 +- Makefile.Pi.PCF8574 | 2 +- README.md | 10 +- UMP.cpp | 283 -------------------------------------- UMP.h | 63 --------- UMP/UMP.ino | 203 --------------------------- 19 files changed, 20 insertions(+), 675 deletions(-) delete mode 100644 UMP.cpp delete mode 100644 UMP.h delete mode 100644 UMP/UMP.ino diff --git a/Conf.cpp b/Conf.cpp index fcfbbfe..122be9a 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -37,7 +37,6 @@ enum SECTION { SECTION_NXDNID_LOOKUP, SECTION_MODEM, SECTION_TRANSPARENT, - SECTION_UMP, SECTION_DSTAR, SECTION_DMR, SECTION_FUSION, @@ -122,8 +121,6 @@ m_transparentRemoteAddress(), m_transparentRemotePort(0U), m_transparentLocalPort(0U), m_transparentSendFrameType(0U), -m_umpEnabled(false), -m_umpPort(), m_dstarEnabled(false), m_dstarModule("C"), m_dstarSelfOnly(false), @@ -360,8 +357,6 @@ bool CConf::read() section = SECTION_MODEM; else if (::strncmp(buffer, "[Transparent Data]", 18U) == 0) section = SECTION_TRANSPARENT; - else if (::strncmp(buffer, "[UMP]", 5U) == 0) - section = SECTION_UMP; else if (::strncmp(buffer, "[D-Star]", 8U) == 0) section = SECTION_DSTAR; else if (::strncmp(buffer, "[DMR]", 5U) == 0) @@ -568,11 +563,6 @@ bool CConf::read() m_transparentLocalPort = (unsigned int)::atoi(value); else if (::strcmp(key, "SendFrameType") == 0) m_transparentSendFrameType = (unsigned int)::atoi(value); - } else if (section == SECTION_UMP) { - if (::strcmp(key, "Enable") == 0) - m_umpEnabled = ::atoi(value) == 1; - else if (::strcmp(key, "Port") == 0) - m_umpPort = value; } else if (section == SECTION_DSTAR) { if (::strcmp(key, "Enable") == 0) m_dstarEnabled = ::atoi(value) == 1; @@ -1358,16 +1348,6 @@ unsigned int CConf::getTransparentSendFrameType() const return m_transparentSendFrameType; } -bool CConf::getUMPEnabled() const -{ - return m_umpEnabled; -} - -std::string CConf::getUMPPort() const -{ - return m_umpPort; -} - bool CConf::getDStarEnabled() const { return m_dstarEnabled; diff --git a/Conf.h b/Conf.h index b14c77d..01d36fd 100644 --- a/Conf.h +++ b/Conf.h @@ -101,10 +101,6 @@ public: unsigned int getTransparentLocalPort() const; unsigned int getTransparentSendFrameType() const; - // The UMP section - bool getUMPEnabled() const; - std::string getUMPPort() const; - // The D-Star section bool getDStarEnabled() const; std::string getDStarModule() const; @@ -417,9 +413,6 @@ private: unsigned int m_transparentLocalPort; unsigned int m_transparentSendFrameType; - bool m_umpEnabled; - std::string m_umpPort; - bool m_dstarEnabled; std::string m_dstarModule; bool m_dstarSelfOnly; diff --git a/Display.cpp b/Display.cpp index eb580bf..e26219f 100644 --- a/Display.cpp +++ b/Display.cpp @@ -28,7 +28,6 @@ #include "CASTInfo.h" #include "Conf.h" #include "Modem.h" -#include "UMP.h" #include "Log.h" #if defined(HD44780) @@ -537,11 +536,11 @@ int CDisplay::writeNXDNIntEx(const class CUserDBentry& source, bool group, unsig /* Factory method extracted from MMDVMHost.cpp - BG5HHP */ -CDisplay* CDisplay::createDisplay(const CConf& conf, CUMP* ump, IModem* modem) +CDisplay* CDisplay::createDisplay(const CConf& conf, IModem* modem) { - CDisplay *display = NULL; + CDisplay *display = NULL; - std::string type = conf.getDisplay(); + std::string type = conf.getDisplay(); unsigned int dmrid = conf.getDMRId(); LogInfo("Display Parameters"); @@ -604,13 +603,6 @@ CDisplay* CDisplay::createDisplay(const CConf& conf, CUMP* ump, IModem* modem) if (port == "modem") { ISerialPort* serial = new IModemSerialPort(modem); display = new CNextion(conf.getCallsign(), dmrid, serial, brightness, displayClock, utc, idleBrightness, screenLayout, txFrequency, rxFrequency, displayTempInF); - } else if (port == "ump") { - if (ump != NULL) { - display = new CNextion(conf.getCallsign(), dmrid, ump, brightness, displayClock, utc, idleBrightness, screenLayout, txFrequency, rxFrequency, displayTempInF); - } else { - LogInfo(" NullDisplay loaded"); - display = new CNullDisplay; - } } else { unsigned int baudrate = 9600U; if (screenLayout == 4U) diff --git a/Display.h b/Display.h index f4343e8..c5e7d72 100644 --- a/Display.h +++ b/Display.h @@ -29,7 +29,6 @@ class CConf; class IModem; -class CUMP; class CDisplay { @@ -87,7 +86,7 @@ public: void clock(unsigned int ms); - static CDisplay* createDisplay(const CConf& conf, CUMP* ump, IModem* modem); + static CDisplay* createDisplay(const CConf& conf, IModem* modem); protected: virtual void setIdleInt() = 0; diff --git a/MMDVM.ini b/MMDVM.ini index e34e1ad..e64da52 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -76,11 +76,6 @@ RemotePort=40094 LocalPort=40095 # SendFrameType=0 -[UMP] -Enable=0 -# Port=\\.\COM4 -Port=/dev/ttyACM1 - [D-Star] Enable=1 Module=C diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 938200f..238c14a 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -136,7 +136,6 @@ m_pocsagNetwork(NULL), m_fmNetwork(NULL), m_ax25Network(NULL), m_display(NULL), -m_ump(NULL), m_mode(MODE_IDLE), m_dstarRFModeHang(10U), m_dmrRFModeHang(10U), @@ -285,21 +284,7 @@ int CMMDVMHost::run() if (!ret) return 1; - if (m_conf.getUMPEnabled()) { - std::string port = m_conf.getUMPPort(); - - LogInfo("Universal MMDVM Peripheral"); - LogInfo(" Port: %s", port.c_str()); - - m_ump = new CUMP(port); - bool ret = m_ump->open(); - if (!ret) { - delete m_ump; - m_ump = NULL; - } - } - - m_display = CDisplay::createDisplay(m_conf,m_ump,m_modem); + m_display = CDisplay::createDisplay(m_conf, m_modem); if (m_dstarEnabled && m_conf.getDStarNetworkEnabled()) { ret = createDStarNetwork(); @@ -708,14 +693,11 @@ int CMMDVMHost::run() LogMessage("MMDVMHost-%s is running", VERSION); while (!m_killed) { - bool lockout1 = m_modem->hasLockout(); - bool lockout2 = false; + bool lockout = m_modem->hasLockout(); - if (m_ump != NULL) - lockout2 = m_ump->getLockout(); - if ((lockout1 || lockout2) && m_mode != MODE_LOCKOUT) + if (lockout && m_mode != MODE_LOCKOUT) setMode(MODE_LOCKOUT); - else if ((!lockout1 && !lockout2) && m_mode == MODE_LOCKOUT) + else if (!lockout && m_mode == MODE_LOCKOUT) setMode(MODE_IDLE); bool error = m_modem->hasError(); @@ -724,13 +706,6 @@ int CMMDVMHost::run() else if (!error && m_mode == MODE_ERROR) setMode(MODE_IDLE); - if (m_ump != NULL) { - bool tx = m_modem->hasTX(); - m_ump->setTX(tx); - bool cd = m_modem->hasCD(); - m_ump->setCD(cd); - } - unsigned char data[500U]; unsigned int len; bool ret; @@ -1231,9 +1206,6 @@ int CMMDVMHost::run() pocsagTimer.start(); } - if (m_ump != NULL) - m_ump->clock(ms); - if (ms < 5U) CThread::sleep(5U); } @@ -1246,11 +1218,6 @@ int CMMDVMHost::run() m_display->close(); delete m_display; - if (m_ump != NULL) { - m_ump->close(); - delete m_ump; - } - if (m_dmrLookup != NULL) m_dmrLookup->stop(); @@ -1877,8 +1844,6 @@ void CMMDVMHost::setMode(unsigned char mode) if (m_ax25 != NULL) m_ax25->enable(false); m_modem->setMode(MODE_DSTAR); - if (m_ump != NULL) - m_ump->setMode(MODE_DSTAR); m_mode = MODE_DSTAR; m_modeTimer.start(); m_cwIdTimer.stop(); @@ -1923,8 +1888,6 @@ void CMMDVMHost::setMode(unsigned char mode) if (m_ax25 != NULL) m_ax25->enable(false); m_modem->setMode(MODE_DMR); - if (m_ump != NULL) - m_ump->setMode(MODE_DMR); if (m_duplex) { m_modem->writeDMRStart(true); m_dmrTXTimer.start(); @@ -1973,8 +1936,6 @@ void CMMDVMHost::setMode(unsigned char mode) if (m_ax25 != NULL) m_ax25->enable(false); m_modem->setMode(MODE_YSF); - if (m_ump != NULL) - m_ump->setMode(MODE_YSF); m_mode = MODE_YSF; m_modeTimer.start(); m_cwIdTimer.stop(); @@ -2019,8 +1980,6 @@ void CMMDVMHost::setMode(unsigned char mode) if (m_ax25 != NULL) m_ax25->enable(false); m_modem->setMode(MODE_P25); - if (m_ump != NULL) - m_ump->setMode(MODE_P25); m_mode = MODE_P25; m_modeTimer.start(); m_cwIdTimer.stop(); @@ -2065,8 +2024,6 @@ void CMMDVMHost::setMode(unsigned char mode) if (m_ax25 != NULL) m_ax25->enable(false); m_modem->setMode(MODE_NXDN); - if (m_ump != NULL) - m_ump->setMode(MODE_NXDN); m_mode = MODE_NXDN; m_modeTimer.start(); m_cwIdTimer.stop(); @@ -2103,8 +2060,6 @@ void CMMDVMHost::setMode(unsigned char mode) if (m_pocsag != NULL) m_pocsag->enable(false); m_modem->setMode(MODE_M17); - if (m_ump != NULL) - m_ump->setMode(MODE_M17); m_mode = MODE_M17; m_modeTimer.start(); m_cwIdTimer.stop(); @@ -2149,8 +2104,6 @@ void CMMDVMHost::setMode(unsigned char mode) if (m_ax25 != NULL) m_ax25->enable(false); m_modem->setMode(MODE_POCSAG); - if (m_ump != NULL) - m_ump->setMode(MODE_POCSAG); m_mode = MODE_POCSAG; m_modeTimer.start(); m_cwIdTimer.stop(); @@ -2199,8 +2152,6 @@ void CMMDVMHost::setMode(unsigned char mode) m_dmrTXTimer.stop(); } m_modem->setMode(MODE_FM); - if (m_ump != NULL) - m_ump->setMode(MODE_FM); m_display->setFM(); m_mode = MODE_FM; m_modeTimer.start(); @@ -2250,8 +2201,6 @@ void CMMDVMHost::setMode(unsigned char mode) m_dmrTXTimer.stop(); } m_modem->setMode(MODE_IDLE); - if (m_ump != NULL) - m_ump->setMode(MODE_IDLE); m_display->setLockout(); m_mode = MODE_LOCKOUT; m_modeTimer.stop(); @@ -2301,8 +2250,6 @@ void CMMDVMHost::setMode(unsigned char mode) m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); } - if (m_ump != NULL) - m_ump->setMode(MODE_IDLE); m_display->setError("MODEM"); m_mode = MODE_ERROR; m_modeTimer.stop(); @@ -2352,8 +2299,6 @@ void CMMDVMHost::setMode(unsigned char mode) m_dmrTXTimer.stop(); } m_modem->setMode(MODE_IDLE); - if (m_ump != NULL) - m_ump->setMode(MODE_IDLE); if (m_mode == MODE_ERROR) { m_modem->sendCWId(m_callsign); m_cwIdTimer.setTimeout(m_cwIdTime); diff --git a/MMDVMHost.h b/MMDVMHost.h index 7419d34..9f2e46b 100644 --- a/MMDVMHost.h +++ b/MMDVMHost.h @@ -44,7 +44,6 @@ #include "Timer.h" #include "Modem.h" #include "Conf.h" -#include "UMP.h" #include @@ -79,7 +78,6 @@ private: CFMNetwork* m_fmNetwork; CAX25Network* m_ax25Network; CDisplay* m_display; - CUMP* m_ump; unsigned char m_mode; unsigned int m_dstarRFModeHang; unsigned int m_dmrRFModeHang; diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index d6a2d48..e68e630 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -251,7 +251,6 @@ - @@ -354,7 +353,6 @@ - diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index 6838866..5764b22 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -215,9 +215,6 @@ Header Files - - Header Files - Header Files @@ -526,9 +523,6 @@ Source Files - - Source Files - Source Files @@ -641,4 +635,4 @@ Source Files - + \ No newline at end of file diff --git a/Makefile b/Makefile index b468d12..adbd0a2 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ OBJECTS = \ Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o \ - RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ + RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi b/Makefile.Pi index 5404405..a25a1c0 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -14,7 +14,7 @@ OBJECTS = \ Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o \ - RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ + RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index be09a46..c747908 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -15,7 +15,7 @@ OBJECTS = \ Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o \ - RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ + RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index 3bc37d1..99004d7 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -14,7 +14,7 @@ OBJECTS = \ Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o \ - RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ + RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index 720fd69..14c4a48 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -19,7 +19,7 @@ OBJECTS = \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o OLED.o P25Audio.o P25Control.o P25Data.o \ P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o \ RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o \ - UDPSocket.o UMP.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + UDPSocket.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index 0999ac2..d1675f6 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -15,7 +15,7 @@ OBJECTS = \ Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o \ - RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UMP.o \ + RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/README.md b/README.md index 813e945..a02236f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ These are the source files for building the MMDVMHost, the program that interfaces to the MMDVM or DVMega on the one side, and a suitable network on -the other. It supports D-Star, DMR, P25 Phase 1, NXDN, System Fusion, -POCSAG, and FM on the MMDVM, and D-Star, DMR, and System Fusion on the DVMega. +the other. It supports D-Star, DMR, P25 Phase 1, NXDN, System Fusion, M17, +POCSAG, FM, and AX.25 on the MMDVM, and D-Star, DMR, and System Fusion on the DVMega. On the D-Star side the MMDVMHost interfaces with the ircDDB Gateway, on DMR it can connect to BrandMeister, DMR+, TGIF, HB Link, XLX or @@ -9,7 +9,8 @@ can connect to BrandMeister, DMR+, TGIF, HB Link, XLX or networks at once) on System Fusion it connects to the YSF Gateway to allow access to the FCS and YSF networks. On P25 it connects to the P25 Gateway. On NXDN it connects to the NXDN Gateway which provides access to the NXDN and -NXCore talk groups. It uses the DAPNET Gateway to access DAPNET to receive +NXCore talk groups. On M17 it uses the M17 Gateway to access the M17 reflector system. +It uses the DAPNET Gateway to access DAPNET to receive paging messages. Finally it uses the FM Gateway to interface to existing FM networks. @@ -28,8 +29,7 @@ these are: The Nextion displays can connect to the UART on the Raspberry Pi, or via a USB to TTL serial converter like the FT-232RL. It may also be connected to the UART -output of the MMDVM modem (Arduino Due, STM32, Teensy), or to the UART output -on the UMP. +output of the MMDVM modem (Arduino Due, STM32, Teensy). The HD44780 displays are integrated with wiringPi for Raspberry Pi based platforms. diff --git a/UMP.cpp b/UMP.cpp deleted file mode 100644 index 4aca606..0000000 --- a/UMP.cpp +++ /dev/null @@ -1,283 +0,0 @@ -/* -* Copyright (C) 2016,2020 by Jonathan Naylor G4KLX -* -* This program is free software; you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation; either version 2 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program; if not, write to the Free Software -* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. -*/ - -#include "Defines.h" -#include "Utils.h" -#include "Log.h" -#include "UMP.h" - -#include -#include -#include - -const unsigned char UMP_FRAME_START = 0xF0U; - -const unsigned char UMP_HELLO = 0x00U; - -const unsigned char UMP_SET_MODE = 0x01U; -const unsigned char UMP_SET_TX = 0x02U; -const unsigned char UMP_SET_CD = 0x03U; - -const unsigned char UMP_WRITE_SERIAL = 0x10U; -const unsigned char UMP_READ_SERIAL = 0x11U; - -const unsigned char UMP_STATUS = 0x50U; - -const unsigned int BUFFER_LENGTH = 255U; - -CUMP::CUMP(const std::string& port) : -m_serial(port, 115200U), -m_open(false), -m_buffer(NULL), -m_length(0U), -m_offset(0U), -m_lockout(false), -m_mode(MODE_IDLE), -m_tx(false), -m_cd(false) -{ - m_buffer = new unsigned char[BUFFER_LENGTH]; -} - -CUMP::~CUMP() -{ - delete[] m_buffer; -} - -bool CUMP::open() -{ - if (m_open) - return true; - - LogMessage("Opening the UMP"); - - bool ret = m_serial.open(); - if (!ret) - return false; - - unsigned char buffer[3U]; - - buffer[0U] = UMP_FRAME_START; - buffer[1U] = 3U; - buffer[2U] = UMP_HELLO; - - // CUtils::dump(1U, "Transmitted", buffer, 3U); - - int n = m_serial.write(buffer, 3U); - if (n != 3) { - m_serial.close(); - return false; - } - - m_open = true; - - return true; -} - -bool CUMP::setMode(unsigned char mode) -{ - if (mode == m_mode) - return true; - - m_mode = mode; - - unsigned char buffer[4U]; - - buffer[0U] = UMP_FRAME_START; - buffer[1U] = 4U; - buffer[2U] = UMP_SET_MODE; - buffer[3U] = mode; - - // CUtils::dump(1U, "Transmitted", buffer, 4U); - - return m_serial.write(buffer, 4U) == 4; -} - -bool CUMP::setTX(bool on) -{ - if (on == m_tx) - return true; - - m_tx = on; - - unsigned char buffer[4U]; - - buffer[0U] = UMP_FRAME_START; - buffer[1U] = 4U; - buffer[2U] = UMP_SET_TX; - buffer[3U] = on ? 0x01U : 0x00U; - - // CUtils::dump(1U, "Transmitted", buffer, 4U); - - return m_serial.write(buffer, 4U) == 4; -} - -bool CUMP::setCD(bool on) -{ - if (on == m_cd) - return true; - - m_cd = on; - - unsigned char buffer[4U]; - - buffer[0U] = UMP_FRAME_START; - buffer[1U] = 4U; - buffer[2U] = UMP_SET_CD; - buffer[3U] = on ? 0x01U : 0x00U; - - // CUtils::dump(1U, "Transmitted", buffer, 4U); - - return m_serial.write(buffer, 4U) == 4; -} - -bool CUMP::getLockout() const -{ - return m_lockout; -} - -int CUMP::write(const unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - unsigned char buffer[250U]; - - buffer[0U] = UMP_FRAME_START; - buffer[1U] = length + 3U; - buffer[2U] = UMP_WRITE_SERIAL; - - ::memcpy(buffer + 3U, data, length); - - // CUtils::dump(1U, "Transmitted", buffer, length + 3U); - - return m_serial.write(buffer, length + 3U); -} - -// To be implemented later if needed -int CUMP::read(unsigned char* data, unsigned int length) -{ - assert(data != NULL); - assert(length > 0U); - - return 0; -} - -void CUMP::clock(unsigned int ms) -{ - if (m_offset == 0U) { - // Get the start of the frame or nothing at all - int ret = m_serial.read(m_buffer + 0U, 1U); - if (ret < 0) { - LogError("Error when reading from the UMP"); - return; - } - - if (ret == 0) - return; - - if (m_buffer[0U] != UMP_FRAME_START) - return; - - m_offset = 1U; - } - - if (m_offset == 1U) { - // Get the length of the frame - int ret = m_serial.read(m_buffer + 1U, 1U); - if (ret < 0) { - LogError("Error when reading from the UMP"); - m_offset = 0U; - return; - } - - if (ret == 0) - return; - - if (m_buffer[1U] >= 250U) { - LogError("Invalid length received from the UMP - %u", m_buffer[1U]); - m_offset = 0U; - return; - } - - m_length = m_buffer[1U]; - m_offset = 2U; - } - - if (m_offset == 2U) { - // Get the frame type - int ret = m_serial.read(m_buffer + 2U, 1U); - if (ret < 0) { - LogError("Error when reading from the UMP"); - m_offset = 0U; - return; - } - - if (ret == 0) - return; - - switch (m_buffer[2U]) { - case UMP_STATUS: - case UMP_READ_SERIAL: - break; - - default: - LogError("Unknown message, type: %02X", m_buffer[2U]); - m_offset = 0U; - return; - } - - m_offset = 3U; - } - - if (m_offset >= 3U) { - while (m_offset < m_length) { - int ret = m_serial.read(m_buffer + m_offset, m_length - m_offset); - if (ret < 0) { - LogError("Error when reading from the UMP"); - m_offset = 0U; - return; - } - - if (ret == 0) - return; - - if (ret > 0) - m_offset += ret; - } - } - - m_offset = 0U; - - // CUtils::dump(1U, "Received", m_buffer, m_length); - - if (m_buffer[2U] == UMP_STATUS) - m_lockout = (m_buffer[3U] & 0x01U) == 0x01U; -} - -void CUMP::close() -{ - if (!m_open) - return; - - LogMessage("Closing the UMP"); - - m_serial.close(); - - m_open = false; -} diff --git a/UMP.h b/UMP.h deleted file mode 100644 index 1acc3c1..0000000 --- a/UMP.h +++ /dev/null @@ -1,63 +0,0 @@ -/* -* Copyright (C) 2016 by Jonathan Naylor G4KLX -* -* This program is free software; you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation; either version 2 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program; if not, write to the Free Software -* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. -*/ - -#if !defined(UMP_H) -#define UMP_H - -#include "SerialController.h" -#include "SerialPort.h" - -#include - -class CUMP : public ISerialPort -{ -public: - CUMP(const std::string& port); - virtual ~CUMP(); - - virtual bool open(); - - bool setMode(unsigned char mode); - - bool setTX(bool on); - - bool setCD(bool on); - - bool getLockout() const; - - virtual int read(unsigned char* buffer, unsigned int length); - - virtual int write(const unsigned char* buffer, unsigned int length); - - void clock(unsigned int ms); - - virtual void close(); - -private: - CSerialController m_serial; - bool m_open; - unsigned char* m_buffer; - unsigned int m_length; - unsigned int m_offset; - bool m_lockout; - unsigned char m_mode; - bool m_tx; - bool m_cd; -}; - -#endif diff --git a/UMP/UMP.ino b/UMP/UMP.ino deleted file mode 100644 index 4cf98e3..0000000 --- a/UMP/UMP.ino +++ /dev/null @@ -1,203 +0,0 @@ -/* -* Copyright (C) 2016,2018 by Jonathan Naylor G4KLX -* -* This program is free software; you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation; either version 2 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program; if not, write to the Free Software -* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. -*/ - -#if !defined(__AVR_ATmega1280__) && !defined(__AVR_ATmega2560__) && !defined(__AVR_ATmega32U4__) && !defined(__SAM3X8E__) && !defined(__MK20DX256__) -#include -#endif - -#if !defined(PIN_LED) -#define PIN_LED 13 -#endif - -#define PIN_DSTAR 3 -#define PIN_DMR 4 -#define PIN_YSF 5 -#define PIN_P25 6 -#define PIN_NXDN 7 -#define PIN_POCSAG 8 - -#define PIN_TX 10 -#define PIN_CD 11 - -#define PIN_LOCKOUT 12 - -#if defined(__MK20DX256__) -#define FLASH_DELAY 200000U -#else -#define FLASH_DELAY 3200U -#endif - -#if !defined(__AVR_ATmega1280__) && !defined(__AVR_ATmega2560__) && !defined(__AVR_ATmega32U4__) && !defined(__SAM3X8E__) && !defined(__MK20DX256__) -AltSoftSerial mySerial; -#endif - -// Use the LOCKOUT function on the UMP -// #define USE_LOCKOUT - -void setup() -{ - Serial.begin(115200); - -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega32U4__) || defined(__SAM3X8E__) || defined(__MK20DX256__) - Serial1.begin(9600); -#else - mySerial.begin(9600); -#endif - - pinMode(PIN_LED, OUTPUT); - pinMode(PIN_DSTAR, OUTPUT); - pinMode(PIN_DMR, OUTPUT); - pinMode(PIN_YSF, OUTPUT); - pinMode(PIN_P25, OUTPUT); - pinMode(PIN_NXDN, OUTPUT); - pinMode(PIN_POCSAG, OUTPUT); - pinMode(PIN_TX, OUTPUT); - pinMode(PIN_CD, OUTPUT); - pinMode(PIN_LOCKOUT, INPUT); - - digitalWrite(PIN_DSTAR, LOW); - digitalWrite(PIN_DMR, LOW); - digitalWrite(PIN_YSF, LOW); - digitalWrite(PIN_P25, LOW); - digitalWrite(PIN_NXDN, LOW); - digitalWrite(PIN_POCSAG, LOW); - digitalWrite(PIN_TX, LOW); - digitalWrite(PIN_CD, LOW); -} - -#define UMP_FRAME_START 0xF0U - -#define UMP_HELLO 0x00U - -#define UMP_SET_MODE 0x01U -#define UMP_SET_TX 0x02U -#define UMP_SET_CD 0x03U - -#define UMP_WRITE_SERIAL 0x10U - -#define UMP_STATUS 0x50U - -#define MODE_IDLE 0U -#define MODE_DSTAR 1U -#define MODE_DMR 2U -#define MODE_YSF 3U -#define MODE_P25 4U -#define MODE_NXDN 5U -#define MODE_POCSAG 6U - -bool m_started = false; -uint32_t m_count = 0U; -bool m_led = false; - -uint8_t m_buffer[256U]; -uint8_t m_offset = 0U; -uint8_t m_length = 0U; - -bool m_lockout = false; - -void loop() -{ - while (Serial.available()) { - uint8_t c = Serial.read(); - - if (m_offset == 0U) { - if (c == UMP_FRAME_START) { - m_buffer[m_offset] = c; - m_offset = 1U; - } - } else if (m_offset == 1U) { - m_length = m_buffer[m_offset] = c; - m_offset = 2U; - } else { - m_buffer[m_offset] = c; - m_offset++; - - if (m_length == m_offset) { - switch (m_buffer[2U]) { - case UMP_HELLO: - m_started = true; - break; - case UMP_SET_MODE: - digitalWrite(PIN_DSTAR, m_buffer[3U] == MODE_DSTAR ? HIGH : LOW); - digitalWrite(PIN_DMR, m_buffer[3U] == MODE_DMR ? HIGH : LOW); - digitalWrite(PIN_YSF, m_buffer[3U] == MODE_YSF ? HIGH : LOW); - digitalWrite(PIN_P25, m_buffer[3U] == MODE_P25 ? HIGH : LOW); - digitalWrite(PIN_NXDN, m_buffer[3U] == MODE_NXDN ? HIGH : LOW); - digitalWrite(PIN_POCSAG, m_buffer[3U] == MODE_POCSAG ? HIGH : LOW); - break; - case UMP_SET_TX: - digitalWrite(PIN_TX, m_buffer[3U] == 0x01U ? HIGH : LOW); - break; - case UMP_SET_CD: - digitalWrite(PIN_CD, m_buffer[3U] == 0x01U ? HIGH : LOW); - break; - case UMP_WRITE_SERIAL: -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega32U4__) || defined(__SAM3X8E__) || defined(__MK20DX256__) - Serial1.write(m_buffer + 3U, m_length - 3U); -#else - mySerial.write(m_buffer + 3U, m_length - 3U); -#endif - break; - default: - break; - } - - m_length = 0U; - m_offset = 0U; - } - } - } - - bool lockout = false; -#if defined(USE_LOCKOUT) - lockout = digitalRead(PIN_LOCKOUT) == HIGH; -#endif - if (lockout != m_lockout) { - uint8_t data[4U]; - data[0U] = UMP_FRAME_START; - data[1U] = 4U; - data[2U] = UMP_STATUS; - data[3U] = lockout ? 0x01U : 0x00U; - Serial.write(data, 4U); - - m_lockout = lockout; - } - -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega32U4__) || defined(__SAM3X8E__) || defined(__MK20DX256__) - while (Serial1.available()) - Serial1.read(); -#else - while (mySerial.available()) - mySerial.read(); -#endif - - m_count++; - if (m_started) { - if (m_count > FLASH_DELAY) { - digitalWrite(PIN_LED, m_led ? LOW : HIGH); - m_led = !m_led; - m_count = 0U; - } - } else { - if (m_count > (FLASH_DELAY * 3U)) { - digitalWrite(PIN_LED, m_led ? LOW : HIGH); - m_led = !m_led; - m_count = 0U; - } - } -} From dbd779a6d3de0bcf8c6faf83593a3dc92aa77b04 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 10 Nov 2020 10:01:05 +0000 Subject: [PATCH 148/163] Fix obvious crash issue. --- SerialModem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index 3f91e97..32b057c 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -1922,7 +1922,7 @@ bool CSerialModem::readVersion() LogInfo("Unknown CPU type: %u", m_buffer[6U]); break; } - char modeText[10U]; + char modeText[100U]; ::strcpy(modeText, "Modes:"); if ((m_buffer[4U] & 0x01U) == 0x01U) ::strcat(modeText, " D-Star"); From eece693f0582b7fcec73ba5e0531df862174da27 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 10 Nov 2020 10:08:59 +0000 Subject: [PATCH 149/163] Remove support for the Hobbytronics TFT display. --- Display.cpp | 10 +- MMDVMHost.vcxproj | 2 - MMDVMHost.vcxproj.filters | 6 - Makefile | 4 +- Makefile.Pi | 4 +- Makefile.Pi.Adafruit | 4 +- Makefile.Pi.HD44780 | 4 +- Makefile.Pi.OLED | 4 +- Makefile.Pi.PCF8574 | 4 +- README.md | 1 - TFTSerial.cpp | 630 -------------------------------------- TFTSerial.h | 92 ------ TFTSerial/DMR_sm.bmp | Bin 24054 -> 0 bytes TFTSerial/DStar_sm.bmp | Bin 24054 -> 0 bytes TFTSerial/M17_sm.bmp | Bin 24054 -> 0 bytes TFTSerial/MMDVM_sm.bmp | Bin 25494 -> 0 bytes TFTSerial/NXDN_sm.bmp | Bin 23574 -> 0 bytes TFTSerial/P25_sm.bmp | Bin 24054 -> 0 bytes TFTSerial/YSF_sm.bmp | Bin 24054 -> 0 bytes 19 files changed, 15 insertions(+), 750 deletions(-) delete mode 100644 TFTSerial.cpp delete mode 100644 TFTSerial.h delete mode 100644 TFTSerial/DMR_sm.bmp delete mode 100644 TFTSerial/DStar_sm.bmp delete mode 100644 TFTSerial/M17_sm.bmp delete mode 100644 TFTSerial/MMDVM_sm.bmp delete mode 100644 TFTSerial/NXDN_sm.bmp delete mode 100644 TFTSerial/P25_sm.bmp delete mode 100644 TFTSerial/YSF_sm.bmp diff --git a/Display.cpp b/Display.cpp index e26219f..7d4b1a9 100644 --- a/Display.cpp +++ b/Display.cpp @@ -21,7 +21,6 @@ #include "SerialController.h" #include "ModemSerialPort.h" #include "NullDisplay.h" -#include "TFTSerial.h" #include "TFTSurenoo.h" #include "LCDproc.h" #include "Nextion.h" @@ -546,7 +545,7 @@ CDisplay* CDisplay::createDisplay(const CConf& conf, IModem* modem) LogInfo("Display Parameters"); LogInfo(" Type: %s", type.c_str()); - if (type == "TFT Serial" || type == "TFT Surenoo") { + if (type == "TFT Surenoo") { std::string port = conf.getTFTSerialPort(); unsigned int brightness = conf.getTFTSerialBrightness(); @@ -557,12 +556,9 @@ CDisplay* CDisplay::createDisplay(const CConf& conf, IModem* modem) if (port == "modem") serial = new IModemSerialPort(modem); else - serial = new CSerialController(port, (type == "TFT Serial") ? 9600U : 115200U); + serial = new CSerialController(port, 115200U); - if (type == "TFT Surenoo") - display = new CTFTSurenoo(conf.getCallsign(), dmrid, serial, brightness, conf.getDuplex()); - else - display = new CTFTSerial(conf.getCallsign(), dmrid, serial, brightness); + display = new CTFTSurenoo(conf.getCallsign(), dmrid, serial, brightness, conf.getDuplex()); } else if (type == "Nextion") { std::string port = conf.getNextionPort(); unsigned int brightness = conf.getNextionBrightness(); diff --git a/MMDVMHost.vcxproj b/MMDVMHost.vcxproj index e68e630..f54bca0 100644 --- a/MMDVMHost.vcxproj +++ b/MMDVMHost.vcxproj @@ -246,7 +246,6 @@ - @@ -348,7 +347,6 @@ - diff --git a/MMDVMHost.vcxproj.filters b/MMDVMHost.vcxproj.filters index 5764b22..f9d3698 100644 --- a/MMDVMHost.vcxproj.filters +++ b/MMDVMHost.vcxproj.filters @@ -80,9 +80,6 @@ Header Files - - Header Files - Header Files @@ -397,9 +394,6 @@ Source Files - - Source Files - Source Files diff --git a/Makefile b/Makefile index adbd0a2..bd052ec 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,8 @@ OBJECTS = \ Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o \ - RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ - UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UserDB.o UserDBentry.o \ + Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi b/Makefile.Pi index a25a1c0..c82bbbd 100644 --- a/Makefile.Pi +++ b/Makefile.Pi @@ -14,8 +14,8 @@ OBJECTS = \ Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o \ - RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ - UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UserDB.o UserDBentry.o \ + Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.Adafruit b/Makefile.Pi.Adafruit index c747908..355c95d 100644 --- a/Makefile.Pi.Adafruit +++ b/Makefile.Pi.Adafruit @@ -15,8 +15,8 @@ OBJECTS = \ Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o \ - RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ - UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UserDB.o UserDBentry.o \ + Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.HD44780 b/Makefile.Pi.HD44780 index 99004d7..d4bbe63 100644 --- a/Makefile.Pi.HD44780 +++ b/Makefile.Pi.HD44780 @@ -14,8 +14,8 @@ OBJECTS = \ Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o \ - RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ - UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UserDB.o UserDBentry.o \ + Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.OLED b/Makefile.Pi.OLED index 14c4a48..3c9a487 100644 --- a/Makefile.Pi.OLED +++ b/Makefile.Pi.OLED @@ -18,8 +18,8 @@ OBJECTS = \ Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o OLED.o P25Audio.o P25Control.o P25Data.o \ P25LowSpeedData.o P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o \ - RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o \ - UDPSocket.o UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + RS129.o RS241213.o RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ + UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/Makefile.Pi.PCF8574 b/Makefile.Pi.PCF8574 index d1675f6..0798819 100644 --- a/Makefile.Pi.PCF8574 +++ b/Makefile.Pi.PCF8574 @@ -15,8 +15,8 @@ OBJECTS = \ Mutex.o NetworkInfo.o Nextion.o NullDisplay.o NullModem.o NXDNAudio.o NXDNControl.o NXDNConvolution.o NXDNCRC.o NXDNFACCH1.o NXDNIcomNetwork.o \ NXDNKenwoodNetwork.o NXDNLayer3.o NXDNLICH.o NXDNLookup.o NXDNNetwork.o NXDNSACCH.o NXDNUDCH.o P25Audio.o P25Control.o P25Data.o P25LowSpeedData.o \ P25Network.o P25NID.o P25Trellis.o P25Utils.o PseudoTTYController.o POCSAGControl.o POCSAGNetwork.o QR1676.o RemoteControl.o RS129.o RS241213.o \ - RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSerial.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o \ - UserDB.o UserDBentry.o Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o + RSSIInterpolator.o SerialController.o SerialModem.o SerialPort.o StopWatch.o Sync.o TFTSurenoo.o Thread.o Timer.o UDPSocket.o UserDB.o UserDBentry.o \ + Utils.o YSFControl.o YSFConvolution.o YSFFICH.o YSFNetwork.o YSFPayload.o all: MMDVMHost RemoteCommand diff --git a/README.md b/README.md index a02236f..69f2e0a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ these are: - Adafruit 16x2 LCD+Keypad Kits (I2C) - Connection via PCF8574 GPIO Extender (I2C) - Nextion TFTs (all sizes, both Basic and Enhanced versions) -- TFT display sold by Hobbytronics in UK - OLED 128x64 (SSD1306) - LCDproc diff --git a/TFTSerial.cpp b/TFTSerial.cpp deleted file mode 100644 index 2d0e9dc..0000000 --- a/TFTSerial.cpp +++ /dev/null @@ -1,630 +0,0 @@ -/* - * Copyright (C) 2015,2016,2018,2020 by Jonathan Naylor G4KLX - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - */ - -#include "TFTSerial.h" -#include "Log.h" - -#include -#include -#include - -const unsigned char ROTATION_PORTRAIT_LEFT = 0U; -const unsigned char ROTATION_LANDSCAPE_UD = 1U; -const unsigned char ROTATION_PORTRAIT_RIGHT = 2U; -const unsigned char ROTATION_LANDSCAPE = 3U; - -const unsigned char COLOUR_BLACK = 0U; -const unsigned char COLOUR_BLUE = 1U; -const unsigned char COLOUR_RED = 2U; -const unsigned char COLOUR_GREEN = 3U; -const unsigned char COLOUR_CYAN = 4U; -const unsigned char COLOUR_MAGENTA = 5U; -const unsigned char COLOUR_YELLOW = 6U; -const unsigned char COLOUR_WHITE = 7U; - -const unsigned char FONT_SMALL = 1U; -const unsigned char FONT_MEDIUM = 2U; -const unsigned char FONT_LARGE = 3U; - -// x = 0 to 159, y = 0 to 127 - Landscape -// x = 0 to 127, y = 0 to 159 - Portrait - -CTFTSerial::CTFTSerial(const std::string& callsign, unsigned int dmrid, ISerialPort* serial, unsigned int brightness) : -CDisplay(), -m_callsign(callsign), -m_dmrid(dmrid), -m_serial(serial), -m_brightness(brightness), -m_mode(MODE_IDLE) -{ - assert(serial != NULL); - assert(brightness >= 0U && brightness <= 100U); -} - -CTFTSerial::~CTFTSerial() -{ -} - -bool CTFTSerial::open() -{ - bool ret = m_serial->open(); - if (!ret) { - LogError("Cannot open the port for the TFT Serial"); - delete m_serial; - return false; - } - - setRotation(ROTATION_LANDSCAPE); - - setBrightness(m_brightness); - - setBackground(COLOUR_WHITE); - - setForeground(COLOUR_BLACK); - - setIdle(); - - return true; -} - -void CTFTSerial::setIdleInt() -{ - // Clear the screen - clearScreen(); - - setFontSize(FONT_LARGE); - - // Draw MMDVM logo - displayBitmap(0U, 0U, "MMDVM_sm.bmp"); - - char text[30]; - ::sprintf(text, "%-6s / %u", m_callsign.c_str(), m_dmrid); - - gotoPosPixel(18U, 55U); - displayText(text); - - gotoPosPixel(45U, 90U); - displayText("IDLE"); - - m_mode = MODE_IDLE; -} - -void CTFTSerial::setErrorInt(const char* text) -{ - assert(text != NULL); - - // Clear the screen - clearScreen(); - - setFontSize(FONT_MEDIUM); - - // Draw MMDVM logo - displayBitmap(0U, 0U, "MMDVM_sm.bmp"); - - setForeground(COLOUR_RED); - - gotoPosPixel(18U, 55U); - displayText(text); - - gotoPosPixel(18U, 90U); - displayText("ERROR"); - - setForeground(COLOUR_BLACK); - - m_mode = MODE_ERROR; -} - -void CTFTSerial::setLockoutInt() -{ - // Clear the screen - clearScreen(); - - setFontSize(FONT_LARGE); - - // Draw MMDVM logo - displayBitmap(0U, 0U, "MMDVM_sm.bmp"); - - gotoPosPixel(20U, 60U); - displayText("LOCKOUT"); - - m_mode = MODE_LOCKOUT; -} - -void CTFTSerial::setQuitInt() -{ - // Clear the screen - clearScreen(); - - setFontSize(FONT_LARGE); - - // Draw MMDVM logo - displayBitmap(0U, 0U, "MMDVM_sm.bmp"); - - gotoPosPixel(20U, 60U); - displayText("STOPPED"); - - m_mode = MODE_QUIT; -} - -void CTFTSerial::setFMInt() -{ - // Clear the screen - clearScreen(); - - setFontSize(FONT_LARGE); - - // Draw MMDVM logo - displayBitmap(0U, 0U, "MMDVM_sm.bmp"); - - gotoPosPixel(20U, 60U); - displayText("FM"); - - m_mode = MODE_FM; -} - -void CTFTSerial::writeDStarInt(const char* my1, const char* my2, const char* your, const char* type, const char* reflector) -{ - assert(my1 != NULL); - assert(my2 != NULL); - assert(your != NULL); - assert(type != NULL); - assert(reflector != NULL); - - if (m_mode != MODE_DSTAR) { - // Clear the screen - clearScreen(); - - setFontSize(FONT_MEDIUM); - - // Draw D-Star insignia - displayBitmap(0U, 0U, "DStar_sm.bmp"); - } - - char text[30U]; - - ::sprintf(text, "%s %.8s/%4.4s", type, my1, my2); - gotoPosPixel(5U, 70U); - displayText(text); - - ::sprintf(text, "%.8s", your); - gotoPosPixel(5U, 90U); - displayText(text); - - if (::strcmp(reflector, " ") != 0) { - ::sprintf(text, "via %.8s", reflector); - gotoPosPixel(5U, 110U); - displayText(text); - } else { - gotoPosPixel(5U, 110U); - displayText(" "); - } - - m_mode = MODE_DSTAR; -} - -void CTFTSerial::clearDStarInt() -{ - gotoPosPixel(5U, 70U); - displayText(" Listening "); - - gotoPosPixel(5U, 90U); - displayText(" "); - - gotoPosPixel(5U, 110U); - displayText(" "); -} - -void CTFTSerial::writeDMRInt(unsigned int slotNo, const std::string& src, bool group, const std::string& dst, const char* type) -{ - assert(type != NULL); - - if (m_mode != MODE_DMR) { - // Clear the screen - clearScreen(); - - setFontSize(FONT_MEDIUM); - - // Draw DMR insignia - displayBitmap(0U, 0U, "DMR_sm.bmp"); - - if (slotNo == 1U) { - gotoPosPixel(5U, 90U); - displayText("2 Listening"); - } else { - gotoPosPixel(5U, 55U); - displayText("1 Listening"); - } - } - - if (slotNo == 1U) { - char text[30U]; - - ::sprintf(text, "1 %s %s", type, src.c_str()); - gotoPosPixel(5U, 55U); - displayText(text); - - ::sprintf(text, "%s%s", group ? "TG" : "", dst.c_str()); - gotoPosPixel(65U, 72U); - displayText(text); - } else { - char text[30U]; - - ::sprintf(text, "2 %s %s", type, src.c_str()); - gotoPosPixel(5U, 90U); - displayText(text); - - ::sprintf(text, "%s%s", group ? "TG" : "", dst.c_str()); - gotoPosPixel(65U, 107U); - displayText(text); - } - - m_mode = MODE_DMR; -} - -void CTFTSerial::clearDMRInt(unsigned int slotNo) -{ - if (slotNo == 1U) { - gotoPosPixel(5U, 55U); - displayText("1 Listening "); - - gotoPosPixel(65U, 72U); - displayText(" "); - } else { - gotoPosPixel(5U, 90U); - displayText("2 Listening "); - - gotoPosPixel(65U, 107U); - displayText(" "); - } -} - -void CTFTSerial::writeFusionInt(const char* source, const char* dest, unsigned char dgid, const char* type, const char* origin) -{ - assert(source != NULL); - assert(dest != NULL); - assert(type != NULL); - assert(origin != NULL); - - if (m_mode != MODE_YSF) { - // Clear the screen - clearScreen(); - - setFontSize(FONT_MEDIUM); - - // Draw the System Fusion insignia - displayBitmap(0U, 0U, "YSF_sm.bmp"); - } - - char text[30U]; - ::sprintf(text, "%s %.10s", type, source); - - gotoPosPixel(5U, 70U); - displayText(text); - - ::sprintf(text, " DG-ID %u", dgid); - - gotoPosPixel(5U, 90U); - displayText(text); - - if (::strcmp(origin, " ") != 0) { - ::sprintf(text, "at %.10s", origin); - gotoPosPixel(5U, 110U); - displayText(text); - } else { - gotoPosPixel(5U, 110U); - displayText(" "); - } - - m_mode = MODE_YSF; -} - -void CTFTSerial::clearFusionInt() -{ - gotoPosPixel(5U, 70U); - displayText(" Listening "); - - gotoPosPixel(5U, 90U); - displayText(" "); - - gotoPosPixel(5U, 110U); - displayText(" "); -} - -void CTFTSerial::writeP25Int(const char* source, bool group, unsigned int dest, const char* type) -{ - assert(source != NULL); - assert(type != NULL); - - if (m_mode != MODE_P25) { - // Clear the screen - clearScreen(); - - setFontSize(FONT_MEDIUM); - - // Draw the P25 insignia - displayBitmap(0U, 0U, "P25_sm.bmp"); - } - - char text[30U]; - ::sprintf(text, "%s %.10s", type, source); - - gotoPosPixel(5U, 70U); - displayText(text); - - ::sprintf(text, " %s%u", group ? "TG" : "", dest); - - gotoPosPixel(5U, 90U); - displayText(text); - - m_mode = MODE_P25; -} - -void CTFTSerial::clearP25Int() -{ - gotoPosPixel(5U, 70U); - displayText(" Listening "); - - gotoPosPixel(5U, 90U); - displayText(" "); - - gotoPosPixel(5U, 110U); - displayText(" "); -} - -void CTFTSerial::writeNXDNInt(const char* source, bool group, unsigned int dest, const char* type) -{ - assert(source != NULL); - assert(type != NULL); - - if (m_mode != MODE_NXDN) { - // Clear the screen - clearScreen(); - - setFontSize(FONT_MEDIUM); - - // Draw the NXDN insignia - displayBitmap(0U, 0U, "NXDN_sm.bmp"); - } - - char text[30U]; - ::sprintf(text, "%s %.10s", type, source); - - gotoPosPixel(5U, 70U); - displayText(text); - - ::sprintf(text, " %s%u", group ? "TG" : "", dest); - - gotoPosPixel(5U, 90U); - displayText(text); - - m_mode = MODE_NXDN; -} - -void CTFTSerial::clearNXDNInt() -{ - gotoPosPixel(5U, 70U); - displayText(" Listening "); - - gotoPosPixel(5U, 90U); - displayText(" "); - - gotoPosPixel(5U, 110U); - displayText(" "); -} - -void CTFTSerial::writeM17Int(const char* source, const char* dest, const char* type) -{ - assert(source != NULL); - assert(dest != NULL); - assert(type != NULL); - - if (m_mode != MODE_M17) { - // Clear the screen - clearScreen(); - - setFontSize(FONT_MEDIUM); - - // Draw the M17 insignia - displayBitmap(0U, 0U, "M17_sm.bmp"); - } - - char text[30U]; - ::sprintf(text, "%s %.10s", type, source); - - gotoPosPixel(5U, 70U); - displayText(text); - - ::sprintf(text, " %s", dest); - - gotoPosPixel(5U, 90U); - displayText(text); - - m_mode = MODE_M17; -} - -void CTFTSerial::clearM17Int() -{ - gotoPosPixel(5U, 70U); - displayText(" Listening "); - - gotoPosPixel(5U, 90U); - displayText(" "); - - gotoPosPixel(5U, 110U); - displayText(" "); -} - -void CTFTSerial::writePOCSAGInt(uint32_t ric, const std::string& message) -{ - gotoPosPixel(15U, 90U); - displayText("POCSAG TX"); - - m_mode = MODE_CW; -} - -void CTFTSerial::clearPOCSAGInt() -{ - gotoPosPixel(45U, 90U); - displayText("IDLE"); -} - -void CTFTSerial::writeCWInt() -{ - gotoPosPixel(45U, 90U); - displayText("CW TX"); - - m_mode = MODE_CW; -} - -void CTFTSerial::clearCWInt() -{ - gotoPosPixel(45U, 90U); - displayText("IDLE"); -} - -void CTFTSerial::close() -{ - m_serial->close(); - delete m_serial; -} - -void CTFTSerial::clearScreen() -{ - m_serial->write((unsigned char*)"\x1B\x00\xFF", 3U); -} - -void CTFTSerial::setForeground(unsigned char colour) -{ - assert(colour >= 0U && colour <= 7U); - - m_serial->write((unsigned char*)"\x1B\x01", 2U); - m_serial->write(&colour, 1U); - m_serial->write((unsigned char*)"\xFF", 1U); -} - -void CTFTSerial::setBackground(unsigned char colour) -{ - assert(colour >= 0U && colour <= 7U); - - m_serial->write((unsigned char*)"\x1B\x02", 2U); - m_serial->write(&colour, 1U); - m_serial->write((unsigned char*)"\xFF", 1U); -} - -void CTFTSerial::setRotation(unsigned char rotation) -{ - assert(rotation >= 0U && rotation <= 3U); - - m_serial->write((unsigned char*)"\x1B\x03", 2U); - m_serial->write(&rotation, 1U); - m_serial->write((unsigned char*)"\xFF", 1U); -} - -void CTFTSerial::setFontSize(unsigned char size) -{ - assert(size >= 1U && size <= 3U); - - m_serial->write((unsigned char*)"\x1B\x04", 2U); - m_serial->write(&size, 1U); - m_serial->write((unsigned char*)"\xFF", 1U); -} - -void CTFTSerial::gotoBegOfLine() -{ - m_serial->write((unsigned char*)"\x1B\x05\xFF", 3U); -} - -void CTFTSerial::gotoPosText(unsigned char x, unsigned char y) -{ - m_serial->write((unsigned char*)"\x1B\x06", 2U); - m_serial->write(&x, 1U); - m_serial->write(&y, 1U); - m_serial->write((unsigned char*)"\xFF", 1U); -} - -void CTFTSerial::gotoPosPixel(unsigned char x, unsigned char y) -{ - m_serial->write((unsigned char*)"\x1B\x07", 2U); - m_serial->write(&x, 1U); - m_serial->write(&y, 1U); - m_serial->write((unsigned char*)"\xFF", 1U); -} - -void CTFTSerial::drawLine(unsigned char x1, unsigned char y1, unsigned char x2, unsigned char y2) -{ - m_serial->write((unsigned char*)"\x1B\x08", 2U); - m_serial->write(&x1, 1U); - m_serial->write(&y1, 1U); - m_serial->write(&x2, 1U); - m_serial->write(&y2, 1U); - m_serial->write((unsigned char*)"\xFF", 1U); -} - -void CTFTSerial::drawBox(unsigned char x1, unsigned char y1, unsigned char x2, unsigned char y2, bool filled) -{ - if (filled) - m_serial->write((unsigned char*)"\x1B\x0A", 2U); - else - m_serial->write((unsigned char*)"\x1B\x09", 2U); - - m_serial->write(&x1, 1U); - m_serial->write(&y1, 1U); - m_serial->write(&x2, 1U); - m_serial->write(&y2, 1U); - m_serial->write((unsigned char*)"\xFF", 1U); -} - -void CTFTSerial::drawCircle(unsigned char x, unsigned char y, unsigned char radius, bool filled) -{ - if (filled) - m_serial->write((unsigned char*)"\x1B\x0C", 2U); - else - m_serial->write((unsigned char*)"\x1B\x0B", 2U); - - m_serial->write(&x, 1U); - m_serial->write(&y, 1U); - m_serial->write(&radius, 1U); - m_serial->write((unsigned char*)"\xFF", 1U); -} - -void CTFTSerial::displayBitmap(unsigned char x, unsigned char y, const char* filename) -{ - assert(filename != NULL); - - m_serial->write((unsigned char*)"\x1B\x0D", 2U); - m_serial->write(&x, 1U); - m_serial->write(&y, 1U); - m_serial->write((unsigned char*)filename, (unsigned int)::strlen(filename)); - m_serial->write((unsigned char*)"\xFF", 1U); -} - -void CTFTSerial::setBrightness(unsigned char brightness) -{ - assert(brightness >= 0U && brightness <= 100U); - - m_serial->write((unsigned char*)"\x1B\x0E", 2U); - m_serial->write(&brightness, 1U); - m_serial->write((unsigned char*)"\xFF", 1U); -} - -void CTFTSerial::displayText(const char* text) -{ - assert(text != NULL); - - m_serial->write((unsigned char*)text, (unsigned int)::strlen(text)); -} diff --git a/TFTSerial.h b/TFTSerial.h deleted file mode 100644 index 86d4130..0000000 --- a/TFTSerial.h +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2015,2016,2018,2020 by Jonathan Naylor G4KLX - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - */ - -#if !defined(TFTSERIAL_H) -#define TFTSERIAL_H - -#include "Display.h" -#include "Defines.h" -#include "SerialPort.h" - -#include - -class CTFTSerial : public CDisplay -{ -public: - CTFTSerial(const std::string& callsign, unsigned int dmrid, ISerialPort* serial, unsigned int brightness); - virtual ~CTFTSerial(); - - virtual bool open(); - - virtual void close(); - -protected: - virtual void setIdleInt(); - virtual void setErrorInt(const char* text); - virtual void setLockoutInt(); - virtual void setQuitInt(); - virtual void setFMInt(); - - virtual void writeDStarInt(const char* my1, const char* my2, const char* your, const char* type, const char* reflector); - virtual void clearDStarInt(); - - virtual void writeDMRInt(unsigned int slotNo, const std::string& src, bool group, const std::string& dst, const char* type); - virtual void clearDMRInt(unsigned int slotNo); - - virtual void writeFusionInt(const char* source, const char* dest, unsigned char dgid, const char* type, const char* origin); - virtual void clearFusionInt(); - - virtual void writeP25Int(const char* source, bool group, unsigned int dest, const char* type); - virtual void clearP25Int(); - - virtual void writeNXDNInt(const char* source, bool group, unsigned int dest, const char* type); - virtual void clearNXDNInt(); - - virtual void writeM17Int(const char* source, const char* dest, const char* type); - virtual void clearM17Int(); - - virtual void writePOCSAGInt(uint32_t ric, const std::string& message); - virtual void clearPOCSAGInt(); - - virtual void writeCWInt(); - virtual void clearCWInt(); - -private: - std::string m_callsign; - unsigned int m_dmrid; - ISerialPort* m_serial; - unsigned int m_brightness; - unsigned char m_mode; - - void clearScreen(); - void setBackground(unsigned char colour); - void setForeground(unsigned char colour); - void setRotation(unsigned char rotation); - void setFontSize(unsigned char size); - void gotoBegOfLine(); - void gotoPosText(unsigned char x, unsigned char y); - void gotoPosPixel(unsigned char x, unsigned char y); - void drawLine(unsigned char x1, unsigned char y1, unsigned char x2, unsigned char y2); - void drawBox(unsigned char x1, unsigned char y1, unsigned char x2, unsigned char y2, bool filled); - void drawCircle(unsigned char x, unsigned char y, unsigned char radius, bool filled); - void displayBitmap(unsigned char x, unsigned char y, const char* filename); - void setBrightness(unsigned char brightness); - void displayText(const char* text); -}; - -#endif diff --git a/TFTSerial/DMR_sm.bmp b/TFTSerial/DMR_sm.bmp deleted file mode 100644 index 2fae46a3c491253e54cf05c22140722c0475c53a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24054 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 diff --git a/TFTSerial/DStar_sm.bmp b/TFTSerial/DStar_sm.bmp deleted file mode 100644 index f4e2afc1982b92f99f9fba2ee3b1fc9ff86ee394..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24054 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 diff --git a/TFTSerial/MMDVM_sm.bmp b/TFTSerial/MMDVM_sm.bmp deleted file mode 100644 index 674d1c6360b1b6adb7f9296af3121cb3c0c3cca3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25494 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 diff --git a/TFTSerial/NXDN_sm.bmp b/TFTSerial/NXDN_sm.bmp deleted file mode 100644 index 30420d87fc103c16695e4786c48af97583df7c27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23574 zcmeHOXN*o;6g4D7jZV}E(G$J*r-tZ6@1hH$L??(|Lv*4OB3g(N3DJJ&f+z{2cR}>_ z-r^)@&g^r~y7#+Y=Er;YOJ-)Dz1LcM-}8OVc>Sq8^BPAnJktN)LSh z{yhmU<(_mYQGl~&&k}iG zk;|7a6Z-V&(|r|u0Yo2fAE(!11yLISKjONXoo3ge&Y}hcDe%`!!ZM^Am5w-v6)2Ajr zgnjDNDXnH3L~!%w%{E?mmM>o}6msTN+a3a#(8QOupr~e}MvW3cUe=K#M+DIe71FnF z-##I(n~xqnBBCEYe9%Z?@SaziqeqVxtPo97-o}j^U4rGymnTFODpI6~P0yDvA18kM z_RVH-3ZgcpNs}hNtl)qF0|wYBlqX}xj4qw2Q>VIEZQ8VPv9v#R>eMcJaDTUM-L#-E zN|!F}(h*2k)AN2TSg@cAulyqEty{M^zG1_Lf`aDJqenR_)@XwU4eTuNOqw*wC55jf zPYRqaityjQecPoF(0}F16`^T1fByVw>#SV4(!>yZ_Uzd<9Uk1;goTjFnmlPx9o+O-<8Yx^$_nR=jv|6GQCW zxpUie&J>r2wO;qd|;ImoC`~ zF?sIXxkD0(LE!xP^9k4vjUPXr6jQO{ucJk-SN81LeZNMxZrw`sUCPSuB4U*zCL&+M zxq0)ZnP}(Eom^Z*yL9Pd(!<$gb!f(n8Kx$tb)o>?#L9+Ax9#fHt8)b&wFONNzZo%2 zHo>M%Rt8Ydh!G=9Fzlj5f1Q8xeD&+s&jg29dok(Z>~Z79dDaFeCaqc%_p%E{{P*wQ z*9x9SojP?0orJ_HnJv!8-Zg91v?R%HU&86U=2jqbE+BFcm`B7-M)UE9c9X zFO?=HbOh!K4H+`Tx1{>@>$~X0zk2m*Uu80SgfgK|n>LN49y4Z);1ck;bLSLz!-fq6 z<_fun3Zeks%$6;iOMmRxv5HIP@-D9O^VBLDVnrf|ES{yLHnN5TaY95$WUo-6f(zfN zQzr$+l7b7v{UTS0=B!z>Tq*GF3_*U6^DnHcSFZ*siHnHxiWMssMDy9RXM$)pOO+}` zMB27(Yl4L>)-AVf+a>^JQ)Mwd!meGrHfDvwshj)v?IPD zZX_Sh9zA*lsR5Xd9Xo1GZme0eCZHzY+8`o)3$JqJ%7RM@K7aoFx^?Ra0MoA4ty{YQ z7@~+=A(~|65TocEIdZtN!;f^UR;}WIfF+eIS(51J(6VLA0t!xwM90RP z9zm>Ix2_pA?cTk6SE*9P^avYL*d-B8vng_gXkxX>6bx?n>C;E0Bb3vpPX{cCh=>qI ztwA)g^C*aB6Fd7vgg>Hp?b>B>4;(mPDhIbQyi_`b!e{zs8N?>~B!u-nrAgK+uwz2< z>d>)c$4DYOgolRa5+i^9{5Ds6#2SvNgM%mPZ*r~TgFw@#fukRTwlNMgPIT9T!&#ke*DN&>%|{^ zQ5#;aGa6cRHUQWd$2`3$sY&z?AEg{K6bkX5TzB`M6%yLa#G zD>4Dx#wN8*A3AiXi-lDP8y~BuY15`IebbNqSx@DYCr_F%VS7^G#L)|$iImPoX#^;2yVv6GYv#3fpalXh`>e{ty8ZV5A6DPXpY0{(-;1ErG@4NJs zA1gvcmLJQ+UWVZE%~oW*d-txOpvm`!z_Rb+;Aa>4^yR!{T6e4z^LcJNV7;BEaXyRUC87r8>dfQ z3cg%h&Gxv&mH+eS&o;&MU@YoV5X*YL$9~F|Eo&+Z+xM1SxNxCKiD{SFz64Cr#F05u z+_W=i&TP}Q2P>Z@*tT(cT!U@H#>eWx_ZObh*t!+3u3fthA3oey?ZJZw*i#T()ojtC z1)1GUDd1lZ%*Q)!d6Lc+!J-Ntw%AVc>Sq8^BPAnJjr2cjN`dLZh7 Ks0aSfJ@5}d(?}!$ diff --git a/TFTSerial/P25_sm.bmp b/TFTSerial/P25_sm.bmp deleted file mode 100644 index 5ba24f609b837d732147cbb0704ec38d481ab7d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24054 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 From a4ba00f5676833a3a3686331d9eaceac42f8e7ca Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 10 Nov 2020 11:45:13 +0000 Subject: [PATCH 150/163] Small cleanups. --- MMDVM.ini | 2 +- SerialModem.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MMDVM.ini b/MMDVM.ini index e64da52..f35e75e 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -198,7 +198,7 @@ Debug=0 [DMR Network] Enable=1 -Address=44.131.4.1 +Address=127.0.0.1 Port=62031 Local=62032 Jitter=360 diff --git a/SerialModem.cpp b/SerialModem.cpp index 32b057c..add57ff 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -1910,16 +1910,16 @@ bool CSerialModem::readVersion() LogInfo("MMDVM protocol version: %u, description: %.*s", m_protocolVersion, m_length - 23U, m_buffer + 23U); switch (m_buffer[6U]) { case 0U: - LogInfo("Atmel ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U], m_buffer[19U], m_buffer[20U], m_buffer[21U], m_buffer[22U]); + LogInfo("CPU: Atmel ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U], m_buffer[19U], m_buffer[20U], m_buffer[21U], m_buffer[22U]); break; case 1U: - LogInfo("NXP ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U], m_buffer[19U], m_buffer[20U], m_buffer[21U], m_buffer[22U]); + LogInfo("CPU: NXP ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U], m_buffer[19U], m_buffer[20U], m_buffer[21U], m_buffer[22U]); break; case 2U: - LogInfo("ST-Micro ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U]); + LogInfo("CPU: ST-Micro ARM, UDID: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", m_buffer[7U], m_buffer[8U], m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U], m_buffer[13U], m_buffer[14U], m_buffer[15U], m_buffer[16U], m_buffer[17U], m_buffer[18U]); break; default: - LogInfo("Unknown CPU type: %u", m_buffer[6U]); + LogInfo("CPU: Unknown type: %u", m_buffer[6U]); break; } char modeText[100U]; From 8c65b1c1d986a245e110411d30ba083af02c2c24 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 10 Nov 2020 13:50:30 +0000 Subject: [PATCH 151/163] Remove the out of date ISSUES.txt --- ISSUES.txt | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 ISSUES.txt diff --git a/ISSUES.txt b/ISSUES.txt deleted file mode 100644 index 537f79f..0000000 --- a/ISSUES.txt +++ /dev/null @@ -1,9 +0,0 @@ -D-Star: On some radios, the header is not decoded correctly. It looks like frequency drift at the beginning of the transmission. - -DMR: DMO mode doesn't wake up older radios like other radios do. - -YSF: No known issues. - -P25: Upgrade the filters, processing power in the Due permiting. - -NXDN: No known issues. From ffe0fd0cbb67d32d3792a4728a5f0b1e5417f6f2 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 10 Nov 2020 16:58:03 +0000 Subject: [PATCH 152/163] Change the M17 enable bit position for version 1 protocol. --- SerialModem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index add57ff..9b893c8 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -2031,7 +2031,7 @@ bool CSerialModem::setConfig1() if (m_pocsagEnabled) buffer[4U] |= 0x20U; if (m_m17Enabled) - buffer[4U] |= 0x40U; + buffer[4U] |= 0x80U; buffer[5U] = m_txDelay / 10U; // In 10ms units From c1bb4c825e0a8680269283dea8bb309dbad728ac Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Wed, 11 Nov 2020 11:47:46 +0000 Subject: [PATCH 153/163] Change M17 enable bit position. --- SerialModem.cpp | 2 +- Version.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index 9b893c8..add57ff 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -2031,7 +2031,7 @@ bool CSerialModem::setConfig1() if (m_pocsagEnabled) buffer[4U] |= 0x20U; if (m_m17Enabled) - buffer[4U] |= 0x80U; + buffer[4U] |= 0x40U; buffer[5U] = m_txDelay / 10U; // In 10ms units diff --git a/Version.h b/Version.h index 5fe7f6f..27d6669 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201110"; +const char* VERSION = "20201111"; #endif From bb5e20538dae560f52f1e11b49cf2bb167f35017 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Wed, 11 Nov 2020 18:35:52 +0000 Subject: [PATCH 154/163] Change the M17 logging slightly. --- M17Control.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/M17Control.cpp b/M17Control.cpp index 060f4ef..9a9f359 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -416,7 +416,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) for (unsigned int i = 2U; i < (M17_FRAME_LENGTH_BYTES + 2U); i++) errors += countBits(rfData[i] ^ data[i]); - LogDebug("M17, FN. %u, errs: %u/384 (%.1f%%)", m_rfFN, errors, float(errors) / 3.84F); + LogDebug("M17, FN: %u, errs: %u/384 (%.1f%%)", m_rfFN, errors, float(errors) / 3.84F); m_rfBits += M17_FRAME_LENGTH_BITS; m_rfErrs += errors; From 021c0fea4da8ffe5e6d78ce69d28db57f6fce256 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Fri, 13 Nov 2020 09:24:37 +0000 Subject: [PATCH 155/163] Fix M17 bug spotted by nostar. --- M17Network.cpp | 2 +- Version.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/M17Network.cpp b/M17Network.cpp index 6fa65ae..56a2135 100644 --- a/M17Network.cpp +++ b/M17Network.cpp @@ -76,7 +76,7 @@ bool CM17Network::open() bool CM17Network::write(const unsigned char* data) { - if (m_addrLen != 0U) + if (m_addrLen == 0U) return false; assert(data != NULL); diff --git a/Version.h b/Version.h index 27d6669..3f190ee 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201111"; +const char* VERSION = "20201113"; #endif From a98f65e3aa99f5b1f890584bfcfe2b41592a51a2 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Fri, 13 Nov 2020 12:10:41 +0000 Subject: [PATCH 156/163] Rename the modem data type MMDVM_SERIAL to MMDVM_SERIAL_DATA to match the modem. --- SerialModem.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/SerialModem.cpp b/SerialModem.cpp index add57ff..8f498d2 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -97,7 +97,7 @@ const unsigned char MMDVM_FM_EOT = 0x67U; const unsigned char MMDVM_ACK = 0x70U; const unsigned char MMDVM_NAK = 0x7FU; -const unsigned char MMDVM_SERIAL = 0x80U; +const unsigned char MMDVM_SERIAL_DATA = 0x80U; const unsigned char MMDVM_TRANSPARENT = 0x90U; const unsigned char MMDVM_QSO_INFO = 0x91U; @@ -864,7 +864,7 @@ void CSerialModem::clock(unsigned int ms) printDebug(); break; - case MMDVM_SERIAL: + case MMDVM_SERIAL_DATA: //MMDVMHost does not process serial data from the display, // so we send it to the transparent port if sendFrameType==1 if (m_sendTransparentDataFrameType > 0U) { @@ -879,6 +879,8 @@ void CSerialModem::clock(unsigned int ms) m_rxTransparentData.addData(m_buffer + m_offset - offset, m_length - m_offset + offset); break; //only break when sendFrameType>0, else message is unknown } + break; + default: LogMessage("Unknown message, type: %02X", m_type); CUtils::dump("Buffer dump", m_buffer, m_length); @@ -1816,11 +1818,11 @@ bool CSerialModem::writeSerial(const unsigned char* data, unsigned int length) assert(data != NULL); assert(length > 0U); - unsigned char buffer[250U]; + unsigned char buffer[255U]; buffer[0U] = MMDVM_FRAME_START; buffer[1U] = length + 3U; - buffer[2U] = MMDVM_SERIAL; + buffer[2U] = MMDVM_SERIAL_DATA; ::memcpy(buffer + 3U, data, length); From 2618a14ae54cc7397394569a3755d353fdf1b125 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Fri, 13 Nov 2020 14:56:39 +0000 Subject: [PATCH 157/163] Handle returned modem serial data properly. --- Modem.h | 9 ++++----- NullModem.h | 9 ++++----- SerialModem.cpp | 47 +++++++++++++++++++++++++++++------------------ SerialModem.h | 11 ++++++----- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/Modem.h b/Modem.h index 566ebbd..faf0917 100644 --- a/Modem.h +++ b/Modem.h @@ -55,9 +55,6 @@ public: virtual unsigned int readM17Data(unsigned char* data) = 0; virtual unsigned int readFMData(unsigned char* data) = 0; virtual unsigned int readAX25Data(unsigned char* data) = 0; - virtual unsigned int readTransparentData(unsigned char* data) = 0; - - virtual unsigned int readSerial(unsigned char* data, unsigned int length) = 0; virtual bool hasDStarSpace() const = 0; virtual bool hasDMRSpace1() const = 0; @@ -88,8 +85,6 @@ public: virtual bool writeFMData(const unsigned char* data, unsigned int length) = 0; virtual bool writeAX25Data(const unsigned char* data, unsigned int length) = 0; - virtual bool writeTransparentData(const unsigned char* data, unsigned int length) = 0; - virtual bool writeDStarInfo(const char* my1, const char* my2, const char* your, const char* type, const char* reflector) = 0; virtual bool writeDMRInfo(unsigned int slotNo, const std::string& src, bool group, const std::string& dst, const char* type) = 0; virtual bool writeYSFInfo(const char* source, const char* dest, unsigned char dgid, const char* type, const char* origin) = 0; @@ -103,7 +98,11 @@ public: virtual bool writeDMRShortLC(const unsigned char* lc) = 0; virtual bool writeDMRAbort(unsigned int slotNo) = 0; + virtual bool writeTransparentData(const unsigned char* data, unsigned int length) = 0; + virtual unsigned int readTransparentData(unsigned char* data) = 0; + virtual bool writeSerial(const unsigned char* data, unsigned int length) = 0; + virtual unsigned int readSerial(unsigned char* data, unsigned int length) = 0; virtual unsigned char getMode() const = 0; virtual bool setMode(unsigned char mode) = 0; diff --git a/NullModem.h b/NullModem.h index 6126313..83e60d6 100644 --- a/NullModem.h +++ b/NullModem.h @@ -57,9 +57,6 @@ public: virtual unsigned int readM17Data(unsigned char* data) { return 0U; }; virtual unsigned int readFMData(unsigned char* data) { return 0U; }; virtual unsigned int readAX25Data(unsigned char* data) { return 0U; }; - virtual unsigned int readTransparentData(unsigned char* data) { return 0U; }; - - virtual unsigned int readSerial(unsigned char* data, unsigned int length) { return 0; }; virtual bool hasDStarSpace()const { return true; }; virtual bool hasDMRSpace1() const { return true; }; @@ -89,8 +86,6 @@ public: virtual bool writeFMData(const unsigned char* data, unsigned int length) { return true; }; virtual bool writeAX25Data(const unsigned char* data, unsigned int length) { return true; }; - virtual bool writeTransparentData(const unsigned char* data, unsigned int length) { return true; }; - virtual bool writeConfig() { return true; }; virtual bool writeDStarInfo(const char* my1, const char* my2, const char* your, const char* type, const char* reflector) { return true; }; virtual bool writeDMRInfo(unsigned int slotNo, const std::string& src, bool group, const std::string& dst, const char* type) { return true; }; @@ -105,7 +100,11 @@ public: virtual bool writeDMRShortLC(const unsigned char* lc) { return true; }; virtual bool writeDMRAbort(unsigned int slotNo) { return true; }; + virtual bool writeTransparentData(const unsigned char* data, unsigned int length) { return true; }; + virtual unsigned int readTransparentData(unsigned char* data) { return 0U; }; + virtual bool writeSerial(const unsigned char* data, unsigned int length) { return true; }; + virtual unsigned int readSerial(unsigned char* data, unsigned int length) { return 0U; }; virtual unsigned char getMode() const { return MODE_IDLE; }; virtual bool setMode(unsigned char mode) { return true; }; diff --git a/SerialModem.cpp b/SerialModem.cpp index 8f498d2..3d7652d 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -182,6 +182,8 @@ m_rxFMData(5000U, "Modem RX FM"), m_txFMData(5000U, "Modem TX FM"), m_rxAX25Data(1000U, "Modem RX AX.25"), m_txAX25Data(1000U, "Modem TX AX.25"), +m_rxSerialData(1000U, "Modem RX Serial"), +m_txSerialData(1000U, "Modem TX Serial"), m_rxTransparentData(1000U, "Modem RX Transparent"), m_txTransparentData(1000U, "Modem TX Transparent"), m_sendTransparentDataFrameType(0U), @@ -865,20 +867,9 @@ void CSerialModem::clock(unsigned int ms) break; case MMDVM_SERIAL_DATA: - //MMDVMHost does not process serial data from the display, - // so we send it to the transparent port if sendFrameType==1 - if (m_sendTransparentDataFrameType > 0U) { - if (m_trace) - CUtils::dump(1U, "RX Serial Data", m_buffer, m_length); - - unsigned char offset = m_sendTransparentDataFrameType; - if (offset > 1U) offset = 1U; - unsigned char data = m_length - m_offset + offset; - m_rxTransparentData.addData(&data, 1U); - - m_rxTransparentData.addData(m_buffer + m_offset - offset, m_length - m_offset + offset); - break; //only break when sendFrameType>0, else message is unknown - } + if (m_trace) + CUtils::dump(1U, "RX Serial Data", m_buffer, m_length); + m_rxSerialData.addData(m_buffer + m_offset, m_length - m_offset); break; default: @@ -1099,6 +1090,19 @@ void CSerialModem::clock(unsigned int ms) if (ret != int(len)) LogWarning("Error when writing Transparent data to the MMDVM"); } + + if (!m_txSerialData.isEmpty()) { + unsigned char len = 0U; + m_txSerialData.getData(&len, 1U); + m_txSerialData.getData(m_buffer, len); + + if (m_trace) + CUtils::dump(1U, "TX Serial Data", m_buffer, len); + + int ret = m_serial->write(m_buffer, len); + if (ret != int(len)) + LogWarning("Error when writing Serial data to the MMDVM"); + } } void CSerialModem::close() @@ -1250,13 +1254,18 @@ unsigned int CSerialModem::readTransparentData(unsigned char* data) return len; } -// To be implemented later if needed unsigned int CSerialModem::readSerial(unsigned char* data, unsigned int length) { assert(data != NULL); assert(length > 0U); - return 0U; + unsigned int n = 0U; + while (!m_rxSerialData.isEmpty() && n < length) { + m_rxSerialData.getData(data + n, 1U); + n++; + } + + return n; } bool CSerialModem::hasDStarSpace() const @@ -1826,9 +1835,11 @@ bool CSerialModem::writeSerial(const unsigned char* data, unsigned int length) ::memcpy(buffer + 3U, data, length); - int ret = m_serial->write(buffer, length + 3U); + unsigned char len = length + 3U; + m_txSerialData.addData(&len, 1U); + m_txSerialData.addData(buffer, len); - return ret != int(length + 3U); + return true; } bool CSerialModem::hasTX() const diff --git a/SerialModem.h b/SerialModem.h index 45bfea7..4b56077 100644 --- a/SerialModem.h +++ b/SerialModem.h @@ -75,9 +75,6 @@ public: virtual unsigned int readM17Data(unsigned char* data); virtual unsigned int readFMData(unsigned char* data); virtual unsigned int readAX25Data(unsigned char* data); - virtual unsigned int readTransparentData(unsigned char* data); - - virtual unsigned int readSerial(unsigned char* data, unsigned int length); virtual bool hasDStarSpace() const; virtual bool hasDMRSpace1() const; @@ -108,8 +105,6 @@ public: virtual bool writeFMData(const unsigned char* data, unsigned int length); virtual bool writeAX25Data(const unsigned char* data, unsigned int length); - virtual bool writeTransparentData(const unsigned char* data, unsigned int length); - virtual bool writeDStarInfo(const char* my1, const char* my2, const char* your, const char* type, const char* reflector); virtual bool writeDMRInfo(unsigned int slotNo, const std::string& src, bool group, const std::string& dst, const char* type); virtual bool writeYSFInfo(const char* source, const char* dest, unsigned char dgid, const char* type, const char* origin); @@ -123,7 +118,11 @@ public: virtual bool writeDMRShortLC(const unsigned char* lc); virtual bool writeDMRAbort(unsigned int slotNo); + virtual bool writeTransparentData(const unsigned char* data, unsigned int length); + virtual unsigned int readTransparentData(unsigned char* data); + virtual bool writeSerial(const unsigned char* data, unsigned int length); + virtual unsigned int readSerial(unsigned char* data, unsigned int length); virtual unsigned char getMode() const; virtual bool setMode(unsigned char mode); @@ -205,6 +204,8 @@ private: CRingBuffer m_txFMData; CRingBuffer m_rxAX25Data; CRingBuffer m_txAX25Data; + CRingBuffer m_rxSerialData; + CRingBuffer m_txSerialData; CRingBuffer m_rxTransparentData; CRingBuffer m_txTransparentData; unsigned int m_sendTransparentDataFrameType; From 302a23b6e655f4b9994f03bad408c3b8d7bb16fc Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Fri, 13 Nov 2020 16:01:46 +0000 Subject: [PATCH 158/163] Add M17 to the OLED display. --- OLED.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- OLED.h | 3 +++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/OLED.cpp b/OLED.cpp index c83c714..6adaddf 100644 --- a/OLED.cpp +++ b/OLED.cpp @@ -148,6 +148,28 @@ const unsigned char logo_NXDN_bmp [] = 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; +// Logo M17_sm, 128x16px +// XXX FIXME This is the NXDN logo, it needs replacing with the M17 logo +const unsigned char logo_M17_bmp [] = +{ +0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +0xff, 0xf0, 0x1f, 0xf8, 0x0f, 0x00, 0xff, 0x80, 0x7c, 0x00, 0x0f, 0xff, 0x80, 0x7f, 0xe0, 0x7f, +0xff, 0xe0, 0x0f, 0xf0, 0x1f, 0x80, 0x7e, 0x01, 0xf8, 0x00, 0x00, 0x7f, 0x00, 0x3f, 0xc0, 0x7f, +0xff, 0xc0, 0x07, 0xe0, 0x3f, 0x80, 0x38, 0x07, 0xf0, 0x00, 0x00, 0x3e, 0x00, 0x3f, 0x80, 0xff, +0xff, 0x80, 0x03, 0xc0, 0x3f, 0xc0, 0x00, 0x3f, 0xe0, 0x1f, 0x80, 0x3e, 0x00, 0x1f, 0x01, 0xff, +0xff, 0x00, 0x03, 0x80, 0x7f, 0xe0, 0x00, 0xff, 0xc0, 0x3f, 0x80, 0x3c, 0x00, 0x0e, 0x03, 0xff, +0xfe, 0x00, 0x01, 0x00, 0xff, 0xe0, 0x03, 0xff, 0x80, 0x7f, 0x80, 0x78, 0x08, 0x04, 0x03, 0xff, +0xfc, 0x03, 0x00, 0x01, 0xff, 0x80, 0x01, 0xff, 0x00, 0xff, 0x00, 0xf0, 0x1c, 0x00, 0x07, 0xff, +0xfc, 0x07, 0x80, 0x03, 0xfc, 0x00, 0x01, 0xfe, 0x01, 0xfc, 0x01, 0xe0, 0x1e, 0x00, 0x0f, 0xff, +0xf8, 0x0f, 0xc0, 0x07, 0xf0, 0x0e, 0x00, 0xfc, 0x00, 0x00, 0x07, 0xc0, 0x3f, 0x00, 0x1f, 0xff, +0xf0, 0x1f, 0xe0, 0x0f, 0x80, 0x3f, 0x00, 0x7c, 0x00, 0x00, 0x3f, 0xc0, 0x7f, 0x80, 0x3f, 0xff, +0xe0, 0x3f, 0xf0, 0x0e, 0x01, 0xff, 0x80, 0x38, 0x00, 0x07, 0xff, 0x80, 0xff, 0x80, 0x7f, 0xff, +0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + // Logo POCASG/DAPNET, 128x16px const unsigned char logo_POCSAG_bmp [] = { @@ -500,7 +522,6 @@ void COLED::writeP25Int(const char* source, bool group, unsigned int dest, const OLED_statusbar(); m_display.display(); - } void COLED::clearP25Int() @@ -566,6 +587,36 @@ void COLED::clearNXDNInt() m_display.display(); } +void COLED::writeM17Int(const char* source, const char* dest, const char* type) +{ + m_mode = MODE_M17; + + m_display.clearDisplay(); + m_display.fillRect(0, OLED_LINE2, m_display.width(), m_display.height(), BLACK); + + m_display.setCursor(0,OLED_LINE3); + m_display.printf("%s %s", type, source); + + m_display.setCursor(0,OLED_LINE4); + m_display.printf(" %s", dest); + + OLED_statusbar(); + m_display.display(); +} + +void COLED::clearM17Int() +{ + m_display.fillRect(0, OLED_LINE2, m_display.width(), m_display.height(), BLACK); + + m_display.setCursor(40,OLED_LINE4); + m_display.print("Listening"); + + m_display.setCursor(0,OLED_LINE6); + m_display.printf("%s",m_ipaddress.c_str()); + + m_display.display(); +} + void COLED::writePOCSAGInt(uint32_t ric, const std::string& message) { m_mode = MODE_POCSAG; @@ -583,7 +634,6 @@ void COLED::writePOCSAGInt(uint32_t ric, const std::string& message) OLED_statusbar(); m_display.display(); - } void COLED::clearPOCSAGInt() @@ -658,6 +708,8 @@ void COLED::OLED_statusbar() m_display.drawBitmap(0, 0, logo_P25_bmp, 128, 16, WHITE); else if (m_mode == MODE_NXDN) m_display.drawBitmap(0, 0, logo_NXDN_bmp, 128, 16, WHITE); + else if (m_mode == MODE_M17) + m_display.drawBitmap(0, 0, logo_M17_bmp, 128, 16, WHITE); else if (m_mode == MODE_POCSAG) m_display.drawBitmap(0, 0, logo_POCSAG_bmp, 128, 16, WHITE); else if (m_displayLogoScreensaver) diff --git a/OLED.h b/OLED.h index 5f3ead0..45c78ad 100644 --- a/OLED.h +++ b/OLED.h @@ -70,6 +70,9 @@ public: virtual int writeNXDNIntEx(const class CUserDBentry& source, bool group, unsigned int dest, const char* type); virtual void clearNXDNInt(); + virtual void writeM17Int(const char* source, const char* dest, const char* type); + virtual void clearM17Int(); + virtual void writePOCSAGInt(uint32_t ric, const std::string& message); virtual void clearPOCSAGInt(); From 43733df7a415bd51d4dda29fb68216f85df1cfa4 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 16 Nov 2020 16:56:36 +0000 Subject: [PATCH 159/163] Add M17 to the OLED display, even though it does say NXDN! --- OLED.cpp | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- OLED.h | 3 +++ Version.h | 2 +- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/OLED.cpp b/OLED.cpp index c83c714..abdd2ac 100644 --- a/OLED.cpp +++ b/OLED.cpp @@ -148,6 +148,28 @@ const unsigned char logo_NXDN_bmp [] = 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; +// XXX FIXME This is still the NXDN logo +// Logo M17_sm, 128x16px +const unsigned char logo_M17_bmp [] = +{ +0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +0xff, 0xf0, 0x1f, 0xf8, 0x0f, 0x00, 0xff, 0x80, 0x7c, 0x00, 0x0f, 0xff, 0x80, 0x7f, 0xe0, 0x7f, +0xff, 0xe0, 0x0f, 0xf0, 0x1f, 0x80, 0x7e, 0x01, 0xf8, 0x00, 0x00, 0x7f, 0x00, 0x3f, 0xc0, 0x7f, +0xff, 0xc0, 0x07, 0xe0, 0x3f, 0x80, 0x38, 0x07, 0xf0, 0x00, 0x00, 0x3e, 0x00, 0x3f, 0x80, 0xff, +0xff, 0x80, 0x03, 0xc0, 0x3f, 0xc0, 0x00, 0x3f, 0xe0, 0x1f, 0x80, 0x3e, 0x00, 0x1f, 0x01, 0xff, +0xff, 0x00, 0x03, 0x80, 0x7f, 0xe0, 0x00, 0xff, 0xc0, 0x3f, 0x80, 0x3c, 0x00, 0x0e, 0x03, 0xff, +0xfe, 0x00, 0x01, 0x00, 0xff, 0xe0, 0x03, 0xff, 0x80, 0x7f, 0x80, 0x78, 0x08, 0x04, 0x03, 0xff, +0xfc, 0x03, 0x00, 0x01, 0xff, 0x80, 0x01, 0xff, 0x00, 0xff, 0x00, 0xf0, 0x1c, 0x00, 0x07, 0xff, +0xfc, 0x07, 0x80, 0x03, 0xfc, 0x00, 0x01, 0xfe, 0x01, 0xfc, 0x01, 0xe0, 0x1e, 0x00, 0x0f, 0xff, +0xf8, 0x0f, 0xc0, 0x07, 0xf0, 0x0e, 0x00, 0xfc, 0x00, 0x00, 0x07, 0xc0, 0x3f, 0x00, 0x1f, 0xff, +0xf0, 0x1f, 0xe0, 0x0f, 0x80, 0x3f, 0x00, 0x7c, 0x00, 0x00, 0x3f, 0xc0, 0x7f, 0x80, 0x3f, 0xff, +0xe0, 0x3f, 0xf0, 0x0e, 0x01, 0xff, 0x80, 0x38, 0x00, 0x07, 0xff, 0x80, 0xff, 0x80, 0x7f, 0xff, +0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + // Logo POCASG/DAPNET, 128x16px const unsigned char logo_POCSAG_bmp [] = { @@ -500,7 +522,6 @@ void COLED::writeP25Int(const char* source, bool group, unsigned int dest, const OLED_statusbar(); m_display.display(); - } void COLED::clearP25Int() @@ -566,6 +587,36 @@ void COLED::clearNXDNInt() m_display.display(); } +void COLED::writeM17Int(const char* source, const char* dest, const char* type) +{ + m_mode = MODE_M17; + + m_display.clearDisplay(); + m_display.fillRect(0, OLED_LINE2, m_display.width(), m_display.height(), BLACK); + + m_display.setCursor(0,OLED_LINE3); + m_display.printf("%s %s", type, source); + + m_display.setCursor(0,OLED_LINE4); + m_display.printf(" %s", dest); + + OLED_statusbar(); + m_display.display(); +} + +void COLED::clearM17Int() +{ + m_display.fillRect(0, OLED_LINE2, m_display.width(), m_display.height(), BLACK); + + m_display.setCursor(40,OLED_LINE4); + m_display.print("Listening"); + + m_display.setCursor(0,OLED_LINE6); + m_display.printf("%s",m_ipaddress.c_str()); + + m_display.display(); +} + void COLED::writePOCSAGInt(uint32_t ric, const std::string& message) { m_mode = MODE_POCSAG; @@ -658,6 +709,8 @@ void COLED::OLED_statusbar() m_display.drawBitmap(0, 0, logo_P25_bmp, 128, 16, WHITE); else if (m_mode == MODE_NXDN) m_display.drawBitmap(0, 0, logo_NXDN_bmp, 128, 16, WHITE); + else if (m_mode == MODE_M17) + m_display.drawBitmap(0, 0, logo_M17_bmp, 128, 16, WHITE); else if (m_mode == MODE_POCSAG) m_display.drawBitmap(0, 0, logo_POCSAG_bmp, 128, 16, WHITE); else if (m_displayLogoScreensaver) diff --git a/OLED.h b/OLED.h index 5f3ead0..d7be3e7 100644 --- a/OLED.h +++ b/OLED.h @@ -66,6 +66,9 @@ public: virtual void writeP25Int(const char* source, bool group, unsigned int dest, const char* type); virtual void clearP25Int(); + virtual void writeM17Int(const char* source, const char* dest, const char* type); + virtual void clearM17Int(); + virtual void writeNXDNInt(const char* source, bool group, unsigned int dest, const char* type); virtual int writeNXDNIntEx(const class CUserDBentry& source, bool group, unsigned int dest, const char* type); virtual void clearNXDNInt(); diff --git a/Version.h b/Version.h index 3f190ee..7fc09f6 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201113"; +const char* VERSION = "20201116"; #endif From 8d31808ae89fbb9756c468d3b8047507d149c0d7 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 24 Nov 2020 16:24:32 +0000 Subject: [PATCH 160/163] Increase serial port speed to 460800. --- MMDVM.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MMDVM.ini b/MMDVM.ini index f35e75e..a6ae4e8 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -37,10 +37,10 @@ Time=24 [Modem] # Port=/dev/ttyACM0 -# Port=/dev/ttyAMA0 -Port=\\.\COM4 +Port=/dev/ttyAMA0 +# Port=\\.\COM4 Protocol=uart -Speed=115200 +Speed=460800 # Address=0x22 TXInvert=1 RXInvert=0 From 67b93d6310fe000c82e2dab6bdb1727637f21946 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 26 Nov 2020 11:30:16 +0000 Subject: [PATCH 161/163] Partial work supporting the latest M17 specification. --- Conf.cpp | 8 ++++++++ Conf.h | 2 ++ M17Control.cpp | 27 ++++++++++++++------------- M17Control.h | 3 ++- M17Defines.h | 4 +++- MMDVM.ini | 1 + MMDVMHost.cpp | 12 +++++++----- SerialModem.cpp | 35 +++++++++++++++++++++++++++++------ Sync.cpp | 11 +++++++++-- Sync.h | 3 ++- Version.h | 2 +- 11 files changed, 78 insertions(+), 30 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index 122be9a..854a970 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -172,6 +172,7 @@ m_nxdnRemoteGateway(false), m_nxdnTXHang(5U), m_nxdnModeHang(10U), m_m17Enabled(false), +m_m17ColorCode(1U), m_m17SelfOnly(false), m_m17AllowEncryption(false), m_m17TXHang(5U), @@ -739,6 +740,8 @@ bool CConf::read() } else if (section == SECTION_M17) { if (::strcmp(key, "Enable") == 0) m_m17Enabled = ::atoi(value) == 1; + else if (::strcmp(key, "ColorCode") == 0) + m_m17ColorCode = (unsigned int)::atoi(value); else if (::strcmp(key, "SelfOnly") == 0) m_m17SelfOnly = ::atoi(value) == 1; else if (::strcmp(key, "AllowEncryption") == 0) @@ -1603,6 +1606,11 @@ bool CConf::getM17Enabled() const return m_m17Enabled; } +unsigned int CConf::getM17ColorCode() const +{ + return m_m17ColorCode; +} + bool CConf::getM17SelfOnly() const { return m_m17SelfOnly; diff --git a/Conf.h b/Conf.h index 01d36fd..96b40a0 100644 --- a/Conf.h +++ b/Conf.h @@ -163,6 +163,7 @@ public: // The M17 section bool getM17Enabled() const; + unsigned int getM17ColorCode() const; bool getM17SelfOnly() const; bool getM17AllowEncryption() const; unsigned int getM17TXHang() const; @@ -469,6 +470,7 @@ private: unsigned int m_nxdnModeHang; bool m_m17Enabled; + unsigned int m_m17ColorCode; bool m_m17SelfOnly; bool m_m17AllowEncryption; unsigned int m_m17TXHang; diff --git a/M17Control.cpp b/M17Control.cpp index 9a9f359..d9239ad 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -57,8 +57,9 @@ const unsigned char BIT_MASK_TABLE[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04 #define WRITE_BIT(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) #define READ_BIT(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) -CM17Control::CM17Control(const std::string& callsign, bool selfOnly, bool allowEncryption, CM17Network* network, CDisplay* display, unsigned int timeout, bool duplex, CRSSIInterpolator* rssiMapper) : +CM17Control::CM17Control(const std::string& callsign, unsigned int colorCode, bool selfOnly, bool allowEncryption, CM17Network* network, CDisplay* display, unsigned int timeout, bool duplex, CRSSIInterpolator* rssiMapper) : m_callsign(callsign), +m_colorCode(colorCode), m_selfOnly(selfOnly), m_allowEncryption(allowEncryption), m_network(network), @@ -154,7 +155,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) decorrelator(data + 2U, temp); interleaver(temp, data + 2U); - if (m_rfState == RS_RF_LISTENING) { + if (m_rfState == RS_RF_LISTENING && data[0U] == TAG_HEADER) { m_rfLICH.reset(); CM17Convolution conv; @@ -225,11 +226,11 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) writeFile(data + 2U); #endif if (m_duplex) { - data[0U] = TAG_DATA; + data[0U] = TAG_HEADER; data[1U] = 0x00U; // Generate the sync - CSync::addM17Sync(data + 2U); + CSync::addM17HeaderSync(data + 2U); unsigned char setup[M17_LICH_LENGTH_BYTES]; m_rfLICH.getLinkSetup(setup); @@ -251,7 +252,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) } } - if (m_rfState == RS_RF_LATE_ENTRY) { + if (m_rfState == RS_RF_LATE_ENTRY && data[0U] == TAG_DATA) { CM17Convolution conv; unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; conv.decodeData(data + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_FEC_LENGTH_BYTES, frame); @@ -323,11 +324,11 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) if (m_duplex) { // Create a Link Setup frame - data[0U] = TAG_DATA; + data[0U] = TAG_HEADER; data[1U] = 0x00U; // Generate the sync - CSync::addM17Sync(data + 2U); + CSync::addM17HeaderSync(data + 2U); unsigned char setup[M17_LICH_LENGTH_BYTES]; m_rfLICH.getLinkSetup(setup); @@ -348,7 +349,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) } } - if (m_rfState == RS_RF_AUDIO || m_rfState == RS_RF_DATA) { + if ((m_rfState == RS_RF_AUDIO || m_rfState == RS_RF_DATA) && data[0U] == TAG_DATA) { #if defined(DUMP_M17) writeFile(data + 2U); #endif @@ -391,7 +392,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) rfData[1U] = 0x00U; // Generate the sync - CSync::addM17Sync(rfData + 2U); + CSync::addM17DataSync(rfData + 2U); unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; m_netLICH.getFragment(lich, m_rfFN); @@ -460,7 +461,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) return true; } - if (m_rfState == RS_RF_REJECTED) { + if (m_rfState == RS_RF_REJECTED && data[0U] == TAG_DATA) { CM17Convolution conv; unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; conv.decodeData(data + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_FEC_LENGTH_BYTES, frame); @@ -583,11 +584,11 @@ void CM17Control::writeNetwork() // Create a dummy start message unsigned char start[M17_FRAME_LENGTH_BYTES + 2U]; - start[0U] = TAG_DATA; + start[0U] = TAG_HEADER; start[1U] = 0x00U; // Generate the sync - CSync::addM17Sync(start + 2U); + CSync::addM17HeaderSync(start + 2U); unsigned char setup[M17_LICH_LENGTH_BYTES]; m_netLICH.getLinkSetup(setup); @@ -609,7 +610,7 @@ void CM17Control::writeNetwork() data[1U] = 0x00U; // Generate the sync - CSync::addM17Sync(data + 2U); + CSync::addM17DataSync(data + 2U); m_netFrames++; diff --git a/M17Control.h b/M17Control.h index bff2d19..6838aa7 100644 --- a/M17Control.h +++ b/M17Control.h @@ -34,7 +34,7 @@ class CM17Control { public: - CM17Control(const std::string& callsign, bool selfOnly, bool allowEncryption, CM17Network* network, CDisplay* display, unsigned int timeout, bool duplex, CRSSIInterpolator* rssiMapper); + CM17Control(const std::string& callsign, unsigned int colorCode, bool selfOnly, bool allowEncryption, CM17Network* network, CDisplay* display, unsigned int timeout, bool duplex, CRSSIInterpolator* rssiMapper); ~CM17Control(); bool writeModem(unsigned char* data, unsigned int len); @@ -49,6 +49,7 @@ public: private: std::string m_callsign; + unsigned int m_colorCode; bool m_selfOnly; bool m_allowEncryption; CM17Network* m_network; diff --git a/M17Defines.h b/M17Defines.h index 53b5f63..7cdad35 100644 --- a/M17Defines.h +++ b/M17Defines.h @@ -24,7 +24,9 @@ const unsigned int M17_RADIO_SYMBOL_LENGTH = 5U; // At 24 kHz sample rate const unsigned int M17_FRAME_LENGTH_BITS = 384U; const unsigned int M17_FRAME_LENGTH_BYTES = M17_FRAME_LENGTH_BITS / 8U; -const unsigned char M17_SYNC_BYTES[] = {0x32U, 0x43U}; +const unsigned char M17_HEADER_SYNC_BYTES[] = {0x5DU, 0xDDU}; +const unsigned char M17_DATA_SYNC_BYTES[] = {0xDDU, 0xDDU}; + const unsigned int M17_SYNC_LENGTH_BITS = 16U; const unsigned int M17_SYNC_LENGTH_BYTES = M17_SYNC_LENGTH_BITS / 8U; diff --git a/MMDVM.ini b/MMDVM.ini index a6ae4e8..5880d16 100644 --- a/MMDVM.ini +++ b/MMDVM.ini @@ -133,6 +133,7 @@ TXHang=5 [M17] Enable=1 +ColorCode=3 SelfOnly=0 TXHang=5 # ModeHang=10 diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 238c14a..0673fea 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -619,18 +619,20 @@ int CMMDVMHost::run() } if (m_m17Enabled) { - bool selfOnly = m_conf.getM17SelfOnly(); - bool allowEncryption = m_conf.getM17AllowEncryption(); - unsigned int txHang = m_conf.getM17TXHang(); - m_m17RFModeHang = m_conf.getM17ModeHang(); + bool selfOnly = m_conf.getM17SelfOnly(); + unsigned int colorCode = m_conf.getM17ColorCode(); + bool allowEncryption = m_conf.getM17AllowEncryption(); + unsigned int txHang = m_conf.getM17TXHang(); + m_m17RFModeHang = m_conf.getM17ModeHang(); LogInfo("M17 RF Parameters"); LogInfo(" Self Only: %s", selfOnly ? "yes" : "no"); + LogInfo(" Color Code: %u", colorCode); LogInfo(" Allow Encryption: %s", allowEncryption ? "yes" : "no"); LogInfo(" TX Hang: %us", txHang); LogInfo(" Mode Hang: %us", m_m17RFModeHang); - m_m17 = new CM17Control(m_callsign, selfOnly, allowEncryption, m_m17Network, m_display, m_timeout, m_duplex, rssi); + m_m17 = new CM17Control(m_callsign, colorCode, selfOnly, allowEncryption, m_m17Network, m_display, m_timeout, m_duplex, rssi); } CTimer pocsagTimer(1000U, 30U); diff --git a/SerialModem.cpp b/SerialModem.cpp index 3d7652d..721c736 100644 --- a/SerialModem.cpp +++ b/SerialModem.cpp @@ -79,8 +79,9 @@ const unsigned char MMDVM_P25_LOST = 0x32U; const unsigned char MMDVM_NXDN_DATA = 0x40U; const unsigned char MMDVM_NXDN_LOST = 0x41U; -const unsigned char MMDVM_M17_DATA = 0x45U; -const unsigned char MMDVM_M17_LOST = 0x46U; +const unsigned char MMDVM_M17_HEADER = 0x45U; +const unsigned char MMDVM_M17_DATA = 0x46U; +const unsigned char MMDVM_M17_LOST = 0x47U; const unsigned char MMDVM_POCSAG_DATA = 0x50U; @@ -659,6 +660,20 @@ void CSerialModem::clock(unsigned int ms) } break; + case MMDVM_M17_HEADER: { + if (m_trace) + CUtils::dump(1U, "RX M17 Header", m_buffer, m_length); + + unsigned char data = m_length - 2U; + m_rxM17Data.addData(&data, 1U); + + data = TAG_HEADER; + m_rxM17Data.addData(&data, 1U); + + m_rxM17Data.addData(m_buffer + 3U, m_length - 3U); + } + break; + case MMDVM_M17_DATA: { if (m_trace) CUtils::dump(1U, "RX M17 Data", m_buffer, m_length); @@ -1015,8 +1030,12 @@ void CSerialModem::clock(unsigned int ms) m_txM17Data.getData(&len, 1U); m_txM17Data.getData(m_buffer, len); - if (m_trace) - CUtils::dump(1U, "TX M17 Data", m_buffer, len); + if (m_trace) { + if (m_buffer[2U] == MMDVM_M17_HEADER) + CUtils::dump(1U, "TX M17 Header", m_buffer, len); + else + CUtils::dump(1U, "TX M17 Data", m_buffer, len); + } int ret = m_serial->write(m_buffer, len); if (ret != int(len)) @@ -1471,14 +1490,18 @@ bool CSerialModem::writeM17Data(const unsigned char* data, unsigned int length) assert(data != NULL); assert(length > 0U); - if (data[0U] != TAG_DATA && data[0U] != TAG_EOT) + if (data[0U] != TAG_HEADER && data[0U] != TAG_DATA && data[0U] != TAG_EOT) return false; unsigned char buffer[130U]; buffer[0U] = MMDVM_FRAME_START; buffer[1U] = length + 2U; - buffer[2U] = MMDVM_M17_DATA; + + if (data[0U] == TAG_HEADER) + buffer[2U] = MMDVM_M17_HEADER; + else + buffer[2U] = MMDVM_M17_DATA; ::memcpy(buffer + 3U, data + 1U, length - 1U); diff --git a/Sync.cpp b/Sync.cpp index 34dec37..3e53f64 100644 --- a/Sync.cpp +++ b/Sync.cpp @@ -85,9 +85,16 @@ void CSync::addNXDNSync(unsigned char* data) data[i] = (data[i] & ~NXDN_FSW_BYTES_MASK[i]) | NXDN_FSW_BYTES[i]; } -void CSync::addM17Sync(unsigned char* data) +void CSync::addM17HeaderSync(unsigned char* data) { assert(data != NULL); - ::memcpy(data, M17_SYNC_BYTES, M17_SYNC_LENGTH_BYTES); + ::memcpy(data, M17_HEADER_SYNC_BYTES, M17_SYNC_LENGTH_BYTES); +} + +void CSync::addM17DataSync(unsigned char* data) +{ + assert(data != NULL); + + ::memcpy(data, M17_DATA_SYNC_BYTES, M17_SYNC_LENGTH_BYTES); } diff --git a/Sync.h b/Sync.h index 2118c96..20ee2dd 100644 --- a/Sync.h +++ b/Sync.h @@ -33,7 +33,8 @@ public: static void addNXDNSync(unsigned char* data); - static void addM17Sync(unsigned char* data); + static void addM17HeaderSync(unsigned char* data); + static void addM17DataSync(unsigned char* data); private: }; diff --git a/Version.h b/Version.h index 7fc09f6..c648bad 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201116"; +const char* VERSION = "20201126"; #endif From ec80e9553afecc6e214c6bdf75a8ea03a4801d57 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 26 Nov 2020 13:47:47 +0000 Subject: [PATCH 162/163] Fully implement the latest M17 specification. --- M17Control.cpp | 193 +++++++++++++++++++------------------------------ M17Control.h | 2 + M17Defines.h | 2 +- M17LICH.cpp | 8 +- M17LICH.h | 4 +- 5 files changed, 82 insertions(+), 127 deletions(-) diff --git a/M17Control.cpp b/M17Control.cpp index d9239ad..20289d0 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -79,7 +79,9 @@ m_rfFN(0U), m_rfErrs(0U), m_rfBits(1U), m_rfLICH(), +m_rfLICHn(0U), m_netLICH(), +m_netLICHn(0U), m_rssiMapper(rssiMapper), m_rssi(0U), m_maxRSSI(0U), @@ -172,13 +174,58 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) m_maxRSSI = m_rssi; m_aveRSSI = m_rssi; m_rssiCount = 1U; - m_rfFN = 0U; + m_rfLICHn = 0U; + m_rfFN = 0U; #if defined(DUMP_M17) openFile(); #endif m_rfLICH.setLinkSetup(frame); + m_rfState = RS_RF_LATE_ENTRY; + + return true; + } else { + m_rfState = RS_RF_LATE_ENTRY; + } + } + + if (m_rfState == RS_RF_LATE_ENTRY && data[0U] == TAG_DATA) { + unsigned int frag1, frag2, frag3, frag4; + CM17Utils::splitFragmentLICHFEC(data + 2U + M17_SYNC_LENGTH_BYTES, frag1, frag2, frag3, frag4); + + unsigned int lich1 = CGolay24128::decode24128(frag1); + unsigned int lich2 = CGolay24128::decode24128(frag2); + unsigned int lich3 = CGolay24128::decode24128(frag3); + unsigned int lich4 = CGolay24128::decode24128(frag4); + + unsigned int colorCode = (lich4 >> 7) & 0x1FU; + if (colorCode != m_colorCode) + return false; + + if (!m_rfLICH.isValid()) { + unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; + CM17Utils::combineFragmentLICH(lich1, lich2, lich3, lich4, lich); + + unsigned int n = (lich4 >> 4) & 0x07U; + m_rfLICH.setFragment(lich, n); + } + + bool valid = m_rfLICH.isValid(); + if (valid) { + m_rfFrames = 0U; + m_rfErrs = 0U; + m_rfBits = 1U; + m_rfTimeoutTimer.start(); + m_minRSSI = m_rssi; + m_maxRSSI = m_rssi; + m_aveRSSI = m_rssi; + m_rssiCount = 1U; + m_rfLICHn = 0U; + +#if defined(DUMP_M17) + openFile(); +#endif std::string source = m_rfLICH.getSource(); std::string dest = m_rfLICH.getDest(); @@ -191,15 +238,6 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) } } - if (!m_allowEncryption) { - bool ret = m_rfLICH.isNONCENull(); - if (!ret) { - LogMessage("M17, invalid access attempt from %s to %s", source.c_str(), dest.c_str()); - m_rfState = RS_RF_REJECTED; - return false; - } - } - unsigned char dataType = m_rfLICH.getDataType(); switch (dataType) { case 1U: @@ -222,16 +260,14 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) m_display->writeM17(source.c_str(), dest.c_str(), "R"); -#if defined(DUMP_M17) - writeFile(data + 2U); -#endif if (m_duplex) { + // Create a Link Setup frame data[0U] = TAG_HEADER; data[1U] = 0x00U; // Generate the sync CSync::addM17HeaderSync(data + 2U); - + unsigned char setup[M17_LICH_LENGTH_BYTES]; m_rfLICH.getLinkSetup(setup); @@ -246,106 +282,7 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) writeQueueRF(data); } - return true; - } else { - m_rfState = RS_RF_LATE_ENTRY; - } - } - - if (m_rfState == RS_RF_LATE_ENTRY && data[0U] == TAG_DATA) { - CM17Convolution conv; - unsigned char frame[M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES]; - conv.decodeData(data + 2U + M17_SYNC_LENGTH_BYTES + M17_LICH_FRAGMENT_FEC_LENGTH_BYTES, frame); - - bool valid = CM17CRC::checkCRC(frame, M17_FN_LENGTH_BYTES + M17_PAYLOAD_LENGTH_BYTES + M17_CRC_LENGTH_BYTES); - if (valid) { - m_rfFN = (frame[0U] << 8) + (frame[1U] << 0); - - unsigned int frag1, frag2, frag3, frag4; - CM17Utils::splitFragmentLICHFEC(data + 2U + M17_SYNC_LENGTH_BYTES, frag1, frag2, frag3, frag4); - - unsigned int lich1 = CGolay24128::decode24128(frag1); - unsigned int lich2 = CGolay24128::decode24128(frag2); - unsigned int lich3 = CGolay24128::decode24128(frag3); - unsigned int lich4 = CGolay24128::decode24128(frag4); - - unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; - CM17Utils::combineFragmentLICH(lich1, lich2, lich3, lich4, lich); - - m_rfLICH.setFragment(lich, m_rfFN); - - valid = m_rfLICH.isValid(); - if (valid) { - m_rfFrames = 0U; - m_rfErrs = 0U; - m_rfBits = 1U; - m_rfTimeoutTimer.start(); - m_minRSSI = m_rssi; - m_maxRSSI = m_rssi; - m_aveRSSI = m_rssi; - m_rssiCount = 1U; - -#if defined(DUMP_M17) - openFile(); -#endif - std::string source = m_rfLICH.getSource(); - std::string dest = m_rfLICH.getDest(); - - if (m_selfOnly) { - bool ret = checkCallsign(source); - if (!ret) { - LogMessage("M17, invalid access attempt from %s to %s", source.c_str(), dest.c_str()); - m_rfState = RS_RF_REJECTED; - return false; - } - } - - unsigned char dataType = m_rfLICH.getDataType(); - switch (dataType) { - case 1U: - LogMessage("M17, received RF late entry data transmission from %s to %s", source.c_str(), dest.c_str()); - m_rfState = RS_RF_DATA; - break; - case 2U: - LogMessage("M17, received RF late entry voice transmission from %s to %s", source.c_str(), dest.c_str()); - m_rfState = RS_RF_AUDIO; - break; - case 3U: - LogMessage("M17, received RF late entry voice + data transmission from %s to %s", source.c_str(), dest.c_str()); - m_rfState = RS_RF_AUDIO; - break; - default: - LogMessage("M17, received RF late entry unknown transmission from %s to %s", source.c_str(), dest.c_str()); - m_rfState = RS_RF_DATA; - break; - } - - m_display->writeM17(source.c_str(), dest.c_str(), "R"); - - if (m_duplex) { - // Create a Link Setup frame - data[0U] = TAG_HEADER; - data[1U] = 0x00U; - - // Generate the sync - CSync::addM17HeaderSync(data + 2U); - - unsigned char setup[M17_LICH_LENGTH_BYTES]; - m_rfLICH.getLinkSetup(setup); - - // Add the convolution FEC - CM17Convolution conv; - conv.encodeLinkSetup(setup, data + 2U + M17_SYNC_LENGTH_BYTES); - - unsigned char temp[M17_FRAME_LENGTH_BYTES]; - interleaver(data + 2U, temp); - decorrelator(temp, data + 2U); - - writeQueueRF(data); - } - - // Fall through to the next section - } + // Fall through to the next section } } @@ -395,11 +332,15 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) CSync::addM17DataSync(rfData + 2U); unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; - m_netLICH.getFragment(lich, m_rfFN); + m_netLICH.getFragment(lich, m_rfLICHn); unsigned int frag1, frag2, frag3, frag4; CM17Utils::splitFragmentLICH(lich, frag1, frag2, frag3, frag4); + // Add the Color Code and fragment number + frag4 |= (m_rfLICHn & 0x07U) << 4; + frag4 |= (m_colorCode & 0x1FU) << 7; + // Add Golay to the LICH fragment here unsigned int lich1 = CGolay24128::encode24128(frag1); unsigned int lich2 = CGolay24128::encode24128(frag2); @@ -446,6 +387,10 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) m_rfFrames++; + m_rfLICHn++; + if (m_rfLICHn >= 6U) + m_rfLICHn = 0U; + // EOT? if ((m_rfFN & 0x8000U) == 0x8000U) { std::string source = m_rfLICH.getSource(); @@ -501,6 +446,8 @@ void CM17Control::writeEndRF() m_rfTimeoutTimer.stop(); + m_rfLICH.reset(); + if (m_netState == RS_NET_IDLE) { m_display->clearM17(); @@ -521,6 +468,8 @@ void CM17Control::writeEndNet() m_networkWatchdog.stop(); m_packetTimer.stop(); + m_netLICH.reset(); + m_display->clearM17(); if (m_network != NULL) @@ -580,6 +529,7 @@ void CM17Control::writeNetwork() m_packetTimer.start(); m_elapsed.start(); m_netFrames = 0U; + m_netLICHn = 0U; // Create a dummy start message unsigned char start[M17_FRAME_LENGTH_BYTES + 2U]; @@ -615,14 +565,16 @@ void CM17Control::writeNetwork() m_netFrames++; // Add the fragment LICH - uint16_t fn = (netData[28U] << 8) + (netData[29U] << 0); - unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; - m_netLICH.getFragment(lich, fn); + m_netLICH.getFragment(lich, m_netLICHn); unsigned int frag1, frag2, frag3, frag4; CM17Utils::splitFragmentLICH(lich, frag1, frag2, frag3, frag4); + // Add the Color Code and fragment number + frag4 |= (m_netLICHn & 0x07U) << 4; + frag4 |= (m_colorCode & 0x1FU) << 7; + // Add Golay to the LICH fragment here unsigned int lich1 = CGolay24128::encode24128(frag1); unsigned int lich2 = CGolay24128::encode24128(frag2); @@ -648,7 +600,12 @@ void CM17Control::writeNetwork() writeQueueNet(data); + m_netLICHn++; + if (m_netLICHn >= 6U) + m_netLICHn = 0U; + // EOT handling + uint16_t fn = (netData[28U] << 8) + (netData[29U] << 0); if ((fn & 0x8000U) == 0x8000U) { std::string source = m_netLICH.getSource(); std::string dest = m_netLICH.getDest(); diff --git a/M17Control.h b/M17Control.h index 6838aa7..276478c 100644 --- a/M17Control.h +++ b/M17Control.h @@ -69,7 +69,9 @@ private: unsigned int m_rfErrs; unsigned int m_rfBits; CM17LICH m_rfLICH; + unsigned int m_rfLICHn; CM17LICH m_netLICH; + unsigned int m_netLICHn; CRSSIInterpolator* m_rssiMapper; unsigned char m_rssi; unsigned char m_maxRSSI; diff --git a/M17Defines.h b/M17Defines.h index 7cdad35..5129d9a 100644 --- a/M17Defines.h +++ b/M17Defines.h @@ -33,7 +33,7 @@ const unsigned int M17_SYNC_LENGTH_BYTES = M17_SYNC_LENGTH_BITS / 8U; const unsigned int M17_LICH_LENGTH_BITS = 240U; const unsigned int M17_LICH_LENGTH_BYTES = M17_LICH_LENGTH_BITS / 8U; -const unsigned int M17_LICH_FRAGMENT_LENGTH_BITS = M17_LICH_LENGTH_BITS / 5U; +const unsigned int M17_LICH_FRAGMENT_LENGTH_BITS = M17_LICH_LENGTH_BITS / 6U; const unsigned int M17_LICH_FRAGMENT_LENGTH_BYTES = M17_LICH_FRAGMENT_LENGTH_BITS / 8U; const unsigned int M17_LICH_FRAGMENT_FEC_LENGTH_BITS = M17_LICH_FRAGMENT_LENGTH_BITS * 2U; diff --git a/M17LICH.cpp b/M17LICH.cpp index 0c0469a..e99fd8d 100644 --- a/M17LICH.cpp +++ b/M17LICH.cpp @@ -124,23 +124,19 @@ void CM17LICH::setLinkSetup(const unsigned char* data) m_valid = CM17CRC::checkCRC(m_lich, M17_LICH_LENGTH_BYTES); } -void CM17LICH::getFragment(unsigned char* data, unsigned short fn) const +void CM17LICH::getFragment(unsigned char* data, unsigned int n) const { assert(data != NULL); CM17CRC::encodeCRC(m_lich, M17_LICH_LENGTH_BYTES); - unsigned int n = (fn & 0x7FFFU) % 5U; - ::memcpy(data, m_lich + (n * M17_LICH_FRAGMENT_LENGTH_BYTES), M17_LICH_FRAGMENT_LENGTH_BYTES); } -void CM17LICH::setFragment(const unsigned char* data, unsigned short fn) +void CM17LICH::setFragment(const unsigned char* data, unsigned int n) { assert(data != NULL); - unsigned int n = (fn & 0x7FFFU) % 5U; - ::memcpy(m_lich + (n * M17_LICH_FRAGMENT_LENGTH_BYTES), data, M17_LICH_FRAGMENT_LENGTH_BYTES); m_valid = CM17CRC::checkCRC(m_lich, M17_LICH_LENGTH_BYTES); diff --git a/M17LICH.h b/M17LICH.h index 8842e22..804de3e 100644 --- a/M17LICH.h +++ b/M17LICH.h @@ -46,8 +46,8 @@ public: void getLinkSetup(unsigned char* data) const; void setLinkSetup(const unsigned char* data); - void getFragment(unsigned char* data, unsigned short fn) const; - void setFragment(const unsigned char* data, unsigned short fn); + void getFragment(unsigned char* data, unsigned int n) const; + void setFragment(const unsigned char* data, unsigned int n); private: unsigned char* m_lich; From ef9fedcc01b228afe1ddcc6708d0b7559af8f722 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 26 Nov 2020 14:25:45 +0000 Subject: [PATCH 163/163] Fix the logging messages for late entry. --- M17Control.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/M17Control.cpp b/M17Control.cpp index 20289d0..9be8607 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -203,12 +203,15 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) if (colorCode != m_colorCode) return false; + bool lateEntry = false; if (!m_rfLICH.isValid()) { unsigned char lich[M17_LICH_FRAGMENT_LENGTH_BYTES]; CM17Utils::combineFragmentLICH(lich1, lich2, lich3, lich4, lich); unsigned int n = (lich4 >> 4) & 0x07U; m_rfLICH.setFragment(lich, n); + + lateEntry = true; } bool valid = m_rfLICH.isValid(); @@ -241,19 +244,19 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) unsigned char dataType = m_rfLICH.getDataType(); switch (dataType) { case 1U: - LogMessage("M17, received RF data transmission from %s to %s", source.c_str(), dest.c_str()); + LogMessage("M17, received RF %s data transmission from %s to %s", lateEntry ? "late entry" : "", source.c_str(), dest.c_str()); m_rfState = RS_RF_DATA; break; case 2U: - LogMessage("M17, received RF voice transmission from %s to %s", source.c_str(), dest.c_str()); + LogMessage("M17, received RF %s voice transmission from %s to %s", lateEntry ? "late entry" : "", source.c_str(), dest.c_str()); m_rfState = RS_RF_AUDIO; break; case 3U: - LogMessage("M17, received RF voice + data transmission from %s to %s", source.c_str(), dest.c_str()); + LogMessage("M17, received RF %s voice + data transmission from %s to %s", lateEntry ? "late entry" : "", source.c_str(), dest.c_str()); m_rfState = RS_RF_AUDIO; break; default: - LogMessage("M17, received RF unknown transmission from %s to %s", source.c_str(), dest.c_str()); + LogMessage("M17, received RF %s unknown transmission from %s to %s", lateEntry ? "late entry" : "", source.c_str(), dest.c_str()); m_rfState = RS_RF_DATA; break; }

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 B`lJ8= delta 236 zcmdl#OJ&u3#R(G5YI+@B>nzRbZ0+f6j6lo;#LPg<0>rF9%(gw9jeUmB_G{PJ)qhV9e$KIS gyT}U;WzOmOpEy=**Z<6MMQl1VFQ)}dmExCH0NA5Y82|tP diff --git a/Nextion_G4KLX/NX3224T028.tft b/Nextion_G4KLX/NX3224T028.tft index 034441b1c2c901383fe53d2a7f370b43cc9079f2..22094938b6e0b3f2d1490526b7557668021ff58c 100644 GIT binary patch delta 15069 zcmds;3wRVow#QFDrZXf13}I#xAwnlazNPH)&a%SuI^t< zuiRej{_HRKcSrH5F9sCvDePO^c5>h1Kfm6$*w?*pamI|k#oA{Bid!GE{(Y6ur&t{P zY8SgI$ZxEd9Bem?6Jkob>z4m7D9FEdtarRu8Shocd$sXieZ1GT#A}S{ROR*1~^^#89#W5v!-^v2p)??XjD93BP-S0{8Vm#6G5Sv+-^*g02Eb*eNte#KtjESb}5 zlE@6X(xR)DSbI%n_D0L=vm?sH^yPZ0wtduSq4eU|)baeiqWE1a|GWOeT&tT}S+Y;m zdr_C6^Cq19ye<1Koaz>N{avZ*>?~RTIi5$i9pk!Ij7m>MyP{fAX}6-s{dOzz+{2w# z)b3RNS8m1bl8L>|g1p9OZ^iq1ns(m};Q%MbM_M7KEl13vn^61j6-&ZR$P=k+5xD3! z#GeYcLEP2fmF6@d;U&8X-f{Pe)SvWJ)ibCtd3OA?$|}1BV%sOENL2Mbk?POuZAD8@ zT_V;G;9z>=4}5O*#BlV4Y{Rff8+@LA+pIWOjM6z`3r9q{0c~(P-!mxOhM$U*Z=OLn zG>T8LJHK$M*fAH5XJ_>*QXag1SoX%s)xY2OSjqMg&*JN`#-kOWu4>0}F{Qh!HP0+M zQJ%Z#=;1C1+oRk1;oEkWIMmsdUy$EGm4md7dUaIeGOWg3SCF40?rQE%lZ{Jo8n?S- zn8<793KisgGz6!pSEEkxFYUF4nKibD7UZX?GrUEI?z|d9BH13;a!B+p$iK>tN+(<| zGNw70e7$vVs$JqE<5`Y{`wH?+^lvW_{8YabbxBTehP6E=nqaUL9eS!u(UetK-PC>&bSyNE; zb~qj8%tPUJgu5p^4@J%KzS@y?47;J`iAYYT^CC0-852>s5oei%qNaau>>O0B4e^hK zCn90~guA-t8)?M!EisnV>AWDF6Grv}3rBgO|95+xO$`n!K@hHg)~gULW<|d%L|jch1$;gL%;F%<(IrWkQd-muh-CB{Z2o7^6y28cdL8+ajKe8wfhtjAJplw znX0Vsy~ndpL)uV3dvD%RN0i6SCsLrVPM%iTY*(}{?O;`=^s^$xw>37)N*}5E>n|wq zPFkn(^e@j|M5^u+E9J!(U9!Eb=0aA^1Kf=HEmrK^UE(RLUez%?zRO0tPj;&gJ>euQYOdP#Jk8R61^7~&s`U-Lkg)34N@4Y#M+3BO`68|^$yb~!_ zHHzZ?usT=+9n~n(|G+xPc;ntteFLo`Oe?+-Bd&Ansn*A#=P3d9QLi}r2O`hHR>APe7BN2@QXCx ztd*i(3-!j(ioJj6H$Hs%@VdWC(|y1A<%in*x1ciYL6esDA48 z)@t9?cjn@-PkQWcR&G7l3bXKJ(GP2L0+p-33pc^J0H1$b)3fbWoFdk7M%6Ldn^yJVHHt-kpZs zjcsf8HvpRu=bg#pGM)ED!!KZeGVjDmIVv-1_IGz?1tZDzxy&%zOdh6m0W+`h2Eb1(`xqjU@K?$nK^U( z{@B*VrtSGV`PW}-9L@RInmTWs7M7NsaWhwUY0thXHCJ$CX5`86f(J};&Hf&_Jp5Yg zf(DG7Vf;0_d+fF~mHq5DHy1Kqys;Zc>{n`x<>)?mk!HUP%sB5}SPq`>UXYC%9gpql zyrn9OHM*PBbzp__{ImJI&p&c! zU8AchIltwpX!iaypPYE+pL(_P7S;Raxt?09-!o2%T@-hW%+Kc^BF?GSksZ90?F@yp zwYK^R(J4N8THD{!gkx|x6yz{Y9U!A0h&WjBH#qHW;WM(KrBf9o)!$#DqqqC5e z-6kU|^9nKMZ6jHPHX8{UZ=ghmGMhPzIfpq{GAlz_Adi{S*E~&pxzRAy`I`7)hmoDJ zmdmYSu4b-du9w7_@5>{fxxqfOOcQr(F|Nwk$L01iKW2Wy+%JhUKa)p3^Hb|g<$yT7 z)99E{!NtC3{+oG}`GX`*{Es~Hi9gzxJf?}iY&EXbWt_y$O-2JXjzrczBVAm++qgp{ zEHeWBNjR&P+ntq>RZFqLgt&F_I4vl{n++!Koj3? zHyWHfsPIrNsF^#AfckICgK})?!B32-*wDp*1>oL)eIJk|Yi?rRk{xRtR9E?c$fp*%7_8l?lJ4v|*(T>#UE{-;SiLyUd zwDoYbAF~}eEs(`haenn^N7B-h5~s_dy-170(2lsaf*nbBfKN3l-wJjle*#=Z=Uc&! z_}RA!uc}TnXWij? zKup>Swrj$WdT~pK)N_|kzom66A{WKc^VE>i+7fwC{S4(&O6)`wfc~n|kjCCXbTJup zTuGvbZYF91`%h{T9qv!moOF6ltxLV{Cb|szXV5nc7Wb68+bNmi!PI8y+s^d=5CPB@#bJ^O%#wK;A+O0*n#VN)v^`N?|g^b@~(dwXkpz?v3bkKc6C;{%}9&Ah}gO<`T zY9uG9YM)HXL|UUZppUKt73#*yjcj)Zb?V7>FHj|nd6ek`YIG~;p+T(N!^#k_K0OBN zG&;l?k~%%f_Bc?b@odY08#nWBq&3M=ItCoeWSRxj)k>fo%_{QXRJI zf*K`)LAn_N6jpiTvB7lJBHW&3$hqZhy+?FUWV-qOVhP@`W! zKP6%gRH#0vQxeB#EGq-hxW7m1=xieYqEW9K$C6; z4eAF5=_SxlZ?V3eb@@ycqySb0KeYr+x)D^UJE+m0KpzcaGTSAfO4HbW71U@37^EYhNgC!`g>+COIT!mW zg_VmzJw&OTNCQ<$XZsRRqh?@`hJhwM!}>(lCxd>P!OH8PPH(V12UKYu+w(z<7J@;l z08OfE$VzxYjp~DbYQ{>aISx8q#)(#-N||hDff}_1bLnBwB*FR%tiKE<(PCCgL7kSe zy$n=o1=}k@jn;rUe2bZ6;zm=jmPtJw4A4~}mO6ks7Dbue1yuP@eHF@u-GUwfXHq@< z-LOGPppROzodtTR6Wh69kj8<2dJ!~fIqPd#C;SzwpZuUnSAYg}0E6^+EsTFZ;m?hc z@iu2H1#{^Hm_+sBrJe;EbS0QWkAeYuj`g=dg%*Jtm4X<^tbD-AX12G0I_+S4C#ceH zw)cP-MB*nWoXiJ(f8*q#iAG@61#4($R>`jIn^fg1e`2FQcI!PdzODpZecKidINrDV1* z0yRnjbLbAxq|vO0SRV@p=mk)x66R~nnIPt$S)7IMI&EZq z6Wg2F-U6z$jcvKyYqSf@B|p}73@cEfcA!S>!6fPl>ePpM8*>1t&_K5H*uFc2gG%>s z;$Bdrpp?SFwPeb2VsY-sc&m_u#rNSFxs^fyjC#vBbQ6k_{Hw#R{3kl3C8cBC@!Aie69_r)yGN6Xn>3wmfX z+dIJ^CDxObz7#a+I?$jVU=BS825442jDM5f=ZwvqQ9Ds)GyzS@1r53$%%OjKArXTtZxE?Byy#4^qL3D$oBu}lZ# zxd9Lzoy3WToJavxYQ%P9P@^<3NP|F=o@9L->k~jfm9X*(sMB<|XMifrWP28<(HtX<&2eJkWPVqs)y%b%>N+f ze=taofPR|H`U2KVS^tUkS`B3R=Ac2BgSqq&m_*O8K9}{kz#RGk#N$#!9{-m%lwoNG zYSa?+QD;`Vu#(I6HK0!2*uEZA=|;A@gV^DK9=e^C0j%5!`e+0zBSD=WVf#@~rBQ4@ z4r2Zf;Si*Cph<@~<4aJZuRuQ?XXONl*8yz*3gSIoimbE>YUBcg)D<*oFzZ8DzYp|N zh?OxQ79_Tx0#%wI?GQbUgGT=Vb7>oB()TihDp>y!OrlyBOC=uEsSew9L6z#Uod{~= z2Xp9F(4>c1e}wf>V1No(DFk&YW?O*Rf4<0xm%wyd15Tlv8p;0a1Nvwv+Yf>sdW`Kc zV35{>e%cF~^fT+HSnt$W>b*ge9s&(|1k9zCU=r9g zMuI+inw4i*nZ)*F5cfUXg&^*Gwx0)a--EdCLEQJOpJ4qL5chqm^j{yu_MGkH)R1)1 zfD`hWPoqX)knRMrID**wf*L&u`soE$UIcY|h3#peO0Tg!9n@$hm`mlLNpWe?gB#Q+ z0ZgJaR+@l1U7CjRkCBgrN-a1;K3Qti3e2H=(4;A>7qC7R4A4AQ=7T!D#r7glrNwNQ zf*LIWJ+y(9GFCQ%KKhiE1E5Y{fFXqr;h@rC&Nu?5Q(O}{4g_e>E1-|wW_u|Zq$HAMqNQa-OkDYP^Zvc zoX7)J8pO7INY`ix7^FF%NguHOA?sT}KYhu{VGx@Uw#z}4zG3@YP@{i?9y-p-308gq z>y!6VS)qEMPW})l0w6Y%Y+nQ>Qx-Ud)`JFZ0)2FV?Jq&JteLFDm7qztf(8u)gEWox zQr35~z900^KS71Q1~vKy^wBS@oNk8kkC)EPWwr|9L7MG2P^DUI$AcQx25VCTRvNO> z2=r4MR@#C(WwYHLRH*~o9YKvcgFzYtn)Dj$(^;R{9OK_l%UN0h>a>RKwV+Dt+1>zZ z^gb9QqlI+Q6jbO^P@@*0pE|SB1=Q&pwyyx%4b((n8kX0x|zD#vzG5 zWa%SNr)_L+2UXh1_AXGPkHH*DYAIb@2`ZEgYSaM?P%l<`gF4;9_N|~w{n+jgrqgI} z3f0MwUFHLQA!^2nR$y)Fz;+M}(pb<>0yJqU>#JEm&3gUIq}~QJs6CiVkAq26#Cj>~ z%fTEv2?nTMD_K4ZR7k$VJnE8fFORy@<%`Sw+VYjhQZPn P@BGW1t$$T(O8S2T3T#;Z delta 5011 zcmd7W`*#%e9merzF4<&ZDS_;6KrRk}aEqXz6an$D8>N*NED9(HD5$911O!Srg_VnN z5Do;8jEKVFRtg$h1VoA+jf#qbqIlz`P(h@{o~S6rV(oiAwtqo?shZd4ndiH^v&qix zjC){R>)Jythf7`hEo+dkiwC7%POWOuk+k?!{(qd?Pny6jn!hjgxVk)F5lo+TLkE)rU;)*wCuu-~RMda(X~jc=Y6ZVS_2LIlh`5pAmN2 z`i3F(&PQS2!f5xn^+f9sk*y3i!k}y7i(L4Mq8l%HQ0~ssBnn8?9!_Bua zHik=YWt4>#gBd%*5knYT!W(X5yd8EL8b9s+P$#@(Sp0Kme&`IREeg*>V^jFVhp{u@@dC|C6e@sL#6u0DkO)~cbRJVmxKUFNeKoM$5M3oLr?9!M#L3!( zF;!zyHI`q;gbrFh1$=yR3tr>S0PH*0e-lZ7jD%SM4q5${3zHV!0+_pry93%=#5r ztf#E0MqgVkKZCBeTi$`5p2MuZ$3T~G3Ubs0JvGO)uCk^J`s!x6JG#0iXB$1Sz3#yc zs#_2p0Sz#zQp;tSP*=-6F(XcU{ASBB&^GILS^vHD_2W^0ImXllGn$GiEwuhg>$hUL z&c!+Yi&eKybcZsGsS{@P0H(FT`pxKQ3wo+Xj$~_Iwq}pz*U;CSmfu2G`z^nXp5DcT zj#=}GH7Br0=jw3$JNlLlUl(lodvtZt@(;)XUpM*UyZK1Tke6LuElcA!a!@SUuXSBEY|brEBBHu)Y!%z%derUH!SZ(Py4W3^?w#U z(BSx|$4njw_Sia5jP;@oi^6ltpBxbZe#|ZR_Exc+AuVYF_(bsY7 zPgp)>`Ezvjh2=Bo=`3c|k*kD2{m{{k=;t6IV)|_BYHm9MZiuxS?zGm3QOyrDY zc@|!y7x1W77Dn%jHJH>k%eye4J(l0Xj1mo^C*1-AWiX~+U{(t-tu@xaX#F1R^BYEe zvxXf1fqJqLQ#oe!kS(mjK)bE4!E*UU(Vw~r2I_$^Ww2axtzTvRZtH6>BbN*1l!`G> zSB$AAW|hOVR#?9SbB>;6c-nBIjvq<|F56%ZrfHwB;qpX&O0ABd2NW z&sl#SIZdad`xc_329{Hn)99*^Qir>2q{-Sv)n$%aCMn&bU;s6U{;eb&@$_nTVI7~J!MTb zaw@X?400;6yaU_oQ{13AjiOh50Vef?<@K1*A1&{|jJ|8c@t;!N#?j8okk5b2XcDG0 z-}+6~S6lyg>%B{&+qcA++F`lIVzK5}zux-InAP7fEiUu9eG7C{f}Tn-scuc|^S`@Q zJ#G0~^p&+-j;^k^+#9*V!Gs1{Gsv33nAA9H#-p!EmLEV@Q!GzKPZgNapE1y3>yIFx z|DP~Y`o^kr=<6SrFQBV`TE2*$e#DG=HH{wP4s7*VRai3}IU!ljp{rSzXQQWw zuv~9opf936r!%&27K`OIiyqWRUj>%q=&G*e1bV8ES@p+26Rn?Q{S-`Vp*4%pSEc2p z=xVv;N3fNi!%F?KdGzw-`Z1CkX&d)qLX#~|$BcGkN(V4dt@Yno-}Ta{|1}00i!n{Y za_zuk?X&)@^w|$tqoXnCX?%{6)NHHfST)b`eB^y^c@gryx4Z;--y`pP zVO%I$CTz+zux-ISgy0kAzT{Wz9sV6fLV>fv}Req*7{AD z&^F}w-_GF61Wf9!sM0=b-m!cDeZ6n_19bI~<&V+R5zMG*>u8tPprfAXsTZa+(3(N$ zYl!9B(A6-@!_iZ21S6w$80ckNc*Pc8!<3F#a}>E4v3vqueP;P2dOD2>ePhizYtCbl z>a>ZTP+jy@-*O>xEor$38>uYEsMKzZsRon!!158~H)|U`iB1@(KgM(mX7s4_)zP%rwDrM4jYdSya>^u@}A{OJGGC;0x4VR~u&=l;W` HV{-onB7g3` diff --git a/Nextion_G4KLX/NX4024K032.HMI b/Nextion_G4KLX/NX4024K032.HMI index 7df6343eacb3e26ab4d3451f832b73b4b004da7e..85cdc4811bc8f1c6dc4cd7ead429465df4b0d4c8 100644 GIT binary patch delta 11465 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 delta 239 zcmZ2?Ky~I(r3n(whK(Ey3=9wwLG!O%ctiv!4goV4abT!G(lI^o411=Fd;8j!mjXvz zUvD|=`uNOY*E+$MKz!77yU-C=j|pd77XWd~>@%(mo6or3nRUjsO7MtlB@~0?L2Qso z^%DzNS(v?6yxTmH@j6R$I$L`>8zT@i0WmWWvj8zG5VLJhXJgl%zdh5FWB%>waaNow hx64~|DsxV+a^+mH-O7#gir93%1Wp;26Auq@0|07+PBQ=i diff --git a/Nextion_G4KLX/NX4024K032.tft b/Nextion_G4KLX/NX4024K032.tft index 92b2843e9b7fadf316406772aa42346e99aae699..04da59ad9f8ca248a86b1eab7907c6ea0a0d9f78 100644 GIT binary patch delta 15319 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 delta 5141 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 diff --git a/Nextion_G4KLX/NX4024T032.HMI b/Nextion_G4KLX/NX4024T032.HMI index 03cd4a81fe4eff2c588f9ab3c31d1349bf8646c3..10dbda426d79c22dc1ead57b2a3361c9b50e36b6 100644 GIT binary patch delta 11805 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 diff --git a/Nextion_G4KLX/NX4024T032.tft b/Nextion_G4KLX/NX4024T032.tft index 9405c5ef94a03979c109dcc1a8955ca95d89b99a..31ba9715b3bf5aa7e77995ca40cd0aba57484ba4 100644 GIT binary patch delta 15298 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 F{{T{G6e<7! diff --git a/Nextion_G4KLX/NX4832K035.HMI b/Nextion_G4KLX/NX4832K035.HMI index e1c2cf46cd92ecdc5de60729bb0003c93fa8d835..36930826f2a2fc4c7a2d758812f27684f02a8538 100644 GIT binary patch delta 11543 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