Add Icom terminal and access point mode transmit.

pull/1/head
Jonathan Naylor 8 years ago
parent 56ef55823a
commit 79cf310b05

@ -16,9 +16,7 @@
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/ */
#include "CCITTChecksumReverse.h"
#include "IcomController.h" #include "IcomController.h"
#include "CCITTChecksum.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Timer.h" #include "Timer.h"
#include "Utils.h" #include "Utils.h"
@ -29,8 +27,6 @@
#include <wx/dir.h> #include <wx/dir.h>
#endif #endif
const unsigned char ICOM_FRAME_START = 0xFFU;
const unsigned int MAX_RESPONSES = 30U; const unsigned int MAX_RESPONSES = 30U;
const unsigned int BUFFER_LENGTH = 200U; const unsigned int BUFFER_LENGTH = 200U;
@ -43,10 +39,7 @@ m_buffer(NULL),
m_txData(1000U), m_txData(1000U),
m_txCounter(0U), m_txCounter(0U),
m_pktCounter(0U), m_pktCounter(0U),
m_rx(false), m_txSpace(true)
m_txSpace(0U),
m_txEnabled(false),
m_checksum(false)
{ {
wxASSERT(!port.IsEmpty()); wxASSERT(!port.IsEmpty());
@ -93,12 +86,12 @@ void* CIcomController::Entry()
case RTI_TIMEOUT: case RTI_TIMEOUT:
break; break;
case RTI_ERROR: { case RTI_ERROR:
wxLogMessage(wxT("Stopping Icom Controller thread")); wxLogMessage(wxT("Stopping Icom Controller thread"));
return NULL; return NULL;
case RTI_HEADER: { case RTI_HEADER: {
// CUtils::dump(wxT("RT_HEADER"), m_buffer, length); // CUtils::dump(wxT("RTI_HEADER"), m_buffer, length);
wxMutexLocker locker(m_mutex); wxMutexLocker locker(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
@ -106,14 +99,14 @@ void* CIcomController::Entry()
data[1U] = RADIO_HEADER_LENGTH_BYTES; data[1U] = RADIO_HEADER_LENGTH_BYTES;
m_rxData.addData(data, 2U); m_rxData.addData(data, 2U);
m_rxData.addData(m_buffer + 3U, RADIO_HEADER_LENGTH_BYTES); m_rxData.addData(m_buffer + 2U, RADIO_HEADER_LENGTH_BYTES);
m_rx = true; m_txSpace = true;
} }
break; break;
case RTI_DATA: { case RTI_DATA: {
// CUtils::dump(wxT("RT_DATA"), m_buffer, length); // CUtils::dump(wxT("RTI_DATA"), m_buffer, length);
wxMutexLocker locker(m_mutex); wxMutexLocker locker(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
@ -121,14 +114,14 @@ void* CIcomController::Entry()
data[1U] = DV_FRAME_LENGTH_BYTES; data[1U] = DV_FRAME_LENGTH_BYTES;
m_rxData.addData(data, 2U); m_rxData.addData(data, 2U);
m_rxData.addData(m_buffer + 5U, DV_FRAME_LENGTH_BYTES); m_rxData.addData(m_buffer + 4U, DV_FRAME_LENGTH_BYTES);
m_rx = true; m_txSpace = true;
} }
break; break;
case RTI_EOT: { case RTI_EOT: {
// wxLogMessage(wxT("RT_EOT")); // wxLogMessage(wxT("RTI_EOT"));
wxMutexLocker locker(m_mutex); wxMutexLocker locker(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
@ -136,60 +129,39 @@ void* CIcomController::Entry()
data[1U] = 0U; data[1U] = 0U;
m_rxData.addData(data, 2U); m_rxData.addData(data, 2U);
m_rx = false; m_txSpace = true;
} }
break; break;
default: case RTI_HEADER_ACK:
wxLogMessage(wxT("Unknown message, type: %02X"), m_buffer[3U]); // wxLogMessage(wxT("RTI_HEADER_ACK"));
CUtils::dump(wxT("Buffer dump"), m_buffer, length); m_txSpace = true;
break; break;
}
#ifdef notdef
if (space > 0U) {
if (writeType == DSMTT_NONE && m_txData.hasData()) {
wxMutexLocker locker(m_mutex);
m_txData.getData(&writeType, 1U);
m_txData.getData(&writeLength, 1U);
m_txData.getData(writeBuffer, writeLength);
}
// Only send the start when the TX is off
if (!m_tx && writeType == DSMTT_START) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength); case RTI_DATA_ACK:
if (ret != int(writeLength)) // wxLogMessage(wxT("RTI_DATA_ACK"));
wxLogWarning(wxT("Error when writing the header to the Icom")); m_txSpace = true;
break;
writeType = DSMTT_NONE;
space--;
}
if (space > 4U && writeType == DSMTT_HEADER) { default:
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength); wxLogMessage(wxT("Unknown message, type: %02X"), m_buffer[1U]);
CUtils::dump(wxT("Buffer dump"), m_buffer, length + 1U);
break;
}
int ret = m_serial.write(writeBuffer, writeLength); if (m_txSpace && !m_txData.isEmpty()) {
if (ret != int(writeLength)) m_txData.getData(m_buffer, 1U);
wxLogWarning(wxT("Error when writing the header to the Icom"));
writeType = DSMTT_NONE; unsigned int len = m_buffer[0U];
space -= 4U; m_txData.getData(m_buffer + 1U, len);
}
if (writeType == DSMTT_DATA || writeType == DSMTT_EOT) { // CUtils::dump(wxT("Sending"), m_buffer, len + 1U);
// CUtils::dump(wxT("Write Data"), writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength); int ret = m_serial.write(m_buffer, len + 1U);
if (ret != int(writeLength)) if (ret != int(len + 1U))
wxLogWarning(wxT("Error when writing data to the Icom")); wxLogWarning(wxT("Error when writing the header to the Icom"));
writeType = DSMTT_NONE; m_txSpace = false;
space--;
}
#endif
} }
Sleep(5UL); Sleep(5UL);
@ -204,438 +176,124 @@ void* CIcomController::Entry()
return NULL; return NULL;
} }
#ifdef notdef
void* CIcomController::Entry()
{
wxLogMessage(wxT("Starting Icom Controller thread"));
// Clock every 5ms-ish
CTimer pollTimer(200U, 0U, 100U);
pollTimer.start();
unsigned char writeType = DSMTT_NONE;
unsigned char writeLength = 0U;
unsigned char* writeBuffer = new unsigned char[BUFFER_LENGTH];
unsigned int space = 0U;
while (!m_stopped) {
// Poll the modem status every 100ms
if (pollTimer.hasExpired()) {
bool ret = readStatus();
if (!ret) {
ret = findModem();
if (!ret) {
wxLogMessage(wxT("Stopping Icom Controller thread"));
return NULL;
}
}
pollTimer.start();
}
unsigned int length;
RESP_TYPE_MEGA type = getResponse(m_buffer, length);
switch (type) {
case RTM_TIMEOUT:
break;
case RTM_ERROR: {
bool ret = findModem();
if (!ret) {
wxLogMessage(wxT("Stopping Icom Controller thread"));
return NULL;
}
}
break;
case RTM_RXPREAMBLE:
// wxLogMessage(wxT("RT_PREAMBLE"));
break;
case RTM_START:
// wxLogMessage(wxT("RT_START"));
break;
case RTM_HEADER:
CUtils::dump(wxT("RT_HEADER"), m_buffer, length);
if (length == 7U) {
if (m_buffer[4U] == DVRPTR_NAK)
wxLogWarning(wxT("Received a header NAK from the Icom"));
} else {
bool correct = (m_buffer[5U] & 0x80U) == 0x00U;
if (correct) {
wxMutexLocker locker(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_HEADER;
data[1U] = RADIO_HEADER_LENGTH_BYTES;
m_rxData.addData(data, 2U);
m_rxData.addData(m_buffer + 8U, RADIO_HEADER_LENGTH_BYTES);
m_rx = true;
}
}
break;
case RTM_RXSYNC:
// wxLogMessage(wxT("RT_RXSYNC"));
break;
case RTM_DATA:
CUtils::dump(wxT("RT_DATA"), m_buffer, length);
if (length == 7U) {
if (m_buffer[4U] == DVRPTR_NAK)
wxLogWarning(wxT("Received a data NAK from the Icom"));
} else {
wxMutexLocker locker(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_DATA;
data[1U] = DV_FRAME_LENGTH_BYTES;
m_rxData.addData(data, 2U);
m_rxData.addData(m_buffer + 8U, DV_FRAME_LENGTH_BYTES);
m_rx = true;
}
break;
case RTM_EOT: {
wxLogMessage(wxT("RT_EOT"));
wxMutexLocker locker(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_EOT;
data[1U] = 0U;
m_rxData.addData(data, 2U);
m_rx = false;
}
break;
case RTM_RXLOST: {
wxLogMessage(wxT("RT_LOST"));
wxMutexLocker locker(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_LOST;
data[1U] = 0U;
m_rxData.addData(data, 2U);
m_rx = false;
}
break;
case RTM_GET_STATUS: {
m_txEnabled = (m_buffer[4U] & 0x02U) == 0x02U;
m_checksum = (m_buffer[4U] & 0x08U) == 0x08U;
m_tx = (m_buffer[5U] & 0x02U) == 0x02U;
m_txSpace = m_buffer[8U];
space = m_txSpace - m_buffer[9U];
// CUtils::dump(wxT("GET_STATUS"), m_buffer, length);
// wxLogMessage(wxT("PTT=%d tx=%u space=%u cksum=%d, tx enabled=%d"), int(m_tx), m_txSpace, space, int(m_checksum), int(m_txEnabled));
}
break;
// These should not be received in this loop, but don't complain if we do
case RTM_GET_VERSION:
case RTM_GET_SERIAL:
case RTM_GET_CONFIG:
break;
default:
wxLogMessage(wxT("Unknown message, type: %02X"), m_buffer[3U]);
CUtils::dump(wxT("Buffer dump"), m_buffer, length);
break;
}
if (space > 0U) {
if (writeType == DSMTT_NONE && m_txData.hasData()) {
wxMutexLocker locker(m_mutex);
m_txData.getData(&writeType, 1U);
m_txData.getData(&writeLength, 1U);
m_txData.getData(writeBuffer, writeLength);
}
// Only send the start when the TX is off
if (!m_tx && writeType == DSMTT_START) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing the header to the Icom"));
writeType = DSMTT_NONE;
space--;
}
if (space > 4U && writeType == DSMTT_HEADER) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing the header to the Icom"));
writeType = DSMTT_NONE;
space -= 4U;
}
if (writeType == DSMTT_DATA || writeType == DSMTT_EOT) {
// CUtils::dump(wxT("Write Data"), writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing data to the Icom"));
writeType = DSMTT_NONE;
space--;
}
}
Sleep(5UL);
pollTimer.clock();
}
wxLogMessage(wxT("Stopping Icom Controller thread"));
setEnabled(false);
delete[] writeBuffer;
m_serial.close();
return NULL;
}
#endif
bool CIcomController::writeHeader(const CHeaderData& header) bool CIcomController::writeHeader(const CHeaderData& header)
{ {
#ifdef notdef bool ret = m_txData.hasSpace(43U);
if (!m_txEnabled)
return false;
bool ret = m_txData.hasSpace(64U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write the header")); wxLogWarning(wxT("No space to write the header"));
return false; return false;
} }
m_txCounter++; unsigned char buffer[50U];
if (m_txCounter == 0U)
m_txCounter = 1U;
unsigned char buffer1[10U];
buffer1[0U] = DVRPTR_FRAME_START;
buffer1[1U] = 0x03U;
buffer1[2U] = 0x00U;
buffer1[3U] = DVRPTR_START;
buffer1[4U] = m_txCounter;
buffer1[5U] = 0x00U;
if (m_checksum) {
CCCITTChecksum cksum;
cksum.update(buffer1 + 0U, 6U);
cksum.result(buffer1 + 6U);
} else {
buffer1[6U] = 0x00U;
buffer1[7U] = 0x0BU;
}
unsigned char buffer2[60U]; buffer[0U] = 0x29U;
buffer2[0U] = DVRPTR_FRAME_START; buffer[1U] = 0x20U;
buffer2[1U] = 0x2FU; ::memset(buffer + 2U, ' ', RADIO_HEADER_LENGTH_BYTES);
buffer2[2U] = 0x00U;
buffer2[3U] = DVRPTR_HEADER; buffer[2U] = header.getFlag1();
buffer[3U] = header.getFlag2();
buffer2[4U] = m_txCounter; buffer[4U] = header.getFlag3();
buffer2[5U] = 0x00U;
buffer2[6U] = 0x00U;
buffer2[7U] = 0x00U;
::memset(buffer2 + 8U, ' ', RADIO_HEADER_LENGTH_BYTES);
buffer2[8U] = header.getFlag1();
buffer2[9U] = header.getFlag2();
buffer2[10U] = header.getFlag3();
wxString rpt2 = header.getRptCall2(); wxString rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 11U] = rpt2.GetChar(i); buffer[i + 5U] = rpt2.GetChar(i);
wxString rpt1 = header.getRptCall1(); wxString rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 19U] = rpt1.GetChar(i); buffer[i + 13U] = rpt1.GetChar(i);
wxString your = header.getYourCall(); wxString your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 27U] = your.GetChar(i); buffer[i + 21U] = your.GetChar(i);
wxString my1 = header.getMyCall1(); wxString my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 35U] = my1.GetChar(i); buffer[i + 29U] = my1.GetChar(i);
wxString my2 = header.getMyCall2(); wxString my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer2[i + 43U] = my2.GetChar(i); buffer[i + 37U] = my2.GetChar(i);
CCCITTChecksumReverse cksum1; buffer[41U] = 0xFFU;
cksum1.update(buffer2 + 8U, RADIO_HEADER_LENGTH_BYTES - 2U);
cksum1.result(buffer2 + 47U);
buffer2[49U] = 0x00U;
if (m_checksum) {
CCCITTChecksum cksum;
cksum.update(buffer2 + 0U, 50U);
cksum.result(buffer2 + 50U);
} else {
buffer2[50U] = 0x00U;
buffer2[51U] = 0x0BU;
}
m_txCounter = 0U;
m_pktCounter = 0U; m_pktCounter = 0U;
wxMutexLocker locker(m_mutex); wxMutexLocker locker(m_mutex);
unsigned char type1 = DSMTT_START; m_txData.addData(buffer, 42U);
m_txData.addData(&type1, 1U);
unsigned char len1 = 8U; m_txSpace = false;
m_txData.addData(&len1, 1U);
m_txData.addData(buffer1, 8U);
unsigned char type2 = DSMTT_HEADER;
m_txData.addData(&type2, 1U);
unsigned char len2 = 52U;
m_txData.addData(&len2, 1U);
m_txData.addData(buffer2, 52U);
#endif
return true; return true;
} }
bool CIcomController::writeData(const unsigned char* data, unsigned int, bool end) bool CIcomController::writeData(const unsigned char* data, unsigned int, bool end)
{ {
#ifdef notdef bool ret = m_txData.hasSpace(18U);
if (!m_txEnabled)
return false;
bool ret = m_txData.hasSpace(26U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write data")); wxLogWarning(wxT("No space to write data"));
return false; return false;
} }
unsigned char buffer[30U]; unsigned char buffer[20U];
if (end) { if (end) {
buffer[0U] = DVRPTR_FRAME_START; buffer[0U] = 0x10U;
buffer[1U] = 0x03U; buffer[1U] = 0x22U;
buffer[2U] = 0x00U;
buffer[3U] = DVRPTR_EOT; buffer[2U] = m_txCounter;
buffer[3U] = m_pktCounter | 0x40U;
buffer[4U] = m_txCounter; ::memcpy(buffer + 4U, END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES);
buffer[5U] = 0xFFU;
if (m_checksum) { buffer[16U] = 0xFFU;
CCCITTChecksum cksum;
cksum.update(buffer + 0U, 6U);
cksum.result(buffer + 6U);
} else {
buffer[6U] = 0x00U;
buffer[7U] = 0x0BU;
}
wxMutexLocker locker(m_mutex); wxMutexLocker locker(m_mutex);
unsigned char type = DSMTT_EOT; m_txData.addData(buffer, 17U);
m_txData.addData(&type, 1U);
unsigned char len = 8U;
m_txData.addData(&len, 1U);
m_txData.addData(buffer, 8U); m_txSpace = true;
return true; return true;
} }
buffer[0U] = DVRPTR_FRAME_START; buffer[0U] = 0x10U;
buffer[1U] = 0x13U; buffer[1U] = 0x22U;
buffer[2U] = 0x00U;
buffer[3U] = DVRPTR_DATA; buffer[2U] = m_txCounter;
buffer[3U] = m_pktCounter;
buffer[4U] = m_txCounter;
buffer[5U] = m_pktCounter;
m_txCounter++;
m_pktCounter++; m_pktCounter++;
if (m_pktCounter >= m_txSpace) if (::memcmp(buffer + VOICE_FRAME_LENGTH_BYTES, DATA_SYNC_BYTES, DATA_FRAME_LENGTH_BYTES) == 0 || m_pktCounter == 21U)
m_pktCounter = 0U; m_pktCounter = 0U;
buffer[6U] = 0x00U; ::memcpy(buffer + 4U, data, DV_FRAME_LENGTH_BYTES);
buffer[7U] = 0x00U;
::memcpy(buffer + 8U, data, DV_FRAME_LENGTH_BYTES); buffer[16U] = 0xFFU;
buffer[20U] = 0x00U;
buffer[21U] = 0x00U;
if (m_checksum) {
CCCITTChecksum cksum;
cksum.update(buffer + 0U, 22U);
cksum.result(buffer + 22U);
} else {
buffer[22U] = 0x00U;
buffer[23U] = 0x0BU;
}
wxMutexLocker locker(m_mutex); wxMutexLocker locker(m_mutex);
unsigned char type = DSMTT_DATA; m_txData.addData(buffer, 17U);
m_txData.addData(&type, 1U);
unsigned char len = 24U; m_txSpace = false;
m_txData.addData(&len, 1U);
m_txData.addData(buffer, 24U);
#endif
return true; return true;
} }
unsigned int CIcomController::getSpace() unsigned int CIcomController::getSpace()
{ {
// return m_txData.freeSpace() / 26U; return m_txData.freeSpace() / 18U;
return true;
} }
bool CIcomController::isTXReady() bool CIcomController::isTXReady()
{ {
return true;
#ifdef notdef
if (m_tx)
return false;
return m_txData.isEmpty(); return m_txData.isEmpty();
#endif
} }
wxString CIcomController::getPath() const wxString CIcomController::getPath() const
@ -655,30 +313,17 @@ RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int&
if (ret == 0) if (ret == 0)
return RTI_TIMEOUT; return RTI_TIMEOUT;
if (buffer[0U] != ICOM_FRAME_START) if (buffer[0U] == 0xFFU)
return RTI_TIMEOUT;
do {
ret = m_serial.read(buffer + 1U, 1U);
if (ret < 0) {
wxLogError(wxT("Error when reading from the Icom radio"));
return RTI_ERROR;
}
if (ret == 0)
Sleep(5UL);
} while (ret == 0);
if (buffer[1U] == ICOM_FRAME_START)
return RTI_TIMEOUT; return RTI_TIMEOUT;
length = buffer[1U] + 1U; length = buffer[0U];
if (length >= 100U) { if (length >= 100U) {
wxLogError(wxT("Invalid data received from the Icom radio")); wxLogError(wxT("Invalid data received from the Icom radio"));
return RTI_ERROR; return RTI_ERROR;
} }
unsigned int offset = 2U; unsigned int offset = 1U;
while (offset < length) { while (offset < length) {
ret = m_serial.read(buffer + offset, length - offset); ret = m_serial.read(buffer + offset, length - offset);
@ -694,16 +339,22 @@ RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int&
Sleep(5UL); Sleep(5UL);
} }
// CUtils::dump(wxT("Received"), buffer, length); // CUtils::dump(wxT("Received"), buffer, length + 1U);
switch (length) { switch (buffer[1U]) {
case 45U: case 0x03U:
return RTI_TIMEOUT;
case 0x10U:
return RTI_HEADER; return RTI_HEADER;
case 17U: case 0x12U:
if ((buffer[4U] & 0x40U) == 0x40U) if ((buffer[3U] & 0x40U) == 0x40U)
return RTI_EOT; return RTI_EOT;
else else
return RTI_DATA; return RTI_DATA;
case 0x21U:
return RTI_HEADER_ACK;
case 0x23U:
return RTI_DATA_ACK;
default: default:
return RTI_UNKNOWN; return RTI_UNKNOWN;
} }

@ -32,7 +32,9 @@ enum RESP_TYPE_ICOM {
RTI_UNKNOWN, RTI_UNKNOWN,
RTI_HEADER, RTI_HEADER,
RTI_DATA, RTI_DATA,
RTI_EOT RTI_EOT,
RTI_HEADER_ACK,
RTI_DATA_ACK
}; };
class CIcomController : public CModem { class CIcomController : public CModem {
@ -59,10 +61,7 @@ private:
CRingBuffer<unsigned char> m_txData; CRingBuffer<unsigned char> m_txData;
unsigned char m_txCounter; unsigned char m_txCounter;
unsigned char m_pktCounter; unsigned char m_pktCounter;
bool m_rx; bool m_txSpace;
unsigned int m_txSpace;
bool m_txEnabled;
bool m_checksum;
RESP_TYPE_ICOM getResponse(unsigned char* buffer, unsigned int& length); RESP_TYPE_ICOM getResponse(unsigned char* buffer, unsigned int& length);
}; };

Loading…
Cancel
Save

Powered by TurnKey Linux.