Skip to content

Commit 3ddf132

Browse files
committed
new files
1 parent 24f7598 commit 3ddf132

7 files changed

Lines changed: 1693 additions & 14 deletions

File tree

api/dsr_agent_info_api.cpp

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66
#include <dsr/api/dsr_agent_info_api.h>
77
#include <dsr/api/dsr_api.h>
88
#include <unistd.h>
9+
#include <limits.h>
910
#include <sys/stat.h>
1011
#include <sys/times.h>
1112
#include <QMetaObject>
13+
#include <fstream>
14+
#include <sstream>
15+
#include <vector>
16+
#include <string>
1217

1318
namespace DSR {
1419

@@ -32,6 +37,71 @@ void AgentInfoAPI::heartbeat_tick()
3237
pclose(pipe);
3338
}
3439
};
40+
41+
// ── Deployment self-report helpers ──────────────────────────────────────────────────────────
42+
// Read this process's argv from /proc/self/cmdline (NUL-separated). Empty on failure.
43+
std::vector<std::string> proc_argv()
44+
{
45+
std::vector<std::string> argv;
46+
std::ifstream f("/proc/self/cmdline", std::ios::binary);
47+
if (!f)
48+
return argv;
49+
std::string tok;
50+
for (char ch; f.get(ch); )
51+
{
52+
if (ch == '\0') { if (!tok.empty()) argv.push_back(tok); tok.clear(); }
53+
else tok.push_back(ch);
54+
}
55+
if (!tok.empty()) argv.push_back(tok);
56+
return argv;
57+
}
58+
59+
// This process's current working directory via /proc/self/cwd. Empty on failure.
60+
std::string proc_cwd()
61+
{
62+
char buf[PATH_MAX];
63+
const ssize_t n = ::readlink("/proc/self/cwd", buf, sizeof(buf) - 1);
64+
return n > 0 ? std::string(buf, static_cast<size_t>(n)) : std::string{};
65+
}
66+
67+
// Pick the config path out of argv, mirroring the launcher's topology.py::_config_path:
68+
// the last token that looks like a config (under etc/, or ending in config/.toml/.conf),
69+
// unwrapping an --Ice.Config= prefix. Relative paths are resolved against cwd.
70+
std::string derive_config_path(const std::vector<std::string>& argv, const std::string& cwd)
71+
{
72+
std::string cand;
73+
for (std::string tok : argv)
74+
{
75+
constexpr const char* kIcePrefix = "--Ice.Config=";
76+
if (tok.rfind(kIcePrefix, 0) == 0)
77+
tok = tok.substr(std::string(kIcePrefix).size());
78+
const bool looks_config =
79+
tok.find("etc/") != std::string::npos ||
80+
(tok.size() >= 6 && tok.compare(tok.size() - 6, 6, "config") == 0) ||
81+
tok.find(".toml") != std::string::npos ||
82+
tok.find(".conf") != std::string::npos;
83+
if (looks_config)
84+
cand = tok;
85+
}
86+
if (cand.empty())
87+
return cand;
88+
if (!cand.empty() && cand.front() != '/' && !cwd.empty())
89+
cand = cwd + "/" + cand;
90+
return cand;
91+
}
92+
93+
// Slurp a text file whole. Empty on failure.
94+
std::string slurp_file(const std::string& path)
95+
{
96+
if (path.empty())
97+
return {};
98+
std::ifstream f(path, std::ios::binary);
99+
if (!f)
100+
return {};
101+
std::ostringstream ss;
102+
ss << f.rdbuf();
103+
return ss.str();
104+
}
35105
}
36106

37107

@@ -169,6 +239,12 @@ void AgentInfoAPI::heartbeat_tick()
169239

170240
uint64_t times = get_unix_timestamp();
171241
auto &node_ref = node.value();
242+
// Keep the PID fresh across restarts: the node can outlive the process (persisted graph),
243+
// so a re-launched agent finds its old node here and must overwrite the stale pid. Only
244+
// written when it actually changed, to avoid needless 1 Hz attribute churn/signals.
245+
const auto cur_pid = static_cast<std::uint32_t>(getpid());
246+
if (const auto p = G->get_attrib_by_name<agent_pid_att>(node_ref); not p.has_value() or p.value() != cur_pid)
247+
G->add_or_modify_attrib_local<agent_pid_att>(node_ref, cur_pid);
172248
G->add_or_modify_attrib_local<timestamp_agent_att>(node_ref, times);
173249
G->add_or_modify_attrib_local<timestamp_alivetime_att>(node_ref,
174250
static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::nanoseconds(times - timestamp_start)).count()));
@@ -207,7 +283,22 @@ void AgentInfoAPI::heartbeat_tick()
207283
G->add_or_modify_attrib_local<pos_x_att>(new_node, (float) 10);
208284
G->add_or_modify_attrib_local<pos_y_att>(new_node, (float) 10);
209285
G->add_or_modify_attrib_local<parent_att>(new_node, parent_id);
210-
G->add_or_modify_attrib_local<agent_description_att>(new_node, std::string{"TODO"});
286+
287+
// Deployment self-report (once, at creation): launch command, working dir and the raw
288+
// etc/config used to start this agent. Feeds the "mind" node network view. Best-effort:
289+
// any field that can't be resolved is simply left empty.
290+
const auto argv = proc_argv();
291+
const auto cwd = proc_cwd();
292+
const auto cfg_path = derive_config_path(argv, cwd);
293+
std::string cmd_line;
294+
for (const auto& a : argv) { if (!cmd_line.empty()) cmd_line += ' '; cmd_line += a; }
295+
G->add_or_modify_attrib_local<agent_cmd_att>(new_node, cmd_line);
296+
G->add_or_modify_attrib_local<agent_cwd_att>(new_node, cwd);
297+
G->add_or_modify_attrib_local<agent_config_att>(new_node, slurp_file(cfg_path));
298+
G->add_or_modify_attrib_local<agent_pid_att>(new_node, static_cast<std::uint32_t>(getpid()));
299+
const std::string desc = cfg_path.empty() ? G->get_agent_name()
300+
: G->get_agent_name() + " (" + cfg_path + ")";
301+
G->add_or_modify_attrib_local<agent_description_att>(new_node, desc);
211302
//CPU usage
212303
if (cpu >= 0.0)
213304
{

api/dsr_api.cpp

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -496,9 +496,12 @@ DSRGraph::SampleCallback<Batch> DSRGraph::make_node_attrs_subscription_functor_i
496496
}
497497

498498
template <typename GraphSample>
499-
DSRGraph::SampleCallback<GraphSample> DSRGraph::make_fullgraph_request_functor_impl(const char* channel, std::atomic<bool>& sync, std::atomic<bool>& repeated)
499+
DSRGraph::SampleCallback<GraphSample> DSRGraph::make_fullgraph_request_functor_impl(const char* channel, std::shared_ptr<std::atomic<bool>> sync, std::shared_ptr<std::atomic<bool>> repeated)
500500
{
501-
return [this, channel, &sync, &repeated](GraphSample&& sample, const DSR::Transport::ReceivedSampleInfo& m_info)
501+
// Capture sync/repeated BY VALUE (shared_ptr): this callback is stored in a permanent DDS subscription
502+
// and must keep the flags alive — capturing raw refs to fullgraph_request_thread()'s stack was the
503+
// dangling-write bug. (Fixed 2026-07-21.)
504+
return [this, channel, sync, repeated](GraphSample&& sample, const DSR::Transport::ReceivedSampleInfo& m_info)
502505
{
503506
[[maybe_unused]] static thread_local bool _named = []{ CORTEX_PROFILE_THREAD_NAME("fg_request"); return true; }();
504507
CORTEX_PROFILE_ZONE_N("DSRGraph::fullgraph_request_thread callback");
@@ -522,9 +525,9 @@ DSRGraph::SampleCallback<GraphSample> DSRGraph::make_fullgraph_request_functor_i
522525
}
523526
}, Qt::QueuedConnection);
524527
qDebug() << "Synchronized.";
525-
sync = true;
526-
} else if (!sync && sample.to_id == agent_id) {
527-
repeated = true;
528+
*sync = true;
529+
} else if (!*sync && sample.to_id == agent_id) {
530+
*repeated = true;
528531
}
529532
}
530533
};
@@ -1332,8 +1335,14 @@ void DSRGraph::fullgraph_server_thread()
13321335
std::pair<bool, bool> DSRGraph::fullgraph_request_thread()
13331336
{
13341337
CORTEX_PROFILE_MIN_N("DSRGraph::fullgraph_request_thread");
1335-
std::atomic<bool> sync{false};
1336-
std::atomic<bool> repeated{false};
1338+
// sync/repeated are shared_ptr, NOT stack locals: the GRAPH_ANSWER subscription below is PERMANENT and
1339+
// its callback captures them by value, so they must outlive this function. Previously they were stack
1340+
// atomics captured by reference — they dangled once this function returned, so a later peer's
1341+
// GRAPH_ANSWER (on a FastDDS reader thread) ran `*sync = true` through the dangling reference, writing
1342+
// 1 byte into freed/reused stack memory (the bit-48 wild write that randomly smashed cv::Mat headers
1343+
// etc. under peer churn). (Fixed 2026-07-21.)
1344+
auto sync = std::make_shared<std::atomic<bool>>(false);
1345+
auto repeated = std::make_shared<std::atomic<bool>>(false);
13371346
if (sync_mode == SyncMode::LWW) {
13381347
dsrparticipant.subscribe_graph_answers<LWWGraphSnapshot>(make_fullgraph_request_functor_impl<LWWGraphSnapshot>("LWW_GRAPH_ANSWER", sync, repeated), mtx_entity_creation);
13391348
} else {
@@ -1360,7 +1369,7 @@ std::pair<bool, bool> DSRGraph::fullgraph_request_thread()
13601369

13611370
bool timeout = false;
13621371
std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
1363-
while (!sync and !timeout and !repeated) {
1372+
while (!*sync and !timeout and !*repeated) {
13641373
CORTEX_PROFILE_DETAIL_N("DSRGraph::fullgraph_request_thread wait loop");
13651374
std::this_thread::sleep_for(1000ms);
13661375
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
@@ -1371,7 +1380,7 @@ std::pair<bool, bool> DSRGraph::fullgraph_request_thread()
13711380
dsrparticipant.publish_graph_request(gr);
13721381
}
13731382

1374-
return { sync, repeated };
1383+
return { sync->load(), repeated->load() };
13751384
}
13761385

13771386

api/include/dsr/api/dsr_api.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,13 @@ namespace DSR
796796
template <typename Batch>
797797
SampleCallback<Batch> make_node_attrs_subscription_functor_impl(const char* channel);
798798
template <typename GraphSample>
799-
SampleCallback<GraphSample> make_fullgraph_request_functor_impl(const char* channel, std::atomic<bool>& sync, std::atomic<bool>& repeated);
799+
// sync/repeated are shared_ptr (not raw refs) so the returned callback — stored in a PERMANENT DDS
800+
// subscription — captures them BY VALUE and keeps them alive. Previously they were refs to stack
801+
// locals in fullgraph_request_thread() that dangled after it returned (a later peer's GRAPH_ANSWER
802+
// then wrote through them into freed stack memory: the bit-48 wild write). Fixed 2026-07-21.
803+
SampleCallback<GraphSample> make_fullgraph_request_functor_impl(const char* channel,
804+
std::shared_ptr<std::atomic<bool>> sync,
805+
std::shared_ptr<std::atomic<bool>> repeated);
800806

801807
//Custom function for each rtps topic
802808
class ParticipantChangeFn {

core/include/dsr/core/types/type_checking/dsr_attr_name.h

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,27 @@ REGISTER_TYPE(cpu_usage, float, false)
476476
REGISTER_TYPE(memory_usage, uint32_t , false)
477477
REGISTER_TYPE(num_procs, uint32_t, false)
478478
REGISTER_TYPE(agent_description, std::reference_wrapper<const std::string>, false)
479+
// Deployment metadata, self-reported once at agent-node creation (see AgentInfoAPI). Feeds the
480+
// "mind" node network view: agent_cmd/agent_cwd are the launch command + working dir read from
481+
// /proc/self, agent_config is the raw contents of the etc/config passed on the command line.
482+
REGISTER_TYPE(agent_cmd, std::reference_wrapper<const std::string>, false)
483+
REGISTER_TYPE(agent_cwd, std::reference_wrapper<const std::string>, false)
484+
REGISTER_TYPE(agent_config, std::reference_wrapper<const std::string>, false)
485+
// Process id of the agent (getpid). Lets the mind network view attribute each loopback TCP
486+
// connection to the exact client agent (inode→pid→agent), i.e. per-connection bandwidth.
487+
REGISTER_TYPE(agent_pid, std::uint32_t, false)
488+
// Live health of the agent, self-reported on every state transition (see rc::AgentStatePublisher).
489+
// Two independent axes: the generated GRAFCET/QStateMachine step the agent is executing, and the
490+
// agent-presence lifecycle (are its required peers there?). They are NOT redundant: an agent parked
491+
// in on_waiting_loop waiting for peers still reports FSM "Compute". The publisher also derives a
492+
// worst-wins `color` from the pair so any graph viewer paints the agent node without duplicating
493+
// the mapping.
494+
REGISTER_TYPE(agent_fsm_state, std::string, false) /* Initialize | Compute | Emergency | Restore */
495+
REGISTER_TYPE(agent_presence_state, std::string, false) /* Waiting | Operating | Degraded */
496+
// Live media-plane throughput (bytes/s) the producer (SensorMediaPublisher) writes onto a sensor's
497+
// descriptor node. The heavy frames travel over zero-copy DDS shared memory, invisible to packet
498+
// capture, so this self-reported figure is the only way the mind view can show real media bandwidth.
499+
REGISTER_TYPE(media_bps, float, false)
479500

480501

481502
/*
@@ -573,6 +594,44 @@ REGISTER_TYPE(grid_occupancy_var, std::reference_wrapper<const std::vector<floa
573594
REGISTER_TYPE(grid_field_meta, std::reference_wrapper<const std::vector<float>>, false) // [xmin,ymin,cell,w,h]
574595

575596

597+
// ── table-concept VOXEL MEMORY + active-perception ROI channel (written by table_concept) ──────
598+
// Registered 2026-07-21 so table_scene_graph.cpp can use the TYPE-ATTRIBUTED setters instead of the
599+
// runtime_checked_* string form (see CLAUDE.md). table_voxel_bank_pts / table_roi_offset are [x,y,z]-
600+
// and [ox,oy]-packed float arrays; the ROI + detection scalars drive the controller's lock-on search
601+
// via common/affordance_protocol (its attr_scalar() coerces int/float/bool alike).
602+
// WIRE-TYPE WARNING for whoever edits these next: an attribute's registered type is part of its CONTRACT
603+
// with every consumer. table_detection_alive was int 0/1 until 2026-07-21; flipping it to bool without
604+
// migrating the readers made room_concept throw "INT is not selected, selected is BOOL" at runtime,
605+
// because it read the raw attrs map with Attribute::dec(). Both readers now use the TYPE-ATTRIBUTED
606+
// getter (get_attrib_by_name<table_detection_alive_att>), so a future type change is a COMPILE error
607+
// there instead of a runtime throw. Never change a registered type without migrating every reader.
608+
REGISTER_TYPE(table_voxel_bank_pts, std::reference_wrapper<const std::vector<float>>, false) // [x,y,z]×N
609+
REGISTER_TYPE(table_roi_offset, std::reference_wrapper<const std::vector<float>>, false) // [ox,oy] ∈[-1,1]
610+
REGISTER_TYPE(table_roi_fill, float, false) // projected extent frac
611+
REGISTER_TYPE(table_roi_valid, bool, false) // projects in front of cam
612+
REGISTER_TYPE(table_detection_alive, bool, false) // YOLO firing here
613+
REGISTER_TYPE(table_detection_confidence, float, false) // last mask confidence
614+
REGISTER_TYPE(table_frames_since_detection, int, false) // cycles since fresh mask
615+
616+
// ── chair-concept / cabinet-concept: same voxel-memory + ROI channel as table-concept above ────
617+
// These agents are copies of the table_concept pattern and carry the identical interface, so they get
618+
// the identical types. Readers are common/affordance_protocol (attr_scalar coerces int/float/bool), and
619+
// no consumer reads them with a raw Attribute accessor — verified 2026-07-21 before choosing bool.
620+
REGISTER_TYPE(chair_voxel_bank_pts, std::reference_wrapper<const std::vector<float>>, false) // [x,y,z]xN
621+
REGISTER_TYPE(chair_roi_offset, std::reference_wrapper<const std::vector<float>>, false) // [ox,oy]
622+
REGISTER_TYPE(chair_roi_fill, float, false)
623+
REGISTER_TYPE(chair_roi_valid, bool, false)
624+
REGISTER_TYPE(chair_detection_alive, bool, false)
625+
REGISTER_TYPE(chair_detection_confidence, float, false)
626+
REGISTER_TYPE(chair_frames_since_detection, int, false)
627+
REGISTER_TYPE(cabinet_voxel_bank_pts, std::reference_wrapper<const std::vector<float>>, false) // [x,y,z]xN
628+
REGISTER_TYPE(cabinet_roi_offset, std::reference_wrapper<const std::vector<float>>, false) // [ox,oy]
629+
REGISTER_TYPE(cabinet_roi_fill, float, false)
630+
REGISTER_TYPE(cabinet_roi_valid, bool, false)
631+
REGISTER_TYPE(cabinet_detection_alive, bool, false)
632+
REGISTER_TYPE(cabinet_detection_confidence, float, false)
633+
REGISTER_TYPE(cabinet_frames_since_detection, int, false)
634+
576635
// ── table-concept inference outputs (written by table-concept) ────────────────────────────────
577636
REGISTER_TYPE(free_energy, float, false)
578637
REGISTER_TYPE(model_stable, bool, false)

0 commit comments

Comments
 (0)