mirror of
https://github.com/uhi22/pyPLC.git
synced 2024-11-20 01:13:58 +00:00
added SDP for evse side
This commit is contained in:
parent
162f2c1ed0
commit
7e964a2af3
6 changed files with 190 additions and 27 deletions
|
@ -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))
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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.
|
||||
|
|
144
pyPlcIpv6.py
144
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:
|
||||
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")
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue