/* ; Project: Open Vehicle Monitor System ; Date: 14th March 2017 ; ; Changes: ; 1.0 Initial release ; ; (C) 2011 Michael Stegen / Stegen Electronics ; (C) 2011-2017 Mark Webb-Johnson ; (C) 2011 Sonny Chen @ EPRO/DX ; ; Permission is hereby granted, free of charge, to any person obtaining a copy ; of this software and associated documentation files (the "Software"), to deal ; in the Software without restriction, including without limitation the rights ; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ; copies of the Software, and to permit persons to whom the Software is ; furnished to do so, subject to the following conditions: ; ; The above copyright notice and this permission notice shall be included in ; all copies or substantial portions of the Software. ; ; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ; THE SOFTWARE. */ #include "ovms_log.h" static const char *TAG = "notify"; #include #include #include #include "ovms.h" #include "ovms_notify.h" #include "ovms_command.h" #include "ovms_config.h" #include "ovms_events.h" #include "ovms_script.h" #include "vehicle.h" #include "buffered_shell.h" #include "string.h" #include "ovms_mutex.h" using namespace std; OvmsNotify MyNotify __attribute__ ((init_priority (1820))); // Tracing: // level 0 = no tracing (logging) // level 1 = trace standard (text & data) notifications // level 2 = also trace stream notifications #define DO_TRACE(type) (MyNotify.m_trace == 2 || (MyNotify.m_trace == 1 && strcmp((type), "stream") != 0)) //////////////////////////////////////////////////////////////////////// // Console commands... void notify_trace(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv) { if (strcmp(cmd->GetName(),"all")==0) MyNotify.m_trace = 2; else if (strcmp(cmd->GetName(),"on")==0) MyNotify.m_trace = 1; else MyNotify.m_trace = 0; writer->printf("Notification tracing is now %s\n",cmd->GetName()); } void notify_status(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv) { OvmsRecMutexLock lock(&MyNotify.m_mutex); writer->printf("Notification system has %d readers registered\n", MyNotify.CountReaders()); for (OvmsNotifyCallbackMap_t::iterator itc=MyNotify.m_readers.begin(); itc!=MyNotify.m_readers.end(); itc++) { OvmsNotifyCallbackEntry* mc = itc->second; writer->printf(" %s(%d): verbosity=%d\n", mc->m_caller, mc->m_reader, mc->m_verbosity); } if (MyNotify.m_types.size() > 0) { writer->puts("Notify types:"); for (OvmsNotifyTypeMap_t::iterator itm=MyNotify.m_types.begin(); itm!=MyNotify.m_types.end(); ++itm) { OvmsNotifyType* mt = itm->second; OvmsRecMutexLock lock(&mt->m_mutex); writer->printf(" %s: %d entries\n", mt->m_name, mt->m_entries.size()); for (NotifyEntryMap_t::iterator ite=mt->m_entries.begin(); ite!=mt->m_entries.end(); ++ite) { OvmsNotifyEntry* e = ite->second; writer->printf(" %d: [%d pending] %s\n", ite->first, e->CountPending(), e->GetValue().c_str()); } } } } void notify_raise(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv) { writer->printf("Raise %s notification for %s/%s as %s\n", cmd->GetName(), argv[0], argv[1], argv[2]); if (strcmp(cmd->GetName(),"text")==0) MyNotify.NotifyString(argv[0],argv[1],argv[2]); else if (strcmp(cmd->GetName(),"command")==0) MyNotify.NotifyCommand(argv[0],argv[1],argv[2]); else MyNotify.NotifyErrorCode(atol(argv[0]),atol(argv[1]),(strcmp(argv[2],"yes")==0)); } void notify_errorcode_list(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv) { for (OvmsNotifyErrorCodeMap_t::iterator it=MyNotify.m_errorcodes.begin(); it!=MyNotify.m_errorcodes.end(); ++it) { writer->printf("%u(0x%04.4x) %s raised %d, updated %d, sec(s) ago\n", it->first, it->second->lastdata, (it->second->active)?"active":"cleared", monotonictime-it->second->raised, monotonictime-it->second->updated); } } void notify_errorcode_clear(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv) { for (OvmsNotifyErrorCodeMap_t::iterator it=MyNotify.m_errorcodes.begin(); it!=MyNotify.m_errorcodes.end(); ++it) { delete it->second; } MyNotify.m_errorcodes.clear(); } #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE static duk_ret_t DukOvmsNotifyRaise(duk_context *ctx) { const char *type = duk_to_string(ctx,0); const char *subtype = duk_to_string(ctx,1); const char *message = duk_to_string(ctx,2); if (type && subtype && message) { uint32_t id = MyNotify.NotifyString(type, subtype, message); duk_push_uint(ctx, id); } else { duk_push_uint(ctx, 0); } return 1; /* one return value */ } #endif //#ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE //////////////////////////////////////////////////////////////////////// // OvmsNotifyEntry is the virtual object for notification entries. // These are added to a particular OvmsNotifyType (as a member of a list) // and readers are notified. Once a reader has processed an entry, the // MarkRead function is called. The framework can test IsAllRead() to // see if all readers have processed the entry and housekeep cleanup // appropriately. OvmsNotifyEntry::OvmsNotifyEntry(const char* subtype) { m_pendingreaders = 0; m_id = 0; m_created = esp_log_timestamp(); m_type = NULL; m_subtype = strdup(subtype); } OvmsNotifyEntry::~OvmsNotifyEntry() { if (m_subtype) free(m_subtype); } bool OvmsNotifyEntry::IsRead(size_t reader) { return !(m_pendingreaders & 1ul << reader); } int OvmsNotifyEntry::CountPending() { int cnt = 0; unsigned long pend = m_pendingreaders; for (int i=0; i>= 1; } return cnt; } bool OvmsNotifyEntry::IsAllRead() { return (m_pendingreaders == 0); } const extram::string OvmsNotifyEntry::GetValue() { return extram::string(""); } const char* OvmsNotifyEntry::GetSubType() { return m_subtype; } //////////////////////////////////////////////////////////////////////// // OvmsNotifyEntryString is the notification entry for a constant // string type. OvmsNotifyEntryString::OvmsNotifyEntryString(const char* subtype, const char* value) : OvmsNotifyEntry(subtype) { m_value = extram::string(value); } OvmsNotifyEntryString::~OvmsNotifyEntryString() { } const extram::string OvmsNotifyEntryString::GetValue() { return m_value; } //////////////////////////////////////////////////////////////////////// // OvmsNotifyEntryCommand is the notification entry for a command // callback type. OvmsNotifyEntryCommand::OvmsNotifyEntryCommand(const char* subtype, int verbosity, const char* cmd) : OvmsNotifyEntry(subtype) { m_cmd = new char[strlen(cmd)+1]; strcpy(m_cmd,cmd); BufferedShell* bs = new BufferedShell(false, verbosity); // command notifications can only be raised by the system or "notify raise" in enabled mode, // so we can assume this is a secure shell: bs->SetSecure(true); bs->ProcessChars(m_cmd, strlen(m_cmd)); bs->ProcessChar('\n'); bs->Dump(m_value); delete bs; } OvmsNotifyEntryCommand::~OvmsNotifyEntryCommand() { if (m_cmd) { delete [] m_cmd; m_cmd = NULL; } } const extram::string OvmsNotifyEntryCommand::GetValue() { return m_value; } //////////////////////////////////////////////////////////////////////// // OvmsNotifyType is the container for an ordered list of // OvmsNotifyEntry objects (being the notification data queued) OvmsNotifyType::OvmsNotifyType(const char* name) { m_name = name; m_nextid = 1; } OvmsNotifyType::~OvmsNotifyType() { } uint32_t OvmsNotifyType::QueueEntry(OvmsNotifyEntry* entry) { OvmsRecMutexLock lock(&m_mutex); uint32_t id = m_nextid++; entry->m_id = id; entry->m_type = this; m_entries[id] = entry; if (strcmp(m_name, "data") != 0 && strcmp(m_name, "stream") != 0) { std::string event("notify."); event.append(m_name); event.append("."); event.append(entry->m_subtype); MyEvents.SignalEvent(event, (void*)id); } // Dispatch the callbacks... MyNotify.NotifyReaders(this, entry); // Check if we can cleanup... Cleanup(entry); return id; } uint32_t OvmsNotifyType::AllocateNextID() { OvmsRecMutexLock lock(&m_mutex); return m_nextid++; } void OvmsNotifyType::ClearReader(size_t reader) { OvmsRecMutexLock lock(&m_mutex); if (m_entries.size() > 0) { NotifyEntryMap_t::iterator next; for (NotifyEntryMap_t::iterator ite=m_entries.begin(); ite!=m_entries.end(); ) { OvmsNotifyEntry* e = ite->second; ++ite; e->m_pendingreaders &= ~(1ul << reader); Cleanup(e, &ite); } } } OvmsNotifyEntry* OvmsNotifyType::FirstUnreadEntry(size_t reader, uint32_t floor) { OvmsRecMutexLock lock(&m_mutex); for (NotifyEntryMap_t::iterator ite=m_entries.begin(); ite!=m_entries.end(); ++ite) { OvmsNotifyEntry* e = ite->second; if ((!e->IsRead(reader))&&(e->m_id > floor)) return e; } return NULL; } OvmsNotifyEntry* OvmsNotifyType::FindEntry(uint32_t id) { OvmsRecMutexLock lock(&m_mutex); auto k = m_entries.find(id); if (k == m_entries.end()) return NULL; else return k->second; } void OvmsNotifyType::MarkRead(size_t reader, OvmsNotifyEntry* entry) { OvmsRecMutexLock lock(&m_mutex); entry->m_pendingreaders &= ~(1ul << reader); Cleanup(entry); } void OvmsNotifyType::Cleanup(OvmsNotifyEntry* entry, NotifyEntryMap_t::iterator* next /*=NULL*/) { if (entry->IsAllRead()) { // We can cleanup... auto k = m_entries.find(entry->m_id); if (k != m_entries.end()) { NotifyEntryMap_t::iterator it = m_entries.erase(k); if (next) *next = it; } if (DO_TRACE(m_name)) ESP_LOGD(TAG,"Cleanup type %s id %d",m_name,entry->m_id); delete entry; } } //////////////////////////////////////////////////////////////////////// // OvmsNotifyCallbackEntry contains the callback function for a // particular reader OvmsNotifyCallbackEntry::OvmsNotifyCallbackEntry(const char* caller, size_t reader, int verbosity, OvmsNotifyCallback_t callback, bool configfiltered/*=true*/, OvmsNotifyFilterCallback_t filtercallback/*=NULL*/) { m_caller = caller; m_reader = reader; m_verbosity = verbosity; m_callback = callback; m_configfiltered = configfiltered; m_filtercallback = filtercallback; } OvmsNotifyCallbackEntry::~OvmsNotifyCallbackEntry() { } bool OvmsNotifyCallbackEntry::Accepts(OvmsNotifyType* type, const char* subtype, size_t size) { // Check size if (size > m_verbosity) return false; // Check filter by config: if (m_configfiltered) { // Config syntax options: // a) explicit inclusion: e.g. 'ovmsv2,ovmsv3' (only enable these) // b) explicit exclusion: e.g. '*,-ovmsv2,-ovmsv3' (only disable these) // '-' to disable all, empty/'*' to enable all std::string filter = MyConfig.GetParamValue("notify", subtype); if (!filter.empty()) { if (filter[0] == '*') { if (filter.find(std::string("-")+m_caller) != string::npos) return false; } else if (filter.find(m_caller) == string::npos) { return false; } } } // Check filter by callback: if (m_filtercallback) { if (m_filtercallback(type, subtype) == false) return false; } return true; } //////////////////////////////////////////////////////////////////////// // OvmsNotifyCallbackEntry contains the callback function for a // OvmsNotify::OvmsNotify() { ESP_LOGI(TAG, "Initialising NOTIFICATIONS (1820)"); m_nextreader = 1; #ifdef CONFIG_OVMS_DEV_DEBUGNOTIFICATIONS m_trace = 1; #else m_trace = 0; #endif // #ifdef CONFIG_OVMS_DEV_DEBUGNOTIFICATIONS MyConfig.RegisterParam("notify", "Notification filters", true, true); // Register our commands OvmsCommand* cmd_notify = MyCommandApp.RegisterCommand("notify","NOTIFICATION framework", notify_status, "", 0, 0, false); cmd_notify->RegisterCommand("status","Show notification status",notify_status); OvmsCommand* cmd_notifyraise = cmd_notify->RegisterCommand("raise","NOTIFICATION raise framework"); cmd_notifyraise->RegisterCommand("text","Raise a textual notification",notify_raise,"", 3, 3); cmd_notifyraise->RegisterCommand("command","Raise a command callback notification",notify_raise,"", 3, 3); cmd_notifyraise->RegisterCommand("errorcode","Raise an error code notification",notify_raise,"", 3, 3); OvmsCommand* cmd_notifyerrorcode = cmd_notify->RegisterCommand("errorcode","NOTIFICATION error code framework"); cmd_notifyerrorcode->RegisterCommand("list","List error codes raised",notify_errorcode_list); cmd_notifyerrorcode->RegisterCommand("clear","Clear error code list",notify_errorcode_clear); OvmsCommand* cmd_notifytrace = cmd_notify->RegisterCommand("trace","NOTIFICATION trace framework"); cmd_notifytrace->RegisterCommand("on","Standard notification tracing (text, error & data)",notify_trace); cmd_notifytrace->RegisterCommand("all","Full notification tracing (including streams)",notify_trace); cmd_notifytrace->RegisterCommand("off","Turn notification tracing OFF",notify_trace); RegisterType("info"); // payload: human readable text message RegisterType("error"); // payload: ",," RegisterType("alert"); // payload: human readable text message RegisterType("data"); // payload: MP historical data record (tagged CSV, see MP documentation) RegisterType("stream"); // payload: subtype specific, use for high volume / short latency data streams #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE ESP_LOGI(TAG, "Expanding DUKTAPE javascript engine"); DuktapeObjectRegistration* dto = new DuktapeObjectRegistration("OvmsNotify"); dto->RegisterDuktapeFunction(DukOvmsNotifyRaise, 3, "Raise"); MyDuktape.RegisterDuktapeObject(dto); #endif // CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE } OvmsNotify::~OvmsNotify() { } size_t OvmsNotify::RegisterReader(const char* caller, int verbosity, OvmsNotifyCallback_t callback, bool configfiltered/*=false*/, OvmsNotifyFilterCallback_t filtercallback/*=NULL*/) { OvmsRecMutexLock lock(&m_mutex); size_t reader = m_nextreader++; m_readers[reader] = new OvmsNotifyCallbackEntry(caller, reader, verbosity, callback, configfiltered, filtercallback); return reader; } void OvmsNotify::RegisterReader(size_t reader, const char* caller, int verbosity, OvmsNotifyCallback_t callback, bool configfiltered/*=false*/, OvmsNotifyFilterCallback_t filtercallback/*=NULL*/) { OvmsRecMutexLock lock(&m_mutex); m_readers[reader] = new OvmsNotifyCallbackEntry(caller, reader, verbosity, callback, configfiltered, filtercallback); } void OvmsNotify::ClearReader(size_t reader) { OvmsRecMutexLock lock(&m_mutex); auto k = m_readers.find(reader); if (k != m_readers.end()) { for (OvmsNotifyTypeMap_t::iterator itt=m_types.begin(); itt!=m_types.end(); ++itt) { OvmsNotifyType* t = itt->second; t->ClearReader(k->second->m_reader); } OvmsNotifyCallbackEntry* ec = k->second; m_readers.erase(k); delete ec; } } size_t OvmsNotify::CountReaders() { OvmsRecMutexLock lock(&m_mutex); return m_readers.size(); } OvmsNotifyType* OvmsNotify::GetType(const char* type) { auto k = m_types.find(type); if (k == m_types.end()) return NULL; else return k->second; } void OvmsNotify::NotifyReaders(OvmsNotifyType* type, OvmsNotifyEntry* entry) { OvmsRecMutexLock lock(&m_mutex); for (OvmsNotifyCallbackMap_t::iterator itc=m_readers.begin(); itc!=m_readers.end(); ++itc) { OvmsNotifyCallbackEntry* mc = itc->second; if (mc->Accepts(type, entry->GetSubType(), entry->GetValueSize())) { // deliver notification: if (mc->m_callback(type,entry) == true) entry->m_pendingreaders &= ~(1ul << mc->m_reader); } else { // in case the acceptance filter changed since queueing: entry->m_pendingreaders &= ~(1ul << mc->m_reader); } } } bool OvmsNotify::HasReader(const char* type, const char* subtype, size_t size) { OvmsRecMutexLock lock(&m_mutex); OvmsNotifyType* mt = GetType(type); for (OvmsNotifyCallbackMap_t::iterator itc=m_readers.begin(); itc!=m_readers.end(); ++itc) { OvmsNotifyCallbackEntry* mc = itc->second; if (mc->Accepts(mt, subtype, size)) return true; } return false; } void OvmsNotify::RegisterType(const char* type) { OvmsNotifyType* mt = GetType(type); if (mt == NULL) { mt = new OvmsNotifyType(type); m_types[type] = mt; ESP_LOGI(TAG,"Registered notification type %s",type); } } uint32_t OvmsNotify::NotifyString(const char* type, const char* subtype, const char* value) { OvmsRecMutexLock lock(&m_mutex); OvmsNotifyType* mt = GetType(type); if (mt == NULL) { ESP_LOGW(TAG, "Notification raised for non-existent type %s: %s", type, value); return 0; } if (DO_TRACE(type)) ESP_LOGI(TAG, "Raise text %s/%s: %s", type, subtype, value); // determine all currently active readers accepting the message: std::bitset readers; size_t size = strlen(value); for (OvmsNotifyCallbackMap_t::iterator itc=m_readers.begin(); itc!=m_readers.end(); ++itc) { OvmsNotifyCallbackEntry* mc = itc->second; if (mc->Accepts(mt, subtype, size)) readers.set(mc->m_reader); } if (readers.count() == 0) { ESP_LOGD(TAG, "Abort: no readers for type '%s' subtype '%s' size %d", type, subtype, size); return 0; } // create message: OvmsNotifyEntry* msg = (OvmsNotifyEntry*) new OvmsNotifyEntryString(subtype, value); msg->m_pendingreaders = readers.to_ulong(); ESP_LOGD(TAG, "Created entry type '%s' subtype '%s' size %d has %d readers pending", type, subtype, size, readers.count()); return mt->QueueEntry(msg); } uint32_t OvmsNotify::NotifyCommand(const char* type, const char* subtype, const char* cmd) { OvmsRecMutexLock lock(&m_mutex); OvmsNotifyType* mt = GetType(type); if (mt == NULL) { ESP_LOGW(TAG, "Notification raised for non-existent type %s: %s", type, cmd); return 0; } if (DO_TRACE(type)) ESP_LOGI(TAG, "Raise command %s/%s: %s", type, subtype, cmd); // Strategy: // to minimize RAM usage and command calls we try to reuse higher verbosity messages // if their result length fits for lower verbosity readers as well. // get verbosity levels needed by readers accepting the message: std::map verbosity_msgs; std::bitset readers; for (auto itc=m_readers.begin(); itc!=m_readers.end(); itc++) { OvmsNotifyCallbackEntry* mc = itc->second; if (mc->Accepts(mt, subtype)) { verbosity_msgs[mc->m_verbosity] = NULL; readers.set(mc->m_reader); // cache acceptance } } if (verbosity_msgs.size() == 0) { if (DO_TRACE(type)) { // no readers, but tracing enabled, so log command result: const int verbosity = COMMAND_RESULT_NORMAL; OvmsNotifyEntryCommand *msg = new OvmsNotifyEntryCommand(subtype, verbosity, cmd); ESP_LOGI(TAG, "Raise cmdres[%d] %s/%s: %s", verbosity, type, subtype, msg->GetValue().c_str()); delete msg; } ESP_LOGD(TAG, "Abort: no readers for type '%s' subtype '%s'", type, subtype); return 0; } // fetch verbosity levels beginning at highest verbosity: OvmsNotifyEntryCommand *msg = NULL; size_t msglen = 0; for (auto ritm=verbosity_msgs.rbegin(); ritm!=verbosity_msgs.rend(); ritm++) { int verbosity = ritm->first; if (msg && msglen <= verbosity) { // reuse last verbosity level message: verbosity_msgs[verbosity] = msg; } else { msg = verbosity_msgs[verbosity]; if (!msg) { // create verbosity level message: msg = new OvmsNotifyEntryCommand(subtype, verbosity, cmd); msglen = msg->GetValueSize(); verbosity_msgs[verbosity] = msg; if (DO_TRACE(type)) ESP_LOGI(TAG, "Raise cmdres[%d] %s/%s: %s", verbosity, type, subtype, msg->GetValue().c_str()); } } } // add readers: for (auto itc=m_readers.begin(); itc!=m_readers.end(); itc++) { OvmsNotifyCallbackEntry* mc = itc->second; if (readers.test(mc->m_reader)) { msg = verbosity_msgs[mc->m_verbosity]; msg->m_pendingreaders |= (1ul << mc->m_reader); } } // queue all verbosity level messages beginning at lowest verbosity (fastest delivery): msg = NULL; uint32_t queue_id = 0; for (auto itm=verbosity_msgs.begin(); itm!=verbosity_msgs.end(); itm++) { if (itm->second == msg) continue; // already queued msg = itm->second; ESP_LOGD(TAG, "Created entry type '%s' subtype '%s' verbosity %d has %d readers pending", type, subtype, itm->first, msg->CountPending()); queue_id = mt->QueueEntry(msg); } return queue_id; } /** * NotifyStringf: printf style API */ uint32_t OvmsNotify::NotifyStringf(const char* type, const char* subtype, const char* fmt, ...) { char *buffer = NULL; uint32_t res = 0; va_list args; va_start(args, fmt); int len = vasprintf(&buffer, fmt, args); va_end(args); if (len >= 0) { res = NotifyString(type, subtype, buffer); free(buffer); } return res; } /** * NotifyCommandf: printf style API */ uint32_t OvmsNotify::NotifyCommandf(const char* type, const char* subtype, const char* fmt, ...) { char *buffer = NULL; uint32_t res = 0; va_list args; va_start(args, fmt); int len = vasprintf(&buffer, fmt, args); va_end(args); if (len >= 0) { res = NotifyCommand(type, subtype, buffer); free(buffer); } return res; } void OvmsNotify::NotifyErrorCode(uint32_t code, uint32_t data, bool raised, bool force) { // Raise a notification of an error code // error code (key field) // data associated with the error // true if error is being raised, else false // true if notification should be forced, otherwise auto-suppress dupes // Firstly, let's see if we have a notification record for it already auto k = m_errorcodes.find(code); OvmsNotifyErrorCodeEntry_t* entry; if (k == m_errorcodes.end()) { entry = new OvmsNotifyErrorCodeEntry_t; entry->raised = monotonictime; entry->updated = 0; entry->lastdata = 0; m_errorcodes[code] = entry; } else { entry = k->second; } entry->active = raised; if (raised) entry->lastdata = data; // Handle auto-suppression, if necessary if (!force && (entry->updated>0) && (monotonictime-entry->updated < NOTIFY_ERROR_AUTOSUPPRESS)) { // We need to auto-suppress entry->updated = monotonictime; return; } // OK to raise... entry->updated = monotonictime; if (raised) { NotifyStringf("error", "code", "%s,%u,%u", MyVehicleFactory.ActiveVehicleType(), code, data); } }