Skip to content

Latest commit

 

History

History
646 lines (518 loc) · 21.4 KB

File metadata and controls

646 lines (518 loc) · 21.4 KB

The HAL Python module

This documentation describes the hal Python module, which provides 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.

Basic usage

Simple example creating component and pins
#!/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()
try:
    while True:
        h['out'] = h['in-a'] * h['in-b']
        time.sleep(0.001)
except KeyboardInterrupt:
    raise SystemExit

Class hal

hal constants

Message level constants:

  • hal.MSG_NONE - No messages at all

  • hal.MSG_ERR - Only errors

  • hal.MSG_WARN - Both warnings and errors

  • hal.MSG_INFO - Both informational, warning and error messages

  • hal.MSG_DBG - Additionally include debugging information

  • hal.MSG_ALL - Print all messages encountered, disregarding level

Realtime type constants:

  • hal.REALTIME_TYPE_UNINITIALIZED - Real time module not running

  • hal.REALTIME_TYPE_NONE - No realtime available

  • hal.REALTIME_TYPE_RTAI - RTAI kernel mode

  • hal.REALTIME_TYPE_PREEMPT_DYNAMIC - Preempt dynamic: avaliable in vanilla kernel, not recommended

  • hal.REALTIME_TYPE_PREEMPT_RT - Preempt RT

  • 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

System information:

  • hal.is_kernelspace - One (1) if RTAPI runs in the kernel, otherwise zero (0)

  • 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)

  • hal.is_sim - Inverted hal.is_rt

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 fail with the error: Cannot call before creating component

Example:
#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)

type = hal.get_realtime_type()
hal.get_realtime_type()

Returns the type of the running realtime system. Might return hal.REALTIME_TYPE_UNINITIALIZED if rtapi_app is not running. See realtime type constants.

hal.component_exists(name:string)

Returns a boolean to indicate whether or not the specified component exist at this time.

hal.component_is_ready(name:string)

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.

Example:
if not hal.component_is_ready("testpanel"):
    compmsg = "ready" if hal.component_exists("testpanel") else "loaded"
    print("Expected component 'testpanel' to be {}".format(compmsg))
    os.exit(1)
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 message constants.

hal.get_msg_level()

Return the current message level. See 'hal.set_msg_level()' and message constants for list of possible returned values.

hal.new_sig(name:string, type:enum)

Create a new signal (net) called name. The signal can carry information of type content. Returns True on success.

Example:
if not hal.new_sig("signalname", hal.HAL_BIT):
    ...handle error...
hal.connect(pinname:string, signame:string)

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.

Example:
if not hal.connect("mycomp.pinname", "signalname"):
    ...handle error...
hal.disconnect(pinname:string, signame:string)

Disconnect the pin pinname from signal signame. Both signal and pin must exist. Returns True on success.

Example:
if not hal.disconnect("mycomp.pinname"):
    ...handle error...
hal.pin_has_writer(pinname:string)

Returns True if pin with name pinname is attached to a signal and there is at least one writer. Otherwise, False is returned.

Example:
if hal.pin_has_writer("mycomp.0.pin.02"):
    print("Pin has writer(s)")
else:
    print("Pin has no writer or no signal attached")
hal.set_p(name:string, value:mixed)

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.
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. 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:
hal.set_p("mycomp.0.bit", True)
hal.set_p("mycomp.0.float", 99.99)
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. In that case, the call uses the value to set the port’s queue size and it must be a positive integer. See below on configuring a port.

hal.get_value(name:string)

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.

Example:
value = hal.get_value("iocontrol.0.emc-enable-in")
hal.get_info_pins()

Returns a list of dictionary tuples as in {"NAME":"pinname", "VALUE":<bool|int|float>, "TYPE":<int>, "DIRECTION":<int>}.

hal.get_info_params()

Returns a list of dictionary tuples as in {"NAME":"paramname", "VALUE":<bool|int|float>, "TYPE":<int>, "DIRECTION":<int>}.

hal.get_info_signals()

Returns a list of dictionary tuples as in {"NAME":"signame", "VALUE":<bool|int|float>, "TYPE":<int>, "DRIVER":"name"|None}.

Example:
for pin in hal.get_info_pins():
    for k, v in pin.items():
        print(k, v)

Class hal.component

component constants

Type constants:

  • hal.HAL_BIT - A boolean using True and False

  • hal.HAL_S32 - A signed quantity with range -231…​+231-1

  • hal.HAL_U32 - An unsigned quantity with range 0…​+232-1

  • hal.HAL_S64 - A signed quantity with range -263…​+263-1

  • hal.HAL_U64 - An unsigned quantity with range 0…​+264-1

  • hal.HAL_FLOAT - A floating point quantity of range ±1.80×10308

  • 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:
comp = hal.component("passthrough")
pin = comp.newpin(name:string, type:enum, io:enum)

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:
p_in = comp.newpin("in", hal.HAL_FLOAT, hal.HAL_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. The access argument specifies the allowed param access. Throws a ValueError exception if name already exists.

Example:
p_bloop = comp.newparam("bloop", hal.HAL_FLOAT, hal.HAL_RO)
comp.getitem(name:string)

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)

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)

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()

Returns a dictionary all pin and param names and their values. The pin or param name is the dictionary key.

comp.ready()

Tells the HAL system the component is initialized. Locks out adding pins.

comp.unready()

Allows a component to add pins after 'ready()' has been called. One should call 'ready()' on the component when done.

comp.getprefix()

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'.

comp.setprefix(prefix:string)

Set the prefix used when creating pins and params. The prefix is used to create pins and params called prefix.name and defaults to the components name.

Class hal.stream

The streamer class enables sending typed value blocks, samples of data, between real-time and non-real-time. The values carried in a stream have the same types as pins and params. A stream can source data from pins or sink data into pins using ready made components. The stream carries values through a FIFO queue up to a specified depth. The stream allocates and uses a shared memory segment that is not part of HAL memory and thus does not burden HAL memory usage. Each data sample may contain up to twenty (20) values. The type of the values in a sample must be configured and specified in a type string. The type string consists of the following characters (case insensitive):

  • b - Boolean

  • s - Signed 32-bit

  • u - Unsigned 32-bit

  • l - Signed 64-bit

  • k - Unsigned 64-bit

  • f - Floating point (real)

Two components are available, 'streamer' and 'sampler'. The 'streamer' component writes a set of pins from non-real-time data pushed into a stream. The 'sampler' component samples a set of pins in real-time and makes them available in a stream.

Note

If you use this Python hal module interface for both read and write on the same stream, then you must create the stream with depth and type string before you can attach to it. Instantiating with depth and type string creates the stream. Instantiating without depth attaches to the stream.

Both the 'sampler' and 'streamer' components create the stream. You only need to attach to it from the Python hal module.

Important

A stream can only have one (1) reader and one (1) writer. The stream queue is not designed to support operation with multiple readers or writers.

Example sampler - Python sample reader:
import hal
import time

comp = hal.component("samplereader")
# Attach to the sampler stream
samplereader = hal.stream(comp, hal.sampler_base, "bffs")
# ...
comp.ready()
# ...

hal.set_value("sampler.0.enable", True) # Start streaming samples

while True:
    while samplereader.readable:
        print(samplereader.read())
    time.sleep(0.001) # Don't busy-loop
Example sampler - hal-file sampler:
loadrt sampler depth=100 cfg=bffs

# Disable sampler before adding function to the thread or it would start
# sampling immediately and could fill the queue before we start reading.
setp sampler.0.enable 0

addf sampler.0 servo-thread

net sample-jogger  motion.jog-is-active sampler.0.pin.0
net sample-joint-0 joint.0.pos-fb       sampler.0.pin.1
net sample-joint-1 joint.1.pos-fb       sampler.0.pin.2
net sample-line    motion.program-line  sampler.0.pin.3

stream constants

hal.sampler_base

The 'sampler' component’s (sampler.c) shared memory ID for stream communication.

hal.stream_base

The 'streamer' component’s (streamer.c) shared memory ID for stream communication.

stream methods

stream = hal.stream(comp:object, key:int, depth:int, typestr:string)

Create a stream for the component comp using the shared memory identifier key. The stream’s queue size is allocated for depth samples of typestr format. See the type string list for type meaning.

stream = hal.stream(comp:object, key:int [, typestr:string])

Attach to a stream for the component comp where the shared memory location is identified by key. If the optional typestr is provided, then it will be checked against the existing stream’s configuration. See the type string list for type meaning.

stream.read()

Returns a tuple of sample data from the queue. None is returned if no samples were available.

stream.write(sample:tuple)

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()

Returns True if there are samples available for reading in the stream queue or False if not.

stream.writable()

Returns True if there is space available in the stream queue to hold more samples or False if not.

stream.depth()

Returns the number of currently available samples for read in the queue.

stream.maxdepth()

Returns the queue size as set when the stream was created (and cannot be changed).

stream.element_types()

Return a bytes object with the format type string that was used to create the stream. See type string list for individual elements and type meaning.

stream.num_underruns()

Returns the number of times 'stream.read()' was called when no samples were available from the queue.

stream.num_overruns()

Returns the number of times 'stream.write()' was called when no samples could be stored in the queue.

stream members

stream.sampleno

The last successfully read sample ID number as counted by the stream functions.

Class hal.shm

The shm class is an interface to create and manage shared memory segments.

The shm class should be considered experimental and may or may not work as intended.

Warning

Do not rely on this class. It may be removed in future releases.

shm methods

shm = hal.shm(comp:object, key:int, size:int)

Allocate a size sized shared memory segment with ID key.

shm.getbuffer()

Returns a Python memoryview object of the shared memory segment.

shm.setsize()

This is non-functional. Do not use. You cannot increase or decrease the shared memory segment’s size once it is created.

HAL Port pipes

A HAL port is a byte-oriented pipe that streams bytes from the writer to the reader. 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.

Example - HAL port data writer:
import hal
import time
import struct

comp = hal.component("portwriter")

# Create the write-end
portw = comp.newpin("portpin", hal.HAL_PORT, hal.HAL_OUT)

# Create a signal to link reader and writer
portsig = hal.new_sig("portsig", hal.HAL_PORT)
portw.ready()

# Connect the pins to the signal net
hal.connect("portwriter.portpin", "portsig")
hal.connect("portreader.portpin", "portsig")

# Allocate and set the port's queue size
hal.set_s("portsig", 256)

while True:
    if portw.writable() < 2:
        time.sleep(0.001) # Don't busy-loop
        continue
    cmd, arg = read_command()
    binvals = struct.pack("bb", cmd, arg)
    portw.write(binvals)
Example - HAL port data reader component:
component portreader "Reads data from a HAL port";
pin in port portpin  "Port's read end";

description "Port reader example component";

option singleton;
option period no;
license "GPL";
function _;

;;
void do_abort(void) { /* you write code here */ }
void kill_switch(char x) { (void)x; /* you write some more code here */ }

FUNCTION(_)
{
    unsigned avail = hal_port_readable(portpin_ptr);
    if(avail > 1) {
        rtapi_u8 buf[2];
        if(!hal_port_read(portpin_ptr, buf, sizeof(buf))) {
            rtapi_print_msg(RTAPI_MSG_ERR, "Port read failed\n");
            hal_port_clear(portpin_ptr);  // Try to recover
        } else {
            switch(buf[0]) {
            case 'a': do_abort(); break;
            case 'k': kill_switch(buf[1]); break;
            // ...
            }
        }
    }
}
Example - HAL port hal file to load the reader:
loadrt portreader

addf portreader servo.thread

A more comprehensive implementation can be found in the 'raster.comp' component together with the raster programmer.

Important

A HAL port pipes can only have one (1) reader and one (1) writer. The port queue is not designed to support operation with multiple readers or writers.

Note

A HAL port queue cannot be allocated larger than 64 kiB (65536 bytes). The minimum size is one (1) byte, but that is not recommended. 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)

A port is a special pin type. It is created as a normal pin with type hal.HAL_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()

Returns the number of bytes available for reading from the port queue.

port.writable()

Returns the number of bytes possible to write to the port queue.

port.write(data:bytes)

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. Strings are Unicode encoded and may expand to multiple bytes when converted to UTF-8. 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)

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)

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)

Removes size bytes from the port queue. Returns True on success or False if not.

Example:
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
data = port.peek(2)
# Check the data for a pattern
if data[0] == 123 and data[1] == 42:
    port.peek_commit(2)  # Discard data from the pipe
else:
    # Not the discardable pattern, need 4 bytes then
    wait_for(4)
    data = port.read(4)
    handle_data(data)
port.clear()

Clears the content of the port queue.

port.size()

Return the allocated port queue size. The return value is zero (0) if no queue was allocated for the port.