diff --git a/plugins/channeltx/CMakeLists.txt b/plugins/channeltx/CMakeLists.txt index 88cd7ae61..dbc10b015 100644 --- a/plugins/channeltx/CMakeLists.txt +++ b/plugins/channeltx/CMakeLists.txt @@ -1,6 +1,7 @@ project(mod) add_subdirectory(modam) +add_subdirectory(modchirpchat) add_subdirectory(modnfm) add_subdirectory(modssb) add_subdirectory(modwfm) diff --git a/plugins/channeltx/modchirpchat/CMakeLists.txt b/plugins/channeltx/modchirpchat/CMakeLists.txt new file mode 100644 index 000000000..ad830ab2b --- /dev/null +++ b/plugins/channeltx/modchirpchat/CMakeLists.txt @@ -0,0 +1,67 @@ +project(modchirpchat) + +set(modchirpchat_SOURCES + chirpchatmod.cpp + chirpchatmodsettings.cpp + chirpchatmodsource.cpp + chirpchatmodbaseband.cpp + chirpchatmodplugin.cpp + chirpchatmodencoder.cpp + chirpchatmodencodertty.cpp + chirpchatmodencoderascii.cpp + chirpchatmodencoderlora.cpp + chirpchatmodwebapiadapter.cpp +) + +set(modchirpchat_HEADERS + chirpchatmod.h + chirpchatmodsettings.h + chirpchatmodsource.h + chirpchatmodbaseband.h + chirpchatmodplugin.h + chirpchatmodencoder.h + chirpchatmodencodertty.h + chirpchatmodencoderascii.h + chirpchatmodencoderlora.h + chirpchatmodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(modchirpchat_SOURCES + ${modchirpchat_SOURCES} + chirpchatmodgui.cpp + chirpchatmodgui.ui + ) + set(modchirpchat_HEADERS + ${modchirpchat_HEADERS} + chirpchatmodgui.h + ) + + set(TARGET_NAME modchirpchat) + set(TARGET_LIB "Qt5::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME modchirpchatsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${modchirpchat_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} + swagger +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) diff --git a/plugins/channeltx/modchirpchat/chirpchatmod.cpp b/plugins/channeltx/modchirpchat/chirpchatmod.cpp new file mode 100644 index 000000000..f24877e69 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmod.cpp @@ -0,0 +1,909 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGChannelReport.h" +#include "SWGChirpChatModReport.h" + +#include +#include +#include + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "feature/feature.h" +#include "util/db.h" +#include "maincore.h" + +#include "chirpchatmodbaseband.h" +#include "chirpchatmod.h" + +MESSAGE_CLASS_DEFINITION(ChirpChatMod::MsgConfigureChirpChatMod, Message) +MESSAGE_CLASS_DEFINITION(ChirpChatMod::MsgReportPayloadTime, Message) + +const QString ChirpChatMod::m_channelIdURI = "sdrangel.channeltx.modchirpchat"; +const QString ChirpChatMod::m_channelId = "ChirpChatMod"; + +ChirpChatMod::ChirpChatMod(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), + m_deviceAPI(deviceAPI), + m_currentPayloadTime(0.0), + m_settingsMutex(QMutex::Recursive), + m_sampleRate(48000) +{ + setObjectName(m_channelId); + + m_thread = new QThread(this); + m_basebandSource = new ChirpChatModBaseband(); + m_basebandSource->moveToThread(m_thread); + + applySettings(m_settings, true); + + m_deviceAPI->addChannelSource(this); + m_deviceAPI->addChannelSourceAPI(this); + + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); +} + +ChirpChatMod::~ChirpChatMod() +{ + disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + delete m_networkManager; + m_deviceAPI->removeChannelSourceAPI(this); + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; +} + +void ChirpChatMod::start() +{ + qDebug("ChirpChatMod::start"); + m_basebandSource->reset(); + m_thread->start(); +} + +void ChirpChatMod::stop() +{ + qDebug("ChirpChatMod::stop"); + m_thread->exit(); + m_thread->wait(); +} + +void ChirpChatMod::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); +} + +bool ChirpChatMod::handleMessage(const Message& cmd) +{ + if (MsgConfigureChirpChatMod::match(cmd)) + { + MsgConfigureChirpChatMod& cfg = (MsgConfigureChirpChatMod&) cmd; + qDebug() << "ChirpChatMod::handleMessage: MsgConfigureChirpChatMod"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + // Forward to the source + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "ChirpChatMod::handleMessage: DSPSignalNotification"; + m_basebandSource->getInputMessageQueue()->push(rep); + + // Forward to the GUI + if (getMessageQueueToGUI()) + { + DSPSignalNotification* repToGUI = new DSPSignalNotification(notif); // make a copy + getMessageQueueToGUI()->push(repToGUI); + } + + return true; + } + else + { + return false; + } +} + +void ChirpChatMod::applySettings(const ChirpChatModSettings& settings, bool force) +{ + qDebug() << "ChirpChatMod::applySettings:" + << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << " m_rfBandwidth: " << settings.m_bandwidthIndex + << " bandwidth: " << ChirpChatModSettings::bandwidths[settings.m_bandwidthIndex] + << " m_channelMute: " << settings.m_channelMute + << " m_beaconMessage: " << settings.m_beaconMessage + << " m_cqMessage: " << settings.m_cqMessage + << " m_replyMessage: " << settings.m_replyMessage + << " m_reportMessage:" << settings.m_reportMessage + << " m_replyReportMessage: " << settings.m_replyReportMessage + << " m_rrrMessage: " << settings.m_rrrMessage + << " m_73message: " << settings.m_73Message + << " m_qsoTextMessage: " << settings.m_qsoTextMessage + << " m_textMessage: " << settings.m_textMessage + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIAddress: " << settings.m_reverseAPIPort + << " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex + << " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) { + reverseAPIKeys.append("inputFrequencyOffset"); + } + if ((settings.m_bandwidthIndex != m_settings.m_bandwidthIndex) || force) { + reverseAPIKeys.append("bandwidthIndex"); + } + if ((settings.m_channelMute != m_settings.m_channelMute) || force) { + reverseAPIKeys.append("channelMute"); + } + if ((settings.m_spreadFactor != m_settings.m_spreadFactor) || force) { + reverseAPIKeys.append("spreadFactor"); + } + if ((settings.m_deBits != m_settings.m_deBits) || force) { + reverseAPIKeys.append("deBits"); + } + + if ((settings.m_spreadFactor != m_settings.m_spreadFactor) + || (settings.m_deBits != m_settings.m_deBits) || force) { + m_encoder.setNbSymbolBits(settings.m_spreadFactor, settings.m_deBits); + } + + if ((settings.m_codingScheme != m_settings.m_codingScheme) || force) + { + reverseAPIKeys.append("codingScheme"); + m_encoder.setCodingScheme(settings.m_codingScheme); + } + + if ((settings.m_nbParityBits != m_settings.m_nbParityBits || force)) + { + reverseAPIKeys.append("nbParityBits"); + m_encoder.setLoRaParityBits(settings.m_nbParityBits); + } + + if ((settings.m_hasCRC != m_settings.m_hasCRC) || force) + { + reverseAPIKeys.append("hasCRC"); + m_encoder.setLoRaHasCRC(settings.m_hasCRC); + } + + if ((settings.m_hasHeader != m_settings.m_hasHeader) || force) + { + reverseAPIKeys.append("hasHeader"); + m_encoder.setLoRaHasHeader(settings.m_hasHeader); + } + + if ((settings.m_messageType != m_settings.m_messageType) || force) { + reverseAPIKeys.append("messageType"); + } + if ((settings.m_beaconMessage != m_settings.m_beaconMessage) || force) { + reverseAPIKeys.append("beaconMessage"); + } + if ((settings.m_cqMessage != m_settings.m_cqMessage) || force) { + reverseAPIKeys.append("cqMessage"); + } + if ((settings.m_replyMessage != m_settings.m_replyMessage) || force) { + reverseAPIKeys.append("replyMessage"); + } + if ((settings.m_reportMessage != m_settings.m_reportMessage) || force) { + reverseAPIKeys.append("reportMessage"); + } + if ((settings.m_replyReportMessage != m_settings.m_replyReportMessage) || force) { + reverseAPIKeys.append("replyReportMessage"); + } + if ((settings.m_rrrMessage != m_settings.m_rrrMessage) || force) { + reverseAPIKeys.append("rrrMessage"); + } + if ((settings.m_73Message != m_settings.m_73Message) || force) { + reverseAPIKeys.append("73Message"); + } + if ((settings.m_qsoTextMessage != m_settings.m_qsoTextMessage) || force) { + reverseAPIKeys.append("qsoTextMessage"); + } + if ((settings.m_textMessage != m_settings.m_textMessage) || force) { + reverseAPIKeys.append("textMessage"); + } + if ((settings.m_bytesMessage != m_settings.m_bytesMessage) || force) { + reverseAPIKeys.append("bytesMessage"); + } + + ChirpChatModBaseband::MsgConfigureChirpChatModPayload *payloadMsg = nullptr; + std::vector symbols; + + if ((settings.m_messageType == ChirpChatModSettings::MessageNone) + && ((settings.m_messageType != m_settings.m_messageType) || force)) + { + payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(); + } + else if ((settings.m_messageType == ChirpChatModSettings::MessageBeacon) + && ((settings.m_messageType != m_settings.m_messageType) + || (settings.m_beaconMessage != m_settings.m_beaconMessage) || force)) + { + m_encoder.encodeString(settings.m_beaconMessage, symbols); + payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(symbols); + } + else if ((settings.m_messageType == ChirpChatModSettings::MessageCQ) + && ((settings.m_messageType != m_settings.m_messageType) + || (settings.m_cqMessage != m_settings.m_cqMessage) || force)) + { + m_encoder.encodeString(settings.m_cqMessage, symbols); + payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(symbols); + } + else if ((settings.m_messageType == ChirpChatModSettings::MessageReply) + && ((settings.m_messageType != m_settings.m_messageType) + || (settings.m_replyMessage != m_settings.m_replyMessage) || force)) + { + m_encoder.encodeString(settings.m_replyMessage, symbols); + payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(symbols); + } + else if ((settings.m_messageType == ChirpChatModSettings::MessageReport) + && ((settings.m_messageType != m_settings.m_messageType) + || (settings.m_reportMessage != m_settings.m_reportMessage) || force)) + { + m_encoder.encodeString(settings.m_reportMessage, symbols); + payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(symbols); + } + else if ((settings.m_messageType == ChirpChatModSettings::MessageReplyReport) + && ((settings.m_messageType != m_settings.m_messageType) + || (settings.m_replyReportMessage != m_settings.m_replyReportMessage) || force)) + { + m_encoder.encodeString(settings.m_replyReportMessage, symbols); + payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(symbols); + } + else if ((settings.m_messageType == ChirpChatModSettings::MessageRRR) + && ((settings.m_messageType != m_settings.m_messageType) + || (settings.m_rrrMessage != m_settings.m_rrrMessage) || force)) + { + m_encoder.encodeString(settings.m_rrrMessage, symbols); + payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(symbols); + } + else if ((settings.m_messageType == ChirpChatModSettings::Message73) + && ((settings.m_messageType != m_settings.m_messageType) + || (settings.m_73Message != m_settings.m_73Message) || force)) + { + m_encoder.encodeString(settings.m_73Message, symbols); + payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(symbols); + } + else if ((settings.m_messageType == ChirpChatModSettings::MessageQSOText) + && ((settings.m_messageType != m_settings.m_messageType) + || (settings.m_qsoTextMessage != m_settings.m_qsoTextMessage) || force)) + { + m_encoder.encodeString(settings.m_qsoTextMessage, symbols); + payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(symbols); + } + else if ((settings.m_messageType == ChirpChatModSettings::MessageText) + && ((settings.m_messageType != m_settings.m_messageType) + || (settings.m_textMessage != m_settings.m_textMessage) || force)) + { + m_encoder.encodeString(settings.m_textMessage, symbols); + payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(symbols); + } + else if ((settings.m_messageType == ChirpChatModSettings::MessageBytes) + && ((settings.m_messageType != m_settings.m_messageType) + || (settings.m_bytesMessage != m_settings.m_bytesMessage) || force)) + { + m_encoder.encodeBytes(settings.m_bytesMessage, symbols); + payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(symbols); + } + + if (payloadMsg) + { + m_basebandSource->getInputMessageQueue()->push(payloadMsg); + m_currentPayloadTime = (symbols.size()*(1<push(rpt); + } + } + + if (m_settings.m_streamIndex != settings.m_streamIndex) + { + if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only + { + m_deviceAPI->removeChannelSourceAPI(this); + m_deviceAPI->removeChannelSource(this, m_settings.m_streamIndex); + m_deviceAPI->addChannelSource(this, settings.m_streamIndex); + m_deviceAPI->addChannelSourceAPI(this); + } + + reverseAPIKeys.append("streamIndex"); + } + + ChirpChatModBaseband::MsgConfigureChirpChatModBaseband *msg = + ChirpChatModBaseband::MsgConfigureChirpChatModBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) || + (m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + if (m_featuresSettingsFeedback.size() > 0) { + featuresSendSettings(reverseAPIKeys, settings, force); + } + + m_settings = settings; +} + +QByteArray ChirpChatMod::serialize() const +{ + return m_settings.serialize(); +} + +bool ChirpChatMod::deserialize(const QByteArray& data) +{ + bool success = true; + + if (!m_settings.deserialize(data)) + { + m_settings.resetToDefaults(); + success = false; + } + + MsgConfigureChirpChatMod *msg = MsgConfigureChirpChatMod::create(m_settings, true); + m_inputMessageQueue.push(msg); + + return success; +} + +int ChirpChatMod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setChirpChatModSettings(new SWGSDRangel::SWGChirpChatModSettings()); + response.getChirpChatModSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int ChirpChatMod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + ChirpChatModSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureChirpChatMod *msg = MsgConfigureChirpChatMod::create(settings, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureChirpChatMod *msgToGUI = MsgConfigureChirpChatMod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +void ChirpChatMod::webapiUpdateChannelSettings( + ChirpChatModSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getChirpChatModSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("bandwidthIndex")) { + settings.m_bandwidthIndex = response.getChirpChatModSettings()->getBandwidthIndex(); + } + if (channelSettingsKeys.contains("spreadFactor")) { + settings.m_spreadFactor = response.getChirpChatModSettings()->getSpreadFactor(); + } + if (channelSettingsKeys.contains("deBits")) { + settings.m_deBits = response.getChirpChatModSettings()->getDeBits(); + } + if (channelSettingsKeys.contains("preambleChirps")) { + settings.m_preambleChirps = response.getChirpChatModSettings()->getPreambleChirps(); + } + if (channelSettingsKeys.contains("quietMillis")) { + settings.m_quietMillis = response.getChirpChatModSettings()->getQuietMillis(); + } + if (channelSettingsKeys.contains("syncWord")) { + settings.m_syncWord = response.getChirpChatModSettings()->getSyncWord(); + } + if (channelSettingsKeys.contains("syncWord")) { + settings.m_syncWord = response.getChirpChatModSettings()->getSyncWord(); + } + if (channelSettingsKeys.contains("channelMute")) { + settings.m_channelMute = response.getChirpChatModSettings()->getChannelMute() != 0; + } + if (channelSettingsKeys.contains("codingScheme")) { + settings.m_codingScheme = (ChirpChatModSettings::CodingScheme) response.getChirpChatModSettings()->getCodingScheme(); + } + if (channelSettingsKeys.contains("nbParityBits")) { + settings.m_nbParityBits = response.getChirpChatModSettings()->getNbParityBits(); + } + if (channelSettingsKeys.contains("hasCRC")) { + settings.m_hasCRC = response.getChirpChatModSettings()->getHasCrc() != 0; + } + if (channelSettingsKeys.contains("hasHeader")) { + settings.m_hasHeader = response.getChirpChatModSettings()->getHasHeader() != 0; + } + if (channelSettingsKeys.contains("myCall")) { + settings.m_myCall = *response.getChirpChatModSettings()->getMyCall(); + } + if (channelSettingsKeys.contains("urCall")) { + settings.m_urCall = *response.getChirpChatModSettings()->getUrCall(); + } + if (channelSettingsKeys.contains("myLoc")) { + settings.m_myLoc = *response.getChirpChatModSettings()->getMyLoc(); + } + if (channelSettingsKeys.contains("myRpt")) { + settings.m_myRpt = *response.getChirpChatModSettings()->getMyRpt(); + } + if (channelSettingsKeys.contains("messageType")) { + settings.m_messageType = (ChirpChatModSettings::MessageType) response.getChirpChatModSettings()->getMessageType(); + } + if (channelSettingsKeys.contains("beaconMessage")) { + settings.m_beaconMessage = *response.getChirpChatModSettings()->getBeaconMessage(); + } + if (channelSettingsKeys.contains("cqMessage")) { + settings.m_cqMessage = *response.getChirpChatModSettings()->getCqMessage(); + } + if (channelSettingsKeys.contains("replyMessage")) { + settings.m_replyMessage = *response.getChirpChatModSettings()->getReplyMessage(); + } + if (channelSettingsKeys.contains("reportMessage")) { + settings.m_reportMessage = *response.getChirpChatModSettings()->getReportMessage(); + } + if (channelSettingsKeys.contains("replyReportMessage")) { + settings.m_replyReportMessage = *response.getChirpChatModSettings()->getReplyReportMessage(); + } + if (channelSettingsKeys.contains("rrrMessage")) { + settings.m_rrrMessage = *response.getChirpChatModSettings()->getRrrMessage(); + } + if (channelSettingsKeys.contains("message73")) { + settings.m_73Message = *response.getChirpChatModSettings()->getMessage73(); + } + if (channelSettingsKeys.contains("qsoTextMessage")) { + settings.m_qsoTextMessage = *response.getChirpChatModSettings()->getQsoTextMessage(); + } + if (channelSettingsKeys.contains("textMessage")) { + settings.m_textMessage = *response.getChirpChatModSettings()->getTextMessage(); + } + if (channelSettingsKeys.contains("bytesMessage")) + { + const QList *bytesStr = response.getChirpChatModSettings()->getBytesMessage(); + settings.m_bytesMessage.clear(); + + for (QList::const_iterator it = bytesStr->begin(); it != bytesStr->end(); ++it) + { + bool bStatus = false; + unsigned int byteInt = (**it).toUInt(&bStatus, 16); + + if (bStatus) { + settings.m_bytesMessage.append((char) (byteInt % 256)); + } + } + } + if (channelSettingsKeys.contains("messageRepeat")) { + settings.m_messageRepeat = response.getChirpChatModSettings()->getMessageRepeat(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getChirpChatModSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getChirpChatModSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getChirpChatModSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getChirpChatModSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getChirpChatModSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getChirpChatModSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getChirpChatModSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getChirpChatModSettings()->getReverseApiChannelIndex(); + } +} + +int ChirpChatMod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setChirpChatModReport(new SWGSDRangel::SWGChirpChatModReport()); + response.getChirpChatModReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +void ChirpChatMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const ChirpChatModSettings& settings) +{ + response.getChirpChatModSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getChirpChatModSettings()->setBandwidthIndex(settings.m_bandwidthIndex); + response.getChirpChatModSettings()->setSpreadFactor(settings.m_spreadFactor); + response.getChirpChatModSettings()->setDeBits(settings.m_deBits); + response.getChirpChatModSettings()->setPreambleChirps(settings.m_preambleChirps); + response.getChirpChatModSettings()->setQuietMillis(settings.m_quietMillis); + response.getChirpChatModSettings()->setSyncWord(settings.m_syncWord); + response.getChirpChatModSettings()->setChannelMute(settings.m_channelMute ? 1 : 0); + response.getChirpChatModSettings()->setCodingScheme((int) settings.m_codingScheme); + response.getChirpChatModSettings()->setNbParityBits(settings.m_nbParityBits); + response.getChirpChatModSettings()->setHasCrc(settings.m_hasCRC ? 1 : 0); + response.getChirpChatModSettings()->setHasHeader(settings.m_hasHeader ? 1 : 0); + + if (response.getChirpChatModSettings()->getMyCall()) { + *response.getChirpChatModSettings()->getMyCall() = settings.m_myCall; + } else { + response.getChirpChatModSettings()->setMyCall(new QString(settings.m_myCall)); + } + + if (response.getChirpChatModSettings()->getUrCall()) { + *response.getChirpChatModSettings()->getUrCall() = settings.m_urCall; + } else { + response.getChirpChatModSettings()->setUrCall(new QString(settings.m_urCall)); + } + + if (response.getChirpChatModSettings()->getMyLoc()) { + *response.getChirpChatModSettings()->getMyLoc() = settings.m_myLoc; + } else { + response.getChirpChatModSettings()->setMyLoc(new QString(settings.m_myLoc)); + } + + if (response.getChirpChatModSettings()->getMyRpt()) { + *response.getChirpChatModSettings()->getMyRpt() = settings.m_myRpt; + } else { + response.getChirpChatModSettings()->setMyRpt(new QString(settings.m_myRpt)); + } + + response.getChirpChatModSettings()->setMessageType((int) settings.m_messageType); + + if (response.getChirpChatModSettings()->getBeaconMessage()) { + *response.getChirpChatModSettings()->getBeaconMessage() = settings.m_beaconMessage; + } else { + response.getChirpChatModSettings()->setBeaconMessage(new QString(settings.m_beaconMessage)); + } + + if (response.getChirpChatModSettings()->getCqMessage()) { + *response.getChirpChatModSettings()->getCqMessage() = settings.m_cqMessage; + } else { + response.getChirpChatModSettings()->setCqMessage(new QString(settings.m_cqMessage)); + } + + if (response.getChirpChatModSettings()->getReplyMessage()) { + *response.getChirpChatModSettings()->getReplyMessage() = settings.m_replyMessage; + } else { + response.getChirpChatModSettings()->setReplyMessage(new QString(settings.m_replyMessage)); + } + + if (response.getChirpChatModSettings()->getReportMessage()) { + *response.getChirpChatModSettings()->getReportMessage() = settings.m_reportMessage; + } else { + response.getChirpChatModSettings()->setReportMessage(new QString(settings.m_reportMessage)); + } + + if (response.getChirpChatModSettings()->getReplyReportMessage()) { + *response.getChirpChatModSettings()->getReplyReportMessage() = settings.m_replyReportMessage; + } else { + response.getChirpChatModSettings()->setReplyReportMessage(new QString(settings.m_replyReportMessage)); + } + + if (response.getChirpChatModSettings()->getRrrMessage()) { + *response.getChirpChatModSettings()->getRrrMessage() = settings.m_rrrMessage; + } else { + response.getChirpChatModSettings()->setRrrMessage(new QString(settings.m_rrrMessage)); + } + + if (response.getChirpChatModSettings()->getMessage73()) { + *response.getChirpChatModSettings()->getMessage73() = settings.m_73Message; + } else { + response.getChirpChatModSettings()->setMessage73(new QString(settings.m_73Message)); + } + + if (response.getChirpChatModSettings()->getQsoTextMessage()) { + *response.getChirpChatModSettings()->getQsoTextMessage() = settings.m_qsoTextMessage; + } else { + response.getChirpChatModSettings()->setQsoTextMessage(new QString(settings.m_qsoTextMessage)); + } + + if (response.getChirpChatModSettings()->getTextMessage()) { + *response.getChirpChatModSettings()->getTextMessage() = settings.m_textMessage; + } else { + response.getChirpChatModSettings()->setTextMessage(new QString(settings.m_textMessage)); + } + + response.getChirpChatModSettings()->setBytesMessage(new QList); + QList *bytesStr = response.getChirpChatModSettings()->getBytesMessage(); + + for (QByteArray::const_iterator it = settings.m_bytesMessage.begin(); it != settings.m_bytesMessage.end(); ++it) + { + unsigned char b = *it; + bytesStr->push_back(new QString(tr("%1").arg(b, 2, 16, QChar('0')))); + } + + response.getChirpChatModSettings()->setRgbColor(settings.m_rgbColor); + + if (response.getChirpChatModSettings()->getTitle()) { + *response.getChirpChatModSettings()->getTitle() = settings.m_title; + } else { + response.getChirpChatModSettings()->setTitle(new QString(settings.m_title)); + } + + response.getChirpChatModSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getChirpChatModSettings()->getReverseApiAddress()) { + *response.getChirpChatModSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getChirpChatModSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getChirpChatModSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getChirpChatModSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getChirpChatModSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); +} + +void ChirpChatMod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + response.getChirpChatModReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); + response.getChirpChatModReport()->setChannelSampleRate(m_basebandSource->getChannelSampleRate()); + float fourthsMs = ((1<setPayloadTimeMs(m_currentPayloadTime); + response.getChirpChatModReport()->setTotalTimeMs(m_currentPayloadTime + controlMs); + response.getChirpChatModReport()->setSymbolTimeMs(4.0 * fourthsMs); +} + +void ChirpChatMod::webapiReverseSendSettings(QList& channelSettingsKeys, const ChirpChatModSettings& settings, bool force) +{ + SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); + webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force); + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIDeviceIndex) + .arg(settings.m_reverseAPIChannelIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgChannelSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgChannelSettings; +} + +void ChirpChatMod::featuresSendSettings(QList& channelSettingsKeys, const ChirpChatModSettings& settings, bool force) +{ + QList::iterator it = m_featuresSettingsFeedback.begin(); + MainCore *mainCore = MainCore::instance(); + + for (; it != m_featuresSettingsFeedback.end(); ++it) + { + if (mainCore->existsFeature(*it)) + { + SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); + webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force); + + Feature::MsgChannelSettings *msg = Feature::MsgChannelSettings::create( + this, + channelSettingsKeys, + swgChannelSettings, + force + ); + + (*it)->getInputMessageQueue()->push(msg); + } + else + { + m_featuresSettingsFeedback.removeOne(*it); + } + } +} + +void ChirpChatMod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const ChirpChatModSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(1); // single source (Tx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString(m_channelId)); + swgChannelSettings->setChirpChatModSettings(new SWGSDRangel::SWGChirpChatModSettings()); + SWGSDRangel::SWGChirpChatModSettings *swgChirpChatModSettings = swgChannelSettings->getChirpChatModSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgChirpChatModSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("bandwidthIndex") || force) { + swgChirpChatModSettings->setBandwidthIndex(settings.m_bandwidthIndex); + } + if (channelSettingsKeys.contains("spreadFactor") || force) { + swgChirpChatModSettings->setSpreadFactor(settings.m_spreadFactor); + } + if (channelSettingsKeys.contains("deBits") || force) { + swgChirpChatModSettings->setDeBits(settings.m_deBits); + } + if (channelSettingsKeys.contains("preambleChirps") || force) { + swgChirpChatModSettings->setPreambleChirps(settings.m_preambleChirps); + } + if (channelSettingsKeys.contains("quietMillis") || force) { + swgChirpChatModSettings->setQuietMillis(settings.m_quietMillis); + } + if (channelSettingsKeys.contains("syncWord") || force) { + swgChirpChatModSettings->setSyncWord(settings.m_syncWord); + } + if (channelSettingsKeys.contains("channelMute") || force) { + swgChirpChatModSettings->setChannelMute(settings.m_channelMute ? 1 : 0); + } + if (channelSettingsKeys.contains("codingScheme") || force) { + swgChirpChatModSettings->setCodingScheme((int) settings.m_codingScheme); + } + if (channelSettingsKeys.contains("nbParityBits") || force) { + swgChirpChatModSettings->setNbParityBits(settings.m_nbParityBits); + } + if (channelSettingsKeys.contains("hasCRC") || force) { + swgChirpChatModSettings->setHasCrc(settings.m_hasCRC ? 1 : 0); + } + if (channelSettingsKeys.contains("hasHeader") || force) { + swgChirpChatModSettings->setHasHeader(settings.m_hasHeader ? 1 : 0); + } + if (channelSettingsKeys.contains("myCall") || force) { + swgChirpChatModSettings->setMyCall(new QString(settings.m_myCall)); + } + if (channelSettingsKeys.contains("urCall") || force) { + swgChirpChatModSettings->setUrCall(new QString(settings.m_urCall)); + } + if (channelSettingsKeys.contains("myLoc") || force) { + swgChirpChatModSettings->setMyLoc(new QString(settings.m_myLoc)); + } + if (channelSettingsKeys.contains("myRpt") || force) { + swgChirpChatModSettings->setMyRpt(new QString(settings.m_myRpt)); + } + if (channelSettingsKeys.contains("messageType") || force) { + swgChirpChatModSettings->setMessageType((int) settings.m_messageType); + } + if (channelSettingsKeys.contains("beaconMessage") || force) { + swgChirpChatModSettings->setBeaconMessage(new QString(settings.m_beaconMessage)); + } + if (channelSettingsKeys.contains("cqMessage") || force) { + swgChirpChatModSettings->setCqMessage(new QString(settings.m_cqMessage)); + } + if (channelSettingsKeys.contains("replyMessage") || force) { + swgChirpChatModSettings->setReplyMessage(new QString(settings.m_replyMessage)); + } + if (channelSettingsKeys.contains("reportMessage") || force) { + swgChirpChatModSettings->setReportMessage(new QString(settings.m_reportMessage)); + } + if (channelSettingsKeys.contains("replyReportMessage") || force) { + swgChirpChatModSettings->setReplyReportMessage(new QString(settings.m_replyReportMessage)); + } + if (channelSettingsKeys.contains("rrrMessage") || force) { + swgChirpChatModSettings->setRrrMessage(new QString(settings.m_rrrMessage)); + } + if (channelSettingsKeys.contains("message73") || force) { + swgChirpChatModSettings->setMessage73(new QString(settings.m_73Message)); + } + if (channelSettingsKeys.contains("qsoTextMessage") || force) { + swgChirpChatModSettings->setQsoTextMessage(new QString(settings.m_qsoTextMessage)); + } + if (channelSettingsKeys.contains("textMessage") || force) { + swgChirpChatModSettings->setTextMessage(new QString(settings.m_textMessage)); + } + + if (channelSettingsKeys.contains("bytesMessage") || force) + { + swgChirpChatModSettings->setBytesMessage(new QList); + QList *bytesStr = swgChirpChatModSettings-> getBytesMessage(); + + for (QByteArray::const_iterator it = settings.m_bytesMessage.begin(); it != settings.m_bytesMessage.end(); ++it) + { + unsigned char b = *it; + bytesStr->push_back(new QString(tr("%1").arg(b, 2, 16, QChar('0')))); + } + } + + if (channelSettingsKeys.contains("messageRepeat") || force) { + swgChirpChatModSettings->setMessageRepeat(settings.m_messageRepeat); + } + + if (channelSettingsKeys.contains("rgbColor") || force) { + swgChirpChatModSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgChirpChatModSettings->setTitle(new QString(settings.m_title)); + } +} + +void ChirpChatMod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "ChirpChatMod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("ChirpChatMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +double ChirpChatMod::getMagSq() const +{ + return m_basebandSource->getMagSq(); +} + +void ChirpChatMod::setLevelMeter(QObject *levelMeter) +{ + connect(m_basebandSource, SIGNAL(levelChanged(qreal, qreal, int)), levelMeter, SLOT(levelChanged(qreal, qreal, int))); +} + +uint32_t ChirpChatMod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSinkStreams(); +} + +bool ChirpChatMod::getModulatorActive() const +{ + return m_basebandSource->getActive(); +} diff --git a/plugins/channeltx/modchirpchat/chirpchatmod.h b/plugins/channeltx/modchirpchat/chirpchatmod.h new file mode 100644 index 000000000..9734e465e --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmod.h @@ -0,0 +1,180 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMOD_H_ +#define PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMOD_H_ + +#include +#include +#include + +#include +#include + +#include "dsp/basebandsamplesource.h" +#include "channel/channelapi.h" +#include "util/message.h" + +#include "chirpchatmodsettings.h" +#include "chirpchatmodencoder.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class DeviceAPI; +class CWKeyer; +class ChirpChatModBaseband; + +class ChirpChatMod : public BasebandSampleSource, public ChannelAPI { + Q_OBJECT + +public: + class MsgConfigureChirpChatMod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const ChirpChatModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureChirpChatMod* create(const ChirpChatModSettings& settings, bool force) + { + return new MsgConfigureChirpChatMod(settings, force); + } + + private: + ChirpChatModSettings m_settings; + bool m_force; + + MsgConfigureChirpChatMod(const ChirpChatModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgReportPayloadTime : public Message { + MESSAGE_CLASS_DECLARATION + + public: + float getPayloadTimeMs() const { return m_timeMs; } + static MsgReportPayloadTime* create(float timeMs) { + return new MsgReportPayloadTime(timeMs); + } + + private: + float m_timeMs; //!< time in milliseconds + + MsgReportPayloadTime(float timeMs) : + Message(), + m_timeMs(timeMs) + {} + }; + + //================================================================= + + ChirpChatMod(DeviceAPI *deviceAPI); + virtual ~ChirpChatMod(); + virtual void destroy() { delete this; } + + virtual void start(); + virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) { id = m_channelId; } + virtual const QString& getURI() const { return m_channelIdURI; } + virtual void getTitle(QString& title) { title = m_settings.m_title; } + virtual qint64 getCenterFrequency() const { return m_settings.m_inputFrequencyOffset; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int getNbSinkStreams() const { return 1; } + virtual int getNbSourceStreams() const { return 0; } + + virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const + { + (void) streamIndex; + (void) sinkElseSource; + return m_settings.m_inputFrequencyOffset; + } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const ChirpChatModSettings& settings); + + static void webapiUpdateChannelSettings( + ChirpChatModSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + double getMagSq() const; + CWKeyer *getCWKeyer(); + void setLevelMeter(QObject *levelMeter); + uint32_t getNumberOfDeviceStreams() const; + bool getModulatorActive() const; + + static const QString m_channelIdURI; + static const QString m_channelId; + +private: + DeviceAPI* m_deviceAPI; + QThread *m_thread; + ChirpChatModBaseband* m_basebandSource; + ChirpChatModEncoder m_encoder; // TODO: check if it needs to be on its own thread + ChirpChatModSettings m_settings; + float m_currentPayloadTime; + + SampleVector m_sampleBuffer; + QMutex m_settingsMutex; + + int m_sampleRate; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void applySettings(const ChirpChatModSettings& settings, bool force = false); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + void webapiReverseSendSettings(QList& channelSettingsKeys, const ChirpChatModSettings& settings, bool force); + void featuresSendSettings(QList& channelSettingsKeys, const ChirpChatModSettings& settings, bool force); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const ChirpChatModSettings& settings, + bool force + ); + +private slots: + void networkManagerFinished(QNetworkReply *reply); +}; + + +#endif /* PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMOD_H_ */ diff --git a/plugins/channeltx/modchirpchat/chirpchatmodbaseband.cpp b/plugins/channeltx/modchirpchat/chirpchatmodbaseband.cpp new file mode 100644 index 000000000..ebd1cd4d3 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodbaseband.cpp @@ -0,0 +1,201 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upchannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "chirpchatmodbaseband.h" + +MESSAGE_CLASS_DEFINITION(ChirpChatModBaseband::MsgConfigureChirpChatModBaseband, Message) +MESSAGE_CLASS_DEFINITION(ChirpChatModBaseband::MsgConfigureChirpChatModPayload, Message) + +ChirpChatModBaseband::ChirpChatModBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpChannelizer(&m_source); + + qDebug("ChirpChatModBaseband::ChirpChatModBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &ChirpChatModBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +ChirpChatModBaseband::~ChirpChatModBaseband() +{ + delete m_channelizer; +} + +void ChirpChatModBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void ChirpChatModBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void ChirpChatModBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + qreal rmsLevel, peakLevel; + int numSamples; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } + + m_source.getLevels(rmsLevel, peakLevel, numSamples); + emit levelChanged(rmsLevel, peakLevel, numSamples); +} + +void ChirpChatModBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void ChirpChatModBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool ChirpChatModBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureChirpChatModBaseband::match(cmd)) + { + qDebug() << "ChirpChatModBaseband::handleMessage: MsgConfigureChirpChatModBaseband"; + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureChirpChatModBaseband& cfg = (MsgConfigureChirpChatModBaseband&) cmd; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureChirpChatModPayload::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureChirpChatModPayload& cfg = (MsgConfigureChirpChatModPayload&) cmd; + qDebug() << "ChirpChatModBaseband::handleMessage: MsgConfigureChirpChatModPayload:" << cfg.getPayload().size(); + m_source.setSymbols(cfg.getPayload()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(notif.getSampleRate())); + qDebug() << "ChirpChatModBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_source.applyChannelSettings( + m_channelizer->getChannelSampleRate(), + ChirpChatModSettings::bandwidths[m_settings.m_bandwidthIndex], + m_channelizer->getChannelFrequencyOffset() + ); + + return true; + } + else + { + return false; + } +} + +void ChirpChatModBaseband::applySettings(const ChirpChatModSettings& settings, bool force) +{ + if ((settings.m_bandwidthIndex != m_settings.m_bandwidthIndex) + || (settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + int thisBW = ChirpChatModSettings::bandwidths[settings.m_bandwidthIndex]; + m_channelizer->setChannelization( + thisBW * ChirpChatModSettings::oversampling, + settings.m_inputFrequencyOffset + ); + m_source.applyChannelSettings( + m_channelizer->getChannelSampleRate(), + thisBW, + m_channelizer->getChannelFrequencyOffset() + ); + } + + m_source.applySettings(settings, force); + + m_settings = settings; +} + +int ChirpChatModBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} diff --git a/plugins/channeltx/modchirpchat/chirpchatmodbaseband.h b/plugins/channeltx/modchirpchat/chirpchatmodbaseband.h new file mode 100644 index 000000000..87f55a93d --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodbaseband.h @@ -0,0 +1,119 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_CHIRPCHATMODBASEBAND_H +#define INCLUDE_CHIRPCHATMODBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "chirpchatmodsource.h" + +class UpChannelizer; + +class ChirpChatModBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureChirpChatModBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const ChirpChatModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureChirpChatModBaseband* create(const ChirpChatModSettings& settings, bool force) + { + return new MsgConfigureChirpChatModBaseband(settings, force); + } + + private: + ChirpChatModSettings m_settings; + bool m_force; + + MsgConfigureChirpChatModBaseband(const ChirpChatModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureChirpChatModPayload : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const std::vector& getPayload() const { return m_payload; } + + static MsgConfigureChirpChatModPayload* create() { + return new MsgConfigureChirpChatModPayload(); + } + static MsgConfigureChirpChatModPayload* create(const std::vector& payload) { + return new MsgConfigureChirpChatModPayload(payload); + } + + private: + std::vector m_payload; + + MsgConfigureChirpChatModPayload() : // This is empty payload notification + Message() + {} + MsgConfigureChirpChatModPayload(const std::vector& payload) : + Message() + { m_payload = payload; } + }; + + ChirpChatModBaseband(); + ~ChirpChatModBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + double getMagSq() const { return m_source.getMagSq(); } + int getChannelSampleRate() const; + bool getActive() const { return m_source.getActive(); } + +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: + SampleSourceFifo m_sampleFifo; + UpChannelizer *m_channelizer; + ChirpChatModSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + ChirpChatModSettings m_settings; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const ChirpChatModSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + + +#endif // INCLUDE_CHIRPCHATMODBASEBAND_H diff --git a/plugins/channeltx/modchirpchat/chirpchatmodencoder.cpp b/plugins/channeltx/modchirpchat/chirpchatmodencoder.cpp new file mode 100644 index 000000000..eb2c3ce5d --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodencoder.cpp @@ -0,0 +1,94 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "chirpchatmodencoder.h" +#include "chirpchatmodencodertty.h" +#include "chirpchatmodencoderascii.h" +#include "chirpchatmodencoderlora.h" + +ChirpChatModEncoder::ChirpChatModEncoder() : + m_codingScheme(ChirpChatModSettings::CodingTTY), + m_nbSymbolBits(5), + m_nbParityBits(1), + m_hasCRC(true), + m_hasHeader(true) +{} + +ChirpChatModEncoder::~ChirpChatModEncoder() +{} + +void ChirpChatModEncoder::setNbSymbolBits(unsigned int spreadFactor, unsigned int deBits) +{ + m_spreadFactor = spreadFactor; + + if (deBits >= spreadFactor) { + m_deBits = m_spreadFactor - 1; + } else { + m_deBits = deBits; + } + + m_nbSymbolBits = m_spreadFactor - m_deBits; +} + +void ChirpChatModEncoder::encodeString(const QString& str, std::vector& symbols) +{ + switch (m_codingScheme) + { + case ChirpChatModSettings::CodingTTY: + if (m_nbSymbolBits == 5) { + ChirpChatModEncoderTTY::encodeString(str, symbols); + } + break; + case ChirpChatModSettings::CodingASCII: + if (m_nbSymbolBits == 7) { + ChirpChatModEncoderASCII::encodeString(str, symbols); + } + break; + case ChirpChatModSettings::CodingLoRa: + if (m_nbSymbolBits >= 5) + { + QByteArray bytes = str.toUtf8(); + encodeBytesLoRa(bytes, symbols); + } + break; + default: + break; + } +} + +void ChirpChatModEncoder::encodeBytes(const QByteArray& bytes, std::vector& symbols) +{ + switch (m_codingScheme) + { + case ChirpChatModSettings::CodingLoRa: + encodeBytesLoRa(bytes, symbols); + break; + default: + break; + }; +} + +void ChirpChatModEncoder::encodeBytesLoRa(const QByteArray& bytes, std::vector& symbols) +{ + QByteArray payload(bytes); + + if (m_hasCRC) { + ChirpChatModEncoderLoRa::addChecksum(payload); + } + + ChirpChatModEncoderLoRa::encodeBytes(payload, symbols, m_nbSymbolBits, m_hasHeader, m_hasCRC, m_nbParityBits); +} \ No newline at end of file diff --git a/plugins/channeltx/modchirpchat/chirpchatmodencoder.h b/plugins/channeltx/modchirpchat/chirpchatmodencoder.h new file mode 100644 index 000000000..c7778f56b --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodencoder.h @@ -0,0 +1,54 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODER_H_ +#define PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODER_H_ + +#include +#include "chirpchatmodsettings.h" + +class ChirpChatModEncoder +{ +public: + ChirpChatModEncoder(); + ~ChirpChatModEncoder(); + + void setCodingScheme(ChirpChatModSettings::CodingScheme codingScheme) { m_codingScheme = codingScheme; } + void setNbSymbolBits(unsigned int spreadFactor, unsigned int deBits); + void setLoRaParityBits(unsigned int parityBits) { m_nbParityBits = parityBits; } + void setLoRaHasHeader(bool hasHeader) { m_hasHeader = hasHeader; } + void setLoRaHasCRC(bool hasCRC) { m_hasCRC = hasCRC; } + void encodeString(const QString& str, std::vector& symbols); + void encodeBytes(const QByteArray& bytes, std::vector& symbols); + +private: + // LoRa functions + void encodeBytesLoRa(const QByteArray& bytes, std::vector& symbols); + + // General attributes + ChirpChatModSettings::CodingScheme m_codingScheme; + unsigned int m_spreadFactor; + unsigned int m_deBits; + unsigned int m_nbSymbolBits; + // LoRa attributes + unsigned int m_nbParityBits; //!< 1 to 4 Hamming FEC bits for 4 payload bits + bool m_hasCRC; + bool m_hasHeader; +}; + +#endif // PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODER_H_ + diff --git a/plugins/channeltx/modchirpchat/chirpchatmodencoderascii.cpp b/plugins/channeltx/modchirpchat/chirpchatmodencoderascii.cpp new file mode 100644 index 000000000..d4258fd49 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodencoderascii.cpp @@ -0,0 +1,28 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "chirpchatmodencoderascii.h" + +void ChirpChatModEncoderASCII::encodeString(const QString& str, std::vector& symbols) +{ + QByteArray asciiStr = str.toUtf8(); + QByteArray::const_iterator it = asciiStr.begin(); + + for (; it != asciiStr.end(); ++it) { + symbols.push_back(*it & 0x7F); + } +} diff --git a/plugins/channeltx/modchirpchat/chirpchatmodencoderascii.h b/plugins/channeltx/modchirpchat/chirpchatmodencoderascii.h new file mode 100644 index 000000000..6c83acdc1 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodencoderascii.h @@ -0,0 +1,30 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODERASCII_H_ +#define PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODERASCII_H_ + +#include +#include + +class ChirpChatModEncoderASCII +{ +public: + static void encodeString(const QString& str, std::vector& symbols); +}; + +#endif // PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODERASCII_H_ diff --git a/plugins/channeltx/modchirpchat/chirpchatmodencoderlora.cpp b/plugins/channeltx/modchirpchat/chirpchatmodencoderlora.cpp new file mode 100644 index 000000000..6f44f9fab --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodencoderlora.cpp @@ -0,0 +1,148 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// Inspired by: https://github.com/myriadrf/LoRa-SDR // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "chirpchatmodencoderlora.h" + +void ChirpChatModEncoderLoRa::addChecksum(QByteArray& bytes) +{ + uint16_t crc = sx1272DataChecksum(reinterpret_cast(bytes.data()), bytes.size()); + bytes.append(crc & 0xff); + bytes.append((crc >> 8) & 0xff); +} + +void ChirpChatModEncoderLoRa::encodeBytes( + const QByteArray& bytes, + std::vector& symbols, + unsigned int nbSymbolBits, + bool hasHeader, + bool hasCRC, + unsigned int nbParityBits +) +{ + if (nbSymbolBits < 5) { + return; + } + + const unsigned int numCodewords = roundUp(bytes.size()*2 + (hasHeader ? headerCodewords : 0), nbSymbolBits); // uses payload + CRC for encoding size + unsigned int cOfs = 0; + unsigned int dOfs = 0; + + std::vector codewords(numCodewords); + + if (hasHeader) + { + std::vector hdr(3); + unsigned int payloadSize = bytes.size() - (hasCRC ? 2 : 0); // actual payload size is without CRC + hdr[0] = payloadSize % 256; + hdr[1] = (hasCRC ? 1 : 0) | (nbParityBits << 1); + hdr[2] = headerChecksum(hdr.data()); + + // Nibble decomposition and parity bit(s) addition. LSNibble first. + codewords[cOfs++] = encodeHamming84sx(hdr[0] >> 4); + codewords[cOfs++] = encodeHamming84sx(hdr[0] & 0xf); // length + codewords[cOfs++] = encodeHamming84sx(hdr[1] & 0xf); // crc / fec info + codewords[cOfs++] = encodeHamming84sx(hdr[2] >> 4); // checksum + codewords[cOfs++] = encodeHamming84sx(hdr[2] & 0xf); + } + + unsigned int headerSize = cOfs; + + // fill nbSymbolBits codewords with 8 bit codewords using payload data (ecode and whiten) + encodeFec(codewords, 4, cOfs, dOfs, reinterpret_cast(bytes.data()), nbSymbolBits - headerSize); + Sx1272ComputeWhitening(codewords.data() + headerSize, nbSymbolBits - headerSize, 0, headerParityBits); + + // encode and whiten the rest of the payload with 4 + nbParityBits bits codewords + if (numCodewords > nbSymbolBits) + { + unsigned int cOfs2 = cOfs; + encodeFec(codewords, nbParityBits, cOfs, dOfs, reinterpret_cast(bytes.data()), numCodewords - nbSymbolBits); + Sx1272ComputeWhitening(codewords.data() + cOfs2, numCodewords - nbSymbolBits, nbSymbolBits - headerSize, nbParityBits); + } + + // header is always coded with 8 bits and yields exactly 8 symbols (headerSymbols) + const unsigned int numSymbols = headerSymbols + (numCodewords / nbSymbolBits - 1) * (4 + nbParityBits); + + // interleave the codewords into symbols + symbols.clear(); + symbols.resize(numSymbols); + diagonalInterleaveSx(codewords.data(), nbSymbolBits, symbols.data(), nbSymbolBits, headerParityBits); + + if (numCodewords > nbSymbolBits) { + diagonalInterleaveSx(codewords.data() + nbSymbolBits, numCodewords - nbSymbolBits, symbols.data() + headerSymbols, nbSymbolBits, nbParityBits); + } + + // gray decode + for (auto &sym : symbols) { + sym = grayToBinary16(sym); + } +} + +void ChirpChatModEncoderLoRa::encodeFec( + std::vector &codewords, + unsigned int nbParityBits, + unsigned int& cOfs, + unsigned int& dOfs, + const uint8_t *bytes, + const unsigned int codewordCount +) +{ + for (unsigned int i = 0; i < codewordCount; i++, dOfs++) + { + if (nbParityBits == 1) + { + if (dOfs % 2 == 1) { + codewords[cOfs++] = encodeParity54(bytes[dOfs/2] >> 4); + } else { + codewords[cOfs++] = encodeParity54(bytes[dOfs/2] & 0xf); + } + } + else if (nbParityBits == 2) + { + if (dOfs % 2 == 1) { + codewords[cOfs++] = encodeParity64(bytes[dOfs/2] >> 4); + } else { + codewords[cOfs++] = encodeParity64(bytes[dOfs/2] & 0xf); + } + } + else if (nbParityBits == 3) + { + if (dOfs % 2 == 1) { + codewords[cOfs++] = encodeHamming74sx(bytes[dOfs/2] >> 4); + } else { + codewords[cOfs++] = encodeHamming74sx(bytes[dOfs/2] & 0xf); + } + } + else if (nbParityBits == 4) + { + if (dOfs % 2 == 1) { + codewords[cOfs++] = encodeHamming84sx(bytes[dOfs/2] >> 4); + } else { + codewords[cOfs++] = encodeHamming84sx(bytes[dOfs/2] & 0xf); + } + } + else + { + if (dOfs % 2 == 1) { + codewords[cOfs++] = bytes[dOfs/2] >> 4; + } else { + codewords[cOfs++] = bytes[dOfs/2] & 0xf; + } + } + } +} diff --git a/plugins/channeltx/modchirpchat/chirpchatmodencoderlora.h b/plugins/channeltx/modchirpchat/chirpchatmodencoderlora.h new file mode 100644 index 000000000..6b47a5354 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodencoderlora.h @@ -0,0 +1,274 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// Inspired by: https://github.com/myriadrf/LoRa-SDR // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODERLORA_H_ +#define PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODERLORA_H_ + +#include +#include + +class ChirpChatModEncoderLoRa +{ +public: + static void addChecksum(QByteArray& bytes); + static void encodeBytes( + const QByteArray& bytes, + std::vector& symbols, + unsigned int nbSymbolBits, + bool hasHeader, + bool hasCRC, + unsigned int nbParityBits + ); + +private: + static void encodeFec( + std::vector &codewords, + unsigned int nbParityBits, + unsigned int& cOfs, + unsigned int& dOfs, + const uint8_t *bytes, + const unsigned int codewordCount + ); + + static const unsigned int headerParityBits = 4; + static const unsigned int headerSymbols = 8; + static const unsigned int headerCodewords = 5; + + /*********************************************************************** + * Round functions + **********************************************************************/ + static inline unsigned roundUp(unsigned num, unsigned factor) + { + return ((num + factor - 1) / factor) * factor; + } + + /*********************************************************************** + * Encode a 4 bit word into a 8 bits with parity + * Non standard version used in sx1272. + * https://en.wikipedia.org/wiki/Hamming_code + **********************************************************************/ + static inline unsigned char encodeHamming84sx(const unsigned char x) + { + auto d0 = (x >> 0) & 0x1; + auto d1 = (x >> 1) & 0x1; + auto d2 = (x >> 2) & 0x1; + auto d3 = (x >> 3) & 0x1; + + unsigned char b = x & 0xf; + b |= (d0 ^ d1 ^ d2) << 4; + b |= (d1 ^ d2 ^ d3) << 5; + b |= (d0 ^ d1 ^ d3) << 6; + b |= (d0 ^ d2 ^ d3) << 7; + return b; + } + + /*********************************************************************** + * Encode a 4 bit word into a 7 bits with parity. + * Non standard version used in sx1272. + **********************************************************************/ + static inline unsigned char encodeHamming74sx(const unsigned char x) + { + auto d0 = (x >> 0) & 0x1; + auto d1 = (x >> 1) & 0x1; + auto d2 = (x >> 2) & 0x1; + auto d3 = (x >> 3) & 0x1; + + unsigned char b = x & 0xf; + b |= (d0 ^ d1 ^ d2) << 4; + b |= (d1 ^ d2 ^ d3) << 5; + b |= (d0 ^ d1 ^ d3) << 6; + return b; + } + + /*********************************************************************** + * Encode a 4 bit word into a 6 bits with parity. + **********************************************************************/ + static inline unsigned char encodeParity64(const unsigned char b) + { + auto x = b ^ (b >> 1) ^ (b >> 2); + auto y = x ^ b ^ (b >> 3); + return ((x & 1) << 4) | ((y & 1) << 5) | (b & 0xf); + } + + /*********************************************************************** + * Encode a 4 bit word into a 5 bits with parity. + **********************************************************************/ + static inline unsigned char encodeParity54(const unsigned char b) + { + auto x = b ^ (b >> 2); + x = x ^ (x >> 1); + return (b & 0xf) | ((x << 4) & 0x10); + } + + /*********************************************************************** + * CRC reverse engineered from Sx1272 data stream. + * Modified CCITT crc with masking of the output with an 8bit lfsr + **********************************************************************/ + static inline uint16_t crc16sx(uint16_t crc, const uint16_t poly) + { + for (int i = 0; i < 8; i++) + { + if (crc & 0x8000) { + crc = (crc << 1) ^ poly; + } else { + crc <<= 1; + } + } + + return crc; + } + + static inline uint8_t xsum8(uint8_t t) + { + t ^= t >> 4; + t ^= t >> 2; + t ^= t >> 1; + + return (t & 1); + } + + static inline uint16_t sx1272DataChecksum(const uint8_t *data, int length) + { + uint16_t res = 0; + uint8_t v = 0xff; + uint16_t crc = 0; + + for (int i = 0; i < length; i++) + { + crc = crc16sx(res, 0x1021); + v = xsum8(v & 0xB8) | (v << 1); + res = crc ^ data[i]; + } + + res ^= v; + v = xsum8(v & 0xB8) | (v << 1); + res ^= v << 8; + + return res; + } + + /*********************************************************************** + * Specific checksum for header + **********************************************************************/ + static inline uint8_t headerChecksum(const uint8_t *h) + { + auto a0 = (h[0] >> 4) & 0x1; + auto a1 = (h[0] >> 5) & 0x1; + auto a2 = (h[0] >> 6) & 0x1; + auto a3 = (h[0] >> 7) & 0x1; + + auto b0 = (h[0] >> 0) & 0x1; + auto b1 = (h[0] >> 1) & 0x1; + auto b2 = (h[0] >> 2) & 0x1; + auto b3 = (h[0] >> 3) & 0x1; + + auto c0 = (h[1] >> 0) & 0x1; + auto c1 = (h[1] >> 1) & 0x1; + auto c2 = (h[1] >> 2) & 0x1; + auto c3 = (h[1] >> 3) & 0x1; + + uint8_t res; + res = (a0 ^ a1 ^ a2 ^ a3) << 4; + res |= (a3 ^ b1 ^ b2 ^ b3 ^ c0) << 3; + res |= (a2 ^ b0 ^ b3 ^ c1 ^ c3) << 2; + res |= (a1 ^ b0 ^ b2 ^ c0 ^ c1 ^ c2) << 1; + res |= a0 ^ b1 ^ c0 ^ c1 ^ c2 ^ c3; + + return res; + } + + /*********************************************************************** + * Whitening generator reverse engineered from Sx1272 data stream. + * Each bit of a codeword is combined with the output from a different position in the whitening sequence. + **********************************************************************/ + static inline void Sx1272ComputeWhitening(uint8_t *buffer, uint16_t bufferSize, const int bitOfs, const int nbParityBits) + { + static const int ofs0[8] = {6,4,2,0,-112,-114,-302,-34 }; // offset into sequence for each bit + static const int ofs1[5] = {6,4,2,0,-360 }; // different offsets used for single parity mode (1 == nbParityBits) + static const int whiten_len = 510; // length of whitening sequence + static const uint64_t whiten_seq[8] = { // whitening sequence + 0x0102291EA751AAFFL,0xD24B050A8D643A17L,0x5B279B671120B8F4L,0x032B37B9F6FB55A2L, + 0x994E0F87E95E2D16L,0x7CBCFC7631984C26L,0x281C8E4F0DAEF7F9L,0x1741886EB7733B15L + }; + const int *ofs = (1 == nbParityBits) ? ofs1 : ofs0; + int i, j; + + for (j = 0; j < bufferSize; j++) + { + uint8_t x = 0; + + for (i = 0; i < 4 + nbParityBits; i++) + { + int t = (ofs[i] + j + bitOfs + whiten_len) % whiten_len; + + if (whiten_seq[t >> 6] & ((uint64_t)1 << (t & 0x3F))) { + x |= 1 << i; + } + } + + buffer[j] ^= x; + } + } + + /*********************************************************************** + * Diagonal interleaver + deinterleaver + **********************************************************************/ + static inline void diagonalInterleaveSx( + const uint8_t *codewords, + const size_t numCodewords, + uint16_t *symbols, + const size_t nbSymbolBits, + const size_t nbParityBits + ) + { + for (size_t x = 0; x < numCodewords / nbSymbolBits; x++) + { + const size_t cwOff = x*nbSymbolBits; + const size_t symOff = x*(4 + nbParityBits); + + for (size_t k = 0; k < 4 + nbParityBits; k++) + { + for (size_t m = 0; m < nbSymbolBits; m++) + { + const size_t i = (m + k + nbSymbolBits) % nbSymbolBits; + const auto bit = (codewords[cwOff + i] >> k) & 0x1; + symbols[symOff + k] |= (bit << m); + } + } + } + } + + /*********************************************************************** + * https://en.wikipedia.org/wiki/Gray_code + **********************************************************************/ + + /* + * A more efficient version, for Gray codes of 16 or fewer bits. + */ + static inline unsigned short grayToBinary16(unsigned short num) + { + num = num ^ (num >> 8); + num = num ^ (num >> 4); + num = num ^ (num >> 2); + num = num ^ (num >> 1); + return num; + } +}; + +#endif // PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODERLORA_H_ diff --git a/plugins/channeltx/modchirpchat/chirpchatmodencodertty.cpp b/plugins/channeltx/modchirpchat/chirpchatmodencodertty.cpp new file mode 100644 index 000000000..b7f4f4bf1 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodencodertty.cpp @@ -0,0 +1,133 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "chirpchatmodencodertty.h" + +const signed char ChirpChatModEncoderTTY::asciiToTTYLetters[128] = { +// '\x00' '\x01' '\x02' '\x03' '\x04' '\x05' '\x06' '\x07' + 0x00, -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '\x08' '\t' '\n' '\x0b' '\x0c' '\r' '\x0e' '\x0f' + -1 , -1 , 0x02, -1 , -1 , 0x08, -1 , -1 , +// '\x10' '\x11' '\x12' '\x13' '\x14' '\x15' '\x16' '\x17' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '\x18' '\x19' '\x1a' '\x1b' '\x1c' '\x1d' '\x1e' '\x1f' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// ' ' '!' '"' '#' '$' '%' '&' "'" + 0x04, -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '(' ')' '*' '+' ',' '-' '.' '/' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '0' '1' '2' '3' '4' '5' '6' '7' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '8' '9' ':' ';' '<' '=' '>' '?' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '@' 'A' 'B' 'C' 'D' 'E' 'F' 'G' + -1 , 0x03, 0x19, 0x0e, 0x09, 0x01, 0x0d, 0x1a, +// 'H' 'I' 'J' 'K' 'L' 'M' 'N' 'O' + 0x14, 0x06, 0x0b, 0x0f, 0x12, 0x1c, 0x0c, 0x18, +// 'P' 'Q' 'R' 'S' 'T' 'U' 'V' 'W' + 0x16, 0x17, 0x0a, 0x05, 0x10, 0x07, 0x1e, 0x13, +// 'X' 'Y' 'Z' '[' '\\' ']' '^' '_' + 0x1d, 0x15, 0x11, -1 , -1 , -1 , -1 , -1 , +// '`' 'a' 'b' 'c' 'd' 'e' 'f' 'g' + -1 , 0x03, 0x19, 0x0e, 0x09, 0x01, 0x0d, 0x1a, +// 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' + 0x14, 0x06, 0x0b, 0x0f, 0x12, 0x1c, 0x0c, 0x18, +// 'p' 'q' 'r' 's' 't' 'u' 'v' 'w' + 0x16, 0x17, 0x0a, 0x05, 0x10, 0x07, 0x1e, 0x13, +// 'x' 'y' 'z' '{' '|' '}' '~' '\x7f' + 0x1d, 0x15, 0x11, -1 , -1 , -1 , -1 , -1 + }; + +const signed char ChirpChatModEncoderTTY::asciiToTTYFigures[128] = { +// '\x00' '\x01' '\x02' '\x03' '\x04' '\x05' '\x06' '\x07' + 0x00, -1 , -1 , -1 , -1 , -1 , -1 , 0x05, +// '\x08' '\t' '\n' '\x0b' '\x0c' '\r' '\x0e' '\x0f' + -1 , -1 , 0x02, -1 , -1 , 0x08, -1 , -1 , +// '\x10' '\x11' '\x12' '\x13' '\x14' '\x15' '\x16' '\x17' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '\x18' '\x19' '\x1a' '\x1b' '\x1c' '\x1d' '\x1e' '\x1f' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// ' ' '!' '"' '#' '$' '%' '&' "'" + 0x04, 0x0d, 0x11, 0x14, 0x09, -1 , 0x1a, -1 , +// '(' ')' '*' '+' ',' '-' '.' '/' + 0x0f, 0x12, -1 , -1 , 0x0c, 0x03, 0x1c, 0x1d, +// '0' '1' '2' '3' '4' '5' '6' '7' + 0x16, 0x17, 0x13, 0x01, 0x0a, 0x10, 0x15, 0x07, +// '8' '9' ':' ';' '<' '=' '>' '?' + 0x06, 0x18, 0x0e, 0x1e, -1 , -1 , -1 , 0x19, +// '@' 'A' 'B' 'C' 'D' 'E' 'F' 'G' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'H' 'I' 'J' 'K' 'L' 'M' 'N' 'O' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'P' 'Q' 'R' 'S' 'T' 'U' 'V' 'W' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'X' 'Y' 'Z' '[' '\\' ']' '^' '_' + -1 , -1 , -1 , -1 , 0x0b, -1 , -1 , -1 , +// '`' 'a' 'b' 'c' 'd' 'e' 'f' 'g' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'p' 'q' 'r' 's' 't' 'u' 'v' 'w' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'x' 'y' 'z' '{' '|' '}' '~' '\x7f' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 + }; + +void ChirpChatModEncoderTTY::encodeString(const QString& str, std::vector& symbols) +{ + TTYState ttyState = TTYLetters; + QByteArray asciiStr = str.toUtf8(); + QByteArray::const_iterator it = asciiStr.begin(); + + for (; it != asciiStr.end(); ++it) + { + char asciiChar = *it & 0x7F; + int ttyLetter = asciiToTTYLetters[asciiChar]; + int ttyFigure = asciiToTTYFigures[asciiChar]; + + if (ttyLetter < 0) + { + if (ttyFigure >= 0) + { + if (ttyState != TTYFigures) + { + symbols.push_back(ttyFigures); + ttyState = TTYFigures; + } + + symbols.push_back(ttyFigure); + } // else skip + } + else + { + if (ttyFigure >= 0) + { + symbols.push_back(ttyFigure); // same TTY character no state change + } + else + { + if (ttyState != TTYLetters) + { + symbols.push_back(ttyLetters); + ttyState = TTYLetters; + } + + symbols.push_back(ttyLetter); + } + } + } +} diff --git a/plugins/channeltx/modchirpchat/chirpchatmodencodertty.h b/plugins/channeltx/modchirpchat/chirpchatmodencodertty.h new file mode 100644 index 000000000..ede968710 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodencodertty.h @@ -0,0 +1,42 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODERTTY_H_ +#define PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODERTTY_H_ + +#include +#include + +class ChirpChatModEncoderTTY +{ +public: + static void encodeString(const QString& str, std::vector& symbols); + +private: + enum TTYState + { + TTYLetters, + TTYFigures + }; + + static const signed char asciiToTTYLetters[128]; + static const signed char asciiToTTYFigures[128]; + static const char ttyLetters = 0x1f; + static const char ttyFigures = 0x1b; +}; + +#endif // PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODENCODERTTY_H_ \ No newline at end of file diff --git a/plugins/channeltx/modchirpchat/chirpchatmodgui.cpp b/plugins/channeltx/modchirpchat/chirpchatmodgui.cpp new file mode 100644 index 000000000..c73eab1f1 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodgui.cpp @@ -0,0 +1,593 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +#include "device/deviceuiset.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/db.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "gui/crightclickenabler.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "maincore.h" + +#include "ui_chirpchatmodgui.h" +#include "chirpchatmodgui.h" + + +ChirpChatModGUI* ChirpChatModGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx) +{ + ChirpChatModGUI* gui = new ChirpChatModGUI(pluginAPI, deviceUISet, channelTx); + return gui; +} + +void ChirpChatModGUI::destroy() +{ + delete this; +} + +void ChirpChatModGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray ChirpChatModGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool ChirpChatModGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool ChirpChatModGUI::handleMessage(const Message& message) +{ + if (ChirpChatMod::MsgConfigureChirpChatMod::match(message)) + { + const ChirpChatMod::MsgConfigureChirpChatMod& cfg = (ChirpChatMod::MsgConfigureChirpChatMod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (ChirpChatMod::MsgReportPayloadTime::match(message)) + { + const ChirpChatMod::MsgReportPayloadTime& rpt = (ChirpChatMod::MsgReportPayloadTime&) message; + float fourthsMs = ((1<timePayloadText->setText(tr("%1 ms").arg(QString::number(rpt.getPayloadTimeMs(), 'f', 0))); + ui->timeTotalText->setText(tr("%1 ms").arg(QString::number(rpt.getPayloadTimeMs() + controlMs, 'f', 0))); + ui->timeSymbolText->setText(tr("%1 ms").arg(QString::number(4.0*fourthsMs, 'f', 1))); + return true; + } + else if (DSPSignalNotification::match(message)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) message; + int basebandSampleRate = notif.getSampleRate(); + qDebug() << "ChirpChatModGUI::handleMessage: DSPSignalNotification: m_basebandSampleRate: " << basebandSampleRate; + + if (basebandSampleRate != m_basebandSampleRate) + { + m_basebandSampleRate = basebandSampleRate; + setBandwidths(); + } + + return true; + } + else + { + return false; + } +} + +void ChirpChatModGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void ChirpChatModGUI::handleSourceMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void ChirpChatModGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void ChirpChatModGUI::on_bw_valueChanged(int value) +{ + if (value < 0) { + m_settings.m_bandwidthIndex = 0; + } else if (value < ChirpChatModSettings::nbBandwidths) { + m_settings.m_bandwidthIndex = value; + } else { + m_settings.m_bandwidthIndex = ChirpChatModSettings::nbBandwidths - 1; + } + + int thisBW = ChirpChatModSettings::bandwidths[value]; + ui->bwText->setText(QString("%1 Hz").arg(thisBW)); + m_channelMarker.setBandwidth(thisBW); + + applySettings(); +} + +void ChirpChatModGUI::on_channelMute_toggled(bool checked) +{ + m_settings.m_channelMute = checked; + applySettings(); +} + +void ChirpChatModGUI::on_spread_valueChanged(int value) +{ + m_settings.m_spreadFactor = value; + ui->spreadText->setText(tr("%1").arg(value)); + + applySettings(); +} + +void ChirpChatModGUI::on_deBits_valueChanged(int value) +{ + m_settings.m_deBits = value; + ui->deBitsText->setText(tr("%1").arg(m_settings.m_deBits)); + applySettings(); +} + +void ChirpChatModGUI::on_preambleChirps_valueChanged(int value) +{ + m_settings.m_preambleChirps = value; + ui->preambleChirpsText->setText(tr("%1").arg(m_settings.m_preambleChirps)); + applySettings(); +} + +void ChirpChatModGUI::on_idleTime_valueChanged(int value) +{ + m_settings.m_quietMillis = value * 100; + ui->idleTimeText->setText(tr("%1").arg(m_settings.m_quietMillis / 1000.0, 0, 'f', 1)); + applySettings(); +} + +void ChirpChatModGUI::on_syncWord_editingFinished() +{ + bool ok; + unsigned int syncWord = ui->syncWord->text().toUInt(&ok, 16); + + if (ok) + { + m_settings.m_syncWord = syncWord > 255 ? 0 : syncWord; + applySettings(); + } +} + +void ChirpChatModGUI::on_scheme_currentIndexChanged(int index) +{ + m_settings.m_codingScheme = (ChirpChatModSettings::CodingScheme) index; + ui->fecParity->setEnabled(m_settings.m_codingScheme == ChirpChatModSettings::CodingLoRa); + ui->crc->setEnabled(m_settings.m_codingScheme == ChirpChatModSettings::CodingLoRa); + ui->header->setEnabled(m_settings.m_codingScheme == ChirpChatModSettings::CodingLoRa); + applySettings(); +} + +void ChirpChatModGUI::on_fecParity_valueChanged(int value) +{ + m_settings.m_nbParityBits = value; + ui->fecParityText->setText(tr("%1").arg(m_settings.m_nbParityBits)); + applySettings(); +} + +void ChirpChatModGUI::on_crc_stateChanged(int state) +{ + m_settings.m_hasCRC = (state == Qt::Checked); + applySettings(); +} + +void ChirpChatModGUI::on_header_stateChanged(int state) +{ + m_settings.m_hasHeader = (state == Qt::Checked); + applySettings(); +} + +void ChirpChatModGUI::on_myCall_editingFinished() +{ + m_settings.m_myCall = ui->myCall->text(); + applySettings(); +} + +void ChirpChatModGUI::on_urCall_editingFinished() +{ + m_settings.m_urCall = ui->urCall->text(); + applySettings(); +} + +void ChirpChatModGUI::on_myLocator_editingFinished() +{ + m_settings.m_myLoc = ui->myLocator->text(); + applySettings(); +} + +void ChirpChatModGUI::on_report_editingFinished() +{ + m_settings.m_myRpt = ui->report->text(); + applySettings(); +} + +void ChirpChatModGUI::on_msgType_currentIndexChanged(int index) +{ + m_settings.m_messageType = (ChirpChatModSettings::MessageType) index; + displayCurrentPayloadMessage(); + applySettings(); +} + +void ChirpChatModGUI::on_resetMessages_clicked(bool checked) +{ + (void) checked; + m_settings.setDefaultTemplates(); + displayCurrentPayloadMessage(); + applySettings(); +} + +void ChirpChatModGUI::on_playMessage_clicked(bool checked) +{ + (void) checked; + // Switch to message None then back to current message type to trigger sending process + ChirpChatModSettings::MessageType msgType = m_settings.m_messageType; + m_settings.m_messageType = ChirpChatModSettings::MessageNone; + applySettings(); + m_settings.m_messageType = msgType; + applySettings(); +} + +void ChirpChatModGUI::on_repeatMessage_valueChanged(int value) +{ + m_settings.m_messageRepeat = value; + ui->repeatText->setText(tr("%1").arg(m_settings.m_messageRepeat)); + applySettings(); +} + +void ChirpChatModGUI::on_generateMessages_clicked(bool checked) +{ + (void) checked; + m_settings.generateMessages(); + displayCurrentPayloadMessage(); + applySettings(); +} + +void ChirpChatModGUI::on_messageText_editingFinished() +{ + if (m_settings.m_messageType == ChirpChatModSettings::MessageBeacon) { + m_settings.m_beaconMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageCQ) { + m_settings.m_cqMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageReply) { + m_settings.m_replyMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageReport) { + m_settings.m_reportMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageReplyReport) { + m_settings.m_replyReportMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageRRR) { + m_settings.m_rrrMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == ChirpChatModSettings::Message73) { + m_settings.m_73Message = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageQSOText) { + m_settings.m_qsoTextMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageText) { + m_settings.m_textMessage = ui->messageText->toPlainText(); + } + + applySettings(); +} + +void ChirpChatModGUI::on_hexText_editingFinished() +{ + m_settings.m_bytesMessage = QByteArray::fromHex(ui->hexText->text().toLatin1()); + applySettings(); +} + +void ChirpChatModGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +void ChirpChatModGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicChannelSettingsDialog dialog(&m_channelMarker, this); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); + dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + m_settings.m_rgbColor = m_channelMarker.getColor().rgb(); + m_settings.m_title = m_channelMarker.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex(); + + setWindowTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + else if ((m_contextMenuType == ContextMenuStreamSettings) && (m_deviceUISet->m_deviceMIMOEngine)) + { + DeviceStreamSelectionDialog dialog(this); + dialog.setNumberOfStreams(m_chirpChatMod->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); + m_channelMarker.clearStreamIndexes(); + m_channelMarker.addStreamIndex(m_settings.m_streamIndex); + displayStreamIndex(); + applySettings(); + } + + resetContextMenuType(); +} + +ChirpChatModGUI::ChirpChatModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::ChirpChatModGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_basebandSampleRate(125000), + m_doApplySettings(true), + m_tickCount(0) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose, true); + + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + + m_chirpChatMod = (ChirpChatMod*) channelTx; + m_chirpChatMod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); + + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); + ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::red); + m_channelMarker.setBandwidth(12500); + m_channelMarker.setCenterFrequency(0); + m_channelMarker.setTitle("ChirpChat Modulator"); + m_channelMarker.setSourceOrSinkStream(false); + m_channelMarker.blockSignals(false); + m_channelMarker.setVisible(true); // activate signal on the last setting only + + m_deviceUISet->addChannelMarker(&m_channelMarker); + m_deviceUISet->addRollupWidget(this); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); + + m_settings.setChannelMarker(&m_channelMarker); + + setBandwidths(); + displaySettings(); + applySettings(); +} + +ChirpChatModGUI::~ChirpChatModGUI() +{ + delete ui; +} + +void ChirpChatModGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void ChirpChatModGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + ChirpChatMod::MsgConfigureChirpChatMod *msg = ChirpChatMod::MsgConfigureChirpChatMod::create(m_settings, force); + m_chirpChatMod->getInputMessageQueue()->push(msg); + } +} + +void ChirpChatModGUI::displaySettings() +{ + int thisBW = ChirpChatModSettings::bandwidths[m_settings.m_bandwidthIndex]; + + m_channelMarker.blockSignals(true); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setBandwidth(thisBW); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); + setTitleColor(m_settings.m_rgbColor); + + setWindowTitle(m_channelMarker.getTitle()); + displayStreamIndex(); + displayCurrentPayloadMessage(); + displayBinaryMessage(); + + ui->fecParity->setEnabled(m_settings.m_codingScheme == ChirpChatModSettings::CodingLoRa); + ui->crc->setEnabled(m_settings.m_codingScheme == ChirpChatModSettings::CodingLoRa); + ui->header->setEnabled(m_settings.m_codingScheme == ChirpChatModSettings::CodingLoRa); + + blockApplySettings(true); + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + ui->bwText->setText(QString("%1 Hz").arg(thisBW)); + ui->bw->setValue(m_settings.m_bandwidthIndex); + ui->spread->setValue(m_settings.m_spreadFactor); + ui->spreadText->setText(tr("%1").arg(m_settings.m_spreadFactor)); + ui->deBits->setValue(m_settings.m_deBits); + ui->deBitsText->setText(tr("%1").arg(m_settings.m_deBits)); + ui->preambleChirps->setValue(m_settings.m_preambleChirps); + ui->preambleChirpsText->setText(tr("%1").arg(m_settings.m_preambleChirps)); + ui->idleTime->setValue(m_settings.m_quietMillis / 100); + ui->idleTimeText->setText(tr("%1").arg(m_settings.m_quietMillis / 1000.0, 0, 'f', 1)); + ui->syncWord->setText((tr("%1").arg(m_settings.m_syncWord, 2, 16))); + ui->channelMute->setChecked(m_settings.m_channelMute); + ui->scheme->setCurrentIndex((int) m_settings.m_codingScheme); + ui->fecParity->setValue(m_settings.m_nbParityBits); + ui->fecParityText->setText(tr("%1").arg(m_settings.m_nbParityBits)); + ui->crc->setChecked(m_settings.m_hasCRC); + ui->header->setChecked(m_settings.m_hasHeader); + ui->myCall->setText(m_settings.m_myCall); + ui->urCall->setText(m_settings.m_urCall); + ui->myLocator->setText(m_settings.m_myLoc); + ui->report->setText(m_settings.m_myRpt); + ui->repeatMessage->setValue(m_settings.m_messageRepeat); + ui->repeatText->setText(tr("%1").arg(m_settings.m_messageRepeat)); + ui->msgType->setCurrentIndex((int) m_settings.m_messageType); + blockApplySettings(false); +} + +void ChirpChatModGUI::displayStreamIndex() +{ + if (m_deviceUISet->m_deviceMIMOEngine) { + setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex)); + } else { + setStreamIndicator("S"); // single channel indicator + } +} + +void ChirpChatModGUI::displayCurrentPayloadMessage() +{ + ui->messageText->blockSignals(true); + + if (m_settings.m_messageType == ChirpChatModSettings::MessageNone) { + ui->messageText->clear(); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageBeacon) { + ui->messageText->setText(m_settings.m_beaconMessage); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageCQ) { + ui->messageText->setText(m_settings.m_cqMessage); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageReply) { + ui->messageText->setText(m_settings.m_replyMessage); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageReport) { + ui->messageText->setText(m_settings.m_reportMessage); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageReplyReport) { + ui->messageText->setText(m_settings.m_replyReportMessage); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageRRR) { + ui->messageText->setText(m_settings.m_rrrMessage); + } else if (m_settings.m_messageType == ChirpChatModSettings::Message73) { + ui->messageText->setText(m_settings.m_73Message); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageQSOText) { + ui->messageText->setText(m_settings.m_qsoTextMessage); + } else if (m_settings.m_messageType == ChirpChatModSettings::MessageText) { + ui->messageText->setText(m_settings.m_textMessage); + } + + ui->messageText->blockSignals(false); +} + +void ChirpChatModGUI::displayBinaryMessage() +{ + ui->hexText->setText(m_settings.m_bytesMessage.toHex()); +} + +void ChirpChatModGUI::setBandwidths() +{ + int maxBandwidth = m_basebandSampleRate / ChirpChatModSettings::oversampling; + int maxIndex = 0; + + for (; (maxIndex < ChirpChatModSettings::nbBandwidths) && (ChirpChatModSettings::bandwidths[maxIndex] <= maxBandwidth); maxIndex++) + {} + + if (maxIndex != 0) + { + qDebug("ChirpChatModGUI::setBandwidths: avl: %d max: %d", maxBandwidth, ChirpChatModSettings::bandwidths[maxIndex-1]); + ui->bw->setMaximum(maxIndex - 1); + int index = ui->bw->value(); + ui->bwText->setText(QString("%1 Hz").arg(ChirpChatModSettings::bandwidths[index])); + } +} + +void ChirpChatModGUI::leaveEvent(QEvent*) +{ + m_channelMarker.setHighlighted(false); +} + +void ChirpChatModGUI::enterEvent(QEvent*) +{ + m_channelMarker.setHighlighted(true); +} + +void ChirpChatModGUI::tick() +{ + if (m_tickCount < 10) + { + m_tickCount++; + } + else + { + m_tickCount = 0; + double powDb = CalcDb::dbPower(m_chirpChatMod->getMagSq()); + m_channelPowerDbAvg(powDb); + ui->channelPower->setText(tr("%1 dB").arg(m_channelPowerDbAvg.asDouble(), 0, 'f', 1)); + + if (m_chirpChatMod->getModulatorActive()) { + ui->playMessage->setStyleSheet("QPushButton { background-color : green; }"); + } else { + ui->playMessage->setStyleSheet("QPushButton { background:rgb(79,79,79); }"); + } + } +} diff --git a/plugins/channeltx/modchirpchat/chirpchatmodgui.h b/plugins/channeltx/modchirpchat/chirpchatmodgui.h new file mode 100644 index 000000000..5c40ccc63 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodgui.h @@ -0,0 +1,112 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODLORA_LORAMODGUI_H_ +#define PLUGINS_CHANNELTX_MODLORA_LORAMODGUI_H_ + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "util/movingaverage.h" +#include "util/messagequeue.h" + +#include "chirpchatmod.h" +#include "chirpchatmodsettings.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSource; + +namespace Ui { + class ChirpChatModGUI; +} + +class ChirpChatModGUI : public ChannelGUI { + Q_OBJECT + +public: + static ChirpChatModGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +public slots: + void channelMarkerChangedByCursor(); + +private: + Ui::ChirpChatModGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + ChirpChatModSettings m_settings; + int m_basebandSampleRate; + bool m_doApplySettings; + + ChirpChatMod* m_chirpChatMod; + MovingAverageUtil m_channelPowerDbAvg; + + std::size_t m_tickCount; + MessageQueue m_inputMessageQueue; + + explicit ChirpChatModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx, QWidget* parent = nullptr); + virtual ~ChirpChatModGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void displayStreamIndex(); + void displayCurrentPayloadMessage(); + void displayBinaryMessage(); + void setBandwidths(); + bool handleMessage(const Message& message); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + +private slots: + void handleSourceMessages(); + void on_deltaFrequency_changed(qint64 value); + void on_bw_valueChanged(int value); + void on_spread_valueChanged(int value); + void on_deBits_valueChanged(int value); + void on_preambleChirps_valueChanged(int value); + void on_idleTime_valueChanged(int value); + void on_syncWord_editingFinished(); + void on_channelMute_toggled(bool checked); + void on_scheme_currentIndexChanged(int index); + void on_fecParity_valueChanged(int value); + void on_crc_stateChanged(int state); + void on_header_stateChanged(int state); + void on_myCall_editingFinished(); + void on_urCall_editingFinished(); + void on_myLocator_editingFinished(); + void on_report_editingFinished(); + void on_msgType_currentIndexChanged(int index); + void on_resetMessages_clicked(bool checked); + void on_playMessage_clicked(bool checked); + void on_repeatMessage_valueChanged(int value); + void on_generateMessages_clicked(bool checked); + void on_messageText_editingFinished(); + void on_hexText_editingFinished(); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void tick(); +}; + +#endif /* PLUGINS_CHANNELTX_MODLORA_LORAMODGUI_H_ */ diff --git a/plugins/channeltx/modchirpchat/chirpchatmodgui.ui b/plugins/channeltx/modchirpchat/chirpchatmodgui.ui new file mode 100644 index 000000000..391385f18 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodgui.ui @@ -0,0 +1,1196 @@ + + + ChirpChatModGUI + + + + 0 + 0 + 402 + 373 + + + + + 392 + 180 + + + + + Liberation Sans + 9 + + + + ChirpChat Modulator + + + + + 1 + 20 + 400 + 125 + + + + + 0 + 125 + + + + RF/mod/coder Settings + + + + + 2 + 50 + 32 + 16 + + + + BW + + + + + + 2 + 70 + 32 + 16 + + + + SF + + + + + + 40 + 50 + 261 + 16 + + + + Bandwidth + + + 0 + + + 10 + + + 1 + + + 5 + + + Qt::Horizontal + + + + + + 40 + 70 + 101 + 16 + + + + Spreading factor + + + 7 + + + 12 + + + 1 + + + 10 + + + 10 + + + Qt::Horizontal + + + + + + 150 + 70 + 30 + 16 + + + + + 30 + 0 + + + + 10 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 310 + 50 + 80 + 16 + + + + + 80 + 0 + + + + 7813 Hz + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 200 + 70 + 32 + 16 + + + + DE + + + + + + 370 + 70 + 10 + 16 + + + + + 10 + 0 + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 240 + 70 + 91 + 16 + + + + Low data rate optimize (DE) bits + + + 0 + + + 4 + + + 1 + + + 0 + + + 0 + + + Qt::Horizontal + + + + + + 10 + 10 + 381 + 26 + + + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 60 + 0 + + + + Channel power + + + Qt::RightToLeft + + + -100.0 dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Mute/Unmute channel + + + ... + + + + :/txon.png + :/txoff.png:/txon.png + + + true + + + + + + + + + 40 + 90 + 101 + 16 + + + + Number of preamble chirps + + + 4 + + + 20 + + + 1 + + + 8 + + + 8 + + + Qt::Horizontal + + + + + + 2 + 90 + 32 + 16 + + + + Pre + + + + + + 150 + 90 + 30 + 16 + + + + + 30 + 0 + + + + 8 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 200 + 90 + 32 + 16 + + + + Idle + + + + + + 240 + 90 + 91 + 16 + + + + Idle time beween packets (s) + + + 1 + + + 900 + + + 1 + + + 10 + + + 10 + + + Qt::Horizontal + + + + + + 350 + 90 + 30 + 16 + + + + + 30 + 0 + + + + 60.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 1 + 150 + 400 + 221 + + + + + 0 + 220 + + + + Payload + + + + + 2 + 40 + 32 + 16 + + + + QSO + + + + + + 40 + 40 + 40 + 16 + + + + MyCall + + + + + + 210 + 40 + 50 + 16 + + + + YourCall + + + + + + 90 + 40 + 110 + 16 + + + + Qt::ClickFocus + + + Caller callsign + + + + + + 10 + + + + + + 269 + 40 + 110 + 16 + + + + Qt::ClickFocus + + + Callee callsign + + + + + + 10 + + + + + + 40 + 60 + 40 + 16 + + + + MyLoc + + + + + + 90 + 60 + 110 + 16 + + + + Qt::ClickFocus + + + Caller QRA locator + + + + + + 10 + + + + + + 210 + 60 + 50 + 16 + + + + Report + + + + + + 270 + 60 + 110 + 16 + + + + Qt::ClickFocus + + + Report to callee + + + + + + 10 + + + + + + 340 + 80 + 40 + 20 + + + + Generate standard messages + + + Gen + + + + + + 40 + 80 + 90 + 20 + + + + Message type + + + + None + + + + + Beacon + + + + + CQ + + + + + Reply + + + + + Report + + + + + R-Report + + + + + RRR + + + + + 73 + + + + + QSO Text + + + + + Text + + + + + Bytes + + + + + Test + + + + + + + 2 + 110 + 32 + 16 + + + + Msg + + + + + + 40 + 110 + 355 + 60 + + + + + + + 2 + 175 + 32 + 16 + + + + Hex + + + + + + 40 + 175 + 355 + 20 + + + + + + + 140 + 80 + 20 + 20 + + + + Restore default message templates + + + + + + + :/recycle.png:/recycle.png + + + + + + 170 + 80 + 20 + 20 + + + + Play message + + + + + + + :/play.png:/play.png + + + false + + + + + + 210 + 82 + 51 + 16 + + + + Repeat + + + + + + 260 + 78 + 24 + 24 + + + + Message repetition (0 for infinite) + + + 20 + + + 1 + + + 1 + + + + + + 290 + 82 + 22 + 16 + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 2 + 200 + 40 + 16 + + + + Time + + + + + + 200 + 200 + 60 + 16 + + + + Payload time in milliseconds + + + 00000 ms + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 300 + 200 + 25 + 16 + + + + Ttot + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 330 + 200 + 60 + 16 + + + + Total transmission time in milliseconds + + + 00000 ms + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 2 + 10 + 50 + 16 + + + + Scheme + + + + + + 60 + 8 + 86 + 20 + + + + + LoRa + + + + + ASCII + + + + + TTY + + + + + + + 285 + 10 + 50 + 16 + + + + Header (explicit) + + + HDR + + + + + + 226 + 10 + 50 + 16 + + + + Append CRC to payload + + + CRC + + + + + + 150 + 10 + 32 + 16 + + + + FEC + + + + + + 180 + 5 + 24 + 24 + + + + Number of FEC parity bits (0 to 4) for Hamming code + + + 4 + + + 1 + + + 1 + + + + + + 205 + 10 + 12 + 16 + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 160 + 200 + 35 + 16 + + + + Tpay + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 70 + 200 + 70 + 16 + + + + Payload time in milliseconds + + + 0000.0 ms + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 40 + 200 + 30 + 16 + + + + Tsym + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 377 + 8 + 22 + 20 + + + + Qt::ClickFocus + + + Sync word (1 byte hex) + + + HH + + + 00 + + + + + + 342 + 10 + 32 + 16 + + + + Sync + + + + + + + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+ + CustomTextEdit + QTextEdit +
gui/customtextedit.h
+
+
+ + + + +
diff --git a/plugins/channeltx/modchirpchat/chirpchatmodplugin.cpp b/plugins/channeltx/modchirpchat/chirpchatmodplugin.cpp new file mode 100644 index 000000000..76c75be42 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodplugin.cpp @@ -0,0 +1,90 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "chirpchatmodgui.h" +#endif +#include "chirpchatmod.h" +#include "chirpchatmodwebapiadapter.h" +#include "chirpchatmodplugin.h" + +const PluginDescriptor ChirpChatModPlugin::m_pluginDescriptor = { + ChirpChatMod::m_channelId, + QString("ChirpChat Modulator"), + QString("6.0.0"), + QString("(c) Edouard Griffiths, F4EXB"), + QString("https://github.com/f4exb/sdrangel"), + true, + QString("https://github.com/f4exb/sdrangel") +}; + +ChirpChatModPlugin::ChirpChatModPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& ChirpChatModPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void ChirpChatModPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + // register LoRa modulator + m_pluginAPI->registerTxChannel(ChirpChatMod::m_channelIdURI, ChirpChatMod::m_channelId, this); +} + +void ChirpChatModPlugin::createTxChannel(DeviceAPI *deviceAPI, BasebandSampleSource **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + ChirpChatMod *instance = new ChirpChatMod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* ChirpChatModPlugin::createTxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSource *txChannel) const +{ + return 0; +} +#else +ChannelGUI* ChirpChatModPlugin::createTxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSource *txChannel) const +{ + return ChirpChatModGUI::create(m_pluginAPI, deviceUISet, txChannel); +} +#endif + +ChannelWebAPIAdapter* ChirpChatModPlugin::createChannelWebAPIAdapter() const +{ + return new ChirpChatModWebAPIAdapter(); +} diff --git a/plugins/channeltx/modchirpchat/chirpchatmodplugin.h b/plugins/channeltx/modchirpchat/chirpchatmodplugin.h new file mode 100644 index 000000000..a9d9bd44e --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodplugin.h @@ -0,0 +1,48 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_CHIRPCHATMODPLUGIN_H +#define INCLUDE_CHIRPCHATMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSource; + +class ChirpChatModPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channeltx.modchirpchat") + +public: + explicit ChirpChatModPlugin(QObject* parent = nullptr); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void createTxChannel(DeviceAPI *deviceAPI, BasebandSampleSource **bs, ChannelAPI **cs) const; + virtual ChannelGUI* createTxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSource *rxChannel) const; + virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_CHIRPCHATMODPLUGIN_H diff --git a/plugins/channeltx/modchirpchat/chirpchatmodsettings.cpp b/plugins/channeltx/modchirpchat/chirpchatmodsettings.cpp new file mode 100644 index 000000000..9ecd7061f --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodsettings.cpp @@ -0,0 +1,289 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspengine.h" +#include "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "chirpchatmodsettings.h" + +const int ChirpChatModSettings::bandwidths[] = { + 325, // 384k / 1024 + 750, // 384k / 512 + 1500, // 384k / 256 + 2604, // 333k / 128 + 3125, // 400k / 128 + 3906, // 500k / 128 + 5208, // 333k / 64 + 6250, // 400k / 64 + 7813, // 500k / 64 + 10417, // 333k / 32 + 12500, // 400k / 32 + 15625, // 500k / 32 + 20833, // 333k / 16 + 25000, // 400k / 16 + 31250, // 500k / 16 + 41667, // 333k / 8 + 50000, // 400k / 8 + 62500, // 500k / 8 + 83333, // 333k / 4 + 100000, // 400k / 4 + 125000, // 500k / 4 + 166667, // 333k / 2 + 200000, // 400k / 2 + 250000, // 500k / 2 + 333333, // 333k / 1 + 400000, // 400k / 1 + 500000 // 500k / 1 +}; +const int ChirpChatModSettings::nbBandwidths = 3*8 + 3; +const int ChirpChatModSettings::oversampling = 4; + +ChirpChatModSettings::ChirpChatModSettings() : + m_inputFrequencyOffset(0), + m_channelMarker(0) +{ + resetToDefaults(); +} + +void ChirpChatModSettings::resetToDefaults() +{ + m_bandwidthIndex = 5; + m_spreadFactor = 7; + m_deBits = 0; + m_preambleChirps = 8; + m_quietMillis = 1000; + m_codingScheme = CodingLoRa; + m_nbParityBits = 1; + m_hasCRC = true; + m_hasHeader = true; + m_textMessage = "Hello LoRa"; + m_myCall = "MYCALL"; + m_urCall = "URCALL"; + m_myLoc = "AA00AA"; + m_myRpt = "59"; + m_syncWord = 0x34; + m_channelMute = false; + m_messageRepeat = 1; + m_rgbColor = QColor(255, 0, 255).rgb(); + m_title = "ChirpChat Modulator"; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; + setDefaultTemplates(); +} + +void ChirpChatModSettings::setDefaultTemplates() +{ + // %1: myCall %2: urCall %3: myLoc %4: report + m_beaconMessage = "VVV DE %1 %2"; // Beacon + m_cqMessage = "CQ DE %1 %2"; // caller calls CQ + m_replyMessage = "%1 %2 %3"; // Reply to CQ from caller + m_reportMessage = "%1 %2 %3"; // Report to caller + m_replyReportMessage = "%1 %2 R%3"; // Report to callee + m_rrrMessage = "%1 %2 RRR"; // RRR to callee + m_73Message = "%1 %2 73"; // 73 to caller + m_qsoTextMessage = "%1 %2 %3"; // Freeflow message to caller - %3 is m_textMessage +} + +void ChirpChatModSettings::generateMessages() +{ + m_beaconMessage = m_beaconMessage + .arg(m_myCall).arg(m_myLoc); + m_cqMessage = m_cqMessage + .arg(m_myCall).arg(m_myLoc); + m_replyMessage = m_replyMessage + .arg(m_urCall).arg(m_myCall).arg(m_myLoc); + m_reportMessage = m_reportMessage + .arg(m_urCall).arg(m_myCall).arg(m_myRpt); + m_replyReportMessage = m_replyReportMessage + .arg(m_urCall).arg(m_myCall).arg(m_myRpt); + m_rrrMessage = m_rrrMessage + .arg(m_urCall).arg(m_myCall); + m_73Message = m_73Message + .arg(m_urCall).arg(m_myCall); + m_qsoTextMessage = m_qsoTextMessage + .arg(m_urCall).arg(m_myCall).arg(m_textMessage); +} + +unsigned int ChirpChatModSettings::getNbSFDFourths() const +{ + switch (m_codingScheme) + { + case CodingLoRa: + return 9; + default: + return 8; + } +} + +bool ChirpChatModSettings::hasSyncWord() const +{ + return m_codingScheme == CodingLoRa; +} + +QByteArray ChirpChatModSettings::serialize() const +{ + SimpleSerializer s(1); + s.writeS32(1, m_inputFrequencyOffset); + s.writeS32(2, m_bandwidthIndex); + s.writeS32(3, m_spreadFactor); + s.writeS32(4, m_codingScheme); + + if (m_channelMarker) { + s.writeBlob(5, m_channelMarker->serialize()); + } + + s.writeString(6, m_title); + s.writeS32(7, m_deBits); + s.writeBool(8, m_channelMute); + s.writeU32(9, m_syncWord); + s.writeS32(10, m_preambleChirps); + s.writeS32(11, m_quietMillis); + s.writeBool(12, m_useReverseAPI); + s.writeString(13, m_reverseAPIAddress); + s.writeU32(14, m_reverseAPIPort); + s.writeU32(15, m_reverseAPIDeviceIndex); + s.writeU32(16, m_reverseAPIChannelIndex); + s.writeString(20, m_beaconMessage); + s.writeString(21, m_cqMessage); + s.writeString(22, m_replyMessage); + s.writeString(23, m_reportMessage); + s.writeString(24, m_replyReportMessage); + s.writeString(25, m_rrrMessage); + s.writeString(26, m_73Message); + s.writeString(27, m_qsoTextMessage); + s.writeString(28, m_textMessage); + s.writeBlob(29, m_bytesMessage); + s.writeS32(30, (int) m_messageType); + s.writeS32(31, m_nbParityBits); + s.writeBool(32, m_hasCRC); + s.writeBool(33, m_hasHeader); + s.writeString(40, m_myCall); + s.writeString(41, m_urCall); + s.writeString(42, m_myLoc); + s.writeString(43, m_myRpt); + s.writeS32(44, m_messageRepeat); + s.writeBool(50, m_useReverseAPI); + s.writeString(51, m_reverseAPIAddress); + s.writeU32(52, m_reverseAPIPort); + s.writeU32(53, m_reverseAPIDeviceIndex); + s.writeU32(54, m_reverseAPIChannelIndex); + s.writeS32(55, m_streamIndex); + + return s.final(); +} + +bool ChirpChatModSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + QByteArray bytetmp; + unsigned int utmp; + int tmp; + + d.readS32(1, &m_inputFrequencyOffset, 0); + d.readS32(2, &m_bandwidthIndex, 0); + d.readS32(3, &m_spreadFactor, 0); + d.readS32(4, &tmp, 0); + m_codingScheme = (CodingScheme) tmp; + + if (m_channelMarker) + { + d.readBlob(5, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + + d.readString(6, &m_title, "LoRa Demodulator"); + d.readS32(7, &m_deBits, 0); + d.readBool(8, &m_channelMute, false); + d.readU32(9, &utmp, 0x34); + m_syncWord = utmp > 255 ? 0 : utmp; + d.readS32(10, &m_preambleChirps, 8); + d.readS32(11, &m_quietMillis, 1000); + d.readBool(11, &m_useReverseAPI, false); + d.readString(12, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(13, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(14, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(15, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + d.readString(20, &m_beaconMessage, "VVV DE %1 %2"); + d.readString(21, &m_cqMessage, "CQ DE %1 %2"); + d.readString(22, &m_replyMessage, "%2 %1 %3"); + d.readString(23, &m_reportMessage, "%2 %1 %3"); + d.readString(24, &m_replyReportMessage, "%2 %1 R%3"); + d.readString(25, &m_rrrMessage, "%2 %1 RRR"); + d.readString(26, &m_73Message, "%2 %1 73"); + d.readString(27, &m_qsoTextMessage, "%2 %1 Hello LoRa"); + d.readString(28, &m_textMessage, "Hello LoRa"); + d.readBlob(29, &m_bytesMessage); + d.readS32(30, &tmp, 0); + m_messageType = (MessageType) tmp; + d.readS32(31, &m_nbParityBits, 1); + d.readBool(32, &m_hasCRC, true); + d.readBool(33, &m_hasHeader, true); + d.readString(40, &m_myCall, "MYCALL"); + d.readString(41, &m_urCall, "URCALL"); + d.readString(42, &m_myLoc, "AA00AA"); + d.readString(43, &m_myRpt, "59"); + d.readS32(44, &m_messageRepeat, 1); + + d.readBool(50, &m_useReverseAPI, false); + d.readString(51, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(52, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(53, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(54, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + d.readS32(55, &m_streamIndex, 0); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} diff --git a/plugins/channeltx/modchirpchat/chirpchatmodsettings.h b/plugins/channeltx/modchirpchat/chirpchatmodsettings.h new file mode 100644 index 000000000..f2eed957e --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodsettings.h @@ -0,0 +1,108 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODSETTINGS_H_ +#define PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODSETTINGS_H_ + +#include +#include + +#include + +class Serializable; + +struct ChirpChatModSettings +{ + enum CodingScheme + { + CodingLoRa, //!< Standard LoRa + CodingASCII, //!< plain ASCII (7 bits) + CodingTTY //!< plain TTY (5 bits) + }; + + enum MessageType + { + MessageNone, + MessageBeacon, + MessageCQ, + MessageReply, + MessageReport, + MessageReplyReport, + MessageRRR, + Message73, + MessageQSOText, + MessageText, + MessageBytes + }; + + int m_inputFrequencyOffset; + int m_bandwidthIndex; + int m_spreadFactor; + int m_deBits; //!< Low data rate optmize (DE) bits + int m_preambleChirps; //!< Number of preamble chirps + int m_quietMillis; //!< Number of milliseconds to pause between transmissions + int m_nbParityBits; //!< Hamming parity bits (LoRa) + bool m_hasCRC; //!< Payload has CRC (LoRa) + bool m_hasHeader; //!< Header present before actual payload (LoRa) + unsigned char m_syncWord; + bool m_channelMute; + CodingScheme m_codingScheme; + QString m_myCall; //!< QSO mode: my callsign + QString m_urCall; //!< QSO mode: your callsign + QString m_myLoc; //!< QSO mode: my locator + QString m_myRpt; //!< QSO mode: my report + MessageType m_messageType; + QString m_beaconMessage; + QString m_cqMessage; + QString m_replyMessage; + QString m_reportMessage; + QString m_replyReportMessage; + QString m_rrrMessage; + QString m_73Message; + QString m_qsoTextMessage; + QString m_textMessage; + QByteArray m_bytesMessage; + int m_messageRepeat; + uint32_t m_rgbColor; + QString m_title; + int m_streamIndex; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + uint16_t m_reverseAPIChannelIndex; + + Serializable *m_channelMarker; + + static const int bandwidths[]; + static const int nbBandwidths; + static const int oversampling; + + ChirpChatModSettings(); + void resetToDefaults(); + void setDefaultTemplates(); + void generateMessages(); + unsigned int getNbSFDFourths() const; //!< Get the number of SFD period fourths (depends on coding scheme) + bool hasSyncWord() const; //!< Only LoRa has a syncword (for the moment) + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + + + +#endif /* PLUGINS_CHANNELTX_MODCHIRPCHAT_CHIRPCHATMODSETTINGS_H_ */ diff --git a/plugins/channeltx/modchirpchat/chirpchatmodsource.cpp b/plugins/channeltx/modchirpchat/chirpchatmodsource.cpp new file mode 100644 index 000000000..900375ee3 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodsource.cpp @@ -0,0 +1,415 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "chirpchatmodsource.h" + +const int ChirpChatModSource::m_levelNbSamples = 480; // every 10ms + +ChirpChatModSource::ChirpChatModSource() : + m_channelSampleRate(48000), + m_channelFrequencyOffset(0), + m_phaseIncrements(nullptr), + m_modPhasor(0.0f), + m_levelCalcCount(0), + m_peakLevel(0.0f), + m_levelSum(0.0f), + m_repeatCount(0), + m_active(false) +{ + m_magsq = 0.0; + + initSF(m_settings.m_spreadFactor); + initTest(m_settings.m_spreadFactor, m_settings.m_deBits); + reset(); + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +ChirpChatModSource::~ChirpChatModSource() +{ + delete[] m_phaseIncrements; +} + +void ChirpChatModSource::initSF(unsigned int sf) +{ + m_fftLength = 1 << sf; + m_state = ChirpChatStateIdle; + m_quarterSamples = (m_fftLength/4)*ChirpChatModSettings::oversampling; + + float halfAngle = M_PI/ChirpChatModSettings::oversampling; + float phase = -halfAngle; + double accumulator = 0; + + if (m_phaseIncrements) { + delete[] m_phaseIncrements; + } + + m_phaseIncrements = new double[2*m_fftLength*ChirpChatModSettings::oversampling]; + phase = -halfAngle; + + for (int i = 0; i < m_fftLength*ChirpChatModSettings::oversampling; i++) + { + m_phaseIncrements[i] = phase; + phase += (2*halfAngle) / (m_fftLength*ChirpChatModSettings::oversampling); + } + + std::copy( + m_phaseIncrements, + m_phaseIncrements+m_fftLength*ChirpChatModSettings::oversampling, + m_phaseIncrements+m_fftLength*ChirpChatModSettings::oversampling + ); +} + +void ChirpChatModSource::initTest(unsigned int sf, unsigned int deBits) +{ + unsigned int fftLength = 1< 1.0f) // decimate + { + modulateSample(); + + while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + else + { + if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + + m_interpolatorDistanceRemain += m_interpolatorDistance; + + ci *= m_carrierNco.nextIQ(); // shift to carrier frequency + + if (!(m_state == ChirpChatStateIdle)) + { + double magsq = std::norm(ci); + magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + } + + sample.m_real = (FixReal) ci.real(); + sample.m_imag = (FixReal) ci.imag(); +} + +void ChirpChatModSource::modulateSample() +{ + if (m_state == ChirpChatStateIdle) + { + m_modSample = Complex{0.0, 0.0}; + m_sampleCounter++; + + if (m_sampleCounter == m_quietSamples*ChirpChatModSettings::oversampling) // done with quiet time + { + m_chirp0 = 0; + m_chirp = m_fftLength*ChirpChatModSettings::oversampling - 1; + + if (m_symbols.size() != 0) // some payload to transmit + { + if (m_settings.m_messageRepeat == 0) // infinite + { + m_state = ChirpChatStatePreamble; + m_active = true; + } + else + { + if (m_repeatCount != 0) + { + m_repeatCount--; + m_state = ChirpChatStatePreamble; + m_active = true; + } + else + { + m_active = false; + } + } + } + else + { + m_active = false; + } + } + } + else if (m_state == ChirpChatStatePreamble) + { + m_modPhasor += m_phaseIncrements[m_chirp]; // up chirps + m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor)); + m_fftCounter++; + + if (m_fftCounter == m_fftLength*ChirpChatModSettings::oversampling) + { + m_chirpCount++; + m_fftCounter = 0; + + if (m_chirpCount == m_settings.m_preambleChirps) + { + m_chirpCount = 0; + + if (m_settings.hasSyncWord()) + { + m_chirp0 = ((m_settings.m_syncWord >> ((1-m_chirpCount)*4)) & 0xf)*8; + m_chirp = (m_chirp0 + m_fftLength)*ChirpChatModSettings::oversampling - 1; + m_state = ChirpChatStateSyncWord; + } + else + { + m_sampleCounter = 0; + m_chirp0 = 0; + m_chirp = m_fftLength*ChirpChatModSettings::oversampling - 1; + m_state = ChirpChatStateSFD; + } + } + } + } + else if (m_state == ChirpChatStateSyncWord) + { + m_modPhasor += m_phaseIncrements[m_chirp]; // up chirps + m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor)); + m_fftCounter++; + + if (m_fftCounter == m_fftLength*ChirpChatModSettings::oversampling) + { + m_chirpCount++; + m_chirp0 = ((m_settings.m_syncWord >> ((1-m_chirpCount)*4)) & 0xf)*8; + m_chirp = (m_chirp0 + m_fftLength)*ChirpChatModSettings::oversampling - 1; + m_fftCounter = 0; + + if (m_chirpCount == 2) + { + m_sampleCounter = 0; + m_chirpCount = 0; + m_chirp0 = 0; + m_chirp = m_fftLength*ChirpChatModSettings::oversampling - 1; + m_state = ChirpChatStateSFD; + } + } + } + else if (m_state == ChirpChatStateSFD) + { + m_modPhasor -= m_phaseIncrements[m_chirp]; // down chirps + m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor)); + m_fftCounter++; + m_sampleCounter++; + + if (m_fftCounter == m_fftLength*ChirpChatModSettings::oversampling) + { + m_chirp0 = 0; + m_chirp = m_fftLength*ChirpChatModSettings::oversampling - 1; + m_fftCounter = 0; + } + + if (m_sampleCounter == m_quarterSamples) + { + m_chirpCount++; + m_sampleCounter = 0; + } + + if (m_chirpCount == m_settings.getNbSFDFourths()) + { + m_fftCounter = 0; + m_chirpCount = 0; + m_chirp0 = encodeSymbol(m_symbols[m_chirpCount]); + m_chirp = (m_chirp0 + m_fftLength)*ChirpChatModSettings::oversampling - 1; + m_state = ChirpChatStatePayload; + } + } + else if (m_state == ChirpChatStatePayload) + { + m_modPhasor += m_phaseIncrements[m_chirp]; // up chirps + m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor)); + m_fftCounter++; + + if (m_fftCounter == m_fftLength*ChirpChatModSettings::oversampling) + { + m_chirpCount++; + + if (m_chirpCount == m_symbols.size()) + { + reset(); + m_state = ChirpChatStateIdle; + } + else + { + m_chirp0 = encodeSymbol(m_symbols[m_chirpCount]); + m_chirp = (m_chirp0 + m_fftLength)*ChirpChatModSettings::oversampling - 1; + m_fftCounter = 0; + } + } + } + + // limit phasor range to ]-pi,pi] + if (m_modPhasor > M_PI) { + m_modPhasor -= (2.0f * M_PI); + } + + m_chirp++; + + if (m_chirp >= (m_chirp0 + m_fftLength)*ChirpChatModSettings::oversampling) { + m_chirp = m_chirp0*ChirpChatModSettings::oversampling; + } +} + +unsigned short ChirpChatModSource::encodeSymbol(unsigned short symbol) +{ + if (m_settings.m_deBits == 0) { + return symbol; + } + + unsigned int deWidth = 1<& symbols) +{ + m_symbols = symbols; + qDebug("ChirpChatModSource::setSymbols: m_symbols: %lu", m_symbols.size()); + m_repeatCount = m_settings.m_messageRepeat; + m_state = ChirpChatStateIdle; // first reset to idle + reset(); + m_sampleCounter = m_quietSamples*ChirpChatModSettings::oversampling - 1; // start immediately +} diff --git a/plugins/channeltx/modchirpchat/chirpchatmodsource.h b/plugins/channeltx/modchirpchat/chirpchatmodsource.h new file mode 100644 index 000000000..8e1b00b95 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodsource.h @@ -0,0 +1,113 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_CHIRPCHATMODSOURCE_H +#define INCLUDE_CHIRPCHATMODSOURCE_H + +#include + +#include "dsp/channelsamplesource.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/firfilter.h" +#include "util/movingaverage.h" + +#include "chirpchatmodsettings.h" + +class ChirpChatModSource : public ChannelSampleSource +{ +public: + ChirpChatModSource(); + virtual ~ChirpChatModSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples) {} + + double getMagSq() const { return m_magsq; } + void getLevels(qreal& rmsLevel, qreal& peakLevel, int& numSamples) const + { + rmsLevel = m_rmsLevel; + peakLevel = m_peakLevelOut; + numSamples = m_levelNbSamples; + } + void applySettings(const ChirpChatModSettings& settings, bool force = false); + void applyChannelSettings(int channelSampleRate, int bandwidth, int channelFrequencyOffset, bool force = false); + void setSymbols(const std::vector& symbols); + bool getActive() const { return m_active; } + +private: + enum ChirpChatState + { + ChirpChatStateIdle, //!< Quiet time + ChirpChatStatePreamble, //!< Transmit preamble + ChirpChatStateSyncWord, //!< Tramsmit sync word + ChirpChatStateSFD, //!< Transmit SFD + ChirpChatStatePayload //!< Tramsmoit payload + }; + + int m_channelSampleRate; + int m_channelFrequencyOffset; + int m_bandwidth; + ChirpChatModSettings m_settings; + + ChirpChatState m_state; + double *m_phaseIncrements; + std::vector m_symbols; + unsigned int m_fftLength; //!< chirp length in samples + unsigned int m_chirp; //!< actual chirp index in chirps table + unsigned int m_chirp0; //!< half index of chirp start in chirps table + unsigned int m_sampleCounter; //!< actual sample counter + unsigned int m_fftCounter; //!< chirp sample counter + unsigned int m_chirpCount; //!< chirp or quarter chirp counter + unsigned int m_quietSamples; //!< number of samples during quiet period + unsigned int m_quarterSamples; //!< number of samples in a quarter chirp + unsigned int m_repeatCount; //!< message repetition counter + bool m_active; //!< modulator is in a sending sequence (icluding periodic quiet times) + + NCO m_carrierNco; + double m_modPhasor; //!< baseband modulator phasor + Complex m_modSample; + + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + bool m_interpolatorConsumed; + + Bandpass m_bandpass; + + double m_magsq; + MovingAverageUtil m_movingAverage; + + quint32 m_levelCalcCount; + qreal m_rmsLevel; + qreal m_peakLevelOut; + Real m_peakLevel; + Real m_levelSum; + + static const int m_levelNbSamples; + + void initSF(unsigned int sf); //!< Init tables, FFTs, depending on spread factor + void initTest(unsigned int sf, unsigned int deBits); + void reset(); + void processOneSample(Complex& ci); + void calculateLevel(Real& sample); + void modulateSample(); + unsigned short encodeSymbol(unsigned short symbol); //!< Encodes symbol with possible DE bits spacing +}; + +#endif // INCLUDE_CHIRPCHATMODSOURCE_H diff --git a/plugins/channeltx/modchirpchat/chirpchatmodwebapiadapter.cpp b/plugins/channeltx/modchirpchat/chirpchatmodwebapiadapter.cpp new file mode 100644 index 000000000..6674b0e46 --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodwebapiadapter.cpp @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGChannelSettings.h" +#include "chirpchatmod.h" +#include "chirpchatmodwebapiadapter.h" + +ChirpChatModWebAPIAdapter::ChirpChatModWebAPIAdapter() +{} + +ChirpChatModWebAPIAdapter::~ChirpChatModWebAPIAdapter() +{} + +int ChirpChatModWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setChirpChatModSettings(new SWGSDRangel::SWGChirpChatModSettings()); + response.getChirpChatModSettings()->init(); + ChirpChatMod::webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int ChirpChatModWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + ChirpChatMod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + ChirpChatMod::webapiFormatChannelSettings(response, m_settings); + return 200; +} diff --git a/plugins/channeltx/modchirpchat/chirpchatmodwebapiadapter.h b/plugins/channeltx/modchirpchat/chirpchatmodwebapiadapter.h new file mode 100644 index 000000000..af00be7be --- /dev/null +++ b/plugins/channeltx/modchirpchat/chirpchatmodwebapiadapter.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_CHIRPCHATMOD_WEBAPIADAPTER_H +#define INCLUDE_CHIRPCHATMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "chirpchatmodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class ChirpChatModWebAPIAdapter : public ChannelWebAPIAdapter { +public: + ChirpChatModWebAPIAdapter(); + virtual ~ChirpChatModWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + +private: + ChirpChatModSettings m_settings; +}; + +#endif // INCLUDE_LORAMOD_WEBAPIADAPTER_H diff --git a/plugins/channeltx/modchirpchat/readme.md b/plugins/channeltx/modchirpchat/readme.md new file mode 100644 index 000000000..46dcfefd4 --- /dev/null +++ b/plugins/channeltx/modchirpchat/readme.md @@ -0,0 +1,225 @@ +

LoRa modulator plugin

+ +

Introduction

+ +This plugin can be used to code and modulate a transmission signal based on Chirp Spread Spectrum (CSS). The basic idea is to transform each symbol of a MFSK modulation to an ascending frequency ramp shifted in time. It could equally be a descending ramp but this one is reserved to detect a break in the preamble sequence (synchronization). This plugin has been designed to work in conjunction with the ChirpChat demodulator plugin that should be used ideally on the reception side. + +It has clearly been inspired by the LoRa technique but is designed for experimentation and extension to other protocols mostly inspired by amateur radio techniques using chirp modulation to transmit symbols. Thanks to the MFSK to chirp translation it is possible to adapt any MFSK based mode. + +LoRa is a property of Semtech and the details of the protocol are not made public. However a LoRa compatible protocol has been implemented based on the reverse engineering performed by the community. It is mainly based on the work done in https://github.com/myriadrf/LoRa-SDR. You can find more information about LoRa and chirp modulation here: + + - To get an idea of what is LoRa: [here](https://www.link-labs.com/blog/what-is-lora) + - A detailed inspection of LoRa modulation and protocol: [here](https://static1.squarespace.com/static/54cecce7e4b054df1848b5f9/t/57489e6e07eaa0105215dc6c/1464376943218/Reversing-Lora-Knight.pdf) + +This LoRa encoder is designed for experimentation. For production grade applications it is recommended to use dedicated hardware instead. + +Modulation characteristics from LoRa have been augmented with more bandwidths and FFT bin collations (DE factor). Plain TTY and ASCII have also been added and there are plans to add some more complex typically amateur radio MFSK based modes like JT65. + +Note: this plugin is officially supported since version 6. + +

Interface

+ +![ChitpChat Modulator plugin GUI](../../../doc/img/ChirpChatMod_plugin.png) + +

1: Frequency shift from center frequency of reception

+ +Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Left click on a digit sets the cursor position at this digit. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. + +

2: Channel power

+ +The signal is frequency modulated with a constant envelope hence this value should be constant. To prevent possible overshoots the signal is reduced by 1 dB from the full scale. Thus this should always display `-1 dB`. + +

3: Channel mute

+ +Use this button to mute/unmute transmission. + +

4: Bandwidth

+ +This is the bandwidth of the ChirpChat signal. Similarly to LoRa the signal sweeps between the lower and the upper frequency of this bandwidth. The sample rate of the ChirpChat signal in seconds is exactly one over this bandwidth in Hertz. + +In the LoRa standard there are 2 base bandwidths: 500 and 333.333 kHz. A 400 kHz base has been added. Possible bandwidths are obtained by a division of these base bandwidths by a power of two from 1 to 64. Extra divisor of 128 is provided to achieve smaller bandwidths that can fit in a SSB channel. Finally special divisors from a 384 kHz base are provided to allow even more narrow bandwidths. + +Thus available bandwidths are: + + - **500000** (500000 / 1) Hz + - **400000** (400000 / 1) Hz not in LoRa standard + - **333333** (333333 / 1) Hz + - **250000** (500000 / 2) Hz + - **200000** (400000 / 2) Hz not in LoRa standard + - **166667** (333333 / 2) Hz + - **125000** (500000 / 4) Hz + - **100000** (400000 / 4) Hz not in LoRa standard + - **83333** (333333 / 4) Hz + - **62500** (500000 / 8) Hz + - **50000** (400000 / 8) Hz not in LoRa standard + - **41667** (333333 / 8) Hz + - **31250** (500000 / 16) Hz + - **25000** (400000 / 16) Hz not in LoRa standard + - **20833** (333333 / 16) Hz + - **15625** (500000 / 32) Hz + - **12500** (400000 / 32) Hz not in LoRa standard + - **10417** (333333 / 32) Hz + - **7813** (500000 / 64) Hz + - **6250** (400000 / 64) Hz not in LoRa standard + - **5208** (333333 / 64) Hz + - **3906** (500000 / 128) Hz not in LoRa standard + - **3125** (400000 / 128) Hz not in LoRa standard + - **2604** (333333 / 128) Hz not in LoRa standard + - **1500** (384000 / 256) Hz not in LoRa standard + - **750** (384000 / 512) Hz not in LoRa standard + - **375** (384000 / 1024) Hz not in LoRa standard + +The ChirpChat signal is oversampled by four therefore it needs a baseband of at least four times the bandwidth. This drives the maximum value on the slider automatically. + +

5: Spread Factor

+ +This is the Spread Factor parameter of the ChirpChat signal. This is the log2 of the possible frequency shifts used over the bandwidth (3). The number of symbols is 2SF-DE where SF is the spread factor and DE the Distance Enhancement factor (8). To + +

6: Distance Enhancement factor

+ +The LoRa standard specifies 0 (no DE) or 2 (DE active). The ChirpChat range is extended to all values between 0 and 4 bits. + +This is the log2 of the number of frequency shifts separating two consecutive shifts that represent a symbol. On the receiving side this decreases the probabilty to detect the wrong symbol as an adjacent FFT bin. It can also overcome frequency drift on long messages. + +In practice it is difficult on the Rx side to make correct decodes if only one FFT bin is used to code one symbol (DE=0). It is therefore recommended to use a factor of 1 or more. + +

8: Number of preamble chirps

+ +This is the number of preamble chirps to transmit that are used for the Rx to synchronize. The LoRa standard specifies it can be between 2 and 65535. Here it is limited to the 4 to 20 range that corresponds to realistic values. The RN2483 uses 6 preamble chirps. You may use 12 preamble chirps or more to facilitate signal acquisition with poor SNR on the Rx side. + +

9: Idle time between transmissions

+ +When sending a message repeatedly this is the time between the end of one transmission and the start of the next transmission. + +

10: Message and encoding details

+ +![ChirpChat Modulator plugin GUI](../../../doc/img/ChirpChatMod_payload.png) + +ChirpChat is primarily designed to make QSOs in the amateur radio sense. To be efficient the messages have to be kept short and minimal therefore the standard exchange follows WSJT scheme and is reflected in the sequence of messages you can follow with the message selection combo (10.9): CQ, Reply to CQ, Report to callee, Report to caller (R-Report), RRR and 73. + +To populate messages you can specify your callsign (10.5), the other party callsign (10.6), your QRA locator (10.7) and a signal report (10.8) + +

10.1: Modulation scheme

+ + - **LoRa**: LoRa compatible + - **ASCII**: 7 bit plain ASCII without FEC and CRC. Requires exactly 7 bit effective samples thus SF-DE = 7 where SF is the spreading factor (5) and DE the distance enhancement factor (6) + - **TTY**: 5 bit Baudot (Teletype) without FEC and CRC. Requires exactly 5 bit effective samples thus SF-DE = 5 where SF is the spreading factor (5) and DE the distance enhancement factor (6) + +

10.2: Number of FEC parity bits (LoRa)

+ +This is a LoRa specific feature. Each byte of the payload is split into two four bit nibbles and Hamming code of various "strength" in number of parity bits can be applied to these nibbles. The number of parity bits can vary from 1 to 4. 0 (no FEC) has been added but is not part of the LoRa original standard: + + - **0**: no FEC + - **1**: 1 bit parity thus Hamming H(4,5) applies + - **2**: 2 bit parity thus Hamming H(4,6) applies + - **3**: 3 bit parity thus Hamming H(4,7) applies + - **4**: 4 bit parity thus Hamming H(4,8) applies + +

10.3: Append two byte CRC to payload (LoRa)

+ +This is a LoRa specific feature. A 2 bytes CRC can be appended to the payload. + +

10.4: Send a header at the start of the payload (LoRa)

+ +This is a LoRa specific feature and is also known as explicit (with header) or implicit (without header) modes. In explicit mode a header with net payload length in bytes, presence of a CRC and number of parity bits is prepended to the actual payload. This header has a 1 byte CRC and is coded with H(4,8) FEC. + +

10.5: My callsign (QSO mode)

+ +Enter your callsign so it can populate message palceholders (See next) + +

10.6: Your callsign (QSO mode)

+ +Enter the other party callsign so it can populate message palceholders (See next) + +

10.7: My locator (QSO mode)

+ +Enter your Maidenhead QRA locator so it can populate message palceholders (See next) + +

10.8: My report (QSO mode)

+ +Enter the signal report you will send to the other party so it can populate message palceholders (See next) + +

10.9: Message selector

+ +This lets you choose which pre-formatted message to send: + + - **None**: empty message. In fact this is used to make a transition to trigger sending of the sa,e ,essage again. It is used internally by the "play" button (11) and can be used with the REST API. + - **Beacon**: a beacon message + - **CQ**: (QSO mode) CQ general call message + - **Reply**: (QSO mode) reply to a CQ call + - **Report**: (QSO mode) signal report to the callee of a CQ call + - **R-Report**: (QSO mode) signal report to the caller of a CQ call + - **RRR**: (QSO mode) report received confirmation to the callee + - **73**: (QSO mode) confirmation back to the caller and closing the QSO + - **QSO text**: (QSO mode) free form message with callsigns + - **Text**: plain text + - **Bytes**: binary message in the form of a string of bytes. Use the hex window (12) to specify the message + +

10.10: Revert to standard messages

+ +Reformat all predefined messages in standard messages with placeholders. The Generate button (13) replaces the placeholders with the given QSO elements (10.5 to 10.8) + + - **Beacon**: `VVV DE %1 %2` + - **CQ**: `CQ DE %1 %2` + - **Reply**: `%1 %2 %3` + - **Report**: `%1 %2 %3` + - **R-Reply**: `%1 %2 R%3` + - **RRR**: `%1 %2 RRR` + - **73**: `%1 %2 73` + - **QSO text**: `%1 %2 %3` + +

10.11 Play current message immediately

+ +This starts playing the current selected message immediately. It may be necessary to use this button after a change of modulation and/or coding parameters. + +

10.12: Number of message repetitions

+ +The message is repeated this number of times (use 0 for infinite). The end of one message sequence and the start of the next is separated by the delay specified with the "Idle" slidebar (9) + +

10.13: Generate messages

+ +This applies the QSO elements (10.5 to 10.8) to the placeholders in messages to generate the final messages: + + - **Beacon**: `VVV DE %1 %2`: `%1` is my call (10.5) and `%2` is my locator (10.7) + - **CQ**: `CQ DE %1 %2`: `%1` is my call (10.5) and `%2` is my locator (10.7) + - **Reply**: `%1 %2 %3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is my locator (10.7) + - **Report**: `%1 %2 %3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is my report (10.8) + - **R-Reply**: `%1 %2 R%3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is my report (10.8) + - **RRR**: `%1 %2 RRR`: `%1` is your call (10.6) and `%2` is my call (10.5) + - **73**: `%1 %2 73`: `%1` is your call (10.6) and `%2` is my call (10.5) + - **QSO text**: `%1 %2 %3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is the text specified as the free form text message + +

10.14: Sync word

+ +This is a LoRa specific feature and is the sync word (byte) to transmit entered as a 2 nibble hexadecimal number. + +

11: Message text

+ +This window lets you edit the message selected in (10.9). You can use `%n` placeholders that depend on the type of message selected. + + - **Beacon**: `%1` is my callsign and `%2` is my locator + - **CQ message**: `%1` is my callsign and `%2` is my locator + - **Reply**: `%1` is the other callsign, `%2` is my callsign and `%3` is my locator + - **Report**: `%1` is the other callsign, `%2` is my callsign and `%3` is my report + - **R-Report**: `%1` is the other callsign, `%2` is my callsign and `%3` is my report + - **RRR**: `%1` is the other callsign and `%2` is my callsign + - **73**: `%1` is the other callsign and `%2` is my callsign + - **QSO Text**: `%1` is the other callsign, `%2` is my callsign and `%3` is the free text message + - **Text**: free text message no placeholders + - **Bytes**: binary message no placeholders + +

12: Message bytes

+ +Use this line editor to specify the hex string used as the bytes message. + +

13: Symbol time

+ +This is the duration of a symbol or chirp in milliseconds + +

14: Payload time

+ +This is the duration of the message payload in milliseconds. It excludes the preamble, the sync word and synchronization (SFD) sequence. + +

15: Total time

+ +This is the duration of the full message in milliseconds. diff --git a/sdrgui/CMakeLists.txt b/sdrgui/CMakeLists.txt index 0284b7008..81fb4441b 100644 --- a/sdrgui/CMakeLists.txt +++ b/sdrgui/CMakeLists.txt @@ -25,6 +25,7 @@ set(sdrgui_SOURCES gui/commandoutputdialog.cpp gui/comboboxnoarrow.cpp gui/crightclickenabler.cpp + gui/customtextedit.cpp gui/cwkeyergui.cpp gui/devicestreamselectiondialog.cpp gui/deviceuserargsdialog.cpp @@ -107,6 +108,7 @@ set(sdrgui_HEADERS gui/commandoutputdialog.h gui/comboboxnoarrow.h gui/crightclickenabler.h + gui/customtextedit.h gui/cwkeyergui.h gui/devicestreamselectiondialog.h gui/deviceuserargsdialog.h diff --git a/sdrgui/gui/customtextedit.cpp b/sdrgui/gui/customtextedit.cpp new file mode 100644 index 000000000..67ceb13a8 --- /dev/null +++ b/sdrgui/gui/customtextedit.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// Subclasses QTextEdit to implement and editingFinished() signal like // +// QLineEdit's one // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "customtextedit.h" + +CustomTextEdit::CustomTextEdit(QWidget* parent) : QTextEdit(parent), textChanged(false), trackChange(false) +{ + connect(this,&QTextEdit::textChanged,this,&CustomTextEdit::handleTextChange); +} + +void CustomTextEdit::focusInEvent(QFocusEvent* e) +{ + trackChange = true; + textChanged = false; + QTextEdit::focusInEvent(e); +} + +void CustomTextEdit::focusOutEvent(QFocusEvent *e) +{ + QTextEdit::focusOutEvent(e); + trackChange = false; + + if (textChanged) + { + textChanged = false; + emit editingFinished(); + } +} + +void CustomTextEdit::handleTextChange() +{ + if (trackChange) { + textChanged = true; + } +} diff --git a/sdrgui/gui/customtextedit.h b/sdrgui/gui/customtextedit.h new file mode 100644 index 000000000..1012ca146 --- /dev/null +++ b/sdrgui/gui/customtextedit.h @@ -0,0 +1,43 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// Subclasses QTextEdit to implement and editingFinished() signal like // +// QLineEdit's one // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "export.h" + +class SDRGUI_API CustomTextEdit : public QTextEdit +{ + Q_OBJECT +public: + CustomTextEdit(QWidget* parent = nullptr); + +protected: + void focusInEvent(QFocusEvent* e); + void focusOutEvent(QFocusEvent *e); + +signals: + void editingFinished(); + +private slots: + void handleTextChange(); + +private: + bool textChanged, trackChange; +};