From 5cbab700351dcc52628ce4d557c949617acf9508 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 7 Oct 2026 19:01:13 +0200 Subject: [PATCH] Track radio speakers independently of member list refreshes --- tools/chatgui/src/memberlistmodel.cpp | 58 +++++++++++++++++++++++---- tools/chatgui/src/memberlistmodel.h | 9 +++++ 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/tools/chatgui/src/memberlistmodel.cpp b/tools/chatgui/src/memberlistmodel.cpp index 2e4967e2..1dd89a70 100644 --- a/tools/chatgui/src/memberlistmodel.cpp +++ b/tools/chatgui/src/memberlistmodel.cpp @@ -1,5 +1,6 @@ #include #include "memberlistmodel.h" +#include "debug_ui.h" #include "avatars/avatarmanager.h" #include "../transport/gui_bridge.h" #include @@ -76,6 +77,7 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const { if (role == Qt::UserRole + 2) return it->isSelf; if (role == Qt::UserRole + 3) return it->flags; if (role == Qt::UserRole + 4) return (bool)it->storage; + if (role == RadioSpeakingRole) return it->isSelf ? m_selfTransmitting : m_radioTalkers.contains(it->nodeId); return {}; } return {}; @@ -83,6 +85,11 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const { void MemberListModel::setChannel(const QString& channelId, bool isOwner) { beginResetModel(); + if (m_channelId != channelId) { + DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio members: channel %s -> %s cleared talkers=%d self_tx=%d", + qPrintable(m_channelId), qPrintable(channelId), int(m_radioTalkers.size()), m_selfTransmitting); + m_radioTalkers.clear(); m_radioActive = false; m_selfTransmitting = false; + } m_channelId = channelId; m_isOwner = isOwner; m_totalCount = 0; @@ -188,12 +195,7 @@ void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data) if ((item.flags & CHAT_MEMBER_FLAG_DELETED) && !m_isOwner) { /* deleted member — remove from non-owner's list */ - if (row >= 0) { - beginRemoveRows(QModelIndex(), row, row); - m_cache.remove(row); - m_totalCount--; - endRemoveRows(); - } + onMemberRemoved(chId, item.nodeId); return; } @@ -202,24 +204,64 @@ void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data) emit dataChanged(index(row), index(row)); } else { /* new member — add at end */ - row = m_totalCount++; - m_cache[row] = item; + row = m_totalCount; beginInsertRows(QModelIndex(), row, row); + m_cache[row] = item; + m_totalCount++; endInsertRows(); } } void MemberListModel::onMemberRemoved(const QString& chId, uint64_t nodeId) { if (chId != m_channelId) return; + if (m_radioTalkers.remove(nodeId)) + DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio members: removed talker ch=%s node=%016llx", + qPrintable(chId), (unsigned long long)nodeId); for (auto it = m_cache.begin(); it != m_cache.end(); ++it) { if (it->nodeId == nodeId) { int row = it.key(); beginRemoveRows(QModelIndex(), row, row); m_cache.remove(row); + for (int next = row + 1; next < m_totalCount; ++next) m_cache[next - 1] = m_cache.take(next); m_totalCount--; endRemoveRows(); return; } } } + +/* Состояние собственного TX берём из аудио-движка: оно включает PTT и VAD. */ +void MemberListModel::setRadioState(quint64 groupId, bool active, bool transmitting) { + active = active && groupId != 0 && groupId == m_channelId.toULongLong(); + transmitting = active && transmitting; + if (m_radioActive == active && m_selfTransmitting == transmitting) return; + DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio members: state ch=%s group=%016llx active=%d self_tx=%d talkers=%d", + qPrintable(m_channelId), (unsigned long long)groupId, active, transmitting, int(m_radioTalkers.size())); + m_radioActive = active; m_selfTransmitting = transmitting; + if (!active) m_radioTalkers.clear(); + if (m_totalCount) emit dataChanged(index(0), index(m_totalCount - 1), {RadioSpeakingRole}); +} + +/* START может прийти до списка участников; STOP снимает только указанный источник. */ +void MemberListModel::onRadioTalk(quint64 groupId, quint64 nodeId, bool on) { + if (!groupId || !nodeId) { + DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio members: invalid talk group=%016llx node=%016llx on=%d", + (unsigned long long)groupId, (unsigned long long)nodeId, on); + return; + } + if (groupId != m_channelId.toULongLong() || (on && !m_radioActive)) { + DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio members: ignored talk ch=%s group=%016llx node=%016llx on=%d active=%d", + qPrintable(m_channelId), (unsigned long long)groupId, (unsigned long long)nodeId, on, m_radioActive); + return; + } + if (m_radioTalkers.contains(nodeId) == on) return; + if (on) m_radioTalkers.insert(nodeId); + else m_radioTalkers.remove(nodeId); + DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio members: talk ch=%s node=%016llx on=%d talkers=%d", + qPrintable(m_channelId), (unsigned long long)nodeId, on, int(m_radioTalkers.size())); + for (auto it = m_cache.cbegin(); it != m_cache.cend(); ++it) if (it->nodeId == nodeId) { + emit dataChanged(index(it.key()), index(it.key()), {RadioSpeakingRole}); + return; + } +} diff --git a/tools/chatgui/src/memberlistmodel.h b/tools/chatgui/src/memberlistmodel.h index 5aad3b03..a518da54 100644 --- a/tools/chatgui/src/memberlistmodel.h +++ b/tools/chatgui/src/memberlistmodel.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,7 @@ public: /* Высота строки 36 px, между аватарками остаётся 2 px. */ static constexpr int kRowHeight = 36; static constexpr int kAvatarSize = kRowHeight - 2; + static constexpr int RadioSpeakingRole = Qt::UserRole + 5; explicit MemberListModel(QObject* parent = nullptr); @@ -47,6 +49,10 @@ public: void onMemberUpdated(const QString& chId, const uint8_t* data); void onMemberRemoved(const QString& chId, uint64_t nodeId); + /* Эфемерное состояние рации; вызывается в GUI-потоке, независимо от обновлений строк. */ + void setRadioState(quint64 groupId, bool active, bool transmitting); + void onRadioTalk(quint64 groupId, quint64 nodeId, bool on); + private: void loadBatch(int from, int count); QPixmap makeAvatar(QColor color, QChar letter) const; @@ -55,6 +61,9 @@ private: bool m_isOwner = false; int m_totalCount = 0; mutable QMap m_cache; + QSet m_radioTalkers; + bool m_radioActive = false; + bool m_selfTransmitting = false; QTimer* m_rttTimer; };