diff --git a/Tools/dronecan_shell.py b/Tools/dronecan_shell.py new file mode 100755 index 00000000000..0d3814e3b60 --- /dev/null +++ b/Tools/dronecan_shell.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +Open an NSH shell on a DroneCAN node over uavcan.protocol.AccessCommandShell. +""" + +import argparse +import logging +import os +import select +import sys +import termios +import time +import dronecan + +REQ_RESET_SHELL = 1 +REQ_READ_OUTPUT = 64 | 128 +RSP_SHELL_ERROR = 2 +RSP_PENDING_OUTPUT = 64 | 128 + +MAX_INPUT = 128 +TIMEOUT = 1.0 +POLL_INTERVAL = 0.2 +SCAN_TIME = 5.0 + + +def spin(node, timeout=0.02): + """Wrapper around node.spin() that ignores TransferError exceptions""" + try: + node.spin(timeout=timeout) + except dronecan.transport.TransferError: + pass + + +def scan_nodes(node): + """Returns {node_id: name} for the nodes heard from during the scan.""" + print(f"Scanning the bus ({SCAN_TIME:.0f} s)...") + nodes = {} + handler = node.add_handler(dronecan.uavcan.protocol.NodeStatus, + lambda e: nodes.setdefault(e.transfer.source_node_id, '?')) + deadline = time.monotonic() + SCAN_TIME + + while time.monotonic() < deadline: + spin(node, 0.1) + + handler.remove() + nodes.pop(node.node_id, None) + + # ask everyone for its name at once, then wait out a single timeout + for node_id in nodes: + node.request(dronecan.uavcan.protocol.GetNodeInfo.Request(), node_id, + lambda e, nid=node_id: e and nodes.__setitem__(nid, bytes(e.response.name).decode('utf-8', errors='replace')), + timeout=TIMEOUT) + + deadline = time.monotonic() + TIMEOUT + 0.5 + + while time.monotonic() < deadline: + spin(node, 0.05) + + return nodes + + +def choose_node(nodes): + for node_id, name in sorted(nodes.items()): + print(f" {node_id}: {name}") + + while True: + choice = input("node id: ").strip() + + if choice.isdigit() and int(choice) in nodes: + return int(choice) + + print("pick one from the list") + + +def terminal(node, target_node_id): + pending = [(b'\n', 0)] # a newline first, so NSH prints a prompt + in_flight = False + next_poll = 0.0 + + def send(data, flags=0): + nonlocal in_flight + in_flight = True + + def on_response(event): + nonlocal in_flight, next_poll + in_flight = False + next_poll = time.monotonic() + POLL_INTERVAL + + if event is None: + return + + if event.response.output: + sys.stdout.write(bytes(event.response.output).decode('utf-8', errors='replace')) + sys.stdout.flush() + + if event.response.flags & RSP_SHELL_ERROR: + sys.stdout.write("\r\n[shell error, Ctrl-C starts a new one]\r\n") + sys.stdout.flush() + + if event.response.flags & RSP_PENDING_OUTPUT: + next_poll = time.monotonic() # more waiting, ask again now + + node.request(dronecan.uavcan.protocol.AccessCommandShell.Request( + flags=REQ_READ_OUTPUT | flags, input=bytearray(data)), + target_node_id, on_response, timeout=TIMEOUT) + + fd = sys.stdin.fileno() + old_attr = termios.tcgetattr(fd) + attr = termios.tcgetattr(fd) + # ISIG off so Ctrl-C reaches us as a byte instead of a local signal + attr[3] &= ~termios.ECHO & ~termios.ICANON & ~termios.ISIG + termios.tcsetattr(fd, termios.TCSANOW, attr) + line = b'' + + sys.stdout.write(f"node {target_node_id}: Ctrl-C interrupts, Ctrl-D goes back\r\n") + sys.stdout.flush() + + try: + while True: + if select.select([fd], [], [], 0)[0]: + for b in os.read(fd, 4096): + if b == 0x04: # Ctrl-D + return + + if b == 0x03: # Ctrl-C + # a stdin 0x03 raises no signal on the far end, so reset the shell + sys.stdout.write('^C\r\n') + line = b'' + pending.append((b'\n', REQ_RESET_SHELL)) + elif b in (0x0d, 0x0a): # Enter + sys.stdout.write('\r\n') + line += b'\n' + pending.extend((line[i:i + MAX_INPUT], 0) for i in range(0, len(line), MAX_INPUT)) + line = b'' + elif b in (0x7f, 0x08) and line: # backspace + line = line[:-1] + sys.stdout.write('\b \b') + elif 0x20 <= b < 0x7f: + line += bytes([b]) + sys.stdout.write(chr(b)) + + sys.stdout.flush() + + if not in_flight: + if pending: + send(*pending.pop(0)) + elif time.monotonic() >= next_poll: + send(b'') + + spin(node) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_attr) + sys.stdout.write('\r\n') + sys.stdout.flush() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("device", help="e.g /dev/ttyACM0") + parser.add_argument("--node-id", type=int, default=100, + help="node ID to use for this shell (default: 100)") + parser.add_argument("--allocator", action="store_true", + help="this node will allocate node IDs for other nodes") + parser.add_argument("--baudrate", type=int, default=115200, help="serial baudrate (default: 115200)") + parser.add_argument("--bitrate", type=int, default=1000000, help="CAN bitrate (default: 1000000)") + args = parser.parse_args() + + # Set the logging level to CRITICAL to suppress debug/info messages from the dronecan library + logging.getLogger('dronecan').setLevel(logging.CRITICAL) + node = dronecan.make_node(args.device, node_id=args.node_id, + bitrate=args.bitrate, baudrate=args.baudrate) + node.health = 0 # HEALTH_OK + node.mode = 0 # MODE_OPERATIONAL + + if args.allocator: + monitor = dronecan.app.node_monitor.NodeMonitor(node) + dronecan.app.dynamic_node_id.CentralizedServer(node, monitor) + + try: + while True: + nodes = scan_nodes(node) + + if not nodes: + print("no nodes on the bus" + ("" if args.allocator else "; try --allocator")) + return 1 + + terminal(node, choose_node(nodes)) + except (EOFError, KeyboardInterrupt): + print() + except dronecan.driver.DriverError as ex: + print(f"\nlost the adapter: {ex}") + return 1 + finally: + node.close() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/drivers/uavcannode/CMakeLists.txt b/src/drivers/uavcannode/CMakeLists.txt index d2e7edf463c..3da9e2a00ef 100644 --- a/src/drivers/uavcannode/CMakeLists.txt +++ b/src/drivers/uavcannode/CMakeLists.txt @@ -118,6 +118,14 @@ add_custom_command(OUTPUT px4_uavcan_dsdlc_run.stamp ) add_custom_target(px4_uavcan_dsdlc DEPENDS px4_uavcan_dsdlc_run.stamp) +set(UAVCANNODE_SHELL_SRCS) +if(CONFIG_UAVCANNODE_COMMAND_SHELL) + list(APPEND UAVCANNODE_SHELL_SRCS + UavcanNodeShell.hpp + UavcanNodeShell.cpp + ) +endif() + px4_add_module( MODULE drivers__uavcannode MAIN uavcannode @@ -137,6 +145,7 @@ px4_add_module( UavcanNode.hpp UavcanNodeParamManager.hpp UavcanNodeParamManager.cpp + ${UAVCANNODE_SHELL_SRCS} MODULE_CONFIG uavcannode_params.yaml DEPENDS diff --git a/src/drivers/uavcannode/Kconfig b/src/drivers/uavcannode/Kconfig index ad4ffa948c9..d6370931c29 100644 --- a/src/drivers/uavcannode/Kconfig +++ b/src/drivers/uavcannode/Kconfig @@ -18,6 +18,10 @@ if DRIVERS_UAVCANNODE bool "Include beep command" default n + config UAVCANNODE_COMMAND_SHELL + bool "Include NSH shell access over DroneCAN" + default n + config UAVCANNODE_ESC_RAW_COMMAND bool "Include ESC raw command" default n diff --git a/src/drivers/uavcannode/UavcanNode.cpp b/src/drivers/uavcannode/UavcanNode.cpp index 16344c86a27..db7f7b0d0d3 100644 --- a/src/drivers/uavcannode/UavcanNode.cpp +++ b/src/drivers/uavcannode/UavcanNode.cpp @@ -47,6 +47,10 @@ extern "C" __attribute__((weak)) const char *board_get_uavcan_hw_name(void) #include #include +#if defined(CONFIG_UAVCANNODE_COMMAND_SHELL) +#include +#endif // CONFIG_UAVCANNODE_COMMAND_SHELL + #if defined(CONFIG_UAVCANNODE_BATTERY_INFO) #include "Publishers/BatteryInfo.hpp" #endif // CONFIG_UAVCANNODE_BATTERY_INFO @@ -176,6 +180,9 @@ UavcanNode::UavcanNode(CanInitHelper *can_init, uint32_t bitrate, uavcan::ICanDr _node(can_driver, system_clock, _pool_allocator), _time_sync_slave(_node), _fw_update_listner(_node), +#if defined(CONFIG_UAVCANNODE_COMMAND_SHELL) + _command_shell_server(_node), +#endif // CONFIG_UAVCANNODE_COMMAND_SHELL _param_server(_node), _dyn_node_id_client(_node), _reset_timer(_node) @@ -214,6 +221,10 @@ UavcanNode::~UavcanNode() } while (_instance); } +#if defined(CONFIG_UAVCANNODE_COMMAND_SHELL) + close_shell(); +#endif // CONFIG_UAVCANNODE_COMMAND_SHELL + _publisher_list.clear(); _subscriber_list.clear(); @@ -356,6 +367,83 @@ void UavcanNode::cb_beginfirmware_update(const uavcan::ReceivedDataStructure &req) +{ + // no early return on an empty request: write() flushes whatever is still queued, + // and an idle poll is the only thing that arrives while the user is not typing + uint8_t input_buf[decltype(req.input)::MaxSize]; + + for (size_t i = 0; i < req.input.size(); ++i) { + input_buf[i] = req.input[i]; + } + + return _shell->write(input_buf, req.input.size()); +} + +void UavcanNode::shell_send(uavcan::ServiceResponseDataStructure &rsp) +{ + uint8_t output_buf[decltype(rsp.output)::MaxSize]; + const size_t available = _shell->available(); + // cap to the response's max payload size + const size_t to_read = math::min(available, sizeof(output_buf)); + const size_t n = to_read > 0 ? _shell->read(output_buf, to_read) : 0; + + for (size_t i = 0; i < n; ++i) { + rsp.output.push_back(output_buf[i]); + } +} + +void UavcanNode::cb_access_command_shell(const uavcan::ReceivedDataStructure + &req, uavcan::ServiceResponseDataStructure &rsp) +{ + const uavcan::NodeID source = req.getSrcNodeID(); + + // reset applies regardless of current owner; otherwise a single shared session + if ((req.flags & req.FLAG_RESET_SHELL) || (_shell != nullptr && _shell_owner != source)) { + close_shell(); + } + + if (_shell != nullptr && !_shell->is_running()) { + close_shell(); + } + + if (_shell == nullptr) { + _shell = new uavcannode::UavcanNodeShell(); + + if (_shell == nullptr || _shell->start() < 0) { + PX4_ERR("AccessCommandShell: failed to start shell"); + delete _shell; + _shell = nullptr; + rsp.flags = rsp.FLAG_SHELL_ERROR; + return; + } + + _shell_owner = source; + } + + if (!shell_recv(req)) { + PX4_ERR("AccessCommandShell: shell stdin backed up, input dropped"); + rsp.flags |= rsp.FLAG_SHELL_ERROR; + } + + shell_send(rsp); + + // shell_send() may not have drained everything that fit in one response + if (_shell->available() > 0) { + rsp.flags |= rsp.FLAG_HAS_PENDING_STDOUT; + } +} + +#endif // CONFIG_UAVCANNODE_COMMAND_SHELL + int UavcanNode::init(uavcan::NodeID node_id, UAVCAN_DRIVER::BusEvent &bus_events) { _node.setName(board_get_uavcan_hw_name()); @@ -374,6 +462,15 @@ int UavcanNode::init(uavcan::NodeID node_id, UAVCAN_DRIVER::BusEvent &bus_events return PX4_ERROR; } +#if defined(CONFIG_UAVCANNODE_COMMAND_SHELL) + + if (_command_shell_server.start(AccessCommandShellCallback(this, &UavcanNode::cb_access_command_shell)) < 0) { + PX4_ERR("command shell server start failed"); + return PX4_ERROR; + } + +#endif // CONFIG_UAVCANNODE_COMMAND_SHELL + #if defined(CONFIG_UAVCANNODE_BATTERY_INFO) _publisher_list.add(new BatteryInfo(this, _node)); #endif // CONFIG_UAVCANNODE_BATTERY_INFO diff --git a/src/drivers/uavcannode/UavcanNode.hpp b/src/drivers/uavcannode/UavcanNode.hpp index a800952eba9..f5fc46962f4 100644 --- a/src/drivers/uavcannode/UavcanNode.hpp +++ b/src/drivers/uavcannode/UavcanNode.hpp @@ -50,6 +50,11 @@ #include "allocator.hpp" #include "UavcanNodeParamManager.hpp" +#if defined(CONFIG_UAVCANNODE_COMMAND_SHELL) +#include "UavcanNodeShell.hpp" +#include +#endif // CONFIG_UAVCANNODE_COMMAND_SHELL + #include #include #include @@ -172,6 +177,29 @@ private: void cb_beginfirmware_update(const uavcan::ReceivedDataStructure &req, uavcan::ServiceResponseDataStructure &rsp); +#if defined(CONFIG_UAVCANNODE_COMMAND_SHELL) + typedef uavcan::protocol::AccessCommandShell AccessCommandShell; + + typedef uavcan::MethodBinder &, + uavcan::ServiceResponseDataStructure &)> + AccessCommandShellCallback; + + uavcan::ServiceServer _command_shell_server; + void cb_access_command_shell(const uavcan::ReceivedDataStructure &req, + uavcan::ServiceResponseDataStructure &rsp); + + bool shell_recv(const uavcan::ReceivedDataStructure &req); + void shell_send(uavcan::ServiceResponseDataStructure &rsp); + + uavcannode::UavcanNodeShell *_shell{nullptr}; + uavcan::NodeID _shell_owner; + + void close_shell(); + + +#endif // CONFIG_UAVCANNODE_COMMAND_SHELL + IntrusiveSortedList _publisher_list; IntrusiveSortedList _subscriber_list; diff --git a/src/drivers/uavcannode/UavcanNodeShell.cpp b/src/drivers/uavcannode/UavcanNodeShell.cpp new file mode 100644 index 00000000000..8484ffc9638 --- /dev/null +++ b/src/drivers/uavcannode/UavcanNodeShell.cpp @@ -0,0 +1,234 @@ +/**************************************************************************** + * + * Copyright (c) 2026 PX4 Development Team. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name PX4 nor the names of its contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + ****************************************************************************/ + +#include "UavcanNodeShell.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef __PX4_NUTTX +#include +#endif /* __PX4_NUTTX */ + +using namespace uavcannode; + +UavcanNodeShell::~UavcanNodeShell() +{ + // closing the pipes gives EOF, letting the shell task exit on its own + if (_to_shell_fd >= 0) { + close(_to_shell_fd); + } + + if (_from_shell_fd >= 0) { + close(_from_shell_fd); + } +} + +bool UavcanNodeShell::is_running() +{ + // true if task id still exists + return _task >= 0 && nxsched_get_tcb(_task) != nullptr; +} + +int UavcanNodeShell::start() +{ +#if !defined(__PX4_NUTTX) + return -1; +#else + int p1[2], p2[2]; + + if (pipe(p1) != 0) { + return -errno; + } + + if (pipe(p2) != 0) { + close(p1[0]); + close(p1[1]); + return -errno; + } + + int ret = 0; + + _from_shell_fd = p1[0]; + _to_shell_fd = p2[1]; + _shell_fds[0] = p2[0]; + _shell_fds[1] = p1[1]; + + // Only stdin is nonblocking: it is written from the CAN work queue, which must not + // sleep on a full pipe. Reads stay blocking like the mavlink shell, since shell_send() + // never asks for more than FIONREAD reported. + fcntl(_to_shell_fd, F_SETFL, fcntl(_to_shell_fd, F_GETFL, 0) | O_NONBLOCK); + + // lock while temporarily redirecting fd 0/1 for the child task +#ifdef __PX4_NUTTX + sched_lock(); +#endif /* __PX4_NUTTX */ + + int fd_backups[2]; + + for (int i = 0; i < 2; ++i) { + fd_backups[i] = dup(i); + + if (fd_backups[i] == -1) { + ret = -errno; + } + } + + if (ret == 0) { + // only redirect once both backups are safe, otherwise fd 0/1 could not be restored + dup2(_shell_fds[0], 0); + dup2(_shell_fds[1], 1); + + // the new task inherits fd 0/1 at spawn time, so the redirect only needs to hold until here + _task = px4_task_spawn_cmd("uavcan_shell", + SCHED_DEFAULT, + SCHED_PRIORITY_DEFAULT, + 2048, + &UavcanNodeShell::shell_start_thread, + nullptr); + + if (_task < 0) { + ret = -1; + } + } + + // restore fd's of the caller task + for (int i = 0; i < 2; ++i) { + if (dup2(fd_backups[i], i) == -1) { + ret = -errno; + } + + close(fd_backups[i]); + } + + // the task now owns these through its own fd 0/1 + close(_shell_fds[0]); + close(_shell_fds[1]); + +#ifdef __PX4_NUTTX + sched_unlock(); +#endif /* __PX4_NUTTX */ + + return ret; +#endif /* __PX4_NUTTX */ +} + +int UavcanNodeShell::shell_start_thread(int argc, char *argv[]) +{ +#ifdef __PX4_NUTTX + dup2(1, 2); // redirect stderr to stdout + + const int ret = nsh_consolemain(0, NULL); + + if (ret) { + PX4_ERR("uavcan shell failed: %d%s", ret, (ret == -ENOMEM) ? " (out of memory)" : ""); + return ret; + } + +#endif /* __PX4_NUTTX */ + + return 0; +} + +bool UavcanNodeShell::write(const uint8_t *buffer, size_t len) +{ + flush_input(); + + if (len > sizeof(_input) - _input_len) { + return false; + } + + memcpy(&_input[_input_len], buffer, len); + _input_len += len; + + flush_input(); + + return true; +} + +void UavcanNodeShell::flush_input() +{ + if (_input_len == 0) { + return; + } + + const int ret = ::write(_to_shell_fd, _input, _input_len); + + if (ret < 0) { + // EAGAIN only means the pipe is full right now, so the bytes stay queued for the + // next attempt. Any other error, EPIPE once nsh is gone, will never succeed, so + // the queue is dropped rather than retried forever. + if (errno != EAGAIN && errno != EINTR) { + _input_len = 0; + } + + return; + } + + _input_len -= ret; + + if (_input_len > 0) { + memmove(_input, &_input[ret], _input_len); + } +} + +size_t UavcanNodeShell::read(uint8_t *buffer, size_t len) +{ + const int ret = ::read(_from_shell_fd, buffer, len); + + // The fd is blocking and shell_send() never asks for more than FIONREAD reported, so + // this only fails if the read was interrupted. Zero means nsh closed its end. + if (ret < 0) { + return 0; + } + + return ret; +} + +size_t UavcanNodeShell::available() +{ + int ret = 0; + + if (ioctl(_from_shell_fd, FIONREAD, (unsigned long)&ret) == OK) { + return ret; + } + + return 0; +} diff --git a/src/drivers/uavcannode/UavcanNodeShell.hpp b/src/drivers/uavcannode/UavcanNodeShell.hpp new file mode 100644 index 00000000000..fde3e5d508b --- /dev/null +++ b/src/drivers/uavcannode/UavcanNodeShell.hpp @@ -0,0 +1,102 @@ +/**************************************************************************** + * + * Copyright (c) 2026 PX4 Development Team. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name PX4 nor the names of its contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + ****************************************************************************/ + +/** + * @file UavcanNodeShell.hpp + * An NSH shell instance driven through a pair of pipes, exposed remotely via the + * DroneCAN uavcan.protocol.AccessCommandShell service. + */ + +#pragma once + +#include +#include +#include + +namespace uavcannode +{ + +class UavcanNodeShell +{ +public: + UavcanNodeShell() = default; + ~UavcanNodeShell(); + + /** + * Start the shell task. Must only be called once per instance. + * @return 0 on success, <0 errno otherwise. + */ + int start(); + + /** Whether the shell task still exists + * @return true if the shell task exists. + */ + bool is_running(); + + /** + * Queue data for the shell's stdin. Taken whole or not at all, since half a command + * line would silently merge with the next one. + * @return true if accepted, false if dropped + */ + bool write(const uint8_t *buffer, size_t len); + + /** + * Read from the shell's stdout/stderr. + */ + size_t read(uint8_t *buffer, size_t len); + + /** + * Number of bytes available to read(). + */ + size_t available(); + + static constexpr size_t MaxInputSize = 128; + +private: + int _to_shell_fd = -1; ///< write end of the pipe feeding the shell's stdin + int _from_shell_fd = -1; ///< read end of the pipe draining the shell's stdout+stderr + int _shell_fds[2] = { -1, -1 }; ///< the shell task's own ends of the two pipes + px4_task_t _task = -1; + + uint8_t _input[MaxInputSize]; + size_t _input_len = 0; + + void flush_input(); + + static int shell_start_thread(int argc, char *argv[]); + + UavcanNodeShell(const UavcanNodeShell &) = delete; + UavcanNodeShell operator=(const UavcanNodeShell &) = delete; +}; + +} // namespace uavcannode