383 lines
8.7 KiB
C++
383 lines
8.7 KiB
C++
|
|
#include <new>
|
|
#include <HardwareSerial.h>
|
|
#include "Commander.h"
|
|
|
|
|
|
Commander *Commander::serial_instance_ = nullptr;
|
|
|
|
|
|
void Commander::EnableSerial() {
|
|
assert(!running_);
|
|
assert(serial_instance_ == nullptr);
|
|
serial_instance_ = this;
|
|
enable_serial_ = true;
|
|
}
|
|
|
|
|
|
void Commander::SetFallbackHandler(const std::function<void(const std::vector<CmdString> &,
|
|
CommanderClient *)> &handler) {
|
|
assert(!running_);
|
|
handler_ = handler;
|
|
}
|
|
|
|
|
|
void Commander::Begin() {
|
|
assert(!running_);
|
|
|
|
running_ = true;
|
|
if (enable_serial_) {
|
|
serial_client_ = new SerialCommanderClient(this);
|
|
serial_client_->Begin();
|
|
serial_instance_ = this;
|
|
}
|
|
if (enable_wifi_) {
|
|
assert(port_ > 0);
|
|
tcp_server_ = new AsyncServer(port_);
|
|
tcp_server_->onClient(Commander::WifiOnClientHandler, this);
|
|
xTaskCreate(Commander::TaskFn, "Commander", 10000, this, 1, &this->task_handle_);
|
|
}
|
|
|
|
}
|
|
|
|
void Commander::Stop() {
|
|
assert(running_);
|
|
vTaskDelete(task_handle_);
|
|
// todo: remove the serial handler, disconnect all wifi tcp clients, stop the wifi tcp server
|
|
running_ = false;
|
|
}
|
|
|
|
void Commander::WifiOnClient(AsyncClient *client) {
|
|
wifi_clients_.emplace_back(this, client);
|
|
}
|
|
|
|
|
|
void Commander::WifiOnClientHandler(void *thisarg, AsyncClient *client) {
|
|
((Commander *)thisarg)->WifiOnClient(client);
|
|
}
|
|
|
|
void Commander::EnableWifi(uint16_t port) {
|
|
assert(!running_);
|
|
enable_wifi_ = true;
|
|
port_ = port;
|
|
}
|
|
|
|
void Command::SetPrompt(const String& prompt) {
|
|
prompt_ = prompt;
|
|
}
|
|
|
|
void Commander::TaskFn(void *thisarg) {
|
|
((Commander*)thisarg)->Task();
|
|
}
|
|
|
|
[[noreturn]] void Commander::Task() {
|
|
while(true) {
|
|
ulTaskNotifyTake(pdTRUE, 1000); // todo: number of ticks, can it be made infinite?
|
|
// todo: something something queue of newly connected wifi clients, actually use a queue here maybe instead of a notification
|
|
}
|
|
}
|
|
|
|
void CommanderClient::DipatchCurrentCommandLine() {
|
|
assert(!current_line_.empty());
|
|
// todo: somehow enter into new shell if a shell command was called without arguments
|
|
const Command* shell = shell_stack_.back()->Dispatch(current_line_, this);
|
|
if (shell != nullptr) {
|
|
shell_stack_.push_back(shell);
|
|
}
|
|
current_line_.clear();
|
|
}
|
|
|
|
void CommanderClient::ProcessCommandLine() {
|
|
if (current_line_.empty()) {
|
|
current_line_.emplace_back("");
|
|
}
|
|
size_t pos = 0;
|
|
for (char c : buffer_) {
|
|
auto ¤t = current_line_.back();
|
|
switch (c) {
|
|
case ' ':
|
|
if (in_quotes_ || in_escape_) {
|
|
current.concat(' ');
|
|
} else {
|
|
if (current.length() > 0) {
|
|
current_line_.emplace_back("");
|
|
}
|
|
}
|
|
break;
|
|
case '"':
|
|
if (in_escape_) {
|
|
current.concat('"');
|
|
in_escape_ = false;
|
|
} else {
|
|
if (in_quotes_) {
|
|
in_quotes_ = false;
|
|
current_line_.emplace_back("");
|
|
} else {
|
|
in_quotes_ = true;
|
|
if (current.length() > 0) {
|
|
current_line_.emplace_back("");
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case '\\':
|
|
if (in_escape_) {
|
|
current.concat('\\');
|
|
in_escape_ = false;
|
|
} else {
|
|
in_escape_ = true;
|
|
}
|
|
break;
|
|
case '\n':
|
|
if (in_escape_ || in_quotes_) {
|
|
current.concat('\n');
|
|
print("> "); // line continuation prompt
|
|
if (in_escape_) {
|
|
in_escape_ = false;
|
|
}
|
|
} else {
|
|
goto line_completed;
|
|
}
|
|
break;
|
|
default:
|
|
if (c >= 0x20) { // ignore control characters etc (also ignores tab)
|
|
current.concat(c);
|
|
}
|
|
break;
|
|
}
|
|
pos += 1;
|
|
}
|
|
goto end;
|
|
|
|
line_completed:
|
|
pos += 1;
|
|
DipatchCurrentCommandLine();
|
|
|
|
end:
|
|
buffer_.remove(0, pos);
|
|
}
|
|
|
|
const String &CommanderClient::GetPrompt() {
|
|
return shell_stack_.back()->prompt_;
|
|
}
|
|
|
|
const String &CommanderClient::GetWelcomeMessage() {
|
|
return shell_stack_.back()->welcome_message_;
|
|
}
|
|
|
|
|
|
void CommanderClient::BufferAppend(const String &str) {
|
|
buffer_.concat(str);
|
|
}
|
|
|
|
void CommanderClient::BufferAppend(char ch) {
|
|
buffer_.concat(ch);
|
|
}
|
|
/*
|
|
template<typename T>
|
|
bool CommanderClient::ReadLine(T *out) {
|
|
static_assert(!std::is_same<T, int64_t>::value, "64-bit not supported");
|
|
static_assert(!std::is_same<T, uint64_t>::value, "64-bit not supported");
|
|
static_assert(std::is_integral<T>::value, "integer type required");
|
|
|
|
CmdString line = ReadLine();
|
|
if (line.isEmpty()) return false;
|
|
int64_t res;
|
|
int i = 0;
|
|
|
|
if (!std::is_unsigned<T>::value) {
|
|
bool negative = false;
|
|
if (line[0] == '-') {
|
|
i = 1;
|
|
negative = true;
|
|
if (line.length() < 2) return false;
|
|
}
|
|
|
|
char ch = line[i];
|
|
if (ch >= '0' && ch <= '9') {
|
|
res = ch - '0';
|
|
if (negative) {
|
|
res = -res;
|
|
}
|
|
} else {
|
|
return false;
|
|
};
|
|
i += 1;
|
|
}
|
|
|
|
for (; i < line.length(); i++) {
|
|
char ch = line[i];
|
|
if (ch >= '0' && ch <= '9') {
|
|
uint8_t digit = ch - '0';
|
|
res *= 10;
|
|
res += digit;
|
|
if (res > std::numeric_limits<T>::max() || res < std::numeric_limits<T>::min()) {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
*out = res;
|
|
return true;
|
|
}*/
|
|
|
|
CmdString CommanderClient::ReadLine() {
|
|
CmdString ret;
|
|
int c = read();
|
|
while (c != '\n') {
|
|
if (c >= 0x20) { // ignore control chars, todo: make this better I guess? line editing capabilities?
|
|
write(c);
|
|
ret += (char)c;
|
|
}
|
|
c = read();
|
|
}
|
|
write('\n');
|
|
return ret;
|
|
}
|
|
|
|
SerialCommanderClient *SerialCommanderClient::single_instance_ = nullptr;
|
|
|
|
size_t SerialCommanderClient::write(uint8_t byte) { return Serial.write(byte); }
|
|
|
|
int SerialCommanderClient::available() { return Serial.available(); }
|
|
|
|
int SerialCommanderClient::read() { return Serial.read(); }
|
|
|
|
int SerialCommanderClient::peek() { return Serial.peek(); }
|
|
|
|
void SerialCommanderClient::flush() { Serial.flush(); }
|
|
|
|
void SerialCommanderClient::OnReceive() {
|
|
assert(task_handle_ != nullptr);
|
|
vTaskNotifyGiveFromISR(task_handle_, nullptr);
|
|
}
|
|
|
|
SerialCommanderClient::SerialCommanderClient(Commander *commander) : CommanderClient(commander) {
|
|
assert(single_instance_ == nullptr);
|
|
single_instance_ = this;
|
|
println(GetWelcomeMessage());
|
|
println("");
|
|
print(GetPrompt());
|
|
}
|
|
|
|
|
|
void SerialCommanderClient::Task() {
|
|
while(true) {
|
|
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10000)); // todo: number of ticks, possible to wait infinitely long?
|
|
if (in_command_) continue;
|
|
in_command_ = true;
|
|
while(Serial.available()) {
|
|
int ch = Serial.read();
|
|
if (ch < 0) {
|
|
break;
|
|
}
|
|
Serial.write(ch); // echo the typed character so the user sees it
|
|
BufferAppend((char)ch);
|
|
if (ch == '\n') {
|
|
ProcessCommandLine();
|
|
print(GetPrompt());
|
|
}
|
|
}
|
|
in_command_ = false;
|
|
}
|
|
}
|
|
|
|
void WifiCommanderClient::OnData(void *data, size_t len) {
|
|
// todo
|
|
}
|
|
|
|
size_t WifiCommanderClient::write(uint8_t byte) {
|
|
// todo: also implement a write(const char*) or something like it
|
|
}
|
|
|
|
int WifiCommanderClient::available() {
|
|
// todo
|
|
}
|
|
|
|
int WifiCommanderClient::read() {
|
|
// todo
|
|
}
|
|
|
|
int WifiCommanderClient::peek() {
|
|
// todo
|
|
}
|
|
|
|
void WifiCommanderClient::flush() {
|
|
// todo
|
|
}
|
|
|
|
void WifiCommanderClient::OnDataHandler(void *thisarg, AsyncClient *, void *data, size_t len) {
|
|
((WifiCommanderClient *)thisarg)->OnData(data, len);
|
|
}
|
|
|
|
WifiCommanderClient::WifiCommanderClient(Commander *commander, AsyncClient *client)
|
|
: CommanderClient(commander), async_client_(client) {
|
|
async_client_->onData(WifiCommanderClient::OnDataHandler, this);
|
|
}
|
|
|
|
Command *Command::RegisterCommand(const String &name,
|
|
const std::function<void(const std::vector<CmdString> &, CommanderClient *)> &handler) {
|
|
commands_.push_back(Command(name, handler, false));
|
|
return &commands_[commands_.size() - 1];
|
|
}
|
|
|
|
Command *Command::RegisterCommandWithShell(const String &name) {
|
|
commands_.push_back(Command(name, nullptr, true));
|
|
return &commands_[commands_.size() - 1];
|
|
}
|
|
|
|
const Command* Command::Dispatch(std::vector<CmdString> cmdline, CommanderClient *client) const {
|
|
if (!cmdline.empty()) {
|
|
auto &first = cmdline.front();
|
|
for (const auto &item : commands_) {
|
|
if (item.name_.equals(first)) {
|
|
if (item.shell_) {
|
|
return &item;
|
|
}
|
|
item.Dispatch(std::vector<CmdString>(cmdline.begin() + 1, cmdline.end()),
|
|
client); // todo maybe optimize this vector
|
|
return nullptr;
|
|
}
|
|
}
|
|
if (first.equals("?")) {
|
|
DefaultHelpHandler(client);
|
|
return nullptr;
|
|
}
|
|
}
|
|
if (handler_ == nullptr) {
|
|
DefaultHandler(cmdline, client);
|
|
} else {
|
|
handler_(cmdline, client);
|
|
}
|
|
}
|
|
|
|
void Command::DefaultHandler(const std::vector<CmdString> &cmdline, CommanderClient *client) const {
|
|
if (commands_.empty()) {
|
|
client->println("this command is not implemented");
|
|
} else {
|
|
if (cmdline.empty()) {
|
|
DefaultHelpHandler(client);
|
|
} else {
|
|
client->print("unknown sub-command: ");
|
|
client->println(cmdline.front());
|
|
}
|
|
}
|
|
}
|
|
|
|
void Command::DefaultHelpHandler(CommanderClient *client) const {
|
|
client->println("the following commands are available: ");
|
|
for (const auto &item : commands_) {
|
|
client->print(" ");
|
|
client->print(item.name_);
|
|
client->print(" ");
|
|
client->print(item.description_);
|
|
client->println();
|
|
}
|
|
}
|
|
|
|
void Command::SetWelcomeMessage(const String &message) {
|
|
welcome_message_ = message;
|
|
}
|