mirror of
https://github.com/PX4/PX4-Autopilot.git
synced 2026-10-06 09:02:52 +08:00
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:
Executable
+201
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user