From 6f3696f1756889c97b1bad1fdfef81eaed6ad05a Mon Sep 17 00:00:00 2001 From: Bertho Stultiens Date: Sat, 15 Aug 2026 16:11:58 +0200 Subject: [PATCH] hal: Add the HAL query API and add IntEnum types. --- debian/control.top.in | 1 + docs/src/config/python-hal-interface.adoc | 622 ++++++++++++++++------ docs/src/config/python-lcnc_realtime.adoc | 6 +- lib/python/hal.py | 7 + src/configure.ac | 10 + src/hal/Submakefile | 4 +- src/hal/halmodule.cc | 207 ++++++- src/hal/halqrec.hh | 106 ++++ src/hal/halquery.cc | 534 +++++++++++++++++++ tests/halmodule.3/expected | 60 +++ tests/halmodule.3/halquerytest.py | 73 +++ tests/halmodule.3/test.hal | 24 + 12 files changed, 1483 insertions(+), 171 deletions(-) create mode 100644 src/hal/halqrec.hh create mode 100644 src/hal/halquery.cc create mode 100644 tests/halmodule.3/expected create mode 100755 tests/halmodule.3/halquerytest.py create mode 100644 tests/halmodule.3/test.hal diff --git a/debian/control.top.in b/debian/control.top.in index 63539badee6..f503e9b9e7d 100644 --- a/debian/control.top.in +++ b/debian/control.top.in @@ -40,6 +40,7 @@ Build-Depends: psmisc, python3, python3-dev, + python3-pybind11, python3-tk, python3-xlib, tcl, diff --git a/docs/src/config/python-hal-interface.adoc b/docs/src/config/python-hal-interface.adoc index 1fe98f654f2..55f52eacf5f 100644 --- a/docs/src/config/python-hal-interface.adoc +++ b/docs/src/config/python-hal-interface.adoc @@ -10,8 +10,10 @@ a Python API for creating and accessing HAL pins and signals. [IMPORTANT] ==== The classes inside the Python module `hal` are layered on top of the module implementation called `_hal`. -You should only access the `hal` API as described in this document. -The inherited methods, members and properties from `_hal` may change without notice. +Many features described in this document are __**only**__ available in the `hal` Python module. + +You should only import and access the `hal` module API as described in this document. +The base-class methods, members, properties and values may change without notice. ==== == Basic usage @@ -22,14 +24,16 @@ The inherited methods, members and properties from `_hal` may change without not #!/usr/bin/env python3 import hal import time -h = hal.component("multiply") -h.newpin("in-a", hal.HAL_FLOAT, hal.HAL_IN) -h.newpin("in-b", hal.HAL_FLOAT, hal.HAL_IN) -h.newpin("out", hal.HAL_FLOAT, hal.HAL_OUT) -h.ready() + +comp = hal.component("multiply") +comp.newpin("in-a", hal.Type.REAL, hal.Dir.IN) +comp.newpin("in-b", hal.Type.REAL, hal.Dir.IN) +comp.newpin("out", hal.Type.REAL, hal.Dir.OUT) +comp.ready() + try: while True: - h['out'] = h['in-a'] * h['in-b'] + comp['out'] = comp['in-a'] * comp['in-b'] time.sleep(0.001) except KeyboardInterrupt: raise SystemExit @@ -37,9 +41,80 @@ except KeyboardInterrupt: == Class `hal` -=== hal constants +=== hal enumerations and constants + +There are many constants used to indicate type, direction, levels and much more. +These are usually integer values with a symbolic name. +It is preferable in code to use the symbolic name for readability and portability. + +Enumerations have been added to aid the readability even more. +They use the Python IntEnum base class. +The IntEnum values behave as numerical values, but add clear visibility features for readability. +The old defined constants are still supported for backwards compatibility. + +.Example: +[source,python] +---- +# Old (deprecated) style: +comp.newpin("in-a", hal.HAL_FLOAT, hal.HAL_IN) + +# New style: +comp.newpin("in-a", hal.Type.REAL, hal.Dir.IN) +---- + +==== HAL types + +The `hal.Type` enum is used to specify the type of pin, parameter and signal: -Message level constants: +* `hal.Type.BOOL` - A boolean using `True` and `False` +* `hal.Type.SINT` - A signed quantity with range -2^63^...+2^63^-1 +* `hal.Type.UINT` - An unsigned quantity with range 0...+2^64^-1 +* `hal.Type.REAL` - A floating point quantity of range ±1.80×10^308^ +* `hal.Type.PORT` - An opaque quantity representing a communication channel (pins only) +* `hal.Type.S32` - A signed quantity with range -2^31^...+2^31^-1 (see notes below) +* `hal.Type.U32` - An unsigned quantity with range 0...+2^32^-1 (see notes below) + +There are aliases in the `hal.Type` enum for all types with the `HAL_` prefix (e.g. `hal.Type.HAL_BOOL`, etc.). + +[NOTE] +==== +The constants previously used (`hal.HAL_BIT`, `hal.HAL_FLOAT`, etc.) are still available. +You should upgrade your code to use the enumerated types with the new names instead. +==== + +[IMPORTANT] +==== +The 32-bit types will soon be replaced by 64-bit types. +The names will remain for some time, but they will map to the larger type automatically. +The `S32` and `U32` names will then no longer be unique and the range will be larger. +==== + +==== HAL direction + +Just like types, `hal.Dir` enum is the enumeration to specify direction of pins and parameters. +The direction for both pin and parameter direction is unified in one enumerated type: + +* `hal.Dir.IN` - An input pin +* `hal.Dir.OUT` - An output pin +* `hal.Dir.IO` - A bidirections pin +* `hal.Dir.RW` - A read/write parameter +* `hal.Dir.RO` - A read-only parameter + +Also the direction enums are available with the `HAL_` prefix (like `hal.Dir.HAL_IN`). + +==== Old style HAL constants (deprecated) + +Old style constants (deprecated): +`hal.HAL_BIT`, `hal.HAL_S32`, `hal.HAL_U32`, `hal.HAL_S64`, `hal.HAL_U64`, `hal.HAL_FLOAT`, `hal.HAL_PORT`. + +Old style deprecated pin direction constants (deprecated): +`hal.HAL_IN`, `hal.HAL_OUT`, `hal.HAL_IO`. + +Old style deprecated parameter access constants (deprecated): +`hal.HAL_RO`, `hal.HAL_RW`. + +[[halconstantsmsg]] +==== Message level constants * `hal.MSG_NONE` - No messages at all * `hal.MSG_ERR` - Only errors @@ -48,17 +123,32 @@ Message level constants: * `hal.MSG_DBG` - Additionally include debugging information * `hal.MSG_ALL` - Print all messages encountered, disregarding level -Realtime type constants: +[[halconstantsrt]] +==== Realtime type enumerations + +The enumerated type `hal.RTType` is used in the `hal.get_realtime_type()` method. + +* `hal.RTType.UNINITIALIZED` - Real time module not running +* `hal.RTType.NONE` - No realtime available +* `hal.RTType.UNKNOWN` - Only used when LINUXCNC_FORCE_REALTIME=1 is set. Unknown, no PREEMPT_DYNAMIC but SCHED_FIFO is available. Not recommended. +* `hal.RTType.PREEMPT_DYNAMIC` - Preempt dynamic: available in vanilla kernel. Only used when LINUXCNC_FORCE_REALTIME=1 is set. Not recommended. +* `hal.RTType.PREEMPT_RT` - Preempt RT +* `hal.RTType.RTAI` - RTAI kernel mode +* `hal.RTType.LXRT` - LXRT, userspace implementation for RTAI +* `hal.RTType.XENOMAI` - Xenomai 3 +* `hal.RTType.XENOMAI_EVL` - Xenomai 4 aka Xenomai EVL + +The realtime enumerations are also available as constants under the `hal.REALTIME_TYPE_xxx` name where `xxx` is one of the above. -* `hal.REALTIME_TYPE_UNINITIALIZED` - Real time module not running -* `hal.REALTIME_TYPE_NONE` - No realtime available -* `hal.REALTIME_TYPE_UNKNOWN` - Only used when LINUXCNC_FORCE_REALTIME=1 is set. Unknown, no PREEMPT_DYNAMIC but SCHED_FIFO is available. Not recommended. -* `hal.REALTIME_TYPE_PREEMPT_DYNAMIC` - Preempt dynamic: available in vanilla kernel. Only used when LINUXCNC_FORCE_REALTIME=1 is set. Not recommended. -* `hal.REALTIME_TYPE_PREEMPT_RT` - Preempt RT -* `hal.REALTIME_TYPE_RTAI` - RTAI kernel mode -* `hal.REALTIME_TYPE_LXRT` - LXRT, userspace implementation for RTAI -* `hal.REALTIME_TYPE_XENOMAI` - Xenomai 3 -* `hal.REALTIME_TYPE_XENOMAI_EVL` - Xenomai 4 aka Xenomai EVL +==== Component type enumeration + +The enumerated type `hal.CompType` is used in `hal.query.comp()` and `hal.query.comps()`. + +* `hal.CompType.REALTIME` - a realtime component +* `hal.CompType.USER` - a user non-realtime component +* `hal.CompType.OTHER` - (HAL internal value; should normally not be visible) + +==== Other constants System information: @@ -66,46 +156,39 @@ System information: * `hal.is_userspace` - Inverted `hal.is_kernelspace` * `hal.kernel_version` - A string specifying the real-time kernel version if `hal.is_kernelspace` is one. Otherwise it specifies `"Not Available"`. -* `hal.is_rt` - One (1) if the system runs in real-time, otherwise zero (0) *DEPRECATED*: Use `lcnc_realtime.verify()` -* `hal.is_sim` - Inverted `hal.is_rt` *DEPRECATED*: Use `lcnc_realtime.verify()` +* `hal.is_rt` - One (1) if the system runs in real-time, otherwise zero (0) *DEPRECATED*: + Use xref:python-lcnc_realtime.adoc[`lcnc_realtime.verify()`] +* `hal.is_sim` - Inverted `hal.is_rt` *DEPRECATED*: + Use xref:python-lcnc_realtime.adoc[`lcnc_realtime.verify()`] -=== hal methods -hal.is_initialized():: -Returns a boolean to indicate whether hal is initialized. The hal is initialized when there is -at least one component. If this is not the case, many of the following functions will throw -a `RuntimeError` exception: `Cannot call before creating component` +=== hal methods +hal.is_initialized() -> bool:: +Returns a boolean to indicate whether hal is initialized. +This methods should *always* return `True` because the HAL module is automatically initialized at import time. ++ .Example: [source,python] ---- -#The hal must be initialized to use many of the hal functions -#If it is not initialized, create a component which will initialize it -#The component will be cleaned up after comp goes out of scope -#Use pid as component identifier, so it is unlikely to collide -#with existing components -comp_name = f"halpy{os.getpid()}" -if not hal.is_initialized(): - comp = hal.component(comp_name) +import hal -type = hal.get_realtime_type() +assert hal.is_initialized(), "fatal: HAL failed to initialize at import" ---- -hal.get_realtime_type():: +hal.get_realtime_type() -> hal.RTType:: Returns the type of the running realtime system. -Might return `hal.REALTIME_TYPE_UNINITIALIZED` if `rtapi_app` is not running. -See xref:_hal_constants[realtime type constants]. + -Throws a `RuntimeError` exception if HAL is not initialized. +Might return `hal.RTType.UNINITIALIZED` if `rtapi_app` is not running. +See xref:halconstantsrt[realtime type constants]. +See also xref:python-lcnc_realtime.adoc[LinuxCNC Realtime check]. -hal.component_exists(_name_:string):: -Returns a boolean to indicate whether or not the specified component exist at this time. + -Throws a `RuntimeError` exception if HAL is not initialized. +hal.component_exists(_name_:string) -> bool:: +Returns a boolean to indicate whether or not the specified component exist at this time. -hal.component_is_ready(_name_:string):: +hal.component_is_ready(_name_:string) -> bool:: Returns a boolean to indicate whether or not the specified component is in the ready state. -Also returns False if the component does not exist. + -Throws a `RuntimeError` exception if HAL is not initialized. - +Also returns False if the component does not exist. ++ .Example: [source,python] ---- @@ -117,31 +200,29 @@ if not hal.component_is_ready("testpanel"): hal.set_msg_level(_lvl_:int):: Set the message level that controls the amount of information being printed and forwarded from real-time components. -The _lvl_ argument must be one of the xref:_hal_constants[message constants]. +The _lvl_ argument must be one of the xref:halconstantsmsg[message constants]. -hal.get_msg_level():: +hal.get_msg_level() -> int:: Return the current message level. -See 'hal.set_msg_level()' and xref:_hal_constants[message constants] for list of possible returned values. +See 'hal.set_msg_level()' and xref:halconstantsmsg[message constants] for list of possible returned values. -hal.new_sig(_name_:string, _type_:enum):: +hal.new_sig(_name_:string, _type_:enum) -> bool:: Create a new signal (net) called _name_. The signal can carry information of _type_ content. -Returns `True` on success. + -Throws a `RuntimeError` exception if HAL is not initialized. - +Returns `True` on success. ++ .Example: [source,python] ---- -if not hal.new_sig("signalname", hal.HAL_BIT): +if not hal.new_sig("signalname", hal.Type.BOOL): ...handle error... ---- -hal.connect(_pinname_:string, _signame_:string):: +hal.connect(_pinname_:string, _signame_:string) -> bool:: Connect the pin _pinname_ to signal _signame_. Both signal and pin must exist and both pin and signal must be of the same type. -Returns `True` on success. + -Throws a `RuntimeError` exception if HAL is not initialized. - +Returns `True` on success. ++ .Example: [source,python] ---- @@ -149,12 +230,11 @@ if not hal.connect("mycomp.pinname", "signalname"): ...handle error... ---- -hal.disconnect(_pinname_:string, _signame_:string):: +hal.disconnect(_pinname_:string, _signame_:string) -> bool:: Disconnect the pin _pinname_ from signal _signame_. Both signal and pin must exist. -Returns `True` on success. + -Throws a `RuntimeError` exception if HAL is not initialized. - +Returns `True` on success. ++ .Example: [source,python] ---- @@ -162,16 +242,15 @@ if not hal.disconnect("mycomp.pinname"): ...handle error... ---- -hal.pin_has_writer(_pinname_:string):: +hal.pin_has_writer(_pinname_:string) -> bool:: Returns `True` if pin with name _pinname_ is attached to a signal and there is at least one writer. -Otherwise, `False` is returned. + -Throws a `RuntimeError` exception if HAL is not initialized. - +Otherwise, `False` is returned. ++ .Example: [source,python] ---- if hal.pin_has_writer("mycomp.0.pin.02"): - print("Pin has writer(s)") + print("Pin has writer") else: print("Pin has no writer or no signal attached") ---- @@ -181,7 +260,6 @@ Sets the pin or param called _name_ to _value_. The _name_ is the full name of the pin or param. The search order is pin names first, then parameter names. + Throws a `RuntimeError` exception if the _name_ is not found. + -Throws a `RuntimeError` exception if HAL is not initialized. + The type of _value_ depends on the type of the pin or param. Integer scalar types may use integers or a textual representation of an integer to set the value. Floating point type may use both integer, floating point and textual representation thereof to set the value. @@ -189,7 +267,7 @@ Booleans accept `True`, `False` and map the integer value zero (0) to false. The exact floating point value of zero (0.0) also maps for false. Booleans may also be of text "0", "1", "on", "off", "true", "false", "yes" or "no". Textual representations are case insensitive. - ++ .Example: [source,python] ---- @@ -201,94 +279,79 @@ hal.set_s(_name_:string, _value_:mixed):: Sets the signal (net) called _name_ to _value_. The same rules for _value_ apply to 'set_s()' as to 'set_p()'. + + -The 'set_s()' method has one special case when the signal is of type `hal.HAL_PORT` and it is fully connected. +The 'set_s()' method has one special case when the signal is of type `hal.Type.PORT` and it is fully connected. In that case, the call uses the _value_ to set the port's queue size and it must be a positive integer. -See below xref:_hal_port_pipes[on configuring a port]. + -Throws a `RuntimeError` exception if HAL is not initialized. +See below xref:_hal_port_pipes[on configuring a port]. -hal.get_p(_name_:string):: +hal.get_p(_name_:string) -> bool|int|float|None:: Returns the value of the pin or param with _name_. If _name_ refers to a pin and that pin is connected, then the connected signal's value is returned. Boolean types return `True` or `False`. Integer scalar types return an integer. Floating point types return a float. -A `RuntimeError` exception is thrown if no pin or param is found by that name. + -Throws a `RuntimeError` exception if HAL is not initialized. +The value `None` is returned if no pin or param is found by that name. -hal.get_s(_name_:string):: +hal.get_s(_name_:string) -> bool|int|float|None:: Returns the value of the signal with _name_. Boolean types return `True` or `False`. Integer scalar types return an integer. Floating point types return a float. -A `RuntimeError` exception is thrown if no signal is found by that name. + -Throws a `RuntimeError` exception if HAL is not initialized. +The value `None` is returned if no signal is found by that name. -hal.get_value(_name_:string):: +hal.get_value(_name_:string) -> bool|int|float:: Returns the value of the pin, param or signal with _name_, searched in that order. Boolean types return `True` or `False`. Integer scalar types return an integer. Floating point types return a float. -A `RuntimeError` exception is thrown if no pin, param or signal is found by that name. + -Throws a `RuntimeError` exception if HAL is not initialized. - +A `RuntimeError` exception is thrown if no pin, param or signal is found by that name. ++ .Example: [source,python] ---- value = hal.get_value("iocontrol.0.emc-enable-in") ---- -hal.get_info_pins():: -Returns a list of dictionary tuples as in `{"NAME":"pinname", "VALUE":, "TYPE":, "DIRECTION":}`. + -Throws a `RuntimeError` exception if HAL is not initialized. +hal.get_info_pins() -> list(dict):: +*DEPRECATED*: replaced by xref:halquerypins[`hal.query.pins()`]. + +Returns a list of dictionary tuples as in `{"NAME":"pinname", "VALUE":, "TYPE":, "DIRECTION":}`. ++ +.Example: +[source,python] +---- +# Old style (deprecated): +for i in hal.get_info_pins(): + print(i['NAME'], i['TYPE'], i['DIRECTION'], i['VALUE']) -hal.get_info_params():: -Returns a list of dictionary tuples as in `{"NAME":"paramname", "VALUE":, "TYPE":, "DIRECTION":}`. + -Throws a `RuntimeError` exception if HAL is not initialized. +# New style (see hal.query sub-module below): +for pinname,pindetail in hal.query.pins().items(): + print(pinname, pindetail['type'], pindetail['dir'], pindetail['value']) +---- -hal.get_info_signals():: -Returns a list of dictionary tuples as in `{"NAME":"signame", "VALUE":, "TYPE":, "DRIVER":"name"|None}`. + -Throws a `RuntimeError` exception if HAL is not initialized. +hal.get_info_params() -> list(dict):: +*DEPRECATED*: replaced by xref:halqueryparams[`hal.query.params()`]. + +Returns a list of dictionary tuples as in `{"NAME":"paramname", "VALUE":, "TYPE":, "DIRECTION":}`. +hal.get_info_signals() -> list(dict):: +*DEPRECATED*: replaced by xref:halquerysignals[`hal.query.signals()`]. + +Returns a list of dictionary tuples as in `{"NAME":"signame", "VALUE":, "TYPE":, "DRIVER":"name"|None}`. ++ .Example: [source,python] ---- -for pin in hal.get_info_pins(): - for k, v in pin.items(): - print(k, v) +# New style (see hal.query sub-module below) +for signame,sigdetail in hal.query.signals().items(): + print(signame, sigdetail['type'], sigdetail['value'], sigdetail['driver']) ---- == Class `hal.component` -=== component constants - -Type constants: - -* `hal.HAL_BIT` - A boolean using `True` and `False` -* `hal.HAL_S32` - A signed quantity with range -2^31^...+2^31^-1 -* `hal.HAL_U32` - An unsigned quantity with range 0...+2^32^-1 -* `hal.HAL_S64` - A signed quantity with range -2^63^...+2^63^-1 -* `hal.HAL_U64` - An unsigned quantity with range 0...+2^64^-1 -* `hal.HAL_FLOAT` - A floating point quantity of range ±1.80×10^308^ -* `hal.HAL_PORT` - An opaque quantity representing a communication channel (pins only) - -Pin direction constants: - -* `hal.HAL_IN` - An input pin -* `hal.HAL_OUT` - An output pin -* `hal.HAL_IO` - A bidirections pin - -Parameter access constants: - -* `hal.HAL_RO` - A read-only param (cannot be set by other components) -* `hal.HAL_RW` - A read-write param - === component methods comp = hal.component(_name_:string [, _prefix_:string]):: The component itself is created by a call to the constructor `hal.component`. The arguments are the HAL component _name_ and (optionally) the _prefix_ used for pin and param names. If the prefix is not specified, the component name is used. - ++ .Example: [source,python] ---- @@ -300,44 +363,44 @@ Create new pin with actual name `prefix.__name__`. The pin _type_ must be one of the types described above. The _io_ specifies the direction of the pin. Throws a `ValueError` exception if _name_ already exists. - ++ .Example: [source,python] ---- -p_in = comp.newpin("in", hal.HAL_FLOAT, hal.HAL_IN) +p_in = comp.newpin("in", hal.Type.FLOAT, hal.Dir.IN) ---- param = comp.newparam(_name_:string, _type_:enum, _access_:enum):: Create new parameter with actual name `prefix.__name__`. The pin _type_ argument must be one of the types described above. -Parameters cannot be of type `hal.HAL_PORT`. +Parameters cannot be of type `hal.Type.PORT`. The _access_ argument specifies the allowed param access. Throws a `ValueError` exception if _name_ already exists. - ++ .Example: [source,python] ---- -p_bloop = comp.newparam("bloop", hal.HAL_FLOAT, hal.HAL_RO) +p_bloop = comp.newparam("bloop", hal.Type.FLOAT, hal.Dir.RO) ---- -comp.getitem(_name_:string):: +comp.getitem(_name_:string) -> object:: Return the pin or param item object _name_ previously created with 'comp.newpin()' or 'comp.newparam()'. Throws an `AttributeError` exception if no pin or param named _name_ is found. Use 'comp.getpin()' or 'comp.getparam()' to find the specific type. -comp.getpin(_pinname_:string):: +comp.getpin(_pinname_:string) -> object:: Return the pin item object _pinname_ previously created with 'comp.newpin()'. Throws an `AttributeError` exception if no pin names _pinname_ is found. A param called _pinname_ will not be found and throws an exception. Use 'comp.getitem()' to find either. -comp.getparam(_paramname_:string):: +comp.getparam(_paramname_:string) -> object:: Return the param item object _paramname_ previously created with 'copm.newparam()'. Throws an `AttributeError` exception if no pin names _paramname_ is found. A pin called _paramname_ will not be found and throws an exception. Use 'comp.getitem()' to find either. -comp.getpins():: +comp.getpins() -> dict:: Returns a dictionary all pin and param names and their values. The pin or param name is the dictionary key. @@ -348,7 +411,7 @@ comp.unready():: Allows a component to add pins after 'ready()' has been called. One should call 'ready()' on the component when done. -comp.getprefix():: +comp.getprefix() -> string:: Returns the current component's prefix used when creating pins and params. It defaults to the component name when not set in the constructor or by 'setprefix'. @@ -450,7 +513,7 @@ Attach to a stream for the component _comp_ where the shared memory location is If the optional _typestr_ is provided, then it will be checked against the existing stream's configuration. See the xref:type-string[type string] list for type meaning. -stream.read():: +stream.read() -> tuple|None:: Returns a tuple of sample data from the queue. `None` is returned if no samples were available. stream.write(_sample_:tuple):: @@ -458,26 +521,26 @@ Writes the _sample_ argument to the stream queue. The _sample_ must contain the correct number of elements and match the types (or be convertible) of the stream's configuration. An `IOError` exception is thrown if the sample could not be written to the queue. -stream.readable():: +stream.readable() -> bool:: Returns `True` if there are samples available for reading in the stream queue or `False` if not. -stream.writable():: +stream.writable() -> bool:: Returns `True` if there is space available in the stream queue to hold more samples or `False` if not. -stream.depth():: +stream.depth() -> int:: Returns the number of currently available samples for read in the queue. -stream.maxdepth():: +stream.maxdepth() -> int:: Returns the queue size as set when the stream was created (and cannot be changed). -stream.element_types():: +stream.element_types() -> int:: Return a bytes object with the format type string that was used to create the stream. See xref:type-string[type string] list for individual elements and type meaning. -stream.num_underruns():: +stream.num_underruns() -> int:: Returns the number of times 'stream.read()' was called when no samples were available from the queue. -stream.num_overruns():: +stream.num_overruns() -> int:: Returns the number of times 'stream.write()' was called when no samples could be stored in the queue. === stream members @@ -485,6 +548,265 @@ Returns the number of times 'stream.write()' was called when no samples could be stream.sampleno:: The last successfully read sample ID number as counted by the stream functions. +== Sub-module `hal.query` + +The `hal.query` sub-module is for getting information about all of HAL's internal structures. +These include: + +* Pins +* Parameters +* Signals +* Components +* Functions +* Threads + +Each category can be queried by name or ID to get information about the specific item or you can get them all. +The methods in this sub-module return a dictionary with all available information. + +=== hal.query methods + +[[halquerypin]]hal.query.pin(_name_:string) -> dict|None:: + Retrieve all information of the pin _name_. + Returns `None` if the _name_ was not found. + Otherwise, returns a dictionary with the pin information: ++ +[source,python] +---- +result = hal.query.pin("my.pin.name") +result = { + "haltype" : "pin", + "name" : "my.pin.name", + "type" : hal.Type., + "dir" : hal.Dir., + "value" : , + "alias" : "alias.name"|None, + "signal" : "signal.name"|None, + "comp" : "component-name", + "comp_id" : +} +---- + +[[halqueryparam]]hal.query.param(_name_:string) -> dict|None:: + Retrieve all information of the parameter _name_. + Returns `None` if the _name_ was not found. + Otherwise, returns a dictionary with the parameter information: ++ +[source,python] +---- +result = hal.query.param("my.param.name") +result = { + "haltype" : "parameter", + "name" : "my.param.name", + "type" : hal.Type., + "dir" : hal.Dir., + "value" : , + "alias" : "alias.name"|None, + "comp" : "component-name", + "comp_id" : +} +---- + +[[halquerysignal]]hal.query.signal(_name_:string) -> dict|None:: + Retrieve all information of the signal _name_. + Returns `None` if the _name_ was not found. + Otherwise, returns a dictionary with the signal information: ++ +[source,python] +---- +result = hal.query.signal("my.signal.name") +result = { + "haltype" : "signal", + "name" : "my.signal.name", + "type" : hal.Type., + "value" : , + "writers" : , + "readers" : , + "bidirs" : , + "driver" : "driver.pin.name"|None +} +---- + The `readers`, `writers` and `bidirs` indicate how many pins are connected. + There can only be one `writers` and many `readers`. + Or, there can be many `bidirs` and many `readers`. + The `driver` is the connected pin name of type `hal.Dir.OUT` or `None` if `writers` is zero. + +[[halquerycomp]]hal.query.comp(_name_:string) -> dict|None:: +dict|None = hal.query.comp(_id_:int):: + Retrieve all information of the component _name_ or by integer component _id_. + Returns `None` if the _name_ or _id_ was not found. + Otherwise, returns a dictionary with the component information: ++ +[source,python] +---- +result = hal.query.comp("component-name") +result = { + "haltype" : "component", + "name" : "component-name", + "type" : hal.CompType. + "id" : + "pid" : |0 + "ready" : True|False, + "insmod" : "comp command line options" +} +---- + The `pid` field is set to zero (0) for realtime components and the process ID for user-space components. + The `insmod` field is the loadrt command line (excluding loadrt) and is only set for realtime components. + The `type` field is currently only either `hal.CompType.REALTIME` or `hal.CompType.USER`. + +[[halqueryfunct]]hal.query.funct(_name_:string) -> dict|None:: + Retrieve all information of the function _name_. + Returns `None` if the _name_ was not found. + Otherwise, returns a dictionary with the function information: ++ +[source,python] +---- +result = hal.query.funct("my.funct.name") +result = { + "haltype" : "function", + "name" : "my.funct.name", + "comp" : "component-name", + "comp_id" : , + "users" : , + "reentrant" : True|False +} +---- + The `users` field indicates how many threads use this function. + Only functions that have `reentrant` set to `True` can have more than one user. + +[[halquerythread]]hal.query.thread(_name_:string) -> dict|None:: + Retrieve all information of the thread _name_. + Returns `None` if the _name_ was not found. + Otherwise, returns a dictionary with the thread information: ++ +[source,python] +---- +result = hal.query.thread("my.thread.name") +result = { + "haltype" : "thread", + "name" : "my.thread.name", + "comp" : "component-name", + "comp_id" : , + "priority" : , + "period" : , # in nanoseconds + "functions" : ( + { + "haltype" : "threadfunction", + "name" : "my.funct.name", + "index" : , + "is_init" : True|False + }, + ... + ) +} +---- + The `functions` field is a list of dictionaries. + Each entry describes a function that runs in the thread. + The order of execution within the thread is indicated by zero-based `index`. + Any function in the `functions` list should have the corresponding `users` field on a `hal.query.funct(name)` set. + + The `is_init` field can only contain `True` if the thread was setup but never run. + Init functions are automatically removed once they have executed once and never show up again. + +hal.query.signalpins(_name_:string) -> dict:: + Retrieve information about all pins connected to signal _name_. + Returns `None` if the signal _name_ is not found. + The dictionary returned may be empty and has the format: ++ +[source,python] +---- +result = hal.query.signalpins("my.signal") +result = { + "pin-r" : { "haltype": "pin", "name": "pin-r", "dir": hal.Dir.IN "signal": "my.signal", ...}, + "pin-w" : { "haltype": "pin", "name": "pin-w", "dir": hal.Dir.OUT "signal": "my.signal", ...}, + "pin-x" : { "haltype": "pin", ...}, + ... +} +---- + See xref:halquerypin[`hal.query.pin()`] for description for pin dictionary details. + +[[halquerypins]]hal.query.pins() -> dict:: + Retrieve information about all pins. + The dictionary returned has the format: ++ +[source,python] +---- +{ + "pin-a" : { "haltype": "pin", "name": "pin-a", "type":...}, + "pin-b" : { "haltype": "pin", ...}, + ... +} +---- + See xref:halquerypin[`hal.query.pin()`] for description of the pin dictionary details. + +[[halqueryparams]]hal.query.params() -> dict:: + Retrieve information about all params. + The dictionary returned has the format: ++ +[source,python] +---- +{ + "param-a" : { "haltype": "parameter", "name": "param-a", "type":...}, + "param-b" : { "haltype": "parameter", ...}, + ... +} +---- + See xref:halqueryparam[`hal.query.param()`] for description of the param dictionary details. + +[[halquerysignals]]hal.query.signals() -> dict:: + Retrieve information about all signals. + The dictionary returned has the format: ++ +[source,python] +---- +{ + "sig-a" : { "haltype": "signal", "name": "sig-a", "type":...}, + "sig-b" : { "haltype": "signal", ...}, + ... +} +---- + See `hal.query.signal()` for description of the signal dictionary details. + +hal.query.comps() -> dict:: + Retrieve information about all components. + The dictionary returned has the format: ++ +[source,python] +---- +{ + "comp-a" : { "haltype": "component", "name": "comp-a", "type":...}, + "comp-b" : { "haltype": "component", ...}, + ... +} +---- + See xref:halquerycomp[`hal.query.comp()`] for description of the component dictionary details. + +hal.query.functs() -> dict:: + Retrieve information about all functions. + The dictionary returned has the format: ++ +[source,python] +---- +{ + "funct-a" : { "haltype": "function", "name": "funct-a", ...}, + "funct-b" : { "haltype": "function", ...}, + ... +} +---- + See xref:halqueryfunct[`hal.query.funct()`] for description of function dictionary details. + +hal.query.threads() -> dict:: + Retrieve information about all threads. + The dictionary returned has the format: ++ +[source,python] +---- +{ + "thread-a" : { "haltype": "thread", "name": "thread-a", ...}, + "thread-b" : { "haltype": "thread", ...}, + ... +} +---- + See xref:halquerythread[`hal.query.thread()`] for description of thread dictionary details. + == Class `hal.shm` The `shm` class is an interface to create and manage shared memory segments. @@ -513,7 +835,7 @@ A HAL port is a byte-oriented pipe that streams bytes from the writer to the rea It may be used to transport data between real-time and non-real-time in either direction. The HAL port pipe facility is distinct from the HAL stream facility in that it has no concept of typed data. The reader and writer of a port must handle a binary byte oriented data-stream. -A HAL port uses HAL pins of type `hal.HAL_PORT` to communicate. +A HAL port uses HAL pins of type `hal.Type.PORT` to communicate. .Example - HAL port data writer: [source,python] @@ -525,10 +847,10 @@ import struct comp = hal.component("portwriter") # Create the write-end -portw = comp.newpin("portpin", hal.HAL_PORT, hal.HAL_OUT) +portw = comp.newpin("portpin", hal.Type.PORT, hal.Dir.OUT) # Create a signal to link reader and writer -portsig = hal.new_sig("portsig", hal.HAL_PORT) +portsig = hal.new_sig("portsig", hal.Type.PORT) portw.ready() # Connect the pins to the signal net @@ -608,25 +930,26 @@ You should analyze your usage and find the appropriate size to set. === Port pin methods -port = comp.newpin(_name_:string, hal.HAL_PORT, _io_:enum):: +port = comp.newpin(_name_:string, hal.Type.PORT, _io_:enum):: A port is a special pin type. -It is created as a normal pin with type `hal.HAL_PORT`. +It is created as a normal pin with type `hal.Type.PORT`. You need two pins for a port, one input and one output. Both ends of a port are connected with a signal (net). Setting the signal will allocate the port's queue. -port.readable():: +port.readable() -> int:: Returns the number of bytes available for reading from the port queue. -port.writable():: +port.writable() -> int:: Returns the number of bytes possible to write to the port queue. -port.write(_data_:bytes):: +port.write(_data_:bytes) -> bool:: Write a buffer onto the port queue. The argument _data_ can be either a bytes buffer or a string. If it is a string, then it will be converted to a UTF-8 bytes buffer. Returns `True` if the _data_ was successfully written to the port queue and `False` if not. The 'write()' call fails if the port queue has not enough free capacity for the entire _data_ argument. ++ [NOTE] ==== You should be careful when writing a string. @@ -634,17 +957,17 @@ Strings are Unicode encoded and may expand to multiple bytes when converted to U Therefore, the length of the string may not match the length of the UTF-8 bytes buffer and not fit into the queue. ==== -port.read(_size_:int):: +port.read(_size_:int) -> bytes:: Reads _size_ bytes from the port and removes the bytes from the port queue. The port is tested whether _size_ bytes are available before attempting to read. Returns a bytes buffer upon success or `False` on failure. -port.peek(_size_:int):: +port.peek(_size_:int) -> bytes:: Reads _size_ bytes from the port without removing the bytes from the port queue. The port is tested whether _size_ bytes are available before attempting to peek. Returns a bytes buffer upon success or `False` on failure. -port.peek_commit(_size_:int):: +port.peek_commit(_size_:int) -> bool:: Removes _size_ bytes from the port queue. Returns `True` on success or `False` if not. @@ -654,7 +977,6 @@ Returns `True` on success or `False` if not. def wait_for(n): while port.readable() < n: time.sleep(0.01) - continue wait_for(2) # Peek at the first 2 bytes of the data pipe @@ -672,6 +994,6 @@ else: port.clear():: Clears the content of the port queue. -port.size():: +port.size() -> int:: Return the allocated port queue size. The return value is zero (0) if no queue was allocated for the port. diff --git a/docs/src/config/python-lcnc_realtime.adoc b/docs/src/config/python-lcnc_realtime.adoc index b576f5e40a4..e98b03c9b61 100644 --- a/docs/src/config/python-lcnc_realtime.adoc +++ b/docs/src/config/python-lcnc_realtime.adoc @@ -23,14 +23,14 @@ print("lcnc_realtime.status " + str(lcnc_realtime.status())) == methods -lcnc_realtime.verify():: +lcnc_realtime.verify() -> bool:: Returns a boolean to indicate whether the system is realtime capable. -lcnc_realtime.verify_info():: +lcnc_realtime.verify_info() -> tuple[bool, str]:: Returns a tuple `(capable, type)`, where `capable` is the same boolean as `verify()` and `type` is a human-readable string naming the realtime type that rtapi is running (for example `Preempt RT`, `RTAI` or `Xenomai`), or `No realtime` when none is available. -lcnc_realtime.status():: +lcnc_realtime.status() -> bool:: Returns a boolean to indicate whether the realtime backend is running. diff --git a/lib/python/hal.py b/lib/python/hal.py index e800dd3dcdf..68fa796eebc 100644 --- a/lib/python/hal.py +++ b/lib/python/hal.py @@ -29,9 +29,16 @@ import _hal from _hal import * +from _hal import query +import sys import warnings import lcnc_realtime +# _hal.query is a submodule, not a plain attribute. Registering it in +# sys.modules under its dotted name makes 'import hal.query' and 'from hal +# import query' work as they would for a real package. +sys.modules['hal.query'] = query + def __getattr__(name): if name == 'is_rt': warnings.warn(f"{name} is deprecated, use lcnc_realtime.verify() instead", FutureWarning, stacklevel=2) diff --git a/src/configure.ac b/src/configure.ac index 03bf65e37e1..beab00651e4 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -1567,6 +1567,16 @@ if test "x$HAVE_LIBFMT" = "xno"; then AC_MSG_ERROR([libfmt header not found. Install with 'sudo apt-get install libfmt-dev']) fi +AC_LANG_PUSH([C++]) +CPPFLAGS_SAVED="$CPPFLAGS" +CPPFLAGS="$PYTHON_CPPFLAGS $CPPFLAGS" +AC_CHECK_HEADERS([pybind11/pybind11.h],[HAVE_PYBIND11=yes],[HAVE_PYBIND11=no]) +CPPFLAGS="$CPPFLAGS_SAVED" +AC_LANG_POP([C++]) +if test "x$HAVE_PYBIND11" = "xno"; then + AC_MSG_ERROR([pybind11 header not found. Install with 'sudo apt-get install python3-pybind11']) +fi + AC_CHECK_HEADERS([sys/capability.h], [], [ AC_MSG_ERROR([libcap header not found. Please install libcap-dev]) ]) diff --git a/src/hal/Submakefile b/src/hal/Submakefile index 1827d7e10c8..851e2c88881 100644 --- a/src/hal/Submakefile +++ b/src/hal/Submakefile @@ -20,13 +20,13 @@ $(HALLIB).0: $(call TOOBJS, $(HALLIBSRCS)) @rm -f $@ $(Q)$(CC) $(LDFLAGS) -Wl,-soname,$(notdir $@) -shared -o $@ $^ $(HALLIB_LIBS) $(ULAPI_LDFLAGS) -HALMODULESRCS := hal/halmodule.cc hal/utils/setps_util.c +HALMODULESRCS := hal/halmodule.cc hal/utils/setps_util.c hal/halquery.cc PYSRCS += $(HALMODULESRCS) HALMODULE := ../lib/python/_hal.so $(HALMODULE): $(call TOOBJS, $(HALMODULESRCS)) $(HALLIB) $(ECHO) Linking python module $(notdir $@) - $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ + $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ -lfmt TARGETS += $(HALLIB) ../lib/liblinuxcnchal.so.0 PYTARGETS += $(HALMODULE) diff --git a/src/hal/halmodule.cc b/src/hal/halmodule.cc index 4fe9f7165e0..115dc28fa9a 100644 --- a/src/hal/halmodule.cc +++ b/src/hal/halmodule.cc @@ -26,6 +26,8 @@ #include #include #include + +#include "halqrec.hh" #include "utils/setps_util.h" #define EXCEPTION_IF_NOT_LIVE(retval) do { \ @@ -1383,7 +1385,11 @@ PyObject *get_p(PyObject * /*self*/, PyObject *args) int rv = hal_get_p(&q, NULL, NULL); if(0 == rv) return halref_to_object(q.pp.type, &q.pp.value); - // Get here: most likely no pin/param with that name + if(-ENOENT == rv) { + Py_INCREF(Py_None); + return Py_None; + } + // Get here: most likely a serious error PyErr_Format(PyExc_RuntimeError, "get_p: %s: %s", q.name, hal_strerror(rv)); return NULL; } @@ -1400,7 +1406,11 @@ PyObject *get_s(PyObject * /*self*/, PyObject *args) int rv = hal_get_s(&q, NULL, NULL); if(0 == rv) return halref_to_object(q.sig.type, &q.sig.value); - // Get here: most likely no signal with that name + if(-ENOENT == rv) { + Py_INCREF(Py_None); + return Py_None; + } + // Get here: most likely a serious error PyErr_Format(PyExc_RuntimeError, "get_s: %s: %s", q.name, hal_strerror(rv)); return NULL; } @@ -1432,9 +1442,8 @@ PyObject *get_value(PyObject * /*self*/, PyObject *args) /*######################################*/ /* Get a dict of pin info for all pins in system */ -static int pinparaminfo_cb(hal_query_t *q, void *arg) +static int pinparaminfo_add(const hal_query_t *q, PyObject *lst) { - PyObject *lst = static_cast(arg); PyObject *obj; static const char str_n[] = "NAME"; static const char str_v[] = "VALUE"; @@ -1490,15 +1499,19 @@ static int pinparaminfo_cb(hal_query_t *q, void *arg) PyObject *get_info_pins(PyObject * /*self*/, PyObject * /*args*/) { - PyObject* python_list = PyList_New(0); + HalQRec qrec(1024); // We normally have many pins hal_query_t q = {}; q.qtype = HAL_QTYPE_PIN; // Only handle pins - int rv = hal_list_p(&q, pinparaminfo_cb, python_list); + int rv = hal_list_p(&q, HalQRec::get_qrec_cb, &qrec); if(0 != rv) { - Py_DECREF(python_list); PyErr_Format(PyExc_RuntimeError, "hal_list_p: returned '%s' (%d)", hal_strerror(rv), rv); return NULL; } + + PyObject* python_list = PyList_New(0); + for(size_t i = 0; i < qrec.size(); i++) { + pinparaminfo_add(qrec.rec(i), python_list); + } return python_list; } @@ -1515,9 +1528,8 @@ static int siginfo_writer_cb(hal_query_t *q, void *arg) return 0; } -static int siginfo_cb(hal_query_t *q, void *arg) +static int siginfo_add(const hal_query_t *q, PyObject *lst) { - PyObject *lst = static_cast(arg); PyObject *obj; static const char str_n[] = "NAME"; static const char str_v[] = "VALUE"; @@ -1581,14 +1593,18 @@ static int siginfo_cb(hal_query_t *q, void *arg) PyObject *get_info_signals(PyObject * /*self*/, PyObject * /*args*/) { - PyObject* python_list = PyList_New(0); + HalQRec qrec(256); // We normally have many signals hal_query_t q = {}; - int rv = hal_list_s(&q, siginfo_cb, python_list); + int rv = hal_list_s(&q, HalQRec::get_qrec_cb, &qrec); if(0 != rv) { - Py_DECREF(python_list); PyErr_Format(PyExc_RuntimeError, "hal_list_s: returned '%s' (%d)", hal_strerror(rv), rv); return NULL; } + + PyObject* python_list = PyList_New(0); + for(size_t i = 0; i < qrec.size(); i++) { + siginfo_add(qrec.rec(i), python_list); + } return python_list; } @@ -1596,21 +1612,38 @@ PyObject *get_info_signals(PyObject * /*self*/, PyObject * /*args*/) /* Get a dict of parameter info for all parameters in system */ PyObject *get_info_params(PyObject * /*self*/, PyObject * /*args*/) { - PyObject* python_list = PyList_New(0); + HalQRec qrec(512); // We normally have many parameters hal_query_t q = {}; q.qtype = HAL_QTYPE_PARAM; // Only handle parameters - int rv = hal_list_p(&q, pinparaminfo_cb, python_list); + int rv = hal_list_p(&q, HalQRec::get_qrec_cb, &qrec); if(0 != rv) { - Py_DECREF(python_list); PyErr_Format(PyExc_RuntimeError, "hal_list_p: returned '%s' (%d)", hal_strerror(rv), rv); return NULL; } + + PyObject* python_list = PyList_New(0); + for(size_t i = 0; i < qrec.size(); i++) { + pinparaminfo_add(qrec.rec(i), python_list); + } return python_list; } static PyObject *pyhal_get_realtime_type(PyObject * /*self*/, PyObject * /*o*/) { int res = hal_get_realtime_type(); - return PyLong_FromLong(res); + + // Get an IntEnum _hal.RTType.X instance from the result. + // This may be slower than a cached module/IntEnum ref, but this is + // normally only called once. + PyObject *m = PyImport_ImportModule("_hal"); + if(!m) + return NULL; + PyObject *cls = PyObject_GetAttrString(m, "RTType"); + Py_DECREF(m); + if(!cls) + return NULL; + PyObject *e = PyObject_CallFunction(cls, "l", (long)res); + Py_DECREF(cls); + return e; } static PyObject *pyhal_is_initialized(PyObject * /*self*/, PyObject * /*o*/) { @@ -1631,6 +1664,8 @@ static int pyshm_init(PyObject *_self, PyObject *args, PyObject * /*kw*/) { self->comp = NULL; self->shm_id = -1; + rtapi_print_msg(RTAPI_MSG_ERR, "halmodule: hal.shm has been deprecated.\n"); + if(!PyArg_ParseTuple(args, "O!ik", &halobject_type, &self->comp, &self->key, &self->size)) return -1; @@ -2143,6 +2178,112 @@ static struct PyModuleDef hal_moduledef = { NULL, /* m_free */ }; +// Member order matters: interactive tools pick the first spelling when +// several members share a value, so the table lists the preferred +// spelling of each value first (bool, real, sint, uint, port, s32, u32) +// and the alternatives (s64, u64, and the HAL_* spellings) after. The +// enum module preserves dict order, so the first occurrence of each +// value is the canonical member. +struct halenum_member_t { + const char *name; + long value; +}; + +static const halenum_member_t halenum_type_members[] = { + {"BOOL", HAL_BOOL}, + {"REAL", HAL_REAL}, + {"SINT", HAL_SINT}, + {"UINT", HAL_UINT}, + {"PORT", HAL_PORT}, + {"S32", HAL_S32}, + {"U32", HAL_U32}, + {"HAL_BOOL", HAL_BOOL}, + {"HAL_REAL", HAL_REAL}, + {"HAL_SINT", HAL_SINT}, + {"HAL_UINT", HAL_UINT}, + {"HAL_PORT", HAL_PORT}, + {"HAL_S32", HAL_S32}, + {"HAL_U32", HAL_U32}, + {} +}; + +static const halenum_member_t halenum_dir_members[] = { + {"IN", HAL_IN}, + {"OUT", HAL_OUT}, + {"IO", HAL_IO}, + {"RO", HAL_RO}, + {"RW", HAL_RW}, + {"HAL_IN", HAL_IN}, + {"HAL_OUT", HAL_OUT}, + {"HAL_IO", HAL_IO}, + {"HAL_RO", HAL_RO}, + {"HAL_RW", HAL_RW}, + {} +}; + +// Not previously registered, no need to have compat names. +static const halenum_member_t halenum_comp_members[] = { + {"UNKNOWN", HAL_COMP_TYPE_UNKNOWN}, + {"USER", HAL_COMP_TYPE_USER}, + {"REALTIME", HAL_COMP_TYPE_REALTIME}, + {"OTHER", HAL_COMP_TYPE_OTHER}, + {} +}; + +// Previously registered, but it is new so we don't do compat names. +static const halenum_member_t halenum_rt_members[] = { + {"UNINITIALIZED", REALTIME_TYPE_UNINITIALIZED}, + {"NONE", REALTIME_TYPE_NONE}, + {"UNKNOWN", REALTIME_TYPE_UNKNOWN}, + {"PREEMPT_DYNAMIC", REALTIME_TYPE_PREEMPT_DYNAMIC}, + {"PREEMPT_RT", REALTIME_TYPE_PREEMPT_RT}, + {"RTAI", REALTIME_TYPE_RTAI}, + {"LXRT", REALTIME_TYPE_LXRT}, + {"XENOMAI", REALTIME_TYPE_XENOMAI}, + {"XENOMAI_EVL", REALTIME_TYPE_XENOMAI_EVL}, +}; + +// Build an enum.IntEnum subclass from a member table. The class claims +// __module__ "hal", its public home, so repr() and pickle look right. +// Returns a new reference, or NULL with an exception set. +static PyObject *halenum_build(const char *clsname, const halenum_member_t *members) +{ + PyObject *enummod = PyImport_ImportModule("enum"); + if(!enummod) + return NULL; + PyObject *intenum = PyObject_GetAttrString(enummod, "IntEnum"); + Py_DECREF(enummod); + if(!intenum) + return NULL; + + PyObject *names = PyDict_New(); + if(!names) { + Py_DECREF(intenum); + return NULL; + } + for(size_t i = 0; members[i].name; i++) { + PyObject *v = PyLong_FromLong(members[i].value); + if(!v || PyDict_SetItemString(names, members[i].name, v)) { + Py_XDECREF(v); + Py_DECREF(names); + Py_DECREF(intenum); + return NULL; + } + Py_DECREF(v); + } + + PyObject *args = Py_BuildValue("(sO)", clsname, names); + PyObject *kwargs = Py_BuildValue("{ss}", "module", "hal"); + PyObject *cls = (args && kwargs) ? PyObject_Call(intenum, args, kwargs) : NULL; + Py_XDECREF(args); + Py_XDECREF(kwargs); + Py_DECREF(names); + Py_DECREF(intenum); + return cls; +} + +int halquery_add_submodule(PyObject *); // Not gonna make a header for this + PyMODINIT_FUNC PyInit__hal(void); PyMODINIT_FUNC PyInit__hal(void) { @@ -2196,6 +2337,34 @@ PyMODINIT_FUNC PyInit__hal(void) PyModule_AddIntConstant(m, "HAL_OUT", HAL_OUT); PyModule_AddIntConstant(m, "HAL_IO", HAL_IO); + // IntEnum tagging classes for type and direction, built from the + // hal.h constants (see halenum.hh). Registered here so that every + // consumer, Python or C++, shares the same two classes. + PyObject *eobj = halenum_build("Type", halenum_type_members); + if(!eobj || PyModule_AddObject(m, "Type", eobj)) { + Py_XDECREF(eobj); + Py_DECREF(m); + return NULL; + } + eobj = halenum_build("Dir", halenum_dir_members); + if(!eobj || PyModule_AddObject(m, "Dir", eobj)) { + Py_XDECREF(eobj); + Py_DECREF(m); + return NULL; + } + eobj = halenum_build("CompType", halenum_comp_members); + if(!eobj || PyModule_AddObject(m, "CompType", eobj)) { + Py_XDECREF(eobj); + Py_DECREF(m); + return NULL; + } + eobj = halenum_build("RTType", halenum_rt_members); + if(!eobj || PyModule_AddObject(m, "RTType", eobj)) { + Py_XDECREF(eobj); + Py_DECREF(m); + return NULL; + } + PyModule_AddIntConstant(m, "REALTIME_TYPE_UNINITIALIZED", REALTIME_TYPE_UNINITIALIZED); PyModule_AddIntConstant(m, "REALTIME_TYPE_NONE", REALTIME_TYPE_NONE); PyModule_AddIntConstant(m, "REALTIME_TYPE_UNKNOWN", REALTIME_TYPE_UNKNOWN); @@ -2218,6 +2387,12 @@ PyMODINIT_FUNC PyInit__hal(void) PyModule_AddStringConstant(m, "kernel_version", "Not Available"); #endif + // Now that we have everything registered, add the halquery sub-module + if(halquery_add_submodule(m) < 0) { + Py_DECREF(m); + return NULL; + } + PyRun_SimpleString( "(lambda s=__import__('signal'):" "s.signal(s.SIGTERM, s.default_int_handler))()"); diff --git a/src/hal/halqrec.hh b/src/hal/halqrec.hh new file mode 100644 index 00000000000..fd79ddbcebb --- /dev/null +++ b/src/hal/halqrec.hh @@ -0,0 +1,106 @@ +#ifndef __HAL_HALQREC_HH +#define __HAL_HALQREC_HH +// +// HAL Python query API +// +// Copyright (c) 2026 B.Stultiens +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// + +#include +#include +#include +#include +#include + +// +// Result recording class +// To collect copies of hal_query_t structures in a HAL query callback. +// +// Uses malloc and friends to avoid throwing exceptions in the collection part. +// However, it does throw exceptions on initial construction and if you try to +// address an invalid recorded record. In both cases it throws a +// std::runtime_error with an appropriate message. +// +// The allocated array is cleaned up as soon as the instance is destructed. +// +class HalQRec +{ +public: + HalQRec(size_t nmin = 64) + : n(0), na(nmin), qr(nullptr) + { + if(na < 1) + na = 1; + qr = static_cast(calloc(na, sizeof(*qr))); + if(!qr) + throw std::runtime_error(fmt::format("HalQRec: failed to calloc {} hal_query_t elements", na)); + } + + ~HalQRec() + { + if(qr) + free(qr); + } + + // It is important that the append() method does not throw exceptions. It + // is called with the HAL mutex locked and we cannot exit the callback + // uncontrolled. Otherwise, we'd have HAL permanently locked. + // The append() method returns zero (0) on success or -ENOMEM when out of + // memory. + int append(const hal_query_t *q) noexcept { + if(n >= na){ + hal_query_t *qrn = static_cast(reallocarray(qr, na * 2, sizeof(*qr))); + if(!qrn) + return -ENOMEM; + qr = qrn; + na *= 2; + memset(&qr[n], 0, (na - n) * sizeof(*qr)); + } + qr[n] = *q; + n++; + return 0; + } + + // + // Generic callback collecting all results copying them over in + // a results array for later examination. + // + static int get_qrec_cb(hal_query_t *q, void *arg) noexcept { + return reinterpret_cast(arg)->append(q); + } + + size_t size() const { return n; } + size_t maxsize() const { return na; } + const hal_query_t *rec(size_t i) const { + if(i < n) + return &qr[i]; + if(n > 0) + throw std::runtime_error(fmt::format("HalQRec: Index {} out of range [0,{}]", i, n-1)); + else + throw std::runtime_error(fmt::format("HalQRec: Index {} out of range, no entries available", i)); + } + +private: + size_t n; + size_t na; + hal_query_t *qr; + + HalQRec(const HalQRec &) = delete; + HalQRec &operator=(const HalQRec &) = delete; +}; + +#endif diff --git a/src/hal/halquery.cc b/src/hal/halquery.cc new file mode 100644 index 00000000000..64fb36a92a1 --- /dev/null +++ b/src/hal/halquery.cc @@ -0,0 +1,534 @@ +// +// HAL Python query API +// +// Copyright (c) 2026 B.Stultiens +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +#include + +#include "halqrec.hh" + +namespace py = pybind11; + +// These are IntEnum imported from _hal +static py::object halenumtype; // hal_type_t +static py::object halenumdir; // hal_pdir_t +static py::object halenumcomp; // hal_comp_type_t + +static py::object value_object(hal_type_t type, hal_refs_u u, bool ispin) +{ + switch(type) { + case HAL_BOOL: return py::bool_(hal_get_bool(u.b)); + case HAL_S32: return py::int_(hal_get_si32(u.s)); + case HAL_SINT: return py::int_(hal_get_sint(u.s)); + case HAL_U32: return py::int_(hal_get_ui32(u.u)); + case HAL_UINT: return py::int_(hal_get_uint(u.u)); + case HAL_REAL: return py::float_(hal_get_real(u.r)); + case HAL_PORT: + // FIXME + // Needs to change when we break the API and change hal_port_t + if(ispin) + return py::int_(hal_port_buffer_size(reinterpret_cast(u.u))); + else + return py::int_(0); // HAL_PORT cannot be a parameter + default: return py::none(); + } +} + +// NOTE: +// Using py::dict("bla"_a = value) results in a cppcheck portability warning. +// We simply circumvent the problem by constructing the dict piece by piece. +// +static py::object build_dict_comp(const hal_query_t *q) +{ + py::dict dict; + dict["haltype"] = "component"; + dict["name"] = q->name; + dict["type"] = halenumcomp((long)q->comp.type); + dict["id"] = q->comp.comp_id; + dict["pid"] = q->comp.pid; + dict["ready"] = py::bool_(q->comp.ready); + dict["insmod"] = q->comp.insmod; + return dict; +} + +static py::object build_dict_pin_param(const hal_query_t *q, bool ispin) +{ + py::dict dict; + dict["haltype"] = ispin ? "pin" : "parameter"; + dict["name"] = q->name; + dict["type"] = halenumtype((long)q->pp.type); + dict["dir"] = halenumdir((long)q->pp.dir); + dict["value"] = value_object(q->pp.type, q->pp.ref, ispin); + dict["alias"] = q->pp.alias; + if(ispin) + dict["signal"] = q->pp.signal; + dict["comp"] = q->pp.comp; + dict["comp_id"] = q->pp.comp_id; + return dict; +} + +static py::object build_dict_signal(const hal_query_t *q, const py::object &drv) +{ + py::dict dict; + dict["haltype"] = "signal"; + dict["name"] = q->name; + dict["type"] = halenumtype((long)q->sig.type); + dict["value"] = value_object(q->sig.type, q->sig.ref, false); + dict["writers"] = q->sig.writers; + dict["readers"] = q->sig.readers; + dict["bidirs"] = q->sig.bidirs; + dict["driver"] = drv; + return dict; +} + +static py::object build_dict_funct(const hal_query_t *q) +{ + py::dict dict; + dict["haltype"] = "function"; + dict["name"] = q->name; + dict["comp"] = q->funct.comp; + dict["comp_id"] = q->funct.comp_id; + dict["users"] = q->funct.users; + dict["reentrant"] = py::bool_(q->funct.reentrant); + return dict; +} + +static py::object build_dict_threadfunct(const hal_query_t *q) +{ + py::dict dict; + dict["haltype"] = "threadfunction"; + dict["name"] = q->thread.funct; + dict["index"] = q->thread.functidx; + dict["is_init"] = py::bool_(q->thread.is_init); + return dict; +} + +static py::object build_dict_thread(const hal_query_t *q, py::list &functs) +{ + py::dict dict; + dict["haltype"] = "thread"; + dict["name"] = q->name; + dict["comp"] = q->thread.comp; + dict["comp_id"] = q->thread.comp_id; + dict["priority"] = q->thread.priority; + dict["period"] = q->thread.period; + dict["functions"] = functs; + return dict; +} + +// +// Fetch the name of the signal driver if there is one +// +static py::object fetch_signal_driver(const hal_query_t *sig) +{ + // Can only have a driver when there is a writer + if(sig->sig.writers <= 0) + return py::none(); + + hal_query_t q = {}; + q.name = sig->name; + HalQRec qrec; + int rv = hal_list_p_s(&q, HalQRec::get_qrec_cb, &qrec); + if(0 == rv) { + for(size_t i = 0; i < qrec.size(); i++) { + if(HAL_OUT == qrec.rec(i)->pp.dir) { + // This is the driver pin + return py::str(qrec.rec(i)->name); + } + } + // Getting here would be bad... + // However, there is apparently no driver (anymore) + return py::none(); + } + throw std::runtime_error(fmt::format("fetch_signal_driver({}): hal_list_p_s: error={} ({})", sig->name, rv, hal_strerror(rv))); +} + +// +// *** Get component on name/ID *** +// +static py::object get_comp_i(int comp_id) +{ + hal_query_t q = {}; + int rv = hal_comp_by_id(comp_id, &q); + switch(rv) { + case -ENOENT: return py::none(); + case 0: return build_dict_comp(&q); + default: + throw std::runtime_error(fmt::format("get_comp_i({}): hal_comp_by_id: error={} ({})", comp_id, rv, hal_strerror(rv))); + } +} + +static py::object get_comp_s(const std::string &name) +{ + hal_query_t q = {}; + int rv = hal_comp_by_name(name.c_str(), &q); + switch(rv) { + case -ENOENT: return py::none(); + case 0: return build_dict_comp(&q); + default: + throw std::runtime_error(fmt::format("get_comp_s({}): hal_comp_by_name: error={} ({})", name, rv, hal_strerror(rv))); + } +} + +// +// *** Get named pin/param *** +// +static py::object get_pin_param(const std::string &name, bool ispin) +{ + hal_query_t q = {}; + q.qtype = ispin ? HAL_QTYPE_PIN : HAL_QTYPE_PARAM; + q.name = name.c_str(); + int rv = hal_getref_p(&q); + switch(rv) { + case -ENOENT: return py::none(); + case 0: return build_dict_pin_param(&q, ispin); + default: + throw std::runtime_error(fmt::format("get_pin_param({},{}): hal_get_p: error={} ({})", name, ispin, rv, hal_strerror(rv))); + } +} + +static py::object get_pin(const std::string &name) +{ + return get_pin_param(name, true); +} + +static py::object get_param(const std::string &name) +{ + return get_pin_param(name, false); +} + +// +// *** Get named signal *** +// +static py::object get_signal(const std::string &name) +{ + hal_query_t q = {}; + q.name = name.c_str(); + int rv = hal_getref_s(&q); + switch(rv) { + case -ENOENT: return py::none(); + case 0: return build_dict_signal(&q, fetch_signal_driver(&q)); + default: + throw std::runtime_error(fmt::format("get_signal({}): hal_get_s: error={} ({})", name, rv, hal_strerror(rv))); + } +} + +// +// *** Get named function *** +// +static int get_funct_cb(hal_query_t *q, void *arg) +{ + if(!strcmp(q->name, (const char *)arg)) + return 1; // Found it, break the loop without error + return 0; +} + +static py::object get_funct(const std::string &name) +{ + hal_query_t q = {}; + q.name = name.c_str(); + int rv = hal_list_funct(&q, get_funct_cb, (void *)name.c_str()); + switch(rv) { + case 0: return py::none(); + case 1: return build_dict_funct(&q); + default: + throw std::runtime_error(fmt::format("get_funct({}): hal_list_funct: error={} ({})", name, rv, hal_strerror(rv))); + } +} + +// +// *** Get named thread *** +// +typedef struct { + const char *name; + HalQRec *qrec; +} threadlist_t; + +static int get_thread_cb(hal_query_t *q, void *arg) +{ + threadlist_t *tlp = reinterpret_cast(arg); + if(!strcmp(tlp->name, q->name)) { + // The first match has q->qtype set to HAL_QTYPE_THREAD. The following + // matches with have it set to HAL_QTYPE_THREAD_FUNCT. + return tlp->qrec->append(q); + } else if(tlp->qrec->size() > 0) { + // This is a new thread. We scooped up the matched thread's data, so + // it is fine to quit without error. + return 1; + } + return 0; +} + +static py::object get_thread(const std::string &name) +{ + HalQRec qrec; + threadlist_t tl = { .name = name.c_str(), .qrec = &qrec }; + + hal_query_t q = {}; + q.qtype = HAL_QTYPE_THREAD_FUNCT; + int rv = hal_list_thread(&q, get_thread_cb, &tl); + if(rv < 0) { + throw std::runtime_error(fmt::format("get_thread({}): hal_list_thread: error={} ({})", name, rv, hal_strerror(rv))); + } + + if(qrec.size() < 1) { + // Thread name not found + return py::none(); + } + py::list flst; + for(size_t i = 1; i < qrec.size(); i++) { + flst.append(build_dict_threadfunct(qrec.rec(i))); + } + + return build_dict_thread(qrec.rec(0), flst); +} + +// +// *** Get all pins connected to a named signal *** +// +static py::object get_signalpins(const std::string &name) +{ + hal_query_t q = {}; + q.name = name.c_str(); + + HalQRec qrec; + + int rv = hal_list_p_s(&q, HalQRec::get_qrec_cb, &qrec); + if(-ENOENT == rv) { + return py::none(); + } else if(rv < 0) { + throw std::runtime_error(fmt::format("get_signalpins({}): hal_list_p_s: error={} ({})", name, rv, hal_strerror(rv))); + } + + py::dict dict; + for(size_t i = 0; i < qrec.size(); i++) { + dict[qrec.rec(i)->name] = build_dict_pin_param(qrec.rec(i), true); + } + return dict; +} + +// +// *** Get all pins *** +// +static py::object build_pps(const HalQRec &qrec, bool ispin) +{ + py::dict dict; + + for(size_t i = 0; i < qrec.size(); i++) { + dict[qrec.rec(i)->name] = build_dict_pin_param(qrec.rec(i), ispin); + } + return dict; +} + +static py::dict get_pins() +{ + HalQRec qrec(1024); // There are generally a lot of pins + + hal_query_t q = {}; + q.qtype = HAL_QTYPE_PIN; + int rv = hal_list_p(&q, HalQRec::get_qrec_cb, &qrec); + if(rv < 0) { + throw std::runtime_error(fmt::format("get_pins(): hal_list_p: error={} ({})", rv, hal_strerror(rv))); + } + + return build_pps(qrec, true); +} + +// +// *** Get all params *** +// +static py::dict get_params() +{ + HalQRec qrec(512); // There are generally a lot of params + + hal_query_t q = {}; + q.qtype = HAL_QTYPE_PARAM; + int rv = hal_list_p(&q, HalQRec::get_qrec_cb, &qrec); + if(rv < 0) { + throw std::runtime_error(fmt::format("get_params(): hal_list_p: error={} ({})", rv, hal_strerror(rv))); + } + + return build_pps(qrec, false); +} + +// +// *** Get all signals *** +// +static py::dict get_signals() +{ + HalQRec qrec(256); // The are generally a lot of signals + + hal_query_t q = {}; + int rv = hal_list_s(&q, HalQRec::get_qrec_cb, &qrec); + if(rv < 0) { + throw std::runtime_error(fmt::format("get_signals(): hal_list_s: error={} ({})", rv, hal_strerror(rv))); + } + + py::dict dict; + for(size_t i = 0; i < qrec.size(); i++) { + dict[qrec.rec(i)->name] = build_dict_signal(qrec.rec(i), fetch_signal_driver(qrec.rec(i))); + } + return dict; +} + +// +// *** Get all components *** +// +static py::dict get_comps() +{ + HalQRec qrec; + + hal_query_t q = {}; + int rv = hal_list_comp(&q, HalQRec::get_qrec_cb, &qrec); + if(rv < 0) { + throw std::runtime_error(fmt::format("get_comps(): hal_list_comp: error={} ({})", rv, hal_strerror(rv))); + } + + py::dict dict; + for(size_t i = 0; i < qrec.size(); i++) { + dict[qrec.rec(i)->name] = build_dict_comp(qrec.rec(i)); + } + return dict; +} + +// +// *** Get all functions *** +// +static py::dict get_functs() +{ + HalQRec qrec; + + hal_query_t q = {}; + int rv = hal_list_funct(&q, HalQRec::get_qrec_cb, &qrec); + if(rv < 0) { + throw std::runtime_error(fmt::format("get_functs(): hal_list_funct: error={} ({})", rv, hal_strerror(rv))); + } + + py::dict dict; + for(size_t i = 0; i < qrec.size(); i++) { + dict[qrec.rec(i)->name] = build_dict_funct(qrec.rec(i)); + } + return dict; +} + +// +// *** Get all threads *** +// +static py::dict get_threads() +{ + HalQRec qrec; + + hal_query_t q = {}; + q.qtype = HAL_QTYPE_THREAD_FUNCT; + int rv = hal_list_thread(&q, HalQRec::get_qrec_cb, &qrec); + if(rv < 0) { + throw std::runtime_error(fmt::format("get_threads(): hal_list_thread: error={} ({})", rv, hal_strerror(rv))); + } + + py::dict dict; + if(qrec.size() < 1) + return dict; // No threads, return empty list + + py::list flst; + size_t tag = 0; + for(size_t i = 1; i < qrec.size(); i++) { + if(HAL_QTYPE_THREAD == qrec.rec(i)->qtype) { + tag = i; + } else { + // This must be HAL_QTYPE_THREAD_FUNCT + flst.append(build_dict_threadfunct(qrec.rec(i))); + } + if(i == qrec.size() - 1 || (i < qrec.size() - 1 && HAL_QTYPE_THREAD == qrec.rec(i+1)->qtype)) { + // There is no next enrty or next entry is new thread + dict[qrec.rec(tag)->name] = build_dict_thread(qrec.rec(tag), flst); + if(i < qrec.size() - 1) { + flst = py::list(); // New thread follows, new function list + } + } + } + return dict; +} + +static const char halquery_module_doc[] = + "Query interface to LinuxCNC's HAL internals\n" + "\n" + "This module allows you to retrieve information about all HAL\n" + "internal constructs, such as:\n" + " - pins\n" + " - parameters\n" + " - signals\n" + " - components\n" + " - functions\n" + " - threads\n" + "\n" + "Typical usage:\n" + " import hal\n" + " # print info on one component\n" + " print(\"Component xyz:\", hal.query.comp(\"xyz\"))\n" + "\n" + " # print info on all signals\n" + " print(\"Signals:\")\n" + " print(hal.query.signals())\n" + ; + +// +// NOTE: +// The halquery is a sub-module to hal under hal.query. That means importing it +// requires hal/_hal to be imported. That is a good thing because then we don't +// have to think about hal_lib_init() and hal_lib_exit() anymore. The _hal base +// will do that for us and it is imported before this sub-module is. +// +int halquery_add_submodule(PyObject *parent) +{ + try { + py::module_ p = py::reinterpret_borrow(parent); + py::module_ m = p.def_submodule("query", halquery_module_doc); + + m.def("pin", &get_pin, "Get information about the named pin or None if not found."); + m.def("param", &get_param, "Get information about the named param or None if not found."); + m.def("signal", &get_signal, "Get information about the named signal or None if not found."); + m.def("comp", &get_comp_i, "Get information about the component by integer ID or None if not found."); + m.def("comp", &get_comp_s, "Get information about the component by name or None if not found."); + m.def("funct", &get_funct, "Get information about the named function or None if not found."); + m.def("thread", &get_thread, "Get information about the named thread or None if not found."); + + m.def("signalpins", &get_signalpins, "Get information about all pins connected to named signal or None if not found"); + + m.def("pins", &get_pins, "Get information about all pins as a dictionary."); + m.def("params", &get_params, "Get information about all params as a dictionary."); + m.def("signals", &get_signals, "Get information about all signals as a dictionary."); + m.def("comps", &get_comps, "Get information about all components as a dictionary."); + m.def("functs", &get_functs, "Get information about all functions as a dictionary."); + m.def("threads", &get_threads, "Get information about all threads as a dictionary."); + + halenumtype = p.attr("Type"); + halenumdir = p.attr("Dir"); + halenumcomp = p.attr("CompType"); + // These inc_ref()'s will leak the enum references on purpose. This + // prevents any freeing of the underlying PyObject when the program + // ends. The py::object destructor can otherwise cause interference. + halenumtype.inc_ref(); + halenumdir.inc_ref(); + halenumcomp.inc_ref(); + return 0; + } catch(const std::exception &e) { + PyErr_SetString(PyExc_ImportError, e.what()); + return -1; + } +} + +// vim: ts=4 sw=4 et diff --git a/tests/halmodule.3/expected b/tests/halmodule.3/expected new file mode 100644 index 00000000000..140572ef7d0 --- /dev/null +++ b/tests/halmodule.3/expected @@ -0,0 +1,60 @@ +# Pins +and2.0.in0 {'haltype': 'pin', 'name': 'and2.0.in0', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-input-a', 'comp': 'and2', 'comp_id': 'ID'} +and2.0.in1 {'haltype': 'pin', 'name': 'and2.0.in1', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-input-b', 'comp': 'and2', 'comp_id': 'ID'} +and2.0.out {'haltype': 'pin', 'name': 'and2.0.out', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-xor-a', 'comp': 'and2', 'comp_id': 'ID'} +and2.0.time {'haltype': 'pin', 'name': 'and2.0.time', 'type': , 'dir': , 'value': 0, 'alias': None, 'signal': None, 'comp': 'and2', 'comp_id': 'ID'} +or2.0.in0 {'haltype': 'pin', 'name': 'or2.0.in0', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-input-a', 'comp': 'or2', 'comp_id': 'ID'} +or2.0.in1 {'haltype': 'pin', 'name': 'or2.0.in1', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-input-b', 'comp': 'or2', 'comp_id': 'ID'} +or2.0.out {'haltype': 'pin', 'name': 'or2.0.out', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-xor-b', 'comp': 'or2', 'comp_id': 'ID'} +or2.0.time {'haltype': 'pin', 'name': 'or2.0.time', 'type': , 'dir': , 'value': 0, 'alias': None, 'signal': None, 'comp': 'or2', 'comp_id': 'ID'} +testthread.threadbeat {'haltype': 'pin', 'name': 'testthread.threadbeat', 'type': , 'dir': , 'value': 0, 'alias': None, 'signal': None, 'comp': '__testthread', 'comp_id': 'ID'} +testthread.time {'haltype': 'pin', 'name': 'testthread.time', 'type': , 'dir': , 'value': 0, 'alias': None, 'signal': None, 'comp': '__testthread', 'comp_id': 'ID'} +xor2.0.in0 {'haltype': 'pin', 'name': 'xor2.0.in0', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-xor-a', 'comp': 'xor2', 'comp_id': 'ID'} +xor2.0.in1 {'haltype': 'pin', 'name': 'xor2.0.in1', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-xor-b', 'comp': 'xor2', 'comp_id': 'ID'} +xor2.0.out {'haltype': 'pin', 'name': 'xor2.0.out', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-output', 'comp': 'xor2', 'comp_id': 'ID'} +xor2.0.time {'haltype': 'pin', 'name': 'xor2.0.time', 'type': , 'dir': , 'value': 0, 'alias': None, 'signal': None, 'comp': 'xor2', 'comp_id': 'ID'} +# Params +and2.0.tmax {'haltype': 'parameter', 'name': 'and2.0.tmax', 'type': , 'dir': , 'value': 0, 'alias': None, 'comp': 'and2', 'comp_id': 'ID'} +and2.0.tmax-increased {'haltype': 'parameter', 'name': 'and2.0.tmax-increased', 'type': , 'dir': , 'value': False, 'alias': None, 'comp': 'and2', 'comp_id': 'ID'} +or2.0.tmax {'haltype': 'parameter', 'name': 'or2.0.tmax', 'type': , 'dir': , 'value': 0, 'alias': None, 'comp': 'or2', 'comp_id': 'ID'} +or2.0.tmax-increased {'haltype': 'parameter', 'name': 'or2.0.tmax-increased', 'type': , 'dir': , 'value': False, 'alias': None, 'comp': 'or2', 'comp_id': 'ID'} +testthread.tmax {'haltype': 'parameter', 'name': 'testthread.tmax', 'type': , 'dir': , 'value': 0, 'alias': None, 'comp': '__testthread', 'comp_id': 'ID'} +xor2.0.tmax {'haltype': 'parameter', 'name': 'xor2.0.tmax', 'type': , 'dir': , 'value': 0, 'alias': None, 'comp': 'xor2', 'comp_id': 'ID'} +xor2.0.tmax-increased {'haltype': 'parameter', 'name': 'xor2.0.tmax-increased', 'type': , 'dir': , 'value': False, 'alias': None, 'comp': 'xor2', 'comp_id': 'ID'} +# Signals +net-input-a {'haltype': 'signal', 'name': 'net-input-a', 'type': , 'value': False, 'writers': 0, 'readers': 2, 'bidirs': 0, 'driver': None} +net-input-b {'haltype': 'signal', 'name': 'net-input-b', 'type': , 'value': False, 'writers': 0, 'readers': 2, 'bidirs': 0, 'driver': None} +net-output {'haltype': 'signal', 'name': 'net-output', 'type': , 'value': False, 'writers': 1, 'readers': 0, 'bidirs': 0, 'driver': 'xor2.0.out'} +net-xor-a {'haltype': 'signal', 'name': 'net-xor-a', 'type': , 'value': False, 'writers': 1, 'readers': 1, 'bidirs': 0, 'driver': 'and2.0.out'} +net-xor-b {'haltype': 'signal', 'name': 'net-xor-b', 'type': , 'value': False, 'writers': 1, 'readers': 1, 'bidirs': 0, 'driver': 'or2.0.out'} +# Components +xor2 {'haltype': 'component', 'name': 'xor2', 'type': , 'id': 'ID', 'pid': 0, 'ready': True, 'insmod': 'count=1'} +or2 {'haltype': 'component', 'name': 'or2', 'type': , 'id': 'ID', 'pid': 0, 'ready': True, 'insmod': 'count=1'} +and2 {'haltype': 'component', 'name': 'and2', 'type': , 'id': 'ID', 'pid': 0, 'ready': True, 'insmod': 'count=1'} +__testthread {'haltype': 'component', 'name': '__testthread', 'type': , 'id': 'ID', 'pid': 0, 'ready': True, 'insmod': None} +threads {'haltype': 'component', 'name': 'threads', 'type': , 'id': 'ID', 'pid': 0, 'ready': True, 'insmod': 'name1=testthread period1=1000000'} +# Functions +and2.0 {'haltype': 'function', 'name': 'and2.0', 'comp': 'and2', 'comp_id': 'ID', 'users': 1, 'reentrant': False} +or2.0 {'haltype': 'function', 'name': 'or2.0', 'comp': 'or2', 'comp_id': 'ID', 'users': 1, 'reentrant': False} +xor2.0 {'haltype': 'function', 'name': 'xor2.0', 'comp': 'xor2', 'comp_id': 'ID', 'users': 1, 'reentrant': False} +# Threads +testthread {'haltype': 'thread', 'name': 'testthread', 'comp': '__testthread', 'comp_id': 'ID', 'priority': 0, 'period': 1000000, 'functions': [{'haltype': 'threadfunction', 'name': 'and2.0', 'index': 0, 'is_init': False}, {'haltype': 'threadfunction', 'name': 'or2.0', 'index': 1, 'is_init': False}, {'haltype': 'threadfunction', 'name': 'xor2.0', 'index': 2, 'is_init': False}]} +# Signal pins +and2.0.out {'haltype': 'pin', 'name': 'and2.0.out', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-xor-a', 'comp': 'and2', 'comp_id': 'ID'} +xor2.0.in0 {'haltype': 'pin', 'name': 'xor2.0.in0', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-xor-a', 'comp': 'xor2', 'comp_id': 'ID'} +# Intended failures +pin('does-not-exist') fails correctly +param('does-not-exist') fails correctly +signal('does-not-exist') fails correctly +comp('does-not-exist') fails correctly +comp(65534) fails correctly +funct('does-not-exist') fails correctly +thread('does-not-exist') fails correctly +signalpins('does-not-exist') fails correctly +# Simple named queries +Pin: {'haltype': 'pin', 'name': 'or2.0.in1', 'type': , 'dir': , 'value': False, 'alias': None, 'signal': 'net-input-b', 'comp': 'or2', 'comp_id': 'ID'} +Param: {'haltype': 'parameter', 'name': 'testthread.tmax', 'type': , 'dir': , 'value': 0, 'alias': None, 'comp': '__testthread', 'comp_id': 'ID'} +Signal: {'haltype': 'signal', 'name': 'net-output', 'type': , 'value': False, 'writers': 1, 'readers': 0, 'bidirs': 0, 'driver': 'xor2.0.out'} +Component: {'haltype': 'component', 'name': 'and2', 'type': , 'id': 'ID', 'pid': 0, 'ready': True, 'insmod': 'count=1'} +Function: {'haltype': 'function', 'name': 'xor2.0', 'comp': 'xor2', 'comp_id': 'ID', 'users': 1, 'reentrant': False} +Thread: {'haltype': 'thread', 'name': 'testthread', 'comp': '__testthread', 'comp_id': 'ID', 'priority': 0, 'period': 1000000, 'functions': [{'haltype': 'threadfunction', 'name': 'and2.0', 'index': 0, 'is_init': False}, {'haltype': 'threadfunction', 'name': 'or2.0', 'index': 1, 'is_init': False}, {'haltype': 'threadfunction', 'name': 'xor2.0', 'index': 2, 'is_init': False}]} diff --git a/tests/halmodule.3/halquerytest.py b/tests/halmodule.3/halquerytest.py new file mode 100755 index 00000000000..58bec69e8bd --- /dev/null +++ b/tests/halmodule.3/halquerytest.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import hal + +# There is a chance that component IDs are not always consistent. They +# should be, but there are no guarantees. Therefore, we simply mask the +# number and use 'ID' as replacement. +def fix_comp_id(d): + if 'comp_id' in d: + d['comp_id'] = "ID" + return d + +def fix_id(d): + if 'id' in d: + d['id'] = "ID" + return d + +# Call the listing query methods. +print("# Pins") +for n,v in hal.query.pins().items(): + print(n, fix_comp_id(v)) + +print("# Params") +for n,v in hal.query.params().items(): + print(n, fix_comp_id(v)) + +print("# Signals") +for n,v in hal.query.signals().items(): + print(n, v) + +print("# Components") +for n,v in hal.query.comps().items(): + if not n.startswith("halcmd"): # halcmd add a component 'halcmd' + print(n, fix_id(v)) + +print("# Functions") +for n,v in hal.query.functs().items(): + print(n, fix_comp_id(v)) + +print("# Threads") +for n,v in hal.query.threads().items(): + print(n, fix_comp_id(v)) + +print("# Signal pins") +for n,v in hal.query.signalpins('net-xor-a').items(): + print(n, fix_comp_id(v)) + +print("# Intended failures") +if None is hal.query.pin('does-not-exist'): + print("pin('does-not-exist') fails correctly") +if None is hal.query.param('does-not-exist'): + print("param('does-not-exist') fails correctly") +if None is hal.query.signal('does-not-exist'): + print("signal('does-not-exist') fails correctly") +if None is hal.query.comp('does-not-exist'): + print("comp('does-not-exist') fails correctly") +if None is hal.query.comp(65534): + print("comp(65534) fails correctly") +if None is hal.query.funct('does-not-exist'): + print("funct('does-not-exist') fails correctly") +if None is hal.query.thread('does-not-exist'): + print("thread('does-not-exist') fails correctly") +if None is hal.query.signalpins('does-not-exist'): + print("signalpins('does-not-exist') fails correctly") + +# Call the named query methods +print("# Simple named queries") +print("Pin:", fix_comp_id(hal.query.pin('or2.0.in1'))) +print("Param:", fix_comp_id(hal.query.param('testthread.tmax'))) +print("Signal:", hal.query.signal('net-output')) +print("Component:", fix_id(hal.query.comp('and2'))) +print("Function:", fix_comp_id(hal.query.funct('xor2.0'))) +print("Thread:", fix_comp_id(hal.query.thread('testthread'))) diff --git a/tests/halmodule.3/test.hal b/tests/halmodule.3/test.hal new file mode 100644 index 00000000000..0e277175a7f --- /dev/null +++ b/tests/halmodule.3/test.hal @@ -0,0 +1,24 @@ +# The goal of this test is to run the 'halquerytest.py' script that will +# read all HAL internals via the query API. We only need a mapped HAL +# shared memory segment for that. It is rather unimportant what is +# loaded. We just need to ensure we have a working HAL system and some +# things we can look at. +loadrt threads name1=testthread period1=1000000 + +loadrt and2 count=1 +loadrt or2 count=1 +loadrt xor2 count=1 + +addf and2.0 testthread +addf or2.0 testthread +addf xor2.0 testthread + +net net-input-a and2.0.in0 or2.0.in0 +net net-input-b and2.0.in1 or2.0.in1 +net net-xor-a and2.0.out xor2.0.in0 +net net-xor-b or2.0.out xor2.0.in1 +net net-output xor2.0.out + +# We don't care whether the thread is running. We only want to extract +# the HAL information and show it. +loadusr -w ./halquerytest.py