added SDP for evse side

This commit is contained in:
uhi22 2022-11-16 00:22:49 +01:00
parent 162f2c1ed0
commit 7e964a2af3
6 changed files with 190 additions and 27 deletions

View file

@ -1,5 +1,9 @@
#
# Address Manager
#
# Keeps, provides and finds the MAC and IPv6 addresses
#
import subprocess import subprocess
import os import os
from helpers import * # prettyMac etc from helpers import * # prettyMac etc
@ -17,6 +21,8 @@ class addressManager():
self.localIpv6Addresses=[] self.localIpv6Addresses=[]
self.findLocalMacAddress() self.findLocalMacAddress()
self.findLinkLocalIpv6Address() self.findLinkLocalIpv6Address()
self.pevIp=""
self.SeccIp=""
pass pass
def findLinkLocalIpv6Address(self): def findLinkLocalIpv6Address(self):
@ -89,6 +95,28 @@ class addressManager():
self.pevIp = pevIp self.pevIp = pevIp
print("[addressManager] pev has IP " + self.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): def getLocalMacAddress(self):
print("[addressManager] will give local MAC " + prettyMac(self.localMac)) print("[addressManager] will give local MAC " + prettyMac(self.localMac))

View file

@ -168,15 +168,18 @@ class fsmPev():
self.cyclesInState = 0 self.cyclesInState = 0
self.rxData = [] self.rxData = []
if (not self.Tcp.isConnected): 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): if (not self.Tcp.isConnected):
print("connection failed") print("connection failed")
else: else:
print("connected") print("connected")
def __init__(self): def __init__(self, addressManager):
print("initializing fsmPev") print("initializing fsmPev")
self.Tcp = pyPlcTcpSocket.pyPlcTcpClientSocket() self.Tcp = pyPlcTcpSocket.pyPlcTcpClientSocket()
self.addressManager = addressManager
self.state = stateNotYetInitialized self.state = stateNotYetInitialized
self.cyclesInState = 0 self.cyclesInState = 0
self.rxData = [] self.rxData = []

View file

@ -643,11 +643,12 @@ class pyPlcHomeplug():
def isTooLong(self): def isTooLong(self):
# The timeout handling function. # The timeout handling function.
return (self.pevSequenceCyclesInState > 50) return (self.pevSequenceCyclesInState > 500)
def runPevSequencer(self): def runPevSequencer(self):
# in PEV mode, initiate the SLAC sequence # 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.iAmPev==1):
if (self.pevSequenceState==0): # waiting for start condition if (self.pevSequenceState==0): # waiting for start condition
# In real life we would check whether we see 5% PWM on the pilot line. # In real life we would check whether we see 5% PWM on the pilot line.
@ -659,6 +660,8 @@ class pyPlcHomeplug():
self.enterState(1) self.enterState(1)
return return
if (self.pevSequenceState==1): # waiting for SLAC_PARAM.CNF if (self.pevSequenceState==1): # waiting for SLAC_PARAM.CNF
if (self.isTooLong()):
self.enterState(0)
return return
if (self.pevSequenceState==2): # received SLAC_PARAM.CNF if (self.pevSequenceState==2): # received SLAC_PARAM.CNF
self.composeStartAttenCharInd() self.composeStartAttenCharInd()
@ -704,6 +707,8 @@ class pyPlcHomeplug():
if (self.pevSequenceState==6): # waiting for ATTEN_CHAR.IND if (self.pevSequenceState==6): # waiting for ATTEN_CHAR.IND
# todo: it is possible that we receive this message from multiple chargers. We need # todo: it is possible that we receive this message from multiple chargers. We need
# to select the charger with the loudest reported signals. # to select the charger with the loudest reported signals.
if (self.isTooLong()):
self.enterState(0)
return return
if (self.pevSequenceState==7): # ATTEN_CHAR.IND was received and the nearest charger decided if (self.pevSequenceState==7): # ATTEN_CHAR.IND was received and the nearest charger decided
self.composeAttenCharRsp() self.composeAttenCharRsp()
@ -722,6 +727,8 @@ class pyPlcHomeplug():
self.enterState(9) self.enterState(9)
return return
if (self.pevSequenceState==9): # waiting for SLAC_MATCH.CNF if (self.pevSequenceState==9): # waiting for SLAC_MATCH.CNF
if (self.isTooLong()):
self.enterState(0)
return return
if (self.pevSequenceState==10): # SLAC is finished, SET_KEY.REQ is transmitted. Wait some time, until 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. # the homeplug modem made the reset and is ready with the new key.
@ -741,6 +748,7 @@ class pyPlcHomeplug():
self.transmit(self.mytransmitbuffer) self.transmit(self.mytransmitbuffer)
self.pevSequenceDelayCycles = 20 self.pevSequenceDelayCycles = 20
self.enterState(12) self.enterState(12)
return
if (self.pevSequenceState==12): if (self.pevSequenceState==12):
if (self.pevSequenceDelayCycles>0): if (self.pevSequenceDelayCycles>0):
self.pevSequenceDelayCycles-=1 self.pevSequenceDelayCycles-=1
@ -749,14 +757,23 @@ class pyPlcHomeplug():
print("[PEVSLAC] Number of modems in the AVLN: " + str(self.numberOfSoftwareVersionResponses)) print("[PEVSLAC] Number of modems in the AVLN: " + str(self.numberOfSoftwareVersionResponses))
if (self.numberOfSoftwareVersionResponses<2): if (self.numberOfSoftwareVersionResponses<2):
print("[PEVSLAC] ERROR: There should be at least two modems, one from car and one from charger.") 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) self.enterState(0)
else: else:
# inform the higher-level state machine, that now it can start the SDP / IPv6 communication # 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 self.enterState(13) # Final state is reached
return 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 # Todo: if (self.isUnplugged()): self.pevSequenceState=0
# Or we just check the connection cyclically by sending software version requests... # Or we just check the connection cyclically by sending software version requests...
self.pevSequenceDelayCycles = 500 self.pevSequenceDelayCycles = 500
@ -800,6 +817,7 @@ class pyPlcHomeplug():
self.callbackAvlnEstablished = callbackAvlnEstablished self.callbackAvlnEstablished = callbackAvlnEstablished
self.addressManager = addrMan self.addressManager = addrMan
self.pevSequenceState = 0 self.pevSequenceState = 0
self.pevSequenceCyclesInState = 0
self.numberOfSoftwareVersionResponses = 0 self.numberOfSoftwareVersionResponses = 0
#self.sniffer = pcap.pcap(name=None, promisc=True, immediate=True, timeout_ms=50) #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. # eth3 means: Third entry from back, in the list of interfaces, which is provided by pcap.findalldevs.

View file

@ -2,12 +2,18 @@
# This module handles the IPv6 related functionality of the communication between charging station and car. # This module handles the IPv6 related functionality of the communication between charging station and car.
# #
# It has the following sub-functionalities: # 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 # Eth --> IPv6 --> UDP --> V2GTP --> SDP
# | # |
# v # v
# Eth <-- IPv6 <-- UDP <-- V2GTP <-- SDP # 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: # Abbreviations:
# SECC: Supply Equipment Communication Controller. The "computer" of the charging station. # SECC: Supply Equipment Communication Controller. The "computer" of the charging station.
@ -23,22 +29,22 @@ import udpChecksum
class ipv6handler(): class ipv6handler():
def fillMac(self, macbytearray, position=6): # position 6 is the source MAC def fillMac(self, macbytearray, position=6): # position 6 is the source MAC
for i in range(0, 6): for i in range(0, 6):
self.EthResponse[6+i] = macbytearray[i] self.EthTxFrame[6+i] = macbytearray[i]
def packResponseIntoEthernet(self, buffer): def packResponseIntoEthernet(self, buffer):
# packs the IP packet into an ethernet packet # 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 destination MAC
# 6 bytes source MAC # 6 bytes source MAC
# 2 bytes EtherType # 2 bytes EtherType
for i in range(0, 6): # fill the destination MAC with the source MAC of the received package 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.fillMac(self.ownMac) # bytes 6 to 11 are the source MAC
self.EthResponse[12] = 0x86 # 86dd is IPv6 self.EthTxFrame[12] = 0x86 # 86dd is IPv6
self.EthResponse[13] = 0xdd self.EthTxFrame[13] = 0xdd
for i in range(0, len(buffer)): for i in range(0, len(buffer)):
self.EthResponse[14+i] = buffer[i] self.EthTxFrame[14+i] = buffer[i]
self.transmit(self.EthResponse) self.transmit(self.EthTxFrame)
def packResponseIntoIp(self, buffer): def packResponseIntoIp(self, buffer):
@ -125,7 +131,7 @@ class ipv6handler():
self.packResponseIntoUdp(self.V2Gframe) self.packResponseIntoUdp(self.V2Gframe)
def evaluateUdpPayload(self): 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 if ((self.udpPayload[0]==0x01) and (self.udpPayload[1]==0xFE)): # protocol version 1 and inverted
# it is a V2GTP message # it is a V2GTP message
if (self.iAmEvse): if (self.iAmEvse):
@ -133,7 +139,8 @@ class ipv6handler():
self.EvccIp = self.sourceIp self.EvccIp = self.sourceIp
self.addressManager.setPevIp(self.EvccIp) self.addressManager.setPevIp(self.EvccIp)
showAsHex(self.udpPayload, "V2GTP ") 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] v2gptPayloadType = self.udpPayload[2] * 256 + self.udpPayload[3]
# 0x8001 EXI encoded V2G message (Will NOT come with UDP. Will come with TCP.) # 0x8001 EXI encoded V2G message (Will NOT come with UDP. Will come with TCP.)
# 0x9000 SDP request message (SECC Discovery) # 0x9000 SDP request message (SECC Discovery)
@ -151,15 +158,124 @@ class ipv6handler():
print("seccDiscoveryReqTransportProtocol " + str(seccDiscoveryReqTransportProtocol) + " is not supported") print("seccDiscoveryReqTransportProtocol " + str(seccDiscoveryReqTransportProtocol) + " is not supported")
else: else:
# This was a valid SDP request. Let's respond, if we are the charger. # 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): 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() self.sendSdpResponse()
else: else:
print("v2gptPayloadLen on SDP request is " + str(v2gptPayloadLen) + " not supported") print("v2gptPayloadLen on SDP request is " + str(v2gptPayloadLen) + " not supported")
else: 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") 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): def enterPevMode(self):
self.iAmEvse = 0 # not emulating a charging station self.iAmEvse = 0 # not emulating a charging station
self.iAmPev = 1 # emulating a vehicle self.iAmPev = 1 # emulating a vehicle
@ -238,5 +354,3 @@ class ipv6handler():
if (self.iAmEvse): if (self.iAmEvse):
# If we are an charger, we need to support the SDP, which requires to know our IPv6 adrress. # If we are an charger, we need to support the SDP, which requires to know our IPv6 adrress.
self.SeccIp = self.addressManager.getLinkLocalIpv6Address("bytearray") self.SeccIp = self.addressManager.getLinkLocalIpv6Address("bytearray")

View file

@ -24,7 +24,7 @@ class pyPlcTcpClientSocket():
def connect(self, host, port): def connect(self, host, port):
try: try:
print("connecting...") print("connecting to " + str(host) + " port " + str(port) + "...")
# for connecting, we are still in blocking-mode because # 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" # 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: # We set a shorter timeout, so we do not block too long if the connection is not established:

View file

@ -26,7 +26,7 @@ class pyPlcWorker():
if (self.mode == C_EVSE_MODE): if (self.mode == C_EVSE_MODE):
self.evse = fsmEvse.fsmEvse() self.evse = fsmEvse.fsmEvse()
if (self.mode == C_PEV_MODE): if (self.mode == C_PEV_MODE):
self.pev = fsmPev.fsmPev() self.pev = fsmPev.fsmPev(self.addressManager)
def addToTrace(self, s): def addToTrace(self, s):
self.callbackAddToTrace(s) self.callbackAddToTrace(s)
@ -66,7 +66,7 @@ class pyPlcWorker():
self.hp.enterPevMode() self.hp.enterPevMode()
if (not hasattr(self, 'pev')): if (not hasattr(self, 'pev')):
print("creating pev") print("creating pev")
self.pev = fsmPev.fsmPev() self.pev = fsmPev.fsmPev(self.addressManager)
self.pev.reInit() self.pev.reInit()
if (strAction == "E"): if (strAction == "E"):
print("switching to EVSE mode") print("switching to EVSE mode")