552 lines
19 KiB
C++
552 lines
19 KiB
C++
/*
|
|
; 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 = "vehicle";
|
|
|
|
#include <stdio.h>
|
|
#include <algorithm>
|
|
#include <ovms_command.h>
|
|
#include <ovms_script.h>
|
|
#include <ovms_metrics.h>
|
|
#include <ovms_notify.h>
|
|
#include <metrics_standard.h>
|
|
#ifdef CONFIG_OVMS_COMP_WEBSERVER
|
|
#include <ovms_webserver.h>
|
|
#endif // #ifdef CONFIG_OVMS_COMP_WEBSERVER
|
|
#include <ovms_peripherals.h>
|
|
#include <string_writer.h>
|
|
#include "vehicle.h"
|
|
|
|
|
|
/**
|
|
* PollerStateTicker: check for state changes (stub, override with vehicle implementation)
|
|
* This is called by VehicleTicker1() just before the next PollerSend().
|
|
* Implement your poller state transition logic in this method, so the changes
|
|
* will get applied immediately.
|
|
*/
|
|
void OvmsVehicle::PollerStateTicker()
|
|
{
|
|
}
|
|
|
|
|
|
/**
|
|
* IncomingPollReply: poll response handler (stub, override with vehicle implementation)
|
|
* This is called by PollerReceive() on each valid response frame for the current request.
|
|
* Be aware responses may consist of multiple frames, detectable e.g. by mlremain > 0.
|
|
* A typical pattern is to collect frames in a buffer until mlremain == 0.
|
|
*
|
|
* @param bus
|
|
* CAN bus the current poll is done on
|
|
* @param type
|
|
* OBD2 mode / UDS polling type, e.g. VEHICLE_POLL_TYPE_READDTC
|
|
* @param pid
|
|
* PID addressed (depending on the request type, may be none / 8 bit / 16 bit)
|
|
* @param data
|
|
* Payload
|
|
* @param length
|
|
* Payload size
|
|
* @param mlremain
|
|
* Remaining bytes expected to complete the response after this frame
|
|
*
|
|
* @member m_poll_moduleid_sent
|
|
* The CAN ID addressed by the current request (txmoduleid)
|
|
* @member m_poll_ml_frame
|
|
* Frame number of the response, 0 = first frame / new response
|
|
* @member m_poll_ml_offset
|
|
* Byte position of this frame's payload part in the response, 0 = first frame
|
|
* @member m_poll_plcur
|
|
* Pointer to the currently processed poll entry
|
|
*/
|
|
void OvmsVehicle::IncomingPollReply(canbus* bus, uint16_t type, uint16_t pid, uint8_t* data, uint8_t length, uint16_t mlremain)
|
|
{
|
|
}
|
|
|
|
|
|
/**
|
|
* IncomingPollError: poll response error handler (stub, override with vehicle implementation)
|
|
* This is called by PollerReceive() on reception of an OBD/UDS Negative Response Code (NRC),
|
|
* except if the code is requestCorrectlyReceived-ResponsePending (0x78), which is handled
|
|
* by the poller. See ISO 14229 Annex A.1 for the list of NRC codes.
|
|
*
|
|
* @param bus
|
|
* CAN bus the current poll is done on
|
|
* @param type
|
|
* OBD2 mode / UDS polling type, e.g. VEHICLE_POLL_TYPE_READDTC
|
|
* @param pid
|
|
* PID addressed (depending on the request type, may be none / 8 bit / 16 bit)
|
|
* @param code
|
|
* NRC detail code
|
|
*
|
|
* @member m_poll_moduleid_sent
|
|
* The CAN ID addressed by the current request (txmoduleid)
|
|
* @member m_poll_plcur
|
|
* Pointer to the currently processed poll entry
|
|
*/
|
|
void OvmsVehicle::IncomingPollError(canbus* bus, uint16_t type, uint16_t pid, uint16_t code)
|
|
{
|
|
}
|
|
|
|
|
|
/**
|
|
* IncomingPollTxCallback: poller TX callback (stub, override with vehicle implementation)
|
|
* This is called by PollerTxCallback() on TX success/failure for a poller request.
|
|
* You can use this to detect CAN bus issues, e.g. if the car switches off the OBD port.
|
|
*
|
|
* ATT: this is executed in the main CAN task context. Keep it simple.
|
|
* Complex processing here will affect overall CAN performance.
|
|
*
|
|
* @param bus
|
|
* CAN bus the current poll is done on
|
|
* @param txid
|
|
* The module TX ID of the current poll
|
|
* @param type
|
|
* OBD2 mode / UDS polling type, e.g. VEHICLE_POLL_TYPE_READDTC
|
|
* @param pid
|
|
* PID addressed (depending on the request type, may be none / 8 bit / 16 bit)
|
|
* @param success
|
|
* Frame transmission success
|
|
*/
|
|
void OvmsVehicle::IncomingPollTxCallback(canbus* bus, uint32_t txid, uint16_t type, uint16_t pid, bool success)
|
|
{
|
|
}
|
|
|
|
|
|
/**
|
|
* PollSetPidList: set the default bus and the polling list to process
|
|
* Call this to install a new polling list or restart the list.
|
|
* This won't change the polling state; you can change the list while keeping the state.
|
|
* The list is changed without waiting for pending responses to finish (except PollSingleRequests).
|
|
*
|
|
* @param bus
|
|
* CAN bus to use as the default bus (for all poll entries with bus=0) or NULL to stop polling
|
|
* @param plist
|
|
* The polling list to use or NULL to stop polling
|
|
*/
|
|
void OvmsVehicle::PollSetPidList(canbus* bus, const poll_pid_t* plist)
|
|
{
|
|
OvmsRecMutexLock slock(&m_poll_single_mutex);
|
|
OvmsRecMutexLock lock(&m_poll_mutex);
|
|
m_poll_bus = bus;
|
|
m_poll_bus_default = bus;
|
|
m_poll_plist = plist;
|
|
m_poll_ticker = 0;
|
|
m_poll_sequence_cnt = 0;
|
|
m_poll_wait = 0;
|
|
m_poll_plcur = NULL;
|
|
m_poll_entry = {};
|
|
m_poll_txmsgid = 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* PollSetState: set the polling state
|
|
* Call this to change the polling state and restart the current polling list.
|
|
* This won't do anything if the state is already active. The state is changed without
|
|
* waiting for pending responses to finish (except PollSingleRequests).
|
|
*
|
|
* @param state
|
|
* The polling state to activate (0 … VEHICLE_POLL_NSTATES)
|
|
*/
|
|
void OvmsVehicle::PollSetState(uint8_t state)
|
|
{
|
|
if ((state < VEHICLE_POLL_NSTATES)&&(state != m_poll_state))
|
|
{
|
|
OvmsRecMutexLock slock(&m_poll_single_mutex);
|
|
OvmsRecMutexLock lock(&m_poll_mutex);
|
|
m_poll_state = state;
|
|
m_poll_ticker = 0;
|
|
m_poll_sequence_cnt = 0;
|
|
m_poll_wait = 0;
|
|
m_poll_plcur = NULL;
|
|
m_poll_entry = {};
|
|
m_poll_txmsgid = 0;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* PollSetThrottling: configure polling speed / niceness
|
|
* If multiple requests are due at the same poll tick (second), this controls how many of
|
|
* them will be sent in series without a delay, i.e. as soon as the response/timeout for
|
|
* the previous request occurred.
|
|
*
|
|
* @param sequence_max
|
|
* Polls allowed to be sent in sequence per time tick (second), default 1, 0 = no limit.
|
|
*
|
|
* The configuration is kept unchanged over calls to PollSetPidList() or PollSetState().
|
|
*/
|
|
void OvmsVehicle::PollSetThrottling(uint8_t sequence_max)
|
|
{
|
|
OvmsRecMutexLock lock(&m_poll_mutex);
|
|
m_poll_sequence_max = sequence_max;
|
|
}
|
|
|
|
|
|
/**
|
|
* PollSetResponseSeparationTime: configure ISO TP multi frame response timing
|
|
* See: https://en.wikipedia.org/wiki/ISO_15765-2
|
|
*
|
|
* @param septime
|
|
* Separation Time (ST), the minimum delay time between frames. Default: 25 milliseconds
|
|
* ST values up to 127 (0x7F) specify the minimum number of milliseconds to delay between frames,
|
|
* while values in the range 241 (0xF1) to 249 (0xF9) specify delays increasing from
|
|
* 100 to 900 microseconds.
|
|
*
|
|
* The configuration is kept unchanged over calls to PollSetPidList() or PollSetState().
|
|
*/
|
|
void OvmsVehicle::PollSetResponseSeparationTime(uint8_t septime)
|
|
{
|
|
assert (septime <= 127 || (septime >= 241 && septime <= 249));
|
|
OvmsRecMutexLock lock(&m_poll_mutex);
|
|
m_poll_fc_septime = septime;
|
|
}
|
|
|
|
|
|
/**
|
|
* PollSetChannelKeepalive: configure keepalive timeout for channel oriented protocols
|
|
*
|
|
* The VWTP poller will keep a channel connection to a module open for this
|
|
* many seconds after the last poll data transmission has taken place, to
|
|
* minimize connection overhead in case the next poll is sent to the same
|
|
* module.
|
|
*
|
|
* Devices may stay awake as long as the channel is open, so don't disable
|
|
* the timeout except for special purposes.
|
|
*
|
|
* @param keepalive_seconds
|
|
* Channel inactivity timeout in seconds, 0 = disable, default/init = 60
|
|
*/
|
|
void OvmsVehicle::PollSetChannelKeepalive(uint16_t keepalive_seconds)
|
|
{
|
|
m_poll_ch_keepalive = keepalive_seconds;
|
|
}
|
|
|
|
|
|
/**
|
|
* PollerSend: internal: start next due request
|
|
*/
|
|
void OvmsVehicle::PollerSend(bool fromTicker)
|
|
{
|
|
OvmsRecMutexLock lock(&m_poll_mutex);
|
|
|
|
// ESP_LOGD(TAG, "PollerSend(%d): entry at[type=%02X, pid=%X], ticker=%u, wait=%u, cnt=%u/%u",
|
|
// fromTicker, m_poll_plcur->type, m_poll_plcur->pid,
|
|
// m_poll_ticker, m_poll_wait, m_poll_sequence_cnt, m_poll_sequence_max);
|
|
|
|
if (fromTicker)
|
|
{
|
|
// Timer ticker call: reset throttling counter, check response timeout
|
|
m_poll_sequence_cnt = 0;
|
|
if (m_poll_wait > 0) m_poll_wait--;
|
|
|
|
// Protocol specific ticker calls:
|
|
PollerVWTPTicker();
|
|
}
|
|
if (m_poll_wait > 0) return;
|
|
|
|
// Check poll bus & list:
|
|
if (!m_poll_bus_default || !m_poll_plist || m_poll_plist->txmoduleid == 0) return;
|
|
|
|
// Restart poll list cursor:
|
|
if (m_poll_plcur == NULL) m_poll_plcur = m_poll_plist;
|
|
|
|
m_poll_entry = {};
|
|
|
|
while (m_poll_plcur->txmoduleid != 0)
|
|
{
|
|
if ((m_poll_plcur->polltime[m_poll_state] > 0) &&
|
|
((m_poll_ticker % m_poll_plcur->polltime[m_poll_state]) == 0))
|
|
{
|
|
// We need to poll this one...
|
|
m_poll_entry = *m_poll_plcur;
|
|
m_poll_protocol = m_poll_plcur->protocol;
|
|
m_poll_type = m_poll_plcur->type;
|
|
m_poll_pid = m_poll_plcur->pid;
|
|
|
|
switch (m_poll_plcur->pollbus)
|
|
{
|
|
case 1:
|
|
m_poll_bus = m_can1;
|
|
break;
|
|
case 2:
|
|
m_poll_bus = m_can2;
|
|
break;
|
|
case 3:
|
|
m_poll_bus = m_can3;
|
|
break;
|
|
case 4:
|
|
m_poll_bus = m_can4;
|
|
break;
|
|
default:
|
|
m_poll_bus = m_poll_bus_default;
|
|
}
|
|
|
|
// Dispatch transmission start to protocol handler:
|
|
if (m_poll_protocol == VWTP_20)
|
|
PollerVWTPStart(fromTicker);
|
|
else
|
|
PollerISOTPStart(fromTicker);
|
|
|
|
m_poll_plcur++;
|
|
m_poll_sequence_cnt++;
|
|
return;
|
|
}
|
|
|
|
// Poll entry is not due, check next
|
|
m_poll_plcur++;
|
|
}
|
|
|
|
// Completed checking all poll entries for the current m_poll_ticker
|
|
// ESP_LOGD(TAG, "PollerSend(%d): cycle complete for ticker=%u", fromTicker, m_poll_ticker);
|
|
m_poll_plcur = m_poll_plist;
|
|
m_poll_ticker++;
|
|
if (m_poll_ticker > 3600) m_poll_ticker -= 3600;
|
|
}
|
|
|
|
|
|
/**
|
|
* PollerTxCallback: internal: process poll request callbacks
|
|
*/
|
|
void OvmsVehicle::PollerTxCallback(const CAN_frame_t* frame, bool success)
|
|
{
|
|
OvmsRecMutexLock lock(&m_poll_mutex);
|
|
|
|
// Check for a late callback:
|
|
if (!m_poll_wait || !m_poll_plist || frame->origin != m_poll_bus || frame->MsgID != m_poll_txmsgid)
|
|
return;
|
|
|
|
// Forward to protocol handler:
|
|
if (m_poll_protocol == VWTP_20)
|
|
PollerVWTPTxCallback(frame, success);
|
|
|
|
// On failure, try to speed up the current poll timeout:
|
|
if (!success)
|
|
{
|
|
m_poll_wait = 0;
|
|
if (m_poll_single_rxbuf)
|
|
{
|
|
m_poll_single_rxerr = POLLSINGLE_TXFAILURE;
|
|
m_poll_single_rxbuf = NULL;
|
|
m_poll_single_rxdone.Give();
|
|
}
|
|
}
|
|
|
|
// Forward to application:
|
|
IncomingPollTxCallback(m_poll_bus, m_poll_moduleid_sent, m_poll_type, m_poll_pid, success);
|
|
}
|
|
|
|
|
|
/**
|
|
* PollerReceive: internal: process poll response frame
|
|
*/
|
|
void OvmsVehicle::PollerReceive(CAN_frame_t* frame, uint32_t msgid)
|
|
{
|
|
// obsolete
|
|
}
|
|
|
|
|
|
/**
|
|
* PollSingleRequest: perform prioritized synchronous single OBD2/UDS request
|
|
* Pass a full OBD2/UDS request (mode/type, PID, additional payload).
|
|
* The request is sent immediately, aborting a running poll list request. The previous
|
|
* poller state is automatically restored after the request has been performed, but
|
|
* without any guarantee for repetition or omission of an aborted poll.
|
|
* On success, the response buffer will contain the response payload (may be empty).
|
|
*
|
|
* See OvmsVehicleFactory::obdii_request() for a usage example.
|
|
*
|
|
* ATT: must not be called from within the vehicle task context -- deadlock situation!
|
|
*
|
|
* @param bus CAN bus to use for the request
|
|
* @param txid CAN ID to send to (0x7df = broadcast)
|
|
* @param rxid CAN ID to expect response from (broadcast: 0)
|
|
* @param request Request to send (binary string) (type + pid + up to 4095 bytes payload)
|
|
* @param response Response buffer (binary string) (multiple response frames assembled)
|
|
* @param timeout_ms Timeout for poller/response in milliseconds
|
|
* @param protocol Protocol variant: ISOTP_STD / ISOTP_EXTADR / ISOTP_EXTFRAME
|
|
*
|
|
* @return POLLSINGLE_OK (0) -- success, response is valid
|
|
* POLLSINGLE_TIMEOUT (-1) -- timeout/poller unavailable
|
|
* POLLSINGLE_TXFAILURE (-2) -- CAN transmission failure
|
|
* else (>0) -- UDS NRC detail code
|
|
* Note: response is only valid with return value 0
|
|
*/
|
|
int OvmsVehicle::PollSingleRequest(canbus* bus, uint32_t txid, uint32_t rxid,
|
|
std::string request, std::string& response,
|
|
int timeout_ms /*=3000*/, uint8_t protocol /*=ISOTP_STD*/)
|
|
{
|
|
if (!m_ready)
|
|
return -1;
|
|
|
|
if (!m_registeredlistener)
|
|
{
|
|
m_registeredlistener = true;
|
|
MyCan.RegisterListener(m_rxqueue);
|
|
}
|
|
|
|
OvmsRecMutexLock slock(&m_poll_single_mutex, pdMS_TO_TICKS(timeout_ms));
|
|
if (!slock.IsLocked())
|
|
return -1;
|
|
|
|
// prepare single poll:
|
|
OvmsVehicle::poll_pid_t poll[] =
|
|
{
|
|
{ txid, rxid, 0, 0, { 999, 999, 999, 999 }, 0, protocol },
|
|
POLL_LIST_END
|
|
};
|
|
|
|
assert(request.size() > 0);
|
|
poll[0].type = request[0];
|
|
poll[0].xargs.tag = POLL_TXDATA;
|
|
|
|
if (POLL_TYPE_HAS_16BIT_PID(poll[0].type))
|
|
{
|
|
assert(request.size() >= 3);
|
|
poll[0].xargs.pid = request[1] << 8 | request[2];
|
|
poll[0].xargs.datalen = LIMIT_MAX(request.size()-3, 4095);
|
|
poll[0].xargs.data = (const uint8_t*)request.data()+3;
|
|
}
|
|
else if (POLL_TYPE_HAS_8BIT_PID(poll[0].type))
|
|
{
|
|
assert(request.size() >= 2);
|
|
poll[0].xargs.pid = request.at(1);
|
|
poll[0].xargs.datalen = LIMIT_MAX(request.size()-2, 4095);
|
|
poll[0].xargs.data = (const uint8_t*)request.data()+2;
|
|
}
|
|
else
|
|
{
|
|
poll[0].xargs.pid = 0;
|
|
poll[0].xargs.datalen = LIMIT_MAX(request.size()-1, 4095);
|
|
poll[0].xargs.data = (const uint8_t*)request.data()+1;
|
|
}
|
|
|
|
// acquire poller access:
|
|
if (!m_poll_mutex.Lock(pdMS_TO_TICKS(timeout_ms)))
|
|
return -1;
|
|
|
|
// save poller state:
|
|
canbus* p_bus = m_poll_bus_default;
|
|
const poll_pid_t* p_list = m_poll_plist;
|
|
const poll_pid_t* p_plcur = m_poll_plcur;
|
|
uint32_t p_ticker = m_poll_ticker;
|
|
|
|
// start single poll:
|
|
PollSetPidList(bus, poll);
|
|
m_poll_single_rxdone.Take(0);
|
|
m_poll_single_rxbuf = &response;
|
|
PollerSend(true);
|
|
m_poll_mutex.Unlock();
|
|
|
|
// wait for response:
|
|
bool rxok = m_poll_single_rxdone.Take(pdMS_TO_TICKS(timeout_ms));
|
|
|
|
// restore poller state:
|
|
m_poll_mutex.Lock();
|
|
PollSetPidList(p_bus, p_list);
|
|
m_poll_plcur = p_plcur;
|
|
m_poll_ticker = p_ticker;
|
|
m_poll_single_rxbuf = NULL;
|
|
m_poll_mutex.Unlock();
|
|
|
|
return (rxok == pdFALSE) ? -1 : m_poll_single_rxerr;
|
|
}
|
|
|
|
|
|
/**
|
|
* PollSingleRequest: perform prioritized synchronous single OBD2/UDS request
|
|
* Convenience wrapper for standard PID polls, see above for main implementation.
|
|
*
|
|
* @param bus CAN bus to use for the request
|
|
* @param txid CAN ID to send to (0x7df = broadcast)
|
|
* @param rxid CAN ID to expect response from (broadcast: 0)
|
|
* @param polltype OBD2/UDS poll type …
|
|
* @param pid … and PID to poll
|
|
* @param response Response buffer (binary string) (multiple response frames assembled)
|
|
* @param timeout_ms Timeout for poller/response in milliseconds
|
|
* @param protocol Protocol variant: ISOTP_STD / ISOTP_EXTADR / ISOTP_EXTFRAME
|
|
*
|
|
* @return POLLSINGLE_OK ( 0) -- success, response is valid
|
|
* POLLSINGLE_TIMEOUT (-1) -- timeout/poller unavailable
|
|
* POLLSINGLE_TXFAILURE (-2) -- CAN transmission failure
|
|
* else (>0) -- UDS NRC detail code
|
|
* Note: response is only valid with return value 0
|
|
*/
|
|
int OvmsVehicle::PollSingleRequest(canbus* bus, uint32_t txid, uint32_t rxid,
|
|
uint8_t polltype, uint16_t pid, std::string& response,
|
|
int timeout_ms /*=3000*/, uint8_t protocol /*=ISOTP_STD*/)
|
|
{
|
|
std::string request;
|
|
request = (char) polltype;
|
|
if (POLL_TYPE_HAS_16BIT_PID(polltype))
|
|
{
|
|
request += (char) (pid >> 8);
|
|
request += (char) (pid & 0xff);
|
|
}
|
|
else if (POLL_TYPE_HAS_8BIT_PID(polltype))
|
|
{
|
|
request += (char) (pid & 0xff);
|
|
}
|
|
return PollSingleRequest(bus, txid, rxid, request, response, timeout_ms, protocol);
|
|
}
|
|
|
|
|
|
/**
|
|
* PollResultCodeName: get text representation of result code
|
|
*/
|
|
const char* OvmsVehicle::PollResultCodeName(int code)
|
|
{
|
|
switch (code)
|
|
{
|
|
case 0x10: return "generalReject";
|
|
case 0x11: return "serviceNotSupported";
|
|
case 0x12: return "subFunctionNotSupported";
|
|
case 0x13: return "incorrectMessageLengthOrInvalidFormat";
|
|
case 0x14: return "responseTooLong";
|
|
case 0x21: return "busyRepeatRequest";
|
|
case 0x22: return "conditionsNotCorrect";
|
|
case 0x24: return "requestSequenceError";
|
|
case 0x25: return "noResponseFromSubnetComponent";
|
|
case 0x26: return "failurePreventsExecutionOfRequestedAction";
|
|
case 0x31: return "requestOutOfRange";
|
|
case 0x33: return "securityAccessDenied";
|
|
case 0x35: return "invalidKey";
|
|
case 0x36: return "exceedNumberOfAttempts";
|
|
case 0x37: return "requiredTimeDelayNotExpired";
|
|
case 0x70: return "uploadDownloadNotAccepted";
|
|
case 0x71: return "transferDataSuspended";
|
|
case 0x72: return "generalProgrammingFailure";
|
|
case 0x73: return "wrongBlockSequenceCounter";
|
|
case 0x78: return "requestCorrectlyReceived-ResponsePending";
|
|
case 0x7e: return "subFunctionNotSupportedInActiveSession";
|
|
case 0x7f: return "serviceNotSupportedInActiveSession";
|
|
default: return NULL;
|
|
}
|
|
}
|