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);
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;
}
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);
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;
}
frame.MsgID = uv;
@ -570,7 +570,8 @@ static const char* const CAN_log_type_names[] = {
"Status",
"Comment",
"Info",
"Event"
"Event",
"Metric"
};
const char* GetCanLogTypeName(CAN_log_type_t type)

View File

@ -251,6 +251,7 @@ typedef enum
CAN_LogInfo_Comment, // general comment
CAN_LogInfo_Config, // logger setup info (type, file, filters, vehicle)
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;
// 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_Config:
case CAN_LogInfo_Event:
case CAN_LogInfo_Metric:
snprintf(buf,sizeof(buf),"%ld.%06ld %c%s %s %s",
message->timestamp.tv_sec, message->timestamp.tv_usec,
busnumber,
(message->type == CAN_LogInfo_Event) ? "CEV" : "CXX",
(message->type == CAN_LogInfo_Event) ? "CEV" : (message->type == CAN_LogInfo_Metric) ? "CMT" : "CXX",
GetCanLogTypeName(message->type),
message->text);
break;
@ -151,7 +152,7 @@ std::string canformat_crtd::getheader(struct timeval *time)
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);

View File

@ -42,6 +42,8 @@ static const char *TAG = "canlog";
#include "ovms_peripherals.h"
#include "metrics_standard.h"
static const char *CAN_PARAM = "can";
////////////////////////////////////////////////////////////////////////
// Command Processing
////////////////////////////////////////////////////////////////////////
@ -352,8 +354,12 @@ canlog::canlog(const char* type, std::string format, canformat::canformat_serve_
using std::placeholders::_1;
using std::placeholders::_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));
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()
{
MyEvents.DeregisterEvent(IDTAG);
MyMetrics.DeregisterListener(IDTAG);
if (m_task)
{
@ -383,6 +390,7 @@ canlog::~canlog()
case CAN_LogInfo_Comment:
case CAN_LogInfo_Config:
case CAN_LogInfo_Event:
case CAN_LogInfo_Metric:
free(msg.text);
break;
default:
@ -418,6 +426,7 @@ void canlog::RxTask(void *context)
case CAN_LogInfo_Comment:
case CAN_LogInfo_Config:
case CAN_LogInfo_Event:
case CAN_LogInfo_Metric:
me->OutputMsg(msg);
free(msg.text);
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)
{
// 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());
}
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()
{
return m_type;

View File

@ -95,6 +95,11 @@ class canlogconnection: public InternalRamAllocated
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
{
@ -105,6 +110,7 @@ class canlog : public InternalRamAllocated
public:
static void RxTask(void* context);
void EventListener(std::string event, void* data);
void MetricListener(OvmsMetric* metric);
public:
const char* GetType();
@ -152,6 +158,16 @@ class canlog : public InternalRamAllocated
uint32_t m_msgcount;
uint32_t m_dropcount;
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__

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_Config:
case CAN_LogInfo_Event:
case CAN_LogInfo_Metric:
ESP_LOGD(TAG,"%s",result.c_str());
break;
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)
: canlogconnection(logger, format, mode)
: canlogconnection(logger, format, mode), m_file_size(0)
{
m_file = NULL;
}
@ -114,7 +114,10 @@ void canlog_vfs_conn::OutputMsg(CAN_log_message_t& msg, std::string &result)
}
if (result.length()>0)
{
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();
if (header.length()>0)
{
fwrite(header.c_str(),header.length(),1,clc->m_file);
clc->m_file_size += header.length();
}
m_connmap[NULL] = clc;
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 result = canlog::GetInfo();

View File

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

View File

@ -75,7 +75,7 @@ class ConsoleSSH : public OvmsConsole
void Sent();
void Exit();
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);
int RecvCallback(char* buf, uint32_t size);
bool IsDraining() { return m_drain > 0; }

View File

@ -64,7 +64,7 @@ class ConsoleTelnet : public OvmsConsole
void Receive();
void Exit();
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);
protected:

View File

@ -119,7 +119,7 @@ void modem::Task()
.use_ref_tick = 0,
};
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,
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:
uart_driver_delete(m_uartnum);
ESP_LOGD(TAG, "UART shutdown");
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);
}
@ -662,7 +671,6 @@ void modem::State1Enter(modem_state1_t newstate)
case PoweredOff:
ClearNetMetrics();
MyEvents.SignalEvent("system.modem.poweredoff", NULL);
if (MyBoot.IsShuttingDown()) MyBoot.ShutdownReady(TAG);
StopMux();
if (m_driver)
{
@ -1018,13 +1026,16 @@ void modem::StandardLineHandler(int channel, OvmsBuffer* buf, std::string line)
line = m_line_buffer;
}
const char *cp = line.c_str();
if ((line.length()>2)&&(cp[0]!='$')&&(cp[1])!='G')
if (line.compare(0, 2, "$G") == 0)
{
// Log incoming data other than GPS NMEA
ESP_LOGD(TAG, "mux-rx-line #%d: %s", channel, line.c_str());
// GPS NMEA URC:
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))
{
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)
{
std::string pincode = MyConfig.GetParamValue("modem", "pincode");
ESP_LOGE(TAG,"Wrong PIN code entered!");
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);
}
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):
// sent on all free channels, so we only process m_mux_channel_CMD
else if (channel == m_mux_channel_CMD && line.compare(0, 7, "+CUSD: ") == 0)
// sent on all free channels or only on POLL, so we only process m_mux_channel_POLL
else if (channel == m_mux_channel_POLL && line.compare(0, 7, "+CUSD: ") == 0)
{
// Format: +CUSD: 0,"…msg…",15
// 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)
{
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) &&
(MyConfig.GetParamValueBool("modem", "enable.gps", false)) )
(force || MyConfig.GetParamValueBool("modem", "enable.gps", false)) )
{
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();
if (!m_mux || !m_driver)
{
ESP_LOGE(TAG, "StartNMEA failed: MUX or driver not available");
}
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()
{
if (m_nmea != NULL)
{
ESP_LOGV(TAG, "Stopping NMEA");
m_driver->ShutdownNMEA();
m_nmea->Shutdown();
delete m_nmea;
m_nmea = NULL;
if (!m_mux || !m_driver)
{
ESP_LOGE(TAG, "StopNMEA failed: MUX or driver not available");
}
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;
xQueueSend(m_queue,&ev,0);
QueueHandle_t queue = m_queue;
if (queue) xQueueSend(queue,&ev,0);
}
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)
{
if (m_nmea != NULL)
{
while (channel->m_buffer.HasLine() >= 0)
{
m_nmea->IncomingLine(channel->m_buffer.ReadLine());
}
}
else
{
channel->m_buffer.EmptyAll();
}
StandardIncomingHandler(channel->m_channel, &channel->m_buffer);
}
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.data.newstate = newstate;
xQueueSend(m_queue,&ev,0);
QueueHandle_t queue = m_queue;
if (queue) xQueueSend(queue,&ev,0);
}
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);
}
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)
{
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);
}
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);
// Our instances:
// 'driver': Driver to use (default: auto)

View File

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

View File

@ -82,9 +82,9 @@ void modemdriver::Restart()
{
ESP_LOGI(TAG, "Restart");
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
m_modem->SetState1(modem::PoweringOff);
m_modem->SendSetState1(modem::PoweringOff);
}
void modemdriver::PowerOff()
@ -101,6 +101,7 @@ void modemdriver::PowerCycle()
m_powercyclefactor = m_powercyclefactor % 3;
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
#ifdef CONFIG_OVMS_COMP_MAX7317
MyPeripherals->m_max7317->Output(MODEM_EGPIO_PWR, 0); // Modem EN/PWR line low
@ -142,7 +143,12 @@ void modemdriver::ShutdownNMEA()
{
// Switch off GPS:
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
{ 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());
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());
}

View File

@ -290,9 +290,10 @@ void OvmsLocation::Store(std::string& buf)
void OvmsLocation::Render(std::string& buf)
{
char val[32];
snprintf(val, sizeof(val), "%0.6f,%0.6f (%dm)", m_latitude, m_longitude, m_radius);
buf = val;
metric_unit_t user_length = OvmsMetricGetUserUnit(GrpDistanceShort, Meters);
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;
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!
}
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)
{
const char *name = argv[0];
float latitude, longitude;
int radius = LOCATION_DEFRADIUS;
int base_value = radius;
metric_unit_t user_length = Meters;
if (strcmp(name, "?") == 0)
{
@ -391,12 +403,37 @@ void location_set(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc,
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];
snprintf(val,sizeof(val),"%0.6f,%0.6f,%d",latitude,longitude,radius);
MyConfig.SetParamValue(LOCATIONS_PARAM,name,val);
writer->puts("Location defined");
radius = UnitConvert(user_length, Meters, radius);
}
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)
@ -409,12 +446,35 @@ void location_radius(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int ar
writer->printf("Error: No location %s defined\n",name);
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;
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);
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)
@ -477,6 +537,16 @@ int location_validate(OvmsWriter* writer, OvmsCommand* cmd, int argc, const char
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)
{
const char* const* rargv = writer->GetArgv();
@ -634,8 +704,8 @@ OvmsLocations::OvmsLocations()
// Register our commands
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("set","Set the position of a location",location_set, "<name> [<latitude> <longitude> [<radius>]]", 1, 4);
cmd_location->RegisterCommand("radius","Set the radius of a location",location_radius, "<name> <radius>", 2, 2, true, location_validate);
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 (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("status","Show location status",location_status);
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
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());
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 <esp_system.h>
#include <esp_ota_ops.h>
#if ESP_IDF_VERSION_MAJOR < 4
#include "strverscmp.h"
#endif
#include "ovms_ota.h"
#include "ovms_command.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]);
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");
if (f == NULL)
@ -242,7 +244,7 @@ void ota_flash_vfs(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc
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);
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");
if (url.empty())
url = "ovms-ota.bit-cloud.de";
url = "api.openvehicles.com/firmware/ota";
url.append("/");
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 url = MyConfig.GetParamValue("ota","server");
if (url.empty())
url = "ovms-ota.bit-cloud.de";
url = "api.openvehicles.com/firmware/ota";
url.append("/");
url.append(GetOVMSProduct());
url.append("/");
@ -931,7 +933,11 @@ static void OTAFlashTask(void *pvParameters)
if (fromsd)
{
#ifdef CONFIG_OVMS_COMP_SDCARD
success = MyOTA.AutoFlashSD();
#else
success = false;
#endif //CONFIG_OVMS_COMP_SDCARD
}
else
{
@ -1029,7 +1035,7 @@ bool OvmsOTA::AutoFlash(bool force)
std::string tag = MyConfig.GetParamValue("ota","tag");
std::string url = MyConfig.GetParamValue("ota","server");
if (url.empty())
url = "ovms-ota.bit-cloud.de";
url = "api.openvehicles.com/firmware/ota";
url.append("/");
url.append(GetOVMSProduct());

View File

@ -36,7 +36,10 @@ static const char *TAG = "pluginstore";
#include <sys/stat.h>
#include <string>
#include <string.h>
#include "esp_idf_version.h"
#if ESP_IDF_VERSION_MAJOR < 4
#include "strverscmp.h"
#endif
#include "ovms_plugins.h"
#include "ovms_command.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(val);
m_headers.append("\r\n");
if (key == "User-Agent")
if (strcasecmp(key.c_str(), "User-Agent") == 0)
have_useragent = true;
else if (key == "Content-Type")
else if (strcasecmp(key.c_str(), "Content-Type") == 0)
have_contenttype = true;
}
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);
val.assign(hm->header_values[i].p, hm->header_values[i].len);
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?

View File

@ -140,8 +140,10 @@ void tpms_status(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc,
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->puts(StandardMetrics.ms_v_tpms_pressure->IsStale() ? " [stale]" : "");
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())
{
writer->printf("Temperature.[°C]: ");
for (auto val : StandardMetrics.ms_v_tpms_temp->AsVector())
metric_unit_t user_temp = OvmsMetricGetUserUnit(GrpTemp, Celcius);
writer->printf("Temperature.[%s]: ", OvmsMetricUnitLabel(user_temp));
for (auto val : StandardMetrics.ms_v_tpms_temp->AsVector(user_temp))
writer->printf(" %8.1f", val);
writer->puts(StandardMetrics.ms_v_tpms_temp->IsStale() ? " [stale]" : "");
data_shown = true;

View File

@ -402,6 +402,180 @@ $.fn.loadcmd = function(command, filter, timeout) {
var monitorTimer, last_monotonic = 0;
var ws, ws_inhibit = 0;
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 loghist = [];
const loghist_maxsize = 100;
@ -415,6 +589,7 @@ function initSocketConnection(){
ws.onopen = function(ev) {
console.log("WebSocket OPENED", ev);
$(".receiver").subscribe();
subscribeToTopic("units/#");
};
ws.onerror = function(ev) { console.log("WebSocket ERROR", ev); };
ws.onclose = function(ev) { console.log("WebSocket CLOSED", ev); };
@ -443,6 +618,21 @@ function initSocketConnection(){
$.extend(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") {
processNotification(msg.notify);
$(".receiver").trigger("msg:notify", msg.notify);
@ -513,7 +703,14 @@ function processNotification(msg) {
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) {
return this.each(function() {
var subscriptions = $(this).data("subscriptions");
@ -526,13 +723,7 @@ $.fn.subscribe = function(topics) {
var tops = topics ? topics.split(' ') : [];
for (var i = 0; i < tops.length; i++) {
if (tops[i] && !subs.includes(tops[i])) {
try {
console.log("subscribe " + tops[i]);
if (ws) ws.send("subscribe " + tops[i]);
subs.push(tops[i]);
} catch (e) {
console.log(e);
}
subscribeToTopic(tops[i]);
}
}
$(this).data("subscriptions", subs.join(' '));
@ -1721,20 +1912,43 @@ $(function(){
// Metrics displays:
$("body").on('msg:metrics', '.receiver', function(e, update) {
$(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;
// filter:
var keys = metric.split(","), val;
var metricName = "";
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;
// process:
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")) {
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);
$el.children(".value").text(vf);
} else if ($el.hasClass("progress")) {

View File

@ -829,6 +829,180 @@ $.fn.loadcmd = function(command, filter, timeout) {
var monitorTimer, last_monotonic = 0;
var ws, ws_inhibit = 0;
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 loghist = [];
const loghist_maxsize = 100;
@ -842,6 +1016,7 @@ function initSocketConnection(){
ws.onopen = function(ev) {
console.log("WebSocket OPENED", ev);
$(".receiver").subscribe();
subscribeToTopic("units/#");
};
ws.onerror = function(ev) { console.log("WebSocket ERROR", ev); };
ws.onclose = function(ev) { console.log("WebSocket CLOSED", ev); };
@ -870,6 +1045,21 @@ function initSocketConnection(){
$.extend(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") {
processNotification(msg.notify);
$(".receiver").trigger("msg:notify", msg.notify);
@ -940,7 +1130,14 @@ function processNotification(msg) {
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) {
return this.each(function() {
var subscriptions = $(this).data("subscriptions");
@ -953,13 +1150,7 @@ $.fn.subscribe = function(topics) {
var tops = topics ? topics.split(' ') : [];
for (var i = 0; i < tops.length; i++) {
if (tops[i] && !subs.includes(tops[i])) {
try {
console.log("subscribe " + tops[i]);
if (ws) ws.send("subscribe " + tops[i]);
subs.push(tops[i]);
} catch (e) {
console.log(e);
}
subscribeToTopic(tops[i]);
}
}
$(this).data("subscriptions", subs.join(' '));
@ -2148,20 +2339,43 @@ $(function(){
// Metrics displays:
$("body").on('msg:metrics', '.receiver', function(e, update) {
$(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;
// filter:
var keys = metric.split(","), val;
var metricName = "";
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;
// process:
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")) {
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);
$el.children(".value").text(vf);
} 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
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
``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
display. This is covered by the ``metric`` widget class family as shown here.
@ -30,6 +49,11 @@ The following example covers…
- Gauges
- 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
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>");
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 extram::string text);
void print(const char* text);
void printf(const char *fmt, ...);
void printf(const char *fmt, ...) __attribute__ ((format (printf, 2, 3)));
void done();
void panel_start(const char* type, const char* title);
void panel_end(const char* footer="");
@ -367,6 +367,8 @@ enum WebSocketTxJobType
WSTX_Config, // payload: config (todo)
WSTX_Notify, // payload: notification
WSTX_LogBuffers, // payload: logbuffers
WSTX_UnitMetricUpdate, // payload: -
WSTX_UnitPrefsUpdate, // payload: -
};
struct WebSocketTxJob
@ -412,6 +414,10 @@ class WebSocketHandler : public MgHandler, public OvmsWriter
void Unsubscribe(std::string topic);
bool IsSubscribedTo(std::string topic);
void SubscriptionChanged();
void UnitsCheckSubscribe();
void UnitsCheckVehicleSubscribe();
// OvmsWriter:
public:
void Log(LogBuffers* message);
@ -428,7 +434,10 @@ class WebSocketHandler : public MgHandler, public OvmsWriter
WebSocketTxJob m_job = {};
int m_sent = 0;
int m_ack = 0;
int m_last = 0; // last entry sent up
std::set<std::string> m_subscriptions;
bool m_units_subscribed;
bool m_units_prefs_subscribed;
};
struct WebSocketSlot
@ -469,7 +478,7 @@ class HttpCommandStream : public OvmsShell, public MgHandler
void Initialize(bool print);
virtual bool IsInteractive() { return false; }
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);
void Log(LogBuffers* message);
};
@ -536,7 +545,7 @@ class OvmsWebServer : public ExternalRamAllocated
static void HandleLogin(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 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:
static void HandleStatus(PageEntry_t& p, PageContext_t& c);
@ -610,6 +619,80 @@ class OvmsWebServer : public ExternalRamAllocated
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:

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_dropcntref = 0;
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:
SetMonitoring(true);
@ -116,39 +121,42 @@ void WebSocketHandler::ProcessTxJob()
case WSTX_MetricsAll:
case WSTX_MetricsUpdate:
{
// Note: this loops over the metrics by index, keeping the checked count
// in m_sent. 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.
// 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.
// find start:
int i;
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:
std::string msg;
msg.reserve(2*XFER_CHUNK_SIZE+128);
msg = "{\"metrics\":{";
for (i=0; m && msg.size() < XFER_CHUNK_SIZE; m=m->m_next) {
if (m->IsModifiedAndClear(m_modifier) || m_job.type == WSTX_MetricsAll) {
if (i) msg += ',';
msg += '\"';
msg += m->m_name;
msg += "\":";
msg += m->AsJSON();
i++;
if (m) {
std::string msg;
msg.reserve(2*XFER_CHUNK_SIZE+128);
msg = "{\"metrics\":{";
for (i=0; m && msg.size() < XFER_CHUNK_SIZE; m=m->m_next) {
++m_last;
if (m->IsModifiedAndClear(m_modifier) || m_job.type == WSTX_MetricsAll) {
if (i) msg += ',';
msg += '\"';
msg += m->m_name;
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?
if (!m && m_ack == m_sent) {
if (m_sent)
@ -158,7 +166,135 @@ void WebSocketHandler::ProcessTxJob()
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:
{
if (m_sent && m_ack == m_job.notification->GetValueSize()+1) {
@ -300,7 +436,7 @@ bool WebSocketHandler::GetNextTxJob()
if (!m_jobqueue) return false;
if (xQueueReceive(m_jobqueue, &m_job, 0) == pdTRUE) {
// init new job state:
m_sent = m_ack = 0;
m_sent = m_ack = m_last = 0;
return true;
} else {
return false;
@ -610,17 +746,31 @@ void OvmsWebServer::UpdateTicker(TimerHandle_t timer)
break;
}
}
// trigger metrics update:
// trigger metrics update if required.
unsigned long mask_all = MyMetrics.GetUnitSendAll();
for (auto slot: MyWebServer.m_client_slots) {
if (slot.handler)
if (slot.handler) {
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);
}
/**
* Notifications:
*/
@ -642,18 +792,57 @@ void WebSocketHandler::Subscribe(std::string topic)
}
m_subscriptions.insert(topic);
ESP_LOGD(TAG, "WebSocketHandler[%p]: subscription '%s' added", m_nc, topic.c_str());
SubscriptionChanged();
}
void WebSocketHandler::Unsubscribe(std::string topic)
{
bool changed = false;
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()))) {
ESP_LOGD(TAG, "WebSocketHandler[%p]: subscription '%s' removed", m_nc, (*it).c_str());
it = m_subscriptions.erase(it);
changed = true;
} else {
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)

View File

@ -74,6 +74,12 @@ void OvmsWebServer::HandleStatus(PageEntry_t& p, PageContext_t& c)
c.done();
return;
}
else {
// "network restart", "wifi reconnect"
OutputReconnect(p, c, NULL, cmd.c_str());
c.done();
return;
}
}
PAGE_HOOK("body.pre");
@ -209,7 +215,10 @@ void OvmsWebServer::HandleStatus(PageEntry_t& p, PageContext_t& c)
c.panel_start("primary", "Network");
output = ExecuteCommand("network status");
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(
"</div>"
@ -218,7 +227,10 @@ void OvmsWebServer::HandleStatus(PageEntry_t& p, PageContext_t& c)
c.panel_start("primary", "Wifi");
output = ExecuteCommand("wifi status");
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(
"</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.panel_end(
"<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 off\">Stop 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 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>"
"</ul>");
@ -591,9 +605,13 @@ void OvmsWebServer::HandleCfgPassword(PageEntry_t& p, PageContext_t& c)
void OvmsWebServer::HandleCfgVehicle(PageEntry_t& p, PageContext_t& c)
{
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::map<metric_group_t,std::string> units_values;
metric_group_list_t unit_groups;
OvmsMetricGroupConfigList(unit_groups);
if (c.method == "POST") {
// process form submission:
vehicleid = c.getvar("vehicleid");
@ -601,7 +619,13 @@ void OvmsWebServer::HandleCfgVehicle(PageEntry_t& p, PageContext_t& c)
vehiclename = c.getvar("vehiclename");
timezone = c.getvar("timezone");
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_ref = c.getvar("bat12v_ref");
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", "timezone", timezone);
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("vehicle", "12v.ref", bat12v_ref);
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");
timezone = MyConfig.GetParamValue("vehicle", "timezone");
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_ref = MyConfig.GetParamValue("vehicle", "12v.ref");
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));
c.input_radiobtn_start("Distance units", "units_distance");
c.input_radiobtn_option("units_distance", "Kilometers", "K", units_distance == "K");
c.input_radiobtn_option("units_distance", "Miles", "M", units_distance == "M");
c.input_radiobtn_end();
for ( auto grpiter = unit_groups.begin(); grpiter != unit_groups.end(); ++grpiter) {
std::string name = OvmsMetricGroupName(*grpiter);
metric_unit_set_t group_units;
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",
"<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 (c.getvar("action") == "save")
{
if (c.getvar("action") == "save") {
// save:
param->m_map.clear();
param->m_map = std::move(pmap);
@ -975,9 +1034,8 @@ void OvmsWebServer::HandleCfgPushover(PageEntry_t& p, PageContext_t& c)
OutputHome(p, c);
c.done();
return;
}
else if (c.getvar("action") == "test")
{
} else if (c.getvar("action") == "test")
{
std::string reply;
std::string popup;
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("expire").c_str()),
true /* receive server reply as reply/pushover-type notification */ ))
{
{
c.alert("danger", "<p class=\"lead\">Could not send test message!</p>");
}
}
}
}
else {
// 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><input type=\"text\" class=\"form-control\" name=\"nfy_%d\" value=\"%s\" placeholder=\"Enter notification type/subtype\""
" 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);
gen_options_priority(kv.second);
@ -3931,7 +3989,7 @@ void OvmsWebServer::HandleEditor(PageEntry_t& p, PageContext_t& c)
"text-align: center !important;\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-W { color: darkorange; }\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 error, info;
std::string vehicletype, units_distance;
std::string vehicletype, units_distance, units_temp, units_pressure;
std::string server, vehicleid, password;
if (c.method == "POST") {
@ -905,6 +905,8 @@ std::string OvmsWebServer::CfgInit4(PageEntry_t& p, PageContext_t& c, std::strin
// process form input:
vehicletype = c.getvar("vehicletype");
units_distance = c.getvar("units_distance");
units_temp = c.getvar("units_temp");
units_pressure = c.getvar("units_pressure");
server = c.getvar("server");
vehicleid = c.getvar("vehicleid");
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>";
// 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);
// configure server:
@ -945,14 +962,17 @@ std::string OvmsWebServer::CfgInit4(PageEntry_t& p, PageContext_t& c, std::strin
// read configuration:
vehicleid = MyConfig.GetParamValue("vehicle", "id");
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");
password = MyConfig.GetParamValue("password","server.v2");
// default data server = ota server:
if (server.empty()) {
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";
else
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_end();
c.input_radiobtn_start("Distance units", "units_distance");
c.input_radiobtn_option("units_distance", "Kilometers", "K", units_distance == "K");
c.input_radiobtn_option("units_distance", "Miles", "M", units_distance == "M");
bool is_metric = units_distance != "miles";
c.input_radiobtn_start("Distance related units", "units_distance");
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_radio_start("OVMS data server", "server");

View File

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

View File

@ -49,22 +49,32 @@
* & &amp;
*/
std::string PageContext::encode_html(const char* text) {
std::string buf;
for (int i=0; i<strlen(text); i++) {
if (text[i] == '\"')
buf += "&quot;";
else if (text[i] == '\'')
buf += "&#x27;";
else if(text[i] == '<')
buf += "&lt;";
else if(text[i] == '>')
buf += "&gt;";
else if(text[i] == '&')
buf += "&amp;";
else
buf += text[i];
int len = strlen(text);
std::string buf;
buf.reserve(len);
for (int i=0; i < len; i++) {
char ch = text[i];
switch(ch) {
case '\"':
buf.append("&quot;");
break;
case '\'':
buf.append("&#x27;");
break;
case '<':
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) {
@ -72,23 +82,31 @@ std::string PageContext::encode_html(std::string text) {
}
extram::string PageContext::encode_html(const extram::string& text) {
extram::string buf;
extram::string buf;
buf.reserve(text.length() + 500);
for (int i=0; i<text.length(); i++) {
if (text[i] == '\"')
buf += "&quot;";
else if (text[i] == '\'')
buf += "&#x27;";
else if(text[i] == '<')
buf += "&lt;";
else if(text[i] == '>')
buf += "&gt;";
else if(text[i] == '&')
buf += "&amp;";
else
buf += text[i];
for (int i=0; i<text.length(); i++) {
char ch = text[i];
switch (ch) {
case '\"':
buf.append("&quot;");
break;
case '\'':
buf.append("&#x27;");
break;
case '<':
buf.append("&lt;");
break;
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())
@ -102,11 +120,12 @@ extram::string PageContext::encode_html(const extram::string& text) {
*
*/
std::string PageContext::make_id(const char* text) {
std::string buf;
std::string buf;
char lc = 0;
for (int i=0; i<strlen(text); i++) {
if (isalnum(text[i]))
buf += (lc = tolower(text[i]));
int len = strlen(text);
for (int i=0; i<len; i++) {
if (isalnum(text[i]))
buf += (lc = tolower(text[i]));
else if (lc && lc != '-')
buf += (lc = '-');
}
@ -114,7 +133,7 @@ std::string PageContext::make_id(const char* text) {
lc = buf.back();
buf.pop_back();
}
return buf;
return buf;
}
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>"
"<div class=\"col-sm-9\">"
"<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\">"
"<input class=\"slider-enable\" type=\"%s\" %s> "
"<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)
, label
, defval, value, min, max, step
, (enabled != 0) ? "true" : "false"
, (enabled < 0) ? "hidden" : "checkbox" // -1 => no checkbox
, (enabled > 0) ? "checked" : ""
, (enabled == 0) ? "disabled" : ""
@ -714,7 +734,8 @@ void OvmsWebServer::OutputReboot(PageEntry_t& p, PageContext_t& c)
/**
* 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(
"<div class=\"alert alert-warning\">"
@ -733,8 +754,16 @@ void OvmsWebServer::OutputReconnect(PageEntry_t& p, PageContext_t& c, const char
"$(\"#dots\").append(\"\");"
"}"
"}, 1000);"
"</script>"
, 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";
}
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);
}

View File

@ -199,7 +199,7 @@ void scanStart(int, OvmsWriter* writer, OvmsCommand*, int argc, const char* cons
if (start > end)
{
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;
}
@ -209,7 +209,7 @@ void scanStart(int, OvmsWriter* writer, OvmsCommand*, int argc, const char* cons
}
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;
}
if (!valid)
@ -229,7 +229,7 @@ void scanStart(int, OvmsWriter* writer, OvmsCommand*, int argc, const char* cons
if (valid)
{
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);
}
}

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)
: pcp(name)
{
m_host = SDMMC_HOST_DEFAULT();
m_host = sdmmc_host_t SDMMC_HOST_DEFAULT();
if (mode1bit)
{
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
// 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");
break;
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;
}
return modem::None;

View File

@ -73,6 +73,7 @@ void simcom7000::PowerCycle()
m_powercyclefactor = m_powercyclefactor % 3;
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
#ifdef CONFIG_OVMS_COMP_MAX7317
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");
break;
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;
}
return modem::None;

View File

@ -80,6 +80,7 @@ void simcom7600::StartupNMEA()
{
m_modem->muxtx(GetMuxChannelCMD(), "AT+CGPS=0\r\n");
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+CGPSINFOCFG=5,258\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"); }
}
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()
{
if (m_modem->m_mux != NULL)
@ -126,6 +142,7 @@ void simcom7600::PowerCycle()
m_powercyclefactor = m_powercyclefactor % 3;
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
#ifdef CONFIG_OVMS_COMP_MAX7317
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");
break;
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;
}
return modem::None;

View File

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

View File

@ -7,6 +7,8 @@
# 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_SRCDIRS:=src
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_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));
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)
{
metric_unit_t rangeUnit = (MyConfig.GetParamValue("vehicle", "units.distance") == "M") ? Miles : Kilometers;
bool chargeport_open = StdMetrics.ms_v_door_chargeport->AsBool();
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:
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("-", speedUnit, 1).c_str());
writer->printf("%s\n", StdMetrics.ms_v_bat_range_speed->AsUnitString("-", ToUser, 1).c_str());
}
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();
if (duration_soc > 0)
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);
int duration_range = StdMetrics.ms_v_charge_duration_range->AsInt();
if (duration_range > 0)
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);
}
@ -1062,12 +1062,12 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStat(int verbosity, OvmsWrite
if (StdMetrics.ms_v_charge_kwh_grid->IsDefined())
{
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())
{
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
@ -1075,35 +1075,35 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStat(int verbosity, OvmsWrite
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())
{
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());
}
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());
}
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());
}
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());
}
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());
}
@ -1115,12 +1115,12 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStat(int verbosity, OvmsWrite
*/
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 speedUnit = (rangeUnit == Miles) ? Mph : Kph;
metric_unit_t accelUnit = (rangeUnit == Miles) ? MphPS : KphPS;
metric_unit_t consumUnit = (rangeUnit == Miles) ? WattHoursPM : WattHoursPK;
metric_unit_t rangeUnit = OvmsMetricGetUserUnit(GrpDistance, Kilometers);
metric_unit_t speedUnit = OvmsMetricGetUserUnit(GrpSpeed, Kph);
metric_unit_t accelUnit = OvmsMetricGetUserUnit(GrpAccel, KphPS);
metric_unit_t consumUnit = OvmsMetricGetUserUnit(GrpConsumption, WattHoursPK);
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* speedUnitLabel = OvmsMetricUnitLabel(speedUnit);
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* 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)
? 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_recd = StdMetrics.ms_v_bat_energy_recd->AsFloat();
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_diff = soc - m_drive_startsoc;
@ -1158,7 +1158,7 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStatTrip(int verbosity, OvmsW
<< "Trip "
<< std::fixed
<< std::setprecision(1)
<< trip_length << rangeUnitLabel
<< UnitConvert(Kilometers, rangeUnit, trip_length) << rangeUnitLabel
<< " Avg "
<< std::setprecision(0)
<< speed_avg << speedUnitLabel
@ -1166,11 +1166,11 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::CommandStatTrip(int verbosity, OvmsW
<< ((alt_diff >= 0) ? "+" : "")
<< alt_diff << altitudeUnitLabel
;
if (wh_per_rangeunit != 0)
if (wh_per_km != 0)
{
buf
<< "\nEnergy "
<< wh_per_rangeunit << consumUnitLabel
<< UnitConvert(WattHoursPK, consumUnit, wh_per_km) << consumUnitLabel
<< ", "
<< energy_recd_perc << "% recd"
;
@ -2012,68 +2012,82 @@ OvmsVehicle::vehicle_command_t OvmsVehicle::ProcessMsgCommand(std::string &resul
*/
void OvmsVehicle::GetDashboardConfig(DashboardConfig& cfg)
{
cfg.gaugeset1 =
"yAxis: [{"
// Speed:
"min: 0, max: 200,"
"plotBands: ["
"{ from: 0, to: 120, className: 'green-band' },"
"{ from: 120, to: 160, className: 'yellow-band' },"
"{ from: 160, to: 200, className: 'red-band' }]"
"},{"
// Voltage:
"min: 310, max: 410,"
"plotBands: ["
"{ from: 310, to: 325, className: 'red-band' },"
"{ from: 325, to: 340, className: 'yellow-band' },"
"{ from: 340, to: 410, className: 'green-band' }]"
"},{"
// SOC:
"min: 0, max: 100,"
"plotBands: ["
"{ from: 0, to: 12.5, className: 'red-band' },"
"{ from: 12.5, to: 25, className: 'yellow-band' },"
"{ from: 25, to: 100, className: 'green-band' }]"
"},{"
// Efficiency:
"min: 0, max: 400,"
"plotBands: ["
"{ from: 0, to: 200, className: 'green-band' },"
"{ from: 200, to: 300, className: 'yellow-band' },"
"{ from: 300, to: 400, className: 'red-band' }]"
"},{"
// Power:
"min: -50, max: 200,"
"plotBands: ["
"{ from: -50, to: 0, className: 'violet-band' },"
"{ from: 0, to: 100, className: 'green-band' },"
"{ from: 100, to: 150, className: 'yellow-band' },"
"{ from: 150, to: 200, className: 'red-band' }]"
"},{"
// Charger temperature:
"min: 20, max: 80, tickInterval: 20,"
"plotBands: ["
"{ from: 20, to: 65, className: 'normal-band border' },"
"{ from: 65, to: 80, className: 'red-band border' }]"
"},{"
// Battery temperature:
"min: -15, max: 65, tickInterval: 25,"
"plotBands: ["
"{ from: -15, to: 0, className: 'red-band border' },"
"{ from: 0, to: 50, className: 'normal-band border' },"
"{ from: 50, to: 65, className: 'red-band border' }]"
"},{"
// Inverter temperature:
"min: 20, max: 80, tickInterval: 20,"
"plotBands: ["
"{ from: 20, to: 70, className: 'normal-band border' },"
"{ from: 70, to: 80, className: 'red-band border' }]"
"},{"
// Motor temperature:
"min: 50, max: 125, tickInterval: 25,"
"plotBands: ["
"{ from: 50, to: 110, className: 'normal-band border' },"
"{ from: 110, to: 125, className: 'red-band border' }]"
"}]";
// Speed:
dash_gauge_t speed_dash(NULL,Kph);
speed_dash.SetMinMax(0, 200, 5);
speed_dash.AddBand("green", 0, 120);
speed_dash.AddBand("yellow", 120, 160);
speed_dash.AddBand("red", 160, 200);
// Voltage:
dash_gauge_t voltage_dash(NULL,Volts);
voltage_dash.SetMinMax(310, 410);
voltage_dash.AddBand("red", 310, 325);
voltage_dash.AddBand("yellow", 325, 340);
voltage_dash.AddBand("green", 340, 410);
// SOC:
dash_gauge_t soc_dash("SOC ",Percentage);
soc_dash.SetMinMax(0, 100);
soc_dash.AddBand("red", 0, 12.5);
soc_dash.AddBand("yellow", 12.5, 25);
soc_dash.AddBand("green", 25, 100);
// Efficiency:
dash_gauge_t eff_dash(NULL,WattHoursPK);
eff_dash.SetMinMax(0, 400);
eff_dash.AddBand("green", 0, 200);
eff_dash.AddBand("yellow", 200, 300);
eff_dash.AddBand("red", 300, 400);
// Power:
dash_gauge_t power_dash(NULL,kW);
power_dash.SetMinMax(-50, 200);
power_dash.AddBand("violet", -50, 0);
power_dash.AddBand("green", 0, 100);
power_dash.AddBand("yellow", 100, 150);
power_dash.AddBand("red", 150, 200);
// Charger temperature:
dash_gauge_t charget_dash("CHG ",Celcius);
charget_dash.SetMinMax(20, 80);
charget_dash.SetTick(20);
charget_dash.AddBand("normal", 20, 65);
charget_dash.AddBand("red", 65, 80);
// Battery temperature:
dash_gauge_t batteryt_dash("BAT ",Celcius);
batteryt_dash.SetMinMax(-15, 65);
batteryt_dash.SetTick(25);
batteryt_dash.AddBand("red", -15, 0);
batteryt_dash.AddBand("normal", 0, 50);
batteryt_dash.AddBand("red", 50, 65);
// Inverter temperature:
dash_gauge_t invertert_dash("PEM ",Celcius);
invertert_dash.SetMinMax(20, 80);
invertert_dash.SetTick(20);
invertert_dash.AddBand("normal", 20, 70);
invertert_dash.AddBand("red", 70, 80);
// Motor temperature:
dash_gauge_t motort_dash("MOT ",Celcius);
motort_dash.SetMinMax(50, 125);
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

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);
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 twarn=0, talert=0;
@ -642,13 +650,13 @@ void OvmsVehicle::BmsStatus(int verbosity, OvmsWriter* writer, vehicle_bms_statu
if (show_temperature)
{
writer->puts("Temperature:");
writer->printf(" Average: %5.1fC [%5.1fC - %5.1fC]\n",
StdMetrics.ms_v_bat_pack_tavg->AsFloat(),
StdMetrics.ms_v_bat_pack_tmin->AsFloat(),
StdMetrics.ms_v_bat_pack_tmax->AsFloat());
writer->printf(" Deviation: SD %6.2fC [max %.2fC], %d warnings, %d alerts\n",
StdMetrics.ms_v_bat_pack_tstddev->AsFloat(),
StdMetrics.ms_v_bat_pack_tstddev_max->AsFloat(),
writer->printf(" Average: %5.1f%s [%5.1f%s - %5.1f%s]\n",
StdMetrics.ms_v_bat_pack_tavg->AsFloat(0, user_temp), temp_unit.c_str(),
StdMetrics.ms_v_bat_pack_tmin->AsFloat(0, user_temp), temp_unit.c_str(),
StdMetrics.ms_v_bat_pack_tmax->AsFloat(0, user_temp), temp_unit.c_str());
writer->printf(" Deviation: SD %6.2f%s [max %.2f%s], %d warnings, %d alerts\n",
StdMetrics.ms_v_bat_pack_tstddev->AsFloat(0, user_temp), temp_unit.c_str(),
StdMetrics.ms_v_bat_pack_tstddev_max->AsFloat(0, user_temp), temp_unit.c_str(),
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))
{
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;
++kt;
}
@ -788,8 +796,9 @@ bool OvmsVehicle::FormatBmsAlerts(int verbosity, OvmsWriter* writer, bool show_w
writer->printf("%s\n", has_valerts ? "" : ", cells OK");
// Temperatures:
// (Note: '°' is not SMS safe, so we only output 'C')
writer->printf("Temperature: StdDev %.1fC", StdMetrics.ms_v_bat_pack_tstddev_max->AsFloat());
metric_unit_t user_temp = OvmsMetricGetUserUnit(GrpTemp, Celcius);
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++)
{
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++;
if (verbose || has_talerts <= 5)
{
float dev = StdMetrics.ms_v_bat_cell_tdevmax->GetElemValue(i);
writer->printf("\n %c #%02d: %+3.1fC", (sts==OvmsStatus::Warn) ? '?' : '!', i+1, dev);
float dev = StdMetrics.ms_v_bat_cell_tdevmax->GetElemValue(i, user_temp);
writer->printf("\n %c #%02d: %+3.1f%s", (sts==OvmsStatus::Warn) ? '?' : '!', i+1, dev, temp_unit.c_str());
}
else
{