From 7e964a2af35549631e9767974ccafd64154c588e Mon Sep 17 00:00:00 2001 From: uhi22 Date: Wed, 16 Nov 2022 00:22:49 +0100 Subject: [PATCH] added SDP for evse side --- addressManager.py | 30 +++++++++- fsmPev.py | 7 ++- pyPlcHomeplug.py | 28 +++++++-- pyPlcIpv6.py | 146 +++++++++++++++++++++++++++++++++++++++++----- pyPlcTcpSocket.py | 2 +- pyPlcWorker.py | 4 +- 6 files changed, 190 insertions(+), 27 deletions(-) diff --git a/addressManager.py b/addressManager.py index f4594cb..c9bc55c 100644 --- a/addressManager.py +++ b/addressManager.py @@ -1,5 +1,9 @@ - +# +# Address Manager +# +# Keeps, provides and finds the MAC and IPv6 addresses +# import subprocess import os from helpers import * # prettyMac etc @@ -17,6 +21,8 @@ class addressManager(): self.localIpv6Addresses=[] self.findLocalMacAddress() self.findLinkLocalIpv6Address() + self.pevIp="" + self.SeccIp="" pass def findLinkLocalIpv6Address(self): @@ -89,6 +95,28 @@ class addressManager(): self.pevIp = pevIp print("[addressManager] pev has IP " + self.pevIp) + def setSeccIp(self, SeccIp): + # During SDP, the IPv6 of the charger was found out. Store it, maybe we need it later. + if (type(SeccIp)==type(bytearray([0]))): + # the parameter was a bytearray. We want a string, so convert it. + # print("this is a byte array") + if (len(SeccIp)!=16): + print("[addressManager] ERROR: setSeccIp: invalid ip address len " + str(len(SeccIp))) + return + s = "" + for i in range(0, 16): + s = s + twoCharHex(SeccIp[i]) + if (((i % 2)==1) and (i!=15)): + s = s + ":" + self.SeccIp = s.lower() + else: + # the parameter was a string, assumingly. Just take it over. + self.SeccIp = SeccIp + print("[addressManager] EVCC has IP " + self.SeccIp) + + def getSeccIp(self): + # The IPv6 address of the charger. Type is String. + return self.SeccIp def getLocalMacAddress(self): print("[addressManager] will give local MAC " + prettyMac(self.localMac)) diff --git a/fsmPev.py b/fsmPev.py index 1b84215..2a9d2bb 100644 --- a/fsmPev.py +++ b/fsmPev.py @@ -168,15 +168,18 @@ class fsmPev(): self.cyclesInState = 0 self.rxData = [] if (not self.Tcp.isConnected): - self.Tcp.connect('fe80:0000:0000:0000:c690:83f3:fbcb:980e', 15118) # todo: use the EVSE IP address which was found out with SDP + #evseIp = 'fe80:0000:0000:0000:c690:83f3:fbcb:980e' + evseIp = self.addressManager.getSeccIp() # the EVSE IP address which was found out with SDP + self.Tcp.connect(evseIp, 15118) if (not self.Tcp.isConnected): print("connection failed") else: print("connected") - def __init__(self): + def __init__(self, addressManager): print("initializing fsmPev") self.Tcp = pyPlcTcpSocket.pyPlcTcpClientSocket() + self.addressManager = addressManager self.state = stateNotYetInitialized self.cyclesInState = 0 self.rxData = [] diff --git a/pyPlcHomeplug.py b/pyPlcHomeplug.py index 0b43977..33897b3 100644 --- a/pyPlcHomeplug.py +++ b/pyPlcHomeplug.py @@ -643,11 +643,12 @@ class pyPlcHomeplug(): def isTooLong(self): # The timeout handling function. - return (self.pevSequenceCyclesInState > 50) + return (self.pevSequenceCyclesInState > 500) def runPevSequencer(self): # in PEV mode, initiate the SLAC sequence - # Todo: Timing between the states, and timeout handling to be implemented + self.pevSequenceCyclesInState+=1 + # Todo: timeout handling to be implemented if (self.iAmPev==1): if (self.pevSequenceState==0): # waiting for start condition # In real life we would check whether we see 5% PWM on the pilot line. @@ -659,6 +660,8 @@ class pyPlcHomeplug(): self.enterState(1) return if (self.pevSequenceState==1): # waiting for SLAC_PARAM.CNF + if (self.isTooLong()): + self.enterState(0) return if (self.pevSequenceState==2): # received SLAC_PARAM.CNF self.composeStartAttenCharInd() @@ -704,6 +707,8 @@ class pyPlcHomeplug(): if (self.pevSequenceState==6): # waiting for ATTEN_CHAR.IND # todo: it is possible that we receive this message from multiple chargers. We need # to select the charger with the loudest reported signals. + if (self.isTooLong()): + self.enterState(0) return if (self.pevSequenceState==7): # ATTEN_CHAR.IND was received and the nearest charger decided self.composeAttenCharRsp() @@ -722,6 +727,8 @@ class pyPlcHomeplug(): self.enterState(9) return if (self.pevSequenceState==9): # waiting for SLAC_MATCH.CNF + if (self.isTooLong()): + self.enterState(0) return if (self.pevSequenceState==10): # SLAC is finished, SET_KEY.REQ is transmitted. Wait some time, until # the homeplug modem made the reset and is ready with the new key. @@ -741,6 +748,7 @@ class pyPlcHomeplug(): self.transmit(self.mytransmitbuffer) self.pevSequenceDelayCycles = 20 self.enterState(12) + return if (self.pevSequenceState==12): if (self.pevSequenceDelayCycles>0): self.pevSequenceDelayCycles-=1 @@ -749,14 +757,23 @@ class pyPlcHomeplug(): print("[PEVSLAC] Number of modems in the AVLN: " + str(self.numberOfSoftwareVersionResponses)) if (self.numberOfSoftwareVersionResponses<2): print("[PEVSLAC] ERROR: There should be at least two modems, one from car and one from charger.") - self.callbackAvlnEstablished(0) + self.callbackAvlnEstablished(0) # report that we lost the connection + self.addressManager.setSeccIp("") # forget the IPv6 of the charger self.enterState(0) else: # inform the higher-level state machine, that now it can start the SDP / IPv6 communication - self.callbackAvlnEstablished(1) + self.ipv6.initiateSdpRequest() self.enterState(13) # Final state is reached return - if (self.pevSequenceState==13): # AVLN is established. Nothing more to do, just wait until unplugging. + if (self.pevSequenceState==13): # AVLN is established. SDP request was sent. Waiting for SDP response. + if (len(self.addressManager.getSeccIp())>0): + print("[PEVSLAC] Now we know the chargers IP.") + self.callbackAvlnEstablished(1) + self.enterState(14) + if (self.isTooLong()): + self.enterState(0) + return + if (self.pevSequenceState==14): # AVLN is established. SDP finished. Nothing more to do, just wait until unplugging. # Todo: if (self.isUnplugged()): self.pevSequenceState=0 # Or we just check the connection cyclically by sending software version requests... self.pevSequenceDelayCycles = 500 @@ -800,6 +817,7 @@ class pyPlcHomeplug(): self.callbackAvlnEstablished = callbackAvlnEstablished self.addressManager = addrMan self.pevSequenceState = 0 + self.pevSequenceCyclesInState = 0 self.numberOfSoftwareVersionResponses = 0 #self.sniffer = pcap.pcap(name=None, promisc=True, immediate=True, timeout_ms=50) # eth3 means: Third entry from back, in the list of interfaces, which is provided by pcap.findalldevs. diff --git a/pyPlcIpv6.py b/pyPlcIpv6.py index 0fd8d16..224e8c1 100644 --- a/pyPlcIpv6.py +++ b/pyPlcIpv6.py @@ -2,12 +2,18 @@ # This module handles the IPv6 related functionality of the communication between charging station and car. # # It has the following sub-functionalities: -# - IP.UDP.SDP: listen to requests from the car, and responding to them. +# - IP.UDP.SDP for EvseMode: listen to requests from the car, and responding to them. # Eth --> IPv6 --> UDP --> V2GTP --> SDP # | # v # Eth <-- IPv6 <-- UDP <-- V2GTP <-- SDP # +# - IP.UDP.SDP for PevMode: initiate an SDP request, and listen to the response of the charger +# +---- Eth <-- IPv6 <-- UDP <-- V2GTP <-- SDP +# homeplug +# EVSE +# homeplug +# +---> Eth --> IPv6 --> UDP --> V2GTP --> SDP # # Abbreviations: # SECC: Supply Equipment Communication Controller. The "computer" of the charging station. @@ -23,22 +29,22 @@ import udpChecksum class ipv6handler(): def fillMac(self, macbytearray, position=6): # position 6 is the source MAC for i in range(0, 6): - self.EthResponse[6+i] = macbytearray[i] + self.EthTxFrame[6+i] = macbytearray[i] def packResponseIntoEthernet(self, buffer): # packs the IP packet into an ethernet packet - self.EthResponse = bytearray(len(buffer) + 6 + 6 + 2) # Ethernet header needs 14 bytes: + self.EthTxFrame = bytearray(len(buffer) + 6 + 6 + 2) # Ethernet header needs 14 bytes: # 6 bytes destination MAC # 6 bytes source MAC # 2 bytes EtherType for i in range(0, 6): # fill the destination MAC with the source MAC of the received package - self.EthResponse[i] = self.myreceivebuffer[6+i] + self.EthTxFrame[i] = self.myreceivebuffer[6+i] self.fillMac(self.ownMac) # bytes 6 to 11 are the source MAC - self.EthResponse[12] = 0x86 # 86dd is IPv6 - self.EthResponse[13] = 0xdd + self.EthTxFrame[12] = 0x86 # 86dd is IPv6 + self.EthTxFrame[13] = 0xdd for i in range(0, len(buffer)): - self.EthResponse[14+i] = buffer[i] - self.transmit(self.EthResponse) + self.EthTxFrame[14+i] = buffer[i] + self.transmit(self.EthTxFrame) def packResponseIntoIp(self, buffer): @@ -125,7 +131,7 @@ class ipv6handler(): self.packResponseIntoUdp(self.V2Gframe) def evaluateUdpPayload(self): - if (self.destinationport == 15118): # port for the SECC + if ((self.destinationport == 15118) or (self.sourceport == 15118)): # port for the SECC if ((self.udpPayload[0]==0x01) and (self.udpPayload[1]==0xFE)): # protocol version 1 and inverted # it is a V2GTP message if (self.iAmEvse): @@ -133,7 +139,8 @@ class ipv6handler(): self.EvccIp = self.sourceIp self.addressManager.setPevIp(self.EvccIp) showAsHex(self.udpPayload, "V2GTP ") - self.evccPort = self.sourceport + if (self.destinationport == 15118): #if the destination is the charger, + self.evccPort = self.sourceport #then the source is the vehicle v2gptPayloadType = self.udpPayload[2] * 256 + self.udpPayload[3] # 0x8001 EXI encoded V2G message (Will NOT come with UDP. Will come with TCP.) # 0x9000 SDP request message (SECC Discovery) @@ -151,15 +158,124 @@ class ipv6handler(): print("seccDiscoveryReqTransportProtocol " + str(seccDiscoveryReqTransportProtocol) + " is not supported") else: # This was a valid SDP request. Let's respond, if we are the charger. - print("ok, this was a valid SDP request") if (self.iAmEvse==1): - print("We are the SECC. Sending SDP response.") + print("Ok, this was a valid SDP request. We are the SECC. Sending SDP response.") self.sendSdpResponse() else: print("v2gptPayloadLen on SDP request is " + str(v2gptPayloadLen) + " not supported") - else: - print("v2gptPayloadType " + hex(v2gptPayloadType) + " not supported") + return + if (v2gptPayloadType == 0x9001): + # it is a SDP response from the charger to the car + if (self.iAmPev): + v2gptPayloadLen = self.udpPayload[4] * 256 ** 3 + self.udpPayload[5] * 256 ** 2 + self.udpPayload[6] * 256 + self.udpPayload[7] + if (v2gptPayloadLen == 20): + # 20 is the only valid length for a SDP response. + print("[PEV] Received SDP response") + # at byte 8 of the UDP payload starts the IPv6 address of the charger. + for i in range(0, 16): + self.SeccIp[i] = self.udpPayload[8+i] # 16 bytes IP address of the charger + self.addressManager.setSeccIp(self.SeccIp) + return + print("v2gptPayloadType " + hex(v2gptPayloadType) + " not supported") + def initiateSdpRequest(self): + if (self.iAmPev == 1): + # We are the car. We want to find out the IPv6 address of the charger. We + # send a SECC Discovery Request. + # The payload is just two bytes: 10 00. + # First step is, to pack this payload into a V2GTP frame. + print("[PEV] initiating SDP request") + self.v2gtpFrame = bytearray(8 + 2) # 8 byte header plus 2 bytes payload + self.v2gtpFrame[0] = 0x01 # version + self.v2gtpFrame[1] = 0xFE # version inverted + self.v2gtpFrame[2] = 0x90 # 9000 means SDP request message + self.v2gtpFrame[3] = 0x00 + self.v2gtpFrame[4] = 0x00 + self.v2gtpFrame[5] = 0x00 + self.v2gtpFrame[6] = 0x00 + self.v2gtpFrame[7] = 0x02 # payload size + self.v2gtpFrame[8] = 0x10 # payload + self.v2gtpFrame[9] = 0x00 # payload + # Second step: pack this into an UDP frame. + self.packRequestIntoUdp(self.v2gtpFrame) + + def packRequestIntoUdp(self, buffer): + # embeds the (SDP) request into the lower-layer-protocol: UDP + # Reference: wireshark trace of the ioniq car + self.UdpRequest = bytearray(len(buffer) + 8) # UDP header needs 8 bytes: + # 2 bytes source port + # 2 bytes destination port + # 2 bytes length (incl checksum) + # 2 bytes checksum + self.pevPort = 50032 # "random" port. Todo: Do we need to ask the OS for a unique number, to avoid collision with existing port? + self.UdpRequest[0] = self.pevPort >> 8 + self.UdpRequest[1] = self.pevPort & 0xFF + self.UdpRequest[2] = 15118 >> 8 + self.UdpRequest[3] = 15118 & 0xFF + + lenInclChecksum = len(buffer) + 8 + self.UdpRequest[4] = lenInclChecksum >> 8 + self.UdpRequest[5] = lenInclChecksum & 0xFF + # checksum will be calculated afterwards + self.UdpRequest[6] = 0 + self.UdpRequest[7] = 0 + for i in range(0, len(buffer)): + self.UdpRequest[8+i] = buffer[i] + #showAsHex(self.UdpRequest, "UDP request ") + self.broadcastIPv6 = [ 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] + # The content of buffer is ready. We can calculate the checksum. see https://en.wikipedia.org/wiki/User_Datagram_Protocol + checksum = udpChecksum.calculateUdpChecksumForIPv6(self.UdpRequest, self.EvccIp, self.broadcastIPv6) + self.UdpRequest[6] = checksum >> 8 + self.UdpRequest[7] = checksum & 0xFF + self.packRequestIntoIp(self.UdpRequest) + + def packRequestIntoIp(self, buffer): + # embeds the (SDP) request into the lower-layer-protocol: IP, Ethernet + self.IpRequest = bytearray(len(buffer) + 8 + 16 + 16) # IP6 header needs 40 bytes: + # 4 bytes traffic class, flow + # 2 bytes destination port + # 2 bytes length (incl checksum) + # 2 bytes checksum + self.IpRequest[0] = 0x60 # traffic class, flow + self.IpRequest[1] = 0 + self.IpRequest[2] = 0 + self.IpRequest[3] = 0 + plen = len(buffer) # length of the payload. Without headers. + self.IpRequest[4] = plen >> 8 + self.IpRequest[5] = plen & 0xFF + self.IpRequest[6] = 0x11 # next level protocol, 0x11 = UDP in this case + self.IpRequest[7] = 0x0A # hop limit + for i in range(0, 16): + self.IpRequest[8+i] = self.EvccIp[i] # source IP address + for i in range(0, 16): + self.IpRequest[24+i] = self.broadcastIPv6[i] # destination IP address + for i in range(0, len(buffer)): + self.IpRequest[40+i] = buffer[i] + #showAsHex(self.IpRequest, "IpRequest ") + self.packRequestIntoEthernet(self.IpRequest) + + def packRequestIntoEthernet(self, buffer): + # packs the IP packet into an ethernet packet + self.EthTxFrame = bytearray(len(buffer) + 6 + 6 + 2) # Ethernet header needs 14 bytes: + # 6 bytes destination MAC + # 6 bytes source MAC + # 2 bytes EtherType + # fill the destination MAC with the IPv6 multicast + self.EthTxFrame[0] = 0x33 + self.EthTxFrame[1] = 0x33 + self.EthTxFrame[2] = 0x00 + self.EthTxFrame[3] = 0x00 + self.EthTxFrame[4] = 0x00 + self.EthTxFrame[5] = 0x01 + self.fillMac(self.ownMac) # bytes 6 to 11 are the source MAC + self.EthTxFrame[12] = 0x86 # 86dd is IPv6 + self.EthTxFrame[13] = 0xdd + for i in range(0, len(buffer)): + self.EthTxFrame[14+i] = buffer[i] + self.transmit(self.EthTxFrame) + + + def enterPevMode(self): self.iAmEvse = 0 # not emulating a charging station self.iAmPev = 1 # emulating a vehicle @@ -238,5 +354,3 @@ class ipv6handler(): if (self.iAmEvse): # If we are an charger, we need to support the SDP, which requires to know our IPv6 adrress. self.SeccIp = self.addressManager.getLinkLocalIpv6Address("bytearray") - - \ No newline at end of file diff --git a/pyPlcTcpSocket.py b/pyPlcTcpSocket.py index 1da5958..a0a5d85 100644 --- a/pyPlcTcpSocket.py +++ b/pyPlcTcpSocket.py @@ -24,7 +24,7 @@ class pyPlcTcpClientSocket(): def connect(self, host, port): try: - print("connecting...") + print("connecting to " + str(host) + " port " + str(port) + "...") # for connecting, we are still in blocking-mode because # otherwise we run into error "[Errno 10035] A non-blocking socket operation could not be completed immediately" # We set a shorter timeout, so we do not block too long if the connection is not established: diff --git a/pyPlcWorker.py b/pyPlcWorker.py index 14fe46f..bdf7e6e 100644 --- a/pyPlcWorker.py +++ b/pyPlcWorker.py @@ -26,7 +26,7 @@ class pyPlcWorker(): if (self.mode == C_EVSE_MODE): self.evse = fsmEvse.fsmEvse() if (self.mode == C_PEV_MODE): - self.pev = fsmPev.fsmPev() + self.pev = fsmPev.fsmPev(self.addressManager) def addToTrace(self, s): self.callbackAddToTrace(s) @@ -66,7 +66,7 @@ class pyPlcWorker(): self.hp.enterPevMode() if (not hasattr(self, 'pev')): print("creating pev") - self.pev = fsmPev.fsmPev() + self.pev = fsmPev.fsmPev(self.addressManager) self.pev.reInit() if (strAction == "E"): print("switching to EVSE mode")