在上一篇博客中,我們完成了MQTT服務器的搭建以及服務器和客戶端SSL證書的簽發,併成功用MQTT.fx軟件作爲客戶端連接發送信息。在實際應用中,我們需要根據需求來開發自己的客戶端應用程序,在本博文中,我將使用Python來開發一個模擬車載終端發送MQTT消息的應用。
在V2X領域,有很多標準化組織(ETSI/ISO/DSRC)制定了車輛V2X消息規範,例如我國的V2X標準中定義了以下5種消息規範:
- BSM,即Basic Safety Message,基礎安全消息,包括速度,轉向,剎車,雙閃,位置等等,多被用在V2V場景即變道預警,盲區預警,交叉路口碰撞預警等等;
- RSI,即Road Side Information,路側信息,用於事件的下方,路側RSU集成,平臺下發,多被用於V2I場景即道路施工,限速標誌,超速預警,公交車道預警等等;
- RSM,即Road Safety Message,路側安全消息,也是V2I,主要對接路側的邊緣設備,用於事件的識別,比如,車輛發生事故,車輛異常,異物闖入等等;
- SPAT,即Signal phase timing message,交通燈相位與時序消息,也是V2I,路側RSU集成信號機,或者信號機通過UU方式傳入到平臺,用於車速引導,綠波推送場景等等;
- MAP,地圖消息,地圖消息和SPAT消息一起使用,MAP消息可以描述一個路口,和該路口的紅綠燈也存在對應關係;
歐洲的ETSI也定義了類似的消息規範:
- CAM,Cooperative Awareness Message,合作感知信息,這是時間觸發信息,提供車輛的速度、位置、方向燈以及交通信號系統如交通信號燈的狀態,天氣提醒等信息;
- DENM,Decentralized Environmental Notification Message,分散環境通知信息,主要用於道路危險預警應用,是時間觸發型信息,一旦通過車載設備檢測到了安全隱患事件(例如前方車輛緊急剎車、道路施工警告等),車載ITS的相關應用就立即發射DENM信息,接收車輛可對比車輛自身位置與行車路線,判斷事件對自車的關聯性並預測可能的碰撞風險,以及提前通知駕駛員採取有效的措施,根據事件地點和類型,可能要求收到DENM信息的車輛向外轉發;
本文將按照歐洲標準規範,採用CAM來上報車輛的位置信息(每秒2次的頻率),採用DENM來上報車輛的緊急狀態事件(以緊急剎車爲例)。這2個消息協議的規範可以在ETSI的網站中找到,我選擇的是CAM ETSI EN 302 637-2 V1.4.1 以及 DENM ETSI EN 302 637-3 V1.3.1這2個最新版本的規範,下載對應的規範的PDF文檔後,可以在文檔中找到規範的具體的ASN格式的定義,注意這2個規範中都引用ETSI TS 102 894-2 V1.3.1的定義,因此在生成CAM和DENM的ASN文件時,需要把TS 102894這份規範裏面的字段定義也拷進去才能構成一個完整的ASN文件的定義。
有了CAM和DENM的ASN定義文件之後,我們就可以構建消息,採用UPER進行編碼,再把二進制數據編碼爲BASE64字符串,然後發送MQTT消息。
以下是模擬兩輛車在一段道路上行駛,這2輛車以每秒2條的頻率發送CAM消息報告位置,其中一輛車會隨機上報DENM消息。道路數據我是先用Google Map來進行GPS點的抓取,先在map上找一條道路,然後在其中以大致均勻的間隔取幾個點,然後調用Google的roads API,在path參數裏傳入這幾個點的座標,設置interpolate爲true,這樣Google就會自動幫我進行點的插值。我選了一條大概1.6公里的道路,傳入8個點,最終返回了54個點的座標。把返回值進行解析後保存這些點的座標在一個CSV文件中,作爲車輛的模擬行駛路線。
代碼如下:
import paho.mqtt.client as mqtt
import ssl
import base64
import asn1tools
import json
import time
import threading
import random
import datetime
start_timestamp = int(datetime.datetime(2004,1,1).timestamp()*1000)
def on_connect(client, userdata, flags, rc):
print("Connected with result code "+str(rc))
def on_message(client, userdata, msg):
print(msg.topic+" "+str(msg.payload))
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.tls_set(ca_certs="cert/root.crt", certfile="cert/vehicle1.crt", keyfile="cert/vehicle1_key.pem",
cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None)
client.connect("broker", 8883, 60)
# Define the message to publish
cam_asn = asn1tools.compile_files('CAM.asn', 'uper')
cam_json = {
'header': {
'protocolVersion': 1,
'messageID': 2,
'stationID': 123
},
'cam': {
'generationDeltaTime': 123,
'camParameters': {
'basicContainer': {
'stationType': 0,
'referencePosition': {
'latitude': 12345678,
'longitude': 87654321,
'positionConfidenceEllipse': {
'semiMajorConfidence': 4095,
'semiMinorConfidence': 4095,
'semiMajorOrientation': 3601
},
'altitude': {
'altitudeValue': 800001,
'altitudeConfidence': 'unavailable'
}
}
},
'highFrequencyContainer': (
'basicVehicleContainerHighFrequency', {
'heading': {
'headingValue': 3601,
'headingConfidence': 127
},
'speed': {
'speedValue': 30,
'speedConfidence': 127
},
'driveDirection': 'unavailable',
'vehicleLength': {
'vehicleLengthValue': 1023,
'vehicleLengthConfidenceIndication': 'unavailable'
},
'vehicleWidth': 62,
'longitudinalAcceleration': {
'longitudinalAccelerationValue': 161,
'longitudinalAccelerationConfidence': 102
},
'curvature': {
'curvatureValue': 1023,
'curvatureConfidence': 'unavailable'
},
'curvatureCalculationMode': 'unavailable',
'yawRate': {
'yawRateValue': 32767,
'yawRateConfidence': 'unavailable'
}
}
)
}
}
}
denm_asn = asn1tools.compile_files('DENM.asn', 'uper')
denm_json = {
'header': {
'protocolVersion': 1,
'messageID': 2,
'stationID': 123
},
'denm': {
'management': {
'actionID': {
'originatingStationID': 123,
'sequenceNumber': 1
},
'detectionTime': 1,
'referenceTime': 1,
'eventPosition': {
'latitude': 123,
'longitude': 321,
'positionConfidenceEllipse': {
'semiMajorConfidence': 4095,
'semiMinorConfidence': 4095,
'semiMajorOrientation': 3601
},
'altitude': {
'altitudeValue': 800001,
'altitudeConfidence': 'unavailable'
}
},
'validityDuration': 20,
'stationType': 0
},
'situation': {
'informationQuality': 0,
'eventType': {
'causeCode': 99,
'subCauseCode': 1
},
'linkedCause': {
'causeCode': 99,
'subCauseCode': 1
}
}
}
}
# Read the simulation location list
roads_csv = open('roads.csv', 'r')
lines = roads_csv.readlines()
locations = []
for i in range(1, len(lines)):
record = lines[i].split(',')
lat = int(float(record[1])*10000000)
lon = int(float(record[2])*10000000)
locations.append((lat, lon))
def publish_data(VIN, denm_flag=False):
for i in range(1):
rand_denm = random.randint(0, len(locations))
for j in range(len(locations)):
cam_json['cam']['camParameters']['basicContainer']['referencePosition']['latitude'] = locations[j][0]
cam_json['cam']['camParameters']['basicContainer']['referencePosition']['longitude'] = locations[j][1]
cam_encoded = cam_asn.encode('CAM', cam_json)
cam_b64 = str(base64.b64encode(cam_encoded),'utf-8')
client.publish("vehicle/cam/"+VIN,cam_b64)
if denm_flag:
if j == rand_denm:
denm_json['denm']['management']['actionID']['sequenceNumber'] = i
denm_json['denm']['management']['eventPosition']['latitude'] = locations[j][0]
denm_json['denm']['management']['eventPosition']['longitude'] = locations[j][1]
denm_json['denm']['situation']['eventType']['causeCode'] = 99
denm_json['denm']['situation']['eventType']['subCauseCode'] = 1
detect_timestamp = int(datetime.datetime.utcnow().timestamp()*1000) - start_timestamp
denm_json['denm']['management']['detectionTime'] = detect_timestamp
denm_json['denm']['management']['referenceTime'] = detect_timestamp
denm_encoded = denm_asn.encode('DENM', denm_json)
denm_b64 = str(base64.b64encode(denm_encoded),'utf-8')
client.publish("vehicle/denm/"+VIN,denm_b64)
time.sleep(0.5)
time.sleep(10)
class myThread (threading.Thread):
def __init__(self, VIN, denm_flag):
threading.Thread.__init__(self)
self.VIN = VIN
self.denm_flag = denm_flag
def run(self):
publish_data(self.VIN, self.denm_flag)
thread1 = myThread("Vehicle1", True)
thread2 = myThread("Vehicle2", False)
thread1.start()
time.sleep(1)
thread2.start()
thread1.join()
thread2.join()
client.disconnect()
服務器端訂閱CAM和DENM主題,並打印接收到的信息,代碼如下:
import paho.mqtt.client as mqtt
import ssl
import base64
import asn1tools
import json
import time
import threading
import random
import datetime
start_timestamp = int(datetime.datetime(2004,1,1).timestamp()*1000)
def on_connect(client, userdata, flags, rc):
print("Connected with result code "+str(rc))
def on_message(client, userdata, msg):
print(msg.topic+" "+str(msg.payload))
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.tls_set(ca_certs="cert/root.crt", certfile="cert/v2xapp.crt", keyfile="cert/v2xapp_key.pem",
cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None)
client.connect("broker", 8883, 60)
# Define the message to publish
cam_asn = asn1tools.compile_files('CAM.asn', 'uper')
cam_json = {
'header': {
'protocolVersion': 1,
'messageID': 2,
'stationID': 123
},
'cam': {
'generationDeltaTime': 123,
'camParameters': {
'basicContainer': {
'stationType': 0,
'referencePosition': {
'latitude': 12345678,
'longitude': 87654321,
'positionConfidenceEllipse': {
'semiMajorConfidence': 4095,
'semiMinorConfidence': 4095,
'semiMajorOrientation': 3601
},
'altitude': {
'altitudeValue': 800001,
'altitudeConfidence': 'unavailable'
}
}
},
'highFrequencyContainer': (
'basicVehicleContainerHighFrequency', {
'heading': {
'headingValue': 3601,
'headingConfidence': 127
},
'speed': {
'speedValue': 30,
'speedConfidence': 127
},
'driveDirection': 'unavailable',
'vehicleLength': {
'vehicleLengthValue': 1023,
'vehicleLengthConfidenceIndication': 'unavailable'
},
'vehicleWidth': 62,
'longitudinalAcceleration': {
'longitudinalAccelerationValue': 161,
'longitudinalAccelerationConfidence': 102
},
'curvature': {
'curvatureValue': 1023,
'curvatureConfidence': 'unavailable'
},
'curvatureCalculationMode': 'unavailable',
'yawRate': {
'yawRateValue': 32767,
'yawRateConfidence': 'unavailable'
}
}
)
}
}
}
denm_asn = asn1tools.compile_files('DENM.asn', 'uper')
denm_json = {
'header': {
'protocolVersion': 1,
'messageID': 2,
'stationID': 123
},
'denm': {
'management': {
'actionID': {
'originatingStationID': 123,
'sequenceNumber': 1
},
'detectionTime': 1,
'referenceTime': 1,
'eventPosition': {
'latitude': 123,
'longitude': 321,
'positionConfidenceEllipse': {
'semiMajorConfidence': 4095,
'semiMinorConfidence': 4095,
'semiMajorOrientation': 3601
},
'altitude': {
'altitudeValue': 800001,
'altitudeConfidence': 'unavailable'
}
},
'validityDuration': 20,
'stationType': 0
},
'situation': {
'informationQuality': 0,
'eventType': {
'causeCode': 99,
'subCauseCode': 1
},
'linkedCause': {
'causeCode': 99,
'subCauseCode': 1
}
}
}
}
client.subscribe('vehicle/#', 0)
client.loop_start()
start = time.time()
while True:
time.sleep(1)
end = time.time()
if int(end - start) > 30:
client.loop_stop()
break
client.disconnect()