Finalize project
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
FROM python:3.8-slim
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN pip install -r requirements.txt
|
||||
RUN cd deps; ls; pip install dse-shared-libs-1.tar.gz;
|
||||
ENTRYPOINT [ "python" ]
|
||||
CMD [ "orchestration_server.py" ]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
from orchestrator import Orchestrator
|
||||
|
||||
if __name__ == '__main__':
|
||||
Orchestrator().setup_msg_queues()
|
||||
@@ -0,0 +1,178 @@
|
||||
import datetime
|
||||
import pickle
|
||||
import sys
|
||||
import os
|
||||
from typing import List, Dict
|
||||
from datetime import datetime, timedelta
|
||||
from math import floor
|
||||
|
||||
import requests
|
||||
from dse_shared_libs import daf, traffic_light_state, traffic_light_color, target_velocity
|
||||
from dse_shared_libs.daf import DAF
|
||||
from dse_shared_libs.message_broker_wrapper import MBWrapper
|
||||
# necessary to unpickle daf object
|
||||
from dse_shared_libs.target_velocity import TargetVelocity
|
||||
from dse_shared_libs.traffic_light_state import TrafficLightState
|
||||
from dse_shared_libs.traffic_light_color import TrafficLightColor
|
||||
from requests import Session
|
||||
|
||||
sys.modules['daf'] = daf
|
||||
sys.modules['traffic_light_state'] = traffic_light_state
|
||||
sys.modules['traffic_light_color'] = traffic_light_color
|
||||
sys.modules['target_velocity'] = target_velocity
|
||||
|
||||
ENTITY_IDENT_URL = 'http://entityident:5002/api/v1/resources/'
|
||||
|
||||
SCALING = int(os.environ.get('DSE2021_SCALING', 1))
|
||||
|
||||
|
||||
class Orchestrator:
|
||||
# vehicle ids
|
||||
vins: List[str] = []
|
||||
# traffic lights {tlid: {color: color, switching_time: in_seconds}}
|
||||
tls: Dict[str, Dict] = {}
|
||||
|
||||
# cache all active incoming daf connections
|
||||
_daf_mbs: Dict[str, MBWrapper] = {}
|
||||
_velocity_mbs: Dict[str, MBWrapper] = {}
|
||||
|
||||
_session: Session
|
||||
|
||||
def __init__(self):
|
||||
# re-use http session, is faster then request.ing everytime
|
||||
self._session = requests.Session()
|
||||
|
||||
available = False
|
||||
while not available:
|
||||
try:
|
||||
response = self._session.get(ENTITY_IDENT_URL + 'cars')
|
||||
cars = response.json()
|
||||
|
||||
response = self._session.get(ENTITY_IDENT_URL + 'traffic_lights')
|
||||
traffic_lights = response.json()
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print("Is the entity_ident_server running and reachable?")
|
||||
continue
|
||||
available = True
|
||||
|
||||
for car in cars['cursor']:
|
||||
self.vins.append(car['vin'])
|
||||
|
||||
for traffic_light in traffic_lights['cursor']:
|
||||
self.tls[traffic_light['id']] = {'color': traffic_light['color'],
|
||||
'switching_time': traffic_light['switchingTime'],
|
||||
'last_switch': datetime.utcnow()}
|
||||
|
||||
def setup_msg_queues(self):
|
||||
"""
|
||||
Setup the message queues for communicating with vehicles and traffic lights.
|
||||
A receiving and a sending queue for vehicles.
|
||||
A receiving queue for traffic lights.
|
||||
"""
|
||||
# spawn the vehicle related message broker channels
|
||||
for vin in self.vins:
|
||||
daf_channel = MBWrapper(exchange_name='vehicle_daf_{}'.format(vin), callback=self.handle_daf_receive)
|
||||
daf_channel.setup_receiver()
|
||||
self._daf_mbs[vin] = daf_channel
|
||||
|
||||
velocity_channel = MBWrapper(exchange_name='vehicle_velocity_{}'.format(vin))
|
||||
velocity_channel.setup_sender()
|
||||
self._velocity_mbs[vin] = velocity_channel
|
||||
|
||||
# spawn the traffic light related mb channels
|
||||
for tl in self.tls:
|
||||
tl_channel = MBWrapper(exchange_name='traffic_light_state_{}'.format(tl),
|
||||
callback=self.handle_tl_state_receive)
|
||||
tl_channel.setup_receiver()
|
||||
|
||||
def handle_daf_receive(self, pickle_binary):
|
||||
"""
|
||||
Gets the daf object's pickle binary dump.
|
||||
Unpickle, calculate new target velocity based on daf and current tl data and
|
||||
respond new target velocity for this vehicle.
|
||||
|
||||
:param pickle_binary: daf object pickle binary dump
|
||||
"""
|
||||
received_daf_object: DAF = pickle.loads(pickle_binary)
|
||||
loc = received_daf_object.gps_location
|
||||
print('Received DAF object: {}'.format(received_daf_object))
|
||||
|
||||
response = self._session.get(ENTITY_IDENT_URL + 'traffic_lights_geo',
|
||||
params={'lat': loc.latitude, 'lon': loc.longitude})
|
||||
traffic_lights_geo = response.json()
|
||||
current_vel = received_daf_object.velocity
|
||||
|
||||
target_vel = self._compute_velocity(traffic_lights_geo, current_vel)
|
||||
|
||||
response_channel = self._velocity_mbs[received_daf_object.vehicle_identification_number]
|
||||
print('Target velocity: {}'.format(target_vel))
|
||||
response_channel.send(pickle.dumps(
|
||||
TargetVelocity(vin=received_daf_object.vehicle_identification_number, target_velocity=target_vel,
|
||||
timestamp=datetime.utcnow())))
|
||||
|
||||
def handle_tl_state_receive(self, msg):
|
||||
"""
|
||||
Gets a dict full of traffic light state information:
|
||||
{
|
||||
'tlid': str,
|
||||
'color': TrafficLightColor,
|
||||
'last_switch': datetime
|
||||
}
|
||||
Updates internal information about available TLs
|
||||
"""
|
||||
tl_state: TrafficLightState = pickle.loads(msg)
|
||||
self.tls[tl_state.tlid]['color'] = tl_state.color
|
||||
self.tls[tl_state.tlid]['last_switch'] = tl_state.last_switch
|
||||
print(tl_state)
|
||||
|
||||
def _compute_velocity(self, traffic_lights_geo, current_vel):
|
||||
target_vel = 130 * SCALING
|
||||
|
||||
print('Nearest traffic lights: {}'.format(traffic_lights_geo['cursor']))
|
||||
for traffic_light in traffic_lights_geo['cursor']:
|
||||
# Should only ever contain one traffic light (the next in line)
|
||||
tl_id = traffic_light['id']
|
||||
distance = traffic_light['calculatedRange']
|
||||
switching_time = self.tls[tl_id]['switching_time'] / float(SCALING)
|
||||
# Time until next switch must be scaled accordingly
|
||||
next_switch_time = self.tls[tl_id]['last_switch'] + timedelta(seconds=switching_time)
|
||||
time_until_switch = (next_switch_time - datetime.utcnow()).total_seconds()
|
||||
print('Distance to TL: {}'.format(distance))
|
||||
print('Time until switch in seconds: {}'.format(time_until_switch))
|
||||
|
||||
if self.tls[tl_id]['color'] is TrafficLightColor.RED:
|
||||
speed_needed_max = (distance / float(time_until_switch)) * 3.6
|
||||
if speed_needed_max < 130 * SCALING:
|
||||
if current_vel < speed_needed_max:
|
||||
target_vel = current_vel
|
||||
else:
|
||||
target_vel = floor(speed_needed_max)
|
||||
|
||||
else:
|
||||
# Cannot make it on next green
|
||||
i = 2
|
||||
while speed_needed_max > 130 * SCALING:
|
||||
next_green_phase_start = time_until_switch + switching_time * i
|
||||
speed_needed_max = (distance / float(next_green_phase_start)) * 3.6
|
||||
i = i + 2
|
||||
target_vel = floor(speed_needed_max)
|
||||
|
||||
elif self.tls[tl_id]['color'] is TrafficLightColor.GREEN:
|
||||
# Check if we can reach TL in time
|
||||
speed_needed_min = (distance / float(time_until_switch)) * 3.6
|
||||
if speed_needed_min < 130 * SCALING:
|
||||
if current_vel < speed_needed_min:
|
||||
target_vel = 130 * SCALING
|
||||
else:
|
||||
target_vel = current_vel
|
||||
else:
|
||||
i = 1
|
||||
speed_needed_max = 132 * SCALING
|
||||
while speed_needed_max > 130 * SCALING:
|
||||
next_green_phase_start = time_until_switch + switching_time * i
|
||||
speed_needed_max = (distance / float(next_green_phase_start)) * 3.6
|
||||
i = i + 2
|
||||
target_vel = floor(speed_needed_max)
|
||||
else:
|
||||
print('Unknown Traffic Light Color!')
|
||||
return target_vel
|
||||
@@ -0,0 +1,3 @@
|
||||
flask
|
||||
geopy
|
||||
requests
|
||||
@@ -0,0 +1,54 @@
|
||||
import datetime
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from dse_shared_libs.mock.response import MyResponse
|
||||
from dse_shared_libs.traffic_light_color import TrafficLightColor
|
||||
|
||||
from orchestrator import Orchestrator
|
||||
|
||||
|
||||
class TestOrchestrator(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.timestamp = datetime.datetime.utcnow()
|
||||
with patch('requests.sessions.Session.get', MyResponse):
|
||||
self.orc = Orchestrator()
|
||||
|
||||
def test_full_speed_if_nothing_in_range(self):
|
||||
tl_geo = {'cursor': []}
|
||||
current_vel = 55.0
|
||||
|
||||
target_vel = self.orc._compute_velocity(tl_geo, current_vel)
|
||||
self.assertEqual(130, target_vel)
|
||||
|
||||
def test_keep_speed_if_far_away_and_green(self):
|
||||
self.orc.tls = {'1': {'color': TrafficLightColor.GREEN, 'switching_time': 1000, 'last_switch': self.timestamp}}
|
||||
tl_geo = {'cursor': [
|
||||
{'id': '1', 'calculatedRange': 1000}
|
||||
]}
|
||||
current_vel = 55.0
|
||||
|
||||
target_vel = self.orc._compute_velocity(tl_geo, current_vel)
|
||||
self.assertEqual(current_vel, target_vel)
|
||||
|
||||
def test_slow_down_if_passing_RED_not_possible(self):
|
||||
self.orc.tls = {'1': {'color': TrafficLightColor.RED, 'switching_time': 100000, 'last_switch': self.timestamp}}
|
||||
tl_geo = {'cursor': [
|
||||
{'id': '1', 'calculatedRange': 1}
|
||||
]}
|
||||
current_vel = 130.0
|
||||
|
||||
target_vel = self.orc._compute_velocity(tl_geo, current_vel)
|
||||
self.assertNotEqual(current_vel, target_vel)
|
||||
self.assertEqual(0, target_vel)
|
||||
|
||||
def test_adjust_speed_to_get_over_on_green_if_currently_red(self):
|
||||
self.orc.tls = {'1': {'color': TrafficLightColor.RED, 'switching_time': 100, 'last_switch': self.timestamp}}
|
||||
tl_geo = {'cursor': [
|
||||
{'id': '1', 'calculatedRange': 1000}
|
||||
]}
|
||||
current_vel = 130.0
|
||||
|
||||
target_vel = self.orc._compute_velocity(tl_geo, current_vel)
|
||||
self.assertNotEqual(current_vel, target_vel)
|
||||
self.assertEqual(36.0, target_vel)
|
||||
Reference in New Issue
Block a user