(std::move(device), callback);
+
+ if (stream_->start()) {
+ setStreaming(true);
+ } else {
+ stream_.reset();
+ QMessageBox::warning(this, "Error", "Failed to start streaming");
+ }
+ }
+}
+
+void MainWindow::onDeviceTypeChanged(int index) {
+ bool is_custom = (index == 6); // "Custom" is last item
+ ui_->label_address->setVisible(is_custom);
+ ui_->input_address->setVisible(is_custom);
+ ui_->label_port->setVisible(is_custom);
+ ui_->input_port->setVisible(is_custom);
+
+ // Update stream name placeholder to reflect auto-generated name
+ static const cbdev::DeviceType device_type_map[] = {
+ cbdev::DeviceType::HUB1, cbdev::DeviceType::HUB2, cbdev::DeviceType::HUB3,
+ cbdev::DeviceType::NSP, cbdev::DeviceType::LEGACY_NSP,
+ cbdev::DeviceType::NPLAY, cbdev::DeviceType::CUSTOM
+ };
+ if (index >= 0 && index < 7) {
+ auto display = blackrock::deviceTypeDisplayName(device_type_map[index]);
+ ui_->input_name->setPlaceholderText(
+ QString("(auto: Blackrock-%1)").arg(QString::fromStdString(display)));
+ }
+}
+
+void MainWindow::onBrowseCCF() {
+ QString filename = QFileDialog::getOpenFileName(
+ this, "Select CCF File", QString(),
+ "CCF Files (*.ccf);;All Files (*)"
+ );
+ if (!filename.isEmpty()) {
+ ui_->input_ccf->setText(filename);
+ }
+}
+
+void MainWindow::onLoadConfig() {
+ QString filename = QFileDialog::getOpenFileName(
+ this, "Load Configuration", last_config_path_,
+ "Configuration Files (*.cfg);;All Files (*)"
+ );
+ if (!filename.isEmpty()) {
+ loadConfig(filename);
+ }
+}
+
+void MainWindow::onSaveConfig() {
+ QString filename = QFileDialog::getSaveFileName(
+ this, "Save Configuration", last_config_path_,
+ "Configuration Files (*.cfg);;All Files (*)"
+ );
+ if (!filename.isEmpty()) {
+ saveConfig(filename);
+ }
+}
+
+void MainWindow::onAbout() {
+ QString info = QString(
+ "BlackrockOutlet
"
+ "Version 0.1.0
"
+ "Blackrock Cerebus/Neuroport LSL streaming application.
"
+ "
"
+ "LSL Library: %1
"
+ "Protocol: %2
"
+ ).arg(QString::number(lsl::library_version()),
+ QString::fromStdString(lsl::library_info()));
+
+ QMessageBox::about(this, "About BlackrockOutlet", info);
+}
+
+void MainWindow::loadConfig(const QString& filename) {
+ auto config = blackrock::ConfigManager::load(filename.toStdString());
+ if (!config) {
+ QMessageBox::warning(this, "Load Failed",
+ QString("Failed to load configuration from:\n%1").arg(filename));
+ return;
+ }
+
+ ui_->input_name->setText(QString::fromStdString(config->stream_name));
+
+ // Find device type index
+ static const char* device_types[] = {
+ "hub1", "hub2", "hub3", "nsp", "legacy_nsp", "nplay", "custom"
+ };
+ for (int i = 0; i < 7; ++i) {
+ if (config->device_type == device_types[i]) {
+ ui_->combo_device_type->setCurrentIndex(i);
+ break;
+ }
+ }
+
+ ui_->input_address->setText(QString::fromStdString(config->custom_address));
+ ui_->input_port->setValue(config->custom_port);
+ ui_->input_ccf->setText(QString::fromStdString(config->ccf_file));
+
+ // Sample group: 0=auto(index 0), 1-6=group(index 1-6), -1=none(index 7)
+ if (config->sample_group == -1) {
+ ui_->combo_group->setCurrentIndex(7);
+ } else if (config->sample_group >= 0 && config->sample_group <= 6) {
+ ui_->combo_group->setCurrentIndex(config->sample_group);
+ }
+
+ // Data format: index 0=Raw, index 1=Scaled
+ ui_->combo_format->setCurrentIndex(config->scaled ? 1 : 0);
+
+ ui_->check_heartbeat->setChecked(config->heartbeat);
+
+ last_config_path_ = filename;
+ updateStatus("Loaded: " + filename, false);
+}
+
+void MainWindow::saveConfig(const QString& filename) {
+ blackrock::AppConfig config;
+ config.stream_name = ui_->input_name->text().toStdString();
+ config.stream_type = "ECEPhys";
+
+ static const char* device_types[] = {
+ "hub1", "hub2", "hub3", "nsp", "legacy_nsp", "nplay", "custom"
+ };
+ int idx = ui_->combo_device_type->currentIndex();
+ if (idx >= 0 && idx < 7) config.device_type = device_types[idx];
+
+ config.custom_address = ui_->input_address->text().toStdString();
+ config.custom_port = static_cast(ui_->input_port->value());
+ config.ccf_file = ui_->input_ccf->text().toStdString();
+
+ int group_idx = ui_->combo_group->currentIndex();
+ config.sample_group = (group_idx == 7) ? -1 : group_idx;
+ config.scaled = (ui_->combo_format->currentIndex() == 1);
+ config.heartbeat = ui_->check_heartbeat->isChecked();
+
+ if (blackrock::ConfigManager::save(config, filename.toStdString())) {
+ last_config_path_ = filename;
+ updateStatus("Saved: " + filename, false);
+ } else {
+ QMessageBox::warning(this, "Save Failed",
+ QString("Failed to save configuration to:\n%1").arg(filename));
+ }
+}
+
+QString MainWindow::findDefaultConfigFile() {
+ QFileInfo exe_info(QCoreApplication::applicationFilePath());
+ QString default_name = "BlackrockOutlet.cfg";
+
+ QStringList search_paths = {
+ QDir::currentPath(),
+ exe_info.absolutePath()
+ };
+ search_paths.append(QStandardPaths::standardLocations(QStandardPaths::ConfigLocation));
+
+ for (const auto& path : search_paths) {
+ QString full_path = path + QDir::separator() + default_name;
+ if (QFileInfo::exists(full_path)) return full_path;
+ }
+ return QString();
+}
+
+void MainWindow::updateStatus(const QString& message, bool is_error) {
+ ui_->statusbar->showMessage(message, is_error ? 0 : 5000);
+ ui_->statusbar->setStyleSheet(is_error ? "color: red;" : "");
+}
+
+void MainWindow::setStreaming(bool streaming) {
+ ui_->linkButton->setText(streaming ? "Unlink" : "Link");
+
+ ui_->input_name->setEnabled(!streaming);
+ ui_->combo_device_type->setEnabled(!streaming);
+ ui_->input_address->setEnabled(!streaming);
+ ui_->input_port->setEnabled(!streaming);
+ ui_->input_ccf->setEnabled(!streaming);
+ ui_->btn_browse_ccf->setEnabled(!streaming);
+ ui_->combo_group->setEnabled(!streaming);
+ ui_->combo_format->setEnabled(!streaming);
+ ui_->check_heartbeat->setEnabled(!streaming);
+}
diff --git a/src/gui/MainWindow.hpp b/src/gui/MainWindow.hpp
new file mode 100644
index 0000000..32c3115
--- /dev/null
+++ b/src/gui/MainWindow.hpp
@@ -0,0 +1,46 @@
+#pragma once
+/**
+ * @file MainWindow.hpp
+ * @brief Main window for BlackrockOutlet GUI application
+ */
+
+#include
+#include
+
+namespace Ui {
+class MainWindow;
+}
+
+namespace blackrock {
+class StreamThread;
+}
+
+class MainWindow : public QMainWindow {
+ Q_OBJECT
+
+public:
+ explicit MainWindow(const QString& config_file = QString(), QWidget* parent = nullptr);
+ ~MainWindow() override;
+
+protected:
+ void closeEvent(QCloseEvent* event) override;
+
+private slots:
+ void onLinkButtonClicked();
+ void onDeviceTypeChanged(int index);
+ void onBrowseCCF();
+ void onLoadConfig();
+ void onSaveConfig();
+ void onAbout();
+
+private:
+ void loadConfig(const QString& filename);
+ void saveConfig(const QString& filename);
+ QString findDefaultConfigFile();
+ void updateStatus(const QString& message, bool is_error);
+ void setStreaming(bool streaming);
+
+ std::unique_ptr ui_;
+ std::unique_ptr stream_;
+ QString last_config_path_;
+};
diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui
new file mode 100644
index 0000000..49a2404
--- /dev/null
+++ b/src/gui/MainWindow.ui
@@ -0,0 +1,313 @@
+
+
+ MainWindow
+
+
+ BlackrockOutlet
+
+
+
+ 400
+ 360
+
+
+
+
+ -
+
+
-
+
+
+ Device Type
+
+
+
+ -
+
+
-
+
+ Hub 1
+
+
+ -
+
+ Hub 2
+
+
+ -
+
+ Hub 3
+
+
+ -
+
+ NSP
+
+
+ -
+
+ Legacy NSP
+
+
+ -
+
+ nPlay
+
+
+ -
+
+ Custom
+
+
+
+
+ -
+
+
+ Address
+
+
+
+ -
+
+
+ 192.168.137.128
+
+
+
+ -
+
+
+ Port
+
+
+
+ -
+
+
+ 0
+
+
+ 65535
+
+
+ 51001
+
+
+
+ -
+
+
+ CCF File
+
+
+
+ -
+
+
-
+
+
+ (optional)
+
+
+
+ -
+
+
+ Browse...
+
+
+
+ 80
+ 16777215
+
+
+
+
+
+
+ -
+
+
+ Sample Group
+
+
+
+ -
+
+
-
+
+ Auto (first active)
+
+
+ -
+
+ Group 1 (500 Hz)
+
+
+ -
+
+ Group 2 (1 kHz)
+
+
+ -
+
+ Group 3 (2 kHz)
+
+
+ -
+
+ Group 4 (10 kHz)
+
+
+ -
+
+ Group 5 (30 kHz)
+
+
+ -
+
+ Group 6 (30 kHz raw)
+
+
+ -
+
+ None (heartbeat only)
+
+
+
+
+ -
+
+
+ Stream Name
+
+
+
+ -
+
+
+
+
+
+ (auto: Blackrock-Hub1)
+
+
+
+ -
+
+
+ Data Format
+
+
+
+ -
+
+
-
+
+ Raw (int16)
+
+
+ -
+
+ Scaled (uV, float32)
+
+
+
+
+ -
+
+
+ Stream device heartbeat timestamps
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 40
+
+
+
+ Link
+
+
+
+
+
+
+
+
+
+ &Load Configuration...
+
+
+ Ctrl+L
+
+
+
+
+ &Save Configuration...
+
+
+ Ctrl+S
+
+
+
+
+ &Quit
+
+
+ Ctrl+Q
+
+
+
+
+ &About...
+
+
+
+
+
+
diff --git a/src/gui/main.cpp b/src/gui/main.cpp
new file mode 100644
index 0000000..fb8ad75
--- /dev/null
+++ b/src/gui/main.cpp
@@ -0,0 +1,26 @@
+/**
+ * @file main.cpp
+ * @brief GUI entry point for BlackrockOutlet
+ */
+
+#include "MainWindow.hpp"
+#include
+
+int main(int argc, char* argv[]) {
+ QApplication app(argc, argv);
+
+ app.setApplicationName("BlackrockOutlet");
+ app.setApplicationVersion("1.0.0");
+ app.setOrganizationName("LabStreamingLayer");
+ app.setOrganizationDomain("labstreaminglayer.org");
+
+ QString config_file;
+ if (argc > 1) {
+ config_file = QString::fromLocal8Bit(argv[1]);
+ }
+
+ MainWindow window(config_file);
+ window.show();
+
+ return app.exec();
+}