diff --git a/README.md b/README.md index 32bff39..348e9c5 100644 --- a/README.md +++ b/README.md @@ -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`.) @@ -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: @@ -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:`). @@ -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' @@ -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) diff --git a/elm/__version__.py b/elm/__version__.py index 3f6fab6..8a81504 100644 --- a/elm/__version__.py +++ b/elm/__version__.py @@ -1 +1 @@ -__version__ = '1.0.3' +__version__ = '1.0.4' diff --git a/elm/elm.py b/elm/elm.py index fb978af..cb97ff5 100644 --- a/elm/elm.py +++ b/elm/elm.py @@ -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__ @@ -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 @@ -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: @@ -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 @@ -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(" ", "") diff --git a/elm/interpreter.py b/elm/interpreter.py index e8e7e87..74b2bae 100644 --- a/elm/interpreter.py +++ b/elm/interpreter.py @@ -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"\ diff --git a/elm/obd_message.py b/elm/obd_message.py index 55bae86..6d2c16c 100644 --- a/elm/obd_message.py +++ b/elm/obd_message.py @@ -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 diff --git a/obd_dictionary/__init__.py b/obd_dictionary/__init__.py index 8498854..59afc46 100644 --- a/obd_dictionary/__init__.py +++ b/obd_dictionary/__init__.py @@ -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 @@ -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( @@ -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 + "': {")