diff --git a/firmware_v5/telelogger/README.md b/firmware_v5/telelogger/README.md index 2f45c2e9..9a4685bc 100644 --- a/firmware_v5/telelogger/README.md +++ b/firmware_v5/telelogger/README.md @@ -16,14 +16,15 @@ Data Transmissions ------------------ Data transmission over UDP and HTTP protocols are implemented with following hardware. - * WiFi (ESP32 built-in) * WiFi Mesh (ESP-MDF for ESP32) * 3G WCDMA (SIM5360) * 4G LTE CAT-4 (SIM7600) * 4G LTE CAT-M1 (SIM7070) -UDP mode implements a client for [Freematics Hub](https://freematics.com/hub/). HTTP mode implements a client for [Traccar](https://www.traccar.org) under [OsmAnd](https://www.traccar.org/osmand/) protocol. +There two ways of sending data: +1. UDP mode implements a `freematics` protocol client for [Freematics Hub](https://freematics.com/hub/) or [Traccar](https://www.traccar.org) (sends more data, [protocol's API](https://freematics.com/pages/hub/api/), uses 5170 port). +2. HTTP/HTTPS mode implements a `osmand` protocol client for Traccar (sends only location data, [protocol's API](https://www.traccar.org/osmand/), uses 5055 port) Data Storage ------------ diff --git a/firmware_v5/telelogger/config.h b/firmware_v5/telelogger/config.h index 103b055a..7df1f64f 100644 --- a/firmware_v5/telelogger/config.h +++ b/firmware_v5/telelogger/config.h @@ -5,7 +5,7 @@ * Circular Buffer Configuration **************************************/ #define BUFFER_SLOTS 32 /* max number of buffer */ -#define BUFFER_LENGTH 128 /* bytes per slot */ +#define BUFFER_LENGTH 256 /* bytes per slot */ #define SERIALIZE_BUFFER_SIZE 1024 /* bytes */ /************************************** @@ -132,7 +132,9 @@ // motion threshold for waking up #define MOTION_THRESHOLD 0.4f /* moving vehicle motion threshold in G */ // engine jumpstart voltage -#define JUMPSTART_VOLTAGE 14 /* V */ +#define JUMPSTART_VOLTAGE 13.5f /* V */ +#define LOW_BATTERY_VOLTAGE 11.6f /* V */ +#define USB_VOLTAGE 5.0f /* V, don't consider voltages below this, assume we are debugging on USB */ /************************************** * Additional features @@ -142,6 +144,8 @@ #define PIN_SENSOR1 34 #define PIN_SENSOR2 26 +#define BEEP_TABLE {2000} /* Hz, each value is a 100ms cycle (use "0" to add gaps) */ + #define COOLING_DOWN_TEMP 65 /* celsius degrees */ #define ENABLE_BLE 0 diff --git a/firmware_v5/telelogger/dashboard/dashboard.js b/firmware_v5/telelogger/dashboard/dashboard.js old mode 100644 new mode 100755 index c1660bcd..6a1c360b --- a/firmware_v5/telelogger/dashboard/dashboard.js +++ b/firmware_v5/telelogger/dashboard/dashboard.js @@ -79,8 +79,14 @@ function processInput(data) if (ret = checkData(con, "HTTPD:")) { document.getElementById("wifi").innerHTML = ret.indexOf("NO") >= 0 ? imgCross : imgTick; } + if (ret = checkData(con, "Joining")) { + document.getElementById("wifi").innerText = ret; + } if (ret = checkData(con, "WiFi IP:")) { - document.getElementById("wifi").innerHTML = imgTick + " IP:" + ret; + document.getElementById("wifi").innerHTML += " " + imgTick + " IP:" + ret; + } + if (ret = checkData(con, "No WiFi")) { + document.getElementById("wifi").innerHTML = imgCross; } if (ret = checkData(con, "IMEI:")) { document.getElementById("sim_card").innerHTML = imgTick; diff --git a/firmware_v5/telelogger/teleclient.cpp b/firmware_v5/telelogger/teleclient.cpp index cc7084f9..8a3513a6 100644 --- a/firmware_v5/telelogger/teleclient.cpp +++ b/firmware_v5/telelogger/teleclient.cpp @@ -160,11 +160,14 @@ bool TeleClientUDP::notify(byte event, const char* payload) netbuf.init(128); netbuf.header(devid); netbuf.dispatch(buf, sprintf(buf, "EV=%X", (unsigned int)event)); + // ticker time in milliseconds netbuf.dispatch(buf, sprintf(buf, "TS=%lu", millis())); + // local device time in seconds + struct timeval timeval1; + gettimeofday(&timeval1, NULL); + uint32_t timestampSec = timeval1.tv_sec; + netbuf.dispatch(buf, sprintf(buf, "TM=%lu", timestampSec)); netbuf.dispatch(buf, sprintf(buf, "ID=%s", devid)); - if (rssi) { - netbuf.dispatch(buf, sprintf(buf, "SSI=%d", (int)rssi)); - } if (vin[0]) { netbuf.dispatch(buf, sprintf(buf, "VIN=%s", vin)); } @@ -209,10 +212,18 @@ bool TeleClientUDP::notify(byte event, const char* payload) if (event == EVENT_LOGIN) { // extract info from server response char *p = strstr(data, "TM="); + unsigned long t_seconds = 0; + unsigned long t_microseconds = 0; + if (p) { + t_seconds = atol(p + 3); + } + p = strstr(data, "TN="); if (p) { + t_microseconds = atol(p + 3); + } + if (t_seconds > 0) { // set local time from server - unsigned long tm = atol(p + 3); - struct timeval tv = { .tv_sec = (time_t)tm, .tv_usec = 0 }; + struct timeval tv = { .tv_sec = (time_t)t_seconds, .tv_usec = (time_t)t_microseconds }; settimeofday(&tv, NULL); } p = strstr(data, "SN="); @@ -283,6 +294,7 @@ bool TeleClientUDP::transmit(const char* packetBuffer, unsigned int packetSize) txBytes += packetSize; txCount++; success = true; + lastSyncTime = millis(); } return success; } diff --git a/firmware_v5/telelogger/teleclient.h b/firmware_v5/telelogger/teleclient.h index 13fb00af..29ee2ba8 100644 --- a/firmware_v5/telelogger/teleclient.h +++ b/firmware_v5/telelogger/teleclient.h @@ -7,6 +7,7 @@ #define EVENT_COMMAND 5 #define EVENT_ACK 6 #define EVENT_PING 7 +#define EVENT_LOW_BATTERY 8 #define BUFFER_STATE_EMPTY 0 #define BUFFER_STATE_FILLING 1 @@ -183,4 +184,4 @@ class TeleClientHTTP : public TeleClient #endif private: bool started = false; -}; \ No newline at end of file +}; diff --git a/firmware_v5/telelogger/telelogger.h b/firmware_v5/telelogger/telelogger.h index d2ce73a0..6a64e05e 100644 --- a/firmware_v5/telelogger/telelogger.h +++ b/firmware_v5/telelogger/telelogger.h @@ -102,7 +102,7 @@ class CStorageRAM: public CStorage { void tailer() { //if (m_cache[m_cacheBytes - 1] == ',') m_cacheBytes--; - m_cacheBytes += sprintf(m_cache + m_cacheBytes, "*%X", (unsigned int)checksum(m_cache, m_cacheBytes)); + m_cacheBytes += sprintf(m_cache + m_cacheBytes, "*%02X", (unsigned int)checksum(m_cache, m_cacheBytes)); } void untailer() { diff --git a/firmware_v5/telelogger/telelogger.ino b/firmware_v5/telelogger/telelogger.ino index d1109c67..722e1847 100644 --- a/firmware_v5/telelogger/telelogger.ino +++ b/firmware_v5/telelogger/telelogger.ino @@ -49,11 +49,13 @@ typedef struct { uint32_t ts; } PID_POLLING_INFO; +#define MAX_POLLING_TIER 3 PID_POLLING_INFO obdData[]= { {PID_SPEED, 1}, {PID_RPM, 1}, {PID_THROTTLE, 1}, {PID_ENGINE_LOAD, 1}, + {PID_FUEL_LEVEL, 1}, {PID_FUEL_PRESSURE, 2}, {PID_TIMING_ADVANCE, 2}, {PID_COOLANT_TEMP, 3}, @@ -74,6 +76,7 @@ float deviceTemp = 0; int16_t rssi = 0; char vin[18] = {0}; uint16_t dtc[6] = {0}; +// value 1440 means, that voltage is 14.4V int16_t batteryVoltage = 0; GPS_DATA* gd = 0; @@ -82,6 +85,7 @@ char isoTime[32] = {0}; // stats data uint32_t lastMotionTime = 0; +uint32_t lastMEMSMotionTime = 0; uint32_t timeoutsOBD = 0; uint32_t timeoutsNet = 0; uint32_t lastStatsTime = 0; @@ -181,6 +185,20 @@ void processExtInputs(CBuffer* buffer) } #endif +CStorageRAM latestLocationStore; + +void prepareData(CStorageRAM* store, CBuffer* buffer) { + // here we use CStorageRAM to serialize data correctly + store->purge(); +#if SERVER_PROTOCOL == PROTOCOL_UDP + store->header(devid); +#endif + + store->timestamp(buffer->timestamp); + buffer->serialize(*store); + store->tailer(); +} + /******************************************************************************* HTTP API *******************************************************************************/ @@ -232,39 +250,60 @@ int handlerControl(UrlHandlerParam* param) #if ENABLE_OBD void processOBD(CBuffer* buffer) { - static int idx[2] = {0, 0}; - int tier = 1; - for (byte i = 0; i < sizeof(obdData) / sizeof(obdData[0]); i++) { - if (obdData[i].tier > tier) { - // reset previous tier index - idx[tier - 2] = 0; - // keep new tier number - tier = obdData[i].tier; - // move up current tier index - i += idx[tier - 2]++; - // check if into next tier - if (obdData[i].tier != tier) { - idx[tier - 2]= 0; - i--; - continue; - } + static int tierCache[MAX_POLLING_TIER-1] = {}; + + uint8_t tier = 1; + uint8_t obdCount = sizeof(obdData) / sizeof(obdData[0]); + + for (uint8_t idx = 0; idx < obdCount; idx++) { + if (obdData[idx].tier > tier) { + // reset previous tier index + if (tier > 1) { + tierCache[tier - 2] = 0; + } + + // keep new tier number + tier = obdData[idx].tier; + + // move up current tier index + idx += tierCache[tier - 2]++; + + // check if into next tier + if (idx == obdCount - 1) { + tierCache[tier - 2] = 0; + } + else if (obdData[idx].tier != tier) { + tierCache[tier - 2] = 0; + idx--; + continue; + } + } + + uint8_t pid = obdData[idx].pid; + if (!obd.isValidPID(pid)) { + continue; } - byte pid = obdData[i].pid; - if (!obd.isValidPID(pid)) continue; + int value; if (obd.readPID(pid, value)) { - obdData[i].ts = millis(); - obdData[i].value = value; + obdData[idx].ts = millis(); + obdData[idx].value = value; buffer->add((uint16_t)pid | 0x100, value); } else { timeoutsOBD++; printTimeoutStats(); break; } - if (tier > 1) break; + + if (tier > 1) { + break; + } } + int kph = obdData[0].value; - if (kph >= 2) lastMotionTime = millis(); + if (kph >= 2) { + lastMotionTime = millis(); + } } #endif @@ -397,10 +436,8 @@ void processMEMS(CBuffer* buffer) value[2] = ori.roll; buffer->add(PID_ORIENTATION, value); #endif - if (temp != deviceTemp) { - deviceTemp = temp; - buffer->add(PID_DEVICE_TEMP, (int)temp); - } + deviceTemp = temp; + buffer->add(PID_MEMS_TEMP, (int) (temp * 10)); #if 0 // calculate motion float motion = 0; @@ -470,9 +507,12 @@ void printTime() *******************************************************************************/ void initialize() { - // turn on buzzer at 2000Hz frequency - sys.buzzer(2000); - delay(100); + // startup buzzer + const uint16_t beeps[] = BEEP_TABLE; + for (byte i = 0; i < sizeof(beeps) / sizeof(beeps[0]); i++) { + sys.buzzer(beeps[i]); + delay(100); + } // turn off buzzer sys.buzzer(0); @@ -536,7 +576,7 @@ void initialize() } #endif - // re-try OBD if connection not established + // get VIN and DTCs from ECU #if ENABLE_OBD if (state.check(STATE_OBD_READY)) { char buf[128]; @@ -734,9 +774,12 @@ bool waitMotion(long timeout) } // check movement if (motion >= MOTION_THRESHOLD * MOTION_THRESHOLD) { - //lastMotionTime = millis(); - Serial.println(motion); - return true; + updateBatteryVoltage(); + if (millis() - lastMEMSMotionTime < 500 && (batteryVoltage > JUMPSTART_VOLTAGE * 100 || batteryVoltage < USB_VOLTAGE * 100)) return true; + Serial.print("lastMotionTime: "); + Serial.println(millis() - lastMEMSMotionTime); + lastMEMSMotionTime = millis(); + //delay(50); } } while ((long)(millis() - t) < timeout || timeout == -1); return false; @@ -780,13 +823,14 @@ void process() #endif #if ENABLE_OBD - if (sys.devType > 12) { - batteryVoltage = (float)(analogRead(A0) * 12 * 370) / 4095; - } else if (state.check(STATE_OBD_READY)) { - batteryVoltage = obd.getVoltage() * 100; - } + updateBatteryVoltage(); if (batteryVoltage) { buffer->add(PID_BATTERY_VOLTAGE, (int)batteryVoltage); + if (batteryVoltage > JUMPSTART_VOLTAGE * 100) { + buffer->add(PID_IGNITION, (int) 1); + } else { + buffer->add(PID_IGNITION, (int) 0); + } } #endif @@ -794,17 +838,39 @@ void process() processExtInputs(buffer); #endif +#if NET_DEVICE >= SIM800 + rssi = teleClient.net.getSignal(); + if (rssi) { + buffer->add(PID_CELL_RSSI, rssi); + } +#endif + +#if NET_DEVICE == NET_SIM7600 + if (teleClient.net.cellTower->mcc != 0) { + buffer->add(PID_CELL_MCC, teleClient.net.cellTower->mcc); + buffer->add(PID_CELL_MNC, teleClient.net.cellTower->mnc); + buffer->add(PID_CELL_LAC, (uint32_t) teleClient.net.cellTower->lac); + buffer->add(PID_CELL_CID, (uint32_t) teleClient.net.cellTower->cellid); + } +#endif + #if ENABLE_MEMS processMEMS(buffer); #endif - processGPS(buffer); + bool receivedGPS = processGPS(buffer); + float cpuTemp = readChipTemperature(); + buffer->add(PID_CPU_TEMP, (int) (cpuTemp * 10)); if (!state.check(STATE_MEMS_READY)) { - deviceTemp = readChipTemperature(); - buffer->add(PID_DEVICE_TEMP, deviceTemp); + deviceTemp = cpuTemp; } + // format device time + struct timeval timeval1; + gettimeofday(&timeval1, NULL); + buffer->add(PID_DEVICE_TIME_SEC, (uint32_t) timeval1.tv_sec); + buffer->add(PID_DEVICE_TIME_MCS, (uint32_t) timeval1.tv_usec); buffer->timestamp = millis(); buffer->state = BUFFER_STATE_FILLED; @@ -814,9 +880,10 @@ void process() lastStatsTime = startTime; } + prepareData(&latestLocationStore, buffer); #if STORAGE != STORAGE_NONE if (state.check(STATE_STORAGE_READY)) { - buffer->serialize(logger); + logger.dispatch(latestLocationStore.buffer(), latestLocationStore.length()); uint16_t sizeKB = (uint16_t)(logger.size() >> 10); if (sizeKB != lastSizeKB) { logger.flush(); @@ -842,7 +909,7 @@ void process() } } if (stationary) { - // stationery timeout + // stationary timeout Serial.print("Stationary for "); Serial.print(motionless); Serial.println(" secs"); @@ -872,7 +939,7 @@ bool initNetwork() String ip = teleClient.net.getIP(); if (ip.length()) { state.set(STATE_NET_CONNECTED); - Serial.print("IP:"); + Serial.print("WiFi IP:"); Serial.println(ip); #if ENABLE_OLED oled.println(ip); @@ -990,11 +1057,11 @@ void telemetry(void* inst) #if GNSS == GNSS_INTERNAL || GNSS == GNSS_EXTERNAL if (state.check(STATE_GPS_READY)) { - Serial.println("GNSS OFF"); #if GNSS_ALWAYS_ON sys.gpsEnd(false); #else sys.gpsEnd(true); + Serial.println("GNSS OFF"); #endif state.clear(STATE_GPS_READY); } @@ -1027,7 +1094,16 @@ void telemetry(void* inst) if (initNetwork()) { Serial.print("Ping..."); bool success = teleClient.ping(); + bool successData = teleClient.transmit(latestLocationStore.buffer(), latestLocationStore.length()); Serial.println(success ? "OK" : "NO"); + + // check for low battery when pinging + updateBatteryVoltage(); + if (batteryVoltage < LOW_BATTERY_VOLTAGE * 100 && batteryVoltage > USB_VOLTAGE * 100) { + if (teleClient.notify(EVENT_LOW_BATTERY, "")) { + Serial.println("EVENT_LOW_BATTERY sent"); + } + } } teleClient.shutdown(); state.clear(STATE_NET_READY | STATE_NET_CONNECTED); @@ -1056,17 +1132,12 @@ void telemetry(void* inst) } buffer->state = BUFFER_STATE_LOCKED; -#if SERVER_PROTOCOL == PROTOCOL_UDP - store.header(devid); -#endif - store.timestamp(buffer->timestamp); - buffer->serialize(store); + prepareData(&store, buffer); buffer->purge(); - store.tailer(); - //Serial.println(store.buffer()); // start transmission if (ledMode == 0) digitalWrite(PIN_LED, HIGH); + if (teleClient.transmit(store.buffer(), store.length())) { // successfully sent connErrors = 0; @@ -1078,11 +1149,9 @@ void telemetry(void* inst) } if (ledMode == 0) digitalWrite(PIN_LED, LOW); - store.purge(); - teleClient.inbound(); if (syncInterval > 10000 && millis() - teleClient.lastSyncTime > syncInterval) { - Serial.println("Instable connection"); + Serial.println("Unstable connection"); connErrors++; timeoutsNet++; } @@ -1248,6 +1317,14 @@ void showSysInfo() #endif } +void updateBatteryVoltage() { + if (state.check(STATE_OBD_READY)) { + batteryVoltage = obd.getVoltage() * 100; + } else if (sys.devType > 12) { + batteryVoltage = (float)(analogRead(A0) * 12 * 370) / 4095; + } +} + #if CONFIG_MODE_TIMEOUT void configMode() { @@ -1355,6 +1432,7 @@ void setup() Serial.println("HTTPD:NO"); } #endif + latestLocationStore.init(SERIALIZE_BUFFER_SIZE); #if ENABLE_BLE // init BLE diff --git a/libraries/FreematicsPlus/FreematicsBase.h b/libraries/FreematicsPlus/FreematicsBase.h index 1cd46061..e239c9d4 100644 --- a/libraries/FreematicsPlus/FreematicsBase.h +++ b/libraries/FreematicsPlus/FreematicsBase.h @@ -10,6 +10,11 @@ #include +// time values +#define PID_TICKER 0x0 +#define PID_DEVICE_TIME_SEC 0x1 +#define PID_DEVICE_TIME_MCS 0x2 + // non-OBD/custom PIDs (no mode number) #define PID_GPS_LATITUDE 0xA #define PID_GPS_LONGITUDE 0xB @@ -23,17 +28,25 @@ #define PID_ACC 0x20 #define PID_GYRO 0x21 #define PID_COMPASS 0x22 +#define PID_MEMS_TEMP 0x23 #define PID_BATTERY_VOLTAGE 0x24 #define PID_ORIENTATION 0x25 +// cell tower info +#define PID_CELL_RSSI 0x40 // dBm +#define PID_CELL_MCC 0x41 +#define PID_CELL_MNC 0x42 +#define PID_CELL_LAC 0x43 +#define PID_CELL_CID 0x44 + // custom PIDs for calculated data #define PID_TRIP_DISTANCE 0x30 #define PID_DATA_SIZE 0x80 -#define PID_CSQ 0x81 -#define PID_DEVICE_TEMP 0x82 +#define PID_CPU_TEMP 0x82 #define PID_DEVICE_HALL 0x83 #define PID_EXT_SENSOR1 0x90 #define PID_EXT_SENSOR2 0x91 +#define PID_IGNITION 0x92 typedef struct { float pitch; diff --git a/libraries/FreematicsPlus/FreematicsNetwork.cpp b/libraries/FreematicsPlus/FreematicsNetwork.cpp index f0b22fa4..df836797 100644 --- a/libraries/FreematicsPlus/FreematicsNetwork.cpp +++ b/libraries/FreematicsPlus/FreematicsNetwork.cpp @@ -249,6 +249,7 @@ String ClientSIM800::getIP() return ""; } +// converts CSQ into dBm int ClientSIM800::getSignal() { if (sendCommand("AT+CSQ\r", 500)) { @@ -918,10 +919,26 @@ bool ClientSIM7600::setup(const char* apn, unsigned int timeout) if (!strstr(m_buffer, "NO SERVICE")) break; success = false; } + cellTower->mcc = 0; delay(100); } while (millis() - t < timeout); if (!success) break; + // save cellular base station location + // https://simcom.ee/documents/SIM7600E/SIM7500_SIM7600%20Series_AT%20Command%20Manual%20_V1.10.pdf + // +CPSI: WCDMA,Online,276-03,0x0141,247079,WCDMA IMT 2000,275,10739,0,13.5,99,9,15,500 + // +CPSI: ,,-,,... + char *p_start = strstr(m_buffer, "+CPSI:") + 7; + cellTower->radiotype = strtok(p_start, ","); + cellTower->status = strtok(NULL, ","); + char *mccmnc = strtok(NULL, ","); + char* p1 = strchr(mccmnc, '-'); + if (p1) *p1 = 0; + cellTower->mcc = atoi(mccmnc); + cellTower->mnc = atoi(p1+1); + cellTower->lac = strtol(strtok(NULL, ","), &p1, 16); + cellTower->cellid = atol(strtok(NULL, ",")); + success = false; do { delay(100); diff --git a/libraries/FreematicsPlus/FreematicsNetwork.h b/libraries/FreematicsPlus/FreematicsNetwork.h index c8a0abf9..006c2c95 100644 --- a/libraries/FreematicsPlus/FreematicsNetwork.h +++ b/libraries/FreematicsPlus/FreematicsNetwork.h @@ -46,6 +46,15 @@ typedef struct { uint8_t second; } NET_LOCATION; +typedef struct { + char* radiotype; + char* status; + int mcc; + int mnc; + long lac; // 16 bit number + long cellid; // A valid CID ranges from 0 to 65535 (216 − 1) on GSM and CDMA networks and from 0 to 268,435,455 (228 − 1) on UMTS and LTE networks. +} CELL_TOWER; + class HTTPClient { public: @@ -204,6 +213,8 @@ class ClientSIM7600 : public ClientSIM5360 bool setup(const char* apn, unsigned int timeout = 30000); void end(); bool setGPS(bool on); + int readCellTowerData(int &mcc, int &mnc, long &lac, long &cellid); + CELL_TOWER* cellTower = new CELL_TOWER; }; class UDPClientSIM7600 : public ClientSIM7600 diff --git a/libraries/FreematicsPlus/FreematicsPlus.cpp b/libraries/FreematicsPlus/FreematicsPlus.cpp index d4fd60e6..a4869cf4 100644 --- a/libraries/FreematicsPlus/FreematicsPlus.cpp +++ b/libraries/FreematicsPlus/FreematicsPlus.cpp @@ -594,7 +594,7 @@ bool FreematicsESP32::gpsBegin(int baudrate) success = true; break; } - } while (millis() - t < 1000); + } while (millis() - t < 2000); if (success) { gpsData = new GPS_DATA; memset(gpsData, 0, sizeof(GPS_DATA)); @@ -840,11 +840,16 @@ void FreematicsESP32::xbTogglePower() } void FreematicsESP32::buzzer(int freq) +{ + buzzer(freq, 255); +} + +void FreematicsESP32::buzzer(int freq, uint8_t volume) { #ifdef PIN_BUZZER if (freq) { - ledcWriteTone(0, 2000); - ledcWrite(0, 255); + ledcWriteTone(0, freq); + ledcWrite(0, volume); } else { ledcWrite(0, 0); } diff --git a/libraries/FreematicsPlus/FreematicsPlus.h b/libraries/FreematicsPlus/FreematicsPlus.h index fcfb3df9..f70d7ff2 100644 --- a/libraries/FreematicsPlus/FreematicsPlus.h +++ b/libraries/FreematicsPlus/FreematicsPlus.h @@ -165,6 +165,7 @@ class FreematicsESP32 : public CFreematics void xbTogglePower(); // control internal buzzer (if present) void buzzer(int freq); + void buzzer(int freq, uint8_t volume); // reset co-processor void resetLink(); // reactivate co-processor