Files
Jon BenistonandClaude Opus 5 9c53811f94 MCP: spectrum history tools, window listing and hiding, inline audio that fits
get_spectrum_history reports what has been on a band over the last
while from the spectrum's scroll history, per bin and as a list of
signals with their duty cycles; get_waterfall_image returns the same
history as a picture, the first tool to put image content beside the
text in a result. Both switch scrolling on when it is off and say so.

list_windows shows every window with its workspace and whether it is
hidden, and set_workspace takes hidden alongside the workspace index.

capture_audio's inline copy is now mono and decimated to 16 kHz, so a
ten second capture fits under the 1 MB result limit; the file on disk
is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:21:16 +01:00

186 lines
8.4 KiB
C++

///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Jon Beniston, M7RCE <jon@beniston.com> //
// Some code by AI //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_MCPCAPTURE_H_
#define INCLUDE_FEATURE_MCPCAPTURE_H_
#include <QDateTime>
#include <QJsonObject>
#include <QMap>
#include <QAtomicInt>
#include <QMutex>
#include <QObject>
#include <QString>
#include <cstdint>
class WebAPIAdapterInterface;
class DataFifo;
class MessageQueue;
class ObjectPipe;
// Captures demodulated audio from a demodulator through its "demod" data pipe, the same
// pipe the Demod Analyzer feature uses. Only some channel types provide it.
//
// The pipes are registered and removed on the main thread; the samples are read from the
// calling (HTTP) thread, which is safe because the FIFO is mutex protected.
class MCPAudioCapture : public QObject
{
Q_OBJECT
public:
explicit MCPAudioCapture(WebAPIAdapterInterface *adapter, QObject *parent = nullptr);
//!< Blocks for the requested duration. Called from an HTTP thread.
QJsonObject capture(int deviceSetIndex, int channelIndex, double seconds, const QString& path, bool inlineAudio);
//!< The server is stopping: a capture in progress ends now, and one waiting for the main
//!< thread to attach stops waiting, since that thread is waiting for this one
void setStopping(bool stopping) { m_stopping.storeRelease(stopping ? 1 : 0); }
private slots:
void handlePipeToBeDeleted(int reason, QObject *object);
private:
struct Attachment
{
DataFifo *m_fifo;
MessageQueue *m_reportQueue;
Attachment() : m_fifo(nullptr), m_reportQueue(nullptr) {}
};
WebAPIAdapterInterface *m_adapter;
QMutex m_captureMutex; //!< One capture at a time
QMutex m_attachMutex; //!< Guards the attachment map
// Pipes are registered once per channel and kept until the channel is destroyed. Repeatedly
// registering and unregistering is not safe: the pipe registry leaves a stale entry behind
// when its garbage collector deletes a pipe, which the next unregister would dereference.
QMap<QObject *, Attachment> m_attachments;
QObject *m_pendingChannel; //!< Results of attach(), which runs on the main thread
DataFifo *m_pendingFifo;
MessageQueue *m_pendingReportQueue;
QString m_attachError;
QAtomicInt m_stopping;
// Set when the channel being captured from goes away, so the capture loop stops touching
// the FIFO before the pipe registry destroys it
QObject *m_capturingChannel;
QAtomicInt m_captureAborted;
// Held while the capture is inside either pipe element, the sample FIFO or the report
// queue. The pipe teardown handler takes it after setting the abort flag, so it cannot
// return (and the registry cannot free either element) while a read is in progress.
QMutex m_fifoMutex;
bool attach(int deviceSetIndex, int channelIndex);
//!< Runs attach() on the main thread and waits for it, unless the server stops first
bool attachFromMainThread(int deviceSetIndex, int channelIndex);
int reportedSampleRate(MessageQueue *queue, int timeoutMs);
int sampleRateFromReport(int deviceSetIndex, int channelIndex);
};
// Recording of baseband IQ to files through a File Sink channel.
//
// All files are written under a capture directory; paths that would escape it are refused,
// so an agent cannot write anywhere on the machine. The channel is created and deleted by
// MCPTools, which owns the waiting on SDRangel's asynchronous channel operations.
class MCPCapture
{
public:
explicit MCPCapture(WebAPIAdapterInterface *adapter);
void setCaptureDir(const QString& dir);
QString getCaptureDir() const;
QJsonObject startIQRecording(int deviceSetIndex, int channelIndex, const QString& fileName,
int frequencyOffset, int log2Decim);
//!< Checks a capture file name the way startIQRecording will, so a caller can refuse it before
//!< setting anything up. Throws for one outside the capture directory
void checkFileName(const QString& fileName, const QString& defaultPrefix) { resolvePath(fileName, defaultPrefix); }
//!< Stops the recording of the channel currently at this index
QJsonObject stopIQRecording(int deviceSetIndex, int channelIndex);
//!< Stops the recording of this channel wherever it has moved to
QJsonObject stopIQRecording(uint64_t channelUid);
//!< Drops the entry for a channel that is being deleted
void forgetRecording(uint64_t channelUid);
QJsonObject captureAudio(int deviceSetIndex, int channelIndex, double seconds, const QString& fileName, bool inlineAudio);
QJsonObject status();
//!< See MCPTools::setStopping
void setStopping(bool stopping);
//!< Current index of a channel, or false if it has gone. Indices shift when a lower
//!< numbered channel is deleted, so they are resolved again before every action.
//!< Channels are held by UID rather than by pointer across any wait: a deleted channel's
//!< address is reused by the next one created, and a UID never is
static bool locateChannel(uint64_t channelUid, int& deviceSetIndex, int& channelIndex);
//!< The UID of the channel at an index, or 0 when there is none
static uint64_t channelUidAt(int deviceSetIndex, int channelIndex);
//!< Inline audio is only offered for short clips, as it is base64 encoded into the reply. The
//!< inline copy is mono at m_inlineSampleRate, whatever the file holds: MCP clients refuse a
//!< tool result over about a megabyte, which 10 s of 48 kHz audio exceeds once encoded. Even
//!< so, a reply that would still be over m_maxInlineBytes goes without the audio
static constexpr int m_maxInlineSeconds = 20;
static constexpr int m_inlineSampleRate = 16000;
static constexpr int m_maxInlineBytes = 900000;
//!< Blocking captures hold an HTTP connection open, so they are kept short
static constexpr int m_maxBlockingSeconds = 30;
private:
struct Recording
{
QString m_fileBase; //!< Path without the timestamp and extension SDRangel appends
QDateTime m_started;
bool m_valid;
Recording() : m_valid(false) {}
};
WebAPIAdapterInterface *m_adapter;
mutable QMutex m_mutex;
QAtomicInt m_stopping;
QString m_captureDir;
// Keyed by the channel's UID rather than its index, because indices are renumbered
// whenever a lower numbered channel is deleted, which can happen while a recording runs
QMap<uint64_t, Recording> m_recordings;
MCPAudioCapture m_audio;
QString resolvePath(const QString& name, const QString& defaultPrefix);
QJsonObject channelSettings(int deviceSetIndex, int channelIndex);
QString channelType(const QJsonObject& settings);
void patchChannel(int deviceSetIndex, int channelIndex, const QString& type, const QJsonObject& settings);
void recordAction(int deviceSetIndex, int channelIndex, const QString& type, bool record);
QJsonObject channelReport(int deviceSetIndex, int channelIndex);
void pruneRecordings(); //!< Drops entries whose channel has been deleted
static QJsonObject filesFor(const Recording& recording);
};
#endif // INCLUDE_FEATURE_MCPCAPTURE_H_