Skip to content

Fix PTZ action commands getting ignored - #322

Closed
glikely wants to merge 2 commits into
mainfrom
fix-ptz-action-thread
Closed

Fix PTZ action commands getting ignored#322
glikely wants to merge 2 commits into
mainfrom
fix-ptz-action-thread

Conversation

@glikely

@glikely glikely commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Switching to the prochandler API broke the action source because the proc handler lambdas were ignoring all calls from threads other than the GUI thread. This was done because the calldata structure usually lives on the callers stack, and if a call was queued on the correct thread, e.g. by calling QMetaObject::invokeMethod(), then the calldata would be gone by the time the method ran. However, the action source triggers don't come from the UI thread.

To fix this, rework the proc handlers to extract the calldata arguments in the callers thread, and then use invokeMethod() to call the device method, using the default Qt::AutoConnection mode so that calls from the GUI thread are direct, but calls from other threads get queued. For most methods this works fine because the calls are commands to the device.

The one exception is the ::get() method which needs to return data to the caller. This is the one proc handler method that still needs to be called by the GUI thread, and it gets ignored if it is not. This perhaps could be fixed by using a BlockingQueued connection, but it is unclear if it is needed and that change can be done in another patch.

@glikely
glikely force-pushed the fix-ptz-action-thread branch from 5516761 to 4992717 Compare June 19, 2026 12:11
glikely added 2 commits June 19, 2026 16:00
Signed-off-by: Grant Likely <grant.likely@secretlab.ca>
Switching to the prochandler API broke the action source because the
proc handler lambdas were ignoring all calls from threads other than the
GUI thread. This was done because the calldata structure usually lives
on the callers stack, and if a call was queued on the correct thread,
e.g. by calling QMetaObject::invokeMethod(), then the calldata would be
gone by the time the method ran. However, the action source triggers
don't come from the UI thread.

To fix this, rework the proc handlers to extract the calldata arguments
in the callers thread, and then use invokeMethod() to call the device
method, using the default Qt::AutoConnection mode so that calls from the
GUI thread are direct, but calls from other threads get queued. For most
methods this works fine because the calls are commands to the device.

The one exception is the ::get() method which needs to return data to
the caller. This is the one proc handler method that still needs to be
called by the GUI thread, and it gets ignored if it is not. This perhaps
could be fixed by using a BlockingQueued connection, but it is unclear
if it is needed and that change can be done in another patch.

Fixes #321
Signed-off-by: Grant Likely <grant.likely@secretlab.ca>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses PTZ action/proc_handler commands being ignored when invoked from non-GUI threads by extracting calldata_t arguments on the caller thread and then dispatching device operations onto the device’s Qt thread via QMetaObject::invokeMethod(Qt::AutoConnection).

Changes:

  • Removed the “GUI-thread-only” proc_handler restriction and updated PTZDevice proc handlers to queue device actions safely after copying calldata_t inputs.
  • Made PTZDevice movement/preset/set handlers invoke device slots via QMetaObject::invokeMethod() to support calls originating from non-UI threads.
  • Added thread-guard logging for get() (still requiring same-thread execution) and updated a few tuple accessors to std::get.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/settings.cpp Uses std::get for tuple access in hotkey enumeration callbacks.
src/ptz-controls.cpp Uses std::get for tuple access in hotkey lookup callback.
src/ptz-device.hpp Promotes movement APIs to Qt slots and updates calldata wrappers to queue work via invokeMethod().
src/ptz-device.cpp Removes GUI-thread rejection in proc handler lambda; queues movement/preset/set operations via invokeMethod(); keeps get() thread-restricted.
src/ptz-visca.cpp Adds thread guards to VISCA get()/set() (but set() currently reintroduces cross-thread ignoring).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ptz-device.cpp
Comment on lines 31 to 35
#define ptz_ph_lambda(_method) [](void *p, calldata_t *cd) \
{ \
auto ptz = static_cast<PTZDevice *>(p); \
if (!ptz) { \
blog(LOG_ERROR, "PTZ proc_handler called without PTZDevice pointer"); \
Comment thread src/ptz-visca.cpp
Comment on lines +796 to +799
if (QThread::currentThread() != thread()) {
ptz_log(LOG_ERROR, "PTZVisca::set(calldata) called from non-GUI thread; ignored");
return;
}
Comment thread src/ptz-device.cpp
void PTZDevice::get(calldata_t *cd) const
{
if (QThread::currentThread() != thread()) {
ptz_log(LOG_ERROR, "PTZDevice::get(calldata) called from non-GUI thread; ignored");
Comment thread src/ptz-visca.cpp
void PTZVisca::get(calldata_t *cd) const
{
if (QThread::currentThread() != thread()) {
ptz_log(LOG_ERROR, "PTZVisca::get(calldata) called from non-GUI thread; ignored");
@EdueskaWeiz

Copy link
Copy Markdown
Contributor

Reviewed this against the Copilot notes; two of them look worth acting on before merge.

1. PTZVisca::set() still drops cross-thread calls. PTZDevice::set() now extracts the calldata values and queues the call, but the VISCA override added in this PR keeps the old early return, so power_on, wb_mode and wb_onepush_trigger are silently ignored for exactly the callers this PR is meant to fix. Same pattern as the base class works here — pull the values out on the caller thread and queue the send():

 void PTZVisca::set(calldata_t *cd)
 {
-	if (QThread::currentThread() != thread()) {
-		ptz_log(LOG_ERROR, "PTZVisca::set(calldata) called from non-GUI thread; ignored");
-		return;
-	}
 	bool power_on;
 	if (calldata_get_bool(cd, "power_on", &power_on))
-		send(VISCA_CAM_Power, {power_on});
+		QMetaObject::invokeMethod(this, [this, power_on]() { send(VISCA_CAM_Power, {power_on}); });
 
 	long long wb_mode;
 	if (calldata_get_int(cd, "wb_mode", &wb_mode))
-		send(VISCA_CAM_WB_Mode, {(int)wb_mode});
+		QMetaObject::invokeMethod(this, [this, wb_mode]() { send(VISCA_CAM_WB_Mode, {(int)wb_mode}); });
 
 	bool trigger;
 	if (calldata_get_bool(cd, "wb_onepush_trigger", &trigger) && trigger)
-		send(VISCA_CAM_WB_OnePushTrigger);
+		QMetaObject::invokeMethod(this, [this]() { send(VISCA_CAM_WB_OnePushTrigger); });
 
 	PTZDevice::set(cd);
 }

The functor overload of invokeMethod() takes the context object, so it queues onto the device thread the same way the Q_ARG calls do, and the captured values are copies so the transient calldata_t* is not touched afterwards.

2. The block comment above ptz_ph_lambda now describes the old rule. It still says the proc_handler must be called from the GUI thread and that queueing with QMetaObject::invokeMethod() would leave calldata stale — which is exactly what this PR now does, after copying the values out. Worth rewording so the next reader does not reintroduce the guard.

The two low-severity notes are fair as well: the remaining checks compare against QObject::thread(), so "non-device thread" reads more accurately than "non-GUI thread" in the PTZDevice::get() and PTZVisca::get() messages.

@glikely glikely closed this Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants