Compare commits

19 Commits

Author SHA1 Message Date
865abb5213 La til gtk tabeell 2023-12-04 12:14:00 +01:00
5e899fca62 Fikser fra løpsdagen 2023-12-02 13:25:17 +01:00
a75af1a99b La til støtte for starttid i tidsberegninga 2023-11-28 16:38:04 +01:00
18f49c1a0c Bug fikser med totaltid og strekktid 2023-11-28 01:34:18 +01:00
4048cfe94f la til forklaring i csven 2023-11-28 01:33:53 +01:00
a39c5e4615 La til gen kommando 2023-11-28 01:33:25 +01:00
f1612cf838 Starta på søke tui 2023-11-24 00:42:02 +01:00
24f8c30c81 Fiksa gaflinger 2023-11-24 00:41:38 +01:00
95f10000bb Småforbedringer 2023-11-21 22:34:40 +01:00
056b97c0e2 Oppdaterte requirements 2023-11-21 22:33:31 +01:00
568c935a55 La til git og fargelegging 2023-11-21 22:32:14 +01:00
c280c5f6a0 La til __main__ entrypoint 2023-11-20 15:00:32 +01:00
7fab832321 mtr simulator 2023-11-19 17:54:43 +01:00
9e35cc55af Første veson av mtr leser 2023-11-19 17:54:27 +01:00
3894eb7284 Starta på innganspunktet for programmet, lagde en init funksjon 2023-11-17 21:52:44 +01:00
de4f288a4a Fiksa iof xml import 2023-11-17 21:52:19 +01:00
7591523b37 Lagde funskjoner for å lese inn og ut seperate csv, mtr og db filer 2023-11-17 21:49:33 +01:00
e5a672ac70 Starta på yaml io for separate mtr og config filer 2023-11-17 13:55:21 +01:00
3f4051426e Fiksa strekktidsgenerering 2023-11-08 17:31:11 +01:00
13 changed files with 1224 additions and 64 deletions

2
otime/__main__.py Normal file
View File

@@ -0,0 +1,2 @@
from cli import main
main()

141
otime/cli.py Normal file
View File

@@ -0,0 +1,141 @@
import argparse
import file_io
import iof_xml
import serial
import subprocess
import os
import otime
import iof_xml
import pdf
from pdf import format_m_s
from rich import print
import search_tui
from rich.traceback import install
install(show_locals=True)
# Main entrypoint for now. Cli with two options; init will make the files needed and run will start the program from the specified directory
def main():
parser = argparse.ArgumentParser(description='Otime very alpha version 😁')
subparsers = parser.add_subparsers(dest='command')
parser_init = subparsers.add_parser('init', help='Create project files')
parser_init.add_argument('--entries', required=True, dest='entries_file', action='store', default='./',
help='The xml file with entries')
parser_init.add_argument('--courses', required=True, dest='courses_file', action='store', default='./',
help='The xml with courses')
parser_init.add_argument('--dir', required=False, dest='dir', action='store', default='./',
help='Specify a directort, if not set the files are created in the current directory')
parser_init = subparsers.add_parser('run', help='run otime')
parser_init.add_argument('--dir', required=False, dest='dir', action='store', default='./', help='specify a directory')
parser_init.add_argument('--port', required=False, dest='port', action='store', help='specify a serial port')
parser_init.add_argument('--xml', required=False, dest='xml_path', action='store', default=None, help='Where the xml result file should be saved')
parser_init = subparsers.add_parser('gen', help='Generate result files')
parser_init.add_argument('--dir', required=False, dest='dir', action='store', default='./', help='specify a directory')
parser_init.add_argument('--xml', required=False, dest='xml_path', action='store', default=None, help='Where the xml result file should be saved')
parser_init = subparsers.add_parser('view', help='View otime data')
parser_init.add_argument('--dir', required=False, dest='dir', action='store', default='./', help='specify a directory')
parser_init.add_argument('--xml', required=False, dest='xml_path', action='store', default=None, help='Where the xml result file should be saved')
parser_init = subparsers.add_parser('mtr', help='run mtr commands')
parser_init.add_argument('--port', required=False, dest='port', action='store', help='specify a serial port')
args = parser.parse_args()
try:
if not args.xml_path:
args.xml_path = args.dir + '/output'
except AttributeError:
pass
match args.command:
case 'init':
init_dir(args.dir, args.entries_file, args.courses_file)
case 'run':
run(args.port, args.dir, args.xml_path)
case 'view':
search_tui.main(args.dir)
case 'gen':
gen(args.dir, args.xml_path)
case 'mtr':
mtr = serial.Serial(port=args.port, baudrate=9600, timeout=40)
mtr.write(b'/SA')
def init_dir(project_dir, entries_xml_file, courses_xml_file):
# Lager mappe med en config fil, en csv fil med løpere og en fil med mtr data
csv_db_path = project_dir + '/runners.csv'
config_path = project_dir + '/config.yaml'
mtr_path = project_dir + 'mtr.yaml'
event = iof_xml.event_from_xml_entries(entries_xml_file)
event.courses = iof_xml.courses_from_xml(courses_xml_file)
print(f'Read {len(event.runners)} runners, {len(event.card_dumps)} ecards, {len(event.courses)} courses and {len(event.o_classes)} classes')
print('Remember to manually link the courses and classes in the config file')
file_io.write_runners_csv(event, csv_db_path)
file_io.write_config(event, config_path)
file_io.write_card_dumps(event, mtr_path)
os.makedirs(project_dir+'/output')
subprocess.run(['git', 'init', project_dir])
subprocess.run(['git', 'add', './*'], cwd=project_dir)
subprocess.run(['git', 'commit', '-m', f'Project initiated by otime'], cwd=project_dir)
def run(port='/dev/ttyUSB0', project_dir='./', xml_path='./output/'):
mtr = serial.Serial(port=port, baudrate=9600, timeout=40)
config_path = project_dir + '/config.yaml'
mtr_path = project_dir + '/mtr.yaml'
csv_path = project_dir + '/runners.csv'
while True:
if mtr.in_waiting > 0:
mtr.read_until(expected=b'\xFF\xFF\xFF\xFF')
size = mtr.read(size=1)
if size == b'\xe6':
event = file_io.event_from_yaml_and_csv(config_path, mtr_path, csv_path)
meat = mtr.read(229)
full = b'\xFF\xFF\xFF\xFF' + size + meat
card_dump = otime.CardDump.from_mtr_bytes(full)
event.card_dumps.append(card_dump)
file_io.write_card_dumps(event, mtr_path)
print(runner_info(event, card_dump))
subprocess.run(['git', 'add', './*'], cwd=project_dir, stdout=subprocess.DEVNULL)
subprocess.run(['git', 'commit', '-m', f'Added {card_dump.card}'], cwd=project_dir, stdout=subprocess.DEVNULL)
iof_xml.create_result_file(event, xml_path + '/results.xml')
pdf.create_result_list(event, project_dir + '/output/results.pdf')
elif size == b'\x37':
meat = mtr.read(55)
inspect(status)
def gen(project_dir='./', xml_path='./output/'):
config_path = project_dir + '/config.yaml'
mtr_path = project_dir + '/mtr.yaml'
csv_path = project_dir + '/runners.csv'
event = file_io.event_from_yaml_and_csv(config_path, mtr_path, csv_path)
subprocess.run(['git', 'add', './*'], cwd=project_dir, stdout=subprocess.DEVNULL)
subprocess.run(['git', 'commit', '-m', f'Manually run'], cwd=project_dir, stdout=subprocess.DEVNULL)
iof_xml.create_result_file(event, xml_path + '/results.xml')
pdf.create_result_list(event, project_dir + '/output/results.pdf')
def runner_info(event, card_dump):
runner = next((i for i in event.runners if str(i.card_id) == str(card_dump.card)), None)
if runner is None:
return card_dump
result = event.get_runner_result(runner.id)
if result is None:
return '🧐 Dette skal ikke skje...'
if result.status == 'OK':
result_text = f'[green]Place: {result.place}. Time: [bold][yellow]{format_m_s(result.total_time)}[/yellow][/bold][/green]'
elif result.status == 'MissingPunch':
result_text = f'[red]Missed Control! Time: [bold][yellow]{format_m_s(result.total_time)}[/yellow][/bold][/red]'
else:
result_text = f'[blue]{result.status}, Time: [bold][yellow]{result.total_time}[/yellow][/bold][/blue]'
return result_text + f' [blue]{result.fullname()}[/blue], {result.o_class}, {result.club}, [italic]Card: {result.card_id}[/italic]'
if __name__ == '__main__':
main()

52
otime/file_io.py Normal file
View File

@@ -0,0 +1,52 @@
from yaml import load, dump
try:
from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
from yaml import Loader, Dumper
from copy import deepcopy
import otime
# Disse funksjonene er for å kunne lese og skrive seperate config, mtr og databasefiler.
def write_config(event, file_path):
output_event = deepcopy(event)
output_event.runners = []
output_event.card_dumps = []
with open(file_path, 'w') as f:
dump(output_event, f)
def write_card_dumps(event, file_path):
card_dumps = deepcopy(event.card_dumps)
with open(file_path, 'w') as f:
dump(card_dumps, f)
def write_runners_csv(event, file_path):
with open(file_path, 'w') as f:
f.write('ID;Status;Fornavn, Etternavn;Klasse;klubb;Brikke;Gafling;Starttid\n')
for i in event.runners:
f.write(f'{i.id};{i.status_override};{i.first}, {i.last};{i.o_class};{i.club};{i.card_id};{i.fork};{i.start_time}\n')
def event_from_yaml_and_csv(config_path, mtr_path, csv_path):
try:
with open(mtr_path, 'r') as f:
card_dumps = load(f, Loader=Loader)
except FileNotFoundError:
card_dumps=[]
with open(config_path, 'r') as f:
event = load(f, Loader=Loader)
with open(csv_path, 'r') as f:
data = [i.split(';') for i in f.readlines()]
data.pop(0)
for i in data: i[2] = i[2].split(',')
for i in data:
# Setter starttid til None hvis den ikke er satt
if len(i[7]) > 8:
i[7] = i[7]
else:
i[7] = None
runners = [otime.Runner(id=i[0], status_override=i[1], first=i[2][0], last=i[2][1].strip(), o_class=i[3], club=i[4], card_id=int(i[5]), fork=int(i[6]), start_time=i[7]) for i in data]
event.card_dumps = card_dumps
event.runners = runners
return event

View File

@@ -1,5 +1,6 @@
import datetime
import xml.etree.ElementTree as ET
import otime
def xml_child(parent, tag, content):
# Used to make creating xml files easier
@@ -15,7 +16,7 @@ def create_result_file(event, file_path, o_classes=[]):
root.set('iofVersion', '3.0')
root.set('createTime', datetime.datetime.now().isoformat(timespec='seconds'))
root.set('creator', 'oTime')
root.set('status', 'Complete')
#root.set('status', 'Complete')
tree = ET.ElementTree(root)
xml_event = ET.SubElement(root, 'Event')
xml_child(xml_event, 'Id', event.id)
@@ -29,6 +30,9 @@ def create_result_file(event, file_path, o_classes=[]):
# <PersonResult>
for n in i.runner_results:
if n.status == 'DidNotStart':
continue
person_result = ET.SubElement(class_result, 'PersonResult')
# <Person>
person = ET.SubElement(person_result, 'Person')
@@ -63,16 +67,17 @@ def create_result_file(event, file_path, o_classes=[]):
xml_child(result, 'Status', n.status)
# <SplitTime>
# TODO: ta utgangspunkt i løypa, ikke det brikka har stempla
for code, split in zip(i.course.codes, n.splits):
for code, split in zip(i.course.codes[n.fork][:-1], n.splits[:-1]):
st = ET.SubElement(result, 'SplitTime')
xml_child(st, 'ControlCode', code)
xml_child(st, 'Time', split)
# </SplitTime>
elif n.status == 'MissingPunch':
xml_child(result, 'Status', n.status)
for code, split in zip(i.course.codes, n.splits):
for code, split in zip(i.course.codes[n.fork][:-1], n.splits[:-1]):
st = ET.SubElement(result, 'SplitTime')
xml_child(st, 'ControlCode', code)
if split != 0: xml_child(st, 'Time', split)
else:
xml_child(result, 'Status', n.status)
else:
@@ -119,8 +124,8 @@ def runners_from_xml_entries(xml_file):
card = None
start_time = None
runners.append(Runner(rid, first, last, club=club_name, club_id=club_id,
country=country,card=card, o_class_str=class_str,
runners.append(otime.Runner(rid, first, last, club=club_name, club_id=club_id,
country=country,card_id=card, o_class=class_str,
start_time=start_time, fee_id=fee_id))
return runners
@@ -150,7 +155,7 @@ def fees_from_xml_entries(xml_file):
except AttributeError:
to_birth_date = None
fee_objs.append(Fee(fee_id, name, currency, amount,
fee_objs.append(otime.Fee(fee_id, name, currency, amount,
from_birth_date=from_birth_date, to_birth_date=to_birth_date))
return fee_objs
@@ -170,5 +175,38 @@ def courses_from_xml(xml_file):
controls.remove('STA1')
controls.remove('FIN1')
controls = [int(l) for l in controls]
courseobjs.append(Course(name, controls))
courseobjs.append(otime.Course(name, [controls]))
return courseobjs
def event_from_xml_entries(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
url = '{http://www.orienteering.org/datastandard/3.0}'
event_el = root.find(f'./{url}Event')
event_id = int(event_el.find(f'./{url}Id').text)
name = event_el.find(f'./{url}Name').text
organiser = event_el.find(f'./{url}Organiser/{url}Name').text
start_ds = event_el.find(f'./{url}StartTime/{url}Date').text
start_ts = event_el.find(f'./{url}StartTime/{url}Time').text[:-1]
start_time = datetime.datetime.fromisoformat(f'{start_ds}T{start_ts}')
end_ds = event_el.find(f'./{url}EndTime/{url}Date').text
end_ts = event_el.find(f'./{url}EndTime/{url}Time').text[:-1]
end_time = datetime.datetime.fromisoformat(f'{end_ds}T{end_ts}')
person_entries = root.findall(f'./{url}PersonEntry')
class_names = []
for p_entry in person_entries:
class_names.append(p_entry.find(f'./{url}Class/{url}Name').text)
o_classes = [otime.OClass(i, []) for i in set(class_names)]
runners = runners_from_xml_entries(xml_file)
# TODO: fiks fees
#fees = fees_from_xml_entries(xml_file)
fees = []
return otime.Event(event_id, name, organiser=organiser, runners=runners,
fees=fees, start_time=start_time, end_time=end_time, o_classes=o_classes)

View File

@@ -2,12 +2,11 @@ import copy
import datetime
import re
import xml.etree.ElementTree as etree
import pdf
class Runner:
def __init__(self, id: int, first: str, last: str, club=None, club_id=None,
country=None, card_id=None, o_class_str=None, o_class=None,
fork=0, start_time=None, fee_id=None, fee=None):
fork=0, start_time=None, fee_id=None, fee=None, status_override=''):
self.id = id
self.first = first
self.last = last
@@ -19,7 +18,7 @@ class Runner:
self.fork = fork
self.start_time = start_time
self.fee_id = fee_id
self.status_override = ''
self.status_override = status_override
def __repr__(self):
return(f'name({self.fullname()})')
@@ -95,7 +94,7 @@ class CardDump:
splits.append(time)
# Extract start time:
year = int.from_bytes(datamsg[8:9], 'little')
year = 2000 + int.from_bytes(datamsg[8:9], 'little')
month = int.from_bytes(datamsg[9:10], 'little')
day = int.from_bytes(datamsg[10:11], 'little')
hours = int.from_bytes(datamsg[11:12], 'little')
@@ -162,12 +161,11 @@ class CardDump:
# Stored in Event.courses
class Course:
def __init__(self, name, codes, forked=False, variations=None):
def __init__(self, name, codes, forked=False):
self.name = name
# Codes is a list of courses
self.codes = codes
self.forked = forked
# Variations is a list
self.variations = variations
def __repr__(self):
return f'name({self.name})'
@@ -183,7 +181,7 @@ class OClass:
class RunnerResult:
def __init__(self, runner_id: int, first: str, last: str, status: str, place: int, total_time: int, splits: list[int], end_time,
club=None, club_id=None, country=None, card_id=None, o_class=None, controls=None, fork=0, start_time=None, fee_id=None):
club=None, club_id=None, country=None, card_id=None, o_class=None, controls=None, fork=0, start_time=datetime.datetime(1973, 1, 1), fee_id=None):
self.id = runner_id
self.first = first
self.last = last
@@ -266,33 +264,54 @@ class Event:
runner = self.get_runner(id)
if runner.status_override: return runner.status_override
o_class = self.get_o_class(runner.o_class)
if not o_class:
return 'Inactive'
course = self.get_course(o_class.course)
if self.get_card_dump(runner.card_id) == None:
if not self.get_card_dump(runner.card_id):
return 'Active'
if contains(course.codes, self.get_card_dump(runner.card_id).controls):
if contains(course.codes[runner.fork], self.get_card_dump(runner.card_id).controls):
return 'OK'
else:
return 'MissingPunch'
def get_runner_result(self, runner_id):
runner = self.get_runner(runner_id)
return next((i for i in produce_class_result(self, runner.o_class).runner_results if i.id == runner_id), None)
def get_runner_o_class(self, id):
runner = self.get_runner(id)
return next((copy.copy(i) for i in self.o_classes if i.name == runner.o_class), None)
def get_runner_time(self, id):
runner = self.get_runner(id)
card_dump = self.get_card_dump(runner.card_id)
if card_dump == None:
course = self.get_course(self.get_runner_o_class(id).course)
if card_dump == None or course == None:
return False
f_control = card_dump.controls[-1]
f_control = course.codes[runner.fork][-1]
# TODO: Må gjøres mer robust
try:
if f_control not in card_dump.controls:
# Hvis løperen ikke har vært på sistepost tar vi siste stempling istedet
return card_dump.splits[-1]
index = card_dump.controls.index(f_control)
# Hvis løperen ikke har en startid spesifisert brukes brikketid
if runner.start_time is None:
return card_dump.splits[index]
except:
return False
#Hvis det er en startid finner jeg tidsforskjellen mellom brikkestart og faktisk start og trekker den fra totaltida
else:
time_list = runner.start_time.split(':') # hour, minute, second
start_datetime = self.start_time.replace(hour=int(time_list[0]), minute=int(time_list[1]), second=int(time_list[2]))
diff = start_datetime - card_dump.s_time
return card_dump.splits[index] - diff.total_seconds()
def get_runner_splits(self, id):
# Tida brukt frem til hver post, ikke tida fra forrige post
try:
runner = self.get_runner(id)
card_dump = self.get_card_dump(runner.card_id)
course = self.get_course(self.get_o_class(runner.o_class).course)
codes = course.codes[runner.fork]
except AttributeError:
return None
@@ -300,21 +319,18 @@ class Event:
return None
split_iter = zip(card_dump.controls, card_dump.splits).__iter__()
splits = [0] * len(course.codes)
for n, control in enumerate(course.codes):
splits = [0] * len(codes)
for n, control in enumerate(codes):
if control not in card_dump.controls:
continue
split_debt = 0
while True:
try:
punched_control, split = next(split_iter)
except StopIteration:
break
if punched_control == control:
splits[n] = split + split_debt
splits[n] = split
break
else:
split_debt += split
return splits
def get_runner_controls(self, id):
@@ -332,36 +348,13 @@ class Event:
def get_result(self, o_classes: list[str] = []) -> list[ClassResult]:
if not o_classes:
o_classes = [i.name for i in self.o_classes]
return [produce_class_result(copy.deepcopy(self), i) for i in o_classes]
return [produce_class_result(self, i) for i in o_classes]
def read_xml_entries(self, xml_file):
self.add_runners(*runners_from_xml_entries(xml_file))
self.add_fees(*fees_from_xml_entries(xml_file))
# Må endres
def from_xml_entries(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
url = '{http://www.orienteering.org/datastandard/3.0}'
event_el = root.find(f'./{url}Event')
event_id = int(event_el.find(f'./{url}Id').text)
name = event_el.find(f'./{url}Name').text
organiser = event_el.find(f'./{url}Organiser/{url}Name').text
start_ds = event_el.find(f'./{url}StartTime/{url}Date').text
start_ts = event_el.find(f'./{url}StartTime/{url}Time').text[:-1]
start_time = datetime.datetime.fromisoformat(f'{start_ds}T{start_ts}')
end_ds = event_el.find(f'./{url}EndTime/{url}Date').text
end_ts = event_el.find(f'./{url}EndTime/{url}Time').text[:-1]
end_time = datetime.datetime.fromisoformat(f'{end_ds}T{end_ts}')
runners = runners_from_xml_entries(xml_file)
fees = fees_from_xml_entries(xml_file)
return Event(event_id, name, organiser=organiser, runners=runners,
fees=fees, start_time=start_time, end_time=end_time)
def read_xml_courses(self, xml_file):
self.courses = courses_from_xml(xml_file)
@@ -403,11 +396,11 @@ def produce_class_result(event, o_class_name) -> ClassResult:
other_runners = [i for i in runners if i not in ok_runners and i not in dsq_runners]
results = [RunnerResult(i.id, i.first, i.last, event.get_runner_status(i.id), ok_runners.index(i)+1, event.get_runner_time(i.id), event.get_runner_splits(i.id),
event.get_runner_end_clock(i.id), i.club, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id), start_time=event.get_card_dump(i.card_id).s_time) for i in ok_runners]
event.get_runner_end_clock(i.id), i.club, 0, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id), start_time=event.get_card_dump(i.card_id).s_time) for i in ok_runners]
results += [RunnerResult(i.id, i.first, i.last, event.get_runner_status(i.id), 0, event.get_runner_time(i.id), event.get_runner_splits(i.id),
event.get_runner_end_clock(i.id), i.club, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id), start_time=event.get_card_dump(i.card_id).s_time) for i in dsq_runners]
event.get_runner_end_clock(i.id), i.club, 0, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id), start_time=event.get_card_dump(i.card_id).s_time) for i in dsq_runners]
results += [RunnerResult(i.id, i.first, i.last, event.get_runner_status(i.id), 0, event.get_runner_time(i.id), event.get_runner_splits(i.id),
event.get_runner_end_clock(i.id), i.club, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id)) for i in other_runners]
event.get_runner_end_clock(i.id), i.club, 0, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id)) for i in other_runners]
return ClassResult(o_class.name, event.get_course(o_class.course), results)
@@ -424,7 +417,7 @@ def courses_from_ttime_conf(ttime_file):
n = n.split(',')
loops += 1
n = list(map(int, n))
courses.append(Course('course_'+str(loops), n))
courses.append(Course('course_'+str(loops), [n]))
return courses
def classes_from_ttime_conf(ttime_file, courses):

View File

@@ -11,7 +11,7 @@ def create_result_list(event, file_path, o_classes=[]):
pdf = FPDF()
pdf.add_page()
pdf.add_font("LiberationSans", fname="../../otime/data/fonts/LiberationSans-Regular.ttf")
pdf.add_font("LiberationSans", fname="otime/data/fonts/LiberationSans-Regular.ttf")
pdf.set_font("LiberationSans", size=10)
line_height = pdf.font_size * 1.5
col_width = pdf.epw / 4 # distribute content evenly
@@ -38,13 +38,13 @@ def create_split_result_list(event, file_path, o_classes=[]):
pdf = FPDF()
pdf.add_page(orientation='L')
pdf.add_font("LiberationSans", fname="../../otime/data/fonts/LiberationSans-Regular.ttf")
pdf.add_font("LiberationSans", fname="otime/data/fonts/LiberationSans-Regular.ttf")
pdf.set_font("LiberationSans", size=8)
line_height = pdf.font_size * 1.5
col_width = pdf.epw / 4 # distribute content evenly
for class_result in results:
col_width = pdf.epw / (21+len(class_result.course.codes))
col_width = 10
pdf.write(txt=class_result.name)
pdf.ln(line_height)
for runner in class_result.runner_results:

30
otime/search_tui.py Normal file
View File

@@ -0,0 +1,30 @@
from textual.app import App, ComposeResult
from textual.widgets import DataTable
import otime
import file_io
from pdf import format_m_s
global ROWS
class TableApp(App):
def compose(self) -> ComposeResult:
yield DataTable()
def on_mount(self) -> None:
global ROWS
table = self.query_one(DataTable)
table.add_columns(*ROWS[0])
table.add_rows(ROWS[1:])
def main(path):
event = file_io.event_from_yaml_and_csv(path + '/config.yaml', path + '/mtr.yaml', path + '/runners.csv')
result = event.get_result()
global ROWS
ROWS = []
for o_class in result:
ROWS += [(str(i.id), str(i.place), i.fullname(), i.o_class, i.club, str(i.card_id), format_m_s(i.total_time), str(i.controls)) for i in o_class.runner_results]
print(ROWS)
app = TableApp()
app.run()
if __name__ == "__main__":
main()

260
otime/ui.py Normal file
View File

@@ -0,0 +1,260 @@
# Copyright (C) 2021 Tim Lauridsen < tla[at]rasmil.dk >
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to
# the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
"""
Sample Python Gtk4 Application
"""
import sys
import time
from typing import List
import copy
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, GObject, Gio, Adw
from widgets import ColumnViewListStore
import otime
import file_io
class ColumnElem(GObject.GObject):
"""custom data element for a ColumnView model (Must be based on GObject)"""
def __init__(self, name: str):
super(ColumnElem, self).__init__()
self.name = name
def __repr__(self):
return f"ColumnElem(name: {self.name})"
"""
class OtimeRunnerView(Gtk.ListView):
__gtype_name__ = "OtimeRunnerRowView"
__gsignals__ = {"refresh": (GObject.SignalFlags.RUN_FIRST, None, ())}
selection = Gtk.Template.Child()
class YumexQueueRow(Gtk.Box):
__gtype_name__ = "YumexQueueRow"
icon = Gtk.Template.Child()
text = Gtk.Template.Child()
dep = Gtk.Template.Child()
def __init__(self, view, **kwargs):
super().__init__(**kwargs)
self.view: OtimeRunnerView = view
self.pkg: YumexPackage = None
"""
class MyColumnViewColumn(ColumnViewListStore):
"""Custom ColumnViewColumn"""
def __init__(
self, win: Gtk.ApplicationWindow, col_view: Gtk.ColumnView, data: List
):
# Init ListView with store model class.
super(MyColumnViewColumn, self).__init__(ColumnElem, col_view)
self.win = win
# put some data into the model
for elem in data:
self.add(ColumnElem(elem))
def factory_setup(self, widget, item: Gtk.ListItem):
"""Gtk.SignalListItemFactory::setup signal callback
Handles the creation widgets to put in the ColumnViewColumn
"""
label = Gtk.Text()
label.set_halign(Gtk.Align.START)
label.set_hexpand(True)
label.set_margin_start(10)
item.set_child(label)
def factory_bind(self, widget, item: Gtk.ListItem):
"""Gtk.SignalListItemFactory::bind signal callback
Handles adding data for the model to the widgets created in setup
"""
label = item.get_child() # Get the Gtk.Label stored in the ListItem
data = item.get_item() # get the model item, connected to current ListItem
label.set_text(data.name) # Update Gtk.Label with data from model item
class MyWindow(Adw.ApplicationWindow):
def __init__(self, title, width, height, **kwargs):
super(MyWindow, self).__init__(**kwargs)
self.set_default_size(width, height)
box = Gtk.Box()
box.props.orientation = Gtk.Orientation.VERTICAL
header = Gtk.HeaderBar()
stack = Adw.ViewStack()
switcher = Adw.ViewSwitcherTitle()
switcher.set_stack(stack)
header.set_title_widget(switcher)
box.append(header)
content = self.setup_content()
page1 = stack.add_titled(content, "Løpere", "Løpere")
box_p2 = Gtk.Box()
page2 = stack.add_titled(box_p2, "page2", "Page 2")
box.append(stack)
self.set_content(box)
def setup_content(self):
"""Add a page with a text selector to the stack"""
# ColumnView with custom columns
self.columnview = Gtk.ColumnView()
self.columnview.set_show_column_separators(True)
sw = Gtk.ScrolledWindow()
self.columnview = Gtk.ColumnView()
factory_c1 = Gtk.SignalListItemFactory()
factory_c1.connect("setup", setup_c)
factory_c1.connect("bind", bind_c1)
factory_c2 = Gtk.SignalListItemFactory()
factory_c2.connect("setup", setup_c)
factory_c2.connect("bind", bind_c2)
factory_c3 = Gtk.SignalListItemFactory()
factory_c3.connect("setup", setup_c)
factory_c3.connect("bind", bind_c3)
factory_c4 = Gtk.SignalListItemFactory()
factory_c4.connect("setup", setup_c)
factory_c4.connect("bind", bind_c4)
factory_c5 = Gtk.SignalListItemFactory()
factory_c5.connect("setup", setup_c)
factory_c5.connect("bind", bind_c5)
selection = Gtk.SingleSelection()
store = Gio.ListStore.new(DataObject)
selection.set_model(store)
self.columnview.set_model(selection)
columns = []
columns.append(Gtk.ColumnViewColumn.new("Fornavn", factory_c1))
columns.append(Gtk.ColumnViewColumn.new("Etternavn ", factory_c2))
columns.append(Gtk.ColumnViewColumn.new("Klubb", factory_c3))
columns.append(Gtk.ColumnViewColumn.new("klasse", factory_c4))
columns.append(Gtk.ColumnViewColumn.new("eCard", factory_c5))
for i in columns:
self.columnview.append_column(i)
project_dir = '/home/trygve/Dokumenter/ÅbN&FC_2'
config_path = project_dir + '/config.yaml'
mtr_path = project_dir + '/mtr.yaml'
csv_path = project_dir + '/runners.csv'
event = file_io.event_from_yaml_and_csv(config_path, mtr_path, csv_path)
data = [DataObject(i.first, i.last, i.club, i.o_class, i.card_id) for i in event.runners]
for i in data:
store.append(i)
lw_frame = Gtk.Frame()
lw_frame.set_valign(Gtk.Align.FILL)
lw_frame.set_vexpand(True)
lw_frame.set_margin_start(20)
lw_frame.set_margin_end(20)
lw_frame.set_margin_top(10)
lw_frame.set_margin_bottom(10)
sw.set_child(self.columnview)
lw_frame.set_child(sw)
return lw_frame
class DataObject(GObject.GObject):
__gtype_name__ = 'DataObject'
text = GObject.Property(type=str, default=None)
def __init__(self, f_name, l_name, club, o_class, card):
super().__init__()
self.f_name = f_name
self.l_name = l_name
self.club = club
self.o_class = o_class
self.card = str(card)
def setup_c(widget, item):
"""Setup the widget to show in the Gtk.Listview"""
text = Gtk.Text()
item.set_child(text)
def bind_c1(widget, item):
"""bind data from the store object to the widget"""
label = item.get_child()
obj = item.get_item()
label.set_text(obj.f_name)
label.bind_property("f_name", obj, "f_name")
def bind_c2(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.l_name)
def bind_c3(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.club)
def bind_c4(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.o_class)
def bind_c5(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.card)
class Application(Adw.Application):
"""Main Aplication class"""
def __init__(self):
super().__init__(
application_id="net.trygve.otime.alpha", flags=Gio.ApplicationFlags.FLAGS_NONE
)
def do_activate(self):
win = self.props.active_window
if not win:
win = MyWindow("Otime veldig alpha", 800, 800, application=self)
win.present()
def main():
"""Run the main application"""
app = Application()
return app.run(sys.argv)
if __name__ == "__main__":
main()

547
otime/widgets.py Normal file
View File

@@ -0,0 +1,547 @@
# Copyright (C) 2021 Tim Lauridsen < tla[at]rasmil.dk >
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to
# the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
"""
Misc classes to build Gtk4 Applications in python 3.9
It takes some of the hassle away from building Gtk4 application in Python
So you can create an cool application, without all the boilerplate code
"""
import os.path
from abc import abstractmethod
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, Gio, GLib, Gdk
from material import MATERIAL
def rgb_to_hex(r, g, b):
if isinstance(r, float):
r *= 255
g *= 255
b *= 255
return "#{0:02X}{1:02X}{2:02X}".format(int(r), int(g), int(b))
def color_to_hex(color):
return rgb_to_hex(color.red, color.green, color.blue)
def get_font_markup(fontdesc, text):
return f'<span font_desc="{fontdesc}">{text}</span>'
class MaterialColorDialog(Gtk.ColorChooserDialog):
""" Color chooser dialog with Material design colors """
def __init__(self, title, parent):
Gtk.ColorChooserDialog.__init__(self)
self.set_title(title)
self.set_transient_for(parent)
self.set_modal(True)
# build list of material colors in Gdk.RGBA format
color_values = []
for color_name in MATERIAL.colors:
colors = MATERIAL.get_palette(color_name)
for col in colors:
color = Gdk.RGBA()
color.parse(col)
color_values.append(color)
num_colors = 14
self.add_palette(Gtk.Orientation.HORIZONTAL, num_colors, color_values)
self.set_property('show-editor', False)
def get_color(self):
selected_color = self.get_rgba()
return color_to_hex(selected_color)
class ListViewBase(Gtk.ListView):
""" ListView base class, it setup the basic factory, selection model & data model
handlers must be overloaded & implemented in a sub class
"""
def __init__(self, model_cls):
Gtk.ListView.__init__(self)
# Use the signal Factory, so we can connect our own methods to setup
self.factory = Gtk.SignalListItemFactory()
# connect to Gtk.SignalListItemFactory signals
# check https://docs.gtk.org/gtk4/class.SignalListItemFactory.html for details
self.factory.connect('setup', self.on_factory_setup)
self.factory.connect('bind', self.on_factory_bind)
self.factory.connect('unbind', self.on_factory_unbind)
self.factory.connect('teardown', self.on_factory_teardown)
# Create data model, use our own class as elements
self.set_factory(self.factory)
self.store = self.setup_store(model_cls)
# create a selection model containing our data model
self.model = self.setup_model(self.store)
self.model.connect('selection-changed', self.on_selection_changed)
# set the selection model to the view
self.set_model(self.model)
def setup_model(self, store: Gio.ListModel) -> Gtk.SelectionModel:
""" Setup the selection model to use in Gtk.ListView
Can be overloaded in subclass to use another Gtk.SelectModel model
"""
return Gtk.SingleSelection.new(store)
@abstractmethod
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model
must be overloaded in subclass to use another Gio.ListModel
"""
raise NotImplemented
def add(self, elem):
""" add element to the data model """
self.store.append(elem)
# Gtk.SignalListItemFactory signal callbacks
# transfer to some some callback stubs, there can be overloaded in
# a subclass.
def on_factory_setup(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Setup the widgets to go into the ListView """
self.factory_setup(widget, item)
def on_factory_bind(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" GtkSignalListItemFactory::bind signal callback
apply data from model to widgets set in setup"""
self.factory_bind(widget, item)
def on_factory_unbind(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::unbind signal callback
Undo the the binding done in ::bind if needed
"""
self.factory_unbind(widget, item)
def on_factory_teardown(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Undo the creation done in ::setup if needed
"""
self.factory_teardown(widget, item)
def on_selection_changed(self, widget, position, n_items):
# get the current selection (GtkBitset)
selection = widget.get_selection()
# the the first value in the GtkBitset, that contain the index of the selection in the data model
# as we use Gtk.SingleSelection, there can only be one ;-)
ndx = selection.get_nth(0)
self.selection_changed(widget, ndx)
# --------------------> abstract callback methods <--------------------------------
# Implement these methods in your subclass
@abstractmethod
def factory_setup(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" Setup the widgets to go into the ListView (Overload in subclass) """
pass
@abstractmethod
def factory_bind(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" apply data from model to widgets set in setup (Overload in subclass)"""
pass
@abstractmethod
def factory_unbind(self, widget: Gtk.ListView, item: Gtk.ListItem):
pass
@abstractmethod
def factory_teardown(self, widget: Gtk.ListView, item: Gtk.ListItem):
pass
@abstractmethod
def selection_changed(self, widget, ndx):
""" trigged when selecting in listview is changed
ndx: is the index in the data store model that is selected
"""
pass
class ListViewListStore(ListViewBase):
""" ListView base with an Gio.ListStore as data model
It can contain misc objects derived from GObject
"""
def __init__(self, model_cls):
super(ListViewListStore, self).__init__(model_cls)
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model """
return Gio.ListStore.new(model_cls)
class ListViewStrings(ListViewBase):
""" Add ListView with only strings """
def __init__(self):
super(ListViewStrings, self).__init__(Gtk.StringObject)
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model
Can be overloaded in subclass to use another Gio.ListModel
"""
return Gtk.StringList()
class SelectorBase(Gtk.ListBox):
""" Selector base class """
def __init__(self):
Gtk.ListBox.__init__(self)
# Setup the listbox
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.connect('row-selected', self.on_row_changes)
self._rows = {}
self.ndx = 0
self.callback = None
def add_row(self, name, markup):
""" Overload this in a subclass"""
raise NotImplemented
def on_row_changes(self, widget, row):
ndx = row.get_index()
if self.callback:
self.callback(self._rows[ndx])
else:
print(f'Row Selected : {self._rows[ndx]}')
def set_callback(self, callback):
self.callback = callback
class TextSelector(SelectorBase):
""" Vertical Selector Widget that contains a number of strings where one can be selected """
def add_row(self, name: str, markup: str):
""" Add a named row to the selector with at given icon name"""
# get the image
label = Gtk.Label()
label.set_markup(markup)
# set the widget size request to 32x32 px, so we get some margins
# label.set_size_request(100, 24)
label.set_single_line_mode(True)
label.set_halign(Gtk.Align.START)
label.set_hexpand(True)
label.set_xalign(0)
label.set_margin_start(5)
label.set_margin_end(10)
row = self.append(label)
# store the index names, so we can find it on selection
self._rows[self.ndx] = name
self.ndx += 1
class IconSelector(SelectorBase):
""" Vertical Selector Widget that contains a number of icons where one can be selected """
def add_row(self, name, icon_name):
""" Add a named row to the selector with at given icon name"""
# get the image
pix = Gtk.Image.new_from_icon_name(icon_name)
# set the widget size request to 32x32 px, so we get some margins
pix.set_size_request(32, 32)
row = self.append(pix)
# store the index names, so we can find it on selection
self._rows[self.ndx] = name
self.ndx += 1
class ViewColumnBase(Gtk.ColumnViewColumn):
""" ColumnViewColumn base class, it setup the basic factory, selection model & data model
handlers must be overloaded & implemented in a sub class
"""
def __init__(self, model_cls, col_view):
Gtk.ColumnViewColumn.__init__(self)
self.col_view = col_view
# Use the signal Factory, so we can connect our own methods to setup
self.factory = Gtk.SignalListItemFactory()
# connect to Gtk.SignalListItemFactory signals
# check https://docs.gtk.org/gtk4/class.SignalListItemFactory.html for details
self.factory.connect('setup', self.on_factory_setup)
self.factory.connect('bind', self.on_factory_bind)
self.factory.connect('unbind', self.on_factory_unbind)
self.factory.connect('teardown', self.on_factory_teardown)
# Create data model, use our own class as elements
self.set_factory(self.factory)
self.store = self.setup_store(model_cls)
# create a selection model containing our data model
self.model = self.setup_model(self.store)
self.model.connect('selection-changed', self.on_selection_changed)
# add model to the ColumnView
self.col_view.set_model(self.model)
def setup_model(self, store: Gio.ListModel) -> Gtk.SelectionModel:
""" Setup the selection model to use in Gtk.ListView
Can be overloaded in subclass to use another Gtk.SelectModel model
"""
return Gtk.SingleSelection.new(store)
@abstractmethod
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model
must be overloaded in subclass to use another Gio.ListModel
"""
raise NotImplemented
def add(self, elem):
""" add element to the data model """
self.store.append(elem)
# Gtk.SignalListItemFactory signal callbacks
# transfer to some some callback stubs, there can be overloaded in
# a subclass.
def on_factory_setup(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Setup the widgets to go into the ListView """
self.factory_setup(widget, item)
def on_factory_bind(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" GtkSignalListItemFactory::bind signal callback
apply data from model to widgets set in setup"""
self.factory_bind(widget, item)
def on_factory_unbind(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::unbind signal callback
Undo the the binding done in ::bind if needed
"""
self.factory_unbind(widget, item)
def on_factory_teardown(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Undo the creation done in ::setup if needed
"""
self.factory_teardown(widget, item)
def on_selection_changed(self, widget, position, n_items):
# get the current selection (GtkBitset)
selection = widget.get_selection()
# the the first value in the GtkBitset, that contain the index of the selection in the data model
# as we use Gtk.SingleSelection, there can only be one ;-)
ndx = selection.get_nth(0)
self.selection_changed(widget, ndx)
# --------------------> abstract callback methods <--------------------------------
# Implement these methods in your subclass
@abstractmethod
def factory_setup(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
""" Setup the widgets to go into the ColumnViewColumn (Overload in subclass) """
pass
@abstractmethod
def factory_bind(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
""" apply data from model to widgets set in setup (Overload in subclass)"""
pass
@abstractmethod
def factory_unbind(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
pass
@abstractmethod
def factory_teardown(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
pass
@abstractmethod
def selection_changed(self, widget, ndx):
""" trigged when selecting in listview is changed
ndx: is the index in the data store model that is selected
"""
pass
class ColumnViewListStore(ViewColumnBase):
""" ColumnViewColumn base with an Gio.ListStore as data model
It can contain misc objects derived from GObject
"""
def __init__(self, model_cls, col_view):
super(ColumnViewListStore, self).__init__(model_cls, col_view)
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model """
return Gio.ListStore.new(model_cls)
class SearchBar(Gtk.SearchBar):
""" Wrapper for Gtk.Searchbar Gtk.SearchEntry"""
def __init__(self, win=None):
super(SearchBar, self).__init__()
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
box.set_spacing(10)
# Add SearchEntry
self.entry = Gtk.SearchEntry()
self.entry.set_hexpand(True)
box.append(self.entry)
# Add Search Options button (Menu content need to be added)
btn = Gtk.MenuButton()
btn.set_icon_name('preferences-other-symbolic')
self.search_options = btn
box.append(btn)
self.set_child(box)
# connect search entry to seach bar
self.connect_entry(self.entry)
if win:
# set key capture from main window, it will show searchbar, when you start typing
self.set_key_capture_widget(win)
# show close button in search bar
self.set_show_close_button(True)
# Set search mode to off by default
self.set_search_mode(False)
def set_callback(self, callback):
""" Connect the search entry activate to an callback handler"""
self.entry.connect('activate', callback)
class ButtonRow(Gtk.Box):
""" Row of button"""
def __init__(self, btn_list: list, callback):
super(ButtonRow, self).__init__(orientation=Gtk.Orientation.HORIZONTAL)
# self.set_halign(Gtk.Align.CENTER)
self.set_margin_start(20)
self.set_margin_top(20)
self.set_spacing(10)
for title in btn_list:
btn = Gtk.Button()
btn.set_label(f'{title}')
btn.connect('clicked', callback)
self.append(btn)
class SwitchRow(Gtk.Box):
def __init__(self, text):
super(SwitchRow, self).__init__(orientation=Gtk.Orientation.HORIZONTAL)
# Switch to control overlay visibility
self.label = Gtk.Label.new(text)
self.label.set_halign(Gtk.Align.FILL)
self.label.set_valign(Gtk.Align.CENTER)
self.label.set_hexpand(True)
self.label.set_xalign(0.0)
self.label.set_margin_start(20)
self.label.set_margin_bottom(20)
self.append(self.label)
self.switch = Gtk.Switch()
self.switch.set_halign(Gtk.Align.END)
self.switch.set_margin_end(20)
self.switch.set_margin_bottom(20)
self.append(self.switch)
def connect(self, signal, callback):
self.switch.connect(signal, callback)
def set_state(self, state):
self.switch.set_state(state)
class MenuButton(Gtk.MenuButton):
"""
Wrapper class for at Gtk.Menubutton with a menu defined
in a Gtk.Builder xml string
"""
def __init__(self, xml, name, icon_name='open-menu-symbolic'):
super(MenuButton, self).__init__()
builder = Gtk.Builder()
builder.add_from_string(xml)
menu = builder.get_object(name)
self.set_menu_model(menu)
self.set_icon_name(icon_name)
class Stack(Gtk.Stack):
""" Wrapper for Gtk.Stack with with a StackSwitcher """
def __init__(self):
super(Stack, self).__init__()
self.switcher = Gtk.StackSwitcher()
self.switcher.set_stack(self)
self._pages = {}
def add_page(self, name, title, widget):
page = self.add_child(widget)
page.set_name(name)
page.set_title(title)
self._pages[name] = page
return page
class Window(Gtk.ApplicationWindow):
""" custom Gtk.ApplicationWindow with a headerbar"""
def __init__(self, title, width, height, **kwargs):
super(Window, self).__init__(**kwargs)
self.set_default_size(width, height)
self.headerbar = Gtk.HeaderBar()
self.set_titlebar(self.headerbar)
label = Gtk.Label()
label.set_text(title)
self.headerbar.set_title_widget(label)
# custom CSS provider
self.css_provider = None
def load_css(self, css_fn):
"""create a provider for custom styling"""
if css_fn and os.path.exists(css_fn):
css_provider = Gtk.CssProvider()
try:
css_provider.load_from_path(css_fn)
except GLib.Error as e:
print(f"Error loading CSS : {e} ")
return None
print(f'loading custom styling : {css_fn}')
self.css_provider = css_provider
def _add_widget_styling(self, widget):
if self.css_provider:
context = widget.get_style_context()
context.add_provider(
self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
def add_custom_styling(self, widget):
self._add_widget_styling(widget)
# iterate children recursive
for child in widget:
self.add_custom_styling(child)
def create_action(self, name, callback):
""" Add an Action and connect to a callback """
action = Gio.SimpleAction.new(name, None)
action.connect("activate", callback)
self.add_action(action)

View File

@@ -1 +1,3 @@
fpdf2
pyserial
rich

View File

@@ -0,0 +1,2 @@
#!/bin/bash
socat -d -d pty,rawer,b9600 pty,rawer,b9600

View File

@@ -0,0 +1,78 @@
import sys
sys.path.insert(0, '../../otime')
import argparse
import serial
import otime
from rich import inspect
def main():
global package_n
package_n = 0
argparser = argparse.ArgumentParser(
description=("Acts as a live mtr reading data line by line from a log file and sending them over serial"))
argparser.add_argument('port', help='Serial port identifier')
argparser.add_argument(
'-f', '--file',
help=(
"Mtr log file to use"))
argparser.add_argument(
'-v', '--verbose', action='store_true', help='Verbose output')
args = argparser.parse_args()
port = serial.Serial(port=args.port, baudrate=9600)
event = otime.Event(0, 'NoName')
event.read_mtr_file(args.file)
for card in event.card_dumps:
input('Press enter to send next card')
if args.verbose == True:
inspect(card)
data = card_to_bytes(card)
port.write(data)
def card_to_bytes(card):
global package_n
package_n += 1
msg = b'\xFF\xFF\xFF\xFF\xE6\x4D\x9C\x3E'
year = int(str(card.read_time.year)[2:4])
month = card.read_time.month
day = card.read_time.day
hour = card.read_time.hour
minute = card.read_time.minute
second = card.read_time.second
msg += year.to_bytes(1, byteorder='little')
msg += month.to_bytes(1, byteorder='little')
msg += day.to_bytes(1, byteorder='little')
msg += hour.to_bytes(1, byteorder='little')
msg += minute.to_bytes(1, byteorder='little')
msg += second.to_bytes(1, byteorder='little')
#ms
msg += b'\x00\x00'
msg += package_n.to_bytes(4, byteorder='little')
msg += card.card.to_bytes(3, byteorder='little')
# Gidder ikke å finne ut hva Producweek Producyear er
msg += b'\x00\x00'
# Hopper overogså over ECardHeadSum
msg += b'\x00'
for n in range(50):
try:
control = card.controls[n]
split = card.splits[n]
except:
control = 0
split = 0
msg += control.to_bytes(1, byteorder='little')
msg += split.to_bytes(2, byteorder='little')
# 56 byte string
msg += int(0).to_bytes(56, byteorder='little')
# Checksum
csum = sum(msg) % 256
msg += csum.to_bytes(1, byteorder='little')
msg += b'\x00'
return msg
if __name__ == '__main__':
main()

View File

@@ -3,16 +3,31 @@ sys.path.insert(0, '../../otime')
import otime
import pdf
import iof_xml
import file_io
def main():
def ttime_testing():
event = otime.Event(0, 'TEEEST', start_time=None, end_time=None,organiser='Tygbe')
event.read_ttime_cnf('tt.cnf')
event.read_ttime_db('db.csv')
event.read_mtr_file('mtr.csv')
pdf.create_split_result_list(event, 'output/result.pdf')
iof_xml.create_result_file(event, 'output/result.xml')
iof_xml.create_result_file(event, '/home/trygve/Prosjekter/simple-liveresults/resultater/Resultater.xml')
print(event.get_runner_status('1400'))
results = event.get_result()
print([pdf.format_m_s(i) for i in event.get_runner_splits('17')])
print([pdf.format_m_s(i) for i in event.get_card_dump(event.get_runner('17').card_id).splits])
file_io.write_config(event, 'output/config.yaml')
file_io.write_card_dumps(event, 'output/mtr.yaml')
file_io.write_runners_csv(event, 'output/runners.csv')
print(file_io.event_from_yaml_and_csv('output/config.yaml', 'output/mtr.yaml', 'output/runners.csv'))
def xml_testing():
event = iof_xml.event_from_xml_entries('entries.xml')
event.courses = iof_xml.courses_from_xml('course.xml')
file_io.write_config(event, 'output/config.yaml')
file_io.write_card_dumps(event, 'output/mtr.yaml')
file_io.write_runners_csv(event, 'output/runners.csv')
print(file_io.event_from_yaml_and_csv('output/config.yaml', 'output/mtr.yaml', 'output/runners.csv'))
if __name__ == '__main__':
main()
xml_testing()