├── LICENSE ├── README.md ├── ecu-simulator.py ├── pids.py └── ui.py /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ECU Simulator 2 | 3 | Author: 2019 Sergey Shcherbakov 4 | 5 | ## Preparing environment 6 | 7 | ### Prerequisits 8 | 9 | * OS: Linux Debian/Ubuntu, tested on Ubuntu/Kubuntu 16.04+ 10 | * HW: any SocketCAN compatible module 11 | 12 | ### Preparing host environment 13 | 14 | * Update packages cache 15 | ``` 16 | sudo apt update 17 | ``` 18 | 19 | * Install mandatory packages 20 | ``` 21 | sudo apt install python3 python3-pip 22 | ``` 23 | 24 | * Install Python CAN module 25 | ``` 26 | sudo pip3 install python-can 27 | ``` 28 | 29 | * Download simulator software 30 | ``` 31 | git clone https://github.com/shchers/ecu-simulator.git 32 | ``` 33 | 34 | * Connect/enable CAN module 35 | 36 | ## Running test 37 | 38 | * Configure CAN interface. Pay attention that according to standard you can run at __250 or 500 kbps__ 39 | 40 | * Connect OBD-II probe to CAN interface 41 | 42 | * Go to script 43 | ``` 44 | cd ecu-simulator 45 | ``` 46 | 47 | * Run script 48 | ``` 49 | puthon3 ecu-simulator.py 50 | ``` 51 | 52 | # UI 53 | 54 | * Install additional packages, mandatory for UI 55 | ``` 56 | sudo apt install python3-tk 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /ecu-simulator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | 5 | from random import randint 6 | 7 | import logging as log 8 | import getopt, sys 9 | 10 | import can 11 | from can.bus import BusState 12 | 13 | def service1(bus, msg): 14 | if msg.data[2] == 0x00: 15 | log.debug(">> Caps") 16 | msg = can.Message(arbitration_id=0x7e8, 17 | data=[0x06, 0x41, 0x00, 0xBF, 0xDF, 0xB9, 0x91], 18 | is_extended_id=False) 19 | bus.send(msg) 20 | elif msg.data[2] == 0x04: 21 | log.debug(">> Calculated engine load") 22 | msg = can.Message(arbitration_id=0x7e8, 23 | data=[0x03, 0x41, 0x04, 0x20], 24 | is_extended_id=False) 25 | bus.send(msg) 26 | elif msg.data[2] == 0x05: 27 | log.debug(">> Engine coolant temperature") 28 | msg = can.Message(arbitration_id=0x7e8, 29 | data=[0x03, 0x41, 0x05, randint(88 + 40, 95 + 40)], 30 | is_extended_id=False) 31 | bus.send(msg) 32 | elif msg.data[2] == 0x0B: 33 | log.debug(">> Intake manifold absolute pressure") 34 | msg = can.Message(arbitration_id=0x7e8, 35 | data=[0x04, 0x41, 0x0B, randint(10, 40)], 36 | is_extended_id=False) 37 | bus.send(msg) 38 | elif msg.data[2] == 0x0C: 39 | log.debug(">> RPM") 40 | msg = can.Message(arbitration_id=0x7e8, 41 | data=[0x04, 0x41, 0x0C, randint(18, 70), randint(0, 255)], 42 | is_extended_id=False) 43 | bus.send(msg) 44 | elif msg.data[2] == 0x0D: 45 | log.debug(">> Speed") 46 | msg = can.Message(arbitration_id=0x7e8, 47 | data=[0x03, 0x41, 0x0D, randint(40, 60)], 48 | is_extended_id=False) 49 | bus.send(msg) 50 | elif msg.data[2] == 0x0F: 51 | log.debug(">> Intake air temperature") 52 | msg = can.Message(arbitration_id=0x7e8, 53 | data=[0x03, 0x41, 0x0F, randint(60, 64)], 54 | is_extended_id=False) 55 | bus.send(msg) 56 | elif msg.data[2] == 0x10: 57 | log.debug(">> MAF air flow rate") 58 | msg = can.Message(arbitration_id=0x7e8, 59 | data=[0x04, 0x41, 0x10, 0x00, 0xFA], 60 | is_extended_id=False) 61 | bus.send(msg) 62 | elif msg.data[2] == 0x11: 63 | log.debug(">> Throttle position") 64 | msg = can.Message(arbitration_id=0x7e8, 65 | data=[0x03, 0x41, 0x11, randint(20, 60)], 66 | is_extended_id=False) 67 | bus.send(msg) 68 | elif msg.data[2] == 0x33: 69 | log.debug(">> Absolute Barometric Pressure") 70 | msg = can.Message(arbitration_id=0x7e8, 71 | data=[0x03, 0x41, 0x33, randint(20, 60)], 72 | is_extended_id=False) 73 | bus.send(msg) 74 | else: 75 | log.warning("!!! Service 1, unknown code 0x%02x", msg.data[2]) 76 | 77 | 78 | def receive_all(): 79 | 80 | bus = can.interface.Bus(bustype='socketcan',channel='can0') 81 | #bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) 82 | #bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) 83 | 84 | #bus.state = BusState.ACTIVE 85 | #bus.state = BusState.PASSIVE 86 | 87 | try: 88 | while True: 89 | msg = bus.recv(1) 90 | if msg is not None: 91 | #print(msg) 92 | if msg.arbitration_id == 0x7df and msg.data[1] == 0x01: 93 | service1(bus, msg) 94 | else: 95 | log.warning("Unknown ID %d or service code 0x%02x", msg.arbitration_id, msg.data[1]) 96 | 97 | except KeyboardInterrupt: 98 | pass 99 | 100 | def usage(): 101 | # DOTO: implement 102 | pass 103 | 104 | def main(): 105 | try: 106 | opts, args = getopt.getopt(sys.argv[1:], "l:v", ["loglevel="]) 107 | except getopt.GetoptError as err: 108 | # print help information and exit: 109 | print(err) # will print something like "option -a not recognized" 110 | usage() 111 | sys.exit(2) 112 | 113 | loglevel = "INFO" 114 | 115 | for o, a in opts: 116 | if o == "-v": 117 | loglevel = "DEBUG" 118 | elif o in ("-l", "--loglevel"): 119 | loglevel = a 120 | elif o in ("-h", "--help"): 121 | usage() 122 | sys.exit() 123 | else: 124 | assert False, "unhandled option" 125 | 126 | numeric_level = getattr(log, loglevel.upper(), None) 127 | if not isinstance(numeric_level, int): 128 | raise ValueError('Invalid log level: %s' % loglevel) 129 | log.basicConfig(level=numeric_level) 130 | receive_all() 131 | 132 | if __name__ == "__main__": 133 | main(); 134 | -------------------------------------------------------------------------------- /pids.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import tkinter as tk 4 | 5 | # PIDs dictionary. As a reference used Wiki - https://en.wikipedia.org/wiki/OBD-II_PIDs 6 | Pids = { 7 | 1 : { 8 | 0x00 : { 9 | "name" : "PIDs supported [01 - 20]", # PID name 10 | "len" : 4, # Response length 11 | "is-property" : True, # Not a value, but changable param 12 | "static" : True, # Static param 13 | "min" : 0, # Min param value, not used if "is-property" 14 | "max" : 0, # Max param value, not used if "is-property" 15 | "units" : "n/a", # Value units 16 | }, 17 | 0x01 : { 18 | "name" : "Monitor status since DTCs cleared", 19 | "len" : 4, 20 | "is-property" : True, 21 | "static" : False, 22 | "min" : 0, 23 | "max" : 0, 24 | "units" : "n/a", 25 | }, 26 | 0x02 : { 27 | "name" : "Freeze DTC", 28 | "len" : 2, 29 | "is-property" : True, 30 | "static" : False, 31 | "min" : 0, 32 | "max" : 0, 33 | "units" : "n/a", 34 | }, 35 | 0x03 : { 36 | "name" : "Fuel system status", 37 | "len" : 2, 38 | "is-property" : True, 39 | "static" : False, 40 | "min" : 0, 41 | "max" : 0, 42 | "units" : "n/a", 43 | }, 44 | 0x04 : { 45 | "name" : "Calculated engine load", 46 | "len" : 1, 47 | "is-property" : False, 48 | "static" : False, 49 | "min" : 0, 50 | "max" : 100, 51 | "units" : "%", 52 | }, 53 | 0x05 : { 54 | "name" : "Engine coolant temperature", 55 | "len" : 1, 56 | "is-property" : False, 57 | "static" : False, 58 | "min" : -40, 59 | "max" : 215, 60 | "units" : "°C", 61 | }, 62 | 0x06 : { 63 | "name" : "Short term fuel trim-Bank 1", 64 | "len" : 1, 65 | "is-property" : False, 66 | "static" : False, 67 | "min" : -100, 68 | "max" : 99.2, 69 | "units" : "%", 70 | }, 71 | 0x07 : { 72 | "name" : "Long term fuel trim-Bank 1", 73 | "len" : 1, 74 | "is-property" : False, 75 | "static" : False, 76 | "min" : -100, 77 | "max" : 99.2, 78 | "units" : "%", 79 | }, 80 | 0x08 : { 81 | "name" : "Short term fuel trim-Bank 2", 82 | "len" : 1, 83 | "is-property" : False, 84 | "static" : False, 85 | "min" : -100, 86 | "max" : 99.2, 87 | "units" : "%", 88 | }, 89 | 0x09 : { 90 | "name" : "Long term fuel trim-Bank 2", 91 | "len" : 1, 92 | "is-property" : False, 93 | "static" : False, 94 | "min" : -100, 95 | "max" : 99.2, 96 | "units" : "%", 97 | }, 98 | 0x0a : { 99 | "name" : "Fuel pressure", 100 | "len" : 1, 101 | "is-property" : False, 102 | "static" : False, 103 | "min" : 0, 104 | "max" : 765, 105 | "units" : "kPa", 106 | }, 107 | 0x0b : { 108 | "name" : "Intake manifold absolute pressure", 109 | "len" : 1, 110 | "is-property" : False, 111 | "static" : False, 112 | "min" : 0, 113 | "max" : 255, 114 | "units" : "kPa", 115 | }, 116 | 0x0c : { 117 | "name" : "Engine RPM", 118 | "len" : 2, 119 | "is-property" : False, 120 | "static" : False, 121 | "min" : 0, 122 | "max" : 16383.75, 123 | "units" : "rpm", 124 | }, 125 | 0x0d : { 126 | "name" : "Vehicle speed", 127 | "len" : 1, 128 | "is-property" : False, 129 | "static" : False, 130 | "min" : 0, 131 | "max" : 255, 132 | "units" : "km/h", 133 | }, 134 | 0x0e : { 135 | "name" : "Timing advance", 136 | "len" : 1, 137 | "is-property" : False, 138 | "static" : False, 139 | "min" : -64, 140 | "max" : 63.5, 141 | "units" : "° before TDC", 142 | }, 143 | 0x0f : { 144 | "name" : "Intake air temperature", 145 | "len" : 1, 146 | "is-property" : False, 147 | "static" : False, 148 | "min" : -40, 149 | "max" : 215, 150 | "units" : "°C", 151 | }, 152 | 0x10 : { 153 | "name" : "MAF air flow rate", 154 | "len" : 2, 155 | "is-property" : False, 156 | "static" : False, 157 | "min" : 0, 158 | "max" : 655.35, 159 | "units" : "grams/sec", 160 | }, 161 | 0x11 : { 162 | "name" : "Throttle position", 163 | "len" : 1, 164 | "is-property" : False, 165 | "static" : False, 166 | "min" : 0, 167 | "max" : 100, 168 | "units" : "%", 169 | }, 170 | 0x12 : { 171 | "name" : "Commanded secondary air status", 172 | "len" : 1, 173 | "is-property" : True, 174 | "static" : False, 175 | "min" : 0, 176 | "max" : 0, 177 | "units" : "", 178 | }, 179 | 0x13 : { 180 | "name" : "Oxygen sensors present (in 2 banks)", 181 | "len" : 1, 182 | "is-property" : True, 183 | "static" : False, 184 | "min" : 0, 185 | "max" : 0, 186 | "units" : "", 187 | }, 188 | 0x14 : { 189 | "name" : "Oxygen Sensor 1", 190 | "len" : 1, 191 | "is-property" : True, 192 | "static" : False, 193 | "min" : 0, 194 | "max" : 0, 195 | "units" : "", 196 | }, 197 | 0x15 : { 198 | "name" : "Oxygen Sensor 2", 199 | "len" : 1, 200 | "is-property" : True, 201 | "static" : False, 202 | "min" : 0, 203 | "max" : 0, 204 | "units" : "", 205 | }, 206 | 0x16 : { 207 | "name" : "Oxygen Sensor 3", 208 | "len" : 1, 209 | "is-property" : True, 210 | "static" : False, 211 | "min" : 0, 212 | "max" : 0, 213 | "units" : "" 214 | }, 215 | 0x17 : { 216 | "name" : "Oxygen Sensor 4", 217 | "len" : 1, 218 | "is-property" : True, 219 | "static" : False, 220 | "min" : 0, 221 | "max" : 0, 222 | "units" : "", 223 | }, 224 | 0x18 : { 225 | "name" : "Oxygen Sensor 5", 226 | "len" : 1, 227 | "is-property" : True, 228 | "static" : False, 229 | "min" : 0, 230 | "max" : 0, 231 | "units" : "", 232 | }, 233 | 0x19 : { 234 | "name" : "Oxygen Sensor 6", 235 | "len" : 1, 236 | "is-property" : True, 237 | "static" : False, 238 | "min" : 0, 239 | "max" : 0, 240 | "units" : "", 241 | }, 242 | 0x1a : { 243 | "name" : "Oxygen Sensor 7", 244 | "len" : 1, 245 | "is-property" : True, 246 | "static" : False, 247 | "min" : 0, 248 | "max" : 0, 249 | "units" : "", 250 | }, 251 | 0x1b : { 252 | "name" : "Oxygen Sensor 8", 253 | "len" : 1, 254 | "is-property" : True, 255 | "static" : False, 256 | "min" : 0, 257 | "max" : 0, 258 | "units" : "", 259 | }, 260 | 0x1c : { 261 | "name" : "OBD standards this vehicle conforms to", 262 | "len" : 1, 263 | "is-property" : True, 264 | "static" : False, 265 | "min" : 0, 266 | "max" : 0, 267 | "units" : "", 268 | }, 269 | 0x1d : { 270 | "name" : "Oxygen sensors present (in 4 banks)", 271 | "len" : 1, 272 | "is-property" : True, 273 | "static" : False, 274 | "min" : 0, 275 | "max" : 0, 276 | "units" : "", 277 | }, 278 | 0x1e : { 279 | "name" : "Auxiliary input status", 280 | "len" : 1, 281 | "is-property" : True, 282 | "static" : False, 283 | "min" : 0, 284 | "max" : 0, 285 | "units" : "", 286 | }, 287 | 0x1f : { 288 | "name" : "Run time since engine start", 289 | "len" : 1, 290 | "is-property" : False, 291 | "static" : False, 292 | "min" : 0, 293 | "max" : 65535, 294 | "units" : "seconds", 295 | }, 296 | 0x20 : { 297 | "name" : "PIDs supported [21 - 40]", 298 | "len" : 1, 299 | "is-property" : True, 300 | "static" : False, 301 | "min" : 0, 302 | "max" : 0, 303 | "units" : "", 304 | }, 305 | } 306 | } 307 | 308 | 309 | class Application(tk.Frame): 310 | def __init__(self, master=None): 311 | super().__init__(master) 312 | self.master = master 313 | self.master.minsize(width=400, height=800) 314 | tk.Grid.rowconfigure(master, 0, weight=1) 315 | tk.Grid.columnconfigure(master, 0, weight=1) 316 | 317 | # Variables 318 | self.var = [tk.BooleanVar() for i in range(32)] 319 | 320 | self.create_controls() 321 | 322 | def create_controls(self): 323 | frame=tk.Frame(self.master) 324 | frame.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W) 325 | 326 | row_id = 0 327 | 328 | for i in range(32): 329 | try: 330 | pid_name = 'PID ${:02X} - {:s}'.format(i + 1, Pids[1][i + 1]["name"]) 331 | except: 332 | pid_name = 'PID ${:02X}'.format(i + 1) 333 | 334 | cb = tk.Checkbutton(frame, text=pid_name, 335 | variable=self.var[i], command=self.on_cb_changed) 336 | cb.grid(row=row_id, column=3, sticky=tk.W) 337 | row_id += 1 338 | 339 | self.pids_entry = tk.Entry(frame, state="readonly") 340 | self.pids_entry.grid(row=row_id, column=3, sticky=tk.W+tk.E) 341 | 342 | def on_cb_changed(self): 343 | val = 0 344 | for i in range(32): 345 | if self.var[i].get(): 346 | val |= (1 << (7-(i % 8))) << (int(i/8) * 8) 347 | 348 | a = val & 0xff 349 | b = (val >> 8) & 0xff 350 | c = (val >> 16) & 0xff 351 | d = (val >> 24) & 0xff 352 | 353 | print('0x{:02X} 0x{:02X} 0x{:02X} 0x{:02X}'.format(a, b, c, d)) 354 | 355 | self.pids_entry['state'] = 'normal' 356 | self.pids_entry.delete(0, tk.END) 357 | self.pids_entry.insert(0, '0x{:02X} 0x{:02X} 0x{:02X} 0x{:02X}'.format(a, b, c, d)) 358 | self.pids_entry['state'] = 'readonly' 359 | 360 | if __name__ == "__main__": 361 | window = tk.Tk() 362 | window.title("PIDs caps") 363 | app = Application(master=window) 364 | app.mainloop() 365 | -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import tkinter as tk 4 | from tkinter import filedialog, messagebox 5 | import glob 6 | import os 7 | import getopt 8 | import sys 9 | from random import randint 10 | from datetime import datetime 11 | import threading 12 | import logging as log 13 | 14 | import can 15 | from can.bus import BusState 16 | 17 | class Application(tk.Frame): 18 | def __init__(self, master=None): 19 | super().__init__(master) 20 | self.master = master 21 | self.master.minsize(width=800, height=600) 22 | master.protocol("WM_DELETE_WINDOW", self.close_app) 23 | tk.Grid.rowconfigure(master, 0, weight=1) 24 | tk.Grid.columnconfigure(master, 0, weight=1) 25 | 26 | # CAN bus 27 | self.event = threading.Event() 28 | self.bus = None 29 | self.can_is_started = False 30 | 31 | # Ceate variables 32 | self.can_device_var = tk.StringVar() 33 | self.speed_var = tk.IntVar() 34 | self.speed_var_auto = tk.BooleanVar() 35 | self.speed_var_min = tk.IntVar() 36 | self.speed_var_max = tk.IntVar() 37 | self.rpm_var = tk.DoubleVar() 38 | self.rpm_var_auto = tk.BooleanVar() 39 | self.rpm_var_min = tk.DoubleVar() 40 | self.rpm_var_max = tk.DoubleVar() 41 | 42 | self.create_controls() 43 | 44 | def close_app(self): 45 | if self.can_is_started: 46 | self.can_disconnect() 47 | 48 | self.master.destroy() 49 | 50 | def get_can_devices(self): 51 | # XXX: not really good to limit the list only to can 52 | # TODO: add support for different CAN interfaces 53 | devices = glob.glob('/sys/class/net/can*') 54 | for i in range(len(devices)): 55 | devices[i] = os.path.basename(devices[i]) 56 | return devices 57 | 58 | def create_controls(self): 59 | frame=tk.Frame(self.master) 60 | frame.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W) 61 | 62 | frame.columnconfigure(1, weight=1) 63 | frame.columnconfigure(4, weight=1) 64 | frame.columnconfigure(5, weight=1) 65 | 66 | 67 | # Row index 68 | row_id = 0 69 | 70 | can_frame = tk.LabelFrame(frame, text="Interface") 71 | can_frame.grid(row=row_id, column=0, padx=(10, 10), sticky=tk.W+tk.W+tk.N+tk.S) 72 | 73 | devices = self.get_can_devices() 74 | if len(devices) > 0: 75 | self.can_device_var.set(devices[0]) 76 | else: 77 | devices.append('') 78 | 79 | self.can_options = tk.OptionMenu(can_frame, self.can_device_var, *devices) 80 | self.can_options.grid(row=0, column=0, pady=(5, 10), sticky=tk.W+tk.E) 81 | 82 | btn_refresh = tk.Button(can_frame, text="R", command=self.refresh_list) 83 | btn_refresh.grid(row=0, column=1, pady=(5, 10), sticky=tk.E) 84 | 85 | self.connect = tk.Button(can_frame, text="Connect", command=self.can_connect) 86 | self.connect.grid(row=1, column=0, sticky=tk.W+tk.E) 87 | 88 | self.disconnect = tk.Button(can_frame, text="Disconnect", state="disabled", command=self.can_disconnect) 89 | self.disconnect.grid(row=2, column=0, sticky=tk.W+tk.E) 90 | 91 | # New row - Speed entry 92 | row_id += 1 93 | 94 | self.lbl_speed = tk.Label(frame, text="Speed, km/h") 95 | self.lbl_speed.grid(row=row_id, column=0, padx=(10, 10), sticky=tk.W) 96 | 97 | self.sc_speed = tk.Scale(frame, from_=0, to=255, orient=tk.HORIZONTAL, 98 | variable=self.speed_var) 99 | self.sc_speed.grid(row=row_id, column=1, padx=(5, 5), sticky=tk.W+tk.E) 100 | 101 | self.cb_speed_auto = tk.Checkbutton(frame, text="Auto mode", 102 | variable=self.speed_var_auto, command=self.on_cb_speed_auto) 103 | self.cb_speed_auto.grid(row=row_id, column=3) 104 | 105 | self.sc_speed_min = tk.Scale(frame, from_=0, to=254, orient=tk.HORIZONTAL, label="Min", 106 | state="disabled", variable=self.speed_var_min, command=self.on_sc_speed) 107 | self.sc_speed_min.grid(row=row_id, column=4, padx=(5, 5), sticky=tk.W+tk.E) 108 | 109 | self.sc_speed_max = tk.Scale(frame, from_=1, to=255, orient=tk.HORIZONTAL, label="Max", 110 | state="disabled", variable=self.speed_var_max, command=self.on_sc_speed) 111 | self.sc_speed_max.grid(row=row_id, column=5, padx=(5, 10), sticky=tk.W+tk.E) 112 | 113 | # New row - RPM entry 114 | row_id += 1 115 | 116 | self.lbl_rpm = tk.Label(frame, text="RPM, km/h") 117 | self.lbl_rpm.grid(row=row_id, column=0, padx=(10, 10), sticky=tk.W) 118 | 119 | self.sc_rpm = tk.Scale(frame, from_=0, to=16383.75, orient=tk.HORIZONTAL, 120 | resolution=0.25, variable=self.rpm_var) 121 | self.sc_rpm.grid(row=row_id, column=1, padx=(5, 5), sticky=tk.W+tk.E) 122 | 123 | self.cb_rpm_auto = tk.Checkbutton(frame, text="Auto mode", 124 | variable=self.rpm_var_auto, command=self.on_cb_rpm_auto) 125 | self.cb_rpm_auto.grid(row=row_id, column=3) 126 | 127 | self.sc_rpm_min = tk.Scale(frame, from_=0, to=16383.75, orient=tk.HORIZONTAL, label="Min", 128 | resolution=0.25, state="disabled", variable=self.rpm_var_min, command=self.on_sc_rpm) 129 | self.sc_rpm_min.grid(row=row_id, column=4, padx=(5, 5), sticky=tk.W+tk.E) 130 | 131 | self.sc_rpm_max = tk.Scale(frame, from_=1, to=16383.75, orient=tk.HORIZONTAL, label="Max", 132 | resolution=0.25, state="disabled", variable=self.rpm_var_max, command=self.on_sc_rpm) 133 | self.sc_rpm_max.grid(row=row_id, column=5, padx=(5, 10), sticky=tk.W+tk.E) 134 | 135 | # New row 136 | row_id += 1 137 | 138 | tk.Grid.rowconfigure(frame, row_id, weight=1) 139 | 140 | log_frame = tk.Frame(frame) 141 | log_frame.grid(row=row_id, column=0, columnspan=6, padx=(10, 10), pady=(10, 10), sticky=tk.W+tk.E+tk.N+tk.S) 142 | log_frame.columnconfigure(0, weight=1) 143 | log_frame.rowconfigure(0, weight=1) 144 | 145 | scrollbar = tk.Scrollbar(log_frame) 146 | scrollbar.grid(row=0, column=1, sticky=tk.E+tk.N+tk.S) 147 | 148 | self.logbox = tk.Text(log_frame, background="black", foreground="medium spring green", font="Mono 10", 149 | yscrollcommand=scrollbar.set) 150 | self.logbox.configure(state='normal') 151 | self.logbox.insert(tk.END, "Select interface and press 'Connect' button.\n") 152 | self.logbox.configure(state='disabled') 153 | self.logbox.grid(row=0, column=0, sticky=tk.W+tk.E+tk.N+tk.S) 154 | 155 | scrollbar.config(command=self.logbox.yview) 156 | 157 | # New row 158 | row_id += 1 159 | 160 | buttons_frame = tk.Frame(frame) 161 | buttons_frame.grid(row=row_id, column=0, columnspan=6, sticky=tk.E+tk.S) 162 | 163 | btn_clearlog = tk.Button(buttons_frame, text="Clear log", command=self.clear_log) 164 | btn_clearlog.grid(row=row_id, column=0, padx=(10, 5), pady=(10, 10)) 165 | 166 | btn_savelog = tk.Button(buttons_frame, text="Save log", command=self.save_log) 167 | btn_savelog.grid(row=row_id, column=1, padx=(10, 5), pady=(10, 10)) 168 | 169 | btn_quit = tk.Button(buttons_frame, text="Quit", command=self.close_app) 170 | btn_quit.grid(row=row_id, column=2, padx=(5, 10), pady=(10, 10)) 171 | 172 | def refresh_list(self): 173 | # Get list of CAN devices 174 | devices = self.get_can_devices() 175 | 176 | # Reset menu 177 | menu = self.can_options['menu'] 178 | menu.delete(0, tk.END) 179 | self.can_device_var.set('') 180 | 181 | # Load list of devices to menu 182 | for device in devices: 183 | menu.add_command(label=device, 184 | command=lambda value=device: 185 | self.om_variable.set(value)) 186 | 187 | # Set the first device in the list as a default 188 | if len(devices) > 0: 189 | self.can_device_var.set(devices[0]) 190 | 191 | def can_disconnect(self): 192 | self.can_is_started = False 193 | self.bus.shutdown() 194 | self.h_receiver.join(timeout=90) 195 | if self.h_receiver.is_alive(): 196 | self.add_log("Error: CAN bus thread is not stopped!") 197 | 198 | self.can_options['state'] = 'normal' 199 | self.connect['state'] = 'normal' 200 | self.disconnect['state'] = 'disabled' 201 | 202 | self.event.clear() 203 | self.add_log('Bus {:s} is disconnected'.format(self.can_device_var.get())) 204 | 205 | def can_connect(self): 206 | if self.can_device_var.get() == '': 207 | messagebox.showwarning(message="CAN interface is not available or selected") 208 | return 209 | 210 | self.bus = can.interface.Bus(bustype='socketcan', channel=self.can_device_var.get()) 211 | if self.bus is None: 212 | self.add_log('Bus {:s} cannot be connected'.format(self.can_device_var.get())) 213 | return 214 | 215 | self.h_receiver = threading.Thread(target=self.receive_all) 216 | self.h_receiver.start() 217 | 218 | self.can_is_started = True 219 | self.disconnect['state'] = 'normal' 220 | self.connect['state'] = 'disabled' 221 | self.can_options['state'] = 'disabled' 222 | 223 | self.event.set() 224 | self.add_log('Bus {:s} is connected'.format(self.can_device_var.get())) 225 | 226 | def add_log(self, message): 227 | self.logbox.configure(state='normal') 228 | # Add timestamp to message 229 | mpt = '{:%Y.%m.%d-%H:%M:%S.%f}: {:s}\n'.format(datetime.utcnow(), message) 230 | self.logbox.insert(tk.END, mpt) 231 | self.logbox.configure(state='disabled') 232 | self.logbox.see(tk.END) 233 | 234 | def clear_log(self): 235 | self.logbox.configure(state='normal') 236 | self.logbox.delete(1.0, tk.END) 237 | self.logbox.configure(state='disabled') 238 | 239 | def save_log(self): 240 | files = [ 241 | ('Logs', '*.log'), 242 | ('All Files', "*.*"), 243 | ('Text files', '*.txt')] 244 | file_handler = filedialog.asksaveasfile(title = "Save log", defaultextension=".log", filetypes=files) 245 | if file_handler is None: 246 | # Looks like "Cancel" button is pressed 247 | return 248 | 249 | print(file_handler) 250 | file_handler.write(str(self.logbox.get(1.0, tk.END))) 251 | file_handler.close() 252 | 253 | def on_sc_speed(self, val): 254 | if self.speed_var_min.get() > self.speed_var_max.get(): 255 | self.speed_var_max.set(self.speed_var_min.get() + 1) 256 | 257 | def on_cb_speed_auto(self): 258 | if self.speed_var_auto.get() != True: 259 | self.sc_speed_min['state'] = "disabled" 260 | self.sc_speed_max['state'] = "disabled" 261 | self.sc_speed['state'] = "normal" 262 | else: 263 | self.sc_speed_min['state'] = "normal" 264 | self.sc_speed_max['state'] = "normal" 265 | self.sc_speed['state'] = "disabled" 266 | pass 267 | 268 | def on_sc_rpm(self, val): 269 | if self.rpm_var_min.get() > self.rpm_var_max.get(): 270 | self.rpm_var_max.set(self.rpm_var_min.get() + 1) 271 | 272 | def on_cb_rpm_auto(self): 273 | if self.rpm_var_auto.get() != True: 274 | self.sc_rpm_min['state'] = "disabled" 275 | self.sc_rpm_max['state'] = "disabled" 276 | self.sc_rpm['state'] = "normal" 277 | else: 278 | self.sc_rpm_min['state'] = "normal" 279 | self.sc_rpm_max['state'] = "normal" 280 | self.sc_rpm['state'] = "disabled" 281 | pass 282 | 283 | def service1(self, msg): 284 | if msg.data[2] == 0x00: 285 | log.debug(">> Caps") 286 | msg = can.Message(arbitration_id=0x7e8, 287 | data=[0x06, 0x41, 0x00, 0x18, 0x3B, 0x80, 0x00], 288 | is_extended_id=False) 289 | self.bus.send(msg) 290 | elif msg.data[2] == 0x04: 291 | log.debug(">> Calculated engine load") 292 | msg = can.Message(arbitration_id=0x7e8, 293 | data=[0x03, 0x41, 0x04, 0x20], 294 | is_extended_id=False) 295 | self.bus.send(msg) 296 | elif msg.data[2] == 0x05: 297 | log.debug(">> Engine coolant temperature") 298 | msg = can.Message(arbitration_id=0x7e8, 299 | data=[0x03, 0x41, 0x05, randint(88 + 40, 95 + 40)], 300 | is_extended_id=False) 301 | self.bus.send(msg) 302 | elif msg.data[2] == 0x0B: 303 | log.debug(">> Intake manifold absolute pressure") 304 | msg = can.Message(arbitration_id=0x7e8, 305 | data=[0x04, 0x41, 0x0B, randint(10, 40)], 306 | is_extended_id=False) 307 | self.bus.send(msg) 308 | elif msg.data[2] == 0x0C: 309 | log.debug(">> RPM") 310 | 311 | if self.rpm_var_auto.get(): 312 | val = randint(self.rpm_var_min.get(), self.rpm_var_max.get()) 313 | else: 314 | val = self.rpm_var.get() 315 | 316 | val *= 4 317 | valA = int(val / 256) 318 | valB = int(val - valA*256) 319 | 320 | msg = can.Message(arbitration_id=0x7e8, 321 | data=[0x04, 0x41, 0x0C, valA, valB], 322 | is_extended_id=False) 323 | self.bus.send(msg) 324 | elif msg.data[2] == 0x0D: 325 | log.debug(">> Speed") 326 | 327 | if self.speed_var_auto.get(): 328 | val = randint(self.speed_var_min.get(), self.speed_var_max.get()) 329 | else: 330 | val = self.speed_var.get() 331 | 332 | msg = can.Message(arbitration_id=0x7e8, 333 | data=[0x03, 0x41, 0x0D, val], 334 | is_extended_id=False) 335 | self.bus.send(msg) 336 | elif msg.data[2] == 0x0F: 337 | log.debug(">> Intake air temperature") 338 | msg = can.Message(arbitration_id=0x7e8, 339 | data=[0x03, 0x41, 0x0F, randint(60, 64)], 340 | is_extended_id=False) 341 | self.bus.send(msg) 342 | elif msg.data[2] == 0x10: 343 | log.debug(">> MAF air flow rate") 344 | msg = can.Message(arbitration_id=0x7e8, 345 | data=[0x04, 0x41, 0x10, 0x00, 0xFA], 346 | is_extended_id=False) 347 | self.bus.send(msg) 348 | elif msg.data[2] == 0x11: 349 | log.debug(">> Throttle position") 350 | msg = can.Message(arbitration_id=0x7e8, 351 | data=[0x03, 0x41, 0x11, randint(20, 60)], 352 | is_extended_id=False) 353 | self.bus.send(msg) 354 | elif msg.data[2] == 0x33: 355 | log.debug(">> Absolute Barometric Pressure") 356 | msg = can.Message(arbitration_id=0x7e8, 357 | data=[0x03, 0x41, 0x33, randint(20, 60)], 358 | is_extended_id=False) 359 | self.bus.send(msg) 360 | else: 361 | self.add_log('Service 1, unknown PID=0x{:02x}'.format(msg.data[2])) 362 | 363 | def service9(self, msg): 364 | if msg.data[2] == 0x02: 365 | log.debug(">> VIN code") 366 | msg = can.Message(arbitration_id=0x7e8, 367 | data=[0x10, 0x14, 0x49, 0x02, 0x01, 0x33, 0x46, 0x41], 368 | is_extended_id=False) 369 | self.bus.send(msg) 370 | # 371 | # XXX: Need to be designed and implemented correct handling for "continue" request: 372 | # 7E0 [8] 30 00 00 00 00 00 00 00 373 | # 374 | # Right now we just sending all VIN code, i.e. without hand-shaking - that is not good 375 | # 376 | # Also, here hardcoded VIN of some unknown Ford and it need to be replaced with editable entry 377 | # 378 | msg = can.Message(arbitration_id=0x7e8, 379 | data=[0x21, 0x44, 0x50, 0x34, 0x46, 0x4A, 0x32, 0x42], 380 | is_extended_id=False) 381 | self.bus.send(msg) 382 | msg = can.Message(arbitration_id=0x7e8, 383 | data=[0x22, 0x4D, 0x31, 0x31, 0x33, 0x39, 0x31, 0x33], 384 | is_extended_id=False) 385 | self.bus.send(msg) 386 | else: 387 | self.add_log('Service 9, unknown PID=0x{:02x}'.format(msg.data[2])) 388 | 389 | def receive_all(self): 390 | self.event.wait() 391 | 392 | while self.can_is_started: 393 | msg = self.bus.recv(1) 394 | # Just skip 'bad' messages 395 | if msg is None: 396 | continue 397 | 398 | if msg.arbitration_id != 0x7df: 399 | self.add_log('Unknown Id 0x{:03x}'.format(msg.arbitration_id)) 400 | continue 401 | 402 | if msg.data[1] == 0x01: 403 | self.service1(msg) 404 | elif msg.data[1] == 0x09: 405 | self.service9(msg) 406 | else: 407 | self.add_log('Service {:d} is not supported'.format(msg.data[1])) 408 | 409 | if __name__ == "__main__": 410 | try: 411 | opts, args = getopt.getopt(sys.argv[1:], "l:v", ["loglevel="]) 412 | except getopt.GetoptError as err: 413 | # print help information and exit: 414 | print(err) # will print something like "option -a not recognized" 415 | usage() 416 | sys.exit(2) 417 | 418 | loglevel = "INFO" 419 | 420 | for o, a in opts: 421 | if o == "-v": 422 | loglevel = "DEBUG" 423 | elif o in ("-l", "--loglevel"): 424 | loglevel = a 425 | elif o in ("-h", "--help"): 426 | usage() 427 | sys.exit() 428 | else: 429 | assert False, "unhandled option" 430 | 431 | numeric_level = getattr(log, loglevel.upper(), None) 432 | if not isinstance(numeric_level, int): 433 | raise ValueError('Invalid log level: %s' % loglevel) 434 | log.basicConfig(level=numeric_level) 435 | 436 | window = tk.Tk() 437 | window.title("ECU Simulator") 438 | app = Application(master=window) 439 | app.mainloop() 440 | --------------------------------------------------------------------------------