From 5f28bf9a865c7a65c330c38d1f5e05f05f959c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Buleandr=C4=83?= Date: Wed, 30 Sep 2026 08:34:19 +0000 Subject: [PATCH] feat(nsh via dronecan): Implement nsh via dronecan (#28403) Queue unwritten stdin instead of dropping it when a request is larger than the pipe can take at once, and drain the queue without blocking the CAN work queue. Only stdin needs to stay nonblocking (it's written from that work queue); stdout reads stay blocking, gated on FIONREAD, like the mavlink shell already does. Report a shell session that has exited as an explicit error instead of silently swallowing EPIPE and returning empty output. Surface that error in the Python client as well. Flush the queued stdin on idle polls too, not just when new input arrives, since an idle poll is all the client sends while the user isn't typing. Only redirect fd 0/1 for the child once both stdio backups succeeded, so a dup() failure can't leave the node's own stdin/stdout pointing into the shell pipes. Distinguish EAGAIN (retry later) from EPIPE (never will succeed) when flushing queued stdin, so a dead shell doesn't hold the queue forever refusing new input. Only compile the shell sources when CONFIG_UAVCANNODE_COMMAND_SHELL is enabled, matching how every other optional uavcannode feature is gated, instead of pulling nshlib into every board's image. Detect a dead shell task with nxsched_get_tcb(), the same lookup top/cpuload.cpp use, instead of a POSIX-only helper with no NuttX implementation. Fix GetNodeInfo name decoding in the client to tolerate non-UTF-8 bytes instead of crashing the scan. Signed-off-by: danielbuleandra --- Tools/dronecan_shell.py | 201 ++++++++++++++++++ src/drivers/uavcannode/CMakeLists.txt | 9 + src/drivers/uavcannode/Kconfig | 4 + src/drivers/uavcannode/UavcanNode.cpp | 97 +++++++++ src/drivers/uavcannode/UavcanNode.hpp | 28 +++ src/drivers/uavcannode/UavcanNodeShell.cpp | 234 +++++++++++++++++++++ src/drivers/uavcannode/UavcanNodeShell.hpp | 102 +++++++++ 7 files changed, 675 insertions(+) create mode 100755 Tools/dronecan_shell.py create mode 100644 src/drivers/uavcannode/UavcanNodeShell.cpp create mode 100644 src/drivers/uavcannode/UavcanNodeShell.hpp 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