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 nestedQEventLoopuntil 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.See also
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
_initmessage (carrying the hardware key, model, the user script path, the class name, and the enabled proxy list configured via setEnabledProxies), then sendsinitializeso 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.pyuserScriptPath – Absolute path to the user’s driver script
className – Name of the Python class to instantiate inside the user script
- Returns:
trueif the process started and both handshake steps succeeded;falseotherwise (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.commrelay requests from the Python script.The pointer is borrowed; PythonProcess does not take ownership. Trampolines call this every time their
p_commis reassigned.
-
void setSettingsCallbacks(SettingsGetter getter, SettingsSetter setter)
Bind the SettingsStorage bridge for
self.settingsrelay.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
_initmessage.The Python side exposes these as
self.settings.keyandself.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 (
1for single-shot,Nfor 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
-
using SettingsGetter = std::function<QVariant(const QString &key, const QVariant &defaultVal)>