| 153 | } |
| 154 | |
| 155 | void DaxIqModel::applyStreamStatus(quint32 streamId, const QMap<QString, QString>& kvs) |
| 156 | { |
| 157 | // Find which channel this stream belongs to |
| 158 | int ch = -1; |
| 159 | if (kvs.contains("daxiq_channel")) |
| 160 | ch = kvs["daxiq_channel"].toInt(); |
| 161 | |
| 162 | // Try to find existing stream by ID |
| 163 | int idx = -1; |
| 164 | for (int i = 0; i < NUM_CHANNELS; ++i) { |
| 165 | if (m_streams[i].streamId == streamId && m_streams[i].exists) { |
| 166 | idx = i; |
| 167 | break; |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | // New stream — assign by channel |
| 172 | bool isNew = false; |
| 173 | if (idx < 0 && ch >= 1 && ch <= NUM_CHANNELS) { |
| 174 | idx = channelIndex(ch); |
| 175 | m_streams[idx].streamId = streamId; |
| 176 | m_streams[idx].exists = true; |
| 177 | isNew = true; |
| 178 | qCDebug(lcProtocol) << "DaxIqModel: new IQ stream ch" << ch |
| 179 | << "id" << Qt::hex << streamId; |
| 180 | } |
| 181 | |
| 182 | if (idx < 0) return; |
| 183 | |
| 184 | auto& s = m_streams[idx]; |
| 185 | |
| 186 | // Create the IQ pipe when the stream first appears (isNew), not only on a |
| 187 | // rate CHANGE. The default daxiq_rate=48000 equals IqStream::sampleRate{48000}, |
| 188 | // so a fresh enable at the default rate produced no rate delta and the pipe |
| 189 | // was never built (no module-pipe-source, no /tmp/aethersdr-iq-N.pipe, no |
| 190 | // node for WS2 to name). Guard merged: fire on existence OR rate change, and |
| 191 | // keep it OUTSIDE the daxiq_rate-present check so a status message that |
| 192 | // establishes the stream without carrying daxiq_rate still triggers it. |
| 193 | // destroyPipe is idempotent (m_pipeFds[idx]>=0 guard), so destroy-then-create |
| 194 | // is safe even with no prior pipe. |
| 195 | { |
| 196 | int newRate = kvs.contains("daxiq_rate") ? kvs["daxiq_rate"].toInt() |
| 197 | : s.sampleRate; |
| 198 | if (isNew || newRate != s.sampleRate) { |
| 199 | s.sampleRate = newRate; |
| 200 | QMetaObject::invokeMethod(m_worker, [this, ch = s.channel, newRate] { |
| 201 | m_worker->destroyPipe(ch); |
| 202 | m_worker->createPipe(ch, newRate); |
| 203 | }); |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | // If the user selected a non-default rate before enabling, the radio creates |
| 208 | // the stream at its 48k default; re-apply the desired rate now that it exists |
| 209 | // (proven path: "stream set ... daxiq_rate="). The rate-change status it |
| 210 | // produces flows back through the guard above and rebuilds the pipe at rate. |
| 211 | const bool reapplyPending = (isNew && m_desiredRate[idx] != s.sampleRate); |
| 212 | if (reapplyPending) { |
no test coverage detected