OVMS3/OVMS.V3/main/ovms_command.cpp

1608 lines
44 KiB
C++
Raw Permalink Normal View History

/*
; Project: Open Vehicle Monitor System
; Date: 14th March 2017
;
; Changes:
; 1.0 Initial release
;
; (C) 2011 Michael Stegen / Stegen Electronics
; (C) 2011-2017 Mark Webb-Johnson
; (C) 2011 Sonny Chen @ EPRO/DX
;
; Permission is hereby granted, free of charge, to any person obtaining a copy
; of this software and associated documentation files (the "Software"), to deal
; in the Software without restriction, including without limitation the rights
; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
; copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:
;
; The above copyright notice and this permission notice shall be included in
; all copies or substantial portions of the Software.
;
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
; THE SOFTWARE.
*/
#include "ovms_log.h"
static const char *TAG = "command";
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <functional>
#include <esp_log.h>
#include <sys/time.h>
#include <sys/types.h>
#include <dirent.h>
#include "freertos/FreeRTOS.h"
#include "ovms_command.h"
#include "ovms_config.h"
#include "ovms_events.h"
#include "ovms_peripherals.h"
#include "ovms_utils.h"
#include "ovms_script.h"
#include "buffered_shell.h"
#include "log_buffers.h"
#include "ovms_semaphore.h"
OvmsCommandApp MyCommandApp __attribute__ ((init_priority (1010)));
bool CompareCharPtr::operator()(const char* a, const char* b)
{
return strcmp(a, b) < 0;
}
OvmsWriter::OvmsWriter()
{
std::string p = MyConfig.GetParamValue("password","module");
if (p.empty())
m_issecure = true;
else
m_issecure = false;
m_insert = NULL;
m_userData = NULL;
m_monitoring = false;
}
OvmsWriter::~OvmsWriter()
{
}
void OvmsWriter::Exit()
{
puts("This console cannot exit.");
}
void OvmsWriter::RegisterInsertCallback(InsertCallback cb, void* ctx)
{
m_insert = cb;
m_userData = ctx;
}
void OvmsWriter::DeregisterInsertCallback(InsertCallback cb)
{
if (m_insert == cb)
{
m_insert = NULL;
m_userData = NULL;
finalise();
ProcessChar('\n');
}
}
bool OvmsWriter::IsSecure()
{
return m_issecure;
}
void OvmsWriter::SetSecure(bool secure)
{
m_issecure = secure;
}
OvmsCommand* OvmsCommandMap::FindUniquePrefix(const char* key)
{
int len = strlen(key);
OvmsCommand* found = NULL;
for (iterator it = begin(); it != end(); ++it)
{
if (strncmp(it->first, key, len) == 0)
{
if (len == strlen(it->first))
{
return it->second;
}
if (found)
{
return NULL;
}
else
{
found = it->second;
}
}
}
return found;
}
OvmsCommand* OvmsCommandMap::FindCommand(const char* key)
{
iterator it = find(key);
if (it == end())
return NULL;
else
return it->second;
}
char ** OvmsCommandMap::GetCompletion(OvmsWriter* writer, const char* token)
{
unsigned int index = 0;
char** tokens = writer->SetCompletion(index, NULL);
if (token)
{
for (iterator it = begin(); it != end(); ++it)
{
if (it->second->IsSecure() && !writer->IsSecure())
continue;
if (strncmp(it->first, token, strlen(token)) == 0)
writer->SetCompletion(index++, it->first);
}
}
return tokens;
}
OvmsCommand::OvmsCommand()
{
m_execute = NULL;
m_usage_template= "";
m_parent = NULL;
m_validate = NULL;
}
OvmsCommand::OvmsCommand(const char* name, const char* title, OvmsCommandExecuteCallback_t execute,
const char *usage, int min, int max, bool secure,
OvmsCommandValidateCallback_t validate)
{
m_name = name;
m_title = title;
m_execute = execute;
m_usage_template= !usage ? "" : usage;
m_min = min;
m_max = max;
m_parent = NULL;
m_secure = secure;
m_validate = validate;
}
OvmsCommand::~OvmsCommand()
{
for (auto it = m_children.begin(); it != m_children.end(); ++it)
{
OvmsCommand* cmd = it->second;
delete cmd;
}
m_children.clear();
}
const char* OvmsCommand::GetName()
{
return m_name;
}
const char* OvmsCommand::GetTitle()
{
return m_title;
}
// Dynamic generation of "Usage:" messages. Syntax of the usage template string:
// - Prefix is "Usage: " followed by names from ancestors (if any) and name of self
// - "$C" expands to children as child1|child2|child3
// - "[$C]" expands to optional children as [child1|child2|child3]
// - $G$ expands to the usage of the first child (typically used after $C)
// - $Gfoo$ expands to the usage of the child named "foo"
// - $L lists a full usage line for each of the children
// - Parameters after command and subcommand tokens may be explicit like " <metric>"
// - Empty usage template "" defaults to "$C" for non-terminal OvmsCommand
void OvmsCommand::PutUsage(OvmsWriter* writer)
{
std::string result ="Usage: ";
size_t pos = result.size();
for (OvmsCommand* parent = m_parent; parent && parent->m_parent; parent = parent->m_parent)
{
if (parent->m_validate)
{
size_t len = strlen(parent->m_usage_template);
char* dollar = index(parent->m_usage_template, '$');
if (dollar)
{
len = dollar - parent->m_usage_template;
if (len > 0 && *(dollar-1) == '[')
--len;
}
else
result.insert(pos, " ");
result.insert(pos, parent->m_usage_template, len);
}
result.insert(pos, " ");
result.insert(pos, parent->m_name);
}
result += m_name;
result += " ";
ExpandUsage(m_usage_template, writer, result);
writer->puts(result.c_str());
}
void OvmsCommand::ExpandUsage(const char* templ, OvmsWriter* writer, std::string& result)
{
std::string usage = (*templ || m_children.empty()) ? templ : m_execute ? "[$C]" : "$C";
size_t pos;
if ((pos = usage.find("$L")) != std::string::npos)
{
result += usage.substr(0, pos);
pos += 2;
size_t z = result.size();
bool found = false;
for (OvmsCommandMap::iterator it = m_children.begin(); it != m_children.end(); ++it)
{
OvmsCommand* child = it->second;
if (!child->m_secure || writer->m_issecure)
{
if (found)
{
result += "\n";
result += result.substr(0, z);
}
result += it->first;
result += " ";
found = true;
child->ExpandUsage(child->m_usage_template, writer, result);
}
}
if (result.size() == z)
{
result = "All subcommands require 'enable' mode";
pos = usage.size(); // Don't append any more
}
result += usage.substr(pos);
return;
}
if ((pos = usage.find("$C")) != std::string::npos)
{
result += usage.substr(0, pos);
pos += 2;
size_t z = result.size();
bool found = false;
for (OvmsCommandMap::iterator it = m_children.begin(); it != m_children.end(); ++it)
{
if (!it->second->m_secure || writer->m_issecure)
{
if (found)
result += "|";
result += it->first;
found = true;
}
}
if (result.size() == z)
{
result = "All subcommands require 'enable' mode";
pos = usage.size(); // Don't append any more
}
}
else pos = 0;
size_t pos2;
if ((pos2 = usage.find("$G", pos)) != std::string::npos)
{
result += usage.substr(pos, pos2-pos);
pos2 += 2;
size_t pos3;
OvmsCommandMap::iterator it = m_children.end();
if ((pos3 = usage.find('$', pos2)) != std::string::npos)
{
if (pos3 == pos2)
it = m_children.begin();
else
it = m_children.find(usage.substr(pos2, pos3-pos2).c_str());
pos = pos3 + 1;
if (it != m_children.end() && (!it->second->m_secure || writer->m_issecure))
{
OvmsCommand* child = it->second;
child->ExpandUsage(child->m_usage_template, writer, result);
}
}
else
pos = pos2;
if (it == m_children.end())
result += "ERROR IN USAGE TEMPLATE";
}
result += usage.substr(pos);
}
OvmsCommand* OvmsCommand::RegisterCommand(const char* name, const char* title, OvmsCommandExecuteCallback_t execute,
const char *usage, int min, int max, bool secure,
OvmsCommandValidateCallback_t validate)
{
// Protect against duplicate registrations
OvmsCommand* cmd = FindCommand(name);
if (cmd == NULL)
{
cmd = new OvmsCommand(name, title, execute, usage, min, max, secure, validate);
m_children[name] = cmd;
cmd->m_parent = this;
}
return cmd;
}
bool OvmsCommand::UnregisterCommand(const char* name)
{
if (name == NULL)
{
// Unregister this command
return m_parent->UnregisterCommand(m_name);
}
// Unregister the specified child command
auto pos = m_children.find(name);
if (pos == m_children.end())
{
return false;
}
else
{
OvmsCommand* cmd = pos->second;
m_children.erase(pos);
delete cmd;
return true;
}
}
char ** OvmsCommand::Complete(OvmsWriter* writer, int argc, const char * const * argv)
{
writer->SetCompletion(0, NULL); // Start with no completion tokens
if (m_validate)
{
int used = -1;
if (argc > 0)
used = m_validate(writer, this, argc > m_max ? m_max : argc, argv, true);
if (used < 0 || used == argc)
return writer->GetCompletions();
argc -= used;
argv += used;
}
if (argc <= 1)
{
return m_children.GetCompletion(writer, argc > 0 ? argv[0] : "");
}
OvmsCommand* cmd = m_children.FindUniquePrefix(argv[0]);
if (!cmd)
{
return writer->SetCompletion(0, NULL);
}
return cmd->Complete(writer, argc-1, ++argv);
}
void OvmsCommand::Execute(int verbosity, OvmsWriter* writer, int argc, const char * const * argv)
{
// if (argc>0)
// {
// printf("Execute(%s/%d) verbosity=%d argc=%d first=%s\n",m_title, m_children.size(), verbosity,argc,argv[0]);
// }
// else
// {
// printf("Execute(%s/%d) verbosity=%d (no args)\n",m_title, m_children.size(), verbosity);
// }
if (m_execute && (m_children.empty() || argc == 0))
{
//puts("Executing directly...");
if (argc < m_min || argc > m_max || (argc > 0 && strcmp(argv[argc-1],"?")==0))
{
PutUsage(writer);
return;
}
if ((!m_secure)||(m_secure && writer->m_issecure))
m_execute(verbosity,writer,this,argc,argv);
else
writer->puts("Error: Secure command requires 'enable' mode");
return;
}
else
{
if (m_validate && argc >= m_min)
{
int used = m_validate(writer, this, argc > m_max ? m_max : argc, argv, false);
if (used < 0)
{
if (argc > 0 && strcmp(argv[argc-1],"?") != 0)
writer->puts("Unrecognised command");
PutUsage(writer);
return;
}
argc -= used;
argv += used;
}
//puts("Looking for a matching command");
if (argc <= 0)
{
if (m_execute)
{
if ((!m_secure)||(m_secure && writer->m_issecure))
m_execute(verbosity,writer,this,argc,argv);
else
writer->puts("Error: Secure command requires 'enable' mode");
return;
}
writer->puts("Subcommand required");
PutUsage(writer);
return;
}
if (strcmp(argv[0],"?")==0)
{
// Skip usage line if it's just the one-line list of children.
if (*m_usage_template || m_execute)
PutUsage(writer);
// Show available commands
int avail = 0;
for (OvmsCommandMap::iterator it=m_children.begin(); it!=m_children.end(); ++it)
{
if (it->second->IsSecure() && !writer->m_issecure)
continue;
const char* k = it->first;
const char* v = it->second->GetTitle();
writer->printf("%-20.20s %s\n",k,v);
++avail;
}
if (!avail)
writer->printf("All subcommands require 'enable' mode\n");
return;
}
OvmsCommand* cmd = m_children.FindUniquePrefix(argv[0]);
if (!cmd)
{
writer->puts("Unrecognised command");
if (GetParent()) // No usage line for root command
PutUsage(writer);
return;
}
cmd->Execute(verbosity,writer,argc-1,++argv);
}
}
OvmsCommand* OvmsCommand::GetParent()
{
return m_parent;
}
OvmsCommand* OvmsCommand::FindCommand(const char* name)
{
return m_children.FindCommand(name);
}
// List all OvmsCommand objects in the tree as a tab-separated CSV-format table
// (which requires doubling " marks).
void OvmsCommand::Display(OvmsWriter* writer, int level)
{
static const char* const spaces = " ";
static const int len = strlen(spaces);
static const char* const end = spaces + len;
if (level >= 0)
{
const char* usage = m_usage_template;
if (!usage)
usage = "NULL";
const char* p = usage;
int quotes = 0;
for ( ; *p; ++p)
if (*p == '"')
++quotes;
char* m = (char*)malloc(p - m_usage_template + quotes + 1);
if (m)
{
char* q = m;
for (p = usage; *p; )
{
if (*p == '"')
*q++ = '"';
*q++ = *p++;
}
*q = '\0';
usage = m;
}
if (2*level > len)
level = 0;
writer->printf("\"%s%s\"\t\"%s\"\t\"%s\"\t%d\t%d\t%s\t%s\t%s\t%s\n",
end-2*level, m_name, m_title, usage, m_min, m_max, m_children.empty() ? "--" : "children",
m_execute ? "execute" : "--", m_secure ? "secure" : "--", m_validate ? "validate" : "--");
if (m)
free(m);
}
for (OvmsCommandMap::iterator it=m_children.begin(); it!=m_children.end(); ++it)
it->second->Display(writer, level + 1);
}
void help(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
writer->puts("Enter a single \"?\" to get the root command list.");
writer->puts("Commands can have multiple levels of subcommands.");
writer->puts("Use \"command [...] ?\" to get the list of subcommands and parameters.");
writer->puts("Commands can be abbreviated, push <TAB> for auto completion at any level");
writer->puts("including at the start of a subcommand to get a list of subcommands.");
writer->puts("Use \"enable\" to enter secure (admin) mode.");
}
void cmd_exit(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
writer->Exit();
}
void log_level(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
const char* tag = "*";
if (argc > 0)
tag = argv[0];
const char* title = cmd->GetTitle();
esp_log_level_t level_num = (esp_log_level_t)(*(title+strlen(title)-2) - '0');
esp_log_level_set(tag, level_num);
writer->printf("Logging level for %s set to %s\n",tag,cmd->GetName());
}
void log_file(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
std::string path;
if (argc == 1)
path = argv[0];
else
path = MyConfig.GetParamValue("log", "file.path");
if (MyConfig.ProtectedPath(path))
{
writer->puts("Error: protected path");
return;
}
if (!MyCommandApp.SetLogfile(path))
{
writer->puts("Error: VFS file cannot be opened for append");
return;
}
writer->printf("Logging to file: %s\n", path.c_str());
}
void log_close(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
std::string path = MyCommandApp.GetLogfile();
if (path.empty())
{
writer->puts("Error: no log file path has been set");
return;
}
if (MyCommandApp.CloseLogfile())
writer->printf("File logging to '%s' stopped\n", path.c_str());
else
writer->printf("Error: stop file logging to '%s' failed, see log for details\n", path.c_str());
}
void log_open(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
std::string path = MyCommandApp.GetLogfile();
if (path.empty())
{
writer->puts("Error: no log file path has been set");
return;
}
if (MyCommandApp.OpenLogfile())
writer->printf("File logging to '%s' started\n", path.c_str());
else
writer->printf("Error: start file logging to '%s' failed, see log for details\n", path.c_str());
}
void log_status(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
MyCommandApp.ShowLogStatus(verbosity, writer);
}
void log_expire(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
if (MyCommandApp.m_expiretask)
{
writer->puts("Abort: expire task is currently running");
return;
}
int keepdays;
if (argc == 0)
keepdays = MyConfig.GetParamValueInt("log", "file.keepdays", 30);
else
keepdays = atoi(argv[0]);
MyCommandApp.ExpireLogFiles(verbosity, writer, keepdays);
}
static OvmsCommand* monitor;
static OvmsCommand* monitor_yes;
void log_monitor(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
bool state;
if (cmd == monitor)
state = !writer->IsMonitoring();
else
state = (cmd == monitor_yes);
writer->printf("Monitoring log messages %s\n", state ? "enabled" : "disabled");
writer->SetMonitoring(state);
}
typedef struct
{
std::string password;
int tries;
} PasswordContext;
bool enableInsert(OvmsWriter* writer, void* v, char ch)
{
PasswordContext* pc = (PasswordContext*)v;
if (ch == '\n')
{
std::string p = MyConfig.GetParamValue("password","module");
if (p.compare(pc->password) == 0)
{
writer->SetSecure(true);
writer->printf("\nSecure mode");
delete pc;
return false;
}
if (++pc->tries == 3)
{
writer->printf("\nError: %d incorrect password attempts", pc->tries);
vTaskDelay(5000 / portTICK_PERIOD_MS);
delete pc;
return false;
}
writer->printf("\nSorry, try again.\nPassword:");
pc->password.erase();
return true;
}
if (ch == 'C'-0100)
{
delete pc;
return false;
}
if (ch != '\r')
pc->password.append(1, ch);
return true;
}
void enable(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
std::string p = MyConfig.GetParamValue("password","module");
if ((p.empty())||((argc==1)&&(p.compare(argv[0])==0)))
{
writer->SetSecure(true);
writer->puts("Secure mode");
}
else if (argc == 1)
{
vTaskDelay(5000 / portTICK_PERIOD_MS);
writer->puts("Error: Invalid password");
}
else
{
PasswordContext* pc = new PasswordContext;
pc->tries = 0;
writer->printf("Password:");
writer->RegisterInsertCallback(enableInsert, pc);
}
}
void disable(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
writer->SetSecure(false);
}
void cmd_sleep(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
int milliseconds = atof(argv[0]) * 1000;
if (milliseconds >= 0)
vTaskDelay(pdMS_TO_TICKS(milliseconds));
}
void cmd_echo(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
int i;
for (i = 0; i < argc; i++)
writer->puts(argv[i]);
if (!i)
writer->puts("");
}
#ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE
static duk_ret_t DukOvmsCommandExec(duk_context *ctx)
{
const char *cmd = duk_to_string(ctx,0);
if (cmd != NULL)
{
BufferedShell* bs = new BufferedShell(false, COMMAND_RESULT_NORMAL);
bs->SetSecure(true); // this is an authorized channel
bs->ProcessChars(cmd, strlen(cmd));
bs->ProcessChar('\n');
std::string val; bs->Dump(val);
delete bs;
duk_push_string(ctx, val.c_str());
return 1; /* one return value */
}
else
{
return 0;
}
}
//static void DukOvmsCommandRegisterRun(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
// {
// writer->printf("Duktape: Command %s run\n",cmd->GetName());
// }
static duk_ret_t DukOvmsCommandRegister(duk_context *ctx)
{
std::string filename, function;
int linenumber = 0;
MyDuktape.DukGetCallInfo(ctx, &filename, &linenumber, &function);
const char *fullcommand = duk_to_string(ctx,0);
const char *name = duk_to_string(ctx,1);
const char *title = duk_to_string(ctx,2);
const char *usage = duk_to_string(ctx,3);
uint32_t min = duk_is_number(ctx,4) ? duk_to_uint32(ctx,4) : 0;
uint32_t max = duk_is_number(ctx,5) ? duk_to_uint32(ctx,5) : 0;
MyDuktape.RegisterDuktapeConsoleCommand(
ctx, 0,
filename.c_str(),
fullcommand,
name,
title,
usage,
min,
max);
ESP_LOGD(TAG,"Duktape: Script %s %s:%d registered command %s/%s",
filename.c_str(), function.c_str(), linenumber, fullcommand, name);
return 0;
}
#endif // #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE
OvmsCommandApp::OvmsCommandApp()
{
ESP_LOGI(TAG, "Initialising COMMAND (1010)");
m_logfile = NULL;
m_logfile_path = "";
m_logfile_size = 0;
m_logfile_maxsize = 0;
m_logtask = NULL;
m_logtask_queue = NULL;
m_logtask_dropcnt = 0;
m_logfile_cyclecnt = 0;
m_expiretask = 0;
m_root.RegisterCommand("help", "Ask for help", help, "", 0, 0, false);
m_root.RegisterCommand("exit", "End console session", cmd_exit, "", 0, 0, false);
OvmsCommand* cmd_log = MyCommandApp.RegisterCommand("log","LOG framework", log_status, "", 0, 0, false);
cmd_log->RegisterCommand("file", "Start logging to specified file", log_file, "[<vfspath>]\nDefault: config log[file.path]", 0, 1);
cmd_log->RegisterCommand("open", "Start file logging", log_open);
cmd_log->RegisterCommand("close", "Stop file logging", log_close);
cmd_log->RegisterCommand("status", "Show logging status", log_status);
cmd_log->RegisterCommand("expire", "Expire old log files", log_expire, "[<keepdays>]", 0, 1);
OvmsCommand* level_cmd = cmd_log->RegisterCommand("level", "Set logging level", NULL, "$C [<tag>]", 0, 0, false);
level_cmd->RegisterCommand("verbose", "Log at the VERBOSE level (5)", log_level , "[<tag>]", 0, 1);
level_cmd->RegisterCommand("debug", "Log at the DEBUG level (4)", log_level , "[<tag>]", 0, 1);
level_cmd->RegisterCommand("info", "Log at the INFO level (3)", log_level , "[<tag>]", 0, 1);
level_cmd->RegisterCommand("warn", "Log at the WARN level (2)", log_level , "[<tag>]", 0, 1, false);
level_cmd->RegisterCommand("error", "Log at the ERROR level (1)", log_level , "[<tag>]", 0, 1, false);
level_cmd->RegisterCommand("none", "No logging (0)", log_level , "[<tag>]", 0, 1, false);
monitor = cmd_log->RegisterCommand("monitor", "Monitor log on this console", log_monitor);
monitor_yes = monitor->RegisterCommand("yes", "Monitor log", log_monitor);
monitor->RegisterCommand("no", "Don't monitor log", log_monitor);
m_root.RegisterCommand("enable","Enter secure mode (enable access to all commands)", enable, "[<password>]", 0, 1, false);
m_root.RegisterCommand("disable","Leave secure mode (disable access to most commands)", disable);
m_root.RegisterCommand("sleep", "Script utility: pause execution", cmd_sleep,
"<seconds>\nFractions of seconds are supported, e.g. 0.2 = 200 ms", 1, 1);
m_root.RegisterCommand("echo", "Script utility: output text", cmd_echo,
"[<text>] […]\nOutputs up to 10 arguments as separate lines, just a newline if no text is given.", 0, 10);
#ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE
ESP_LOGI(TAG, "Expanding DUKTAPE javascript engine");
DuktapeObjectRegistration* dto = new DuktapeObjectRegistration("OvmsCommand");
dto->RegisterDuktapeFunction(DukOvmsCommandExec, 1, "Exec");
dto->RegisterDuktapeFunction(DukOvmsCommandRegister, 6, "Register");
MyDuktape.RegisterDuktapeObject(dto);
#endif // #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE
}
OvmsCommandApp::~OvmsCommandApp()
{
}
void OvmsCommandApp::ConfigureLogging()
{
MyConfig.RegisterParam("log", "Logging configuration", true, true);
using std::placeholders::_1;
using std::placeholders::_2;
MyEvents.RegisterEvent(TAG, "config.changed", std::bind(&OvmsCommandApp::EventHandler, this, _1, _2));
MyEvents.RegisterEvent(TAG, "sd.mounted", std::bind(&OvmsCommandApp::EventHandler, this, _1, _2));
MyEvents.RegisterEvent(TAG, "sd.unmounting", std::bind(&OvmsCommandApp::EventHandler, this, _1, _2));
MyEvents.RegisterEvent(TAG, "ticker.3600", std::bind(&OvmsCommandApp::EventHandler, this, _1, _2));
ReadConfig();
}
OvmsCommand* OvmsCommandApp::RegisterCommand(const char* name, const char* title, OvmsCommandExecuteCallback_t execute,
const char *usage, int min, int max, bool secure)
{
return m_root.RegisterCommand(name, title, execute, usage, min, max, secure);
}
bool OvmsCommandApp::UnregisterCommand(const char* name)
{
// Unregister the specified child command
return m_root.UnregisterCommand(name);
}
OvmsCommand* OvmsCommandApp::FindCommand(const char* name)
{
return m_root.FindCommand(name);
}
OvmsCommand* OvmsCommandApp::FindCommandFullName(const char* name)
{
OvmsCommand* found = &m_root;
const char* p = name;
while (*p != 0)
{
const char* d = strchr(p,' ');
if (d)
{
std::string command(p,0,d-p);
found = found->FindCommand(command.c_str());
p = d+1;
}
else
{
found = found->FindCommand(p);
return found;
}
if (found==NULL) return found;
}
return found;
}
void OvmsCommandApp::RegisterConsole(OvmsWriter* writer)
{
m_consoles.insert(writer);
}
void OvmsCommandApp::DeregisterConsole(OvmsWriter* writer)
{
m_consoles.erase(writer);
}
int OvmsCommandApp::Log(const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
size_t ret = Log(fmt, args);
va_end(args);
return ret;
}
int OvmsCommandApp::Log(const char* fmt, va_list args)
{
LogBuffers* lb;
TaskHandle_t task = xTaskGetCurrentTaskHandle();
PartialLogs::iterator it = m_partials.find(task);
if (it == m_partials.end())
lb = new LogBuffers();
else
{
lb = it->second;
m_partials.erase(task);
}
int ret = LogBuffer(lb, fmt, args);
lb->set(m_consoles.size());
for (ConsoleSet::iterator it = m_consoles.begin(); it != m_consoles.end(); ++it)
{
(*it)->Log(lb);
}
return ret;
}
int OvmsCommandApp::LogPartial(const char* fmt, ...)
{
LogBuffers* lb;
TaskHandle_t task = xTaskGetCurrentTaskHandle();
PartialLogs::iterator it = m_partials.find(task);
if (it == m_partials.end())
{
lb = new LogBuffers();
m_partials[task] = lb;
}
else
{
lb = it->second;
}
va_list args;
va_start(args, fmt);
int ret = LogBuffer(lb, fmt, args);
va_end(args);
return ret;
}
int OvmsCommandApp::LogBuffer(LogBuffers* lb, const char* fmt, va_list args)
{
char *buffer;
int ret = vasprintf(&buffer, fmt, args);
if (ret < 0) return ret;
// Replace CR/LF except last by "|", but don't leave '|' at the end.
// An ESC sequence to change color may be appended after the log text.
char* s;
for (s=buffer; *s; s++)
{
if (*s=='\r' || *s=='\n')
{
char *t = s;
if (*(s+1) == '\033')
++s;
else if (*(s+1) != '\0')
{
*s = '|';
continue;
}
while (t > buffer && *(t-1) == '|')
--t;
while ((*t++ = *s++)) ;
break;
}
}
lb->append(buffer);
return ret;
}
int OvmsCommandApp::HexDump(const char* tag, const char* prefix, const char* data, size_t length, size_t colsize /*=16*/)
{
char* buffer = NULL;
int rlength = (int)length;
while (rlength>0)
{
rlength = FormatHexDump(&buffer, data, rlength, colsize);
data += colsize;
ESP_LOGV(tag, "%s: %s", prefix, buffer);
}
if (buffer)
free(buffer);
return length;
}
char ** OvmsCommandApp::Complete(OvmsWriter* writer, int argc, const char * const * argv)
{
return m_root.Complete(writer, argc, argv);
}
void OvmsCommandApp::Execute(int verbosity, OvmsWriter* writer, int argc, const char * const * argv)
{
if (argc == 0)
{
writer->puts("Error: Empty command unrecognised");
}
else
{
writer->SetArgv(argv);
m_root.Execute(verbosity, writer, argc, argv);
}
}
/**
* LogTask: file logging task
*/
struct LogTaskCmd
{
enum
{
LTC_Log, // write data.logbuffers to file
LTC_Exit, // close file, give data.cmdack, exit
} type;
union
{
LogBuffers* logbuffers;
OvmsSemaphore* cmdack;
} data;
};
static void LogTaskEntry(void* me)
{
((OvmsCommandApp*)me)->LogTask();
}
void OvmsCommandApp::LogTask()
{
LogTaskCmd cmd;
char tb[64];
m_logtask_linecnt = 0;
m_logtask_fsynctime = 0;
m_logtask_laststamp = -11;
m_logtask_basetime.tv_sec = 0;
m_logtask_basetime.tv_usec = 0;
// syncperiod: 0 = never, <0 = every n lines, >0 = after n/2 seconds idle
uint32_t linecnt_synced = 0;
int syncperiod = MyConfig.GetParamValueInt("log", "file.syncperiod", 3);
TickType_t timeout = (syncperiod<=0) ? portMAX_DELAY : pdMS_TO_TICKS(syncperiod*500);
for (;;)
{
if (xQueueReceive(m_logtask_queue, (void*)&cmd, timeout) == pdTRUE)
{
// cmd received:
if (cmd.type == LogTaskCmd::LTC_Log)
{
// write logbuffers messages:
for (auto it = cmd.data.logbuffers->begin(); it != cmd.data.logbuffers->end(); it++)
{
std::string le = stripesc(*it);
if (*(le.data() + 1) == ' ' && *(le.data() + 2) == '(')
{
struct timeval stamp;
stamp.tv_sec = atoi(le.data() + 3);
stamp.tv_usec = (stamp.tv_sec % 1000) * 1000;
stamp.tv_sec /= 1000;
// If 10 seconds have elapsed since the previous log message or if a
// real base time hasn't been set yet, recalculate the correspondence
// of real time to system time.
if (stamp.tv_sec - m_logtask_laststamp > 10 || m_logtask_basetime.tv_sec < 1609459200)
{
struct timeval daytime, uptime;
gettimeofday(&daytime, NULL);
uptime.tv_sec = xTaskGetTickCount();
uptime.tv_usec = (uptime.tv_sec % 100) * 10000;
uptime.tv_sec /= 100;
daytime.tv_usec -= daytime.tv_usec % 10000; // Always show 0 for ms units
timersub(&daytime, &uptime, &m_logtask_basetime);
}
m_logtask_laststamp = stamp.tv_sec;
// write timestamp:
timeradd(&m_logtask_basetime, &stamp, &stamp);
struct tm* tmu = localtime(&stamp.tv_sec);
strftime(tb, sizeof(tb), "%Y-%m-%d %H:%M:%S", tmu);
m_logfile_size += fwrite(tb, 1, strlen(tb), m_logfile);
snprintf(tb, sizeof(tb), ".%03lu ", stamp.tv_usec / 1000);
int len = strlen(tb);
strftime(tb+len, sizeof(tb)-len, "%Z ", tmu);
m_logfile_size += fwrite(tb, 1, strlen(tb), m_logfile);
}
// write log entry:
m_logfile_size += fwrite(le.data(), 1, le.size(), m_logfile);
m_logtask_linecnt++;
}
cmd.data.logbuffers->release();
// check file size:
if (m_logfile_maxsize && m_logfile_size > (m_logfile_maxsize*1024))
{
if (!CycleLogfile())
break;
}
else if (syncperiod < 0 && m_logtask_linecnt >= linecnt_synced - syncperiod)
{
linecnt_synced = m_logtask_linecnt;
uint32_t t0 = esp_timer_get_time();
fflush(m_logfile);
fsync(fileno(m_logfile));
m_logtask_fsynctime += esp_timer_get_time() - t0;
}
// check file status:
if (ferror(m_logfile))
{
ESP_LOGE(TAG, "LogTask: writing to file failed, terminating");
break;
}
}
else if (cmd.type == LogTaskCmd::LTC_Exit)
{
break;
}
}
else
{
// cmd timeout: anything to sync?
if (m_logtask_linecnt != linecnt_synced)
{
linecnt_synced = m_logtask_linecnt;
uint32_t t0 = esp_timer_get_time();
fflush(m_logfile);
fsync(fileno(m_logfile));
m_logtask_fsynctime += esp_timer_get_time() - t0;
}
}
}
// cleanup & terminate:
if (m_logfile)
fclose(m_logfile);
LogTaskCmd drop;
while (xQueueReceive(m_logtask_queue, (void*)&drop, 0) == pdTRUE)
{
if (drop.type == LogTaskCmd::LTC_Log)
{
drop.data.logbuffers->release();
}
else if (drop.type == LogTaskCmd::LTC_Exit)
{
if (drop.data.cmdack)
drop.data.cmdack->Give();
}
}
vQueueDelete(m_logtask_queue);
m_logfile = NULL;
m_logtask_queue = NULL;
m_logtask = NULL;
if (cmd.type == LogTaskCmd::LTC_Exit && cmd.data.cmdack)
cmd.data.cmdack->Give();
vTaskDelete(NULL);
}
bool OvmsCommandApp::StartLogTask(FILE* file)
{
OvmsMutexLock lock(&m_logtask_mutex);
m_logfile = file;
if (m_logtask)
return true;
// create queue:
m_logtask_dropcnt = 0;
m_logtask_queue = xQueueCreate(CONFIG_OVMS_LOGFILE_QUEUE_SIZE, sizeof(LogTaskCmd));
if (!m_logtask_queue)
{
ESP_LOGE(TAG, "StartLogTask: unable to create queue (out of memory)");
return false;
}
// create task:
BaseType_t res = xTaskCreatePinnedToCore(LogTaskEntry, "OVMS FileLog", 3*1024, (void*)this,
CONFIG_OVMS_LOGFILE_TASK_PRIORITY, &m_logtask, CORE(1));
if (res != pdPASS)
{
ESP_LOGE(TAG, "StartLogTask: unable to create task, error code=%d", res);
vQueueDelete(m_logtask_queue);
m_logtask_queue = NULL;
return false;
}
// register as logging console:
SetMonitoring(true);
MyCommandApp.RegisterConsole(this);
return true;
}
bool OvmsCommandApp::StopLogTask()
{
OvmsMutexLock lock(&m_logtask_mutex);
if (!m_logtask)
return true;
// detach from logging:
SetMonitoring(false);
MyCommandApp.DeregisterConsole(this);
// send exit command to task:
OvmsSemaphore ack;
LogTaskCmd cmd;
cmd.type = LogTaskCmd::LTC_Exit;
cmd.data.cmdack = &ack;
if (xQueueSend(m_logtask_queue, &cmd, (portTickType)portMAX_DELAY) != pdTRUE)
{
ESP_LOGE(TAG, "StopLogTask: unable to send command to task");
return false;
}
// …and wait for it to finish:
ack.Take();
return true;
}
bool OvmsCommandApp::CloseLogfile()
{
if (!m_logfile)
return true;
if (!StopLogTask())
return false;
ESP_LOGI(TAG, "CloseLogfile: file logging stopped");
return true;
}
bool OvmsCommandApp::OpenLogfile()
{
if (m_logfile && !CloseLogfile())
return false;
if (m_logfile_path.empty())
return true;
#ifdef CONFIG_OVMS_COMP_SDCARD
if (startsWith(m_logfile_path, "/sd") &&
(!MyPeripherals || !MyPeripherals->m_sdcard || !MyPeripherals->m_sdcard->isavailable()))
{
ESP_LOGW(TAG, "OpenLogfile: cannot open '%s', will retry on SD mount", m_logfile_path.c_str());
return false;
}
#endif // #ifdef CONFIG_OVMS_COMP_SDCARD
// get current file size:
struct stat st;
if (stat(m_logfile_path.c_str(), &st) == 0)
m_logfile_size = st.st_size;
else
m_logfile_size = 0;
// open file, start task:
FILE* file = fopen(m_logfile_path.c_str(), "a+");
if (file == NULL)
{
ESP_LOGE(TAG, "OpenLogfile: cannot open '%s'", m_logfile_path.c_str());
return false;
}
if (!StartLogTask(file))
{
ESP_LOGE(TAG, "OpenLogfile: cannot start log task on '%s'", m_logfile_path.c_str());
return false;
}
ESP_LOGI(TAG, "OpenLogfile: now logging to file '%s'", m_logfile_path.c_str());
return true;
}
bool OvmsCommandApp::SetLogfile(std::string path)
{
if (path.empty())
{
// close:
if (m_logfile && !CloseLogfile())
{
ESP_LOGE(TAG, "SetLogfile: error closing '%s'", m_logfile_path.c_str());
return false;
}
m_logfile_path = "";
}
else
{
// check new path:
if (MyConfig.ProtectedPath(path))
{
ESP_LOGE(TAG, "SetLogfile: '%s' is a protected path", path.c_str());
return false;
}
// open:
m_logfile_path = path;
if (!OpenLogfile())
return false;
}
return true;
}
bool OvmsCommandApp::CycleLogfile()
{
if (!m_logfile || m_logfile_path.empty())
return false;
fclose(m_logfile);
m_logfile = NULL;
char ts[20];
time_t tm = time(NULL);
strftime(ts, sizeof(ts), ".%Y%m%d-%H%M%S", localtime(&tm));
std::string archpath = m_logfile_path;
archpath.append(ts);
if (rename(m_logfile_path.c_str(), archpath.c_str()) == 0)
{
ESP_LOGI(TAG, "CycleLogfile: log file '%s' archived as '%s'", m_logfile_path.c_str(), archpath.c_str());
m_logfile_cyclecnt++;
}
else
{
ESP_LOGE(TAG, "CycleLogfile: rename log file '%s' to '%s' failed", m_logfile_path.c_str(), archpath.c_str());
}
return OpenLogfile();
}
void OvmsCommandApp::Log(LogBuffers* msg)
{
if (!m_logtask || !m_logtask_queue)
{
msg->release();
return;
}
// send to LogTask:
LogTaskCmd cmd;
cmd.type = LogTaskCmd::LTC_Log;
cmd.data.logbuffers = msg;
if (xQueueSend(m_logtask_queue, &cmd, 0) != pdTRUE)
{
m_logtask_dropcnt++;
msg->release();
}
}
void OvmsCommandApp::SetLoglevel(std::string tag, std::string level)
{
int level_num;
if (level == "verbose")
level_num = 5;
else if (level == "debug")
level_num = 4;
else if (level == "info")
level_num = 3;
else if (level == "warn")
level_num = 2;
else if (level == "error")
level_num = 1;
else if (level == "none")
level_num = 0;
else
level_num = CONFIG_LOG_DEFAULT_LEVEL;
if (tag.empty())
esp_log_level_set("*", (esp_log_level_t)level_num);
else
esp_log_level_set(tag.c_str(), (esp_log_level_t)level_num);
}
void OvmsCommandApp::ExpireLogFiles(int verbosity, OvmsWriter* writer, int keepdays)
{
if (keepdays <= 0)
{
if (writer)
writer->printf("Abort: expire disabled (keepdays=%d)\n", keepdays);
else
ESP_LOGD(TAG, "ExpireLogFiles: disabled (keepdays=%d)", keepdays);
return;
}
// get archive directory:
std::string archdir = m_logfile_path;
std::string::size_type p = archdir.find_last_of('/');
if (p == std::string::npos)
{
if (writer)
writer->puts("Error: log path not set");
else
ESP_LOGE(TAG, "ExpireLogFiles: log path not set");
return;
}
archdir.resize(p);
DIR *dir = opendir(archdir.c_str());
if (!dir)
{
if (writer)
writer->printf("Error: cannot open log directory '%s'\n", archdir.c_str());
else
ESP_LOGE(TAG, "ExpireLogFiles: cannot open log directory '%s'", archdir.c_str());
return;
}
else if (writer && verbosity >= COMMAND_RESULT_NORMAL)
{
writer->printf("Scanning directory '%s'...\n", archdir.c_str());
}
time_t tm = time(NULL) - keepdays * 86400;
struct dirent *dp;
char path[PATH_MAX];
struct stat st;
int delcnt = 0;
while ((dp = readdir(dir)) != NULL)
{
snprintf(path, sizeof(path), "%s/%s", archdir.c_str(), dp->d_name);
if (strncmp(path, m_logfile_path.c_str(), m_logfile_path.size()) != 0)
continue;
if (stat(path, &st))
{
if (writer)
writer->printf("Error: cannot stat '%s'\n", path);
else
ESP_LOGE(TAG, "ExpireLogFiles: cannot stat '%s'", path);
continue;
}
if (st.st_mtime < tm)
{
if (writer && verbosity >= COMMAND_RESULT_NORMAL)
writer->printf("Deleting '%s'\n", path);
else
ESP_LOGD(TAG, "ExpireLogFiles: deleting '%s'", path);
if (unlink(path))
{
if (writer)
writer->printf("Error: cannot delete '%s'\n", path);
else
ESP_LOGE(TAG, "ExpireLogFiles: cannot delete '%s'", path);
}
else
delcnt++;
}
}
closedir(dir);
if (writer)
writer->printf("Done, %d file(s) deleted.\n", delcnt);
else
ESP_LOGI(TAG, "ExpireLogFiles: %d file(s) deleted", delcnt);
}
void OvmsCommandApp::ExpireTask(void* data)
{
int keepdays = MyConfig.GetParamValueInt("log", "file.keepdays", 30);
MyCommandApp.ExpireLogFiles(0, NULL, keepdays);
MyCommandApp.m_expiretask = 0;
vTaskDelete(NULL);
}
void OvmsCommandApp::ShowLogStatus(int verbosity, OvmsWriter* writer)
{
writer->printf(
"Log listeners : %u\n"
"File logging status: %s\n"
" Log file path : %s\n"
" Current size : %.1f kB\n"
" Cycle size : %u kB\n"
" Cycle count : %u\n"
" Dropped messages : %u\n"
" Messages logged : %u\n"
" Total fsync time : %.1f s\n"
, m_consoles.size()
, m_logfile ? "active" : "inactive"
, m_logfile_path.empty() ? "-" : m_logfile_path.c_str()
, (float) m_logfile_size / 1024.0f
, m_logfile_maxsize
, m_logfile_cyclecnt
, m_logtask_dropcnt
, m_logtask_linecnt
, m_logtask_fsynctime / 1e6);
}
void OvmsCommandApp::EventHandler(std::string event, void* data)
{
if (event == "config.changed")
{
OvmsConfigParam* param = (OvmsConfigParam*) data;
if (param && param->GetName() == "log")
ReadConfig();
}
else if (event == "sd.mounted")
{
if (startsWith(m_logfile_path, "/sd"))
OpenLogfile();
}
else if (event == "sd.unmounting")
{
if (startsWith(m_logfile_path, "/sd"))
CloseLogfile();
}
else if (event == "ticker.3600")
{
int keepdays = MyConfig.GetParamValueInt("log", "file.keepdays", 30);
time_t utm = time(NULL);
struct tm* ltm = localtime(&utm);
if (keepdays && ltm->tm_hour == 0 && !m_expiretask)
xTaskCreatePinnedToCore(ExpireTask, "OVMS ExpireLogs", 4096, NULL, 0, &m_expiretask, CORE(1));
}
}
void OvmsCommandApp::ReadConfig()
{
OvmsConfigParam* param = MyConfig.CachedParam("log");
// configure log levels:
std::string level = MyConfig.GetParamValue("log", "level");
if (!level.empty())
SetLoglevel("*", level);
for (auto const& kv : param->m_map)
{
if (startsWith(kv.first, "level.") && !kv.second.empty())
SetLoglevel(kv.first.substr(6), kv.second);
}
// configure log file:
m_logfile_maxsize = MyConfig.GetParamValueInt("log", "file.maxsize", 1024);
if (MyConfig.GetParamValueBool("log", "file.enable", false) == true)
SetLogfile(MyConfig.GetParamValue("log", "file.path"));
}
void OvmsCommandApp::Display(OvmsWriter* writer)
{
m_root.Display(writer, -1);
}
OvmsCommandTask::OvmsCommandTask(int _verbosity, OvmsWriter* _writer, OvmsCommand* _cmd, int _argc, const char* const* _argv)
: TaskBase(_cmd->GetName(), CONFIG_OVMS_SYS_COMMAND_STACK_SIZE, CONFIG_OVMS_SYS_COMMAND_PRIORITY)
{
m_state = OCS_Init;
// clone command arguments:
verbosity = _verbosity;
writer = _writer;
cmd = _cmd;
argc = _argc;
if (argc == 0)
argv = NULL;
else
{
argv = (char**) ExternalRamMalloc(argc * sizeof(char*));
for (int i=0; i < argc; i++)
argv[i] = strdup(_argv[i]);
}
}
OvmsCommandState_t OvmsCommandTask::Prepare()
{
return (writer->IsInteractive()) ? OCS_RunLoop : OCS_RunOnce;
}
bool OvmsCommandTask::Run()
{
m_state = Prepare();
switch (m_state)
{
case OCS_RunLoop:
// start task:
writer->RegisterInsertCallback(Terminator, (void*) this);
if (!Instantiate())
{
delete this;
return false;
}
return true;
break;
case OCS_RunOnce:
Service();
Cleanup();
delete this;
return true;
break;
default:
// preparation failed:
delete this;
return false;
break;
}
}
OvmsCommandTask::~OvmsCommandTask()
{
if (m_state == OCS_StopRequested)
writer->puts("^C");
writer->DeregisterInsertCallback(Terminator);
if (argv)
{
for (int i=0; i < argc; i++)
free(argv[i]);
free(argv);
}
}
bool OvmsCommandTask::Terminator(OvmsWriter* writer, void* userdata, char ch)
{
if (ch == 3) // Ctrl-C
((OvmsCommandTask*) userdata)->m_state = OCS_StopRequested;
return true;
}