Finalize project
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,99 @@
|
||||
import pickle
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from circuitbreaker import circuit
|
||||
|
||||
# Default switching time in seconds
|
||||
from dse_shared_libs.traffic_light_color import TrafficLightColor
|
||||
from dse_shared_libs.message_broker_wrapper import MBWrapper
|
||||
from dse_shared_libs.traffic_light_state import TrafficLightState
|
||||
from pika.exceptions import AMQPConnectionError
|
||||
|
||||
SWITCHING_TIME = 15
|
||||
# Scale speed of switching by factor x
|
||||
SCALING = int(os.environ.get('DSE2021_SCALING', 1))
|
||||
|
||||
|
||||
class TrafficLight:
|
||||
# id of traffic light
|
||||
tlid: str
|
||||
# time between switches in seconds
|
||||
switching_time: int
|
||||
# timestamp of last switch
|
||||
last_switch: datetime
|
||||
# currently lit color
|
||||
current_color: TrafficLightColor
|
||||
# marks if traffic light started
|
||||
running: bool = False
|
||||
# stores starting color
|
||||
_starting_color: TrafficLightColor
|
||||
# stores runner thread
|
||||
_t: threading.Thread
|
||||
|
||||
# outgoing traffic light state MBWrapper
|
||||
_tl_mb: MBWrapper
|
||||
|
||||
def __init__(self,
|
||||
tlid: str,
|
||||
switching_time: int = SWITCHING_TIME,
|
||||
starting_color: TrafficLightColor = TrafficLightColor.RED):
|
||||
"""
|
||||
Init a new Traffic Light client. This will already initialize its message queue.
|
||||
Start it with self.start().
|
||||
|
||||
:param tlid: ID of the traffic light
|
||||
:param switching_time: time in seconds to switch between states (TrafficLightColor.RED and .GREEN)
|
||||
:param starting_color: TrafficLightColor to start with
|
||||
"""
|
||||
self.tlid = tlid
|
||||
self.switching_time = switching_time
|
||||
self._starting_color = starting_color
|
||||
|
||||
self._tl_mb = MBWrapper(exchange_name='traffic_light_state_{}'.format(self.tlid))
|
||||
self._tl_mb.setup_sender()
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts the traffic light by spawning a new thread..
|
||||
"""
|
||||
self.running = True
|
||||
self._t = threading.Thread(target=self._switcher)
|
||||
self._t.start()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
The running thread will stop its execution at the next evaluation of while self.running.
|
||||
"""
|
||||
self.running = False
|
||||
|
||||
@circuit(failure_threshold=10, expected_exception=AMQPConnectionError)
|
||||
def send_status_update(self):
|
||||
"""
|
||||
Sends a status update containing an updated TrafficLightState object to the orchestrator.
|
||||
This update contains the tlid, current_color and timestamp of the last switch.
|
||||
"""
|
||||
print(self.tlid, self.current_color, self.last_switch)
|
||||
self._tl_mb.send(pickle.dumps(
|
||||
TrafficLightState(tlid=self.tlid, color=self.current_color, last_switch=self.last_switch)))
|
||||
|
||||
def _switcher(self):
|
||||
"""
|
||||
Toggles the traffic lights state every self.switching_time / SCALING seconds.
|
||||
When it switches, it notifies the orchestrator with self.send_status_update()
|
||||
"""
|
||||
num_colors = len(TrafficLightColor)
|
||||
# set current color to the one before starting color, because switch immediately happens in loop
|
||||
# therefore it sleeps on the end of the loop and first status is immediately sent to msg broker
|
||||
self.current_color = TrafficLightColor((self._starting_color.value - 1) % num_colors)
|
||||
while self.running:
|
||||
self.current_color = TrafficLightColor((self.current_color.value + 1) % num_colors)
|
||||
self.last_switch = datetime.utcnow()
|
||||
self.send_status_update()
|
||||
time.sleep(self.switching_time / SCALING)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
...
|
||||
@@ -0,0 +1,277 @@
|
||||
import pickle
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
import geopy
|
||||
import geopy.distance
|
||||
from circuitbreaker import circuit
|
||||
from dse_shared_libs.daf import DAF
|
||||
|
||||
from dse_shared_libs.message_broker_wrapper import MBWrapper
|
||||
|
||||
# Lat, Long
|
||||
from dse_shared_libs.target_velocity import TargetVelocity
|
||||
from pika.exceptions import AMQPConnectionError
|
||||
|
||||
STARTING_POINT = geopy.Point(47.89053, 16.20703)
|
||||
# Driving direction in degrees: 0=N, 90=E, 180=S, 270=W
|
||||
BEARING = 2
|
||||
# Scale speed of vehicles by factor x
|
||||
SCALING = int(os.environ.get('DSE2021_SCALING', 1))
|
||||
# in km/h
|
||||
STARTING_VELOCITY = 130 * SCALING
|
||||
# Interval between status updates in seconds (is not scaled)
|
||||
UPDATE_INTERVAL = 1
|
||||
# Specify if NCE should happen
|
||||
NCE = int(os.environ.get('DSE2021_NCE', 1))
|
||||
# At x km the NCE shall happen
|
||||
NCE_KM = 2.4
|
||||
# Time in seconds to recover from NCE (will be scaled)
|
||||
TIME_TO_RECOVER = 15
|
||||
# Resets vehicle at km x
|
||||
RESET_KM = 4.3
|
||||
|
||||
|
||||
class Vehicle:
|
||||
# vehicle identification number
|
||||
vin: str
|
||||
# velocity in km/h
|
||||
velocity: float
|
||||
# stores the timestamp most recently used to calculate driving distance
|
||||
last_update: Union[datetime, None]
|
||||
# marks if vehicle is currently driving, a new one isn't, has to be started via self.start_driving()
|
||||
driving: bool = False
|
||||
# stores last velocity before NCE, is used to recover after NCE
|
||||
_last_velocity: float
|
||||
# stores the so-far driven kilometers
|
||||
_driven_kms: int
|
||||
# stores the starting point for resetting the vehicle
|
||||
_starting_point: geopy.Point
|
||||
# stores the gps_location, shall be accessed by gps_location, where the actual value is calculated and returned
|
||||
_gps_location: geopy.Point
|
||||
# stores if nce possible
|
||||
_nce_possible = True
|
||||
# stores if nce already happened, it only happens (up to) once
|
||||
_nce_happened = False
|
||||
# stores if we are still recovering from NCE
|
||||
_recovering = False
|
||||
|
||||
# continuous driving thread
|
||||
_t: threading.Thread
|
||||
# NCE recovery thread
|
||||
_rt: threading.Thread
|
||||
|
||||
# outgoing daf info MBWrapper
|
||||
_daf_mb: MBWrapper
|
||||
# incoming target velocity MBWrapper
|
||||
_velocity_mb: MBWrapper
|
||||
|
||||
def __init__(self,
|
||||
vin: str,
|
||||
starting_point: geopy.Point = STARTING_POINT,
|
||||
starting_velocity: float = STARTING_VELOCITY):
|
||||
"""
|
||||
Initialize a new vehicle client. Already initializes the corresponding sending and receiving message queue.
|
||||
A vehicle sends every UPDATE_INTERVAL seconds its current DAF to the orchestrator. It receives as response the
|
||||
new TargetVelocity to achieve a possible green wave. After a fixed amount of kilometers, the NCE event is
|
||||
happening. The car then stands still for TIME_TO_RECOVER seconds. Afterwards, it starts driving with the last
|
||||
known velocity. While it is recovering, it ignores the target velocity responses from the orchestrator.
|
||||
A vehicle drives a full route between start and end. After reaching the end, it restores the internal state and
|
||||
starts again from the beginning.
|
||||
|
||||
:param vin: Vehicle Identification Number
|
||||
:param starting_point: point on globe to start at
|
||||
:param starting_velocity: velocity to start driving with
|
||||
"""
|
||||
self.vin = vin
|
||||
self._starting_point = starting_point
|
||||
self._gps_location = starting_point
|
||||
self.velocity = starting_velocity
|
||||
|
||||
self._daf_mb = MBWrapper(exchange_name='vehicle_daf_{}'.format(vin))
|
||||
self._daf_mb.setup_sender()
|
||||
|
||||
self._velocity_mb = MBWrapper(exchange_name='vehicle_velocity_{}'.format(self.vin), callback=self.new_velocity)
|
||||
self._velocity_mb.setup_receiver()
|
||||
|
||||
@property
|
||||
def nce(self):
|
||||
"""
|
||||
On accessing this property, it is calculated if the NCE shall happen. NCE only happens up to once per route.
|
||||
If the NCE happens, the car stops driving (velocity = 0) for TIME_TO_RECOVER seconds. In this time,
|
||||
responses from the orchestrator are ignored. After recovery, the vehicle starts driving again with the last
|
||||
known velocity.
|
||||
|
||||
:return: True if NCE invoked, otherwise False
|
||||
"""
|
||||
d = geopy.distance.distance(kilometers=0.4)
|
||||
tl2_loc = geopy.Point(47.90937, 16.20814)
|
||||
# Calculate point 400m south of traffic light 2
|
||||
nce_point = d.destination(point=tl2_loc, bearing=182)
|
||||
if NCE == 1 and self._nce_possible and not self._nce_happened:
|
||||
if self._gps_location.latitude >= nce_point.latitude:
|
||||
self._nce_happened = True
|
||||
self._last_velocity = self.velocity
|
||||
self.velocity = 0
|
||||
|
||||
def _sleep_and_recover():
|
||||
recover_in = TIME_TO_RECOVER / SCALING
|
||||
self._recovering = True
|
||||
print('\nNCE !!! Recovering in {} (scaled) seconds.\n'.format(recover_in))
|
||||
time.sleep(recover_in)
|
||||
print('\nRecovered.\n')
|
||||
self._recovering = False
|
||||
self.velocity = self._last_velocity
|
||||
|
||||
self._rt = threading.Thread(target=_sleep_and_recover)
|
||||
self._rt.start()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@nce.setter
|
||||
def nce(self, val):
|
||||
self._nce_happened = val
|
||||
|
||||
@property
|
||||
def daf(self):
|
||||
"""
|
||||
Return the DAF object of the vehicle. The properties are always evaluated on calling. That guarantees accurate
|
||||
values.
|
||||
|
||||
:return: current "Datenaufzeichnung für automatisiertes Fahren" (DAF) object
|
||||
"""
|
||||
|
||||
# ATTENTION: ORDER MANDATORY (except for static vin)
|
||||
return DAF(vehicle_identification_number=self.vin,
|
||||
# first deduce nce - is calculated, sets velocity to 0 if NCE
|
||||
near_crash_event=self.nce,
|
||||
# then get current location based on last location and current speed
|
||||
gps_location=self.gps_location,
|
||||
# finally get last_update, is updated by gps_location deduction
|
||||
timestamp=self.last_update,
|
||||
# is set to 0 if nce
|
||||
velocity=self.velocity,
|
||||
)
|
||||
|
||||
@property
|
||||
def gps_location(self):
|
||||
"""
|
||||
An accurate current position of the vehicle at the moment the property is called is retunred.
|
||||
The gps location is derived from the last position this property was called at and the
|
||||
time the vehicle was driving since then. Therefore, it is not necessary to call this function exactly at a given
|
||||
time span, because it calculates the driven kms relative to the calling time.
|
||||
"""
|
||||
# Define starting point.
|
||||
start = self._gps_location
|
||||
|
||||
# Get old and updated timestamps
|
||||
old_timestamp = self.last_update
|
||||
updated_timestamp = datetime.utcnow()
|
||||
self.last_update = updated_timestamp
|
||||
|
||||
# get driving time between timestamps (in seconds)
|
||||
driving_time = (updated_timestamp - old_timestamp).total_seconds()
|
||||
|
||||
# reached distance in kilometers: convert km/h to km/s and multiply by driving time
|
||||
kilometers = self.velocity / 3600 * driving_time
|
||||
|
||||
# Define a general distance object, initialized with a distance of k km.
|
||||
d = geopy.distance.distance(kilometers=kilometers)
|
||||
|
||||
# Use the `destination` method with a bearing of BEARING degrees
|
||||
# in order to move from point `start` to kilometers km bearing.
|
||||
self._gps_location = d.destination(point=start, bearing=BEARING)
|
||||
|
||||
# update driven kilometers
|
||||
self._driven_kms += kilometers
|
||||
return self._gps_location
|
||||
|
||||
@property
|
||||
def driven_kms(self):
|
||||
"""
|
||||
:returns: a string representation of the driven kms
|
||||
"""
|
||||
return '{}km'.format(round(self._driven_kms, 2))
|
||||
|
||||
def start_driving(self):
|
||||
"""
|
||||
Starts a thread responsible for driving. This thread continuously updates internal values and
|
||||
informs the message broker about the current state (DAF) of the vehicle.
|
||||
"""
|
||||
print('{} starts driving ... SCALING: x{}\n\n'.format(self.vin, SCALING))
|
||||
self.last_update = datetime.utcnow()
|
||||
self._driven_kms = 0
|
||||
|
||||
self._t = threading.Thread(target=self.drive)
|
||||
self._t.start()
|
||||
|
||||
def drive(self):
|
||||
"""
|
||||
Drive until not self.driving == update status every UPDATE_INTERVAL seconds.
|
||||
"""
|
||||
self.driving = True
|
||||
while self.driving:
|
||||
self.check_reset()
|
||||
self.send_status_update()
|
||||
time.sleep(UPDATE_INTERVAL)
|
||||
|
||||
def stop_driving(self):
|
||||
"""
|
||||
Set self.driving to stop driving thread gracefully.
|
||||
"""
|
||||
self.driving = False
|
||||
self._gps_location = self._starting_point
|
||||
self._driven_kms = 0
|
||||
|
||||
def check_reset(self):
|
||||
"""
|
||||
Checks if end of route is reached and resets vehicle to starting conditions.
|
||||
It also resets on malicious coordinates.
|
||||
|
||||
Afterwards, the vehicle is still driving, but again from start.
|
||||
"""
|
||||
if self._driven_kms >= RESET_KM:
|
||||
self._reset()
|
||||
|
||||
if self._gps_location.latitude < self._starting_point.latitude or \
|
||||
self._gps_location.longitude < self._starting_point.longitude:
|
||||
self._reset()
|
||||
|
||||
@circuit(failure_threshold=10, expected_exception=AMQPConnectionError)
|
||||
def send_status_update(self):
|
||||
"""
|
||||
Sends the current DAF to the orchestrator. The orchestrator will then respond asynchronously on the response
|
||||
queue.
|
||||
"""
|
||||
print(self.driven_kms, '\t', self.daf)
|
||||
self._daf_mb.send(pickle.dumps(self.daf))
|
||||
|
||||
def new_velocity(self, response: bytes):
|
||||
"""
|
||||
Will be invoked if new target velocity message received from orchestrator.
|
||||
:param response: pickled TargetVelocity object
|
||||
"""
|
||||
response: TargetVelocity = pickle.loads(response)
|
||||
velocity = response.target_velocity
|
||||
print('Received new velocity {}'.format(velocity))
|
||||
|
||||
if not self._recovering:
|
||||
self.velocity = velocity
|
||||
else:
|
||||
print('We are still recovering ... ignoring new target velocity.')
|
||||
|
||||
def _reset(self):
|
||||
print('\n\nEnd of route reached ... resetting and restarting vehicle')
|
||||
self._gps_location = self._starting_point
|
||||
self._driven_kms = 0
|
||||
self.last_update = datetime.utcnow()
|
||||
self.nce = False
|
||||
self.velocity = STARTING_VELOCITY
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
...
|
||||
Reference in New Issue
Block a user