Skip to content

Commit 24f7598

Browse files
committed
Pydsr agents did not connect to G
A clean pydsr agent joining a live fleet matched every DDS participant, received the full-graph answer and reported "Synchronized" — then stayed empty forever (get_nodes() -> []). Live deltas were equally dead. dsr_api.cpp marshals every remote apply — the initial import_full_graph and the node/edge/node-attr/edge-attr delta batches — with QMetaObject::invokeMethod(this, ..., Qt::QueuedConnection). A queued invoke only runs when the target object's thread spins a Qt event loop. The wrapper never created one, so the applies were posted and never executed. The DDS and CRDT layers were healthy the whole time; only the delivery hop was missing. pydsr now owns a GraphEventPump: a dedicated QThread running an event loop. The DSRGraph is both constructed and destroyed on it (BlockingQueuedConnection), so its Qt affinity is that thread for its whole lifetime and no posted event is ever delivered to a thread with no loop or one already gone. The holder becomes std::unique_ptr<DSRGraph, GraphOnPumpThreadDeleter> so teardown is routed there too, with the GIL released. A QThread only dispatches posted events if a QCoreApplication instance exists — without one invokeMethod silently no-ops. ensure_qapp() creates one lazily (at first DSRGraph construction, not module import) and only when none exists, so a PySide/PyQt host's own QApplication is reused instead of tripping Qt's "instance already exists" fatal. Signals were never affected: pydsr builds with SignalMode::Queue, whose QueuedSignalRunner dispatches on its own ThreadPool. Wrapper-only change; api/ is untouched, so C++ agent behaviour is unchanged. Verified against a live 4-agent fleet: plain script with no user-side Qt syncs 0 -> 47 nodes; deltas alive (~35 node-attr signals/s); explicit del is clean; 3x construct/destroy cycles clean; PySide6 coexistence reuses the host app. Known nit: the constructor blocks the pump's event loop while it waits for the full-graph answer, so the graph populates immediately after the ctor returns rather than during it.
1 parent 9f76a6e commit 24f7598

8 files changed

Lines changed: 451 additions & 97 deletions

File tree

gui/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ set(headers_to_moc
3333
include/dsr/gui/viewers/graph_viewer/graph_node_laser_widget.h
3434
include/dsr/gui/viewers/graph_viewer/graph_node_rgbd_widget.h
3535
include/dsr/gui/viewers/graph_viewer/graph_node_person_widget.h
36+
include/dsr/gui/viewers/graph_viewer/graph_node_mind_widget.h
3637
include/dsr/gui/viewers/graph_viewer/graph_node_widget.h
3738
include/dsr/gui/viewers/tree_viewer/tree_viewer.h
3839
include/dsr/gui/viewers/_abstract_graphic_view.h
@@ -84,6 +85,21 @@ set_target_properties(dsr_gui
8485

8586
target_compile_options(dsr_gui PUBLIC -O3 -g -fmax-errors=5 -std=c++20 -fno-char8_t )
8687

88+
# Fast DDS Statistics consumer for the mind view (opt-in, dev/debug only). OFF by default so normal
89+
# and DEPLOYMENT builds contain ZERO statistics code. eProsima installs the statistics *type* headers
90+
# only as .idl, so enabling this also needs a Fast-DDS SOURCE tree to include the generated types from:
91+
# cmake -B build -DRC_DDS_STATS=ON -DRC_DDS_STATS_SRC=/path/to/Fast-DDS/src/cpp
92+
# At runtime it is still inert unless the env var RC_DDS_STATS=1 is set (a second, no-rebuild gate).
93+
option(RC_DDS_STATS "Build the Fast DDS Statistics consumer into the mind view (dev only)" OFF)
94+
if(RC_DDS_STATS)
95+
if(NOT RC_DDS_STATS_SRC OR NOT EXISTS "${RC_DDS_STATS_SRC}/statistics/types/typesPubSubTypes.hpp")
96+
message(FATAL_ERROR "RC_DDS_STATS=ON requires -DRC_DDS_STATS_SRC=<Fast-DDS>/src/cpp (statistics type headers)")
97+
endif()
98+
message(STATUS "DDS Statistics consumer ENABLED for dsr_gui (types from ${RC_DDS_STATS_SRC})")
99+
target_compile_definitions(dsr_gui PRIVATE RC_DDS_STATS=1)
100+
target_include_directories(dsr_gui PRIVATE $<BUILD_INTERFACE:${RC_DDS_STATS_SRC}>)
101+
endif()
102+
87103
# AddressSanitizer (opt-in): cmake -B build-asan -DENABLE_ASAN=ON
88104
# Only needed for redzone-precise detection INSIDE the graph viewer. Do NOT `make install`
89105
# an ASan build to /usr/local/lib — it would break every non-ASan agent. Build it locally and

gui/include/dsr/gui/viewers/graph_viewer/graph_colors.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ class GraphColors
2929
}
3030
if(colors.count(key)>0)
3131
return colors[key];
32-
else
33-
return default_color;
32+
// An unlisted NODE type gets a stable, distinct colour derived from its name instead of one
33+
// flat default — otherwise every new type in the system renders identically and the graph
34+
// stops carrying information (see stable_fallback_color in node_colors.h). Edges keep their
35+
// single default: they are thin lines where a colour-per-type would be noise, not signal.
36+
if constexpr(std::is_same_v<Ta, DSR::Node>)
37+
return stable_fallback_color(key);
38+
return default_color;
3439
};
3540
};
3641

gui/include/dsr/gui/viewers/graph_viewer/graph_node.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ using namespace DSR;
4242

4343
class GraphEdge;
4444
class QGraphicsSceneMouseEvent;
45+
// Small "+"/"-" child item drawn beside a node that has collapsible children. Defined in
46+
// graph_node.cpp — it only needs to handle its own clicks.
47+
class CollapseBadge;
4548

4649

4750
class GraphNode : public QObject, public QGraphicsEllipseItem
@@ -50,7 +53,7 @@ Q_OBJECT
5053
Q_PROPERTY(QColor node_color READ _node_color WRITE set_node_color)
5154
private:
5255
QPointF newPos;
53-
QGraphicsSimpleTextItem *tag;
56+
QGraphicsSimpleTextItem *tag = nullptr; // created on first setTag(), reused after (see setTag)
5457
std::string type;
5558
std::shared_ptr<DSR::GraphViewer> graph_viewer;
5659
QBrush node_brush;
@@ -60,6 +63,7 @@ Q_OBJECT
6063
std::unique_ptr<QWidget> node_widget;
6164
std::set<std::string> cached_edge_types;
6265
QPoint press_screen_pos_; // where the last left press started (click-vs-drag discrimination)
66+
CollapseBadge *collapse_badge = nullptr; // created on first set_collapse_indicator()
6367

6468
public:
6569
static constexpr int DEFAULT_DIAMETER = 20;
@@ -86,6 +90,9 @@ Q_OBJECT
8690
std::string getColor() const { return plain_color.toStdString(); };
8791
void setType(const std::string &type_);
8892
std::string getType() const { return type;};
93+
// Shows/hides the "+"/"-" collapse handle beside the node. The node holds no policy: what
94+
// "collapsed" hides is decided by GraphViewer, which owns the parent/child index.
95+
void set_collapse_indicator(bool has_collapsible_children, bool collapsed);
8996
QRectF boundingRect() const override;
9097
QPainterPath shape() const override;
9198
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
@@ -118,6 +125,8 @@ public slots:
118125
// stored inline in the graph. The agent, which owns the media-plane (DDS)
119126
// subscriber, connects to this (QueuedConnection) and opens its own viewer.
120127
void view_data_signal(uint64_t id, const std::string &type);
128+
// The user clicked the "+"/"-" badge. GraphViewer decides which children to hide/show.
129+
void toggle_children_signal(uint64_t id);
121130

122131
private:
123132
// True if the node still holds its type's raw payload as a graph attribute.

gui/include/dsr/gui/viewers/graph_viewer/graph_viewer.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ namespace DSR
5555
void del_edge_SLOT(std::uint64_t from, std::uint64_t to, const std::string &edge_tag);
5656
void del_node_SLOT(uint64_t id); // remove node from visual graph
5757
void hide_show_node_SLOT(uint64_t id, bool visible);
58+
// Collapse/expand the RT subtree hanging from a node (the "+"/"-" badge)
59+
void toggle_children_SLOT(uint64_t parent_id);
5860
// Others
5961
void toggle_animation(bool state);
6062
void compute_layout(const char * alg = "dot");
@@ -79,6 +81,22 @@ namespace DSR
7981
QGraphicsEllipseItem *central_point;
8082
QMenu *contextMenu, *showMenu;
8183
std::map<std::string,std::set<std::uint64_t>> type_id_map;
84+
85+
// ---- collapsible children (any RT subtree folded away from its parent) ----
86+
// Edge type that defines parenthood in DSR (see dsr_rt_api.cpp, which also maintains
87+
// parent_att/level_att). Any node with at least one RT child gets a "+"/"-" badge.
88+
static constexpr const char *PARENT_EDGE_TYPE = "RT";
89+
std::map<std::uint64_t, std::set<std::uint64_t>> collapsible_children; // parent -> RT children
90+
std::set<std::uint64_t> collapsed_parents; // parents currently folded (purely visual)
91+
// Keeps `collapsible_children` in sync with the RT edges arriving/leaving from G
92+
void note_parent_edge(std::uint64_t from, std::uint64_t to, const std::string &edge_tag, bool added);
93+
// All descendants of `root` reachable through RT edges, root NOT included. With
94+
// stop_at_collapsed the walk does not descend past a node the user folded on its own.
95+
std::vector<std::uint64_t> subtree_ids(std::uint64_t root, bool stop_at_collapsed) const;
96+
void refresh_collapse_badge(std::uint64_t parent_id);
97+
// Re-applies the stored collapsed/expanded state (used when new nodes or edges arrive
98+
// into an already collapsed parent, and after a full createGraph())
99+
void apply_collapse_state(std::uint64_t parent_id);
82100
int timerId = 0;
83101
// Coalesces the (expensive, full-scene) setSceneRect/fitInView refit so a burst of
84102
// high-frequency node/attribute updates triggers it at most a few times per second

gui/include/dsr/gui/viewers/graph_viewer/node_colors.h

Lines changed: 143 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,90 +5,163 @@
55
#ifndef DSR_NODE_COLORS_H
66
#define DSR_NODE_COLORS_H
77

8+
#include <cstdint>
89
#include <map>
910
#include <string>
11+
#include <vector>
1012

13+
/*
14+
* Node colours, keyed by node type.
15+
*
16+
* Grouped into semantic FAMILIES so a graph reads at a glance: blues are the robot platform,
17+
* cyans/teals are sensors, greens are cognition/navigation, browns are structure and furniture,
18+
* pinks/magentas are a detected person. You should be able to spot "that is a sensor" without
19+
* reading the label.
20+
*
21+
* Two rules to preserve when editing:
22+
*
23+
* 1. `agent` MUST stay a neutral grey. It is the "this agent has not reported its health yet"
24+
* state; rc::AgentStatePublisher overwrites it live with green/orange/red (see
25+
* common/agent_state_publisher/agent_status.h in active_inference). Giving `agent` a real
26+
* colour here would make a silent or dead agent look meaningful.
27+
* 2. Avoid plain "green" / "orange" / "red" for ordinary node types, so those three keep reading
28+
* as agent health rather than as a node category.
29+
*
30+
* Every value must be a CSS3 colour name (list at the bottom of this file). QColor yields BLACK for
31+
* anything it cannot parse; GraphNode::set_color now rejects and logs such values rather than
32+
* painting the node black, but keep them valid anyway.
33+
*
34+
* A type NOT listed here no longer collapses onto one flat default — see stable_fallback_color().
35+
*/
1136
static const std::map<std::string, std::string> node_colors = {
12-
{ "root", "red"},
37+
// -- Structure / world ------------------------------------------------------------------------
38+
{ "root", "red"}, // the single graph root; long-standing, kept
1339
{ "transform", "SteelBlue"},
14-
{ "room", "Gray"},
15-
{ "differentialrobot", "GoldenRod"},
16-
{ "omnirobot", "Gray"},
17-
{ "robot", "Gray"},
18-
{ "path_to_target", "Gray"},
19-
{ "intention", "Gray"},
20-
{ "rgbd", "Gray"},
21-
{ "pan_tilt", "Gray"},
22-
{ "battery", "Gray"},
23-
{ "pose", "Gray"},
24-
{ "laser", "GreenYellow"},
25-
{ "camera", "Gray"},
26-
{ "imu", "LightSalmon"},
27-
{ "slam_device", "Gray"},
28-
{ "object", "Gray"},
29-
{ "affordance_space", "Gray"},
30-
{ "person", "Gray"},
31-
{ "personal_space", "Gray"},
40+
{ "mind", "MediumPurple"}, // parent of the agent nodes
41+
{ "room", "Tan"},
42+
{ "wall", "CadetBlue"},
43+
{ "floor", "BurlyWood"},
3244
{ "plane", "Khaki"},
33-
{ "box", "Gray"},
34-
{ "cylinder", "Gray"},
35-
{ "ball", "Gray"},
45+
{ "grid", "Wheat"},
3646
{ "mesh", "LightBlue"},
37-
{ "face", "Gray"},
38-
{ "body", "Gray"},
39-
{ "chest", "Gray"},
40-
{ "nose", "Gray"},
41-
{ "left_eye", "Gray"},
42-
{ "right_eye", "Gray"},
43-
{ "left_ear", "Gray"},
44-
{ "right_ear", "Gray"},
45-
{ "left_arm", "Gray"},
46-
{ "right_arm", "Gray"},
47-
{ "left_shoulder", "Gray"},
48-
{ "right_shoulder", "Gray"},
49-
{ "left_elbow", "Gray"},
50-
{ "right_elbow", "Gray"},
51-
{ "left_wrist", "Gray"},
52-
{ "right_wrist", "Gray"},
53-
{ "left_hand", "Gray"},
54-
{ "right_hand", "Gray"},
55-
{ "left_hip", "Gray"},
56-
{ "right_hip", "Gray"},
57-
{ "left_leg", "Gray"},
58-
{ "right_leg", "Gray"},
59-
{ "left_knee", "Gray"},
60-
{ "right_knee", "Gray"},
61-
{ "mug", "Gray"},
62-
{ "cup", "Gray"},
63-
{ "noodles", "Gray"},
64-
{ "table", "Gray"},
65-
{ "chair", "Gray"},
66-
{ "shelve", "Gray"},
67-
{ "dish", "Gray"},
68-
{ "spoon", "Gray"},
69-
{ "testtype", "Gray"},
70-
{ "glass", "Gray"},
71-
{ "plant", "Gray"},
72-
{ "microwave", "Gray"},
73-
{ "oven", "Gray"},
74-
{ "vase", "Gray"},
75-
{ "refrigerator", "Gray"},
7647

77-
//melex-rodao types
78-
{ "road", "Gray"},
79-
{ "building", "Gray"},
80-
{ "vehicle", "Gray"},
81-
{ "gps", "Gray"},
82-
{ "grid", "Gray"},
48+
// -- Robot platform ---------------------------------------------------------------------------
49+
{ "robot", "RoyalBlue"},
50+
{ "omnirobot", "CornflowerBlue"},
51+
{ "differentialrobot", "GoldenRod"},
52+
{ "body", "LightSteelBlue"},
53+
{ "pan_tilt", "PowderBlue"},
54+
{ "battery", "YellowGreen"},
8355

84-
{ "wall", "SteelBlue"},
85-
{ "floor", "BurlyWood"},
56+
// -- Sensors ----------------------------------------------------------------------------------
57+
{ "rgbd", "MediumTurquoise"},
58+
{ "camera", "Turquoise"},
59+
{ "laser", "GreenYellow"},
60+
{ "imu", "LightSalmon"},
61+
{ "slam_device", "DarkCyan"},
62+
{ "gps", "DeepSkyBlue"},
63+
64+
// -- Cognition / navigation -------------------------------------------------------------------
65+
{ "path_to_target", "SpringGreen"},
66+
{ "intention", "MediumSpringGreen"},
67+
{ "pose", "Aquamarine"},
8668
{ "affordance", "DarkOrange"},
69+
{ "affordance_space", "Coral"},
70+
{ "object", "DarkSeaGreen"},
71+
72+
// -- Geometric primitives ---------------------------------------------------------------------
73+
{ "box", "SlateBlue"},
74+
{ "cylinder", "MediumSlateBlue"},
75+
{ "ball", "DarkSlateBlue"},
76+
77+
// -- Furniture / household objects --------------------------------------------------------------
78+
{ "table", "Peru"},
79+
{ "chair", "Sienna"},
80+
{ "shelve", "Chocolate"},
81+
{ "microwave", "DarkOliveGreen"},
82+
{ "oven", "IndianRed"},
83+
{ "refrigerator", "PaleTurquoise"},
84+
{ "vase", "MediumSeaGreen"},
85+
{ "plant", "ForestGreen"},
86+
{ "mug", "Plum"},
87+
{ "cup", "Violet"},
88+
{ "glass", "PaleGreen"},
89+
{ "dish", "Moccasin"},
90+
{ "spoon", "PaleGoldenRod"},
91+
{ "noodles", "NavajoWhite"},
92+
{ "testtype", "Gold"},
8793

88-
//Agent
94+
// -- Person & skeleton --------------------------------------------------------------------------
95+
// One pink/magenta family so a detected person reads as a single cluster. Left/right pairs share a
96+
// colour ON PURPOSE (the symmetry is the point), and joints are separated from limbs: 22 near
97+
// identical pinks would carry less information than two clearly different ones.
98+
{ "person", "HotPink"},
99+
{ "personal_space", "Pink"},
100+
{ "face", "LightPink"},
101+
{ "nose", "PaleVioletRed"},
102+
{ "left_eye", "Orchid"},
103+
{ "right_eye", "Orchid"},
104+
{ "left_ear", "Orchid"},
105+
{ "right_ear", "Orchid"},
106+
{ "chest", "DeepPink"},
107+
// joints
108+
{ "left_shoulder", "MediumOrchid"},
109+
{ "right_shoulder", "MediumOrchid"},
110+
{ "left_elbow", "MediumOrchid"},
111+
{ "right_elbow", "MediumOrchid"},
112+
{ "left_wrist", "MediumOrchid"},
113+
{ "right_wrist", "MediumOrchid"},
114+
{ "left_hip", "MediumOrchid"},
115+
{ "right_hip", "MediumOrchid"},
116+
{ "left_knee", "MediumOrchid"},
117+
{ "right_knee", "MediumOrchid"},
118+
// limbs / extremities
119+
{ "left_arm", "MediumVioletRed"},
120+
{ "right_arm", "MediumVioletRed"},
121+
{ "left_hand", "MediumVioletRed"},
122+
{ "right_hand", "MediumVioletRed"},
123+
{ "left_leg", "MediumVioletRed"},
124+
{ "right_leg", "MediumVioletRed"},
125+
126+
// -- melex-rodao types ---------------------------------------------------------------------------
127+
{ "road", "DarkKhaki"},
128+
{ "building", "SaddleBrown"},
129+
{ "vehicle", "DarkSalmon"},
130+
131+
// -- Agent ---------------------------------------------------------------------------------------
132+
// KEEP NEUTRAL GREY - see rule 1 above. This is the "health not reported yet" colour; the live
133+
// value is written over it by rc::AgentStatePublisher.
89134
{ "agent", "Gray"}
90135
};
91136

137+
/*
138+
* Colour for a type with no entry above.
139+
*
140+
* Unlisted types used to collapse onto a single flat default, so a graph full of new types was a
141+
* wall of identical nodes carrying no information. Instead, hash the type name into a curated
142+
* palette: distinct, readable, never grey, never the three agent-health colours.
143+
*
144+
* The hash is FNV-1a spelled out here rather than std::hash, because the colour must be THE SAME in
145+
* every agent's viewer and across rebuilds - std::hash offers no such guarantee. The same type name
146+
* always yields the same colour, and a newly introduced type gets a distinct one for free.
147+
*/
148+
inline std::string stable_fallback_color(const std::string &type)
149+
{
150+
static const std::vector<std::string> palette = {
151+
"DodgerBlue", "MediumAquamarine", "SandyBrown", "MediumOrchid",
152+
"LightSeaGreen", "Chocolate", "CadetBlue", "DarkKhaki",
153+
"PaleVioletRed", "OliveDrab", "SlateBlue", "MediumPurple",
154+
"DarkTurquoise", "IndianRed", "Thistle", "YellowGreen",
155+
};
156+
std::uint32_t h = 2166136261u; // FNV-1a offset basis
157+
for (const unsigned char c : type)
158+
{
159+
h ^= c;
160+
h *= 16777619u; // FNV-1a prime
161+
}
162+
return palette[h % palette.size()];
163+
}
164+
92165
#endif //DSR_NODE_COLORS_H
93166

94167
// VALID COLOR NAMES https://www.w3.org/TR/2018/REC-css-color-3-20180619/

gui/viewers/graph_viewer/graph_edge.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ void GraphEdge::draw_arc(QPainter* painter) const
194194

195195
void GraphEdge::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event)
196196
{
197-
if (event->button()==Qt::RightButton) {
197+
if (event->button()==Qt::LeftButton) {
198198
mouse_double_clicked();
199199
}
200200
QGraphicsLineItem::mouseDoubleClickEvent(event);
@@ -207,7 +207,7 @@ bool GraphEdge::eventFilter(QObject* object, QEvent* event)
207207
{
208208
if(event->type() == QEvent::GraphicsSceneMouseDoubleClick){
209209
auto mouseEvent = static_cast<QGraphicsSceneMouseEvent*>(event);
210-
if (mouseEvent->button()==Qt::RightButton) {
210+
if (mouseEvent->button()==Qt::LeftButton) {
211211
mouse_double_clicked();
212212
}
213213
return true;

0 commit comments

Comments
 (0)