Skip to content

Commit 3d1fab3

Browse files
halcmd: add -p option to generate PlantUML diagram of HAL pins/signals
Emit bracket-style component boxes grouped by instance, with the component type name (loadrt/loadusr module name) in parentheses. Signals are rendered as queue entities so one writer can fan out to multiple readers via a single node. Components with all unconnected pins are filtered out. Also documents the new -p option in the halcmd man page. This patch was created with help from OpenCode using local llama.cpp server with Qwen 3.6.
1 parent 633d470 commit 3d1fab3

6 files changed

Lines changed: 425 additions & 1 deletion

File tree

docs/man/man1/halcmd.1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.\" Copyright (c) 2003 John Kasunich
22
.\" (jmkasunich AT users DOT sourceforge DOT net)
3+
.\" Copyright (c) 2026 Petter Reinholdtsen <pere@hungry.com>
34
.\"
45
.\" This is free documentation; you can redistribute it and/or
56
.\" modify it under the terms of the GNU General Public License as
@@ -87,6 +88,9 @@ display results of each command
8788
\fB\-V\fR
8889
display lots of debugging junk
8990
.TP
91+
\fB\-p\fR
92+
Generate a PlantUML diagram of HAL pins and signals. Components are rendered as bracket-style boxes grouped by instance, with the component type name in parentheses. Signals are shown as queue entities, allowing one writer to fan out to multiple readers via a single node. Only components with at least one connected pin are included. Output is written to stdout.
93+
.TP
9094
\fB\-h\fR [\fI<command>\fR]
9195
display a help screen and exit, displays extended help on \fIcommand\fR if specified
9296
.SH COMMANDS

src/hal/utils/halcmd_commands.cc

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* <fenn AT users DOT sourceforge DOT net>
1212
* Stephen Wille Padnos
1313
* <swpadnos AT users DOT sourceforge DOT net>
14+
* Petter Reinholdtsen <pere@hungry.com>
1415
*
1516
* This program is free software; you can redistribute it and/or
1617
* modify it under the terms of version 2 of the GNU General
@@ -2823,6 +2824,264 @@ int do_setexact_cmd() {
28232824
return retval;
28242825
}
28252826

2827+
#include <map>
2828+
#include <vector>
2829+
#include <algorithm>
2830+
2831+
static void plantuml_escape_alias(const char *name, FILE *dst) {
2832+
const char *p = name;
2833+
while (*p) {
2834+
if (*p == '-' || *p == '.' || *p == ' ') fputc('_', dst);
2835+
else fputc(*p, dst);
2836+
p++;
2837+
}
2838+
}
2839+
2840+
static void plantuml_escape_string(const char *name, FILE *dst) {
2841+
const char *p = name;
2842+
while (*p) {
2843+
if (*p == '\\' || *p == '"') fputc('\\', dst);
2844+
fputc(*p, dst);
2845+
p++;
2846+
}
2847+
}
2848+
2849+
2850+
static std::string pin_to_instance(const char *pin_name) {
2851+
const char *last_dot = strrchr(pin_name, '.');
2852+
if (last_dot) return std::string(pin_name, last_dot - pin_name);
2853+
return std::string(pin_name);
2854+
}
2855+
2856+
static const char *pin_field_name(const char *pin_name) {
2857+
const char *last_dot = strrchr(pin_name, '.');
2858+
return last_dot ? last_dot + 1 : pin_name;
2859+
}
2860+
2861+
void generate_plantuml(FILE *dst) {
2862+
struct pin_entry_t {
2863+
char full_name[HAL_NAME_LEN+1];
2864+
bool connected;
2865+
};
2866+
2867+
struct inst_pins_t {
2868+
char display_name[HAL_NAME_LEN+1];
2869+
char comp_type[HAL_NAME_LEN+1];
2870+
std::vector<pin_entry_t> in_pins;
2871+
std::vector<pin_entry_t> out_pins;
2872+
};
2873+
2874+
// Signal entity: hexagon with connected writer/readers per pin
2875+
struct sig_pin_t {
2876+
char alias[HAL_NAME_LEN+1]; // instance alias (e.g. and2_0)
2877+
char field[64]; // last segment of pin name
2878+
};
2879+
2880+
struct sig_entry_t {
2881+
char name[HAL_NAME_LEN+1];
2882+
sig_pin_t *writer; // single writer pin (or NULL)
2883+
std::vector<sig_pin_t *> readers;
2884+
};
2885+
2886+
std::map<std::string, inst_pins_t *> inst_map;
2887+
std::vector<sig_entry_t> signals;
2888+
2889+
try {
2890+
rtapi_mutex_get(&(hal_data->mutex));
2891+
2892+
// Collect all pins per instance
2893+
SHMFIELD(hal_pin_t) next = hal_data->pin_list_ptr;
2894+
while (next != 0) {
2895+
hal_pin_t *pin = SHMPTR(next);
2896+
std::string inst_name = pin_to_instance(pin->name);
2897+
2898+
if (inst_map.find(inst_name) == inst_map.end()) {
2899+
inst_pins_t *ip = new inst_pins_t();
2900+
snprintf(ip->display_name, sizeof(ip->display_name), "%s", inst_name.c_str());
2901+
ip->comp_type[0] = '\0';
2902+
ip->in_pins.reserve(32);
2903+
ip->out_pins.reserve(32);
2904+
inst_map[inst_name] = ip;
2905+
}
2906+
2907+
// Look up component type from pin owner
2908+
hal_comp_t *owner = SHMPTR(pin->owner_ptr);
2909+
if (owner && strlen(inst_map[inst_name]->comp_type) == 0) {
2910+
snprintf(inst_map[inst_name]->comp_type, sizeof(inst_map[inst_name]->comp_type), "%s", owner->name);
2911+
}
2912+
2913+
pin_entry_t pe;
2914+
snprintf(pe.full_name, sizeof(pe.full_name), "%s", pin->name);
2915+
pe.connected = false;
2916+
2917+
if (pin->dir == HAL_IN) {
2918+
inst_map[inst_name]->in_pins.push_back(pe);
2919+
} else {
2920+
inst_map[inst_name]->out_pins.push_back(pe);
2921+
}
2922+
2923+
next = pin->next_ptr;
2924+
}
2925+
2926+
// Walk signals to find connected pins and build signal entries
2927+
SHMFIELD(hal_sig_t) sig_next = hal_data->sig_list_ptr;
2928+
while (sig_next != 0) {
2929+
hal_sig_t *sig = SHMPTR(sig_next);
2930+
hal_pin_t *writer = NULL;
2931+
std::vector<hal_pin_t *> readers;
2932+
2933+
hal_pin_t *pin = halpr_find_pin_by_sig(sig, 0);
2934+
while (pin != 0) {
2935+
if (pin->dir == HAL_OUT) writer = pin;
2936+
else readers.push_back(pin);
2937+
pin = halpr_find_pin_by_sig(sig, pin);
2938+
}
2939+
2940+
// Emit signals that have at least one writer or reader connected
2941+
if (writer || !readers.empty()) {
2942+
sig_entry_t se;
2943+
snprintf(se.name, sizeof(se.name), "%s", sig->name);
2944+
se.writer = NULL;
2945+
se.readers.reserve(readers.size());
2946+
2947+
// Build writer entry and mark pin connected
2948+
if (writer) {
2949+
std::string wi = pin_to_instance(writer->name);
2950+
auto *wip = inst_map[wi];
2951+
if (wip) {
2952+
for (auto &pe : wip->out_pins) {
2953+
if (strcmp(pe.full_name, writer->name) == 0) pe.connected = true;
2954+
}
2955+
}
2956+
sig_pin_t *sp = new sig_pin_t();
2957+
snprintf(sp->alias, sizeof(sp->alias), "%s", wi.c_str());
2958+
const char *wf = pin_field_name(writer->name);
2959+
size_t wflen = strlen(wf);
2960+
if (wflen >= sizeof(sp->field)) wflen = sizeof(sp->field) - 1;
2961+
memcpy(sp->field, wf, wflen);
2962+
sp->field[wflen] = '\0';
2963+
se.writer = sp;
2964+
}
2965+
2966+
// Build reader entries and mark pins connected
2967+
for (auto &reader : readers) {
2968+
std::string ri = pin_to_instance(reader->name);
2969+
auto *rip = inst_map[ri];
2970+
if (rip) {
2971+
for (auto &pe : rip->in_pins) {
2972+
if (strcmp(pe.full_name, reader->name) == 0) pe.connected = true;
2973+
}
2974+
}
2975+
sig_pin_t *sp = new sig_pin_t();
2976+
snprintf(sp->alias, sizeof(sp->alias), "%s", ri.c_str());
2977+
const char *rf = pin_field_name(reader->name);
2978+
size_t rflen = strlen(rf);
2979+
if (rflen >= sizeof(sp->field)) rflen = sizeof(sp->field) - 1;
2980+
memcpy(sp->field, rf, rflen);
2981+
sp->field[rflen] = '\0';
2982+
se.readers.push_back(sp);
2983+
}
2984+
2985+
signals.push_back(se);
2986+
}
2987+
2988+
sig_next = sig->next_ptr;
2989+
}
2990+
2991+
rtapi_mutex_give(&(hal_data->mutex));
2992+
} catch (...) {
2993+
rtapi_mutex_give(&(hal_data->mutex));
2994+
2995+
// Cleanup any partial allocations before re-throwing
2996+
for (auto &se : signals) {
2997+
delete se.writer;
2998+
for (auto &rp : se.readers) delete rp;
2999+
}
3000+
for (auto &kv : inst_map) delete kv.second;
3001+
throw;
3002+
}
3003+
3004+
fprintf(dst, "@startuml\n");
3005+
3006+
// Emit only instances that have at least one connected pin
3007+
for (auto &kv : inst_map) {
3008+
inst_pins_t *ip = kv.second;
3009+
bool has_conn = false;
3010+
for (const auto &pe : ip->in_pins) if (pe.connected) { has_conn = true; break; }
3011+
for (const auto &pe : ip->out_pins) if (pe.connected) { has_conn = true; break; }
3012+
if (!has_conn) continue;
3013+
3014+
fputs("[", dst);
3015+
fputs(ip->display_name, dst);
3016+
if (strlen(ip->comp_type) > 0) {
3017+
fprintf(dst, " (%s)", ip->comp_type);
3018+
}
3019+
3020+
for (const auto &pe : ip->in_pins) {
3021+
const char *f = pin_field_name(pe.full_name);
3022+
fputs("\\n", dst);
3023+
fputs(f, dst);
3024+
fputs(" in", dst);
3025+
if (!pe.connected) fputs(" (unconnected)", dst);
3026+
}
3027+
for (const auto &pe : ip->out_pins) {
3028+
const char *f = pin_field_name(pe.full_name);
3029+
fputs("\\n", dst);
3030+
fputs(f, dst);
3031+
fputs(" out", dst);
3032+
if (!pe.connected) fputs(" (unconnected)", dst);
3033+
}
3034+
3035+
fprintf(dst, "] as ");
3036+
plantuml_escape_alias(ip->display_name, dst);
3037+
fprintf(dst, "\n");
3038+
}
3039+
3040+
// Emit signals as queue entities (hexagon not available in PlantUML v1.2020)
3041+
for (size_t i = 0; i < signals.size(); i++) {
3042+
char alias_buf[HAL_NAME_LEN + 1];
3043+
snprintf(alias_buf, sizeof(alias_buf), "sig_%zu", i);
3044+
fprintf(dst, "queue \"");
3045+
plantuml_escape_string(signals[i].name, dst);
3046+
fprintf(dst, "\" as %s\n", alias_buf);
3047+
}
3048+
3049+
// Emit edges: writer --> signal and signal --> reader
3050+
for (size_t i = 0; i < signals.size(); i++) {
3051+
char alias_buf[HAL_NAME_LEN + 1];
3052+
snprintf(alias_buf, sizeof(alias_buf), "sig_%zu", i);
3053+
3054+
if (signals[i].writer) {
3055+
plantuml_escape_alias(signals[i].writer->alias, dst);
3056+
fprintf(dst, " \"");
3057+
plantuml_escape_string(signals[i].writer->field, dst);
3058+
fprintf(dst, "\" --> ");
3059+
fputs(alias_buf, dst);
3060+
fprintf(dst, "\n");
3061+
}
3062+
for (auto &rp : signals[i].readers) {
3063+
fputs(alias_buf, dst);
3064+
fprintf(dst, " --> \"");
3065+
plantuml_escape_string(rp->field, dst);
3066+
fprintf(dst, "\" ");
3067+
plantuml_escape_alias(rp->alias, dst);
3068+
fprintf(dst, "\n");
3069+
}
3070+
}
3071+
3072+
// Cleanup signal allocations
3073+
for (auto &se : signals) {
3074+
delete se.writer;
3075+
for (auto &rp : se.readers) delete rp;
3076+
}
3077+
3078+
for (auto &kv : inst_map) {
3079+
delete kv.second;
3080+
}
3081+
3082+
fprintf(dst, "@enduml\n");
3083+
}
3084+
28263085
int do_help_cmd(char *command)
28273086
{
28283087
if (!command) {

src/hal/utils/halcmd_commands.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* <fenn AT users DOT sourceforge DOT net>
1212
* Stephen Wille Padnos
1313
* <swpadnos AT users DOT sourceforge DOT net>
14+
* Petter Reinholdtsen <pere@hungry.com>
1415
*
1516
* This program is free software; you can redistribute it and/or
1617
* modify it under the terms of version 2 of the GNU General
@@ -86,6 +87,7 @@ extern int do_loadusr_cmd(const char *args[]);
8687
extern int do_waitusr_cmd(char *comp_name);
8788
extern int do_save_cmd(const char *type, char *filename);
8889
extern int do_setexact_cmd(void);
90+
extern void generate_plantuml(FILE *dst);
8991

9092
pid_t hal_systemv_nowait(const char *const argv[]);
9193
int hal_systemv(const char *const argv[]);

src/hal/utils/halcmd_main.c

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* <fenn AT users DOT sourceforge DOT net>
1212
* Stephen Wille Padnos
1313
* <swpadnos AT users DOT sourceforge DOT net>
14+
* Petter Reinholdtsen <pere@hungry.com>
1415
*
1516
* This program is free software; you can redistribute it and/or
1617
* modify it under the terms of version 2 of the GNU General
@@ -87,6 +88,8 @@ static char *prompt_continue = "halcmd+: ";
8788

8889
#define MAX_EXTEND_LINES 20
8990

91+
static int plantuml_mode = 0;
92+
9093
/***********************************************************************
9194
* LOCAL FUNCTION DEFINITIONS *
9295
************************************************************************/
@@ -113,7 +116,7 @@ int main(int argc, char **argv)
113116
keep_going = 0;
114117
/* start parsing the command line, options first */
115118
while(1) {
116-
c = getopt(argc, argv, "+RCfi:kqQsvVhe");
119+
c = getopt(argc, argv, "+RCfi:kqQsvVhep");
117120
if(c == -1) break;
118121
switch(c) {
119122
case 'R':
@@ -160,6 +163,10 @@ int main(int argc, char **argv)
160163
case 'e':
161164
echo_mode = 1;
162165
break;
166+
case 'p':
167+
/* -p = generate PlantUML diagram of HAL pins/signals */
168+
plantuml_mode = 1;
169+
break;
163170
case 'f':
164171
filemode = 1;
165172
break;
@@ -251,6 +258,12 @@ int main(int argc, char **argv)
251258

252259
if ( halcmd_startup(0) != 0 ) return 1;
253260

261+
if (plantuml_mode) {
262+
generate_plantuml(stdout);
263+
halcmd_shutdown();
264+
return 0;
265+
}
266+
254267
errorcount = 0;
255268
/* HAL init is OK, let's process the command(s) */
256269
if (srcfile == NULL) {
@@ -411,6 +424,7 @@ static void print_help_general(int showR)
411424
printf("\n halcmd [options] -f [filename]\n\n");
412425
printf("options:\n\n");
413426
printf(" -e echo the commands from stdin to stderr\n");
427+
printf(" -p Generate PlantUML diagram of HAL pins and signals\n");
414428
printf(" -f [filename] Read commands from 'filename', not command\n");
415429
printf(" line. If no filename, read from stdin.\n");
416430
#ifndef NO_INI

0 commit comments

Comments
 (0)