Skip to content

Commit

Permalink
Version 1.0.4.
Browse files Browse the repository at this point in the history
Software changed to manage the invalid answer message in ELM_R_UNKNOWN,
which defaults to ?\r\r and can be changed.
Documentation updated accordingly.
Added loglevel keyword.

Fixes #8

ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
  • Loading branch information
Ircama committed Mar 7, 2021
1 parent 78246b6 commit 4b543f0
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 12 deletions.
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ The emulator provides a monitoring front-end, supporting commands and controllin
At the `CMD> ` prompt, the emulator accepts the following commands:

- `help` = List available commands (or detailed help with "help cmd").
- `loglevel` = If an argument is given, set the logging level, otherwise show the current one. Valid numbers: CRITICAL=50, ERROR=40, WARNING=30, INFO=20, DEBUG=10.
- `quit` (or end-of-file/Control-D, or break/Control-C) = quit the program
- `counters` = print the number of each executed PIDs (upper case names), the values associated to some 'AT' PIDs (*cmd_...*), the unknown requests, the emulator response delay, the total number of executed commands (*commands*) and the current scenario (*scenario*). The related dictionary is `emulator.counters`.
- `pause` = pause the execution. (Related attribute is `emulator.threadState = emulator.THREAD.PAUSED`.)
Expand Down Expand Up @@ -190,7 +191,13 @@ To simulate that the adapter is not connected to the vehicle:
emulator.answer['AT_R_VOLT'] = '0.0V'
```

This dictionary can be used to modify answers within a workflow. The front-end allows implementing basic Python workflows and, when used in batch mode, can also be controlled by a piped external supervisor. The following examples show some simple workflows in interactive mode.
The `emulator.ELM_R_UNKNOWN` parameter allows customizing the message returned in case of unknown/invalid command. The default message is `?\r`, with an addition of a trailing `\r`. This message can be customized; for example, to just get `\r`, set the following:

```python
emulator.ELM_R_UNKNOWN = ''
```

The dictionary can be used to modify answers within a workflow. The front-end allows implementing basic Python workflows and, when used in batch mode, can also be controlled by a piped external supervisor. The following examples show some simple workflows in interactive mode.

Example of automation which suspends the emulator for 10 seconds:

Expand All @@ -216,7 +223,7 @@ car

## Configuring response strings

Response strings allow embedding Python statements and expressions. Specifically, `Response`, `ResponseHeader`, `ResponseFooter` and `emulator.answer` support single and multiple in-line Python commands (expressions or statements) when embraced between `\0` tags: this feature for instance can be used to embed real-time delays between strings or to differentiate answers. The return value of a statement is ignored. The evaluation of an expression is substituted. Spaces inside `\0` tags are allowed and can be used to improve readability. Example: `'Response' = 'SEARCHING...\0 time.sleep(1) \0\rUNABLE TO CONNECT\r'`. This returns `SEARCHING...`, then waits one second, then returns `\rUNABLE TO CONNECT\r`. Notice that, as `time.sleep` is a statement, the related return value is ignored.
Response strings allow embedding Python statements and expressions. Specifically, `Response`, `ResponseHeader`, `ResponseFooter`, `emulator.answer` and `emulator.ELM_R_UNKNOWN` support single and multiple in-line Python commands (expressions or statements) when embraced between `\0` tags: this feature for instance can be used to embed real-time delays between strings or to differentiate answers. The return value of a statement is ignored. The evaluation of an expression is substituted. Spaces inside `\0` tags are allowed and can be used to improve readability. Example: `'Response' = 'SEARCHING...\0 time.sleep(1) \0\rUNABLE TO CONNECT\r'`. This returns `SEARCHING...`, then waits one second, then returns `\rUNABLE TO CONNECT\r`. Notice that, as `time.sleep` is a statement, the related return value is ignored.

Further processing can be achieved through a *lambda function* applied to `ResponseHeader`, `ResponseFooter`. It has to manage the following parameters: *self*, *cmd*, *pid*, *val* (e.g., `lambda self, cmd, pid, val:`).

Expand All @@ -241,7 +248,7 @@ Example of PID definition within the `ObdMessage` dictionary:

In the above example, the first time *ResponseHeader* is executed, the produced response is `SEARCHING...`, followed by a one-second delay and then `\rUNABLE TO CONNECT\r`. For all subsequent messages, the response will be different and produces `'NO DATA\r'`.

The ability to add dynamic differentiators and delays within responses enables testing specific use cases and exceptions that are difficult to be achieved through a real connection with a car. These not only apply to the `ObdMessage` dictionary (by editing *obd_message.py*), but also to `emulator.answer`, that can be configured through the command line. Consider for instance the following dynamic configuration via command line:
The ability to add dynamic differentiators and delays within responses enables testing specific use cases and exceptions that are difficult to be achieved through a real connection with a car. These not only apply to the `ObdMessage` dictionary (by editing *obd_message.py*), but also to `emulator.answer` and `emulator.ELM_R_UNKNOWN`, that can be configured through the command line. Consider for instance the following dynamic configuration via command line:

```python
emulator.answer['SPEED'] = '\0 ECU_R_ADDR_E + " 03 41 0D 0A " if randint(0, 100) > 20 else "NO DATA" \0\r'
Expand Down Expand Up @@ -300,7 +307,21 @@ emulator.counters["ELM_PIDS_A"] = 0

Logs are written to *elm.log*, file, rotated to *elm.log.1* and *elm.log.2* when its size reaches 1 MB. [Logging](https://docs.python.org/3/howto/logging.html) is controlled through the `elm.yaml` file (in the current directory by default). Its path can be set through the *ELM_LOG_CFG* environment variable. This file follows the [Python’s builtin logging module format](https://docs.djangoproject.com/en/2.2/topics/logging/#a-quick-logging-primer) and allows customizing the configuration of the logging process.

The logging level can be dynamically changed through `logging.getLogger().handlers[n].setLevel()`. To check that *console* is the first handler (e.g., `handlers[0]`), run `for n, l in enumerate(logging.getLogger().handlers): print(n, l.name)`. For instance, if *console* refers to the first handler (default settings of the provided `elm.yaml` file), the following commands will change the logging level:
The logging level can be dynamically changed through the `loglevel` command.

To read the current log level:

```shell
loglevel
```

To set log level to debug:

```shell
loglevel 10
```

Alternatively, the logging level can be set through `logging.getLogger().handlers[n].setLevel()`. To check that *console* is the first handler (e.g., `handlers[0]`), run `for n, l in enumerate(logging.getLogger().handlers): print(n, l.name)`. For instance, if *console* refers to the first handler (default settings of the provided `elm.yaml` file), the following commands will change the logging level:

```python
logging.getLogger().handlers[0].setLevel(logging.DEBUG)
Expand Down
2 changes: 1 addition & 1 deletion elm/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.0.3'
__version__ = '1.0.4'
9 changes: 5 additions & 4 deletions elm/elm.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import traceback
import errno
from random import randint
from .obd_message import ObdMessage, ECU_ADDR_E, ELM_R_OK
from .obd_message import ObdMessage, ECU_ADDR_E, ELM_R_OK, ELM_R_UNKNOWN
from .__version__ import __version__


Expand Down Expand Up @@ -103,6 +103,7 @@ def setSortedOBDMsg(self):

def __init__(self, batch_mode=False, serial_port=""):
self.ObdMessage = ObdMessage
self.ELM_R_UNKNOWN = ELM_R_UNKNOWN
self.set_defaults()
self.setSortedOBDMsg()
self.batch_mode = batch_mode
Expand Down Expand Up @@ -323,9 +324,9 @@ def write(self, resp):
except Exception as e:
logging.error("Cannot execute '%s': %s", i, e)
else:
logging.debug("Write: %s", repr(i))
if nospaces:
i = re.sub(r'[ \t]+', '', i)
logging.debug("Write: %s", repr(i))
if os.name == 'nt':
self.master_fd.write(i.encode())
else:
Expand Down Expand Up @@ -425,7 +426,7 @@ def handle(self, cmd):
logging.error(
"Internal error - Missing response for %s, PID %s",
cmd, pid)
return self.ELM_R_OK
return ELM_R_OK
if "unknown_" + cmd not in self.counters:
self.counters["unknown_" + cmd] = 0
self.counters["unknown_" + cmd] += 1
Expand All @@ -437,7 +438,7 @@ def handle(self, cmd):
repr(cmd), self.counters["cmd_header"])
else:
logging.info("Unknown ELM command: %s", repr(cmd))
return ""
return self.ELM_R_UNKNOWN

def sanitize(self, cmd):
cmd = cmd.replace(" ", "")
Expand Down
13 changes: 13 additions & 0 deletions elm/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,19 @@ def do_reset(self, arg):
self.emulator.set_defaults()
print("Reset done.")

def do_loglevel(self, arg):
"If an argument is given, set the logging level,\n"\
"otherwise show the current one.\n"\
"CRITICAL=50, ERROR=40, WARNING=30, INFO=20, DEBUG=10."
if arg and arg.isnumeric():
logging.getLogger().handlers[0].setLevel(int(arg))
print("Logging level set to",
logging.getLogger().handlers[0].level)
else:
print(
"Current logging level:",
logging.getLogger().handlers[0].level)

def do_counters(self, arg):
"Print the number of each executed PID (upper case names), the values\n"\
"associated to some 'AT' PIDs, the unknown requests, the emulator response\n"\
Expand Down
1 change: 1 addition & 0 deletions elm/obd_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ECU_R_ADDR_S = "7B8" # Responses sent by 7B0 Skid Control ECU 7B0/7B8

ELM_R_OK = "OK\r"
ELM_R_UNKNOWN = "?\r"
ELM_MAX_RESP = '[0123456]?$'

# This dictionary uses the ISO 15765-4 CAN 11 bit ID 500 kbaud protocol
Expand Down
8 changes: 5 additions & 3 deletions obd_dictionary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
ecu = {
"7B0": 'ECU_ADDR_S', # Skid Control address ECU
"7B8": 'ECU_R_ADDR_S', # Responses sent by 7B0 Skid Control ECU 7B0/7B8
"7E2": 'ECU_ADDR_H', # HVECU address (Hybrid contol module)
"7E2": 'ECU_ADDR_H', # HVECU address (Hybrid control module)
"7EA": 'ECU_R_ADDR_H', # Resp. sent by HVECU (Hybrid Ctrl module) 7E2/7EA
"7E0": 'ECU_ADDR_E', # Engine ECU address
"7E8": 'ECU_R_ADDR_E', # Responses sent by ECM (engine Ctrl module) 7E0/7E8
Expand Down Expand Up @@ -181,7 +181,7 @@ def obd_dictionary():
type=argparse.FileType('r'),
nargs="?",
help='include AT Commands within probes. '
'If a dictionary file is given, also extract AT Commnands'
'If a dictionary file is given, also extract AT Commands'
' from the input file and add them to the output',
metavar='FILE')
parser.add_argument(
Expand Down Expand Up @@ -296,7 +296,9 @@ def obd_dictionary():

# Print header information
print("\n".join([ecu[k] + ' = "' + k + '"' for k in ecu]))
print('ELM_R_OK = "OK\\r"\nELM_MAX_RESP = "[0123456]?$"\n')
print('ELM_R_OK = "OK\\r"\n'
'ELM_R_UNKNOWN = "?\r"\n'
'ELM_MAX_RESP = "[0123456]?$"\n')
print("ObdMessage = {")
print(" '" + args.car_name + "': {")

Expand Down

0 comments on commit 4b543f0

Please sign in to comment.