Sync upstream

This commit is contained in:
Carsten Schmiemann 2023-01-24 20:50:05 +01:00
parent 9eb77d6aef
commit 1b7124c2c7
40 changed files with 1744 additions and 339 deletions

View File

@ -213,7 +213,7 @@ void can_tx(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const
uint32_t uv = strtoul(argv[0], &ep, 16); uint32_t uv = strtoul(argv[0], &ep, 16);
if (*ep != '\0' || uv > idmax) if (*ep != '\0' || uv > idmax)
{ {
writer->printf("Error: Invalid CAN ID \"%s\" (0x%lx max)\n", argv[0], idmax); writer->printf("Error: Invalid CAN ID \"%s\" (0x%x max)\n", argv[0], idmax);
return; return;
} }
frame.MsgID = uv; frame.MsgID = uv;
@ -298,7 +298,7 @@ void can_testtx(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, c
uint32_t uv = strtoul(argv[0], &ep, 16); uint32_t uv = strtoul(argv[0], &ep, 16);
if (*ep != '\0' || uv > idmax) if (*ep != '\0' || uv > idmax)
{ {
writer->printf("Error: Invalid CAN ID \"%s\" (0x%lx max)\n", argv[0], idmax); writer->printf("Error: Invalid CAN ID \"%s\" (0x%x max)\n", argv[0], idmax);
return; return;
} }
frame.MsgID = uv; frame.MsgID = uv;
@ -570,7 +570,8 @@ static const char* const CAN_log_type_names[] = {
"Status", "Status",
"Comment", "Comment",
"Info", "Info",
"Event" "Event",
"Metric"
}; };
const char* GetCanLogTypeName(CAN_log_type_t type) const char* GetCanLogTypeName(CAN_log_type_t type)

View File

@ -251,6 +251,7 @@ typedef enum
CAN_LogInfo_Comment, // general comment CAN_LogInfo_Comment, // general comment
CAN_LogInfo_Config, // logger setup info (type, file, filters, vehicle) CAN_LogInfo_Config, // logger setup info (type, file, filters, vehicle)
CAN_LogInfo_Event, // system event (i.e. vehicle started) CAN_LogInfo_Event, // system event (i.e. vehicle started)
CAN_LogInfo_Metric, // system or plugin metric (i.e. v.p.altitude)
} CAN_log_type_t; } CAN_log_type_t;
// Log message: // Log message:

View File

@ -123,10 +123,11 @@ std::string canformat_crtd::get(CAN_log_message_t* message)
case CAN_LogInfo_Comment: case CAN_LogInfo_Comment:
case CAN_LogInfo_Config: case CAN_LogInfo_Config:
case CAN_LogInfo_Event: case CAN_LogInfo_Event:
case CAN_LogInfo_Metric:
snprintf(buf,sizeof(buf),"%ld.%06ld %c%s %s %s", snprintf(buf,sizeof(buf),"%ld.%06ld %c%s %s %s",
message->timestamp.tv_sec, message->timestamp.tv_usec, message->timestamp.tv_sec, message->timestamp.tv_usec,
busnumber, busnumber,
(message->type == CAN_LogInfo_Event) ? "CEV" : "CXX", (message->type == CAN_LogInfo_Event) ? "CEV" : (message->type == CAN_LogInfo_Metric) ? "CMT" : "CXX",
GetCanLogTypeName(message->type), GetCanLogTypeName(message->type),
message->text); message->text);
break; break;
@ -151,7 +152,7 @@ std::string canformat_crtd::getheader(struct timeval *time)
time = &t; time = &t;
} }
snprintf(buf,sizeof(buf),"%ld.%06ld CXX OVMS CRTD\n%ld.%06ld CVR 3.0\n", snprintf(buf,sizeof(buf),"%ld.%06ld CXX OVMS CRTD\n%ld.%06ld CVR 3.1\n",
time->tv_sec, time->tv_usec, time->tv_sec, time->tv_usec,
time->tv_sec, time->tv_usec); time->tv_sec, time->tv_usec);

View File

@ -42,6 +42,8 @@ static const char *TAG = "canlog";
#include "ovms_peripherals.h" #include "ovms_peripherals.h"
#include "metrics_standard.h" #include "metrics_standard.h"
static const char *CAN_PARAM = "can";
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
// Command Processing // Command Processing
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
@ -352,8 +354,12 @@ canlog::canlog(const char* type, std::string format, canformat::canformat_serve_
using std::placeholders::_1; using std::placeholders::_1;
using std::placeholders::_2; using std::placeholders::_2;
MyEvents.RegisterEvent(IDTAG, "*", std::bind(&canlog::EventListener, this, _1, _2)); MyEvents.RegisterEvent(IDTAG, "*", std::bind(&canlog::EventListener, this, _1, _2));
MyEvents.RegisterEvent(IDTAG,"config.mounted", std::bind(&canlog::UpdatedConfig, this, _1, _2));
MyEvents.RegisterEvent(IDTAG,"config.changed", std::bind(&canlog::UpdatedConfig, this, _1, _2));
MyMetrics.RegisterListener(IDTAG, "*", std::bind(&canlog::MetricListener, this, _1));
int queuesize = MyConfig.GetParamValueInt("can", "log.queuesize",100); int queuesize = MyConfig.GetParamValueInt(CAN_PARAM, "log.queuesize",100);
LoadConfig();
m_queue = xQueueCreate(queuesize, sizeof(CAN_log_message_t)); m_queue = xQueueCreate(queuesize, sizeof(CAN_log_message_t));
xTaskCreatePinnedToCore(RxTask, "OVMS CanLog", 4096, (void*)this, 10, &m_task, CORE(1)); xTaskCreatePinnedToCore(RxTask, "OVMS CanLog", 4096, (void*)this, 10, &m_task, CORE(1));
} }
@ -361,6 +367,7 @@ canlog::canlog(const char* type, std::string format, canformat::canformat_serve_
canlog::~canlog() canlog::~canlog()
{ {
MyEvents.DeregisterEvent(IDTAG); MyEvents.DeregisterEvent(IDTAG);
MyMetrics.DeregisterListener(IDTAG);
if (m_task) if (m_task)
{ {
@ -383,6 +390,7 @@ canlog::~canlog()
case CAN_LogInfo_Comment: case CAN_LogInfo_Comment:
case CAN_LogInfo_Config: case CAN_LogInfo_Config:
case CAN_LogInfo_Event: case CAN_LogInfo_Event:
case CAN_LogInfo_Metric:
free(msg.text); free(msg.text);
break; break;
default: default:
@ -418,6 +426,7 @@ void canlog::RxTask(void *context)
case CAN_LogInfo_Comment: case CAN_LogInfo_Comment:
case CAN_LogInfo_Config: case CAN_LogInfo_Config:
case CAN_LogInfo_Event: case CAN_LogInfo_Event:
case CAN_LogInfo_Metric:
me->OutputMsg(msg); me->OutputMsg(msg);
free(msg.text); free(msg.text);
break; break;
@ -429,13 +438,179 @@ void canlog::RxTask(void *context)
} }
} }
/**
* Parse a comma-separated list of filters, and assign them to a member of the class.
* We have 3 kind of comparisons, and an unlimited list of filters.
*
* A filter can be:
* - A "startsWith" comparison - when ending with '*',
* - An "endsWith" comparison - when starting with '*',
* - Invalid (and skipped) if empty, or with a '*' in any other position than beginning or end,
* - A "string equal" comparison for all other cases
*/
static void LoadFilters(conn_filters_arr_t &member, const std::string &value)
{
// Empty all previously defined filters, for all operators
for (int i=0; i<COUNT_OF_OPERATORS; i++)
{
member[i].clear();
}
if (!value.empty())
{
std::stringstream stream (value);
std::string item;
unsigned char comparison_operator;
// Comma-separated list
while (getline (stream, item, ','))
{
trim(item); // Removing leading and trailing spaces
if (item.empty())
{
ESP_LOGW(TAG, "LoadFilters: skipping empty value in the filter list");
continue;
}
// Check if there is a wildcard ('*') in any other place than first or last position
size_t wildcard_position = item.find('*', 1);
if ((wildcard_position != std::string::npos) && (wildcard_position != item.size()-1))
{
ESP_LOGW(TAG, "LoadFilters: skipping incorrect value (%s) in the filter list (wildcard in wrong position)", item.c_str());
continue;
}
// Depending on the presence and position of the wildcard, push the filter
// in the proper vector (without the wildcard)
if (item.front() == '*')
{
comparison_operator = OPERATOR_ENDSWITH;
item.erase(0, 1);
}
else if (item.back() == '*')
{
comparison_operator = OPERATOR_STARTSWITH;
item.pop_back();
}
else
{
comparison_operator = OPERATOR_EQUALS;
}
member[comparison_operator].push_back(item);
}
}
// for (int i=0; i<COUNT_OF_OPERATORS; i++)
// {
// ESP_LOGI(TAG, "LoadFilters: filters for operator %d:", i);
// for (std::vector<std::string>::iterator it=member[i].begin(); it!=member[i].end(); ++it)
// {
// ESP_LOGI(TAG, "LoadFilters: filter value '%s'", it->c_str());
// }
// if (member[i].begin() == member[i].end())
// {
// ESP_LOGI(TAG, "LoadFilters: (empty filter list)");
// }
// }
}
/**
* Load, or reload, the configuration of events and metrics filters.
*
* The configuration item is a string containing a comma-separated list of filters.
*/
void canlog::LoadConfig()
{
std::size_t str_hash;
std::string list_of_events_filters = MyConfig.GetParamValue(CAN_PARAM, "log.events_filters", "x*,vehicle*");
str_hash = std::hash<std::string>{}(list_of_events_filters);
if (str_hash != m_events_filters_hash)
{
m_events_filters_hash = str_hash;
LoadFilters(m_events_filters, list_of_events_filters);
MyCan.LogInfo(NULL, CAN_LogInfo_Config, ("Events filters: " + list_of_events_filters).c_str());
}
std::string list_of_metrics_filters = MyConfig.GetParamValue(CAN_PARAM, "log.metrics_filters");
str_hash = std::hash<std::string>{}(list_of_metrics_filters);
if (str_hash != m_metrics_filters_hash)
{
m_metrics_filters_hash = str_hash;
LoadFilters(m_metrics_filters, list_of_metrics_filters);
MyCan.LogInfo(NULL, CAN_LogInfo_Config, ("Metrics filters: " + list_of_metrics_filters).c_str());
}
}
/**
* Load, or reload, the configuration if a config event occurred.
*/
void canlog::UpdatedConfig(std::string event, void* data)
{
if (event == "config.changed")
{
// Only reload if our parameter has changed
OvmsConfigParam*p = (OvmsConfigParam*)data;
if (p->GetName() != CAN_PARAM)
{
return;
}
}
LoadConfig();
}
/**
* Check if a value matches in a list of filters.
*
* Match can be a:
* - startsWith match,
* - endsWith match,
* - equality match.
*/
static bool CheckFilter(conn_filters_arr_t &member, const std::string &value)
{
for (int i=0; i<COUNT_OF_OPERATORS; i++)
{
for (std::vector<std::string>::iterator it=member[i].begin(); it!=member[i].end(); ++it)
{
if ((i == OPERATOR_STARTSWITH) && (startsWith(value, *it)))
{
return true;
}
else if ((i == OPERATOR_ENDSWITH) && (endsWith(value, *it)))
{
return true;
}
else if ((i == OPERATOR_EQUALS) && (value == *it))
{
return true;
}
}
}
return false;
}
void canlog::EventListener(std::string event, void* data) void canlog::EventListener(std::string event, void* data)
{ {
// Log vehicle custom (x…) & framework events: // Log vehicle custom (x…) & framework events:
if (startsWith(event, 'x') || startsWith(event, "vehicle")) if (CheckFilter(m_events_filters, event))
LogInfo(NULL, CAN_LogInfo_Event, event.c_str()); LogInfo(NULL, CAN_LogInfo_Event, event.c_str());
} }
void canlog::MetricListener(OvmsMetric* metric)
{
std::string name = metric->m_name;
// Log metrics (in JSON for later parsing):
if (CheckFilter(m_metrics_filters, name))
{
std::string metric_text = "{ ";
metric_text += "\"name\": \"" + json_encode(name) + "\", ";
metric_text += "\"value\": " + metric->AsJSON() + ", ";
metric_text += "\"unit\": \"" + json_encode(std::string(OvmsMetricUnitLabel(metric->GetUnits()))) + "\" }";
LogInfo(NULL, CAN_LogInfo_Metric, metric_text.c_str());
}
}
const char* canlog::GetType() const char* canlog::GetType()
{ {
return m_type; return m_type;

View File

@ -95,6 +95,11 @@ class canlogconnection: public InternalRamAllocated
uint32_t m_filtercount; uint32_t m_filtercount;
}; };
#define OPERATOR_STARTSWITH 0
#define OPERATOR_ENDSWITH 1
#define OPERATOR_EQUALS 2
#define COUNT_OF_OPERATORS 3
typedef std::array<std::vector<std::string>, COUNT_OF_OPERATORS> conn_filters_arr_t;
class canlog : public InternalRamAllocated class canlog : public InternalRamAllocated
{ {
@ -105,6 +110,7 @@ class canlog : public InternalRamAllocated
public: public:
static void RxTask(void* context); static void RxTask(void* context);
void EventListener(std::string event, void* data); void EventListener(std::string event, void* data);
void MetricListener(OvmsMetric* metric);
public: public:
const char* GetType(); const char* GetType();
@ -152,6 +158,16 @@ class canlog : public InternalRamAllocated
uint32_t m_msgcount; uint32_t m_msgcount;
uint32_t m_dropcount; uint32_t m_dropcount;
uint32_t m_filtercount; uint32_t m_filtercount;
protected:
virtual void UpdatedConfig(std::string event, void* data);
virtual void LoadConfig();
protected:
conn_filters_arr_t m_events_filters;
size_t m_events_filters_hash = 0;
conn_filters_arr_t m_metrics_filters;
size_t m_metrics_filters_hash = 0;
}; };
#endif // __CANLOG_H__ #endif // __CANLOG_H__

View File

@ -109,6 +109,7 @@ void canlog_monitor_conn::OutputMsg(CAN_log_message_t& msg, std::string &result)
case CAN_LogInfo_Comment: case CAN_LogInfo_Comment:
case CAN_LogInfo_Config: case CAN_LogInfo_Config:
case CAN_LogInfo_Event: case CAN_LogInfo_Event:
case CAN_LogInfo_Metric:
ESP_LOGD(TAG,"%s",result.c_str()); ESP_LOGD(TAG,"%s",result.c_str());
break; break;
default: default:

View File

@ -89,7 +89,7 @@ OvmsCanLogVFSInit::OvmsCanLogVFSInit()
canlog_vfs_conn::canlog_vfs_conn(canlog* logger, std::string format, canformat::canformat_serve_mode_t mode) canlog_vfs_conn::canlog_vfs_conn(canlog* logger, std::string format, canformat::canformat_serve_mode_t mode)
: canlogconnection(logger, format, mode) : canlogconnection(logger, format, mode), m_file_size(0)
{ {
m_file = NULL; m_file = NULL;
} }
@ -114,7 +114,10 @@ void canlog_vfs_conn::OutputMsg(CAN_log_message_t& msg, std::string &result)
} }
if (result.length()>0) if (result.length()>0)
{
fwrite(result.c_str(),result.length(),1,m_file); fwrite(result.c_str(),result.length(),1,m_file);
m_file_size += result.length();
}
} }
@ -181,7 +184,10 @@ bool canlog_vfs::Open()
std::string header = m_formatter->getheader(); std::string header = m_formatter->getheader();
if (header.length()>0) if (header.length()>0)
{
fwrite(header.c_str(),header.length(),1,clc->m_file); fwrite(header.c_str(),header.length(),1,clc->m_file);
clc->m_file_size += header.length();
}
m_connmap[NULL] = clc; m_connmap[NULL] = clc;
m_isopen = true; m_isopen = true;
@ -207,6 +213,47 @@ void canlog_vfs::Close()
} }
} }
size_t canlog_vfs::GetFileSize()
{
size_t result = 0;
if (m_isopen)
{
for (conn_map_t::iterator it=m_connmap.begin(); it!=m_connmap.end(); ++it)
{
canlog_vfs_conn * clc = static_cast<canlog_vfs_conn *>(it->second);
result += clc->m_file_size;
}
}
return result;
}
std::string canlog_vfs_conn::GetStats()
{
char bufsize[15];
format_file_size(bufsize, sizeof(bufsize), m_file_size);
std::string result = "Size:";
result.append(bufsize);
result.append(" ");
result.append(canlogconnection::GetStats());
return result;
}
std::string canlog_vfs::GetStats()
{
char bufsize[15];
size_t size = GetFileSize();
format_file_size(bufsize, sizeof(bufsize), size);
std::string result = "Size:";
result.append(bufsize);
result.append(" ");
result.append(canlog::GetStats());
return result;
}
std::string canlog_vfs::GetInfo() std::string canlog_vfs::GetInfo()
{ {
std::string result = canlog::GetInfo(); std::string result = canlog::GetInfo();

View File

@ -39,9 +39,11 @@ class canlog_vfs_conn: public canlogconnection
public: public:
virtual void OutputMsg(CAN_log_message_t& msg, std::string &result); virtual void OutputMsg(CAN_log_message_t& msg, std::string &result);
virtual std::string GetStats();
public: public:
FILE* m_file; FILE* m_file;
size_t m_file_size;
}; };
@ -55,9 +57,11 @@ class canlog_vfs : public canlog
virtual bool Open(); virtual bool Open();
virtual void Close(); virtual void Close();
virtual std::string GetInfo(); virtual std::string GetInfo();
virtual size_t GetFileSize();
public: public:
virtual void MountListener(std::string event, void* data); virtual void MountListener(std::string event, void* data);
virtual std::string GetStats();
public: public:
std::string m_path; std::string m_path;

View File

@ -75,7 +75,7 @@ class ConsoleSSH : public OvmsConsole
void Sent(); void Sent();
void Exit(); void Exit();
int puts(const char* s); int puts(const char* s);
int printf(const char* fmt, ...); int printf(const char* fmt, ...) __attribute__ ((format (printf, 2, 3)));
ssize_t write(const void *buf, size_t nbyte); ssize_t write(const void *buf, size_t nbyte);
int RecvCallback(char* buf, uint32_t size); int RecvCallback(char* buf, uint32_t size);
bool IsDraining() { return m_drain > 0; } bool IsDraining() { return m_drain > 0; }

View File

@ -64,7 +64,7 @@ class ConsoleTelnet : public OvmsConsole
void Receive(); void Receive();
void Exit(); void Exit();
int puts(const char* s); int puts(const char* s);
int printf(const char* fmt, ...); int printf(const char* fmt, ...) __attribute__ ((format (printf, 2, 3)));
ssize_t write(const void *buf, size_t nbyte); ssize_t write(const void *buf, size_t nbyte);
protected: protected:

View File

@ -119,7 +119,7 @@ void modem::Task()
.use_ref_tick = 0, .use_ref_tick = 0,
}; };
uart_param_config(m_uartnum, &uart_config); uart_param_config(m_uartnum, &uart_config);
uart_set_pin(m_uartnum, m_txpin, m_rxpin, 0, 0); uart_set_pin(m_uartnum, m_txpin, m_rxpin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(m_uartnum, uart_driver_install(m_uartnum,
CONFIG_OVMS_HW_CELLULAR_MODEM_UART_SIZE, CONFIG_OVMS_HW_CELLULAR_MODEM_UART_SIZE,
CONFIG_OVMS_HW_CELLULAR_MODEM_UART_SIZE, CONFIG_OVMS_HW_CELLULAR_MODEM_UART_SIZE,
@ -245,11 +245,20 @@ void modem::Task()
} }
} }
} }
if (m_state1 == PoweredOff && MyBoot.IsShuttingDown())
{
m_task = 0;
}
} }
// Shutdown: // Shutdown:
uart_driver_delete(m_uartnum); ESP_LOGD(TAG, "UART shutdown");
m_queue = 0; m_queue = 0;
uart_wait_tx_done(m_uartnum, portMAX_DELAY);
uart_flush(m_uartnum);
uart_driver_delete(m_uartnum);
if (MyBoot.IsShuttingDown()) MyBoot.ShutdownReady(TAG);
vTaskDelete(NULL); vTaskDelete(NULL);
} }
@ -662,7 +671,6 @@ void modem::State1Enter(modem_state1_t newstate)
case PoweredOff: case PoweredOff:
ClearNetMetrics(); ClearNetMetrics();
MyEvents.SignalEvent("system.modem.poweredoff", NULL); MyEvents.SignalEvent("system.modem.poweredoff", NULL);
if (MyBoot.IsShuttingDown()) MyBoot.ShutdownReady(TAG);
StopMux(); StopMux();
if (m_driver) if (m_driver)
{ {
@ -1018,13 +1026,16 @@ void modem::StandardLineHandler(int channel, OvmsBuffer* buf, std::string line)
line = m_line_buffer; line = m_line_buffer;
} }
const char *cp = line.c_str(); if (line.compare(0, 2, "$G") == 0)
if ((line.length()>2)&&(cp[0]!='$')&&(cp[1])!='G')
{ {
// Log incoming data other than GPS NMEA // GPS NMEA URC:
ESP_LOGD(TAG, "mux-rx-line #%d: %s", channel, line.c_str()); if (m_nmea) m_nmea->IncomingLine(line);
return;
} }
// Log incoming data other than GPS NMEA
ESP_LOGD(TAG, "mux-rx-line #%d: %s", channel, line.c_str());
if ((line.compare(0, 8, "CONNECT ") == 0)&&(m_state1 == NetStart)&&(m_state1_userdata == 1)) if ((line.compare(0, 8, "CONNECT ") == 0)&&(m_state1 == NetStart)&&(m_state1_userdata == 1))
{ {
ESP_LOGI(TAG, "PPP Connection is ready to start"); ESP_LOGI(TAG, "PPP Connection is ready to start");
@ -1175,9 +1186,10 @@ void modem::StandardLineHandler(int channel, OvmsBuffer* buf, std::string line)
} }
else if (line.compare(0, 30, "+CME ERROR: incorrect password") == 0) else if (line.compare(0, 30, "+CME ERROR: incorrect password") == 0)
{ {
std::string pincode = MyConfig.GetParamValue("modem", "pincode");
ESP_LOGE(TAG,"Wrong PIN code entered!"); ESP_LOGE(TAG,"Wrong PIN code entered!");
MyEvents.SignalEvent("system.modem.wrongpingcode", NULL); MyEvents.SignalEvent("system.modem.wrongpingcode", NULL);
MyNotify.NotifyStringf("alert", "modem.wrongpincode", "Wrong pin code (%s) entered!", MyConfig.GetParamValue("modem", "pincode")); MyNotify.NotifyStringf("alert", "modem.wrongpincode", "Wrong pin code (%s) entered!", pincode.c_str());
MyConfig.SetParamValueBool("modem","wrongpincode",true); MyConfig.SetParamValueBool("modem","wrongpincode",true);
} }
else if (line.compare(0, 28, "+CME ERROR: SIM not inserted") == 0) else if (line.compare(0, 28, "+CME ERROR: SIM not inserted") == 0)
@ -1188,8 +1200,8 @@ void modem::StandardLineHandler(int channel, OvmsBuffer* buf, std::string line)
} }
// MMI/USSD response (URC): // MMI/USSD response (URC):
// sent on all free channels, so we only process m_mux_channel_CMD // sent on all free channels or only on POLL, so we only process m_mux_channel_POLL
else if (channel == m_mux_channel_CMD && line.compare(0, 7, "+CUSD: ") == 0) else if (channel == m_mux_channel_POLL && line.compare(0, 7, "+CUSD: ") == 0)
{ {
// Format: +CUSD: 0,"…msg…",15 // Format: +CUSD: 0,"…msg…",15
// The message string may contain CR/LF so can come on multiple lines, with unknown length // The message string may contain CR/LF so can come on multiple lines, with unknown length
@ -1333,7 +1345,7 @@ void modem::StartTask()
if (!m_task) if (!m_task)
{ {
ESP_LOGV(TAG, "Starting modem task"); ESP_LOGV(TAG, "Starting modem task");
xTaskCreatePinnedToCore(MODEM_task, "OVMS Cellular", CONFIG_OVMS_HW_CELLULAR_MODEM_STACK_SIZE, (void*)this, 20, &m_task, CORE(0)); xTaskCreatePinnedToCore(MODEM_task, "OVMS Cellular", CONFIG_OVMS_HW_CELLULAR_MODEM_STACK_SIZE, (void*)this, 20, (void**)&m_task, CORE(0));
} }
} }
@ -1351,27 +1363,42 @@ void modem::StopTask()
} }
} }
void modem::StartNMEA() bool modem::StartNMEA(bool force /*=false*/)
{ {
if ( (m_nmea == NULL) && if ( (m_nmea == NULL) &&
(MyConfig.GetParamValueBool("modem", "enable.gps", false)) ) (force || MyConfig.GetParamValueBool("modem", "enable.gps", false)) )
{ {
ESP_LOGV(TAG, "Starting NMEA"); if (!m_mux || !m_driver)
m_nmea = new GsmNMEA(m_mux, m_mux_channel_NMEA, m_mux_channel_CMD); {
m_nmea->Startup(); ESP_LOGE(TAG, "StartNMEA failed: MUX or driver not available");
m_driver->StartupNMEA(); }
else
{
ESP_LOGV(TAG, "Starting NMEA");
m_nmea = new GsmNMEA(m_mux, m_mux_channel_NMEA, m_mux_channel_CMD);
m_nmea->Startup();
m_driver->StartupNMEA();
}
} }
return (m_nmea != NULL);
} }
void modem::StopNMEA() void modem::StopNMEA()
{ {
if (m_nmea != NULL) if (m_nmea != NULL)
{ {
ESP_LOGV(TAG, "Stopping NMEA"); if (!m_mux || !m_driver)
m_driver->ShutdownNMEA(); {
m_nmea->Shutdown(); ESP_LOGE(TAG, "StopNMEA failed: MUX or driver not available");
delete m_nmea; }
m_nmea = NULL; else
{
ESP_LOGV(TAG, "Stopping NMEA");
m_driver->ShutdownNMEA();
m_nmea->Shutdown();
delete m_nmea;
m_nmea = NULL;
}
} }
} }
@ -1446,7 +1473,8 @@ void modem::Ticker(std::string event, void* data)
ev.event.type = TICKER1; ev.event.type = TICKER1;
xQueueSend(m_queue,&ev,0); QueueHandle_t queue = m_queue;
if (queue) xQueueSend(queue,&ev,0);
} }
void modem::EventListener(std::string event, void* data) void modem::EventListener(std::string event, void* data)
@ -1474,17 +1502,7 @@ void modem::IncomingMuxData(GsmMuxChannel* channel)
} }
else if (channel->m_channel == m_mux_channel_NMEA) else if (channel->m_channel == m_mux_channel_NMEA)
{ {
if (m_nmea != NULL) StandardIncomingHandler(channel->m_channel, &channel->m_buffer);
{
while (channel->m_buffer.HasLine() >= 0)
{
m_nmea->IncomingLine(channel->m_buffer.ReadLine());
}
}
else
{
channel->m_buffer.EmptyAll();
}
} }
else if (channel->m_channel == m_mux_channel_DATA) else if (channel->m_channel == m_mux_channel_DATA)
{ {
@ -1523,7 +1541,8 @@ void modem::SendSetState1(modem_state1_t newstate)
ev.event.type = SETSTATE; ev.event.type = SETSTATE;
ev.event.data.newstate = newstate; ev.event.data.newstate = newstate;
xQueueSend(m_queue,&ev,0); QueueHandle_t queue = m_queue;
if (queue) xQueueSend(queue,&ev,0);
} }
bool modem::IsStarted() bool modem::IsStarted()
@ -1722,6 +1741,53 @@ void modem_setstate(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int arg
writer->printf("Error: Unrecognised state %s\n",statename); writer->printf("Error: Unrecognised state %s\n",statename);
} }
void modem_gps_status(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
writer->printf("GPS status: autostart %s, currently %s.\n",
MyConfig.GetParamValueBool("modem", "enable.gps", false) ? "enabled" : "disabled",
(MyModem && MyModem->m_nmea) ? "running" : "not running");
}
void modem_gps_start(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
if (!MyModem || !MyModem->m_mux || !MyModem->m_mux->IsMuxUp())
{
writer->puts("ERROR: Modem not ready");
return;
}
if (MyModem->m_nmea)
{
writer->puts("GPS already running.");
return;
}
if (MyModem->StartNMEA(true))
{
writer->puts("GPS started (may take a minute to find satellites).");
}
else
{
writer->puts("ERROR: GPS startup failed.");
}
}
void modem_gps_stop(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{
if (!MyModem || !MyModem->m_mux || !MyModem->m_mux->IsMuxUp())
{
writer->puts("ERROR: Modem not ready");
return;
}
if (!MyModem->m_nmea)
{
writer->puts("GPS already stopped.");
return;
}
MyModem->StopNMEA();
writer->puts("GPS stopped.");
}
void cellular_drivers(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv) void cellular_drivers(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{ {
writer->puts("Type Name"); writer->puts("Type Name");
@ -1777,6 +1843,11 @@ CellularModemInit::CellularModemInit()
cmd_setstate->RegisterCommand(ModemState1Name((modem::modem_state1_t)x),"Force CELLULAR MODEM state change",modem_setstate); cmd_setstate->RegisterCommand(ModemState1Name((modem::modem_state1_t)x),"Force CELLULAR MODEM state change",modem_setstate);
} }
OvmsCommand* cmd_gps = cmd_cellular->RegisterCommand("gps", "GPS/GNSS state control", modem_gps_status);
cmd_gps->RegisterCommand("status", "GPS/GNSS status", modem_gps_status);
cmd_gps->RegisterCommand("start", "Start GPS/GNSS", modem_gps_start);
cmd_gps->RegisterCommand("stop", "Stop GPS/GNSS", modem_gps_stop);
MyConfig.RegisterParam("modem", "Modem Configuration", true, true); MyConfig.RegisterParam("modem", "Modem Configuration", true, true);
// Our instances: // Our instances:
// 'driver': Driver to use (default: auto) // 'driver': Driver to use (default: auto)

View File

@ -58,7 +58,7 @@ class modem : public pcp, public InternalRamAllocated
~modem(); ~modem();
protected: protected:
TaskHandle_t m_task; volatile TaskHandle_t m_task;
volatile QueueHandle_t m_queue; volatile QueueHandle_t m_queue;
int m_baud; int m_baud;
int m_rxpin; int m_rxpin;
@ -201,7 +201,7 @@ class modem : public pcp, public InternalRamAllocated
// High level API functions // High level API functions
void StartTask(); void StartTask();
void StopTask(); void StopTask();
void StartNMEA(); bool StartNMEA(bool force=false);
void StopNMEA(); void StopNMEA();
void StartMux(); void StartMux();
void StopMux(); void StopMux();

View File

@ -82,9 +82,9 @@ void modemdriver::Restart()
{ {
ESP_LOGI(TAG, "Restart"); ESP_LOGI(TAG, "Restart");
if (MyConfig.GetParamValueBool("auto", "modem", false)) if (MyConfig.GetParamValueBool("auto", "modem", false))
m_modem->SetState1((m_modem->GetState1() != modem::PoweredOff) ? modem::PowerOffOn : modem::PoweringOn); m_modem->SendSetState1((m_modem->GetState1() != modem::PoweredOff) ? modem::PowerOffOn : modem::PoweringOn);
else else
m_modem->SetState1(modem::PoweringOff); m_modem->SendSetState1(modem::PoweringOff);
} }
void modemdriver::PowerOff() void modemdriver::PowerOff()
@ -101,6 +101,7 @@ void modemdriver::PowerCycle()
m_powercyclefactor = m_powercyclefactor % 3; m_powercyclefactor = m_powercyclefactor % 3;
ESP_LOGI(TAG, "Power Cycle %dms", psd); ESP_LOGI(TAG, "Power Cycle %dms", psd);
uart_wait_tx_done(m_modem->m_uartnum, portMAX_DELAY);
uart_flush(m_modem->m_uartnum); // Flush the ring buffer, to try to address MUX start issues uart_flush(m_modem->m_uartnum); // Flush the ring buffer, to try to address MUX start issues
#ifdef CONFIG_OVMS_COMP_MAX7317 #ifdef CONFIG_OVMS_COMP_MAX7317
MyPeripherals->m_max7317->Output(MODEM_EGPIO_PWR, 0); // Modem EN/PWR line low MyPeripherals->m_max7317->Output(MODEM_EGPIO_PWR, 0); // Modem EN/PWR line low
@ -142,7 +143,12 @@ void modemdriver::ShutdownNMEA()
{ {
// Switch off GPS: // Switch off GPS:
if (m_modem->m_mux != NULL) if (m_modem->m_mux != NULL)
{ m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPS=0\r\n"); } {
// send single commands, as each can fail:
m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPSNMEA=0\r\n");
vTaskDelay(pdMS_TO_TICKS(100));
m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPS=0\r\n");
}
else else
{ ESP_LOGE(TAG, "Attempt to transmit on non running mux"); } { ESP_LOGE(TAG, "Attempt to transmit on non running mux"); }
} }

View File

@ -136,7 +136,7 @@ bool OvmsHttpClient::Request(std::string url, const char* method)
{ {
// ESP_LOGI(TAG, "Got response %s",m_buf->ReadLine().c_str()); // ESP_LOGI(TAG, "Got response %s",m_buf->ReadLine().c_str());
std::string header = m_buf->ReadLine(); std::string header = m_buf->ReadLine();
if (header.compare(0,15,"Content-Length:") == 0) if (strncasecmp(header.c_str(), "Content-Length:", 15) == 0)
{ {
m_bodysize = atoi(header.substr(15).c_str()); m_bodysize = atoi(header.substr(15).c_str());
} }

View File

@ -290,9 +290,10 @@ void OvmsLocation::Store(std::string& buf)
void OvmsLocation::Render(std::string& buf) void OvmsLocation::Render(std::string& buf)
{ {
char val[32]; metric_unit_t user_length = OvmsMetricGetUserUnit(GrpDistanceShort, Meters);
snprintf(val, sizeof(val), "%0.6f,%0.6f (%dm)", m_latitude, m_longitude, m_radius);
buf = val; buf = string_format("%0.6f,%0.6f (%d%s)",
m_latitude, m_longitude, UnitConvert(Meters, user_length, m_radius), OvmsMetricUnitLabel(user_length));
bool first = true; bool first = true;
for (ActionList::iterator it = m_actions.begin(); it != m_actions.end(); ++it) for (ActionList::iterator it = m_actions.begin(); it != m_actions.end(); ++it)
{ {
@ -369,11 +370,22 @@ void location_list(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc
writer->puts("NOTE: ACC actions are not implemented yet!"); // XXX IMPLEMENT AND REMOVE THIS! writer->puts("NOTE: ACC actions are not implemented yet!"); // XXX IMPLEMENT AND REMOVE THIS!
} }
int location_set_validate(OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv, bool complete)
{
if (argc == 5)
{
return OvmsMetricUnit_Validate(writer, argc, argv[4], complete, GrpDistanceShort);
}
return -1;
}
void location_set(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv) void location_set(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
{ {
const char *name = argv[0]; const char *name = argv[0];
float latitude, longitude; float latitude, longitude;
int radius = LOCATION_DEFRADIUS; int radius = LOCATION_DEFRADIUS;
int base_value = radius;
metric_unit_t user_length = Meters;
if (strcmp(name, "?") == 0) if (strcmp(name, "?") == 0)
{ {
@ -391,12 +403,37 @@ void location_set(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc,
longitude = MyLocations.m_longitude; longitude = MyLocations.m_longitude;
} }
if (argc > 3) radius = atoi(argv[3]); if (argc > 3)
{
radius = atoi(argv[3]);
base_value = radius;
if (argc < 5)
user_length = OvmsMetricGetUserUnit(GrpDistanceShort, Meters);
else
{
user_length = OvmsMetricUnitFromName(argv[4]);
if (user_length == UnitNotFound)
{
writer->printf("Error: Invalid Metric %s\n", argv[4]);
return;
}
user_length = OvmsMetricCheckUnit(Meters, user_length);
if (user_length == UnitNotFound)
{
writer->printf("Error: Metric %s is not a length unit\n", argv[4]);
return;
}
}
char val[32]; radius = UnitConvert(user_length, Meters, radius);
snprintf(val,sizeof(val),"%0.6f,%0.6f,%d",latitude,longitude,radius); }
MyConfig.SetParamValue(LOCATIONS_PARAM,name,val);
writer->puts("Location defined"); std::string val = string_format("%0.6f,%0.6f,%d",latitude,longitude,radius);
MyConfig.SetParamValue(LOCATIONS_PARAM,name,val.c_str());
if (user_length == Meters)
writer->printf("Location defined with radius of %dm\n", base_value);
else
writer->printf("Location defined with radius of %d%s = %dm\n", base_value, OvmsMetricUnitLabel(user_length), radius);
} }
void location_radius(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv) void location_radius(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
@ -409,12 +446,35 @@ void location_radius(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int ar
writer->printf("Error: No location %s defined\n",name); writer->printf("Error: No location %s defined\n",name);
return; return;
} }
metric_unit_t user_length;
if (argc < 3)
user_length = OvmsMetricGetUserUnit(GrpDistanceShort, Meters);
else
{
user_length = OvmsMetricUnitFromName(argv[2]);
if (user_length == UnitNotFound)
{
writer->printf("Error: Invalid Metric %s\n", argv[2]);
return;
}
user_length = OvmsMetricCheckUnit(Meters, user_length);
if (user_length == UnitNotFound)
{
writer->printf("Error: Metric %s is not a length unit\n", argv[2]);
return;
}
}
std::string buf; std::string buf;
OvmsLocation* loc = *locp; OvmsLocation* loc = *locp;
loc->m_radius = atoi(argv[1]); int base_value = atoi(argv[1]);
int radius_m = UnitConvert(user_length, Meters, base_value);
loc->m_radius = radius_m;
loc->Store(buf); loc->Store(buf);
writer->puts("Location radius set"); if (user_length == Meters)
writer->printf("Location radius set to %dm\n", base_value);
else
writer->printf("Location radius set to %d%s = %dm\n", base_value, OvmsMetricUnitLabel(user_length), radius_m);
} }
void location_rm(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv) void location_rm(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
@ -477,6 +537,16 @@ int location_validate(OvmsWriter* writer, OvmsCommand* cmd, int argc, const char
return -1; return -1;
} }
int location_radius_validate(OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv, bool complete)
{
switch (argc)
{
case 1: return MyLocations.m_locations.Validate(writer, argc, argv[0], complete);
case 3: return OvmsMetricUnit_Validate(writer, argc, argv[2], complete, GrpDistanceShort);
}
return -1;
}
void location_action(int verbosity, OvmsWriter* writer, enum LocationAction act, std::string& params) void location_action(int verbosity, OvmsWriter* writer, enum LocationAction act, std::string& params)
{ {
const char* const* rargv = writer->GetArgv(); const char* const* rargv = writer->GetArgv();
@ -634,8 +704,8 @@ OvmsLocations::OvmsLocations()
// Register our commands // Register our commands
OvmsCommand* cmd_location = MyCommandApp.RegisterCommand("location","LOCATION framework", location_status, "", 0, 0, false); OvmsCommand* cmd_location = MyCommandApp.RegisterCommand("location","LOCATION framework", location_status, "", 0, 0, false);
cmd_location->RegisterCommand("list","Show all locations",location_list); cmd_location->RegisterCommand("list","Show all locations",location_list);
cmd_location->RegisterCommand("set","Set the position of a location",location_set, "<name> [<latitude> <longitude> [<radius>]]", 1, 4); cmd_location->RegisterCommand("set","Set the position of a location",location_set, "<name> [<latitude> <longitude> [<radius> [<unit>]] ]", 1, 5, true, location_set_validate);
cmd_location->RegisterCommand("radius","Set the radius of a location",location_radius, "<name> <radius>", 2, 2, true, location_validate); cmd_location->RegisterCommand("radius","Set the radius of a location (defaults to user 'height' units)",location_radius, "<name> <radius> [<unit>]", 2, 3, true, location_radius_validate);
cmd_location->RegisterCommand("rm","Remove a defined location",location_rm, "<name>", 1, 1, true, location_validate); cmd_location->RegisterCommand("rm","Remove a defined location",location_rm, "<name>", 1, 1, true, location_validate);
cmd_location->RegisterCommand("status","Show location status",location_status); cmd_location->RegisterCommand("status","Show location status",location_status);
OvmsCommand* cmd_action = cmd_location->RegisterCommand("action","Set an action for a location"); OvmsCommand* cmd_action = cmd_location->RegisterCommand("action","Set an action for a location");

View File

@ -198,7 +198,7 @@ size_t OvmsNetHttpAsyncClient::IncomingData(void *data, size_t length)
{ {
// Process the header // Process the header
ESP_LOGD(TAG, "OvmsNetHttpAsyncClient Headers got %s", header.c_str()); ESP_LOGD(TAG, "OvmsNetHttpAsyncClient Headers got %s", header.c_str());
if (header.compare(0,15,"Content-Length:") == 0) if (strncasecmp(header.c_str(), "Content-Length:", 15) == 0)
{ {
m_bodysize = atoi(header.substr(15).c_str()); m_bodysize = atoi(header.substr(15).c_str());
ESP_LOGD(TAG, "OvmsNetHttpAsyncClient content-length is %d", m_bodysize); ESP_LOGD(TAG, "OvmsNetHttpAsyncClient content-length is %d", m_bodysize);

View File

@ -38,7 +38,9 @@ static const char *TAG = "ota";
#include <string.h> #include <string.h>
#include <esp_system.h> #include <esp_system.h>
#include <esp_ota_ops.h> #include <esp_ota_ops.h>
#if ESP_IDF_VERSION_MAJOR < 4
#include "strverscmp.h" #include "strverscmp.h"
#endif
#include "ovms_ota.h" #include "ovms_ota.h"
#include "ovms_command.h" #include "ovms_command.h"
#include "ovms_boot.h" #include "ovms_boot.h"
@ -180,7 +182,7 @@ void ota_flash_vfs(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc
writer->printf("Error: Cannot find file %s\n",argv[0]); writer->printf("Error: Cannot find file %s\n",argv[0]);
return; return;
} }
writer->printf("Source image is %d bytes in size\n",ds.st_size); writer->printf("Source image is %ld bytes in size\n",ds.st_size);
FILE* f = fopen(argv[0], "r"); FILE* f = fopen(argv[0], "r");
if (f == NULL) if (f == NULL)
@ -242,7 +244,7 @@ void ota_flash_vfs(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc
return; return;
} }
writer->printf("OTA flash was successful\n Flashed %d bytes from %s\n Next boot will be from '%s'\n", writer->printf("OTA flash was successful\n Flashed %ld bytes from %s\n Next boot will be from '%s'\n",
ds.st_size,argv[0],target->label); ds.st_size,argv[0],target->label);
MyConfig.SetParamValue("ota", "vfs.mru", argv[0]); MyConfig.SetParamValue("ota", "vfs.mru", argv[0]);
} }
@ -288,7 +290,7 @@ void ota_flash_http(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int arg
url = MyConfig.GetParamValue("ota","server"); url = MyConfig.GetParamValue("ota","server");
if (url.empty()) if (url.empty())
url = "ovms-ota.bit-cloud.de"; url = "api.openvehicles.com/firmware/ota";
url.append("/"); url.append("/");
url.append(GetOVMSProduct()); url.append(GetOVMSProduct());
@ -830,7 +832,7 @@ void OvmsOTA::GetStatus(ota_info& info, bool check_update /*=true*/)
std::string tag = MyConfig.GetParamValue("ota","tag"); std::string tag = MyConfig.GetParamValue("ota","tag");
std::string url = MyConfig.GetParamValue("ota","server"); std::string url = MyConfig.GetParamValue("ota","server");
if (url.empty()) if (url.empty())
url = "ovms-ota.bit-cloud.de"; url = "api.openvehicles.com/firmware/ota";
url.append("/"); url.append("/");
url.append(GetOVMSProduct()); url.append(GetOVMSProduct());
url.append("/"); url.append("/");
@ -931,7 +933,11 @@ static void OTAFlashTask(void *pvParameters)
if (fromsd) if (fromsd)
{ {
#ifdef CONFIG_OVMS_COMP_SDCARD
success = MyOTA.AutoFlashSD(); success = MyOTA.AutoFlashSD();
#else
success = false;
#endif //CONFIG_OVMS_COMP_SDCARD
} }
else else
{ {
@ -1029,7 +1035,7 @@ bool OvmsOTA::AutoFlash(bool force)
std::string tag = MyConfig.GetParamValue("ota","tag"); std::string tag = MyConfig.GetParamValue("ota","tag");
std::string url = MyConfig.GetParamValue("ota","server"); std::string url = MyConfig.GetParamValue("ota","server");
if (url.empty()) if (url.empty())
url = "ovms-ota.bit-cloud.de"; url = "api.openvehicles.com/firmware/ota";
url.append("/"); url.append("/");
url.append(GetOVMSProduct()); url.append(GetOVMSProduct());

View File

@ -36,7 +36,10 @@ static const char *TAG = "pluginstore";
#include <sys/stat.h> #include <sys/stat.h>
#include <string> #include <string>
#include <string.h> #include <string.h>
#include "esp_idf_version.h"
#if ESP_IDF_VERSION_MAJOR < 4
#include "strverscmp.h" #include "strverscmp.h"
#endif
#include "ovms_plugins.h" #include "ovms_plugins.h"
#include "ovms_command.h" #include "ovms_command.h"
#include "ovms_config.h" #include "ovms_config.h"

View File

@ -180,9 +180,9 @@ DuktapeHTTPRequest::DuktapeHTTPRequest(duk_context *ctx, int obj_idx)
m_headers.append(": "); m_headers.append(": ");
m_headers.append(val); m_headers.append(val);
m_headers.append("\r\n"); m_headers.append("\r\n");
if (key == "User-Agent") if (strcasecmp(key.c_str(), "User-Agent") == 0)
have_useragent = true; have_useragent = true;
else if (key == "Content-Type") else if (strcasecmp(key.c_str(), "Content-Type") == 0)
have_contenttype = true; have_contenttype = true;
} }
duk_pop(ctx); // enum duk_pop(ctx); // enum
@ -325,7 +325,7 @@ void DuktapeHTTPRequest::MongooseCallback(struct mg_connection *nc, int ev, void
key.assign(hm->header_names[i].p, hm->header_names[i].len); key.assign(hm->header_names[i].p, hm->header_names[i].len);
val.assign(hm->header_values[i].p, hm->header_values[i].len); val.assign(hm->header_values[i].p, hm->header_values[i].len);
m_response_headers.push_back(std::make_pair(key, val)); m_response_headers.push_back(std::make_pair(key, val));
if (key == "Location") location = val; if (strcasecmp(key.c_str(), "Location") == 0) location = val;
} }
// follow redirect? // follow redirect?

View File

@ -140,8 +140,10 @@ void tpms_status(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc,
if (StandardMetrics.ms_v_tpms_pressure->IsDefined()) if (StandardMetrics.ms_v_tpms_pressure->IsDefined())
{ {
writer->printf("Pressure...[kPa]: ");
for (auto val : StandardMetrics.ms_v_tpms_pressure->AsVector()) metric_unit_t user_pressure = OvmsMetricGetUserUnit(GrpPressure, kPa);
writer->printf("Pressure...[%s]: ", OvmsMetricUnitLabel(user_pressure) );
for (auto val : StandardMetrics.ms_v_tpms_pressure->AsVector(user_pressure))
writer->printf(" %8.1f", val); writer->printf(" %8.1f", val);
writer->puts(StandardMetrics.ms_v_tpms_pressure->IsStale() ? " [stale]" : ""); writer->puts(StandardMetrics.ms_v_tpms_pressure->IsStale() ? " [stale]" : "");
data_shown = true; data_shown = true;
@ -149,8 +151,9 @@ void tpms_status(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc,
if (StandardMetrics.ms_v_tpms_temp->IsDefined()) if (StandardMetrics.ms_v_tpms_temp->IsDefined())
{ {
writer->printf("Temperature.[°C]: "); metric_unit_t user_temp = OvmsMetricGetUserUnit(GrpTemp, Celcius);
for (auto val : StandardMetrics.ms_v_tpms_temp->AsVector()) writer->printf("Temperature.[%s]: ", OvmsMetricUnitLabel(user_temp));
for (auto val : StandardMetrics.ms_v_tpms_temp->AsVector(user_temp))
writer->printf(" %8.1f", val); writer->printf(" %8.1f", val);
writer->puts(StandardMetrics.ms_v_tpms_temp->IsStale() ? " [stale]" : ""); writer->puts(StandardMetrics.ms_v_tpms_temp->IsStale() ? " [stale]" : "");
data_shown = true; data_shown = true;

View File

@ -402,6 +402,180 @@ $.fn.loadcmd = function(command, filter, timeout) {
var monitorTimer, last_monotonic = 0; var monitorTimer, last_monotonic = 0;
var ws, ws_inhibit = 0; var ws, ws_inhibit = 0;
var metrics = {}; var metrics = {};
var units = { metrics: {}, prefs: {} };
mi_to_km = function(mi) { return mi * 1.609347; }
km_to_mi = function(km) { return km * 0.6213700; }
pkm_to_pmi = function(pkm) { return pkm * 1.609347; }
pmi_to_pkm = function(pmi) { return pmi * 0.6213700; }
no_conversion = function (value) { return value;}
x_to_kx = function (value) { return value/1000; }
kx_to_x = function (value) { return value*1000; }
const feet_per_mile = 5280;
var unit_conversions = {
"native": no_conversion,
"km>miles": km_to_mi,
"km>meters": kx_to_x,
"km>feet": function (value) { return km_to_mi(value) * feet_per_mile; },
"miles>km": mi_to_km,
"miles>meters": function (value) { return (mi_to_km(value)*1000); },
"miles>feet": function (value) { return value * feet_per_mile; },
"meters>miles": function (value) { return km_to_mi(value/1000); },
"meters>km": x_to_kx,
"meters>feet": function (value) { return km_to_mi(value/1000) * feet_per_mile; },
"feet>km": function (value) { return mi_to_km(value/feet_per_mile); },
"feet>meters": function (value) { return (mi_to_km(value/feet_per_mile)*1000); },
"feet>miles": function (value) { return value / feet_per_mile; },
"kmphps>miphps": km_to_mi,
"kmphps>mpss": function (value) { return value/3.6; },
"kmphps>ftpss": function (value) { return km_to_mi(value)*feet_per_mile/3600; },
"miphps>kmphps": mi_to_km,
"miphps>mpss": function (value) { return mi_to_km(value)/3.6; },
"miphps>ftpss": function (value) { return value*feet_per_mile/3600; },
"mpss>kmphps": function (value) { return (value*3.6); },
"mpss>miphps": function (value) { return km_to_mi(value)*3.6; },
"mpss>ftpss": function (value) { return km_to_mi(value)*feet_per_mile; },
"ftpss>kmphps": function (value) { return (mi_to_km(value/feet_per_mile)*3.6); },
"ftpss>miphps": function (value) { return value *3600/feet_per_mile; },
"ftpss>mpss": function (value) { return mi_to_km(value/feet_per_mile)*1000; },
"kw>watts": kx_to_x,
"watts>kw": x_to_kx,
"kwh>watthours": kx_to_x,
"watthours>kwh": x_to_kx,
"whpkm>whpmi": pkm_to_pmi,
"whpkm>kwhp100km": function (value) { return value / 10; },
"whpkm>kmpkwh": function (value) { return value ? 1000.0 / value : 0; },
"whpkm>mipkwh": function (value) { return value ? (km_to_mi(1000.0 / value)) : 0; },
"whpmi>whpkm": pmi_to_pkm,
"whpmi>kwhp100km": function (value) { return pmi_to_pkm(value) / 10; },
"whpmi>kmpkwh": function (value) { return value ? (mi_to_km(1000.0 / value)) : 0; },
"whpmi>mipkwh": function (value) { return value ? (1000.0 / value) : 0; },
"kwhp100km>whpmi": function (value) { return pkm_to_pmi(value * 10); },
"kwhp100km>whpkm": function (value) { return value * 10; },
"kwhp100km>kmpkwh": function (value) { return value ? (100.0 / value) : 0; },
"kwhp100km>mipkwh": function (value) { return value ? km_to_mi(100.0 / value) : 0; },
"kmpkwh>whpmi": function (value) { return value ? (1000.0 / km_to_mi(value)) : 0;},
"kmpkwh>whpkm": function (value) { return value ? (1/(1000.0 * value)) : 0;},
"kmpkwh>kwhp100km": function (value) { return value ? (100.0/value) : 0;},
"kmpkwh>mipkwh": km_to_mi,
"mipkwh>whpmi": function (value) { return value ? 1000/value : 0;},
"mipkwh>whpkm": function (value) { return value ? (1000 / mi_to_km(value)) : 0;},
"mipkwh>kwhp100km": function (value) { return value ? (100.0/mi_to_km(value)) : 0;},
"mipkwh>kmpkwh": mi_to_km,
"celcius>fahrenheit": function (value) { return ((value*9)/5) + 32; },
"fahrenheit>celcius": function (value) { return ((value-32)*5)/9; },
"kpa>pa": kx_to_x,
"kpa>bar": function (value) { return value/100; },
"kpa>psi": function (value) { return value * 0.14503773773020923; },
"pa>kpa": x_to_kx,
"pa>bar": function (value) { return value/100000; },
"pa>psi": function (value) { return value * 0.00014503773773020923; },
"psi>kpa": function (value) { return value * 6.894757293168361; },
"psi>pa": function (value) { return value * 6894.757293168361; },
"psi>bar": function (value) { return value * 0.06894757293168361; },
"bar>pa": function (value) { return value*100000; },
"bar>kpa": function (value) { return value*100; },
"bar>psi": function (value) { return value * 14.503773773020923; },
"seconds>minutes": function (value) { return value/60; },
"seconds>hours": function (value) { return value/3600; },
"minutes>seconds": function (value) { return value*60; },
"minutes>hours": function (value) { return value/60; },
"hours>seconds": function (value) { return value*3600; },
"hours>minutes": function (value) { return value*60; },
"kmph>miph": km_to_mi,
"miph>kmph": mi_to_km,
"dbm>sq": function (value) { return Math.round((value <= -51) ? ((value + 113)/2) : 0); },
"sq>dbm": function (value) { return Math.round((value <= 31) ? (-113 + (value*2)) : 0); },
"percent>permille": function (value) { return value*10.0; },
"permille>percent": function (value) { return value*0.10; }
}
convertUnitFunction = function (from, to) {
return unit_conversions[from + ">" + to] || no_conversion;
}
convertUnits = function (from, to, value) {
return convertUnitFunction(from, to)(value);
}
units.convertMetricToUserUnits = function (value, name) {
if (value == undefined)
return value
var unit_entry = this.metrics[name];
if (unit_entry == undefined)
return value
var cnvfn = unit_entry.user_fn;
if (cnvfn == undefined) {
cnvfn = convertUnitFunction(unit_entry.native, unit_entry.code);
this.metrics[name].user_fn = cnvfn;
}
return cnvfn(value);
}
units.userUnitLabelFromMetric = function (name) {
var unit_entry = this.metrics[name];
if (unit_entry == undefined)
return "";
return unit_entry.label;
}
units.unitLabelToUser = function (unitType, defaultLabel) {
var res = this.prefs[unitType];
return (res && res.label) ? res.label : defaultLabel
}
units.unitValueToUser = function (unitType, value) {
var entry = this.prefs[unitType];
if (!entry)
return value;
return convertUnits(value, unitType, entry.unit);
}
// Works for units and metrics collection.
metricsProxyHas = function(target, name) {
return target[name] != undefined
}
var metrics_all = new Proxy(metrics, {
get: function(target, name) {
if (name == Symbol.toStringTag)
return 'metrics_all[]';
if (!(typeof name === "string" || name instanceof String))
return undefined;
var names = name.split('#',2);
var name = names[0];
var value_type = names[1]
if (value_type === "unit")
return units.userUnitLabelFromMetric(name);
var value = target[name];
if (value_type === "label")
value = units.convertMetricToUserUnits(value, name)
return value;
},
has: metricsProxyHas
});
var metrics_user = new Proxy(metrics, {
get:
function(target, name) {
if (name == Symbol.toStringTag)
return 'metrics_user[]';
return units.convertMetricToUserUnits(target[name], name)
},
has: metricsProxyHas
});
var metrics_label = new Proxy(units.metrics, {
get:
function(target, name) {
if (name == Symbol.toStringTag)
return 'metrics_label[]';
var unit_entry = target[name];
if (unit_entry == undefined) {
return "";
}
return unit_entry.label;
},
has: metricsProxyHas
});
var shellhist = [""], shellhpos = 0; var shellhist = [""], shellhpos = 0;
var loghist = []; var loghist = [];
const loghist_maxsize = 100; const loghist_maxsize = 100;
@ -415,6 +589,7 @@ function initSocketConnection(){
ws.onopen = function(ev) { ws.onopen = function(ev) {
console.log("WebSocket OPENED", ev); console.log("WebSocket OPENED", ev);
$(".receiver").subscribe(); $(".receiver").subscribe();
subscribeToTopic("units/#");
}; };
ws.onerror = function(ev) { console.log("WebSocket ERROR", ev); }; ws.onerror = function(ev) { console.log("WebSocket ERROR", ev); };
ws.onclose = function(ev) { console.log("WebSocket CLOSED", ev); }; ws.onclose = function(ev) { console.log("WebSocket CLOSED", ev); };
@ -443,6 +618,21 @@ function initSocketConnection(){
$.extend(metrics, msg.metrics); $.extend(metrics, msg.metrics);
$(".receiver").trigger("msg:metrics", msg.metrics); $(".receiver").trigger("msg:metrics", msg.metrics);
} }
else if (msgtype == "units") {
for (var subtype in msg.units) {
if (subtype == "metrics") {
$.extend(units.metrics, msg.units.metrics);
$(".receiver").trigger("msg:units:metrics", msg.units.metrics);
var msgmetrics = {};
for (metricname in msg.units.metrics)
msgmetrics[metricname] = metrics[metricname];
$(".receiver").trigger("msg:metrics", msgmetrics);
} else if (subtype == "prefs") {
$.extend(units.prefs, msg.units.prefs);
$(".receiver").trigger("msg:units:prefs", msg.units.prefs);
}
}
}
else if (msgtype == "notify") { else if (msgtype == "notify") {
processNotification(msg.notify); processNotification(msg.notify);
$(".receiver").trigger("msg:notify", msg.notify); $(".receiver").trigger("msg:notify", msg.notify);
@ -513,7 +703,14 @@ function processNotification(msg) {
confirmdialog(opts.title, opts.body, ["OK"], opts.timeout); confirmdialog(opts.title, opts.body, ["OK"], opts.timeout);
} }
subscribeToTopic = function (topic) {
try {
console.debug("subscribe " + topic);
if (ws) ws.send("subscribe " + topic);
} catch (e) {
console.log(e);
}
}
$.fn.subscribe = function(topics) { $.fn.subscribe = function(topics) {
return this.each(function() { return this.each(function() {
var subscriptions = $(this).data("subscriptions"); var subscriptions = $(this).data("subscriptions");
@ -526,13 +723,7 @@ $.fn.subscribe = function(topics) {
var tops = topics ? topics.split(' ') : []; var tops = topics ? topics.split(' ') : [];
for (var i = 0; i < tops.length; i++) { for (var i = 0; i < tops.length; i++) {
if (tops[i] && !subs.includes(tops[i])) { if (tops[i] && !subs.includes(tops[i])) {
try { subscribeToTopic(tops[i]);
console.log("subscribe " + tops[i]);
if (ws) ws.send("subscribe " + tops[i]);
subs.push(tops[i]);
} catch (e) {
console.log(e);
}
} }
} }
$(this).data("subscriptions", subs.join(' ')); $(this).data("subscriptions", subs.join(' '));
@ -1721,20 +1912,43 @@ $(function(){
// Metrics displays: // Metrics displays:
$("body").on('msg:metrics', '.receiver', function(e, update) { $("body").on('msg:metrics', '.receiver', function(e, update) {
$(this).find(".metric").each(function() { $(this).find(".metric").each(function() {
var $el = $(this), metric = $el.data("metric"), prec = $el.data("prec"), scale = $el.data("scale"); var $el = $(this), metric = $el.data("metric"), prec = $el.data("prec"), scale = $el.data("scale"), useUser = $el.data("user");
if (!metric) return; if (!metric) return;
// filter: // filter:
var keys = metric.split(","), val; var keys = metric.split(","), val;
var metricName = "";
for (var i=0; i<keys.length; i++) { for (var i=0; i<keys.length; i++) {
if ((val = update[keys[i]]) != null) break; metricName = keys[i];
if ((val = update[metricName]) != null) {
break;
}
} }
if (val == null) return; if (val == null) return;
// process: // process:
if ($el.hasClass("text")) { if ($el.hasClass("text")) {
$el.children(".value").text(val); var elt = $el.children(".value");
if (elt) elt.text(val);
elt = $el.children(".unit");
if (elt) elt.text(val);
} else if ($el.hasClass("number")) { } else if ($el.hasClass("number")) {
var vf = val; var vf = val;
if (scale != null) vf = Number(vf) * scale; if (scale != null)
vf = Number(vf) * scale;
else {
var mun = units.userUnitLabelFromMetric(metricName);
if (mun != "") {
// If there's a .unit.. then convert it.
item = $el.children(".unit");
if (item) {
item.text(mun);
useUser = true;
}
}
if (useUser)
vf = units.convertMetricToUserUnits(vf, metricName);
}
if (prec != null) vf = Number(vf).toFixed(prec); if (prec != null) vf = Number(vf).toFixed(prec);
$el.children(".value").text(vf); $el.children(".value").text(vf);
} else if ($el.hasClass("progress")) { } else if ($el.hasClass("progress")) {

View File

@ -829,6 +829,180 @@ $.fn.loadcmd = function(command, filter, timeout) {
var monitorTimer, last_monotonic = 0; var monitorTimer, last_monotonic = 0;
var ws, ws_inhibit = 0; var ws, ws_inhibit = 0;
var metrics = {}; var metrics = {};
var units = { metrics: {}, prefs: {} };
mi_to_km = function(mi) { return mi * 1.609347; }
km_to_mi = function(km) { return km * 0.6213700; }
pkm_to_pmi = function(pkm) { return pkm * 1.609347; }
pmi_to_pkm = function(pmi) { return pmi * 0.6213700; }
no_conversion = function (value) { return value;}
x_to_kx = function (value) { return value/1000; }
kx_to_x = function (value) { return value*1000; }
const feet_per_mile = 5280;
var unit_conversions = {
"native": no_conversion,
"km>miles": km_to_mi,
"km>meters": kx_to_x,
"km>feet": function (value) { return km_to_mi(value) * feet_per_mile; },
"miles>km": mi_to_km,
"miles>meters": function (value) { return (mi_to_km(value)*1000); },
"miles>feet": function (value) { return value * feet_per_mile; },
"meters>miles": function (value) { return km_to_mi(value/1000); },
"meters>km": x_to_kx,
"meters>feet": function (value) { return km_to_mi(value/1000) * feet_per_mile; },
"feet>km": function (value) { return mi_to_km(value/feet_per_mile); },
"feet>meters": function (value) { return (mi_to_km(value/feet_per_mile)*1000); },
"feet>miles": function (value) { return value / feet_per_mile; },
"kmphps>miphps": km_to_mi,
"kmphps>mpss": function (value) { return value/3.6; },
"kmphps>ftpss": function (value) { return km_to_mi(value)*feet_per_mile/3600; },
"miphps>kmphps": mi_to_km,
"miphps>mpss": function (value) { return mi_to_km(value)/3.6; },
"miphps>ftpss": function (value) { return value*feet_per_mile/3600; },
"mpss>kmphps": function (value) { return (value*3.6); },
"mpss>miphps": function (value) { return km_to_mi(value)*3.6; },
"mpss>ftpss": function (value) { return km_to_mi(value)*feet_per_mile; },
"ftpss>kmphps": function (value) { return (mi_to_km(value/feet_per_mile)*3.6); },
"ftpss>miphps": function (value) { return value *3600/feet_per_mile; },
"ftpss>mpss": function (value) { return mi_to_km(value/feet_per_mile)*1000; },
"kw>watts": kx_to_x,
"watts>kw": x_to_kx,
"kwh>watthours": kx_to_x,
"watthours>kwh": x_to_kx,
"whpkm>whpmi": pkm_to_pmi,
"whpkm>kwhp100km": function (value) { return value / 10; },
"whpkm>kmpkwh": function (value) { return value ? 1000.0 / value : 0; },
"whpkm>mipkwh": function (value) { return value ? (km_to_mi(1000.0 / value)) : 0; },
"whpmi>whpkm": pmi_to_pkm,
"whpmi>kwhp100km": function (value) { return pmi_to_pkm(value) / 10; },
"whpmi>kmpkwh": function (value) { return value ? (mi_to_km(1000.0 / value)) : 0; },
"whpmi>mipkwh": function (value) { return value ? (1000.0 / value) : 0; },
"kwhp100km>whpmi": function (value) { return pkm_to_pmi(value * 10); },
"kwhp100km>whpkm": function (value) { return value * 10; },
"kwhp100km>kmpkwh": function (value) { return value ? (100.0 / value) : 0; },
"kwhp100km>mipkwh": function (value) { return value ? km_to_mi(100.0 / value) : 0; },
"kmpkwh>whpmi": function (value) { return value ? (1000.0 / km_to_mi(value)) : 0;},
"kmpkwh>whpkm": function (value) { return value ? (1/(1000.0 * value)) : 0;},
"kmpkwh>kwhp100km": function (value) { return value ? (100.0/value) : 0;},
"kmpkwh>mipkwh": km_to_mi,
"mipkwh>whpmi": function (value) { return value ? 1000/value : 0;},
"mipkwh>whpkm": function (value) { return value ? (1000 / mi_to_km(value)) : 0;},
"mipkwh>kwhp100km": function (value) { return value ? (100.0/mi_to_km(value)) : 0;},
"mipkwh>kmpkwh": mi_to_km,
"celcius>fahrenheit": function (value) { return ((value*9)/5) + 32; },
"fahrenheit>celcius": function (value) { return ((value-32)*5)/9; },
"kpa>pa": kx_to_x,
"kpa>bar": function (value) { return value/100; },
"kpa>psi": function (value) { return value * 0.14503773773020923; },
"pa>kpa": x_to_kx,
"pa>bar": function (value) { return value/100000; },
"pa>psi": function (value) { return value * 0.00014503773773020923; },
"psi>kpa": function (value) { return value * 6.894757293168361; },
"psi>pa": function (value) { return value * 6894.757293168361; },
"psi>bar": function (value) { return value * 0.06894757293168361; },
"bar>pa": function (value) { return value*100000; },
"bar>kpa": function (value) { return value*100; },
"bar>psi": function (value) { return value * 14.503773773020923; },
"seconds>minutes": function (value) { return value/60; },
"seconds>hours": function (value) { return value/3600; },
"minutes>seconds": function (value) { return value*60; },
"minutes>hours": function (value) { return value/60; },
"hours>seconds": function (value) { return value*3600; },
"hours>minutes": function (value) { return value*60; },
"kmph>miph": km_to_mi,
"miph>kmph": mi_to_km,
"dbm>sq": function (value) { return Math.round((value <= -51) ? ((value + 113)/2) : 0); },
"sq>dbm": function (value) { return Math.round((value <= 31) ? (-113 + (value*2)) : 0); },
"percent>permille": function (value) { return value*10.0; },
"permille>percent": function (value) { return value*0.10; }
}
convertUnitFunction = function (from, to) {
return unit_conversions[from + ">" + to] || no_conversion;
}
convertUnits = function (from, to, value) {
return convertUnitFunction(from, to)(value);
}
units.convertMetricToUserUnits = function (value, name) {
if (value == undefined)
return value
var unit_entry = this.metrics[name];
if (unit_entry == undefined)
return value
var cnvfn = unit_entry.user_fn;
if (cnvfn == undefined) {
cnvfn = convertUnitFunction(unit_entry.native, unit_entry.code);
this.metrics[name].user_fn = cnvfn;
}
return cnvfn(value);
}
units.userUnitLabelFromMetric = function (name) {
var unit_entry = this.metrics[name];
if (unit_entry == undefined)
return "";
return unit_entry.label;
}
units.unitLabelToUser = function (unitType, defaultLabel) {
var res = this.prefs[unitType];
return (res && res.label) ? res.label : defaultLabel
}
units.unitValueToUser = function (unitType, value) {
var entry = this.prefs[unitType];
if (!entry)
return value;
return convertUnits(value, unitType, entry.unit);
}
// Works for units and metrics collection.
metricsProxyHas = function(target, name) {
return target[name] != undefined
}
var metrics_all = new Proxy(metrics, {
get: function(target, name) {
if (name == Symbol.toStringTag)
return 'metrics_all[]';
if (!(typeof name === "string" || name instanceof String))
return undefined;
var names = name.split('#',2);
var name = names[0];
var value_type = names[1]
if (value_type === "unit")
return units.userUnitLabelFromMetric(name);
var value = target[name];
if (value_type === "label")
value = units.convertMetricToUserUnits(value, name)
return value;
},
has: metricsProxyHas
});
var metrics_user = new Proxy(metrics, {
get:
function(target, name) {
if (name == Symbol.toStringTag)
return 'metrics_user[]';
return units.convertMetricToUserUnits(target[name], name)
},
has: metricsProxyHas
});
var metrics_label = new Proxy(units.metrics, {
get:
function(target, name) {
if (name == Symbol.toStringTag)
return 'metrics_label[]';
var unit_entry = target[name];
if (unit_entry == undefined) {
return "";
}
return unit_entry.label;
},
has: metricsProxyHas
});
var shellhist = [""], shellhpos = 0; var shellhist = [""], shellhpos = 0;
var loghist = []; var loghist = [];
const loghist_maxsize = 100; const loghist_maxsize = 100;
@ -842,6 +1016,7 @@ function initSocketConnection(){
ws.onopen = function(ev) { ws.onopen = function(ev) {
console.log("WebSocket OPENED", ev); console.log("WebSocket OPENED", ev);
$(".receiver").subscribe(); $(".receiver").subscribe();
subscribeToTopic("units/#");
}; };
ws.onerror = function(ev) { console.log("WebSocket ERROR", ev); }; ws.onerror = function(ev) { console.log("WebSocket ERROR", ev); };
ws.onclose = function(ev) { console.log("WebSocket CLOSED", ev); }; ws.onclose = function(ev) { console.log("WebSocket CLOSED", ev); };
@ -870,6 +1045,21 @@ function initSocketConnection(){
$.extend(metrics, msg.metrics); $.extend(metrics, msg.metrics);
$(".receiver").trigger("msg:metrics", msg.metrics); $(".receiver").trigger("msg:metrics", msg.metrics);
} }
else if (msgtype == "units") {
for (var subtype in msg.units) {
if (subtype == "metrics") {
$.extend(units.metrics, msg.units.metrics);
$(".receiver").trigger("msg:units:metrics", msg.units.metrics);
var msgmetrics = {};
for (metricname in msg.units.metrics)
msgmetrics[metricname] = metrics[metricname];
$(".receiver").trigger("msg:metrics", msgmetrics);
} else if (subtype == "prefs") {
$.extend(units.prefs, msg.units.prefs);
$(".receiver").trigger("msg:units:prefs", msg.units.prefs);
}
}
}
else if (msgtype == "notify") { else if (msgtype == "notify") {
processNotification(msg.notify); processNotification(msg.notify);
$(".receiver").trigger("msg:notify", msg.notify); $(".receiver").trigger("msg:notify", msg.notify);
@ -940,7 +1130,14 @@ function processNotification(msg) {
confirmdialog(opts.title, opts.body, ["OK"], opts.timeout); confirmdialog(opts.title, opts.body, ["OK"], opts.timeout);
} }
subscribeToTopic = function (topic) {
try {
console.debug("subscribe " + topic);
if (ws) ws.send("subscribe " + topic);
} catch (e) {
console.log(e);
}
}
$.fn.subscribe = function(topics) { $.fn.subscribe = function(topics) {
return this.each(function() { return this.each(function() {
var subscriptions = $(this).data("subscriptions"); var subscriptions = $(this).data("subscriptions");
@ -953,13 +1150,7 @@ $.fn.subscribe = function(topics) {
var tops = topics ? topics.split(' ') : []; var tops = topics ? topics.split(' ') : [];
for (var i = 0; i < tops.length; i++) { for (var i = 0; i < tops.length; i++) {
if (tops[i] && !subs.includes(tops[i])) { if (tops[i] && !subs.includes(tops[i])) {
try { subscribeToTopic(tops[i]);
console.log("subscribe " + tops[i]);
if (ws) ws.send("subscribe " + tops[i]);
subs.push(tops[i]);
} catch (e) {
console.log(e);
}
} }
} }
$(this).data("subscriptions", subs.join(' ')); $(this).data("subscriptions", subs.join(' '));
@ -2148,20 +2339,43 @@ $(function(){
// Metrics displays: // Metrics displays:
$("body").on('msg:metrics', '.receiver', function(e, update) { $("body").on('msg:metrics', '.receiver', function(e, update) {
$(this).find(".metric").each(function() { $(this).find(".metric").each(function() {
var $el = $(this), metric = $el.data("metric"), prec = $el.data("prec"), scale = $el.data("scale"); var $el = $(this), metric = $el.data("metric"), prec = $el.data("prec"), scale = $el.data("scale"), useUser = $el.data("user");
if (!metric) return; if (!metric) return;
// filter: // filter:
var keys = metric.split(","), val; var keys = metric.split(","), val;
var metricName = "";
for (var i=0; i<keys.length; i++) { for (var i=0; i<keys.length; i++) {
if ((val = update[keys[i]]) != null) break; metricName = keys[i];
if ((val = update[metricName]) != null) {
break;
}
} }
if (val == null) return; if (val == null) return;
// process: // process:
if ($el.hasClass("text")) { if ($el.hasClass("text")) {
$el.children(".value").text(val); var elt = $el.children(".value");
if (elt) elt.text(val);
elt = $el.children(".unit");
if (elt) elt.text(val);
} else if ($el.hasClass("number")) { } else if ($el.hasClass("number")) {
var vf = val; var vf = val;
if (scale != null) vf = Number(vf) * scale; if (scale != null)
vf = Number(vf) * scale;
else {
var mun = units.userUnitLabelFromMetric(metricName);
if (mun != "") {
// If there's a .unit.. then convert it.
item = $el.children(".unit");
if (item) {
item.text(mun);
useUser = true;
}
}
if (useUser)
vf = units.convertMetricToUserUnits(vf, metricName);
}
if (prec != null) vf = Number(vf).toFixed(prec); if (prec != null) vf = Number(vf).toFixed(prec);
$el.children(".value").text(vf); $el.children(".value").text(vf);
} else if ($el.hasClass("progress")) { } else if ($el.hasClass("progress")) {

View File

@ -6,9 +6,28 @@ OVMS V3 is based on metrics. Metrics can be single numerical or textual values o
like sets and arrays. The web framework keeps all metrics in a global object, which can be read like sets and arrays. The web framework keeps all metrics in a global object, which can be read
simply by e.g. ``metrics["v.b.soc"]``. simply by e.g. ``metrics["v.b.soc"]``.
In addition to the raw metric values, there are 2 main proxy arrays that give access to the
user-configured versions of the raw metric values. The ``metrics_user`` array
converts the 'metrics' value to user-configured value and the ``metrics_label`` array
provides the corresponding label for that metric.
So for example the user could configure distance values to be in miles, and in this case
``metrics["v.p.odometer"]`` would still contain the value in km (the default) but
``metrics_user["v.p.odometer"]`` would give the value converted to miles and
``metrics_label["v.p.odometer"]`` would return "M".
The user conversion information is contained in another object ``units``. ``units.metrics``
has the user configuration for each metric and ``units.prefs`` has the user configuration
for each group of metrics (distance, temperature, consumption, pressure etc). There also some methods
for general conversions allowing user preferences.
- The method ``units.unitLabelToUser(unitType,name)`` will return the user
defined label for that 'unitType', defaulting to ``name``.
- The method ``units.unitValueToUser(unitType,value)`` will convert ``value``
to the user defined unit (if set) for the group.
Metrics updates (as well as other updates) are sent to all DOM elements having the Metrics updates (as well as other updates) are sent to all DOM elements having the
``receiver`` class. To hook into these updates, simply add an event listener for ``receiver`` class. To hook into these updates, simply add an event listener for
``msg:metrics``. ``msg:metrics:``. The event ``msg:units:metrics`` is called when ``units.metrics`` is change
and ``msg:units:prefs`` when ``units.prefs`` are changed.
Listening to the event is not necessary though if all you need is some metrics Listening to the event is not necessary though if all you need is some metrics
display. This is covered by the ``metric`` widget class family as shown here. display. This is covered by the ``metric`` widget class family as shown here.
@ -30,6 +49,11 @@ The following example covers…
- Gauges - Gauges
- Charts - Charts
Where a number element of class 'metric' contains both elements of class
'value' and 'unit', these will be automatically displayed in the units selected
in the user preferences. Having a 'data-user' attribute will also cause the
'value' element to be displayed in user units (unless 'data-scale' attribute is present).
Gauges & charts use the HighCharts library, which is included in the web server. The other widgets Gauges & charts use the HighCharts library, which is included in the web server. The other widgets
are simple standard Bootstrap widgets extended by an automatic metrics value update mechanism. are simple standard Bootstrap widgets extended by an automatic metrics value update mechanism.

View File

@ -1086,3 +1086,105 @@ void OvmsWebServer::HandleLogout(PageEntry_t& p, PageContext_t& c)
"<script>loggedin = false; $(\"#menu\").load(\"/menu\"); loaduri(\"#main\", \"get\", \"/home\", {})</script>"); "<script>loggedin = false; $(\"#menu\").load(\"/menu\"); loaduri(\"#main\", \"get\", \"/home\", {})</script>");
c.done(); c.done();
} }
// Dash Gauge implementations.
//
dash_gauge_t::dash_gauge_t(const char *titlePrefix, metric_unit_t defUnit, metric_group_t group)
{
has_tick = false;
title_prefix = titlePrefix ? titlePrefix : "";
if (group == GrpNone)
group = GetMetricGroup(defUnit);
if (group == GrpOther)
user_unit = defUnit;
else
user_unit = MyUnitConfig.GetUserUnit(group, defUnit);
base_unit = defUnit;
}
float dash_gauge_t::UntConvert( float inValue ) const
{
return UnitConvert(base_unit, user_unit, inValue);
}
float dash_gauge_t::UntConvert( float inValue, float roundValue ) const
{
if (base_unit == user_unit)
return inValue;
return truncf(UnitConvert(base_unit, user_unit, inValue) / roundValue) * roundValue;
}
void dash_gauge_t::SetMinMax( float minValue, float maxValue)
{
min_value = UntConvert(minValue);
float temp_max = UntConvert(maxValue);
if (min_value < temp_max)
max_value = temp_max;
else {
max_value = min_value;
min_value = temp_max;
}
}
void dash_gauge_t::SetMinMax( float minValue, float maxValue, float roundValue)
{
min_value = UntConvert(minValue, roundValue);
float temp_max = UntConvert(maxValue, roundValue);
if (min_value < temp_max)
max_value = temp_max;
else {
max_value = min_value;
min_value = temp_max;
}
}
void dash_gauge_t::SetTick( float tickValue)
{
tick_value = UntConvert(tickValue);
has_tick =true;
}
void dash_gauge_t::SetTick( float tickValue, float roundValue)
{
tick_value = UntConvert(tickValue);
has_tick = true;
}
void dash_gauge_t::DoAddBand( const std::string &colour, float minValue, float maxValue, bool round, float roundValue)
{
dash_plot_band_t new_band;
new_band.colour = colour;
new_band.min_value = round ? UntConvert(minValue,roundValue) : UntConvert(minValue);
float temp_max = round ? UntConvert(maxValue, roundValue) : UntConvert(maxValue);
if (new_band.min_value <= temp_max) {
new_band.max_value = temp_max;
bands.insert(bands.end(), new_band);
} else
{
new_band.max_value = new_band.min_value;
new_band.min_value = temp_max;
bands.insert(bands.begin(), new_band);
}
}
std::ostream &dash_gauge_t::Output(std::ostream &ostream) const
{
ostream <<
"{"
"title: { text: '"<< title_prefix << PageContext::encode_html(OvmsMetricUnitLabel(user_unit)) << "' },";
if (min_value < max_value) {
ostream <<
"min: " << min_value << ", max: " << max_value << ",";
}
if (has_tick)
ostream << "tickInterval: " << tick_value << ",";
ostream << "plotBands: [";
bool first = true;
for (auto iter = bands.begin() ; iter != bands.end(); ++iter) {
if (first)
first = false;
else
ostream << ",";
ostream <<
"{ from: " << iter->min_value << ", to: " << iter->max_value << ", className: '" << iter->colour <<"-band' }";
}
ostream << "]}";
return ostream;
}

View File

@ -155,7 +155,7 @@ struct PageContext : public ExternalRamAllocated
void print(const std::string text); void print(const std::string text);
void print(const extram::string text); void print(const extram::string text);
void print(const char* text); void print(const char* text);
void printf(const char *fmt, ...); void printf(const char *fmt, ...) __attribute__ ((format (printf, 2, 3)));
void done(); void done();
void panel_start(const char* type, const char* title); void panel_start(const char* type, const char* title);
void panel_end(const char* footer=""); void panel_end(const char* footer="");
@ -367,6 +367,8 @@ enum WebSocketTxJobType
WSTX_Config, // payload: config (todo) WSTX_Config, // payload: config (todo)
WSTX_Notify, // payload: notification WSTX_Notify, // payload: notification
WSTX_LogBuffers, // payload: logbuffers WSTX_LogBuffers, // payload: logbuffers
WSTX_UnitMetricUpdate, // payload: -
WSTX_UnitPrefsUpdate, // payload: -
}; };
struct WebSocketTxJob struct WebSocketTxJob
@ -412,6 +414,10 @@ class WebSocketHandler : public MgHandler, public OvmsWriter
void Unsubscribe(std::string topic); void Unsubscribe(std::string topic);
bool IsSubscribedTo(std::string topic); bool IsSubscribedTo(std::string topic);
void SubscriptionChanged();
void UnitsCheckSubscribe();
void UnitsCheckVehicleSubscribe();
// OvmsWriter: // OvmsWriter:
public: public:
void Log(LogBuffers* message); void Log(LogBuffers* message);
@ -428,7 +434,10 @@ class WebSocketHandler : public MgHandler, public OvmsWriter
WebSocketTxJob m_job = {}; WebSocketTxJob m_job = {};
int m_sent = 0; int m_sent = 0;
int m_ack = 0; int m_ack = 0;
int m_last = 0; // last entry sent up
std::set<std::string> m_subscriptions; std::set<std::string> m_subscriptions;
bool m_units_subscribed;
bool m_units_prefs_subscribed;
}; };
struct WebSocketSlot struct WebSocketSlot
@ -469,7 +478,7 @@ class HttpCommandStream : public OvmsShell, public MgHandler
void Initialize(bool print); void Initialize(bool print);
virtual bool IsInteractive() { return false; } virtual bool IsInteractive() { return false; }
int puts(const char* s); int puts(const char* s);
int printf(const char* fmt, ...); int printf(const char* fmt, ...) __attribute__ ((format (printf, 2, 3)));
ssize_t write(const void *buf, size_t nbyte); ssize_t write(const void *buf, size_t nbyte);
void Log(LogBuffers* message); void Log(LogBuffers* message);
}; };
@ -536,7 +545,7 @@ class OvmsWebServer : public ExternalRamAllocated
static void HandleLogin(PageEntry_t& p, PageContext_t& c); static void HandleLogin(PageEntry_t& p, PageContext_t& c);
static void HandleLogout(PageEntry_t& p, PageContext_t& c); static void HandleLogout(PageEntry_t& p, PageContext_t& c);
static void OutputReboot(PageEntry_t& p, PageContext_t& c); static void OutputReboot(PageEntry_t& p, PageContext_t& c);
static void OutputReconnect(PageEntry_t& p, PageContext_t& c, const char* info=NULL); static void OutputReconnect(PageEntry_t& p, PageContext_t& c, const char* info=NULL, const char* cmd=NULL);
public: public:
static void HandleStatus(PageEntry_t& p, PageContext_t& c); static void HandleStatus(PageEntry_t& p, PageContext_t& c);
@ -610,6 +619,80 @@ class OvmsWebServer : public ExternalRamAllocated
extern OvmsWebServer MyWebServer; extern OvmsWebServer MyWebServer;
/** Dashboard Gauge generator.
* Handles unit conversions.
*/
struct dash_gauge_t {
protected:
struct dash_plot_band_t {
std::string colour;
float min_value, max_value;
};
std::string title_prefix;
metric_unit_t user_unit, base_unit;
float min_value, max_value, tick_value;
bool has_tick;
std::vector<dash_plot_band_t> bands;
void DoAddBand( const std::string &colour, float minValue, float maxValue, bool round, float roundValue);
public:
/**
* @param titlePrefix The title/caption prefix (to the unit)
* @param defUnit The unit used as a default for these metrics.
* @param group (Optional) The group of units it belong to.
*/
dash_gauge_t(const char *titlePrefix, metric_unit_t defUnit, metric_group_t group = GrpNone);
/**
* Convert a unit to the user unit.
*/
float UntConvert( float inValue ) const;
/**
* Convert a unit to the user unit with rounding to the nearest value.
*/
float UntConvert( float inValue, float roundValue ) const;
/**
* Set the minimum and maximum values for the gauge (in original units).
*/
void SetMinMax( float minValue, float maxValue);
/**
* Set the minimum and maximum values for the gauge (in original units) including
* rounding to the nearest value.
*/
void SetMinMax( float minValue, float maxValue, float roundValue);
/**
* Set the tickmark intervals (in original units).
*/
void SetTick( float tickValue);
/**
* Set the tickmark intervals (in original units) with rounding to nearest.
*/
void SetTick( float tickValue, float roundValue);
/** Zero the minimum. Used when 0 cannot be originally used as a minimum.
*/
inline void ZeroMin() { if (max_value > 0) min_value = 0; }
/** Add a colour band to the gauge.
*/
inline void AddBand( const std::string &colour, float minValue, float maxValue)
{
DoAddBand(colour, minValue, maxValue, false, 0);
}
/** Add a colour band to the gauge with rounding to the nearest value.
*/
inline void AddBand( const std::string &colour, float minValue, float maxValue, float roundValue)
{
DoAddBand(colour, minValue, maxValue, true, roundValue);
}
/** Output this element to a stream.
*/
std::ostream &Output(std::ostream &ostream) const;
};
inline std::ostream &operator<<(std::ostream &out, const dash_gauge_t& gauge)
{
return gauge.Output(out);
}
/** /**
* DashboardConfig: * DashboardConfig:

View File

@ -71,7 +71,12 @@ WebSocketHandler::WebSocketHandler(mg_connection* nc, size_t slot, size_t modifi
m_jobqueue_overflow_dropcnt = 0; m_jobqueue_overflow_dropcnt = 0;
m_jobqueue_overflow_dropcntref = 0; m_jobqueue_overflow_dropcntref = 0;
m_job.type = WSTX_None; m_job.type = WSTX_None;
m_sent = m_ack = 0; m_sent = m_ack = m_last = 0;
m_units_subscribed = false;
m_units_prefs_subscribed = false;
MyMetrics.InitialiseSlot(m_slot);
MyUnitConfig.InitialiseSlot(m_slot);
// Register as logging console: // Register as logging console:
SetMonitoring(true); SetMonitoring(true);
@ -116,39 +121,42 @@ void WebSocketHandler::ProcessTxJob()
case WSTX_MetricsAll: case WSTX_MetricsAll:
case WSTX_MetricsUpdate: case WSTX_MetricsUpdate:
{ {
// Note: this loops over the metrics by index, keeping the checked count // Note: this loops over the metrics by index, keeping the last checked position
// in m_sent. It will not detect new metrics added between polls if they are // in m_last. It will not detect new metrics added between polls if they are
// inserted before m_sent, so new metrics may not be sent until first changed. // inserted before m_last, so new metrics may not be sent until first changed.
// The Metrics set normally is static, so this should be no problem. // The Metrics set normally is static, so this should be no problem.
// find start: // find start:
int i; int i;
OvmsMetric* m; OvmsMetric* m;
for (i=0, m=MyMetrics.m_first; i < m_sent && m != NULL; m=m->m_next, i++); for (i=0, m=MyMetrics.m_first; i < m_last && m != NULL; m=m->m_next, i++);
// build msg: // build msg:
std::string msg; if (m) {
msg.reserve(2*XFER_CHUNK_SIZE+128); std::string msg;
msg = "{\"metrics\":{"; msg.reserve(2*XFER_CHUNK_SIZE+128);
for (i=0; m && msg.size() < XFER_CHUNK_SIZE; m=m->m_next) { msg = "{\"metrics\":{";
if (m->IsModifiedAndClear(m_modifier) || m_job.type == WSTX_MetricsAll) { for (i=0; m && msg.size() < XFER_CHUNK_SIZE; m=m->m_next) {
if (i) msg += ','; ++m_last;
msg += '\"'; if (m->IsModifiedAndClear(m_modifier) || m_job.type == WSTX_MetricsAll) {
msg += m->m_name; if (i) msg += ',';
msg += "\":"; msg += '\"';
msg += m->AsJSON(); msg += m->m_name;
i++; msg += "\":";
msg += m->AsJSON();
i++;
}
}
// send msg:
if (i) {
msg += "}}";
ESP_EARLY_LOGV(TAG, "WebSocket msg: %s", msg.c_str());
mg_send_websocket_frame(m_nc, WEBSOCKET_OP_TEXT, msg.data(), msg.size());
m_sent += i;
} }
} }
// send msg:
if (i) {
msg += "}}";
ESP_EARLY_LOGV(TAG, "WebSocket msg: %s", msg.c_str());
mg_send_websocket_frame(m_nc, WEBSOCKET_OP_TEXT, msg.data(), msg.size());
m_sent += i;
}
// done? // done?
if (!m && m_ack == m_sent) { if (!m && m_ack == m_sent) {
if (m_sent) if (m_sent)
@ -158,7 +166,135 @@ void WebSocketHandler::ProcessTxJob()
break; break;
} }
case WSTX_UnitMetricUpdate:
{
// Note: this loops over the metrics by index, keeping the last checked position
// in m_last. It will not detect new metrics added between polls if they are
// inserted before m_last, so new metrics may not be sent until first changed.
// The Metrics set normally is static, so this should be no problem.
ESP_EARLY_LOGD(TAG, "WebSocketHandler[%p/%d]: ProcessTxJob MetricsUnitUpdate, last=%d sent=%d ack=%d", m_nc, m_modifier, m_last, m_sent, m_ack);
// find start:
int i;
OvmsMetric* m;
for (i=0, m=MyMetrics.m_first; i < m_last && m != NULL; m=m->m_next, i++);
ESP_EARLY_LOGD(TAG, "WebSocketHandler[%p/%d]: ProcessTxJob MetricsUnitUpdate, i=%d", m_nc, m_modifier, i);
if (m) { // Bypass this if we are on the 'just sent' leg.
// build msg:
std::string msg;
msg.reserve(2*XFER_CHUNK_SIZE+128);
msg = "{\"units\":{\"metrics\":{";
// Cache the user mappings for each group.
for (i=0; m && msg.size() < XFER_CHUNK_SIZE; m=m->m_next) {
++m_last;
bool send = m->IsUnitSendAndClear(m_modifier);
if (send) {
if (i)
msg += ',';
metric_unit_t units = m->m_units;
metric_unit_t user_units = MyUnitConfig.GetUserUnit(units);
if (user_units == UnitNotFound)
user_units = Native;
std::string unitlabel = OvmsMetricUnitLabel((user_units == Native) ? units : user_units);
const char *metricname = (units == Native) ? "Other" : OvmsMetricUnitName(units);
if (metricname == NULL)
metricname = "";
const char *user_metricname = (user_units == Native) ? metricname : OvmsMetricUnitName(user_units);
if (user_metricname == NULL)
user_metricname = metricname;
std::string entry = string_format("\"%s\":{\"native\":\"%s\",\"code\":\"%s\",\"label\":\"%s\"}",
m->m_name, metricname, user_metricname, json_encode(unitlabel).c_str()
);
msg += entry;
i++;
}
}
// send msg:
if (i) {
msg += "}}}";
ESP_EARLY_LOGD(TAG, "WebSocket msg: %s", msg.c_str());
mg_send_websocket_frame(m_nc, WEBSOCKET_OP_TEXT, msg.data(), msg.size());
m_sent += i;
}
}
// done?
if (!m && m_ack == m_sent) {
if (m_sent)
ESP_EARLY_LOGD(TAG, "WebSocketHandler[%p/%d]: ProcessTxJob MetricsUnitsUpdate done, sent=%d metrics", m_nc, m_modifier, m_sent);
ClearTxJob(m_job);
}
break;
}
case WSTX_UnitPrefsUpdate:
{
// Note: this loops over the metrics by index, keeping the last checked position
// in m_last. It will not detect new metrics added between polls if they are
// inserted before m_last, so new metrics may not be sent until first changed.
// The Metrics set normally is static, so this should be no problem.
ESP_EARLY_LOGD(TAG, "WebSocketHandler[%p/%d]: ProcessTxJob MetricsVehicleUpdate, last=%d sent=%d ack=%d", m_nc, m_modifier, m_last, m_sent, m_ack);
if (m_last < MyUnitConfig.config_groups.size()) {
// Bypass this if we are on the 'just sent' leg.
// build msg:
std::string msg;
msg.reserve(2*XFER_CHUNK_SIZE+128);
msg = "{\"units\":{\"prefs\":{";
// Cache the user mappings for each group.
int i = 0;
for (int groupindex = m_last;
groupindex < MyUnitConfig.config_groups.size() && msg.size() < XFER_CHUNK_SIZE;
++groupindex) {
++m_last;
metric_group_t group = MyUnitConfig.config_groups[groupindex];
bool send = MyUnitConfig.IsModifiedAndClear(group, m_modifier);
if (send) {
metric_unit_t user_units = MyUnitConfig.GetUserUnit(group);
std::string unitLabel;
if (user_units == UnitNotFound)
unitLabel = "null";
else {
unitLabel = '"';
unitLabel += json_encode(std::string(OvmsMetricUnitLabel(user_units)));
unitLabel += '"';
}
const char *groupName = OvmsMetricGroupName(group);
const char *unitName = (user_units == Native) ? "Native" : OvmsMetricUnitName(user_units);
std::string entry = string_format("%s\"%s\":{\"unit\":\"%s\",\"label\":%s}",
i ? "," : "",
groupName, unitName, unitLabel.c_str()
);
msg += entry;
i++;
}
}
// send msg:
if (i) {
msg += "}}}";
ESP_EARLY_LOGD(TAG, "WebSocket msg: %s", msg.c_str());
mg_send_websocket_frame(m_nc, WEBSOCKET_OP_TEXT, msg.data(), msg.size());
m_sent += i;
}
}
// done?
if (m_last >= MyUnitConfig.config_groups.size() && m_ack == m_sent) {
if (m_sent)
ESP_EARLY_LOGD(TAG, "WebSocketHandler[%p/%d]: ProcessTxJob MetricsUnitsUpdate done, sent=%d metrics", m_nc, m_modifier, m_sent);
ClearTxJob(m_job);
}
break;
}
case WSTX_Notify: case WSTX_Notify:
{ {
if (m_sent && m_ack == m_job.notification->GetValueSize()+1) { if (m_sent && m_ack == m_job.notification->GetValueSize()+1) {
@ -300,7 +436,7 @@ bool WebSocketHandler::GetNextTxJob()
if (!m_jobqueue) return false; if (!m_jobqueue) return false;
if (xQueueReceive(m_jobqueue, &m_job, 0) == pdTRUE) { if (xQueueReceive(m_jobqueue, &m_job, 0) == pdTRUE) {
// init new job state: // init new job state:
m_sent = m_ack = 0; m_sent = m_ack = m_last = 0;
return true; return true;
} else { } else {
return false; return false;
@ -610,17 +746,31 @@ void OvmsWebServer::UpdateTicker(TimerHandle_t timer)
break; break;
} }
} }
// trigger metrics update: // trigger metrics update if required.
unsigned long mask_all = MyMetrics.GetUnitSendAll();
for (auto slot: MyWebServer.m_client_slots) { for (auto slot: MyWebServer.m_client_slots) {
if (slot.handler) if (slot.handler) {
slot.handler->AddTxJob({ WSTX_MetricsUpdate, NULL }); slot.handler->AddTxJob({ WSTX_MetricsUpdate, NULL });
if (slot.handler->m_units_subscribed) {
unsigned long bit = 1ul << slot.handler->m_modifier;
bool addJob = (bit & mask_all) != 0;
if (addJob) {
// Trigger Units update:
slot.handler->AddTxJob({ WSTX_UnitMetricUpdate, NULL });
}
}
if (slot.handler->m_units_prefs_subscribed) {
// Triger unit group config update.
if (MyUnitConfig.HasModified(slot.handler->m_modifier))
slot.handler->AddTxJob({ WSTX_UnitPrefsUpdate, NULL });
}
}
} }
xSemaphoreGive(MyWebServer.m_client_mutex); xSemaphoreGive(MyWebServer.m_client_mutex);
} }
/** /**
* Notifications: * Notifications:
*/ */
@ -642,18 +792,57 @@ void WebSocketHandler::Subscribe(std::string topic)
} }
m_subscriptions.insert(topic); m_subscriptions.insert(topic);
ESP_LOGD(TAG, "WebSocketHandler[%p]: subscription '%s' added", m_nc, topic.c_str()); ESP_LOGD(TAG, "WebSocketHandler[%p]: subscription '%s' added", m_nc, topic.c_str());
SubscriptionChanged();
} }
void WebSocketHandler::Unsubscribe(std::string topic) void WebSocketHandler::Unsubscribe(std::string topic)
{ {
bool changed = false;
for (auto it = m_subscriptions.begin(); it != m_subscriptions.end();) { for (auto it = m_subscriptions.begin(); it != m_subscriptions.end();) {
if (mg_mqtt_match_topic_expression(mg_mk_str(topic.c_str()), mg_mk_str((*it).c_str()))) { if (mg_mqtt_match_topic_expression(mg_mk_str(topic.c_str()), mg_mk_str((*it).c_str()))) {
ESP_LOGD(TAG, "WebSocketHandler[%p]: subscription '%s' removed", m_nc, (*it).c_str()); ESP_LOGD(TAG, "WebSocketHandler[%p]: subscription '%s' removed", m_nc, (*it).c_str());
it = m_subscriptions.erase(it); it = m_subscriptions.erase(it);
changed = true;
} else { } else {
it++; it++;
} }
} }
if (changed)
SubscriptionChanged();
}
void WebSocketHandler::SubscriptionChanged()
{
UnitsCheckSubscribe();
UnitsCheckVehicleSubscribe();
}
void WebSocketHandler::UnitsCheckSubscribe()
{
bool newSubscribe = IsSubscribedTo("units/metrics");
if (newSubscribe != m_units_subscribed) {
m_units_subscribed = newSubscribe;
if (newSubscribe) {
ESP_LOGD(TAG, "WebSocketHandler[%p/%d]: Subscribed to units/metrics", m_nc, m_modifier);
MyMetrics.SetAllUnitSend(m_modifier);
} else {
ESP_LOGD(TAG, "WebSocketHandler[%p/%d]: Unsubscribed from units/metrics", m_nc, m_modifier);
}
}
}
void WebSocketHandler::UnitsCheckVehicleSubscribe()
{
bool newSubscribe = IsSubscribedTo("units/prefs");
if (newSubscribe != m_units_prefs_subscribed) {
m_units_prefs_subscribed = newSubscribe;
if (newSubscribe) {
ESP_LOGD(TAG, "WebSocketHandler[%p/%d]: Subscribed to units/prefs", m_nc, m_modifier);
MyUnitConfig.InitialiseSlot(m_modifier);
} else {
ESP_LOGD(TAG, "WebSocketHandler[%p/%d]: Unsubscribed from units/prefs", m_nc, m_modifier);
}
}
} }
bool WebSocketHandler::IsSubscribedTo(std::string topic) bool WebSocketHandler::IsSubscribedTo(std::string topic)

View File

@ -74,6 +74,12 @@ void OvmsWebServer::HandleStatus(PageEntry_t& p, PageContext_t& c)
c.done(); c.done();
return; return;
} }
else {
// "network restart", "wifi reconnect"
OutputReconnect(p, c, NULL, cmd.c_str());
c.done();
return;
}
} }
PAGE_HOOK("body.pre"); PAGE_HOOK("body.pre");
@ -209,7 +215,10 @@ void OvmsWebServer::HandleStatus(PageEntry_t& p, PageContext_t& c)
c.panel_start("primary", "Network"); c.panel_start("primary", "Network");
output = ExecuteCommand("network status"); output = ExecuteCommand("network status");
c.printf("<samp class=\"monitor\" data-updcmd=\"network status\" data-events=\"^network\">%s</samp>", _html(output)); c.printf("<samp class=\"monitor\" data-updcmd=\"network status\" data-events=\"^network\">%s</samp>", _html(output));
c.panel_end(); c.panel_end(
"<ul class=\"list-inline\">"
"<li><button type=\"button\" class=\"btn btn-default btn-sm\" name=\"action\" value=\"network restart\">Restart network</button></li>"
"</ul>");
c.print( c.print(
"</div>" "</div>"
@ -218,7 +227,10 @@ void OvmsWebServer::HandleStatus(PageEntry_t& p, PageContext_t& c)
c.panel_start("primary", "Wifi"); c.panel_start("primary", "Wifi");
output = ExecuteCommand("wifi status"); output = ExecuteCommand("wifi status");
c.printf("<samp class=\"monitor\" data-updcmd=\"wifi status\" data-events=\"\\.wifi\\.\">%s</samp>", _html(output)); c.printf("<samp class=\"monitor\" data-updcmd=\"wifi status\" data-events=\"\\.wifi\\.\">%s</samp>", _html(output));
c.panel_end(); c.panel_end(
"<ul class=\"list-inline\">"
"<li><button type=\"button\" class=\"btn btn-default btn-sm\" name=\"action\" value=\"wifi reconnect\">Reconnect Wifi</button></li>"
"</ul>");
c.print( c.print(
"</div>" "</div>"
@ -229,8 +241,10 @@ void OvmsWebServer::HandleStatus(PageEntry_t& p, PageContext_t& c)
c.printf("<samp class=\"monitor\" data-updcmd=\"cellular status\" data-events=\"\\.modem\\.\">%s</samp>", _html(output)); c.printf("<samp class=\"monitor\" data-updcmd=\"cellular status\" data-events=\"\\.modem\\.\">%s</samp>", _html(output));
c.panel_end( c.panel_end(
"<ul class=\"list-inline\">" "<ul class=\"list-inline\">"
"<li><button type=\"button\" class=\"btn btn-default btn-sm\" data-target=\"#modem-cmdres\" data-cmd=\"power cellular on\">Start cellular modem</button></li>" "<li><button type=\"button\" class=\"btn btn-default btn-sm\" data-target=\"#modem-cmdres\" data-cmd=\"power cellular on\">Start modem</button></li>"
"<li><button type=\"button\" class=\"btn btn-default btn-sm\" data-target=\"#modem-cmdres\" data-cmd=\"power cellular off\">Stop cellular modem</button></li>" "<li><button type=\"button\" class=\"btn btn-default btn-sm\" data-target=\"#modem-cmdres\" data-cmd=\"power cellular off\">Stop modem</button></li>"
"<li><button type=\"button\" class=\"btn btn-default btn-sm\" data-target=\"#modem-cmdres\" data-cmd=\"cellular gps start\">Start GPS</button></li>"
"<li><button type=\"button\" class=\"btn btn-default btn-sm\" data-target=\"#modem-cmdres\" data-cmd=\"cellular gps stop\">Stop GPS</button></li>"
"<li><samp id=\"modem-cmdres\" class=\"samp-inline\"></samp></li>" "<li><samp id=\"modem-cmdres\" class=\"samp-inline\"></samp></li>"
"</ul>"); "</ul>");
@ -591,9 +605,13 @@ void OvmsWebServer::HandleCfgPassword(PageEntry_t& p, PageContext_t& c)
void OvmsWebServer::HandleCfgVehicle(PageEntry_t& p, PageContext_t& c) void OvmsWebServer::HandleCfgVehicle(PageEntry_t& p, PageContext_t& c)
{ {
std::string error, info; std::string error, info;
std::string vehicleid, vehicletype, vehiclename, timezone, timezone_region, units_distance, pin; std::string vehicleid, vehicletype, vehiclename, timezone, timezone_region, pin;
std::string bat12v_factor, bat12v_ref, bat12v_alert; std::string bat12v_factor, bat12v_ref, bat12v_alert;
std::map<metric_group_t,std::string> units_values;
metric_group_list_t unit_groups;
OvmsMetricGroupConfigList(unit_groups);
if (c.method == "POST") { if (c.method == "POST") {
// process form submission: // process form submission:
vehicleid = c.getvar("vehicleid"); vehicleid = c.getvar("vehicleid");
@ -601,7 +619,13 @@ void OvmsWebServer::HandleCfgVehicle(PageEntry_t& p, PageContext_t& c)
vehiclename = c.getvar("vehiclename"); vehiclename = c.getvar("vehiclename");
timezone = c.getvar("timezone"); timezone = c.getvar("timezone");
timezone_region = c.getvar("timezone_region"); timezone_region = c.getvar("timezone_region");
units_distance = c.getvar("units_distance"); for ( auto grpiter = unit_groups.begin(); grpiter != unit_groups.end(); ++grpiter) {
std::string name = OvmsMetricGroupName(*grpiter);
std::string cfg = "units_";
cfg += name;
units_values[*grpiter] = c.getvar(cfg);
}
bat12v_factor = c.getvar("bat12v_factor"); bat12v_factor = c.getvar("bat12v_factor");
bat12v_ref = c.getvar("bat12v_ref"); bat12v_ref = c.getvar("bat12v_ref");
bat12v_alert = c.getvar("bat12v_alert"); bat12v_alert = c.getvar("bat12v_alert");
@ -627,7 +651,12 @@ void OvmsWebServer::HandleCfgVehicle(PageEntry_t& p, PageContext_t& c)
MyConfig.SetParamValue("vehicle", "name", vehiclename); MyConfig.SetParamValue("vehicle", "name", vehiclename);
MyConfig.SetParamValue("vehicle", "timezone", timezone); MyConfig.SetParamValue("vehicle", "timezone", timezone);
MyConfig.SetParamValue("vehicle", "timezone_region", timezone_region); MyConfig.SetParamValue("vehicle", "timezone_region", timezone_region);
MyConfig.SetParamValue("vehicle", "units.distance", units_distance); for ( auto grpiter = unit_groups.begin(); grpiter != unit_groups.end(); ++grpiter) {
std::string name = OvmsMetricGroupName(*grpiter);
std::string value = units_values[*grpiter];
OvmsMetricSetUserConfig(*grpiter, value);
}
MyConfig.SetParamValue("system.adc", "factor12v", bat12v_factor); MyConfig.SetParamValue("system.adc", "factor12v", bat12v_factor);
MyConfig.SetParamValue("vehicle", "12v.ref", bat12v_ref); MyConfig.SetParamValue("vehicle", "12v.ref", bat12v_ref);
MyConfig.SetParamValue("vehicle", "12v.alert", bat12v_alert); MyConfig.SetParamValue("vehicle", "12v.alert", bat12v_alert);
@ -656,7 +685,8 @@ void OvmsWebServer::HandleCfgVehicle(PageEntry_t& p, PageContext_t& c)
vehiclename = MyConfig.GetParamValue("vehicle", "name"); vehiclename = MyConfig.GetParamValue("vehicle", "name");
timezone = MyConfig.GetParamValue("vehicle", "timezone"); timezone = MyConfig.GetParamValue("vehicle", "timezone");
timezone_region = MyConfig.GetParamValue("vehicle", "timezone_region"); timezone_region = MyConfig.GetParamValue("vehicle", "timezone_region");
units_distance = MyConfig.GetParamValue("vehicle", "units.distance"); for ( auto grpiter = unit_groups.begin(); grpiter != unit_groups.end(); ++grpiter)
units_values[*grpiter] = OvmsMetricGetUserConfig(*grpiter);
bat12v_factor = MyConfig.GetParamValue("system.adc", "factor12v"); bat12v_factor = MyConfig.GetParamValue("system.adc", "factor12v");
bat12v_ref = MyConfig.GetParamValue("vehicle", "12v.ref"); bat12v_ref = MyConfig.GetParamValue("vehicle", "12v.ref");
bat12v_alert = MyConfig.GetParamValue("vehicle", "12v.alert"); bat12v_alert = MyConfig.GetParamValue("vehicle", "12v.alert");
@ -703,10 +733,40 @@ void OvmsWebServer::HandleCfgVehicle(PageEntry_t& p, PageContext_t& c)
, _attr(timezone_region) , _attr(timezone_region)
, _attr(timezone)); , _attr(timezone));
c.input_radiobtn_start("Distance units", "units_distance"); for ( auto grpiter = unit_groups.begin(); grpiter != unit_groups.end(); ++grpiter) {
c.input_radiobtn_option("units_distance", "Kilometers", "K", units_distance == "K"); std::string name = OvmsMetricGroupName(*grpiter);
c.input_radiobtn_option("units_distance", "Miles", "M", units_distance == "M"); metric_unit_set_t group_units;
c.input_radiobtn_end(); if (OvmsMetricGroupUnits(*grpiter,group_units)) {
bool use_select = group_units.size() > 3;
std::string cfg = "units_";
cfg += name;
std::string value = units_values[*grpiter];
if (use_select)
c.input_select_start(OvmsMetricGroupLabel(*grpiter), cfg.c_str() );
else
c.input_radiobtn_start(OvmsMetricGroupLabel(*grpiter), cfg.c_str() );
bool checked = value.empty();
if (use_select)
c.input_select_option( "Default", "", checked);
else
c.input_radiobtn_option(cfg.c_str(), "Default", "", checked);
for (auto unititer = group_units.begin(); unititer != group_units.end(); ++unititer) {
const char* unit_name = OvmsMetricUnitName(*unititer);
const char* unit_label = OvmsMetricUnitLabel(*unititer);
checked = value == unit_name;
if (use_select)
c.input_select_option( unit_label, unit_name, checked);
else
c.input_radiobtn_option(cfg.c_str(), unit_label, unit_name, checked);
}
if (use_select)
c.input_select_end();
else
c.input_radiobtn_end();
}
}
c.input_password("PIN", "pin", "", "empty = no change", c.input_password("PIN", "pin", "", "empty = no change",
"<p>Vehicle PIN code used for unlocking etc.</p>", "autocomplete=\"section-vehiclepin new-password\""); "<p>Vehicle PIN code used for unlocking etc.</p>", "autocomplete=\"section-vehiclepin new-password\"");
@ -963,8 +1023,7 @@ void OvmsWebServer::HandleCfgPushover(PageEntry_t& p, PageContext_t& c)
} }
if (error == "") { if (error == "") {
if (c.getvar("action") == "save") if (c.getvar("action") == "save") {
{
// save: // save:
param->m_map.clear(); param->m_map.clear();
param->m_map = std::move(pmap); param->m_map = std::move(pmap);
@ -975,9 +1034,8 @@ void OvmsWebServer::HandleCfgPushover(PageEntry_t& p, PageContext_t& c)
OutputHome(p, c); OutputHome(p, c);
c.done(); c.done();
return; return;
} } else if (c.getvar("action") == "test")
else if (c.getvar("action") == "test") {
{
std::string reply; std::string reply;
std::string popup; std::string popup;
c.head(200); c.head(200);
@ -991,10 +1049,10 @@ void OvmsWebServer::HandleCfgPushover(PageEntry_t& p, PageContext_t& c)
atoi(c.getvar("retry").c_str()), atoi(c.getvar("retry").c_str()),
atoi(c.getvar("expire").c_str()), atoi(c.getvar("expire").c_str()),
true /* receive server reply as reply/pushover-type notification */ )) true /* receive server reply as reply/pushover-type notification */ ))
{ {
c.alert("danger", "<p class=\"lead\">Could not send test message!</p>"); c.alert("danger", "<p class=\"lead\">Could not send test message!</p>");
}
} }
}
} }
else { else {
// output error, return to form: // output error, return to form:
@ -1163,7 +1221,7 @@ void OvmsWebServer::HandleCfgPushover(PageEntry_t& p, PageContext_t& c)
"<td><button type=\"button\" class=\"btn btn-danger\" onclick=\"delRow(this)\"><strong>✖</strong></button></td>" "<td><button type=\"button\" class=\"btn btn-danger\" onclick=\"delRow(this)\"><strong>✖</strong></button></td>"
"<td><input type=\"text\" class=\"form-control\" name=\"nfy_%d\" value=\"%s\" placeholder=\"Enter notification type/subtype\"" "<td><input type=\"text\" class=\"form-control\" name=\"nfy_%d\" value=\"%s\" placeholder=\"Enter notification type/subtype\""
" autocomplete=\"section-notification-type\"></td>" " autocomplete=\"section-notification-type\"></td>"
"<td width=\"20%\"><select class=\"form-control\" name=\"np_%d\" size=\"1\">" "<td width=\"20%%\"><select class=\"form-control\" name=\"np_%d\" size=\"1\">"
, max, _attr(name) , max, _attr(name)
, max); , max);
gen_options_priority(kv.second); gen_options_priority(kv.second);
@ -3931,7 +3989,7 @@ void OvmsWebServer::HandleEditor(PageEntry_t& p, PageContext_t& c)
"text-align: center !important;\n" "text-align: center !important;\n"
"}\n" "}\n"
"}\n" "}\n"
".log { font-size: 87%; color: gray; }\n" ".log { font-size: 87%%; color: gray; }\n"
".log.log-I { color: green; }\n" ".log.log-I { color: green; }\n"
".log.log-W { color: darkorange; }\n" ".log.log-W { color: darkorange; }\n"
".log.log-E { color: red; }\n" ".log.log-E { color: red; }\n"

View File

@ -885,7 +885,7 @@ std::string OvmsWebServer::CfgInit3(PageEntry_t& p, PageContext_t& c, std::strin
std::string OvmsWebServer::CfgInit4(PageEntry_t& p, PageContext_t& c, std::string step) std::string OvmsWebServer::CfgInit4(PageEntry_t& p, PageContext_t& c, std::string step)
{ {
std::string error, info; std::string error, info;
std::string vehicletype, units_distance; std::string vehicletype, units_distance, units_temp, units_pressure;
std::string server, vehicleid, password; std::string server, vehicleid, password;
if (c.method == "POST") { if (c.method == "POST") {
@ -905,6 +905,8 @@ std::string OvmsWebServer::CfgInit4(PageEntry_t& p, PageContext_t& c, std::strin
// process form input: // process form input:
vehicletype = c.getvar("vehicletype"); vehicletype = c.getvar("vehicletype");
units_distance = c.getvar("units_distance"); units_distance = c.getvar("units_distance");
units_temp = c.getvar("units_temp");
units_pressure = c.getvar("units_pressure");
server = c.getvar("server"); server = c.getvar("server");
vehicleid = c.getvar("vehicleid"); vehicleid = c.getvar("vehicleid");
password = c.getvar("password"); password = c.getvar("password");
@ -917,7 +919,22 @@ std::string OvmsWebServer::CfgInit4(PageEntry_t& p, PageContext_t& c, std::strin
error += "<li data-input=\"vehicleid\">Vehicle ID may only contain ASCII letters, digits and '-'</li>"; error += "<li data-input=\"vehicleid\">Vehicle ID may only contain ASCII letters, digits and '-'</li>";
// configure vehicle: // configure vehicle:
MyConfig.SetParamValue("vehicle", "units.distance", units_distance); OvmsMetricSetUserConfig(GrpDistance, units_distance);
if (units_distance == "miles") {
OvmsMetricSetUserConfig(GrpDistanceShort, "feet");
OvmsMetricSetUserConfig(GrpSpeed, "miph");
OvmsMetricSetUserConfig(GrpAccel, "miphps");
OvmsMetricSetUserConfig(GrpAccelShort, "ftpss");
OvmsMetricSetUserConfig(GrpConsumption, "mipkwh");
} else {
// Set to their defaults.
OvmsMetricSetUserConfig(GrpDistanceShort, "");
OvmsMetricSetUserConfig(GrpSpeed, "");
OvmsMetricSetUserConfig(GrpAccel, "");
OvmsMetricSetUserConfig(GrpAccelShort, "");
OvmsMetricSetUserConfig(GrpConsumption, "");
}
MyConfig.SetParamValue("auto", "vehicle.type", vehicletype); MyConfig.SetParamValue("auto", "vehicle.type", vehicletype);
// configure server: // configure server:
@ -945,14 +962,17 @@ std::string OvmsWebServer::CfgInit4(PageEntry_t& p, PageContext_t& c, std::strin
// read configuration: // read configuration:
vehicleid = MyConfig.GetParamValue("vehicle", "id"); vehicleid = MyConfig.GetParamValue("vehicle", "id");
vehicletype = MyConfig.GetParamValue("auto", "vehicle.type"); vehicletype = MyConfig.GetParamValue("auto", "vehicle.type");
units_distance = MyConfig.GetParamValue("vehicle", "units.distance", "K"); units_distance = OvmsMetricGetUserConfig(GrpDistance);
units_temp = OvmsMetricGetUserConfig(GrpTemp);
units_pressure = OvmsMetricGetUserConfig(GrpPressure);
server = MyConfig.GetParamValue("server.v2", "server"); server = MyConfig.GetParamValue("server.v2", "server");
password = MyConfig.GetParamValue("password","server.v2"); password = MyConfig.GetParamValue("password","server.v2");
// default data server = ota server: // default data server = ota server:
if (server.empty()) { if (server.empty()) {
server = MyConfig.GetParamValue("ota", "server"); server = MyConfig.GetParamValue("ota", "server");
if (startsWith(server, "ovms-ota.bit-cloud.de")) if (startsWith(server, "ovms-ota.bit-cloud.de"))
server = "ovms-server.bit-cloud.de"; server = "ovms-server.bit-cloud.de";
else else
server = "ovms.dexters-web.de"; server = "ovms.dexters-web.de";
@ -1029,9 +1049,22 @@ std::string OvmsWebServer::CfgInit4(PageEntry_t& p, PageContext_t& c, std::strin
c.input_select_option(k->second.name, k->first, (vehicletype == k->first)); c.input_select_option(k->second.name, k->first, (vehicletype == k->first));
c.input_select_end(); c.input_select_end();
c.input_radiobtn_start("Distance units", "units_distance"); bool is_metric = units_distance != "miles";
c.input_radiobtn_option("units_distance", "Kilometers", "K", units_distance == "K"); c.input_radiobtn_start("Distance related units", "units_distance");
c.input_radiobtn_option("units_distance", "Miles", "M", units_distance == "M"); c.input_radiobtn_option("units_distance", "Metric (km & metres)", "", is_metric);
c.input_radiobtn_option("units_distance", "Imperial (miles & feet)", "miles", !is_metric);
c.input_radiobtn_end();
is_metric = units_temp != "fahrenheit";
c.input_radiobtn_start("Temperature units", "units_temp");
c.input_radiobtn_option("units_temp", "Metric (°C)", "", is_metric);
c.input_radiobtn_option("units_temp", "Imperial (°F)", "fahrenheit", !is_metric);
c.input_radiobtn_end();
is_metric = units_pressure != "psi";
c.input_radiobtn_start("Pressure units", "units_pressure");
c.input_radiobtn_option("units_pressure", "Metric (kPa)", "", is_metric);
c.input_radiobtn_option("units_pressure", "Imperial (PSI)", "psi", !is_metric);
c.input_radiobtn_end(); c.input_radiobtn_end();
c.input_radio_start("OVMS data server", "server"); c.input_radio_start("OVMS data server", "server");

View File

@ -305,7 +305,7 @@ void OvmsWebServer::HandleDashboard(PageEntry_t& p, PageContext_t& c)
"</div>" "</div>"
"<div class=\"underlay\">" "<div class=\"underlay\">"
"<div class=\"voltage-value\"><span class=\"value\">0</span></div>" "<div class=\"voltage-value\"><span class=\"value\">0</span></div>"
"<div class=\"soc-value\"><span class=\"value\">0</span></div>" "<div class=\"soc-value\"><span class=\"value\">0</span><span class=\"unit\">%</span></div>"
"<div class=\"consumption-value\"><span class=\"value\">0</span></div>" "<div class=\"consumption-value\"><span class=\"value\">0</span></div>"
"<div class=\"power-value\"><span class=\"value\">0</span></div>" "<div class=\"power-value\"><span class=\"value\">0</span></div>"
"</div>" "</div>"
@ -320,29 +320,29 @@ void OvmsWebServer::HandleDashboard(PageEntry_t& p, PageContext_t& c)
"var gaugeset1;" "var gaugeset1;"
"" ""
"function get_dashboard_data() {" "function get_dashboard_data() {"
"var rmin = metrics[\"v.b.range.est\"]||0, rmax = metrics[\"v.b.range.ideal\"]||0;\n" "var rmin = metrics_user[\"v.b.range.est\"]||0, rmax = metrics_user[\"v.b.range.ideal\"]||0;\n"
"var euse = metrics[\"v.b.energy.used\"]||0, erec = metrics[\"v.b.energy.recd\"]||0;\n" "var euse = metrics_user[\"v.b.energy.used\"]||0, erec = metrics_user[\"v.b.energy.recd\"]||0;\n"
"var voltage = metrics[\"v.b.voltage\"]||0, soc = metrics[\"v.b.soc\"]||0;\n" "var voltage = metrics[\"v.b.voltage\"]||0, soc = metrics_user[\"v.b.soc\"]||0;\n"
"var consumption = metrics[\"v.b.consumption\"]||0, power = metrics[\"v.b.power\"]||0;\n" "var consumption = metrics_user[\"v.b.consumption\"]||0, power = metrics_user[\"v.b.power\"]||0;\n"
"euse = Math.floor(euse*10)/10; erec = Math.floor(erec*10)/10;" "euse = Math.floor(euse*10)/10; erec = Math.floor(erec*10)/10;"
"if (rmin > rmax) { var x = rmin; rmin = rmax; rmax = x; }" "if (rmin > rmax) { var x = rmin; rmin = rmax; rmax = x; }"
"var md = {" "var md = {"
"range: { value: \"\" + rmin.toFixed(0) + \"\" + rmax.toFixed(0) }," "range: { value: \"\" + rmin.toFixed(0) + \"\" + rmax.toFixed(0), unit: metrics_label[\"v.b.range.est\"] },"
"energy: { value: \"\" + euse.toFixed(1) + \"\" + erec.toFixed(1) }," "energy: { value: \"\" + euse.toFixed(1) + \"\" + erec.toFixed(1), unit: metrics_label[\"v.b.energy.used\"] },"
"voltage: { value: voltage.toFixed(0) }," "voltage: { value: voltage.toFixed(0) },"
"soc: { value: soc.toFixed(0) }," "soc: { value: soc.toFixed(0), unit: metrics_label[\"v.b.soc\"] },"
"consumption: { value: consumption.toFixed(0) }," "consumption: { value: consumption.toFixed(0), unit: metrics_label[\"v.b.consumption\"] },"
"power: { value: power.toFixed(0) }," "power: { value: power.toFixed(0), unit: metrics_label[\"v.b.power\"] },"
"series: [" "series: ["
"{ data: [metrics[\"v.p.speed\"]] }," "{ data: [metrics_user[\"v.p.speed\"]] },"
"{ data: [metrics[\"v.b.voltage\"]] }," "{ data: [metrics[\"v.b.voltage\"]] },"
"{ data: [metrics[\"v.b.soc\"]] }," "{ data: [metrics_user[\"v.b.soc\"]] },"
"{ data: [metrics[\"v.b.consumption\"]] }," "{ data: [metrics_user[\"v.b.consumption\"]] },"
"{ data: [metrics[\"v.b.power\"]] }," "{ data: [metrics_user[\"v.b.power\"]] },"
"{ data: [metrics[\"v.c.temp\"]] }," "{ data: [metrics_user[\"v.c.temp\"]] },"
"{ data: [metrics[\"v.b.temp\"]] }," "{ data: [metrics_user[\"v.b.temp\"]] },"
"{ data: [metrics[\"v.i.temp\"]] }," "{ data: [metrics_user[\"v.i.temp\"]] },"
"{ data: [metrics[\"v.m.temp\"]] }]," "{ data: [metrics_user[\"v.m.temp\"]] }],"
"};" "};"
"return md;" "return md;"
"}" "}"
@ -350,9 +350,12 @@ void OvmsWebServer::HandleDashboard(PageEntry_t& p, PageContext_t& c)
"function update_dashboard() {" "function update_dashboard() {"
"var md = get_dashboard_data();" "var md = get_dashboard_data();"
"$('.range-value .value').text(md.range.value);" "$('.range-value .value').text(md.range.value);"
"$('.range-value .unit').text(md.range.unit);"
"$('.energy-value .value').text(md.energy.value);" "$('.energy-value .value').text(md.energy.value);"
"$('.energy-value .unit').text(md.energy.unit);"
"$('.voltage-value .value').text(md.voltage.value);" "$('.voltage-value .value').text(md.voltage.value);"
"$('.soc-value .value').text(md.soc.value);" "$('.soc-value .value').text(md.soc.value);"
"$('.soc-value .unit').text(md.soc.unit);"
"$('.consumption-value .value').text(md.consumption.value);" "$('.consumption-value .value').text(md.consumption.value);"
"$('.power-value .value').text(md.power.value);" "$('.power-value .value').text(md.power.value);"
"gaugeset1.update({ series: md.series });" "gaugeset1.update({ series: md.series });"
@ -534,7 +537,7 @@ void OvmsWebServer::HandleDashboard(PageEntry_t& p, PageContext_t& c)
"" ""
"/* Inject vehicle config: */" "/* Inject vehicle config: */"
"for (var i = 0; i < chart_config.yAxis.length; i++) {" "for (var i = 0; i < chart_config.yAxis.length; i++) {"
"$.extend(chart_config.yAxis[i], vehicle_config.yAxis[i]);" "$.extend(true, chart_config.yAxis[i], vehicle_config.yAxis[i]);"
"}" "}"
"" ""
"gaugeset1 = Highcharts.chart('gaugeset1', chart_config," "gaugeset1 = Highcharts.chart('gaugeset1', chart_config,"
@ -1088,20 +1091,20 @@ void OvmsWebServer::HandleBmsCellMonitor(PageEntry_t& p, PageContext_t& c)
"// get_temp_data: build boxplot dataset from metrics\n" "// get_temp_data: build boxplot dataset from metrics\n"
"function get_temp_data() {\n" "function get_temp_data() {\n"
"var data = { cells: [], temps: [], devmax: [], tempmean: 0, sdlo: 0, sdhi: 0, sdmaxlo: 0, sdmaxhi: 0 };\n" "var data = { cells: [], temps: [], devmax: [], tempmean: 0, sdlo: 0, sdhi: 0, sdmaxlo: 0, sdmaxhi: 0 };\n"
"var cnt = metrics[\"v.b.c.temp\"] ? metrics[\"v.b.c.temp\"].length : 0;\n" "var cnt = metrics_user[\"v.b.c.temp\"] ? metrics_user[\"v.b.c.temp\"].length : 0;\n"
"if (cnt == 0)\n" "if (cnt == 0)\n"
"return data;\n" "return data;\n"
"var i, act, min, max, devmax, dalert, dlow, dhigh;\n" "var i, act, min, max, devmax, dalert, dlow, dhigh;\n"
"data.tempmean = metrics[\"v.b.p.temp.avg\"] || 0;\n" "data.tempmean = metrics_user[\"v.b.p.temp.avg\"] || 0;\n"
"data.sdlo = data.tempmean - (metrics[\"v.b.p.temp.stddev\"] || 0);\n" "data.sdlo = data.tempmean - (metrics_user[\"v.b.p.temp.stddev\"] || 0);\n"
"data.sdhi = data.tempmean + (metrics[\"v.b.p.temp.stddev\"] || 0);\n" "data.sdhi = data.tempmean + (metrics_user[\"v.b.p.temp.stddev\"] || 0);\n"
"data.sdmaxlo = data.tempmean - (metrics[\"v.b.p.temp.stddev.max\"] || 0);\n" "data.sdmaxlo = data.tempmean - (metrics_user[\"v.b.p.temp.stddev.max\"] || 0);\n"
"data.sdmaxhi = data.tempmean + (metrics[\"v.b.p.temp.stddev.max\"] || 0);\n" "data.sdmaxhi = data.tempmean + (metrics_user[\"v.b.p.temp.stddev.max\"] || 0);\n"
"for (i=0; i<cnt; i++) {\n" "for (i=0; i<cnt; i++) {\n"
"act = metrics[\"v.b.c.temp\"][i];\n" "act = metrics_user[\"v.b.c.temp\"][i];\n"
"min = metrics[\"v.b.c.temp.min\"][i] || act;\n" "min = metrics_user[\"v.b.c.temp.min\"][i] || act;\n"
"max = metrics[\"v.b.c.temp.max\"][i] || act;\n" "max = metrics_user[\"v.b.c.temp.max\"][i] || act;\n"
"devmax = metrics[\"v.b.c.temp.dev.max\"][i] || 0;\n" "devmax = metrics_user[\"v.b.c.temp.dev.max\"][i] || 0;\n"
"dalert = metrics[\"v.b.c.temp.alert\"][i] || 0;\n" "dalert = metrics[\"v.b.c.temp.alert\"][i] || 0;\n"
"if (devmax > 0) {\n" "if (devmax > 0) {\n"
"dlow = data.tempmean;\n" "dlow = data.tempmean;\n"

View File

@ -49,22 +49,32 @@
* & &amp; * & &amp;
*/ */
std::string PageContext::encode_html(const char* text) { std::string PageContext::encode_html(const char* text) {
std::string buf; int len = strlen(text);
for (int i=0; i<strlen(text); i++) { std::string buf;
if (text[i] == '\"') buf.reserve(len);
buf += "&quot;"; for (int i=0; i < len; i++) {
else if (text[i] == '\'') char ch = text[i];
buf += "&#x27;"; switch(ch) {
else if(text[i] == '<') case '\"':
buf += "&lt;"; buf.append("&quot;");
else if(text[i] == '>') break;
buf += "&gt;"; case '\'':
else if(text[i] == '&') buf.append("&#x27;");
buf += "&amp;"; break;
else case '<':
buf += text[i]; buf.append("&lt;");
break;
case '>':
buf.append("&gt;");
break;
case '&':
buf.append("&amp;");
break;
default:
buf.append(&ch,1);
}
} }
return buf; return buf;
} }
std::string PageContext::encode_html(std::string text) { std::string PageContext::encode_html(std::string text) {
@ -72,23 +82,31 @@ std::string PageContext::encode_html(std::string text) {
} }
extram::string PageContext::encode_html(const extram::string& text) { extram::string PageContext::encode_html(const extram::string& text) {
extram::string buf; extram::string buf;
buf.reserve(text.length() + 500); buf.reserve(text.length() + 500);
for (int i=0; i<text.length(); i++) { for (int i=0; i<text.length(); i++) {
if (text[i] == '\"') char ch = text[i];
buf += "&quot;"; switch (ch) {
else if (text[i] == '\'') case '\"':
buf += "&#x27;"; buf.append("&quot;");
else if(text[i] == '<') break;
buf += "&lt;"; case '\'':
else if(text[i] == '>') buf.append("&#x27;");
buf += "&gt;"; break;
else if(text[i] == '&') case '<':
buf += "&amp;"; buf.append("&lt;");
else break;
buf += text[i]; case '>':
buf.append("&gt;");
break;
case '&':
buf.append("&amp;");
break;
default:
buf.append(&ch,1);
}
} }
return buf; return buf;
} }
#define _attr(text) (encode_html(text).c_str()) #define _attr(text) (encode_html(text).c_str())
@ -102,11 +120,12 @@ extram::string PageContext::encode_html(const extram::string& text) {
* *
*/ */
std::string PageContext::make_id(const char* text) { std::string PageContext::make_id(const char* text) {
std::string buf; std::string buf;
char lc = 0; char lc = 0;
for (int i=0; i<strlen(text); i++) { int len = strlen(text);
if (isalnum(text[i])) for (int i=0; i<len; i++) {
buf += (lc = tolower(text[i])); if (isalnum(text[i]))
buf += (lc = tolower(text[i]));
else if (lc && lc != '-') else if (lc && lc != '-')
buf += (lc = '-'); buf += (lc = '-');
} }
@ -114,7 +133,7 @@ std::string PageContext::make_id(const char* text) {
lc = buf.back(); lc = buf.back();
buf.pop_back(); buf.pop_back();
} }
return buf; return buf;
} }
std::string PageContext::make_id(std::string text) { std::string PageContext::make_id(std::string text) {
@ -361,7 +380,7 @@ void PageContext::input_slider(const char* label, const char* name, int size, co
"<label class=\"control-label col-sm-3\" for=\"input-%s\">%s:</label>" "<label class=\"control-label col-sm-3\" for=\"input-%s\">%s:</label>"
"<div class=\"col-sm-9\">" "<div class=\"col-sm-9\">"
"<div class=\"form-control slider\" data-default=\"%g\" data-reset=\"false\"" "<div class=\"form-control slider\" data-default=\"%g\" data-reset=\"false\""
" data-value=\"%g\" data-min=\"%g\" data-max=\"%g\" data-step=\"%g\">" " data-value=\"%g\" data-min=\"%g\" data-max=\"%g\" data-step=\"%g\" data-checked=\"%s\">"
"<div class=\"slider-control form-inline\">" "<div class=\"slider-control form-inline\">"
"<input class=\"slider-enable\" type=\"%s\" %s> " "<input class=\"slider-enable\" type=\"%s\" %s> "
"<input class=\"form-control slider-value\" %s type=\"number\" style=\"width:%dpx;\"" "<input class=\"form-control slider-value\" %s type=\"number\" style=\"width:%dpx;\""
@ -380,6 +399,7 @@ void PageContext::input_slider(const char* label, const char* name, int size, co
, _attr(name) , _attr(name)
, label , label
, defval, value, min, max, step , defval, value, min, max, step
, (enabled != 0) ? "true" : "false"
, (enabled < 0) ? "hidden" : "checkbox" // -1 => no checkbox , (enabled < 0) ? "hidden" : "checkbox" // -1 => no checkbox
, (enabled > 0) ? "checked" : "" , (enabled > 0) ? "checked" : ""
, (enabled == 0) ? "disabled" : "" , (enabled == 0) ? "disabled" : ""
@ -714,7 +734,8 @@ void OvmsWebServer::OutputReboot(PageEntry_t& p, PageContext_t& c)
/** /**
* OutputReconnect: output reconnect script * OutputReconnect: output reconnect script
*/ */
void OvmsWebServer::OutputReconnect(PageEntry_t& p, PageContext_t& c, const char* info /*=NULL*/) void OvmsWebServer::OutputReconnect(PageEntry_t& p, PageContext_t& c, const char* info /*=NULL*/,
const char* cmd /*=NULL*/)
{ {
c.printf( c.printf(
"<div class=\"alert alert-warning\">" "<div class=\"alert alert-warning\">"
@ -733,8 +754,16 @@ void OvmsWebServer::OutputReconnect(PageEntry_t& p, PageContext_t& c, const char
"$(\"#dots\").append(\"\");" "$(\"#dots\").append(\"\");"
"}" "}"
"}, 1000);" "}, 1000);"
"</script>"
, info ? info : "Reconnecting…"); , info ? info : "Reconnecting…");
if (cmd) {
c.printf(
"loadcmd(\"%s\");"
, __attr(cmd));
}
c.print(
"</script>");
} }

View File

@ -76,7 +76,7 @@ void pushover_send_message(int verbosity, OvmsWriter* writer, OvmsCommand* cmd,
sound = "pushover"; sound = "pushover";
} }
writer->printf("Sending PUSHOVER message \"%s\" with priority %d and sound %s\n",argv[0],priority,sound); writer->printf("Sending PUSHOVER message \"%s\" with priority %d and sound %s\n",argv[0],priority,sound.c_str());
MyPushoverClient.SendMessage(argv[0], priority, sound); MyPushoverClient.SendMessage(argv[0], priority, sound);
} }

View File

@ -199,7 +199,7 @@ void scanStart(int, OvmsWriter* writer, OvmsCommand*, int argc, const char* cons
if (start > end) if (start > end)
{ {
writer->printf( writer->printf(
"Error: Invalid Start PID %04x is after End PID %04x\n", start, end "Error: Invalid Start PID %04lx is after End PID %04lx\n", start, end
); );
valid = false; valid = false;
} }
@ -209,7 +209,7 @@ void scanStart(int, OvmsWriter* writer, OvmsCommand*, int argc, const char* cons
} }
if (POLL_TYPE_HAS_8BIT_PID(polltype) && end > 0xff) if (POLL_TYPE_HAS_8BIT_PID(polltype) && end > 0xff)
{ {
writer->printf("Error: Poll type %x PID range is 00..ff\n"); writer->printf("Error: Poll type %lx PID range is 00..ff\n", polltype);
valid = false; valid = false;
} }
if (!valid) if (!valid)
@ -229,7 +229,7 @@ void scanStart(int, OvmsWriter* writer, OvmsCommand*, int argc, const char* cons
if (valid) if (valid)
{ {
s_scanner = new OvmsReToolsPidScanner(can, ecu, rxid_low, rxid_high, polltype, start, end, step, timeout); s_scanner = new OvmsReToolsPidScanner(can, ecu, rxid_low, rxid_high, polltype, start, end, step, timeout);
writer->printf("Scan started: bus %d, ecu %x, rxid %x-%x, polltype %x, PID %x-%x (step %x), timeout %d seconds\n", writer->printf("Scan started: bus %ld, ecu %lx, rxid %lx-%lx, polltype %lx, PID %lx-%lx (step %lx), timeout %d seconds\n",
bus, ecu, rxid_low, rxid_high, polltype, start, end, step, timeout); bus, ecu, rxid_low, rxid_high, polltype, start, end, step, timeout);
} }
} }

View File

@ -97,13 +97,13 @@ static void IRAM_ATTR sdcard_isr_handler(void* arg)
sdcard::sdcard(const char* name, bool mode1bit, bool autoformat, int cdpin) sdcard::sdcard(const char* name, bool mode1bit, bool autoformat, int cdpin)
: pcp(name) : pcp(name)
{ {
m_host = SDMMC_HOST_DEFAULT(); m_host = sdmmc_host_t SDMMC_HOST_DEFAULT();
if (mode1bit) if (mode1bit)
{ {
m_host.flags = SDMMC_HOST_FLAG_1BIT; m_host.flags = SDMMC_HOST_FLAG_1BIT;
} }
m_slot = SDMMC_SLOT_CONFIG_DEFAULT(); m_slot = sdmmc_slot_config_t SDMMC_SLOT_CONFIG_DEFAULT();
// Disable driver-level CD pin, as we do this ourselves // Disable driver-level CD pin, as we do this ourselves
// if (cdpin) // if (cdpin)
// { // {

View File

@ -151,7 +151,9 @@ modem::modem_state1_t simcom5360::State1Ticker1(modem::modem_state1_t curstate)
m_modem->tx("AT+CMUXSRVPORT=0,5\r\n"); m_modem->tx("AT+CMUXSRVPORT=0,5\r\n");
break; break;
case 20: case 20:
m_modem->tx("AT+CMUX=0\r\n"); // start MUX mode, route URCs to MUX channel 3 (POLL)
// Note: NMEA URCs will still be sent only on channel 1 (NMEA) by the SIMCOM 5360
m_modem->tx("AT+CMUX=0;+CATR=6\r\n");
break; break;
} }
return modem::None; return modem::None;

View File

@ -73,6 +73,7 @@ void simcom7000::PowerCycle()
m_powercyclefactor = m_powercyclefactor % 3; m_powercyclefactor = m_powercyclefactor % 3;
ESP_LOGI(TAG, "Power Cycle (SIM7000) %dms",psd); ESP_LOGI(TAG, "Power Cycle (SIM7000) %dms",psd);
uart_wait_tx_done(m_modem->m_uartnum, portMAX_DELAY);
uart_flush(m_modem->m_uartnum); // Flush the ring buffer, to try to address MUX start issues uart_flush(m_modem->m_uartnum); // Flush the ring buffer, to try to address MUX start issues
#ifdef CONFIG_OVMS_COMP_MAX7317 #ifdef CONFIG_OVMS_COMP_MAX7317
MyPeripherals->m_max7317->Output(MODEM_EGPIO_PWR, 0); // Modem EN/PWR line low MyPeripherals->m_max7317->Output(MODEM_EGPIO_PWR, 0); // Modem EN/PWR line low
@ -125,7 +126,8 @@ modem::modem_state1_t simcom7000::State1Ticker1(modem::modem_state1_t curstate)
m_modem->tx("AT+CGMR;+ICCID\r\n"); m_modem->tx("AT+CGMR;+ICCID\r\n");
break; break;
case 20: case 20:
m_modem->tx("AT+CMUX=0\r\n"); // start MUX mode, route URCs to MUX channel 3 (POLL)
m_modem->tx("AT+CMUX=0;+CATR=6\r\n");
break; break;
} }
return modem::None; return modem::None;

View File

@ -80,6 +80,7 @@ void simcom7600::StartupNMEA()
{ {
m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPS=0\r\n"); m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPS=0\r\n");
vTaskDelay(2000 / portTICK_PERIOD_MS); vTaskDelay(2000 / portTICK_PERIOD_MS);
// send single commands, as each can fail:
m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPSNMEA=258\r\n"); m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPSNMEA=258\r\n");
m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPSINFOCFG=5,258\r\n"); m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPSINFOCFG=5,258\r\n");
m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPS=1,1\r\n"); m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPS=1,1\r\n");
@ -88,6 +89,21 @@ void simcom7600::StartupNMEA()
{ ESP_LOGE(TAG, "Attempt to transmit on non running mux"); } { ESP_LOGE(TAG, "Attempt to transmit on non running mux"); }
} }
void simcom7600::ShutdownNMEA()
{
// Switch off GPS:
if (m_modem->m_mux != NULL)
{
// send single commands, as each can fail:
m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPSNMEA=0\r\n");
m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPSINFOCFG=0\r\n");
vTaskDelay(pdMS_TO_TICKS(100));
m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPS=0\r\n");
}
else
{ ESP_LOGE(TAG, "Attempt to transmit on non running mux"); }
}
void simcom7600::StatusPoller() void simcom7600::StatusPoller()
{ {
if (m_modem->m_mux != NULL) if (m_modem->m_mux != NULL)
@ -126,6 +142,7 @@ void simcom7600::PowerCycle()
m_powercyclefactor = m_powercyclefactor % 3; m_powercyclefactor = m_powercyclefactor % 3;
ESP_LOGI(TAG, "Power Cycle (SIM7600) %dms",psd); ESP_LOGI(TAG, "Power Cycle (SIM7600) %dms",psd);
uart_wait_tx_done(m_modem->m_uartnum, portMAX_DELAY);
uart_flush(m_modem->m_uartnum); // Flush the ring buffer, to try to address MUX start issues uart_flush(m_modem->m_uartnum); // Flush the ring buffer, to try to address MUX start issues
#ifdef CONFIG_OVMS_COMP_MAX7317 #ifdef CONFIG_OVMS_COMP_MAX7317
MyPeripherals->m_max7317->Output(MODEM_EGPIO_PWR, 0); // Modem EN/PWR line low MyPeripherals->m_max7317->Output(MODEM_EGPIO_PWR, 0); // Modem EN/PWR line low
@ -176,7 +193,11 @@ modem::modem_state1_t simcom7600::State1Ticker1(modem::modem_state1_t curstate)
m_modem->tx("AT+CGMR;+ICCID\r\n"); m_modem->tx("AT+CGMR;+ICCID\r\n");
break; break;
case 20: case 20:
m_modem->tx("AT+CMUX=0\r\n"); // start MUX mode, route URCs to MUX channel 3 (POLL)
// Note: NMEA URCs will now also be sent on channel 3 by the SIMCOM 7600;
// without +CATR, NMEA URCs are sent identically on all channels;
// there is no option to route these separately to channel 1 (NMEA)
m_modem->tx("AT+CMUX=0;+CATR=6\r\n");
break; break;
} }
return modem::None; return modem::None;

View File

@ -50,6 +50,7 @@ class simcom7600 : public modemdriver
int GetMuxChannelPOLL() { return 3; } int GetMuxChannelPOLL() { return 3; }
int GetMuxChannelCMD() { return 4; } int GetMuxChannelCMD() { return 4; }
void StartupNMEA(); void StartupNMEA();
void ShutdownNMEA();
void StatusPoller(); void StatusPoller();
void PowerCycle(); void PowerCycle();

View File

@ -7,6 +7,8 @@
# please read the ESP-IDF documents if you need to do this. # please read the ESP-IDF documents if you need to do this.
# #
ifeq ($(shell expr $(IDF_VERSION_MAJOR) \< 4), 1)
COMPONENT_ADD_INCLUDEDIRS:=src COMPONENT_ADD_INCLUDEDIRS:=src
COMPONENT_SRCDIRS:=src COMPONENT_SRCDIRS:=src
COMPONENT_ADD_LDFLAGS = -Wl,--whole-archive -l$(COMPONENT_NAME) -Wl,--no-whole-archive COMPONENT_ADD_LDFLAGS = -Wl,--whole-archive -l$(COMPONENT_NAME) -Wl,--no-whole-archive
endif

View File

@ -358,6 +358,8 @@ OvmsVehicle::OvmsVehicle()
m_brakelight_ignftbrk = false; m_brakelight_ignftbrk = false;
m_tpms_lastcheck = 0; m_tpms_lastcheck = 0;
m_inv_energyused = 0;
m_inv_energyrecd = 0;
m_rxqueue = xQueueCreate(CONFIG_OVMS_VEHICLE_CAN_RX_QUEUE_SIZE,sizeof(CAN_frame_t)); m_rxqueue = xQueueCreate(CONFIG_OVMS_VEHICLE_CAN_RX_QUEUE_SIZE,sizeof(CAN_frame_t));
xTaskCreatePinnedToCore(OvmsVehicleRxTask, "OVMS Vehicle", xTaskCreatePinnedToCore(OvmsVehicleRxTask, "OVMS Vehicle",
@ -979,7 +981,6 @@ bool OvmsVehicle::TPMSWrite(std::vector<uint32_t> &tpms)
*/ */
OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStat(int verbosity, OvmsWriter* writer) OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStat(int verbosity, OvmsWriter* writer)
{ {
metric_unit_t rangeUnit = (MyConfig.GetParamValue("vehicle", "units.distance") == "M") ? Miles : Kilometers;
bool chargeport_open = StdMetrics.ms_v_door_chargeport->AsBool(); bool chargeport_open = StdMetrics.ms_v_door_chargeport->AsBool();
std::string charge_state = StdMetrics.ms_v_charge_state->AsString(); std::string charge_state = StdMetrics.ms_v_charge_state->AsString();
@ -1030,10 +1031,9 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStat(int verbosity, OvmsWrite
} }
// Charge speed: // Charge speed:
if (StdMetrics.ms_v_bat_range_speed->AsFloat() != 0) if (StdMetrics.ms_v_bat_range_speed->IsDefined() && StdMetrics.ms_v_bat_range_speed->AsFloat() != 0)
{ {
metric_unit_t speedUnit = (rangeUnit == Miles) ? Mph : Kph; writer->printf("%s\n", StdMetrics.ms_v_bat_range_speed->AsUnitString("-", ToUser, 1).c_str());
writer->printf("%s\n", StdMetrics.ms_v_bat_range_speed->AsUnitString("-", speedUnit, 1).c_str());
} }
else if (show_vc) else if (show_vc)
{ {
@ -1048,13 +1048,13 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStat(int verbosity, OvmsWrite
int duration_soc = StdMetrics.ms_v_charge_duration_soc->AsInt(); int duration_soc = StdMetrics.ms_v_charge_duration_soc->AsInt();
if (duration_soc > 0) if (duration_soc > 0)
writer->printf("%s: %d:%02dh\n", writer->printf("%s: %d:%02dh\n",
(char*) StdMetrics.ms_v_charge_limit_soc->AsUnitString("SOC", Native, 0).c_str(), (char*) StdMetrics.ms_v_charge_limit_soc->AsUnitString("SOC", ToUser, 0).c_str(),
duration_soc / 60, duration_soc % 60); duration_soc / 60, duration_soc % 60);
int duration_range = StdMetrics.ms_v_charge_duration_range->AsInt(); int duration_range = StdMetrics.ms_v_charge_duration_range->AsInt();
if (duration_range > 0) if (duration_range > 0)
writer->printf("%s: %d:%02dh\n", writer->printf("%s: %d:%02dh\n",
(char*) StdMetrics.ms_v_charge_limit_range->AsUnitString("Range", rangeUnit, 0).c_str(), (char*) StdMetrics.ms_v_charge_limit_range->AsUnitString("Range", ToUser, 0).c_str(),
duration_range / 60, duration_range % 60); duration_range / 60, duration_range % 60);
} }
@ -1062,12 +1062,12 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStat(int verbosity, OvmsWrite
if (StdMetrics.ms_v_charge_kwh_grid->IsDefined()) if (StdMetrics.ms_v_charge_kwh_grid->IsDefined())
{ {
writer->printf("Drawn: %s\n", writer->printf("Drawn: %s\n",
StdMetrics.ms_v_charge_kwh_grid->AsUnitString("-", Native, 1).c_str()); StdMetrics.ms_v_charge_kwh_grid->AsUnitString("-", ToUser, 1).c_str());
} }
if (StdMetrics.ms_v_charge_kwh->IsDefined()) if (StdMetrics.ms_v_charge_kwh->IsDefined())
{ {
writer->printf("Charged: %s\n", writer->printf("Charged: %s\n",
StdMetrics.ms_v_charge_kwh->AsUnitString("-", Native, 1).c_str()); StdMetrics.ms_v_charge_kwh->AsUnitString("-", ToUser, 1).c_str());
} }
} }
else else
@ -1075,35 +1075,35 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStat(int verbosity, OvmsWrite
writer->puts("Not charging"); writer->puts("Not charging");
} }
writer->printf("SOC: %s\n", (char*) StdMetrics.ms_v_bat_soc->AsUnitString("-", Native, 1).c_str()); writer->printf("SOC: %s\n", (char*) StdMetrics.ms_v_bat_soc->AsUnitString("-", ToUser, 1).c_str());
if (StdMetrics.ms_v_bat_range_ideal->IsDefined()) if (StdMetrics.ms_v_bat_range_ideal->IsDefined())
{ {
const std::string& range_ideal = StdMetrics.ms_v_bat_range_ideal->AsUnitString("-", rangeUnit, 0); const std::string& range_ideal = StdMetrics.ms_v_bat_range_ideal->AsUnitString("-", ToUser, 0);
writer->printf("Ideal range: %s\n", range_ideal.c_str()); writer->printf("Ideal range: %s\n", range_ideal.c_str());
} }
if (StdMetrics.ms_v_bat_range_est->IsDefined()) if (StdMetrics.ms_v_bat_range_est->IsDefined())
{ {
const std::string& range_est = StdMetrics.ms_v_bat_range_est->AsUnitString("-", rangeUnit, 0); const std::string& range_est = StdMetrics.ms_v_bat_range_est->AsUnitString("-", ToUser, 0);
writer->printf("Est. range: %s\n", range_est.c_str()); writer->printf("Est. range: %s\n", range_est.c_str());
} }
if (StdMetrics.ms_v_pos_odometer->IsDefined()) if (StdMetrics.ms_v_pos_odometer->IsDefined())
{ {
const std::string& odometer = StdMetrics.ms_v_pos_odometer->AsUnitString("-", rangeUnit, 1); const std::string& odometer = StdMetrics.ms_v_pos_odometer->AsUnitString("-", ToUser, 1);
writer->printf("ODO: %s\n", odometer.c_str()); writer->printf("ODO: %s\n", odometer.c_str());
} }
if (StdMetrics.ms_v_bat_cac->IsDefined()) if (StdMetrics.ms_v_bat_cac->IsDefined())
{ {
const std::string& cac = StdMetrics.ms_v_bat_cac->AsUnitString("-", Native, 1); const std::string& cac = StdMetrics.ms_v_bat_cac->AsUnitString("-", ToUser, 1);
writer->printf("CAC: %s\n", cac.c_str()); writer->printf("CAC: %s\n", cac.c_str());
} }
if (StdMetrics.ms_v_bat_soh->IsDefined()) if (StdMetrics.ms_v_bat_soh->IsDefined())
{ {
const std::string& soh = StdMetrics.ms_v_bat_soh->AsUnitString("-", Native, 0); const std::string& soh = StdMetrics.ms_v_bat_soh->AsUnitString("-", ToUser, 0);
writer->printf("SOH: %s\n", soh.c_str()); writer->printf("SOH: %s\n", soh.c_str());
} }
@ -1115,12 +1115,12 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStat(int verbosity, OvmsWrite
*/ */
OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStatTrip(int verbosity, OvmsWriter* writer) OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStatTrip(int verbosity, OvmsWriter* writer)
{ {
metric_unit_t rangeUnit = (MyConfig.GetParamValue("vehicle", "units.distance") == "M") ? Miles : Kilometers; metric_unit_t rangeUnit = OvmsMetricGetUserUnit(GrpDistance, Kilometers);
metric_unit_t speedUnit = (rangeUnit == Miles) ? Mph : Kph; metric_unit_t speedUnit = OvmsMetricGetUserUnit(GrpSpeed, Kph);
metric_unit_t accelUnit = (rangeUnit == Miles) ? MphPS : KphPS; metric_unit_t accelUnit = OvmsMetricGetUserUnit(GrpAccel, KphPS);
metric_unit_t consumUnit = (rangeUnit == Miles) ? WattHoursPM : WattHoursPK; metric_unit_t consumUnit = OvmsMetricGetUserUnit(GrpConsumption, WattHoursPK);
metric_unit_t energyUnit = kWh; metric_unit_t energyUnit = kWh;
metric_unit_t altitudeUnit = (rangeUnit == Miles) ? Feet : Meters; metric_unit_t altitudeUnit = OvmsMetricGetUserUnit(GrpDistanceShort, Meters);
const char* rangeUnitLabel = OvmsMetricUnitLabel(rangeUnit); const char* rangeUnitLabel = OvmsMetricUnitLabel(rangeUnit);
const char* speedUnitLabel = OvmsMetricUnitLabel(speedUnit); const char* speedUnitLabel = OvmsMetricUnitLabel(speedUnit);
const char* accelUnitLabel = OvmsMetricUnitLabel(accelUnit); const char* accelUnitLabel = OvmsMetricUnitLabel(accelUnit);
@ -1128,7 +1128,7 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStatTrip(int verbosity, OvmsW
const char* energyUnitLabel = OvmsMetricUnitLabel(energyUnit); const char* energyUnitLabel = OvmsMetricUnitLabel(energyUnit);
const char* altitudeUnitLabel = OvmsMetricUnitLabel(altitudeUnit); const char* altitudeUnitLabel = OvmsMetricUnitLabel(altitudeUnit);
float trip_length = StdMetrics.ms_v_pos_trip->AsFloat(0, rangeUnit); float trip_length = StdMetrics.ms_v_pos_trip->AsFloat(0);
float speed_avg = (m_drive_speedcnt > 0) float speed_avg = (m_drive_speedcnt > 0)
? UnitConvert(Kph, speedUnit, (float)(m_drive_speedsum / m_drive_speedcnt)) ? UnitConvert(Kph, speedUnit, (float)(m_drive_speedsum / m_drive_speedcnt))
@ -1144,7 +1144,7 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStatTrip(int verbosity, OvmsW
float energy_used = StdMetrics.ms_v_bat_energy_used->AsFloat(); float energy_used = StdMetrics.ms_v_bat_energy_used->AsFloat();
float energy_recd = StdMetrics.ms_v_bat_energy_recd->AsFloat(); float energy_recd = StdMetrics.ms_v_bat_energy_recd->AsFloat();
float energy_recd_perc = (energy_used > 0) ? energy_recd / energy_used * 100 : 0; float energy_recd_perc = (energy_used > 0) ? energy_recd / energy_used * 100 : 0;
float wh_per_rangeunit = (trip_length > 0) ? (energy_used - energy_recd) * 1000 / trip_length : 0; float wh_per_km = (trip_length > 0) ? (energy_used - energy_recd) * 1000 / trip_length : 0;
float soc = StdMetrics.ms_v_bat_soc->AsFloat(); float soc = StdMetrics.ms_v_bat_soc->AsFloat();
float soc_diff = soc - m_drive_startsoc; float soc_diff = soc - m_drive_startsoc;
@ -1158,7 +1158,7 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStatTrip(int verbosity, OvmsW
<< "Trip " << "Trip "
<< std::fixed << std::fixed
<< std::setprecision(1) << std::setprecision(1)
<< trip_length << rangeUnitLabel << UnitConvert(Kilometers, rangeUnit, trip_length) << rangeUnitLabel
<< " Avg " << " Avg "
<< std::setprecision(0) << std::setprecision(0)
<< speed_avg << speedUnitLabel << speed_avg << speedUnitLabel
@ -1166,11 +1166,11 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStatTrip(int verbosity, OvmsW
<< ((alt_diff >= 0) ? "+" : "") << ((alt_diff >= 0) ? "+" : "")
<< alt_diff << altitudeUnitLabel << alt_diff << altitudeUnitLabel
; ;
if (wh_per_rangeunit != 0) if (wh_per_km != 0)
{ {
buf buf
<< "\nEnergy " << "\nEnergy "
<< wh_per_rangeunit << consumUnitLabel << UnitConvert(WattHoursPK, consumUnit, wh_per_km) << consumUnitLabel
<< ", " << ", "
<< energy_recd_perc << "% recd" << energy_recd_perc << "% recd"
; ;
@ -2012,68 +2012,82 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::ProcessMsgCommand(std::string &resul
*/ */
void OvmsVehicle::GetDashboardConfig(DashboardConfig& cfg) void OvmsVehicle::GetDashboardConfig(DashboardConfig& cfg)
{ {
cfg.gaugeset1 = // Speed:
"yAxis: [{" dash_gauge_t speed_dash(NULL,Kph);
// Speed: speed_dash.SetMinMax(0, 200, 5);
"min: 0, max: 200," speed_dash.AddBand("green", 0, 120);
"plotBands: [" speed_dash.AddBand("yellow", 120, 160);
"{ from: 0, to: 120, className: 'green-band' }," speed_dash.AddBand("red", 160, 200);
"{ from: 120, to: 160, className: 'yellow-band' },"
"{ from: 160, to: 200, className: 'red-band' }]" // Voltage:
"},{" dash_gauge_t voltage_dash(NULL,Volts);
// Voltage: voltage_dash.SetMinMax(310, 410);
"min: 310, max: 410," voltage_dash.AddBand("red", 310, 325);
"plotBands: [" voltage_dash.AddBand("yellow", 325, 340);
"{ from: 310, to: 325, className: 'red-band' }," voltage_dash.AddBand("green", 340, 410);
"{ from: 325, to: 340, className: 'yellow-band' },"
"{ from: 340, to: 410, className: 'green-band' }]" // SOC:
"},{" dash_gauge_t soc_dash("SOC ",Percentage);
// SOC: soc_dash.SetMinMax(0, 100);
"min: 0, max: 100," soc_dash.AddBand("red", 0, 12.5);
"plotBands: [" soc_dash.AddBand("yellow", 12.5, 25);
"{ from: 0, to: 12.5, className: 'red-band' }," soc_dash.AddBand("green", 25, 100);
"{ from: 12.5, to: 25, className: 'yellow-band' },"
"{ from: 25, to: 100, className: 'green-band' }]" // Efficiency:
"},{" dash_gauge_t eff_dash(NULL,WattHoursPK);
// Efficiency: eff_dash.SetMinMax(0, 400);
"min: 0, max: 400," eff_dash.AddBand("green", 0, 200);
"plotBands: [" eff_dash.AddBand("yellow", 200, 300);
"{ from: 0, to: 200, className: 'green-band' }," eff_dash.AddBand("red", 300, 400);
"{ from: 200, to: 300, className: 'yellow-band' },"
"{ from: 300, to: 400, className: 'red-band' }]" // Power:
"},{" dash_gauge_t power_dash(NULL,kW);
// Power: power_dash.SetMinMax(-50, 200);
"min: -50, max: 200," power_dash.AddBand("violet", -50, 0);
"plotBands: [" power_dash.AddBand("green", 0, 100);
"{ from: -50, to: 0, className: 'violet-band' }," power_dash.AddBand("yellow", 100, 150);
"{ from: 0, to: 100, className: 'green-band' }," power_dash.AddBand("red", 150, 200);
"{ from: 100, to: 150, className: 'yellow-band' },"
"{ from: 150, to: 200, className: 'red-band' }]" // Charger temperature:
"},{" dash_gauge_t charget_dash("CHG ",Celcius);
// Charger temperature: charget_dash.SetMinMax(20, 80);
"min: 20, max: 80, tickInterval: 20," charget_dash.SetTick(20);
"plotBands: [" charget_dash.AddBand("normal", 20, 65);
"{ from: 20, to: 65, className: 'normal-band border' }," charget_dash.AddBand("red", 65, 80);
"{ from: 65, to: 80, className: 'red-band border' }]"
"},{" // Battery temperature:
// Battery temperature: dash_gauge_t batteryt_dash("BAT ",Celcius);
"min: -15, max: 65, tickInterval: 25," batteryt_dash.SetMinMax(-15, 65);
"plotBands: [" batteryt_dash.SetTick(25);
"{ from: -15, to: 0, className: 'red-band border' }," batteryt_dash.AddBand("red", -15, 0);
"{ from: 0, to: 50, className: 'normal-band border' }," batteryt_dash.AddBand("normal", 0, 50);
"{ from: 50, to: 65, className: 'red-band border' }]" batteryt_dash.AddBand("red", 50, 65);
"},{"
// Inverter temperature: // Inverter temperature:
"min: 20, max: 80, tickInterval: 20," dash_gauge_t invertert_dash("PEM ",Celcius);
"plotBands: [" invertert_dash.SetMinMax(20, 80);
"{ from: 20, to: 70, className: 'normal-band border' }," invertert_dash.SetTick(20);
"{ from: 70, to: 80, className: 'red-band border' }]" invertert_dash.AddBand("normal", 20, 70);
"},{" invertert_dash.AddBand("red", 70, 80);
// Motor temperature:
"min: 50, max: 125, tickInterval: 25," // Motor temperature:
"plotBands: [" dash_gauge_t motort_dash("MOT ",Celcius);
"{ from: 50, to: 110, className: 'normal-band border' }," motort_dash.SetMinMax(50, 125);
"{ from: 110, to: 125, className: 'red-band border' }]" motort_dash.SetTick(25);
"}]"; motort_dash.AddBand("normal", 50, 110);
motort_dash.AddBand("red", 110, 125);
std::ostringstream str;
str
<< batteryt_dash // Battery temperature
<< voltage_dash // Voltage
<< motort_dash // Motor temperature
<< invertert_dash // Inverter temperature
<< power_dash // Power
<< soc_dash // SOC
<< charget_dash // Charger temperature
<< speed_dash // Speed
<< eff_dash; // Efficiency
cfg.gaugeset1 = str.str();
} }
#endif // #ifdef CONFIG_OVMS_COMP_WEBSERVER #endif // #ifdef CONFIG_OVMS_COMP_WEBSERVER

View File

@ -605,6 +605,14 @@ void OvmsVehicle::BmsStatus(int verbosity, OvmsWriter* writer, vehicle_bms_statu
writer->printf("No BMS %s data available\n", datatype); writer->printf("No BMS %s data available\n", datatype);
return; return;
} }
metric_unit_t user_temp = UnitNotFound;
std::string temp_unit;
if (show_temperature)
{
user_temp = OvmsMetricGetUserUnit(GrpTemp, Celcius);
// (To Check: '°' is not SMS safe, so we only output 'C')
temp_unit = OvmsMetricUnitLabel(user_temp);
}
int vwarn=0, valert=0; int vwarn=0, valert=0;
int twarn=0, talert=0; int twarn=0, talert=0;
@ -642,13 +650,13 @@ void OvmsVehicle::BmsStatus(int verbosity, OvmsWriter* writer, vehicle_bms_statu
if (show_temperature) if (show_temperature)
{ {
writer->puts("Temperature:"); writer->puts("Temperature:");
writer->printf(" Average: %5.1fC [%5.1fC - %5.1fC]\n", writer->printf(" Average: %5.1f%s [%5.1f%s - %5.1f%s]\n",
StdMetrics.ms_v_bat_pack_tavg->AsFloat(), StdMetrics.ms_v_bat_pack_tavg->AsFloat(0, user_temp), temp_unit.c_str(),
StdMetrics.ms_v_bat_pack_tmin->AsFloat(), StdMetrics.ms_v_bat_pack_tmin->AsFloat(0, user_temp), temp_unit.c_str(),
StdMetrics.ms_v_bat_pack_tmax->AsFloat()); StdMetrics.ms_v_bat_pack_tmax->AsFloat(0, user_temp), temp_unit.c_str());
writer->printf(" Deviation: SD %6.2fC [max %.2fC], %d warnings, %d alerts\n", writer->printf(" Deviation: SD %6.2f%s [max %.2f%s], %d warnings, %d alerts\n",
StdMetrics.ms_v_bat_pack_tstddev->AsFloat(), StdMetrics.ms_v_bat_pack_tstddev->AsFloat(0, user_temp), temp_unit.c_str(),
StdMetrics.ms_v_bat_pack_tstddev_max->AsFloat(), StdMetrics.ms_v_bat_pack_tstddev_max->AsFloat(0, user_temp), temp_unit.c_str(),
twarn, talert); twarn, talert);
} }
@ -728,7 +736,7 @@ void OvmsVehicle::BmsStatus(int verbosity, OvmsWriter* writer, vehicle_bms_statu
{ {
if (kt < m_bms_readings_t && (reading_left_t > 0)) if (kt < m_bms_readings_t && (reading_left_t > 0))
{ {
writer->printf(" %5.1fC",m_bms_temperatures[kt]); writer->printf(" %5.1f%s",UnitConvert(Celcius, user_temp, m_bms_temperatures[kt]), temp_unit.c_str());
--reading_left_t; --reading_left_t;
++kt; ++kt;
} }
@ -788,8 +796,9 @@ bool OvmsVehicle::FormatBmsAlerts(int verbosity, OvmsWriter* writer, bool show_w
writer->printf("%s\n", has_valerts ? "" : ", cells OK"); writer->printf("%s\n", has_valerts ? "" : ", cells OK");
// Temperatures: // Temperatures:
// (Note: '°' is not SMS safe, so we only output 'C') metric_unit_t user_temp = OvmsMetricGetUserUnit(GrpTemp, Celcius);
writer->printf("Temperature: StdDev %.1fC", StdMetrics.ms_v_bat_pack_tstddev_max->AsFloat()); std::string temp_unit = OvmsMetricUnitLabel(user_temp);
writer->printf("Temperature: StdDev %.1f%s", StdMetrics.ms_v_bat_pack_tstddev_max->AsFloat(0, user_temp), temp_unit.c_str());
for (int i=0; i<m_bms_readings_v; i++) for (int i=0; i<m_bms_readings_v; i++)
{ {
OvmsStatus sts = OvmsStatus(StdMetrics.ms_v_bat_cell_talert->GetElemValue(i)); OvmsStatus sts = OvmsStatus(StdMetrics.ms_v_bat_cell_talert->GetElemValue(i));
@ -802,8 +811,8 @@ bool OvmsVehicle::FormatBmsAlerts(int verbosity, OvmsWriter* writer, bool show_w
has_talerts++; has_talerts++;
if (verbose || has_talerts <= 5) if (verbose || has_talerts <= 5)
{ {
float dev = StdMetrics.ms_v_bat_cell_tdevmax->GetElemValue(i); float dev = StdMetrics.ms_v_bat_cell_tdevmax->GetElemValue(i, user_temp);
writer->printf("\n %c #%02d: %+3.1fC", (sts==OvmsStatus::Warn) ? '?' : '!', i+1, dev); writer->printf("\n %c #%02d: %+3.1f%s", (sts==OvmsStatus::Warn) ? '?' : '!', i+1, dev, temp_unit.c_str());
} }
else else
{ {