/* ; Project: Open Vehicle Monitor System ; Date: 14th September 2020 ; ; Changes: ; 1.0 Initial release ; ; (C) 2011 Michael Stegen / Stegen Electronics ; (C) 2011-2017 Mark Webb-Johnson ; (C) 2011 Sonny Chen @ EPRO/DX ; (C) 2020 Chris Staite ; ; 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 = "re-pid"; #include "retools_pid.h" #include "vehicle.h" namespace { bool ReadHexString(const char* value, unsigned long& output) { char* ptr = nullptr; errno = 0; output = strtoul(value, &ptr, 16); if (ptr == value || errno != 0 || *ptr) { return false; } return true; } bool ReadHexRange(const char* value, unsigned long& output1, unsigned long& output2) { char* ptr = nullptr; errno = 0; output1 = strtoul(value, &ptr, 16); if (ptr == value || errno != 0 || (*ptr && *ptr != '-')) { return false; } if (!*ptr) { output2 = output1; } else { value = ptr + 1; output2 = strtoul(value, &ptr, 16); if (ptr == value || errno != 0 || *ptr) { return false; } } return true; } canbus* GetCan(int bus) { char name[] = "canx"; name[3] = '0' + bus; auto can = reinterpret_cast(MyPcpApp.FindDeviceByName(name)); if (can != nullptr && (can->GetPowerMode() != On || can->m_mode != CAN_MODE_ACTIVE)) { can = nullptr; } return can; } OvmsReToolsPidScanner* s_scanner = nullptr; void scanStart(int, OvmsWriter* writer, OvmsCommand*, int argc, const char* const* argv) { if (s_scanner != nullptr) { if (s_scanner->Complete()) { delete s_scanner; s_scanner = nullptr; } else { writer->puts( "Error: Scan already in progress, stop it or wait for it to complete" ); return; } } unsigned long bus = 0, ecu = 0, rxid_low = 0, rxid_high = 0, start = 0, end = 0; int timeout = 3; unsigned long polltype = VEHICLE_POLL_TYPE_OBDIIEXTENDED; unsigned long step = 1; bool valid = true, have_rxid = false; int argpos = 0; for (int i = 0; i < argc; i++) { if (argv[i][0] == '-') { switch (argv[i][1]) { case 'r': if (!ReadHexRange(argv[i]+2, rxid_low, rxid_high) || rxid_low > 0x7ff || rxid_high > 0x7ff || rxid_high < rxid_low) { writer->printf("Error: Invalid RX ID range %s\n", argv[i]+2); valid = false; } else { have_rxid = true; } break; case 's': if (!ReadHexString(argv[i]+2, step) || step < 1 || step > 0xffff) { writer->printf("Error: Invalid step size %s\n", argv[i]+2); valid = false; } break; case 't': if (!ReadHexString(argv[i]+2, polltype) || polltype < 1 || polltype > 0xff) { writer->printf("Error: Invalid poll type %s\n", argv[i]+2); valid = false; } break; case 'x': timeout = atoi(argv[i]+2); if (timeout < 1 || timeout > 10) { writer->printf("Error: Invalid timeout %s\n", argv[i]+2); valid = false; } break; default: writer->printf("Error: Invalid argument %s\n", argv[i]); valid = false; break; } } else { switch (++argpos) { case 1: if (!ReadHexString(argv[i], bus) || bus < 1 || bus > 4) { writer->printf("Error: Invalid bus to scan %s\n", argv[i]); valid = false; } break; case 2: if (!ReadHexString(argv[i], ecu) || ecu <= 0 || ecu >= 0xfff) { writer->printf("Error: Invalid ECU Id to scan %s\n", argv[i]); valid = false; } break; case 3: if (!ReadHexString(argv[i], start) || start > 0xffff) { writer->printf("Error: Invalid Start PID to scan %s\n", argv[i]); valid = false; } break; case 4: if (!ReadHexString(argv[i], end) || end > 0xffff) { writer->printf("Error: Invalid End PID to scan %s\n", argv[i]); valid = false; } break; default: writer->printf("Error: Invalid argument %s\n", argv[i]); valid = false; break; } } } if (start > end) { writer->printf( "Error: Invalid Start PID %04x is after End PID %04x\n", start, end ); valid = false; } else if (step > 1) { end = start + ((end - start) / step) * step; } if (POLL_TYPE_HAS_8BIT_PID(polltype) && end > 0xff) { writer->printf("Error: Poll type %x PID range is 00..ff\n"); valid = false; } if (!valid) { return; } if (!have_rxid) { rxid_low = rxid_high = ecu + 8; } canbus* can = GetCan(bus); if (can == nullptr) { writer->puts("CAN not started in active mode, please start and try again"); valid = false; } 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", bus, ecu, rxid_low, rxid_high, polltype, start, end, step, timeout); } } void scanStatus(int, OvmsWriter* writer, OvmsCommand*, int, const char* const*) { if (s_scanner == nullptr) { writer->puts("No scan running"); } else if (s_scanner->Complete()) { writer->printf( "Scan complete (%03x %04x-%04x)\n", s_scanner->Ecu(), s_scanner->Start(), s_scanner->End() ); } else { writer->printf( "Scan running (%03x %04x-%04x): %04x\n", s_scanner->Ecu(), s_scanner->Start(), s_scanner->End(), s_scanner->Current() ); } if (s_scanner != nullptr) { s_scanner->Output(writer); } } void scanStop(int, OvmsWriter* writer, OvmsCommand*, int, const char* const*) { if (s_scanner == nullptr) { writer->puts("Error: No scan currently in progress"); return; } writer->printf( "Scan results (%03x %04x-%04x):\n", s_scanner->Ecu(), s_scanner->Start(), s_scanner->Complete() ? s_scanner->End() : s_scanner->Current() ); s_scanner->Output(writer); delete s_scanner; s_scanner = nullptr; writer->puts("Scan stopped"); } } // anon namespace OvmsReToolsPidScanner::OvmsReToolsPidScanner( canbus* bus, uint16_t ecu, uint16_t rxid_low, uint16_t rxid_high, uint8_t polltype, int start, int end, int step, uint8_t timeout) : m_frameCallback(std::bind( &OvmsReToolsPidScanner::FrameCallback, this, std::placeholders::_1, std::placeholders::_2 )), m_bus(bus), m_id(ecu), m_rxid_low(rxid_low), m_rxid_high(rxid_high), m_pollType(polltype), m_startPid(start), m_endPid(end), m_pidStep(step), m_currentPid(end+1), m_ticker(0u), m_timeout(timeout), m_lastFrame(0u), m_startTime(0u), m_lastResponseTime(0u), m_mfRemain(0u), m_task(nullptr), m_rxqueue(nullptr), m_found(), m_foundMutex() { m_rxqueue = xQueueCreate(20,sizeof(CAN_frame_t)); xTaskCreatePinnedToCore( &OvmsReToolsPidScanner::Task, "OVMS RE PID", 4096, this, 5, &m_task, CORE(1) ); MyCan.RegisterListener(m_rxqueue, true); m_currentPid = m_startPid - m_pidStep; MyEvents.RegisterEvent( TAG, "ticker.1", std::bind( &OvmsReToolsPidScanner::Ticker1, this, std::placeholders::_1, std::placeholders::_2 ) ); MyEvents.SignalEvent("retools.pidscan.start", NULL); time(&m_startTime); SendNextFrame(); } OvmsReToolsPidScanner::~OvmsReToolsPidScanner() { if (m_rxqueue) { MyEvents.DeregisterEvent(TAG); MyCan.DeregisterListener(m_rxqueue); vQueueDelete(m_rxqueue); vTaskDelete(m_task); MyEvents.SignalEvent("retools.pidscan.stop", NULL); } } void OvmsReToolsPidScanner::Output(OvmsWriter* writer) const { OvmsMutexLock lock(&m_foundMutex); struct tm tmu; char tb[64]; localtime_r(&m_startTime, &tmu); strftime(tb, sizeof(tb), "%Y-%m-%d %H:%M:%S %Z", &tmu); writer->printf("Scan started : %s\n", tb); if (m_found.size() == 0) { writer->puts("No valid responses received."); } else { localtime_r(&m_lastResponseTime, &tmu); strftime(tb, sizeof(tb), "%Y-%m-%d %H:%M:%S %Z", &tmu); writer->printf("Last response: %s\n", tb); for (auto& found : m_found) { uint16_t rxid, pid; std::vector data; std::tie(rxid, pid, data) = found; writer->printf("%03x[%03x]:%04x", m_id, rxid, pid); for (auto& byte : data) { writer->printf(" %02x", byte); } writer->printf("\n"); } } } void OvmsReToolsPidScanner::Task(void *self) { reinterpret_cast(self)->Task(); } void OvmsReToolsPidScanner::Task() { CAN_frame_t frame; while (1) { if (xQueueReceive( m_rxqueue, &frame, static_cast(portMAX_DELAY) ) == pdTRUE) { if (frame.origin == m_bus) { IncomingPollFrame(&frame); } } } } void OvmsReToolsPidScanner::Ticker1(std::string, void*) { ++m_ticker; if (m_currentPid <= m_endPid && m_ticker - m_lastFrame > m_timeout) { ESP_LOGE(TAG, "Frame response timeout for %x:%x", m_id, m_currentPid); SendNextFrame(); } } void OvmsReToolsPidScanner::FrameCallback(const CAN_frame_t* frame, bool success) { if (success == false) { ESP_LOGE(TAG, "Error sending the frame, terminating scan"); m_currentPid = m_endPid + 1; MyEvents.SignalEvent("retools.pidscan.done", NULL); } } void OvmsReToolsPidScanner::SendNextFrame() { if (m_currentPid == m_endPid) { m_currentPid = m_endPid + 1; ESP_LOGI(TAG, "Scan of %x complete", m_id); MyEvents.SignalEvent("retools.pidscan.done", NULL); return; } m_currentPid += m_pidStep; CAN_frame_t sendFrame = { m_bus, &m_frameCallback, { .B = { 8, 0, CAN_no_RTR, CAN_frame_std, 0 } }, m_id, 0 }; if (POLL_TYPE_HAS_16BIT_PID(m_pollType)) { sendFrame.data = { .u8 = { (ISOTP_FT_SINGLE << 4) + 3, m_pollType, static_cast(m_currentPid >> 8), static_cast(m_currentPid & 0xff) } }; } else { sendFrame.data = { .u8 = { (ISOTP_FT_SINGLE << 4) + 2, m_pollType, static_cast(m_currentPid & 0xff) } }; } m_mfRemain = 0u; if (m_bus->Write(&sendFrame) == ESP_FAIL) { ESP_LOGE(TAG, "Error sending test frame to PID %x:%x", m_id, m_currentPid); m_currentPid = m_endPid + 1; } else { ESP_LOGV(TAG, "Sending test frame to PID %x:%x", m_id, m_currentPid); m_lastFrame = m_ticker; } } void OvmsReToolsPidScanner::IncomingPollFrame(const CAN_frame_t* frame) { if (m_currentPid > m_endPid) { return; } uint8_t frameType = frame->data.u8[0] >> 4; uint16_t frameLength = frame->data.u8[0] & 0x0f; const uint8_t* data = &frame->data.u8[1]; uint16_t dataLength = frameLength; if (frame->MsgID < m_rxid_low || frame->MsgID > m_rxid_high) { // Frame not for us return; } if (frameType == ISOTP_FT_SINGLE) { // All good m_mfRemain = 0; } else if (frameType == ISOTP_FT_FIRST) { frameLength = (frameLength << 8) | data[0]; ++data; dataLength = (frameLength > 6 ? 6 : frameLength); m_mfRemain = frameLength - dataLength; } else if (frameType == ISOTP_FT_CONSECUTIVE) { dataLength = (m_mfRemain > 7 ? 7 : m_mfRemain); m_mfRemain -= dataLength; } else { // Don't support any other types ESP_LOGI(TAG, "Received unknown frame type %x", frameType); return; } if (frameType == ISOTP_FT_CONSECUTIVE) { { OvmsMutexLock lock(&m_foundMutex); std::copy(data, &data[dataLength], std::back_inserter(std::get<2>(m_found.back()))); } if (m_mfRemain == 0u) { SendNextFrame(); } else { m_lastFrame = m_ticker; } } else if (dataLength == 3 && data[0] == UDS_RESP_TYPE_NRC && data[1] == m_pollType) { if (data[2] == UDS_RESP_NRC_RCRRP) { // ResponsePending: ignore, keep waiting ESP_LOGD(TAG, "ResponsePending from %x[%x]:%x", m_id, frame->MsgID, m_currentPid); } else { // …other negative response code: ESP_LOGD(TAG, "Negative response from %x[%x]:%x code %02x", m_id, frame->MsgID, m_currentPid, data[2]); SendNextFrame(); } } else if (dataLength > 3 && data[0] == m_pollType + 0x40) { // Success uint16_t responsePid; const uint8_t* payload; uint16_t payloadLength; if (POLL_TYPE_HAS_16BIT_PID(m_pollType)) { responsePid = data[1] << 8 | data[2]; payload = &data[3]; payloadLength = dataLength - 3; } else { responsePid = data[1]; payload = &data[2]; payloadLength = dataLength - 2; } if (responsePid == m_currentPid) { ESP_LOGD( TAG, "Success response from %x[%x]:%x length %d (0x%02x 0x%02x 0x%02x 0x%02x%s)", m_id, frame->MsgID, m_currentPid, payloadLength, payload[0], payload[1], payload[2], payload[3], (frameType == 0 ? "" : " ...") ); time(&m_lastResponseTime); if (frameType == ISOTP_FT_FIRST) { CAN_frame_t flowControl = { m_bus, &m_frameCallback, { .B = { 8, 0, CAN_no_RTR, CAN_frame_std, 0 } }, m_id, { .u8 = { 0x30, 0, 25, 0, 0, 0, 0, 0 } } }; if (m_bus->Write(&flowControl) == ESP_FAIL) { ESP_LOGE( TAG, "Error sending flow control frame to PID %x:%x", m_id, m_currentPid ); m_currentPid = m_endPid + 1; } else { m_lastFrame = m_ticker; } std::vector response; response.reserve(frameLength); std::copy(payload, &payload[payloadLength], std::back_inserter(response)); OvmsMutexLock lock(&m_foundMutex); m_found.push_back(std::make_tuple(frame->MsgID, responsePid, std::move(response))); } else { OvmsMutexLock lock(&m_foundMutex); m_found.push_back(std::make_tuple(frame->MsgID, responsePid, std::vector(payload, &payload[payloadLength]) )); } if (m_mfRemain == 0u) { SendNextFrame(); } } } } class OvmsReToolsPidScannerInit { public: OvmsReToolsPidScannerInit(); } OvmsReToolsPidScannerInit __attribute__ ((init_priority (8801))); OvmsReToolsPidScannerInit::OvmsReToolsPidScannerInit() { OvmsCommand* cmd_reobdii = MyCommandApp.FindCommandFullName("re obdii"); if (cmd_reobdii == nullptr) { ESP_LOGE(TAG, "PID scan command depends on re command"); return; } OvmsCommand* cmd_scan = cmd_reobdii->RegisterCommand("scan", "ECU PID scanning tool"); cmd_scan->RegisterCommand( "start", "Scan PIDs on an ECU in a given range", &scanStart, " [-s] [-r[-]] [-t] [-x]\n" "Give all values except bus and timeout hexadecimal. Options can be positioned anywhere.\n" "Default is +8, try 0-7ff if you don't know the responding ID.\n" "Default is 22 (ReadDataByIdentifier, 16 bit PID).\n" "Default is 1.\n" "Default is 3 seconds.", 4, 8 ); cmd_scan->RegisterCommand("status", "The status of the PID scan", &scanStatus); cmd_scan->RegisterCommand("stop", "Stop the current scan", &scanStop); }