Watching Scene Events

SceneEventService.WatchSceneEvents is a server-streaming RPC and the only change-notification channel for the scene. It sends one SceneEvent for every committed change, in order. Use it to keep a GUI or a cache in sync with the server.

Events

SceneEvent

Field

Meaning

change

CHANGE_ADDED, CHANGE_UPDATED, CHANGE_REMOVED, CHANGE_RENAMED, or CHANGE_REFRESHED (bulk change: resynchronize everything).

origin

ORIGIN_CONFIG: a saved config changed (persisted). ORIGIN_FRAME: a frame moved in the transform layer only, such as an unsaved drag.

entity

Config type and name. Set only when origin == ORIGIN_CONFIG.

frames

Names of frames created or moved. May be empty for a config event whose geometry was already current; that is normal.

new_name

For CHANGE_RENAMED. entity.name and frames carry the old name.

WatchSceneEventsRequest can filter by origin_filter (ORIGIN_UNKNOWN streams all) and type_filter (CONFIG_TYPE_ALL streams all; applies to config events). An empty request streams everything.

Python

flexiv.pdk.watch offers two ways to consume a stream. Both take an open_stream callable that opens the RPC, and call it again to reconnect automatically with exponential backoff if the stream drops. A stream the server ends, including with CANCELLED, is reopened too; only close(), stop_all() or leaving the loop stops it.

Subscriber + callback

Runs the stream on a background thread and calls your callback for each event. Best for GUIs and long-lived subscriptions. Call close() on the returned Subscription, or stop_all() on the Subscriber, to stop. Both also work as context managers.

watch() generator

A blocking generator you iterate yourself. Best for scripts and tests. Breaking out of the loop cancels the call.

def describe(event) -> str:
    entity = (f"{ConfigType.Name(event.entity.type)}:{event.entity.name!r}"
              if event.HasField("entity") else "-")
    return (
        f"change={SceneEvent.Change.Name(event.change)} "
        f"origin={SceneEvent.Origin.Name(event.origin)} "
        f"entity={entity} frames={list(event.frames)}"
    )


def main():
    target = os.environ.get("PDK_SERVER_ADDR", "localhost:50052")
    print(f"Connecting to the PDK server at {target}...")

    with Client(target) as client:
        request = WatchSceneEventsRequest()  # empty filters => all origins/types

        # ---- Option 1: background Subscriber with a callback ----------------
        print("Watching scene events on a background thread for 3 seconds...")
        subscriber = Subscriber()
        subscription = subscriber.subscribe(
            lambda: client.scene_event.stub.WatchSceneEvents(request),
            lambda event: print(f"  [callback] {describe(event)}"),
        )
        try:
            time.sleep(3.0)  # do other work; events arrive on the worker thread
        finally:
            subscription.close()  # stops the stream and joins the worker
        print("Stopped background subscription.")

        # ---- Option 2: blocking generator -----------------------------------
        print("Watching scene events synchronously (Ctrl-C to stop)...")
        try:
            for event in watch(lambda: client.scene_event.stub.WatchSceneEvents(request)):
                print(f"  [generator] {describe(event)}")
        except KeyboardInterrupt:
            print("Interrupted; the underlying stream is cancelled on exit.")
        except grpc.RpcError as error:
            print(f"WatchSceneEvents failed or no server running: {error}")


Reconnection and threading are controlled by Options:

from flexiv.pdk.watch import Options

options = Options(
    executor=None,           # callable(fn) that runs fn on the thread you want
    reconnect=True,
    backoff_initial_s=0.5,
    backoff_max_s=10.0,
    backoff_factor=2.0,
    max_retries=-1,          # -1: retry forever
)
subscription = subscriber.subscribe(open_stream, on_event, options)

An executor lets you deliver events on a GUI thread. For example, with Qt: executor=lambda fn: QTimer.singleShot(0, fn). watch() is single-threaded and ignores executor.

Exceptions raised in your callback are logged (logger flexiv.pdk.watch) and the stream continues.

C++

flexiv::pdk::watch::Subscriber (<flexiv/pdk/watch/subscriber.hpp>):

#include <flexiv/pdk/client.hpp>
#include <flexiv/pdk/watch/subscriber.hpp>

using flexiv::pdk::SceneEvent;
using flexiv::pdk::WatchSceneEventsRequest;

flexiv::pdk::watch::Subscriber subscriber;

flexiv::pdk::watch::Subscription sub = subscriber.Subscribe<SceneEvent>(
    // Open the stream; called again on each reconnect.
    [&client](grpc::ClientContext* ctx) {
      WatchSceneEventsRequest req;
      return client.scene_event().stub->WatchSceneEvents(ctx, req);
    },
    // Handle each event.
    [](const SceneEvent& event) {
      std::cout << event.ShortDebugString() << std::endl;
    });

// Events arrive while `sub` is alive. sub.reset() stops the stream and
// waits until delivery has finished.
  • Subscription is move-only. Discarding the return value ends the watch immediately.

  • Options has the same fields as in Python, except that the backoff fields are backoff_initial and backoff_max (std::chrono::milliseconds) and executor is a std::shared_ptr<Executor>.

  • Implement watch::Executor::Post(std::function<void()>) to forward events to your UI thread. watch::InlineExecutor runs them directly.

  • Subscriber::StopAll() stops every subscription; the destructor does too.

  • Do not let exceptions escape the C++ callback.

Command line

pdk-scene watch     # prints events until Ctrl-C