diff --git a/OVMS.V3/components/can/src/can.cpp b/OVMS.V3/components/can/src/can.cpp index d3363d0..afcd931 100644 --- a/OVMS.V3/components/can/src/can.cpp +++ b/OVMS.V3/components/can/src/can.cpp @@ -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) diff --git a/OVMS.V3/components/can/src/can.h b/OVMS.V3/components/can/src/can.h index f4b757b..fb0f52a 100644 --- a/OVMS.V3/components/can/src/can.h +++ b/OVMS.V3/components/can/src/can.h @@ -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: diff --git a/OVMS.V3/components/can/src/canformat_crtd.cpp b/OVMS.V3/components/can/src/canformat_crtd.cpp index 62207bd..50736f8 100644 --- a/OVMS.V3/components/can/src/canformat_crtd.cpp +++ b/OVMS.V3/components/can/src/canformat_crtd.cpp @@ -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); diff --git a/OVMS.V3/components/can/src/canlog.cpp b/OVMS.V3/components/can/src/canlog.cpp index cffeba1..762fa2e 100644 --- a/OVMS.V3/components/can/src/canlog.cpp +++ b/OVMS.V3/components/can/src/canlog.cpp @@ -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::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{}(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{}(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::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; diff --git a/OVMS.V3/components/can/src/canlog.h b/OVMS.V3/components/can/src/canlog.h index 3bcbe70..4b219d1 100644 --- a/OVMS.V3/components/can/src/canlog.h +++ b/OVMS.V3/components/can/src/canlog.h @@ -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, 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__ diff --git a/OVMS.V3/components/can/src/canlog_monitor.cpp b/OVMS.V3/components/can/src/canlog_monitor.cpp index 15d976f..6002773 100644 --- a/OVMS.V3/components/can/src/canlog_monitor.cpp +++ b/OVMS.V3/components/can/src/canlog_monitor.cpp @@ -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: diff --git a/OVMS.V3/components/can/src/canlog_vfs.cpp b/OVMS.V3/components/can/src/canlog_vfs.cpp index e0a0323..9b57b9f 100644 --- a/OVMS.V3/components/can/src/canlog_vfs.cpp +++ b/OVMS.V3/components/can/src/canlog_vfs.cpp @@ -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(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(); diff --git a/OVMS.V3/components/can/src/canlog_vfs.h b/OVMS.V3/components/can/src/canlog_vfs.h index 77b190d..88e26e4 100644 --- a/OVMS.V3/components/can/src/canlog_vfs.h +++ b/OVMS.V3/components/can/src/canlog_vfs.h @@ -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; diff --git a/OVMS.V3/components/console_ssh/src/console_ssh.h b/OVMS.V3/components/console_ssh/src/console_ssh.h index 1d22d24..7994505 100644 --- a/OVMS.V3/components/console_ssh/src/console_ssh.h +++ b/OVMS.V3/components/console_ssh/src/console_ssh.h @@ -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; } diff --git a/OVMS.V3/components/console_telnet/src/console_telnet.h b/OVMS.V3/components/console_telnet/src/console_telnet.h index fe1885c..d112c5f 100644 --- a/OVMS.V3/components/console_telnet/src/console_telnet.h +++ b/OVMS.V3/components/console_telnet/src/console_telnet.h @@ -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: diff --git a/OVMS.V3/components/ovms_cellular/src/ovms_cellular.cpp b/OVMS.V3/components/ovms_cellular/src/ovms_cellular.cpp index 175eb88..6961ac9 100644 --- a/OVMS.V3/components/ovms_cellular/src/ovms_cellular.cpp +++ b/OVMS.V3/components/ovms_cellular/src/ovms_cellular.cpp @@ -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) diff --git a/OVMS.V3/components/ovms_cellular/src/ovms_cellular.h b/OVMS.V3/components/ovms_cellular/src/ovms_cellular.h index 7e34781..a00db8c 100644 --- a/OVMS.V3/components/ovms_cellular/src/ovms_cellular.h +++ b/OVMS.V3/components/ovms_cellular/src/ovms_cellular.h @@ -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(); diff --git a/OVMS.V3/components/ovms_cellular/src/ovms_cellular_modem_driver.cpp b/OVMS.V3/components/ovms_cellular/src/ovms_cellular_modem_driver.cpp index 9be08e7..85fb8e7 100644 --- a/OVMS.V3/components/ovms_cellular/src/ovms_cellular_modem_driver.cpp +++ b/OVMS.V3/components/ovms_cellular/src/ovms_cellular_modem_driver.cpp @@ -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"); } } diff --git a/OVMS.V3/components/ovms_http/src/ovms_http.cpp b/OVMS.V3/components/ovms_http/src/ovms_http.cpp index c75cfa1..aadd2bf 100644 --- a/OVMS.V3/components/ovms_http/src/ovms_http.cpp +++ b/OVMS.V3/components/ovms_http/src/ovms_http.cpp @@ -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()); } diff --git a/OVMS.V3/components/ovms_location/src/ovms_location.cpp b/OVMS.V3/components/ovms_location/src/ovms_location.cpp index ddf4988..dc8fc26 100644 --- a/OVMS.V3/components/ovms_location/src/ovms_location.cpp +++ b/OVMS.V3/components/ovms_location/src/ovms_location.cpp @@ -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, " [ []]", 1, 4); - cmd_location->RegisterCommand("radius","Set the radius of a location",location_radius, " ", 2, 2, true, location_validate); + cmd_location->RegisterCommand("set","Set the position of a location",location_set, " [ [ []] ]", 1, 5, true, location_set_validate); + cmd_location->RegisterCommand("radius","Set the radius of a location (defaults to user 'height' units)",location_radius, " []", 2, 3, true, location_radius_validate); cmd_location->RegisterCommand("rm","Remove a defined location",location_rm, "", 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"); diff --git a/OVMS.V3/components/ovms_netlib/src/ovms_nethttp.cpp b/OVMS.V3/components/ovms_netlib/src/ovms_nethttp.cpp index 2d6acf4..37f4c2f 100644 --- a/OVMS.V3/components/ovms_netlib/src/ovms_nethttp.cpp +++ b/OVMS.V3/components/ovms_netlib/src/ovms_nethttp.cpp @@ -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); diff --git a/OVMS.V3/components/ovms_ota/src/ovms_ota.cpp b/OVMS.V3/components/ovms_ota/src/ovms_ota.cpp index 723dfa2..f107c63 100644 --- a/OVMS.V3/components/ovms_ota/src/ovms_ota.cpp +++ b/OVMS.V3/components/ovms_ota/src/ovms_ota.cpp @@ -38,7 +38,9 @@ static const char *TAG = "ota"; #include #include #include +#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()); diff --git a/OVMS.V3/components/ovms_plugins/src/ovms_plugins.cpp b/OVMS.V3/components/ovms_plugins/src/ovms_plugins.cpp index d094c73..c3569f5 100644 --- a/OVMS.V3/components/ovms_plugins/src/ovms_plugins.cpp +++ b/OVMS.V3/components/ovms_plugins/src/ovms_plugins.cpp @@ -36,7 +36,10 @@ static const char *TAG = "pluginstore"; #include #include #include +#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" diff --git a/OVMS.V3/components/ovms_script/srcduk/ovms_duk_http.cpp b/OVMS.V3/components/ovms_script/srcduk/ovms_duk_http.cpp index 75126af..638ac4d 100644 --- a/OVMS.V3/components/ovms_script/srcduk/ovms_duk_http.cpp +++ b/OVMS.V3/components/ovms_script/srcduk/ovms_duk_http.cpp @@ -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? diff --git a/OVMS.V3/components/ovms_tpms/src/ovms_tpms.cpp b/OVMS.V3/components/ovms_tpms/src/ovms_tpms.cpp index b5a5b43..464204b 100644 --- a/OVMS.V3/components/ovms_tpms/src/ovms_tpms.cpp +++ b/OVMS.V3/components/ovms_tpms/src/ovms_tpms.cpp @@ -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; diff --git a/OVMS.V3/components/ovms_webserver/assets/ovms.js b/OVMS.V3/components/ovms_webserver/assets/ovms.js index f5565a0..553aa7a 100644 --- a/OVMS.V3/components/ovms_webserver/assets/ovms.js +++ b/OVMS.V3/components/ovms_webserver/assets/ovms.js @@ -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; imiles": 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; iloggedin = false; $(\"#menu\").load(\"/menu\"); loaduri(\"#main\", \"get\", \"/home\", {})"); 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; +} diff --git a/OVMS.V3/components/ovms_webserver/src/ovms_webserver.h b/OVMS.V3/components/ovms_webserver/src/ovms_webserver.h index 5ed0ff1..90c4bb3 100644 --- a/OVMS.V3/components/ovms_webserver/src/ovms_webserver.h +++ b/OVMS.V3/components/ovms_webserver/src/ovms_webserver.h @@ -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 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 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: diff --git a/OVMS.V3/components/ovms_webserver/src/ovms_websockethandler.cpp b/OVMS.V3/components/ovms_webserver/src/ovms_websockethandler.cpp index 1a75301..8303da9 100644 --- a/OVMS.V3/components/ovms_webserver/src/ovms_websockethandler.cpp +++ b/OVMS.V3/components/ovms_webserver/src/ovms_websockethandler.cpp @@ -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) diff --git a/OVMS.V3/components/ovms_webserver/src/web_cfg.cpp b/OVMS.V3/components/ovms_webserver/src/web_cfg.cpp index 52abbb9..18f57d5 100644 --- a/OVMS.V3/components/ovms_webserver/src/web_cfg.cpp +++ b/OVMS.V3/components/ovms_webserver/src/web_cfg.cpp @@ -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("%s", _html(output)); - c.panel_end(); + c.panel_end( + "
    " + "
  • " + "
"); c.print( "" @@ -218,7 +227,10 @@ void OvmsWebServer::HandleStatus(PageEntry_t& p, PageContext_t& c) c.panel_start("primary", "Wifi"); output = ExecuteCommand("wifi status"); c.printf("%s", _html(output)); - c.panel_end(); + c.panel_end( + "
    " + "
  • " + "
"); c.print( "" @@ -229,8 +241,10 @@ void OvmsWebServer::HandleStatus(PageEntry_t& p, PageContext_t& c) c.printf("%s", _html(output)); c.panel_end( "
    " - "
  • " - "
  • " + "
  • " + "
  • " + "
  • " + "
  • " "
  • " "
"); @@ -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 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", "

Vehicle PIN code used for unlocking etc.

", "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", "

Could not send test message!

"); - } } + } } else { // output error, return to form: @@ -1163,7 +1221,7 @@ void OvmsWebServer::HandleCfgPushover(PageEntry_t& p, PageContext_t& c) "" "" - "" , 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" diff --git a/OVMS.V3/components/ovms_webserver/src/web_cfg_init.cpp b/OVMS.V3/components/ovms_webserver/src/web_cfg_init.cpp index 43768b7..4d73915 100644 --- a/OVMS.V3/components/ovms_webserver/src/web_cfg_init.cpp +++ b/OVMS.V3/components/ovms_webserver/src/web_cfg_init.cpp @@ -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 += "
  • Vehicle ID may only contain ASCII letters, digits and '-'
  • "; // 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"); diff --git a/OVMS.V3/components/ovms_webserver/src/web_displays.cpp b/OVMS.V3/components/ovms_webserver/src/web_displays.cpp index 25a3d25..f7c6bdb 100644 --- a/OVMS.V3/components/ovms_webserver/src/web_displays.cpp +++ b/OVMS.V3/components/ovms_webserver/src/web_displays.cpp @@ -305,7 +305,7 @@ void OvmsWebServer::HandleDashboard(PageEntry_t& p, PageContext_t& c) "" "
    " "
    0
    " - "
    0
    " + "
    0%
    " "
    0
    " "
    0
    " "
    " @@ -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 0) {\n" "dlow = data.tempmean;\n" diff --git a/OVMS.V3/components/ovms_webserver/src/web_framework.cpp b/OVMS.V3/components/ovms_webserver/src/web_framework.cpp index 913510c..0449318 100644 --- a/OVMS.V3/components/ovms_webserver/src/web_framework.cpp +++ b/OVMS.V3/components/ovms_webserver/src/web_framework.cpp @@ -49,22 +49,32 @@ * & → & */ std::string PageContext::encode_html(const char* text) { - std::string buf; - for (int i=0; i') - buf += ">"; - else if(text[i] == '&') - buf += "&"; - 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("""); + break; + case '\'': + buf.append("'"); + break; + case '<': + buf.append("<"); + break; + case '>': + buf.append(">"); + break; + case '&': + buf.append("&"); + 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') - buf += ">"; - else if(text[i] == '&') - buf += "&"; - else - buf += text[i]; + for (int i=0; i': + buf.append(">"); + break; + case '&': + buf.append("&"); + 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%s:" "
    " "
    " + " data-value=\"%g\" data-min=\"%g\" data-max=\"%g\" data-step=\"%g\" data-checked=\"%s\">" "
    " " " " 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( "
    " @@ -733,8 +754,16 @@ void OvmsWebServer::OutputReconnect(PageEntry_t& p, PageContext_t& c, const char "$(\"#dots\").append(\"•\");" "}" "}, 1000);" - "" , info ? info : "Reconnecting…"); + + if (cmd) { + c.printf( + "loadcmd(\"%s\");" + , __attr(cmd)); + } + + c.print( + ""); } diff --git a/OVMS.V3/components/pushover/src/pushover.cpp b/OVMS.V3/components/pushover/src/pushover.cpp index 11975cf..32c296b 100644 --- a/OVMS.V3/components/pushover/src/pushover.cpp +++ b/OVMS.V3/components/pushover/src/pushover.cpp @@ -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); } diff --git a/OVMS.V3/components/retools_pidscan/src/retools_pid.cpp b/OVMS.V3/components/retools_pidscan/src/retools_pid.cpp index d732129..74fba47 100644 --- a/OVMS.V3/components/retools_pidscan/src/retools_pid.cpp +++ b/OVMS.V3/components/retools_pidscan/src/retools_pid.cpp @@ -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); } } diff --git a/OVMS.V3/components/sdcard/src/sdcard.cpp b/OVMS.V3/components/sdcard/src/sdcard.cpp index 849a2cc..345bed3 100644 --- a/OVMS.V3/components/sdcard/src/sdcard.cpp +++ b/OVMS.V3/components/sdcard/src/sdcard.cpp @@ -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) // { diff --git a/OVMS.V3/components/simcom/src/simcom_5360.cpp b/OVMS.V3/components/simcom/src/simcom_5360.cpp index 56ad121..05dcccd 100644 --- a/OVMS.V3/components/simcom/src/simcom_5360.cpp +++ b/OVMS.V3/components/simcom/src/simcom_5360.cpp @@ -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; diff --git a/OVMS.V3/components/simcom/src/simcom_7000.cpp b/OVMS.V3/components/simcom/src/simcom_7000.cpp index c809799..2718a15 100644 --- a/OVMS.V3/components/simcom/src/simcom_7000.cpp +++ b/OVMS.V3/components/simcom/src/simcom_7000.cpp @@ -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; diff --git a/OVMS.V3/components/simcom/src/simcom_7600.cpp b/OVMS.V3/components/simcom/src/simcom_7600.cpp index ae72434..5ef00e6 100644 --- a/OVMS.V3/components/simcom/src/simcom_7600.cpp +++ b/OVMS.V3/components/simcom/src/simcom_7600.cpp @@ -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; diff --git a/OVMS.V3/components/simcom/src/simcom_7600.h b/OVMS.V3/components/simcom/src/simcom_7600.h index b1c256e..a555f37 100644 --- a/OVMS.V3/components/simcom/src/simcom_7600.h +++ b/OVMS.V3/components/simcom/src/simcom_7600.h @@ -50,6 +50,7 @@ class simcom7600 : public modemdriver int GetMuxChannelPOLL() { return 3; } int GetMuxChannelCMD() { return 4; } void StartupNMEA(); + void ShutdownNMEA(); void StatusPoller(); void PowerCycle(); diff --git a/OVMS.V3/components/strverscmp/component.mk b/OVMS.V3/components/strverscmp/component.mk index 3ad26e5..c7723ec 100644 --- a/OVMS.V3/components/strverscmp/component.mk +++ b/OVMS.V3/components/strverscmp/component.mk @@ -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 diff --git a/OVMS.V3/components/vehicle/vehicle.cpp b/OVMS.V3/components/vehicle/vehicle.cpp index c220044..5f75eb2 100644 --- a/OVMS.V3/components/vehicle/vehicle.cpp +++ b/OVMS.V3/components/vehicle/vehicle.cpp @@ -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 &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 diff --git a/OVMS.V3/components/vehicle/vehicle_bms.cpp b/OVMS.V3/components/vehicle/vehicle_bms.cpp index 18c4366..c58279e 100644 --- a/OVMS.V3/components/vehicle/vehicle_bms.cpp +++ b/OVMS.V3/components/vehicle/vehicle_bms.cpp @@ -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; iGetElemValue(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 {