PythonProcess

PythonProcess owns the child Python interpreter and the JSON-lines IPC channel to the user driver script behind every Python hardware trampoline. The Python heap stays in a separate process, so a script crash cannot take down the Qt application.

There are two directions of traffic. C++ → Python uses the synchronous sendRequest() API, which writes a JSON method call on the subprocess’s stdin and runs a nested QEventLoop until the matching response arrives. Python → C++ uses three channels: relay requests that service self.comm and self.settings against the trampoline’s CommunicationProtocol and SettingsStorage; log lines forwarded to the global Blackchirp log via bcLog(); and base64-encoded waveform pushes emitted on the waveformReceived signal for digitizer trampolines to drain into the WaveformBuffer.

The standard self.comm, self.settings, and self.log proxies are injected on every subprocess start. Optional, hardware-type-specific proxies — currently self.digi for digitizer push — are gated by setEnabledProxies(): trampolines that need them call it between initPythonProcess() and the first sendRequest().

For the trampoline contract and the user-side script API, see Overview and its sub-pages; the contributor-level architecture of the IPC host and the proxy system is covered in Python Hardware.

API Reference

class PythonProcess : public QObject

QProcess wrapper that owns a Python hardware subprocess and the JSON-lines IPC channel between Blackchirp and the user driver script.

Launches the IPC host script (python_hw_host.py) under the configured Python interpreter, hands it the user’s driver script and class name, and exposes a synchronous request/response API (sendRequest) on top of an asynchronous, line-oriented JSON protocol carried on the subprocess’s stdin and stdout. The Python heap stays in a separate process, so a script crash cannot corrupt the Qt application.

Reentrancy: sendRequest() writes its request and then runs a nested QEventLoop until either a matching response arrives, the subprocess dies, or the configured timeout fires. Because the loop continues to process events, relay requests, log messages, and waveform pushes are handled correctly while a method call is in flight.

Public Types

using SettingsGetter = std::function<QVariant(const QString &key, const QVariant &defaultVal)>

Signature for the SettingsStorage::get bridge installed by setSettingsCallbacks().

using SettingsSetter = std::function<void(const QString &key, const QVariant &val)>

Signature for the SettingsStorage::set bridge installed by setSettingsCallbacks().

Public Functions

explicit PythonProcess(QObject *parent = nullptr)
~PythonProcess()
bool start(const QString &pythonExe, const QString &hostScriptPath, const QString &userScriptPath, const QString &className)

Launch the Python subprocess and run the startup handshake.

Starts pythonExe with hostScriptPath as its argument, sends the initial _init message (carrying the hardware key, model, the user script path, the class name, and the enabled proxy list configured via setEnabledProxies), then sends initialize so the user driver can populate its own state. Returns once both handshake steps have completed successfully.

Parameters:
  • pythonExe – Python executable path (e.g., "/path/to/venv/bin/python3" or "python3")

  • hostScriptPath – Absolute path to python_hw_host.py

  • userScriptPath – Absolute path to the user’s driver script

  • className – Name of the Python class to instantiate inside the user script

Returns:

true if the process started and both handshake steps succeeded; false otherwise (with a processError signal emitted explaining the failure).

void stop()

Stop the Python subprocess if it is running. Idempotent.

bool isRunning() const

True when the subprocess is alive and accepting requests.

QJsonObject sendRequest(const QJsonObject &request)

Send a method call to the user driver and wait for its reply.

Writes request as a JSON line on the subprocess’s stdin and runs a nested QEventLoop until a response with the matching id arrives, the subprocess exits, or timeoutMs() elapses. Relay requests, log messages, and waveform pushes received while waiting are dispatched to their handlers without disturbing the pending response.

Parameters:

request – JSON object with at minimum a "method" key; additional keys are forwarded as keyword arguments to the Python method.

Returns:

Response JSON object containing either "result" on success or "error" plus "traceback" on failure.

void setComm(CommunicationProtocol *comm)

Bind the CommunicationProtocol used to service self.comm relay requests from the Python script.

The pointer is borrowed; PythonProcess does not take ownership. Trampolines call this every time their p_comm is reassigned.

void setSettingsCallbacks(SettingsGetter getter, SettingsSetter setter)

Bind the SettingsStorage bridge for self.settings relay.

The host script’s SettingsProxy turns into calls against getter and setter, which the trampoline typically wires to its own SettingsStorage::get and SettingsStorage::set helpers (the latter is protected, so the bridge is what lets the user script update persistent settings without a friend declaration).

void setHardwareInfo(const QString &key, const QString &model)

Set the hardware key and model strings forwarded to Python in the _init message.

The Python side exposes these as self.settings.key and self.settings.model.

inline int timeoutMs() const

Current sendRequest() timeout in milliseconds.

inline void setTimeoutMs(int ms)

Set the sendRequest() timeout in milliseconds.

inline void setEnabledProxies(const QStringList &proxies)

Select which optional Python proxies are injected on the next subprocess start.

The three standard proxies (comm, settings, log) are always injected. Optional proxies are hardware-type-specific push channels, currently "digi" for digitizer waveform push. Names not registered in the host script’s factory map are ignored. Must be called before start(); typically invoked from the trampoline’s initialize override, immediately after initPythonProcess().

Signals

void waveformReceived(const QByteArray &data, quint64 shotCount)

Emitted when the Python script pushes a waveform to C++.

Parameters:
  • data – Raw shot bytes after base64 decode, in the format configured for the active digitizer (record length × bytes per point × number of records).

  • shotCount – Number of shots represented by data (1 for single-shot, N for pre-accumulated).

void processError(const QString &errorString)

Emitted with a human-readable description when the subprocess fails to start, exits unexpectedly, or returns a malformed reply.

void responseReady()

Internal: wakes the nested QEventLoop in sendRequest() once a matching response has been parsed. Not intended for external consumers.

Private Functions

QJsonObject handleRelayRequest(const QJsonObject &relayReq)
LogHandler::MessageCode parseLogLevel(const QString &level) const
void writeLine(const QJsonObject &obj)

Private Members

QProcess *p_process = {nullptr}
CommunicationProtocol *p_comm = {nullptr}
SettingsGetter d_settingsGetter
SettingsSetter d_settingsSetter
QString d_hwKey
QString d_hwModel
int d_timeoutMs = {30000}
int d_nextId = {1}
QStringList d_enabledProxies
QByteArray d_readBuf
bool d_waitingForResponse = {false}
int d_expectedId = {-1}
QJsonObject d_pendingResponse

Private Slots

void onReadyRead()
void handleStderr()