Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

7 handle race duration #12

Merged
merged 4 commits into from
Dec 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ venv/
logs/
.env
app.db
.DS_Store
.DS_Store
*.bak
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ You can check via `ioreg -c IOSerialBSDClient | grep usb` where the serial conne

## Changes

### 20201111
Update to python 3.9, restructure the code handling track communication and race management.
Add result pages for race types.

### 20200209 - new driver cockpit
Get rid of the digit display for fuel levels - integrate gauge.js to display fuel levels
Calculate sleep time for cu request thread based on current track status
Expand Down
8 changes: 4 additions & 4 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)

app.config['SQLALCHEMY_ECHO'] = True
# app.config['SQLALCHEMY_ECHO'] = True
app.config['SQLALCHEMY_ECHO'] = False

app.logger.info('Trackday App started successfully')

Expand All @@ -52,6 +53,5 @@ def get_locale():
return request.accept_languages.best_match(app.config['LANGUAGES'])


from app import routes, models, errors, services
# to re init database use this import
# from app import models
app.logger.info('Importing routes, models, errors and services')
from app import routes, models, errors
58 changes: 41 additions & 17 deletions app/control_unit_connection.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,54 @@
import eventlet
import serial
import time

from app import app
from app.socket_connection import emit_cu_status
from carreralib import ControlUnit, connection


def connect():
serial_ports = ['/dev/ttyUSB0', '/dev/ttyUSB1', '/dev/ttyUSB2', '/dev/ttyUSB3', '/dev/tty.usbserial']
counter = 0
while True:
serial_port = serial_ports[counter % 5]
app.logger.info("Connecting Control Unit..." + str(serial_port))
class ControlUnitConnection:
def __init__(self):
self.cu = None
self.connect()

def connect(self):
serial_ports = ['/dev/ttyUSB0', '/dev/ttyUSB1', '/dev/ttyUSB2', '/dev/ttyUSB3', '/dev/tty.usbserial']
for serial_port in serial_ports:
app.logger.info("trying " + str(serial_port))

try:
self.cu = ControlUnit(serial_port, timeout=0.1)
app.logger.info(" connected to Control Unit Version {0} using serial port {1}".format(str(self.cu.version()), serial_port))
break
except serial.serialutil.SerialException:
emit_cu_status('not_connected', serial_port)
except connection.TimeoutError:
emit_cu_status('timeout', serial_port)
app.logger.info(" not connected")

def connected(self):
if self.cu is None:
return False
try:
cu = ControlUnit(serial_port, timeout=0.1)
version = cu.version()
app.logger.info("...connected to Control Unit Version: {}", repr(version))
break
version = self.cu.version()
app.logger.info("connected to Control Unit Version: {0}".format(str(version)))
except serial.serialutil.SerialException:
emit_cu_status('not_connected', serial_port)
emit_cu_status('not_connected')
return False
except connection.TimeoutError:
emit_cu_status('timeout', serial_port)
emit_cu_status('timeout')
return False
return True

counter += 1
eventlet.sleep(5)
def reset(self):
for n in range(3):
try:
self.cu.reset()
self.cu.start()
return True
except connection.TimeoutError:
app.logger.error("Timeout while resetting track")
time.sleep(0.1)

emit_cu_status('connected', serial_ports[counter % 5])
return cu
def cu(self):
return self.cu
1 change: 1 addition & 0 deletions app/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class RaceRegistrationForm(FlaskForm):
'Dauer',
choices=[
('0', _l('unlimited')),
('1m', _l('1 Minute')),
('5m', _l('5 Minutes')),
('10m', _l('10 Minutes')),
('20m', _l('20 Minutes')),
Expand Down
72 changes: 55 additions & 17 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from operator import attrgetter
import datetime
import json
from sqlalchemy import inspect, func, or_
from sqlalchemy import inspect, func
from app import app, db


Expand All @@ -14,6 +15,9 @@ class Race(db.Model):
started_at = db.Column(db.DateTime)
finished_at = db.Column(db.DateTime)

def __repr__(self):
return '<Race id: {}, type: {}, status: {}>'.format(self.id, self.type, self.status)

def parsed_grid(self):
if self.grid is None:
return None
Expand Down Expand Up @@ -43,24 +47,39 @@ def stop(self):
app.logger.info("stopping race and adding finished_at timestamp")
self.status = "stopped"
self.finished_at = datetime.datetime.now()
db.session.add(self)
db.session.commit()

def add_lap(self, controller, time):
racer_id = self.racer(controller).id,
car_id = self.car(controller).id
return self.save_lap(controller, time, racer_id, car_id)
racer = self.racer(controller)
car = self.car(controller)
if racer is None or car is None:
app.logger.info("Lap not added for unknown racer or car.")
return None
return self.save_lap(controller, time, racer.id, car.id)

def save_lap(self, controller, time, racer_id, car_id):
lap = Lap(
race_id=self.id,
controller=controller,
time=time,
racer_id=racer_id,
car_id=car_id
)
lap = Lap(race_id=self.id, controller=controller, time=time, racer_id=racer_id, car_id=car_id)
db.session.add(lap)
db.session.commit()
return lap

def has_reached_duration(self):
if str(self.duration) == '0':
return False
if self.duration.endswith('l'):
return self.has_reached_laps(int(self.duration[0:-1]))
elif self.duration.endswith('m'):
return self.has_reached_time(int(self.duration[0:-1]))
else:
return False

def has_reached_laps(self, laps_to_reach):
return self.statistics().maximum_laps() >= laps_to_reach

def has_reached_time(self, minutes_to_reach):
return datetime.datetime.now() - self.started_at >= datetime.timedelta(minutes=minutes_to_reach)

def racer(self, controller):
for grid_entry in self.parsed_grid():
if(grid_entry['controller'] == str(controller)):
Expand Down Expand Up @@ -101,19 +120,21 @@ def laps_by_controller(self, controller):
return Lap.query.filter_by(race_id=self.id, controller=controller)

def denormalize_laps(self):
for l in Lap.query.filter_by(race_id=self.id).all():
for lap in Lap.query.filter_by(race_id=self.id).all():
for grid_entry in self.parsed_grid():
if(int(grid_entry['controller']) == l.controller):
l.racer_id = grid_entry['racer'].id
l.car_id = grid_entry['car'].id
if(int(grid_entry['controller']) == lap.controller):
lap.racer_id = grid_entry['racer'].id
lap.car_id = grid_entry['car'].id
db.session.commit()

def statistics(self):
return Statistics(self)

@staticmethod
def current():
return next(iter(Race.query.filter(or_(Race.status == 'created', Race.status == 'started')).all()), None)
current_race = next(iter(Race.query.filter(Race.status == 'started').all()), None)
app.logger.info("current race is: " + repr(current_race))
return current_race


class Racer(db.Model):
Expand Down Expand Up @@ -172,7 +193,7 @@ class Lap(db.Model):
time = db.Column(db.Integer, index=False, unique=False)

def __repr__(self):
return '<Lap {} {}>'.format(self.controller, self.time)
return '<Lap {} {} {} {}>'.format(self.controller, self.time, self.racer_id, type(self.racer_id))

def to_json(self):
return json.dumps({c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs})
Expand All @@ -186,6 +207,11 @@ def car(self):
def formatted_time(self):
return '{:05.3f} s'.format(self.time / 1000)

@staticmethod
def fastest(race, controller):
app.logger.info("getting fastest lap for race {} and controller {}".format(repr(race), controller))
return Lap.query.filter(Lap.controller == controller, Lap.race_id == race.id).order_by(Lap.time).limit(1).first()


class Season(db.Model):
id = db.Column(db.Integer, primary_key=True)
Expand Down Expand Up @@ -221,6 +247,18 @@ class Statistics:
def __init__(self, race):
self.race = race

def fastest_laps(self):
fastest_laps = []
for grid_entry in self.race.parsed_grid():
fastest_laps.append(Lap.fastest(self.race, grid_entry['controller']))
return sorted(filter(None, fastest_laps), key=attrgetter('time'))

def maximum_laps(self):
laps_for_controller = []
for grid_entry in self.parsed_grid():
laps_for_controller.append(self.lap_count_by_controller(grid_entry['controller']))
return max(laps_for_controller)

def race_time_by_controller(self, controller):
time = self.function_on_laps_by_controller(controller, func.sum(Lap.time).label('race_time'))
return self.format_duration(time)
Expand Down
45 changes: 45 additions & 0 deletions app/observers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from app import app
from app.models import Timing
from app.socket_connection import emit_status, emit_lap, emit_race_finished


class EmittingRaceStatusObserver:
def __init__(self, race):
self.race = race

def notify_status(self, status):
app.logger.info("Processing a new status")
emit_status(status)


class EmittingRaceTimeObserver:
def __init__(self, race):
self.race = race
self.timings = [Timing(num) for num in range(0, 8)]

def notify_timer(self, time):
app.logger.info("Processing a new time")
controller = int(time.address)
timing = self.timings[controller]
timing.newlap(time)
self.race.add_lap(controller, timing.lap_time)
if timing.lap_time is not None and timing.laps > 0:
emit_lap(time, timing)
self.check_duration()

def notify_time_past(self):
app.logger.info("processing past time")
self.check_duration()

def check_duration(self):
if self.race.has_reached_duration():
self.race.stop()
emit_race_finished(self.race.id)


class LoggingDebugObserver:
def notify_timer(self, time):
app.logger.info("received a new time {}".format(time))

def notify_status(self, status):
app.logger.info("received a new status {}".format(status))
27 changes: 27 additions & 0 deletions app/race_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from app import app
from app import track_listener
from app.observers import EmittingRaceStatusObserver, EmittingRaceTimeObserver


def start(race):
# set status to started
if race is not None and race.status == 'created':
race.start()
# register observers and start track listener
return track_listener.start_track_listener(EmittingRaceStatusObserver(race), EmittingRaceTimeObserver(race))


def attach(race):
if not track_listener.track_listener_running():
track_listener.start_track_listener(EmittingRaceStatusObserver(race), EmittingRaceTimeObserver(race))

return track_listener.track_listener_running()


def finish(race):
# set race to stopped
app.logger.info("Race is " + repr(race))
if race is not None and (race.status == 'started' or race.status == 'created'):
race.stop()
# stop track listener
track_listener.stop_track_listener()
Loading