From 403b62c354e3e37e598cf5230cd2b7e5afd96c41 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Tue, 16 May 2023 10:17:17 +0100 Subject: [PATCH] Add AIS slot map and additional message decoding --- doc/img/AISDemod_plugin_slotmap.png | Bin 0 -> 66198 bytes plugins/channelrx/demodais/aisdemod.cpp | 5 +- plugins/channelrx/demodais/aisdemod.h | 11 +- plugins/channelrx/demodais/aisdemodgui.cpp | 373 ++++++++++++-- plugins/channelrx/demodais/aisdemodgui.h | 28 +- plugins/channelrx/demodais/aisdemodgui.ui | 462 ++++++++++-------- .../channelrx/demodais/aisdemodsettings.cpp | 9 +- plugins/channelrx/demodais/aisdemodsettings.h | 6 +- plugins/channelrx/demodais/aisdemodsink.cpp | 106 +--- plugins/channelrx/demodais/aisdemodsink.h | 4 +- plugins/channelrx/demodais/readme.md | 25 +- plugins/feature/ais/aisgui.cpp | 26 +- plugins/feature/ais/aisgui.h | 1 + plugins/feature/ais/aisgui.ui | 11 + plugins/feature/ais/aissettings.h | 2 +- sdrbase/util/ais.cpp | 53 +- sdrbase/util/ais.h | 16 +- 17 files changed, 778 insertions(+), 360 deletions(-) create mode 100644 doc/img/AISDemod_plugin_slotmap.png diff --git a/doc/img/AISDemod_plugin_slotmap.png b/doc/img/AISDemod_plugin_slotmap.png new file mode 100644 index 0000000000000000000000000000000000000000..92e4d599b0d5d23ffb80dc8496deed360ffcf823 GIT binary patch literal 66198 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 KMNUMnLSTY|xu^U9 literal 0 HcmV?d00001 diff --git a/plugins/channelrx/demodais/aisdemod.cpp b/plugins/channelrx/demodais/aisdemod.cpp index a165428fe..0f639d196 100644 --- a/plugins/channelrx/demodais/aisdemod.cpp +++ b/plugins/channelrx/demodais/aisdemod.cpp @@ -223,7 +223,8 @@ bool AISDemod::handleMessage(const Message& cmd) << ais->getType() << "," << "\"" << ais->toString() << "\"" << "," << "\"" << ais->toNMEA() << "\"" << "," - << report.getSlot() << "\n"; + << report.getSlot() << "," + << report.getSlots() << "\n"; delete ais; } @@ -355,7 +356,7 @@ void AISDemod::applySettings(const AISDemodSettings& settings, bool force) if (newFile) { // Write header - m_logStream << "Date,Time,Data,MMSI,Type,Message,NMEA,Slot\n"; + m_logStream << "Date,Time,Data,MMSI,Type,Message,NMEA,Slot,Slots\n"; } } else diff --git a/plugins/channelrx/demodais/aisdemod.h b/plugins/channelrx/demodais/aisdemod.h index ba7bd76bf..d4c523b26 100644 --- a/plugins/channelrx/demodais/aisdemod.h +++ b/plugins/channelrx/demodais/aisdemod.h @@ -73,22 +73,25 @@ public: QByteArray getMessage() const { return m_message; } QDateTime getDateTime() const { return m_dateTime; } int getSlot() const { return m_slot; } + int getSlots() const { return m_slots; } - static MsgMessage* create(QByteArray message, QDateTime dateTime, int slot) + static MsgMessage* create(QByteArray message, QDateTime dateTime, int slot, int totalSlots) { - return new MsgMessage(message, dateTime, slot); + return new MsgMessage(message, dateTime, slot, totalSlots); } private: QByteArray m_message; QDateTime m_dateTime; int m_slot; + int m_slots; - MsgMessage(QByteArray message, QDateTime dateTime, int slot) : + MsgMessage(QByteArray message, QDateTime dateTime, int slot, int totalSlots) : Message(), m_message(message), m_dateTime(dateTime), - m_slot(slot) + m_slot(slot), + m_slots(totalSlots) { } }; diff --git a/plugins/channelrx/demodais/aisdemodgui.cpp b/plugins/channelrx/demodais/aisdemodgui.cpp index 7b60ad873..01c732105 100644 --- a/plugins/channelrx/demodais/aisdemodgui.cpp +++ b/plugins/channelrx/demodais/aisdemodgui.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include "aisdemodgui.h" @@ -40,6 +41,7 @@ #include "util/ais.h" #include "util/csv.h" #include "util/db.h" +#include "util/mmsi.h" #include "gui/basicchannelsettingsdialog.h" #include "gui/devicestreamselectiondialog.h" #include "gui/dialpopup.h" @@ -61,10 +63,12 @@ void AISDemodGUI::resizeTable() // Trailing spaces are for sort arrow int row = ui->messages->rowCount(); ui->messages->setRowCount(row + 1); - ui->messages->setItem(row, MESSAGE_COL_DATE, new QTableWidgetItem("Fri Apr 15 2016-")); + ui->messages->setItem(row, MESSAGE_COL_DATE, new QTableWidgetItem("Frid Apr 15 2016-")); ui->messages->setItem(row, MESSAGE_COL_TIME, new QTableWidgetItem("10:17:00")); ui->messages->setItem(row, MESSAGE_COL_MMSI, new QTableWidgetItem("123456789")); + ui->messages->setItem(row, MESSAGE_COL_COUNTRY, new QTableWidgetItem("flag")); ui->messages->setItem(row, MESSAGE_COL_TYPE, new QTableWidgetItem("Position report")); + ui->messages->setItem(row, MESSAGE_COL_ID, new QTableWidgetItem("25")); ui->messages->setItem(row, MESSAGE_COL_DATA, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZ")); ui->messages->setItem(row, MESSAGE_COL_NMEA, new QTableWidgetItem("!AIVDM,1,1,,A,AAAAAAAAAAAAAAAAAAAAAAAAAAAA,0*00")); ui->messages->setItem(row, MESSAGE_COL_HEX, new QTableWidgetItem("04058804002000069a0760728d9e00000040000000")); @@ -154,8 +158,270 @@ bool AISDemodGUI::deserialize(const QByteArray& data) } } +// Distint palette generator +// https://mokole.com/palette.html +QList AISDemodGUI::m_colors = { + 0xffffff, + 0xff0000, + 0x00ff00, + 0x0000ff, + 0x00ffff, + 0xff00ff, + 0x7fff00, + 0x000080, + 0xa9a9a9, + 0x2f4f4f, + 0x556b2f, + 0x8b4513, + 0x6b8e23, + 0x191970, + 0x006400, + 0x708090, + 0x8b0000, + 0x3cb371, + 0xbc8f8f, + 0x663399, + 0xb8860b, + 0xbdb76b, + 0x008b8b, + 0x4682b4, + 0xd2691e, + 0x9acd32, + 0xcd5c5c, + 0x32cd32, + 0x8fbc8f, + 0x8b008b, + 0xb03060, + 0x66cdaa, + 0x9932cc, + 0x00ced1, + 0xff8c00, + 0xffd700, + 0xc71585, + 0x0000cd, + 0xdeb887, + 0x00ff7f, + 0x4169e1, + 0xe9967a, + 0xdc143c, + 0x00bfff, + 0xf4a460, + 0x9370db, + 0xa020f0, + 0xff6347, + 0xd8bfd8, + 0xdb7093, + 0xf0e68c, + 0xffff54, + 0x6495ed, + 0xdda0dd, + 0x87ceeb, + 0xff1493, + 0xafeeee, + 0xee82ee, + 0xfaf0e6, + 0x98fb98, + 0x7fffd4, + 0xff69b4, + 0xfffacd, + 0xffb6c1, +}; + +QHash m_categoryColors = { + {"Class A Vessel", 0xff0000}, + {"Class B Vessel", 0x0000ff}, + {"Coast", 0x00ff00}, + {"Physical AtoN", 0xffff00}, + {"Virtual AtoN", 0xc0c000}, + {"Mobile AtoN", 0xa0a000}, + {"AtoN", 0x808000}, + {"SAR", 0x00ffff}, + {"SAR Aircraft", 0x00c0c0}, + {"SAR Helicopter", 0x00a0a0}, + {"Group", 0xff00ff}, + {"Man overboard", 0xc000c0}, + {"EPIRB", 0xa000a0}, + {"AMRD", 0x800080}, + {"Craft with parent ship", 0x600060} +}; + +QMutex AISDemodGUI::m_colorMutex; +QHash AISDemodGUI::m_usedInFrame; +QHash AISDemodGUI::m_slotMapColors; +QDateTime AISDemodGUI::m_lastColorUpdate; +QHash AISDemodGUI::m_category; + +QColor AISDemodGUI::getColor(const QString& mmsi) +{ + if (true) + { + if (m_category.contains(mmsi)) + { + QString category = m_category.value(mmsi); + if (m_categoryColors.contains(category)) { + return QColor(m_categoryColors.value(category)); + } + qDebug() << "No color for " << category; + } + else + { + // Use white for no category + return Qt::white; + } + } + else + { + QMutexLocker locker(&m_colorMutex); + + QColor color; + if (m_slotMapColors.contains(mmsi)) + { + m_usedInFrame.insert(mmsi, true); + color = m_slotMapColors.value(mmsi); + } + else + { + if (m_colors.size() > 0) + { + color = m_colors.takeFirst(); + qDebug() << "Taking colour from list " << color << "for" << mmsi << " - remaining " << m_colors.size(); + } + else + { + qDebug() << "Out of colors - looking to reuse"; + // Look for recently unused color + QMutableHashIterator it(m_usedInFrame); + color = Qt::black; + while (it.hasNext()) + { + it.next(); + if (!it.value()) + { + color = m_slotMapColors.value(it.key()); + if (color != Qt::black) + { + qDebug() << "Reusing " << color << " from " << it.key(); + m_slotMapColors.remove(it.key()); + m_usedInFrame.remove(it.key()); + break; + } + } + } + } + if (color != Qt::black) + { + m_slotMapColors.insert(mmsi, color); + m_usedInFrame.insert(mmsi, true); + } + else + { + qDebug() << "No free colours"; + } + } + + // Don't actually draw with black, as it's the background colour + if (color == Qt::black) { + return Qt::white; + } else { + return color; + } + } +} + +void AISDemodGUI::updateColors() +{ + QMutexLocker locker(&m_colorMutex); + + QDateTime currentDateTime = QDateTime::currentDateTime(); + if (!m_lastColorUpdate.isValid() || (m_lastColorUpdate.time().minute() != currentDateTime.time().minute())) + { + QHashIterator it(m_usedInFrame); + while (it.hasNext()) + { + it.next(); + m_usedInFrame.insert(it.key(), false); + } + } + m_lastColorUpdate = currentDateTime; +} + +void AISDemodGUI::updateSlotMap() +{ + QDateTime currentDateTime = QDateTime::currentDateTime(); + + if (!m_lastSlotMapUpdate.isValid() || (m_lastSlotMapUpdate.time().minute() != currentDateTime.time().minute())) + { + // Update slot utilisation stats for previous frame + ui->slotsFree->setText(QString::number(2250 - m_slotsUsed)); + ui->slotsUsed->setText(QString::number(m_slotsUsed)); + ui->slotUtilization->setValue(std::round(m_slotsUsed * 100.0 / 2250.0)); + m_slotsUsed = 0; + // Draw empty grid + m_image.fill(Qt::transparent); + //m_image.fill(Qt::); + m_painter.setPen(Qt::black); + for (int x = 0; x < m_image.width(); x += 5) { + m_painter.drawLine(x, 0, x, m_image.height() - 1); + } + for (int y = 0; y < m_image.height(); y += 5) { + m_painter.drawLine(0, y, m_image.width() - 1, y); + } + updateColors(); + } + ui->slotMap->setPixmap(m_image); + + m_lastSlotMapUpdate = currentDateTime; +} + +void AISDemodGUI::updateCategory(const QString& mmsi, const AISMessage *message) +{ + QMutexLocker locker(&m_colorMutex); + + if (!m_category.contains(mmsi)) + { + // Categorise by MMSI + QString category = MMSI::getCategory(mmsi); + if (category != "Ship") + { + m_category.insert(mmsi, category); + return; + } + + // Handle Search and Rescue Aircraft Report, where MMSI doesn't indicate SAR + if (message->m_id == 9) + { + m_category.insert(mmsi, "SAR"); + return; + } + + // If ship, determine Class A or B by message type + // See table 42 in ITU-R M.1371-5 + if ( (message->m_id <= 12) + || ((message->m_id >= 15) && (message->m_id <= 17)) + || ((message->m_id >= 20) && (message->m_id <= 23)) + || (message->m_id >= 25) + ) + { + m_category.insert(mmsi, "Class A Vessel"); + return; + } + + // Only Class B should transmit Part B static data reports + const AISStaticDataReport *staticDataReport = dynamic_cast(message); + if ( (message->m_id == 18) + || (message->m_id == 19) + || (staticDataReport && (staticDataReport->m_partNumber == 1)) + ) + { + m_category.insert(mmsi, "Class B Vessel"); + return; + } + // Other messages (such as safety) could be broadcast from either Class A or B + } +} + // Add row to table -void AISDemodGUI::messageReceived(const QByteArray& message, const QDateTime& dateTime, int slot) +void AISDemodGUI::messageReceived(const QByteArray& message, const QDateTime& dateTime, int slot, int totalSlots) { AISMessage *ais; @@ -174,7 +440,9 @@ void AISDemodGUI::messageReceived(const QByteArray& message, const QDateTime& da QTableWidgetItem *dateItem = new QTableWidgetItem(); QTableWidgetItem *timeItem = new QTableWidgetItem(); QTableWidgetItem *mmsiItem = new QTableWidgetItem(); + QTableWidgetItem *countryItem = new QTableWidgetItem(); QTableWidgetItem *typeItem = new QTableWidgetItem(); + QTableWidgetItem *idItem = new QTableWidgetItem(); QTableWidgetItem *dataItem = new QTableWidgetItem(); QTableWidgetItem *nmeaItem = new QTableWidgetItem(); QTableWidgetItem *hexItem = new QTableWidgetItem(); @@ -182,24 +450,51 @@ void AISDemodGUI::messageReceived(const QByteArray& message, const QDateTime& da ui->messages->setItem(row, MESSAGE_COL_DATE, dateItem); ui->messages->setItem(row, MESSAGE_COL_TIME, timeItem); ui->messages->setItem(row, MESSAGE_COL_MMSI, mmsiItem); + ui->messages->setItem(row, MESSAGE_COL_COUNTRY, countryItem); ui->messages->setItem(row, MESSAGE_COL_TYPE, typeItem); + ui->messages->setItem(row, MESSAGE_COL_ID, idItem); ui->messages->setItem(row, MESSAGE_COL_DATA, dataItem); ui->messages->setItem(row, MESSAGE_COL_NMEA, nmeaItem); ui->messages->setItem(row, MESSAGE_COL_HEX, hexItem); ui->messages->setItem(row, MESSAGE_COL_SLOT, slotItem); dateItem->setText(dateTime.date().toString()); timeItem->setText(dateTime.time().toString()); - mmsiItem->setText(QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0'))); + QString mmsi = QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0')); + mmsiItem->setText(mmsi); + QIcon *flag = MMSI::getFlagIcon(mmsi); + if (flag) + { + countryItem->setSizeHint(QSize(40, 20)); + countryItem->setIcon(*flag); + } typeItem->setText(ais->getType()); + idItem->setData(Qt::DisplayRole, ais->m_id); dataItem->setText(ais->toString()); nmeaItem->setText(ais->toNMEA()); hexItem->setText(ais->toHex()); slotItem->setData(Qt::DisplayRole, slot); - ui->messages->setSortingEnabled(true); - if (scrollToBottom) { - ui->messages->scrollToBottom(); + if (!m_loadingData) + { + filterRow(row); + ui->messages->setSortingEnabled(true); + if (scrollToBottom) { + ui->messages->scrollToBottom(); + } } - filterRow(row); + + updateCategory(mmsi, ais); + + // Update slot map + updateSlotMap(); + QColor color = getColor(mmsi); + m_painter.setPen(color); + for (int i = 0; i < totalSlots; i++) + { + int y = (slot + i) / m_slotMapWidth; + int x = (slot + i) % m_slotMapWidth; + m_painter.fillRect(x * 5 + 1, y * 5 + 1, 4, 4, color); + } + m_slotsUsed += totalSlots; delete ais; } @@ -221,7 +516,7 @@ bool AISDemodGUI::handleMessage(const Message& message) else if (AISDemod::MsgMessage::match(message)) { AISDemod::MsgMessage& report = (AISDemod::MsgMessage&) message; - messageReceived(report.getMessage(), report.getDateTime(), report.getSlot()); + messageReceived(report.getMessage(), report.getDateTime(), report.getSlot(), report.getSlots()); return true; } else if (DSPSignalNotification::match(message)) @@ -330,18 +625,6 @@ void AISDemodGUI::on_udpFormat_currentIndexChanged(int value) applySettings(); } -void AISDemodGUI::on_channel1_currentIndexChanged(int index) -{ - m_settings.m_scopeCh1 = index; - applySettings(); -} - -void AISDemodGUI::on_channel2_currentIndexChanged(int index) -{ - m_settings.m_scopeCh2 = index; - applySettings(); -} - void AISDemodGUI::on_messages_cellDoubleClicked(int row, int column) { // Get MMSI of message in row double clicked @@ -440,7 +723,9 @@ AISDemodGUI::AISDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseban m_deviceCenterFrequency(0), m_basebandSampleRate(1), m_doApplySettings(true), - m_tickCount(0) + m_tickCount(0), + m_loadingData(false), + m_slotsUsed(0) { setAttribute(Qt::WA_DeleteOnClose, true); m_helpURL = "plugins/channelrx/demodais/readme.md"; @@ -458,8 +743,10 @@ AISDemodGUI::AISDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseban m_scopeVis = m_aisDemod->getScopeSink(); m_scopeVis->setGLScope(ui->glScope); + m_scopeVis->setNbStreams(AISDemodSettings::m_scopeStreams); ui->glScope->connectTimer(MainCore::instance()->getMasterTimer()); ui->scopeGUI->setBuddies(m_scopeVis->getInputMessageQueue(), m_scopeVis, ui->glScope); + ui->scopeGUI->setStreams(QStringList({"IQ", "MagSq", "FM demod", "Gaussian", "RX buf", "Correlation", "Threshold met", "DC offset", "CRC"})); // Scope settings to display the IQ waveforms ui->scopeGUI->setPreTrigger(1); @@ -534,6 +821,16 @@ AISDemodGUI::AISDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseban ui->scopeContainer->setVisible(false); + // Create slot map image + m_image = QPixmap(m_slotMapWidth*5+1, m_slotMapHeight*5+1); + m_image.fill(Qt::transparent); + m_image.fill(Qt::black); + m_painter.begin(&m_image); + m_pen.setColor(Qt::white); + m_painter.setPen(m_pen); + ui->slotMap->setPixmap(m_image); + updateSlotMap(); + displaySettings(); makeUIConnections(); applySettings(true); @@ -612,12 +909,12 @@ void AISDemodGUI::displaySettings() ui->udpPort->setText(QString::number(m_settings.m_udpPort)); ui->udpFormat->setCurrentIndex((int)m_settings.m_udpFormat); - ui->channel1->setCurrentIndex(m_settings.m_scopeCh1); - ui->channel2->setCurrentIndex(m_settings.m_scopeCh2); - ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename)); ui->logEnable->setChecked(m_settings.m_logEnabled); + ui->showSlotMap->setChecked(m_settings.m_showSlotMap); + ui->slotMapWidget->setVisible(m_settings.m_showSlotMap); + // Order and size columns QHeaderView *header = ui->messages->horizontalHeader(); for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) @@ -662,13 +959,22 @@ void AISDemodGUI::tick() (100.0f + powDbPeak) / 100.0f, nbMagsqSamples); - if (m_tickCount % 4 == 0) { + if (m_tickCount % 4 == 0) + { ui->channelPower->setText(QString::number(powDbAvg, 'f', 1)); + updateSlotMap(); } m_tickCount++; } +void AISDemodGUI::on_showSlotMap_clicked(bool checked) +{ + ui->slotMapWidget->setVisible(checked); + m_settings.m_showSlotMap = checked; + applySettings(); +} + void AISDemodGUI::on_logEnable_clicked(bool checked) { m_settings.m_logEnabled = checked; @@ -704,6 +1010,8 @@ void AISDemodGUI::on_logOpen_clicked() QFile file(fileNames[0]); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QDateTime startTime = QDateTime::currentDateTime(); + m_loadingData = true; QTextStream in(&file); QString error; QHash colIndexes = CSV::readHeader(in, {"Date", "Time", "Data", "Slot"}, error); @@ -713,7 +1021,8 @@ void AISDemodGUI::on_logOpen_clicked() int timeCol = colIndexes.value("Time"); int dataCol = colIndexes.value("Data"); int slotCol = colIndexes.value("Slot"); - int maxCol = std::max({dateCol, timeCol, dataCol, slotCol}); + int slotsCol = colIndexes.contains("Slots") ? colIndexes.value("Slots") : -1; + int maxCol = std::max({dateCol, timeCol, dataCol, slotCol, slotsCol}); QMessageBox dialog(this); dialog.setText("Reading messages"); @@ -725,7 +1034,7 @@ void AISDemodGUI::on_logOpen_clicked() QStringList cols; QList aisPipes; - MainCore::instance()->getMessagePipes().getMessagePipes(this, "ais", aisPipes); + MainCore::instance()->getMessagePipes().getMessagePipes(m_aisDemod, "ais", aisPipes); while (!cancelled && CSV::readRow(in, &cols)) { @@ -736,9 +1045,10 @@ void AISDemodGUI::on_logOpen_clicked() QDateTime dateTime(date, time); QByteArray bytes = QByteArray::fromHex(cols[dataCol].toLatin1()); int slot = cols[slotCol].toInt(); + int totalSlots = slotsCol == -1 ? 1 : cols[slotsCol].toInt(); // Add to table - messageReceived(bytes, dateTime, slot); + messageReceived(bytes, dateTime, slot, totalSlots); // Forward to AIS feature for (const auto& pipe : aisPipes) @@ -764,6 +1074,10 @@ void AISDemodGUI::on_logOpen_clicked() { QMessageBox::critical(this, "AIS Demod", error); } + m_loadingData = false; + ui->messages->setSortingEnabled(true); + QDateTime finishTime = QDateTime::currentDateTime(); + qDebug() << "Read CSV in " << startTime.secsTo(finishTime); } else { @@ -789,8 +1103,7 @@ void AISDemodGUI::makeUIConnections() QObject::connect(ui->logEnable, &ButtonSwitch::clicked, this, &AISDemodGUI::on_logEnable_clicked); QObject::connect(ui->logFilename, &QToolButton::clicked, this, &AISDemodGUI::on_logFilename_clicked); QObject::connect(ui->logOpen, &QToolButton::clicked, this, &AISDemodGUI::on_logOpen_clicked); - QObject::connect(ui->channel1, QOverload::of(&QComboBox::currentIndexChanged), this, &AISDemodGUI::on_channel1_currentIndexChanged); - QObject::connect(ui->channel2, QOverload::of(&QComboBox::currentIndexChanged), this, &AISDemodGUI::on_channel2_currentIndexChanged); + QObject::connect(ui->showSlotMap, &ButtonSwitch::clicked, this, &AISDemodGUI::on_showSlotMap_clicked); } void AISDemodGUI::updateAbsoluteCenterFrequency() diff --git a/plugins/channelrx/demodais/aisdemodgui.h b/plugins/channelrx/demodais/aisdemodgui.h index 252fbcf98..61f18fa00 100644 --- a/plugins/channelrx/demodais/aisdemodgui.h +++ b/plugins/channelrx/demodais/aisdemodgui.h @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include "channel/channelgui.h" #include "dsp/channelmarker.h" @@ -88,20 +90,39 @@ private: AISDemod* m_aisDemod; uint32_t m_tickCount; MessageQueue m_inputMessageQueue; + bool m_loadingData; QMenu *messagesMenu; // Column select context menu QMenu *copyMenu; + QPixmap m_image; + QPainter m_painter; + QPen m_pen; + QDateTime m_lastSlotMapUpdate; + int m_slotsUsed; + static QMutex m_colorMutex; + static QHash m_usedInFrame; // Indicates if MMSI used in current frame + static QHash m_slotMapColors; // MMSI to color + static QHash m_category; // MMSI to category + static QList m_colors; + static QDateTime m_lastColorUpdate; + static const int m_slotMapWidth = 50; // 2250 slots per minute + static const int m_slotMapHeight = 45; + explicit AISDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); virtual ~AISDemodGUI(); void blockApplySettings(bool block); void applySettings(bool force = false); void displaySettings(); - void messageReceived(const QByteArray& message, const QDateTime& dateTime, int slot); + void messageReceived(const QByteArray& message, const QDateTime& dateTime, int slot, int slots); bool handleMessage(const Message& message); void makeUIConnections(); void updateAbsoluteCenterFrequency(); + void updateSlotMap(); + static void updateColors(); + static QColor getColor(const QString& mmsi); + static void updateCategory(const QString& mmsi, const AISMessage *message); void leaveEvent(QEvent*); void enterEvent(EnterEventType*); @@ -113,7 +134,9 @@ private: MESSAGE_COL_DATE, MESSAGE_COL_TIME, MESSAGE_COL_MMSI, + MESSAGE_COL_COUNTRY, MESSAGE_COL_TYPE, + MESSAGE_COL_ID, MESSAGE_COL_DATA, MESSAGE_COL_NMEA, MESSAGE_COL_HEX, @@ -131,12 +154,11 @@ private slots: void on_udpAddress_editingFinished(); void on_udpPort_editingFinished(); void on_udpFormat_currentIndexChanged(int value); - void on_channel1_currentIndexChanged(int index); - void on_channel2_currentIndexChanged(int index); void on_messages_cellDoubleClicked(int row, int column); void on_logEnable_clicked(bool checked=false); void on_logFilename_clicked(); void on_logOpen_clicked(); + void on_showSlotMap_clicked(bool checked=false); void filterRow(int row); void filter(); void messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); diff --git a/plugins/channelrx/demodais/aisdemodgui.ui b/plugins/channelrx/demodais/aisdemodgui.ui index b2eced5b8..0453b5d38 100644 --- a/plugins/channelrx/demodais/aisdemodgui.ui +++ b/plugins/channelrx/demodais/aisdemodgui.ui @@ -7,7 +7,7 @@ 0 0 388 - 446 + 985 @@ -557,6 +557,32 @@ + + + + + 24 + 16777215 + + + + Show/hide slot map + + + + + + + :/constellation.png:/constellation.png + + + true + + + true + + + @@ -632,10 +658,10 @@ - 0 - 210 - 391 - 171 + 10 + 150 + 361 + 351 @@ -647,79 +673,225 @@ Received Messages - - - 2 - - - 3 - - - 3 - - - 3 - - - 3 - + - - - Received packets + + + Qt::Vertical - - QAbstractItemView::NoEditTriggers - - - - Date - - - - - Time - - - - - MMSI - - - - - Type - - - - - Data + + + + 0 + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 64 + 35 + + + + Slot map + + + QFrame::Panel + + + QFrame::Sunken + + + + + + true + + + Qt::AlignCenter + + + + + + + + + Used + + + + + + + Number of used slots in previous frame + + + true + + + + + + + Qt::Vertical + + + + + + + Free + + + + + + + Number of free slots in previous frame + + + true + + + + + + + Qt::Vertical + + + + + + + Utilisation + + + + + + + Slot utilisation in % for previous frame + + + 24 + + + + + + + + - Packet data as ASCII + Received packets - - - - NMEA + + QAbstractItemView::NoEditTriggers - - - - Hex - - - Packet data as hex - - - - - Slot - - - Time slot - - + + + Date + + + Date message was received + + + + + Time + + + Time message was received + + + + + MMSI + + + Maritime Mobile Service Identity + + + + + Country + + + Country with jurisdiction over station/vessel + + + + + Type + + + Message type + + + + + Id + + + Message type identifier + + + + + Data + + + Decoded message data + + + + + NMEA + + + Message data in NMEA format + + + + + Hex + + + Message data as hex + + + + + Slot + + + Time slot + + + @@ -728,7 +900,7 @@ 20 - 400 + 510 716 341 @@ -758,150 +930,6 @@ 3 - - - - - - Real - - - - - - - - 0 - 0 - - - - - I - - - - - Q - - - - - Mag Sq - - - - - FM demod - - - - - Gaussian - - - - - RX buf - - - - - Correlation - - - - - Threshold met - - - - - DC offset - - - - - CRC - - - - - - - - - 0 - 0 - - - - Imag - - - - - - - - 0 - 0 - - - - - I - - - - - Q - - - - - Mag Sq - - - - - FM demod - - - - - Gaussian - - - - - RX buf - - - - - Correlation - - - - - Threshold met - - - - - DC offset - - - - - CRC - - - - - - diff --git a/plugins/channelrx/demodais/aisdemodsettings.cpp b/plugins/channelrx/demodais/aisdemodsettings.cpp index b719dfb0e..3757aa862 100644 --- a/plugins/channelrx/demodais/aisdemodsettings.cpp +++ b/plugins/channelrx/demodais/aisdemodsettings.cpp @@ -43,10 +43,9 @@ void AISDemodSettings::resetToDefaults() m_udpAddress = "127.0.0.1"; m_udpPort = 9999; m_udpFormat = Binary; - m_scopeCh1 = 5; - m_scopeCh2 = 6; m_logFilename = "ais_log.csv"; m_logEnabled = false; + m_showSlotMap = false; m_rgbColor = QColor(102, 0, 0).rgb(); m_title = "AIS Demodulator"; m_streamIndex = 0; @@ -78,8 +77,6 @@ QByteArray AISDemodSettings::serialize() const s.writeString(7, m_udpAddress); s.writeU32(8, m_udpPort); s.writeS32(9, (int)m_udpFormat); - s.writeS32(10, m_scopeCh1); - s.writeS32(11, m_scopeCh2); s.writeU32(12, m_rgbColor); s.writeString(13, m_title); @@ -105,6 +102,7 @@ QByteArray AISDemodSettings::serialize() const s.writeS32(26, m_workspaceIndex); s.writeBlob(27, m_geometryBytes); s.writeBool(28, m_hidden); + s.writeBool(29, m_showSlotMap); for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) s.writeS32(100 + i, m_messageColumnIndexes[i]); @@ -146,8 +144,6 @@ bool AISDemodSettings::deserialize(const QByteArray& data) } d.readS32(9, (int *)&m_udpFormat, (int)Binary); - d.readS32(10, &m_scopeCh1, 0); - d.readS32(11, &m_scopeCh2, 0); d.readU32(12, &m_rgbColor, QColor(102, 0, 0).rgb()); d.readString(13, &m_title, "AIS Demodulator"); @@ -192,6 +188,7 @@ bool AISDemodSettings::deserialize(const QByteArray& data) d.readS32(26, &m_workspaceIndex, 0); d.readBlob(27, &m_geometryBytes); d.readBool(28, &m_hidden, false); + d.readBool(29, &m_showSlotMap, false); for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) { d.readS32(100 + i, &m_messageColumnIndexes[i], i); diff --git a/plugins/channelrx/demodais/aisdemodsettings.h b/plugins/channelrx/demodais/aisdemodsettings.h index 9e0ca9b86..eba2418b0 100644 --- a/plugins/channelrx/demodais/aisdemodsettings.h +++ b/plugins/channelrx/demodais/aisdemodsettings.h @@ -27,7 +27,7 @@ class Serializable; // Number of columns in the tables -#define AISDEMOD_MESSAGE_COLUMNS 8 +#define AISDEMOD_MESSAGE_COLUMNS 10 struct AISDemodSettings { @@ -44,11 +44,10 @@ struct AISDemodSettings Binary, NMEA } m_udpFormat; - int m_scopeCh1; - int m_scopeCh2; QString m_logFilename; bool m_logEnabled; + bool m_showSlotMap; quint32 m_rgbColor; QString m_title; @@ -69,6 +68,7 @@ struct AISDemodSettings int m_messageColumnSizes[AISDEMOD_MESSAGE_COLUMNS]; //!< Size of the columns in the table static const int AISDEMOD_CHANNEL_SAMPLE_RATE = 57600; //!< 6x 9600 baud rate (use even multiple so Gausian filter has odd number of taps) + static const int m_scopeStreams = 9; AISDemodSettings(); void resetToDefaults(); diff --git a/plugins/channelrx/demodais/aisdemodsink.cpp b/plugins/channelrx/demodais/aisdemodsink.cpp index bd567290e..ca1c74323 100644 --- a/plugins/channelrx/demodais/aisdemodsink.cpp +++ b/plugins/channelrx/demodais/aisdemodsink.cpp @@ -47,7 +47,9 @@ AISDemodSink::AISDemodSink(AISDemod *aisDemod) : m_demodBuffer.resize(1<<12); m_demodBufferFill = 0; - m_sampleBuffer.resize(m_sampleBufferSize); + for (int i = 0; i < AISDemodSettings::m_scopeStreams; i++) { + m_sampleBuffer[i].resize(m_sampleBufferSize); + } applySettings(m_settings, true); applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); @@ -59,18 +61,28 @@ AISDemodSink::~AISDemodSink() delete[] m_train; } -void AISDemodSink::sampleToScope(Complex sample) +void AISDemodSink::sampleToScope(Complex sample, Real magsq, Real fmDemod, Real filt, Real rxBuf, Real corr, Real thresholdMet, Real dcOffset, Real crcValid) { if (m_scopeSink) { - Real r = std::real(sample) * SDR_RX_SCALEF; - Real i = std::imag(sample) * SDR_RX_SCALEF; - m_sampleBuffer[m_sampleBufferIndex++] = Sample(r, i); - + m_sampleBuffer[0][m_sampleBufferIndex] = sample; + m_sampleBuffer[1][m_sampleBufferIndex] = Complex(m_magsq, 0.0f); + m_sampleBuffer[2][m_sampleBufferIndex] = Complex(fmDemod, 0.0f); + m_sampleBuffer[3][m_sampleBufferIndex] = Complex(filt, 0.0f); + m_sampleBuffer[4][m_sampleBufferIndex] = Complex(rxBuf, 0.0f); + m_sampleBuffer[5][m_sampleBufferIndex] = Complex(corr, 0.0f); + m_sampleBuffer[6][m_sampleBufferIndex] = Complex(thresholdMet, 0.0f); + m_sampleBuffer[7][m_sampleBufferIndex] = Complex(dcOffset, 0.0f); + m_sampleBuffer[8][m_sampleBufferIndex] = Complex(crcValid, 0.0f); + m_sampleBufferIndex++; if (m_sampleBufferIndex == m_sampleBufferSize) { - std::vector vbegin; - vbegin.push_back(m_sampleBuffer.begin()); + std::vector vbegin; + + for (int i = 0; i < AISDemodSettings::m_scopeStreams; i++) { + vbegin.push_back(m_sampleBuffer[i].begin()); + } + m_scopeSink->feed(vbegin, m_sampleBufferSize); m_sampleBufferIndex = 0; } @@ -252,10 +264,13 @@ void AISDemodSink::processOneSample(Complex &ci) // This is unlikely to be accurate in absolute terms, given we don't know latency from SDR or buffering within SDRangel // But can be used to get an idea of congestion QDateTime currentTime = QDateTime::currentDateTime(); - QDateTime startDateTime = currentTime.addMSecs(-(totalBitCount + 8 + 24 + 8) * (1000.0 / m_settings.m_baud)); // Add ramp up, preamble and start-flag + int txTimeMs = (totalBitCount + 8 + 24 + 8) * (1000.0 / m_settings.m_baud); // Add ramp up, preamble and start-flag + QDateTime startDateTime = currentTime.addMSecs(-txTimeMs); int ms = startDateTime.time().second() * 1000 + startDateTime.time().msec(); - int slot = ms / 26.67; // 2250 slots per minute, 26ms per slot - AISDemod::MsgMessage *msg = AISDemod::MsgMessage::create(rxPacket, currentTime, slot); + float slotTime = 60.0f * 1000.0f / 2250.0f; // 2250 slots per minute, 26.6ms per slot + int slot = ms / slotTime; + int totalSlots = std::ceil(txTimeMs / slotTime); + AISDemod::MsgMessage *msg = AISDemod::MsgMessage::create(rxPacket, currentTime, slot, totalSlots); getMessageQueueToChannel()->push(msg); } @@ -318,74 +333,7 @@ void AISDemodSink::processOneSample(Complex &ci) } // Select signals to feed to scope - Complex scopeSample; - switch (m_settings.m_scopeCh1) - { - case 0: - scopeSample.real(ci.real() / SDR_RX_SCALEF); - break; - case 1: - scopeSample.real(ci.imag() / SDR_RX_SCALEF); - break; - case 2: - scopeSample.real(magsq); - break; - case 3: - scopeSample.real(fmDemod); - break; - case 4: - scopeSample.real(filt); - break; - case 5: - scopeSample.real(m_rxBuf[m_rxBufIdx]); - break; - case 6: - scopeSample.real(corr / 100.0); - break; - case 7: - scopeSample.real(thresholdMet); - break; - case 8: - scopeSample.real(dcOffset); - break; - case 9: - scopeSample.real(scopeCRCValid ? 1.0 : (scopeCRCInvalid ? -1.0 : 0)); - break; - } - switch (m_settings.m_scopeCh2) - { - case 0: - scopeSample.imag(ci.real() / SDR_RX_SCALEF); - break; - case 1: - scopeSample.imag(ci.imag() / SDR_RX_SCALEF); - break; - case 2: - scopeSample.imag(magsq); - break; - case 3: - scopeSample.imag(fmDemod); - break; - case 4: - scopeSample.imag(filt); - break; - case 5: - scopeSample.imag(m_rxBuf[m_rxBufIdx]); - break; - case 6: - scopeSample.imag(corr / 100.0); - break; - case 7: - scopeSample.imag(thresholdMet); - break; - case 8: - scopeSample.imag(dcOffset); - break; - case 9: - scopeSample.imag(scopeCRCValid ? 1.0 : (scopeCRCInvalid ? -1.0 : 0)); - break; - } - sampleToScope(scopeSample); + sampleToScope(ci / SDR_RX_SCALEF, magsq, fmDemod, filt, m_rxBuf[m_rxBufIdx], corr / 100.0, thresholdMet, dcOffset, scopeCRCValid ? 1.0 : (scopeCRCInvalid ? -1.0 : 0)); // Send demod signal to Demod Analzyer feature m_demodBuffer[m_demodBufferFill++] = fmDemod * std::numeric_limits::max(); diff --git a/plugins/channelrx/demodais/aisdemodsink.h b/plugins/channelrx/demodais/aisdemodsink.h index 2a93c9570..b6bbbe41b 100644 --- a/plugins/channelrx/demodais/aisdemodsink.h +++ b/plugins/channelrx/demodais/aisdemodsink.h @@ -128,13 +128,13 @@ private: QVector m_demodBuffer; int m_demodBufferFill; - SampleVector m_sampleBuffer; + ComplexVector m_sampleBuffer[AISDemodSettings::m_scopeStreams]; static const int m_sampleBufferSize = AISDemodSettings::AISDEMOD_CHANNEL_SAMPLE_RATE / 20; int m_sampleBufferIndex; void processOneSample(Complex &ci); MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; } - void sampleToScope(Complex sample); + void sampleToScope(Complex sample, Real magsq, Real fmDemod, Real filt, Real rxBuf, Real corr, Real thresholdMet, Real dcOffset, Real crcValid); }; #endif // INCLUDE_AISDEMODSINK_H diff --git a/plugins/channelrx/demodais/readme.md b/plugins/channelrx/demodais/readme.md index 97d47e1ca..621e14811 100644 --- a/plugins/channelrx/demodais/readme.md +++ b/plugins/channelrx/demodais/readme.md @@ -82,19 +82,40 @@ Click to specify the name of the .csv file which received AIS messages are logge Click to specify a previously written AIS .csv log file, which is read and used to update the table. +

Slot Map

+ +AIS uses TMDA (Time Division Multiple Access), whereby each one minute frame is divided into 2,250 26.6ms slots. +The slot map shows which slots within a frame are used. The slot map is drawn as bitmap of 50x45 pixels. + +![AIS Slot Map](../../../doc/img/AISDemod_plugin_slotmap.png) + +Slots are by category: + +* Red: Class A Mobile +* Blue: Class B Mobile +* Green: Base Station +* Yellow: AtoN (Aid-to-Navigation) +* Cyan: Search and Rescue +* Magenta: Other (Man overboard / EPIRB / AMRD). + +Due to SDR to SDRangel latency being unknown, the slot map is likely to have some offset, as slot timing is calculated based on the time messages +are demodulated in SDRangel. +

Received Messages Table

The received messages table displays information about each AIS message received. Only messages with valid CRCs are displayed. -![AIS Demodulator plugin GUI](../../../doc/img/AISDemod_plugin_messages.png) +![AIS Received Messages Table](../../../doc/img/AISDemod_plugin_messages.png) * Date - The date the message was received. * Time - The time the message was received. * MMSI - The Maritime Mobile Service Identity number of the source of the message. Double clicking on this column will search for the MMSI on https://www.vesselfinder.com/ +* Country - The country with jurisdiction over station/vessel. * Type - The type of AIS message. E.g. Position report, Base station report or Ship static and voyage related data. +* Id - Message type numeric identifier. * Data - A textual decode of the message displaying the most interesting fields. * NMEA - The message in NMEA format. * Hex - The message in hex format. -* Slot - Time slot (0-2249). Due to SDR to SDRangel latency being unknown, this is likely to have some offset. +* Slot - Time slot (0-2249). Right clicking on the table header allows you to select which columns to show. The columns can be reordered by left clicking and dragging the column header. Right clicking on an item in the table allows you to copy the value to the clipboard. diff --git a/plugins/feature/ais/aisgui.cpp b/plugins/feature/ais/aisgui.cpp index 641a27150..481d021df 100644 --- a/plugins/feature/ais/aisgui.cpp +++ b/plugins/feature/ais/aisgui.cpp @@ -31,6 +31,7 @@ #include "gui/dialogpositioner.h" #include "mainwindow.h" #include "device/deviceuiset.h" +#include "util/mmsi.h" #include "ui_aisgui.h" #include "ais.h" @@ -377,6 +378,7 @@ void AISGUI::resizeTable() int row = ui->vessels->rowCount(); ui->vessels->setRowCount(row + 1); ui->vessels->setItem(row, VESSEL_COL_MMSI, new QTableWidgetItem("123456789")); + ui->vessels->setItem(row, VESSEL_COL_COUNTRY, new QTableWidgetItem("flag")); ui->vessels->setItem(row, VESSEL_COL_TYPE, new QTableWidgetItem("Base station")); ui->vessels->setItem(row, VESSEL_COL_LATITUDE, new QTableWidgetItem("90.000000-")); ui->vessels->setItem(row, VESSEL_COL_LONGITUDE, new QTableWidgetItem("180.00000-")); @@ -503,6 +505,7 @@ void AISGUI::sendToMap(const QString &name, const QString &label, void AISGUI::updateVessels(AISMessage *ais, QDateTime dateTime) { QTableWidgetItem *mmsiItem; + QTableWidgetItem *countryItem; QTableWidgetItem *typeItem; QTableWidgetItem *latitudeItem; QTableWidgetItem *longitudeItem; @@ -534,6 +537,7 @@ void AISGUI::updateVessels(AISMessage *ais, QDateTime dateTime) { // Update existing item mmsiItem = ui->vessels->item(row, VESSEL_COL_MMSI); + countryItem = ui->vessels->item(row, VESSEL_COL_COUNTRY); typeItem = ui->vessels->item(row, VESSEL_COL_TYPE); latitudeItem = ui->vessels->item(row, VESSEL_COL_LATITUDE); longitudeItem = ui->vessels->item(row, VESSEL_COL_LONGITUDE); @@ -563,6 +567,7 @@ void AISGUI::updateVessels(AISMessage *ais, QDateTime dateTime) ui->vessels->setRowCount(row + 1); mmsiItem = new QTableWidgetItem(); + countryItem = new QTableWidgetItem(); typeItem = new QTableWidgetItem(); latitudeItem = new QTableWidgetItem(); longitudeItem = new QTableWidgetItem(); @@ -580,6 +585,7 @@ void AISGUI::updateVessels(AISMessage *ais, QDateTime dateTime) lastUpdateItem = new QTableWidgetItem(); messagesItem = new QTableWidgetItem(); ui->vessels->setItem(row, VESSEL_COL_MMSI, mmsiItem); + ui->vessels->setItem(row, VESSEL_COL_COUNTRY, countryItem); ui->vessels->setItem(row, VESSEL_COL_TYPE, typeItem); ui->vessels->setItem(row, VESSEL_COL_LATITUDE, latitudeItem); ui->vessels->setItem(row, VESSEL_COL_LONGITUDE, longitudeItem); @@ -605,7 +611,15 @@ void AISGUI::updateVessels(AISMessage *ais, QDateTime dateTime) previousType = typeItem->text(); previousShipType = shipTypeItem->text(); - mmsiItem->setText(QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0'))); + QString mmsi = QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0')); + mmsiItem->setText(mmsi); + QIcon *flag = MMSI::getFlagIcon(mmsi); + if (flag) + { + countryItem->setSizeHint(QSize(40, 20)); + countryItem->setIcon(*flag); + } + lastUpdateItem->setData(Qt::DisplayRole, dateTime); messagesItem->setData(Qt::DisplayRole, messagesItem->data(Qt::DisplayRole).toInt() + 1); @@ -991,24 +1005,24 @@ void AISGUI::vessels_customContextMenuRequested(QPoint pos) QAction* mmsiAction = new QAction(QString("View MMSI %1 on vesselfinder.com...").arg(mmsi), tableContextMenu); connect(mmsiAction, &QAction::triggered, this, [mmsi]()->void { - QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.net/vessels?name=%1").arg(mmsi))); + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(mmsi))); }); tableContextMenu->addAction(mmsiAction); if (!imo.isEmpty()) { - QAction* imoAction = new QAction(QString("View IMO %1 on vesselfinder.net...").arg(imo), tableContextMenu); + QAction* imoAction = new QAction(QString("View IMO %1 on vesselfinder.com...").arg(imo), tableContextMenu); connect(imoAction, &QAction::triggered, this, [imo]()->void { - QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.net/vessels?name=%1").arg(imo))); + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(imo))); }); tableContextMenu->addAction(imoAction); } if (!name.isEmpty()) { - QAction* nameAction = new QAction(QString("View %1 on vesselfinder.net...").arg(name), tableContextMenu); + QAction* nameAction = new QAction(QString("View %1 on vesselfinder.com...").arg(name), tableContextMenu); connect(nameAction, &QAction::triggered, this, [name]()->void { - QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.net/vessels?name=%1").arg(name))); + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(name))); }); tableContextMenu->addAction(nameAction); } diff --git a/plugins/feature/ais/aisgui.h b/plugins/feature/ais/aisgui.h index 893a59d7a..f09fe8ba7 100644 --- a/plugins/feature/ais/aisgui.h +++ b/plugins/feature/ais/aisgui.h @@ -106,6 +106,7 @@ private: enum VesselCol { VESSEL_COL_MMSI, + VESSEL_COL_COUNTRY, VESSEL_COL_TYPE, VESSEL_COL_LATITUDE, VESSEL_COL_LONGITUDE, diff --git a/plugins/feature/ais/aisgui.ui b/plugins/feature/ais/aisgui.ui index bfeaf9342..68918a104 100644 --- a/plugins/feature/ais/aisgui.ui +++ b/plugins/feature/ais/aisgui.ui @@ -89,10 +89,21 @@ Maritime Mobile Service Identity
+ + + Country + + + Country with jurisdiction over station/vessel + + Type + + Message type + diff --git a/plugins/feature/ais/aissettings.h b/plugins/feature/ais/aissettings.h index f1ff92e74..d2ac77812 100644 --- a/plugins/feature/ais/aissettings.h +++ b/plugins/feature/ais/aissettings.h @@ -27,7 +27,7 @@ class Serializable; // Number of columns in the tables -#define AIS_VESSEL_COLUMNS 16 +#define AIS_VESSEL_COLUMNS 18 struct AISSettings { diff --git a/sdrbase/util/ais.cpp b/sdrbase/util/ais.cpp index 5ce502314..ddc0b5974 100644 --- a/sdrbase/util/ais.cpp +++ b/sdrbase/util/ais.cpp @@ -336,12 +336,24 @@ QString AISPositionReport::getStatusString(int status) return statuses[status]; } +QString AISPositionReport::getType() +{ + if (m_id == 1) { + return "Position report (Scheduled)"; + } else if (m_id == 2) { + return "Position report (Assigned)"; + } else { + return "Position report (Interrogated)"; + } +} + QString AISPositionReport::toString() { + QString speed = m_speedOverGround == 1022 ? ">102.2" : QString::number(m_speedOverGround); return QString("Lat: %1%6 Lon: %2%6 Speed: %3 knts Course: %4%6 Status: %5") .arg(m_latitude) .arg(m_longitude) - .arg(m_speedOverGround) + .arg(speed) .arg(m_course) .arg(AISPositionReport::getStatusString(m_status)) .arg(QChar(0xb0)); @@ -444,7 +456,7 @@ AISSARAircraftPositionReport::AISSARAircraftPositionReport(QByteArray ba) : int sog = ((ba[6] & 0x3f) << 4) | ((ba[7] >> 4) & 0xf); m_speedOverGroundAvailable = sog != 1023; - m_speedOverGround = sog * 0.1f; + m_speedOverGround = sog; m_positionAccuracy = (ba[7] >> 3) & 0x1; @@ -467,12 +479,14 @@ AISSARAircraftPositionReport::AISSARAircraftPositionReport(QByteArray ba) : QString AISSARAircraftPositionReport::toString() { + QString altitude = m_altitude == 4094 ? ">4094" : QString::number(m_altitude); + QString speed = m_speedOverGround == 1022 ? ">1022" : QString::number(m_speedOverGround); return QString("Lat: %1%6 Lon: %2%6 Speed: %3 knts Course: %4%6 Alt: %5 m") .arg(m_latitude) .arg(m_longitude) - .arg(m_speedOverGround) + .arg(speed) .arg(m_course) - .arg(m_altitude) + .arg(altitude) .arg(QChar(0xb0)); } @@ -520,6 +534,18 @@ AISInterrogation::AISInterrogation(QByteArray ba) : AISAssignedModeCommand::AISAssignedModeCommand(QByteArray ba) : AISMessage(ba) { + m_destinationIdA = ((ba[5] & 0xff) << 22) | ((ba[6] & 0xff) << 14) | ((ba[7] & 0xff) << 6) | ((ba[8] >> 2) & 0x3f); + m_offsetA = ((ba[8] & 0x3) << 10) | ((ba[9] & 0xff) << 2) | ((ba[10] >> 6) & 0x3); + m_incrementA = ((ba[10] & 0x3f) << 4) | ((ba[11] >> 4) & 0xf); + m_bAvailable = false; +} + +QString AISAssignedModeCommand::toString() +{ + return QString("Dest A: %1 Offset A: %2 Inc A: %3") + .arg(m_destinationIdA) + .arg(m_offsetA) + .arg(m_incrementA); } AISGNSSBroadcast::AISGNSSBroadcast(QByteArray ba) : @@ -721,6 +747,25 @@ QString AISStaticDataReport::toString() AISSingleSlotBinaryMessage::AISSingleSlotBinaryMessage(QByteArray ba) : AISMessage(ba) { + m_destinationIndicator = (ba[4] >> 1) & 1; + m_binaryDataFlag = ba[4] & 1; + if (m_destinationIndicator) { + m_destinationId = ((ba[5] & 0xff) << 22) | ((ba[6] & 0xff) << 14) | ((ba[7] & 0xff) << 6) | ((ba[8] >> 2) & 0x3f); + } + m_destinationIdAvailable = m_destinationIndicator; +} + +QString AISSingleSlotBinaryMessage::toString() +{ + QStringList s; + + s.append(QString("Destination: %1").arg(m_destinationIndicator ? "Broadcast" : "Addressed")); + s.append(QString("Flag: %1").arg(m_binaryDataFlag ? "Unstructured" : "Structured")); + if (m_destinationIdAvailable) { + s.append(QString("Destination Id: %1").arg(m_destinationId)); + } + + return s.join(" "); } AISMultipleSlotBinaryMessage::AISMultipleSlotBinaryMessage(QByteArray ba) : diff --git a/sdrbase/util/ais.h b/sdrbase/util/ais.h index 3de22e8ac..a226b04b7 100644 --- a/sdrbase/util/ais.h +++ b/sdrbase/util/ais.h @@ -79,7 +79,7 @@ public: int m_specialManoeuvre; AISPositionReport(const QByteArray ba); - virtual QString getType() override { return "Position report"; } + virtual QString getType() override; virtual bool hasPosition() { return m_latitudeAvailable && m_longitudeAvailable; } virtual float getLatitude() { return m_latitude; } virtual float getLongitude() { return m_longitude; } @@ -229,8 +229,16 @@ public: class SDRBASE_API AISAssignedModeCommand : public AISMessage { public: + int m_destinationIdA; + int m_offsetA; + int m_incrementA; + int m_destinationIdB; + int m_offsetB; + int m_incrementB; + bool m_bAvailable; AISAssignedModeCommand(const QByteArray ba); virtual QString getType() override { return "Assigned mode command"; } + virtual QString toString() override; }; class SDRBASE_API AISGNSSBroadcast : public AISMessage { @@ -345,8 +353,14 @@ public: class SDRBASE_API AISSingleSlotBinaryMessage : public AISMessage { public: + bool m_destinationIndicator; + bool m_binaryDataFlag; + int m_destinationId; + bool m_destinationIdAvailable; + AISSingleSlotBinaryMessage(const QByteArray ba); virtual QString getType() override { return "Single slot binary message"; } + virtual QString toString() override; }; class SDRBASE_API AISMultipleSlotBinaryMessage : public AISMessage {