diff --git a/components/control_center/src/app/app.module.ts b/components/control_center/src/app/app.module.ts index 2860e89..8e719fb 100644 --- a/components/control_center/src/app/app.module.ts +++ b/components/control_center/src/app/app.module.ts @@ -6,7 +6,6 @@ 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 {WebsocketService} from './services/websocket.service'; import {LoggerModule, NgxLoggerLevel} from 'ngx-logger'; import {environment} from '../environments/environment'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; @@ -39,7 +38,6 @@ import {AgmCoreModule, GoogleMapsAPIWrapper} from '@agm/core'; providers: [ GoogleMapsAPIWrapper, RestService, - WebsocketService, { provide: HTTP_INTERCEPTORS, useClass: InterceptorService, multi: true }, diff --git a/components/control_center/src/app/component/landing/landing.component.css b/components/control_center/src/app/component/landing/landing.component.css index ea5170a..4c1354f 100644 --- a/components/control_center/src/app/component/landing/landing.component.css +++ b/components/control_center/src/app/component/landing/landing.component.css @@ -1,5 +1,3 @@ -html, body { - height: 100%; - padding: 0; - margin: 0; +#map{ + height : 100vh; width: 100%; padding:0px; margin: 0px; } diff --git a/components/control_center/src/app/component/landing/landing.component.html b/components/control_center/src/app/component/landing/landing.component.html index f5a10eb..7a44825 100644 --- a/components/control_center/src/app/component/landing/landing.component.html +++ b/components/control_center/src/app/component/landing/landing.component.html @@ -1,8 +1,26 @@ - - - - - + + + +

VIN: {{m.value.vin}}

+

OEM: {{m.value.oem}}

+

Model Type: {{m.value.modelType}}

+

Velocity: {{m.value.velocity}}

+

Timestamp: {{m.value.timestamp}}

+

NCE: {{m.value.near_crash_event}}

+
+
+ + +

Id: {{m.value.id}}

+

Switching Time: {{m.value.switchingTime}}

+

Range: {{m.value.range}}

+

Color: {{m.value.color}}

+

Last Switch: {{m.value.last_switch}}

+
diff --git a/components/control_center/src/app/component/landing/landing.component.ts b/components/control_center/src/app/component/landing/landing.component.ts index 481018e..b9c7bc6 100644 --- a/components/control_center/src/app/component/landing/landing.component.ts +++ b/components/control_center/src/app/component/landing/landing.component.ts @@ -1,7 +1,9 @@ import {Component, OnInit, ViewChild} from '@angular/core'; -import {AgmMap, AgmMarker} from '@agm/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'; @Component({ selector: 'app-landing', @@ -22,72 +24,137 @@ export class LandingComponent implements OnInit { zoom = 14; center = {lat: 47.90620, lng: 16.20785}; - // Test Data - markers = [ - { - Id: 1, - name: 'Car-1', - lat: 47.89053, - lng: 16.20703, - visible: true, - iconUrl: 'assets/pictures/car.png' - }, - { - Id: 2, - name: 'Car-2', - lat: 47.89853, - lng: 16.20703, - visible: true, - iconUrl: 'assets/pictures/car.png' - }, - ]; - traffic_light_markers = []; + 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() { - let traffic_lights = []; + this.getTrafficLights(); + this.getCars(); + } + + getCarEvents(vin) { + this.timeInterval = interval(1000) + .pipe( + startWith(0), + switchMap(() => this.restService.getCarEvents(vin)) + ).subscribe((data: any) => { + const carEvent = data.body['cursor']; + const car = this.car_markers.get(vin); + car['velocity'] = carEvent['velocity']; + car['timestamp'] = carEvent['timestamp']; + car['near_crash_event'] = carEvent['near_crash_event']; + if (car['near_crash_event']) { + car['iconUrl'] = this.CAR_IMAGE_NCE; + this.playCrashSound(); + } else { + car['iconUrl'] = this.CAR_IMAGE; + } + car['lat'] = carEvent['gps_location']['latitude']; + car['lng'] = carEvent['gps_location']['longitude']; + this.car_markers.set(car['vin'], car); + + }, + err => this.logger.error(err), + () => { + this.logger.debug('loaded traffic light events'); + } + ); + } + + getTrafficLightEvents(tlid) { + + this.timeInterval = interval(1000) + .pipe( + startWith(0), + switchMap(() => this.restService.getTrafficLightEvents(tlid)) + ).subscribe( + (data: any) => { + const traffic_light_event = data.body['cursor']; + const traffic_light = this.traffic_light_markers.get(traffic_light_event['tlid']); + traffic_light['color'] = traffic_light_event['color']; + traffic_light['last_switch'] = traffic_light_event['last_switch']; + traffic_light['iconUrl'] = this.trafficLightToImage(traffic_light['color']); + + this.traffic_light_markers.set(traffic_light_event['tlid'], traffic_light); + }, + err => this.logger.error(err), + () => { + this.logger.debug('loaded traffic light events'); + } + ); + } + + getTrafficLights() { this.restService.getTrafficLights().subscribe( (data: any) => { - traffic_lights = data; - console.log(data['cursor']); for (const traffic_light of data['cursor']) { - traffic_light['iconUrl'] = 'assets/pictures/traffic_light_red.png'; + traffic_light['iconUrl'] = this.trafficLightToImage(traffic_light['color']); traffic_light['lat'] = traffic_light['location'][1]; traffic_light['lng'] = traffic_light['location'][0]; - console.log(traffic_light); - this.traffic_light_markers.push(traffic_light); + this.traffic_light_markers.set(traffic_light['id'], traffic_light); } }, err => this.logger.error(err), () => { this.logger.debug('loaded traffic lights'); + for (const value of this.traffic_light_markers.values()) { + this.getTrafficLightEvents(value['id']); + } } ); } - - getDelta(source, destination, steps) { - return { - lat: (destination.lat - source.lat) / steps, - lng: (destination.lng - source.lng) / steps - }; - } - - toggleMarker() { - const delta = this.getDelta(this.markers[0], this.markers[1], 100); - - this.move(this.markers[0], delta, 0, 10, 100); + getCars() { + this.restService.getCars().subscribe( + (data: any) => { + for (const car of data['cursor']) { + car['iconUrl'] = 'assets/pictures/car.png'; + this.car_markers.set(car['vin'], car); + } + }, + err => this.logger.error(err), + () => { + this.logger.debug('loaded cars'); + console.log(this.car_markers); + for (const value of this.car_markers.values()) { + this.getCarEvents(value['vin']); + } + } + ); } - move(source, delta, counter, delay, steps) { - source.lat += delta.lat; - source.lng += delta.lng; - if (counter !== steps) { - counter++; - setTimeout(this.move.bind(this), delay, source, delta, counter, delay, steps); - } + private trafficLightToImage(trafficLight) { + return (trafficLight === 'RED') ? this.TL_RED_IMAGE : this.TL_GREEN_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(); + } } diff --git a/components/control_center/src/app/services/interceptor.service.ts b/components/control_center/src/app/services/interceptor.service.ts index 9d9eaab..25d436d 100644 --- a/components/control_center/src/app/services/interceptor.service.ts +++ b/components/control_center/src/app/services/interceptor.service.ts @@ -32,7 +32,7 @@ export class InterceptorService implements HttpInterceptor { } }); - this.logger.debug('Interceptor works'); + //this.logger.debug('Interceptor works'); // pipe the response observable return next.handle(req).pipe( diff --git a/components/control_center/src/app/services/rest.service.ts b/components/control_center/src/app/services/rest.service.ts index 0a60896..1ea55ab 100644 --- a/components/control_center/src/app/services/rest.service.ts +++ b/components/control_center/src/app/services/rest.service.ts @@ -2,7 +2,6 @@ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {NGXLogger} from 'ngx-logger'; import {environment} from '../../environments/environment'; -import {Observable} from 'rxjs'; @Injectable() export class RestService { @@ -14,7 +13,20 @@ export class RestService { ) { } + 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'); + } + } diff --git a/components/control_center/src/app/services/websocket.service.ts b/components/control_center/src/app/services/websocket.service.ts deleted file mode 100644 index c936698..0000000 --- a/components/control_center/src/app/services/websocket.service.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {Injectable} from '@angular/core'; -import {NGXLogger} from 'ngx-logger'; -import {fromEvent, interval, Observable} from 'rxjs'; - -import {environment} from '../../environments/environment'; -import {WSEvents} from '../interfaces/interface'; - -@Injectable({ - providedIn: 'root' -}) -export class WebsocketService { - private wsEndpoint = environment.ws_location + ':' + environment.ws_port + '/test-ws-endpoint/'; - private readonly ws: WebSocket; - private wsEvents: WSEvents; - - constructor(private logger: NGXLogger) { - this.logger.debug('Initiating ws connection on', this.wsEndpoint); - this.ws = new WebSocket(this.wsEndpoint); - - interval(5000).subscribe(() => { - // continuously check if the connection is still open - if (this.ws.readyState !== WebSocket.OPEN) { - this.logger.error('Lost websocket connection ...', this.ws); - } - }); - - } - - wsTestCall(msg: string): WSEvents { - this.logger.debug( - 'Performing ws test call', - 'current ws ready state ==', this.ws.readyState + ',', - 'expected == 1 == open'); - - if (this.ws.readyState === WebSocket.OPEN) { - this.ws.send(msg); - } else { - return undefined; - } - - if (!this.wsEvents) { - this.wsEvents = { - message: fromEvent(this.ws, 'message'), - error: fromEvent(this.ws, 'error'), - close: fromEvent(this.ws, 'close') - }; - } - - return this.wsEvents; - } - -} diff --git a/components/control_center/src/assets/pictures/car_orange.png b/components/control_center/src/assets/pictures/car_orange.png new file mode 100644 index 0000000..50470ea Binary files /dev/null and b/components/control_center/src/assets/pictures/car_orange.png differ diff --git a/components/control_center/src/assets/sound/crash.mp3 b/components/control_center/src/assets/sound/crash.mp3 new file mode 100644 index 0000000..93cb06a Binary files /dev/null and b/components/control_center/src/assets/sound/crash.mp3 differ diff --git a/components/control_center/src/environments/environment.prod.ts b/components/control_center/src/environments/environment.prod.ts index f88694a..69acba3 100644 --- a/components/control_center/src/environments/environment.prod.ts +++ b/components/control_center/src/environments/environment.prod.ts @@ -2,8 +2,7 @@ import {NgxLoggerLevel} from 'ngx-logger'; export const environment = { production: true, - location: window.location.hostname, + location: 'xway', port: 5004, - ws_url_root: 'ws://' + window.location.hostname + ':' + window.location.port + '/', log_level: NgxLoggerLevel.WARN, }; diff --git a/components/control_center/src/environments/environment.ts b/components/control_center/src/environments/environment.ts index 152fea1..81fa12d 100644 --- a/components/control_center/src/environments/environment.ts +++ b/components/control_center/src/environments/environment.ts @@ -6,10 +6,8 @@ import {NgxLoggerLevel} from 'ngx-logger'; export const environment = { production: false, - location: 'xwayserver', + location: 'xway', port: 5004, - ws_location: 'ws://127.0.0.1', - ws_port: 8000, log_level: NgxLoggerLevel.DEBUG, }; diff --git a/components/control_center/src/styles.css b/components/control_center/src/styles.css index 102e8ab..6873ece 100644 --- a/components/control_center/src/styles.css +++ b/components/control_center/src/styles.css @@ -4,6 +4,11 @@ @import '~bootstrap/dist/css/bootstrap.min.css'; body { - width: 95%; - margin: auto; + width: 100%; + margin: 0; + padding: 0; +} + +.gm-style-iw-a { + opacity: 0.8 !important; } diff --git a/components/entitiy_ident/mongo/traffic_lights.json b/components/entitiy_ident/mongo/traffic_lights.json index cf91b92..52c18cf 100644 --- a/components/entitiy_ident/mongo/traffic_lights.json +++ b/components/entitiy_ident/mongo/traffic_lights.json @@ -4,20 +4,20 @@ "location": [16.20719, 47.89584], "range": 542, "switchingTime": 15, - "initialColor": "RED" + "color": "RED" }, { "id": "2", "location": [16.20814, 47.90937], "range": 725, "switchingTime": 20, - "initialColor": "GREEN" + "color": "GREEN" }, { "id": "3", "location": [16.20917, 47.92703], "range": 910, "switchingTime": 25, - "initialColor": "RED" + "color": "RED" } ] \ No newline at end of file diff --git a/components/i_feed/devices/traffic_light.py b/components/i_feed/devices/traffic_light.py index 22e972b..3b3c7e8 100644 --- a/components/i_feed/devices/traffic_light.py +++ b/components/i_feed/devices/traffic_light.py @@ -39,6 +39,14 @@ class TrafficLight: 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 diff --git a/components/i_feed/devices/vehicle.py b/components/i_feed/devices/vehicle.py index 64d0b39..a0c5bd5 100644 --- a/components/i_feed/devices/vehicle.py +++ b/components/i_feed/devices/vehicle.py @@ -70,6 +70,19 @@ class Vehicle: 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 @@ -85,6 +98,10 @@ class Vehicle: 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 """ if self._nce_possible and not self._nce_happened: @@ -115,10 +132,13 @@ class Vehicle: @property def daf(self): """ - :return: "Datenaufzeichnung für automatisiertes Fahren" (DAF) object + 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 + # 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, @@ -133,10 +153,10 @@ class Vehicle: @property def gps_location(self): """ - Update self.gps_location with given speed in km/h and the driven time in seconds - :param velocity: in km/h - :param time: in seconds - :param bearing: direction in degrees: 0=N, 90=E, 180=S, 270=W + 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 @@ -165,6 +185,9 @@ class Vehicle: @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): @@ -211,12 +234,16 @@ class Vehicle: @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 + Will be invoked if new target velocity message received from orchestrator. :param response: pickled TargetVelocity object """ response: TargetVelocity = pickle.loads(response) diff --git a/components/orchestration/orchestrator.py b/components/orchestration/orchestrator.py index 8db0469..cfd64b7 100644 --- a/components/orchestration/orchestrator.py +++ b/components/orchestration/orchestrator.py @@ -52,7 +52,7 @@ class Orchestrator: self.vins.append(car['vin']) for traffic_light in traffic_lights['cursor']: - self.tls[traffic_light['id']] = {'color': traffic_light['initialColor'], + self.tls[traffic_light['id']] = {'color': traffic_light['color'], 'switching_time': traffic_light['switchingTime'], 'last_switch': datetime.now()} diff --git a/components/shared/dse_shared_libs/message_broker_wrapper.py b/components/shared/dse_shared_libs/message_broker_wrapper.py index 07b3a79..334591f 100644 --- a/components/shared/dse_shared_libs/message_broker_wrapper.py +++ b/components/shared/dse_shared_libs/message_broker_wrapper.py @@ -24,6 +24,20 @@ class MBWrapper: 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 @@ -40,22 +54,37 @@ class MBWrapper: 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): + 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(): """ - Consumer thread which waits for incoming messages, invokes self._receive + 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) @@ -73,6 +102,11 @@ class MBWrapper: 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) @@ -84,9 +118,16 @@ class MBWrapper: 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: @@ -102,6 +143,7 @@ class MBWrapper: def _receive(self, ch, method, properties, body): """ - Reduce complexity, only forward the message body to the callback method + Reduce complexity, only forward the message body to the callback method. The others are not necessary for our + application example. """ self.callback(body) diff --git a/components/x_way/requirements.txt b/components/x_way/requirements.txt index 55c2263..7df827e 100644 --- a/components/x_way/requirements.txt +++ b/components/x_way/requirements.txt @@ -1,3 +1,5 @@ flask Flask-Cors -requests \ No newline at end of file +requests +Flask-PyMongo +jsonify \ No newline at end of file diff --git a/components/x_way/x_way_server.py b/components/x_way/x_way_server.py index a3b6d9e..ee6582f 100644 --- a/components/x_way/x_way_server.py +++ b/components/x_way/x_way_server.py @@ -12,18 +12,12 @@ ENTITY_IDENT_URL = 'http://entityident:5002/api/v1/resources/' EVENT_STORE_URL = 'http://eventstore:5001/api/keys/' -@app.route('/') -def hello_world(): - return 'Hello World' - - - @app.route('/api/v1/resources/car_events', methods=['GET']) def get_cars_events(): vin = request.args.get('vin') try: - response = requests.get(EVENT_STORE_URL + 'DAF:' + vin + '/') + response = requests.get(EVENT_STORE_URL + 'DAF:' + vin + '/0/') cars = json.loads(response.text) except requests.exceptions.ConnectionError as e: @@ -32,9 +26,24 @@ def get_cars_events(): return json_util.dumps({'cursor': cars}) + +@app.route('/api/v1/resources/traffic_light_events', methods=['GET']) +def get_traffic_light_events(): + 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 json_util.dumps({'cursor': traffic_lights}) + + @app.route('/api/v1/resources/cars', methods=['GET']) def get_cars(): - try: response = requests.get(ENTITY_IDENT_URL + 'cars') cars = response.json()['cursor'] @@ -48,7 +57,6 @@ def get_cars(): @app.route('/api/v1/resources/traffic_lights', methods=['GET']) def get_traffic_lights(): - try: response = requests.get(ENTITY_IDENT_URL + 'traffic_lights') traffic_lights = response.json()['cursor'] diff --git a/kubernetes/docker-compose.yml b/kubernetes/docker-compose.yml index ed3a692..1b4e599 100644 --- a/kubernetes/docker-compose.yml +++ b/kubernetes/docker-compose.yml @@ -90,4 +90,14 @@ services: depends_on: - entityident - orchestration - - eventstore \ No newline at end of file + - eventstore + controlcenter: + build: + context: ../components/control_center + dockerfile: Dockerfile + expose: + - 80 + ports: + - 80:80 + depends_on: + - xway \ No newline at end of file