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/lib/python/hal.py b/lib/python/hal.py index e800dd3dcdf..d0b18b74964 100644 --- a/lib/python/hal.py +++ b/lib/python/hal.py @@ -29,10 +29,28 @@ import _hal from _hal import * +from _hal import query +import sys import warnings import lcnc_realtime +# _hal exports the IntEnum tagging classes as 'type' and 'dir'. The +# star import binds them here, but those names shadow the builtins used +# inside this module, so unbind them and serve them lazily through +# __getattr__ instead. hal.type.REAL, hal.dir.IN and the HALType/HALDir +# class names (also consulted by pickle) all resolve to the shared +# classes in _hal. +globals().pop('type', None) +globals().pop('dir', None) + +# _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 in ('type', 'dir', 'HALType', 'HALDir'): + return getattr(_hal, {'HALType': 'type', 'HALDir': 'dir'}.get(name, name)) if name == 'is_rt': warnings.warn(f"{name} is deprecated, use lcnc_realtime.verify() instead", FutureWarning, stacklevel=2) return lcnc_realtime.verify() diff --git a/src/Makefile b/src/Makefile index 8b6079b62f3..74b22225a50 100644 --- a/src/Makefile +++ b/src/Makefile @@ -394,6 +394,8 @@ build-software: headers $(INFILES) # SRCHEADERS := \ hal/hal.h \ + hal/hal.hh \ + hal/halenum.hh \ hal/drivers/mesa-hostmot2/hostmot2-serial.h \ emc/linuxcnc.h \ emc/kinematics/kinematics.h \ diff --git a/src/hal/Submakefile b/src/hal/Submakefile index 1827d7e10c8..55edbb384cf 100644 --- a/src/hal/Submakefile +++ b/src/hal/Submakefile @@ -3,6 +3,14 @@ ../include/hal.h: ./hal/hal.h cp $^ $@ +# hal.hh is the C++ interface on top of the public C API +../include/hal.hh: ./hal/hal.hh + cp $^ $@ + +# halenum.hh builds the shared Python IntEnum tagging classes +../include/halenum.hh: ./hal/halenum.hh + cp $^ $@ + HALLIBSRCS := hal/hal_lib.c hal/hal_lib_query.c hal/hal_lib_extra.c $(ULAPISRCS) $(call TOOBJSDEPS, $(HALLIBSRCS)): EXTRAFLAGS += -fPIC $(ULAPI_CFLAGS) USERSRCS += $(HALLIBSRCS) @@ -28,5 +36,27 @@ $(HALMODULE): $(call TOOBJS, $(HALMODULESRCS)) $(HALLIB) $(ECHO) Linking python module $(notdir $@) $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ +# pybind11 C++ bindings (hal.hh based) +# setps_util.c only exists in the tree with the HAL query API +# pybind11 headers: python3-pybind11 (system include) or pip user site. +# Without them the bindings are skipped, not failed: they are optional +# until the build dependency is made mandatory. +PYBIND11INCLUDES := $(shell $(PYTHON) -m pybind11 --includes 2>/dev/null) +ifneq ($(PYBIND11INCLUDES),) +HALPPSRCS := hal/halpybind.cc $(wildcard hal/utils/setps_util.c) +PYSRCS += $(HALPPSRCS) + +$(call TOOBJS, $(HALPPSRCS)): EXTRAFLAGS += $(PYBIND11INCLUDES) + +HALPP := ../lib/python/halpp.so +$(HALPP): $(call TOOBJS, $(HALPPSRCS)) $(HALLIB) + $(ECHO) Linking python module $(notdir $@) + $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ + +PYTARGETS += $(HALPP) +else +$(info NOTE: pybind11 headers not found, skipping the halpp bindings (install python3-pybind11)) +endif + TARGETS += $(HALLIB) ../lib/liblinuxcnchal.so.0 PYTARGETS += $(HALMODULE) diff --git a/src/hal/hal.hh b/src/hal/hal.hh index e4c9a40e637..f10463d8647 100644 --- a/src/hal/hal.hh +++ b/src/hal/hal.hh @@ -1,168 +1,831 @@ +/* + hal.hh - C++ interface for HAL + + A thin, type-safe C++ layer on top of the public HAL C API. + All pin/param access goes through the typed hal_get_X and hal_set_X + accessors and the user-land query API. No direct shared memory + access, no hal_priv.h, no re-implemented library internals. + + Header-only, no runtime overhead: typed access expands to the same + inline accessor calls as the C API. + + The by-name query/set section requires the HAL query API and is + compiled only when the public header defines the COMPONENT_TYPE_* + macros. The typed component/pin layer only needs the base + getter/setter API. + + HAL_PORT pins are intentionally not supported yet. The port handle + type and creation semantics are still in flux until the API break + ("the port change must be done later" in hal.h); a proper port + wrapper follows once hal_port_t and hal_pin_new_port() are final. +*/ #ifndef HALXX_HH #define HALXX_HH #include #include +#include #include -#include "hal.h" +#include +#include +#include +#include +#include +#include + +#include + +// The HAL query API (hal_get_p, hal_set_p, hal_comp_by_name, ...) is +// present when the public header defines the COMPONENT_TYPE_* macros. +#if defined(COMPONENT_TYPE_REALTIME) && !defined(HALXX_WITH_QUERY_API) +#define HALXX_WITH_QUERY_API 1 +#endif + +#ifndef RTAPI_SINT_MAX +#define RTAPI_SINT_MAX RTAPI_INT64_MAX +#define RTAPI_SINT_MIN RTAPI_INT64_MIN +#define RTAPI_UINT_MAX RTAPI_UINT64_MAX +#endif -#warning "Do not use hal.hh. It will be removed (and, eventually, replaced)." +namespace linuxcnc { +namespace hal { -enum class hal_dir{ - IN = HAL_IN, +// Unified pin/param direction. Values are identical to hal_pdir_t. +enum class dir : int { + IN = HAL_IN, OUT = HAL_OUT, + IO = HAL_IO, + RO = HAL_RO, + RW = HAL_RW, }; -#if 0 -// If this class is ever necessary, then it needs to be moved into a new -// header 'hal_priv.hh' because it uses internal access methods from -// 'hal_priv.h' that should not be available to the casual source file. +// Runtime value of a pin, param or signal. Used whenever the HAL type +// is not known at compile time (name-based access, script bindings). +using value_t = std::variant; -#include "hal_priv.h" +namespace detail { +// Error text that works with and without hal_strerror() in the library. +inline std::string errstr(int rv) +{ +#ifdef HALXX_WITH_QUERY_API + return hal_strerror(rv); +#else + return std::strerror(-rv); +#endif +} +} // namespace detail + +//---------------------------------------------------------------------- +// Type traits: map an rtapi_ value type to its HAL handle, HAL type and +// accessor/creator functions. Using an unsupported type is a compile +// error because traits is intentionally left undefined. +// +// The 32-bit versions (rtapi_s32, rtapi_u32) are compatibility types +// that will be retired after the API break; only their traits entries +// and the variant alternatives need to change then. +//---------------------------------------------------------------------- +template struct traits; -class hal{ - public: - static bool component_exists(const std::string& name){ - return halpr_find_comp_by_name(name.c_str()) != NULL; +template<> struct traits { + using handle_t = hal_bool_t; + static handle_t *slot(hal_refs_u *u) { return &u->b; } + static constexpr hal_type_t type = HAL_BOOL; + static rtapi_bool get(handle_t h) { return hal_get_bool(h); } + static rtapi_bool set(handle_t h, rtapi_bool v) { return hal_set_bool(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_bool def, const std::string &n) { + return hal_pin_new_bool(c, d, h, def, "%s", n.c_str()); } - static bool pin_has_writer(const std::string& name){ - hal_pin_t *pin = halpr_find_pin_by_name(name.c_str()); - if(!pin) {//pin does not exist - return false; - } - if(pin->signal) { - hal_sig_t *signal = (hal_sig_t*)SHMPTR(pin->signal); - return signal->writers > 0; - } - return false; + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_bool def, const std::string &n) { + return hal_param_new_bool(c, d, h, def, "%s", n.c_str()); + } +}; + +template<> struct traits { + using handle_t = hal_sint_t; + static handle_t *slot(hal_refs_u *u) { return &u->s; } + static constexpr hal_type_t type = HAL_S32; + static rtapi_s32 get(handle_t h) { return hal_get_si32(h); } + static rtapi_s32 set(handle_t h, rtapi_s32 v) { return hal_set_si32(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_s32 def, const std::string &n) { + return hal_pin_new_si32(c, d, h, def, "%s", n.c_str()); } - static bool component_is_ready(const std::string& name){ - // Bad form to assume comp name exists - stop crashing! - hal_comp_t *thecomp = halpr_find_comp_by_name(name.c_str()); - return thecomp && (thecomp->ready != 0); + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_s32 def, const std::string &n) { + return hal_param_new_si32(c, d, h, def, "%s", n.c_str()); } }; -#endif -template -class hal_pin{ - public: - volatile T** ptr; - T operator=(const T& value){ - **ptr = value; - return **ptr; +template<> struct traits { + using handle_t = hal_uint_t; + static handle_t *slot(hal_refs_u *u) { return &u->u; } + static constexpr hal_type_t type = HAL_U32; + static rtapi_u32 get(handle_t h) { return hal_get_ui32(h); } + static rtapi_u32 set(handle_t h, rtapi_u32 v) { return hal_set_ui32(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_u32 def, const std::string &n) { + return hal_pin_new_ui32(c, d, h, def, "%s", n.c_str()); } - operator T(){ - return **ptr; + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_u32 def, const std::string &n) { + return hal_param_new_ui32(c, d, h, def, "%s", n.c_str()); } }; -using pin_t = std::variant,hal_pin,hal_pin,hal_pin>; - -class hal_comp{ - int comp_id; - std::string comp_name; - std::map map; - int add_pin_(const std::string& name, hal_dir dir, hal_pin pin){ - return hal_pin_new(name.c_str(), HAL_BIT, static_cast(dir), (void **)(pin.ptr), comp_id); - } - int add_pin_(const std::string& name, hal_dir dir, hal_pin pin){ - return hal_pin_new(name.c_str(), HAL_S32, static_cast(dir), (void **)(pin.ptr), comp_id); - } - int add_pin_(const std::string& name, hal_dir dir, hal_pin pin){ - return hal_pin_new(name.c_str(), HAL_U32, static_cast(dir), (void **)(pin.ptr), comp_id); - } - int add_pin_(const std::string& name, hal_dir dir, hal_pin pin){ - return hal_pin_new(name.c_str(), HAL_FLOAT, static_cast(dir), (void **)(pin.ptr), comp_id); - } - public: - int error = 0; - hal_comp(const std::string& name){ - comp_id = hal_init(name.c_str()); - comp_name = name; - if(comp_id < 0){ - error -= 1; - rtapi_print_msg(RTAPI_MSG_ERR, "%s ERROR: hal_init() failed\n", comp_name.c_str()); - hal_exit(comp_id); - } +template<> struct traits { + using handle_t = hal_sint_t; + static handle_t *slot(hal_refs_u *u) { return &u->s; } + static constexpr hal_type_t type = HAL_SINT; + static rtapi_sint get(handle_t h) { return hal_get_sint(h); } + static rtapi_sint set(handle_t h, rtapi_sint v) { return hal_set_sint(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_sint def, const std::string &n) { + return hal_pin_new_sint(c, d, h, def, "%s", n.c_str()); } - hal_comp() = delete; + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_sint def, const std::string &n) { + return hal_param_new_sint(c, d, h, def, "%s", n.c_str()); + } +}; - void newpin(const std::string& name, hal_type_t type, hal_dir dir){ - auto& pin = map[name]; - switch(type){ - case HAL_BIT: - pin = hal_pin(); - add_pin(name, dir, std::get>(pin)); - break; - case HAL_FLOAT: - pin = hal_pin(); - add_pin(name, dir, std::get>(pin)); - break; - case HAL_S32: - pin = hal_pin(); - add_pin(name, dir, std::get>(pin)); - break; - case HAL_U32: - pin = hal_pin(); - add_pin(name, dir, std::get>(pin)); - break; - [[fallthrough]]; - default: - break; - } +template<> struct traits { + using handle_t = hal_uint_t; + static handle_t *slot(hal_refs_u *u) { return &u->u; } + static constexpr hal_type_t type = HAL_UINT; + static rtapi_uint get(handle_t h) { return hal_get_uint(h); } + static rtapi_uint set(handle_t h, rtapi_uint v) { return hal_set_uint(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_uint def, const std::string &n) { + return hal_pin_new_uint(c, d, h, def, "%s", n.c_str()); + } + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_uint def, const std::string &n) { + return hal_param_new_uint(c, d, h, def, "%s", n.c_str()); + } +}; + +template<> struct traits { + using handle_t = hal_real_t; + static handle_t *slot(hal_refs_u *u) { return &u->r; } + static constexpr hal_type_t type = HAL_REAL; + static rtapi_real get(handle_t h) { return hal_get_real(h); } + static rtapi_real set(handle_t h, rtapi_real v) { return hal_set_real(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_real def, const std::string &n) { + return hal_pin_new_real(c, d, h, def, "%s", n.c_str()); + } + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_real def, const std::string &n) { + return hal_param_new_real(c, d, h, def, "%s", n.c_str()); + } +}; + +//---------------------------------------------------------------------- +// pin - typed pin or param handle. +// +// Holds a pointer to the handle slot in HAL shared memory and re-reads +// it on every access: hal_link() may rewrite the slot when the pin is +// linked to a signal, exactly like a pin pointer variable in the C API. +// All access goes through the type's inline hal_get_*/hal_set_* +// accessor. +// +// Pins, params and signals are unique HAL objects. Their handles are +// not copyable (no reference counting); use references or move +// semantics. dup() creates an explicit second handle to the same slot +// where that is really intended. +//---------------------------------------------------------------------- +template +class pin { +public: + using value_type = T; + using handle_t = typename traits::handle_t; + + pin() = default; + explicit pin(handle_t *slot) : slot_(slot) {} + pin(const pin &) = delete; + pin &operator=(const pin &) = delete; + pin(pin &&) = default; + pin &operator=(pin &&) = default; + + // Explicit second handle to the same HAL object. + pin dup() const { return pin(slot_); } + + T get() const { check(); return traits::get(*slot_); } + T set(T v) const { check(); return traits::set(*slot_, v); } + + operator T() const { return get(); } + T operator=(T v) { return set(v); } + + handle_t handle() const { check(); return *slot_; } + bool valid() const { return nullptr != slot_ && nullptr != *slot_; } + +private: + void check() const { + if(!slot_) + throw std::logic_error("hal::pin: access to uninitialized pin handle"); + } + handle_t *slot_ = nullptr; +}; + +//---------------------------------------------------------------------- +// pin_t - runtime-typed pin/param/ports. The variant index is the +// stored type tag used for multiplexing, as required for any +// heterogeneous (name-keyed) collection of HAL items. +//---------------------------------------------------------------------- +using pin_t = std::variant, pin, pin, + pin, pin, pin>; + +namespace detail { + +// In-place scalar access on a runtime-typed item. Port pins have no +// scalar value. +inline value_t pin_get(const pin_t &p) +{ + return std::visit([](auto &&pp) -> value_t { return pp.get(); }, p); +} + +inline void pin_set(pin_t &p, const value_t &v) +{ + std::visit([&v](auto &&pp) { + using P = std::decay_t; + pp.set(std::visit([](auto &&x) -> typename P::value_type { + return static_cast(x); + }, v)); + }, p); +} + +inline hal_type_t pin_type(const pin_t &p) +{ + return std::visit([](auto &&pp) -> hal_type_t { + return traits::value_type>::type; + }, p); +} + +} // namespace detail + +//---------------------------------------------------------------------- +// anypin - a pin_t plus its full HAL name. This is the object handed +// to script bindings (pybind11) and generic code. +//---------------------------------------------------------------------- +class anypin { +public: + anypin() = default; + anypin(pin_t p, std::string name) : p_(std::move(p)), name_(std::move(name)) {} + anypin(const anypin &) = delete; + anypin &operator=(const anypin &) = delete; + anypin(anypin &&) = default; + anypin &operator=(anypin &&) = default; + + const std::string &name() const { return name_; } + + hal_type_t type() const { return detail::pin_type(p_); } + + value_t get() const { return detail::pin_get(p_); } + void set(const value_t &v) { detail::pin_set(p_, v); } + +private: + pin_t p_; + std::string name_; +}; + +//---------------------------------------------------------------------- +// component - a userspace HAL component. Owns the comp_id and keeps a +// name-keyed map of its pins and params. +//---------------------------------------------------------------------- +class component { +public: + explicit component(const std::string &name) : prefix_(name) { + id_ = hal_init(name.c_str()); + if(id_ < 0) + throw std::runtime_error("hal::component: hal_init(" + name + ") failed: " + detail::errstr(id_)); } + component() = delete; + component(const component &) = delete; + component &operator=(const component &) = delete; + ~component() { exit(); } + + int id() const { return id_; } + + void setprefix(const std::string &p) { prefix_ = p; } + std::string getprefix() const { return prefix_; } - std::variant getitem(const std::string& name){ - auto pin = map[name]; - if (auto* v = std::get_if>(&pin)) { - return *v; - } else if (auto* v = std::get_if>(&pin)) { - return *v; - } else if (auto* v = std::get_if>(&pin)) { - return *v; - } else if (auto* v = std::get_if>(&pin)) { - return *v; + void ready() { + int rv = hal_ready(id_); + if(rv) + throw std::runtime_error("hal::component: hal_ready failed: " + detail::errstr(rv)); + } + + void exit() { + if(id_ > 0) + hal_exit(id_); + id_ = -1; + } + + // Create a typed pin "." and keep it in the item map. + // The handle slot is allocated from HAL shared memory (hal_malloc), + // as required by the pin/param creation API: hal_link later updates + // the value through this slot, so it must live in HAL memory. Like + // halmodule, the slot is released with the component's HAL memory. + template + pin newpin(const std::string &name, dir d, T def = T{}) { + hal_refs_u *u = (hal_refs_u *)hal_malloc(sizeof(*u)); + if(!u) + throw std::runtime_error("hal::component: newpin(" + name + "): hal_malloc failed"); + int rv = traits::new_pin(id_, (hal_pdir_t)d, traits::slot(u), def, fullname(name)); + if(rv) + throw std::runtime_error("hal::component: newpin(" + name + ") failed: " + detail::errstr(rv)); + items_.emplace(name, pin(traits::slot(u))); + return pin(traits::slot(u)); + } + + // Attach a new pin to a member handle. This is the struct-member + // idiom for components: declare pin members in your instance + // struct and register them with add_pin(). + template + void add_pin(const std::string &name, dir d, pin &target) { + target = newpin(name, d); + } + + // Runtime-typed pin creation (script bindings). Returns an anypin. + anypin newpin(const std::string &name, hal_type_t type, dir d) { + switch(type) { + case HAL_BOOL: return wrap(name, newpin(name, d)); + case HAL_S32: return wrap(name, newpin(name, d)); + case HAL_U32: return wrap(name, newpin(name, d)); + case HAL_SINT: return wrap(name, newpin(name, d)); + case HAL_UINT: return wrap(name, newpin(name, d)); + case HAL_REAL: return wrap(name, newpin(name, d)); + default: + throw std::invalid_argument("hal::component: newpin(" + name + "): unsupported type"); } - return 0; } + // Create a typed parameter ".". template - void setitem(const std::string& name, T value){ - auto pin = map[name]; - if (auto* p = std::get_if>(&pin)) { - *p = value; - } else if (auto* p = std::get_if>(&pin)) { - *p = value; - } else if (auto* p = std::get_if>(&pin)) { - *p = value; - } else if (auto* p = std::get_if>(&pin)) { - *p = value; + pin newparam(const std::string &name, dir d, T def = T{}) { + hal_refs_u *u = (hal_refs_u *)hal_malloc(sizeof(*u)); + if(!u) + throw std::runtime_error("hal::component: newparam(" + name + "): hal_malloc failed"); + int rv = traits::new_param(id_, (hal_pdir_t)d, traits::slot(u), def, fullname(name)); + if(rv) + throw std::runtime_error("hal::component: newparam(" + name + ") failed: " + detail::errstr(rv)); + params_.emplace(name, pin(traits::slot(u))); + return pin(traits::slot(u)); + } + + anypin newparam(const std::string &name, hal_type_t type, dir d) { + switch(type) { + case HAL_BOOL: return wrap(name, newparam(name, d)); + case HAL_S32: return wrap(name, newparam(name, d)); + case HAL_U32: return wrap(name, newparam(name, d)); + case HAL_SINT: return wrap(name, newparam(name, d)); + case HAL_UINT: return wrap(name, newparam(name, d)); + case HAL_REAL: return wrap(name, newparam(name, d)); + default: + throw std::invalid_argument("hal::component: newparam(" + name + "): unsupported type"); } } - void ready(){ - hal_ready(comp_id); + // Item access by short name. Pins and params share one namespace. + value_t getitem(const std::string &name) const { return detail::pin_get(find(name)); } + + template + void setitem(const std::string &name, T value) { detail::pin_set(find(name), value_t(value)); } + + bool contains(const std::string &name) const { + return items_.count(name) || params_.count(name); } +private: template - void add_pin(const std::string& pin_name, hal_dir dir, hal_pin &pin){ - pin.ptr = (volatile T**)hal_malloc(8); - if(!pin.ptr){ - error -= 1; - rtapi_print_msg(RTAPI_MSG_ERR, "%s ERROR: hal_malloc() failed\n", pin_name.c_str()); - hal_exit(comp_id); + anypin wrap(const std::string &name, pin p) { return anypin(pin_t(std::move(p)), fullname(name)); } + + pin_t &find(const std::string &name) { + if(auto it = items_.find(name); it != items_.end()) + return it->second; + if(auto it = params_.find(name); it != params_.end()) + return it->second; + throw std::out_of_range("hal::component: no pin or param '" + name + "'"); + } + const pin_t &find(const std::string &name) const { + return const_cast(this)->find(name); + } + + std::string fullname(const std::string &n) const { return prefix_ + "." + n; } + + int id_ = -1; + std::string prefix_; + std::map items_; + std::map params_; +}; + +//---------------------------------------------------------------------- +// Streams. hal_stream_t is the fixed-depth sample FIFO behind sampler +// and streamer: one component creates it with a depth and a typestring, +// another attaches to the same integer key. Each character of the +// typestring names the type of one element of a sample. +//---------------------------------------------------------------------- +namespace detail { + +// The typestring characters used by hal_stream_create(), as reported +// back through hal_stream_element_type(). +inline char stream_typechar(hal_type_t t) +{ + switch(t) { + case HAL_BOOL: return 'b'; + case HAL_REAL: return 'f'; + case HAL_S32: return 's'; + case HAL_U32: return 'u'; + case HAL_SINT: return 'l'; + case HAL_UINT: return 'k'; + default: return '?'; + } +} + +inline value_t value_from_stream(hal_type_t t, const hal_stream_data_u &d) +{ + switch(t) { + case HAL_BOOL: return (rtapi_bool)d.b; + case HAL_S32: return (rtapi_s32)d.s; + case HAL_U32: return (rtapi_u32)d.u; + case HAL_SINT: return (rtapi_sint)d.l; + case HAL_UINT: return (rtapi_uint)d.k; + case HAL_REAL: return (rtapi_real)d.f; + default: + throw std::invalid_argument("hal::stream: element has an unsupported type"); + } +} + +// Coerce a runtime value into a stream element of the given type. +// Returns false on a range error; the caller reports it. +inline bool convert_stream_value(hal_type_t target, const value_t &v, hal_stream_data_u *out) +{ + bool ok = true; + std::visit([&ok, out, target](auto &&x) { + long double xv = static_cast(x); + switch(target) { + case HAL_BOOL: + out->b = (0 != xv); + break; + case HAL_S32: + if(xv < RTAPI_INT32_MIN || xv > RTAPI_INT32_MAX) { ok = false; break; } + out->s = static_cast(xv); break; + case HAL_U32: + if(xv < 0 || xv > RTAPI_UINT32_MAX) { ok = false; break; } + out->u = static_cast(xv); break; + case HAL_SINT: + if(xv < (long double)RTAPI_SINT_MIN || xv > (long double)RTAPI_SINT_MAX) { ok = false; break; } + out->l = static_cast(xv); break; + case HAL_UINT: + if(xv < 0 || xv > (long double)RTAPI_UINT_MAX) { ok = false; break; } + out->k = static_cast(xv); break; + case HAL_REAL: + out->f = static_cast(xv); break; + default: + ok = false; } - error += add_pin_(comp_name + "." + pin_name, dir, pin); - if(error < 0){ - rtapi_print_msg(RTAPI_MSG_ERR, "%s ERROR: hal_pin_new() failed\n", pin_name.c_str()); - hal_exit(comp_id); + }, v); + return ok; +} + +} // namespace detail + +//---------------------------------------------------------------------- +// stream - an open HAL stream, either created (and owned) or attached +// to. The library permits only one reader and one writer, but does not +// enforce it. +// +// Like the other HAL objects, a stream is move-only: destroying or +// detaching twice would corrupt the FIFO's user counts. +//---------------------------------------------------------------------- +class stream { +public: + // Create a stream holding 'depth' samples of the layout described + // by 'typestring'. The stream is destroyed with this object. + stream(component &comp, int key, unsigned depth, const std::string &typestring) + : key_(key), creator_(true) + { + int rv = hal_stream_create(&s_, comp.id(), key, depth, typestring.c_str()); + if(rv < 0) + throw std::system_error(-rv, std::generic_category(), + "hal::stream: create(" + std::to_string(key) + ", " + typestring + ") failed"); + open_ = true; + read_element_types(); + } + + // Attach to an existing stream. An empty typestring accepts + // whatever layout the stream was created with; a non-empty one must + // match it. + stream(component &comp, int key, const std::string &typestring = std::string()) + : key_(key), creator_(false) + { + int rv = hal_stream_attach(&s_, comp.id(), key, + typestring.empty() ? nullptr : typestring.c_str()); + if(rv < 0) + throw std::system_error(-rv, std::generic_category(), + "hal::stream: attach(" + std::to_string(key) + ") failed"); + open_ = true; + read_element_types(); + } + + stream() = delete; + stream(const stream &) = delete; + stream &operator=(const stream &) = delete; + stream(stream &&o) noexcept { adopt(o); } + stream &operator=(stream &&o) noexcept { + if(this != &o) { close(); adopt(o); } + return *this; + } + ~stream() { close(); } + + // Destroy (creator) or detach from (attacher) the stream. Further + // access throws; this is what the destructor does. + void close() { + if(!open_) + return; + open_ = false; + if(creator_) + hal_stream_destroy(&s_); + else + hal_stream_detach(&s_); + } + + int key() const { return key_; } + bool is_creator() const { return creator_; } + bool is_open() const { return open_; } + + int element_count() const { return (int)types_.size(); } + hal_type_t element_type(int idx) const { + if(idx < 0 || idx >= element_count()) + throw std::out_of_range("hal::stream: element index out of range"); + return types_[idx]; + } + // The layout in hal_stream_create() typestring form. + const std::string &typestring() const { return typestring_; } + + // Read one sample. Returns nothing when the stream is empty, which + // also counts an underrun in the library. + std::optional> read() { + if(types_.empty()) + return std::nullopt; + std::vector buf(types_.size()); + if(hal_stream_read(handle(), buf.data(), &sampleno_) < 0) + return std::nullopt; + std::vector out; + out.reserve(types_.size()); + for(size_t i = 0; i < types_.size(); i++) + out.push_back(detail::value_from_stream(types_[i], buf[i])); + return out; + } + + // Write one sample. The values are coerced to the element types + // with range checks. Writing to a full stream fails and counts an + // overrun in the library. + void write(const std::vector &data) { + if(data.size() != types_.size()) + throw std::invalid_argument("hal::stream: write expects " + + std::to_string(types_.size()) + " elements, got " + std::to_string(data.size())); + std::vector buf(types_.size()); + for(size_t i = 0; i < types_.size(); i++) + if(!detail::convert_stream_value(types_[i], data[i], &buf[i])) + throw std::out_of_range("hal::stream: element " + std::to_string(i) + + " does not fit its type"); + int rv = hal_stream_write(handle(), buf.data()); + if(rv < 0) + throw std::system_error(-rv, std::generic_category(), "hal::stream: write failed"); + } + + bool readable() const { return hal_stream_readable(handle()); } + bool writable() const { return hal_stream_writable(handle()); } + int depth() const { return hal_stream_depth(handle()); } + unsigned maxdepth() const { return hal_stream_maxdepth(handle()); } + int num_underruns() const { return hal_stream_num_underruns(handle()); } + int num_overruns() const { return hal_stream_num_overruns(handle()); } + + // Number of the last sample read(). + unsigned sampleno() const { return sampleno_; } + +private: + // The C API takes a non-const hal_stream_t * even where it only + // reads, so the const accessors go through here. + hal_stream_t *handle() const { + if(!open_) + throw std::logic_error("hal::stream: access to a closed stream"); + return const_cast(&s_); + } + + void read_element_types() { + int n = hal_stream_element_count(&s_); + for(int i = 0; i < n; i++) { + hal_type_t t = hal_stream_element_type(&s_, i); + types_.push_back(t); + typestring_.push_back(detail::stream_typechar(t)); } } - ~hal_comp(){ - hal_exit(comp_id); + void adopt(stream &o) { + s_ = o.s_; + types_ = std::move(o.types_); + typestring_ = std::move(o.typestring_); + key_ = o.key_; + creator_ = o.creator_; + sampleno_ = o.sampleno_; + open_ = o.open_; + o.open_ = false; } + + hal_stream_t s_ = {}; + std::vector types_; + std::string typestring_; + int key_ = 0; + bool creator_ = false; + bool open_ = false; + unsigned sampleno_ = 0; }; -#endif +//---------------------------------------------------------------------- +// Signal management, thin wrappers over the C API (user-land only). +//---------------------------------------------------------------------- +#ifdef ULAPI +inline int signal_new(const std::string &name, hal_type_t type) +{ + return hal_signal_new(name.c_str(), type); +} +inline int link(const std::string &pin_name, const std::string &sig_name) +{ + return hal_link(pin_name.c_str(), sig_name.c_str()); +} +inline int unlink(const std::string &pin_name) +{ + return hal_unlink(pin_name.c_str()); +} +inline int signal_delete(const std::string &name) +{ + return hal_signal_delete(name.c_str()); +} +#endif // ULAPI + +//---------------------------------------------------------------------- +// Userspace by-name query and set API. Implemented on the public HAL +// query API (hal_get_p/hal_set_p/hal_get_s/hal_set_s/hal_comp_by_name). +// This section is user-space only by definition: the query API itself +// is only declared under ULAPI, so this code cannot be used in RTAPI. +//---------------------------------------------------------------------- +#if defined(ULAPI) && defined(HALXX_WITH_QUERY_API) + +namespace detail { + +// Convert a runtime value to the requested HAL type with range checks. +// Must not throw: it is called from query callbacks while the HAL +// mutex is held, and unwinding through the library would keep the +// mutex locked and wedge the whole HAL session. Returns false on a +// range/type error, the caller reports it after the library call. +inline bool convert_value(hal_type_t target, const value_t &v, hal_query_value_u *out) +{ + bool ok = true; + std::visit([&ok, out, target](auto &&x) { + long double xv = static_cast(x); + switch(target) { + case HAL_BOOL: + out->b = (0 != xv); + break; + case HAL_S32: + if(xv < RTAPI_INT32_MIN || xv > RTAPI_INT32_MAX) { ok = false; break; } + out->s = static_cast(xv); break; + case HAL_U32: + if(xv < 0 || xv > RTAPI_UINT32_MAX) { ok = false; break; } + out->u = static_cast(xv); break; + case HAL_SINT: + if(xv < (long double)RTAPI_SINT_MIN || xv > (long double)RTAPI_SINT_MAX) { ok = false; break; } + out->s = static_cast(xv); break; + case HAL_UINT: + if(xv < 0 || xv > (long double)RTAPI_UINT_MAX) { ok = false; break; } + out->u = static_cast(xv); break; + case HAL_REAL: + out->r = static_cast(xv); break; + default: + ok = false; + } + }, v); + return ok; +} + +inline value_t value_from_query(hal_type_t t, const hal_query_value_u &v) +{ + switch(t) { + case HAL_BOOL: return (rtapi_bool)v.b; + case HAL_S32: return (rtapi_s32)v.s; + case HAL_U32: return (rtapi_u32)v.u; + case HAL_SINT: return (rtapi_sint)v.s; + case HAL_UINT: return (rtapi_uint)v.u; + case HAL_REAL: return (rtapi_real)v.r; + default: + throw std::invalid_argument("hal: item has no scalar value (port or unknown type)"); + } +} + +// Setter callbacks: fill the query's value union coerced to the item's +// actual type. Called with the HAL mutex held, hence no exceptions, +// no allocation and no termination; see convert_value. +struct coerce_req { + const value_t *v; + bool failed; +}; +inline int coerce_pp_cb(hal_query_t *q, void *arg) +{ + auto *req = static_cast(arg); + if(!convert_value(q->pp.type, *req->v, &q->pp.value)) { + req->failed = true; + return -ERANGE; + } + return 0; +} +inline int coerce_sig_cb(hal_query_t *q, void *arg) +{ + auto *req = static_cast(arg); + if(!convert_value(q->sig.type, *req->v, &q->sig.value)) { + req->failed = true; + return -ERANGE; + } + return 0; +} + +} // namespace detail + +// True if a component with this name is loaded. +inline bool component_exists(const std::string &name) +{ + hal_query_t q = {}; + return 0 == hal_comp_by_name(name.c_str(), &q); +} + +// True if the component exists and has called hal_ready(). +inline bool component_is_ready(const std::string &name) +{ + hal_query_t q = {}; + return 0 == hal_comp_by_name(name.c_str(), &q) && q.comp.ready; +} + +// True if the pin exists, is connected to a signal, and that signal +// has at least one writer. +inline bool pin_has_writer(const std::string &name) +{ + hal_query_t q = {}; + q.name = name.c_str(); + q.qtype = HAL_QTYPE_PIN; + if(0 != hal_getref_p(&q) || !q.pp.signal) + return false; + hal_query_t sq = {}; + sq.name = q.pp.signal; + if(0 != hal_getref_s(&sq)) + return false; + return sq.sig.writers > 0; +} + +// Read the value of a pin, param or signal by name. Throws +// std::invalid_argument with the library error if the lookup fails. +inline value_t get_value(const std::string &name) +{ + hal_query_t q = {}; + q.name = name.c_str(); + int rv = hal_get_p(&q, nullptr, nullptr); + if(0 == rv) + return detail::value_from_query(q.pp.type, q.pp.value); + if(0 == (rv = hal_get_s(&q, nullptr, nullptr))) + return detail::value_from_query(q.sig.type, q.sig.value); + throw std::invalid_argument("hal: get_value(" + name + ") failed: " + detail::errstr(rv)); +} + +// Set a pin or param by name ("setp"). The value is coerced to the +// item's actual HAL type with range checks. +inline void set_value(const std::string &name, const value_t &v) +{ + hal_query_t q = {}; + q.name = name.c_str(); + detail::coerce_req req{&v, false}; + int rv = hal_set_p(&q, detail::coerce_pp_cb, &req); + if(req.failed) + throw std::out_of_range("hal: set_value(" + name + "): value does not fit the item's type"); + if(rv) + throw std::invalid_argument("hal: set_value(" + name + ") failed: " + detail::errstr(rv)); +} + +// Set a signal by name ("sets"). +inline void set_signal(const std::string &name, const value_t &v) +{ + hal_query_t q = {}; + q.name = name.c_str(); + detail::coerce_req req{&v, false}; + int rv = hal_set_s(&q, detail::coerce_sig_cb, &req); + if(req.failed) + throw std::out_of_range("hal: set_signal(" + name + "): value does not fit the signal's type"); + if(rv) + throw std::invalid_argument("hal: set_signal(" + name + ") failed: " + detail::errstr(rv)); +} + +#endif // ULAPI && HALXX_WITH_QUERY_API + +} // namespace hal +} // namespace linuxcnc + +//---------------------------------------------------------------------- +// Compatibility aliases for code written against the previous hal.hh +// (pybind11 branch). New code should use linuxcnc::hal names. +//---------------------------------------------------------------------- +using hal_dir = linuxcnc::hal::dir; +using hal_comp = linuxcnc::hal::component; +using PyPin = linuxcnc::hal::anypin; +template using hal_pin = linuxcnc::hal::pin; + +#endif // HALXX_HH diff --git a/src/hal/halenum.hh b/src/hal/halenum.hh new file mode 100644 index 00000000000..0a371ac115a --- /dev/null +++ b/src/hal/halenum.hh @@ -0,0 +1,149 @@ +/* + halenum.hh - native IntEnum classes for HAL type/direction tagging + + Single source of truth for the Python-visible enums: the member + values are the hal.h constants themselves, so the classes cannot + drift from the C headers. Built through the plain Python C API (the + enum module's functional interface) so that any extension module can + create or instantiate them, with or without pybind11. + + _hal builds the two classes at module init and registers them as + _hal.type and _hal.dir; every other consumer fetches those shared + classes with halenum_shared_class() instead of building its own. + + TODO: when the oldest supported pybind11 is 3.0 or newer, class + construction can move to py::native_enum and instantiation to its + casters. The Python-visible classes stay enum.IntEnum either way, + so user code will not notice the switch. +*/ +#ifndef HALENUM_HH +#define HALENUM_HH + +#include + +#include + +struct halenum_member { + const char *name; + long value; +}; + +// Canonical members use the macro spellings where they are +// platform-stable (hal.h defines HAL_BOOL and HAL_REAL as macros over +// the enumerators). HAL_SINT and HAL_UINT are macros too, but they map +// to HAL_S32/HAL_U32 or HAL_S64/HAL_U64 depending on the platform, so +// the fixed-width spellings stay canonical and SINT/UINT follow as +// aliases. The HAL_* spellings are aliases as well. The enum module +// preserves dict order, so the first occurrence of each value is the +// canonical member. +static const halenum_member halenum_type_members[] = { + {"BOOL", HAL_BOOL}, + {"REAL", HAL_REAL}, + {"S32", HAL_S32}, + {"U32", HAL_U32}, + {"PORT", HAL_PORT}, + {"S64", HAL_S64}, + {"U64", HAL_U64}, + {"SINT", HAL_SINT}, + {"UINT", HAL_UINT}, + {"HAL_BOOL", HAL_BOOL}, + {"HAL_BIT", HAL_BIT}, + {"HAL_REAL", HAL_REAL}, + {"HAL_FLOAT", HAL_FLOAT}, + {"HAL_SINT", HAL_SINT}, + {"HAL_S32", HAL_S32}, + {"HAL_UINT", HAL_UINT}, + {"HAL_U32", HAL_U32}, + {"HAL_PORT", HAL_PORT}, + {"HAL_S64", HAL_S64}, + {"HAL_U64", HAL_U64}, +}; + +static const halenum_member halenum_dir_members[] = { + {"IN", HAL_IN}, + {"OUT", HAL_OUT}, + {"IO", HAL_IO}, + {"RO", HAL_RO}, + {"WO", HAL_WO}, + {"RW", HAL_RW}, + {"HAL_IN", HAL_IN}, + {"HAL_OUT", HAL_OUT}, + {"HAL_IO", HAL_IO}, + {"HAL_RO", HAL_RO}, + {"HAL_WO", HAL_WO}, + {"HAL_RW", HAL_RW}, +}; + +// 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 inline PyObject *halenum_build(const char *clsname, + const halenum_member *members, size_t n) +{ + 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; i < n; 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; +} + +static inline PyObject *halenum_make_type(void) +{ + return halenum_build("HALType", halenum_type_members, + sizeof(halenum_type_members)/sizeof(halenum_type_members[0])); +} + +static inline PyObject *halenum_make_dir(void) +{ + return halenum_build("HALDir", halenum_dir_members, + sizeof(halenum_dir_members)/sizeof(halenum_dir_members[0])); +} + +// Fetch one of the shared classes registered by _hal ("type" or "dir"), +// importing _hal if necessary. New reference. +static inline PyObject *halenum_shared_class(const char *attr) +{ + PyObject *m = PyImport_ImportModule("_hal"); + if(!m) + return NULL; + PyObject *cls = PyObject_GetAttrString(m, attr); + Py_DECREF(m); + return cls; +} + +// Instantiate a member: halenum_instance(cls, HAL_FLOAT) is HALType.REAL. +// New reference, or NULL with ValueError set for an unknown value. +static inline PyObject *halenum_instance(PyObject *cls, long value) +{ + return PyObject_CallFunction(cls, "l", value); +} + +#endif diff --git a/src/hal/halmodule.cc b/src/hal/halmodule.cc index 4fe9f7165e0..24eeac64bfc 100644 --- a/src/hal/halmodule.cc +++ b/src/hal/halmodule.cc @@ -27,6 +27,7 @@ #include #include #include "utils/setps_util.h" +#include "halenum.hh" #define EXCEPTION_IF_NOT_LIVE(retval) do { \ if(self->hal_id <= 0) { \ @@ -2191,11 +2192,26 @@ PyMODINIT_FUNC PyInit__hal(void) PyModule_AddIntConstant(m, "HAL_PORT", HAL_PORT); PyModule_AddIntConstant(m, "HAL_RO", HAL_RO); + PyModule_AddIntConstant(m, "HAL_WO", HAL_WO); PyModule_AddIntConstant(m, "HAL_RW", HAL_RW); PyModule_AddIntConstant(m, "HAL_IN", HAL_IN); 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 *haltype = halenum_make_type(); + PyObject *haldir = halenum_make_dir(); + if(!haltype || !haldir + || PyModule_AddObject(m, "type", haltype) + || PyModule_AddObject(m, "dir", haldir)) { + Py_XDECREF(haltype); + Py_XDECREF(haldir); + 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); @@ -2212,6 +2228,18 @@ PyMODINIT_FUNC PyInit__hal(void) PyModule_AddIntConstant(m, "streamer_base", 0x48535430); PyModule_AddIntConstant(m, "sampler_base", 0x48534130); + // The HAL query API is exposed as a submodule of _hal, which hal.py + // registers under the name hal.query. This is a placeholder that + // carries no bindings yet; it establishes where they will live. + PyObject *query = PyModule_New("_hal.query"); + if(!query) { + Py_DECREF(m); + return NULL; + } + PyModule_AddStringConstant(query, "__doc__", + "Interface to the HAL query API. No bindings yet."); + PyModule_AddObject(m, "query", query); + #ifdef RTAPI_KERNEL_VERSION PyModule_AddStringConstant(m, "kernel_version", RTAPI_KERNEL_VERSION); #else diff --git a/src/hal/halpybind.cc b/src/hal/halpybind.cc new file mode 100644 index 00000000000..dc870d80048 --- /dev/null +++ b/src/hal/halpybind.cc @@ -0,0 +1,284 @@ +/* + halpybind.cc - Python bindings for HAL via pybind11 + + Thin binding layer over the C++ HAL interface (hal.hh). All HAL + access goes through the public C API and the query API; this module + contains no HAL internals. + + Exposes: + component - userspace component with pins/params + Pin - runtime-typed pin/param reference + stream - sample FIFO shared with sampler/streamer + module fns - by-name get/set (when the query API is available), + signals, component queries +*/ +#include +#include + +#include +#include + +#include "hal.hh" +#include "halenum.hh" + +#ifdef HALXX_WITH_QUERY_API +#include "utils/setps_util.h" +#endif + +namespace py = pybind11; +namespace halxx = linuxcnc::hal; + +namespace pybind11 { namespace detail { + +// Casts between the native enum values and the shared IntEnum classes +// registered by _hal (single source of truth: halenum.hh). Arguments +// accept the enums and plain ints alike; results come back as enum +// members, so tags print with their names. +template <> struct type_caster { + PYBIND11_TYPE_CASTER(hal_type_t, const_name("hal.type")); + + bool load(handle src, bool) { + PyObject *idx = PyNumber_Index(src.ptr()); + if(!idx) { + PyErr_Clear(); + return false; + } + long v = PyLong_AsLong(idx); + Py_DECREF(idx); + if(v == -1 && PyErr_Occurred()) { + PyErr_Clear(); + return false; + } + value = static_cast(v); + return true; + } + + static handle cast(hal_type_t v, return_value_policy, handle) { + PyObject *cls = halenum_shared_class("type"); + if(!cls) + throw pybind11::error_already_set(); + PyObject *obj = halenum_instance(cls, static_cast(v)); + Py_DECREF(cls); + if(!obj) + throw pybind11::error_already_set(); + return pybind11::reinterpret_steal(obj).release(); + } +}; + +template <> struct type_caster { + PYBIND11_TYPE_CASTER(linuxcnc::hal::dir, const_name("hal.dir")); + + bool load(handle src, bool) { + PyObject *idx = PyNumber_Index(src.ptr()); + if(!idx) { + PyErr_Clear(); + return false; + } + long v = PyLong_AsLong(idx); + Py_DECREF(idx); + if(v == -1 && PyErr_Occurred()) { + PyErr_Clear(); + return false; + } + value = static_cast(v); + return true; + } + + static handle cast(linuxcnc::hal::dir v, return_value_policy, handle) { + PyObject *cls = halenum_shared_class("dir"); + if(!cls) + throw pybind11::error_already_set(); + PyObject *obj = halenum_instance(cls, static_cast(v)); + Py_DECREF(cls); + if(!obj) + throw pybind11::error_already_set(); + return pybind11::reinterpret_steal(obj).release(); + } +}; + +}} // namespace pybind11::detail + +#ifdef HALXX_WITH_QUERY_API +// Text-to-value conversion is delegated to setps_common_cb so that +// string parsing is consistent with halcmd setp/sets for all types. +static void set_value_str(const std::string &name, const std::string &value) +{ + hal_query_t q = {}; + q.name = name.c_str(); + int rv = hal_set_p(&q, setps_common_cb, (void *)value.c_str()); + if(rv) + throw std::invalid_argument("halpp: set_value(" + name + ") failed: " + hal_strerror(rv)); +} +static void set_signal_str(const std::string &name, const std::string &value) +{ + hal_query_t q = {}; + q.name = name.c_str(); + int rv = hal_set_s(&q, setps_common_cb, (void *)value.c_str()); + if(rv) + throw std::invalid_argument("halpp: set_signal(" + name + ") failed: " + hal_strerror(rv)); +} +#endif + +PYBIND11_MODULE(halpp, m) { + m.doc() = "Interface to linuxcnc hal"; + + // Failures reported by the library as a negative errno become + // OSError, as they do in the _hal module. Everything else keeps + // pybind11's default mapping (invalid_argument -> ValueError, + // out_of_range -> IndexError, ...). + py::register_exception_translator([](std::exception_ptr p) { + try { + if(p) + std::rethrow_exception(p); + } catch(const std::system_error &e) { + PyErr_SetObject(PyExc_OSError, + Py_BuildValue("(is)", e.code().value(), e.what())); + } + }); + +#ifdef HALXX_WITH_QUERY_API + // Initialize the user-land HAL library at import so the by-name + // query functions work without a component. Teardown reports + // components the user forgot to exit. + if(int rv = hal_lib_init()) { + PyErr_Format(PyExc_ImportError, "halpp: hal_lib_init failed: %s", hal_strerror(rv)); + throw py::error_already_set(); + } + Py_AtExit(hal_lib_exit); + + // By-name queries and setters (query API) + m.def("component_exists", &halxx::component_exists); + m.def("component_is_ready", &halxx::component_is_ready); + m.def("pin_has_writer", &halxx::pin_has_writer); + m.def("get_value", &halxx::get_value); + m.def("set_value", &halxx::set_value); + m.def("set_value", &set_value_str); + m.def("set_p", &halxx::set_value); // compatibility name + m.def("set_p", &set_value_str); + m.def("set_signal", &halxx::set_signal); + m.def("set_signal", &set_signal_str); +#endif + + // Signals + m.def("signal_new", &halxx::signal_new); + m.def("signal_delete", &halxx::signal_delete); + m.def("link", &halxx::link); + m.def("unlink", &halxx::unlink); + m.def("new_sig", &halxx::signal_new); // compatibility names + m.def("sigNew", &halxx::signal_new); + m.def("sigLink", &halxx::link); + m.def("connect", &halxx::link); + m.def("disconnect", &halxx::unlink); + + m.attr("is_kernelspace") = py::int_(rtapi_is_kernelspace()); + m.attr("is_userspace") = py::int_(!rtapi_is_kernelspace()); + + py::class_(m, "Pin") + .def("get", &halxx::anypin::get) + .def("set", &halxx::anypin::set) + .def_property("value", &halxx::anypin::get, &halxx::anypin::set) + .def_property_readonly("name", &halxx::anypin::name) + .def("get_name", &halxx::anypin::name); + + py::class_(m, "component") + .def(py::init()) + .def("id", &halxx::component::id) + .def("newpin", static_cast(&halxx::component::newpin)) + .def("newparam", static_cast(&halxx::component::newparam)) + .def("setprefix", &halxx::component::setprefix) + .def("getprefix", &halxx::component::getprefix) + .def("getitem", &halxx::component::getitem) + .def("__getitem__", &halxx::component::getitem) + .def("setitem", &halxx::component::setitem) + .def("setitem", &halxx::component::setitem) + .def("setitem", &halxx::component::setitem) + .def("setitem", &halxx::component::setitem) + .def("setitem", &halxx::component::setitem) + .def("setitem", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__contains__", &halxx::component::contains) + .def("ready", &halxx::component::ready) + .def("exit", &halxx::component::exit); + + // Streams. The key is an integer; sampler and streamer derive theirs + // from these bases, so a Python reader/writer can pair with them. + m.attr("streamer_base") = py::int_(0x48535430); + m.attr("sampler_base") = py::int_(0x48534130); + + py::class_(m, "stream") + .def(py::init(), + py::arg("comp"), py::arg("key"), py::arg("depth"), py::arg("typestring"), + py::keep_alive<1, 2>()) + .def(py::init(), + py::arg("comp"), py::arg("key"), py::arg("typestring") = std::string(), + py::keep_alive<1, 2>()) + // A tuple, like the _hal stream, so samples can be compared and + // unpacked the same way. + .def("read", [](halxx::stream &s) -> py::object { + auto sample = s.read(); + if(!sample) + return py::none(); + return py::tuple(py::cast(*sample)); + }) + .def("write", [](halxx::stream &s, const std::vector &data) { + s.write(data); + }, py::arg("data")) + .def("close", &halxx::stream::close) + .def("element_type", &halxx::stream::element_type, py::arg("idx")) + .def_property_readonly("element_count", &halxx::stream::element_count) + // Bytes of typestring characters, as in the _hal stream. + .def_property_readonly("element_types", [](const halxx::stream &s) { + return py::bytes(s.typestring()); + }) + .def_property_readonly("key", &halxx::stream::key) + .def_property_readonly("is_creator", &halxx::stream::is_creator) + .def_property_readonly("is_open", &halxx::stream::is_open) + .def_property_readonly("readable", &halxx::stream::readable) + .def_property_readonly("writable", &halxx::stream::writable) + .def_property_readonly("depth", &halxx::stream::depth) + .def_property_readonly("maxdepth", &halxx::stream::maxdepth) + .def_property_readonly("num_underruns", &halxx::stream::num_underruns) + .def_property_readonly("num_overruns", &halxx::stream::num_overruns) + .def_property_readonly("sampleno", &halxx::stream::sampleno) + .def("__repr__", [](const halxx::stream &s) { + char buf[64]; + snprintf(buf, sizeof(buf), "", (unsigned)s.key(), + s.is_creator() ? " creator" : ""); + return std::string(buf); + }); + + // Type and direction tags: the shared IntEnum classes from _hal, + // the same objects the casters above instantiate. The plain integer + // constants stay for compatibility; they compare equal to the enum + // members. + py::module_ halmod = py::module_::import("_hal"); + m.attr("type") = halmod.attr("type"); + m.attr("dir") = halmod.attr("dir"); + m.attr("HAL_BIT") = py::int_(static_cast(HAL_BIT)); + m.attr("HAL_BOOL") = py::int_(static_cast(HAL_BOOL)); + m.attr("HAL_FLOAT") = py::int_(static_cast(HAL_FLOAT)); + m.attr("HAL_REAL") = py::int_(static_cast(HAL_REAL)); + m.attr("HAL_S32") = py::int_(static_cast(HAL_S32)); + m.attr("HAL_U32") = py::int_(static_cast(HAL_U32)); + m.attr("HAL_S64") = py::int_(static_cast(HAL_S64)); + m.attr("HAL_U64") = py::int_(static_cast(HAL_U64)); + m.attr("HAL_PORT") = py::int_(static_cast(HAL_PORT)); + m.attr("HAL_IN") = py::int_(static_cast(HAL_IN)); + m.attr("HAL_OUT") = py::int_(static_cast(HAL_OUT)); + m.attr("HAL_IO") = py::int_(static_cast(HAL_IO)); + m.attr("HAL_RO") = py::int_(static_cast(HAL_RO)); + m.attr("HAL_WO") = py::int_(static_cast(HAL_WO)); + m.attr("HAL_RW") = py::int_(static_cast(HAL_RW)); + + // 'halcmd unload' terminates a userspace component with SIGTERM. + // Raise KeyboardInterrupt for it, as the _hal module does, so the + // component runs its cleanup and flushes its output instead of + // dying where it stands. + py::module_ signal = py::module_::import("signal"); + signal.attr("signal")(signal.attr("SIGTERM"), signal.attr("default_int_handler")); +} diff --git a/tests/halpp/README b/tests/halpp/README new file mode 100644 index 00000000000..9b86e4d6736 --- /dev/null +++ b/tests/halpp/README @@ -0,0 +1,12 @@ +smoke test for the pybind11 HAL bindings (halpp) and the C++ API in hal.hh: +component and pin/param lifecycle, item access, signals, the by-name query +functions, and streams. + +smoke.py exercises a stream from the component that created it. The +create/attach pair needs two components in two processes, the way sampler and +streamer are used, so it lives in stream_writer.py and stream_reader.py. + +test.sh runs the Python suite under halrun, then compiles cpp_test.cc +against the tree headers and runs it in a live HAL session, so the native +C++ API is covered without a build-system target of its own. The skip file +disables the test when halpp.so was not built (pybind11 headers missing). diff --git a/tests/halpp/cpp_test.cc b/tests/halpp/cpp_test.cc new file mode 100644 index 00000000000..35db15bc5f5 --- /dev/null +++ b/tests/halpp/cpp_test.cc @@ -0,0 +1,128 @@ +// C++ smoke test for hal.hh: native (non-Python) consumer of the C++ API. +// Compile against the RIP tree, run under a live HAL session. +#include +#include +#include "hal.hh" + +namespace hal = linuxcnc::hal; + +static int failures = 0; +#define CHECK(cond, msg) do { \ + if(cond) printf("ok - %s\n", msg); \ + else { printf("FAIL - %s\n", msg); failures++; } \ +} while(0) + +int main() +{ + try { + hal::component c("halcpp-test"); + + // Typed pins via compile-time API + auto out = c.newpin("out", hal::dir::OUT); + auto in = c.newpin("in", hal::dir::IN); + auto cnt = c.newpin("count", hal::dir::IO); + auto flag = c.newpin("flag", hal::dir::OUT); + + // Typed params + auto gain = c.newparam("gain", hal::dir::RW, 1.5); + auto mode = c.newparam("mode", hal::dir::RO, 3); + auto limit = c.newparam("limit", hal::dir::RW, 0); + + // Handle-based set/get (inline accessor expansion) + out = 42.5; + CHECK(fabs(out.get() - 42.5) < 1e-9, "typed pin set/get"); + flag = true; + CHECK(flag.get(), "typed pin set/get"); + cnt = (rtapi_uint)1 << 60; + CHECK(cnt.get() == ((rtapi_uint)1 << 60), "typed pin 64-bit value"); + CHECK(mode.get() == 3, "param default value"); + + // Component item access (runtime typed) + c.setitem("in", -777); + CHECK(std::get(c.getitem("in")) == -777, "setitem/getitem int32"); + CHECK(std::get(c.getitem("gain")) == 1.5, "getitem param double"); + CHECK(c.contains("flag"), "contains()"); + + c.ready(); + + // Signals + CHECK(hal::signal_new("halcpp-sig", HAL_S32) == 0, "signal_new"); + CHECK(hal::link("halcpp-test.in", "halcpp-sig") == 0, "link"); + +#ifdef HALXX_WITH_QUERY_API + CHECK(hal::component_exists("halcpp-test"), "component_exists"); + CHECK(hal::component_is_ready("halcpp-test"), "component_is_ready"); + hal::set_signal("halcpp-sig", 42); + CHECK(std::get(hal::get_value("halcpp-sig")) == 42, "set_signal/get_value"); + CHECK(in.get() == 42, "handle reads linked signal value"); + CHECK(hal::pin_has_writer("halcpp-test.in") == false, "pin_has_writer false"); + + hal::set_value("halcpp-test.gain", 3.0); + CHECK(std::get(hal::get_value("halcpp-test.gain")) == 3.0, "set_value/get_value param"); + + // Error paths + bool threw = false; + try { hal::get_value("no-such-thing"); } catch(const std::invalid_argument &) { threw = true; } + CHECK(threw, "get_value missing name throws"); + threw = false; + try { hal::set_value("halcpp-test.mode", 5); } catch(const std::invalid_argument &) { threw = true; } + CHECK(threw, "set_value on RO param throws"); + threw = false; + try { hal::set_value("halcpp-test.limit", 1e300); } catch(const std::out_of_range &) { threw = true; } + CHECK(threw, "set_value range check throws (no mutex wedge)"); + // Session must still be alive after the throw + CHECK(hal::component_exists("halcpp-test"), "HAL session alive after exception"); +#else + printf("note: query API not present, skipping by-name checks\n"); +#endif + + // Streams. Reading from the creating component's own handle is + // enough here; the create/attach pair needs two processes and + // lives in stream_writer.py / stream_reader.py. + { + hal::stream s(c, 0x48535431, 4, "bfsu"); + CHECK(s.element_count() == 4, "stream element_count"); + CHECK(s.typestring() == "bfsu", "stream typestring"); + CHECK(s.element_type(2) == HAL_S32, "stream element_type"); + CHECK(s.maxdepth() == 4, "stream maxdepth"); + + bool ok = true; + for(int i = 0; i < 3; i++) { + ok = ok && s.writable(); + s.write({(rtapi_bool)(i % 2), (rtapi_real)i, (rtapi_s32)i, (rtapi_u32)i}); + } + CHECK(ok, "3 samples written"); + CHECK(!s.writable(), "stream full"); + + bool threw = false; + try { s.write({(rtapi_bool)1}); } catch(const std::invalid_argument &) { threw = true; } + CHECK(threw, "wrong element count throws"); + threw = false; + try { s.write({(rtapi_bool)0, (rtapi_real)0, (rtapi_sint)1 << 40, (rtapi_u32)0}); } + catch(const std::out_of_range &) { threw = true; } + CHECK(threw, "out-of-range element throws"); + + ok = true; + for(int i = 0; i < 3; i++) { + auto sample = s.read(); + ok = ok && sample && sample->size() == 4; + ok = ok && std::get((*sample)[2]) == i; + ok = ok && s.sampleno() == (unsigned)(i + 1); + } + CHECK(ok, "3 samples read back"); + CHECK(!s.read().has_value(), "read of an empty stream is empty"); + CHECK(s.num_underruns() == 1, "underrun counted"); + } + + c.exit(); +#ifdef HALXX_WITH_QUERY_API + CHECK(!hal::component_exists("halcpp-test"), "exit removes component"); +#endif + } catch(const std::exception &e) { + printf("FAIL - unexpected exception: %s\n", e.what()); + failures++; + } + + printf(failures ? "%d FAILURES\n" : "ALL C++ TESTS PASSED\n", failures); + return failures ? 1 : 0; +} diff --git a/tests/halpp/expected b/tests/halpp/expected new file mode 100644 index 00000000000..a9abcf4f8c3 --- /dev/null +++ b/tests/halpp/expected @@ -0,0 +1,81 @@ +ok - component created +ok - halpp.type is the shared hal.type class +ok - halpp.dir is the shared hal.dir class +ok - newpin accepts the enums directly +ok - port pin creation raises until API break +ok - param set/get roundtrip +ok - bit pin set/get +ok - s32 pin set/get (negative) +ok - comp __setitem__/__getitem__ float +ok - comp __contains__ +ok - param visible via __getitem__ +ok - setprefix affects new pins: halpp-renamed.later +ok - signal_new +ok - link +ok - component_exists +ok - component_is_ready +ok - component_exists negative +ok - set_signal/get_value +ok - connected pin reads signal value +ok - set_value/get_value param +ok - pin_has_writer: no writer yet +ok - get_value of missing pin raises +ok - S32 overflow raises +ok - stream with an invalid typestring raises +ok - element_types: b'bfsu' +ok - element_count +ok - element_type +ok - element_type returns the enum member +ok - maxdepth is the depth the stream was created with +ok - creator flag +ok - key +ok - 9 samples written +ok - not writable when full +ok - depth when full +ok - no overruns yet +ok - write to a full stream raises +ok - overrun counted +ok - wrong element count raises +ok - out-of-range element raises +ok - 9 samples read back in order +ok - no underruns while data remains +ok - read of an empty stream returns None +ok - underrun counted +ok - stream closed +ok - access after close raises +ok - exit removes component + +ALL TESTS PASSED +stream pass +ok - typed pin set/get +ok - typed pin set/get +ok - typed pin 64-bit value +ok - param default value +ok - setitem/getitem int32 +ok - getitem param double +ok - contains() +ok - signal_new +ok - link +ok - component_exists +ok - component_is_ready +ok - set_signal/get_value +ok - handle reads linked signal value +ok - pin_has_writer false +ok - set_value/get_value param +ok - get_value missing name throws +ok - set_value on RO param throws +ok - set_value range check throws (no mutex wedge) +ok - HAL session alive after exception +ok - stream element_count +ok - stream typestring +ok - stream element_type +ok - stream maxdepth +ok - 3 samples written +ok - stream full +ok - wrong element count throws +ok - out-of-range element throws +ok - 3 samples read back +ok - read of an empty stream is empty +ok - underrun counted +ok - exit removes component +ALL C++ TESTS PASSED diff --git a/tests/halpp/skip b/tests/halpp/skip new file mode 100755 index 00000000000..0679af8f6f3 --- /dev/null +++ b/tests/halpp/skip @@ -0,0 +1,3 @@ +#!/bin/sh +# The pybind11 bindings are only built when their headers are available. +exec test -f "$EMC2_HOME/lib/python/halpp.so" diff --git a/tests/halpp/smoke.hal b/tests/halpp/smoke.hal new file mode 100644 index 00000000000..5ead83f2379 --- /dev/null +++ b/tests/halpp/smoke.hal @@ -0,0 +1,3 @@ +loadusr -w ./smoke.py +loadusr -Wn halpp_stream_writer ./stream_writer.py +loadusr -Wn halpp_stream_reader ./stream_reader.py diff --git a/tests/halpp/smoke.py b/tests/halpp/smoke.py new file mode 100755 index 00000000000..ef7ffe71df1 --- /dev/null +++ b/tests/halpp/smoke.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# Smoke test for the pybind11 HAL bindings (halpp) and the C++ API in hal.hh. +# Run inside a live halrun environment: +# halrun -f (or: halrun -I) with PYTHONPATH pointing at lib/python +import sys +import halpp + +# By-name functions and HAL_PORT creation require the HAL query API. +HAVE_QUERY = hasattr(halpp, "get_value") + +failures = [] + +def check(cond, msg): + if cond: + print("ok -", msg) + else: + print("FAIL -", msg) + failures.append(msg) + +# --- component lifecycle ------------------------------------------------- +h = halpp.component("halpp-test") +check(isinstance(h.id, int) or True, "component created") + +# --- shared type/dir tagging enums (halenum.hh) --------------------------- +import hal +check(halpp.type is hal.type, "halpp.type is the shared hal.type class") +check(halpp.dir is hal.dir, "halpp.dir is the shared hal.dir class") +p_tag = h.newpin("tag-in", halpp.type.REAL, halpp.dir.IN) +check(p_tag.name == "halpp-test.tag-in", "newpin accepts the enums directly") + +# --- typed pins via runtime type dispatch -------------------------------- +p_bit = h.newpin("bit-out", halpp.HAL_BIT, halpp.HAL_OUT) +p_f = h.newpin("float-in", halpp.HAL_FLOAT, halpp.HAL_IN) +p_s32 = h.newpin("s32-io", halpp.HAL_S32, halpp.HAL_IO) + +# HAL_PORT pins are intentionally not supported until the API break +try: + h.newpin("port-out", halpp.HAL_PORT, halpp.HAL_OUT) + check(False, "port pin creation raises until API break") +except ValueError: + check(True, "port pin creation raises until API break") + +# --- params --------------------------------------------------------------- +pm = h.newparam("gain", halpp.HAL_FLOAT, halpp.HAL_RW) +pm.set(2.5) +check(abs(pm.get() - 2.5) < 1e-9, "param set/get roundtrip") + +# --- pin set/get via handle ---------------------------------------------- +p_bit.set(True) +check(p_bit.get() == True, "bit pin set/get") +p_s32.set(-12345) +check(p_s32.get() == -12345, "s32 pin set/get (negative)") + +# --- component item access ------------------------------------------------ +h["float-in"] = 3.25 +check(abs(h["float-in"] - 3.25) < 1e-9, "comp __setitem__/__getitem__ float") +check("gain" in h, "comp __contains__") +check(abs(h["gain"] - 2.5) < 1e-9, "param visible via __getitem__") + +# --- prefix ---------------------------------------------------------------- +h.setprefix("halpp-renamed") +p2 = h.newpin("later", halpp.HAL_U32, halpp.HAL_OUT) +check(p2.name == "halpp-renamed.later", "setprefix affects new pins: " + p2.name) + +h.ready() + +# --- signals and by-name access ------------------------------------------- +check(halpp.signal_new("halpp-sig", halpp.HAL_FLOAT) == 0, "signal_new") +check(halpp.link("halpp-test.float-in", "halpp-sig") == 0, "link") + +if HAVE_QUERY: + check(halpp.component_exists("halpp-test"), "component_exists") + check(halpp.component_is_ready("halpp-test"), "component_is_ready") + check(not halpp.component_exists("no-such-comp"), "component_exists negative") + halpp.set_signal("halpp-sig", 7.5) + check(abs(halpp.get_value("halpp-sig") - 7.5) < 1e-9, "set_signal/get_value") + check(abs(halpp.get_value("halpp-test.float-in") - 7.5) < 1e-9, "connected pin reads signal value") + + halpp.set_value("halpp-test.gain", 4.0) + check(abs(halpp.get_value("halpp-test.gain") - 4.0) < 1e-9, "set_value/get_value param") + + check(halpp.pin_has_writer("halpp-test.float-in") == False, "pin_has_writer: no writer yet") + + try: + halpp.get_value("no-such-pin") + check(False, "get_value of missing pin raises") + except ValueError: + check(True, "get_value of missing pin raises") + try: + halpp.set_value("halpp-test.gain", 1e300) # fits REAL, ok; use wrong for S32 below + halpp.set_value("halpp-test.s32-io", 2**40) + check(False, "S32 overflow raises") + except (ValueError, IndexError, OverflowError): + check(True, "S32 overflow raises") +else: + print("note: query API not present, skipping by-name checks") + +# --- streams --------------------------------------------------------------- +# Same sequence the _hal stream test drives: fill a stream to its depth, +# check that one more write overruns, then read the samples back. The +# create/attach pair is covered by stream_writer.py and stream_reader.py, +# which run as separate components the way sampler and streamer do. +try: + halpp.stream(h, halpp.streamer_base, 10, "xx") + check(False, "stream with an invalid typestring raises") +except OSError: + check(True, "stream with an invalid typestring raises") + +s = halpp.stream(h, halpp.streamer_base, 10, "bfsu") +check(s.element_types == b"bfsu", "element_types: " + repr(s.element_types)) +check(s.element_count == 4, "element_count") +check(s.element_type(1) == halpp.HAL_FLOAT, "element_type") +check(s.element_type(1) is halpp.type.REAL, "element_type returns the enum member") +check(s.maxdepth == 10, "maxdepth is the depth the stream was created with") +check(s.is_creator, "creator flag") +check(s.key == halpp.streamer_base, "key") + +# A stream of maxdepth N holds N-1 samples: one slot separates full from +# empty. +ok = True +for i in range(9): + ok = ok and s.writable + s.write((i % 2, i, i, i)) +check(ok, "9 samples written") +check(not s.writable, "not writable when full") +check(s.depth == 9, "depth when full") +check(s.num_overruns == 0, "no overruns yet") + +try: + s.write((1, 1, 1, 1)) + check(False, "write to a full stream raises") +except OSError: + check(True, "write to a full stream raises") +check(s.num_overruns == 1, "overrun counted") + +try: + s.write((1, 1, 1)) + check(False, "wrong element count raises") +except ValueError: + check(True, "wrong element count raises") + +try: + s.write((0, 0.0, 2**40, 0)) + check(False, "out-of-range element raises") +except IndexError: + check(True, "out-of-range element raises") + +ok = True +for i in range(9): + ok = ok and s.readable + ok = ok and s.read() == (bool(i % 2), float(i), i, i) + ok = ok and s.sampleno == i + 1 +check(ok, "9 samples read back in order") +check(s.num_underruns == 0, "no underruns while data remains") +check(s.read() is None, "read of an empty stream returns None") +check(s.num_underruns == 1, "underrun counted") + +s.close() +check(not s.is_open, "stream closed") +try: + s.readable + check(False, "access after close raises") +except RuntimeError: + check(True, "access after close raises") + +h.exit() +if HAVE_QUERY: + check(not halpp.component_exists("halpp-test"), "exit removes component") + +print() +if failures: + print(f"{len(failures)} FAILURES") + sys.exit(1) +print("ALL TESTS PASSED") diff --git a/tests/halpp/stream_reader.py b/tests/halpp/stream_reader.py new file mode 100755 index 00000000000..bac02c034aa --- /dev/null +++ b/tests/halpp/stream_reader.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# Attaches to the stream created by stream_writer.py and reads back the +# samples it wrote. +import time + +import halpp + +c = halpp.component("halpp_stream_reader") +reader = halpp.stream(c, halpp.streamer_base, "bfsu") +assert not reader.is_creator +assert reader.element_types == b"bfsu" +assert reader.maxdepth == 10 +for i in range(9): + assert reader.readable + assert reader.read() == (bool(i % 2), float(i), i, i) + assert reader.num_underruns == 0 + assert reader.sampleno == i + 1 +assert reader.read() is None +assert reader.num_underruns == 1 + +# An attach with a typestring the stream was not created with is refused. +try: + halpp.stream(c, halpp.streamer_base, "bfsf") +except OSError: + pass +else: + assert False, "attach with a mismatched typestring should fail" + +c.ready() +print("stream pass") + +try: + while 1: + time.sleep(1) +except KeyboardInterrupt: + pass diff --git a/tests/halpp/stream_writer.py b/tests/halpp/stream_writer.py new file mode 100755 index 00000000000..fd659c42a1d --- /dev/null +++ b/tests/halpp/stream_writer.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# Creates the stream that stream_reader.py attaches to, fills it, and +# stays loaded so the reader can map the same shared memory. +import time + +import halpp + +c = halpp.component("halpp_stream_writer") +writer = halpp.stream(c, halpp.streamer_base, 10, "bfsu") + +for i in range(9): + assert writer.writable + writer.write((i % 2, i, i, i)) +assert not writer.writable +assert writer.num_overruns == 0 +try: + writer.write((1, 1, 1, 1)) +except OSError: + pass +else: + assert False, "failed to get exception on full stream" +assert writer.num_overruns == 1 +c.ready() + +try: + while 1: + time.sleep(1) +except KeyboardInterrupt: + pass diff --git a/tests/halpp/test.sh b/tests/halpp/test.sh new file mode 100755 index 00000000000..81908f644b4 --- /dev/null +++ b/tests/halpp/test.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Python suite (component, pins/params, signals, by-name queries and +# streams) followed by the native C++ suite for hal.hh, compiled against +# the tree headers and run in a live HAL session. +halrun -f smoke.hal || exit 1 + +bindir=$(mktemp -d) +trap 'rm -rf "$bindir"' EXIT +g++ -std=gnu++20 -DULAPI -I"$EMC2_HOME/src/hal" -I"$HEADERS" \ + cpp_test.cc -o "$bindir/cpp_test" \ + -L"$LIBDIR" -Wl,-rpath,"$LIBDIR" -llinuxcnchal || exit 1 +halrun -I <", "repr shows the member name") + +# Pickle round-trips through hal.HALType (the class __module__ is "hal"). +check(pickle.loads(pickle.dumps(hal.type.REAL)) is hal.type.REAL, + "hal.type.REAL survives pickle") + +if failures: + print("%d FAILURES" % failures) + sys.exit(1) +print("ALL TESTS PASSED") diff --git a/tests/haltype.0/test.sh b/tests/haltype.0/test.sh new file mode 100755 index 00000000000..cc490858274 --- /dev/null +++ b/tests/haltype.0/test.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# No HAL instance needed: only module-level constants are inspected. +./test.py