├── .gitignore ├── README.md └── elm ├── __init__.py ├── __main__.py └── elm.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-ELM 2 | ========== 3 | 4 | A python simulator for the ELM327 OBD-II adapter. Built for testing [python-OBD](https://github.com/brendanwhitfield/python-OBD). 5 | 6 | This is really just some rough sketches right now, check back later. 7 | -------------------------------------------------------------------------------- /elm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendan-w/python-ELM/97d6984eababf0e6aef42ff06220509624aa78fe/elm/__init__.py -------------------------------------------------------------------------------- /elm/__main__.py: -------------------------------------------------------------------------------- 1 | 2 | from .elm import ELM 3 | import time 4 | 5 | e = ELM(None, None) 6 | 7 | with e as pts_name: 8 | print("Running on %s" % pts_name) 9 | while True: 10 | time.sleep(1) 11 | -------------------------------------------------------------------------------- /elm/elm.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | import os 4 | import pty 5 | import threading 6 | 7 | 8 | class ELM: 9 | 10 | ELM_VALID_CHARS = r"[a-zA-Z0-9 \n\r]*" 11 | 12 | # AT commands 13 | ELM_AT = r"^AT" 14 | 15 | ELM_RESET = r"ATZ$" 16 | ELM_WARM_START = r"ATWS$" 17 | ELM_DEFAULTS = r"ATD$" 18 | ELM_VERSION = r"ATI$" 19 | ELM_ECHO = r"ATE[01]$" 20 | ELM_HEADERS = r"ATH[01]$" 21 | ELM_LINEFEEDS = r"ATL[01]$" 22 | ELM_DESCRIBE_PROTO = r"ATDP$" 23 | ELM_DESCRIBE_PROTO_N = r"ATDPN$" 24 | ELM_SET_PROTO = r"ATSPA?[0-9A-C]$" 25 | ELM_ERASE_PROTO = r"ATSP00$" 26 | ELM_TRY_PROTO = r"ATTPA?[0-9A-C]$" 27 | 28 | # responses 29 | ELM_OK = "OK" 30 | 31 | 32 | def __init__(self, protocols, ecus): 33 | self.set_defaults() 34 | 35 | 36 | def __enter__(self): 37 | 38 | # make a new pty 39 | self.master_fd, self.slave_fd = pty.openpty() 40 | self.slave_name = os.ttyname(self.slave_fd) 41 | 42 | # start the read thread 43 | self.running = True 44 | self.thread = threading.Thread(target=self.run) 45 | self.thread.daemon = True 46 | self.thread.start() 47 | 48 | return self.slave_name 49 | 50 | 51 | def __exit__(self, exc_type, exc_value, traceback): 52 | self.running = False 53 | # self.thread.join() 54 | os.close(self.slave_fd) 55 | os.close(self.master_fd) 56 | return False # don't suppress any exceptions 57 | 58 | 59 | def run(self): 60 | """ the ELM's main IO loop """ 61 | while self.running: 62 | 63 | # get the latest command 64 | self.cmd = self.read() 65 | print("recv:", repr(self.cmd)) 66 | 67 | # if it didn't contain any egregious errors, handle it 68 | if self.validate(self.cmd): 69 | resp = self.handle(self.cmd) 70 | self.write(resp) 71 | 72 | 73 | def read(self): 74 | """ 75 | reads the next newline delimited command from the port 76 | filters 77 | 78 | returns a normallized string command 79 | """ 80 | buffer = "" 81 | 82 | while True: 83 | c = os.read(self.master_fd, 1).decode() 84 | 85 | if c == '\n': 86 | break 87 | 88 | if c == '\r': 89 | continue # ignore carraige returns 90 | 91 | buffer += c 92 | 93 | return buffer 94 | 95 | 96 | def write(self, resp): 97 | """ write a response to the port """ 98 | 99 | n = "\r\n" if self.linefeeds else "\r" 100 | 101 | resp += n + ">" 102 | 103 | if self.echo: 104 | resp = self.cmd + n + resp 105 | 106 | print("write:", repr(resp)) 107 | 108 | return os.write(self.master_fd, resp.encode()) 109 | 110 | 111 | def validate(self, cmd): 112 | 113 | if not re.match(self.ELM_VALID_CHARS, cmd): 114 | return False 115 | 116 | # TODO: more tests 117 | 118 | return True 119 | 120 | 121 | def handle(self, cmd): 122 | """ handles all commands """ 123 | 124 | cmd = self.sanitize(cmd) 125 | 126 | print("handling:", repr(cmd)) 127 | 128 | if re.match(self.ELM_AT, cmd): 129 | if re.match(self.ELM_ECHO, cmd): 130 | self.echo = (cmd[3] == '1') 131 | print("set ECHO %s" % self.echo) 132 | return self.ELM_OK 133 | elif re.match(self.ELM_HEADERS, cmd): 134 | self.headers = (cmd[3] == '1') 135 | print("set HEADERS %s" % self.headers) 136 | return self.ELM_OK 137 | elif re.match(self.ELM_LINEFEEDS, cmd): 138 | self.linefeeds = (cmd[3] == '1') 139 | print("set LINEFEEDS %s" % self.linefeeds) 140 | return self.ELM_OK 141 | else: 142 | pass 143 | else: 144 | pass 145 | 146 | return "" 147 | 148 | 149 | def sanitize(self, cmd): 150 | cmd = cmd.replace(" ", "") 151 | cmd = cmd.upper() 152 | return cmd 153 | 154 | 155 | def set_defaults(self): 156 | """ returns all settings to their defaults """ 157 | self.echo = True 158 | self.headers = True 159 | self.linefeeds = True 160 | --------------------------------------------------------------------------------