634 lines
19 KiB
C++
634 lines
19 KiB
C++
/*
|
|
; 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<canbus*>(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<uint8_t> 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<OvmsReToolsPidScanner*>(self)->Task();
|
|
}
|
|
|
|
void OvmsReToolsPidScanner::Task()
|
|
{
|
|
CAN_frame_t frame;
|
|
while (1)
|
|
{
|
|
if (xQueueReceive(
|
|
m_rxqueue, &frame, static_cast<portTickType>(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<uint8_t>(m_currentPid >> 8),
|
|
static_cast<uint8_t>(m_currentPid & 0xff)
|
|
} };
|
|
}
|
|
else
|
|
{
|
|
sendFrame.data = { .u8 = {
|
|
(ISOTP_FT_SINGLE << 4) + 2, m_pollType,
|
|
static_cast<uint8_t>(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<uint8_t> 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<uint8_t>(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,
|
|
"<bus> <ecu> <start_pid> <end_pid> [-s<pid_step>] [-r<rxid>[-<rxid>]] [-t<poll_type>] [-x<timeout>]\n"
|
|
"Give all values except bus and timeout hexadecimal. Options can be positioned anywhere.\n"
|
|
"Default <rxid> is <ecu>+8, try 0-7ff if you don't know the responding ID.\n"
|
|
"Default <poll_type> is 22 (ReadDataByIdentifier, 16 bit PID).\n"
|
|
"Default <pid_step> is 1.\n"
|
|
"Default <timeout> 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);
|
|
}
|