Skip to content

Commit 5d8ba65

Browse files
authored
Merge pull request #38 from labstreaminglayer/refactor_stream_tracking
Refactor stream tracking
2 parents b9d1f9d + 4a95a0d commit 5d8ba65

5 files changed

Lines changed: 133 additions & 59 deletions

File tree

LabRecorder.cfg

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
; a warning is issued if one of the streams is not present when the record button is pressed
4545
; The syntax is as in: RequiredStreams = "BioSemi (MyHostname)","PhaseSpace (MyHostname)","Eyelink (AnotherHostname)"
4646
; where the format is identical to what the LabRecorder displays in the "Record from streams" list.
47-
; RequiredStreams="RequiredExample (PC)"
47+
; There must not be any spaces within the parentheses.
48+
; RequiredStreams="RequiredExample (PC-HOSTNAME)"
4849

4950
; === Online Sync ===
5051
; A list of sync settings. Each setting follows the following format: "SrcStreamName (SrcHostName) post_FLAG"
@@ -62,3 +63,7 @@
6263
; RCSPort to set the port number for the socket; default 22345
6364
RCSEnabled=1
6465
RCSPort=22345
66+
67+
; === Auto Start Recording ===
68+
; Set AutoStart to 1 to automatically start recording as soon as the config file has concluded parsing.
69+
; AutoStart=1

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ The Ubuntu releases do not typically ship with the libsl dependencies so you wil
2121
The LabRecorder displays a list of currently present device streams under "Record from Streams". If you have turned on a device after you have already started the recorder, click the "Update" button to update the list (this takes ca. 2 seconds).
2222
> For testing you can use a "dummy" device from the `lslexamples` found in the [liblsl release assets](https://github.com/sccn/liblsl/releases) (for example SendData<!--, SendStringMarkers, and SendDataSimple-->).
2323
24-
If you cannot see streams that are provided on another computer, read the section Network Troubleshooting on the NetworkConnectivity page. You can select which streams you want to record from and which not by checking the check boxes next to them.
24+
If you cannot see streams that are provided on another computer, read the section Network Troubleshooting on the NetworkConnectivity page.
25+
26+
You can select which streams you want to record from and which not by checking the check boxes next to them.
2527
> ![labrecorder-default.png](doc/labrecorder-default.png)
2628
29+
Note that if you have multiple streams with the same name and host, it is impossible to check only 1. If any is checked then they will all be recorded.
30+
2731
The entry in "Saving to..." shows you the file name (or file name template) where your recording will be stored. You can change this by modifying the Study Root folder (e.g., by clicking the browse button) and the `File Name / Template` field. If the respective directory does not yet exist, it will be created automatically (except if you do not have the permissions to create it). The file name string may contain placeholders that will be replaced by the values in the fields below. Checking the BIDS box will automatically change the filename template to be BIDS compliant. If the file that you are trying to record to already exists, the existing file will be renamed (the string `_oldX` will be appended where X is the lowest number that is not yet occupied by another existing file). This way, it is impossible to accidentally overwrite data.
2832

2933
The Block/Task field can be overwriten or selected among a list of items found in the configuration file.

src/mainwindow.cpp

Lines changed: 104 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,12 @@ MainWindow::MainWindow(QWidget *parent, const char *config_file)
8080
}
8181
});
8282

83-
QString cfgfilepath = find_config_file(config_file);
84-
load_config(cfgfilepath);
85-
8683
timer = std::make_unique<QTimer>(this);
8784
connect(&*timer, &QTimer::timeout, this, &MainWindow::statusUpdate);
8885
timer->start(1000);
86+
87+
QString cfgfilepath = find_config_file(config_file);
88+
load_config(cfgfilepath);
8989
}
9090

9191
void MainWindow::statusUpdate() const {
@@ -119,13 +119,14 @@ void MainWindow::blockSelected(const QString &block) {
119119

120120
void MainWindow::load_config(QString filename) {
121121
qInfo() << "loading config file " << QDir::toNativeSeparators(filename);
122+
bool auto_start = false;
122123
try {
123124
QSettings pt(QDir::cleanPath(filename), QSettings::Format::IniFormat);
124125

125126
// ----------------------------
126127
// required streams
127128
// ----------------------------
128-
requiredStreams = pt.value("RequiredStreams").toStringList();
129+
missingStreams = pt.value("RequiredStreams").toStringList().toSet();
129130

130131
// ----------------------------
131132
// online sync streams
@@ -239,28 +240,34 @@ void MainWindow::load_config(QString filename) {
239240
}
240241
}
241242

243+
if (pt.contains("AutoStart")) {
244+
auto_start = pt.value("AutoStart").toBool();
245+
}
246+
242247
} catch (std::exception &e) { qWarning() << "Problem parsing config file: " << e.what(); }
243248
// std::cout << "refreshing streams ..." <<std::endl;
244249
refreshStreams();
245-
}
250+
251+
if (auto_start) { startRecording(); }
252+
}
246253

247254
void MainWindow::save_config(QString filename) {
248255
QSettings settings(filename, QSettings::Format::IniFormat);
249256
settings.setValue("StudyRoot", QDir::cleanPath(ui->rootEdit->text()));
250257
if (!ui->check_bids->isChecked())
251258
settings.setValue("PathTemplate", QDir::cleanPath(ui->lineEdit_template->text()));
252-
qInfo() << requiredStreams;
259+
// Build QStringList from missingStreams and knownStreams that are missing.
260+
QStringList requiredStreams = missingStreams.values();
261+
for (auto &k : knownStreams) {
262+
if (k.checked) { requiredStreams.append(k.listName()); }
263+
}
264+
qInfo() << missingStreams;
253265
settings.setValue("RequiredStreams", requiredStreams);
254266
// Stub.
255267
}
256268

257-
QSet<QString> MainWindow::getCheckedStreams() const {
258-
QSet<QString> checked;
259-
for (int i = 0; i < ui->streamList->count(); i++) {
260-
QListWidgetItem *item = ui->streamList->item(i);
261-
if (item->checkState() == Qt::Checked) checked.insert(item->text());
262-
}
263-
return checked;
269+
QString info_to_listName(lsl::stream_info info) {
270+
return QString::fromStdString(info.name() + " (" + info.hostname() + ")");
264271
}
265272

266273
/**
@@ -271,42 +278,87 @@ QSet<QString> MainWindow::getCheckedStreams() const {
271278
std::vector<lsl::stream_info> MainWindow::refreshStreams() {
272279
std::vector<lsl::stream_info> resolvedStreams = lsl::resolve_streams(1.0);
273280

274-
QSet<QString> foundStreamNames;
275-
for (auto &s : resolvedStreams)
276-
foundStreamNames.insert(QString::fromStdString(s.name() + " (" + s.hostname() + ")"));
277-
278-
const QSet<QString> previouslyChecked = getCheckedStreams();
279-
// Missing streams: all checked or required streams that weren't found
280-
// requiredStreams.toSet() is deprecated. Eventually change to: QSet<QString>(requiredStreams.begin(), requiredStreams.end())
281-
missingStreams = (previouslyChecked + requiredStreams.toSet()) - foundStreamNames;
282-
283-
// (Re-)Populate the UI list
281+
// For each item in resolvedStreams, ignore if already in knownStreams, otherwise add to knownStreams.
282+
// if in missingStreams then also mark it as required (--> checked by default) and remove from missingStreams.
283+
for (auto& s : resolvedStreams) {
284+
bool known = false;
285+
for (auto &k : knownStreams) {
286+
known |= s.name() == k.name && s.type() == k.type && s.source_id() == k.id;
287+
}
288+
if (!known) {
289+
bool found = missingStreams.contains(info_to_listName(s));
290+
knownStreams << StreamItem(s.name(), s.type(), s.source_id(), s.hostname(), found);
291+
if (found) { missingStreams.remove(info_to_listName(s)); }
292+
}
293+
}
294+
// For each item in knownStreams, update its checked status from GUI. (only works for streams found on a previous refresh)
295+
// 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.
296+
for (auto &k : knownStreams) {
297+
QList<QListWidgetItem *> foundItems = ui->streamList->findItems(k.listName(), Qt::MatchCaseSensitive);
298+
if (foundItems.count() > 0) {
299+
bool checked = false;
300+
for (auto &fi : foundItems) { checked |= fi->checkState() == Qt::Checked; }
301+
k.checked = checked;
302+
}
303+
}
304+
// For each item in knownStreams; if it is not resolved then drop it. If it was checked then add back to missingStreams.
305+
int k_ind = 0;
306+
while (k_ind < knownStreams.count()) {
307+
StreamItem k = knownStreams.at(k_ind);
308+
bool resolved = false;
309+
size_t r_ind = 0;
310+
while (!resolved && r_ind < resolvedStreams.size()) {
311+
lsl::stream_info r = resolvedStreams[r_ind];
312+
resolved |= (r.name() == k.name) && (r.type() == k.type) && (r.source_id() == k.id);
313+
r_ind++;
314+
}
315+
if (!resolved) {
316+
if (k.checked) { missingStreams += k.listName(); }
317+
knownStreams.removeAt(k_ind);
318+
} else {
319+
k_ind++;
320+
}
321+
}
322+
// Clear the streamList
323+
// Add missing items first.
324+
// Then add knownStreams (only in list if resolved).
284325
const QBrush good_brush(QColor(0, 128, 0)), bad_brush(QColor(255, 0, 0));
285326
ui->streamList->clear();
286-
for (auto &&streamName : foundStreamNames + missingStreams) {
287-
auto *item = new QListWidgetItem(streamName, ui->streamList);
288-
289-
item->setCheckState(previouslyChecked.contains(streamName) ? Qt::Checked : Qt::Unchecked);
290-
item->setForeground(missingStreams.contains(streamName) ? bad_brush : good_brush);
291-
327+
for (auto& m : missingStreams) {
328+
auto *item = new QListWidgetItem(m, ui->streamList);
329+
item->setCheckState(Qt::Checked);
330+
item->setForeground(bad_brush);
331+
ui->streamList->addItem(item);
332+
}
333+
for (auto& k : knownStreams) {
334+
auto *item = new QListWidgetItem(k.listName(), ui->streamList);
335+
item->setCheckState(k.checked ? Qt::Checked : Qt::Unchecked);
336+
item->setForeground(good_brush);
292337
ui->streamList->addItem(item);
293338
}
294339

295-
return resolvedStreams;
340+
// return a std::vector of streams of checked and not missing streams.
341+
std::vector<lsl::stream_info> requestedAndAvailableStreams;
342+
for (auto &r : resolvedStreams) {
343+
for (auto &k : knownStreams) {
344+
if ((r.name() == k.name) && (r.type() == k.type) && (r.source_id() == k.id)) {
345+
if (k.checked) { requestedAndAvailableStreams.push_back(r); }
346+
break;
347+
}
348+
}
349+
}
350+
return requestedAndAvailableStreams;
296351
}
297352

298353
void MainWindow::startRecording() {
299354
if (!currentRecording) {
300355

301356
// automatically refresh streams
302-
const std::vector<lsl::stream_info> resolvedStreams = refreshStreams();
303-
const QSet<QString> checked = getCheckedStreams();
357+
const std::vector<lsl::stream_info> requestedAndAvailableStreams = refreshStreams();
304358

305359
if (!hideWarnings) {
306360
// if a checked stream is now missing
307-
// change to "checked.intersects(missingStreams) as soon as Ubuntu 16.04/Qt 5.5 is EOL
308-
QSet<QString> missing = checked;
309-
if (!missing.intersect(missingStreams).isEmpty()) {
361+
if (!missingStreams.isEmpty()) {
310362
// are you sure?
311363
QMessageBox msgBox(QMessageBox::Warning, "Stream not found",
312364
"At least one of the streams that you checked seems to be offline",
@@ -316,8 +368,8 @@ void MainWindow::startRecording() {
316368
if (msgBox.exec() != QMessageBox::Yes) return;
317369
}
318370

319-
if (checked.isEmpty()) {
320-
QMessageBox msgBox(QMessageBox::Warning, "No streams selected",
371+
if (requestedAndAvailableStreams.size() == 0) {
372+
QMessageBox msgBox(QMessageBox::Warning, "No available streams selected",
321373
"You have selected no streams", QMessageBox::Yes | QMessageBox::No, this);
322374
msgBox.setInformativeText("Do you want to start recording anyway?");
323375
msgBox.setDefaultButton(QMessageBox::No);
@@ -363,20 +415,24 @@ void MainWindow::startRecording() {
363415
return;
364416
}
365417

366-
// go through all the listed streams
367-
std::vector<lsl::stream_info> checkedStreams;
368-
369-
for (const lsl::stream_info &stream : resolvedStreams)
370-
if (checked.contains(
371-
QString::fromStdString(stream.name() + " (" + stream.hostname() + ')')))
372-
checkedStreams.push_back(stream);
373-
418+
374419
std::vector<std::string> watchfor;
375-
for (const QString &missing : missingStreams) watchfor.push_back(missing.toStdString());
420+
for (const QString &missing : missingStreams) {
421+
// Convert missing to query expected by lsl::resolve_stream
422+
// name='BioSemi' and hostname=AASDFSDF
423+
// QRegularExpression rx("(\S+)\s+\((\S+)\)");
424+
QStringList name_host = missing.split(QRegExp("\\s+"));
425+
std::string query = "name='" + name_host[0].toStdString() + "'";
426+
if (name_host.size() > 1) {
427+
std::string host = name_host[1].toStdString();
428+
query += " and hostname='" + host.substr(1, host.length() - 2) + "'";
429+
}
430+
watchfor.push_back(query);
431+
}
376432
qInfo() << "Missing: " << missingStreams;
377433

378-
currentRecording = std::make_unique<recording>(
379-
recFilename.toStdString(), checkedStreams, watchfor, syncOptionsByStreamName, true);
434+
currentRecording = std::make_unique<recording>(recFilename.toStdString(),
435+
requestedAndAvailableStreams, watchfor, syncOptionsByStreamName, true);
380436
ui->stopButton->setEnabled(true);
381437
ui->startButton->setEnabled(false);
382438
startTime = (int)lsl::local_clock();

src/mainwindow.h

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,19 @@ class MainWindow;
1818
class recording;
1919
class RemoteControlSocket;
2020

21-
class RecorderItem {
22-
21+
class StreamItem {
22+
2323
public:
24-
QListWidgetItem listItem;
25-
std::string uid;
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) {}
27+
28+
QString listName() { return QString::fromStdString(name + " (" + host + ")"); }
29+
std::string name;
30+
std::string type;
31+
std::string id;
32+
std::string host;
33+
bool checked;
2634
};
2735

2836

@@ -52,7 +60,6 @@ private slots:
5260
void rcsportValueChangedInt(int value);
5361

5462
private:
55-
QSet<QString> getCheckedStreams() const;
5663
QString replaceFilename(QString fullfile) const;
5764
// function for loading / saving the config file
5865
QString find_config_file(const char *filename);
@@ -66,9 +73,9 @@ private slots:
6673
int startTime;
6774
std::unique_ptr<QTimer> timer;
6875

69-
QStringList requiredStreams;
70-
std::map<std::string, int> syncOptionsByStreamName;
76+
QList<StreamItem> knownStreams;
7177
QSet<QString> missingStreams;
78+
std::map<std::string, int> syncOptionsByStreamName;
7279

7380
// QString recFilename;
7481
QString legacyTemplate;

src/recording.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,12 @@ void recording::record_from_query_results(const std::string &query) {
133133
// for each result...
134134
for (auto &result : results) {
135135
// if it is a new stream...
136+
std::string _uid = result.uid();
137+
std::string _src_id = result.source_id();
136138
if (!known_uids.count(result.uid()))
137139
// and doesn't have a previously seen source id...
138-
if (!(!result.source_id().empty() &&
139-
(!known_source_ids.count(result.source_id())))) {
140+
if (!result.source_id().empty() &&
141+
(!known_source_ids.count(result.source_id()))) {
140142
std::cout << "Found a new stream named " << result.name()
141143
<< ", adding it to the recording." << std::endl;
142144
// start a new recording thread

0 commit comments

Comments
 (0)