Skip to content

Commit c2e7868

Browse files
committed
Add remote stream selection command
1 parent 8419550 commit c2e7868

5 files changed

Lines changed: 146 additions & 54 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ If you check the box to EnableRCS then LabRecorder exposes some rudimentary cont
8787
Currently supported commands include:
8888
* `select all`
8989
* `select none`
90-
* `start`
90+
* `select <query>` - checks streams matching an LSL resolver predicate such as `name='BioSemi'`, `type='EEG'`, or `name='BioSemi' and hostname='LabPC1'`.
91+
* `start` - starts recording the current stream selection; returns an error if no streams are selected.
9192
* `stop`
9293
* `update`
9394
* `filename ...`

src/mainwindow.cpp

Lines changed: 94 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -287,37 +287,67 @@ QString info_to_listName(const lsl::stream_info& info) {
287287
return QString::fromStdString(info.name() + " (" + info.hostname() + ")");
288288
}
289289

290+
void MainWindow::updateKnownStreamSelectionFromUi() {
291+
for (int i = 0; i < ui->streamList->count(); i++) {
292+
QListWidgetItem *item = ui->streamList->item(i);
293+
bool ok = false;
294+
int knownIndex = item->data(Qt::UserRole).toInt(&ok);
295+
if (ok && knownIndex >= 0 && knownIndex < knownStreams.count())
296+
knownStreams[knownIndex].checked = item->checkState() == Qt::Checked;
297+
}
298+
}
299+
300+
void MainWindow::rebuildStreamList() {
301+
const QBrush good_brush(QColor(0, 128, 0)), bad_brush(QColor(255, 0, 0));
302+
ui->streamList->clear();
303+
for (auto& m : std::as_const(missingStreams)) {
304+
auto *item = new QListWidgetItem(m, ui->streamList);
305+
item->setCheckState(Qt::Checked);
306+
item->setForeground(bad_brush);
307+
ui->streamList->addItem(item);
308+
}
309+
for (int i = 0; i < knownStreams.count(); i++) {
310+
const auto &k = knownStreams[i];
311+
auto *item = new QListWidgetItem(k.listName(), ui->streamList);
312+
item->setData(Qt::UserRole, i);
313+
item->setCheckState(k.checked ? Qt::Checked : Qt::Unchecked);
314+
item->setForeground(good_brush);
315+
item->setToolTip(QString("Name: %1\nType: %2\nSource ID: %3\nHostname: %4\nUID: %5")
316+
.arg(QString::fromStdString(k.name),
317+
QString::fromStdString(k.type),
318+
QString::fromStdString(k.id),
319+
QString::fromStdString(k.host),
320+
QString::fromStdString(k.uid)));
321+
ui->streamList->addItem(item);
322+
}
323+
}
324+
290325
/**
291326
* @brief MainWindow::refreshStreams Find streams, generate a list of missing streams
292327
* and fill the UI streamlist.
293328
* @return A vector of found stream_infos
294329
*/
295330
std::vector<lsl::stream_info> MainWindow::refreshStreams() {
296331
const std::vector<lsl::stream_info> resolvedStreams = lsl::resolve_streams(1.0);
332+
updateKnownStreamSelectionFromUi();
297333

298334
// For each item in resolvedStreams, ignore if already in knownStreams, otherwise add to knownStreams.
299335
// if in missingStreams then also mark it as required (--> checked by default) and remove from missingStreams.
300336
for (const auto& s : resolvedStreams) {
301337
bool known = false;
302338
for (auto &k : knownStreams) {
303-
known |= s.name() == k.name && s.type() == k.type && s.source_id() == k.id;
339+
if (k.matches(s)) {
340+
k.updateInfo(s);
341+
known = true;
342+
break;
343+
}
304344
}
305345
if (!known) {
306346
bool found = missingStreams.contains(info_to_listName(s));
307-
knownStreams << StreamItem(s.name(), s.type(), s.source_id(), s.hostname(), found);
347+
knownStreams << StreamItem(s, found);
308348
if (found) { missingStreams.remove(info_to_listName(s)); }
309349
}
310350
}
311-
// For each item in knownStreams, update its checked status from GUI. (only works for streams found on a previous refresh)
312-
// Because we search by name + host, entries aren't guaranteed to be unique, so checking one entry with matching name and host checks them all.
313-
for (auto &k : knownStreams) {
314-
QList<QListWidgetItem *> foundItems = ui->streamList->findItems(k.listName(), Qt::MatchCaseSensitive);
315-
if (foundItems.count() > 0) {
316-
bool checked = false;
317-
for (auto &fi : foundItems) { checked |= fi->checkState() == Qt::Checked; }
318-
k.checked = checked;
319-
}
320-
}
321351
// For each item in knownStreams; if it is not resolved then drop it. If it was checked then add back to missingStreams.
322352
int k_ind = 0;
323353
while (k_ind < knownStreams.count()) {
@@ -326,7 +356,7 @@ std::vector<lsl::stream_info> MainWindow::refreshStreams() {
326356
size_t r_ind = 0;
327357
while (!resolved && r_ind < resolvedStreams.size()) {
328358
const lsl::stream_info r = resolvedStreams[r_ind];
329-
resolved |= (r.name() == k.name) && (r.type() == k.type) && (r.source_id() == k.id);
359+
resolved |= k.matches(r);
330360
r_ind++;
331361
}
332362
if (!resolved) {
@@ -339,31 +369,13 @@ std::vector<lsl::stream_info> MainWindow::refreshStreams() {
339369
// Clear the streamList
340370
// Add missing items first.
341371
// Then add knownStreams (only in list if resolved).
342-
const QBrush good_brush(QColor(0, 128, 0)), bad_brush(QColor(255, 0, 0));
343-
ui->streamList->clear();
344-
for (auto& m : std::as_const(missingStreams)) {
345-
auto *item = new QListWidgetItem(m, ui->streamList);
346-
item->setCheckState(Qt::Checked);
347-
item->setForeground(bad_brush);
348-
ui->streamList->addItem(item);
349-
}
350-
for (auto& k : knownStreams) {
351-
auto *item = new QListWidgetItem(k.listName(), ui->streamList);
352-
item->setCheckState(k.checked ? Qt::Checked : Qt::Unchecked);
353-
item->setForeground(good_brush);
354-
item->setToolTip(QString("Name: %1\nType: %2\nSource ID: %3\nHostname: %4")
355-
.arg(QString::fromStdString(k.name),
356-
QString::fromStdString(k.type),
357-
QString::fromStdString(k.id),
358-
QString::fromStdString(k.host)));
359-
ui->streamList->addItem(item);
360-
}
372+
rebuildStreamList();
361373

362374
// return a std::vector of streams of checked and not missing streams.
363375
std::vector<lsl::stream_info> requestedAndAvailableStreams;
364376
for (const auto &r : resolvedStreams) {
365377
for (auto &k : knownStreams) {
366-
if ((r.name() == k.name) && (r.type() == k.type) && (r.source_id() == k.id)) {
378+
if (k.matches(r)) {
367379
if (k.checked) { requestedAndAvailableStreams.push_back(r); }
368380
break;
369381
}
@@ -507,6 +519,40 @@ void MainWindow::selectNoStreams() {
507519
}
508520
}
509521

522+
bool MainWindow::hasSelectedStreams() const {
523+
for (int i = 0; i < ui->streamList->count(); i++) {
524+
const QListWidgetItem *item = ui->streamList->item(i);
525+
if (item->checkState() == Qt::Checked) return true;
526+
}
527+
return false;
528+
}
529+
530+
void MainWindow::selectStreams(const QString &query) {
531+
updateKnownStreamSelectionFromUi();
532+
std::vector<lsl::stream_info> matchedStreams;
533+
try {
534+
matchedStreams = lsl::resolve_stream(query.toStdString(), 0, 1.0);
535+
} catch (std::exception &e) {
536+
qWarning() << "Invalid stream selection query" << query << ":" << e.what();
537+
return;
538+
}
539+
540+
for (const auto &stream : matchedStreams) {
541+
bool known = false;
542+
for (auto &k : knownStreams) {
543+
if (k.matches(stream)) {
544+
k.updateInfo(stream);
545+
k.checked = true;
546+
known = true;
547+
break;
548+
}
549+
}
550+
if (!known) knownStreams << StreamItem(stream, true);
551+
missingStreams.remove(info_to_listName(stream));
552+
}
553+
rebuildStreamList();
554+
}
555+
510556
void MainWindow::buildBidsTemplate() {
511557
// path/to/CurrentStudy/sub-%p/ses-%s/eeg/sub-%p_ses-%s_task-%b[_acq-%a]_run-%r_eeg.xdf
512558

@@ -647,6 +693,7 @@ void MainWindow::enableRcs(bool bEnable) {
647693
connect(rcs.get(), &RemoteControlSocket::filename, this, &MainWindow::rcsUpdateFilename);
648694
connect(rcs.get(), &RemoteControlSocket::select_all, this, &MainWindow::selectAllStreams);
649695
connect(rcs.get(), &RemoteControlSocket::select_none, this, &MainWindow::selectNoStreams);
696+
connect(rcs.get(), &RemoteControlSocket::select_stream, this, &MainWindow::selectStreams);
650697
}
651698
bool oldState = ui->rcsCheckBox->blockSignals(true);
652699
ui->rcsCheckBox->setChecked(bEnable);
@@ -660,17 +707,27 @@ void MainWindow::rcsportValueChangedInt(int value) {
660707
}
661708
}
662709

663-
void MainWindow::rcsStartRecording() {
664-
// since we want to avoid a pop-up window when streams are missing or unchecked,
665-
// we'll check all the streams and start recording
710+
void MainWindow::rcsStartRecording(QTcpSocket *sock) {
711+
// Remote start should record the current stream selection. Do not call
712+
// selectAllStreams() here; doing so would override TCP `select <query>` commands.
713+
// hideWarnings suppresses non-critical confirmation dialogs for remote control.
714+
if (!hasSelectedStreams()) {
715+
qWarning() << "Remote start rejected: no streams selected";
716+
if (sock) sock->write("ERROR no streams selected");
717+
return;
718+
}
719+
const bool oldHideWarnings = hideWarnings;
666720
hideWarnings = true;
667-
selectAllStreams();
668721
startRecording();
722+
hideWarnings = oldHideWarnings;
723+
if (sock) sock->write("OK");
669724
}
670725

671726
void MainWindow::rcsStopRecording() {
727+
const bool oldHideWarnings = hideWarnings;
672728
hideWarnings = true;
673729
stopRecording();
730+
hideWarnings = oldHideWarnings;
674731
}
675732

676733
void MainWindow::rcsUpdateFilename(QString s) {

src/mainwindow.h

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,47 @@ class MainWindow;
1717

1818
class recording;
1919
class RemoteControlSocket;
20+
class QTcpSocket;
2021

2122
class StreamItem {
2223

2324
public:
24-
StreamItem(std::string stream_name, std::string stream_type, std::string source_id,
25-
std::string hostname, bool required)
26-
: name(stream_name), type(stream_type), id(source_id), host(hostname), checked(required) {}
25+
StreamItem(const lsl::stream_info &info, bool required)
26+
: name(info.name()), type(info.type()), id(info.source_id()), host(info.hostname()),
27+
uid(info.uid()), sessionId(info.session_id()), channelCount(info.channel_count()),
28+
nominalSrate(info.nominal_srate()), channelFormat(info.channel_format()),
29+
createdAt(info.created_at()), checked(required) {}
2730

28-
QString listName() { return QString::fromStdString(name + " (" + host + ")"); }
31+
QString listName() const { return QString::fromStdString(name + " (" + host + ")"); }
32+
bool matches(const lsl::stream_info &info) const {
33+
if (!id.empty() || !info.source_id().empty())
34+
return name == info.name() && type == info.type() && id == info.source_id();
35+
return name == info.name() && type == info.type() && host == info.hostname() &&
36+
sessionId == info.session_id() && channelCount == info.channel_count() &&
37+
nominalSrate == info.nominal_srate() && channelFormat == info.channel_format();
38+
}
39+
void updateInfo(const lsl::stream_info &info) {
40+
name = info.name();
41+
type = info.type();
42+
id = info.source_id();
43+
host = info.hostname();
44+
uid = info.uid();
45+
sessionId = info.session_id();
46+
channelCount = info.channel_count();
47+
nominalSrate = info.nominal_srate();
48+
channelFormat = info.channel_format();
49+
createdAt = info.created_at();
50+
}
2951
std::string name;
3052
std::string type;
3153
std::string id;
3254
std::string host;
55+
std::string uid;
56+
std::string sessionId;
57+
int32_t channelCount;
58+
double nominalSrate;
59+
lsl::channel_format_t channelFormat;
60+
double createdAt;
3361
bool checked;
3462
};
3563

@@ -50,13 +78,14 @@ private slots:
5078
void stopRecording(void);
5179
void selectAllStreams();
5280
void selectNoStreams();
81+
void selectStreams(const QString &query);
5382
void buildFilename();
5483
void buildBidsTemplate();
5584
void printReplacedFilename();
5685
void enableRcs(bool bEnable);
5786
void rcsCheckBoxChanged(bool checked);
5887
void rcsUpdateFilename(QString s);
59-
void rcsStartRecording();
88+
void rcsStartRecording(QTcpSocket *sock);
6089
void rcsStopRecording();
6190
void rcsportValueChangedInt(int value);
6291

@@ -65,6 +94,9 @@ private slots:
6594
// function for loading / saving the config file
6695
QString find_config_file(const char *filename);
6796
QString counterPlaceholder() const;
97+
bool hasSelectedStreams() const;
98+
void updateKnownStreamSelectionFromUi();
99+
void rebuildStreamList();
68100
void load_config(QString filename);
69101
void save_config(QString filename);
70102

src/tcpinterface.cpp

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,21 @@ void RemoteControlSocket::addClient() {
1717

1818
void RemoteControlSocket::handleLine(QString s, QTcpSocket *sock) {
1919
qInfo() << s;
20-
if (s == "start")
21-
emit start();
22-
else if (s == "stop")
20+
if (s == "start") {
21+
emit start(sock);
22+
return;
23+
} else if (s == "stop")
2324
emit stop();
2425
else if (s == "update")
2526
emit refresh_streams();
26-
else if (s.contains("filename")) {
27+
else if (s == "filename" || s.startsWith("filename ")) {
2728
emit filename(s);
28-
} else if (s.contains("select")) {
29-
if (s.contains("all")) {
30-
emit select_all();
31-
} else if (s.contains("none")) {
32-
emit select_none();
33-
}
29+
} else if (s == "select all") {
30+
emit select_all();
31+
} else if (s == "select none") {
32+
emit select_none();
33+
} else if (s.startsWith("select ")) {
34+
emit select_stream(s.mid(QStringLiteral("select ").size()));
3435
}
3536
sock->write("OK");
3637
// TODO: select /deselect streams

src/tcpinterface.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ class RemoteControlSocket : public QObject {
1616

1717
signals:
1818
void refresh_streams();
19-
void start();
19+
void start(QTcpSocket *sock);
2020
void stop();
2121
void filename(QString s);
2222
void select_all();
2323
void select_none();
24+
void select_stream(QString query);
2425

2526
public slots:
2627
void addClient();

0 commit comments

Comments
 (0)