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/src/emc/task/taskclass.cc b/src/emc/task/taskclass.cc index 9e03dcd6940..a100058f4e0 100644 --- a/src/emc/task/taskclass.cc +++ b/src/emc/task/taskclass.cc @@ -40,23 +40,26 @@ using namespace linuxcnc; ********************************************************************/ int Task::iocontrol_hal_init(void) { - iocontrol.add_pin("user-enable-out", hal_dir::OUT, iocontrol_data.user_enable_out); - iocontrol.add_pin("emc-enable-in", hal_dir::IN, iocontrol_data.emc_enable_in); - iocontrol.add_pin("user-request-enable", hal_dir::OUT, iocontrol_data.user_request_enable); - iocontrol.add_pin("coolant-mist", hal_dir::OUT, iocontrol_data.coolant_mist); - iocontrol.add_pin("coolant-flood", hal_dir::OUT, iocontrol_data.coolant_flood); - iocontrol.add_pin("tool-prep-pocket", hal_dir::OUT, iocontrol_data.tool_prep_pocket); - iocontrol.add_pin("tool-from-pocket", hal_dir::OUT, iocontrol_data.tool_from_pocket); - iocontrol.add_pin("tool-prep-index", hal_dir::OUT, iocontrol_data.tool_prep_index); - iocontrol.add_pin("tool-prep-number", hal_dir::OUT, iocontrol_data.tool_prep_number); - iocontrol.add_pin("tool-number", hal_dir::OUT, iocontrol_data.tool_number); - iocontrol.add_pin("tool-prepare", hal_dir::OUT, iocontrol_data.tool_prepare); - iocontrol.add_pin("tool-prepared", hal_dir::IN, iocontrol_data.tool_prepared); - iocontrol.add_pin("tool-change", hal_dir::OUT, iocontrol_data.tool_change); - iocontrol.add_pin("tool-changed", hal_dir::IN, iocontrol_data.tool_changed); - iocontrol.ready(); - if (iocontrol.error < 0) + try { + iocontrol.add_pin("user-enable-out", hal_dir::OUT, iocontrol_data.user_enable_out); + iocontrol.add_pin("emc-enable-in", hal_dir::IN, iocontrol_data.emc_enable_in); + iocontrol.add_pin("user-request-enable", hal_dir::OUT, iocontrol_data.user_request_enable); + iocontrol.add_pin("coolant-mist", hal_dir::OUT, iocontrol_data.coolant_mist); + iocontrol.add_pin("coolant-flood", hal_dir::OUT, iocontrol_data.coolant_flood); + iocontrol.add_pin("tool-prep-pocket", hal_dir::OUT, iocontrol_data.tool_prep_pocket); + iocontrol.add_pin("tool-from-pocket", hal_dir::OUT, iocontrol_data.tool_from_pocket); + iocontrol.add_pin("tool-prep-index", hal_dir::OUT, iocontrol_data.tool_prep_index); + iocontrol.add_pin("tool-prep-number", hal_dir::OUT, iocontrol_data.tool_prep_number); + iocontrol.add_pin("tool-number", hal_dir::OUT, iocontrol_data.tool_number); + iocontrol.add_pin("tool-prepare", hal_dir::OUT, iocontrol_data.tool_prepare); + iocontrol.add_pin("tool-prepared", hal_dir::IN, iocontrol_data.tool_prepared); + iocontrol.add_pin("tool-change", hal_dir::OUT, iocontrol_data.tool_change); + iocontrol.add_pin("tool-changed", hal_dir::IN, iocontrol_data.tool_changed); + iocontrol.ready(); + } catch(const std::exception &e) { + fprintf(stderr, "iocontrol_hal_init: %s\n", e.what()); return -1; + } return 0; } diff --git a/src/hal/Submakefile b/src/hal/Submakefile index 85b4c56f121..d672d7596bc 100644 --- a/src/hal/Submakefile +++ b/src/hal/Submakefile @@ -28,5 +28,19 @@ $(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 +HALPPSRCS := hal/halpybind.cc $(wildcard hal/utils/setps_util.c) +PYSRCS += $(HALPPSRCS) + +# pybind11 headers: python3-pybind11 (system include) or pip user site +$(call TOOBJS, $(HALPPSRCS)): EXTRAFLAGS += $(shell python3 -m pybind11 --includes 2>/dev/null) + +HALPP := ../lib/python/halpp.so +$(HALPP): $(call TOOBJS, $(HALPPSRCS)) $(HALLIB) + $(ECHO) Linking python module $(notdir $@) + $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ + TARGETS += $(HALLIB) ../lib/liblinuxcnchal.so.0 PYTARGETS += $(HALMODULE) +PYTARGETS += $(HALPP) diff --git a/src/hal/hal.hh b/src/hal/hal.hh index 3e4af13a4e6..c748a39b4ca 100644 --- a/src/hal/hal.hh +++ b/src/hal/hal.hh @@ -1,166 +1,605 @@ +/* + 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 + +// 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 -enum class hal_dir{ - IN = HAL_IN, +#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 + +namespace linuxcnc { +namespace hal { + +// 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 -class hal{ - public: - static bool component_exists(const std::string& name){ - return halpr_find_comp_by_name(name.c_str()) != NULL; +//---------------------------------------------------------------------- +// 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; + +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_; } - 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; + 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_; } + + 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); - } - 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); - } + 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_; +}; + +//---------------------------------------------------------------------- +// 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 { - ~hal_comp(){ - hal_exit(comp_id); +// 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; +} -#endif +} // 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/halpybind.cc b/src/hal/halpybind.cc new file mode 100644 index 00000000000..54ff57bddd9 --- /dev/null +++ b/src/hal/halpybind.cc @@ -0,0 +1,138 @@ +/* + 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 + module fns - by-name get/set (when the query API is available), + signals, component queries +*/ +#include +#include + +#include "hal.hh" + +#ifdef HALXX_WITH_QUERY_API +#include "utils/setps_util.h" +#endif + +namespace py = pybind11; +namespace halxx = linuxcnc::hal; + +#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"; + +#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); + + py::enum_(m, "hal_type_t") + .value("HAL_BIT", HAL_BIT) + .value("HAL_BOOL", HAL_BOOL) + .value("HAL_FLOAT", HAL_FLOAT) + .value("HAL_REAL", HAL_REAL) + .value("HAL_S32", HAL_S32) + .value("HAL_U32", HAL_U32) + .value("HAL_S64", HAL_S64) + .value("HAL_U64", HAL_U64) + .value("HAL_PORT", HAL_PORT) + .export_values(); + + py::enum_(m, "hal_dir") + .value("HAL_IN", halxx::dir::IN) + .value("HAL_OUT", halxx::dir::OUT) + .value("HAL_IO", halxx::dir::IO) + .value("HAL_RO", halxx::dir::RO) + .value("HAL_RW", halxx::dir::RW) + .export_values(); +} diff --git a/tests/halpp/cpp_test.cc b/tests/halpp/cpp_test.cc new file mode 100644 index 00000000000..ccdb8b796ce --- /dev/null +++ b/tests/halpp/cpp_test.cc @@ -0,0 +1,90 @@ +// 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 + + 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/smoke.py b/tests/halpp/smoke.py new file mode 100644 index 00000000000..87330b556cb --- /dev/null +++ b/tests/halpp/smoke.py @@ -0,0 +1,99 @@ +#!/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") + +# --- 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") + +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")