Additional FTP command support plus unit test

- Restructured to a flatter class implementation
- Added support for create/remove directory, remove file
- Refactored error codes and command opcodes
- Added unit test support
- Fixed bugs found by unit test
This commit is contained in:
Don Gagne
2014-09-02 15:36:32 -07:00
parent 37ea85968d
commit 828fb5efd1
2 changed files with 423 additions and 307 deletions
+311 -134
View File
@@ -31,38 +31,39 @@
* *
****************************************************************************/ ****************************************************************************/
/// @file mavlink_ftp.cpp
/// @author px4dev, Don Gagne <don@thegagnes.com>
#include <crc32.h> #include <crc32.h>
#include <unistd.h> #include <unistd.h>
#include <stdio.h> #include <stdio.h>
#include <fcntl.h> #include <fcntl.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <errno.h>
#include "mavlink_ftp.h" #include "mavlink_ftp.h"
#include "mavlink_tests/mavlink_ftp_test.h"
// Uncomment the line below to get better debug output. Never commit with this left on. // Uncomment the line below to get better debug output. Never commit with this left on.
//#define MAVLINK_FTP_DEBUG //#define MAVLINK_FTP_DEBUG
MavlinkFTP *MavlinkFTP::_server;
MavlinkFTP * MavlinkFTP *
MavlinkFTP::getServer() MavlinkFTP::get_server(void)
{ {
// XXX this really cries out for some locking... static MavlinkFTP server;
if (_server == nullptr) { return &server;
_server = new MavlinkFTP;
}
return _server;
} }
MavlinkFTP::MavlinkFTP() : MavlinkFTP::MavlinkFTP() :
_session_fds{}, _request_bufs{},
_workBufs{}, _request_queue{},
_workFree{}, _request_queue_sem{},
_lock{} _utRcvMsgFunc{},
_ftp_test{}
{ {
// initialise the request freelist // initialise the request freelist
dq_init(&_workFree); dq_init(&_request_queue);
sem_init(&_lock, 0, 1); sem_init(&_request_queue_sem, 0, 1);
// initialize session list // initialize session list
for (size_t i=0; i<kMaxSession; i++) { for (size_t i=0; i<kMaxSession; i++) {
@@ -71,167 +72,247 @@ MavlinkFTP::MavlinkFTP() :
// drop work entries onto the free list // drop work entries onto the free list
for (unsigned i = 0; i < kRequestQueueSize; i++) { for (unsigned i = 0; i < kRequestQueueSize; i++) {
_qFree(&_workBufs[i]); _return_request(&_request_bufs[i]);
} }
} }
#ifdef MAVLINK_FTP_UNIT_TEST
void
MavlinkFTP::set_unittest_worker(ReceiveMessageFunc_t rcvMsgFunc, MavlinkFtpTest *ftp_test)
{
_utRcvMsgFunc = rcvMsgFunc;
_ftp_test = ftp_test;
}
#endif
void void
MavlinkFTP::handle_message(Mavlink* mavlink, mavlink_message_t *msg) MavlinkFTP::handle_message(Mavlink* mavlink, mavlink_message_t *msg)
{ {
// get a free request // get a free request
auto req = _dqFree(); struct Request* req = _get_request();
// if we couldn't get a request slot, just drop it // if we couldn't get a request slot, just drop it
if (req != nullptr) { if (req == nullptr) {
warnx("Dropping FTP request: queue full\n");
return;
}
// decode the request if (msg->msgid == MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL) {
if (req->decode(mavlink, msg)) { mavlink_msg_file_transfer_protocol_decode(msg, &req->message);
// and queue it for the worker #ifdef MAVLINK_FTP_UNIT_TEST
work_queue(LPWORK, &req->work, &MavlinkFTP::_workerTrampoline, req, 0); if (!_utRcvMsgFunc) {
} else { warnx("Incorrectly written unit test\n");
_qFree(req); return;
}
// We use fake ids when unit testing
req->serverSystemId = MavlinkFtpTest::serverSystemId;
req->serverComponentId = MavlinkFtpTest::serverComponentId;
req->serverChannel = MavlinkFtpTest::serverChannel;
#else
// Not unit testing, use the real thing
req->serverSystemId = mavlink->get_system_id();
req->serverComponentId = mavlink->get_component_id();
req->serverChannel = mavlink->get_channel();
#endif
// This is the system id we want to target when sending
req->targetSystemId = msg->sysid;
if (req->message.target_system == req->serverSystemId) {
req->mavlink = mavlink;
#ifdef MAVLINK_FTP_UNIT_TEST
// We are running in Unit Test mode. Don't queue, just call _worket directly.
_process_request(req);
#else
// We are running in normal mode. Queue the request to the worker
work_queue(LPWORK, &req->work, &MavlinkFTP::_worker_trampoline, req, 0);
#endif
return;
} }
} }
_return_request(req);
} }
/// @brief Queued static work queue routine to handle mavlink messages
void void
MavlinkFTP::_workerTrampoline(void *arg) MavlinkFTP::_worker_trampoline(void *arg)
{ {
auto req = reinterpret_cast<Request *>(arg); Request* req = reinterpret_cast<Request *>(arg);
auto server = MavlinkFTP::getServer(); MavlinkFTP* server = MavlinkFTP::get_server();
// call the server worker with the work item // call the server worker with the work item
server->_worker(req); server->_process_request(req);
} }
/// @brief Processes an FTP message
void void
MavlinkFTP::_worker(Request *req) MavlinkFTP::_process_request(Request *req)
{ {
auto hdr = req->header(); PayloadHeader *payload = reinterpret_cast<PayloadHeader *>(&req->message.payload[0]);
ErrorCode errorCode = kErrNone; ErrorCode errorCode = kErrNone;
uint32_t messageCRC;
// basic sanity checks; must validate length before use // basic sanity checks; must validate length before use
if (hdr->size > kMaxDataLength) { if (payload->size > kMaxDataLength) {
errorCode = kErrNoRequest; errorCode = kErrInvalidDataSize;
goto out; goto out;
} }
// check request CRC to make sure this is one of ours // check request CRC to make sure this is one of ours
messageCRC = hdr->crc32; if (_payload_crc32(payload) != payload->crc32) {
hdr->crc32 = 0; errorCode = kErrCrc;
hdr->padding[0] = 0;
hdr->padding[1] = 0;
hdr->padding[2] = 0;
if (crc32(req->rawData(), req->dataSize()) != messageCRC) {
errorCode = kErrNoRequest;
goto out; goto out;
warnx("ftp: bad crc"); warnx("ftp: bad crc");
} }
#ifdef MAVLINK_FTP_DEBUG #ifdef MAVLINK_FTP_DEBUG
printf("ftp: channel %u opc %u size %u offset %u\n", req->channel(), hdr->opcode, hdr->size, hdr->offset); printf("ftp: channel %u opc %u size %u offset %u\n", req->serverChannel, payload->opcode, payload->size, payload->offset);
#endif #endif
switch (hdr->opcode) { switch (payload->opcode) {
case kCmdNone: case kCmdNone:
break; break;
case kCmdTerminate: case kCmdTerminateSession:
errorCode = _workTerminate(req); errorCode = _workTerminate(payload);
break; break;
case kCmdReset: case kCmdResetSessions:
errorCode = _workReset(); errorCode = _workReset(payload);
break; break;
case kCmdList: case kCmdListDirectory:
errorCode = _workList(req); errorCode = _workList(payload);
break; break;
case kCmdOpen: case kCmdOpenFile:
errorCode = _workOpen(req, false); errorCode = _workOpen(payload, false);
break; break;
case kCmdCreate: case kCmdCreateFile:
errorCode = _workOpen(req, true); errorCode = _workOpen(payload, true);
break; break;
case kCmdRead: case kCmdReadFile:
errorCode = _workRead(req); errorCode = _workRead(payload);
break; break;
case kCmdWrite: case kCmdWriteFile:
errorCode = _workWrite(req); errorCode = _workWrite(payload);
break; break;
case kCmdRemove: case kCmdRemoveFile:
errorCode = _workRemove(req); errorCode = _workRemoveFile(payload);
break; break;
case kCmdCreateDirectory:
errorCode = _workCreateDirectory(payload);
break;
case kCmdRemoveDirectory:
errorCode = _workRemoveDirectory(payload);
break;
default: default:
errorCode = kErrNoRequest; errorCode = kErrUnknownCommand;
break; break;
} }
out: out:
// handle success vs. error // handle success vs. error
if (errorCode == kErrNone) { if (errorCode == kErrNone) {
hdr->opcode = kRspAck; payload->opcode = kRspAck;
#ifdef MAVLINK_FTP_DEBUG #ifdef MAVLINK_FTP_DEBUG
warnx("FTP: ack\n"); warnx("FTP: ack\n");
#endif #endif
} else { } else {
warnx("FTP: nak %u", errorCode); warnx("FTP: nak %u", errorCode);
hdr->opcode = kRspNak; payload->opcode = kRspNak;
hdr->size = 1; payload->size = 1;
hdr->data[0] = errorCode; payload->data[0] = errorCode;
if (errorCode == kErrFailErrno) {
payload->size = 2;
payload->data[1] = errno;
}
} }
// respond to the request // respond to the request
_reply(req); _reply(req);
// free the request buffer back to the freelist _return_request(req);
_qFree(req);
} }
/// @brief Sends the specified FTP reponse message out through mavlink
void void
MavlinkFTP::_reply(Request *req) MavlinkFTP::_reply(Request *req)
{ {
auto hdr = req->header(); PayloadHeader *payload = reinterpret_cast<PayloadHeader *>(&req->message.payload[0]);
hdr->seqNumber = req->header()->seqNumber + 1; payload->seqNumber = payload->seqNumber + 1;
// message is assumed to be already constructed in the request buffer, so generate the CRC payload->crc32 = _payload_crc32(payload);
hdr->crc32 = 0;
hdr->padding[0] = 0;
hdr->padding[1] = 0;
hdr->padding[2] = 0;
hdr->crc32 = crc32(req->rawData(), req->dataSize());
// then pack and send the reply back to the request source mavlink_message_t msg;
req->reply(); msg.checksum = 0;
#ifndef MAVLINK_FTP_UNIT_TEST
uint16_t len =
#endif
mavlink_msg_file_transfer_protocol_pack_chan(req->serverSystemId, // Sender system id
req->serverComponentId, // Sender component id
req->serverChannel, // Channel to send on
&msg, // Message to pack payload into
0, // Target network
req->targetSystemId, // Target system id
0, // Target component id
(const uint8_t*)payload); // Payload to pack into message
bool success = true;
#ifdef MAVLINK_FTP_UNIT_TEST
// Unit test hook is set, call that instead
_utRcvMsgFunc(&msg, _ftp_test);
#else
Mavlink *mavlink = req->mavlink;
mavlink->lockMessageBufferMutex();
success = mavlink->message_buffer_write(&msg, len);
mavlink->unlockMessageBufferMutex();
#endif
if (!success) {
warnx("FTP TX ERR");
}
#ifdef MAVLINK_FTP_DEBUG
else {
warnx("wrote: sys: %d, comp: %d, chan: %d, checksum: %d",
req->serverSystemId,
req->serverComponentId,
req->serverChannel,
msg.checksum);
}
#endif
} }
/// @brief Responds to a List command
MavlinkFTP::ErrorCode MavlinkFTP::ErrorCode
MavlinkFTP::_workList(Request *req) MavlinkFTP::_workList(PayloadHeader* payload)
{ {
auto hdr = req->header();
char dirPath[kMaxDataLength]; char dirPath[kMaxDataLength];
strncpy(dirPath, req->dataAsCString(), kMaxDataLength); strncpy(dirPath, _data_as_cstring(payload), kMaxDataLength);
DIR *dp = opendir(dirPath); DIR *dp = opendir(dirPath);
if (dp == nullptr) { if (dp == nullptr) {
warnx("FTP: can't open path '%s'", dirPath); warnx("FTP: can't open path '%s'", dirPath);
return kErrNotDir; return kErrFailErrno;
} }
#ifdef MAVLINK_FTP_DEBUG #ifdef MAVLINK_FTP_DEBUG
warnx("FTP: list %s offset %d", dirPath, hdr->offset); warnx("FTP: list %s offset %d", dirPath, payload->offset);
#endif #endif
ErrorCode errorCode = kErrNone; ErrorCode errorCode = kErrNone;
@@ -239,19 +320,19 @@ MavlinkFTP::_workList(Request *req)
unsigned offset = 0; unsigned offset = 0;
// move to the requested offset // move to the requested offset
seekdir(dp, hdr->offset); seekdir(dp, payload->offset);
for (;;) { for (;;) {
// read the directory entry // read the directory entry
if (readdir_r(dp, &entry, &result)) { if (readdir_r(dp, &entry, &result)) {
warnx("FTP: list %s readdir_r failure\n", dirPath); warnx("FTP: list %s readdir_r failure\n", dirPath);
errorCode = kErrIO; errorCode = kErrFailErrno;
break; break;
} }
// no more entries? // no more entries?
if (result == nullptr) { if (result == nullptr) {
if (hdr->offset != 0 && offset == 0) { if (payload->offset != 0 && offset == 0) {
// User is requesting subsequent dir entries but there were none. This means the user asked // User is requesting subsequent dir entries but there were none. This means the user asked
// to seek past EOF. // to seek past EOF.
errorCode = kErrEOF; errorCode = kErrEOF;
@@ -299,94 +380,95 @@ MavlinkFTP::_workList(Request *req)
} }
// Move the data into the buffer // Move the data into the buffer
hdr->data[offset++] = direntType; payload->data[offset++] = direntType;
strcpy((char *)&hdr->data[offset], buf); strcpy((char *)&payload->data[offset], buf);
#ifdef MAVLINK_FTP_DEBUG #ifdef MAVLINK_FTP_DEBUG
printf("FTP: list %s %s\n", dirPath, (char *)&hdr->data[offset-1]); printf("FTP: list %s %s\n", dirPath, (char *)&payload->data[offset-1]);
#endif #endif
offset += nameLen + 1; offset += nameLen + 1;
} }
closedir(dp); closedir(dp);
hdr->size = offset; payload->size = offset;
return errorCode; return errorCode;
} }
/// @brief Responds to an Open command
MavlinkFTP::ErrorCode MavlinkFTP::ErrorCode
MavlinkFTP::_workOpen(Request *req, bool create) MavlinkFTP::_workOpen(PayloadHeader* payload, bool create)
{ {
auto hdr = req->header(); int session_index = _find_unused_session();
int session_index = _findUnusedSession();
if (session_index < 0) { if (session_index < 0) {
return kErrNoSession; warnx("FTP: Open failed - out of sessions\n");
return kErrNoSessionsAvailable;
} }
char *filename = _data_as_cstring(payload);
uint32_t fileSize = 0; uint32_t fileSize = 0;
if (!create) { if (!create) {
struct stat st; struct stat st;
if (stat(req->dataAsCString(), &st) != 0) { if (stat(filename, &st) != 0) {
return kErrNotFile; return kErrFailErrno;
} }
fileSize = st.st_size; fileSize = st.st_size;
} }
int oflag = create ? (O_CREAT | O_EXCL | O_APPEND) : O_RDONLY; int oflag = create ? (O_CREAT | O_EXCL | O_APPEND) : O_RDONLY;
int fd = ::open(req->dataAsCString(), oflag); int fd = ::open(filename, oflag);
if (fd < 0) { if (fd < 0) {
return create ? kErrPerm : kErrNotFile; return kErrFailErrno;
} }
_session_fds[session_index] = fd; _session_fds[session_index] = fd;
hdr->session = session_index; payload->session = session_index;
if (create) { if (create) {
hdr->size = 0; payload->size = 0;
} else { } else {
hdr->size = sizeof(uint32_t); payload->size = sizeof(uint32_t);
*((uint32_t*)hdr->data) = fileSize; *((uint32_t*)payload->data) = fileSize;
} }
return kErrNone; return kErrNone;
} }
/// @brief Responds to a Read command
MavlinkFTP::ErrorCode MavlinkFTP::ErrorCode
MavlinkFTP::_workRead(Request *req) MavlinkFTP::_workRead(PayloadHeader* payload)
{ {
auto hdr = req->header(); int session_index = payload->session;
int session_index = hdr->session; if (!_valid_session(session_index)) {
return kErrInvalidSession;
if (!_validSession(session_index)) {
return kErrNoSession;
} }
// Seek to the specified position // Seek to the specified position
#ifdef MAVLINK_FTP_DEBUG #ifdef MAVLINK_FTP_DEBUG
warnx("seek %d", hdr->offset); warnx("seek %d", payload->offset);
#endif #endif
if (lseek(_session_fds[session_index], hdr->offset, SEEK_SET) < 0) { if (lseek(_session_fds[session_index], payload->offset, SEEK_SET) < 0) {
// Unable to see to the specified location // Unable to see to the specified location
warnx("seek fail"); warnx("seek fail");
return kErrEOF; return kErrEOF;
} }
int bytes_read = ::read(_session_fds[session_index], &hdr->data[0], kMaxDataLength); int bytes_read = ::read(_session_fds[session_index], &payload->data[0], kMaxDataLength);
if (bytes_read < 0) { if (bytes_read < 0) {
// Negative return indicates error other than eof // Negative return indicates error other than eof
warnx("read fail %d", bytes_read); warnx("read fail %d", bytes_read);
return kErrIO; return kErrFailErrno;
} }
hdr->size = bytes_read; payload->size = bytes_read;
return kErrNone; return kErrNone;
} }
/// @brief Responds to a Write command
MavlinkFTP::ErrorCode MavlinkFTP::ErrorCode
MavlinkFTP::_workWrite(Request *req) MavlinkFTP::_workWrite(PayloadHeader* payload)
{ {
#if 0 #if 0
// NYI: Coming soon // NYI: Coming soon
@@ -409,35 +491,44 @@ MavlinkFTP::_workWrite(Request *req)
hdr->size = result; hdr->size = result;
return kErrNone; return kErrNone;
#else #else
return kErrPerm; return kErrUnknownCommand;
#endif #endif
} }
/// @brief Responds to a RemoveFile command
MavlinkFTP::ErrorCode MavlinkFTP::ErrorCode
MavlinkFTP::_workRemove(Request *req) MavlinkFTP::_workRemoveFile(PayloadHeader* payload)
{ {
//auto hdr = req->header(); char file[kMaxDataLength];
strncpy(file, _data_as_cstring(payload), kMaxDataLength);
// for now, send error reply
return kErrPerm; if (unlink(file) == 0) {
payload->size = 0;
return kErrNone;
} else {
return kErrFailErrno;
}
} }
/// @brief Responds to a Terminate command
MavlinkFTP::ErrorCode MavlinkFTP::ErrorCode
MavlinkFTP::_workTerminate(Request *req) MavlinkFTP::_workTerminate(PayloadHeader* payload)
{ {
auto hdr = req->header(); if (!_valid_session(payload->session)) {
return kErrInvalidSession;
if (!_validSession(hdr->session)) {
return kErrNoSession;
} }
::close(_session_fds[hdr->session]); ::close(_session_fds[payload->session]);
_session_fds[payload->session] = -1;
payload->size = 0;
return kErrNone; return kErrNone;
} }
/// @brief Responds to a Reset command
MavlinkFTP::ErrorCode MavlinkFTP::ErrorCode
MavlinkFTP::_workReset(void) MavlinkFTP::_workReset(PayloadHeader* payload)
{ {
for (size_t i=0; i<kMaxSession; i++) { for (size_t i=0; i<kMaxSession; i++) {
if (_session_fds[i] != -1) { if (_session_fds[i] != -1) {
@@ -446,11 +537,44 @@ MavlinkFTP::_workReset(void)
} }
} }
payload->size = 0;
return kErrNone; return kErrNone;
} }
/// @brief Responds to a RemoveDirectory command
MavlinkFTP::ErrorCode
MavlinkFTP::_workRemoveDirectory(PayloadHeader* payload)
{
char dir[kMaxDataLength];
strncpy(dir, _data_as_cstring(payload), kMaxDataLength);
if (rmdir(dir) == 0) {
payload->size = 0;
return kErrNone;
} else {
return kErrFailErrno;
}
}
/// @brief Responds to a CreateDirectory command
MavlinkFTP::ErrorCode
MavlinkFTP::_workCreateDirectory(PayloadHeader* payload)
{
char dir[kMaxDataLength];
strncpy(dir, _data_as_cstring(payload), kMaxDataLength);
if (mkdir(dir, S_IRWXU | S_IRWXG | S_IRWXO) == 0) {
payload->size = 0;
return kErrNone;
} else {
return kErrFailErrno;
}
}
/// @brief Returns true if the specified session is a valid open session
bool bool
MavlinkFTP::_validSession(unsigned index) MavlinkFTP::_valid_session(unsigned index)
{ {
if ((index >= kMaxSession) || (_session_fds[index] < 0)) { if ((index >= kMaxSession) || (_session_fds[index] < 0)) {
return false; return false;
@@ -458,8 +582,9 @@ MavlinkFTP::_validSession(unsigned index)
return true; return true;
} }
/// @brief Returns an unused session index
int int
MavlinkFTP::_findUnusedSession(void) MavlinkFTP::_find_unused_session(void)
{ {
for (size_t i=0; i<kMaxSession; i++) { for (size_t i=0; i<kMaxSession; i++) {
if (_session_fds[i] == -1) { if (_session_fds[i] == -1) {
@@ -470,16 +595,68 @@ MavlinkFTP::_findUnusedSession(void)
return -1; return -1;
} }
/// @brief Guarantees that the payload data is null terminated.
/// @return Returns a pointer to the payload data as a char *
char * char *
MavlinkFTP::Request::dataAsCString() MavlinkFTP::_data_as_cstring(PayloadHeader* payload)
{ {
// guarantee nul termination // guarantee nul termination
if (header()->size < kMaxDataLength) { if (payload->size < kMaxDataLength) {
requestData()[header()->size] = '\0'; payload->data[payload->size] = '\0';
} else { } else {
requestData()[kMaxDataLength - 1] = '\0'; payload->data[kMaxDataLength - 1] = '\0';
} }
// and return data // and return data
return (char *)&(header()->data[0]); return (char *)&(payload->data[0]);
}
/// @brief Returns a unused Request entry. NULL if none available.
MavlinkFTP::Request *
MavlinkFTP::_get_request(void)
{
_lock_request_queue();
Request* req = reinterpret_cast<Request *>(dq_remfirst(&_request_queue));
_unlock_request_queue();
return req;
}
/// @brief Locks a semaphore to provide exclusive access to the request queue
void
MavlinkFTP::_lock_request_queue(void)
{
do {}
while (sem_wait(&_request_queue_sem) != 0);
}
/// @brief Unlocks the semaphore providing exclusive access to the request queue
void
MavlinkFTP::_unlock_request_queue(void)
{
sem_post(&_request_queue_sem);
}
/// @brief Returns a no longer needed request to the queue
void
MavlinkFTP::_return_request(Request *req)
{
_lock_request_queue();
dq_addlast(&req->work.dq, &_request_queue);
_unlock_request_queue();
}
/// @brief Returns the 32 bit CRC for the payload, crc32 and padding members are set to 0 for calculation.
uint32_t
MavlinkFTP::_payload_crc32(PayloadHeader *payload)
{
// We calculate CRC with crc and padding set to 0.
uint32_t saveCRC = payload->crc32;
payload->crc32 = 0;
payload->padding[0] = 0;
payload->padding[1] = 0;
payload->padding[2] = 0;
uint32_t retCRC = crc32((const uint8_t*)payload, payload->size + sizeof(PayloadHeader));
payload->crc32 = saveCRC;
return retCRC;
} }
+112 -173
View File
@@ -33,17 +33,8 @@
#pragma once #pragma once
/** /// @file mavlink_ftp.h
* @file mavlink_ftp.h /// @author px4dev, Don Gagne <don@thegagnes.com>
*
* MAVLink remote file server.
*
* A limited number of requests (currently 2) may be outstanding at a time.
* Additional messages will be discarded.
*
* Messages consist of a fixed header, followed by a data area.
*
*/
#include <dirent.h> #include <dirent.h>
#include <queue.h> #include <queue.h>
@@ -54,183 +45,131 @@
#include "mavlink_messages.h" #include "mavlink_messages.h"
#include "mavlink_main.h" #include "mavlink_main.h"
class MavlinkFtpTest;
/// @brief MAVLink remote file server. Support FTP like commands using MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL message.
/// A limited number of requests (kRequestQueueSize) may be outstanding at a time. Additional messages will be discarded.
class MavlinkFTP class MavlinkFTP
{ {
public: public:
/// @brief Returns the one Mavlink FTP server in the system.
static MavlinkFTP* get_server(void);
/// @brief Contructor is only public so unit test code can new objects.
MavlinkFTP(); MavlinkFTP();
/// @brief Adds the specified message to the work queue.
void handle_message(Mavlink* mavlink, mavlink_message_t *msg);
typedef void (*ReceiveMessageFunc_t)(const mavlink_message_t *msg, MavlinkFtpTest* ftpTest);
/// @brief Sets up the server to run in unit test mode.
/// @param rcvmsgFunc Function which will be called to handle outgoing mavlink messages.
/// @param ftp_test MavlinkFtpTest object which the function is associated with
void set_unittest_worker(ReceiveMessageFunc_t rcvMsgFunc, MavlinkFtpTest *ftp_test);
static MavlinkFTP *getServer(); /// @brief This is the payload which is in mavlink_file_transfer_protocol_t.payload. We pad the structure ourselves to
/// 32 bit alignment to avoid usage of any pack pragmas.
// static interface struct PayloadHeader
void handle_message(Mavlink* mavlink,
mavlink_message_t *msg);
private:
static const unsigned kRequestQueueSize = 2;
static MavlinkFTP *_server;
/// @brief Trying to pack structures across differing compilers is questionable for Clients, so we pad the
/// structure ourselves to 32 bit alignment which should get us what we want.
struct RequestHeader
{ {
uint16_t seqNumber; ///< sequence number for message uint16_t seqNumber; ///< sequence number for message
uint8_t session; ///< Session id for read and write commands uint8_t session; ///< Session id for read and write commands
uint8_t opcode; ///< Command opcode uint8_t opcode; ///< Command opcode
uint8_t size; ///< Size of data uint8_t size; ///< Size of data
uint8_t padding[3]; uint8_t padding[3]; ///< 32 bit aligment padding
uint32_t crc32; ///< CRC for entire Request structure, with crc32 and padding set to 0 uint32_t crc32; ///< CRC for entire Request structure, with crc32 and padding set to 0
uint32_t offset; ///< Offsets for List and Read commands uint32_t offset; ///< Offsets for List and Read commands
uint8_t data[]; uint8_t data[]; ///< command data, varies by Opcode
}; };
/// @brief Command opcodes
enum Opcode : uint8_t enum Opcode : uint8_t
{ {
kCmdNone, // ignored, always acked kCmdNone, ///< ignored, always acked
kCmdTerminate, // releases sessionID, closes file kCmdTerminateSession, ///< Terminates open Read session
kCmdReset, // terminates all sessions kCmdResetSessions, ///< Terminates all open Read sessions
kCmdList, // list files in <path> from <offset> kCmdListDirectory, ///< List files in <path> from <offset>
kCmdOpen, // opens <path> for reading, returns <session> kCmdOpenFile, ///< Opens file at <path> for reading, returns <session>
kCmdRead, // reads <size> bytes from <offset> in <session> kCmdReadFile, ///< Reads <size> bytes from <offset> in <session>
kCmdCreate, // creates <path> for writing, returns <session> kCmdCreateFile, ///< Creates file at <path> for writing, returns <session>
kCmdWrite, // appends <size> bytes at <offset> in <session> kCmdWriteFile, ///< Appends <size> bytes to file in <session>
kCmdRemove, // remove file (only if created by server?) kCmdRemoveFile, ///< Remove file at <path>
kCmdCreateDirectory, ///< Creates directory at <path>
kRspAck, kCmdRemoveDirectory, ///< Removes Directory at <path>, must be empty
kRspNak
kRspAck, ///< Ack response
kRspNak ///< Nak response
}; };
/// @brief Error codes returned in Nak response PayloadHeader.data[0].
enum ErrorCode : uint8_t enum ErrorCode : uint8_t
{ {
kErrNone, kErrNone,
kErrNoRequest, kErrFail, ///< Unknown failure
kErrNoSession, kErrFailErrno, ///< Command failed, errno sent back in PayloadHeader.data[1]
kErrSequence, kErrInvalidDataSize, ///< PayloadHeader.size is invalid
kErrNotDir, kErrInvalidSession, ///< Session is not currently open
kErrNotFile, kErrNoSessionsAvailable, ///< All available Sessions in use
kErrEOF, kErrEOF, ///< Offset past end of file for List and Read commands
kErrNotAppend, kErrUnknownCommand, ///< Unknown command opcode
kErrTooBig, kErrCrc ///< CRC on Payload is incorrect
kErrIO, };
kErrPerm
}; private:
/// @brief Unit of work which is queued to work_queue
int _findUnusedSession(void); struct Request
bool _validSession(unsigned index);
static const unsigned kMaxSession = 2;
int _session_fds[kMaxSession];
class Request
{ {
public: work_s work; ///< work queue entry
union { Mavlink *mavlink; ///< Mavlink to reply to
dq_entry_t entry; uint8_t serverSystemId; ///< System ID to send from
work_s work; uint8_t serverComponentId; ///< Component ID to send from
}; uint8_t serverChannel; ///< Channel to send to
uint8_t targetSystemId; ///< System ID to target reply to
bool decode(Mavlink *mavlink, mavlink_message_t *fromMessage) {
if (fromMessage->msgid == MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL) {
_systemId = fromMessage->sysid;
_mavlink = mavlink;
mavlink_msg_file_transfer_protocol_decode(fromMessage, &_message);
return _message.target_system == _mavlink->get_system_id();
}
return false;
}
void reply() {
// XXX the proper way would be an IOCTL / uORB call, rather than exploiting the
// flat memory architecture, as we're operating between threads here.
mavlink_message_t msg;
msg.checksum = 0;
unsigned len = mavlink_msg_file_transfer_protocol_pack_chan(_mavlink->get_system_id(), // Sender system id
_mavlink->get_component_id(), // Sender component id
_mavlink->get_channel(), // Channel to send on
&msg, // Message to pack payload into
0, // Target network
_systemId, // Target system id
0, // Target component id
rawData()); // Payload to pack into message
_mavlink->lockMessageBufferMutex();
bool success = _mavlink->message_buffer_write(&msg, len);
_mavlink->unlockMessageBufferMutex();
if (!success) {
warnx("FTP TX ERR");
}
#ifdef MAVLINK_FTP_DEBUG
else {
warnx("wrote: sys: %d, comp: %d, chan: %d, len: %d, checksum: %d",
_mavlink->get_system_id(),
_mavlink->get_component_id(),
_mavlink->get_channel(),
len,
msg.checksum);
}
#endif
}
uint8_t *rawData() { return &_message.payload[0]; }
RequestHeader *header() { return reinterpret_cast<RequestHeader *>(&_message.payload[0]); }
uint8_t *requestData() { return &(header()->data[0]); }
unsigned dataSize() { return header()->size + sizeof(RequestHeader); }
mavlink_channel_t channel() { return _mavlink->get_channel(); }
char *dataAsCString();
private:
Mavlink *_mavlink;
mavlink_file_transfer_protocol_t _message;
uint8_t _systemId;
mavlink_file_transfer_protocol_t message; ///< Protocol message
}; };
Request *_get_request(void);
void _return_request(Request *req);
void _lock_request_queue(void);
void _unlock_request_queue(void);
uint32_t _payload_crc32(PayloadHeader *hdr);
char *_data_as_cstring(PayloadHeader* payload);
static void _worker_trampoline(void *arg);
void _process_request(Request *req);
void _reply(Request *req);
static const char kDirentFile = 'F'; ErrorCode _workList(PayloadHeader *payload);
static const char kDirentDir = 'D'; ErrorCode _workOpen(PayloadHeader *payload, bool create);
static const char kDirentUnknown = 'U'; ErrorCode _workRead(PayloadHeader *payload);
static const uint8_t kMaxDataLength = MAVLINK_MSG_FILE_TRANSFER_PROTOCOL_FIELD_PAYLOAD_LEN - sizeof(RequestHeader); ErrorCode _workWrite(PayloadHeader *payload);
ErrorCode _workTerminate(PayloadHeader *payload);
ErrorCode _workReset(PayloadHeader* payload);
ErrorCode _workRemoveDirectory(PayloadHeader *payload);
ErrorCode _workCreateDirectory(PayloadHeader *payload);
ErrorCode _workRemoveFile(PayloadHeader *payload);
/// Request worker; runs on the low-priority work queue to service static const unsigned kRequestQueueSize = 2; ///< Max number of queued requests
/// remote requests. Request _request_bufs[kRequestQueueSize]; ///< Request buffers which hold work
/// dq_queue_t _request_queue; ///< Queue of available Request buffers
static void _workerTrampoline(void *arg); sem_t _request_queue_sem; ///< Semaphore for locking access to _request_queue
void _worker(Request *req);
int _find_unused_session(void);
/// Reply to a request (XXX should be a Request method) bool _valid_session(unsigned index);
///
void _reply(Request *req); static const char kDirentFile = 'F'; ///< Identifies File returned from List command
static const char kDirentDir = 'D'; ///< Identifies Directory returned from List command
ErrorCode _workList(Request *req); static const char kDirentUnknown = 'U'; ///< Identifies Unknown entry returned from List command
ErrorCode _workOpen(Request *req, bool create);
ErrorCode _workRead(Request *req); /// @brief Maximum data size in RequestHeader::data
ErrorCode _workWrite(Request *req); static const uint8_t kMaxDataLength = MAVLINK_MSG_FILE_TRANSFER_PROTOCOL_FIELD_PAYLOAD_LEN - sizeof(PayloadHeader);
ErrorCode _workRemove(Request *req);
ErrorCode _workTerminate(Request *req); static const unsigned kMaxSession = 2; ///< Max number of active sessions
ErrorCode _workReset(); int _session_fds[kMaxSession]; ///< Session file descriptors, 0 for empty slot
// work freelist ReceiveMessageFunc_t _utRcvMsgFunc; ///< Unit test override for mavlink message sending
Request _workBufs[kRequestQueueSize]; MavlinkFtpTest *_ftp_test; ///< Additional parameter to _utRcvMsgFunc;
dq_queue_t _workFree;
sem_t _lock;
void _qLock() { do {} while (sem_wait(&_lock) != 0); }
void _qUnlock() { sem_post(&_lock); }
void _qFree(Request *req) {
_qLock();
dq_addlast(&req->entry, &_workFree);
_qUnlock();
}
Request *_dqFree() {
_qLock();
auto req = reinterpret_cast<Request *>(dq_remfirst(&_workFree));
_qUnlock();
return req;
}
}; };