├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── isobus ├── __init__.py ├── bin │ ├── __init__.py │ └── vtclient.py ├── cf.py ├── common.py ├── constants.py ├── ibsinterface.py ├── log.py └── vt │ ├── __init__.py │ ├── client.py │ └── interface.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | iop/* 2 | scripts/* 3 | tags 4 | *.log 5 | *.swp 6 | *.pyc 7 | *-test.py 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 jboomer 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | python-isobus 2 | ============= 3 | 4 | Implementation of the ISOBUS (ISO11783) standard in Python. Currently only the VT client control function is implemented. 5 | 6 | Installation 7 | ------------------------- 8 | Using pip, this will also install dependencies if necessary: 9 | 10 | :: 11 | 12 | pip install git+git://github.com/jboomer/python-isobus.git 13 | 14 | This will also install a command line tool: vtclient 15 | 16 | Alternatively, clone this repo and run 17 | 18 | :: 19 | 20 | setup.py install 21 | 22 | 23 | TODO 24 | ---- 25 | - VTClient : Notifier for activation messages 26 | - VTClient : Implement aux client (aux maintenance) 27 | - Make common 'ISOBUS CF' class w/ address claim etc. 28 | - Implement receiving TP session 29 | - Implement BAM 30 | - Unit tests! 31 | - VTClient : List connected VTs 32 | - VTClient : Get versions command 33 | - Test w/Windows 34 | - comments in scripts 35 | - VTClient : GUI interface w/ pygtk or pyQT 36 | - Use sphinx documentation for API 37 | - VTClient : Read aliases from file, to be distributed with pool? 38 | -------------------------------------------------------------------------------- /isobus/__init__.py: -------------------------------------------------------------------------------- 1 | """ ISOBUS description """ 2 | 3 | # isobus imports: 4 | from isobus.vt.client import VTClient 5 | from isobus.common import IBSException 6 | -------------------------------------------------------------------------------- /isobus/bin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboomer/python-isobus/da09e6ecaf27a540a87d19319e4bdeb36685aea3/isobus/bin/__init__.py -------------------------------------------------------------------------------- /isobus/bin/vtclient.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import time #sleep 4 | import cmd #shell 5 | import sys #argv 6 | import argparse 7 | import os 8 | import glob 9 | 10 | import isobus 11 | 12 | 13 | class InputNumber(): 14 | """ Interpret a number from string, 0x or 0X prefix is hex. 15 | May throw ValueError 16 | """ 17 | def __init__(self, fromtext, aliases=None): 18 | if aliases is not None and fromtext in aliases.keys(): 19 | self.value = aliases[fromtext] 20 | else: 21 | if fromtext.startswith('0x') or fromtext.startswith('0X'): 22 | self.value = int(fromtext, 16) 23 | else: 24 | self.value = int(fromtext) 25 | 26 | def _append_slash_if_dir(p): 27 | if p and os.path.isdir(p) and p[-1] != os.sep: 28 | return p + os.sep 29 | else: 30 | return p 31 | 32 | 33 | class VTClientShell(cmd.Cmd) : 34 | """ CLI for the VT Connection """ 35 | intro = 'VT Client CLI, type help or ? to list commands' 36 | prompt = 'vtclient> ' 37 | file = None 38 | aliases = dict() 39 | 40 | def do_setalias(self, args): 41 | 'Set an alias for an object ID or source address: setalias KEY VALUE' 42 | arglist = args.split() 43 | if len(arglist) == 2: 44 | try: 45 | self.aliases[arglist[0]] = InputNumber(arglist[1]).value 46 | except ValueError: 47 | print('Syntax error') 48 | else: 49 | print('{0} arguments expected, {1} given'.format(2, len(arglist))) 50 | 51 | def do_setsrc(self, sa): 52 | 'Sets the source address to use (0x0A is default): setsrc SA' 53 | if not vtClient.alive: 54 | try: 55 | vtClient.SetSrc(InputNumber(sa).value) 56 | except ValueError: 57 | print('Please enter a source address such as 0x0A or 38') 58 | else: 59 | print('Disconnect before changing source address') 60 | 61 | def do_setfi(self, fi): 62 | 'Sets the function instance to use (0x00 is default): setfi FUNCTIONINSTANCE' 63 | try: 64 | vtClient.functionInstance = InputNumber(fi).value 65 | except ValueError: 66 | print('Please enter a function instance such as 0x01 or 3') 67 | 68 | 69 | def do_loadver(self, version): 70 | 'Load a version of the working set: loadver ABC1234' 71 | try: 72 | vtClient.LoadVersion(version) 73 | except isobus.IBSException as e: 74 | print('Error: {reason}'.format(reason=e.args[0])) 75 | 76 | def do_storever(self, version): 77 | 'Store a version of the working set: storever ABC1234' 78 | if len(version) == 7: 79 | try: 80 | vtClient.StoreVersion(version) 81 | except isobus.IBSException as e: 82 | print('Error: {reason}'.format(reason=e.args[0])) 83 | else: 84 | print('Version string is not 7 characters') 85 | 86 | def do_poolup(self, filename): 87 | 'Upload a complete pool from file: poolup FILENAME.IOP' 88 | try: 89 | with open(filename, "rb") as iopfile: 90 | try: 91 | vtClient.UploadPoolData([byte for byte in iopfile.read()]) 92 | except isobus.IBSException as e: 93 | print('Error: {reason}'.format(reason=e.args[0])) 94 | except FileNotFoundError: 95 | print('File not found') 96 | except IsADirectoryError: 97 | print('{fName} is a directory'.format(fName=filename)) 98 | 99 | def do_partpool(self, filename): 100 | 'Upload part of a pool (does not send EoOP): poolpart FILENAME.IOP' 101 | try: 102 | with open(filename, "rb") as iopfile: 103 | try: 104 | vtClient.UploadPoolData([byte for byte in iopfile.read()], False) 105 | except isobus.IBSException as e: 106 | print('Error: {reason}'.format(reason=e.args[0])) 107 | except FileNotFoundError: 108 | print('File not found') 109 | 110 | def do_delpool(self, arg): 111 | 'Delete object pool from volatile memory' 112 | try: 113 | vtClient.DeleteObjectPool() 114 | except isobus.IBSException as e: 115 | print('Error: {reason}'.format(reason=e.args[0])) 116 | 117 | def do_connect(self, vtsa): 118 | 'Connect to a VT: connect SA ' 119 | try: 120 | vtClient.ConnectToVT(InputNumber(vtsa).value) 121 | print('Connected to VT with sa {sa}'.format(sa = vtsa)) 122 | except ValueError: 123 | print("Please enter a source address such as 0x0A or 38") 124 | except isobus.IBSException: 125 | print('Failed to connect to VT with sa {sa}'.format(sa=vtsa)) 126 | 127 | def do_disconnect(self, arg): 128 | 'Disconnect From a VT: disconnect' 129 | vtClient.DisconnectFromVT() 130 | 131 | def do_chgmask(self, args): 132 | 'Send a Change Mask command: chgmask WSID MASKID' 133 | try: 134 | vtClient.ChangeActiveMask(*self._get_int_args(args)) 135 | except ValueError: 136 | print('Syntax error') 137 | except isobus.IBSException as e: 138 | print('Error: {reason}'.format(reason=e.args[0])) 139 | 140 | def do_chgskmask(self, args): 141 | 'Send a Change SK Mask command: chgskmask MASKID SOFTKEYMASKID' 142 | try: 143 | arglist = self._get_int_args(args) 144 | if len(arglist) == 2: 145 | try: 146 | vtClient.ChangeSKMask(*arglist, alarm=False) 147 | except isobus.IBSException as e: 148 | print('Error: {reason}'.format(reason=e.args[0])) 149 | else: 150 | print('Syntax error') 151 | except ValueError: 152 | print('Syntax error') 153 | 154 | def do_chgattr(self, args): 155 | 'Send a change attribute command: chgattr OBJID ATTRID VALUE' 156 | arglist = self._get_int_args(args) 157 | if len(arglist) == 3: 158 | try: 159 | vtClient.ChangeAttribute(*arglist) 160 | except ValueError: 161 | print('Syntax error') 162 | except isobus.IBSException as e: 163 | print('Error: {reason}'.format(reason=e.args[0])) 164 | else: 165 | print('Syntax error') 166 | 167 | def do_chgnumval(self, args): 168 | 'Send a change numeric value command: chgnumval OBJID VALUE' 169 | try: 170 | arglist = self._get_int_args(args) 171 | if len(arglist) == 2: 172 | try: 173 | vtClient.ChangeNumericValue(*arglist) 174 | except isobus.IBSException as e: 175 | print('Error: {reason}'.format(reason=e.args[0])) 176 | else: 177 | print('{0} arguments expected, {1} given'.format(2, len(arglist))) 178 | except ValueError: 179 | print('Invalid syntax') 180 | 181 | def do_chgstrval(self, args): 182 | 'Send a change string value command: chgstrval OBJID VALUE' 183 | arglist = args.split() 184 | if len(arglist) == 2: 185 | try: 186 | objid = InputNumber(arglist[0], self.aliases).value 187 | try: 188 | vtClient.ChangeStringValue(objid, arglist[1]) 189 | except isobus.IBSException as e: 190 | print('Error: {reason}'.format(reason=e.args[0])) 191 | except ValueError: 192 | print('Invalid syntax') 193 | else: 194 | print('Invalid syntax : expects 2 arguments') 195 | 196 | def do_chglistitem(self, args): 197 | 'Send a change list item command: chglistitem OBJID INDEX VALUE' 198 | try: 199 | arglist = self._get_int_args(args) 200 | if len(arglist) == 3: 201 | try: 202 | vtClient.ChangeListItem(*arglist) 203 | except isobus.IBSException as e: 204 | print('Error: {reason}'.format(reason=e.args[0])) 205 | else: 206 | print('{0} arguments expected, {1} given'.format(3, len(arglist))) 207 | except ValueError: 208 | print('Invalid syntax') 209 | 210 | def do_esc(self, arg): 211 | 'Send an ESC command to the VT, to escape user input: esc' 212 | try: 213 | ret = vtClient.ESCInput() 214 | print('ESC object 0x{objid:04X}'.format(objid=ret)) 215 | except isobus.IBSException as e: 216 | print('Error: {reason}'.format(reason=e.args[0])) 217 | 218 | def do_identify(self, arg): 219 | 'Send a message to all VTs to display (propietary) identification means' 220 | vtClient.IdentifyVTs() 221 | 222 | def do_sleep(self, arg): 223 | 'Sleep a few seconds (for scripts): sleep SECONDS(float)' 224 | try: 225 | time.sleep(float(arg)) 226 | except ValueError: 227 | print('Syntax error') 228 | 229 | def do_exit(self, arg): 230 | 'Exit the CLI: exit' 231 | print("Exiting...") 232 | return True 233 | 234 | def complete_poolup(self, text, line, begidx, endidx): 235 | return self._tab_complete_filepath(text, line, begidx, endidx) 236 | 237 | def complete_partpool(self, text, line, begidx, endidx): 238 | return self._tab_complete_filepath(text, line, begidx, endidx) 239 | 240 | def _tab_complete_filepath(self, text, line, begidx, endidx): 241 | before_arg = line.rfind(" ", 0, begidx) 242 | if before_arg == -1: 243 | return # arg not found 244 | 245 | fixed = line[before_arg+1:begidx] # fixed portion of the arg 246 | arg = line[before_arg+1:endidx] 247 | pattern = arg + '*' 248 | 249 | completions = [] 250 | for path in glob.glob(pattern): 251 | path = _append_slash_if_dir(path) 252 | completions.append(path.replace(fixed, "", 1)) 253 | return completions 254 | 255 | 256 | def _get_int_args(self, args): 257 | return [InputNumber(x, self.aliases).value for x in args.split()] 258 | 259 | # Parse command line arguments 260 | parser = argparse.ArgumentParser(description = 'CLI for a VT Client') 261 | parser.add_argument('-s', '--script' 262 | , help='Run a script instead of the interactive CL') 263 | parser.add_argument('-i', '--interface' 264 | , default='socketcan_native' 265 | , choices=['pcan', 'socketcan_native', 'socketcan_ctypes'] 266 | , help='Interface, default=socketcan_native(only in Python3!)') 267 | parser.add_argument('-c', '--channel' 268 | , default='vcan0' 269 | , help='Channel/bus name, default=vcan0') 270 | args = parser.parse_args() 271 | 272 | # Construct global objects 273 | vtClient = isobus.VTClient(interface = args.interface, channel = args.channel) 274 | vtClient.SetSrc(0x0A) # Default source address 275 | 276 | def main(): 277 | shell = VTClientShell() 278 | if args.script is not None: 279 | with open(args.script, 'r') as script: 280 | for line in script.readlines(): 281 | shell.onecmd(line) 282 | input('Done!') 283 | else : 284 | try: 285 | shell.cmdloop() 286 | except KeyboardInterrupt: 287 | print('Exiting...') 288 | 289 | if __name__ == "__main__": 290 | main() 291 | -------------------------------------------------------------------------------- /isobus/cf.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | 4 | from isobus.ibsinterface import IBSInterface 5 | from isobus.constants import * 6 | 7 | def BuildISOBUSName(**kwargs): 8 | # Set the default values TODO: Check these values 9 | settings = dict( configurable = 0x0 10 | , industryGroup = 0x2 11 | , deviceClassInstance = 0X0 12 | , deviceClass = 0x0 13 | , function = FUNCTION_VT 14 | , functionInstance = 0x0 15 | , ECUInstance = 0x0 16 | , manufacturerCode = 0x00 17 | , idNumber = 0x01FF 18 | ) 19 | for setting in settings.keys(): 20 | if setting in kwargs.keys(): 21 | settings[setting] = kwargs[setting] 22 | 23 | ibsName =( (settings['configurable'] << 63) 24 | | (settings['industryGroup'] << 60) 25 | | (settings['deviceClassInstance'] << 56) 26 | | (settings['deviceClass'] << 49) 27 | | (settings['function'] << 40) 28 | | (settings['functionInstance'] << 35) 29 | | (settings['ECUInstance'] << 32) 30 | | (settings['manufacturerCode'] << 21) 31 | | (settings['idNumber'] << 0 )) 32 | return ibsName 33 | 34 | class IBSControlFunction(): 35 | """Generic ISOBUS node""" 36 | 37 | # Return True or False on commands based on success 38 | 39 | def __init__(self, interface, channel) : 40 | self.connection = IBSInterface(interface, channel) 41 | self.sa = 0xFE 42 | self.functionInstance = 0x00 43 | 44 | def ClaimAddress(self, sa, ibsName): 45 | # TODO: Check if SA is not already claimed 46 | # TODO: Handle configurable address? 47 | self.connection.SendRequestAddressClaim(sa) 48 | waittime = 250 + (random.randint(0, 255) * 0.6) 49 | time.sleep(waittime / 1000.0) 50 | self.connection.SendAddressClaim(ibsName, sa) 51 | time.sleep(0.250) 52 | 53 | #TODO: Implement PART 12 here? 54 | 55 | -------------------------------------------------------------------------------- /isobus/common.py: -------------------------------------------------------------------------------- 1 | class IBSException(Exception): 2 | pass 3 | 4 | #TODO: This doesn't make sense for negative numbers, should they even be allowed? 5 | class NumericValue(): 6 | """ To store a number which can be read to/from LE or BE """ 7 | def __init__(self, value): 8 | self.value = value 9 | 10 | @classmethod 11 | def FromLEBytes(cls, databytes): 12 | """ Construct from array of bytes in LE order """ 13 | value = sum([(databytes[n] << n*8) for n in range(len(databytes))]) 14 | return cls(value) 15 | 16 | @classmethod 17 | def FromBEBytes(cls, databytes): 18 | """ Construct from array of bytes in BE order """ 19 | value = sum([(databytes[n] << n*8) for n in reversed(range(len(databytes)))]) 20 | return cls(value) 21 | 22 | def AsLEBytes(self, nBytes = 4): 23 | """ Returns a list of nBytes bytes in Little-Endian order """ 24 | return [(self.value >> (n * 8) & 0xFF) for n in range(nBytes)] 25 | 26 | def AsBEBytes(self, nBytes = 4): 27 | """ Returns a list of nBytes bytes in Big-Endian order """ 28 | return [(self.value >> (n * 8) & 0xFF) for n in reversed(range(nBytes))] 29 | 30 | def AsString(self): 31 | """ Returns the value as a string with hex and decimal representation""" 32 | return '0x{value1:08X} ({value2})'.format(value1 = self.value, value2 = self.value) 33 | 34 | def Value(self): 35 | return self.value 36 | 37 | class IBSID(): 38 | """ Represents a CAN ID in ISOBUS communication """ 39 | def __init__(self, da, sa, pgn, prio=6): 40 | self.da = da 41 | self.sa = sa 42 | self.pgn = pgn 43 | self.prio = prio 44 | 45 | def GetCANID(self): 46 | """ Return the CAN ID as a 29 bit identifier """ 47 | canid = 0 48 | if ((self.pgn >> 8) & 0xFF) < 0xEF: 49 | # PDU1 50 | canid = (((self.prio & 0x7) << 26) 51 | | ((self.pgn & 0xFF00) << 8) 52 | | ((self.da & 0xFF) << 8) 53 | | (self.sa & 0xFF)) 54 | else : 55 | # PDU2 56 | canid = (((self.prio & 0x7) << 26) 57 | | ((self.pgn & 0xFFFF) << 8) 58 | | (self.sa & 0xFF)) 59 | 60 | return canid 61 | 62 | @classmethod 63 | def FromCANID(cls, canid): 64 | """ Get values from 29 bit identifier """ 65 | prio = (canid >> 26) & 0x7 66 | sa = canid & 0xFF 67 | 68 | pgn = 0 69 | da = 0xFF 70 | 71 | if ((canid >> 16) & 0xFF) <= 0xEF: 72 | #Destination specific 73 | pgn = (canid >> 8) & 0xFF00 74 | da = (canid >> 8) & 0xFF 75 | else: 76 | #Broadcast 77 | pgn = (canid >> 8) & 0xFFFF 78 | 79 | return cls(da, sa, pgn, prio) 80 | -------------------------------------------------------------------------------- /isobus/constants.py: -------------------------------------------------------------------------------- 1 | # Data bytes 2 | RESERVED = 0xFF 3 | NA = 0xFE 4 | 5 | # Source addresses 6 | SA_GLOBAL = 0xFF 7 | SA_NULL = 0xFE 8 | 9 | # PGNs 10 | PGN_ECU2VT = 0xE700 11 | PGN_VT2ECU = 0xE600 12 | PGN_REQUEST = 0xEA00 13 | PGN_ADDRCLAIM = 0xEE00 14 | PGN_TP_CM = 0xEC00 15 | PGN_TP_DT = 0xEB00 16 | PGN_ETP_CM = 0xC800 17 | PGN_ETP_DT = 0xC700 18 | 19 | #ISOBUS NAME 20 | FUNCTION_VT = 0x3E 21 | -------------------------------------------------------------------------------- /isobus/ibsinterface.py: -------------------------------------------------------------------------------- 1 | import can 2 | import math # ceil 3 | import time #sleep 4 | 5 | from isobus.common import NumericValue 6 | from isobus.common import IBSID 7 | from isobus.common import IBSException 8 | from isobus.constants import * 9 | from isobus.log import log 10 | 11 | from can.interfaces.interface import Bus 12 | 13 | class IBSRxHandler(): 14 | 15 | def __init__(self, pgns): 16 | self.pgnlist = pgns 17 | 18 | def RxMessage(ibsid, data): 19 | raise NotImplemented('Rx handler not implemented!') 20 | 21 | 22 | class IBSInterface(can.Listener): 23 | """ This class defines the methods for a minimal ISOBUS CF. 24 | This means address claiming procedures (part 5) and diagnostics (part 12). 25 | Other interfaces inherit from this class to implement specific parts, such as 26 | IBSVTInterface (part 6). 27 | """ 28 | 29 | def __init__(self, interface, channel): 30 | can.rc['interface'] = interface 31 | can.rc['channel'] = channel 32 | log.info('Opening CAN connection on {0}'.format( 33 | can.rc['channel'])) 34 | self.bus = Bus(bitrate=250000) 35 | self.periodic_tasks = list() 36 | self._rxHandlers = list() 37 | 38 | #Have to use a separate bus object, otherwise WaitForIsobusmessage does not work 39 | #self.notifier = can.Notifier(Bus(), [self]) 40 | 41 | def __del__(self): 42 | self.bus.shutdown() 43 | 44 | def on_message_received(self, mesg): 45 | #TODO: Check 'listening' flag, if listening, store in queue 46 | # Change 'WaitForIBSMessage' accordingly, so we don't have to call recv on bus 47 | # Then we need a 'Start listening' and 'StopListening' + flushqueue method 48 | 49 | ibsid = IBSID.FromCANID(mesg.arbitration_id) 50 | log.debug('Rx Mesg PGN {pgn:04X} SA {sa:02X} DA {da:02X}'.format( 51 | pgn=ibsid.pgn, sa=ibsid.sa, da=ibsid.da)) 52 | 53 | #TODO: Smarter filters, SA, DA, PGN, muxbyte? 54 | for handler in self._rxHandlers: 55 | if pgn in handler.pgnlist: 56 | handler.RxMessage(ibsid, mesg.data) 57 | 58 | def AddRxHandler(self, handler): 59 | self._rxHandlers.append(handler) 60 | 61 | 62 | def AddPeriodicMessage(self, ibsid, contents, period): 63 | # For socketcan_native, bit 32 (MSb) needs to be set for extended ID 64 | # Is fixed in latest python-can though! 65 | log.debug('Adding periodic message ID : 0x{mesgid:08X} period {T}'.format( 66 | mesgid = ibsid.GetCANID(), T=period)) 67 | msg = can.Message(arbitration_id=(ibsid.GetCANID() | (1 << 31)), 68 | data=contents, 69 | extended_id=True) 70 | self.periodic_tasks.append(can.send_periodic(can.rc['channel'], msg, period)) 71 | 72 | def StopPeriodicMessage(self, ibsid): 73 | # For socketcan_native, bit 32 (MSb) needs to be set for extended ID 74 | # Is fixed in latest python-can though! 75 | for periodMsg in self.periodic_tasks: 76 | if periodMsg.can_id == (ibsid.GetCANID() | (1 << 31)): 77 | log.debug('Stopping periodic message ID : 0x{mesgid:08X}'.format( 78 | mesgid = ibsid.GetCANID())) 79 | self.periodic_tasks.remove(periodMsg) 80 | periodMsg.stop() 81 | break 82 | 83 | def ModifyPeriodicMessage(self, ibsid, newContent): 84 | # For socketcan_native, bit 32 (MSb) needs to be set for extended ID 85 | # Is fixed in latest python-can though! 86 | msg = can.Message(arbitration_id=(ibsid.GetCANID() | (1 << 31)), 87 | data=newContent, 88 | extended_id=True) 89 | for periodMsg in self.periodic_task: 90 | if periodMsg.can_id == canid: 91 | periodMsg.modify_data(self, msg) 92 | break 93 | 94 | def SendRequestAddressClaim(self, sa): 95 | log.debug('Sending Request Address Claim') 96 | self.SendRequest(sa, da=SA_GLOBAL, reqPGN=PGN_ADDRCLAIM) 97 | 98 | def SendAddressClaim(self, ibsName, sa): 99 | log.debug('Sending Address claim for name {:016X}'.format( 100 | ibsName)) 101 | candata = NumericValue(ibsName).AsLEBytes(8) 102 | self._SendIBSMessage(PGN_ADDRCLAIM, SA_GLOBAL, sa, candata) 103 | 104 | def SendRequest(self, sa, da, reqPGN): 105 | self._SendIBSMessage(PGN_REQUEST, sa, da, NumericValue(reqPGN).AsLEBytes(3)) 106 | 107 | ## PROTECTED FUNCTIONS 108 | def _SendCANMessage(self, canid, candata): 109 | if len(candata) <= 8: 110 | msg = can.Message(arbitration_id=canid, 111 | data=candata, 112 | extended_id=True) 113 | try: 114 | self.bus.send(msg) 115 | except can.CanError: 116 | log.warning('Error sending message') 117 | 118 | def _WaitForIBSMessage(self, pgn, fromsa, tosa, muxByte, maxtime=3.0): 119 | # TODO: Also accept incoming TP session 120 | # TODO: Can we miss messages because we start listening too late? 121 | 122 | received = False 123 | data = [RESERVED] * 8 # Dummy data for when nothing is received 124 | starttime = time.time() 125 | matchID = IBSID(da = tosa, sa = fromsa, pgn = pgn, prio = 6) 126 | while not(received): 127 | mesg = self.bus.recv(0.5) 128 | if mesg is not None: 129 | receivedID = IBSID.FromCANID(mesg.arbitration_id) 130 | if (receivedID.pgn == matchID.pgn 131 | and receivedID.da == matchID.da 132 | and receivedID.sa == matchID.sa 133 | and mesg.data[0] == muxByte): 134 | received= True 135 | data = mesg.data 136 | break 137 | if (time.time() - starttime) > maxtime: 138 | log.debug('Timeout waiting for CAN ID {canid:08X}'.format(canid=matchID.GetCANID())) 139 | break 140 | 141 | return received, data 142 | 143 | def _SendIBSMessage(self, pgn, da, sa, data, prio=6): 144 | if len(data) <= 8: 145 | canid = IBSID(da, sa, pgn, prio).GetCANID() 146 | self._SendCANMessage(canid, data) 147 | elif len(data) <= 1785: 148 | self._SendTPMessage(pgn, da, sa, data) 149 | elif len(data) <= 117440505: 150 | self._SendETPMessage(pgn, da, sa, data) 151 | else: 152 | log.warning('ERROR : CAN message too large to send') 153 | 154 | def _SendTPMessage(self, pgn, da, sa, data): 155 | log.debug('(TP) Request starting TP for {n} bytes'.format(n=len(data))) 156 | tpcm_id = IBSID(da, sa, pgn=PGN_TP_CM, prio=6) 157 | tpdt_id = IBSID(da, sa, pgn=PGN_TP_DT, prio=7) 158 | 159 | # Send RTS 160 | rts_control = 0x10 161 | nr_of_packets = int(math.ceil(len(data) / 7.0)) 162 | rts_data = ([rts_control] 163 | + NumericValue(len(data)).AsLEBytes(2) 164 | + [nr_of_packets, RESERVED] 165 | + NumericValue(pgn).AsLEBytes(3)) 166 | 167 | log.debug('(TP) Sending RTS for PGN {0} : {1} bytes in {2} packets'.format( 168 | pgn, len(data), nr_of_packets)) 169 | self._SendCANMessage(tpcm_id.GetCANID(), rts_data) 170 | 171 | # Check the CTS 172 | #FIXME: Only send min(nrOfPackets,maxPackets), what to do if less? 173 | [received, ctsdata] = self._WaitForIBSMessage(0xEC00, da, sa, 0x11) 174 | if received: 175 | log.debug('(TP) Received CTS for max {0} packets, next packet {1}'.format( 176 | ctsdata[1], ctsdata[2])) 177 | 178 | else: 179 | return False 180 | 181 | 182 | # Pack with 0xFF 183 | if len(data) % 7 > 0: 184 | data = data + list([RESERVED] * (7 - (len(data) % 7))) 185 | 186 | 187 | # Send bytes 188 | for seqN in range(nr_of_packets): 189 | log.debug('(TP) Send package {n}'.format(n=seqN + 1)) 190 | startByte = seqN * 7 191 | self._SendCANMessage(tpdt_id.GetCANID(), [seqN + 1] + data[startByte:startByte + 7]) 192 | # sleep 1 msec, otherwise hardware buffer gets full! 193 | time.sleep(0.001) 194 | 195 | 196 | def _SendETPMessage(self, pgn, da, sa, data): 197 | log.debug('(ETP) Request starting ETP for {n} bytes'.format(n=len(data))) 198 | etpcm_id = IBSID(da, sa, PGN_ETP_CM, prio=6) 199 | etpdt_id = IBSID(da, sa, PGN_ETP_DT, prio=7) 200 | 201 | mesg_size = len(data) 202 | 203 | # Send RTS 204 | rts_control = 0x14 205 | totalPackets = int(math.ceil(len(data) / 7.0)) 206 | 207 | log.debug("(ETP) Sending {0} bytes in {1} packets".format( 208 | len(data), totalPackets)) 209 | 210 | rts_data = ([rts_control] 211 | + NumericValue(mesg_size).AsLEBytes(4) 212 | + NumericValue(pgn).AsLEBytes(3) 213 | ) 214 | self._SendCANMessage(etpcm_id.GetCANID(), rts_data) 215 | 216 | # Pack data with 0xFF to multiple of 7 217 | if len(data) % 7 > 0: 218 | data = data + list([RESERVED] * (7 - (len(data) % 7))) 219 | 220 | 221 | # Setup for the data transfer 222 | nextPacket = 1 223 | maxSentPackets = 0 224 | done = False 225 | 226 | while not(done): 227 | # TODO: What is the time out for this one? 228 | [received, ctsdata] = self._WaitForIBSMessage(0xC800, da, sa, 0x15) 229 | if received: 230 | nextPacket = NumericValue.FromLEBytes(ctsdata[2:5]).Value() 231 | maxSentPackets = ctsdata[1] 232 | log.debug("(ETP) Received CTS for max {0} packets, next packet {1}".format( 233 | maxSentPackets, nextPacket) 234 | ) 235 | else : 236 | log.warning('(ETP) Wait for CTS timed out') 237 | break 238 | 239 | packetOffset = nextPacket - 1 240 | 241 | nPackets = min(maxSentPackets, totalPackets - packetOffset) 242 | 243 | log.debug('(ETP) Sending {0} packets with packet offset {1}'.format( 244 | nPackets, packetOffset)) 245 | log.debug('(ETP) bytes[{0} - {1}]'.format( 246 | packetOffset * 7, packetOffset * 7 + nPackets * 7 - 1)) 247 | 248 | dpoData = ([0x16] 249 | + [nPackets] 250 | + NumericValue(packetOffset).AsLEBytes(3) 251 | + NumericValue(pgn).AsLEBytes(3)) 252 | self._SendCANMessage(etpcm_id.GetCANID(), dpoData) 253 | 254 | for n in range(nPackets) : 255 | startByte = (n * 7) + (packetOffset * 7) 256 | # Send send data[n * 7 + dataoffset: n* 7 +7 + dataoffset] 257 | self._SendCANMessage(etpdt_id.GetCANID(), [n + 1] + data[startByte:startByte+7]) 258 | 259 | # If it is the last packet, quit the loop 260 | if (n + nextPacket) >= totalPackets: 261 | done = True 262 | break 263 | 264 | time.sleep(0.001) 265 | 266 | # TODO: Optionally wait for EOMA 267 | -------------------------------------------------------------------------------- /isobus/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.handlers 3 | 4 | # create log 5 | log = logging.getLogger('isobus') 6 | log.setLevel(logging.DEBUG) 7 | 8 | # create console handler and set level to debug 9 | ch = logging.StreamHandler() 10 | ch.setLevel(logging.DEBUG) 11 | 12 | # File handler 13 | fh = logging.FileHandler('isobus.log', mode='w') 14 | 15 | # Syslog handler 16 | sh = logging.handlers.SysLogHandler() 17 | sh.ident = 'isobus ' 18 | 19 | # create formatter 20 | formatter = logging.Formatter('%(asctime)s %(levelname)s : %(message)s') 21 | slFormatter = logging.Formatter('%(levelname)s %(message)s') 22 | 23 | # add formatter to ch 24 | ch.setFormatter(formatter) 25 | fh.setFormatter(formatter) 26 | sh.setFormatter(slFormatter) 27 | 28 | # TODO: Make a function to start logging to file / stdout/ stderr? Or use syslog? 29 | 30 | # add sh to log 31 | log.addHandler(sh) 32 | -------------------------------------------------------------------------------- /isobus/vt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboomer/python-isobus/da09e6ecaf27a540a87d19319e4bdeb36685aea3/isobus/vt/__init__.py -------------------------------------------------------------------------------- /isobus/vt/client.py: -------------------------------------------------------------------------------- 1 | import time 2 | from isobus.vt.interface import IBSVTInterface 3 | from isobus.common import IBSException 4 | from isobus.log import log 5 | from isobus.ibsinterface import IBSRxHandler 6 | from isobus.cf import IBSControlFunction 7 | from isobus.cf import BuildISOBUSName 8 | 9 | class VTClient(IBSControlFunction): 10 | """VT Client (implement) simulation""" 11 | 12 | # Return True or False on commands based on success 13 | 14 | def __init__(self, interface, channel) : 15 | self.connection = IBSVTInterface(interface, channel) 16 | self.sa = 0xFE 17 | self.da = 0xFF 18 | self.alive = False 19 | self.functionInstance = 0x00 20 | 21 | def SetSrc(self, sa): 22 | if sa >= 0 and sa <= 0xFE: 23 | self.sa = sa 24 | 25 | def ConnectToVT(self, da): 26 | gotStatus, _ = self.connection.WaitForStatusMessage(da) 27 | ibsName = BuildISOBUSName(functionInstance = self.functionInstance) 28 | if gotStatus: 29 | self.ClaimAddress(self.sa, ibsName) 30 | self.connection.SendWSMaintenance(True, self.sa, da) 31 | time.sleep(0.5) 32 | self.connection.StartWSMaintenace(self.sa, da) 33 | self.alive = True 34 | self.da = da 35 | else: 36 | raise IBSException("Failed to connect to VT") 37 | 38 | def LoadVersion(self, version): 39 | self._CheckAlive() 40 | 41 | self.connection.SendLoadVersionCommand(version, self.sa, self.da) 42 | log.debug('Loading version {0}...'.format(version)) 43 | 44 | # TODO wait for load version response max 3 status messages w/parsing = 0 45 | [receivedResponse, error] = self.connection.WaitLoadVersionResponse(self.da, self.sa) 46 | if receivedResponse and (error == 0): 47 | pass 48 | else: 49 | raise IBSException("Did not load version, error code: {0}".format(error)) 50 | 51 | def StoreVersion(self, version): 52 | self._CheckAlive() 53 | log.debug('Storing version {0}'.format(version)) 54 | 55 | self.connection.SendStoreVersioncommand(version, self.da, self.sa) 56 | 57 | # TODO wait for load version response max 3 status messages w/parsing = 0 58 | [receivedResponse, error] = self.connection.WaitStoreVersionResponse(self.da, self.sa) 59 | if receivedResponse and (error == 0): 60 | pass 61 | else: 62 | raise IBSException("Did not store version, error code: {0}".format(error)) 63 | 64 | 65 | def UploadPoolData(self, data, eoop=True): 66 | self._CheckAlive() 67 | 68 | self.connection.SendGetMemory(len(data), self.da, self.sa) 69 | [receivedMemResp, version, enoughMemory] = self.connection.WaitForGetMemoryResponse( 70 | self.da, self.sa) 71 | 72 | if receivedMemResp and enoughMemory: 73 | 74 | self.connection.SendPoolUpload(self.da, self.sa, data) 75 | 76 | if eoop: 77 | self.connection.SendEndOfObjectPool(self.da, self.sa) 78 | [received, error] = self.connection.WaitEndOfObjectPoolResponse( 79 | self.da, self.sa) 80 | if received and error == 0: 81 | pass 82 | elif received: 83 | raise IBSException("Received error code {0}".format(error)) 84 | else: 85 | raise IBSException("EoOP Response timed out") 86 | elif receivedMemResp: 87 | raise IBSException('Not enough memory available') 88 | else: 89 | raise IBSException('No Get Memory Response received') 90 | 91 | 92 | 93 | def DeleteObjectPool(self): 94 | self._CheckAlive() 95 | 96 | self.connection.SendDeleteObjectPool(self.da, self.sa) 97 | 98 | # TODO wait for load version response max 3 status messages w/parsing = 0 99 | [receivedResponse, error] = self.connection.WaitDeleteObjectPoolResponse( 100 | self.da, self.sa) 101 | if receivedResponse and (error == 0): 102 | pass 103 | elif receivedResponse: 104 | raise IBSException("Got error: {0}".format(error)) 105 | else: 106 | raise IBSException("Response timed out") 107 | 108 | 109 | 110 | def ChangeActiveMask(self, wsid, maskid): 111 | self._CheckAlive() 112 | 113 | self.connection.SendChangeActiveMask(wsid, maskid, self.sa, self.da) 114 | 115 | [receivedResponse, newMaskID, error] = ( 116 | self.connection.WaitForChangeActiveMaskResponse(self.da, self.sa)) 117 | 118 | if receivedResponse and (error == 0): 119 | log.debug("New active mask = 0X{:04X}".format(newMaskID)) 120 | ret = True 121 | elif receivedResponse: 122 | raise IBSException("Error change active mask, error code: {0}".format(error)) 123 | else: 124 | raise IBSException("No response received") 125 | 126 | 127 | def ChangeSKMask(self, maskid, skmaskid, alarm=False): 128 | self._CheckAlive() 129 | 130 | self.connection.SendChangeSKMask(maskid, skmaskid, alarm, self.da, self.sa) 131 | 132 | [receivedResponse, error, newSKMaskID] = ( 133 | self.connection.WaitForChangeSKMaskResponse(self.da, self.sa)) 134 | if receivedResponse and (error == 0): 135 | return newSKMaskID 136 | elif receivedResponse: 137 | raise IBSException("Error change active mask, error code: {0}".format(error)) 138 | else: 139 | raise IBSException("No response received") 140 | 141 | 142 | def ChangeAttribute(self, objid, attrid, value): 143 | self._CheckAlive() 144 | 145 | self.connection.SendChangeAttribute(objid, attrid, value, self.da, self.sa) 146 | 147 | [receivedResponse, error] = ( 148 | self.connection.WaitChangeAttributeResponse(self.da, self.sa)) 149 | 150 | if receivedResponse and (error == 0): 151 | pass 152 | elif receivedResponse: 153 | raise IBSException("Error change attribute, error code: {0}".format(error)) 154 | else: 155 | raise IBSException("No response received") 156 | 157 | 158 | def ChangeNumericValue(self, objid, value): 159 | self._CheckAlive() 160 | 161 | self.connection.SendChangeNumericValue(objid, value, vtsa = self.da, ecusa = self.sa) 162 | [receivedResponse, error] = ( 163 | self.connection.WaitForChangeNumericValueResponse(self.da, self.sa)) 164 | 165 | if receivedResponse and (error == 0): 166 | pass 167 | elif receivedResponse: 168 | raise IBSException("Error change numeric value, error code: {0}".format(error)) 169 | else: 170 | raise IBSException("No response received") 171 | 172 | def ChangeStringValue(self, objid, value): 173 | self._CheckAlive() 174 | 175 | self.connection.SendChangeStringValue(objid, value, vtsa = self.da, ecusa = self.sa) 176 | [receivedResponse, error] = ( 177 | self.connection.WaitForChangeStringValueResponse(self.da, self.sa)) 178 | 179 | if receivedResponse and (error == 0): 180 | pass 181 | elif receivedResponse: 182 | raise IBSException("Error change string value, error code: {0}".format(error)) 183 | else: 184 | raise IBSException("No response received") 185 | 186 | def ChangeListItem(self, objid, index, value): 187 | self._CheckAlive() 188 | 189 | self.connection.SendChangeListItemCommand(self.da, self.sa, objid, index, value) 190 | [receivedResponse, error] = ( 191 | self.connection.WaitForChangeListItemResponse(self.da, self.sa)) 192 | 193 | if receivedResponse and (error == 0): 194 | pass 195 | elif receivedResponse: 196 | raise IBSException("Error change list item, error code: {0}".format(error)) 197 | else: 198 | raise IBSException("No response received") 199 | 200 | 201 | 202 | def ESCInput(self): 203 | self._CheckAlive() 204 | 205 | self.connection.SendEscCommand(self.da, self.sa) 206 | 207 | [receivedResponse, error, escObject] = ( 208 | self.connection.WaitForESCResponse(self.da, self.sa)) 209 | 210 | if receivedResponse and (error == 0): 211 | return escObject 212 | elif receivedResponse and (error == 1): 213 | raise IBSException("No input object open") 214 | elif receivedResponse: 215 | raise IBSException("Error code: {0}".format(error)) 216 | else: 217 | raise IBSException("No response received") 218 | 219 | def DisconnectFromVT(self): 220 | if self.alive: 221 | self.connection.StopWSMaintenance(self.sa, self.da) 222 | self.alive = False 223 | 224 | def IdentifyVTs(self): 225 | self.connection.SendIdentifyVT(self.sa) 226 | 227 | def _CheckAlive(self): 228 | if not self.alive: 229 | raise IBSException('Not connected to a VT') 230 | -------------------------------------------------------------------------------- /isobus/vt/interface.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from isobus.common import NumericValue 4 | from isobus.ibsinterface import IBSInterface 5 | from isobus.common import IBSID 6 | from isobus.constants import * 7 | from isobus.log import log 8 | from isobus.common import IBSException 9 | 10 | 11 | class IBSVTInterface(IBSInterface): 12 | """ Implements ISOBUS part 6 funcationality (Version 3) 13 | Extends the ISOBUS general interface 14 | """ 15 | 16 | def WaitForStatusMessage(self, vtsa): 17 | return self._WaitForIBSMessage(PGN_VT2ECU, vtsa, 0xFF, 0xFE) 18 | 19 | def SendChangeActiveMask(self, wsid, maskid, sa, da): 20 | candata = ([0xAD] 21 | + NumericValue(wsid).AsLEBytes(2) 22 | + NumericValue(maskid).AsLEBytes(2) 23 | + [0xFF, 0xFF, 0xFF]) 24 | 25 | self._SendIBSMessage(PGN_ECU2VT, da, sa, candata) 26 | 27 | def WaitForChangeActiveMaskResponse(self, vtsa, ecusa): 28 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0xAD) 29 | return received, NumericValue.FromLEBytes(data[1:3]).Value(), data[3] 30 | 31 | def SendChangeSKMask(self, maskid, skmaskid, alarm, vtsa, ecusa): 32 | candata = [0xFF] * 8 33 | if alarm: 34 | candata = ([0xAE] 35 | + [0x02] 36 | + NumericValue(maskid).AsLEBytes(2) 37 | + NumericValue(skmaskid).AsLEBytes(2) 38 | + [0xFF, 0xFF]) 39 | else: 40 | candata = ([0xAE] 41 | + [0x01] 42 | + NumericValue(maskid).AsLEBytes(2) 43 | + NumericValue(skmaskid).AsLEBytes(2) 44 | + [0xFF, 0xFF]) 45 | 46 | self._SendIBSMessage(PGN_ECU2VT, vtsa, ecusa, candata) 47 | 48 | def WaitForChangeSKMaskResponse(self, vtsa, ecusa): 49 | """ Wait for the Change Soft Key Mask response message 50 | Return True for received, error code, and new SK mask ID 51 | """ 52 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0xAE) 53 | return received, data[5], NumericValue.FromLEBytes(data[3:5]).Value() 54 | 55 | 56 | def SendChangeAttribute(self, objid, attrid, value, vtsa, ecusa): 57 | candata = ([0xAF] 58 | + NumericValue(objid).AsLEBytes(2) 59 | + NumericValue(attrid).AsLEBytes(1) 60 | + NumericValue(value).AsLEBytes(4)) 61 | 62 | self._SendIBSMessage(PGN_ECU2VT, vtsa, ecusa, candata) 63 | 64 | def WaitChangeAttributeResponse(self, vtsa, ecusa): 65 | """ 66 | Wait for a response for the change attribute command 67 | Return True for received and Error code 68 | """ 69 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0xAF) 70 | return received, data[4] 71 | 72 | def SendEscCommand(self, vtsa, ecusa): 73 | candata = [0x92] + (7 * [0xFF]) 74 | self._SendIBSMessage(PGN_ECU2VT, vtsa, ecusa, candata) 75 | 76 | def WaitForESCResponse(self, vtsa, ecusa): 77 | """ 78 | Wait for ESC response 79 | @return True for received, error code and aborted input object ID 80 | """ 81 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0x92) 82 | return received, data[3], NumericValue.FromLEBytes(data[1:3]).Value() 83 | 84 | def SendWSMaintenance(self, initiating, sa, da): 85 | initBit = 0 86 | if (initiating) : 87 | initBit = 1 88 | 89 | candata = [0xFF, (initBit & 0x1), 0x3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] 90 | self._SendIBSMessage(PGN_ECU2VT, da, sa, candata) 91 | 92 | def StartWSMaintenace(self, sa, da): 93 | candata = [0xFF, 0x00, 0x3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] 94 | ibsid = IBSID(sa = sa, da = da, pgn = PGN_ECU2VT, prio = 6) 95 | self.AddPeriodicMessage(ibsid, candata, 1.0) 96 | 97 | def StopWSMaintenance(self, sa, da): 98 | # For socketcan_native, bit 32 (MSb) needs to be set for extended ID 99 | # Is fixed in latest python-can though! 100 | ibsid = IBSID(sa = sa, da = da, pgn = PGN_ECU2VT, prio = 6) 101 | self.StopPeriodicMessage(ibsid) 102 | 103 | def SendLoadVersionCommand(self, version, sa, da): 104 | if len(version) == 7: 105 | candata = [0xD1] + [ord(x) for x in version] 106 | self._SendIBSMessage(PGN_ECU2VT, da, sa, candata) 107 | else : 108 | raise IBSException("Version {0} is not 7 characters".format(version)) 109 | 110 | def SendStoreVersioncommand(self, version, da, sa): 111 | if len(version) == 7: 112 | candata = [0xD0] + [ord(x) for x in version] 113 | self._SendIBSMessage(PGN_ECU2VT, da, sa, candata) 114 | else : 115 | raise IBSException("Version {0} is not 7 characters".format(version)) 116 | 117 | def WaitLoadVersionResponse(self, vtsa, ecusa): 118 | #TODO: Should wait 3 status messages w/parsing bit=0 i.o. 3 seconds 119 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0xD1) 120 | return received, data[5] 121 | 122 | def WaitStoreVersionResponse(self, vtsa, ecusa): 123 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0xD0) 124 | return received, data[5] 125 | 126 | def SendGetMemory(self, memRequired, vtsa, ecusa): 127 | candata = ( [0xC0, 0xFF] 128 | + NumericValue(memRequired).AsLEBytes(4) 129 | + [0xFF, 0xFF]) 130 | self._SendIBSMessage(PGN_ECU2VT, vtsa, ecusa, candata) 131 | 132 | def WaitForGetMemoryResponse(self, vtsa, ecusa): 133 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0xC0) 134 | version = data[1] 135 | enoughMemory = True 136 | if data[2] == 0x01: 137 | enoughMemory = False 138 | return received, version, enoughMemory 139 | 140 | def SendChangeNumericValue(self, objid, value, vtsa, ecusa): 141 | candata =([0xA8] 142 | + NumericValue(objid).AsLEBytes(2) 143 | + [0xFF] 144 | + NumericValue(value).AsLEBytes(4)) 145 | self._SendIBSMessage(PGN_ECU2VT, vtsa, ecusa, candata) 146 | 147 | def WaitForChangeNumericValueResponse(self, vtsa, ecusa): 148 | """ 149 | Return true for received, error code 150 | """ 151 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0xA8) 152 | return received, data[3] 153 | 154 | def SendChangeStringValue(self, objid, value, vtsa, ecusa): 155 | # TODO: Check for too large strings! 156 | stringData = [ord(x) for x in value] 157 | 158 | if len(stringData) < 3: 159 | stringData = stringData + list([RESERVED] * (3 - len(stringData))) 160 | 161 | candata =([0xB3] 162 | + NumericValue(objid).AsLEBytes(2) 163 | + NumericValue(len(value)).AsLEBytes(2) 164 | + stringData) 165 | self._SendIBSMessage(PGN_ECU2VT, vtsa, ecusa, candata) 166 | 167 | def WaitForChangeStringValueResponse(self, vtsa, ecusa): 168 | """ 169 | Return true for received, error code 170 | """ 171 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0xB3) 172 | return received, data[5] 173 | 174 | 175 | def SendPoolUpload(self, vtsa, ecusa, pooldata): 176 | self._SendIBSMessage(PGN_ECU2VT, vtsa, ecusa, [0x11] + pooldata) 177 | 178 | def SendEndOfObjectPool(self, vtsa, ecusa): 179 | self._SendIBSMessage(PGN_ECU2VT, vtsa, ecusa, [0x12] + [0xFF] * 7) 180 | 181 | def WaitEndOfObjectPoolResponse(self, vtsa, ecusa): 182 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0x12, 5.0) 183 | return received, data[1] 184 | # TODO: Return error codes + faulty objects? 185 | 186 | def SendDeleteObjectPool(self, vtsa, ecusa): 187 | self._SendIBSMessage(PGN_ECU2VT, vtsa, ecusa, [0xB2] + (7 * [0xFF])) 188 | 189 | def WaitDeleteObjectPoolResponse(self, vtsa, ecusa): 190 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0xB2) 191 | return received, data[1] 192 | 193 | def SendChangeListItemCommand(self, vtsa, ecusa, objectid, index, newid): 194 | candata = ([0xB1] 195 | + NumericValue(objectid).AsLEBytes(2) 196 | + [index & 0xFF] 197 | + NumericValue(newid).AsLEBytes(2) 198 | + [RESERVED] * 2) 199 | self._SendIBSMessage(PGN_ECU2VT, vtsa, ecusa, candata) 200 | 201 | def WaitForChangeListItemResponse(self, vtsa, ecusa): 202 | [received, data] = self._WaitForIBSMessage(PGN_VT2ECU, vtsa, ecusa, 0xB1) 203 | return received, data[6] 204 | 205 | def SendIdentifyVT(self, sa): 206 | log.debug('Sending identify VT') 207 | self._SendIBSMessage(PGN_ECU2VT, 0xFF, sa, [0xBB] + (7 * [0xFF])) 208 | 209 | 210 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Always prefer setuptools over distutils 2 | from setuptools import setup, find_packages 3 | # To use a consistent encoding 4 | from codecs import open 5 | from os import path 6 | 7 | here = path.abspath(path.dirname(__file__)) 8 | 9 | # Get the long description from the README file 10 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 11 | long_description = f.read() 12 | 13 | setup( 14 | name='isobus', 15 | 16 | version='0.0.1', 17 | 18 | description='ISO11873 implementation', 19 | long_description=long_description, 20 | 21 | url='https://github.com/jboomer/python-isobus', 22 | 23 | author='jboomer', 24 | #author_email='EMAIL HERE', 25 | 26 | license='MIT', 27 | 28 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 29 | classifiers=[ 30 | 'Development Status :: 3 - Alpha', 31 | 32 | 'Intended Audience :: Developers', 33 | 'Topic :: Software Development :: CAN', 34 | 35 | 'License :: OSI Approved :: MIT License', 36 | 37 | 'Programming Language :: Python :: 2', 38 | 'Programming Language :: Python :: 2.7', 39 | 'Programming Language :: Python :: 3', 40 | 'Programming Language :: Python :: 3.3', 41 | 'Programming Language :: Python :: 3.4', 42 | 'Programming Language :: Python :: 3.5', 43 | ], 44 | 45 | keywords='can j1939 isobus', 46 | 47 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 48 | 49 | install_requires=['python-can'], 50 | 51 | extras_require={ 52 | 'dev': ['check-manifest'], 53 | 'test': ['coverage'], 54 | }, 55 | 56 | # If there are data files included in your packages that need to be 57 | # installed, specify them here. If using Python 2.6 or less, then these 58 | # have to be included in MANIFEST.in as well. 59 | # package_data={ 60 | # 'sample': ['package_data.dat'], 61 | # }, 62 | 63 | # Although 'package_data' is the preferred approach, in some case you may 64 | # need to place data files outside of your packages. See: 65 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 66 | # In this case, 'data_file' will be installed into '/my_data' 67 | # data_files=[('my_data', ['data/data_file'])], 68 | 69 | entry_points={ 70 | 'console_scripts': [ 71 | 'vtclient=isobus.bin.vtclient:main', 72 | ], 73 | }, 74 | ) 75 | --------------------------------------------------------------------------------