diff --git a/APRSWriter.cpp b/APRSWriter.cpp new file mode 100644 index 0000000..1a28a52 --- /dev/null +++ b/APRSWriter.cpp @@ -0,0 +1,599 @@ +/* + * Copyright (C) 2010,2011,2012,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2021 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 +#include +#include + +#include "StringUtils.h" +#include "Log.h" +#include "APRSWriter.h" +#include "DStarDefines.h" +#include "Defs.h" + +CAPRSEntry::CAPRSEntry(const std::string& callsign, const std::string& band, double frequency, double offset, double range, double latitude, double longitude, double agl) : +m_callsign(callsign), +m_band(band), +m_frequency(frequency), +m_offset(offset), +m_range(range), +m_latitude(latitude), +m_longitude(longitude), +m_agl(agl), +m_timer(1000U, 10U), +m_first(true), +m_collector(NULL) +{ + boost::trim(m_callsign); + + m_collector = new CAPRSCollector; +} + +CAPRSEntry::~CAPRSEntry() +{ + delete m_collector; +} + +std::string CAPRSEntry::getCallsign() const +{ + return m_callsign; +} + +std::string CAPRSEntry::getBand() const +{ + return m_band; +} + +double CAPRSEntry::getFrequency() const +{ + return m_frequency; +} + +double CAPRSEntry::getOffset() const +{ + return m_offset; +} + +double CAPRSEntry::getRange() const +{ + return m_range; +} + +double CAPRSEntry::getLatitude() const +{ + return m_latitude; +} + +double CAPRSEntry::getLongitude() const +{ + return m_longitude; +} + +double CAPRSEntry::getAGL() const +{ + return m_agl; +} + +CAPRSCollector* CAPRSEntry::getCollector() const +{ + return m_collector; +} + +void CAPRSEntry::reset() +{ + m_first = true; + m_timer.stop(); + m_collector->reset(); +} + +void CAPRSEntry::clock(unsigned int ms) +{ + m_timer.clock(ms); +} + +bool CAPRSEntry::isOK() +{ + if (m_first) { + m_first = false; + m_timer.start(); + return true; + } + + if (m_timer.hasExpired()) { + m_timer.start(); + return true; + } else { + m_timer.start(); + return false; + } +} + +CAPRSWriter::CAPRSWriter(const std::string& hostname, unsigned int port, const std::string& gateway, const std::string& password, const std::string& address) : +m_thread(NULL), +m_idTimer(1000U), +m_gateway(), +m_address(), +m_port(0U), +m_socket(NULL), +m_array() +{ + assert(!hostname.empty()); + assert(port > 0U); + assert(!gateway.empty()); + assert(!password.empty()); + + m_thread = new CAPRSWriterThread(gateway, password, address, hostname, port); + + m_gateway = gateway; + m_gateway = m_gateway.substr(0, LONG_CALLSIGN_LENGTH - 1U); + boost::trim(m_gateway); +} + +CAPRSWriter::~CAPRSWriter() +{ + for(auto it = m_array.begin(); it != m_array.end(); it++) { + delete it->second; + } + + m_array.clear(); +} + +void CAPRSWriter::setPortFixed(const std::string& callsign, const std::string& band, double frequency, double offset, double range, double latitude, double longitude, double agl) +{ + std::string temp = callsign; + temp.resize(LONG_CALLSIGN_LENGTH - 1U, ' '); + temp += band; + + m_array[temp] = new CAPRSEntry(callsign, band, frequency, offset, range, latitude, longitude, agl); +} + +void CAPRSWriter::setPortMobile(const std::string& callsign, const std::string& band, double frequency, double offset, double range, const std::string& address, unsigned int port) +{ + std::string temp = callsign; + temp.resize(LONG_CALLSIGN_LENGTH - 1U, ' '); + temp += band; + + m_array[temp] = new CAPRSEntry(callsign, band, frequency, offset, range, 0.0, 0.0, 0.0); + + if (m_socket == NULL) { + m_address = CUDPReaderWriter::lookup(address); + m_port = port; + + m_socket = new CUDPReaderWriter(); + } +} + +bool CAPRSWriter::open() +{ + if (m_socket != NULL) { + bool ret = m_socket->open(); + if (!ret) { + delete m_socket; + m_socket = NULL; + return false; + } + + // Poll the GPS every minute + m_idTimer.setTimeout(60U); + } else { + m_idTimer.setTimeout(20U * 60U); + } + + m_idTimer.start(); + + return m_thread->start(); +} + +void CAPRSWriter::writeHeader(const std::string& callsign, const CHeaderData& header) +{ + CAPRSEntry* entry = m_array[callsign]; + if (entry == NULL) { + wxLogError("Cannot find the callsign \"%s\" in the APRS array", callsign.c_str()); + return; + } + + entry->reset(); + + CAPRSCollector* collector = entry->getCollector(); + + collector->writeHeader(header.getMyCall1()); +} + +void CAPRSWriter::writeData(const std::string& callsign, const CAMBEData& data) +{ + if (data.isEnd()) + return; + + CAPRSEntry* entry = m_array[callsign]; + if (entry == NULL) { + wxLogError("Cannot find the callsign \"%s\" in the APRS array", callsign.c_str()); + return; + } + + CAPRSCollector* collector = entry->getCollector(); + + if (data.isSync()) { + collector->sync(); + return; + } + + unsigned char buffer[400U]; + data.getData(buffer, DV_FRAME_MAX_LENGTH_BYTES); + + bool complete = collector->writeData(buffer + VOICE_FRAME_LENGTH_BYTES); + if (!complete) + return; + + if (!m_thread->isConnected()) { + collector->reset(); + return; + } + + // Check the transmission timer + bool ok = entry->isOK(); + if (!ok) { + collector->reset(); + return; + } + + unsigned int length = collector->getData(buffer, 400U); + std::string text((char*)buffer, length); + + auto n = text.find(':'); + if (n == std::string::npos) { + collector->reset(); + return; + } + + std::string header = text.substr(0, n); + std::string body = text.substr(n + 1U); + + // If we already have a q-construct, don't send it on + n = header.find('q'); + if (n != std::string::npos) + return; + + // Remove the trailing \r + n = body.find('\r'); + if (n != std::string::npos) + body = body.substr(0, n); + + std::string output = string_format(wxT("%s,qAR,%s-%s:%s"), header.c_str(), entry->getCallsign().c_str(), entry->getBand().c_str(), body.c_str()); + + char ascii[500U]; + ::memset(ascii, 0x00, 500U); + for (unsigned int i = 0U; i < output.length(); i++) + ascii[i] = output[i]; + + m_thread->write(ascii); + + collector->reset(); +} + +void CAPRSWriter::clock(unsigned int ms) +{ + m_idTimer.clock(ms); + + m_thread->clock(ms); + + if (m_socket != NULL) { + if (m_idTimer.hasExpired()) { + pollGPS(); + m_idTimer.start(); + } + + sendIdFramesMobile(); + } else { + if (m_idTimer.hasExpired()) { + sendIdFramesFixed(); + m_idTimer.start(); + } + } + + for (auto it = m_array.begin(); it != m_array.end(); ++it) + it->second->clock(ms); +} + +bool CAPRSWriter::isConnected() const +{ + return m_thread->isConnected(); +} + +void CAPRSWriter::close() +{ + if (m_socket != NULL) { + m_socket->close(); + delete m_socket; + } + + m_thread->stop(); +} + +bool CAPRSWriter::pollGPS() +{ + assert(m_socket != NULL); + + return m_socket->write((unsigned char*)"ircDDBGateway", 13U, m_address, m_port); +} + +void CAPRSWriter::sendIdFramesFixed() +{ + if (!m_thread->isConnected()) + return; + + time_t now; + ::time(&now); + struct tm* tm = ::gmtime(&now); + + for (auto it = m_array.begin(); it != m_array.end(); ++it) { + CAPRSEntry* entry = it->second; + if (entry == NULL) + continue; + + // Default values aren't passed on + if (entry->getLatitude() == 0.0 && entry->getLongitude() == 0.0) + continue; + + std::string desc; + if (entry->getBand().length() > 1U) { + if (entry->getFrequency() != 0.0) + desc = string_format("Data %.5lfMHz", entry->getFrequency()); + else + desc = "Data"; + } else { + if (entry->getFrequency() != 0.0) + desc = string_format(wxT("Voice %.5lfMHz %c%.4lfMHz"), + entry->getFrequency(), + entry->getOffset() < 0.0 ? '-' : '+', + ::fabs(entry->getOffset())); + else + desc = wxT("Voice"); + } + + std::string band; + if (entry->getFrequency() >= 1200.0) + band = wxT("1.2"); + else if (entry->getFrequency() >= 420.0) + band = wxT("440"); + else if (entry->getFrequency() >= 144.0) + band = wxT("2m"); + else if (entry->getFrequency() >= 50.0) + band = wxT("6m"); + else if (entry->getFrequency() >= 28.0) + band = wxT("10m"); + + double tempLat = ::fabs(entry->getLatitude()); + double tempLong = ::fabs(entry->getLongitude()); + + double latitude = ::floor(tempLat); + double longitude = ::floor(tempLong); + + latitude = (tempLat - latitude) * 60.0 + latitude * 100.0; + longitude = (tempLong - longitude) * 60.0 + longitude * 100.0; + + std::string lat; + if (latitude >= 1000.0F) + lat = string_format(wxT("%.2lf"), latitude); + else if (latitude >= 100.0F) + lat = string_format(wxT("0%.2lf"), latitude); + else if (latitude >= 10.0F) + lat = string_format(wxT("00%.2lf"), latitude); + else + lat = string_format(wxT("000%.2lf"), latitude); + + std::string lon; + if (longitude >= 10000.0F) + lon = string_format(wxT("%.2lf"), longitude); + else if (longitude >= 1000.0F) + lon = string_format(wxT("0%.2lf"), longitude); + else if (longitude >= 100.0F) + lon = string_format(wxT("00%.2lf"), longitude); + else if (longitude >= 10.0F) + lon = string_format(wxT("000%.2lf"), longitude); + else + lon = string_format(wxT("0000%.2lf"), longitude); + + // Convert commas to periods in the latitude and longitude + boost::replace_all(lat, wxT(","), wxT(".")); + boost::replace_all(lon, ",", wxT(".")); + + std::string output; + output = string_format(wxT("%s-S>APDG01,TCPIP*,qAC,%s-GS:;%-7s%-2s*%02d%02d%02dz%s%cD%s%caRNG%04.0lf/A=%06.0lf %s %s"), + m_gateway.c_str(), m_gateway.c_str(), entry->getCallsign().c_str(), entry->getBand().c_str(), + tm->tm_mday, tm->tm_hour, tm->tm_min, + lat.c_str(), (entry->getLatitude() < 0.0F) ? 'S' : 'N', + lon.c_str(), (entry->getLongitude() < 0.0F) ? 'W' : 'E', + entry->getRange() * 0.6214, entry->getAGL() * 3.28, band.c_str(), desc.c_str()); + + char ascii[300U]; + ::memset(ascii, 0x00, 300U); + for (unsigned int i = 0U; i < output.length(); i++) + ascii[i] = output[i]; + + m_thread->write(ascii); + + if (entry->getBand().length() == 1U) { + output = string_format(wxT("%s-%s>APDG02,TCPIP*,qAC,%s-%sS:!%s%cD%s%c&RNG%04.0lf/A=%06.0lf %s %s"), + entry->getCallsign().c_str(), entry->getBand().c_str(), entry->getCallsign().c_str(), entry->getBand().c_str(), + lat.c_str(), (entry->getLatitude() < 0.0F) ? 'S' : 'N', + lon.c_str(), (entry->getLongitude() < 0.0F) ? 'W' : 'E', + entry->getRange() * 0.6214, entry->getAGL() * 3.28, band.c_str(), desc.c_str()); + + ::memset(ascii, 0x00, 300U); + for (unsigned int i = 0U; i < output.length(); i++) + ascii[i] = output[i]; + + m_thread->write(ascii); + } + } +} + +void CAPRSWriter::sendIdFramesMobile() +{ + // Grab GPS data if it's available + unsigned char buffer[200U]; + in_addr address; + unsigned int port; + int ret = m_socket->read(buffer, 200U, address, port); + if (ret <= 0) + return; + + if (!m_thread->isConnected()) + return; + + buffer[ret] = '\0'; + + // Parse the GPS data + char* pLatitude = ::strtok((char*)buffer, ",\n"); // Latitude + char* pLongitude = ::strtok(NULL, ",\n"); // Longitude + char* pAltitude = ::strtok(NULL, ",\n"); // Altitude (m) + char* pVelocity = ::strtok(NULL, ",\n"); // Velocity (kms/h) + char* pBearing = ::strtok(NULL, "\n"); // Bearing + + if (pLatitude == NULL || pLongitude == NULL || pAltitude == NULL) + return; + + double rawLatitude = ::atof(pLatitude); + double rawLongitude = ::atof(pLongitude); + double rawAltitude = ::atof(pAltitude); + + time_t now; + ::time(&now); + struct tm* tm = ::gmtime(&now); + + for (auto it = m_array.begin(); it != m_array.end(); ++it) { + CAPRSEntry* entry = it->second; + if (entry == NULL) + continue; + + std::string desc; + if (entry->getBand().length() > 1U) { + if (entry->getFrequency() != 0.0) + desc = string_format(wxT("Data %.5lfMHz"), entry->getFrequency()); + else + desc = wxT("Data"); + } else { + if (entry->getFrequency() != 0.0) + desc = string_format(wxT("Voice %.5lfMHz %c%.4lfMHz"), + entry->getFrequency(), + entry->getOffset() < 0.0 ? '-' : '+', + ::fabs(entry->getOffset())); + else + desc = wxT("Voice"); + } + + std::string band; + if (entry->getFrequency() >= 1200.0) + band = wxT("1.2"); + else if (entry->getFrequency() >= 420.0) + band = wxT("440"); + else if (entry->getFrequency() >= 144.0) + band = wxT("2m"); + else if (entry->getFrequency() >= 50.0) + band = wxT("6m"); + else if (entry->getFrequency() >= 28.0) + band = wxT("10m"); + + double tempLat = ::fabs(rawLatitude); + double tempLong = ::fabs(rawLongitude); + + double latitude = ::floor(tempLat); + double longitude = ::floor(tempLong); + + latitude = (tempLat - latitude) * 60.0 + latitude * 100.0; + longitude = (tempLong - longitude) * 60.0 + longitude * 100.0; + + std::string lat; + if (latitude >= 1000.0F) + lat = string_format(wxT("%.2lf"), latitude); + else if (latitude >= 100.0F) + lat = string_format(wxT("0%.2lf"), latitude); + else if (latitude >= 10.0F) + lat = string_format(wxT("00%.2lf"), latitude); + else + lat = string_format(wxT("000%.2lf"), latitude); + + std::string lon; + if (longitude >= 10000.0F) + lon = string_format(wxT("%.2lf"), longitude); + else if (longitude >= 1000.0F) + lon = string_format(wxT("0%.2lf"), longitude); + else if (longitude >= 100.0F) + lon = string_format(wxT("00%.2lf"), longitude); + else if (longitude >= 10.0F) + lon = string_format(wxT("000%.2lf"), longitude); + else + lon = string_format(wxT("0000%.2lf"), longitude); + + // Convert commas to periods in the latitude and longitude + boost::replace_all(lat, ",", "."); + boost::replace_all(lon, ",", "."); + + std::string output1; + output1 = string_format(wxT("%s-S>APDG01,TCPIP*,qAC,%s-GS:;%-7s%-2s*%02d%02d%02dz%s%cD%s%ca/A=%06.0lf"), + m_gateway.c_str(), m_gateway.c_str(), entry->getCallsign().c_str(), entry->getBand().c_str(), + tm->tm_mday, tm->tm_hour, tm->tm_min, + lat.c_str(), (rawLatitude < 0.0) ? 'S' : 'N', + lon.c_str(), (rawLongitude < 0.0) ? 'W' : 'E', + rawAltitude * 3.28); + + std::string output2; + if (pBearing != NULL && pVelocity != NULL) { + double rawBearing = ::atof(pBearing); + double rawVelocity = ::atof(pVelocity); + + output2 = string_format(wxT("%03.0lf/%03.0lf"), rawBearing, rawVelocity * 0.539957F); + } + + std::string output3; + output3 = string_format(wxT("RNG%04.0lf %s %s"), entry->getRange() * 0.6214, band.c_str(), desc.c_str()); + + char ascii[300U]; + ::memset(ascii, 0x00, 300U); + unsigned int n = 0U; + for (unsigned int i = 0U; i < output1.length(); i++, n++) + ascii[n] = output1[i]; + for (unsigned int i = 0U; i < output2.length(); i++, n++) + ascii[n] = output2[i]; + for (unsigned int i = 0U; i < output3.length(); i++, n++) + ascii[n] = output3[i]; + + m_thread->write(ascii); + + if (entry->getBand().length() == 1U) { + output1 = string_format(wxT("%s-%s>APDG02,TCPIP*,qAC,%s-%sS:!%s%cD%s%c&/A=%06.0lf"), + entry->getCallsign().c_str(), entry->getBand().c_str(), entry->getCallsign().c_str(), entry->getBand().c_str(), + lat.c_str(), (rawLatitude < 0.0) ? 'S' : 'N', + lon.c_str(), (rawLongitude < 0.0) ? 'W' : 'E', + rawAltitude * 3.28); + + ::memset(ascii, 0x00, 300U); + unsigned int n = 0U; + for (unsigned int i = 0U; i < output1.length(); i++, n++) + ascii[n] = output1[i]; + for (unsigned int i = 0U; i < output2.length(); i++, n++) + ascii[n] = output2[i]; + for (unsigned int i = 0U; i < output3.length(); i++, n++) + ascii[n] = output3[i]; + + m_thread->write(ascii); + } + } +} + diff --git a/APRSWriter.h b/APRSWriter.h new file mode 100644 index 0000000..6deb4f3 --- /dev/null +++ b/APRSWriter.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2010,2011,2012,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2021 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. + */ + +#ifndef APRSWriter_H +#define APRSWriter_H + +#include + +#include "APRSWriterThread.h" +#include "UDPReaderWriter.h" +#include "APRSCollector.h" +#include "DStarDefines.h" +#include "HeaderData.h" +#include "AMBEData.h" +#include "Timer.h" +#include "Defs.h" + +class CAPRSEntry { +public: + CAPRSEntry(const std::string& callsign, const std::string& band, double frequency, double offset, double range, double latitude, double longitude, double agl); + ~CAPRSEntry(); + + std::string getCallsign() const; + std::string getBand() const; + double getFrequency() const; + double getOffset() const; + double getRange() const; + double getLatitude() const; + double getLongitude() const; + double getAGL() const; + CAPRSCollector* getCollector() const; + + // Transmission timer + void reset(); + void clock(unsigned int ms); + bool isOK(); + +private: + std::string m_callsign; + std::string m_band; + double m_frequency; + double m_offset; + double m_range; + double m_latitude; + double m_longitude; + double m_agl; + CTimer m_timer; + bool m_first; + CAPRSCollector* m_collector; +}; + +class CAPRSWriter { +public: + CAPRSWriter(const std::string& hostname, unsigned int port, const std::string& gateway, const std::string& password, const std::string& address); + ~CAPRSWriter(); + + bool open(); + + void setPortFixed(const std::string& callsign, const std::string& band, double frequency, double offset, double range, double latitude, double longitude, double agl); + + void setPortMobile(const std::string& callsign, const std::string& band, double frequency, double offset, double range, const std::string& address, unsigned int port); + + void writeHeader(const std::string& callsign, const CHeaderData& header); + + void writeData(const std::string& callsign, const CAMBEData& data); + + bool isConnected() const; + + void clock(unsigned int ms); + + void close(); + +private: + CAPRSWriterThread* m_thread; + CTimer m_idTimer; + std::string m_gateway; + in_addr m_address; + unsigned int m_port; + CUDPReaderWriter* m_socket; + std::unordered_map m_array; + + bool pollGPS(); + void sendIdFramesFixed(); + void sendIdFramesMobile(); +}; + +#endif + diff --git a/StringUtils.cpp b/StringUtils.cpp new file mode 100644 index 0000000..89eb94d --- /dev/null +++ b/StringUtils.cpp @@ -0,0 +1,33 @@ + +/* + * Copyright (C) 2009-2011,2013 by Jonathan Naylor G4KLX + * Copyright (c) 2017 by Thomas A. Early N7TAE + * Copyright (c) 2021 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 "StringUtils.h" + +template +std::string string_format( const std::string& format, Args ... args ) +{ + int size_s = std::snprintf( nullptr, 0, format.c_str(), args ... ) + 1; // Extra space for '\0' + if( size_s <= 0 ){ throw std::runtime_error( "Error during formatting." ); } + auto size = static_cast( size_s ); + auto buf = std::make_unique( size ); + std::snprintf( buf.get(), size, format.c_str(), args ... ); + return std::string( buf.get(), buf.get() + size - 1 ); // We don't want the '\0' inside +} \ No newline at end of file diff --git a/StringUtils.h b/StringUtils.h new file mode 100644 index 0000000..75e9b4b --- /dev/null +++ b/StringUtils.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2009-2011,2013 by Jonathan Naylor G4KLX + * Copyright (c) 2017 by Thomas A. Early N7TAE + * Copyright (c) 2021 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. + */ + +#pragma once + +#include +#include +#include + +#define wxT(x) std::string(x) + +template +std::string string_format( const std::string& format, Args ... args ); \ No newline at end of file