Finalize project

This commit is contained in:
2021-06-18 15:29:43 +02:00
parent cf7bb3b853
commit 22e06ba6a3
129 changed files with 3 additions and 434 deletions
+67
View File
@@ -0,0 +1,67 @@
# DSE2021 Gruppe 08 Vehice 2 Infrastructure
Anuglar Frontend
Python Bakcned
* Start both middleware AND frontend dev server simultaneously
* See README of subfolder for further information
## Local testing
Add to hostsfile:
`127.0.0.1 rdg
127.0.0.1 mongodb
127.0.0.1 redis`
## Deploy Kubernetes Admin Dashboard
Deploy dashboard:
```
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0/aio/deploy/recommended.yaml
```
Create dashboard-admin.yaml file:
```
apiVersion: v1
kind: ServiceAccount
metadata:
name: admin-user
namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: admin-user
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: admin-user
namespace: kubernetes-dashboard
```
Apply it to kubernetes:
```
kubectl apply -f dashboard-admin.yaml
```
Get login token:
```
kubectl get secret -n kubernetes-dashboard $(kubectl get serviceaccount admin-user -n kubernetes-dashboard -o jsonpath="{.secrets[0].name}") -o jsonpath="{.data.token}" | base64 --decode
```
Start proxy:
```
kubectl proxy
```
Access URL: http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/#/ingress?namespace=default
## Fix Darwin issue with Ingress
```
minikube stop && minikube delete && minikube start --vm=true --driver=hyperkit
```
@@ -0,0 +1,18 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
@@ -0,0 +1,2 @@
dist
node_modules
@@ -0,0 +1,13 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false
@@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
@@ -0,0 +1,17 @@
# Stage 1
FROM node:alpine AS build-image
WORKDIR /app
COPY . .
RUN npm ci && npm run build -- --configuration=production
# Stage 2
FROM nginx:alpine
COPY --from=build-image /app/dist/app-fe /usr/share/nginx/html
EXPOSE 80
@@ -0,0 +1,14 @@
# AIC Angular Frontend
## Tips
* use Jetbrains Webstorm IDE
* don't forget your browser's console (F12)
## Prerequisites
* install [nodejs](https://nodejs.org/en/)
* install [angular-cli globally](https://cli.angular.io/)
## Setup
$ npm install
$ ng serve
@@ -0,0 +1,128 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"app-fe": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/app-fe",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "app-fe:build"
},
"configurations": {
"production": {
"browserTarget": "app-fe:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "app-fe:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "app-fe:serve"
},
"configurations": {
"production": {
"devServerTarget": "app-fe:serve:production"
}
}
}
}
}
},
"defaultProject": "app-fe",
"cli": {
"analytics": false
}
}
@@ -0,0 +1,14 @@
import { AppPage } from './app.po';
describe('frontend-dev App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to app!');
});
});
@@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}
@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}
@@ -0,0 +1,56 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-htmlfile-reporter'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, 'coverage'), reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
htmlReporter: {
outputFile: 'unit-tests.html',
// Optional
pageTitle: 'Unit Tests',
subPageTitle: 'A sample project description',
groupSuites: true,
useCompactStyle: true,
useLegacyStyle: true
},
reporters: ['progress', 'html'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome', 'ChromeHeadless'],
customLaunchers: {
ChromeHeadless: {
base: 'Chrome',
flags: [
'--headless',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu',
'--disable-dev-shm-usage',
'--remote-debugging-port=9222'
]
}
},
singleRun: true
});
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,60 @@
{
"name": "app-fe",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"postinstall": "ngcc"
},
"private": true,
"dependencies": {
"@agm/core": "^3.0.0-beta.0",
"@angular-devkit/schematics": "~9.1.7",
"@angular/animations": "~9.1.9",
"@angular/cdk": "9.2.4",
"@angular/common": "~9.1.9",
"@angular/compiler": "~9.1.9",
"@angular/core": "~9.1.9",
"@angular/forms": "~9.1.9",
"@angular/localize": "~9.1.9",
"@angular/material": "9.2.4",
"@angular/platform-browser": "~9.1.9",
"@angular/platform-browser-dynamic": "~9.1.9",
"@angular/router": "~9.1.9",
"@types/uuid": "8.3.0",
"bootstrap": "~4.3.1",
"jquery": "3.5.1",
"ngx-logger": "4.1.9",
"popper.js": "1.16.0",
"rxjs": "~6.5.5",
"tslib": "^1.10.0",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.7",
"@angular/cli": "~9.1.7",
"@angular/compiler-cli": "~9.1.9",
"@angular/language-service": "~9.1.9",
"@compodoc/compodoc": "~1.1.11",
"@types/googlemaps": "^3.39.13",
"@types/jasmine": "~3.4.6",
"@types/jasminewd2": "~2.0.8",
"@types/node": "^12.12.42",
"codelyzer": "^5.2.2",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.4.1",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.5.4",
"protractor": "~5.4.4",
"ts-node": "~8.5.4",
"tslint": "~5.20.1",
"typescript": "~3.8.3"
}
}
@@ -0,0 +1,29 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const {SpecReporter} = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function () {
}
},
onPrepare() {
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}}));
}
};
@@ -0,0 +1,10 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
@@ -0,0 +1,49 @@
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {AppRoutingModule} from './app-routing.module';
import {LandingComponent} from './component/landing/landing.component';
import {RestService} from './services/rest.service';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {InterceptorService} from './services/interceptor.service';
import {LoggerModule, NgxLoggerLevel} from 'ngx-logger';
import {environment} from '../environments/environment';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatFormFieldModule} from '@angular/material/form-field';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatButtonModule} from '@angular/material/button';
import {MatInputModule} from '@angular/material/input';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {AgmCoreModule, GoogleMapsAPIWrapper} from '@agm/core';
@NgModule({
declarations: [LandingComponent],
imports: [
ReactiveFormsModule,
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
LoggerModule.forRoot({level: environment.log_level, serverLogLevel: NgxLoggerLevel.ERROR}),
HttpClientModule,
MatFormFieldModule,
FormsModule,
MatButtonModule,
MatInputModule,
MatSlideToggleModule,
AgmCoreModule.forRoot({
apiKey: 'AIzaSyCXF-INAlIkG9uPBofGLUCyHthaLxN-3PY',
})
],
// enables injecting
providers: [
GoogleMapsAPIWrapper,
RestService,
{
provide: HTTP_INTERCEPTORS, useClass: InterceptorService, multi: true
},
],
bootstrap: [LandingComponent]
})
export class AppModule {
}
@@ -0,0 +1,3 @@
#map{
height : 100vh; width: 100%; padding:0px; margin: 0px;
}
@@ -0,0 +1,29 @@
<agm-map disableDefaultUI="true" id="map" #map [zoom]="zoom" [latitude]="center.lat" [longitude]="center.lng">
<agm-marker (markerClick)="openInfoWindow(m.value.carEntity.vin)" [openInfoWindow]="true"
*ngFor="let m of car_markers | keyvalue" [iconUrl]="getIconUrlCar(m.value.carEvent.near_crash_event)"
[latitude]="m.value.carEvent.gps_location.latitude" [longitude]="m.value.carEvent.gps_location.longitude" [animation]="(m.value.carEvent.near_crash_event)?'BOUNCE':''">
<agm-info-window style="opacity: 0.5!important;" (infoWindowClose)="closeInfoWindow(m.value.carEntity.vin)"
[isOpen]="isInfoWindowOpen(m.value.carEntity.vin)" [latitude]="m.value.carEvent.gps_location.latitude"
[longitude]="m.value.carEvent.gps_location.longitude">
<p>VIN: {{m.value.carEntity.vin}}</p>
<p>OEM: {{m.value.carEntity.oem}}</p>
<p>Model Type: {{m.value.carEntity.modelType}}</p>
<p>Velocity: {{m.value.carEvent.velocity}}</p>
<p>Timestamp: {{m.value.carEvent.timestamp}}</p>
<p>NCE: {{m.value.carEvent.near_crash_event}}</p>
</agm-info-window>
</agm-marker>
<agm-marker *ngFor="let m of traffic_light_markers | keyvalue" [iconUrl]="trafficLightToImage(m.value.trafficLightEvent.color)"
[latitude]="m.value.trafficLightEntity.location[1]" [longitude]="m.value.trafficLightEntity.location[0]">
<agm-info-window>
<p>Id: {{m.value.trafficLightEntity.id}}</p>
<p>Switching Time: {{m.value.trafficLightEntity.switchingTime}}</p>
<p>Range: {{m.value.trafficLightEntity.range}}</p>
<p>Color: {{m.value.trafficLightEvent.color}}</p>
<p>Last Switch: {{m.value.trafficLightEvent.last_switch}}</p>
</agm-info-window>
</agm-marker>
</agm-map>
@@ -0,0 +1,193 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {AgmMap} from '@agm/core';
import {RestService} from '../../services/rest.service';
import {NGXLogger} from 'ngx-logger';
import {interval, Subscription} from 'rxjs';
import {startWith, switchMap} from 'rxjs/operators';
import {
Car,
CarEntity,
CarEvent,
GeoCoordinates, TrafficLight,
TrafficLightEntity, TrafficLightEvent
} from '../../interfaces/interface';
@Component({
selector: 'app-landing',
templateUrl: './landing.component.html',
styleUrls: ['./landing.component.css']
})
export class LandingComponent implements OnInit {
constructor(
private restService: RestService,
private logger: NGXLogger
) {
}
@ViewChild('map') map: AgmMap;
zoom = 14;
center = {lat: 47.90620, lng: 16.20785};
traffic_light_markers = new Map();
car_markers = new Map();
infoWindows = new Map();
timeInterval: Subscription;
TL_RED_IMAGE = 'assets/pictures/traffic_light_red.png';
TL_GREEN_IMAGE = 'assets/pictures/traffic_light_green.png';
CAR_IMAGE = 'assets/pictures/car.png';
CAR_IMAGE_NCE = 'assets/pictures/car_orange.png';
NCE_SOUND = 'assets/sound/crash.mp3';
ngOnInit() {
this.getTrafficLights();
this.getCars();
}
getCarEvents(vin) {
this.timeInterval = interval(1000)
.pipe(
startWith(0),
switchMap(() => this.restService.getCarEvents(vin))
).subscribe((data: any) => {
if (data.body === '') {
return;
}
const carEvent = data.body as CarEvent;
const car = this.car_markers.get(vin) as Car;
car.carEvent = carEvent;
this.car_markers.set(car['vin'], car);
},
err => this.logger.error(err),
() => {
this.logger.debug('loaded car events');
}
);
}
getTrafficLightEvents(tlid) {
this.timeInterval = interval(1000)
.pipe(
startWith(0),
switchMap(() => this.restService.getTrafficLightEvents(tlid))
).subscribe(
(data: any) => {
const trafficLightEvent = data.body as TrafficLightEvent;
const trafficLight = this.traffic_light_markers.get(tlid) as TrafficLight;
trafficLight.trafficLightEvent = trafficLightEvent;
this.traffic_light_markers.set(tlid, trafficLight);
},
err => this.logger.error(err),
() => {
this.logger.debug('loaded traffic light events');
}
);
}
getTrafficLights() {
this.restService.getTrafficLights().subscribe(
(data: TrafficLightEntity[]) => {
for (const trafficLightEntity of data) {
const trafficLight: TrafficLight = {
trafficLightEntity: trafficLightEntity,
trafficLightEvent: this.getEmptyTrafficLightEvent()
};
this.traffic_light_markers.set(trafficLightEntity['id'], trafficLight);
}
},
err => this.logger.error(err),
() => {
this.logger.debug('loaded traffic lights');
for (const value of this.traffic_light_markers.values()) {
this.getTrafficLightEvents(value.trafficLightEntity['id']);
}
}
);
}
getCars() {
this.restService.getCars().subscribe(
(data: CarEntity[]) => {
for (const carEntity of data) {
const car: Car = {
carEntity: carEntity,
carEvent: this.getEmptyCarEvent()
};
this.car_markers.set(carEntity['vin'], car);
}
},
err => this.logger.error(err),
() => {
this.logger.debug('loaded cars');
for (const value of this.car_markers.values()) {
this.getCarEvents(value.carEntity['vin']);
}
}
);
}
trafficLightToImage(trafficLight) {
return (trafficLight === 'RED') ? this.TL_RED_IMAGE : this.TL_GREEN_IMAGE;
}
getIconUrlCar(near_crash_event) {
if (near_crash_event === true) {
this.playCrashSound();
return this.CAR_IMAGE_NCE;
} else {
return this.CAR_IMAGE;
}
}
openInfoWindow(id) {
this.infoWindows.set(id, true);
}
closeInfoWindow(id) {
this.infoWindows.delete(id);
}
isInfoWindowOpen(id) {
return this.infoWindows.has(id);
}
playCrashSound() {
const audio = new Audio();
audio.src = this.NCE_SOUND;
audio.load();
audio.play();
}
private getEmptyGeoCoordinates(): GeoCoordinates {
return {
latitude: 0,
longitude: 0
};
}
private getEmptyCarEvent(): CarEvent {
return {
near_crash_event: false,
gps_location: this.getEmptyGeoCoordinates(),
timestamp: '',
velocity: 0
};
}
private getEmptyTrafficLightEvent(): TrafficLightEvent {
return {
last_switch: 0,
color: '',
tlid: 0
};
}
}
@@ -0,0 +1,49 @@
import {Observable} from 'rxjs';
export interface WSEvents {
message: Observable<Event>;
error: Observable<Event>;
close: Observable<Event>;
}
export interface GeoCoordinates {
latitude: Number;
longitude: Number;
}
export interface CarEntity {
oem: String;
modelType: String;
vin: String;
}
export interface CarEvent {
near_crash_event: Boolean;
gps_location: GeoCoordinates;
timestamp: String;
velocity: Number;
}
export interface Car {
carEntity: CarEntity;
carEvent: CarEvent;
}
export interface TrafficLightEntity {
id: Number;
location: Number[];
range: Number;
switchingTime: Number;
color: String;
}
export interface TrafficLightEvent {
last_switch: Number;
color: String;
tlid: Number;
}
export interface TrafficLight {
trafficLightEntity: TrafficLightEntity;
trafficLightEvent: TrafficLightEvent;
}
@@ -0,0 +1,52 @@
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {tap} from 'rxjs/operators';
import {NGXLogger} from 'ngx-logger';
@Injectable({
providedIn: 'root'
})
/**
* The InterceptorService intercepts EVERY http event.
*/
export class InterceptorService implements HttpInterceptor {
constructor(private logger: NGXLogger) {
}
/**
* Intercepts every HTTP request and for example adds a token
* @param req http request
* @param next http response handler
*/
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// TODO get the real token
const token = 'A_TOKEN';
// the original request is immutable, so we need to clone it
req = req.clone({
setHeaders: {
// FIXME e.g. if Bearer is used
Authorization: `Bearer ${token}`
}
});
//this.logger.debug('Interceptor works');
// pipe the response observable
return next.handle(req).pipe(
// perform a side effect for every emission on the source Observable
// return an Observable that is identical to the source
tap(event => {
// check if it is the response message
if (event instanceof HttpResponse) {
// TODO so something if necessary
}
}, error => {
// TODO handle error here
this.logger.error('HTTP Response interception error', error);
})
);
}
}
@@ -0,0 +1,32 @@
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {NGXLogger} from 'ngx-logger';
import {environment} from '../../environments/environment';
@Injectable()
export class RestService {
private currentLocation = environment.location + '/api/v1/resources/';
constructor(
private logger: NGXLogger,
private http: HttpClient
) {
}
getTrafficLightEvents(tlid) {
return this.http.get(this.currentLocation + 'traffic_light_events?id=' + tlid, {observe: 'response'});
}
getCarEvents(vin) {
return this.http.get(this.currentLocation + 'car_events?vin=' + vin, {observe: 'response'});
}
getTrafficLights() {
return this.http.get(this.currentLocation + 'traffic_lights');
}
getCars() {
return this.http.get(this.currentLocation + 'cars');
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

@@ -0,0 +1,7 @@
import {NgxLoggerLevel} from 'ngx-logger';
export const environment = {
production: true,
location: '/xway',
log_level: NgxLoggerLevel.WARN,
};
@@ -0,0 +1,20 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
import {NgxLoggerLevel} from 'ngx-logger';
export const environment = {
production: false,
location: 'http://xway:5004',
log_level: NgxLoggerLevel.DEBUG,
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AppFe</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCXF-INAlIkG9uPBofGLUCyHthaLxN-3PY"></script>
</head>
<body>
<app-landing></app-landing>
</body>
</html>
<!--<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>-->
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>-->
<!--<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>-->
@@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
@@ -0,0 +1,79 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
(window as any).global = window;
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';
/** Evergreen browsers require these. **/
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
// import 'core-js/es7/reflect';
/**
* Required to support Web Animations `@angular/platform-browser/animations`.
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
**/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
*/
// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
/**
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*/
// (window as any).__Zone_enable_cross_context_check = true;
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
import '@angular/localize/init';
@@ -0,0 +1,14 @@
/* You can add global styles to this file, and also import other style files */
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
@import '~bootstrap/dist/css/bootstrap.min.css';
body {
width: 100%;
margin: 0;
padding: 0;
}
.gm-style-iw-a {
opacity: 0.8 !important;
}
@@ -0,0 +1,25 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
@@ -0,0 +1,20 @@
@import '~@angular/material/theming';
@include mat-core();
$my-app-primary: mat-palette($mat-grey);
$my-app-accent: mat-palette($mat-amber, 500, 900, A100);
$my-app-warn: mat-palette($mat-red, 700, 900);
$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn);
@include angular-material-theme($my-app-theme);
.alternate-theme {
$alternate-primary: mat-palette($mat-light-blue);
$alternate-accent: mat-palette($mat-yellow, 400);
$alternate-theme: mat-light-theme($alternate-primary, $alternate-accent);
@include angular-material-theme($alternate-theme);
}
@@ -0,0 +1,15 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}
@@ -0,0 +1,23 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"module": "esnext",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2017",
"dom"
],
"allowJs": true
}
}
@@ -0,0 +1,142 @@
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"deprecation": {
"severity": "warn"
},
"eofline": true,
"forin": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-misused-new": true,
"no-non-null-assertion": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
true,
"single"
],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"unified-signatures": true,
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"no-output-on-prefix": true,
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true
}
}
@@ -0,0 +1,8 @@
FROM python:3.8-slim
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
ENV PORT 5002
EXPOSE 5002
ENTRYPOINT [ "python" ]
CMD [ "entity_ident_server.py" ]
@@ -0,0 +1,106 @@
import os
from bson import json_util
from flask import Flask, request
from flask_pymongo import PyMongo
app = Flask(__name__)
app.config["MONGO_URI"] = "mongodb://mongo:27017/entities"
mongo = PyMongo(app)
CAR1_SV = int(os.environ.get('DSE2021_CAR1_SV', 130))
CAR2_SV = int(os.environ.get('DSE2021_CAR2_SV', 130))
CAR3_SV = int(os.environ.get('DSE2021_CAR3_SV', 130))
CAR1_SD = int(os.environ.get('DSE2021_CAR1_SD', 300))
CAR2_SD = int(os.environ.get('DSE2021_CAR2_SD', 500))
CAR3_SD = int(os.environ.get('DSE2021_CAR3_SD', 400))
CAR1_ST = int(os.environ.get('DSE2021_CAR1_ST', 10))
CAR2_ST = int(os.environ.get('DSE2021_CAR2_ST', 15))
CAR3_ST = int(os.environ.get('DSE2021_CAR3_ST', 25))
TL1_R = int(os.environ.get('DSE2021_TL1_R', 2000))
TL2_R = int(os.environ.get('DSE2021_TL2_R', 800))
TL3_R = int(os.environ.get('DSE2021_TL3_R', 1000))
mongo.db.trafficLights.update_one({"id": "1"}, {"$set": {"range": TL1_R}})
mongo.db.trafficLights.update_one({"id": "2"}, {"$set": {"range": TL2_R}})
mongo.db.trafficLights.update_one({"id": "3"}, {"$set": {"range": TL3_R}})
mongo.db.cars.update_one({"vin": "SCBFR7ZA5CC072256"}, {"$set": {"startingVelocity": CAR1_SV}})
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812715"}, {"$set": {"startingVelocity": CAR2_SV}})
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812716"}, {"$set": {"startingVelocity": CAR3_SV}})
mongo.db.cars.update_one({"vin": "SCBFR7ZA5CC072256"}, {"$set": {"startingDistance": CAR1_SD}})
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812715"}, {"$set": {"startingDistance": CAR2_SD}})
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812716"}, {"$set": {"startingDistance": CAR3_SD}})
mongo.db.cars.update_one({"vin": "SCBFR7ZA5CC072256"}, {"$set": {"startingTime": CAR1_ST}})
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812715"}, {"$set": {"startingTime": CAR2_ST}})
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812716"}, {"$set": {"startingTime": CAR3_ST}})
@app.route('/api/v1/resources/cars', methods=['GET'])
def get_cars():
query_parameters = request.args
vin = query_parameters.get('vin')
if vin is not None:
cars = [car for car in mongo.db.cars.find({'vin': vin})]
else:
cars = [car for car in mongo.db.cars.find({})]
return json_util.dumps({'cursor': cars})
@app.route('/api/v1/resources/traffic_lights', methods=['GET'])
def get_traffic_lights():
query_parameters = request.args
tl_id = query_parameters.get('id')
if tl_id is not None:
traffic_lights = [traffic_light for traffic_light in
mongo.db.trafficLights.find({'id': tl_id})]
else:
traffic_lights = [traffic_light for traffic_light in mongo.db.trafficLights.find({})]
return json_util.dumps({'cursor': traffic_lights})
@app.route('/api/v1/resources/traffic_lights_geo', methods=['GET'])
def get_traffic_lights_geo():
query_parameters = request.args
lat = float(query_parameters.get('lat'))
lon = float(query_parameters.get('lon'))
traffic_lights = [
traffic_light for traffic_light in
mongo.db.trafficLights.aggregate([
{"$geoNear": {
"near": {
"type": "Point",
"coordinates": [lon, lat]
},
"spherical": True,
"distanceField": "calculatedRange"
}},
{"$redact": {
"$cond": {
"if": {
"$lte": ["$calculatedRange", "$range"]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
]
)
if lat < traffic_light['location'][1]
]
return json_util.dumps({'cursor': traffic_lights[:1]})
if __name__ == '__main__':
app.run('0.0.0.0', 5002)
@@ -0,0 +1,3 @@
flask
Flask-PyMongo
jsonify
@@ -0,0 +1,7 @@
FROM mongo:4.4
COPY ./cars.json .
COPY ./traffic_lights.json .
ADD import.sh /docker-entrypoint-initdb.d
@@ -0,0 +1,17 @@
[
{
"oem": "BENTLEY",
"modelType": "Continental",
"vin": "SCBFR7ZA5CC072256"
},
{
"oem": "SATURN",
"modelType": "Vue",
"vin": "5GZCZ43D13S812715"
},
{
"oem": "SATURN",
"modelType": "Vue2",
"vin": "5GZCZ43D13S812716"
}
]
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
mongoimport --db entities --collection cars --type json --file cars.json --jsonArray
mongoimport --db entities --collection trafficLights --type json --file traffic_lights.json --jsonArray
mongo --eval 'db.trafficLights.ensureIndex({location:"2dsphere"})' entities
@@ -0,0 +1,20 @@
[
{
"id": "1",
"location": [16.20719, 47.89584],
"switchingTime": 26,
"color": "RED"
},
{
"id": "2",
"location": [16.20814, 47.90937],
"switchingTime": 16,
"color": "GREEN"
},
{
"id": "3",
"location": [16.20917, 47.92703],
"switchingTime": 20,
"color": "RED"
}
]
@@ -0,0 +1,4 @@
FROM redis
ENV PORT 6379
ENV HOSTNAME redis
EXPOSE 6379
@@ -0,0 +1,9 @@
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;
ENV PORT 5001
EXPOSE 5001
ENTRYPOINT [ "python" ]
CMD [ "event_store_server.py" ]
@@ -0,0 +1,106 @@
import json
import pickle
import sys
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
from dse_shared_libs.target_velocity import TargetVelocity
from dse_shared_libs.traffic_light_state import TrafficLightState
from redis import StrictRedis
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
class EventLogger:
def __init__(self, redis: StrictRedis, log_to_redis: bool, verbose: bool):
"""
:param redis:
:param log_to_redis:
:param verbose:
"""
self.redis = redis
self.log_to_redis = log_to_redis
self.verbose = verbose
def setup_msq_queue(self):
"""
Sets up the message queue which calls self.log on msg receive.
"""
mb = MBWrapper(exchange_name='logger', callback=self.log)
mb.setup_receiver()
def get_keys(self):
"""
:returns: the available redis keys.
"""
return json.dumps([key.decode() for key in self.redis.keys()])
def get_list_of(self, key: str):
"""
Returns a list of all elements inside a key. Most recent elements are stored first.
:param key: DAF:<VIN> or TV:<VIN> for either DAF events or TV events
:returns: list of elements
"""
key = self.redis.lrange(key, 0, -1)
return key.decode() if key else json.dumps("")
def get_index_of(self, key: str, index: int):
"""
Returns element matching index in key. Most recent elements are stored first.
:param key: DAF:<VIN> or TV:<VIN> for either DAF events or TV events
:param index: 0 for most recent
"""
key = self.redis.lindex(key, index)
return key.decode() if key else json.dumps("")
def log(self, msg: bytes):
"""
Logging callback. Logs the msg depending on its type to the respective redis key.
Types can be DAF, TrafficLightState or TargetVelocity. If type is unknown, message is still logged as key
UNKNOWN.
:param bytes msg: pickled msg binary to log
"""
key, to_log = self._unpack_message_to_log(msg)
if self.log_to_redis:
self.redis.lpush(key, "{}".format(to_log))
if self.verbose:
print(to_log)
def _unpack_message_to_log(self, msg: bytes):
try:
msg = pickle.loads(msg)
except Exception as e:
print("Could not unpickle [{}]: {}", msg, e)
self.redis: StrictRedis
self.redis.append('ERRORS', str(e))
key: str
if type(msg) == DAF:
msg: DAF
key = 'DAF:{}'.format(msg.vehicle_identification_number)
elif type(msg) == TrafficLightState:
msg: TrafficLightState
key = 'TL:{}'.format(msg.tlid)
elif type(msg) == TargetVelocity:
msg: TargetVelocity
key = 'TV:{}'.format(msg.vin)
else:
key = 'UNKNOWN'
try:
to_log = json.dumps(msg.to_dict())
except AttributeError:
try:
to_log = json.dumps(msg)
except (json.decoder.JSONDecodeError, TypeError):
to_log = str(msg)
return key, to_log
@@ -0,0 +1,68 @@
import threading
from flask import Flask
from flask_redis import Redis
from redis import StrictRedis
from event_logger import EventLogger
# TOGGLE if every received message shall be redirected to redis
LOG_TO_REDIS = True
# TOGGLE if every received message shall be printed to console
VERBOSE = True
# make sure redis (container) is running and accessible
# see https://flask-and-redis.readthedocs.io/en/latest/
# see https://github.com/andymccurdy/redis-py
app = Flask(__name__)
app.config["REDIS_HOST"] = "redis"
app.config["REDIS_PORT"] = 6379
redis: StrictRedis = Redis(app)
@app.route("/")
def get_keys():
keys = redis.keys()
keys.sort()
return 'Existing keys:<br><br>{}'.format(
['<a href="/api/keys/{0}/">{0}</a>'.format(key.decode()) for key in keys]) + \
'DAF: DatenaufzeichnungAutonomesFahren, TL: TrafficLight, TV: TargetVelocity' \
'<br><br>' \
'<br><br>Access Api via <br><br>' \
'GET /api/keys<br>' \
'GET /api/keys/&lt;key&gt;<br>'
@app.route("/api/keys")
def api_get_keys():
"""
:return: list of available redis keys
"""
return el.get_keys()
@app.route("/api/keys/<key>/")
def api_get_of_key(key):
"""
:param key: key to get data of
:return: list of data to the respective key
"""
return el.get_list_of(key)
@app.route("/api/keys/<key>/<index>/")
def api_get_index_of_key(key, index):
"""
:param key: key to get data element of
:param index: index of keys data list
:return: element matching index of key
"""
return el.get_index_of(key, index)
if __name__ == '__main__':
el = EventLogger(redis, LOG_TO_REDIS, VERBOSE)
threading.Thread(target=el.setup_msq_queue).start()
threading.Thread(target=app.run, args=('0.0.0.0', 5001)).start()
@@ -0,0 +1,2 @@
flask
Flask-And-Redis
@@ -0,0 +1,68 @@
import datetime
import json
import pickle
import unittest
import geopy
from dse_shared_libs.daf import DAF
from dse_shared_libs.target_velocity import TargetVelocity
from dse_shared_libs.traffic_light_color import TrafficLightColor
from dse_shared_libs.traffic_light_state import TrafficLightState
from redis import StrictRedis
from event_logger import EventLogger
class TestEventLogger(unittest.TestCase):
def setUp(self) -> None:
self.el = EventLogger(StrictRedis(), False, False)
self.timestamp = datetime.datetime.utcnow()
def test_unpack_daf(self):
daf = DAF(vehicle_identification_number='my_vin',
gps_location=geopy.Point(0, 0, 0),
velocity=130.0,
near_crash_event=False,
timestamp=self.timestamp,
)
key, message = self.el._unpack_message_to_log(pickle.dumps(daf))
self.assertEqual(key, 'DAF:my_vin')
self.assertEqual(message, json.dumps(daf.to_dict()))
def test_unpack_target_velocity(self):
tv = TargetVelocity(vin='my_other_vin',
target_velocity=120.0,
timestamp=self.timestamp)
key, message = self.el._unpack_message_to_log(pickle.dumps(tv))
self.assertEqual(key, 'TV:my_other_vin')
self.assertEqual(message, json.dumps(tv.to_dict()))
def test_unpack_traffic_light_state(self):
tls = TrafficLightState(tlid='my_traffic_light',
color=TrafficLightColor.GREEN,
last_switch=self.timestamp)
key, message = self.el._unpack_message_to_log(pickle.dumps(tls))
self.assertEqual(key, 'TL:my_traffic_light')
self.assertEqual(message, json.dumps(tls.to_dict()))
def test_unpack_unknown_dict(self):
unknown = dict(foo='foo', bar='bar')
key, message = self.el._unpack_message_to_log(pickle.dumps(unknown))
self.assertEqual(key, 'UNKNOWN')
self.assertEqual(message, json.dumps(unknown))
def test_unpack_unknown_object(self):
obj = datetime.datetime.utcnow()
key, message = self.el._unpack_message_to_log(pickle.dumps(obj))
self.assertEqual(key, 'UNKNOWN')
self.assertEqual(message, str(obj))
@@ -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 [ "i_feed.py" ]
@@ -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__":
...
+55
View File
@@ -0,0 +1,55 @@
import sys
import time
import geopy.distance
import requests
from dse_shared_libs import daf, traffic_light_state, traffic_light_color, target_velocity
from devices.traffic_light import TrafficLight
from devices.vehicle import Vehicle
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
"""
USED AS ENTRYPOINT FOR DOCKERFILE
Start all vehicles / traffic lights here
"""
if __name__ == '__main__':
ENTITY_IDENT_URL = 'http://entityident:5002/api/v1/resources/'
available = False
while not available:
try:
response = requests.get(ENTITY_IDENT_URL + 'cars')
cars = response.json()
response = requests.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 traffic_light in traffic_lights['cursor']:
print(traffic_light)
TrafficLight(tlid=traffic_light['id'],
switching_time=traffic_light['switchingTime']).start()
time.sleep(0.5)
for index, car in enumerate(cars['cursor']):
print(car)
# Get location of first TL
tl_1_loc = geopy.Point(47.89584, 16.20719)
# Calculate starting point by going south of TL_location the amount specified in cars.json
d = geopy.distance.distance(kilometers=car['startingDistance'] / float(1000))
starting_point = d.destination(point=tl_1_loc, bearing=182)
starting_velocity = car['startingVelocity']
print('StartingPoint is {} and startingVelocity is {}'.format(starting_point, starting_velocity))
Vehicle(vin=car['vin'], starting_point=starting_point, starting_velocity=starting_velocity).start_driving()
# Sleep for amount of seconds defined in second car before second car starts
if len(cars['cursor']) > 1 and index < len(cars['cursor']) - 1:
time.sleep(cars['cursor'][index + 1]['startingTime'])
@@ -0,0 +1,3 @@
circuitbreaker # fault tolerance
geopy
requests
@@ -0,0 +1,113 @@
import datetime
import pickle
import time
import unittest
from unittest.mock import patch
import geopy
from dse_shared_libs.daf import DAF
from dse_shared_libs.mock.datetime import MyDate
from dse_shared_libs.target_velocity import TargetVelocity
from dse_shared_libs.traffic_light_color import TrafficLightColor
from devices.traffic_light import TrafficLight
from devices.vehicle import Vehicle
from dse_shared_libs.mock.rabbit_mq_mocks import MyBlockingConnection
class TestVehicle(unittest.TestCase):
def setUp(self) -> None:
with patch('pika.BlockingConnection', MyBlockingConnection):
self.vin = 'my_vin'
self.starting_velocity = 130
self.starting_point = geopy.Point(0, 0, 0)
self.timestamp = datetime.datetime.utcnow()
self.v = Vehicle(
vin=self.vin,
starting_point=self.starting_point,
starting_velocity=self.starting_velocity
)
self.v.last_update = self.timestamp
self.v._driven_kms = 0
def tearDown(self) -> None:
time.sleep(0.1)
def test_nce_prop(self):
with patch('time.sleep', return_value=None):
# initially false
self.assertEqual(self.v.nce, False)
# get past nce km
self.v._gps_location.latitude = 48
# now nce should fire
self.assertEqual(self.v.nce, True)
# second call is false again
self.assertEqual(self.v.nce, False)
def test_daf_prop(self):
with patch('devices.vehicle.datetime', MyDate):
MyDate.set_timestamp(self.timestamp)
# test if daf object created properly
daf = DAF(vehicle_identification_number=self.vin,
gps_location=self.starting_point,
velocity=self.starting_velocity,
timestamp=self.timestamp,
near_crash_event=False)
self.assertEqual(self.v.daf, daf)
def test_current_location_prop(self):
with patch('devices.vehicle.datetime', MyDate):
MyDate.set_timestamp(self.timestamp)
# initial position should be starting point
self.assertEqual(self.v.gps_location, self.starting_point)
# lets say last time was about an hour ago
self.v.last_update = self.timestamp - datetime.timedelta(hours=1)
# we drive "almost" directly north
loc = self.v.gps_location
self.assertEqual(round(loc.latitude, 2), 1.17)
self.assertEqual(round(loc.longitude, 2), 0.04)
def test_driven_kms_prop(self):
# initial driven kms are 0
self.assertEqual(self.v.driven_kms, '0km')
# alter driven kms
self.v._driven_kms = 44.24552
# will be rounded to 2 digits
self.assertEqual(self.v.driven_kms, '44.25km')
def test_new_velocity(self):
# starting velocity is set to 130
self.assertEqual(self.v.velocity, 130)
# we get a new velocity, lets say 55 ...
tv = TargetVelocity(vin='my_vin', target_velocity=55, timestamp=self.timestamp)
self.v.new_velocity(pickle.dumps(tv))
# then the vehicle should comply with that
self.assertEqual(self.v.velocity, 55)
class TestTrafficLight(unittest.TestCase):
def setUp(self) -> None:
with patch('pika.BlockingConnection', MyBlockingConnection):
self.tl = TrafficLight(tlid='my_tl',
switching_time=1,
starting_color=TrafficLightColor.RED)
def tearDown(self) -> None:
self.tl.stop()
def test_switching(self):
self.assertFalse(hasattr(self.tl, 'current_color'))
self.tl.start()
self.assertEqual(self.tl.current_color, TrafficLightColor.RED)
time.sleep(1.2)
self.assertEqual(self.tl.current_color, TrafficLightColor.GREEN)
time.sleep(1)
self.assertEqual(self.tl.current_color, TrafficLightColor.RED)
@@ -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" ]
@@ -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)
@@ -0,0 +1,4 @@
FROM rabbitmq
ENV PORT 5672
ENV HOSTNAME rabbitmq
EXPOSE 5672
@@ -0,0 +1,3 @@
build
dist
dse_shared_libs.egg-info
+8
View File
@@ -0,0 +1,8 @@
### Contains shared libraries like rabbitMQ wrapper
* To have life changes (develop package only links to this folder): `pip install -e .`
* To install as independent package: `pip install .`
* Create installable package via `python setup.py sdist`
### Access shared libs via
* `from dse_shared_libs.* import *`
* e.g. `from dse_shared_libs.message_broker_wrapper import MBWrapper`
@@ -0,0 +1,26 @@
from dataclasses import dataclass
from datetime import datetime
import geopy.distance
@dataclass
class DAF:
"""
"Datenaufzeichnung für automatisiertes Fahren" (DAF), see 2021_DSE_Exercise_V1.0_Final.pdf page 7
"""
vehicle_identification_number: str
gps_location: geopy.Point
velocity: float
near_crash_event: bool
timestamp: datetime
def to_dict(self):
return {'vehicle_identification_number': self.vehicle_identification_number,
'gps_location': {'latitude': self.gps_location.latitude,
'longitude': self.gps_location.longitude
},
'velocity': self.velocity,
'near_crash_event': self.near_crash_event,
'timestamp': self.timestamp.timestamp()
}
@@ -0,0 +1,149 @@
import time
from threading import Thread
from typing import Union
import pika
from pika.exceptions import AMQPConnectionError
class MBWrapper:
host: str
exchange_type: str
exchange_name: str
callback: callable
_logger: Union['MBWrapper', None]
_type: str = None
_connection: pika.BlockingConnection
_channel: pika.BlockingConnection.channel
def __init__(self,
host: str = 'rabbitmq',
exchange_type: str = 'fanout',
exchange_name: str = None,
callback: callable = None,
verbose: bool = False):
"""
Initializes a new Message Broker Wrapper (MBWrapper). Initialize this object afterwards with
self.setup_sender() or self.setup_receiver() if messages will be published to or received from the queue.
If the exchange_name is not "logger", the MBWrapper will also initialize an sending queue which forwards
EVERY request to this exchange, too. Therefore, every request routed via a MBWrapper is logged. Except if it is
the logger MBWrapper. This one is initialized in the EventStore Service and receives all messages.
:param host: of running rabbitMQ instance
:param exchange_type: rabbitMQ exchange type to use
:param exchange_name: name of exchange
:param callback: callable callback to execute if message receiver will be set up, can also be set later on.
:param verbose: print handled messages to console
"""
assert exchange_name, 'Please define an exchange name'
# append a connection to the logging broker, if it isn't the logging broker itself
if exchange_name != 'logger':
self._logger = MBWrapper(exchange_name='logger')
self._logger.setup_sender()
else:
self._logger = None
self.host = host
self.exchange_type = exchange_type
self.exchange_name = exchange_name
self.callback = callback
self.verbose = verbose
def print(self, *msg):
"""
Modified print which prints only to stout with self.verbose.
:param msg: to print
"""
if self.verbose:
print(*msg)
def setup_sender(self):
"""
Setup the MBWrapper as sender.
"""
assert self._type != 'receiver', 'MBWrapper is already a receiver. Use another MBWrapper.'
self._type = 'sender'
self._setup_channel()
def setup_receiver(self, callback: callable = None):
"""
Setup the MBWrapper as a receiver.
A callback method which can handle the response bytes as input is mandatory.
Set it here if not already done on init.
"""
if callback:
self.callback = callback
assert self._type != 'sender', 'MBWrapper is already a sender. Use another MBWrapper.'
assert self.callback, \
'Please setup MBWrapper with "on response" self.callback which can handle a byte string as input.'
def consumer():
"""
If initialized as receiver: Consumer thread which waits for incoming messages, invokes self._receive
"""
self._setup_channel()
result = self._channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue
self._channel.queue_bind(exchange=self.exchange_name, queue=queue_name)
self.print('[{}] Waiting for messages.'.format(self.exchange_name))
self._channel.basic_consume(
queue=queue_name, on_message_callback=self._receive, auto_ack=True)
self._channel.start_consuming()
Thread(target=consumer).start()
def send(self, message: bytes):
"""
Send the message to the queue. Forward also to logger.
:param message: msg to send
"""
if type(message) is not bytes:
message = str(message).encode()
self._channel.basic_publish(exchange=self.exchange_name, routing_key='', body=message)
self.print("[{}] Sent {}".format(self.exchange_name, message))
# has a logger IFF not 'logger' as exchange name -> forwards everything to logger exchange, too
if self.__getattribute__('_logger'):
# print('logging')
self._logger.send(message)
def close(self):
"""
Closes the connection.
"""
self._connection.close()
def _setup_channel(self):
"""
Setup the rabbitMQ channel. Retry if not succeeded (Allow the dependencies to
boot). Wait one second between retries.
"""
connect_succeeded = False
while not connect_succeeded:
try:
self._connection = pika.BlockingConnection(
pika.ConnectionParameters(host=self.host))
connect_succeeded = True
except pika.exceptions.AMQPConnectionError:
print('Could not connect, retry')
time.sleep(1)
self._channel = self._connection.channel()
self._channel.exchange_declare(exchange=self.exchange_name, exchange_type=self.exchange_type)
def _receive(self, ch, method, properties, body):
"""
Reduce complexity, only forward the message body to the callback method. The others are not necessary for our
application example.
"""
self.callback(body)
@@ -0,0 +1,10 @@
class MyDate:
timestamp = None
@classmethod
def set_timestamp(cls, timestamp):
cls.timestamp = timestamp
@classmethod
def utcnow(cls):
return cls.timestamp
@@ -0,0 +1,43 @@
class MyQueue:
@property
def queue(self):
return None
class MyMethod:
method = MyQueue()
class MyChannel:
@staticmethod
def exchange_declare(*args, **kwargs):
return None
@staticmethod
def queue_declare(*args, **kwargs):
return MyMethod
@staticmethod
def queue_bind(*args, **kwargs):
return None
@staticmethod
def basic_consume(*args, **kwargs):
return None
@staticmethod
def basic_publish(*args, **kwargs):
return None
@staticmethod
def start_consuming(*args, **kwargs):
return None
class MyBlockingConnection:
def __init__(self, *args, **kwargs):
...
@staticmethod
def channel():
return MyChannel()
@@ -0,0 +1,7 @@
class MyResponse:
def __init__(self, *args, **kwargs):
...
@staticmethod
def json(*args, **kwargs):
return {'cursor': []}
@@ -0,0 +1,15 @@
from dataclasses import dataclass
from datetime import datetime
@dataclass
class TargetVelocity:
vin: str
target_velocity: float
timestamp: datetime
def to_dict(self):
return {'vin': self.vin,
'target_velocity': self.target_velocity,
'timestamp': self.timestamp.timestamp()
}
@@ -0,0 +1,6 @@
from enum import Enum
class TrafficLightColor(Enum):
GREEN = 0
RED = 1
@@ -0,0 +1,16 @@
from dataclasses import dataclass
from datetime import datetime
from dse_shared_libs.traffic_light_color import TrafficLightColor
@dataclass
class TrafficLightState:
tlid: str
color: TrafficLightColor
last_switch: datetime
def to_dict(self):
return {'tlid': self.tlid,
'color': self.color.name,
'last_switch': self.last_switch.timestamp()}
+9
View File
@@ -0,0 +1,9 @@
from setuptools import find_packages, setup
setup(name='dse-shared-libs',
version=1,
packages=find_packages(),
include_package_data=True,
license='BSD License', # example licensez
install_requires=['pika', 'geopy']
)
+8
View File
@@ -0,0 +1,8 @@
FROM python:3.8-slim
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
ENV PORT 5003
EXPOSE 5003
ENTRYPOINT [ "python" ]
CMD [ "x_way_server.py" ]
+6
View File
@@ -0,0 +1,6 @@
# Super Lightweight Flask Web Server
* `pip install requirements.txt`
* `python x_way.py`
* `http://127.0.0.1:5000` -> `Hello World`
* `http://127.0.0.1:5000/api1` -> `api1`
* `http://127.0.0.1:5000/api2` -> `api2`
@@ -0,0 +1,6 @@
flask==1.1.4
Flask-Cors
requests
Flask-PyMongo
jsonify
flask_restx
@@ -0,0 +1,96 @@
import flask
import flask_restx
import requests
from bson import json_util
from flask import Flask, jsonify
from flask_cors import CORS
from flask import request
import json
app = Flask(__name__)
CORS(app)
api_bp = flask.Blueprint("api", __name__, url_prefix="/api/v1")
API = flask_restx.Api(api_bp)
app.register_blueprint(api_bp)
NAMESPACE = API.namespace("resources")
ENTITY_IDENT_URL = 'http://entityident:5002/api/v1/resources/'
EVENT_STORE_URL = 'http://eventstore:5001/api/keys/'
@NAMESPACE.route('/car_events', doc={'description': 'Get most current car event for specific car'})
class CarsEvents(flask_restx.Resource):
@NAMESPACE.doc(params={'vin': {'description': 'Vehicle Identifier Number of car',
'type': 'String', 'default': '5GZCZ43D13S812715'}})
@NAMESPACE.response(200, 'Success')
def get(self):
vin = request.args.get('vin')
try:
response = requests.get(EVENT_STORE_URL + 'DAF:' + vin + '/0/')
cars = json.loads(response.text)
except requests.exceptions.ConnectionError as e:
print("Is the EVENT_STORE_URL running and reachable?")
raise e
return jsonify(cars)
@NAMESPACE.route('/traffic_light_events', doc={'description': 'Get most current traffic light event'
'for specific traffic light'})
class TrafficLightEvents(flask_restx.Resource):
@NAMESPACE.doc(params={'id': {'description': 'Unique ID of traffic light',
'type': 'int', 'default': '1'}})
@NAMESPACE.response(200, 'Success')
def get(self):
id = request.args.get('id')
try:
response = requests.get(EVENT_STORE_URL + 'TL:' + id + '/0/')
traffic_lights = json.loads(response.text)
except requests.exceptions.ConnectionError as e:
print("Is the EVENT_STORE_URL running and reachable?")
raise e
return jsonify(traffic_lights)
@NAMESPACE.route('/cars', doc={'description': 'Get entity details for all cars'})
class Car(flask_restx.Resource):
@NAMESPACE.response(200, 'Success')
def get(self):
try:
response = requests.get(ENTITY_IDENT_URL + 'cars')
cars = response.json()['cursor']
except requests.exceptions.ConnectionError as e:
print("Is the entity_ident_server running and reachable?")
raise e
return jsonify(cars)
@NAMESPACE.route('/traffic_lights', doc={'description': 'Get entity details for all traffic lights'})
class TrafficLights(flask_restx.Resource):
@NAMESPACE.response(200, 'Success')
def get(self):
try:
response = requests.get(ENTITY_IDENT_URL + 'traffic_lights')
traffic_lights = response.json()['cursor']
except requests.exceptions.ConnectionError as e:
print("Is the entity_ident_server running and reachable?")
raise e
return jsonify(traffic_lights)
if __name__ == '__main__':
app.run('0.0.0.0', 5004)
@@ -0,0 +1,19 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: simulation-parameters-configmap
data:
DSE2021_SCALING: "2"
DSE2021_NCE: "1"
DSE2021_CAR1_SV: "130"
DSE2021_CAR2_SV: "130"
DSE2021_CAR3_SV: "130"
DSE2021_CAR1_SD: "300"
DSE2021_CAR2_SD: "500"
DSE2021_CAR3_SD: "400"
DSE2021_CAR1_ST: "10"
DSE2021_CAR2_ST: "15"
DSE2021_CAR3_ST: "25"
DSE2021_TL1_R: "2000"
DSE2021_TL2_R: "800"
DSE2021_TL3_R: "1000"
@@ -0,0 +1,34 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controlcenter
labels:
app: controlcenter
spec:
replicas: 1
selector:
matchLabels:
app: controlcenter
strategy: {}
template:
metadata:
labels:
app: controlcenter
spec:
containers:
- image: gcr.io/resonant-grail-307708/controlcenter
imagePullPolicy: ""
name: controlcenter
ports:
- containerPort: 80
resources: {}
env:
- name: DSE2021_SCALING
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_SCALING
restartPolicy: Always
serviceAccountName: ""
volumes: null
status: {}
@@ -0,0 +1,94 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: entityident
labels:
app: entityident
spec:
replicas: 1
selector:
matchLabels:
app: entityident
strategy: {}
template:
metadata:
labels:
app: entityident
spec:
containers:
- image: gcr.io/resonant-grail-307708/entityident
imagePullPolicy: ""
name: entityident
ports:
- containerPort: 5002
resources: {}
env:
- name: DSE2021_SCALING
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_SCALING
- name: DSE2021_CAR1_SV
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_CAR1_SV
- name: DSE2021_CAR2_SV
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_CAR2_SV
- name: DSE2021_CAR3_SV
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_CAR3_SV
- name: DSE2021_CAR1_SD
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_CAR1_SD
- name: DSE2021_CAR2_SD
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_CAR2_SD
- name: DSE2021_CAR3_SD
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_CAR3_SD
- name: DSE2021_CAR1_ST
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_CAR1_ST
- name: DSE2021_CAR2_ST
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_CAR2_ST
- name: DSE2021_CAR3_ST
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_CAR3_ST
- name: DSE2021_TL1_R
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_TL1_R
- name: DSE2021_TL2_R
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_TL2_R
- name: DSE2021_TL3_R
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_TL3_R
restartPolicy: Always
serviceAccountName: ""
volumes: null
status: {}
@@ -0,0 +1,34 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: eventstore
labels:
app: eventstore
spec:
replicas: 1
selector:
matchLabels:
app: eventstore
strategy: {}
template:
metadata:
labels:
app: eventstore
spec:
containers:
- image: gcr.io/resonant-grail-307708/eventstore
imagePullPolicy: ""
name: eventstore
ports:
- containerPort: 5001
resources: {}
env:
- name: DSE2021_SCALING
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_SCALING
restartPolicy: Always
serviceAccountName: ""
volumes: null
status: {}
@@ -0,0 +1,37 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ifeed
labels:
app: ifeed
spec:
replicas: 1
selector:
matchLabels:
app: ifeed
strategy: {}
template:
metadata:
labels:
app: ifeed
spec:
containers:
- image: gcr.io/resonant-grail-307708/ifeed
imagePullPolicy: ""
name: ifeed
resources: {}
env:
- name: DSE2021_SCALING
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_SCALING
- name: DSE2021_NCE
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_NCE
restartPolicy: Always
serviceAccountName: ""
volumes: null
status: {}
@@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongo
labels:
app: mongo
spec:
replicas: 1
selector:
matchLabels:
app: mongo
strategy: {}
template:
metadata:
labels:
app: mongo
spec:
containers:
- image: gcr.io/resonant-grail-307708/mongo
imagePullPolicy: ""
name: mongo
ports:
- containerPort: 27017
resources: {}
restartPolicy: Always
serviceAccountName: ""
volumes: null
status: {}
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: orchestration
labels:
app: orchestration
spec:
replicas: 1
selector:
matchLabels:
app: orchestration
strategy: {}
template:
metadata:
labels:
app: orchestration
spec:
containers:
- image: gcr.io/resonant-grail-307708/orchestration
imagePullPolicy: ""
name: orchestration
resources: {}
env:
- name: DSE2021_SCALING
valueFrom:
configMapKeyRef:
name: simulation-parameters-configmap
key: DSE2021_SCALING
restartPolicy: Always
serviceAccountName: ""
volumes: null
status: {}

Some files were not shown because too many files have changed in this diff Show More