mirror of
https://github.com/ShaYmez/MMDVM_CM.git
synced 2024-11-15 20:51:49 -05:00
Add USRP2P25
This commit is contained in:
parent
96184ff024
commit
71e8c23224
235
USRP2P25/Conf.cpp
Normal file
235
USRP2P25/Conf.cpp
Normal file
@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright (C) 2015,2016,2017 by Jonathan Naylor G4KLX
|
||||
* Copyright (C) 2018 by Andy Uribe CA6JAU
|
||||
* Copyright (C) 2021 by Doug McLain AD8DP
|
||||
*
|
||||
* 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 "Conf.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cctype>
|
||||
|
||||
const int BUFFER_SIZE = 500;
|
||||
|
||||
enum SECTION {
|
||||
SECTION_NONE,
|
||||
SECTION_P25_NETWORK,
|
||||
SECTION_USRP_NETWORK,
|
||||
SECTION_LOG
|
||||
};
|
||||
|
||||
CConf::CConf(const std::string& file) :
|
||||
m_file(file),
|
||||
m_callsign(),
|
||||
m_daemon(false),
|
||||
m_usrpAddress(),
|
||||
m_usrpDstPort(0U),
|
||||
m_usrpLocalPort(0U),
|
||||
m_usrpGainAdjDb(),
|
||||
m_usrpDebug(false),
|
||||
m_p25DstId(0U),
|
||||
m_p25DstAddress(),
|
||||
m_p25DstPort(0U),
|
||||
m_p25LocalAddress(),
|
||||
m_p25LocalPort(0U),
|
||||
m_p25NetworkDebug(false),
|
||||
m_logDisplayLevel(0U),
|
||||
m_logFileLevel(0U),
|
||||
m_logFilePath(),
|
||||
m_logFileRoot()
|
||||
{
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
SECTION section = SECTION_NONE;
|
||||
|
||||
char buffer[BUFFER_SIZE];
|
||||
while (::fgets(buffer, BUFFER_SIZE, fp) != NULL) {
|
||||
if (buffer[0U] == '#')
|
||||
continue;
|
||||
|
||||
if (buffer[0U] == '[') {
|
||||
if (::strncmp(buffer, "[P25 Network]", 13U) == 0)
|
||||
section = SECTION_P25_NETWORK;
|
||||
else if (::strncmp(buffer, "[USRP Network]", 14U) == 0)
|
||||
section = SECTION_USRP_NETWORK;
|
||||
else if (::strncmp(buffer, "[Log]", 5U) == 0)
|
||||
section = SECTION_LOG;
|
||||
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++;
|
||||
}
|
||||
::fprintf(stderr, "CConf key:val:section == %s:%s:%d\n", key, value, section);
|
||||
if (section == SECTION_P25_NETWORK) {
|
||||
if (::strcmp(key, "Callsign") == 0)
|
||||
m_callsign = value;
|
||||
else if (::strcmp(key, "StartupDstId") == 0)
|
||||
m_p25DstId = (unsigned int)::atoi(value);
|
||||
else if (::strcmp(key, "LocalAddress") == 0)
|
||||
m_p25LocalAddress = value;
|
||||
else if (::strcmp(key, "LocalPort") == 0)
|
||||
m_p25LocalPort = (unsigned int)::atoi(value);
|
||||
else if (::strcmp(key, "DstAddress") == 0)
|
||||
m_p25DstAddress = value;
|
||||
else if (::strcmp(key, "DstPort") == 0)
|
||||
m_p25DstPort = (unsigned int)::atoi(value);
|
||||
else if (::strcmp(key, "GainAdjustdB") == 0)
|
||||
m_p25GainAdjDb = value;
|
||||
else if (::strcmp(key, "Debug") == 0)
|
||||
m_p25NetworkDebug = ::atoi(value) == 1;
|
||||
} else if (section == SECTION_USRP_NETWORK) {
|
||||
if (::strcmp(key, "Address") == 0)
|
||||
m_usrpAddress = value;
|
||||
else if (::strcmp(key, "DstPort") == 0)
|
||||
m_usrpDstPort = (uint32_t)::atoi(value);
|
||||
else if (::strcmp(key, "LocalPort") == 0)
|
||||
m_usrpLocalPort = (uint32_t)::atoi(value);
|
||||
else if (::strcmp(key, "GainAdjustdB") == 0)
|
||||
m_usrpGainAdjDb = value;
|
||||
else if (::strcmp(key, "Debug") == 0)
|
||||
m_usrpDebug = ::atoi(value) == 1;
|
||||
} 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 = (uint32_t)::atoi(value);
|
||||
else if (::strcmp(key, "DisplayLevel") == 0)
|
||||
m_logDisplayLevel = (uint32_t)::atoi(value);
|
||||
}
|
||||
}
|
||||
|
||||
::fclose(fp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CConf::getCallsign() const
|
||||
{
|
||||
return m_callsign;
|
||||
}
|
||||
|
||||
std::string CConf::getP25DstAddress() const
|
||||
{
|
||||
return m_p25DstAddress;
|
||||
}
|
||||
|
||||
unsigned int CConf::getP25DstPort() const
|
||||
{
|
||||
return m_p25DstPort;
|
||||
}
|
||||
|
||||
std::string CConf::getP25LocalAddress() const
|
||||
{
|
||||
return m_p25LocalAddress;
|
||||
}
|
||||
|
||||
unsigned int CConf::getP25LocalPort() const
|
||||
{
|
||||
return m_p25LocalPort;
|
||||
}
|
||||
|
||||
std::string CConf::getP25GainAdjDb() const
|
||||
{
|
||||
return m_p25GainAdjDb;
|
||||
}
|
||||
|
||||
bool CConf::getP25NetworkDebug() const
|
||||
{
|
||||
return m_p25NetworkDebug;
|
||||
}
|
||||
|
||||
bool CConf::getDaemon() const
|
||||
{
|
||||
return m_daemon;
|
||||
}
|
||||
|
||||
std::string CConf::getUSRPAddress() const
|
||||
{
|
||||
return m_usrpAddress;
|
||||
}
|
||||
|
||||
uint16_t CConf::getUSRPDstPort() const
|
||||
{
|
||||
return m_usrpDstPort;
|
||||
}
|
||||
|
||||
uint16_t CConf::getUSRPLocalPort() const
|
||||
{
|
||||
return m_usrpLocalPort;
|
||||
}
|
||||
|
||||
std::string CConf::getUSRPGainAdjDb() const
|
||||
{
|
||||
return m_usrpGainAdjDb;
|
||||
}
|
||||
|
||||
bool CConf::getUSRPDebug() const
|
||||
{
|
||||
return m_usrpDebug;
|
||||
}
|
||||
|
||||
uint32_t CConf::getLogDisplayLevel() const
|
||||
{
|
||||
return m_logDisplayLevel;
|
||||
}
|
||||
|
||||
uint32_t CConf::getLogFileLevel() const
|
||||
{
|
||||
return m_logFileLevel;
|
||||
}
|
||||
|
||||
std::string CConf::getLogFilePath() const
|
||||
{
|
||||
return m_logFilePath;
|
||||
}
|
||||
|
||||
std::string CConf::getLogFileRoot() const
|
||||
{
|
||||
return m_logFileRoot;
|
||||
}
|
85
USRP2P25/Conf.h
Normal file
85
USRP2P25/Conf.h
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2015,2016,2017 by Jonathan Naylor G4KLX
|
||||
* Copyright (C) 2018 by Andy Uribe CA6JAU
|
||||
* Copyright (C) 2021 by Doug McLain AD8DP
|
||||
*
|
||||
* 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(CONF_H)
|
||||
#define CONF_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class CConf
|
||||
{
|
||||
public:
|
||||
CConf(const std::string& file);
|
||||
~CConf();
|
||||
|
||||
bool read();
|
||||
|
||||
// The P25 Network section
|
||||
std::string getCallsign() const;
|
||||
bool getDaemon() const;
|
||||
uint32_t getP25DstId() const;
|
||||
std::string getP25DstAddress() const;
|
||||
uint32_t getP25DstPort() const;
|
||||
std::string getP25LocalAddress() const;
|
||||
uint32_t getP25LocalPort() const;
|
||||
std::string getP25GainAdjDb() const;
|
||||
bool getP25NetworkDebug() const;
|
||||
|
||||
// The USRP Network section
|
||||
std::string getUSRPAddress() const;
|
||||
uint16_t getUSRPDstPort() const;
|
||||
uint16_t getUSRPLocalPort() const;
|
||||
std::string getUSRPGainAdjDb() const;
|
||||
bool getUSRPDebug() const;
|
||||
|
||||
// The Log section
|
||||
uint32_t getLogDisplayLevel() const;
|
||||
uint32_t getLogFileLevel() const;
|
||||
std::string getLogFilePath() const;
|
||||
std::string getLogFileRoot() const;
|
||||
|
||||
private:
|
||||
std::string m_file;
|
||||
std::string m_callsign;
|
||||
bool m_daemon;
|
||||
|
||||
std::string m_usrpAddress;
|
||||
uint16_t m_usrpDstPort;
|
||||
uint16_t m_usrpLocalPort;
|
||||
std::string m_usrpGainAdjDb;
|
||||
bool m_usrpDebug;
|
||||
|
||||
uint32_t m_p25DstId;
|
||||
std::string m_p25DstAddress;
|
||||
uint32_t m_p25DstPort;
|
||||
std::string m_p25LocalAddress;
|
||||
uint32_t m_p25LocalPort;
|
||||
std::string m_p25GainAdjDb;
|
||||
bool m_p25NetworkDebug;
|
||||
|
||||
|
||||
uint32_t m_logDisplayLevel;
|
||||
uint32_t m_logFileLevel;
|
||||
std::string m_logFilePath;
|
||||
std::string m_logFileRoot;
|
||||
};
|
||||
|
||||
#endif
|
136
USRP2P25/Log.cpp
Normal file
136
USRP2P25/Log.cpp
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (C) 2015,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.
|
||||
*/
|
||||
|
||||
#include "Log.h"
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#include <Windows.h>
|
||||
#else
|
||||
#include <sys/time.h>
|
||||
#endif
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstdarg>
|
||||
#include <ctime>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
static unsigned int m_fileLevel = 2U;
|
||||
static std::string m_filePath;
|
||||
static std::string m_fileRoot;
|
||||
|
||||
static FILE* m_fpLog = NULL;
|
||||
|
||||
static unsigned int m_displayLevel = 2U;
|
||||
|
||||
static struct tm m_tm;
|
||||
|
||||
static char LEVELS[] = " DMIWEF";
|
||||
|
||||
static bool LogOpen()
|
||||
{
|
||||
if (m_fileLevel == 0U)
|
||||
return true;
|
||||
|
||||
time_t now;
|
||||
::time(&now);
|
||||
|
||||
struct tm* tm = ::gmtime(&now);
|
||||
|
||||
if (tm->tm_mday == m_tm.tm_mday && tm->tm_mon == m_tm.tm_mon && tm->tm_year == m_tm.tm_year) {
|
||||
if (m_fpLog != NULL)
|
||||
return true;
|
||||
} else {
|
||||
if (m_fpLog != NULL)
|
||||
::fclose(m_fpLog);
|
||||
}
|
||||
|
||||
char filename[100U];
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
::sprintf(filename, "%s\\%s-%04d-%02d-%02d.log", m_filePath.c_str(), m_fileRoot.c_str(), tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
|
||||
#else
|
||||
::sprintf(filename, "%s/%s-%04d-%02d-%02d.log", m_filePath.c_str(), m_fileRoot.c_str(), tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
|
||||
#endif
|
||||
|
||||
m_fpLog = ::fopen(filename, "a+t");
|
||||
m_tm = *tm;
|
||||
|
||||
return m_fpLog != NULL;
|
||||
}
|
||||
|
||||
bool LogInitialise(const std::string& filePath, const std::string& fileRoot, unsigned int fileLevel, unsigned int displayLevel)
|
||||
{
|
||||
m_filePath = filePath;
|
||||
m_fileRoot = fileRoot;
|
||||
m_fileLevel = fileLevel;
|
||||
m_displayLevel = displayLevel;
|
||||
return ::LogOpen();
|
||||
}
|
||||
|
||||
void LogFinalise()
|
||||
{
|
||||
if (m_fpLog != NULL)
|
||||
::fclose(m_fpLog);
|
||||
}
|
||||
|
||||
void Log(unsigned int level, const char* fmt, ...)
|
||||
{
|
||||
assert(fmt != NULL);
|
||||
|
||||
char buffer[300U];
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
SYSTEMTIME st;
|
||||
::GetSystemTime(&st);
|
||||
|
||||
::sprintf(buffer, "%c: %04u-%02u-%02u %02u:%02u:%02u.%03u ", LEVELS[level], st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
|
||||
#else
|
||||
struct timeval now;
|
||||
::gettimeofday(&now, NULL);
|
||||
|
||||
struct tm* tm = ::gmtime(&now.tv_sec);
|
||||
|
||||
::sprintf(buffer, "%c: %04d-%02d-%02d %02d:%02d:%02d.%03lu ", LEVELS[level], tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, now.tv_usec / 1000U);
|
||||
#endif
|
||||
|
||||
va_list vl;
|
||||
va_start(vl, fmt);
|
||||
|
||||
::vsprintf(buffer + ::strlen(buffer), fmt, vl);
|
||||
|
||||
va_end(vl);
|
||||
|
||||
if (level >= m_fileLevel && m_fileLevel != 0U) {
|
||||
bool ret = ::LogOpen();
|
||||
if (!ret)
|
||||
return;
|
||||
|
||||
::fprintf(m_fpLog, "%s\n", buffer);
|
||||
::fflush(m_fpLog);
|
||||
}
|
||||
|
||||
if (level >= m_displayLevel && m_displayLevel != 0U) {
|
||||
::fprintf(stdout, "%s\n", buffer);
|
||||
::fflush(stdout);
|
||||
}
|
||||
|
||||
if (level == 6U) { // Fatal
|
||||
::fclose(m_fpLog);
|
||||
exit(1);
|
||||
}
|
||||
}
|
36
USRP2P25/Log.h
Normal file
36
USRP2P25/Log.h
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2015,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(LOG_H)
|
||||
#define LOG_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#define LogDebug(fmt, ...) Log(1U, fmt, ##__VA_ARGS__)
|
||||
#define LogMessage(fmt, ...) Log(2U, fmt, ##__VA_ARGS__)
|
||||
#define LogInfo(fmt, ...) Log(3U, fmt, ##__VA_ARGS__)
|
||||
#define LogWarning(fmt, ...) Log(4U, fmt, ##__VA_ARGS__)
|
||||
#define LogError(fmt, ...) Log(5U, fmt, ##__VA_ARGS__)
|
||||
#define LogFatal(fmt, ...) Log(6U, fmt, ##__VA_ARGS__)
|
||||
|
||||
extern void Log(unsigned int level, const char* fmt, ...);
|
||||
|
||||
extern bool LogInitialise(const std::string& filePath, const std::string& fileRoot, unsigned int fileLevel, unsigned int displayLevel);
|
||||
extern void LogFinalise();
|
||||
|
||||
#endif
|
113
USRP2P25/MBEVocoder.cpp
Normal file
113
USRP2P25/MBEVocoder.cpp
Normal file
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (C) 2016,2017 by Jonathan Naylor G4KLX
|
||||
* Copyright (C) 2018 by Andy Uribe CA6JAU
|
||||
* Copyright (C) 2020 by Doug McLain AD8DP
|
||||
*
|
||||
* 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 <cstring>
|
||||
#include "MBEVocoder.h"
|
||||
|
||||
|
||||
const uint8_t BIT_MASK_TABLE8[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U };
|
||||
#define WRITE_BIT8(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE8[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE8[(i)&7])
|
||||
#define READ_BIT8(p,i) (p[(i)>>3] & BIT_MASK_TABLE8[(i)&7])
|
||||
|
||||
MBEVocoder::MBEVocoder(void)
|
||||
{
|
||||
}
|
||||
|
||||
void MBEVocoder::decode_4400(int16_t *pcm, uint8_t *imbe)
|
||||
{
|
||||
int16_t frame[8U] = {0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000};
|
||||
unsigned int offset = 0U;
|
||||
|
||||
int16_t mask = 0x0800;
|
||||
for (unsigned int i = 0U; i < 12U; i++, mask >>= 1, offset++)
|
||||
frame[0U] |= READ_BIT8(imbe, offset) != 0x00U ? mask : 0x0000;
|
||||
|
||||
mask = 0x0800;
|
||||
for (unsigned int i = 0U; i < 12U; i++, mask >>= 1, offset++)
|
||||
frame[1U] |= READ_BIT8(imbe, offset) != 0x00U ? mask : 0x0000;
|
||||
|
||||
mask = 0x0800;
|
||||
for (unsigned int i = 0U; i < 12U; i++, mask >>= 1, offset++)
|
||||
frame[2U] |= READ_BIT8(imbe, offset) != 0x00U ? mask : 0x0000;
|
||||
|
||||
mask = 0x0800;
|
||||
for (unsigned int i = 0U; i < 12U; i++, mask >>= 1, offset++)
|
||||
frame[3U] |= READ_BIT8(imbe, offset) != 0x00U ? mask : 0x0000;
|
||||
|
||||
mask = 0x0400;
|
||||
for (unsigned int i = 0U; i < 11U; i++, mask >>= 1, offset++)
|
||||
frame[4U] |= READ_BIT8(imbe, offset) != 0x00U ? mask : 0x0000;
|
||||
|
||||
mask = 0x0400;
|
||||
for (unsigned int i = 0U; i < 11U; i++, mask >>= 1, offset++)
|
||||
frame[5U] |= READ_BIT8(imbe, offset) != 0x00U ? mask : 0x0000;
|
||||
|
||||
mask = 0x0400;
|
||||
for (unsigned int i = 0U; i < 11U; i++, mask >>= 1, offset++)
|
||||
frame[6U] |= READ_BIT8(imbe, offset) != 0x00U ? mask : 0x0000;
|
||||
|
||||
mask = 0x0040;
|
||||
for (unsigned int i = 0U; i < 7U; i++, mask >>= 1, offset++)
|
||||
frame[7U] |= READ_BIT8(imbe, offset) != 0x00U ? mask : 0x0000;
|
||||
|
||||
vocoder.imbe_decode(frame, pcm);
|
||||
}
|
||||
|
||||
void MBEVocoder::encode_4400(int16_t *pcm, uint8_t *imbe)
|
||||
{
|
||||
int16_t frame_vector[8];
|
||||
memset(imbe, 0, 9);
|
||||
|
||||
vocoder.imbe_encode(frame_vector, pcm);
|
||||
//vocoder.set_gain_adjust(1.0);
|
||||
uint32_t offset = 0U;
|
||||
int16_t mask = 0x0800;
|
||||
|
||||
for (uint32_t i = 0U; i < 12U; i++, mask >>= 1, offset++)
|
||||
WRITE_BIT8(imbe, offset, (frame_vector[0U] & mask) != 0);
|
||||
|
||||
mask = 0x0800;
|
||||
for (uint32_t i = 0U; i < 12U; i++, mask >>= 1, offset++)
|
||||
WRITE_BIT8(imbe, offset, (frame_vector[1U] & mask) != 0);
|
||||
|
||||
mask = 0x0800;
|
||||
for (uint32_t i = 0U; i < 12U; i++, mask >>= 1, offset++)
|
||||
WRITE_BIT8(imbe, offset, (frame_vector[2U] & mask) != 0);
|
||||
|
||||
mask = 0x0800;
|
||||
for (uint32_t i = 0U; i < 12U; i++, mask >>= 1, offset++)
|
||||
WRITE_BIT8(imbe, offset, (frame_vector[3U] & mask) != 0);
|
||||
|
||||
mask = 0x0400;
|
||||
for (uint32_t i = 0U; i < 11U; i++, mask >>= 1, offset++)
|
||||
WRITE_BIT8(imbe, offset, (frame_vector[4U] & mask) != 0);
|
||||
|
||||
mask = 0x0400;
|
||||
for (uint32_t i = 0U; i < 11U; i++, mask >>= 1, offset++)
|
||||
WRITE_BIT8(imbe, offset, (frame_vector[5U] & mask) != 0);
|
||||
|
||||
mask = 0x0400;
|
||||
for (uint32_t i = 0U; i < 11U; i++, mask >>= 1, offset++)
|
||||
WRITE_BIT8(imbe, offset, (frame_vector[6U] & mask) != 0);
|
||||
|
||||
mask = 0x0040;
|
||||
for (uint32_t i = 0U; i < 7U; i++, mask >>= 1, offset++)
|
||||
WRITE_BIT8(imbe, offset, (frame_vector[7U] & mask) != 0);
|
||||
|
||||
}
|
37
USRP2P25/MBEVocoder.h
Normal file
37
USRP2P25/MBEVocoder.h
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2016,2017 by Jonathan Naylor G4KLX
|
||||
* Copyright (C) 2018 by Andy Uribe CA6JAU
|
||||
* Copyright (C) 2020 by Doug McLain AD8DP
|
||||
*
|
||||
* 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 INCLUDED_AMBE_ENCODER_H
|
||||
#define INCLUDED_AMBE_ENCODER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <imbe_vocoder_api.h>
|
||||
|
||||
class MBEVocoder {
|
||||
public:
|
||||
void decode_4400(int16_t *, uint8_t *);
|
||||
void encode_4400(int16_t *, uint8_t *);
|
||||
MBEVocoder(void);
|
||||
|
||||
private:
|
||||
imbe_vocoder vocoder;
|
||||
};
|
||||
|
||||
#endif /* INCLUDED_AMBE_ENCODER_H */
|
22
USRP2P25/Makefile
Normal file
22
USRP2P25/Makefile
Normal file
@ -0,0 +1,22 @@
|
||||
CC ?= gcc
|
||||
CXX ?= g++
|
||||
CFLAGS ?= -g -O3 -Wall -std=c++0x -pthread
|
||||
LIBS = -lm -lpthread -limbe_vocoder
|
||||
LDFLAGS ?= -g
|
||||
|
||||
OBJECTS = Conf.o Log.o MBEVocoder.o ModeConv.o P25Network.o StopWatch.o Timer.o UDPSocket.o USRPNetwork.o Utils.o USRP2P25.o
|
||||
|
||||
all: USRP2P25
|
||||
|
||||
USRP2P25: $(OBJECTS)
|
||||
$(CXX) $(OBJECTS) $(CFLAGS) $(LIBS) -o USRP2P25
|
||||
|
||||
%.o: %.cpp
|
||||
$(CXX) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
install:
|
||||
install -m 755 USRP2P25 /usr/local/bin/
|
||||
|
||||
clean:
|
||||
$(RM) USRP2P25 *.o *.d *.bak *~
|
||||
|
216
USRP2P25/ModeConv.cpp
Normal file
216
USRP2P25/ModeConv.cpp
Normal file
@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright (C) 2010,2014,2016,2018 by Jonathan Naylor G4KLX
|
||||
* Copyright (C) 2016 Mathias Weyland, HB9FRV
|
||||
* Copyright (C) 2018 by Andy Uribe CA6JAU
|
||||
* Copyright (C) 2021 by Doug McLain AD8DP
|
||||
*
|
||||
* 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 "ModeConv.h"
|
||||
#include "Utils.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
const uint8_t IMBE_SILENCE[] = {0x04U, 0x0CU, 0xFDU, 0x7BU, 0xFBU, 0x7DU, 0xF2U, 0x7BU, 0x3DU, 0x9EU, 0x44};
|
||||
|
||||
CModeConv::CModeConv() :
|
||||
m_p25N(0U),
|
||||
m_usrpN(0U),
|
||||
m_P25(5000U, "USRP2P25"),
|
||||
m_USRP(5000U, "P252USRP"),
|
||||
m_p25GainMultiplier(1),
|
||||
m_p25Attenuate(false),
|
||||
m_usrpGainMultiplier(3),
|
||||
m_usrpAttenuate(true)
|
||||
{
|
||||
m_mbe = new MBEVocoder();
|
||||
}
|
||||
|
||||
CModeConv::~CModeConv()
|
||||
{
|
||||
}
|
||||
|
||||
void CModeConv::setUSRPGainAdjDb(std::string dbstring)
|
||||
{
|
||||
float db = std::stof(dbstring);
|
||||
|
||||
float ratio = powf(10.0, (db/10.0));
|
||||
if(db < 0){
|
||||
ratio = 1/ratio;
|
||||
m_usrpAttenuate = true;
|
||||
}
|
||||
m_usrpGainMultiplier = (uint16_t)roundf(ratio);
|
||||
}
|
||||
|
||||
void CModeConv::setP25GainAdjDb(std::string dbstring)
|
||||
{
|
||||
float db = std::stof(dbstring);
|
||||
|
||||
float ratio = powf(10.0, (db/10.0));
|
||||
if(db < 0){
|
||||
ratio = 1/ratio;
|
||||
m_p25Attenuate = true;
|
||||
}
|
||||
m_p25GainMultiplier = (uint16_t)roundf(ratio);
|
||||
}
|
||||
|
||||
void CModeConv::putUSRPHeader()
|
||||
{
|
||||
m_P25.addData(&TAG_HEADER, 1U);
|
||||
m_P25.addData(IMBE_SILENCE, 11U);
|
||||
m_p25N += 1U;
|
||||
}
|
||||
|
||||
void CModeConv::putUSRPEOT()
|
||||
{
|
||||
m_P25.addData(&TAG_EOT, 1U);
|
||||
m_P25.addData(IMBE_SILENCE, 11U);
|
||||
m_p25N += 1U;
|
||||
}
|
||||
|
||||
void CModeConv::putUSRP(int16_t* data)
|
||||
{
|
||||
uint8_t imbe[11U];
|
||||
int16_t audio_adjusted[160U];
|
||||
|
||||
for(int i = 0; i < 160; ++i){
|
||||
audio_adjusted[i] = m_usrpAttenuate ? data[i] / m_usrpGainMultiplier : data[i] * m_usrpGainMultiplier;
|
||||
}
|
||||
|
||||
m_mbe->encode_4400(audio_adjusted, imbe);
|
||||
m_P25.addData(&TAG_DATA, 1U);
|
||||
m_P25.addData(imbe, 11U);
|
||||
//CUtils::dump(1U, "NXDN Voice:", data, 9U);
|
||||
m_p25N += 1U;
|
||||
}
|
||||
|
||||
void CModeConv::putP25(uint8_t* data)
|
||||
{
|
||||
int16_t audio[160U];
|
||||
int16_t audio_adjusted[160U];
|
||||
uint8_t imbe[11U];
|
||||
|
||||
switch (data[0U]) {
|
||||
case 0x62U:
|
||||
::memcpy(imbe, data + 10U, 11U);
|
||||
break;
|
||||
case 0x63U:
|
||||
::memcpy(imbe, data + 1U, 11U);
|
||||
break;
|
||||
case 0x64U:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x65U:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x66U:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x67U:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x68U:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x69U:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x6AU:
|
||||
::memcpy(imbe, data + 4U, 11U);
|
||||
break;
|
||||
case 0x6BU:
|
||||
::memcpy(imbe, data + 10U, 11U);
|
||||
break;
|
||||
case 0x6CU:
|
||||
::memcpy(imbe, data + 1U, 11U);
|
||||
break;
|
||||
case 0x6DU:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x6EU:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x6FU:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x70U:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x71U:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x72U:
|
||||
::memcpy(imbe, data + 5U, 11U);
|
||||
break;
|
||||
case 0x73U:
|
||||
::memcpy(imbe, data + 4U, 11U);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
m_mbe->decode_4400(audio, imbe);
|
||||
|
||||
for(int i = 0; i < 160; ++i){
|
||||
audio_adjusted[i] = m_p25Attenuate ? audio[i] / m_p25GainMultiplier : audio[i] * m_p25GainMultiplier;
|
||||
}
|
||||
|
||||
m_USRP.addData(audio_adjusted, 160U);
|
||||
m_usrpN += 1U;
|
||||
|
||||
}
|
||||
|
||||
bool CModeConv::getUSRP(int16_t* data)
|
||||
{
|
||||
if(m_usrpN){
|
||||
--m_usrpN;
|
||||
return m_USRP.getData(data, 160U);
|
||||
}
|
||||
else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t CModeConv::getP25(uint8_t* data)
|
||||
{
|
||||
uint8_t tag[1U];
|
||||
|
||||
tag[0U] = TAG_NODATA;
|
||||
|
||||
if (m_p25N >= 1U) {
|
||||
m_P25.peek(tag, 1U);
|
||||
|
||||
if (tag[0U] != TAG_DATA) {
|
||||
m_P25.getData(tag, 1U);
|
||||
m_P25.getData(data, 11U);
|
||||
m_p25N -= 1U;
|
||||
return tag[0U];
|
||||
}
|
||||
}
|
||||
|
||||
if (m_p25N >= 1U) {
|
||||
m_P25.getData(tag, 1U);
|
||||
m_P25.getData(data, 11U);
|
||||
m_p25N -= 1U;
|
||||
|
||||
return TAG_DATA;
|
||||
}
|
||||
else
|
||||
return TAG_NODATA;
|
||||
}
|
60
USRP2P25/ModeConv.h
Normal file
60
USRP2P25/ModeConv.h
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2010,2014,2016,2018 by Jonathan Naylor G4KLX
|
||||
* Copyright (C) 2018 by Andy Uribe CA6JAU
|
||||
* Copyright (C) 2021 by Doug McLain AD8DP
|
||||
*
|
||||
* 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 "RingBuffer.h"
|
||||
#include "MBEVocoder.h"
|
||||
|
||||
const unsigned char TAG_HEADER = 0x00U;
|
||||
const unsigned char TAG_DATA = 0x01U;
|
||||
const unsigned char TAG_LOST = 0x02U;
|
||||
const unsigned char TAG_EOT = 0x03U;
|
||||
const unsigned char TAG_NODATA = 0x04U;
|
||||
|
||||
#if !defined(MODECONV_H)
|
||||
#define MODECONV_H
|
||||
|
||||
class CModeConv {
|
||||
public:
|
||||
CModeConv();
|
||||
~CModeConv();
|
||||
|
||||
void setUSRPGainAdjDb(std::string dbstring);
|
||||
void setP25GainAdjDb(std::string dbstring);
|
||||
void putUSRP(int16_t* data);
|
||||
void putUSRPHeader();
|
||||
void putUSRPEOT();
|
||||
|
||||
void putP25(unsigned char* data);
|
||||
|
||||
uint32_t getP25(uint8_t* data);
|
||||
bool getUSRP(int16_t* data);
|
||||
private:
|
||||
uint32_t m_p25N;
|
||||
uint32_t m_usrpN;
|
||||
CRingBuffer<uint8_t> m_P25;
|
||||
CRingBuffer<int16_t> m_USRP;
|
||||
MBEVocoder *m_mbe;
|
||||
uint16_t m_p25GainMultiplier;
|
||||
bool m_p25Attenuate;
|
||||
uint16_t m_usrpGainMultiplier;
|
||||
bool m_usrpAttenuate;
|
||||
};
|
||||
|
||||
#endif
|
118
USRP2P25/P25Network.cpp
Normal file
118
USRP2P25/P25Network.cpp
Normal file
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2009-2014,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.
|
||||
*/
|
||||
|
||||
#include "P25Network.h"
|
||||
#include "Utils.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
CP25Network::CP25Network(const std::string& localAddress, unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, const std::string& callsign, bool debug) :
|
||||
m_callsign(callsign),
|
||||
m_address(),
|
||||
m_port(gatewayPort),
|
||||
m_socket(localAddress, localPort),
|
||||
m_debug(debug)
|
||||
{
|
||||
m_callsign.resize(10U, ' ');
|
||||
m_address = CUDPSocket::lookup(gatewayAddress);
|
||||
}
|
||||
|
||||
CP25Network::~CP25Network()
|
||||
{
|
||||
}
|
||||
|
||||
bool CP25Network::open()
|
||||
{
|
||||
LogInfo("Opening P25 network connection");
|
||||
|
||||
return m_socket.open();
|
||||
}
|
||||
|
||||
bool CP25Network::writeData(const unsigned char* data, unsigned int length)
|
||||
{
|
||||
assert(data != NULL);
|
||||
assert(length > 0U);
|
||||
|
||||
if (m_debug)
|
||||
CUtils::dump(1U, "P25 Network Data Sent", data, length);
|
||||
|
||||
return m_socket.write(data, length, m_address, m_port);
|
||||
}
|
||||
|
||||
bool CP25Network::writePoll()
|
||||
{
|
||||
unsigned char data[15U];
|
||||
|
||||
data[0U] = 0xF0U;
|
||||
|
||||
for (unsigned int i = 0U; i < 10U; i++)
|
||||
data[i + 1U] = m_callsign.at(i);
|
||||
|
||||
if (m_debug)
|
||||
CUtils::dump(1U, "P25 Network Poll Sent", data, 11U);
|
||||
|
||||
return m_socket.write(data, 11U, m_address, m_port);
|
||||
}
|
||||
|
||||
bool CP25Network::writeUnlink()
|
||||
{
|
||||
unsigned char data[15U];
|
||||
|
||||
data[0U] = 0xF1U;
|
||||
|
||||
for (unsigned int i = 0U; i < 10U; i++)
|
||||
data[i + 1U] = m_callsign.at(i);
|
||||
|
||||
if (m_debug)
|
||||
CUtils::dump(1U, "P25 Network Unlink Sent", data, 11U);
|
||||
|
||||
return m_socket.write(data, 11U, m_address, m_port);
|
||||
}
|
||||
|
||||
unsigned int CP25Network::readData(unsigned char* data, unsigned int length)
|
||||
{
|
||||
assert(data != NULL);
|
||||
assert(length > 0U);
|
||||
|
||||
in_addr address;
|
||||
unsigned int port;
|
||||
int len = m_socket.read(data, length, address, port);
|
||||
if (len <= 0)
|
||||
return 0U;
|
||||
|
||||
// Check if the data is for us
|
||||
if (m_address.s_addr != address.s_addr || port != m_port) {
|
||||
LogMessage("P25 packet received from an invalid source, %08X != %08X and/or %u != %u", m_address.s_addr, address.s_addr, m_port, port);
|
||||
return 0U;
|
||||
}
|
||||
|
||||
if (m_debug)
|
||||
CUtils::dump(1U, "P25 Network Data Received", data, len);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
void CP25Network::close()
|
||||
{
|
||||
m_socket.close();
|
||||
|
||||
LogInfo("Closing P25 network connection");
|
||||
}
|
52
USRP2P25/P25Network.h
Normal file
52
USRP2P25/P25Network.h
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2009-2014,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.
|
||||
*/
|
||||
|
||||
#ifndef P25Network_H
|
||||
#define P25Network_H
|
||||
|
||||
#include "UDPSocket.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
class CP25Network {
|
||||
public:
|
||||
CP25Network(const std::string& localAddress, unsigned int localPort, const std::string& gatewayAddress, unsigned int gatewayPort, const std::string& callsign, bool debug);
|
||||
~CP25Network();
|
||||
|
||||
bool open();
|
||||
|
||||
bool writeData(const unsigned char* data, unsigned int length);
|
||||
|
||||
unsigned int readData(unsigned char* data, unsigned int length);
|
||||
|
||||
bool writePoll();
|
||||
|
||||
bool writeUnlink();
|
||||
|
||||
void close();
|
||||
|
||||
private:
|
||||
std::string m_callsign;
|
||||
in_addr m_address;
|
||||
unsigned int m_port;
|
||||
CUDPSocket m_socket;
|
||||
bool m_debug;
|
||||
};
|
||||
|
||||
#endif
|
11
USRP2P25/README.md
Normal file
11
USRP2P25/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Description
|
||||
|
||||
This is the source code of USRP2P25, which converts USRP PCM audio and P25 digital mode, based on Jonathan G4KLX's [MMDVM](https://github.com/g4klx) software. Typical uses are connecting P25 reflectors to AllStar nodes and can be used with MMDVM modems in FM mode as stand alone radios.
|
||||
|
||||
# Configuration
|
||||
P25 and USRP both have a configuration value 'GainAdjustDB". Thru trial and error I have found the best balance of audio levels which are set as the defaults in the provided USRP2P25.ini file. For AllStar connections, USRP address and ports are the values defined in your USRP channel based node.
|
||||
|
||||
# Building
|
||||
|
||||
Build and install imbe_vocoder: https://github.com/nostar/imbe_vocoder
|
||||
run 'make' from the source directory.
|
154
USRP2P25/RingBuffer.h
Normal file
154
USRP2P25/RingBuffer.h
Normal file
@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (C) 2006-2009,2012,2013,2015,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.
|
||||
*/
|
||||
|
||||
#ifndef RingBuffer_H
|
||||
#define RingBuffer_H
|
||||
|
||||
#include "Log.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
template<class T> class CRingBuffer {
|
||||
public:
|
||||
CRingBuffer(unsigned int length, const char* name) :
|
||||
m_length(length),
|
||||
m_name(name),
|
||||
m_buffer(NULL),
|
||||
m_iPtr(0U),
|
||||
m_oPtr(0U)
|
||||
{
|
||||
assert(length > 0U);
|
||||
assert(name != NULL);
|
||||
|
||||
m_buffer = new T[length];
|
||||
|
||||
::memset(m_buffer, 0x00, m_length * sizeof(T));
|
||||
}
|
||||
|
||||
~CRingBuffer()
|
||||
{
|
||||
delete[] m_buffer;
|
||||
}
|
||||
|
||||
bool addData(const T* buffer, unsigned int nSamples)
|
||||
{
|
||||
if (nSamples >= freeSpace()) {
|
||||
LogError("%s buffer overflow, clearing the buffer. (%u >= %u)", m_name, nSamples, freeSpace());
|
||||
clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (unsigned int i = 0U; i < nSamples; i++) {
|
||||
m_buffer[m_iPtr++] = buffer[i];
|
||||
|
||||
if (m_iPtr == m_length)
|
||||
m_iPtr = 0U;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getData(T* buffer, unsigned int nSamples)
|
||||
{
|
||||
if (dataSize() < nSamples) {
|
||||
LogError("**** Underflow in %s ring buffer, %u < %u", m_name, dataSize(), nSamples);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (unsigned int i = 0U; i < nSamples; i++) {
|
||||
buffer[i] = m_buffer[m_oPtr++];
|
||||
|
||||
if (m_oPtr == m_length)
|
||||
m_oPtr = 0U;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool peek(T* buffer, unsigned int nSamples)
|
||||
{
|
||||
if (dataSize() < nSamples) {
|
||||
LogError("**** Underflow peek in %s ring buffer, %u < %u", m_name, dataSize(), nSamples);
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned int ptr = m_oPtr;
|
||||
for (unsigned int i = 0U; i < nSamples; i++) {
|
||||
buffer[i] = m_buffer[ptr++];
|
||||
|
||||
if (ptr == m_length)
|
||||
ptr = 0U;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
m_iPtr = 0U;
|
||||
m_oPtr = 0U;
|
||||
|
||||
::memset(m_buffer, 0x00, m_length * sizeof(T));
|
||||
}
|
||||
|
||||
unsigned int freeSpace() const
|
||||
{
|
||||
unsigned int len = m_length;
|
||||
|
||||
if (m_oPtr > m_iPtr)
|
||||
len = m_oPtr - m_iPtr;
|
||||
else if (m_iPtr > m_oPtr)
|
||||
len = m_length - (m_iPtr - m_oPtr);
|
||||
|
||||
if (len > m_length)
|
||||
len = 0U;
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
unsigned int dataSize() const
|
||||
{
|
||||
return m_length - freeSpace();
|
||||
}
|
||||
|
||||
bool hasSpace(unsigned int length) const
|
||||
{
|
||||
return freeSpace() > length;
|
||||
}
|
||||
|
||||
bool hasData() const
|
||||
{
|
||||
return m_oPtr != m_iPtr;
|
||||
}
|
||||
|
||||
bool isEmpty() const
|
||||
{
|
||||
return m_oPtr == m_iPtr;
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned int m_length;
|
||||
const char* m_name;
|
||||
T* m_buffer;
|
||||
unsigned int m_iPtr;
|
||||
unsigned int m_oPtr;
|
||||
};
|
||||
|
||||
#endif
|
105
USRP2P25/StopWatch.cpp
Normal file
105
USRP2P25/StopWatch.cpp
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2015,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 "StopWatch.h"
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
|
||||
CStopWatch::CStopWatch() :
|
||||
m_frequencyS(),
|
||||
m_frequencyMS(),
|
||||
m_start()
|
||||
{
|
||||
::QueryPerformanceFrequency(&m_frequencyS);
|
||||
|
||||
m_frequencyMS.QuadPart = m_frequencyS.QuadPart / 1000ULL;
|
||||
}
|
||||
|
||||
CStopWatch::~CStopWatch()
|
||||
{
|
||||
}
|
||||
|
||||
unsigned long long CStopWatch::time() const
|
||||
{
|
||||
LARGE_INTEGER now;
|
||||
::QueryPerformanceCounter(&now);
|
||||
|
||||
return (unsigned long long)(now.QuadPart / m_frequencyMS.QuadPart);
|
||||
}
|
||||
|
||||
unsigned long long CStopWatch::start()
|
||||
{
|
||||
::QueryPerformanceCounter(&m_start);
|
||||
|
||||
return (unsigned long long)(m_start.QuadPart / m_frequencyS.QuadPart);
|
||||
}
|
||||
|
||||
unsigned int CStopWatch::elapsed()
|
||||
{
|
||||
LARGE_INTEGER now;
|
||||
::QueryPerformanceCounter(&now);
|
||||
|
||||
LARGE_INTEGER temp;
|
||||
temp.QuadPart = (now.QuadPart - m_start.QuadPart) * 1000;
|
||||
|
||||
return (unsigned int)(temp.QuadPart / m_frequencyS.QuadPart);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
|
||||
CStopWatch::CStopWatch() :
|
||||
m_startMS(0ULL)
|
||||
{
|
||||
}
|
||||
|
||||
CStopWatch::~CStopWatch()
|
||||
{
|
||||
}
|
||||
|
||||
unsigned long long CStopWatch::time() const
|
||||
{
|
||||
struct timeval now;
|
||||
::gettimeofday(&now, NULL);
|
||||
|
||||
return now.tv_sec * 1000ULL + now.tv_usec / 1000ULL;
|
||||
}
|
||||
|
||||
unsigned long long CStopWatch::start()
|
||||
{
|
||||
struct timespec now;
|
||||
::clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
|
||||
m_startMS = now.tv_sec * 1000ULL + now.tv_nsec / 1000000ULL;
|
||||
|
||||
return m_startMS;
|
||||
}
|
||||
|
||||
unsigned int CStopWatch::elapsed()
|
||||
{
|
||||
struct timespec now;
|
||||
::clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
|
||||
unsigned long long nowMS = now.tv_sec * 1000ULL + now.tv_nsec / 1000000ULL;
|
||||
|
||||
return nowMS - m_startMS;
|
||||
}
|
||||
|
||||
#endif
|
49
USRP2P25/StopWatch.h
Normal file
49
USRP2P25/StopWatch.h
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2015,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(STOPWATCH_H)
|
||||
#define STOPWATCH_H
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/time.h>
|
||||
#endif
|
||||
|
||||
class CStopWatch
|
||||
{
|
||||
public:
|
||||
CStopWatch();
|
||||
~CStopWatch();
|
||||
|
||||
unsigned long long time() const;
|
||||
|
||||
unsigned long long start();
|
||||
unsigned int elapsed();
|
||||
|
||||
private:
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
LARGE_INTEGER m_frequencyS;
|
||||
LARGE_INTEGER m_frequencyMS;
|
||||
LARGE_INTEGER m_start;
|
||||
#else
|
||||
unsigned long long m_startMS;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif
|
68
USRP2P25/Timer.cpp
Normal file
68
USRP2P25/Timer.cpp
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2009,2010,2015 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 "Timer.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cassert>
|
||||
|
||||
CTimer::CTimer(unsigned int ticksPerSec, unsigned int secs, unsigned int msecs) :
|
||||
m_ticksPerSec(ticksPerSec),
|
||||
m_timeout(0U),
|
||||
m_timer(0U)
|
||||
{
|
||||
assert(ticksPerSec > 0U);
|
||||
|
||||
if (secs > 0U || msecs > 0U) {
|
||||
// m_timeout = ((secs * 1000U + msecs) * m_ticksPerSec) / 1000U + 1U;
|
||||
unsigned long long temp = (secs * 1000ULL + msecs) * m_ticksPerSec;
|
||||
m_timeout = (unsigned int)(temp / 1000ULL + 1ULL);
|
||||
}
|
||||
}
|
||||
|
||||
CTimer::~CTimer()
|
||||
{
|
||||
}
|
||||
|
||||
void CTimer::setTimeout(unsigned int secs, unsigned int msecs)
|
||||
{
|
||||
if (secs > 0U || msecs > 0U) {
|
||||
// m_timeout = ((secs * 1000U + msecs) * m_ticksPerSec) / 1000U + 1U;
|
||||
unsigned long long temp = (secs * 1000ULL + msecs) * m_ticksPerSec;
|
||||
m_timeout = (unsigned int)(temp / 1000ULL + 1ULL);
|
||||
} else {
|
||||
m_timeout = 0U;
|
||||
m_timer = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int CTimer::getTimeout() const
|
||||
{
|
||||
if (m_timeout == 0U)
|
||||
return 0U;
|
||||
|
||||
return (m_timeout - 1U) / m_ticksPerSec;
|
||||
}
|
||||
|
||||
unsigned int CTimer::getTimer() const
|
||||
{
|
||||
if (m_timer == 0U)
|
||||
return 0U;
|
||||
|
||||
return (m_timer - 1U) / m_ticksPerSec;
|
||||
}
|
89
USRP2P25/Timer.h
Normal file
89
USRP2P25/Timer.h
Normal file
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) 2009,2010,2011,2014 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 Timer_H
|
||||
#define Timer_H
|
||||
|
||||
class CTimer {
|
||||
public:
|
||||
CTimer(unsigned int ticksPerSec, unsigned int secs = 0U, unsigned int msecs = 0U);
|
||||
~CTimer();
|
||||
|
||||
void setTimeout(unsigned int secs, unsigned int msecs = 0U);
|
||||
|
||||
unsigned int getTimeout() const;
|
||||
unsigned int getTimer() const;
|
||||
|
||||
unsigned int getRemaining()
|
||||
{
|
||||
if (m_timeout == 0U || m_timer == 0U)
|
||||
return 0U;
|
||||
|
||||
if (m_timer >= m_timeout)
|
||||
return 0U;
|
||||
|
||||
return (m_timeout - m_timer) / m_ticksPerSec;
|
||||
}
|
||||
|
||||
bool isRunning()
|
||||
{
|
||||
return m_timer > 0U;
|
||||
}
|
||||
|
||||
void start(unsigned int secs, unsigned int msecs = 0U)
|
||||
{
|
||||
setTimeout(secs, msecs);
|
||||
|
||||
start();
|
||||
}
|
||||
|
||||
void start()
|
||||
{
|
||||
if (m_timeout > 0U)
|
||||
m_timer = 1U;
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
m_timer = 0U;
|
||||
}
|
||||
|
||||
bool hasExpired()
|
||||
{
|
||||
if (m_timeout == 0U || m_timer == 0U)
|
||||
return false;
|
||||
|
||||
if (m_timer >= m_timeout)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void clock(unsigned int ticks = 1U)
|
||||
{
|
||||
if (m_timer > 0U && m_timeout > 0U)
|
||||
m_timer += ticks;
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned int m_ticksPerSec;
|
||||
unsigned int m_timeout;
|
||||
unsigned int m_timer;
|
||||
};
|
||||
|
||||
#endif
|
212
USRP2P25/UDPSocket.cpp
Normal file
212
USRP2P25/UDPSocket.cpp
Normal file
@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright (C) 2006-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.
|
||||
*/
|
||||
|
||||
#include "UDPSocket.h"
|
||||
#include "Log.h"
|
||||
#include <cassert>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
|
||||
|
||||
CUDPSocket::CUDPSocket(const std::string& address, unsigned int port) :
|
||||
m_address(address),
|
||||
m_port(port),
|
||||
m_fd(-1)
|
||||
{
|
||||
assert(!address.empty());
|
||||
}
|
||||
|
||||
CUDPSocket::CUDPSocket(unsigned int port) :
|
||||
m_address(),
|
||||
m_port(port),
|
||||
m_fd(-1)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
CUDPSocket::~CUDPSocket()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
in_addr CUDPSocket::lookup(const std::string& hostname)
|
||||
{
|
||||
in_addr addr;
|
||||
|
||||
in_addr_t address = ::inet_addr(hostname.c_str());
|
||||
if (address != in_addr_t(-1)) {
|
||||
addr.s_addr = address;
|
||||
LogInfo("inet_addr() returns %x", addr.s_addr);
|
||||
return addr;
|
||||
}
|
||||
|
||||
struct hostent* hp = ::gethostbyname(hostname.c_str());
|
||||
if (hp != NULL) {
|
||||
::memcpy(&addr, hp->h_addr_list[0], sizeof(struct in_addr));
|
||||
LogInfo("gethostbyname() returns %x", addr.s_addr);
|
||||
return addr;
|
||||
}
|
||||
|
||||
LogError("Cannot find address for host %s", hostname.c_str());
|
||||
|
||||
addr.s_addr = INADDR_NONE;
|
||||
return addr;
|
||||
}
|
||||
|
||||
bool CUDPSocket::open()
|
||||
{
|
||||
m_fd = ::socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (m_fd < 0) {
|
||||
|
||||
LogError("Cannot create the UDP socket, err: %d", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_port > 0U) {
|
||||
sockaddr_in addr;
|
||||
::memset(&addr, 0x00, sizeof(sockaddr_in));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(m_port);
|
||||
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
|
||||
if (!m_address.empty()) {
|
||||
|
||||
addr.sin_addr.s_addr = ::inet_addr(m_address.c_str());
|
||||
|
||||
if (addr.sin_addr.s_addr == INADDR_NONE) {
|
||||
LogError("The local address is invalid - %s", m_address.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int reuse = 1;
|
||||
if (::setsockopt(m_fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse)) == -1) {
|
||||
|
||||
LogError("Cannot set the UDP socket option, err: %d", errno);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (::bind(m_fd, (sockaddr*)&addr, sizeof(sockaddr_in)) == -1) {
|
||||
|
||||
LogError("Cannot bind the UDP address, err: %d", errno);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int CUDPSocket::read(unsigned char* buffer, unsigned int length, in_addr& address, unsigned int& port)
|
||||
{
|
||||
assert(buffer != NULL);
|
||||
assert(length > 0U);
|
||||
|
||||
// Check that the readfrom() won't block
|
||||
fd_set readFds;
|
||||
FD_ZERO(&readFds);
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
FD_SET((unsigned int)m_fd, &readFds);
|
||||
#else
|
||||
FD_SET(m_fd, &readFds);
|
||||
#endif
|
||||
|
||||
// Return immediately
|
||||
timeval tv;
|
||||
tv.tv_sec = 0L;
|
||||
tv.tv_usec = 0L;
|
||||
|
||||
int ret = ::select(m_fd + 1, &readFds, NULL, NULL, &tv);
|
||||
if (ret < 0) {
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
LogError("Error returned from UDP select, err: %lu", ::GetLastError());
|
||||
#else
|
||||
LogError("Error returned from UDP select, err: %d", errno);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ret == 0)
|
||||
return 0;
|
||||
|
||||
sockaddr_in addr;
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
int size = sizeof(sockaddr_in);
|
||||
#else
|
||||
socklen_t size = sizeof(sockaddr_in);
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
int len = ::recvfrom(m_fd, (char*)buffer, length, 0, (sockaddr *)&addr, &size);
|
||||
#else
|
||||
ssize_t len = ::recvfrom(m_fd, (char*)buffer, length, 0, (sockaddr *)&addr, &size);
|
||||
#endif
|
||||
if (len <= 0) {
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
LogError("Error returned from recvfrom, err: %lu", ::GetLastError());
|
||||
#else
|
||||
LogError("Error returned from recvfrom, err: %d", errno);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
address = addr.sin_addr;
|
||||
port = ntohs(addr.sin_port);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
bool CUDPSocket::write(const unsigned char* buffer, unsigned int length, const in_addr& address, unsigned int port)
|
||||
{
|
||||
assert(buffer != NULL);
|
||||
assert(length > 0U);
|
||||
|
||||
sockaddr_in addr;
|
||||
::memset(&addr, 0x00, sizeof(sockaddr_in));
|
||||
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr = address;
|
||||
addr.sin_port = htons(port);
|
||||
|
||||
|
||||
ssize_t ret = ::sendto(m_fd, (char *)buffer, length, 0, (sockaddr *)&addr, sizeof(sockaddr_in));
|
||||
|
||||
if (ret < 0) {
|
||||
|
||||
LogError("Error returned from sendto, err: %d", errno);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (ret != ssize_t(length))
|
||||
return false;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CUDPSocket::close()
|
||||
{
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
::closesocket(m_fd);
|
||||
#else
|
||||
::close(m_fd);
|
||||
#endif
|
||||
}
|
54
USRP2P25/UDPSocket.h
Normal file
54
USRP2P25/UDPSocket.h
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2009-2011,2013,2015,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.
|
||||
*/
|
||||
|
||||
#ifndef UDPSocket_H
|
||||
#define UDPSocket_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <netdb.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
|
||||
class CUDPSocket {
|
||||
public:
|
||||
CUDPSocket(const std::string& address, unsigned int port = 0U);
|
||||
CUDPSocket(unsigned int port = 0U);
|
||||
~CUDPSocket();
|
||||
|
||||
bool open();
|
||||
|
||||
int read(unsigned char* buffer, unsigned int length, in_addr& address, unsigned int& port);
|
||||
bool write(const unsigned char* buffer, unsigned int length, const in_addr& address, unsigned int port);
|
||||
|
||||
void close();
|
||||
|
||||
static in_addr lookup(const std::string& hostName);
|
||||
|
||||
private:
|
||||
std::string m_address;
|
||||
unsigned short m_port;
|
||||
int m_fd;
|
||||
};
|
||||
|
||||
#endif
|
524
USRP2P25/USRP2P25.cpp
Normal file
524
USRP2P25/USRP2P25.cpp
Normal file
@ -0,0 +1,524 @@
|
||||
/*
|
||||
* Copyright (C) 2021 by Doug McLain AD8DP
|
||||
*
|
||||
* 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 "USRP2P25.h"
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <fcntl.h>
|
||||
#include <pwd.h>
|
||||
|
||||
const unsigned char REC62[] = {
|
||||
0x62U, 0x02U, 0x02U, 0x0CU, 0x0BU, 0x12U, 0x64U, 0x00U, 0x00U, 0x80U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U};
|
||||
|
||||
const unsigned char REC63[] = {
|
||||
0x63U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC64[] = {
|
||||
0x64U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC65[] = {
|
||||
0x65U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC66[] = {
|
||||
0x66U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC67[] = {
|
||||
0x67U, 0xF0U, 0x9DU, 0x6AU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC68[] = {
|
||||
0x68U, 0x19U, 0xD4U, 0x26U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC69[] = {
|
||||
0x69U, 0xE0U, 0xEBU, 0x7BU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC6A[] = {
|
||||
0x6AU, 0x00U, 0x00U, 0x02U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U};
|
||||
|
||||
const unsigned char REC6B[] = {
|
||||
0x6BU, 0x02U, 0x02U, 0x0CU, 0x0BU, 0x12U, 0x64U, 0x00U, 0x00U, 0x80U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U};
|
||||
|
||||
const unsigned char REC6C[] = {
|
||||
0x6CU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC6D[] = {
|
||||
0x6DU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC6E[] = {
|
||||
0x6EU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC6F[] = {
|
||||
0x6FU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC70[] = {
|
||||
0x70U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC71[] = {
|
||||
0x71U, 0xACU, 0xB8U, 0xA4U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC72[] = {
|
||||
0x72U, 0x9BU, 0xDCU, 0x75U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U};
|
||||
|
||||
const unsigned char REC73[] = {
|
||||
0x73U, 0x00U, 0x00U, 0x02U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U};
|
||||
|
||||
const unsigned char REC80[] = {
|
||||
0x80U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U};
|
||||
|
||||
#define USRP_FRAME_PER 15U
|
||||
#define P25_FRAME_PER 15U
|
||||
|
||||
const char* DEFAULT_INI_FILE = "/etc/USRP2P25.ini";
|
||||
|
||||
const char* HEADER1 = "This software is for use on amateur radio networks only,";
|
||||
const char* HEADER2 = "it is to be used for educational purposes only. Its use on";
|
||||
const char* HEADER3 = "commercial networks is strictly prohibited.";
|
||||
const char* HEADER4 = "Copyright(C) 2021 by AD8DP";
|
||||
|
||||
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <clocale>
|
||||
#include <cctype>
|
||||
|
||||
static bool m_killed = false;
|
||||
|
||||
void sig_handler(int signo)
|
||||
{
|
||||
if (signo == SIGTERM) {
|
||||
m_killed = true;
|
||||
::fprintf(stdout, "Received SIGTERM\n");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
const char* iniFile = DEFAULT_INI_FILE;
|
||||
if (argc > 1) {
|
||||
for (int currentArg = 1; currentArg < argc; ++currentArg) {
|
||||
std::string arg = argv[currentArg];
|
||||
if ((arg == "-v") || (arg == "--version")) {
|
||||
::fprintf(stdout, "USRP2P25 version %s\n", VERSION);
|
||||
return 0;
|
||||
} else if (arg.substr(0, 1) == "-") {
|
||||
::fprintf(stderr, "Usage: USRP2P25 [-v|--version] [filename]\n");
|
||||
return 1;
|
||||
} else {
|
||||
iniFile = argv[currentArg];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture SIGTERM to finish gracelessly
|
||||
if (signal(SIGTERM, sig_handler) == SIG_ERR)
|
||||
::fprintf(stdout, "Can't catch SIGTERM\n");
|
||||
|
||||
CUSRP2P25* gateway = new CUSRP2P25(std::string(iniFile));
|
||||
|
||||
int ret = gateway->run();
|
||||
|
||||
delete gateway;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
CUSRP2P25::CUSRP2P25(const std::string& configFile) :
|
||||
m_callsign(),
|
||||
m_conf(configFile),
|
||||
m_usrpNetwork(NULL),
|
||||
m_p25Network(NULL),
|
||||
m_conv(),
|
||||
m_p25Src(1U),
|
||||
m_p25Dst(1U),
|
||||
m_p25Frame(NULL),
|
||||
m_p25Frames(0U),
|
||||
m_p25info(false),
|
||||
m_usrpFrame(NULL),
|
||||
m_usrpFrames(0U)
|
||||
{
|
||||
m_p25Frame = new uint8_t[100U];
|
||||
m_usrpFrame = new uint8_t[400U];
|
||||
|
||||
::memset(m_p25Frame, 0U, 100U);
|
||||
::memset(m_usrpFrame, 0U, 400U);
|
||||
}
|
||||
|
||||
CUSRP2P25::~CUSRP2P25()
|
||||
{
|
||||
delete[] m_p25Frame;
|
||||
delete[] m_usrpFrame;
|
||||
}
|
||||
|
||||
int CUSRP2P25::run()
|
||||
{
|
||||
bool ret = m_conf.read();
|
||||
if (!ret) {
|
||||
::fprintf(stderr, "USRP2P25: cannot read the .ini file\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
//setlocale(LC_ALL, "C");
|
||||
|
||||
uint32_t logDisplayLevel = m_conf.getLogDisplayLevel();
|
||||
|
||||
if(m_conf.getDaemon())
|
||||
logDisplayLevel = 0U;
|
||||
|
||||
bool m_daemon = m_conf.getDaemon();
|
||||
if (m_daemon) {
|
||||
// Create new process
|
||||
pid_t pid = ::fork();
|
||||
if (pid == -1) {
|
||||
::fprintf(stderr, "Couldn't fork() , exiting\n");
|
||||
return -1;
|
||||
} else if (pid != 0)
|
||||
exit(EXIT_SUCCESS);
|
||||
|
||||
// Create new session and process group
|
||||
if (::setsid() == -1) {
|
||||
::fprintf(stderr, "Couldn't setsid(), exiting\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Set the working directory to the root directory
|
||||
if (::chdir("/") == -1) {
|
||||
::fprintf(stderr, "Couldn't cd /, exiting\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If we are currently root...
|
||||
if (getuid() == 0) {
|
||||
struct passwd* user = ::getpwnam("mmdvm");
|
||||
if (user == NULL) {
|
||||
::fprintf(stderr, "Could not get the mmdvm user, exiting\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
uid_t mmdvm_uid = user->pw_uid;
|
||||
gid_t mmdvm_gid = user->pw_gid;
|
||||
|
||||
// Set user and group ID's to mmdvm:mmdvm
|
||||
if (setgid(mmdvm_gid) != 0) {
|
||||
::fprintf(stderr, "Could not set mmdvm GID, exiting\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (setuid(mmdvm_uid) != 0) {
|
||||
::fprintf(stderr, "Could not set mmdvm UID, exiting\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Double check it worked (AKA Paranoia)
|
||||
if (setuid(0) != -1) {
|
||||
::fprintf(stderr, "It's possible to regain root - something is wrong!, exiting\n");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ret = ::LogInitialise(m_conf.getLogFilePath(), m_conf.getLogFileRoot(), m_conf.getLogFileLevel(), logDisplayLevel);
|
||||
if (!ret) {
|
||||
::fprintf(stderr, "USRP2P25: unable to open the log file\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (m_daemon) {
|
||||
::close(STDIN_FILENO);
|
||||
::close(STDOUT_FILENO);
|
||||
::close(STDERR_FILENO);
|
||||
}
|
||||
|
||||
LogInfo(HEADER1);
|
||||
LogInfo(HEADER2);
|
||||
LogInfo(HEADER3);
|
||||
LogInfo(HEADER4);
|
||||
|
||||
m_callsign = m_conf.getCallsign();
|
||||
|
||||
std::string p25_dstAddress = m_conf.getP25DstAddress();
|
||||
unsigned int p25_dstPort = m_conf.getP25DstPort();
|
||||
std::string p25_localAddress = m_conf.getP25LocalAddress();
|
||||
unsigned int p25_localPort = m_conf.getP25LocalPort();
|
||||
bool p25_debug = m_conf.getP25NetworkDebug();
|
||||
::fprintf(stderr, "%s : %s\n", p25_dstAddress.c_str(), p25_localAddress.c_str());
|
||||
|
||||
m_p25Network = new CP25Network(p25_localAddress, p25_localPort, p25_dstAddress, p25_dstPort, m_callsign, p25_debug);
|
||||
|
||||
ret = m_p25Network->open();
|
||||
if (!ret) {
|
||||
::LogError("Cannot open the P25 network port");
|
||||
::LogFinalise();
|
||||
return 1;
|
||||
}
|
||||
m_p25Network->writePoll();
|
||||
|
||||
std::string usrp_address = m_conf.getUSRPAddress();
|
||||
uint16_t usrp_dstPort = m_conf.getUSRPDstPort();
|
||||
uint16_t usrp_localPort = m_conf.getUSRPLocalPort();
|
||||
bool usrp_debug = m_conf.getUSRPDebug();
|
||||
|
||||
m_conv.setUSRPGainAdjDb(m_conf.getUSRPGainAdjDb());
|
||||
|
||||
m_usrpNetwork = new CUSRPNetwork(usrp_address, usrp_dstPort, usrp_localPort, usrp_debug);
|
||||
|
||||
ret = m_usrpNetwork->open();
|
||||
if (!ret) {
|
||||
::LogError("Cannot open the USRP network port");
|
||||
::LogFinalise();
|
||||
return 1;
|
||||
}
|
||||
|
||||
CTimer networkWatchdog(100U, 0U, 1500U);
|
||||
CTimer pollTimer(1000U, 8U);
|
||||
CStopWatch stopWatch;
|
||||
CStopWatch p25Watch;
|
||||
CStopWatch usrpWatch;
|
||||
|
||||
pollTimer.start();
|
||||
stopWatch.start();
|
||||
p25Watch.start();
|
||||
usrpWatch.start();
|
||||
|
||||
uint16_t p25_cnt = 0;
|
||||
uint32_t usrp_cnt = 0;
|
||||
|
||||
|
||||
LogMessage("Starting USRP2P25-%s", VERSION);
|
||||
|
||||
for (; m_killed == 0;) {
|
||||
uint8_t buffer[2000U];
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
|
||||
uint32_t ms = stopWatch.elapsed();
|
||||
|
||||
if (p25Watch.elapsed() > P25_FRAME_PER) {
|
||||
unsigned int p25FrameType = m_conv.getP25(m_p25Frame);
|
||||
|
||||
if(p25FrameType == TAG_HEADER) {
|
||||
p25_cnt = 0U;
|
||||
p25Watch.start();
|
||||
}
|
||||
else if(p25FrameType == TAG_EOT) {
|
||||
m_p25Network->writeData(REC80, 17U);
|
||||
p25Watch.start();
|
||||
}
|
||||
else if(p25FrameType == TAG_DATA) {
|
||||
unsigned int p25step = p25_cnt % 18U;
|
||||
//CUtils::dump(1U, "P25 Data", m_p25Frame, 11U);
|
||||
|
||||
if (p25_cnt > 2U) {
|
||||
switch (p25step) {
|
||||
case 0x00U:
|
||||
::memcpy(buffer, REC62, 22U);
|
||||
::memcpy(buffer + 10U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 22U);
|
||||
break;
|
||||
case 0x01U:
|
||||
::memcpy(buffer, REC63, 14U);
|
||||
::memcpy(buffer + 1U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 14U);
|
||||
break;
|
||||
case 0x02U:
|
||||
::memcpy(buffer, REC64, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
buffer[1U] = 0x00U;
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x03U:
|
||||
::memcpy(buffer, REC65, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
buffer[1U] = (m_p25Dst >> 16) & 0xFFU;
|
||||
buffer[2U] = (m_p25Dst >> 8) & 0xFFU;
|
||||
buffer[3U] = (m_p25Dst >> 0) & 0xFFU;
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x04U:
|
||||
::memcpy(buffer, REC66, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
buffer[1U] = (m_p25Src >> 16) & 0xFFU;
|
||||
buffer[2U] = (m_p25Src >> 8) & 0xFFU;
|
||||
buffer[3U] = (m_p25Src >> 0) & 0xFFU;
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x05U:
|
||||
::memcpy(buffer, REC67, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x06U:
|
||||
::memcpy(buffer, REC68, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x07U:
|
||||
::memcpy(buffer, REC69, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x08U:
|
||||
::memcpy(buffer, REC6A, 16U);
|
||||
::memcpy(buffer + 4U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 16U);
|
||||
break;
|
||||
case 0x09U:
|
||||
::memcpy(buffer, REC6B, 22U);
|
||||
::memcpy(buffer + 10U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 22U);
|
||||
break;
|
||||
case 0x0AU:
|
||||
::memcpy(buffer, REC6C, 14U);
|
||||
::memcpy(buffer + 1U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 14U);
|
||||
break;
|
||||
case 0x0BU:
|
||||
::memcpy(buffer, REC6D, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x0CU:
|
||||
::memcpy(buffer, REC6E, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x0DU:
|
||||
::memcpy(buffer, REC6F, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x0EU:
|
||||
::memcpy(buffer, REC70, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
buffer[1U] = 0x80U;
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x0FU:
|
||||
::memcpy(buffer, REC71, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x10U:
|
||||
::memcpy(buffer, REC72, 17U);
|
||||
::memcpy(buffer + 5U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 17U);
|
||||
break;
|
||||
case 0x11U:
|
||||
::memcpy(buffer, REC73, 16U);
|
||||
::memcpy(buffer + 4U, m_p25Frame, 11U);
|
||||
m_p25Network->writeData(buffer, 16U);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
p25_cnt++;
|
||||
p25Watch.start();
|
||||
}
|
||||
}
|
||||
|
||||
while (m_p25Network->readData(m_p25Frame, 22U) > 0U) {
|
||||
//CUtils::dump(1U, "P25 Data", m_p25Frame, 22U);
|
||||
if (m_p25Frame[0U] != 0xF0U && m_p25Frame[0U] != 0xF1U) {
|
||||
if (m_p25Frame[0U] == 0x62U && !m_p25info) {
|
||||
m_p25Frames = 0;
|
||||
//m_conv.putP25Header();
|
||||
} else if (m_p25Frame[0U] == 0x65U && !m_p25info) {
|
||||
m_p25Dst = (m_p25Frame[1U] << 16) & 0xFF0000U;
|
||||
m_p25Dst |= (m_p25Frame[2U] << 8) & 0x00FF00U;
|
||||
m_p25Dst |= (m_p25Frame[3U] << 0) & 0x0000FFU;
|
||||
} else if (m_p25Frame[0U] == 0x66U && !m_p25info) {
|
||||
m_p25Src = (m_p25Frame[1U] << 16) & 0xFF0000U;
|
||||
m_p25Src |= (m_p25Frame[2U] << 8) & 0x00FF00U;
|
||||
m_p25Src |= (m_p25Frame[3U] << 0) & 0x0000FFU;
|
||||
LogMessage("Received P25 audio: Src: %d Dst: %d", m_p25Src, m_p25Dst);
|
||||
m_p25info = true;
|
||||
} else if (m_p25Frame[0U] == 0x80U) {
|
||||
LogMessage("P25 received end of voice transmission, %.1f seconds", float(m_p25Frames) / 50.0F);
|
||||
m_p25info = false;
|
||||
//m_conv.putP25EOT();
|
||||
}
|
||||
m_conv.putP25(m_p25Frame);
|
||||
m_p25Frames++;
|
||||
}
|
||||
}
|
||||
|
||||
if (usrpWatch.elapsed() > USRP_FRAME_PER) {
|
||||
int16_t pcm[160];
|
||||
if(m_conv.getUSRP(pcm)){
|
||||
//CUtils::dump(1U, "USRP data:", m_usrpFrame, 33U);
|
||||
const uint32_t cnt = htonl(usrp_cnt);
|
||||
memcpy(m_usrpFrame, "USRP", 4);
|
||||
memset(m_usrpFrame+4, 0, 28);
|
||||
memcpy(m_usrpFrame+4, &cnt, 4);
|
||||
m_usrpFrame[15] = 1;
|
||||
|
||||
for(int i = 0; i < 320; i+=2){
|
||||
m_usrpFrame[32+i] = pcm[(i/2)] & 0xff;
|
||||
m_usrpFrame[32+i+1] = pcm[(i/2)] >> 8;
|
||||
}
|
||||
|
||||
m_usrpNetwork->writeData(m_usrpFrame, 352);
|
||||
usrp_cnt++;
|
||||
usrpWatch.start();
|
||||
}
|
||||
}
|
||||
uint32_t len = 0;
|
||||
while ( (len = m_usrpNetwork->readData(m_usrpFrame, 400)) ) {
|
||||
if((len == 32) && !memcmp(m_usrpFrame, "USRP", 4)) {
|
||||
LogMessage("USRP received end of voice transmission, %.1f seconds", float(m_usrpFrames) / 50.0F);
|
||||
m_conv.putUSRPEOT();
|
||||
m_usrpFrames = 0U;
|
||||
}
|
||||
|
||||
if( (!memcmp(m_usrpFrame, "USRP", 4)) && len == 352) {
|
||||
if(!m_usrpFrames){
|
||||
m_conv.putUSRPHeader();
|
||||
LogMessage("USRP first frame received");
|
||||
}
|
||||
int16_t pcm[160];
|
||||
for(int i = 0; i < 160; ++i){
|
||||
pcm[i] = (m_usrpFrame[32+(i*2)+1] << 8) | m_usrpFrame[32+(i*2)];
|
||||
}
|
||||
m_conv.putUSRP(pcm);
|
||||
m_usrpFrames++;
|
||||
}
|
||||
}
|
||||
|
||||
stopWatch.start();
|
||||
pollTimer.clock(ms);
|
||||
|
||||
if (pollTimer.isRunning() && pollTimer.hasExpired()) {
|
||||
m_p25Network->writePoll();
|
||||
pollTimer.start();
|
||||
}
|
||||
|
||||
if (ms < 5U) ::usleep(5U * 1000U);
|
||||
}
|
||||
|
||||
m_p25Network->close();
|
||||
m_usrpNetwork->close();
|
||||
delete m_usrpNetwork;
|
||||
delete m_p25Network;
|
||||
|
||||
::LogFinalise();
|
||||
|
||||
return 0;
|
||||
}
|
58
USRP2P25/USRP2P25.h
Normal file
58
USRP2P25/USRP2P25.h
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2021 by Doug McLain AD8DP
|
||||
*
|
||||
* 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(USRP2P25_H)
|
||||
#define USRP2P25_H
|
||||
|
||||
#include "ModeConv.h"
|
||||
#include "USRPNetwork.h"
|
||||
#include "P25Network.h"
|
||||
#include "UDPSocket.h"
|
||||
#include "StopWatch.h"
|
||||
#include "Version.h"
|
||||
#include "Timer.h"
|
||||
#include "Utils.h"
|
||||
#include "Conf.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
class CUSRP2P25
|
||||
{
|
||||
public:
|
||||
CUSRP2P25(const std::string& configFile);
|
||||
~CUSRP2P25();
|
||||
|
||||
int run();
|
||||
|
||||
private:
|
||||
std::string m_callsign;
|
||||
CConf m_conf;
|
||||
CUSRPNetwork* m_usrpNetwork;
|
||||
CP25Network* m_p25Network;
|
||||
CModeConv m_conv;
|
||||
uint32_t m_p25Src;
|
||||
uint32_t m_p25Dst;
|
||||
uint8_t* m_p25Frame;
|
||||
uint32_t m_p25Frames;
|
||||
bool m_p25info;
|
||||
uint8_t* m_usrpFrame;
|
||||
uint32_t m_usrpFrames;
|
||||
};
|
||||
|
||||
#endif
|
@ -1,11 +1,11 @@
|
||||
[M17 Network]
|
||||
Callsign=AD8DP D
|
||||
Address=51.81.119.111
|
||||
Name=M17-M17 C
|
||||
[P25 Network]
|
||||
Callsign=AD8DP
|
||||
LocalAddress=127.0.0.1
|
||||
LocalPort=32010
|
||||
DstPort=17000
|
||||
GainAdjustdB=3
|
||||
DstAddress=127.0.0.1
|
||||
DstPort=42020
|
||||
Daemon=0
|
||||
GainAdjustdB=3
|
||||
Debug=1
|
||||
|
||||
[USRP Network]
|
||||
@ -20,5 +20,5 @@ Debug=1
|
||||
DisplayLevel=1
|
||||
FileLevel=1
|
||||
FilePath=.
|
||||
FileRoot=USRP2M17
|
||||
FileRoot=USRP2P25
|
||||
|
78
USRP2P25/USRPNetwork.cpp
Normal file
78
USRP2P25/USRPNetwork.cpp
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2015,2016,2017 by Jonathan Naylor G4KLX
|
||||
* Copyright (C) 2021 by Doug McLain AD8DP
|
||||
*
|
||||
* 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 "USRPNetwork.h"
|
||||
#include "StopWatch.h"
|
||||
#include "Utils.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
CUSRPNetwork::CUSRPNetwork(const std::string& address, uint16_t dstPort, uint16_t localPort, bool debug) :
|
||||
m_address(),
|
||||
m_port(dstPort),
|
||||
m_socket(localPort),
|
||||
m_debug(debug)
|
||||
{
|
||||
m_address = CUDPSocket::lookup(address);
|
||||
CStopWatch stopWatch;
|
||||
}
|
||||
|
||||
CUSRPNetwork::~CUSRPNetwork()
|
||||
{
|
||||
}
|
||||
|
||||
bool CUSRPNetwork::open()
|
||||
{
|
||||
LogMessage("USRP Network, Opening");
|
||||
return m_socket.open();
|
||||
}
|
||||
|
||||
void CUSRPNetwork::close()
|
||||
{
|
||||
m_socket.close();
|
||||
}
|
||||
|
||||
uint32_t CUSRPNetwork::readData(uint8_t* data, uint32_t length)
|
||||
{
|
||||
in_addr address;
|
||||
unsigned int port;
|
||||
int len = m_socket.read(data, length, address, port);
|
||||
if (len <= 0)
|
||||
return 0U;
|
||||
|
||||
// Check if the data is for us
|
||||
if (m_address.s_addr != address.s_addr || port != m_port) {
|
||||
LogMessage("USRP packet received from an invalid source, %08X != %08X and/or %u != %u", m_address.s_addr, address.s_addr, m_port, port);
|
||||
return 0U;
|
||||
}
|
||||
|
||||
if (m_debug)
|
||||
CUtils::dump(1U, "USRP Network Data Received", data, len);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
bool CUSRPNetwork::writeData(const uint8_t* data, uint32_t length)
|
||||
{
|
||||
if (m_debug)
|
||||
CUtils::dump(1U, "USRP Network Data Sent", data, length);
|
||||
|
||||
return m_socket.write(data, length, m_address, m_port);
|
||||
}
|
47
USRP2P25/USRPNetwork.h
Normal file
47
USRP2P25/USRPNetwork.h
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2015,2016,2017 by Jonathan Naylor G4KLX
|
||||
* Copyright (C) 2021 by Doug McLain AD8DP
|
||||
*
|
||||
* 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(USRPNetwork_H)
|
||||
#define USRPNetwork_H
|
||||
|
||||
#include "UDPSocket.h"
|
||||
#include "Timer.h"
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
class CUSRPNetwork
|
||||
{
|
||||
public:
|
||||
CUSRPNetwork(const std::string& address, uint16_t dstPort, uint16_t localPort, bool debug);
|
||||
~CUSRPNetwork();
|
||||
|
||||
uint32_t getConfig(uint8_t* config) const;
|
||||
bool open();
|
||||
bool writeData(const uint8_t* data, uint32_t length);
|
||||
uint32_t readData(uint8_t* data, uint32_t length);
|
||||
void close();
|
||||
private:
|
||||
in_addr m_address;
|
||||
uint16_t m_port;
|
||||
CUDPSocket m_socket;
|
||||
bool m_debug;
|
||||
};
|
||||
|
||||
#endif
|
146
USRP2P25/Utils.cpp
Normal file
146
USRP2P25/Utils.cpp
Normal file
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (C) 2009,2014,2015,2016 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 "Utils.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cassert>
|
||||
|
||||
void CUtils::dump(const std::string& title, const unsigned char* data, unsigned int length)
|
||||
{
|
||||
assert(data != NULL);
|
||||
|
||||
dump(2U, title, data, length);
|
||||
}
|
||||
|
||||
void CUtils::dump(int level, const std::string& title, const unsigned char* data, unsigned int length)
|
||||
{
|
||||
assert(data != NULL);
|
||||
|
||||
::Log(level, "%s", title.c_str());
|
||||
|
||||
unsigned int offset = 0U;
|
||||
|
||||
while (length > 0U) {
|
||||
std::string output;
|
||||
|
||||
unsigned int bytes = (length > 16U) ? 16U : length;
|
||||
|
||||
for (unsigned i = 0U; i < bytes; i++) {
|
||||
char temp[10U];
|
||||
::sprintf(temp, "%02X ", data[offset + i]);
|
||||
output += temp;
|
||||
}
|
||||
|
||||
for (unsigned int i = bytes; i < 16U; i++)
|
||||
output += " ";
|
||||
|
||||
output += " *";
|
||||
|
||||
for (unsigned i = 0U; i < bytes; i++) {
|
||||
unsigned char c = data[offset + i];
|
||||
|
||||
if (::isprint(c))
|
||||
output += c;
|
||||
else
|
||||
output += '.';
|
||||
}
|
||||
|
||||
output += '*';
|
||||
|
||||
::Log(level, "%04X: %s", offset, output.c_str());
|
||||
|
||||
offset += 16U;
|
||||
|
||||
if (length >= 16U)
|
||||
length -= 16U;
|
||||
else
|
||||
length = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
void CUtils::dump(const std::string& title, const bool* bits, unsigned int length)
|
||||
{
|
||||
assert(bits != NULL);
|
||||
|
||||
dump(2U, title, bits, length);
|
||||
}
|
||||
|
||||
void CUtils::dump(int level, const std::string& title, const bool* bits, unsigned int length)
|
||||
{
|
||||
assert(bits != NULL);
|
||||
|
||||
unsigned char bytes[100U];
|
||||
unsigned int nBytes = 0U;
|
||||
for (unsigned int n = 0U; n < length; n += 8U, nBytes++)
|
||||
bitsToByteBE(bits + n, bytes[nBytes]);
|
||||
|
||||
dump(level, title, bytes, nBytes);
|
||||
}
|
||||
|
||||
void CUtils::byteToBitsBE(unsigned char byte, bool* bits)
|
||||
{
|
||||
assert(bits != NULL);
|
||||
|
||||
bits[0U] = (byte & 0x80U) == 0x80U;
|
||||
bits[1U] = (byte & 0x40U) == 0x40U;
|
||||
bits[2U] = (byte & 0x20U) == 0x20U;
|
||||
bits[3U] = (byte & 0x10U) == 0x10U;
|
||||
bits[4U] = (byte & 0x08U) == 0x08U;
|
||||
bits[5U] = (byte & 0x04U) == 0x04U;
|
||||
bits[6U] = (byte & 0x02U) == 0x02U;
|
||||
bits[7U] = (byte & 0x01U) == 0x01U;
|
||||
}
|
||||
|
||||
void CUtils::byteToBitsLE(unsigned char byte, bool* bits)
|
||||
{
|
||||
assert(bits != NULL);
|
||||
|
||||
bits[0U] = (byte & 0x01U) == 0x01U;
|
||||
bits[1U] = (byte & 0x02U) == 0x02U;
|
||||
bits[2U] = (byte & 0x04U) == 0x04U;
|
||||
bits[3U] = (byte & 0x08U) == 0x08U;
|
||||
bits[4U] = (byte & 0x10U) == 0x10U;
|
||||
bits[5U] = (byte & 0x20U) == 0x20U;
|
||||
bits[6U] = (byte & 0x40U) == 0x40U;
|
||||
bits[7U] = (byte & 0x80U) == 0x80U;
|
||||
}
|
||||
|
||||
void CUtils::bitsToByteBE(const bool* bits, unsigned char& byte)
|
||||
{
|
||||
assert(bits != NULL);
|
||||
|
||||
byte = bits[0U] ? 0x80U : 0x00U;
|
||||
byte |= bits[1U] ? 0x40U : 0x00U;
|
||||
byte |= bits[2U] ? 0x20U : 0x00U;
|
||||
byte |= bits[3U] ? 0x10U : 0x00U;
|
||||
byte |= bits[4U] ? 0x08U : 0x00U;
|
||||
byte |= bits[5U] ? 0x04U : 0x00U;
|
||||
byte |= bits[6U] ? 0x02U : 0x00U;
|
||||
byte |= bits[7U] ? 0x01U : 0x00U;
|
||||
}
|
||||
|
||||
void CUtils::bitsToByteLE(const bool* bits, unsigned char& byte)
|
||||
{
|
||||
assert(bits != NULL);
|
||||
|
||||
byte = bits[0U] ? 0x01U : 0x00U;
|
||||
byte |= bits[1U] ? 0x02U : 0x00U;
|
||||
byte |= bits[2U] ? 0x04U : 0x00U;
|
||||
byte |= bits[3U] ? 0x08U : 0x00U;
|
||||
byte |= bits[4U] ? 0x10U : 0x00U;
|
||||
byte |= bits[5U] ? 0x20U : 0x00U;
|
||||
byte |= bits[6U] ? 0x40U : 0x00U;
|
||||
byte |= bits[7U] ? 0x80U : 0x00U;
|
||||
}
|
36
USRP2P25/Utils.h
Normal file
36
USRP2P25/Utils.h
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2009,2014,2015 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; 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.
|
||||
*/
|
||||
|
||||
#ifndef Utils_H
|
||||
#define Utils_H
|
||||
|
||||
#include <string>
|
||||
|
||||
class CUtils {
|
||||
public:
|
||||
static void dump(const std::string& title, const unsigned char* data, unsigned int length);
|
||||
static void dump(int level, const std::string& title, const unsigned char* data, unsigned int length);
|
||||
|
||||
static void dump(const std::string& title, const bool* bits, unsigned int length);
|
||||
static void dump(int level, const std::string& title, const bool* bits, unsigned int length);
|
||||
|
||||
static void byteToBitsBE(unsigned char byte, bool* bits);
|
||||
static void byteToBitsLE(unsigned char byte, bool* bits);
|
||||
|
||||
static void bitsToByteBE(const bool* bits, unsigned char& byte);
|
||||
static void bitsToByteLE(const bool* bits, unsigned char& byte);
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
#endif
|
25
USRP2P25/Version.h
Normal file
25
USRP2P25/Version.h
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2015,2016,2017 by Jonathan Naylor G4KLX
|
||||
* Copyright (C) 2018 by Andy Uribe CA6JAU
|
||||
*
|
||||
* 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(VERSION_H)
|
||||
#define VERSION_H
|
||||
|
||||
const char* VERSION = "20210406";
|
||||
|
||||
#endif
|
Loading…
Reference in New Issue
Block a user