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 <daniel.buleandra@auterion.com>
This commit is contained in:
Daniel Buleandră
2026-09-30 10:34:19 +02:00
committed by GitHub
parent 7e602f4598
commit 5f28bf9a86
7 changed files with 675 additions and 0 deletions
+201
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -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
+4
View File
@@ -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
+97
View File
@@ -47,6 +47,10 @@ extern "C" __attribute__((weak)) const char *board_get_uavcan_hw_name(void)
#include <lib/geo/geo.h>
#include <lib/version/version.h>
#if defined(CONFIG_UAVCANNODE_COMMAND_SHELL)
#include <errno.h>
#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<Uav
}
}
#if defined(CONFIG_UAVCANNODE_COMMAND_SHELL)
void UavcanNode::close_shell()
{
delete _shell;
_shell = nullptr;
}
bool UavcanNode::shell_recv(const uavcan::ReceivedDataStructure<UavcanNode::AccessCommandShell::Request> &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<UavcanNode::AccessCommandShell::Response> &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<UavcanNode::AccessCommandShell::Request>
&req, uavcan::ServiceResponseDataStructure<UavcanNode::AccessCommandShell::Response> &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
+28
View File
@@ -50,6 +50,11 @@
#include "allocator.hpp"
#include "UavcanNodeParamManager.hpp"
#if defined(CONFIG_UAVCANNODE_COMMAND_SHELL)
#include "UavcanNodeShell.hpp"
#include <uavcan/protocol/AccessCommandShell.hpp>
#endif // CONFIG_UAVCANNODE_COMMAND_SHELL
#include <uavcan/helpers/heap_based_pool_allocator.hpp>
#include <uavcan/protocol/global_time_sync_slave.hpp>
#include <uavcan/protocol/file/BeginFirmwareUpdate.hpp>
@@ -172,6 +177,29 @@ private:
void cb_beginfirmware_update(const uavcan::ReceivedDataStructure<UavcanNode::BeginFirmwareUpdate::Request> &req,
uavcan::ServiceResponseDataStructure<UavcanNode::BeginFirmwareUpdate::Response> &rsp);
#if defined(CONFIG_UAVCANNODE_COMMAND_SHELL)
typedef uavcan::protocol::AccessCommandShell AccessCommandShell;
typedef uavcan::MethodBinder<UavcanNode *,
void (UavcanNode::*)(const uavcan::ReceivedDataStructure<UavcanNode::AccessCommandShell::Request> &,
uavcan::ServiceResponseDataStructure<UavcanNode::AccessCommandShell::Response> &)>
AccessCommandShellCallback;
uavcan::ServiceServer<AccessCommandShell, AccessCommandShellCallback> _command_shell_server;
void cb_access_command_shell(const uavcan::ReceivedDataStructure<UavcanNode::AccessCommandShell::Request> &req,
uavcan::ServiceResponseDataStructure<UavcanNode::AccessCommandShell::Response> &rsp);
bool shell_recv(const uavcan::ReceivedDataStructure<UavcanNode::AccessCommandShell::Request> &req);
void shell_send(uavcan::ServiceResponseDataStructure<UavcanNode::AccessCommandShell::Response> &rsp);
uavcannode::UavcanNodeShell *_shell{nullptr};
uavcan::NodeID _shell_owner;
void close_shell();
#endif // CONFIG_UAVCANNODE_COMMAND_SHELL
IntrusiveSortedList<UavcanPublisherBase *> _publisher_list;
IntrusiveSortedList<UavcanSubscriberBase *> _subscriber_list;
File diff suppressed because it is too large Load Diff
+102
View File
@@ -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 <stddef.h>
#include <stdint.h>
#include <px4_platform_common/tasks.h>
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