Skip to content

halcmd: add -p option to generate PlantUML diagram of HAL pins/signals#4214

Open
petterreinholdtsen wants to merge 1 commit into
LinuxCNC:2.9from
petterreinholdtsen:halcmd-plantuml-output
Open

halcmd: add -p option to generate PlantUML diagram of HAL pins/signals#4214
petterreinholdtsen wants to merge 1 commit into
LinuxCNC:2.9from
petterreinholdtsen:halcmd-plantuml-output

Conversation

@petterreinholdtsen

Copy link
Copy Markdown
Collaborator

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.

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.
@BsAtHome

BsAtHome commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Why can't you just use script-mode for output (halcmd -s) and post-process with your own script?
I cannot see a reason why you need to have plantuml specific output to be part of halcmd. If script-mode shows a deficiency for post-processing , then we can simply fix that (in a compatible way).

@petterreinholdtsen

Copy link
Copy Markdown
Collaborator Author

Why can't you just use script-mode for output (halcmd -s) and post-process with your own script?

Who say I can not? I just believe it is more convenient and available for a large audience if the program do this directly and on its own, instead of having to track down a separate tool.

This patch is actually a byproduct of an experiment adding a graph tab to halshow. Have not yet found a way to make that graph pretty and useful, so in the mean time I have been using plantuml to get an overview of the HAL setup and changes on machines. :)

@andypugh

Copy link
Copy Markdown
Collaborator

Do you have a sample of the plantuml output as SVG?

@BsAtHome

Copy link
Copy Markdown
Contributor

But still, the correct way then is to make the script that is part of LinuxCNC for that "large audience". You still need plantuml for it to function. So, you make a script that calls halcmd -s processes the output and wraps it all up. No need to add another mode to halcmd. Just make a script that does it all for that large audience.

The problem I'm addressing here is that you can add N+1 output interfaces to halcmd and still not be able to capture them all for any audience's liking. However, it makes halcmd unnecessarily complex and difficult to maintain. Therefore, using one standard script-friendly output format, which we have, should suffice and everything else is external or encapsulated post-processing.

@petterreinholdtsen

petterreinholdtsen commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

Do you have a sample of the plantuml output as SVG?

Sure. Attached.
simulation

@petterreinholdtsen

Copy link
Copy Markdown
Collaborator Author

The problem I'm addressing here is that you can add N+1 output interfaces to halcmd and still not be able to capture them all for any audience's liking.

While it of course is true and sound very dramatic, I suspect that in reality, there are two major formats that are relevant, the graphviz dot format and plantuml. So N=0 will make a lot of people happy and N=1 will provide a useful format for the wast majority. I would be happy to add a -g option for graphviz dot, if you believe it is vital to increase the audience approval.

@BsAtHome

Copy link
Copy Markdown
Contributor

What about PGF/TikZ? Or R? Or Vym?. Then there are numerous other packages. Just looking at the very long list of available UML tools should give you an indication how many different ways there are to tell a similar story. There is the whole category of argument/concept mapping software and associated languages. You may not be able to surface once you down that rabbit hole and forever stay in the Red Queen's prison with your head chopped off.

FWIW, we already have N=1you know the saying, use N=0, N=1 or N=∞. With N=1 we have the script-friendly output. As said, if it is insufficient or incomplete, then it should be fixed to be generic. But I'd strongly resist adding any specific target language.

@petterreinholdtsen

petterreinholdtsen commented Jun 30, 2026 via email

Copy link
Copy Markdown
Collaborator Author

@andypugh

andypugh commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

The current script-friendly output isn't something that any apt-installable package can parse into a diagram.
This UML output can go straight into existing tools (as could .dot format)
There are a number of third-party tools, though some require a running HAL session to query in order to work, whereas some work from the HAL file itself.

Ideally we would have a version of Halshow where you could drag the blocks around to untangle lines, and the values were displayed live on the nets. But at that point we would be re-inventing Simulink or LabView.

It's worth remembering, also, that most HAL files are fit-and-forget. after initial configuration they are not looked at again for years at a time. For most users there are more valuable places to invest developer effort.

This PR was prompted by trying to get to the bottom of the spindle control on Petter's Mazak, which after a few years was a mystery to both of us. Using this script (converted to Python3) was very helpful when we were trying to work out how the spindle was controlled

@hdiethelm

hdiethelm commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

I agree that halcmd is the wrong place to implement a specific graph format. At the end, someone wants to implement a VisualStudio drawing generator in there... ;-)

However, having ways to generate a graph from the connections is for sure nice and looking at the above repo, parsing the output of halcmd -s looks cumbersome.

Options:

  • Use the available https://linuxcnc.org/docs/devel/html/en/config/python-hal-interface.html to gather the data. It looks like a function to show the output to signal connection is missing. Signal to input can be shown using hal.get_info_signals(). If you add generic functions here that can be used to generate a nice diagram in what ever form you want in a python script will probably be approved. I could help.
  • Add json output to halcmd which can easily parse by most programming languages. I would say that's not needed, the python hal library is the better way.

Adding a few python graph generators into the repo in an appropriate location would probably also be fine.

Together with python hal -> graph generator -> render tool, this can be even integrated into an UI.

The drag mode is also realizable by improving python hal so all info can be gathered and then a python tool that does this. As much as I know, there are nice library's to do that.

@BsAtHome

BsAtHome commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

In principle, you need a "dump" of all modules/threads/functions/components/signal/pins/parameters with all the retained information in a structured format. Although halmodule may be interesting, it is not necessarily the right place. However, there may be a case for it (see below).
The current constructs in these tools require you to instantiate a (temporary) component, just to get access to the shared memory. This is something I am working on so it no longer is a requirement to create temporary components. It simplifies things significantly. Many functions, like getting/setting pins/params/signals do not need a component at all to work.
I already have a complete new query API in (user-space) hal_lib in my tree that lets you look at everything without the need for people poking around in HAL's internal structures (as part of complete removal of access to hal_priv.h). A wrapper could be implemented in halcmd to dump all content in a structured way. It can also be implemented as a query interface in halmodule when it no longer requires a (temporary) component to function. Then you can write a new program "haldump.py" if you like.

@hdiethelm

hdiethelm commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Hmm, is this an issue if you have to create a temporary halmodule? Many components do this like halshow / halcmd and so on.

Of course, having better separation would help and there is really no need for a component just to access shm.

A small python script that shows what is already available in hal python. For the get_info_pins() a DRIVER field is missing and there is also no get_info_threads(). But this could be done and then all should be there needed to create a graph in python.

import hal
import os

comp_name = f"halpy{os.getpid()}"
if not hal.is_initialized():
    comp = hal.component(comp_name)


print("pins-----------")
for pin in hal.get_info_pins():
    for k, v in pin.items():
        print(k, v)
    print()

print("signals-----------")
for sig in hal.get_info_signals():
    for k, v in sig.items():
        print(k, v)
    print()

print("params-----------")
for par in hal.get_info_params():
    for k, v in par.items():
        print(k, v)
    print()

@hdiethelm

hdiethelm commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

A small change in halmodule.cc and I think everything needed to generate a diagram from python is there. If hal.get_info_components() / hal.get_info_threads() is needed, this should also be possible. It would just expand an already existing pattern.

@BsAtHome Do you think this could be a way? Yes, it needs a component but for now, all modules accessing the HAL need one. If your changes gets in, the component creation could be dropped, done.

diff --git a/src/hal/halmodule.cc b/src/hal/halmodule.cc
index b0a132fd0c..8be61a160d 100644
--- a/src/hal/halmodule.cc
+++ b/src/hal/halmodule.cc
@@ -1554,10 +1554,12 @@ PyObject *get_info_pins(PyObject * /*self*/, PyObject * /*args*/) {
     static const char str_v[] = "VALUE";
     static const char str_t[] = "TYPE";
     static const char str_d[] = "DIRECTION";
+    static const char str_s[] = "SIGNAL";
     hal_data_u *d_ptr;
 
     hal_pin_t *pin;
     hal_sig_t *sig;
+    char* sig_name;
 
     PyObject* python_list = PyList_New(0);
     PyObject *obj;
@@ -1573,61 +1575,69 @@ PyObject *get_info_pins(PyObject * /*self*/, PyObject * /*args*/) {
         if (pin->signal != 0) {
             sig = (hal_sig_t*)SHMPTR(pin->signal);
             d_ptr = reinterpret_cast<hal_data_u *>(SHMPTR(sig->data_ptr));
+            sig_name = sig->name;
         } else {
             sig = NULL;
             d_ptr = &(pin->dummysig);
+            sig_name = (char*)"UNCONNECTED";
         }
-
         /* convert to dict of python values */
         switch(type) {
             case HAL_BIT:
-                obj = Py_BuildValue("{s:s,s:N,s:N,s:N}",
+                obj = Py_BuildValue("{s:s,s:N,s:N,s:N,s:s}",
                         str_n, pin->name,
                         str_v, PyBool_FromLong((long)d_ptr->b),
                         str_d, PyLong_FromLong(pin->dir),
-                        str_t, PyLong_FromLong(HAL_BIT));
+                        str_t, PyLong_FromLong(HAL_BIT),
+                        str_s, sig_name);
                 break;
             case HAL_U32:
-                obj = Py_BuildValue("{s:s,s:k,s:N,s:N}",
+                obj = Py_BuildValue("{s:s,s:k,s:N,s:N,s:s}",
                         str_n, pin->name,
                         str_v, (unsigned long)d_ptr->u,
                         str_d, PyLong_FromLong(pin->dir),
-                        str_t, PyLong_FromLong(HAL_U32));
+                        str_t, PyLong_FromLong(HAL_U32),
+                        str_s, sig_name);
                 break;
             case HAL_S32:
-                obj = Py_BuildValue("{s:s,s:l,s:N,s:N}",
+                obj = Py_BuildValue("{s:s,s:l,s:N,s:N,s:s}",
                         str_n, pin->name,
                         str_v, (long)d_ptr->s,
                         str_d, PyLong_FromLong(pin->dir),
-                        str_t, PyLong_FromLong(HAL_S32));
+                        str_t, PyLong_FromLong(HAL_S32),
+                        str_s, sig_name);
                 break;
             case HAL_U64:
-                obj = Py_BuildValue("{s:s,s:K,s:N,s:N}",
+                obj = Py_BuildValue("{s:s,s:K,s:N,s:N,s:s}",
                         str_n, pin->name,
                         str_v, (unsigned long long)d_ptr->lu,
                         str_d, PyLong_FromLong(pin->dir),
-                        str_t, PyLong_FromLong(HAL_S64));
+                        str_t, PyLong_FromLong(HAL_S64),
+                        str_s, sig_name);
                 break;
             case HAL_S64:
-                obj = Py_BuildValue("{s:s,s:L,s:N,s:N}",
+                obj = Py_BuildValue("{s:s,s:L,s:N,s:N,s:s}",
                         str_n, pin->name,
                         str_v, (long long)d_ptr->ls,
                         str_d, PyLong_FromLong(pin->dir),
-                        str_t, PyLong_FromLong(HAL_S64));
+                        str_t, PyLong_FromLong(HAL_S64),
+                        str_s, sig_name);
                 break;
             case HAL_FLOAT:
-                obj = Py_BuildValue("{s:s,s:d,s:N,s:N}",
+                obj = Py_BuildValue("{s:s,s:d,s:N,s:N,s:s}",
                         str_n, pin->name,
                         str_v, (double)d_ptr->f,
                         str_d, PyLong_FromLong(pin->dir),
-                        str_t, PyLong_FromLong(HAL_FLOAT));
+                        str_t, PyLong_FromLong(HAL_FLOAT),
+                        str_s, sig_name);
                 break;
             case HAL_PORT:
-                obj = Py_BuildValue("{s:s,s:l,s:N,s:N}",
+                obj = Py_BuildValue("{s:s,s:l,s:N,s:N,s:s}",
                         str_n, pin->name,
                         str_v, (long)d_ptr->p,
                         str_d, PyLong_FromLong(pin->dir),
-                        str_t, PyLong_FromLong(HAL_PORT));
+                        str_t, PyLong_FromLong(HAL_PORT),
+                        str_s, sig_name);
                 break;
             case HAL_TYPE_UNSPECIFIED: /* fallthrough */ ;
             case HAL_TYPE_UNINITIALIZED: /* fallthrough */ ;
@@ -1636,7 +1646,8 @@ PyObject *get_info_pins(PyObject * /*self*/, PyObject * /*args*/) {
                         str_n, pin->name,
                         str_v, NULL,
                         str_d, PyLong_FromLong(pin->dir),
-                        str_t, NULL);
+                        str_t, NULL,
+                        str_s, sig_name);
                  break;
         }

Python script:

import hal
import os

comp_name = f"halpy{os.getpid()}"
if not hal.is_initialized():
    comp = hal.component(comp_name)


print("pins-----------")
for pin in hal.get_info_pins():
    for k, v in pin.items():
        print(k, v, end='; ')
    print()

print("signals-----------")
for sig in hal.get_info_signals():
    for k, v in sig.items():
        print(k, v, end='; ')
    print()

print("paramteters-----------")
for par in hal.get_info_params():
    for k, v in par.items():
        print(k, v, end='; ')
    print()

Output:

pins-----------
NAME fast.time; VALUE 40; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
NAME lat.bj; VALUE 1731629; DIRECTION 16; TYPE 3; SIGNAL bj; 
NAME lat.bl; VALUE 1756629; DIRECTION 16; TYPE 3; SIGNAL bl; 
NAME lat.bt; VALUE 90; DIRECTION 16; TYPE 3; SIGNAL bt; 
NAME lat.reset; VALUE False; DIRECTION 32; TYPE 1; SIGNAL reset; 
NAME lat.sj; VALUE 1741692; DIRECTION 16; TYPE 3; SIGNAL sj; 
NAME lat.sl; VALUE 2741692; DIRECTION 16; TYPE 3; SIGNAL sl; 
NAME lat.st; VALUE 969250; DIRECTION 16; TYPE 3; SIGNAL st; 
NAME slow.time; VALUE 150; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
NAME timedelta.0.avg-err; VALUE -0.002530682045127165; DIRECTION 32; TYPE 2; SIGNAL UNCONNECTED; 
NAME timedelta.0.current-error; VALUE -24910; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
NAME timedelta.0.current-jitter; VALUE 24910; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
NAME timedelta.0.err; VALUE -52410; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
NAME timedelta.0.jitter; VALUE 1731629; DIRECTION 32; TYPE 3; SIGNAL bj; 
NAME timedelta.0.max; VALUE 1756629; DIRECTION 32; TYPE 3; SIGNAL bl; 
NAME timedelta.0.min; VALUE 79; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
NAME timedelta.0.out; VALUE 90; DIRECTION 32; TYPE 3; SIGNAL bt; 
NAME timedelta.0.reset; VALUE False; DIRECTION 16; TYPE 1; SIGNAL reset; 
NAME timedelta.0.time; VALUE 40; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
NAME timedelta.1.avg-err; VALUE -0.0012931562094168484; DIRECTION 32; TYPE 2; SIGNAL UNCONNECTED; 
NAME timedelta.1.current-error; VALUE -30750; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
NAME timedelta.1.current-jitter; VALUE 30750; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
NAME timedelta.1.err; VALUE -26781; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
NAME timedelta.1.jitter; VALUE 1741692; DIRECTION 32; TYPE 3; SIGNAL sj; 
NAME timedelta.1.max; VALUE 2741692; DIRECTION 32; TYPE 3; SIGNAL sl; 
NAME timedelta.1.min; VALUE 321; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
NAME timedelta.1.out; VALUE 969250; DIRECTION 32; TYPE 3; SIGNAL st; 
NAME timedelta.1.reset; VALUE False; DIRECTION 16; TYPE 1; SIGNAL reset; 
NAME timedelta.1.time; VALUE 150; DIRECTION 32; TYPE 3; SIGNAL UNCONNECTED; 
signals-----------
NAME bj; VALUE 1731629; DRIVER timedelta.0.jitter; TYPE 3; 
NAME bl; VALUE 1756629; DRIVER timedelta.0.max; TYPE 3; 
NAME bt; VALUE 90; DRIVER timedelta.0.out; TYPE 3; 
NAME reset; VALUE False; DRIVER lat.reset; TYPE 1; 
NAME sj; VALUE 1741692; DRIVER timedelta.1.jitter; TYPE 3; 
NAME sl; VALUE 2741692; DRIVER timedelta.1.max; TYPE 3; 
NAME st; VALUE 969250; DRIVER timedelta.1.out; TYPE 3; 
paramteters-----------
NAME fast.tmax; DIRECTION 192; VALUE 46206; TYPE 3; 
NAME slow.tmax; DIRECTION 192; VALUE 8265; TYPE 3; 
NAME timedelta.0.tmax; DIRECTION 192; VALUE 46206; TYPE 3; 
NAME timedelta.0.tmax-increased; DIRECTION 64; VALUE False; TYPE 1; 
NAME timedelta.1.tmax; DIRECTION 192; VALUE 8265; TYPE 3; 
NAME timedelta.1.tmax-increased; DIRECTION 64; VALUE False; TYPE 1;

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.

4 participants