### Command: start JSON example Source: https://syntalos.org/docs/network-ctl-protocol Example of the `start` command broadcast after the controller has received all required `prepare` ACKs and has started its own master timer. The `ts_start_us` field is the controller wall-clock start in microseconds since Unix epoch. ```json { "v": 1, "type": "start", "sender": "rig-ctrl", "run_id": "019312ab-...", "ts_start_us": 1737000000123456 } ``` -------------------------------- ### Install Syntalos on Ubuntu: add repository, apt install, dialout group Source: https://syntalos.org/get Set up the APT repository for Syntalos on Ubuntu, install the package, and grant the current user access to serial devices. Run the setup command first; it adds an APT source. Then install with apt and add the user to the dialout group. ```bash curl -fsSL https://syntalos.org/setup/setup-syntalos-repo.sh | sudo sh ``` ```bash sudo apt install syntalos ``` ```bash sudo usermod -a -G dialout $USER ``` -------------------------------- ### Command: prepare JSON example Source: https://syntalos.org/docs/network-ctl-protocol Example of the `prepare` command broadcast by the controller before a run starts. Listeners should apply the provided parameters and prepare the run. ```json { "v": 1, "type": "prepare", "sender": "rig-ctrl", "run_id": "019312ab-...", "project": "my-project", "subject_id": "M42", "subject_group": "control", "experiment_id": "novel-object-1" } ``` -------------------------------- ### virtual void start() Source: https://syntalos.org/api/files/moduleapi_8h Starts the module execution. ```APIDOC ## virtual void start() ### Description Starts the module execution. ### Method start() ``` -------------------------------- ### start() Source: https://syntalos.org/api/classes/classsyntalos_1_1mlinkmodule Starts the experiment run. ```APIDOC ## virtual void start() override ### Description Run when the experiment is started and the HRTimer has an initial time set. Switches the module into “Started” mode. ``` -------------------------------- ### MLinkModule Start Method Source: https://syntalos.org/api/files/mlinkmodule_8cpp Initializes the module start process, updates input port metadata, and launches the module. ```cpp void MLinkModule::start() { d->portChangesAllowed = false; // timestamp when this module was started auto startTimestampUs = std::chrono::duration_cast( m_syTimer->currentTimePoint().time_since_epoch()) .count(); // update input port metadata if the metadata has changed - this may happen in case of circular module connections for (auto &iport : inPorts()) { if (!iport->hasSubscription()) continue; const auto mdata = iport->subscriptionVar()->metadata(); const auto portId = iport->id().toStdString(); if (d->sentMetadata.value(portId, MetaStringMap()) == mdata) continue; UpdateInputPortMetadataRequest req; req.id = portId; req.metadata = mdata; if (!d->callSliceClientSimple( this, IN_PORT_UPDATE_METADATA_ID, req, 5, IpcCallFlag::TimeoutIsError | IpcCallFlag::SkipWaitOnError)) return; } d->sentMetadata.clear(); // tell the module to launch! d->callClientSimple( this, START_CALL_ID, [&](auto &req) { req.startTimestampUsec = startTimestampUs; }, 2); // stop reading control events in the GUI thread - the module thread will do that for us soon d->ctlEventTimer->stop(); // Start all output streams, regardless of whether they have a master-side IPC // forwarder. This re-snapshots the (now-final and immutable) metadata into all // output-port subscriptions, including pure MLink->MLink ports that have no // forwarder entry in outPortSubs. Destination MLink workers read metadata from // the in-process subscription via UpdateInputPortMetadataRequest and need it set. for (auto &oport : outPorts()) { if (oport->streamVar()->hasSubscribers()) oport->startStream(); } // call generic AbstractModule::start(); } ``` -------------------------------- ### start Source: https://syntalos.org/api/classes/classsyntalos_1_1secondaryclocksynchronizer Starts the synchronizer. ```APIDOC ## start ### Description Starts the synchronizer. ### Method bool start() ### Parameters None. ### Request Example ```cpp bool start(); ``` ### Response - **Return Value** (bool) - True if start succeeded, false otherwise. ``` -------------------------------- ### start() Source: https://syntalos.org/api/classes/classsyntalos_1_1synctimer Starts the timer. ```APIDOC ## start() ### Description Starts the timer. ### Method Member function ### Signature ```cpp void start() ``` ### Parameters None. ### Returns void ``` -------------------------------- ### Install and run Syntalos on Windows via WSL2 Source: https://syntalos.org/docs/setup/install-windows Commands to install and run Syntalos on Windows via WSL2. The first block adds the Flatpak PPA, updates the system, and installs Flatpak and xdg-desktop-portal-kde. The second block adds the Flathub remote and installs Syntalos, requiring two Enter confirmations. The third block creates a symlink so Syntalos appears in Windows menus. The fourth block launches Syntalos from the WSL command line. ```bash sudo add-apt-repository --yes ppa:flatpak/stable sudo apt update && sudo apt -y full-upgrade && sudo apt --purge -y autoremove sudo apt install -y flatpak xdg-desktop-portal-kde ``` ```bash sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo sudo flatpak install org.syntalos.syntalos ``` ```bash sudo mkdir -p /usr/local/share/applications/ sudo ln -s /var/lib/flatpak/exports/share/applications/org.syntalos.syntalos.desktop /usr/local/share/applications/ ``` ```bash flatpak run org.syntalos.syntalos ``` -------------------------------- ### Create Python Virtual Environment Source: https://syntalos.org/api/files/pyvenvmanager_8cpp Initializes a new virtual environment, handling directory creation and temporary file setup for installation commands. ```cpp auto createPythonVirtualEnv(const QString &venvName, const QString &requirementsFile, bool recreate) -> std::expected { auto rtdDir = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); if (rtdDir.isEmpty()) rtdDir = "/tmp"; const auto venvDir = pythonVEnvDirForName(venvName); if (QDir(venvDir).exists()) { if (recreate) { LOG_INFO(logVEnv, "Removing existing virtualenv before recreation: {}", venvDir); if (!QDir(venvDir).removeRecursively()) return std::unexpected(QStringLiteral("Unable to remove existing virtualenv: %1").arg(venvDir)); } else { // nothing to do, the venv already exists return venvDir; } } QDir().mkpath(venvDir); const auto tmpCommandFile = QStringLiteral("%1/sy-venv-%2.sh").arg(rtdDir, createRandomString(6)); QFile shFile(tmpCommandFile); if (!shFile.open(QIODevice::WriteOnly | QIODevice::Text)) { LOG_WARNING(logVEnv, "Unable to open temporary file {} for writing.", tmpCommandFile); return std::unexpected( QStringLiteral("Unable to open temporary file for virtualenv creation: %1").arg(shFile.errorString())); } const auto tmpRequirementsFname = QStringLiteral("%1/%2-requirements_%3.txt") .arg(rtdDir, venvName, createRandomString(4)); QFile tmpReqFile(tmpRequirementsFname); if (!tmpReqFile.open(QIODevice::WriteOnly | QIODevice::Text)) { LOG_WARNING(logVEnv, "Unable to open temporary file {} for writing.", tmpRequirementsFname); return std::unexpected( QStringLiteral("Unable to open temporary file for virtualenv creation: %1").arg(tmpReqFile.errorString())); } ``` -------------------------------- ### start Source: https://syntalos.org/api/classes/classsyntalos_1_1mlinkmodule Starts the module when the experiment is started. ```APIDOC ## start ### Description Run when the experiment is started and the HRTimer has an initial time set. Switches the module into "Started" mode. ### Signature virtual void start() override ``` -------------------------------- ### Install Syntalos via APT Source: https://syntalos.org/docs/setup/install Install the Syntalos package using the APT package manager after setting up the repository. ```bash sudo apt install syntalos ``` -------------------------------- ### Install Syntalos via Flatpak Source: https://syntalos.org/docs/setup/install Commands to install Flatpak, add the Flathub remote, and install the Syntalos Flatpak bundle. ```bash # replace this command with the distribution's native package manager to install Flatpak sudo apt install flatpak # set up Flathub sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo # install Syntalos flatpak install flathub org.syntalos.syntalos ``` -------------------------------- ### start() Source: https://syntalos.org/api/classes/classdatastream Starts the data stream, allowing data to be pushed and received. ```APIDOC ## start() ### Description Starts the data stream, allowing data to be pushed and received. ### Method virtual void start() override ``` -------------------------------- ### MLinkModule constructor with QProcess setup Source: https://syntalos.org/api/files/mlinkmodule_8cpp Constructor for MLinkModule. Sets up the QProcess, enables persistent worker mode, allows port changes, captures output, connects process signals, and starts a control event timer. ```cpp MLinkModule::MLinkModule(QObject *parent) : AbstractModule(parent), d(new MLinkModule::Private(m_log)) { d->proc = new QProcess(this); d->workerMode = ModuleWorkerMode::PERSISTENT; d->portChangesAllowed = true; // merge stdout/stderr of external process with ours by default setOutputCaptured(false); connect(d->proc, &QProcess::readyReadStandardOutput, this, [this]() { if (d->outputCaptured) Q_EMIT processOutputReceived(ChannelStdout, readProcessOutput(ChannelStdout)); }); connect(d->proc, &QProcess::readyReadStandardError, this, [this]() { if (d->outputCaptured) Q_EMIT processOutputReceived(ChannelStderr, readProcessOutput(ChannelStderr)); }); connect( d->proc, static_cast(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus == QProcess::CrashExit) { raiseError(QStringLiteral("Module process crashed with exit code %1! Check the log for details.") .arg(exitCode)); } }); d->ctlEventTimer = new QTimer(this); d->ctlEventTimer->setInterval(100); connect(d->ctlEventTimer, &QTimer::timeout, this, [this]() { handleIncomingControl(); }); } ``` -------------------------------- ### Implement periodic control commands Source: https://syntalos.org/tutorials/03_script-control A complete script example that sends a 2-second start command to a module every 5 seconds. It uses ControlCommand to define the action and schedule_delayed_call for non-blocking timing. ```python import syntalos_mlink as syl oport = syl.get_output_port('control-out') def start(): """Called by Syntalos immediately when the experiment is started.""" send_beep() def send_beep(): ctl = syl.ControlCommand() ctl.kind = syl.ControlCommandKind.START ctl.duration = 2000 # run for 2 sec oport.submit(ctl) if not syl.is_running(): return # run again in 5 sec syl.schedule_delayed_call(5 * 1000, send_beep) ``` -------------------------------- ### Install syntalos-hwsupport on Debian/Ubuntu Source: https://syntalos.org/docs/setup/hardware-support Use this command to manually install the downloaded hardware support package on Debian or Ubuntu systems. ```bash sudo apt install ./syntalos-hwsupport_*_all.deb ``` -------------------------------- ### using StartFn Source: https://syntalos.org/api/namespaces/namespacesyntalos Function type for starting a process. ```cpp using Syntalos::StartFn = typedef std::function; ``` -------------------------------- ### start Source: https://syntalos.org/api/classes/classdatastream Starts the stream. ```APIDOC ## start() ### Description Starts the stream. Overrides VariantDataStream::start. ### Signature ```cpp inline virtual void start() override ``` ``` -------------------------------- ### Install CI dependencies for Debian/Ubuntu Source: https://syntalos.org/docs/setup/install Run the CI system's dependency installation script to install all required packages on Debian or Ubuntu. The script uses APT with fsync/sync disabled for speed, which may risk data corruption during power outages; install packages manually if concerned. ```bash sudo ./tests/ci/install-deps-deb.sh ``` -------------------------------- ### runProcess() Source: https://syntalos.org/api/classes/classsyntalos_1_1mlinkmodule Starts the process. ```APIDOC ## bool runProcess() ### Description Starts the process. ### Returns bool - True if the process started successfully, false otherwise. ``` -------------------------------- ### Install Syntalos package on Debian/Ubuntu Source: https://syntalos.org/docs/setup/install Install the Syntalos .deb package using apt. Requires Debian 13 (Trixie) or later, or Ubuntu 24.04 (Noble Numbat) or later. After installation, launch Syntalos from the application menu or command line. ```bash sudo apt install ./syntalos_*.deb ``` -------------------------------- ### startWallTime() const Source: https://syntalos.org/api/classes/classsyntalos_1_1synctimer Returns the wall-clock time at which the timer was started (system_clock). Captured atomically alongside the master clock in start() / startAt(), so it can be used as an absolute reference for cross-device alignment. ```APIDOC ## startWallTime() const ### Description Returns the wall-clock time at which the timer was started (system_clock). Captured atomically alongside the master clock in start() / startAt(), so it can be used as an absolute reference for cross-device alignment. ### Method Member function ### Signature ```cpp inline std::chrono::system_clock::time_point startWallTime() const ``` ### Parameters None. ### Returns std::chrono::system_clock::time_point - The wall-clock start time. ``` -------------------------------- ### SyntalosLink constructor: enable async start and set INITIALIZING state Source: https://syntalos.org/api/files/syntaloslink_8cpp Sets the module to use asynchronous start and immediately signals initialization. A client using this interface must set the state to IDLE after setting up its basics. ```cpp // we us the fast, async start() by default d->allowAsyncStart = true; // Immediately upon creation, we send a message that we are initializing. // A client using this interface has to set this to IDLE once it has set up the basics. setState(ModuleState::INITIALIZING); } ``` -------------------------------- ### Syntalos link service request loops (UpdateInputPortMetadata, SaveSettings, LoadSettings, PrepareRun, Start) Source: https://syntalos.org/api/files/syntaloslink_8cpp Handles the main service request loops for a Syntalos link module: updating input-port metadata, saving/loading settings, preparing a run, and starting the run. Each loop receives a request, processes it, and replies with success or failure; failures log an error and break the loop. The PrepareRun handler builds a local EDL tree root and sets the module state to PREPARING then READY on success, while Start creates a SyncTimer and flushes output-port publishers. ```cpp // we will have to rebuild the waitset after ports changed d->waitSetDirty = true; } // ---- UpdateInputPortMetadata ---- while (true) { auto req = safeReceive(*d->srvUpdateIPortMetadata); if (!req.has_value()) break; const auto pl = req->payload(); const auto reqUpdateMD = UpdateInputPortMetadataRequest::fromMemory(pl.data(), pl.number_of_bytes()); // update metadata for (const auto &ip : d->inPortInfo) { if (ip->id() == reqUpdateMD.id) { ip->d->metadata = reqUpdateMD.metadata; break; } } Private::replyDoneSlice(*req, true); } // ---- SaveSettings ---- while (true) { auto req = safeReceive(*d->srvSaveSettings); if (!req.has_value()) break; const auto pl = req->payload(); const auto ssReq = SaveSettingsRequest::fromMemory(pl.data(), pl.number_of_bytes()); SaveSettingsResponse ssResp; ssResp.success = true; if (d->saveSettingsCb) ssResp.success = d->saveSettingsCb(ssResp.data, ssReq.baseDir); auto ssRespData = ssResp.toBytes(); auto maybeResponse = req->loan_slice_uninit(ssRespData.size()); if (!maybeResponse.has_value()) { SY_LOG_ERROR( logSyLink, "Failed to loan response ({} bytes) for reply to SaveSettings: {}", ssRespData.size(), iox2::bb::into(maybeResponse.error())); break; } auto rawResponse = std::move(maybeResponse).value(); std::memcpy(rawResponse.payload_mut().data(), ssRespData.data(), ssRespData.size()); iox2::send(iox2::assume_init(std::move(rawResponse))).value(); } // ---- LoadSettings ---- while (true) { auto req = safeReceive(*d->srvLoadSettings); if (!req.has_value()) break; const auto pl = req->payload(); const auto lsReq = LoadSettingsRequest::fromMemory(pl.data(), pl.number_of_bytes()); auto success = true; if (d->loadSettingsCb) success = d->loadSettingsCb(lsReq.data, lsReq.baseDir); Private::replyDoneSlice(*req, success); } // ---- PrepareRun ---- while (true) { auto req = safeReceive(*d->srvPrepareRun); if (!req.has_value()) break; const auto pl = req->payload(); const auto prepReq = PrepareRunRequest::fromMemory(pl.data(), pl.number_of_bytes()); // set up run info and build local EDL tree root d->runInfo = LinkRunInfo{}; d->runInfo.uuid = Uuid(prepReq.runUuid); d->runInfo.subject.id = prepReq.subjectId; d->runInfo.subject.group = prepReq.subjectGroup; d->runInfo.experimentId = prepReq.experimentId; d->runInfo.isEphemeral = prepReq.isEphemeral; d->runInfo.moduleName = prepReq.moduleName; if (!prepReq.edlRootPath.empty()) { const fs::path rootPath(prepReq.edlRootPath); auto rootGroup = std::make_shared(rootPath.filename().string()); rootGroup->setPath(rootPath); rootGroup->setCollectionId(prepReq.runUuid); d->runInfo.rootGroup = std::move(rootGroup); } // prepare the run setState(ModuleState::PREPARING); auto success = true; if (d->prepareRunCb) success = d->prepareRunCb(); Private::replyDoneSlice(*req, success); if (success) setState(ModuleState::READY); } // ---- Start ---- while (true) { auto req = safeReceive(*d->srvStart); if (!req.has_value()) break; const auto timePoint = symaster_timepoint(microseconds_t(req->payload().startTimestampUsec)); d->syTimer = std::make_shared(); d->syTimer->startAt(timePoint); // Ensure all output-port publishers know about subscribers that connected // during the prepare phase. for (auto &oport : d->outPortInfo) { if (oport->d->ioxPub.has_value()) oport->d->ioxPub->handleEvents(); } ``` -------------------------------- ### startStream() Source: https://syntalos.org/api/classes/classsyntalos_1_1streamoutputport Starts the data stream for this output port. ```APIDOC ## startStream() ### Description Starts the data stream for this output port. ``` -------------------------------- ### SimpleTerminal::startShell Source: https://syntalos.org/api/classes/classsyntalos_1_1simpleterminal Starts the configured shell program in the terminal. ```APIDOC ## SimpleTerminal::startShell ### Description Start the shell program. ### Signature ```cpp void startShell() ``` ``` -------------------------------- ### Set up Syntalos APT repository Source: https://syntalos.org/docs/setup/install Run this command in a terminal to add the Syntalos software source to your Debian or Ubuntu system. ```bash curl -fsSL https://syntalos.org/setup/setup-syntalos-repo.sh | sudo sh ``` -------------------------------- ### ACK message JSON example Source: https://syntalos.org/docs/network-ctl-protocol Example of an ACK message sent by a listener on the PUSH socket. The `ack_for` field indicates the phase being acknowledged (`prepare`, `start`, or `stop`). ```json { "v": 1, "type": "ack", "sender": "rig-cam", "run_id": "019312ab-...", "ack_for": "prepare", "success": true } ``` -------------------------------- ### Boilerplate Python script for Firmata control Source: https://syntalos.org/tutorials/04_firmata-interface Initial Python module code that imports syntalos_mlink, gets input/output ports (firmata-in, firmatactl-out, table-out), and defines prepare(), start(), stop() functions. This does nothing yet but sets up the structure for later automation. ```python import syntalos_mlink as syl fm_iport = syl.get_input_port('firmata-in') fm_oport = syl.get_output_port('firmatactl-out') tab_oport = syl.get_output_port('table-out') def prepare() -> bool: """This function is called before a run is started. You can use it for (slow) initializations.""" return True def start(): """This function is called immediately when a run is started. This function should complete extremely quickly.""" pass def stop(): """This function is called once a run is stopped.""" pass ``` -------------------------------- ### MLinkModule::runProcess Source: https://syntalos.org/api/files/mlinkmodule_8cpp Initializes and starts the module process, sets up the environment, and waits for the worker to signal readiness. ```cpp bool MLinkModule::runProcess() { // ensure any existing process does not exist terminateProcess(); // reset connection, just in case we changed our ID try { resetConnection(); } catch (const std::exception &e) { raiseError(QStringLiteral("Failed to set up module IPC connection: %1").arg(e.what())); return false; } if (d->proc->program().isEmpty()) { LOG_ERROR(m_log, "MLink module has not set a worker binary"); return false; } auto penv = moduleBinaryEnv(); penv.insert("SYNTALOS_VERSION", syntalosVersionFull()); penv.insert("SYNTALOS_MODULE_ID", d->clientId.c_str()); if (!d->pyVenvDir.isEmpty()) { penv.remove("PYTHONHOME"); penv.insert("VIRTUAL_ENV", d->pyVenvDir); penv.insert("PATH", QStringLiteral("%1/bin/:%2").arg(d->pyVenvDir, penv.value("PATH", ""))); } // When launching the external process, we are back at initialization // If we are in an error state, we clear it and return to IDLE after a restart. auto prevState = (state() == ModuleState::ERROR) ? ModuleState::IDLE : state(); setState(ModuleState::INITIALIZING); d->proc->setProcessEnvironment(penv); d->proc->start(d->proc->program(), d->proc->arguments()); if (!d->proc->waitForStarted()) return false; // wait for the service to show up & initialize bool workerFound = false; bool moduleInitDone = false; QElapsedTimer timer; timer.start(); do { handleIncomingControl(); if (!workerFound && state() != prevState) workerFound = true; if (state() == ModuleState::IDLE) moduleInitDone = true; if (!workerFound || !moduleInitDone) std::this_thread::sleep_for(microseconds_t(1500)); if (timer.elapsed() > 10 * 1000) break; } while (!workerFound || !moduleInitDone); if (!workerFound) { raiseError( "Module communication interface did not show up in time! The module might have crashed or may not be " "configured correctly."); d->proc->kill(); return false; } if (!moduleInitDone) { raiseError("Module initialization failed! The module might have failed or was taking too long to initialize."); d->proc->kill(); return false; } // verify IPC/API compatibility before issuing further control commands if (!testIpcApiVersion(true)) { d->proc->terminate(); return false; } if (state() != ModuleState::ERROR) setState(prevState); // send settings to the worker, if we have any (might be the case if we are trying // to resurrect a crashed worker) sendSettings(); // Keep control events flowing while the worker is alive and no module run thread is active. d->ctlEventTimer->start(); return true; } ``` -------------------------------- ### Prepare MLinkModule Process Source: https://syntalos.org/api/files/mlinkmodule_8cpp Initializes the module process, verifies communication, sets process niceness and realtime priority, and loads necessary scripts. ```cpp bool MLinkModule::prepare(const RunInfo &info) { // ensure we are reading any messages from the module process d->ctlEventTimer->start(); // at this point, ensure the module process is actually running if (!isProcessRunning()) { if (!runProcess()) return false; } // use version check as a "ping" to see if the worker is alive if (!testIpcApiVersion(false)) { raiseError("Failed to communicate with module process! Version check failed."); return false; } // set module process niceness if (!d->callClientSimple(this, SET_NICENESS_CALL_ID, [&](auto &req) { req.nice = defaultThreadNiceness(); })) return false; // set module process realtime priority, but only if the engine approved this // module for realtime scheduling within the shared RtKit budget (otherwise the // worker would self-elevate past the limit). A priority of 0 disables realtime. if (!d->callClientSimple(this, SET_MAX_RT_PRIORITY_CALL_ID, [&](auto &req) { req.priority = isRealtimeApproved() ? defaultRealtimePriority() : 0; })) return false; // send all port information to the module (sets topology / metadata on worker side) if (!sendPortInformation()) return false; // set the script to be run, if any exists and we are using a transient worker // (for persistent workers, the script has already been loaded in initialize()) // resetPorts is NOT requested here: the worker keeps the ports that sendPortInformation() // just provided so the script can call get_input_port() / get_output_port() at module level. if (d->workerMode == ModuleWorkerMode::TRANSIENT) { if (!loadCurrentScript(false)) return false; } // ensure we use the latest settings data received from the worker handleIncomingControl(); ``` -------------------------------- ### virtual bool loadSettings(const QString &confBaseDir, const QVariantHash &settings, const QByteArray &extraData) Source: https://syntalos.org/api/files/moduleapi_8h Loads module settings from the provided containers. Returns true on success. ```APIDOC ## virtual bool loadSettings(const QString &confBaseDir, const QVariantHash &settings, const QByteArray &extraData) ### Description Loads module settings from the provided containers. Returns true on success. ### Method loadSettings(const QString &confBaseDir, const QVariantHash &settings, const QByteArray &extraData) ### Parameters - **confBaseDir** (QString) - Required - The base directory for configuration files. - **settings** (QVariantHash&) - Required - The settings hash to load from. - **extraData** (QByteArray&) - Required - The extra data buffer to load from. ### Returns bool - True if loading succeeded, false otherwise. ``` -------------------------------- ### allow_async_start: bool Source: https://syntalos.org/docs/pysy-mlink-api Whether start() is run in parallel with other modules for this module. On startup, Syntalos can send START to all external modules in parallel, not waiting for them to finish individual START actions. This is the async-start method, and it results in much better aligned start times of modules. However, some modules may need to modify stream metadata in START and must ensure it is received before their streams are started. Those modules may want to disable async-start for themselves, so they run exclusively and the engine waits for them to complete START actions. ```APIDOC ## allow_async_start: bool ### Description Whether start() is run in parallel with other modules for this module. On startup, Syntalos can send START to all external modules in parallel, not waiting for them to finish individual START actions. This is the async-start method, and it results in much better aligned start times of modules. However, some modules may need to modify stream metadata in START and must ensure it is received before their streams are started. Those modules may want to disable async-start for themselves, so they run exclusively and the engine waits for them to complete START actions. ### Type - **bool** - Boolean flag. ``` -------------------------------- ### MLinkModule Run Preparation Source: https://syntalos.org/api/files/mlinkmodule_8cpp Handles the module's startup preparations, including sending a prepare run request and waiting for the module to reach the READY state. ```cpp // call the module's own startup preparations const auto sg = storageGroup(); PrepareRunRequest prepReq{ .runUuid = info.uuid, .subjectId = info.subject.id.toStdString(), .subjectGroup = info.subject.group.toStdString(), .experimentId = info.experimentId.toStdString(), .isEphemeral = info.isEphemeral, .edlRootPath = sg ? sg->path().string() : std::string{}, .moduleName = name().toStdString(), }; if (!d->callSliceClientSimple( this, PREPARE_RUN_CALL_ID, prepReq, 15, IpcCallFlag::TimeoutIsError | IpcCallFlag::SkipWaitOnError)) return false; QElapsedTimer timer; timer.start(); while (state() != ModuleState::READY) { handleIncomingControl(); qApp->processEvents(); if (state() == ModuleState::ERROR) return false; // we give modules 30sec to prepare, in case they are very slow if (timer.elapsed() > 30 * MS_PER_S) { raiseError("Timeout while waiting for module. Module did not transition to 'ready' state in 30 seconds."); return false; } } // Final drain to pick up any control events (e.g. settings changes) that arrived // concurrently with the READY state. Port changes are acknowledged synchronously // via request-response, so they are already applied by the time READY is seen. handleIncomingControl(); // register output port forwarding from exported data streams to internal data transmission if (!registerOutPortForwarders()) return false; if (state() == ModuleState::ERROR) return false; // ensure common metadata on the output ports is up-to-date updateCommonStreamMetadata(); d->portChangesAllowed = false; return true; } ``` -------------------------------- ### Build Syntalos on Debian/Ubuntu Source: https://syntalos.org/tutorials/d1_new-cpp-module Install the required dependencies for building Syntalos on Debian/Ubuntu, then create a build directory, configure with Meson, and compile with Ninja. Run these commands from the Syntalos source root. ```bash sudo apt install \ git ca-certificates \ cmake \ gettext \ libaravis-dev \ libavcodec-dev \ libavformat-dev \ libavutil-dev \ libegl-dev \ libeigen3-dev \ libglib2.0-dev \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ libkf6archive-dev \ libkf6texteditor-dev \ libkf6dbusaddons-dev \ libmimalloc-dev \ libopencv-dev \ libpipewire-0.3-dev \ libqt6opengl6-dev \ libiceoryx-introspection-dev \ libqt6svg6-dev \ libswscale-dev \ libusb-1.0-0-dev \ libv4l-dev \ libxml2-dev \ libxxhash-dev \ libsystemd-dev \ systemd-dev \ meson \ ninja-build \ qt6-base-dev \ qt6-serialport-dev \ qt6-multimedia-dev \ udev \ uuid-dev mkdir build && cd build meson setup --buildtype=debugoptimized .. ninja sudo ninja install ``` -------------------------------- ### SyncTimer::start Source: https://syntalos.org/api/files/syclock_8h Starts the timer, setting the start time to the current master clock time. ```APIDOC ## SyncTimer::start ### Description Starts the timer, setting the start time to the current master clock time. ### Method void start() noexcept ``` -------------------------------- ### Handle Requirements File and Generate Setup Script Source: https://syntalos.org/api/files/pyvenvmanager_8cpp This block processes the requirements file, creates a temporary shell script for environment initialization, and executes the creation process in a terminal. ```cpp if (requirementsFile.isEmpty()) { // we have no requirements file and want to create a blank venv, so we just create an empty dummy file QTextStream rqfOut(&tmpReqFile); rqfOut << "\n"; tmpReqFile.close(); LOG_INFO(logVEnv, "Creating empty virtualenv {}", venvName); } else { QFile reqFile(requirementsFile); if (!reqFile.open(QIODevice::ReadOnly | QIODevice::Text)) { LOG_WARNING(logVEnv, "Unable to open file {} for reading.", requirementsFile); return std::unexpected(QStringLiteral("Unable to open requirements file \"%1\": %2") .arg(requirementsFile, reqFile.errorString())); } QTextStream rqfIn(&reqFile); QTextStream rqfOut(&tmpReqFile); while (!rqfIn.atEnd()) { QString line = rqfIn.readLine(); // We can filter requirements here, but currently don't need to do so rqfOut << line << "\n"; } tmpReqFile.close(); LOG_INFO(logVEnv, "Creating virtualenv \"{}\" using requirements file: {}", venvName, requirementsFile); } LOG_INFO(logVEnv, "Creating new Python virtualenv in: {}", venvDir); QTextStream out(&shFile); out << "#!/bin/bash\n\n" << "run_check() {\n" << " echo -e \"\\033[1;33m-\\033[0m \\033[1m$@\\033[0m\"\n" << " $@\n" << " if [ $? -ne 0 ]\n" << " then\n" << " echo \"\"\n" << " read -p \"Command failed to run. Press [Enter] to exit.\"\n" << " exit 1\n" << " fi\n" << "}\n" << "export PATH=$PATH:/app/bin\n\n" << "cd " << shellQuote(venvDir) << "\n" << "run_check virtualenv ." << "\n" << "run_check source " << shellQuote(QStringLiteral("%1/bin/activate").arg(venvDir)) << "\n" << "run_check pip install -r " << shellQuote(tmpRequirementsFname) << "\n" << "echo \"\"\n" << "read -p \"Success! Press [Enter] to exit.\"" << "\n"; shFile.flush(); shFile.setPermissions(QFileDevice::ExeUser | QFileDevice::ReadUser | QFileDevice::WriteUser); shFile.close(); int ret = runInTerminal( tmpCommandFile, QStringList(), venvDir, QStringLiteral("Creating virtual Python environment")); shFile.remove(); tmpReqFile.remove(); if (ret == 0) { injectSyntalosMlinkIntoVenv(venvDir); if (!requirementsFile.isEmpty()) { const auto b3sumResult = blake3HashForFile(requirementsFile); if (!b3sumResult.has_value()) return std::unexpected( QStringLiteral("Unable to compute requirements checksum: %1").arg(b3sumResult.error())); if (!writeRequirementsHashMetadata(venvDir, b3sumResult.value())) LOG_WARNING(logVEnv, "Unable to persist requirements metadata for virtualenv: {}", venvName); } return venvDir; } // failure, let's try to remove the bad virtualenv (failures to do so are ignored) QDir(venvDir).removeRecursively(); return std::unexpected( QStringLiteral("Environment creation failed - refer to the terminal log for more information.")); } ``` -------------------------------- ### prepare() Source: https://syntalos.org/api/classes/classsyntalos_1_1mlinkmodule Prepares for an experiment run. ```APIDOC ## virtual bool prepare(const RunInfo &info) override ### Description Prepares for an experiment run. ### Parameters - **info** (const RunInfo&) - Required - Run information. ### Returns bool - True if preparation succeeded, false otherwise. ``` -------------------------------- ### Open File for Writing Source: https://syntalos.org/api/files/tsyncfile_8cpp Opens the file, initializes the header with metadata, and performs 8-byte alignment. ```cpp bool TimeSyncFileWriter::open(const std::string &modName, const Uuid &collectionId, const MetaStringMap &userData) { if (m_file.is_open()) m_file.close(); m_writeFailed = false; m_lastError.clear(); errno = 0; m_file.open(m_fname, std::ios::binary | std::ios::out | std::ios::trunc); if (!m_file) { const auto err = errno; m_lastError = std::format( "Unable to open file '{}' for writing: {}", m_fname, err != 0 ? std::error_code(err, std::generic_category()).message() : std::string("Unknown error")); return false; } // ensure block size is not extremely small if (m_blockSize >= 0 && m_blockSize < 128) m_blockSize = 128; m_bIndex = 0; XXH3_64bits_reset(m_xxh3State); // serialize userData to JSON std::ostringstream jsonBuf; writeJsonObject(jsonBuf, userData); const auto userDataJson = jsonBuf.str(); // compute creation time const auto creationTimeSecs = std::chrono::duration_cast( (m_creationTimeOverride.value_or(std::chrono::system_clock::now())) .time_since_epoch()) .count(); m_creationTimeOverride.reset(); // write file header - magic number raw (not checksummed) writeRawLE(TSYNC_FILE_MAGIC); csWriteValue(TSYNC_FILE_VERSION_MAJOR); csWriteValue(TSYNC_FILE_VERSION_MINOR); csWriteValue(static_cast(creationTimeSecs)); csWriteValue(modName); csWriteValue(collectionId.toHex()); // UUID as hex string csWriteValue(userDataJson); csWriteValue(static_cast(m_tsMode)); csWriteValue(m_blockSize); csWriteValue(m_timeNames.first); csWriteValue(static_cast(m_timeUnits.first)); csWriteValue(static_cast(m_time1DType)); csWriteValue(m_timeNames.second); csWriteValue(static_cast(m_timeUnits.second)); csWriteValue(static_cast(m_time2DType)); m_file.flush(); if (!checkStreamState("Writing header")) return false; const auto headerBytes = static_cast(m_file.tellp()); if (headerBytes <= 0) { m_writeFailed = true; m_lastError = std::format("Could not determine amount of bytes written for header of '{}'.", m_fname); return false; } const int padding = (headerBytes * -1) & (8 - 1); // 8-byte align header for (int i = 0; i < padding; i++) csWriteValue(0); // write end of header and header CRC-32 writeBlockTerminator(false); m_file.flush(); return checkStreamState("Writing header"); } ``` -------------------------------- ### Install Syntalos via Flatpak and allow serial device access Source: https://syntalos.org/get Install Syntalos via Flatpak from Flathub and grant the current user access to serial devices. Requires Flatpak and Flathub to be configured. The Flatpak command installs the org.syntalos.syntalos application. ```bash flatpak install flathub org.syntalos.syntalos ``` ```bash sudo usermod -a -G dialout $USER ``` -------------------------------- ### SecondaryClockSynchronizer.start Source: https://syntalos.org/docs/pysy-mlink-api Initialize and begin synchronization. Returns `True` on success. ```APIDOC ## SecondaryClockSynchronizer.start ### Description Initialize and begin synchronization. Returns `True` on success. ### Method Signature `start() -> bool` ### Parameters None ### Returns - **bool** - `True` on success. ``` -------------------------------- ### Install C++ toolchain on Debian-based systems Source: https://syntalos.org/docs/modules/cpp-workbench Command to install the necessary compiler and build tools required for the C++ Workbench module on Debian-based Linux distributions. ```bash sudo apt install gcc g++ pkgconf meson ninja-build ``` -------------------------------- ### AbstractModule window show and position with QMetaObject::invokeMethod Source: https://syntalos.org/api/files/moduleapi_8cpp Shows how AbstractModule positions and shows a window via QMetaObject::invokeMethod with a queued connection. Handles Wayland (using Winpos::WindowPositioner) and X11 fallback, centering vertically on the cursor on first show and restoring the saved position on re-show. Also raises and activates the window, noting Wayland and X11 behavior. ```cpp QMetaObject::invokeMethod( w, [w, needsInitialPos, cursorPos]() { w->show(); #ifdef SY_PREFER_WAYLAND auto wnd = w->windowHandle(); auto posHelper = wnd ? wnd->findChild() : nullptr; if (wnd && !posHelper) posHelper = new Winpos::WindowPositioner(wnd, wnd); if (posHelper) { if (needsInitialPos) { // First show: center vertically on the cursor. // cursorPos is zone-relative (screen origin subtracted above). // height() is valid here; show() has triggered the layout pass. auto pos = cursorPos; pos.setY(pos.y() - (w->height() / 2)); posHelper->move(pos); } else { // Re-show: restore the last compositor-confirmed position. // On Wayland the surface is destroyed on hide() and recreated // on show(), so the compositor would otherwise reassign a new // default placement. Calling move() here queues the saved // position as pendingPosition; WindowPositioner applies it in // initializeWayland() once the surface role is created. posHelper->move(posHelper->position()); } } #else if (needsInitialPos) { // First show only: on X11 without Winpos, Qt retains the widget // position across hide()/show() cycles so we only need to set it // the very first time. cursorPos is in global screen coordinates. auto pos = cursorPos; pos.setY(pos.y() - (w->height() / 2)); w->move(pos); } #endif // raise() is a no-op on Wayland, but activate does bring // the window to the front if it was hidden on most Wayland // compositors... On X11, raise() is enough, but activating // the window too does not hurt and is what the user expects. w->raise(); w->activateWindow(); }, Qt::QueuedConnection); } } ``` -------------------------------- ### showSettingsUi() Source: https://syntalos.org/api/classes/classsyntalos_1_1mlinkmodule Shows the configuration UI of this module. ```APIDOC ## virtual void showSettingsUi() override ### Description Shows the configuration UI of this module. ``` -------------------------------- ### FUNC_DONE_TIMESTAMP macro definition Source: https://syntalos.org/api/files/syclock_8h Computes a timestamp for when a function completed, not the average between start and end time. Takes a starting timepoint and returns the time since that point in microseconds. ```cpp #define FUNC_DONE_TIMESTAMP( INIT_TIME, F ) ({ \ F; \ std::chrono::duration_cast(symaster_clock::now() - (INIT_TIME)); \ }) ``` -------------------------------- ### mainWinState / setMainWinState Source: https://syntalos.org/api/classes/classsyntalos_1_1globalconfig Gets or sets the main window state (e.g., toolbar/dock layout) as a QByteArray. ```APIDOC ## QByteArray mainWinState() const ### Description Returns the main window state. ### Returns - **QByteArray** - The state data. ## void setMainWinState(const QByteArray &state) ### Description Sets the main window state. ### Parameters - **state** (const QByteArray&) - Required - The state data. ### Returns - void ``` -------------------------------- ### ACK failure JSON example Source: https://syntalos.org/docs/network-ctl-protocol Example of an ACK message on failure, adding an `"error"` field with a human-readable error message. The `success` field is `false`. ```json { ... "success": false, "error": "Module 'camera' failed to initialize" } ```