mirror of
https://github.com/f4exb/sdrangel.git
synced 2026-03-30 19:55:52 -04:00
Merge branch 'f4exb:master' into acars
This commit is contained in:
commit
139b366bfd
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 109 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 48 KiB |
Binary file not shown.
@ -2634,7 +2634,7 @@ int FT8::decode(const float ll174[], int a174[], FT8Params& _params, int use_osd
|
||||
// apply (174, 91) generator mastrix to obtain the 83 parity bits
|
||||
// append the 83 bits to the 91 bits message + crc to obbain the 174 bit payload
|
||||
//
|
||||
void FT8::encode(int a174[], int s77[])
|
||||
void FT8::encode(int a174[], const int s77[])
|
||||
{
|
||||
int a91[91]; // msg + CRC
|
||||
std::fill(a91, a91 + 91, 0);
|
||||
|
||||
@ -273,7 +273,7 @@ public:
|
||||
// adds the 14 bit CRC to obtain 91 bits
|
||||
// apply (174, 91) generator mastrix to obtain the 83 parity bits
|
||||
// append the 83 bits to the 91 bits message e+ crc to obtain the 174 bit payload
|
||||
static void encode(int a174[], int s77[]);
|
||||
static void encode(int a174[], const int s77[]);
|
||||
|
||||
//
|
||||
// set ones and zero symbol indexes
|
||||
|
||||
@ -396,7 +396,7 @@ void LDPC::ft8_crc(int msg1[], int msglen, int out[14])
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < msglen + 14; i++)
|
||||
for (int i = 0; i < 14; i++)
|
||||
{
|
||||
out[i] = msg[msglen + i];
|
||||
}
|
||||
|
||||
@ -432,6 +432,14 @@ void ChirpChatDemod::applySettings(const ChirpChatDemodSettings& settings, bool
|
||||
<< " m_sendViaUDP: " << settings.m_sendViaUDP
|
||||
<< " m_udpAddress: " << settings.m_udpAddress
|
||||
<< " m_udpPort: " << settings.m_udpPort
|
||||
<< " m_decodeActive: " << settings.m_decodeActive
|
||||
<< " m_eomSquelchTenths: " << settings.m_eomSquelchTenths
|
||||
<< " m_nbSymbolsMax: " << settings.m_nbSymbolsMax
|
||||
<< " m_preambleChirps: " << settings.m_preambleChirps
|
||||
<< " m_streamIndex: " << settings.m_streamIndex
|
||||
<< " m_useReverseAPI: " << settings.m_useReverseAPI
|
||||
<< " m_fftWindow: " << settings.m_fftWindow
|
||||
<< " m_invertRamps: " << settings.m_invertRamps
|
||||
<< " m_rgbColor: " << settings.m_rgbColor
|
||||
<< " m_title: " << settings.m_title
|
||||
<< " force: " << force;
|
||||
@ -533,6 +541,9 @@ void ChirpChatDemod::applySettings(const ChirpChatDemodSettings& settings, bool
|
||||
if ((settings.m_autoNbSymbolsMax != m_settings.m_autoNbSymbolsMax) || force) {
|
||||
reverseAPIKeys.append("autoNbSymbolsMax");
|
||||
}
|
||||
if ((settings.m_invertRamps != m_settings.m_invertRamps) || force) {
|
||||
reverseAPIKeys.append("invertRamps");
|
||||
}
|
||||
|
||||
if ((settings.m_udpAddress != m_settings.m_udpAddress) || force)
|
||||
{
|
||||
@ -694,6 +705,9 @@ void ChirpChatDemod::webapiUpdateChannelSettings(
|
||||
uint16_t port = response.getChirpChatDemodSettings()->getUdpPort();
|
||||
settings.m_udpPort = port < 1024 ? 1024 : port;
|
||||
}
|
||||
if (channelSettingsKeys.contains("invertRamps")) {
|
||||
settings.m_invertRamps = response.getChirpChatDemodSettings()->getInvertRamps() != 0;
|
||||
}
|
||||
if (channelSettingsKeys.contains("rgbColor")) {
|
||||
settings.m_rgbColor = response.getChirpChatDemodSettings()->getRgbColor();
|
||||
}
|
||||
@ -757,6 +771,7 @@ void ChirpChatDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings
|
||||
response.getChirpChatDemodSettings()->setHasCrc(settings.m_hasCRC ? 1 : 0);
|
||||
response.getChirpChatDemodSettings()->setHasHeader(settings.m_hasHeader ? 1 : 0);
|
||||
response.getChirpChatDemodSettings()->setSendViaUdp(settings.m_sendViaUDP ? 1 : 0);
|
||||
response.getChirpChatDemodSettings()->setInvertRamps(settings.m_invertRamps ? 1 : 0);
|
||||
|
||||
if (response.getChirpChatDemodSettings()->getUdpAddress()) {
|
||||
*response.getChirpChatDemodSettings()->getUdpAddress() = settings.m_udpAddress;
|
||||
@ -978,6 +993,9 @@ void ChirpChatDemod::webapiFormatChannelSettings(
|
||||
if (channelSettingsKeys.contains("updPort") || force) {
|
||||
swgChirpChatDemodSettings->setUdpPort(settings.m_udpPort);
|
||||
}
|
||||
if (channelSettingsKeys.contains("invertRamps") || force) {
|
||||
swgChirpChatDemodSettings->setInvertRamps(settings.m_invertRamps ? 1 : 0);
|
||||
}
|
||||
if (channelSettingsKeys.contains("rgbColor") || force) {
|
||||
swgChirpChatDemodSettings->setRgbColor(settings.m_rgbColor);
|
||||
}
|
||||
|
||||
@ -338,6 +338,12 @@ void ChirpChatDemodGUI::on_udpPort_editingFinished()
|
||||
applySettings();
|
||||
}
|
||||
|
||||
void ChirpChatDemodGUI::on_invertRamps_stateChanged(int state)
|
||||
{
|
||||
m_settings.m_invertRamps = (state == Qt::Checked);
|
||||
applySettings();
|
||||
}
|
||||
|
||||
void ChirpChatDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown)
|
||||
{
|
||||
(void) widget;
|
||||
@ -529,6 +535,7 @@ void ChirpChatDemodGUI::displaySettings()
|
||||
}
|
||||
|
||||
ui->messageLengthAuto->setChecked(m_settings.m_autoNbSymbolsMax);
|
||||
ui->invertRamps->setChecked(m_settings.m_invertRamps);
|
||||
|
||||
displaySquelch();
|
||||
updateIndexLabel();
|
||||
@ -854,6 +861,7 @@ void ChirpChatDemodGUI::makeUIConnections()
|
||||
QObject::connect(ui->udpSend, &QCheckBox::stateChanged, this, &ChirpChatDemodGUI::on_udpSend_stateChanged);
|
||||
QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &ChirpChatDemodGUI::on_udpAddress_editingFinished);
|
||||
QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &ChirpChatDemodGUI::on_udpPort_editingFinished);
|
||||
QObject::connect(ui->invertRamps, &QCheckBox::stateChanged, this, &ChirpChatDemodGUI::on_invertRamps_stateChanged);
|
||||
}
|
||||
|
||||
void ChirpChatDemodGUI::updateAbsoluteCenterFrequency()
|
||||
|
||||
@ -82,6 +82,7 @@ private slots:
|
||||
void on_udpSend_stateChanged(int state);
|
||||
void on_udpAddress_editingFinished();
|
||||
void on_udpPort_editingFinished();
|
||||
void on_invertRamps_stateChanged(int state);
|
||||
void onWidgetRolled(QWidget* widget, bool rollDown);
|
||||
void onMenuDialogCalled(const QPoint& p);
|
||||
void channelMarkerHighlightedByCursor();
|
||||
|
||||
@ -526,6 +526,23 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_12">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="invertRamps">
|
||||
<property name="toolTip">
|
||||
<string>Invert preamble, SFD and payload ramps</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Inv</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
|
||||
@ -80,6 +80,7 @@ void ChirpChatDemodSettings::resetToDefaults()
|
||||
m_hasCRC = true;
|
||||
m_hasHeader = true;
|
||||
m_sendViaUDP = false;
|
||||
m_invertRamps = false;
|
||||
m_udpAddress = "127.0.0.1";
|
||||
m_udpPort = 9999;
|
||||
m_rgbColor = QColor(255, 0, 255).rgb();
|
||||
@ -121,6 +122,7 @@ QByteArray ChirpChatDemodSettings::serialize() const
|
||||
s.writeBool(15, m_hasHeader);
|
||||
s.writeU32(17, m_preambleChirps);
|
||||
s.writeS32(18, (int) m_fftWindow);
|
||||
s.writeBool(19, m_invertRamps);
|
||||
s.writeBool(20, m_useReverseAPI);
|
||||
s.writeString(21, m_reverseAPIAddress);
|
||||
s.writeU32(22, m_reverseAPIPort);
|
||||
@ -188,6 +190,7 @@ bool ChirpChatDemodSettings::deserialize(const QByteArray& data)
|
||||
d.readU32(17, &m_preambleChirps, 8);
|
||||
d.readS32(18, &tmp, (int) FFTWindow::Rectangle);
|
||||
m_fftWindow = (FFTWindow::Function) tmp;
|
||||
d.readBool(19, &m_invertRamps, false);
|
||||
d.readBool(20, &m_useReverseAPI, false);
|
||||
d.readString(21, &m_reverseAPIAddress, "127.0.0.1");
|
||||
d.readU32(22, &utmp, 0);
|
||||
@ -232,6 +235,113 @@ bool ChirpChatDemodSettings::deserialize(const QByteArray& data)
|
||||
}
|
||||
}
|
||||
|
||||
void ChirpChatDemodSettings::applySettings(const QStringList& settingsKeys, const ChirpChatDemodSettings& settings)
|
||||
{
|
||||
if (settingsKeys.contains("inputFrequencyOffset"))
|
||||
m_inputFrequencyOffset = settings.m_inputFrequencyOffset;
|
||||
if (settingsKeys.contains("bandwidthIndex"))
|
||||
m_bandwidthIndex = settings.m_bandwidthIndex;
|
||||
if (settingsKeys.contains("spreadFactor"))
|
||||
m_spreadFactor = settings.m_spreadFactor;
|
||||
if (settingsKeys.contains("deBits"))
|
||||
m_deBits = settings.m_deBits;
|
||||
if (settingsKeys.contains("fftWindow"))
|
||||
m_fftWindow = settings.m_fftWindow;
|
||||
if (settingsKeys.contains("codingScheme"))
|
||||
m_codingScheme = settings.m_codingScheme;
|
||||
if (settingsKeys.contains("decodeActive"))
|
||||
m_decodeActive = settings.m_decodeActive;
|
||||
if (settingsKeys.contains("eomSquelchTenths"))
|
||||
m_eomSquelchTenths = settings.m_eomSquelchTenths;
|
||||
if (settingsKeys.contains("nbSymbolsMax"))
|
||||
m_nbSymbolsMax = settings.m_nbSymbolsMax;
|
||||
if (settingsKeys.contains("preambleChirps"))
|
||||
m_preambleChirps = settings.m_preambleChirps;
|
||||
if (settingsKeys.contains("nbParityBits"))
|
||||
m_nbParityBits = settings.m_nbParityBits;
|
||||
if (settingsKeys.contains("packetLength"))
|
||||
m_packetLength = settings.m_packetLength;
|
||||
if (settingsKeys.contains("hasCRC"))
|
||||
m_hasCRC = settings.m_hasCRC;
|
||||
if (settingsKeys.contains("hasHeader"))
|
||||
m_hasHeader = settings.m_hasHeader;
|
||||
if (settingsKeys.contains("sendViaUDP"))
|
||||
m_sendViaUDP = settings.m_sendViaUDP;
|
||||
if (settingsKeys.contains("invertRamps"))
|
||||
m_invertRamps = settings.m_invertRamps;
|
||||
if (settingsKeys.contains("udpAddress"))
|
||||
m_udpAddress = settings.m_udpAddress;
|
||||
if (settingsKeys.contains("udpPort"))
|
||||
m_udpPort = settings.m_udpPort;
|
||||
if (settingsKeys.contains("useReverseAPI"))
|
||||
m_useReverseAPI = settings.m_useReverseAPI;
|
||||
if (settingsKeys.contains("reverseAPIAddress"))
|
||||
m_reverseAPIAddress = settings.m_reverseAPIAddress;
|
||||
if (settingsKeys.contains("reverseAPIPort"))
|
||||
m_reverseAPIPort = settings.m_reverseAPIPort;
|
||||
if (settingsKeys.contains("reverseAPIDeviceIndex"))
|
||||
m_reverseAPIDeviceIndex = settings.m_reverseAPIDeviceIndex;
|
||||
if (settingsKeys.contains("reverseAPIChannelIndex"))
|
||||
m_reverseAPIChannelIndex = settings.m_reverseAPIChannelIndex;
|
||||
if (settingsKeys.contains("streamIndex"))
|
||||
m_streamIndex = settings.m_streamIndex;
|
||||
}
|
||||
|
||||
QString ChirpChatDemodSettings::getDebugString(const QStringList& settingsKeys, bool force) const
|
||||
{
|
||||
QString debug;
|
||||
|
||||
if (force || settingsKeys.contains("inputFrequencyOffset"))
|
||||
debug += QString("InputFrequencyOffset: %1 ").arg(m_inputFrequencyOffset);
|
||||
if (force || settingsKeys.contains("bandwidthIndex"))
|
||||
debug += QString("BandwidthIndex: %1 ").arg(m_bandwidthIndex);
|
||||
if (force || settingsKeys.contains("spreadFactor"))
|
||||
debug += QString("SpreadFactor: %1 ").arg(m_spreadFactor);
|
||||
if (force || settingsKeys.contains("deBits"))
|
||||
debug += QString("DEBits: %1 ").arg(m_deBits);
|
||||
if (force || settingsKeys.contains("fftWindow"))
|
||||
debug += QString("FFTWindow: %1 ").arg((int) m_fftWindow);
|
||||
if (force || settingsKeys.contains("codingScheme"))
|
||||
debug += QString("CodingScheme: %1 ").arg((int) m_codingScheme);
|
||||
if (force || settingsKeys.contains("decodeActive"))
|
||||
debug += QString("DecodeActive: %1 ").arg(m_decodeActive);
|
||||
if (force || settingsKeys.contains("eomSquelchTenths"))
|
||||
debug += QString("EOMSquelchTenths: %1 ").arg(m_eomSquelchTenths);
|
||||
if (force || settingsKeys.contains("nbSymbolsMax"))
|
||||
debug += QString("NbSymbolsMax: %1 ").arg(m_nbSymbolsMax);
|
||||
if (force || settingsKeys.contains("preambleChirps"))
|
||||
debug += QString("PreambleChirps: %1 ").arg(m_preambleChirps);
|
||||
if (force || settingsKeys.contains("nbParityBits"))
|
||||
debug += QString("NbParityBits: %1 ").arg(m_nbParityBits);
|
||||
if (force || settingsKeys.contains("packetLength"))
|
||||
debug += QString("PacketLength: %1 ").arg(m_packetLength);
|
||||
if (force || settingsKeys.contains("hasCRC"))
|
||||
debug += QString("HasCRC: %1 ").arg(m_hasCRC);
|
||||
if (force || settingsKeys.contains("hasHeader"))
|
||||
debug += QString("HasHeader: %1 ").arg(m_hasHeader);
|
||||
if (force || settingsKeys.contains("sendViaUDP"))
|
||||
debug += QString("SendViaUDP: %1 ").arg(m_sendViaUDP);
|
||||
if (force || settingsKeys.contains("invertRamps"))
|
||||
debug += QString("InvertRamps: %1 ").arg(m_invertRamps);
|
||||
if (force || settingsKeys.contains("udpAddress"))
|
||||
debug += QString("UDPAddress: %1 ").arg(m_udpAddress);
|
||||
if (force || settingsKeys.contains("udpPort"))
|
||||
debug += QString("UDPPort: %1 ").arg(m_udpPort);
|
||||
if (force || settingsKeys.contains("useReverseAPI"))
|
||||
debug += QString("UseReverseAPI: %1 ").arg(m_useReverseAPI);
|
||||
if (force || settingsKeys.contains("reverseAPIAddress"))
|
||||
debug += QString("ReverseAPIAddress: %1 ").arg(m_reverseAPIAddress);
|
||||
if (force || settingsKeys.contains("reverseAPIPort"))
|
||||
debug += QString("ReverseAPIPort: %1 ").arg(m_reverseAPIPort);
|
||||
if (force || settingsKeys.contains("reverseAPIDeviceIndex"))
|
||||
debug += QString("ReverseAPIDeviceIndex: %1 ").arg(m_reverseAPIDeviceIndex);
|
||||
if (force || settingsKeys.contains("reverseAPIChannelIndex"))
|
||||
debug += QString("ReverseAPIChannelIndex: %1 ").arg(m_reverseAPIChannelIndex);
|
||||
if (force || settingsKeys.contains("streamIndex"))
|
||||
debug += QString("StreamIndex: %1 ").arg(m_streamIndex);
|
||||
return debug;
|
||||
}
|
||||
|
||||
unsigned int ChirpChatDemodSettings::getNbSFDFourths() const
|
||||
{
|
||||
switch (m_codingScheme)
|
||||
|
||||
@ -65,6 +65,7 @@ struct ChirpChatDemodSettings
|
||||
bool m_hasCRC; //!< Payload has CRC (LoRa)
|
||||
bool m_hasHeader; //!< Header present before actual payload (LoRa)
|
||||
bool m_sendViaUDP; //!< Send decoded message via UDP
|
||||
bool m_invertRamps; //!< Invert chirp ramps vs standard LoRa (up/down/up is standard)
|
||||
QString m_udpAddress; //!< UDP address where to send message
|
||||
uint16_t m_udpPort; //!< UDP port where to send message
|
||||
uint32_t m_rgbColor;
|
||||
@ -96,6 +97,8 @@ struct ChirpChatDemodSettings
|
||||
bool hasSyncWord() const; //!< Only LoRa has a syncword (for the moment)
|
||||
QByteArray serialize() const;
|
||||
bool deserialize(const QByteArray& data);
|
||||
void applySettings(const QStringList& settingsKeys, const ChirpChatDemodSettings& settings);
|
||||
QString getDebugString(const QStringList& settingsKeys, bool force=false) const;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -158,7 +158,7 @@ void ChirpChatDemodSink::processSample(const Complex& ci)
|
||||
}
|
||||
else if (m_state == ChirpChatStateDetectPreamble) // look for preamble
|
||||
{
|
||||
m_fft->in()[m_fftCounter++] = ci * m_downChirps[m_chirp]; // de-chirp the up ramp
|
||||
m_fft->in()[m_fftCounter++] = ci * (m_settings.m_invertRamps ? m_upChirps[m_chirp] : m_downChirps[m_chirp]); // de-chirp the preamble ramp
|
||||
|
||||
if (m_fftCounter == m_fftLength)
|
||||
{
|
||||
@ -178,6 +178,11 @@ void ChirpChatDemodSink::processSample(const Complex& ci)
|
||||
m_fftInterpolation
|
||||
) / m_fftInterpolation;
|
||||
|
||||
// When ramps are inverted, FFT output interpretation is reversed
|
||||
if (m_settings.m_invertRamps) {
|
||||
imax = (m_nbSymbols - imax) % m_nbSymbols;
|
||||
}
|
||||
|
||||
if (m_magsqQueue.size() > m_settings.m_preambleChirps) {
|
||||
m_magsqQueue.pop();
|
||||
}
|
||||
@ -246,8 +251,8 @@ void ChirpChatDemodSink::processSample(const Complex& ci)
|
||||
}
|
||||
else if (m_state == ChirpChatStatePreamble) // preamble found look for SFD start
|
||||
{
|
||||
m_fft->in()[m_fftCounter] = ci * m_downChirps[m_chirp]; // de-chirp the up ramp
|
||||
m_fftSFD->in()[m_fftCounter] = ci * m_upChirps[m_chirp]; // de-chirp the down ramp
|
||||
m_fft->in()[m_fftCounter] = ci * (m_settings.m_invertRamps ? m_upChirps[m_chirp] : m_downChirps[m_chirp]); // de-chirp the preamble ramp
|
||||
m_fftSFD->in()[m_fftCounter] = ci * (m_settings.m_invertRamps ? m_downChirps[m_chirp] : m_upChirps[m_chirp]); // de-chirp the SFD ramp
|
||||
m_fftCounter++;
|
||||
|
||||
if (m_fftCounter == m_fftLength)
|
||||
@ -284,6 +289,11 @@ void ChirpChatDemodSink::processSample(const Complex& ci)
|
||||
m_fftInterpolation
|
||||
) / m_fftInterpolation;
|
||||
|
||||
// When ramps are inverted, FFT output interpretation is reversed
|
||||
if (m_settings.m_invertRamps) {
|
||||
imax = (m_nbSymbols - imax) % m_nbSymbols;
|
||||
}
|
||||
|
||||
m_preambleHistory[m_chirpCount] = imax;
|
||||
m_chirpCount++;
|
||||
double preDrop = magsqPre - magsqSFD;
|
||||
@ -374,7 +384,7 @@ void ChirpChatDemodSink::processSample(const Complex& ci)
|
||||
}
|
||||
else if (m_state == ChirpChatStateReadPayload)
|
||||
{
|
||||
m_fft->in()[m_fftCounter] = ci * m_downChirps[m_chirp]; // de-chirp the up ramp
|
||||
m_fft->in()[m_fftCounter] = ci * (m_settings.m_invertRamps ? m_upChirps[m_chirp] : m_downChirps[m_chirp]); // de-chirp the payload ramp
|
||||
m_fftCounter++;
|
||||
|
||||
if (m_fftCounter == m_fftLength)
|
||||
@ -438,6 +448,11 @@ void ChirpChatDemodSink::processSample(const Complex& ci)
|
||||
);
|
||||
}
|
||||
|
||||
// When ramps are inverted, FFT output interpretation is reversed
|
||||
if (m_settings.m_invertRamps) {
|
||||
imax = (m_nbSymbols * m_fftInterpolation - imax) % (m_nbSymbols * m_fftInterpolation);
|
||||
}
|
||||
|
||||
symbol = evalSymbol(imax) % m_nbSymbolsEff;
|
||||
m_decodeMsg->pushBackSymbol(symbol);
|
||||
}
|
||||
|
||||
@ -120,6 +120,12 @@ In practice it is difficult to make correct decodes if only one FFT bin is used
|
||||
|
||||
This is the number of chirps expected in the preamble and has to be agreed between the transmitter and receiver.
|
||||
|
||||
<h3>15: Invert chirp ramps</h3>
|
||||
|
||||
The LoRa standard is up-chirps for the preamble, down-chirps for the SFD and up-chirps for the payload.
|
||||
|
||||
When you check this option it inverts the direction of the chirps thus becoming down-chirps for the preamble, up-chirps for the SFD and down-chirps for the payload.
|
||||
|
||||
<h3>A: Payload controls and indicators</h3>
|
||||
|
||||

|
||||
@ -165,6 +171,8 @@ LoRa mode only. This is the number of parity bits in the Hamming code used in th
|
||||
|
||||
When a header is expected this control is disabled because the value used is the one found in the header.
|
||||
|
||||
In FT8 mode there is no FEC as FEC is handled within the FT payload (with LDPC)
|
||||
|
||||
<h4>A.9: Payload CRC presence</h4>
|
||||
|
||||
LoRa mode: Use this checkbox to tell if you expect a 2 byte CRC at the end of the payload. FT mode: there is always a CRC.
|
||||
@ -284,3 +292,43 @@ Controls are the usual controls of spectrum displays with the following restrict
|
||||
|
||||
- The window type is non operating because the FFT window is chosen by (7)
|
||||
- The FFT size can be changed however it is set to 2<sup>SF</sup> where SF is the spread factor and thus displays correctly
|
||||
|
||||
|
||||
<h2>Common LoRa settings</h2>
|
||||
|
||||
CR is the code rate and translates to FEC according to the following table
|
||||
|
||||
| CR | FEC |
|
||||
| --- | --- |
|
||||
| 4/5 | 1 |
|
||||
| 4/6 | 2 |
|
||||
| 4/7 | 3 |
|
||||
| 4/8 | 4 |
|
||||
|
||||
When DE is on set DE to 2 else 0
|
||||
|
||||
<h3>Generic</h3>
|
||||
|
||||
| Use Case | SF | CR | BW (kHz) | DE Enabled? | Notes |
|
||||
| --------------------- | --------- | ------- | -------- | ----------- | ----------------------------------------------------------- |
|
||||
| High rate/short range | SF7 | 4/5 | 500 | Off | Fastest, urban/close devices |
|
||||
| Balanced/general IoT | SF7–SF9 | 4/5–4/6 | 125–250 | Off | TTN default, good for gateways |
|
||||
| Long range/rural | SF10–SF12 | 4/6–4/8 | 125 | On | Max sensitivity (-137 dBm), slow airtime |
|
||||
| Extreme range | SF12 | 4/8 | 125 | On | Best for weak signals, long packets |
|
||||
|
||||
<h3>Meshtastic</h3>
|
||||
|
||||
<h4>Quick facts</h4>
|
||||
|
||||
- Uses 0x2B sync byte
|
||||
- In Europe the default channel is centered on 869.525 MHz
|
||||
|
||||
<h4>Presets table</h4>
|
||||
|
||||
| Preset | SF | CR | BW (kHz) | DE? | Use Case |
|
||||
| ------------------- | -- | ---- | -------- | --- | ---------------------------------------------------- |
|
||||
| LONG_FAST (default) | 11 | 4/8 | 250 | On | Long range, moderate speed |
|
||||
| MEDIUM_SLOW | 10 | 4/7 | 250 | On | Balanced range/reliability |
|
||||
| SHORT_FAST | 9 | 4/7 | 250 | Off | Urban/short hops, faster |
|
||||
| SHORT_TURBO | 7 | 4/5 | 500 | Off | Max speed, shortest range (region-limited) |
|
||||
| LONG_SLOW | 12 | 4/8 | 125 | On | Extreme range, slowest meshtastic |
|
||||
|
||||
@ -186,6 +186,21 @@ void ChirpChatMod::applySettings(const ChirpChatModSettings& settings, bool forc
|
||||
<< " m_73message: " << settings.m_73Message
|
||||
<< " m_qsoTextMessage: " << settings.m_qsoTextMessage
|
||||
<< " m_textMessage: " << settings.m_textMessage
|
||||
<< " m_bytesMessage: " << settings.m_bytesMessage.toHex()
|
||||
<< " m_spreadFactor: " << settings.m_spreadFactor
|
||||
<< " m_deBits: " << settings.m_deBits
|
||||
<< " m_codingScheme: " << settings.m_codingScheme
|
||||
<< " m_nbParityBits: " << settings.m_nbParityBits
|
||||
<< " m_hasCRC: " << settings.m_hasCRC
|
||||
<< " m_hasHeader: " << settings.m_hasHeader
|
||||
<< " m_messageType: " << settings.m_messageType
|
||||
<< " m_preambleChirps: " << settings.m_preambleChirps
|
||||
<< " m_quietMillis: " << settings.m_quietMillis
|
||||
<< " m_messageRepeat: " << settings.m_messageRepeat
|
||||
<< " m_udpEnabled: " << settings.m_udpEnabled
|
||||
<< " m_udpAddress: " << settings.m_udpAddress
|
||||
<< " m_udpPort: " << settings.m_udpPort
|
||||
<< " m_invertRamps: " << settings.m_invertRamps
|
||||
<< " m_useReverseAPI: " << settings.m_useReverseAPI
|
||||
<< " m_reverseAPIAddress: " << settings.m_reverseAPIAddress
|
||||
<< " m_reverseAPIAddress: " << settings.m_reverseAPIPort
|
||||
@ -216,6 +231,17 @@ void ChirpChatMod::applySettings(const ChirpChatModSettings& settings, bool forc
|
||||
m_encoder.setNbSymbolBits(settings.m_spreadFactor, settings.m_deBits);
|
||||
}
|
||||
|
||||
if ((settings.m_spreadFactor != m_settings.m_spreadFactor)
|
||||
|| (settings.m_bandwidthIndex != m_settings.m_bandwidthIndex) || force)
|
||||
{
|
||||
if (getMessageQueueToGUI())
|
||||
{
|
||||
m_currentPayloadTime = (m_symbols.size()*(1<<settings.m_spreadFactor)*1000.0) / ChirpChatModSettings::bandwidths[settings.m_bandwidthIndex];
|
||||
MsgReportPayloadTime *rpt = MsgReportPayloadTime::create(m_currentPayloadTime, m_symbols.size());
|
||||
getMessageQueueToGUI()->push(rpt);
|
||||
}
|
||||
}
|
||||
|
||||
if ((settings.m_codingScheme != m_settings.m_codingScheme) || force)
|
||||
{
|
||||
reverseAPIKeys.append("codingScheme");
|
||||
@ -273,9 +299,17 @@ void ChirpChatMod::applySettings(const ChirpChatModSettings& settings, bool forc
|
||||
if ((settings.m_bytesMessage != m_settings.m_bytesMessage) || force) {
|
||||
reverseAPIKeys.append("bytesMessage");
|
||||
}
|
||||
if ((settings.m_preambleChirps != m_settings.m_preambleChirps) || force) {
|
||||
reverseAPIKeys.append("preambleChirps");
|
||||
}
|
||||
if ((settings.m_quietMillis != m_settings.m_quietMillis) || force) {
|
||||
reverseAPIKeys.append("quietMillis");
|
||||
}
|
||||
if ((settings.m_invertRamps != m_settings.m_invertRamps) || force) {
|
||||
reverseAPIKeys.append("invertRamps");
|
||||
}
|
||||
|
||||
ChirpChatModBaseband::MsgConfigureChirpChatModPayload *payloadMsg = nullptr;
|
||||
std::vector<unsigned short> symbols;
|
||||
|
||||
if ((settings.m_messageType == ChirpChatModSettings::MessageNone)
|
||||
&& ((settings.m_messageType != m_settings.m_messageType) || force))
|
||||
@ -294,18 +328,19 @@ void ChirpChatMod::applySettings(const ChirpChatModSettings& settings, bool forc
|
||||
|| (settings.m_textMessage != m_settings.m_textMessage)
|
||||
|| (settings.m_bytesMessage != m_settings.m_bytesMessage) || force)
|
||||
{
|
||||
m_encoder.encode(settings, symbols);
|
||||
payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(symbols);
|
||||
m_symbols.clear();
|
||||
m_encoder.encode(settings, m_symbols);
|
||||
payloadMsg = ChirpChatModBaseband::MsgConfigureChirpChatModPayload::create(m_symbols);
|
||||
}
|
||||
|
||||
if (payloadMsg)
|
||||
{
|
||||
m_basebandSource->getInputMessageQueue()->push(payloadMsg);
|
||||
m_currentPayloadTime = (symbols.size()*(1<<settings.m_spreadFactor)*1000.0) / ChirpChatModSettings::bandwidths[settings.m_bandwidthIndex];
|
||||
m_currentPayloadTime = (m_symbols.size()*(1<<settings.m_spreadFactor)*1000.0) / ChirpChatModSettings::bandwidths[settings.m_bandwidthIndex];
|
||||
|
||||
if (getMessageQueueToGUI())
|
||||
{
|
||||
MsgReportPayloadTime *rpt = MsgReportPayloadTime::create(m_currentPayloadTime, symbols.size());
|
||||
MsgReportPayloadTime *rpt = MsgReportPayloadTime::create(m_currentPayloadTime, m_symbols.size());
|
||||
getMessageQueueToGUI()->push(rpt);
|
||||
}
|
||||
}
|
||||
@ -552,6 +587,9 @@ void ChirpChatMod::webapiUpdateChannelSettings(
|
||||
if (channelSettingsKeys.contains("udpPort")) {
|
||||
settings.m_udpPort = response.getChirpChatModSettings()->getUdpPort();
|
||||
}
|
||||
if (channelSettingsKeys.contains("invertRamps")) {
|
||||
settings.m_invertRamps = response.getChirpChatModSettings()->getInvertRamps();
|
||||
}
|
||||
if (channelSettingsKeys.contains("rgbColor")) {
|
||||
settings.m_rgbColor = response.getChirpChatModSettings()->getRgbColor();
|
||||
}
|
||||
@ -703,6 +741,7 @@ void ChirpChatMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings&
|
||||
response.getChirpChatModSettings()->setUdpEnabled(settings.m_udpEnabled);
|
||||
response.getChirpChatModSettings()->setUdpAddress(new QString(settings.m_udpAddress));
|
||||
response.getChirpChatModSettings()->setUdpPort(settings.m_udpPort);
|
||||
response.getChirpChatModSettings()->setInvertRamps(settings.m_invertRamps ? 1 : 0);
|
||||
|
||||
response.getChirpChatModSettings()->setRgbColor(settings.m_rgbColor);
|
||||
|
||||
@ -935,6 +974,9 @@ void ChirpChatMod::webapiFormatChannelSettings(
|
||||
if (channelSettingsKeys.contains("udpPort") || force) {
|
||||
swgChirpChatModSettings->setUdpPort(settings.m_udpPort);
|
||||
}
|
||||
if (channelSettingsKeys.contains("invertRamps") || force) {
|
||||
swgChirpChatModSettings->setInvertRamps(settings.m_invertRamps ? 1 : 0);
|
||||
}
|
||||
|
||||
if (channelSettingsKeys.contains("rgbColor") || force) {
|
||||
swgChirpChatModSettings->setRgbColor(settings.m_rgbColor);
|
||||
|
||||
@ -167,6 +167,7 @@ private:
|
||||
ChirpChatModEncoder m_encoder; // TODO: check if it needs to be on its own thread
|
||||
ChirpChatModSettings m_settings;
|
||||
float m_currentPayloadTime;
|
||||
std::vector<unsigned short> m_symbols;
|
||||
|
||||
SampleVector m_sampleBuffer;
|
||||
QRecursiveMutex m_settingsMutex;
|
||||
|
||||
@ -359,6 +359,12 @@ void ChirpChatModGUI::on_udpPort_editingFinished()
|
||||
applySettings();
|
||||
}
|
||||
|
||||
void ChirpChatModGUI::on_invertRamps_stateChanged(int state)
|
||||
{
|
||||
m_settings.m_invertRamps = (state == Qt::Checked);
|
||||
applySettings();
|
||||
}
|
||||
|
||||
void ChirpChatModGUI::onWidgetRolled(QWidget* widget, bool rollDown)
|
||||
{
|
||||
(void) widget;
|
||||
@ -541,6 +547,7 @@ void ChirpChatModGUI::displaySettings()
|
||||
ui->udpEnabled->setChecked(m_settings.m_udpEnabled);
|
||||
ui->udpAddress->setText(m_settings.m_udpAddress);
|
||||
ui->udpPort->setText(QString::number(m_settings.m_udpPort));
|
||||
ui->invertRamps->setChecked(m_settings.m_invertRamps);
|
||||
getRollupContents()->restoreState(m_rollupState);
|
||||
updateAbsoluteCenterFrequency();
|
||||
blockApplySettings(false);
|
||||
@ -658,6 +665,7 @@ void ChirpChatModGUI::makeUIConnections()
|
||||
QObject::connect(ui->udpEnabled, &QCheckBox::clicked, this, &ChirpChatModGUI::on_udpEnabled_clicked);
|
||||
QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &ChirpChatModGUI::on_udpAddress_editingFinished);
|
||||
QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &ChirpChatModGUI::on_udpPort_editingFinished);
|
||||
QObject::connect(ui->invertRamps, &QCheckBox::stateChanged, this, &ChirpChatModGUI::on_invertRamps_stateChanged);
|
||||
}
|
||||
|
||||
void ChirpChatModGUI::updateAbsoluteCenterFrequency()
|
||||
|
||||
@ -123,6 +123,7 @@ private slots:
|
||||
void on_udpEnabled_clicked(bool checked);
|
||||
void on_udpAddress_editingFinished();
|
||||
void on_udpPort_editingFinished();
|
||||
void on_invertRamps_stateChanged(int state);
|
||||
void onWidgetRolled(QWidget* widget, bool rollDown);
|
||||
void onMenuDialogCalled(const QPoint& p);
|
||||
void tick();
|
||||
|
||||
@ -215,6 +215,23 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="invertRamps">
|
||||
<property name="toolTip">
|
||||
<string>Invert preamble, SFD and payload ramps</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Inv</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@ -88,6 +88,7 @@ void ChirpChatModSettings::resetToDefaults()
|
||||
m_udpEnabled = false;
|
||||
m_udpAddress = "127.0.0.1";
|
||||
m_udpPort = 9998;
|
||||
m_invertRamps = false;
|
||||
m_rgbColor = QColor(255, 0, 255).rgb();
|
||||
m_title = "ChirpChat Modulator";
|
||||
m_streamIndex = 0;
|
||||
@ -169,11 +170,7 @@ QByteArray ChirpChatModSettings::serialize() const
|
||||
s.writeU32(9, m_syncWord);
|
||||
s.writeU32(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.writeBool(12, m_invertRamps);
|
||||
s.writeString(20, m_beaconMessage);
|
||||
s.writeString(21, m_cqMessage);
|
||||
s.writeString(22, m_replyMessage);
|
||||
@ -250,19 +247,7 @@ bool ChirpChatModSettings::deserialize(const QByteArray& data)
|
||||
d.readU32(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.readBool(12, &m_invertRamps, false);
|
||||
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");
|
||||
@ -327,3 +312,181 @@ bool ChirpChatModSettings::deserialize(const QByteArray& data)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void ChirpChatModSettings::applySettings(const QStringList& settingsKeys, const ChirpChatModSettings& settings)
|
||||
{
|
||||
if (settingsKeys.contains("inputFrequencyOffset"))
|
||||
m_inputFrequencyOffset = settings.m_inputFrequencyOffset;
|
||||
if (settingsKeys.contains("bandwidthIndex"))
|
||||
m_bandwidthIndex = settings.m_bandwidthIndex;
|
||||
if (settingsKeys.contains("spreadFactor"))
|
||||
m_spreadFactor = settings.m_spreadFactor;
|
||||
if (settingsKeys.contains("deBits"))
|
||||
m_deBits = settings.m_deBits;
|
||||
if (settingsKeys.contains("codingScheme"))
|
||||
m_codingScheme = settings.m_codingScheme;
|
||||
if (settingsKeys.contains("preambleChirps"))
|
||||
m_preambleChirps = settings.m_preambleChirps;
|
||||
if (settingsKeys.contains("quietMillis"))
|
||||
m_quietMillis = settings.m_quietMillis;
|
||||
if (settingsKeys.contains("invertRamps"))
|
||||
m_invertRamps = settings.m_invertRamps;
|
||||
if (settingsKeys.contains("syncWord"))
|
||||
m_syncWord = settings.m_syncWord;
|
||||
if (settingsKeys.contains("channelMute"))
|
||||
m_channelMute = settings.m_channelMute;
|
||||
if (settingsKeys.contains("title"))
|
||||
m_title = settings.m_title;
|
||||
if (settingsKeys.contains("udpEnabled"))
|
||||
m_udpEnabled = settings.m_udpEnabled;
|
||||
if (settingsKeys.contains("udpAddress"))
|
||||
m_udpAddress = settings.m_udpAddress;
|
||||
if (settingsKeys.contains("udpPort"))
|
||||
m_udpPort = settings.m_udpPort;
|
||||
if (settingsKeys.contains("streamIndex"))
|
||||
m_streamIndex = settings.m_streamIndex;
|
||||
if (settingsKeys.contains("useReverseAPI"))
|
||||
m_useReverseAPI = settings.m_useReverseAPI;
|
||||
if (settingsKeys.contains("reverseAPIAddress"))
|
||||
m_reverseAPIAddress = settings.m_reverseAPIAddress;
|
||||
if (settingsKeys.contains("reverseAPIPort"))
|
||||
m_reverseAPIPort = settings.m_reverseAPIPort;
|
||||
if (settingsKeys.contains("reverseAPIDeviceIndex"))
|
||||
m_reverseAPIDeviceIndex = settings.m_reverseAPIDeviceIndex;
|
||||
if (settingsKeys.contains("reverseAPIChannelIndex"))
|
||||
m_reverseAPIChannelIndex = settings.m_reverseAPIChannelIndex;
|
||||
if (settingsKeys.contains("workspaceIndex"))
|
||||
m_workspaceIndex = settings.m_workspaceIndex;
|
||||
if (settingsKeys.contains("geometryBytes"))
|
||||
m_geometryBytes = settings.m_geometryBytes;
|
||||
if (settingsKeys.contains("hidden"))
|
||||
m_hidden = settings.m_hidden;
|
||||
if (settingsKeys.contains("channelMarker") && m_channelMarker && settings.m_channelMarker)
|
||||
m_channelMarker->deserialize(settings.m_channelMarker->serialize());
|
||||
if (settingsKeys.contains("rollupState") && m_rollupState && settings.m_rollupState)
|
||||
m_rollupState->deserialize(settings.m_rollupState->serialize());
|
||||
if (settingsKeys.contains("beaconMessage"))
|
||||
m_beaconMessage = settings.m_beaconMessage;
|
||||
if (settingsKeys.contains("cqMessage"))
|
||||
m_cqMessage = settings.m_cqMessage;
|
||||
if (settingsKeys.contains("replyMessage"))
|
||||
m_replyMessage = settings.m_replyMessage;
|
||||
if (settingsKeys.contains("reportMessage"))
|
||||
m_reportMessage = settings.m_reportMessage;
|
||||
if (settingsKeys.contains("replyReportMessage"))
|
||||
m_replyReportMessage = settings.m_replyReportMessage;
|
||||
if (settingsKeys.contains("rrrMessage"))
|
||||
m_rrrMessage = settings.m_rrrMessage;
|
||||
if (settingsKeys.contains("73Message"))
|
||||
m_73Message = settings.m_73Message;
|
||||
if (settingsKeys.contains("qsoTextMessage"))
|
||||
m_qsoTextMessage = settings.m_qsoTextMessage;
|
||||
if (settingsKeys.contains("textMessage"))
|
||||
m_textMessage = settings.m_textMessage;
|
||||
if (settingsKeys.contains("bytesMessage"))
|
||||
m_bytesMessage = settings.m_bytesMessage;
|
||||
if (settingsKeys.contains("messageType"))
|
||||
m_messageType = settings.m_messageType;
|
||||
if (settingsKeys.contains("nbParityBits"))
|
||||
m_nbParityBits = settings.m_nbParityBits;
|
||||
if (settingsKeys.contains("hasCRC"))
|
||||
m_hasCRC = settings.m_hasCRC;
|
||||
if (settingsKeys.contains("hasHeader"))
|
||||
m_hasHeader = settings.m_hasHeader;
|
||||
if (settingsKeys.contains("myCall"))
|
||||
m_myCall = settings.m_myCall;
|
||||
if (settingsKeys.contains("urCall"))
|
||||
m_urCall = settings.m_urCall;
|
||||
if (settingsKeys.contains("myLoc"))
|
||||
m_myLoc = settings.m_myLoc;
|
||||
if (settingsKeys.contains("myRpt"))
|
||||
m_myRpt = settings.m_myRpt;
|
||||
if (settingsKeys.contains("messageRepeat"))
|
||||
m_messageRepeat = settings.m_messageRepeat;
|
||||
}
|
||||
|
||||
QString ChirpChatModSettings::getDebugString(const QStringList& settingsKeys, bool force) const
|
||||
{
|
||||
QString debug;
|
||||
if (settingsKeys.contains("inputFrequencyOffset") || force)
|
||||
debug += QString("Input Frequency Offset: %1\n").arg(m_inputFrequencyOffset);
|
||||
if (settingsKeys.contains("bandwidthIndex") || force)
|
||||
debug += QString("Bandwidth Index: %1\n").arg(m_bandwidthIndex);
|
||||
if (settingsKeys.contains("spreadFactor") || force)
|
||||
debug += QString("Spread Factor: %1\n").arg(m_spreadFactor);
|
||||
if (settingsKeys.contains("deBits") || force)
|
||||
debug += QString("DE Bits: %1\n").arg(m_deBits);
|
||||
if (settingsKeys.contains("codingScheme") || force)
|
||||
debug += QString("Coding Scheme: %1\n").arg(m_codingScheme);
|
||||
if (settingsKeys.contains("preambleChirps") || force)
|
||||
debug += QString("Preamble Chirps: %1\n").arg(m_preambleChirps);
|
||||
if (settingsKeys.contains("quietMillis") || force)
|
||||
debug += QString("Quiet Millis: %1\n").arg(m_quietMillis);
|
||||
if (settingsKeys.contains("invertRamps") || force)
|
||||
debug += QString("Invert Ramps: %1\n").arg(m_invertRamps);
|
||||
if (settingsKeys.contains("syncWord") || force)
|
||||
debug += QString("Sync Word: %1\n").arg(m_syncWord);
|
||||
if (settingsKeys.contains("channelMute") || force)
|
||||
debug += QString("Channel Mute: %1\n").arg(m_channelMute);
|
||||
if (settingsKeys.contains("title") || force)
|
||||
debug += QString("Title: %1\n").arg(m_title);
|
||||
if (settingsKeys.contains("udpEnabled") || force)
|
||||
debug += QString("UDP Enabled: %1\n").arg(m_udpEnabled);
|
||||
if (settingsKeys.contains("udpAddress") || force)
|
||||
debug += QString("UDP Address: %1\n").arg(m_udpAddress);
|
||||
if (settingsKeys.contains("udpPort") || force)
|
||||
debug += QString("UDP Port: %1\n").arg(m_udpPort);
|
||||
if (settingsKeys.contains("streamIndex") || force)
|
||||
debug += QString("Stream Index: %1\n").arg(m_streamIndex);
|
||||
if (settingsKeys.contains("useReverseAPI") || force)
|
||||
debug += QString("Use Reverse API: %1\n").arg(m_useReverseAPI);
|
||||
if (settingsKeys.contains("reverseAPIAddress") || force)
|
||||
debug += QString("Reverse API Address: %1\n").arg(m_reverseAPIAddress);
|
||||
if (settingsKeys.contains("reverseAPIPort") || force)
|
||||
debug += QString("Reverse API Port: %1\n").arg(m_reverseAPIPort);
|
||||
if (settingsKeys.contains("reverseAPIDeviceIndex") || force)
|
||||
debug += QString("Reverse API Device Index: %1\n").arg(m_reverseAPIDeviceIndex);
|
||||
if (settingsKeys.contains("reverseAPIChannelIndex") || force)
|
||||
debug += QString("Reverse API Channel Index: %1\n").arg(m_reverseAPIChannelIndex);
|
||||
if (settingsKeys.contains("workspaceIndex") || force)
|
||||
debug += QString("Workspace Index: %1\n").arg(m_workspaceIndex);
|
||||
if (settingsKeys.contains("hidden") || force)
|
||||
debug += QString("Hidden: %1\n").arg(m_hidden);
|
||||
if (settingsKeys.contains("beaconMessage") || force)
|
||||
debug += QString("Beacon Message: %1\n").arg(m_beaconMessage);
|
||||
if (settingsKeys.contains("cqMessage") || force)
|
||||
debug += QString("CQ Message: %1\n").arg(m_cqMessage);
|
||||
if (settingsKeys.contains("replyMessage") || force)
|
||||
debug += QString("Reply Message: %1\n").arg(m_replyMessage);
|
||||
if (settingsKeys.contains("reportMessage") || force)
|
||||
debug += QString("Report Message: %1\n").arg(m_reportMessage);
|
||||
if (settingsKeys.contains("replyReportMessage") || force)
|
||||
debug += QString("Reply Report Message: %1\n").arg(m_replyReportMessage);
|
||||
if (settingsKeys.contains("rrrMessage") || force)
|
||||
debug += QString("RRR Message: %1\n").arg(m_rrrMessage);
|
||||
if (settingsKeys.contains("73Message") || force)
|
||||
debug += QString("73 Message: %1\n").arg(m_73Message);
|
||||
if (settingsKeys.contains("qsoTextMessage") || force)
|
||||
debug += QString("QSO Text Message: %1\n").arg(m_qsoTextMessage);
|
||||
if (settingsKeys.contains("textMessage") || force)
|
||||
debug += QString("Text Message: %1\n").arg(m_textMessage);
|
||||
if (settingsKeys.contains("messageType") || force)
|
||||
debug += QString("Message Type: %1\n").arg(m_messageType);
|
||||
if (settingsKeys.contains("nbParityBits") || force)
|
||||
debug += QString("Number of Parity Bits: %1\n").arg(m_nbParityBits);
|
||||
if (settingsKeys.contains("hasCRC") || force)
|
||||
debug += QString("Has CRC: %1\n").arg(m_hasCRC);
|
||||
if (settingsKeys.contains("hasHeader") || force)
|
||||
debug += QString("Has Header: %1\n").arg(m_hasHeader);
|
||||
if (settingsKeys.contains("myCall") || force)
|
||||
debug += QString("My Call: %1\n").arg(m_myCall);
|
||||
if (settingsKeys.contains("urCall") || force)
|
||||
debug += QString("UR Call: %1\n").arg(m_urCall);
|
||||
if (settingsKeys.contains("myLoc") || force)
|
||||
debug += QString("My Loc: %1\n").arg(m_myLoc);
|
||||
if (settingsKeys.contains("myRpt") || force)
|
||||
debug += QString("My Rpt: %1\n").arg(m_myRpt);
|
||||
if (settingsKeys.contains("messageRepeat") || force)
|
||||
debug += QString("Message Repeat: %1\n").arg(m_messageRepeat);
|
||||
return debug;
|
||||
}
|
||||
|
||||
@ -84,6 +84,7 @@ struct ChirpChatModSettings
|
||||
bool m_udpEnabled;
|
||||
QString m_udpAddress;
|
||||
uint16_t m_udpPort;
|
||||
bool m_invertRamps; //!< Invert chirp ramps vs standard LoRa (up/down/up is standard)
|
||||
uint32_t m_rgbColor;
|
||||
QString m_title;
|
||||
int m_streamIndex;
|
||||
@ -113,6 +114,8 @@ struct ChirpChatModSettings
|
||||
void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; }
|
||||
QByteArray serialize() const;
|
||||
bool deserialize(const QByteArray& data);
|
||||
void applySettings(const QStringList& settingsKeys, const ChirpChatModSettings& settings);
|
||||
QString getDebugString(const QStringList& settingsKeys, bool force=false) const;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -197,7 +197,7 @@ void ChirpChatModSource::modulateSample()
|
||||
}
|
||||
else if (m_state == ChirpChatStatePreamble)
|
||||
{
|
||||
m_modPhasor += m_phaseIncrements[m_chirp]; // up chirps
|
||||
m_modPhasor += (m_settings.m_invertRamps ? -1 : 1) * m_phaseIncrements[m_chirp]; // preamble chirps
|
||||
m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor));
|
||||
m_fftCounter++;
|
||||
|
||||
@ -228,7 +228,7 @@ void ChirpChatModSource::modulateSample()
|
||||
}
|
||||
else if (m_state == ChirpChatStateSyncWord)
|
||||
{
|
||||
m_modPhasor += m_phaseIncrements[m_chirp]; // up chirps
|
||||
m_modPhasor += (m_settings.m_invertRamps ? -1 : 1) * m_phaseIncrements[m_chirp]; // sync chirps same orientation as preamble
|
||||
m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor));
|
||||
m_fftCounter++;
|
||||
|
||||
@ -251,7 +251,7 @@ void ChirpChatModSource::modulateSample()
|
||||
}
|
||||
else if (m_state == ChirpChatStateSFD)
|
||||
{
|
||||
m_modPhasor -= m_phaseIncrements[m_chirp]; // down chirps
|
||||
m_modPhasor -= (m_settings.m_invertRamps ? -1 : 1) * m_phaseIncrements[m_chirp]; // SFD chirps
|
||||
m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor));
|
||||
m_fftCounter++;
|
||||
m_sampleCounter++;
|
||||
@ -280,7 +280,7 @@ void ChirpChatModSource::modulateSample()
|
||||
}
|
||||
else if (m_state == ChirpChatStatePayload)
|
||||
{
|
||||
m_modPhasor += m_phaseIncrements[m_chirp]; // up chirps
|
||||
m_modPhasor += (m_settings.m_invertRamps ? -1 : 1) * m_phaseIncrements[m_chirp]; // payload chirps
|
||||
m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor));
|
||||
m_fftCounter++;
|
||||
|
||||
|
||||
@ -72,6 +72,12 @@ Thus available bandwidths are:
|
||||
|
||||
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.
|
||||
|
||||
<h3>16: Invert chirp ramps</h3>
|
||||
|
||||
The LoRa standard is up-chirps for the preamble, down-chirps for the SFD and up-chirps for the payload.
|
||||
|
||||
When you check this option it inverts the direction of the chirps thus becoming down-chirps for the preamble, up-chirps for the SFD and down-chirps for the payload.
|
||||
|
||||
<h3>5: Spread Factor</h3>
|
||||
|
||||
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 2<sup>SF-DE</sup> where SF is the spread factor and DE the Distance Enhancement factor (6).
|
||||
@ -107,7 +113,7 @@ To populate messages you can specify your callsign (10.5), the other party calls
|
||||
- **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)
|
||||
- **FT**: FT8/FT4 coding is applied using data in (10.5) to (10.8) to encode the 174 bit message payload with CRC and FEC as per FT8/FT4 protocol using a type 1 (standard) type of message. Note that the report (10.8) must comply with the FT rule (coded "-35" to "+99" with a leading 0 for the number) and would usually represent the integer part of the S/N ratio in the ChirpChat demodulator receiver. Calls should not be prefixed nor suffixed and the first 4 characters of the locator must represent a valid 4 character grid square. Plain text messages (13 characters) are also supported with the 0.0 type of message using the text entered in the message box (11). These 174 bits are packed into (SF - DE) bits symbols padded with zero bits if necessary. For the details of the FT protocol see: https://wsjt.sourceforge.io/FT4_FT8_QEX.pdf
|
||||
- **FT**: FT8/FT4 coding is applied using data in (10.5) to (10.8) to encode the 174 bit message payload with CRC and FEC as per FT8/FT4 protocol using a type 1 (standard) type of message. Note that the report (10.8) must comply with the FT rule (coded "-35" to "+99" with a leading 0 for the number) and would usually represent the integer part of the S/N ratio in the ChirpChat demodulator receiver. Calls should not be prefixed nor suffixed and the first 4 characters of the locator must represent a valid 4 character grid square. Plain text messages (13 characters) are also supported with the 0.0 type of message using the text entered in the message box (11). These 174 bits are packed into (SF - DE) bits symbols padded with zero bits if necessary. For the details of the FT protocol see: https://wsjt.sourceforge.io/FT4_FT8_QEX.pdf For example for SF=9 and DE=3 we have 6 bits per symbols so the 174 bits are packed in exactly 29 symbols this should appear in the message length ML (13)
|
||||
|
||||
<h4>10.2: Number of FEC parity bits (LoRa)</h4>
|
||||
|
||||
@ -229,9 +235,9 @@ This window lets you edit the message selected in (10.9). You can use `%n` place
|
||||
|
||||
Use this line editor to specify the hex string used as the bytes message.
|
||||
|
||||
<h3>13: Symbol time</h3>
|
||||
<h3>13: Symbol time and message length</h3>
|
||||
|
||||
This is the duration of a symbol or chirp in milliseconds
|
||||
This is the duration of a symbol or chirp in milliseconds followed by the message length in the number of symbols
|
||||
|
||||
<h3>14: Payload time</h3>
|
||||
|
||||
|
||||
@ -4457,7 +4457,11 @@ margin-bottom: 20px;
|
||||
},
|
||||
"udpPort" : {
|
||||
"type" : "integer",
|
||||
"description" : "UDP destination properties"
|
||||
"description" : "UDP destination port"
|
||||
},
|
||||
"invertRamps" : {
|
||||
"type" : "integer",
|
||||
"description" : "Invert chirp ramps\n * 0 - Normal chirp ramps (upchirps for preamble and payload)\n * 1 - Inverted chirp ramps (downchirps for preamble and payload)\n"
|
||||
},
|
||||
"rgbColor" : {
|
||||
"type" : "integer"
|
||||
@ -4657,6 +4661,10 @@ margin-bottom: 20px;
|
||||
"type" : "integer",
|
||||
"description" : "UDP port to listen for messages to transmit on"
|
||||
},
|
||||
"invertRamps" : {
|
||||
"type" : "integer",
|
||||
"description" : "Invert chirp ramps\n * 0 - Normal chirp ramps (upchirps for preamble and payload)\n * 1 - Inverted chirp ramps (downchirps for preamble and payload)\n"
|
||||
},
|
||||
"rgbColor" : {
|
||||
"type" : "integer"
|
||||
},
|
||||
@ -59832,7 +59840,7 @@ except ApiException as e:
|
||||
</div>
|
||||
<div id="generator">
|
||||
<div class="content">
|
||||
Generated 2026-01-10T11:16:10.140+01:00
|
||||
Generated 2026-01-24T10:44:52.632+01:00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -100,8 +100,14 @@ ChirpChatDemodSettings:
|
||||
description: UDP destination udpAddress
|
||||
type: string
|
||||
udpPort:
|
||||
description: UDP destination properties
|
||||
description: UDP destination port
|
||||
type: integer
|
||||
invertRamps:
|
||||
type: integer
|
||||
description: >
|
||||
Invert chirp ramps
|
||||
* 0 - Normal chirp ramps (upchirps for preamble and payload)
|
||||
* 1 - Inverted chirp ramps (downchirps for preamble and payload)
|
||||
rgbColor:
|
||||
type: integer
|
||||
title:
|
||||
|
||||
@ -139,6 +139,12 @@ ChirpChatModSettings:
|
||||
udpPort:
|
||||
description: UDP port to listen for messages to transmit on
|
||||
type: integer
|
||||
invertRamps:
|
||||
type: integer
|
||||
description: >
|
||||
Invert chirp ramps
|
||||
* 0 - Normal chirp ramps (upchirps for preamble and payload)
|
||||
* 1 - Inverted chirp ramps (downchirps for preamble and payload)
|
||||
rgbColor:
|
||||
type: integer
|
||||
title:
|
||||
|
||||
@ -100,8 +100,14 @@ ChirpChatDemodSettings:
|
||||
description: UDP destination udpAddress
|
||||
type: string
|
||||
udpPort:
|
||||
description: UDP destination properties
|
||||
description: UDP destination port
|
||||
type: integer
|
||||
invertRamps:
|
||||
type: integer
|
||||
description: >
|
||||
Invert chirp ramps
|
||||
* 0 - Normal chirp ramps (upchirps for preamble and payload)
|
||||
* 1 - Inverted chirp ramps (downchirps for preamble and payload)
|
||||
rgbColor:
|
||||
type: integer
|
||||
title:
|
||||
|
||||
@ -139,6 +139,12 @@ ChirpChatModSettings:
|
||||
udpPort:
|
||||
description: UDP port to listen for messages to transmit on
|
||||
type: integer
|
||||
invertRamps:
|
||||
type: integer
|
||||
description: >
|
||||
Invert chirp ramps
|
||||
* 0 - Normal chirp ramps (upchirps for preamble and payload)
|
||||
* 1 - Inverted chirp ramps (downchirps for preamble and payload)
|
||||
rgbColor:
|
||||
type: integer
|
||||
title:
|
||||
|
||||
@ -4457,7 +4457,11 @@ margin-bottom: 20px;
|
||||
},
|
||||
"udpPort" : {
|
||||
"type" : "integer",
|
||||
"description" : "UDP destination properties"
|
||||
"description" : "UDP destination port"
|
||||
},
|
||||
"invertRamps" : {
|
||||
"type" : "integer",
|
||||
"description" : "Invert chirp ramps\n * 0 - Normal chirp ramps (upchirps for preamble and payload)\n * 1 - Inverted chirp ramps (downchirps for preamble and payload)\n"
|
||||
},
|
||||
"rgbColor" : {
|
||||
"type" : "integer"
|
||||
@ -4657,6 +4661,10 @@ margin-bottom: 20px;
|
||||
"type" : "integer",
|
||||
"description" : "UDP port to listen for messages to transmit on"
|
||||
},
|
||||
"invertRamps" : {
|
||||
"type" : "integer",
|
||||
"description" : "Invert chirp ramps\n * 0 - Normal chirp ramps (upchirps for preamble and payload)\n * 1 - Inverted chirp ramps (downchirps for preamble and payload)\n"
|
||||
},
|
||||
"rgbColor" : {
|
||||
"type" : "integer"
|
||||
},
|
||||
@ -59832,7 +59840,7 @@ except ApiException as e:
|
||||
</div>
|
||||
<div id="generator">
|
||||
<div class="content">
|
||||
Generated 2026-01-10T11:16:10.140+01:00
|
||||
Generated 2026-01-24T10:44:52.632+01:00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -66,6 +66,8 @@ SWGChirpChatDemodSettings::SWGChirpChatDemodSettings() {
|
||||
m_udp_address_isSet = false;
|
||||
udp_port = 0;
|
||||
m_udp_port_isSet = false;
|
||||
invert_ramps = 0;
|
||||
m_invert_ramps_isSet = false;
|
||||
rgb_color = 0;
|
||||
m_rgb_color_isSet = false;
|
||||
title = nullptr;
|
||||
@ -134,6 +136,8 @@ SWGChirpChatDemodSettings::init() {
|
||||
m_udp_address_isSet = false;
|
||||
udp_port = 0;
|
||||
m_udp_port_isSet = false;
|
||||
invert_ramps = 0;
|
||||
m_invert_ramps_isSet = false;
|
||||
rgb_color = 0;
|
||||
m_rgb_color_isSet = false;
|
||||
title = new QString("");
|
||||
@ -182,6 +186,7 @@ SWGChirpChatDemodSettings::cleanup() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(title != nullptr) {
|
||||
delete title;
|
||||
}
|
||||
@ -253,6 +258,8 @@ SWGChirpChatDemodSettings::fromJsonObject(QJsonObject &pJson) {
|
||||
|
||||
::SWGSDRangel::setValue(&udp_port, pJson["udpPort"], "qint32", "");
|
||||
|
||||
::SWGSDRangel::setValue(&invert_ramps, pJson["invertRamps"], "qint32", "");
|
||||
|
||||
::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", "");
|
||||
|
||||
::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString");
|
||||
@ -348,6 +355,9 @@ SWGChirpChatDemodSettings::asJsonObject() {
|
||||
if(m_udp_port_isSet){
|
||||
obj->insert("udpPort", QJsonValue(udp_port));
|
||||
}
|
||||
if(m_invert_ramps_isSet){
|
||||
obj->insert("invertRamps", QJsonValue(invert_ramps));
|
||||
}
|
||||
if(m_rgb_color_isSet){
|
||||
obj->insert("rgbColor", QJsonValue(rgb_color));
|
||||
}
|
||||
@ -575,6 +585,16 @@ SWGChirpChatDemodSettings::setUdpPort(qint32 udp_port) {
|
||||
this->m_udp_port_isSet = true;
|
||||
}
|
||||
|
||||
qint32
|
||||
SWGChirpChatDemodSettings::getInvertRamps() {
|
||||
return invert_ramps;
|
||||
}
|
||||
void
|
||||
SWGChirpChatDemodSettings::setInvertRamps(qint32 invert_ramps) {
|
||||
this->invert_ramps = invert_ramps;
|
||||
this->m_invert_ramps_isSet = true;
|
||||
}
|
||||
|
||||
qint32
|
||||
SWGChirpChatDemodSettings::getRgbColor() {
|
||||
return rgb_color;
|
||||
@ -747,6 +767,9 @@ SWGChirpChatDemodSettings::isSet(){
|
||||
if(m_udp_port_isSet){
|
||||
isObjectUpdated = true; break;
|
||||
}
|
||||
if(m_invert_ramps_isSet){
|
||||
isObjectUpdated = true; break;
|
||||
}
|
||||
if(m_rgb_color_isSet){
|
||||
isObjectUpdated = true; break;
|
||||
}
|
||||
|
||||
@ -102,6 +102,9 @@ public:
|
||||
qint32 getUdpPort();
|
||||
void setUdpPort(qint32 udp_port);
|
||||
|
||||
qint32 getInvertRamps();
|
||||
void setInvertRamps(qint32 invert_ramps);
|
||||
|
||||
qint32 getRgbColor();
|
||||
void setRgbColor(qint32 rgb_color);
|
||||
|
||||
@ -196,6 +199,9 @@ private:
|
||||
qint32 udp_port;
|
||||
bool m_udp_port_isSet;
|
||||
|
||||
qint32 invert_ramps;
|
||||
bool m_invert_ramps_isSet;
|
||||
|
||||
qint32 rgb_color;
|
||||
bool m_rgb_color_isSet;
|
||||
|
||||
|
||||
@ -90,6 +90,8 @@ SWGChirpChatModSettings::SWGChirpChatModSettings() {
|
||||
m_udp_address_isSet = false;
|
||||
udp_port = 0;
|
||||
m_udp_port_isSet = false;
|
||||
invert_ramps = 0;
|
||||
m_invert_ramps_isSet = false;
|
||||
rgb_color = 0;
|
||||
m_rgb_color_isSet = false;
|
||||
title = nullptr;
|
||||
@ -180,6 +182,8 @@ SWGChirpChatModSettings::init() {
|
||||
m_udp_address_isSet = false;
|
||||
udp_port = 0;
|
||||
m_udp_port_isSet = false;
|
||||
invert_ramps = 0;
|
||||
m_invert_ramps_isSet = false;
|
||||
rgb_color = 0;
|
||||
m_rgb_color_isSet = false;
|
||||
title = new QString("");
|
||||
@ -270,6 +274,7 @@ SWGChirpChatModSettings::cleanup() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(title != nullptr) {
|
||||
delete title;
|
||||
}
|
||||
@ -362,6 +367,8 @@ SWGChirpChatModSettings::fromJsonObject(QJsonObject &pJson) {
|
||||
|
||||
::SWGSDRangel::setValue(&udp_port, pJson["udpPort"], "qint32", "");
|
||||
|
||||
::SWGSDRangel::setValue(&invert_ramps, pJson["invertRamps"], "qint32", "");
|
||||
|
||||
::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", "");
|
||||
|
||||
::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString");
|
||||
@ -491,6 +498,9 @@ SWGChirpChatModSettings::asJsonObject() {
|
||||
if(m_udp_port_isSet){
|
||||
obj->insert("udpPort", QJsonValue(udp_port));
|
||||
}
|
||||
if(m_invert_ramps_isSet){
|
||||
obj->insert("invertRamps", QJsonValue(invert_ramps));
|
||||
}
|
||||
if(m_rgb_color_isSet){
|
||||
obj->insert("rgbColor", QJsonValue(rgb_color));
|
||||
}
|
||||
@ -835,6 +845,16 @@ SWGChirpChatModSettings::setUdpPort(qint32 udp_port) {
|
||||
this->m_udp_port_isSet = true;
|
||||
}
|
||||
|
||||
qint32
|
||||
SWGChirpChatModSettings::getInvertRamps() {
|
||||
return invert_ramps;
|
||||
}
|
||||
void
|
||||
SWGChirpChatModSettings::setInvertRamps(qint32 invert_ramps) {
|
||||
this->invert_ramps = invert_ramps;
|
||||
this->m_invert_ramps_isSet = true;
|
||||
}
|
||||
|
||||
qint32
|
||||
SWGChirpChatModSettings::getRgbColor() {
|
||||
return rgb_color;
|
||||
@ -1033,6 +1053,9 @@ SWGChirpChatModSettings::isSet(){
|
||||
if(m_udp_port_isSet){
|
||||
isObjectUpdated = true; break;
|
||||
}
|
||||
if(m_invert_ramps_isSet){
|
||||
isObjectUpdated = true; break;
|
||||
}
|
||||
if(m_rgb_color_isSet){
|
||||
isObjectUpdated = true; break;
|
||||
}
|
||||
|
||||
@ -138,6 +138,9 @@ public:
|
||||
qint32 getUdpPort();
|
||||
void setUdpPort(qint32 udp_port);
|
||||
|
||||
qint32 getInvertRamps();
|
||||
void setInvertRamps(qint32 invert_ramps);
|
||||
|
||||
qint32 getRgbColor();
|
||||
void setRgbColor(qint32 rgb_color);
|
||||
|
||||
@ -265,6 +268,9 @@ private:
|
||||
qint32 udp_port;
|
||||
bool m_udp_port_isSet;
|
||||
|
||||
qint32 invert_ramps;
|
||||
bool m_invert_ramps_isSet;
|
||||
|
||||
qint32 rgb_color;
|
||||
bool m_rgb_color_isSet;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user