diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index f8bf06ca..00000000 --- a/.editorconfig +++ /dev/null @@ -1,19 +0,0 @@ -# EditorConfig: http://editorconfig.org/ -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -[*.py] -indent_style = space -indent_size = 4 - -[*.{js,html,css}] -indent_style = space -indent_size = 2 - -[Makefile] -indent_style = tab diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 973bc3ce..00000000 --- a/.gitattributes +++ /dev/null @@ -1,12 +0,0 @@ -ansible-protos/* linguist-detectable=true -dawn/static/website-robot-api-master/assets/main.css linguist-vendored -dawn/static/website-robot-api-master/assets/css/* linguist-vendored -dawn/static/website-robot-api-master/assets/fonts/* linguist-vendored -hibike/Arduino-Makefile/* linguist-vendored -hibike/lib/* linguist-vendored -hibike/lib/hibike/* linguist-vendored=false -shepherd/static/Spoiled_Candies.xcf filter=lfs diff=lfs merge=lfs -text -shepherd/static/Twisted.xcf filter=lfs diff=lfs merge=lfs -text -shepherd/static/BlueTwisted.xcf filter=lfs diff=lfs merge=lfs -text -shepherd/static/GoldTwisted.xcf filter=lfs diff=lfs merge=lfs -text -shepherd/static/icons[[:space:]]copy.ai filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 82d7f1ea..44e4c904 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +dev_handler +shm_api.c + ### https://raw.github.com/github/gitignore/afbff9027d02ccfc680e031f6c295f79ad61662d/C++.gitignore # Prerequisites @@ -404,3 +407,9 @@ hibike/virtual_devices.txt /dawn-* /artifacts .vagrant + +# user token used for google auth flow +user_token.json + + + diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index e51b3430..00000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -8.12.0 diff --git a/.travis.yml b/.travis.yml index 919a68c0..9c382b7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,32 +1,31 @@ dist: xenial sudo: required os: linux - language: python python: 3.7 - -# list packages installable using apt here addons: - apt: - packages: - - python3-setuptools - - python3-pip - - libasound2-dev - - socat - -# requires that commit is for a PR or is on master branch -if : (type = pull_request) OR (type = push AND branch = master) - + apt: + packages: + - python3-setuptools + - python3-pip + - libasound2-dev + - socat +if: "(type = pull_request) OR (type = push AND branch = master)" jobs: - include: - - name: test - script: - - echo "testing" - - - name: format - script: - - echo "formatting" - - - name: build - script: - - echo "building" + include: + - name: test + install: + - pip install -r requirements.txt + script: + - echo "testing" + - cd shepherd + - python3 testing_script.py + - name: format + script: + - echo "formatting" + - name: build + script: + - echo "building" +before_install: +- openssl aes-256-cbc -K $encrypted_6a33d71cd260_key -iv $encrypted_6a33d71cd260_iv + -in shepherd/sheets/user_token.json.enc -out shepherd/sheets/user_token.json -d diff --git a/README.md b/README.md index 3f35e8bd..03ae5836 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,55 @@ # Shepherd -Shepherd is the team that is in charge of field control. +Shepherd is the team that is in charge of field control. Shepherd brings together all the data on the game field into one centralized location, where it keeps track of score, processes game-specific actions, keeps track of time, and informs the scoreboard. +## Sections + +- [Architecture](#Architecture) +- [Installing Dependencies](#Installing-Dependencies) +- [Running Shepherd](#Running-Shepherd) +- [Testing](./shepherd/tests/TESTING_DOCS.md) + ## Architecture Shepherd is essentially a [Flask](https://palletsprojects.com/p/flask/) web app that communicates with: -* Arduino devices on the field over USB serial. -* Each robot's [Runtime](https://github.com/pioneers/PieCentral/tree/master/runtime) instance using MessagePack remote procedure calls over TCP. -* Each driver station's [Dawn](https://github.com/pioneers/PieCentral/tree/master/dawn) instance. -* Each scoreboard client, which is rendered with jQuery. Typically, there is a scoreboard on each side of the field, a projection for spectators, and a fourth for the field control staff. -* Each perk selection tablet (specific to Sugar Blast). +- Arduino devices on the field over USB serial. +- Each robot's [Runtime](https://github.com/pioneers/PieCentral/tree/master/runtime) instance using MessagePack remote procedure calls over TCP. +- Each driver station's [Dawn](https://github.com/pioneers/PieCentral/tree/master/dawn) instance. +- Each scoreboard client, which is rendered with jQuery. Typically, there is a scoreboard on each side of the field, a projection for spectators, and a fourth for the field control staff. +- Each perk selection tablet (specific to Sugar Blast). ## Installing dependencies -### Python dependencies -Run the following -``` -pip install -r requirements.txt +```bash +pip3 install -r requirements.txt ``` -### LCM - -#### Linux -Run the installlcm script -``` -./installlcm -``` - -#### Mac -1. Set the Java Version to 8 -``` -export JAVA_HOME=$(/usr/libexec/java_home -v 1.8) -``` -2. Run the installlcm script -``` -./installlcm -``` +## Architecture -## Running Shepherd +To read about Shepherd in detail, check out the [onboarding readme](https://github.com/pioneers/shepherd-onboarding#about-shepherd). This is where you will find detailed information about what each component of Shepherd does. -### Instructions for 2018-2019, aka Sugar Blast: -This year, we ran Shepherd on Ajax, one of the computers owned by PiE. -In order to make it easier to run Shepherd on various machines, we added needed dependencies to a Pipfile. -This allows you to work in a virtual environment with all necessary dependencies -(except for [LCM](https://lcm-proj.github.io/build_instructions.html), which must be installed beforehand) -by issuing the following commands in PieCentral/shepherd: -``` -pipenv install -pipenv shell -``` -Next, open a terminal pane for each of the below (using tmux) and run the following commands in PieCentral/shepherd directory. +## 2021 Instructions -1: -``` -export FLASK_APP=server.py -flask run -``` +First get the release labelled "working game spring 2021" or grab the core_game_2021 branch. -2: -``` -export FLASK_APP=scoreboard_server.py -flask run -``` +For competition, we have 5 different scripts to run. Matthew has created a tmux script that runs everything we need that we encourage you to modify in future years because it is really easy to use. Do this inside a terminal (not VSCode) so that you have space. -3: ``` -export FLASK_APP=dawn_server.py -flask run +docker restart sheep +docker attach sheep +cd outsideshep/shepherd +./shepherd_tmux.sh ``` - -4: +After you are done, be sure to stop the container to please Samuel. ``` -python3 Shepherd.py +docker stop sheep ``` -5: -``` -python3 Sensors.py -``` +One part of this that has not yet been tested is dev handler communication with Arduino devices. After that, Shepherd should be able to run straight out of the Docker container. -6: -``` -export FLASK_APP=perks_server.py -flask run +If you are not using docker, open a terminal, cd into shepherd and run ``` +./shepherd_tmux.sh +``` \ No newline at end of file diff --git a/makerelease b/makerelease deleted file mode 100755 index 9bad09aa..00000000 --- a/makerelease +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash - -# Usage: makerelease (tag|update) -# -# Script for creating tags in a standard format interactively. - -set -e -source "$(git rev-parse --show-toplevel)/DevOps/frankfurter/scripts/tools/env" - -function get_shepherd_version { - bash -c "cd '$piecentral/shepherd' && pipenv run python3 Shepherd.py --version" -} - -function get_runtime_version { - bash -c "cd '$piecentral/runtime' && pipenv run python3 runtime.py --version" -} - -function get_dawn_version { - cat "${piecentral}/dawn/package.json" | python -c 'import sys, json; print(json.load(sys.stdin)["version"])' -} - -function set_runtime_version { - bash -c "cd '$piecentral/runtime' && sed -i -e 's/^__version__\s*=\s*.*$/__version__ = ($1, $2, $3)/g' runtimeUtil.py" -} - -function set_shepherd_version { - bash -c "cd '$piecentral/shepherd' && sed -i -e 's/^__version__\s*=\s*.*$/__version__ = ($1, $2, $3)/g' Shepherd.py" -} - -function set_dawn_version { - bash -c "cd '$piecentral/dawn' && sed -i -e 's/\"version\":\s*\".*\",$/\"version\": \"$1.$2.$3\",/g' package.json" -} - -cmd="$1" -project="$2" -usage="Usage: $0 (update|tag|help) [options] - -Commands: - update Interactively increment a version number according to - semantic versioning. The updated version number is written - back to the file where the version number for a project is - stored. You must add and commit this change before merging - a feature branch. - tag Use the project's version number to tag the current commit - with a standard name. You should use this after a feature - branch has been merged into master to trigger a release - from Travis. You will need to push the newly generated tag - yourself. - help Display this help message." -if [ "$cmd" != 'tag' ] && [ "$cmd" != 'update' ] || [ ! "$project" ] || [ "$cmd" = 'help' ]; then - echo "$usage" - exit 1 -elif [ ! -d "$project" ]; then - echo -e $red"Error: '$1' is not a project."$clear - exit 2 -fi - -semver_pattern='[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+' - -if [ "$cmd" = 'tag' ]; then - if [ $(git symbolic-ref --short HEAD) != "master" ]; then - echo -e $yellow"Warning: you are not on the 'master' branch."$clear - fi - now=$(date +%Y-%m-%dT%H%M%S) - tag="$project/$(get_"$project"_version)-$now" - git tag "$tag" - echo -e $green"Created tag: $tag"$clear -else - last_release=$(get_"$project"_version) - echo -e $blue"Last release of '$project' was: $last_release"$clear - - last_number_pattern='[[:digit:]]+$' - patch=$(echo "$last_release" | grep -Eo "$last_number_pattern") - minor=$(echo "$last_release" | grep -Eo "^[[:digit:]]+\.[[:digit:]]+" | grep -Eo "$last_number_pattern") - major=$(echo "$last_release" | grep -Eo "^[[:digit:]]+") - - if [ $(prompt "Increment patch version number?") ]; then - patch=$((patch + 1)) - elif [ $(prompt "Increment minor version number?") ]; then - minor=$((minor + 1)) - patch='0' - elif [ $(prompt "Increment major version number?") ]; then - echo -e $yellow"Remember that major version changes are reserved for backwards-incompatible changes."$clear - major=$((major + 1)) - minor='0' - patch='0' - fi - - echo "Next release: $major.$minor.$patch" - if [ $(prompt "Confirm release number?") ]; then - set_"$project"_version "$major" "$minor" "$patch" - else - echo -e $red"Next release number rejected. Aborting."$clear - fi -fi diff --git a/requirements.txt b/requirements.txt index b8a9ca50..3b56fc28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,35 +1,11 @@ -bidict==0.21.2 -cachetools==4.2.1 -certifi==2020.12.5 -chardet==4.0.0 -click==7.1.2 -Flask==1.1.2 -Flask-SocketIO==5.0.1 -gevent==21.1.2 -gevent-websocket==0.10.1 -google-api-core==1.25.1 -google-api-python-client==1.12.8 -google-auth==1.25.0 -google-auth-httplib2==0.0.4 -googleapis-common-protos==1.52.0 -greenlet==1.0.0 -httplib2==0.18.1 -idna==2.10 -itsdangerous==1.1.0 -Jinja2==2.11.3 -MarkupSafe==1.1.1 -oauth2client==4.1.3 -protobuf==3.14.0 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -python-engineio==4.0.0 -python-socketio==5.0.4 -pytz==2021.1 -requests==2.25.1 -rsa==4.7 -six==1.15.0 -uritemplate==3.0.1 -urllib3==1.26.3 -Werkzeug==1.0.1 -zope.event==4.5.0 -zope.interface==5.2.0 +##### Protobuf for Runtime stuff ##### +protobuf + +##### Google Sheets ##### +google-api-python-client +google-auth-httplib2 +google-auth-oauthlib + +##### Flask server ##### +flask-socketio +gevent-websocket diff --git a/shepherd/.gitattributes b/shepherd/.gitattributes deleted file mode 100644 index 9c2f3cd2..00000000 --- a/shepherd/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -static/AIRHORNMLG.wav filter=lfs diff=lfs merge=lfs -text diff --git a/shepherd/Alliance.py b/shepherd/Alliance.py deleted file mode 100644 index 379ab4d7..00000000 --- a/shepherd/Alliance.py +++ /dev/null @@ -1,69 +0,0 @@ -import math -from Utils import * -from Timer import * -from LCM import * - -class Alliance: - """This is the Alliance class, which holds the state values used to track - the scores and states of the alliances - name - an Enum 'GOLD' or 'BLUE' representing the Alliance - team_1_name - String representing name of first team - team_2_name - String representing name of second team - team_1_number - Integer representing team number of first team - team_2_number - Integer representing team number of second team - score - Integer tracking the score of the Alliance - recipe_times - Array storing the times for each recipe - recipe_count - Integer representing the number of recipes - penalties - Array storing the times for each penalty an alliance receives - """ - - def __init__(self, name, team_1_name, team_1_number, team_2_name, - team_2_number, team_1_custom_ip=None, team_2_custom_ip=None): - self.name = name - self.team_1_name = team_1_name - self.team_2_name = team_2_name - self.team_1_number = team_1_number - self.team_2_number = team_2_number - self.score = 0 - self.team_1_connection = False - self.team_2_connection = False - self.team_1_custom_ip = team_1_custom_ip - self.team_2_custom_ip = team_2_custom_ip - self.recipe_times = [] - self.recipe_count = len(self.recipe_times) - self.penalties = [] - - def change_score(self, amount): - """ changes score of this alliance by Amount, - Amount can be negative or positive. - """ - self.score += amount - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.SCORE, - {"alliance" : self.name, "score" : math.floor(self.score)}) - - def reset(self): - self.score = 0 - self.team_1_connection = False - self.team_2_connection = False - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.SCORE, - {"alliance" : self.name, "score" : math.floor(self.score)}) - #TODO: Send info to sensors about reset - #TODO: Send info to UI about reset - #TODO: Move score sends to shepherd.py - self.recipe_times = [] - self.recipe_count = 0 - self.penalties = [] - - def __str__(self): - return (" ") - - def increment(self, _time): - #TODO: Add the time to the recipe_times - self.recipe_times.append(_time) - self.recipe_count += 1 - - def penalty(self, _time): - #TODO: Add penalites - self.penalties.append(_time) diff --git a/shepherd/Code.py b/shepherd/Code.py deleted file mode 100644 index 9cedbcb1..00000000 --- a/shepherd/Code.py +++ /dev/null @@ -1,47 +0,0 @@ -import random -import math -import numpy as np -from Utils import * - -codes = [] -solutions = [] -code_solution = {} - -def generate_code(code_list): - ''' - Take a list of codes that return a new random code - ''' - temp = np.random.permutation(10) - num = 0 - for a in temp: - num = num * 10 + a - while num in code_list: - temp = np.random.permutation(10) - num = 0 - for a in temp: - temp = temp * 10 + a - return num - - -def decode(code): - ''' - Call all functions - ''' - return - -def assign_code_solution(): - ''' - Generate 16 codes and create a code_solution dictionary - ''' - codes.clear() - solutions.clear() - code_solution.clear() - for i in range(16): - # pylint: disable=assignment-from-no-return - new_code = generate_code(codes) - codes.append(new_code) - for code in codes: - solutions.append(decode(code)) - for i in range(16): - code_solution[codes[i]] = solutions[i] - return code_solution diff --git a/shepherd/LCM.py b/shepherd/LCM.py deleted file mode 100644 index 3746d558..00000000 --- a/shepherd/LCM.py +++ /dev/null @@ -1,41 +0,0 @@ -import threading -import json -import lcm # pylint: disable=import-error - -LCM_address = 'udpm://239.255.76.68:7667?ttl=2' - -def lcm_start_read(receive_channel, queue, put_json=False): - ''' - Takes in receiving channel name (string), queue (Python queue object). - Takes whether to add received items to queue as JSON or Python dict. - Creates thread that receives any message to receiving channel and adds - it to queue as tuple (header, dict). - header: string - dict: Python dictionary - ''' - comm = lcm.LCM(LCM_address) - - def handler(_, item): - if put_json: - queue.put(item.decode()) - else: - dic = json.loads(item.decode()) - header = dic.pop('header') - queue.put((header, dic)) - - comm.subscribe(receive_channel, handler) - - def run(): - while True: - comm.handle() - - rec_thread = threading.Thread(target=run) - rec_thread.start() - -def lcm_send(target_channel, header, dic={}): # pylint: disable=dangerous-default-value - ''' - Send header and dictionary to target channel (string) - ''' - dic['header'] = header - json_str = json.dumps(dic) - lcm.LCM(LCM_address).publish(target_channel, json_str.encode()) diff --git a/shepherd/Log.py b/shepherd/Log.py deleted file mode 100644 index 4be41c4a..00000000 --- a/shepherd/Log.py +++ /dev/null @@ -1,28 +0,0 @@ -import datetime -import Shepherd -from Utils import * - -last_header = None -#pylint: disable=redefined-builtin, no-member -def log(Exception): - global last_header - # if Shepherd.match_number <= 0: - # return - now = datetime.datetime.now() - filename = str(now.month) + "-" + str(now.day) + "-" + str(now.year) +\ - "-match-"+ str(Shepherd.match_number) +".txt" - print("a normally fatal exception occured, but Shepherd will continue to run") - print("all known details are logged to logs/"+filename) - file = open("logs/"+filename, "a+") - file.write("\n========================================\n") - file.write("a normally fatal exception occured.\n") - file.write("all relevant data may be found below.\n") - file.write("match: " + str(Shepherd.match_number)+"\n") - file.write("game state: " + str(Shepherd.game_state)+"\n") - file.write("gold alliance: " + str(Shepherd.alliances[ALLIANCE_COLOR.GOLD])+"\n") - file.write("blue alliance: " + str(Shepherd.alliances[ALLIANCE_COLOR.BLUE])+"\n") - file.write("game timer running?: " + str(Shepherd.game_timer.is_running())+"\n") - file.write("the last received header was:" + str(last_header)+"\n") - file.write("a stacktrace of the error may be found below\n") - file.write(str(Exception)) - file.close() diff --git a/shepherd/Makefile b/shepherd/Makefile deleted file mode 100644 index ee3d854d..00000000 --- a/shepherd/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -.PHONY: install artifacts-install lint test artificats - -install: - pip3 install pipenv - pipenv install --dev - ./installlcm - -artifacts-install: - $(nop) - -lint: - pylint *.py - -test: - $(nop) - -artifacts: - zip -r ../artifacts/shepherd.zip . diff --git a/shepherd/Pipfile b/shepherd/Pipfile deleted file mode 100644 index eb9225db..00000000 --- a/shepherd/Pipfile +++ /dev/null @@ -1,19 +0,0 @@ -[[source]] -url = "https://pypi.python.org/simple" -verify_ssl = true - -[requires] -python_version = "3.7" - -[packages] -google-api-python-client = "*" -oauth2client = "*" -pylint = "*" -pyserial = "*" -click = "*" -httplib2 = "*" -google-auth-httplib2 = "*" -google-auth-oauthlib = "*" -msgpack-rpc-python = "*" -numpy = "*" -simpleaudio = "*" diff --git a/shepherd/Pipfile.lock b/shepherd/Pipfile.lock deleted file mode 100644 index 1056809e..00000000 --- a/shepherd/Pipfile.lock +++ /dev/null @@ -1,325 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "d1a49ad664993006ad1310d0b91c672d58358ab4eb7c9e2fdc06ff6e4d29bdbe" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "astroid": { - "hashes": [ - "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", - "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" - ], - "version": "==2.4.1" - }, - "cachetools": { - "hashes": [ - "sha256:1d057645db16ca7fe1f3bd953558897603d6f0b9c51ed9d11eb4d071ec4e2aab", - "sha256:de5d88f87781602201cde465d3afe837546663b168e8b39df67411b0bf10cefc" - ], - "version": "==4.1.0" - }, - "certifi": { - "hashes": [ - "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", - "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" - ], - "version": "==2020.4.5.1" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "click": { - "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" - ], - "version": "==7.0" - }, - "google-api-python-client": { - "hashes": [ - "sha256:06907006ed5ce831018f03af3852d739c0b2489cdacfda6971bcc2075c762858", - "sha256:937eabdc3940977f712fa648a096a5142766b6d0a0f58bc603e2ac0687397ef0" - ], - "version": "==1.7.8" - }, - "google-auth": { - "hashes": [ - "sha256:73b141d122942afe12e8bfdcb6900d5df35c27d39700f078363ba0b1298ad33b", - "sha256:fbf25fee328c0828ef293459d9c649ef84ee44c0b932bb999d19df0ead1b40cf" - ], - "version": "==1.15.0" - }, - "google-auth-httplib2": { - "hashes": [ - "sha256:098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445", - "sha256:f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08" - ], - "version": "==0.0.3" - }, - "google-auth-oauthlib": { - "hashes": [ - "sha256:a0470c19130ddf90c2b07c0c701d72890a7335090903aeb709f003a66416380f", - "sha256:c57303d85199fdba00bc7b8fb21ccf6c2b9d3e69d6830fd69ff951c64cf2c1d6" - ], - "version": "==0.3.0" - }, - "httplib2": { - "hashes": [ - "sha256:4f6988e6399a2546b525a037d56da34aed4d149bbdc0e78523018d5606c26e74", - "sha256:b0e1f3ed76c97380fe2485bc47f25235453b40ef33ca5921bb2897e257a49c4c" - ], - "version": "==0.18.0" - }, - "idna": { - "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" - ], - "version": "==2.9" - }, - "isort": { - "hashes": [ - "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", - "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" - ], - "version": "==4.3.21" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", - "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", - "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", - "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", - "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", - "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", - "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", - "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", - "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", - "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", - "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", - "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", - "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", - "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", - "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", - "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", - "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", - "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", - "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", - "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", - "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" - ], - "version": "==1.4.3" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "msgpack-python": { - "hashes": [ - "sha256:378cc8a6d3545b532dfd149da715abae4fda2a3adb6d74e525d0d5e51f46909b" - ], - "version": "==0.5.6" - }, - "msgpack-rpc-python": { - "hashes": [ - "sha256:ba7188129f2ba95fb9731f906aa82a5abd5827dd54ecfc715008f5ce2314535f" - ], - "version": "==0.4.1" - }, - "numpy": { - "hashes": [ - "sha256:0e2eed77804b2a6a88741f8fcac02c5499bba3953ec9c71e8b217fad4912c56c", - "sha256:1c666f04553ef70fda54adf097dbae7080645435fc273e2397f26bbf1d127bbb", - "sha256:1f46532afa7b2903bfb1b79becca2954c0a04389d19e03dc73f06b039048ac40", - "sha256:315fa1b1dfc16ae0f03f8fd1c55f23fd15368710f641d570236f3d78af55e340", - "sha256:3d5fcea4f5ed40c3280791d54da3ad2ecf896f4c87c877b113576b8280c59441", - "sha256:48241759b99d60aba63b0e590332c600fc4b46ad597c9b0a53f350b871ef0634", - "sha256:4b4f2924b36d857cf302aec369caac61e43500c17eeef0d7baacad1084c0ee84", - "sha256:54fe3b7ed9e7eb928bbc4318f954d133851865f062fa4bbb02ef8940bc67b5d2", - "sha256:5a8f021c70e6206c317974c93eaaf9bc2b56295b6b1cacccf88846e44a1f33fc", - "sha256:754a6be26d938e6ca91942804eb209307b73f806a1721176278a6038869a1686", - "sha256:771147e654e8b95eea1293174a94f34e2e77d5729ad44aefb62fbf8a79747a15", - "sha256:78a6f89da87eeb48014ec652a65c4ffde370c036d780a995edaeb121d3625621", - "sha256:7fde5c2a3a682a9e101e61d97696687ebdba47637611378b4127fe7e47fdf2bf", - "sha256:80d99399c97f646e873dd8ce87c38cfdbb668956bbc39bc1e6cac4b515bba2a0", - "sha256:88a72c1e45a0ae24d1f249a529d9f71fe82e6fa6a3fd61414b829396ec585900", - "sha256:a4f4460877a16ac73302a9c077ca545498d9fe64e6a81398d8e1a67e4695e3df", - "sha256:a61255a765b3ac73ee4b110b28fccfbf758c985677f526c2b4b39c48cc4b509d", - "sha256:ab4896a8c910b9a04c0142871d8800c76c8a2e5ff44763513e1dd9d9631ce897", - "sha256:abbd6b1c2ef6199f4b7ca9f818eb6b31f17b73a6110aadc4e4298c3f00fab24e", - "sha256:b16d88da290334e33ea992c56492326ea3b06233a00a1855414360b77ca72f26", - "sha256:b78a1defedb0e8f6ae1eb55fa6ac74ab42acc4569c3a2eacc2a407ee5d42ebcb", - "sha256:cfef82c43b8b29ca436560d51b2251d5117818a8d1fb74a8384a83c096745dad", - "sha256:d160e57731fcdec2beda807ebcabf39823c47e9409485b5a3a1db3a8c6ce763e" - ], - "version": "==1.16.3" - }, - "oauth2client": { - "hashes": [ - "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac", - "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6" - ], - "version": "==4.1.3" - }, - "oauthlib": { - "hashes": [ - "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", - "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" - ], - "version": "==3.1.0" - }, - "pyasn1": { - "hashes": [ - "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" - ], - "version": "==0.4.8" - }, - "pyasn1-modules": { - "hashes": [ - "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", - "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" - ], - "version": "==0.2.8" - }, - "pylint": { - "hashes": [ - "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", - "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" - ], - "version": "==2.3.1" - }, - "pyserial": { - "hashes": [ - "sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627", - "sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8" - ], - "version": "==3.4" - }, - "requests": { - "hashes": [ - "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", - "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" - ], - "version": "==2.23.0" - }, - "requests-oauthlib": { - "hashes": [ - "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" - ], - "version": "==1.3.0" - }, - "rsa": { - "hashes": [ - "sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66", - "sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487" - ], - "version": "==4.0" - }, - "simpleaudio": { - "hashes": [ - "sha256:3341639374db4056539b247368e580dcab16f6941b6ad50ab8dbae042947296f", - "sha256:3a6a56d45d7c113522a5e33073b405296e58cd2006e1e31df4be437175572e06", - "sha256:3b72b3d07fba29079119a99203fe9d1bacf13d0f36cc9ce7b218af28d96123df", - "sha256:4e0e4c0359f153408e9b8a65342ec0c1d014f36fbe9cdb594e9b6e5348916af8", - "sha256:5d191a1db01c5ce1c5e2e62bf130908881c84fb3b3b77bd191c6ca00b75f1715", - "sha256:7228ef213ca6cd6c5ba87ae760a8784e809a825a19d6b857a07dcc2cc51e80f7", - "sha256:80cbebce76a0d1992c29d60e528e4061a61a88415aa1a06cf7b3af6a9633fed4", - "sha256:8fb659da74abf9c87ae1ea86eb4f5abc0b535e790bfb8bd5f34024940444b941", - "sha256:9ee4e2e40ff5b0d1e5cf831563893296d2ca6b5953b5b7082316fae9d7e4f532", - "sha256:a0e29eae2cfb2e7e69a60072464cb82d1d87f32816aff652cab0315a0d07976d", - "sha256:a934c65402344512906b0e0e1523823d691bc00f3ebb96fec0ebc6cd9bdddbcd", - "sha256:b2ace62cb78e04c53fab52e798d29a5760f0da8fc29a716672a974e8c635449b", - "sha256:c80b04aadc7aa0da23ad1ab669819e38e46123a84f2b310dfe1eb2d27daa5c1b", - "sha256:c8edd2019fea20afad93a938f0e42ca8d9b2c5dfe9569de7bce91d33a2a5b73f", - "sha256:e38ef77ae4ed4f873b8fc4e2eaf04e46a5444fe1b7847ea7ef53423c2c5ee721", - "sha256:fb5300b7b7d1cf93667f37f25bc7efddeea3df18afd742eb9b9a4afe55eea8b2" - ], - "version": "==1.0.2" - }, - "six": { - "hashes": [ - "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", - "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" - ], - "version": "==1.14.0" - }, - "tornado": { - "hashes": [ - "sha256:5ef073ac6180038ccf99411fe05ae9aafb675952a2c8db60592d5daf8401f803", - "sha256:6d14e47eab0e15799cf3cdcc86b0b98279da68522caace2bd7ce644287685f0a", - "sha256:92b7ca81e18ba9ec3031a7ee73d4577ac21d41a0c9b775a9182f43301c3b5f8e", - "sha256:ab587996fe6fb9ce65abfda440f9b61e4f9f2cf921967723540679176915e4c3", - "sha256:b36298e9f63f18cad97378db2222c0e0ca6a55f6304e605515e05a25483ed51a" - ], - "version": "==4.5.3" - }, - "typed-ast": { - "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" - ], - "markers": "implementation_name == 'cpython' and python_version < '3.8'", - "version": "==1.4.1" - }, - "uritemplate": { - "hashes": [ - "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", - "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" - ], - "version": "==3.0.1" - }, - "urllib3": { - "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" - ], - "version": "==1.25.9" - }, - "wrapt": { - "hashes": [ - "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" - ], - "version": "==1.12.1" - } - }, - "develop": {} -} diff --git a/shepherd/Sensors.py b/shepherd/Sensors.py deleted file mode 100644 index a928851e..00000000 --- a/shepherd/Sensors.py +++ /dev/null @@ -1,136 +0,0 @@ -"""import sys -import time -import threading -from multiprocessing import Queue -import serial # pylint: disable=import-error -from LCM import * -from Utils import * - - -# run "ls /dev/tty*" to obtain the two ACM ports. - -buttons_gold_port = "/dev/ttyACM1" #change to correct port -buttons_blue_port = "/dev/ttyACM5" # change to correct port - -alliance_mapping = { - "gold": ALLIANCE_COLOR.GOLD, - "blue": ALLIANCE_COLOR.BLUE -} - -IDENTIFY_TIMEOUT = 5 - -def get_working_serial_ports(excludes: set): - """"""Get a list of working serial ports, excluding some. - Returns a list of `serial.Serial` object. - """""" - import glob - maybe_ports = set(glob.glob("/dev/ttyACM*")) - # maybe_ports = set(glob.glob("/dev/tty.usb*")) - maybe_ports.difference_update(excludes) - - working = [] - for p in maybe_ports: - try: - working.append(serial.Serial(p, baudrate=115200)) - except serial.SerialException: - pass - - return working - - -def identify_relevant_ports(working_ports): - """""" Check which ports have linebreak sensors or bidding stations on them. - Returns a list of tuples containing the object type, alliance, - and its corresponding serial port. - """""" - def maybe_identify_sensor(serial_port, timeout, msg_q): - """"""Check whether a serial port contains a sensor. - Parameters: - serial_port -- the port to check - timeout -- quit reading from the serial port after this - amount of time - msg_q -- a queue to set if a device is successfully identified - """""" - prev_timeout = serial_port.timeout - serial_port.timeout = timeout - try: - msg = serial_port.readline().decode("utf-8") - object_alliance = msg[0:1] - msg_q.put((object_alliance, serial_port)) - except serial.SerialTimeoutException: - pass - serial_port.timeout = prev_timeout - - msg_q = Queue() - threads = [threading.Thread(target=maybe_identify_sensor, - args=(port, - IDENTIFY_TIMEOUT, - msg_q)) for port in working_ports] - for thread in threads: - thread.start() - - time.sleep(IDENTIFY_TIMEOUT) - for thread in threads: - thread.join() - # parse through queue and make appropriate tuples - sensor_ports = [] - while not msg_q.empty(): - sensor_ports += [msg_q.get()] - return sensor_ports - - -def recv_from_btn(ser, alliance_enum): - print("<1> Starting Button Receive Thread", flush=True) - while True: - sensor_msg = ser.readline().decode("utf-8") - sensor_msg.lower() - payload_list = sensor_msg.split(",") - if len(payload_list) == 1: - continue - payload_list[1] = payload_list[1][:-2] - print("<2> Message Received: ", payload_list, flush=True) - button_num = payload_list[1] - send_dictionary = {"alliance" : alliance_enum, "button" : button_num} - lcm_send(LCM_TARGETS.SHEPHERD, SHEPHERD_HEADER.LAUNCH_BUTTON_TRIGGERED, send_dictionary) - print("sent dictionary:" + str(send_dictionary), flush=True) - time.sleep(0.01) - - -def main(): - # working_ports = get_working_serial_ports(set()) - # print("working ports: ", working_ports) - # relevant_ports = identify_relevant_ports(working_ports) - # print("relevant ports: ", relevant_ports) - - # button_serial_blue = None - # button_serial_gold = None - - button_serial_gold = serial.Serial(buttons_gold_port, baudrate=115200) - button_serial_blue = serial.Serial(buttons_blue_port, baudrate=115200) - - # for alliance, port in relevant_ports: - # if alliance == 'b': - # button_serial_blue = port - # elif alliance == 'g': - # button_serial_gold = port - - - button_thread_blue = threading.Thread( - target=recv_from_btn, name="button blue", args=([button_serial_blue, ALLIANCE_COLOR.BLUE]) - ) - button_thread_gold = threading.Thread( - target=recv_from_btn, name="buttons gold", args=([button_serial_gold, ALLIANCE_COLOR.GOLD]) - ) - - button_thread_blue.daemon = True - button_thread_gold.daemon = True - - button_thread_blue.start() - button_thread_gold.start() - - while True: - time.sleep(100) - -if __name__ == "__main__": - main() -""" diff --git a/shepherd/Sheet.py b/shepherd/Sheet.py deleted file mode 100644 index 25343e1a..00000000 --- a/shepherd/Sheet.py +++ /dev/null @@ -1,135 +0,0 @@ -"""To Install: Run `pip install --upgrade google-api-python-client`""" - -from __future__ import print_function - -import os -import csv - -import httplib2 # pylint: disable=import-error -from googleapiclient import discovery # pylint: disable=import-error,no-name-in-module -from oauth2client import client # pylint: disable=import-error -from oauth2client import tools # pylint: disable=import-error -from oauth2client.file import Storage # pylint: disable=import-error - -from Utils import * - -# If modifying these scopes, delete your previously saved credentials -# at ~/.credentials/sheets.googleapis.com-python-quickstart.json -SCOPES = 'https://www.googleapis.com/auth/spreadsheets' -CLIENT_SECRET_FILE = 'Sheets/client_secret.json' -APPLICATION_NAME = 'Google Sheets API Python Quickstart' - - -def get_credentials(): - """Gets valid user credentials from storage. - - If nothing has been stored, or if the stored credentials are invalid, - the OAuth2 flow is completed to obtain the new credentials. - - Returns: - Credentials, the obtained credential. - """ - home_dir = os.path.expanduser('~') - credential_dir = os.path.join(home_dir, '.credentials') - if not os.path.exists(credential_dir): - os.makedirs(credential_dir) - credential_path = os.path.join(credential_dir, - 'sheets.googleapis.com-python-quickstart.json') - - store = Storage(credential_path) - credentials = store.get() - if not credentials or credentials.invalid: - flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES) - flow.user_agent = APPLICATION_NAME - # Needed only for compatibility with Python 2.6 - credentials = tools.run_flow(flow, store) - print('Storing credentials to ' + credential_path) - return credentials - -def get_match(match_number): - #return get_offline_match(match_number) - try: - return get_online_match(match_number) - except httplib2.ServerNotFoundError: - return get_offline_match(match_number) - -def write_scores(match_number, blue_score, gold_score): - try: - write_online_scores(match_number, blue_score, gold_score) - except httplib2.ServerNotFoundError: - print("Unable to write to spreadsheet") - -def get_online_match(match_number): - """ - A lot of this is adapted from google quickstart. - Takes the match number and returns a dictionary with the teams names - and team numbers for that match. - """ - credentials = get_credentials() - http = credentials.authorize(httplib2.Http()) - discoveryUrl = ('https://sheets.googleapis.com/$discovery/rest?' - 'version=v4') - service = discovery.build('sheets', 'v4', http=http, - discoveryServiceUrl=discoveryUrl) - spreadsheetId = CONSTANTS.SPREADSHEET_ID - range_name = "Match Database!A2:J" - spreadsheet = service.spreadsheets() # pylint: disable=no-member - game_data = spreadsheet.values().get( - spreadsheetId=spreadsheetId, range=range_name).execute() - row = 48 - for i, j in enumerate(game_data['values']): - if int(j[0]) == match_number: - row = i - match = game_data['values'][row] - return {"b1name" : match[3], "b1num" : match[2], - "b2name" : match[5], "b2num" : match[4], - "g1name" : match[7], "g1num" : match[6], - "g2name" : match[9], "g2num" : match[8]} - -def get_offline_match(match_number): - """ - reads from the downloaded csv file in the event that the online file cannot - be read from. - """ - csv_file = open(CONSTANTS.CSV_FILE_NAME, newline='') - match_reader = csv.reader(csv_file, delimiter=' ', quotechar='|') - matches = list(match_reader) - match = matches[match_number] - match = " ".join(match) - match = match.split(',') - return {"b1name" : match[3], "b1num" : match[2], - "b2name" : match[5], "b2num" : match[4], - "g1name" : match[7], "g1num" : match[6], - "g2name" : match[9], "g2num" : match[8]} - -# pylint: disable=too-many-locals -def write_online_scores(match_number, blue_score, gold_score): - """ - A method that writes the scores to the sheet - """ - credentials = get_credentials() - http = credentials.authorize(httplib2.Http()) - discoveryUrl = ('https://sheets.googleapis.com/$discovery/rest?' - 'version=v4') - service = discovery.build('sheets', 'v4', http=http, - discoveryServiceUrl=discoveryUrl) - spreadsheetId = CONSTANTS.SPREADSHEET_ID - - range_name = "Match Database!A2:J" - spreadsheet = service.spreadsheets() # pylint: disable=no-member - game_data = spreadsheet.values().get( - spreadsheetId=spreadsheetId, range=range_name).execute() - row = 47 - for i, j in enumerate(game_data['values']): - if int(j[0]) == match_number: - row = i - - range_name = "'Match Database'!K" + str(row + 2) + ":L" + str(row + 2) - score_sheets = service.spreadsheets() # pylint: disable=no-member - game_scores = score_sheets.values().get( - spreadsheetId=spreadsheetId, range=range_name).execute() - game_scores['values'] = [[blue_score, gold_score]] - sheets = service.spreadsheets() # pylint: disable=no-member - sheets.values().update(spreadsheetId=spreadsheetId, - range=range_name, body=game_scores, - valueInputOption="RAW").execute() diff --git a/shepherd/Sheets/client_secret.json b/shepherd/Sheets/client_secret.json deleted file mode 100644 index af1e8669..00000000 --- a/shepherd/Sheets/client_secret.json +++ /dev/null @@ -1 +0,0 @@ -{"installed":{"client_id":"906041237671-fkro33tfu1m79loaikheaft3v9r0b4v2.apps.googleusercontent.com","project_id":"ivory-being-194603","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"qNzg1w7agAM2NZ4TR7fof78F","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} \ No newline at end of file diff --git a/shepherd/Shepherd.py b/shepherd/Shepherd.py deleted file mode 100644 index 6615065b..00000000 --- a/shepherd/Shepherd.py +++ /dev/null @@ -1,491 +0,0 @@ -import argparse -import queue -import random -import time -import datetime -import traceback -from Alliance import * -from LCM import * -from Timer import * -from Utils import * -from Code import * -from runtimeclient import RuntimeClientManager -import Sheet -import bot -import audio - - -clients = RuntimeClientManager((), ()) - -__version__ = (1, 0, 0) - - -########################################### -# Evergreen Methods -########################################### - -#pylint: disable=broad-except -def start(): - ''' - Main loop which processes the event queue and calls the appropriate function - based on game state and the dictionary of available functions - ''' - global LAST_HEADER - global EVENTS - EVENTS = queue.Queue() - lcm_start_read(LCM_TARGETS.SHEPHERD, EVENTS) - while True: - print("GAME STATE OUTSIDE: ", GAME_STATE) - time.sleep(0.1) - payload = EVENTS.get(True) - LAST_HEADER = payload - print(payload) - if GAME_STATE == STATE.SETUP: - func = SETUP_FUNCTIONS.get(payload[0]) - if func is not None: - func(payload[1]) - else: - print("Invalid Event in Setup") - elif GAME_STATE == STATE.AUTO: - func = AUTO_FUNCTIONS.get(payload[0]) - if func is not None: - func(payload[1]) - else: - print("Invalid Event in Auto") - elif GAME_STATE == STATE.WAIT: - func = WAIT_FUNCTIONS.get(payload[0]) - if func is not None: - func(payload[1]) - else: - print("Invalid Event in Wait") - elif GAME_STATE == STATE.TELEOP: - func = TELEOP_FUNCTIONS.get(payload[0]) - if func is not None: - func(payload[1]) - else: - print("Invalid Event in Teleop") - elif GAME_STATE == STATE.END: - func = END_FUNCTIONS.get(payload[0]) - if func is not None: - func(payload[1]) - else: - print("Invalid Event in End") - -#pylint: disable=too-many-locals -def to_setup(args): - ''' - Move to the setup stage which is should push scores from previous game to spreadsheet, - load the teams for the upcoming match, reset all state, and send information to scoreboard. - By the end, should be ready to start match. - ''' - global MATCH_NUMBER - global GAME_STATE - global STARTING_SPOTS - - b1_name, b1_num, b1_starting_spot = args["b1name"], args["b1num"], args["b1_starting_spot"] - b2_name, b2_num, b2_starting_spot = args["b2name"], args["b2num"], args["b2_starting_spot"] - g1_name, g1_num, g1_starting_spot = args["g1name"], args["g1num"], args["g1_starting_spot"] - g2_name, g2_num, g2_starting_spot = args["g2name"], args["g2num"], args["g2_starting_spot"] - - g1_custom_ip = args["g1_custom_ip"] or None - g2_custom_ip = args["g2_custom_ip"] or None - b1_custom_ip = args["b1_custom_ip"] or None - b2_custom_ip = args["b2_custom_ip"] or None - - STARTING_SPOTS = [b1_starting_spot, b2_starting_spot, g1_starting_spot, g2_starting_spot] - - if GAME_STATE == STATE.END: - flush_scores() - - MATCH_NUMBER = args["match_num"] - - if ALLIANCES[ALLIANCE_COLOR.BLUE] is not None: - reset() - - #code_setup() - - ALLIANCES[ALLIANCE_COLOR.BLUE] = Alliance(ALLIANCE_COLOR.BLUE, b1_name, - b1_num, b2_name, b2_num, b1_custom_ip, b2_custom_ip) - ALLIANCES[ALLIANCE_COLOR.GOLD] = Alliance(ALLIANCE_COLOR.GOLD, g1_name, - g1_num, g2_name, g2_num, g1_custom_ip, g2_custom_ip) - - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.TEAMS, { - "b1name" : b1_name, "b1num" : b1_num, - "b2name" : b2_name, "b2num" : b2_num, - "g1name" : g1_name, "g1num" : g1_num, - "g2name" : g2_name, "g2num" : g2_num, - "match_num" : MATCH_NUMBER}) - - GAME_STATE = STATE.SETUP - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.STAGE, {"stage": GAME_STATE}) - print("ENTERING SETUP STATE") - print({"blue_score" : ALLIANCES[ALLIANCE_COLOR.BLUE].score, - "gold_score" : ALLIANCES[ALLIANCE_COLOR.GOLD].score}) - -def to_auto(args): - ''' - Move to the autonomous stage where robots should begin autonomously. - By the end, should be in autonomous state, allowing any function from this - stage to be called and autonomous match timer should have begun. - ''' - #pylint: disable= no-member - global GAME_STATE - global clients - try: - alternate_connections = (ALLIANCES[ALLIANCE_COLOR.BLUE].team_1_custom_ip, - ALLIANCES[ALLIANCE_COLOR.BLUE].team_2_custom_ip, - ALLIANCES[ALLIANCE_COLOR.GOLD].team_1_custom_ip, - ALLIANCES[ALLIANCE_COLOR.GOLD].team_2_custom_ip) - - clients = RuntimeClientManager(( - int(ALLIANCES[ALLIANCE_COLOR.BLUE].team_1_number), - int(ALLIANCES[ALLIANCE_COLOR.BLUE].team_2_number), - ), ( - int(ALLIANCES[ALLIANCE_COLOR.GOLD].team_1_number), - int(ALLIANCES[ALLIANCE_COLOR.GOLD].team_2_number), - ), *alternate_connections) - clients.set_MASTER_ROBOTS(MASTER_ROBOTS[ALLIANCE_COLOR.BLUE], - MASTER_ROBOTS[ALLIANCE_COLOR.GOLD]) - clients.set_starting_zones(STARTING_SPOTS) - except Exception as exc: - log(exc) - return - GAME_TIMER.start_timer(CONSTANTS.AUTO_TIME + 2) - GAME_STATE = STATE.AUTO - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.STAGE, {"stage": GAME_STATE}) - enable_robots(True) - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.STAGE_TIMER_START, - {"time" : CONSTANTS.AUTO_TIME}) - print("ENTERING AUTO STATE") - -def to_wait(args): - ''' - Move to the waiting stage, between autonomous and teleop periods. - By the end, should be in wait state and the robots should be disabled. - Some years, there might be methods that can be called once in the wait stage - ''' - global GAME_STATE - GAME_STATE = STATE.WAIT - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.STAGE, {"stage": GAME_STATE}) - disable_robots() - print("ENTERING WAIT STATE") - -def to_teleop(args): - ''' - Move to teleoperated stage where robots are enabled and controlled manually. - By the end, should be in teleop state and the teleop match timer should be started. - ''' - global GAME_STATE - GAME_STATE = STATE.TELEOP - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.STAGE, {"stage": GAME_STATE}) - - Timer.reset_all() - GAME_TIMER.start_timer(CONSTANTS.TELEOP_TIME + 2) - - enable_robots(False) - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.STAGE_TIMER_START, - {"time" : CONSTANTS.TELEOP_TIME}) - print("ENTERING TELEOP STATE") - -def to_end(args): - ''' - Move to end stage after the match ends. Robots should be disabled here - and final score adjustments can be made. - ''' - global GAME_STATE - lcm_send(LCM_TARGETS.UI, UI_HEADER.SCORES, - {"blue_score" : math.floor(ALLIANCES[ALLIANCE_COLOR.BLUE].score), - "gold_score" : math.floor(ALLIANCES[ALLIANCE_COLOR.GOLD].score)}) - GAME_STATE = STATE.END - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.STAGE, {"stage": GAME_STATE}) - disable_robots() - print("ENTERING END STATE") - -def reset(args=None): - ''' - Resets the current match, moving back to the setup stage but with the current teams loaded in. - Should reset all state being tracked by Shepherd. - ****THIS METHOD MIGHT NEED UPDATING EVERY YEAR BUT SHOULD ALWAYS EXIST**** - ''' - global GAME_STATE, EVENTS, clients - GAME_STATE = STATE.SETUP - Timer.reset_all() - EVENTS = queue.Queue() - lcm_start_read(LCM_TARGETS.SHEPHERD, EVENTS) - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.RESET_TIMERS) - for alliance in ALLIANCES.values(): - if alliance is not None: - alliance.reset() - send_connections(None) - #STARTING_SPOTS = ["unknown", "unknown", "unknown", "unknown"] - clients = RuntimeClientManager((), ()) - disable_robots() - BUTTONS['gold_1'] = False - BUTTONS['gold_2'] = False - BUTTONS['blue_1'] = False - BUTTONS['blue_2'] = False - lcm_send(LCM_TARGETS.TABLET, TABLET_HEADER.RESET) - lcm_send(LCM_TARGETS.DAWN, DAWN_HEADER.RESET) - print("RESET MATCH, MOVE TO SETUP") - -def get_match(args): - ''' - Retrieves the match based on match number and sends this information to the UI - ''' - match_num = int(args["match_num"]) - info = Sheet.get_match(match_num) - info["match_num"] = match_num - lcm_send(LCM_TARGETS.UI, UI_HEADER.TEAMS_INFO, info) - -def score_adjust(args): - ''' - Allow for score to be changed based on referee decisions - ''' - blue_score, gold_score = args["blue_score"], args["gold_score"] - ALLIANCES[ALLIANCE_COLOR.BLUE].score = blue_score - ALLIANCES[ALLIANCE_COLOR.GOLD].score = gold_score - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.SCORE, - {"alliance" : ALLIANCES[ALLIANCE_COLOR.BLUE].name, - "score" : math.floor(ALLIANCES[ALLIANCE_COLOR.BLUE].score)}) - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.SCORE, - {"alliance" : ALLIANCES[ALLIANCE_COLOR.GOLD].name, - "score" : math.floor(ALLIANCES[ALLIANCE_COLOR.GOLD].score)}) - -def get_score(args): - ''' - Send the current blue and gold score to the UI - ''' - if ALLIANCES[ALLIANCE_COLOR.BLUE] is None: - lcm_send(LCM_TARGETS.UI, UI_HEADER.SCORES, - {"blue_score" : None, - "gold_score" : None}) - else: - lcm_send(LCM_TARGETS.UI, UI_HEADER.SCORES, - {"blue_score" : math.floor(ALLIANCES[ALLIANCE_COLOR.BLUE].score), - "gold_score" : math.floor(ALLIANCES[ALLIANCE_COLOR.GOLD].score)}) - -def flush_scores(): - ''' - Sends the most recent match score to the spreadsheet if connected to the internet - ''' - if ALLIANCES[ALLIANCE_COLOR.BLUE] is not None: - Sheet.write_scores(MATCH_NUMBER, ALLIANCES[ALLIANCE_COLOR.BLUE].score, - ALLIANCES[ALLIANCE_COLOR.GOLD].score) - return -1 - -def enable_robots(autonomous): - ''' - Sends message to Dawn to enable all robots. The argument should be a boolean - which is true if we are entering autonomous mode - ''' - try: - clients.set_mode("auto" if autonomous else "teleop") - except Exception as exc: - for client in clients.clients: - try: - client.set_mode("auto" if autonomous else "teleop") - except Exception as exc: - print("A robot failed to be enabled! Big sad :(") - log(exc) - -def disable_robots(): - ''' - Sends message to Dawn to disable all robots - ''' - try: - clients.set_mode("idle") - except Exception as exc: - for client in clients.clients: - try: - client.set_mode("idle") - except Exception as exc: - print("a client has disconnected") - log(exc) - print(exc) - -#pylint: disable=redefined-builtin -def log(Exception): - global LAST_HEADER - # if Shepherd.MATCH_NUMBER <= 0: - # return - now = datetime.datetime.now() - filename = str(now.month) + "-" + str(now.day) + "-" + str(now.year) +\ - "-match-" + str(MATCH_NUMBER) + ".txt" - print("a normally fatal exception occured, but Shepherd will continue to run") - print("all known details are logged to logs/"+filename) - file = open("logs/"+filename, "a+") - file.write("\n========================================\n") - file.write("a normally fatal exception occured.\n") - file.write("all relevant data may be found below.\n") - file.write("match: " + str(MATCH_NUMBER)+"\n") - file.write("game state: " + str(GAME_STATE)+"\n") - file.write("gold alliance: " + str(ALLIANCES[ALLIANCE_COLOR.GOLD])+"\n") - file.write("blue alliance: " + str(ALLIANCES[ALLIANCE_COLOR.BLUE])+"\n") - file.write("game timer running?: " + str(GAME_TIMER.is_running())+"\n") - file.write("the last received header was:" + str(LAST_HEADER)+"\n") - file.write("a stacktrace of the error may be found below\n") - file.write(str(Exception)) - file.write(str(traceback.format_exc())) - file.close() - -########################################### -# Game Specific Methods -########################################### -def disable_robot(args): - ''' - Send message to Dawn to disable the robots of team - ''' - try: - team_number = args["team_number"] - client = clients.clients[int(team_number)] - if client: - client.set_mode("idle") - except Exception as exc: - log(exc) - - -def set_master_robot(args): - ''' - Set the master robot of the alliance - ''' - alliance = args["alliance"] - team_number = args["team_num"] - MASTER_ROBOTS[alliance] = team_number - msg = {"alliance": alliance, "team_number": int(team_number)} - lcm_send(LCM_TARGETS.DAWN, DAWN_HEADER.MASTER, msg) - -def final_score(args): - ''' - send shepherd the final score, send score to scoreboard - ''' - blue_final = args['blue_score'] - gold_final = args['gold_score'] - ALLIANCES[ALLIANCE_COLOR.GOLD].score = gold_final - ALLIANCES[ALLIANCE_COLOR.BLUE].score = blue_final - msg = {"alliance": ALLIANCE_COLOR.GOLD, "amount": gold_final} - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.SCORE, msg) - msg = {"alliance": ALLIANCE_COLOR.BLUE, "amount": blue_final} - lcm_send(LCM_TARGETS.SCOREBOARD, SCOREBOARD_HEADER.SCORE, msg) - -def set_connections(args): - """Set connections""" - #pylint: disable=undefined-variable, not-an-iterable - team = args["team_number"] - connection = boolean(args["connection"]) - dirty = False - for alliance in ALLIANCES.values: - if team == alliance.team_1_number: - if alliance.team_1_connection != connection: - alliance.team_1_connection = connection - dirty = True - if team == alliance.team_2_number: - if alliance.team_2_connection != connection: - alliance.team_2_connection = connection - dirty = True - if dirty: - send_connections(None) - -def send_connections(args): - """Send connections""" - pass #pylint: disable=unnecessary-pass - # msg = {"g_1_connection" : ALLIANCES[ALLIANCE_COLOR.GOLD].team_1_connection, - # "g_2_connection" : ALLIANCES[ALLIANCE_COLOR.GOLD].team_2_connection, - # "b_1_connection" : ALLIANCES[ALLIANCE_COLOR.BLUE].team_1_connection, - # "b_2_connection" : ALLIANCES[ALLIANCE_COLOR.BLUE].team_2_connection} - # lcm_send(LCM_TARGETS.UI, UI_HEADER.CONNECTIONS, msg) - -########################################### -# Event to Function Mappings for each Stage -########################################### - -SETUP_FUNCTIONS = { - SHEPHERD_HEADER.SETUP_MATCH: to_setup, - SHEPHERD_HEADER.SCORE_ADJUST : score_adjust, - SHEPHERD_HEADER.GET_MATCH_INFO : get_match, - SHEPHERD_HEADER.START_NEXT_STAGE: to_auto -} - -AUTO_FUNCTIONS = { - SHEPHERD_HEADER.RESET_MATCH : reset, - SHEPHERD_HEADER.STAGE_TIMER_END : to_wait, - #SHEPHERD_HEADER.CODE_APPLICATION : auto_apply_code, - SHEPHERD_HEADER.ROBOT_OFF : disable_robot, - #SHEPHERD_HEADER.CODE_RETRIEVAL : bounce_code, - SHEPHERD_HEADER.ROBOT_CONNECTION_STATUS: set_connections, - SHEPHERD_HEADER.REQUEST_CONNECTIONS: send_connections - - } - -WAIT_FUNCTIONS = { - SHEPHERD_HEADER.RESET_MATCH : reset, - SHEPHERD_HEADER.SCORE_ADJUST : score_adjust, - SHEPHERD_HEADER.GET_SCORES : get_score, - SHEPHERD_HEADER.START_NEXT_STAGE : to_teleop, - SHEPHERD_HEADER.ROBOT_CONNECTION_STATUS: set_connections, - SHEPHERD_HEADER.REQUEST_CONNECTIONS: send_connections -} - -TELEOP_FUNCTIONS = { - SHEPHERD_HEADER.RESET_MATCH : reset, - SHEPHERD_HEADER.STAGE_TIMER_END : to_end, - #SHEPHERD_HEADER.CODE_APPLICATION : apply_code, - SHEPHERD_HEADER.ROBOT_OFF : disable_robot, - #SHEPHERD_HEADER.CODE_RETRIEVAL : bounce_code, - SHEPHERD_HEADER.ROBOT_CONNECTION_STATUS: set_connections, - SHEPHERD_HEADER.REQUEST_CONNECTIONS: send_connections - -} - -END_FUNCTIONS = { - SHEPHERD_HEADER.RESET_MATCH : reset, - SHEPHERD_HEADER.SCORE_ADJUST : score_adjust, - SHEPHERD_HEADER.GET_SCORES : get_score, - SHEPHERD_HEADER.SETUP_MATCH : to_setup, - SHEPHERD_HEADER.GET_MATCH_INFO : get_match, - SHEPHERD_HEADER.FINAL_SCORE : final_score, - SHEPHERD_HEADER.ROBOT_CONNECTION_STATUS: set_connections, - SHEPHERD_HEADER.REQUEST_CONNECTIONS: send_connections -} - -########################################### -# Evergreen Variables -########################################### - -GAME_STATE = STATE.END -GAME_TIMER = Timer(TIMER_TYPES.MATCH) - -MATCH_NUMBER = -1 -ALLIANCES = {ALLIANCE_COLOR.GOLD: None, ALLIANCE_COLOR.BLUE: None} -EVENTS = None - -LAST_HEADER = None - -########################################### -# Game Specific Variables -########################################### -BUTTONS = {'gold_1': False, 'gold_2': False, 'blue_1': False, 'blue_2': False} -STARTING_SPOTS = ["unknown", "unknown", "unknown", "unknown"] -MASTER_ROBOTS = {ALLIANCE_COLOR.BLUE: None, ALLIANCE_COLOR.GOLD: None} - -STUDENT_DECODE_TIMER = Timer(TIMER_TYPES.STUDENT_DECODE) - -CODES_USED = [] - -#nothing - -def main(): - """Main function""" - parser = argparse.ArgumentParser(description='PiE field control') - parser.add_argument('--version', help='Prints out the Shepherd version number.', - action='store_true') - flags = parser.parse_args() - - if flags.version: - print('.'.join(map(str, __version__))) - else: - start() - - - -if __name__ == '__main__': - main() diff --git a/shepherd/Tester.py b/shepherd/Tester.py new file mode 100644 index 00000000..402cdaad --- /dev/null +++ b/shepherd/Tester.py @@ -0,0 +1,804 @@ +# pylint: disable=invalid-name +import sys +import os +import queue +import time +from typing import Any +from keyword import iskeyword +from utils import * +from ydl import ydl_start_read, ydl_send + +# pylint: disable=global-statement + + +def get_class_from_name(name: str) -> Any: + """ + A helper function used to get a class of name from the globals list. + Globals is a dictionary representing the global scope of this program's + python execution, which means that to a small extent the functions in this + file along with everything imported above is available to this function. + Keep this in mind, since this function is called on potentially unsafe user + entered code. + """ + return globals()[name] + + +def get_attr_from_name(source, name): + """ + A helper function used to get an attribute of a certain name from a given + class (source). + """ + if not isinstance(source, type): + raise Exception('{} is not a class.'.format(source)) + return getattr(source, name) + + +def parse_header(header): + """ + A helper function that translates the headers in format + .
to the string representation stored in utils.py. + Enforces the syntax for referencing the header, as well as the existance of + the header in utils.py. + """ + parts: list = header.split('.') + if len(parts) != 2: + raise Exception('{} is invalid.'.format(header)) + ex = None + klass: type = None + try: + klass = get_class_from_name(parts[0]) + except KeyError: + ex = Exception( + '{} is not recognized. Make sure this is a class of headers in utils.py'.format(parts[0])) + finally: + if ex: + raise ex + ex = None + name: str = '' + try: + name = get_attr_from_name(klass, parts[1]) + except AttributeError: + ex = Exception('{} is not recognized. Make sure this is a header in {} in utils.py'.format( + parts[1], parts[0])) + finally: + if ex: + raise ex + return name + + +def execute_python(script): + """ + A helper function that executes a python expression in the context of the + local scipt environment. + """ + global LOCALVARS + exec(script, LOCALVARS) + + +def evaluate_python(token): + """ + A helper function that evaluates a token against the local scipt environment. + """ + global LOCALVARS + return eval(token, LOCALVARS) + + +def tokenize_wait_exp(expression): + """ + The function that parses WAIT statements. + Enforces syntax and returns a dictionary of the deconstructed WAIT statement. + """ + global TARGET + original_expression = expression + + def helper_min(a, b): + """ + A helper function that returns the min between a and b, but considers -1 + to be an unacceptable return value. + """ + if a == -1 and b == -1: + return None + elif a == -1: + return b + elif b == -1: + return a + else: + return min(a, b) + tokens = expression.split('FROM') + if len(tokens) != 2: + raise Exception('expected to find FROM after
and before in WAIT statement: WAIT {}'.format( + original_expression)) + header = parse_header(remove_outer_spaces(tokens[0])) + expression = tokens[1] + whole_target = remove_outer_spaces(expression).split(' ')[0] + if whole_target.split('.')[0] != 'YDL_TARGETS': + raise Exception('was expecting a target in YDL_TARGETS for WAIT statement: WAIT {}'.format( + original_expression)) + target = get_attr_from_name(YDL_TARGETS, whole_target.split('.')[1]) + if target not in TARGETS: + raise Exception('target for WAIT expression is not the current targets ({}): WAIT {}'.format( + TARGETS, original_expression)) + expression = expression[helper_min(expression.find( + ' SET '), expression.find(' WITH ')):None] + statements = {'SET': [], 'WITH': []} + while ' SET ' in expression or ' WITH ' in expression: + expression = remove_outer_spaces(expression) + type = 'SET' if expression[0:4] == 'SET ' else 'WITH' + if type == 'WITH' and expression[0:5] != 'WITH ': + raise Exception('was expecting WITH or SET statement, or nothing after FROM in WAIT statement: WAIT {}'.format( + original_expression)) + expression = expression[len(type):None] + expression = remove_outer_spaces(expression) + stop_point = helper_min(expression.find( + ' SET '), expression.find(' WITH ')) + statements[type].append(remove_outer_spaces( + expression[0:stop_point])) + expression = expression[stop_point:None] + return {'header': header, 'target': target, 'with_statements': statements['WITH'], 'set_statements': statements['SET']} + + +def wait_function(expression): + """ + The function that parses WAIT statements. + Enforces the syntax of the WAIT statement. + Adds one or multiple new wait conditions to global CURRENT_HEADERS, and sets + global WAITING to True. + """ + original_expression = expression + global CURRENT_HEADERS, WAITING + + def helper_min(a, b): + """ + A helper function that returns the min between a and b, but considers -1 + to be an unacceptable return value. + """ + if a == -1 and b == -1: + return None + elif a == -1: + return b + elif b == -1: + return a + else: + return min(a, b) + CURRENT_HEADERS = [] + WAITING = True + type = 'OR' + while ' AND ' in expression or ' OR ' in expression: + expression = remove_outer_spaces(expression) + found = helper_min(expression.find(' AND '), expression.find(' OR ')) + type = 'AND' if expression[found:found+5] == ' AND ' else 'OR' + header = tokenize_wait_exp(expression[0:found]) + expression = expression[found + len(type) + 2:None] + CURRENT_HEADERS.append( + {'header': header, 'type': type, 'received': False}) + expression = remove_outer_spaces(expression) + CURRENT_HEADERS.append({'header': tokenize_wait_exp( + expression), 'type': type, 'received': False}) + + +def read_next_line(): + """ + A helper function to maintain the file scanning abstraction. + Advances the global LINE pointer to the next line of the script. + """ + global LINE + LINE += 1 + + +def has_next_line(): + """ + A helper function to maintain the file scanning abstraction. + Returns whether or not there is another line to be read. + """ + global LINE, FILE + return LINE < len(FILE) + + +def line_at(line): + """ + A helper function to maintain the file scanning abstraction. + Returns the line with the number passed in. + """ + global FILE + return FILE[line] + + +def jump_to_line(line): + """ + A helper function to maintain the file scanning abstraction. + Sets global LINE, can be through of as a jump. + """ + global LINE + LINE = line + + +def current_line(): + """ + A helper function to maintain the file scanning abstraction. + Returns the line with the number in global LINE. + """ + global LINE, FILE + return FILE[LINE] + + +def process_line(line): + """ + Takes in a line of the script and identifies the statement being used. + Calls the correct executing function on the remainder of the line. + Enforces that all lines start with a valid statement, and attaches + additional information to errors thrown by the executing functions. + + """ + global LINE + if line == '': + return + found = False + for key in COMMANDS: + if line[0:len(key)] == key: + ex = None + try: + COMMANDS[key](remove_outer_spaces(line[len(key):None])) +# pylint: disable=broad-except + except Exception as exx: + ex = Exception( + 'an error occured on line {}:\n{}'.format(LINE + 1, exx)) + finally: + if ex: + raise ex + found = True + break + if not found: + raise Exception( + 'unrecognized command on line {}:\n{}'.format(LINE + 1, line)) + + +def remove_outer_spaces(token): + """ + A helper function used to strip the spaces off the outside edges of a + string. + Essential for making shepherd scripting ignore whitespace. + """ + return token.strip() + + +def if_function(expression): + """ + The function that parses both IF and WHILE statements. + Enforces syntax as well as balanced END statements. + Evaluates the condition given in the statement, and if false crawls forward + in the script to the matching END statement and jumps there, otherwise + execution proceedes normally. + """ + global END_COUNT, LINE, END_COUNT_HEADS + starting_count = END_COUNT + starting_line = LINE + END_COUNT_HEADS[LINE] = END_COUNT + condition = evaluate_python(remove_outer_spaces(expression)) + END_COUNT += 1 + if not condition: + while END_COUNT > starting_count: + read_next_line() + ex = None + line = None + try: + line = remove_outer_spaces(current_line()) + except Exception: + ex = Exception( + "reached end of file while in the IF on line {}. You are probably missing an END".format(starting_line)) + finally: + if ex: + raise ex + if line == '': + continue + found = False + for key in COMMANDS.keys(): + if line[0:len(key)] == key: + found = True + break + if not found: + raise Exception( + 'unrecognized command on line {}:\n{}'.format(LINE + 1, line)) + if line[0:2] == 'IF': + END_COUNT += 1 + if line[0:5] == 'WHILE': + END_COUNT += 1 + if line[0:3] == 'END': + END_COUNT -= 1 + + +def end_function(expression): + """ + The function that parses END statements. + Crawls backwards through the script to find the matching IF or WHILE + statement and jumps to that line if it is a WHILE. + Essentially no processing need happen here for an IF statement. + """ + global END_COUNT, LINE, END_COUNT_HEADS + END_COUNT -= 1 + end_count_heads = list(END_COUNT_HEADS.items()) + end_count_heads.sort() + for item in end_count_heads[::-1]: + if item[0] < LINE and item[1] == END_COUNT: + if line_at(item[0])[0:5] == 'WHILE': + jump_to_line(item[0]-1) + break + + +def pass_function(expression): + """ + The function that parses PASS statements. + Enforces syntax and will print out the result as well as exit the script + interpreter if the test is passed. + """ + expression = remove_outer_spaces(expression) + if expression == '' or evaluate_python(expression): + print("TEST PASSED") + sys.exit(0) + + +def fail_function(expression): + """ + The function that parses FAIL statements. + Enforces syntax and will print out the result as well as exit the script + interpretter if the test is failed. + """ + global LINE + expression = remove_outer_spaces(expression) + if expression == '' or evaluate_python(expression): + print(f"TEST FAILED on line {LINE + 1}") + sys.exit(-1) + + +def assert_function(expression): + """ + The function that parses ASSERT statements. + Enforces syntax and will print out the result of the assertion as well as + exit the script interpretter. + """ + global LINE + expression = remove_outer_spaces(expression) + if expression == '': + raise Exception( + 'expected a python conditional expression after ASSERT') + if evaluate_python(expression): + print("TEST PASSED") + sys.exit(0) + else: + print(f"TEST FAILED on line {LINE + 1}") + sys.exit(-1) + +def sleep_function(expression): + """ + The function that parses SLEEP statements. + Enforces syntax and pauses the .shepherd interpreter for the specified + number of seconds. + """ + expression = remove_outer_spaces(expression) + if expression == '': + raise Exception('expected a time afer after SLEEP') + try: + amount = float(evaluate_python(expression)) + except ValueError as e: + raise Exception(f'expected a time afer after SLEEP, but got {expression}') + time.sleep(amount) + +def timeout_function(expression): + """ + The function that parses TIMEOUT statements. + Enforces syntax and sets the global TIMEOUT variable to the specified amount. + """ + global TIMEOUT + expression = remove_outer_spaces(expression) + if expression == '': + raise Exception('expected a time afer after TIMEOUT') + try: + amount = float(evaluate_python(expression)) + except ValueError as e: + raise Exception(f'expected a time afer after TIMEOUT, but got {expression}') + TIMEOUT = amount + +def discard_function(expression): + """ + The function that parses DISCARD statements. + Enforces syntax and clears the YDL queue so that no outstanding requests + are processed. + """ + global EVENTS + if expression != '': + raise Exception('expected nothing after DISCARD statement') + with EVENTS.mutex: + EVENTS.queue.clear() + +def read_function(line): + """ + The function that parses READ statements. + Enforces syntax, and sets the global TARGET appropriately. + """ + global TARGETS, EVENTS + if remove_outer_spaces(line.split('.')[0]) != 'YDL_TARGETS' or len(line.split('.')) != 2: + raise Exception( + 'was expecting a target in YDL_TARGETS for READ statement: READ {}'.format(line)) + target = get_attr_from_name(YDL_TARGETS, line.split('.')[1]) + if target in TARGETS: + print("[WARNING] Calling READ again on the same target will cause the YDL queue to be \ + cleared. Make sure that this is intended.") + TARGETS += [target] + EVENTS = queue.Queue() + ydl_start_read(target, EVENTS) + print('now reading from ydl target: YDL_TARGETS.{}'.format( + line.split('.')[1])) + + +def tokenize_emit_exp(expression): + """ + The function that parses EMIT statements. + Enforces syntax and returns a dictionary of the deconstructed EMIT statement. + """ + original_expression = expression + + def helper_find(expression, target): + """ + A helper function to find the next instance of 'target' in 'expression', + and return none if it does not exist. + """ + found = expression.find(target) + return found if found >= 0 else None + tokens = expression.split(' TO ') + if len(tokens) != 2: + raise Exception('expected to find TO after
and before in EMIT statement: EMIT {}'.format( + original_expression)) + header = parse_header(remove_outer_spaces(tokens[0])) + expression = tokens[1] + if remove_outer_spaces(expression[0:helper_find(expression, ' WITH ')].split('.')[0]) != 'YDL_TARGETS': + raise Exception('was expecting a target in YDL_TARGETS for EMIT statement: EMIT {}'.format( + original_expression)) + target = get_attr_from_name(YDL_TARGETS, remove_outer_spaces( + expression[0:helper_find(expression, ' WITH ')].split('.')[1])) + expression = expression[helper_find(expression, ' WITH '):None] + statements = [] + while 'WITH ' in expression: + expression = remove_outer_spaces(expression) + if expression[0:5] != 'WITH ': + raise Exception('was expecting WITH statement, or nothing after TO in EMIT statement: EMIT {}'.format( + original_expression)) + expression = expression[len('WITH'):None] + expression = remove_outer_spaces(expression) + statements.append(remove_outer_spaces( + expression[0:helper_find(expression, ' WITH ')])) + expression = expression[helper_find(expression, ' WITH '):None] + return {'header': header, 'target': target, 'with_statements': statements} + + +def emit_function(expression): + """ + The function called to handle the execution of an EMIT statement. + Processes the statement, processes the WITH statements, and then emits the + appropriate header and data via YDL. + """ + emit_expression = tokenize_emit_exp(expression) + data = {} + for with_statement in emit_expression['with_statements']: + with_function_emit(with_statement, data) + ydl_send(emit_expression['target'], emit_expression['header'], data) + + +def with_function_wait(expression, data): + """ + Takes in a WITH statement found in a WAIT statement, and the data that was + present in the header that triggered the processing of this WAIT statement, + and modifies the local script environment accordingly. + Also handles syntax checking of the WITH statement. + """ + parts = parse_with_function_wait(expression) + ex = None + try: + global LOCALVARS + LOCALVARS[parts[0]] = data[parts[1]] + except ValueError: + ex = Exception("{} is undefined".format(parts[0])) + except Exception: + ex = Exception("malformed WITH statement: {}".format(expression)) + finally: + if ex: + raise ex + +def parse_with_function_wait(expression): + """ + Helper function used in a few places to parse and syntax check a WITH + statement in a WAIT statement. + Returns a tuple of form (var_name, data_key) + """ + parts = expression.split('=') + if len(parts) != 2: + raise Exception('WITH statement: {} is invalid.'.format(expression)) + parts[0] = remove_outer_spaces(parts[0]) + parts[1] = remove_outer_spaces(parts[1]) + if parts[1][0] != "'" or parts[1][-1] != "'": + raise Exception( + "expected second argument of WITH statement: {} to be wrapped in '.".format(expression)) + return (parts[0], parts[1][1:-1]) + +def with_infer_function(withs, data): + """ + Called when a WITH statement found in a WAIT statement that uses an INFER is + encountered, and takes in all WITH statements as well as the recieved data. + Modifies the local script environment to store any unused data keys (not + found in other WAIT statements) in variables of the exact same name. + Ensures that valid python variable naming conventions are used. + """ + + def is_valid_variable_name(name): + """ + Quick helper function to check if variable names are valid. + """ + if name[0] == '_': + return False + return name.isidentifier() and not iskeyword(name) + + global LOCALVARS + with_keys = [parse_with_function_wait(w)[1] for w in withs if not 'INFER' in w] + for var in data.keys(): + if not var in with_keys: + if not is_valid_variable_name(var): + raise Exception(f"{var} is not a valid python variable name, and therefore cannot be used in INFER. Use WITH = '{var}' to specify a valid name.") + LOCALVARS[var] = data[var] + +def with_function_emit(expression, data): + """ + Takes in a WITH statement found in an EMIT statement, and the data that will + be issued to the emmited header and modifies the data appropriately. + Also handles syntax checking of the WITH statement. + Example: + code: SCOREBOARD_HEADER.STAGE TO YDL_TARGETS.SCOREBOARD WITH 'stage' = AUTO + expression: 'stage' = AUTO + """ + parts = expression.split('=') + if len(parts) != 2: + raise Exception('WITH statement: {} is invalid.'.format(expression)) + # remove leading and trailing spaces around the '=' + parts[0] = remove_outer_spaces(parts[0]) + parts[1] = remove_outer_spaces(parts[1]) + if parts[0][0] != "'" or parts[0][-1] != "'": + raise Exception( + "expected first argument of WITH statement: {} to be wrapped in '.".format(expression)) + ex = None + try: + data[parts[0][1:-1]] = evaluate_python(parts[1]) + except ValueError: + ex = Exception("{} is undefined".format(parts[1])) + except Exception: + ex = Exception("malformed WITH statement: {}".format(expression)) + finally: + if ex: + raise ex + + +def check_received_headers(): + """ + Returns whether or not the wait conditions are satisfied, so that script + execution should proceed. + This is done by taking the AND and OR statements in the WAIT litterally, + and the python interpretter is fed a string of booleans seperated by the + appropriate python and / or opperators. + This function will also set the global WAITING variable to false once + execution should resume. + """ + global CURRENT_HEADERS, WAITING + python_usable_string = '' + for i in range(len(CURRENT_HEADERS)): + header = CURRENT_HEADERS[i] + python_usable_string += str(header['received']) + " " + if i < len(CURRENT_HEADERS) - 1: + python_usable_string += header['type'].lower() + " " + if(eval(python_usable_string)): + WAITING = False + return True + return False + + +def execute_header(header, data): + """ + Takes in a header data structure and the data from the YDL call and will + modify the local environment accordingly. + Processes all SET and WITH statements in the header individually, and with + no guarantee on order. + In this implementation, all WITH statements are processed first, from + left to right, and then all SET statements, from left to right. + """ + global LOCALVARS + inferred = False + for with_statement in header['header']['with_statements']: + if with_statement == 'INFER': + if inferred: + continue + with_infer_function(header['header']['with_statements'], data) + inferred = True + else: + with_function_wait(with_statement, data) + for set_statement in header['header']['set_statements']: + local_arg = remove_outer_spaces(set_statement.split('=')[0]) + python_expression = remove_outer_spaces(set_statement.split('=')[1]) + LOCALVARS[local_arg] = evaluate_python(python_expression) + + +def accept_header(payload): + """ + Accepts a header and its payload, and will check if that header is + currently being waited on. + If it is, accept header will process all instances of that waited on + header and also set the wait condition to be satisfied for each instance. + """ + global CURRENT_HEADERS + for header in CURRENT_HEADERS: + if header['header']['header'] == payload[0]: + execute_header(header, payload[1]) + header['received'] = True + + +def run_until_wait(): + """ + A useful function that advances script execution until the next WAIT + statement is detected. + Once that statement is detected, it is processed and then run_until_wait + returns. + """ + global WAITING + while has_next_line() and not WAITING: + process_line(remove_outer_spaces(current_line())) + read_next_line() + if not has_next_line() and not WAITING: + print('reached end of test without failing') + sys.exit(0) + + +def start(): + """ + The loop that interacts with YDL! + Here target must be assigned before start() may be called, and so this will + detect and error on that condition. + Otherwise this loop binds a queue to the correct YDL target and processes + YDL events that it recieves. + Each YDL header that is recieved will be processed based on the current + WAIT statements and then if all wait statements have been satisfied, + the code execution will advance to the next WAIT before any more YDL headers + will be processed. + """ + global TARGETS, EVENTS, CURRENT_HEADERS, TIMEOUT + if not TARGETS: + raise Exception("READ needs to be called before the first WAIT.") + + while True: + time.sleep(0.1) + try: + payload = EVENTS.get(block=True, timeout=TIMEOUT) + except queue.Empty: + print(f"TEST FAILED on line {LINE + 1} because WAIT statement TIMEOUT") + sys.exit(-1) + # a quick try block to ensure that errors in WAIT statements get line # + ex = None + try: + accept_header(payload) +# pylint: disable=broad-except + except Exception as exx: + ex = Exception( + 'an error occured on line {}:\n{}'.format(LINE + 1, exx)) + finally: + if ex: + raise ex + if(check_received_headers()): + CURRENT_HEADERS = [] + run_until_wait() + +def main(): + """ + Reads the whole file in and places it in a python list on the heap. + Also handles errors associated with the file system. + Returns 1 if the file cannt be found, 0 otherwise + """ + global FILE + if len(sys.argv) != 2: + print('[ERROR] The tester takes a single argument, the name of a testing file') + return 1 + script_dir = os.path.dirname(__file__) + rel_path = "tests/{}".format(sys.argv[1]) + abs_file_path = os.path.join(script_dir, rel_path) + if not os.path.isfile(abs_file_path): + print('[ERROR] The tester requires a test file, not a folder') + return 1 + file = open(abs_file_path, "r") + for line in file: + line = line[0:None] + if line[-1] == '\n': + line = line[0:-1] + FILE.append(line) + file.close() + print('Starting TEST file: {}'.format(sys.argv[1])) + return 0 + + +""" +A list of the YDL target that we are currently reading from. +""" +TARGETS = [] +""" +The queue used to store incoming YDL requests. These requests are then popped +off in the start function and processed. +""" +TIMEOUT = 30 +""" +The global timeout for all WAIT statements. Can be modified with a TIMEOUT +statement. Test fails if WAIT lasts longer than this timeout. +""" +EVENTS = queue.Queue() +""" +A list containing the contents of the file that was specified when the program +was started. Each line of the file is stored as a string in sequential indexes. +""" +FILE = [] +""" +A varaible which keeps track of whether or not execution is currently happening. +This is set to True when a wait statement is encountered, and set to False when +all headers have been resolved, and execution should continue. +""" +WAITING = False +""" +A dictionary that is populated by the script's execution. This is used as an +environment for python execution in RUN and in the WAIT, SET, and PRINTP +statements. Unlike normal python environments, there are no further frames opened +for code blocks, and this is a facsimile of dynamic typing. +""" +LOCALVARS = {} +""" +A list of header dictionaries. This list is populated by WAIT statements and is +emptied when the headers are all considered resolved. +""" +CURRENT_HEADERS = [] +""" +A varaible keeping track of the current line being executed. +""" +LINE = 0 +""" +A variable keeping track of the current level of indentation (how many unclosed +IF / WHILE statements there are). +""" +END_COUNT = 0 +""" +A dictionary used to keep track / memoize the level of indentation found at each +IF and WHILE statement. This is used by the END statement to backtrack to the +appropriate line and determine if that END is closing an IF or a WHILE. +""" +END_COUNT_HEADS = {} +""" +A dictionary of command names (that start lines) to the functions to be +executed in order to parse that kind of line. Due to how the checking +works, statements that include another statement's syntax should precede +those in this dictionary declaration. +""" +COMMANDS = {'WAIT': wait_function, + 'EMIT': emit_function, + 'RUN': execute_python, + 'READ': read_function, + 'PRINTP': lambda line: print(evaluate_python(line)), + 'PRINT': lambda line: print(line), + 'IF': if_function, + 'WHILE': if_function, + 'END': end_function, + 'PASS': pass_function, + 'FAIL': fail_function, + 'ASSERT': assert_function, + '##': lambda line: None, + 'SLEEP': sleep_function, + 'DISCARD': discard_function, + 'TIMEOUT': timeout_function + } + +if __name__ == '__main__': + """ + The main function! Calls main to read the specified file into the heap, + and then advances until the first wait command where it begins the YDL + interaction found in start. + """ + if main(): + exit(0) + run_until_wait() + start() diff --git a/shepherd/Timer.py b/shepherd/Timer.py deleted file mode 100644 index 26a9fc8d..00000000 --- a/shepherd/Timer.py +++ /dev/null @@ -1,116 +0,0 @@ -import time -import threading -import collections -import heapq -import LCM -from Utils import * - -class busyThread(threading.Thread): - ''' - Subclass that is the actual thread that will be running. - There will only be one for the entire class. - ''' - def __init__(self, queue): - super().__init__() - self.queue = queue - self.stop = threading.Event() - - def run(self): - ''' - When started, thread will run and process Timers in queue until manually stopped - TODO: Add how to send message via LCM in the case of match timer - ''' - while not self.stop.isSet(): - if self.queue and self.queue[0].end_time < time.time(): - Timer.queueLock.acquire() - event = heapq.heappop(self.queue) - if event.timer_type is not None and event.timer_type["NEEDS_FUNCTION"]: - LCM.lcm_send(LCM_TARGETS.SHEPHERD, event.timer_type["FUNCTION"]) - event.active = False - Timer.queueLock.release() - for timer in self.queue: - timer.active = False - - def join(self, timeout=None): - '''Stops this thread. Must be called from different thread (Main Thread)''' - self.stop.set() - super().join(timeout) - Timer.running = False - -class Timer: - """ - This class should spawn another thread that will keep track of a target time - and compare it to the current system time in order to see how much time is left - """ - - eventQueue = [] - thread = busyThread(eventQueue) - running = False - queueLock = threading.Lock() - globalResetCount = 0 - reset_all_count = 0 - - def __init__(self, timer_type): - """ - timer_type - a Enum representing the type of timer that this is: - TIMER_TYPES.MATCH - represents the time of the current - """ - self.active = False - self.timer_type = timer_type - self.end_time = None - self.reset_all_count = Timer.globalResetCount - - def start_timer(self, duration): - """Starts a new timer with the duration (seconds) and sets timer to active. - If Timer is already running, adds duration to Timer""" - self.reset_all_count = Timer.globalResetCount - if self.active: - Timer.queueLock.acquire() - self.end_time += duration - heapq.heapify(Timer.eventQueue) - Timer.queueLock.release() - else: - if not Timer.running: - Timer.running = True - Timer.thread.start() - Timer.queueLock.acquire() - self.end_time = time.time() + duration - heapq.heappush(Timer.eventQueue, self) - self.active = True - Timer.queueLock.release() - - def reset(self): - """Stops the current timer (if any) and sets timer to inactive""" - if self.active and self.reset_all_count == Timer.globalResetCount: - Timer.queueLock.acquire() - Timer.eventQueue.remove(self) - heapq.heapify(Timer.eventQueue) - self.active = False - Timer.queueLock.release() - - def is_running(self): - """Returns true if the timer is currently running""" - return self.active - - @classmethod - def reset_all(cls): - """Resets Timer Thread when game changes""" - if cls.running: - cls.thread.join() - cls.eventQueue = [] - cls.thread = busyThread(cls.eventQueue) - cls.running = False - cls.queueLock = threading.Lock() - cls.globalResetCount = cls.globalResetCount + 1 - - ########################################### - # Timer Comparison Methods - ########################################### - def __lt__(self, other): - return self.end_time < other.end_time - - def __gt__(self, other): - return self.end_time > other.end_time - - def __eq__(self, other): - return self.end_time == other.end_time diff --git a/shepherd/Utils.py b/shepherd/Utils.py deleted file mode 100644 index 2272ef7c..00000000 --- a/shepherd/Utils.py +++ /dev/null @@ -1,141 +0,0 @@ -# pylint: disable=invalid-name -class SHEPHERD_HEADER(): - START_NEXT_STAGE = "start_next_stage" - # START_NEXT_STAGE{}: starts the next stage - - RESET_CURRENT_STAGE = "reset_current_stage" - # RESET_CURRENT_STAGE{}: resets the current stage - - RESET_MATCH = "reset_match" - # RESET_MATCH{}: resets the current match - - GET_MATCH_INFO = "get_match_info" - # GET_MATCH_INFO{match_number}: gets match info for given match number - - SETUP_MATCH = "setup_match" - # SETUP_MATCH{b1name, b1#, b2name, b2#, g1name, g1#, g2name, g2#, match#}: - # sets up the match given the corresponding info about the teams and match number - # also has {g1_custom_ip, g2_custom_ip, b1_custom_ip, b2_custom_ip} - - GET_CONNECTION_STATUS = "get_connection_status" - # GET_CONNECTION_STATUS{}: requested from the Staff UI to check robot - # connection statuses - - GET_SCORES = "get_scores" - # GET_SCORES{}: gets scores of the match - - SCORE_ADJUST = "score_adjust" - # SCORE_ADJUST{blue_score, gold_score}: adjusts the current scores to the input scores - - STAGE_TIMER_END = "stage_timer_end" - # STAGE_TIMER_END{}: ends the stage's timer - - ROBOT_OFF = "robot_off" - # ROBOT_OFF{team_number}: takes in team number and disables their robot - - END_EXTENDED_TELEOP = "end_extended_teleop" - # END_EXTENDED_TELEOP{}: ends the extended teloperated period - - CODE_RETRIEVAL = "code_retrieval" - # CODE_RETRIEVAL{alliance, result}: retrieves code (from sensors.py) - - CODE_APPLICATION = "code_application" - # CODE_APPLICATION{alliance, result}: applies code (from sensors.py) - - MASTER_ROBOT = "master_robot" - - FINAL_SCORE = "final_score" - ASSIGN_TEAMS = "assign_teams" - # ASSIGN_TEAMS{g1num, g2num, b1num, b2num} - TEAM_RETRIEVAL = "team_retrieval" - # TEAM_RETRIEVAL{} - - ROBOT_CONNECTION_STATUS = "robot_connection_status" - #ROBOT_CONNECTION_STATUS{team_number, connection[True/False]} - - REQUEST_CONNECTIONS = "request_connections" - #REQUEST_CONNECTIONS{} - -# pylint: disable=invalid-name -class DAWN_HEADER(): - CODES = "codes" - DECODE = "decode" - SPECIFIC_ROBOT_STATE = "srt" - MASTER = "master" - IP_ADDRESS = "ip_address" - ROBOT_STATE = "rs" - HEARTBEAT = "heartbeat" - RESET = "reset" - #TODO this^ - -class RUNTIME_HEADER(): - SPECIFIC_ROBOT_STATE = "specific_robot_state" - # SPECIFIC_ROBOT_STATE{team_number, autonomous, enabled} - # robot ip is 192.168.128.teamnumber - DECODE = "decode" - # DECODE{team_number, seed} - -# pylint: disable=invalid-name -class UI_HEADER(): - TEAMS_INFO = "teams_info" - SCORES = "scores" - CONNECTIONS = "connections" - #CONNECTIONS{g_1_connection[True/False], g_2_connection[True/False], - # b_1_connection[True/False], b_2_connection[True/False]} - -# pylint: disable=invalid-name -class SCOREBOARD_HEADER(): - SCORE = "score" - TEAMS = "teams" - STAGE = "stage" - STAGE_TIMER_START = "stage_timer_start" - RESET_TIMERS = "reset_timers" - ALL_INFO = "all_info" - -class TABLET_HEADER(): - TEAMS = "teams" - #{b1num, b2num, g1num, g2num} - CODE = "code" - #{alliance, code} - COLLECT_CODES = "collect_codes" - #{} - RESET = "reset" - #{} - -# pylint: disable=invalid-name -class CONSTANTS(): - AUTO_TIME = 30 # 30 - TELEOP_TIME = 180 # 180 - SPREADSHEET_ID = "1vurNOrlIIeCHEtK5aJVDfHrRM1AC2qWvIbtWqUgnmLk" - CSV_FILE_NAME = "Sheets/fc2019.csv" - STUDENT_DECODE_TIME = 1 - -# pylint: disable=invalid-name -class ALLIANCE_COLOR(): - GOLD = "gold" - BLUE = "blue" - -# pylint: disable=invalid-name -class LCM_TARGETS(): - SHEPHERD = "lcm_target_shepherd" - SCOREBOARD = "lcm_target_scoreboard" - SENSORS = "lcm_target_sensors" - UI = "lcm_target_ui" - DAWN = "lcm_target_dawn" - RUNTIME = "lcm_target_runtime" - TABLET = "tablet" - -# pylint: disable=invalid-name -class TIMER_TYPES(): - MATCH = {"TYPE":"match", "NEEDS_FUNCTION": True, - "FUNCTION":SHEPHERD_HEADER.STAGE_TIMER_END} - STUDENT_DECODE = {"TYPE":"student_decode", "NEEDS_FUNCTION": True, - "FUNCTION":SHEPHERD_HEADER.CODE_RETRIEVAL} - -# pylint: disable=invalid-name -class STATE(): - SETUP = "setup" - AUTO = "auto" - WAIT = "wait" - TELEOP = "teleop" - END = "end" diff --git a/shepherd/alliance.py b/shepherd/alliance.py new file mode 100644 index 00000000..4a4cae86 --- /dev/null +++ b/shepherd/alliance.py @@ -0,0 +1,23 @@ +from utils import * + +class Alliance: + """This is the Alliance class, which holds the state values used to track + the scores and states of the alliances + """ + + def __init__(self, robot1, robot2): + self.robot1 = robot1 + self.robot2 = robot2 + self.score = 0 + + def set_score(self, new_score): + self.score = new_score + + def reset(self): + self.robot1.reset() + self.robot2.reset() + self.score = 0 + + def __str__(self): + return f"" + diff --git a/shepherd/audio.py b/shepherd/audio.py deleted file mode 100644 index 43c5cd12..00000000 --- a/shepherd/audio.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -import time -import simpleaudio as sa - - -perk = sa.WaveObject.from_wave_file('static/perkphase.wav') -horn = sa.WaveObject.from_wave_file('static/AIRHORNMLG.wav') -playbacks = [] -def play_perk_music(): - stop_music() - playback = perk.play() - playbacks.append(playback) -def play_horn(): - stop_music() - playback = horn.play() - playbacks.append(playback) -def stop_music(): - for playback in playbacks: - playback.stop() -""" diff --git a/shepherd/bot.py b/shepherd/bot.py deleted file mode 100644 index 3078b81f..00000000 --- a/shepherd/bot.py +++ /dev/null @@ -1,47 +0,0 @@ -import json -import threading -import requests -import Sheet - -# Set the webhook_url to the one provided by Slack when you create the -# webhook at https://my.slack.com/services/new/incoming-webhook/ -webhook_url = 'https://hooks.slack.com/services/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' -# queuing -# webhook_url = 'https://hooks.slack.com/services/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' -# #shepherd-bot-testing - -def notify_queueing(match_num): - send_plain_message("Match number "+str(match_num)+" is ending.") - -def team_numbers_on_deck(b1, b2, g1, g2): - send_plain_message("The following teams are now on deck: \n On the blue side\ -, we have team #%i and team #%i \n On the gold side, we\ - have team #%i and team #%i" % (b1, b2, g1, g2)) - -def team_names_on_deck(b1, b2, g1, g2): - send_plain_message("The following teams are now on deck: \n On the blue side\ -, we have %s and %s \n On the gold side, we\ - have %s and %s" % (b1, b2, g1, g2)) - -def send_plain_message(message): - slack_data = {'text': message} - response = requests.post( - webhook_url, data=json.dumps(slack_data), - headers={'Content-Type': 'application/json'} - ) - if response.status_code != 200: - raise ValueError( - 'Request to slack returned an error %s, the response is:\n%s' - % (response.status_code, response.text) - ) - -def announce_next_match(match_number): - thread = threading.Thread(target=next_match_thread, args=(match_number,), daemon=True) - thread.start() -def next_match_thread(match_number): - next_match_info = Sheet.get_match(int(match_number) + 1) - b1name = next_match_info["b1name"] - b2name = next_match_info["b2name"] - g1name = next_match_info["g1name"] - g2name = next_match_info["g2name"] - team_names_on_deck(b1name, b2name, g1name, g2name) diff --git a/shepherd/challenge_results.py b/shepherd/challenge_results.py new file mode 100644 index 00000000..a211a4a4 --- /dev/null +++ b/shepherd/challenge_results.py @@ -0,0 +1,19 @@ +from collections import defaultdict + +results = [False, False, False, False, False, False, False, False] + +CHALLENGE_RESULTS = defaultdict(lambda: results) +CHALLENGE_RESULTS[12] = [True] * 8 +CHALLENGE_RESULTS[26] = [True] * 8 +CHALLENGE_RESULTS[31] = [True] * 8 +CHALLENGE_RESULTS[35] = [True] * 8 +CHALLENGE_RESULTS[42] = [True, True, True, True, True, False, True, False] +CHALLENGE_RESULTS[44] = [True, True, False, True, True, True, False, False] +CHALLENGE_RESULTS[46] = [True] * 8 +CHALLENGE_RESULTS[6] = [True] * 8 +CHALLENGE_RESULTS[40] = [True, True, True, True, True, True, True, False] +CHALLENGE_RESULTS[1] = [True] * 8 +CHALLENGE_RESULTS[15] = [True, False, True, True, True, True, True, True] +CHALLENGE_RESULTS[10] = [True] * 8 +CHALLENGE_RESULTS[8] = [True] * 8 # Bishop +CHALLENGE_RESULTS[17] = [True, False, True, True, True, False, False, False] diff --git a/shepherd/coding_challenge_problems.py b/shepherd/coding_challenge_problems.py new file mode 100644 index 00000000..ccca7005 --- /dev/null +++ b/shepherd/coding_challenge_problems.py @@ -0,0 +1,217 @@ +import subprocess +import doctest +import os +from utils import * +import importlib +import re + +# TODO: replace with this year's challenges + +CH = None + + +def convert_time(num): + """ + >>> convert_time(0) # same as autograder (0) + [12, 0] + >>> convert_time(2) # one digit number (_) + [12, 2] + >>> convert_time(49) # random two digit number (_._) + [12, 49] + >>> convert_time(30) # two digit number ending in 0 (_0) + [12, 30] + >>> convert_time(600) # three digit number ending in two 0’s (_00) + [6, 0] + >>> convert_time(309) # (_0_) + [3, 9] + >>> convert_time(219) # random three digit number (_._._) + [2, 19] + >>> convert_time(1249) # (12..) + [12, 49] + >>> convert_time(1341) # random four digit number + [1, 41] + >>> convert_time(2310) # 23.. + [11, 10] + """ + return CH.convert_time(num) + + +def eta(pos): + """ + >>> eta([5, 14]) # original position, same as autograder + 0 + >>> eta([-4, -2]) # negative coordinates + 6 + >>> eta([7, 18]) # greater than 5 and 14 + 1 + >>> eta([4, 12]) # less than 5 and 14 + 0 + >>> eta([4, 19]) # one less and one greater + 1 + """ + return CH.eta(pos) + + +def wacky_numbers(num): + """ + >>> wacky_numbers(10) # random positive even number + 1249 + >>> wacky_numbers(-5) # random negative odd number + -32 + >>> wacky_numbers(23) # random positive odd number + 2297 + >>> wacky_numbers(-6) # random negative even number + -12 + >>> wacky_numbers(0) # 0 + 0 + """ + return CH.wacky_numbers(num) + + +def num_increases(num): + """ + >>> num_increases(1) # not large enough to qualify + 0 + >>> num_increases(127) # basic test case of increasing sequence + 2 + >>> num_increases(98431) # basic test case of decreasing sequence + 0 + >>> num_increases(149325) # test a somewhat random sequence + 3 + >>> num_increases(111111111) # flat test case + 0 + """ + return CH.num_increases(num) + + +def wheresArmadillo(animals): + """ + >>> wheresArmadillo(["mouse", "frog", "chicken", "cat", "dog", "armadillo", "pig", "cow", "dinosaur"]) + 15 + >>> wheresArmadillo(["mouse", "frog", "chicken", "turkey", "cat", "dog", "armadillo", "pig", "cow", "buffalo", "dinosaur"]) + 24 + >>> wheresArmadillo(["mouse", "frog", "chicken", "cat", "armadillo", "alligator", "pig", "cow", "dinosaur"]) + 4 + >>> wheresArmadillo(["dog", "armadillo", "pig", "cow", "buffalo", "dinosaur"]) + 2 + >>> wheresArmadillo(["dog", "armadillo"]) + 2 + """ + return CH.wheresArmadillo(animals) + + +def pie_cals_triangle(num): + """ + >>> pie_cals_triangle(1) # small test + 15 + >>> pie_cals_triangle(100) # large test + 1000202010900 + >>> pie_cals_triangle(-1) # negative test + -7 + >>> pie_cals_triangle(0) # zero case + 0 + """ + return CH.pie_cals_triangle(num) + + +def road_trip(d): + """ + >>> road_trip(0) # zero case + 0 + >>> road_trip(12) # small test + 11 + >>> road_trip(51) # medium test + 48 + >>> road_trip(104) # large test + 102 + """ + return CH.road_trip(d) + + +def convertRoman(num): + """ + >>> convertRoman(2) # number less than 4 + 'II' + >>> convertRoman(31) # two digit number without edge case + 'XXXI' + >>> convertRoman(45) # two digit number edge case + 'XLV' + >>> convertRoman(832) # three digit number + 'DCCCXXXII' + >>> convertRoman(1952) # four digit number + 'MCMLII' + """ + return CH.convertRoman(num) + + +def picky_rat(words): + """ + >>> [''] + [''] + >>> picky_rat(['apple', 'banana', 'carrot']) + ['pple', 'bnana', 'carot'] + >>> picky_rat(['carrot', 'banana', 'apple']) + ['pple', 'bnana', 'carot'] + >>> picky_rat(['notarealword', 'notarealwordtoo', 'notarealwordalso']) + ['otarealword', 'ntarealwordalso', 'noarealwordtoo'] + >>> picky_rat(['apple', 'apple', 'apple', 'apple', 'apple', 'apple']) + ['pple', 'aple', 'aple', 'appe', 'appl', 'pple'] + >>> picky_rat(['notarealword', 'notarealwordtoo', 'notarealwordalso']) + ['otarealword', 'ntarealwordalso', 'noarealwordtoo'] + >>> picky_rat(['a', 'e', 'i', 'o' , 'u']) + ['', '', '', '', ''] + """ + return CH.picky_rat(words) + + +def get_results(ip): + global CH + return [True] * 8 + get_challenges(ip) + # TODO: need feedback if they passed visible/hidden tests, also how do we tell them what they failed? + + os.system('clear') + CH = importlib.import_module("student") + doc_tests = doctest.DocTestFinder() + + easy_challenges = [eta, convert_time] + hard_challenges = [picky_rat, convertRoman] + + easy_passed = run_set_of_tests(doc_tests, easy_challenges) + hard_passed = run_set_of_tests(doc_tests, hard_challenges) + print(easy_passed) + print(hard_passed) + + + + # TODO: get the challenge results as list of bools ([passed challenge 1, passed challenge 2, etc.]) + + # list of: + # +------+ +---------+ + # |module| --DocTestFinder-> | DocTest | --DocTestRunner-> results + # +------+ | ^ +---------+ | ^ (printed) + # | | | Example | | | + # v | | ... | v | + # DocTestParser | Example | OutputChecker + # +---------+ + + +def get_challenges(ip): + subprocess.call(['./student_code.sh', ip]) + + +def run_set_of_tests(doc_test, test_set): + """ + takes a list of functions, returns True if all doctests pass. + """ + passed = True + for test in test_set: + passed = passed and run_autograder_on_function(doc_test, test) == 0 + return passed + + +def run_autograder_on_function(doc_tests, function): + failed = 0 + for doc_test in doc_tests.find(function): + failed += doctest.DocTestRunner().run(doc_test)[0] + return failed diff --git a/shepherd/dawn_server.py b/shepherd/dawn_server.py deleted file mode 100644 index fff7994b..00000000 --- a/shepherd/dawn_server.py +++ /dev/null @@ -1,58 +0,0 @@ -import threading -import json -import time -import queue -import gevent # pylint: disable=import-error -from flask import Flask, render_template # pylint: disable=import-error -from flask_socketio import SocketIO, emit, join_room, leave_room, send # pylint: disable=import-error -from Utils import * -from LCM import * - -HOST_URL = "192.168.128.64" # "0.0.0.0" -PORT = 7000 - -#TODO work on this, new headers and deprecated headers. - -app = Flask(__name__) -app.config['SECRET_KEY'] = 'omegalul!' -socketio = SocketIO(app) -master_robots = {ALLIANCE_COLOR.BLUE: 0, ALLIANCE_COLOR.GOLD:0} - -@socketio.on('dawn-to-server-alliance-codes') -def ui_to_server_setup_match(alliance_codes): - lcm_send(LCM_TARGETS.SHEPHERD, SHEPHERD_HEADER.CODE_APPLICATION, json.loads(alliance_codes)) - -def receiver(): - events = gevent.queue.Queue() - lcm_start_read(str.encode(LCM_TARGETS.DAWN), events, put_json=True) - - while True: - if not events.empty(): - event = events.get_nowait() - eventDict = json.loads(event) - print("RECEIVED:", event) - if eventDict["header"] == DAWN_HEADER.ROBOT_STATE: - socketio.emit(DAWN_HEADER.ROBOT_STATE, event) - elif eventDict["header"] == DAWN_HEADER.CODES: - socketio.emit(DAWN_HEADER.CODES, event) - elif eventDict["header"] == DAWN_HEADER.RESET: - master_robots[ALLIANCE_COLOR.BLUE] = 0 - master_robots[ALLIANCE_COLOR.GOLD] = 0 - elif eventDict["header"] == DAWN_HEADER.MASTER: - master_robots[eventDict["alliance"]] = int(eventDict["team_number"]) - # socketio.emit(DAWN_HEADER.MASTER, event) - print(master_robots) - # print({"alliance": ALLIANCE_COLOR.BLUE, - # "team_number": master_robots[ALLIANCE_COLOR.BLUE]}) - # print({"alliance": ALLIANCE_COLOR.GOLD, - # "team_number": master_robots[ALLIANCE_COLOR.GOLD]}) - socketio.emit(DAWN_HEADER.MASTER, json.dumps(master_robots)) - # socketio.emit(DAWN_HEADER.MASTER, json.dumps({"alliance": ALLIANCE_COLOR.BLUE, - # "team_number": master_robots[ALLIANCE_COLOR.BLUE]})) - # socketio.emit(DAWN_HEADER.MASTER, json.dumps({"alliance": ALLIANCE_COLOR.GOLD, - # "team_number": master_robots[ALLIANCE_COLOR.GOLD]})) - socketio.emit(DAWN_HEADER.HEARTBEAT, json.dumps({"heartbeat" : 1})) - socketio.sleep(1) - -socketio.start_background_task(receiver) -socketio.run(app, host=HOST_URL, port=PORT) diff --git a/shepherd/documentation/LCM_README.md b/shepherd/documentation/LCM_README.md deleted file mode 100644 index 6aa0828b..00000000 --- a/shepherd/documentation/LCM_README.md +++ /dev/null @@ -1,28 +0,0 @@ -# [LCM](https://lcm-proj.github.io/) - -[LCM Build Instructions](https://lcm-proj.github.io/build_instructions.html) (Linux or Mac OS strongly recommended) - -Make sure to run setup.py (or the setup script for the language you are using). - -LCM uses UDP multicast to exchange messages, and can be used for to send byte representations of objects (in Python, using `.encode()`) and user-defined data types. There are [tutorials](https://lcm-proj.github.io/tutorial_general.html) for defining these data types in various languages, using `lcm-gen` to generate language-specific bindings, and using `lcm.publish()` and `lcm.subscribe()` to send and receive messages. - -## Using LCM in Shepherd - -[LCM Python API](https://lcm-proj.github.io/python/lcm.LCM-class.html#publish) - -Read [here](https://lcm-proj.github.io/multicast_setup.html) about initializing an LCM object and what address and TTL to choose. - -To communicate over a network, Shepherd uses LCM to send messages to a server, which relays those messages through a websocket. - -## Methods -`LCM.py` is a library of two methods for sending and receiving messages using the LCM communications protocol. - -+ ```lcm_start_read(receive_channel, queue, put_json=False):``` - -Takes in receiving channel name (string), queue (Python queue object) and whether to add received items to queue as JSON or Python dict. Creates thread that receives any message to receiving channel and adds it to queue as tuple (header, dict). -header: string -dict: Python dictionary - -+ ```lcm_send(target_channel, header, dic={}):``` - -Send header (any type) and dic (Python dictionary) to target channel (string). diff --git a/shepherd/documentation/flask_server_and_ui_readme.md b/shepherd/documentation/flask_server_and_ui_readme.md deleted file mode 100644 index 363782bd..00000000 --- a/shepherd/documentation/flask_server_and_ui_readme.md +++ /dev/null @@ -1,90 +0,0 @@ - -# Running the server -In the command line: - - pip install flask-socketio - pip install gevent - export FLASK_APP=[SERVER_NAME.py] - flask run - -Go to localhost:[PORTNUM]/[page_name.html] in a browser. - -### 2018 Configurations: -#### UI: -Server Name: server.py - -Port: 5000 - -Pages: RFID_control.html, score_adjustment.html, staff_gui.html - -#### Scoreboard: -Server Name: scoreboard_server.py - -Port: 5500 - -Pages: Scoreboard.html - -#### Dawn: -Server Name: dawn_server.py - -Port: 7000 - - - -# Server-side modifications - -Reading the official flask-socketio and socket.io docs may help get you started with the socket stuff used here: https://flask-socketio.readthedocs.io/en/latest/ and https://socket.io/docs/. - -Jinja is a general purpose templating language, but here it is basically used as an HTML template that is compatible with Python. For more about Jinja, go check the official docs: http://jinja.pocoo.org/docs/2.10/. - -## Fill in a unique port number not used by another server or by local machine processes -Change the line: -```python -PORT = (NUM) -``` - -## Serving a new page with a Jinja template -Registers a certain app route with a specific Jinja template: -```python -@app.route('/page.html/') -def page(): - return render_template('page.html') -``` -The page.html should be in the 'templates' folder. - -Additionally, the main thing for compatibility with the PiE servers is that anywhere a static dependency would have been linked to in HTML, it must be replaced with a Jinja url_for() call: -```javascript - -``` -becomes -```javascript - -``` - -The socket.io.js file should be in the 'static' folder. - - -## Receiving message from UI and forwarding it to LCM -Use the @socketio.on decorator to register an event handler to a specific callback: -```python -@socketio.on('ui-to-server-message-event-name') -def ui_to_server_message_name(received_data): - lcm_send(LCM_TARGETS.SHEPHERD, SHEPHERD_HEADER.TARGET_NAME, json.loads(received_data)) -``` - -## Sending a particular event to UI -Inside the receiver loop, use socketio.emit to send an event name and some data: -```python -if event[0] == UI_HEADER.EVENT_NAME: - socketio.emit('server-to-ui-message-event-name', json.dumps(event[1], ensure_ascii=False)) -``` - -# Client-side JS modifications - -## Receiving a message from the server -Register an event handler to a specific callback using socket.on: -```javascript -socket.on('server-to-ui-message-event-name', function(data) { - //do stuff -}) -``` diff --git a/shepherd/dummy_sensor.py b/shepherd/dummy_sensor.py new file mode 100644 index 00000000..fdf8e168 --- /dev/null +++ b/shepherd/dummy_sensor.py @@ -0,0 +1,43 @@ +from ydl import ydl_send, ydl_start_read +from utils import * +import time + +# Arduino 1 Write Tests +""" +for i in range(7): + args = {"id" : i} + YDL.ydl_send(YDL_TARGETS.SENSORS, SENSOR_HEADER.TURN_ON_LIGHT, args) + time.sleep(0.5) + YDL.ydl_send(YDL_TARGETS.SENSORS, SENSOR_HEADER.TURN_OFF_LIGHT, args) +""" + +# Arduino 2 Write Tests + +""" +YDL.ydl_send(YDL_TARGETS.SENSORS, SENSOR_HEADER.TURN_ON_FIRE_LIGHT) +time.sleep(2) +YDL.ydl_send(YDL_TARGETS.SENSORS, SENSOR_HEADER.TURN_OFF_FIRE_LIGHT) +""" + +# Arduino 3 Write Tests + +""" +for i in range(10): + YDL.ydl_send(YDL_TARGETS.SENSORS, SENSOR_HEADER.SET_TRAFFIC_LIGHT, {"color": "green"}) + time.sleep(2) + YDL.ydl_send(YDL_TARGETS.SENSORS, SENSOR_HEADER.SET_TRAFFIC_LIGHT, {"color": "red"}) + time.sleep(2) + YDL.ydl_send(YDL_TARGETS.SENSORS, SENSOR_HEADER.SET_TRAFFIC_LIGHT, {"color": "green"}) + time.sleep(2) + YDL.ydl_send(YDL_TARGETS.SENSORS, SENSOR_HEADER.SET_TRAFFIC_LIGHT, {"color": "red"}) + time.sleep(2) + YDL.ydl_send(YDL_TARGETS.SENSORS, SENSOR_HEADER.TURN_OFF_TRAFFIC_LIGHT, {}) +""" + +# Arduino 4 Write Tests + + +YDL.ydl_send(YDL_TARGETS.SENSORS, SENSOR_HEADER.TURN_ON_LASERS) +#time.sleep(2) +# YDL.ydl_send(YDL_TARGETS.SENSORS, SENSOR_HEADER.TURN_OFF_LASERS) + diff --git a/shepherd/fake_runtime.py b/shepherd/fake_runtime.py new file mode 100644 index 00000000..bfb0f172 --- /dev/null +++ b/shepherd/fake_runtime.py @@ -0,0 +1,116 @@ +import socket +import selectors +from protos import run_mode_pb2 +from protos import start_pos_pb2 +from protos import game_state_pb2 +from utils import PROTOBUF_TYPES + +SERVER_ADDR = ("127.0.0.1", 8101) + + + +def send_message(conn, mode: int, protobuf_obj): + msg_str = protobuf_obj.SerializeToString() + msg = bytes(msg_str) + msglen = len(msg).to_bytes(2, "little") + try: + conn.sendall(bytes([mode]) + msglen + msg) + except (ConnectionError, OSError) as ex: + print(f"Error while sending message: {ex}") + + + +class ReadObject: + ''' + An iterable object for receiving messages + Append incoming message bytes to self.inb, + and then you can loop through the object to get the messages + ''' + def __init__(self): + self.got_indentification = False + self.inb = b'' + + def __iter__(self): + return self + + def __next__(self): + if len(self.inb) < 9 and not self.got_indentification: + self.got_indentification = True + print(f"identification byte: {self.inb[0]}") + self.inb = self.inb[1:] + if len(self.inb) < 3: + raise StopIteration + msg_type = int.from_bytes(self.inb[0:1], "little") + msg_len = int.from_bytes(self.inb[1:3], "little") + if len(self.inb) < 3 + msg_len: + raise StopIteration + msg_bytes = self.inb[3: msg_len + 3] + self.inb = self.inb[msg_len + 3:] + + if msg_type == PROTOBUF_TYPES.RUN_MODE: + pb = run_mode_pb2.RunMode() + pb.ParseFromString(msg_bytes) + return "received run mode: " + str(pb.mode) + if msg_type == PROTOBUF_TYPES.START_POS: + pb = start_pos_pb2.StartPos() + pb.ParseFromString(msg_bytes) + return "received start pos: " + str(pb.pos) + if msg_type == PROTOBUF_TYPES.GAME_STATE: + pb = game_state_pb2.GameState() + pb.ParseFromString(msg_bytes) + return "received game state: " + str(pb.state) + return "invalid protobuf type" + +def accept(sel, sock): + ''' + (server method - internal use only) + When sock has a connection ready to accept, + accept the connection and register it in sel + Note that we want the new connections to be blocking, + since dealing with non-blocking writes is a pain + ''' + conn, addr = sock.accept() # Should be ready + print('accepted connection from', addr) + sel.register(conn, selectors.EVENT_READ, ReadObject()) + +def read(sel, conn, obj): + ''' + (server method - internal use only) + When conn has bytes ready to read, read those bytes and + forward messages to the correct subscribers + ''' + data = conn.recv(1024) # Should be ready + if len(data) == 0: + print('closing connection from socket') + sel.unregister(conn) + conn.close() + else: + obj.inb += data + for message in obj: + print(message) + +def start_backend(): + ''' + (server method - internal use only) + Starts the YDL server that processes will use + to communicate with each other + ''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(SERVER_ADDR) + sock.listen() + sock.setblocking(False) + sel = selectors.DefaultSelector() + sel.register(sock, selectors.EVENT_READ, None) + while True: + events = sel.select(timeout=None) + for key, _mask in events: + if key.data is None: + accept(sel, key.fileobj) + else: + read(sel, key.fileobj, key.data) + +if __name__ == "__main__": + print("Starting fake runtime server at address:", SERVER_ADDR) + start_backend() + diff --git a/shepherd/installlcm b/shepherd/installlcm deleted file mode 100755 index ae325fb9..00000000 --- a/shepherd/installlcm +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# installlcm -- Installs LCM on Linux - -lcm_version=1.3.1 -lcm_archive_filename="lcm-$lcm_version.zip" - -# Download -wget "https://github.com/lcm-proj/lcm/releases/download/v$lcm_version/$lcm_archive_filename" -unzip "$lcm_archive_filename" -cd "lcm-$lcm_version" - -# Install -./configure -make -sudo make install -sudo ldconfig - -cd lcm-python -python3 setup.py build -python3 setup.py install -cd .. - -# Cleanup -cd .. -rm -rf "lcm-$lcm_version" -rm "$lcm_archive_filename" diff --git a/shepherd/lowcar/Device/Device.ino b/shepherd/lowcar/Device/Device.ino new file mode 100644 index 00000000..fbf5c110 --- /dev/null +++ b/shepherd/lowcar/Device/Device.ino @@ -0,0 +1,20 @@ +#include + +// flash script will append a #include above this line to include the necessary library for the requested device +#include +#include +#include +#include + +// THIS FILE IS USED BY THE FLASH SCRIPT TO BUILD THE ACTUAL Arduino3.INO FILE (which will be located in Device/Device.ino) + +Device* device; //declare the device + +void setup() { + device = new Arduino3(); //flash script will replace DEVICE with requested Device type + device->set_uid(0x3); //flash script will replace UID_INSERTED with randomly generated 64-bit value +} + +void loop() { + device->loop(); +} diff --git a/shepherd/lowcar/Device_template.cpp b/shepherd/lowcar/Device_template.cpp new file mode 100644 index 00000000..abb2db6a --- /dev/null +++ b/shepherd/lowcar/Device_template.cpp @@ -0,0 +1,19 @@ + +// flash script will append a #include above this line to include the necessary library for the requested device +#include +#include +#include +#include + +// THIS FILE IS USED BY THE FLASH SCRIPT TO BUILD THE ACTUAL DEVICE.INO FILE (which will be located in Device/Device.ino) + +Device* device; //declare the device + +void setup() { + device = new DEVICE(); //flash script will replace DEVICE with requested Device type + device->set_uid(UID_INSERTED); //flash script will replace UID_INSERTED with randomly generated 64-bit value +} + +void loop() { + device->loop(); +} diff --git a/shepherd/lowcar/devices/Arduino1/Arduino1.cpp b/shepherd/lowcar/devices/Arduino1/Arduino1.cpp new file mode 100644 index 00000000..5a5016c8 --- /dev/null +++ b/shepherd/lowcar/devices/Arduino1/Arduino1.cpp @@ -0,0 +1,131 @@ +#include "Arduino1.h" + +// number of switches on a limit switch (how many input pins) +const int Arduino1::NUM_BUTTONS = 7; +const int Arduino1::NUM_LIGHTS = 7; +/* +button1: 2 +light1: 3 +button2: 4 +light2: 5 +button3: 6 +light3: 7 +button4: 8 +light4: 9 +button5: 10 +light5: A0 +button6: 14 +light6: A1 +button7: 15 +light7: A2 +*/ +const uint8_t Arduino1::pins[] = { + // buttons 1 - 7 + 2, + 4, + 6, + 8, + 10, + 12, + 13, + // lights 1 - 7 + 3, + 5, + 7, + 9, + 11, + A0, + A1 +}; + +// Constructor is called once and immediately when the Arduino is plugged in +Arduino1::Arduino1() : Device(DeviceType::ARDUINO1, 13) { +} + +size_t Arduino1::device_read(uint8_t param, uint8_t* data_buf) { + // put pin value into data_buf and return the amount of bytes written + if (param >= Arduino1::NUM_BUTTONS) { + // this->msngr->lowcar_printf("Sorry, can only read from buttons. Please check your shepherd_util.c"); + return 0; + } + data_buf[0] = (digitalRead(this->pins[param]) == HIGH) ? TRUE : FALSE; + + static uint64_t last_update_time[] = {0, 0, 0, 0, 0, 0, 0}; + uint64_t curr = millis(); + + // log each button every 500ms + if (curr - last_update_time[param] > 500) { + // this->msngr->lowcar_printf("button %d is %s", param, data_buf[0] == 1 ? "pressed" : "not pressed"); + last_update_time[param] = curr; + } + + return sizeof(uint8_t); +} +// writable, not readable. should just call device_write id hope. +size_t Arduino1::device_write(uint8_t param, uint8_t* data_buf) { + if (param < Arduino1::NUM_BUTTONS) { + // this->msngr->lowcar_printf("Should not be writing to buttons."); + return 0; + } + if (data_buf[0] == 1) { + digitalWrite(Arduino1::pins[param], HIGH); + // this->msngr->lowcar_printf("Wrote %s to %d", "HIGH", Arduino1::pins[param]); + } + else { + digitalWrite(Arduino1::pins[param], LOW); + this->msngr->lowcar_printf("Wrote %s to %d", "LOW", Arduino1::pins[param]); + } + return sizeof(uint8_t); +} + +void Arduino1::device_enable() { + // todo: ask ben what is diff between device enable and constructor + this->msngr->lowcar_printf("ARDUINO 1 ENABLING"); + + // set all pins to INPUT mode + for (int i = 0; i < Arduino1::NUM_BUTTONS; i++) { + pinMode(Arduino1::pins[i], INPUT); + } + + for (int i = 0; i < Arduino1::NUM_LIGHTS; i++) { + pinMode(Arduino1::pins[i + NUM_BUTTONS], OUTPUT); + } +} + +void Arduino1::device_disable() { + this->msngr->lowcar_printf("ARDUINO 1 DISABLED"); +} + +void Arduino1::device_actions() { + /* + static uint64_t last_update_time = 0; + // static uint64_t last_count_time = 0; + uint64_t curr = millis(); + + // Simulate read-only params changing + if (curr - last_update_time > 500) { + this->runtime += 2; + this->shepherd += 1.9; + this->dawn = !this->dawn; + + this->devops++; + this->atlas += 0.9; + this->infra = TRUE; + + // this->msngr->lowcar_print_float("funtime", this->funtime); + // this->msngr->lowcar_print_float("atlas", this->atlas); + // this->msngr->lowcar_print_float("pdb", this->pdb); + + last_update_time = curr; + } + + // this->dusk++; + // + // //use dusk as a counter to see how fast this loop is going + // if (curr - last_count_time > 1000) { + // last_count_time = curr; + // this->msngr->lowcar_printf("loops in past second: %d", this->dusk); + // this->dusk = 0; + // } + */ +} diff --git a/shepherd/lowcar/devices/Arduino1/Arduino1.h b/shepherd/lowcar/devices/Arduino1/Arduino1.h new file mode 100644 index 00000000..fa3698fa --- /dev/null +++ b/shepherd/lowcar/devices/Arduino1/Arduino1.h @@ -0,0 +1,30 @@ +#pragma once + +#include "Device.h" +#include "defs.h" +#include "StatusLED.h" + +/** + * write stuff + */ + +class Arduino1 : public Device { + public: + // construct an Arduino 1 + Arduino1(); + + virtual size_t device_read(uint8_t param, uint8_t* data_buf); + virtual size_t device_write(uint8_t param, uint8_t* data_buf); + virtual void device_enable(); + virtual void device_disable(); + + // Changes several of the readable params for testing + virtual void device_actions(); + private: + // number of buttons + const static int NUM_BUTTONS; + const static int NUM_LIGHTS; + // TODO: update pins that the buttons read data from (defined in defs.h) + const static uint8_t pins[]; + +}; diff --git a/shepherd/lowcar/devices/Arduino2/Arduino2.cpp b/shepherd/lowcar/devices/Arduino2/Arduino2.cpp new file mode 100644 index 00000000..e38ae7db --- /dev/null +++ b/shepherd/lowcar/devices/Arduino2/Arduino2.cpp @@ -0,0 +1,138 @@ +#include "Arduino2.h" + +const int Arduino2::NUM_LINEBREAKS = 4; // params 0-4 +const int Arduino2::NUM_BUTTONS = 1; +const int Arduino2::NUM_LIGHTS = 1; + +#define S0 2 +#define S1 3 +#define S2 4 +#define S3 5 + +#define LINEBREAK_THRESHOLD 100 + +const uint8_t Arduino2::pins[] = { + 6, // desert_linebreak + 7, // dehydration_linebreak + 8, // hypothermia_linebreak + 9, // airport_linebreak + 11, // fire_lever + 12, // fire_light +}; + +// Constructor is called once and immediately when the Arduino is plugged in +Arduino2::Arduino2() : Device(DeviceType::ARDUINO2, 13) +{ + for(int i = 0; i < 4; i++) { + this->prev_red_frequencies[i] = 6000; + } +} + +size_t Arduino2::device_read(uint8_t param, uint8_t *data_buf) +{ + // return 1; + // this->msngr->lowcar_printf("hullo wurld\n"); + // this->msngr->lowcar_printf("param is %d\n", param); + // put pin value into data_buf + if (param < Arduino2::NUM_LINEBREAKS) + { + // Reading the output frequency + // so i guess this is only gonna work once everything is plugged in + // rn it only works for pin 9. all: Arduino2::pins[param] + int redFrequency = pulseIn(Arduino2::pins[param], LOW, 20000); + + // we should ignore red frequency == 0 because it is a bug + if (redFrequency == 0) { + redFrequency = this->prev_red_frequencies[param]; + } + if (redFrequency <= LINEBREAK_THRESHOLD && redFrequency > 0) + { + data_buf[0] = 0; + } + else + { + data_buf[0] = 1; + } + this->prev_red_frequencies[param] = redFrequency; + } + else if (param < Arduino2::NUM_LINEBREAKS + Arduino2::NUM_BUTTONS) + { + data_buf[0] = (digitalRead(Arduino2::pins[param]) == HIGH) ? 1 : 0; + } + else + { + this->msngr->lowcar_printf("Should not be reading from a light. param is %d", param); + return 0; + } + + /* + static uint64_t last_update_time[] = {0, 0, 0, 0, 0, 0}; + uint64_t curr = millis(); + + // log each button every 500ms + if (curr - last_update_time[param] > 500) { + this->msngr->lowcar_printf("param %d is %s", param, data_buf[0] == 1 ? "true" : "false"); + last_update_time[param] = curr; + } + */ + + return sizeof(uint8_t); +} + +size_t Arduino2::device_write(uint8_t param, uint8_t *data_buf) +{ + int min_index = Arduino2::NUM_LINEBREAKS + Arduino2::NUM_BUTTONS; + if (param < min_index || + param >= min_index + Arduino2::NUM_LIGHTS) + { + this->msngr->lowcar_printf("Trying to write to something that is not fire light."); + return 0; + } + + if (data_buf[0] > 1) + { + this->msngr->lowcar_printf("data_buf[0] is %d, but can only be 0 or 1.", data_buf[0]); + } + + digitalWrite(Arduino2::pins[param], data_buf[0] == 1 ? HIGH : LOW); + + return sizeof(uint8_t); +} + +void Arduino2::device_enable() +{ + this->msngr->lowcar_printf("ARDUINO 2 ENABLING"); + // set all pins to INPUT mode + int num_inputs = Arduino2::NUM_LINEBREAKS + Arduino2::NUM_BUTTONS; + for (int i = 0; i < num_inputs; i++) + { + pinMode(Arduino2::pins[i], INPUT); + } + + for (int i = 0; i < Arduino2::NUM_LIGHTS; i++) + { + pinMode(Arduino2::pins[i + num_inputs], OUTPUT); + } + + // Setting the outputs + pinMode(S0, OUTPUT); + pinMode(S1, OUTPUT); + pinMode(S2, OUTPUT); + pinMode(S3, OUTPUT); + // Setting each linebreak sensor as an input + pinMode(Arduino2::pins[0], INPUT); + pinMode(Arduino2::pins[1], INPUT); + pinMode(Arduino2::pins[2], INPUT); + pinMode(Arduino2::pins[3], INPUT); + // Setting frequency scaling to 20% + digitalWrite(S0, HIGH); + digitalWrite(S1, LOW); + // Setting RED (R) filtered photodiodes to be read + digitalWrite(S2, LOW); + digitalWrite(S3, LOW); +} + +void Arduino2::device_disable() +{ + this->msngr->lowcar_printf("ARDUINO 2 DISABLED"); +} diff --git a/shepherd/lowcar/devices/Arduino2/Arduino2.h b/shepherd/lowcar/devices/Arduino2/Arduino2.h new file mode 100644 index 00000000..b639e01e --- /dev/null +++ b/shepherd/lowcar/devices/Arduino2/Arduino2.h @@ -0,0 +1,28 @@ +#pragma once + +#include "Device.h" +#include "defs.h" +#include "StatusLED.h" + +/** + * write stuff + */ + +class Arduino2 : public Device { + public: + // construct a Dummy device + Arduino2(); + + virtual size_t device_read(uint8_t param, uint8_t* data_buf); + virtual size_t device_write(uint8_t param, uint8_t* data_buf); + virtual void device_enable(); + virtual void device_disable(); + + private: + const static int NUM_LINEBREAKS; + const static int NUM_BUTTONS; + const static int NUM_LIGHTS; + // TODO: update pins that the buttons read data from (defined in defs.h) + const static uint8_t pins[]; + int prev_red_frequencies[4]; +}; diff --git a/shepherd/lowcar/devices/Arduino3/Arduino3.cpp b/shepherd/lowcar/devices/Arduino3/Arduino3.cpp new file mode 100644 index 00000000..e92b33d7 --- /dev/null +++ b/shepherd/lowcar/devices/Arduino3/Arduino3.cpp @@ -0,0 +1,156 @@ +#include "Arduino3.h" + +const int Arduino3::NUM_LINEBREAKS = 2; +const int Arduino3::NUM_BUTTONS = 1; +const int Arduino3::NUM_LIGHT_PINS = 2; + +#define S0 5 +#define S1 6 +#define S2 7 +#define S3 8 + +#define LINEBREAK_THRESHOLD 100 + +const uint8_t Arduino3::pins[] = { + 9, // city_linebreak + 10, // traffic_linebreak + 4, // traffic_button + 2, // traffic_light on = 1, off = 0 + 3, // traffic_light red = 0, green = 1 +}; +// 2 is red, 3 is green, no pin is off + +// Constructor is called once and immediately when the Arduino is plugged in +Arduino3::Arduino3() : Device(DeviceType::ARDUINO3, 13) +{ + for(int i = 0; i < 2; i++) { + this->prev_red_frequencies[i] = 6000; + } +} + +size_t Arduino3::device_read(uint8_t param, uint8_t *data_buf) +{ + // put pin value into data_buf and return the amount of bytes written + if (param < Arduino3::NUM_LINEBREAKS) + { + // Reading the output frequency + int redFrequency = pulseIn(Arduino3::pins[param], LOW, 50000); + + // we should ignore red frequency == 0 because it is a bug + if (redFrequency == 0) { + redFrequency = this->prev_red_frequencies[param]; + } + + if (redFrequency <= LINEBREAK_THRESHOLD && redFrequency > 0) + { + data_buf[0] = 0; + } + else + { + data_buf[0] = 1; + } + this->prev_red_frequencies[param] = redFrequency; + + return sizeof(uint8_t); + } + else if (param < Arduino3::NUM_LINEBREAKS + Arduino3::NUM_BUTTONS) + { + data_buf[0] = (digitalRead(this->pins[param]) == HIGH) ? 1 : 0; + return sizeof(uint8_t); + } + else + { + this->msngr->lowcar_printf("Should not be reading from a light. param is %d", param); + return 0; + } + + /* + static uint64_t last_update_time[] = {0}; + uint64_t curr = millis(); + + // log each button every 500ms + if (curr - last_update_time[param] > 500) { + this->msngr->lowcar_printf("param %d is %s", param, data_buf[0] == 1 ? "true" : "false"); + last_update_time[param] = curr; + } + */ +} + +size_t Arduino3::device_write(uint8_t param, uint8_t *data_buf) +{ + // this->msngr->lowcar_printf("write called with %d", param); + int red_pin = 2; + int green_pin = 3; + int min_index = Arduino3::NUM_LINEBREAKS + Arduino3::NUM_BUTTONS; + int max_index = Arduino3::NUM_LINEBREAKS + Arduino3::NUM_BUTTONS; + if (param < min_index || param > min_index) + { + this->msngr->lowcar_printf("Trying to write to something that is not the traffic light."); + return 0; + } + int *int_buf = (int *) data_buf; + int value = int_buf[0]; + if (value == 0) + { + // off + digitalWrite(red_pin, LOW); + digitalWrite(green_pin, LOW); + return 4; + } + else if (value == 1) + { + // red + digitalWrite(green_pin, LOW); + digitalWrite(red_pin, HIGH); + return 4; + } + else if (value == 2) + { + // green + digitalWrite(red_pin, LOW); + digitalWrite(green_pin, HIGH); + return 4; + } + else + { + this->msngr->lowcar_printf("Traffic light value must be 0, 1 or 2 but was %d", param); + } + + return 0; +} + +void Arduino3::device_enable() +{ + this->msngr->lowcar_printf("ARDUINO 3 ENABLING"); + // set all pins to INPUT mode + int num_inputs = Arduino3::NUM_LINEBREAKS + Arduino3::NUM_BUTTONS; + for (int i = 0; i < num_inputs; i++) + { + pinMode(Arduino3::pins[i], INPUT); + } + + for (int i = 0; i < Arduino3::NUM_LIGHT_PINS; i++) + { + pinMode(Arduino3::pins[i + num_inputs], OUTPUT); + } + + // Setting the outputs + pinMode(S0, OUTPUT); + pinMode(S1, OUTPUT); + pinMode(S2, OUTPUT); + pinMode(S3, OUTPUT); + // Setting the sensorOut as an input + pinMode(this->pins[0], INPUT); + pinMode(this->pins[1], INPUT); + // Setting frequency scaling to 20% + digitalWrite(S0, HIGH); + digitalWrite(S1, LOW); + // Setting RED (R) filtered photodiodes to be read + digitalWrite(S2, LOW); + digitalWrite(S3, LOW); +} + +void Arduino3::device_disable() +{ + this->msngr->lowcar_printf("ARDUINO 3 DISABLED :("); +} diff --git a/shepherd/lowcar/devices/Arduino3/Arduino3.h b/shepherd/lowcar/devices/Arduino3/Arduino3.h new file mode 100644 index 00000000..de09df97 --- /dev/null +++ b/shepherd/lowcar/devices/Arduino3/Arduino3.h @@ -0,0 +1,29 @@ +#pragma once + +#include "Device.h" +#include "defs.h" +#include "StatusLED.h" + +/** + * write stuff + */ + +class Arduino3 : public Device { + public: + // construct a Dummy device + Arduino3(); + + virtual size_t device_read(uint8_t param, uint8_t* data_buf); + virtual size_t device_write(uint8_t param, uint8_t* data_buf); + virtual void device_enable(); + virtual void device_disable(); + + private: + // number of buttons + const static int NUM_LINEBREAKS; + const static int NUM_BUTTONS; + const static int NUM_LIGHT_PINS; + // TODO: update pins that the buttons read data from (defined in defs.h) + const static uint8_t pins[]; + int prev_red_frequencies[2]; +}; diff --git a/shepherd/lowcar/devices/Arduino4/Arduino4.cpp b/shepherd/lowcar/devices/Arduino4/Arduino4.cpp new file mode 100644 index 00000000..5110df45 --- /dev/null +++ b/shepherd/lowcar/devices/Arduino4/Arduino4.cpp @@ -0,0 +1,22 @@ +#include "Arduino4.h" + +const int Arduino4::LASERS_PIN = 2; + +// Constructor is called once and immediately when the Arduino is plugged in +Arduino4::Arduino4() : Device(DeviceType::ARDUINO4, 13) { +} + +size_t Arduino4::device_write(uint8_t param, uint8_t* data_buf) { + digitalWrite(Arduino4::LASERS_PIN, data_buf[0] == 1 ? HIGH: LOW); + return sizeof(uint8_t); +} + +void Arduino4::device_enable() { + this->msngr->lowcar_printf("ARDUINO 4 ENABLING"); + // set pin to OUTPUT mode + pinMode(Arduino4::LASERS_PIN, OUTPUT); +} + +void Arduino4::device_disable() { + this->msngr->lowcar_printf("ARDUINO 4 DISABLED :("); +} \ No newline at end of file diff --git a/shepherd/lowcar/devices/Arduino4/Arduino4.h b/shepherd/lowcar/devices/Arduino4/Arduino4.h new file mode 100644 index 00000000..5ef38589 --- /dev/null +++ b/shepherd/lowcar/devices/Arduino4/Arduino4.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Device.h" +#include "defs.h" +#include "StatusLED.h" + +/** + * write stuff + */ + +class Arduino4 : public Device { + public: + // construct a Dummy device + Arduino4(); + + virtual size_t device_write(uint8_t param, uint8_t* data_buf); + virtual void device_enable(); + virtual void device_disable(); + + private: + // number of lasers + const static int LASERS_PIN; +}; diff --git a/shepherd/lowcar/devices/Device/Device.cpp b/shepherd/lowcar/devices/Device/Device.cpp new file mode 100644 index 00000000..8840e086 --- /dev/null +++ b/shepherd/lowcar/devices/Device/Device.cpp @@ -0,0 +1,157 @@ +#include "Device.h" + +const float Device::MAX_SUB_INTERVAL_MS = 500.0; // maximum tolerable subscription delay, in ms +const float Device::MIN_SUB_INTERVAL_MS = 40.0; // minimum tolerable subscription delay, in ms + +// Device constructor +Device::Device(DeviceType dev_id, uint8_t dev_year, uint32_t timeout) { + this->dev_id.type = dev_id; + this->dev_id.year = dev_year; + this->dev_id.uid = 0; // sets a temporary value + + this->params = 0; // Initialize to no subscribed parameters (empty DEVICE_DATA) + this->sub_interval = 0; // 0 acts as flag indicating no subscription + this->timeout = timeout; // timeout in ms how long we tolerate not receiving a PING from dev handler + this->enabled = FALSE; + + this->msngr = new Messenger(); + this->led = new StatusLED(); + + // device_enable(); // call device's enable function + + this->last_sent_data_time = this->last_received_ping_time = this->curr_time = millis(); +} + +void Device::set_uid(uint64_t uid) { + this->dev_id.uid = uid; +} + +void Device::loop() { + Status sts; + this->curr_time = millis(); + sts = this->msngr->read_message(&(this->curr_msg)); // try to read a new message + + if (sts == Status::SUCCESS) { // we have a message! + switch (this->curr_msg.message_id) { + case MessageID::PING: + this->last_received_ping_time = this->curr_time; + // If this is the first PING received, send an ACKNOWLEDGEMENT + if (!this->enabled) { + this->msngr->lowcar_printf("Device type %d with UID ending in %X contacted; sending ACK", (uint8_t)this->dev_id.type, this->dev_id.uid); + this->msngr->send_message(MessageID::ACKNOWLEDGEMENT, &(this->curr_msg), &(this->dev_id)); + device_enable(); + this->enabled = TRUE; + } + break; + + case MessageID::SUBSCRIPTION_REQUEST: + this->params = *((uint32_t*) this->curr_msg.payload); // Update subscribed params + this->sub_interval = *((uint16_t*) (this->curr_msg.payload + PARAM_BITMAP_BYTES)); // Update the interval at which we send DEVICE_DATA + // Make sure sub_interval is within bounds + this->sub_interval = min(this->sub_interval, MAX_SUB_INTERVAL_MS); + this->sub_interval = max(this->sub_interval, MIN_SUB_INTERVAL_MS); + break; + + case MessageID::DEVICE_WRITE: + device_write_params(&(this->curr_msg)); + break; + + // Receiving some other Message + default: + this->msngr->lowcar_printf("Unrecognized message received by lowcar device"); + break; + } + } else if (sts != Status::NO_DATA) { + this->msngr->lowcar_printf("Error when reading message by lowcar device\n"); + } + + // If it's been too long since we received a PING, disable the device + if ((this->timeout > 0) && (this->curr_time - this->last_received_ping_time >= this->timeout)) { + device_reset(); + this->enabled = FALSE; + } + + // If we still haven't gotten our first PING yet (or dev handler timed out), keep waiting for a PING + if (!(this->enabled)) { + return; + } + + // do device-specific actions. This may change params + device_actions(); + + /* Send another DEVICE_DATA with subscribed parameters if this->sub_interval + * milliseconds passed since the last time we sent a DEVICE_DATA + * If this->sub_interval == 0, don't send DEVICE_DATA + * Note that it is possible that no parameters are subscribed. + * We send an "empty" DEVICE_DATA anyways to let dev handler know we're still online + */ + if ((this->sub_interval > 0) && (this->curr_time - this->last_sent_data_time >= this->sub_interval)) { + this->last_sent_data_time = this->curr_time; + device_read_params(&(this->curr_msg)); + this->msngr->send_message(MessageID::DEVICE_DATA, &(this->curr_msg)); + } + + // Send any queued logs + this->msngr->lowcar_flush(); +} + +// ******************** DEFAULT DEVICE-SPECIFIC METHODS ********************* // + +size_t Device::device_read(uint8_t param, uint8_t* data_buf) { + return 0; // by default, we read 0 bytes into buffer +} + +size_t Device::device_write(uint8_t param, uint8_t* data_buf) { + return 0; // by default, we wrote 0 bytes successfully to device +} + +void Device::device_enable() { + return; // by default, enabling the device does nothing +} + +void Device::device_reset() { + return; // by default, resetting the device does nothing +} + +void Device::device_actions() { + return; // by default, device does nothing on every loop +} + +// ***************************** HELPER METHODS ***************************** // + +void Device::device_read_params(message_t* msg) { + // Clear the message before building device data + msg->message_id = MessageID::DEVICE_DATA; + msg->payload_length = 0; + memset(msg->payload, 0, MAX_PAYLOAD_SIZE); + + // Read all subscribed params + // Set beginning of payload to subscribed param bitmap + uint32_t* payload_ptr_uint32 = (uint32_t*) msg->payload; + *payload_ptr_uint32 = this->params; + + // Loop over param_bitmap and attempt to read data for subscribed bits + msg->payload_length = PARAM_BITMAP_BYTES; + for (uint8_t param_num = 0; (this->params >> param_num) > 0; param_num++) { + if (this->params & (1 << param_num)) { + msg->payload_length += device_read(param_num, msg->payload + msg->payload_length); + } + } +} + +void Device::device_write_params(message_t* msg) { + if (msg->message_id != MessageID::DEVICE_WRITE) { + return; + } + + // Param bitmap of parameters to write is at the beginning of the payload + uint32_t param_bitmap = *((uint32_t*) msg->payload); + + // Loop over param_bitmap and attempt to write data for requested bits + uint8_t* payload_ptr = msg->payload + PARAM_BITMAP_BYTES; + for (uint32_t param_num = 0; (param_bitmap >> param_num) > 0; param_num++) { + if (param_bitmap & (1 << param_num)) { + payload_ptr += device_write((uint8_t) param_num, payload_ptr); + } + } +} diff --git a/shepherd/lowcar/devices/Device/Device.h b/shepherd/lowcar/devices/Device/Device.h new file mode 100644 index 00000000..c3162842 --- /dev/null +++ b/shepherd/lowcar/devices/Device/Device.h @@ -0,0 +1,128 @@ +/** +* Class defining a lowcar device, receiving/sending messages +* and performing actions based on received messages. +*/ + +#ifndef DEVICE_H +#define DEVICE_H + +#include "Messenger.h" +#include "StatusLED.h" +#include "defs.h" + +class Device { + public: + // ********************** UNIVERSAL DEVICE METHODS ********************** // + + /** + * Constructor with default args + * Calls device_enable to enable the device. + * Arguments: + * dev_type: The type of device (ex: LimitSwitch) + * dev_year: The device year + * timeout: the maximum number of milliseconds to wait for a PING from + * dev handler before disabling + */ + Device(DeviceType dev_type, uint8_t dev_year, uint32_t timeout = 1000); + + // Sets the UID of the Device + void set_uid(uint64_t uid); + + /** + * Generic device loop function that wraps all device actions. + * Asks Messenger to read any incoming messages and responds appropriately. + * Sends DEVICE_DATA at specified interval. + * Sends log messages if any are queued. + * Processes inocming DEVICE_WRITE messages. + * Calls device_actions() to do device-type-specific actions. + */ + void loop(); + + // ********************** DEVICE-SPECIFIC METHODS *********************** // + + /* These functions are meant to be overridden by overridden by each device. + * The default implementations do nothing. + * For example, you don't need to overwrite device_write() for a device that + * has only read-only parameters). + */ + + /** + * Reads the value of a paramter into a buffer. + * Helper function used to build a DEVICE_DATA message. + * Arguments: + * param: The 0-indexed index of the parameter to read + * data_buf: The buffer to read the parameter value into + * Returns: + * the size of the parameter read into the buffer, or + * 0 on failure + */ + virtual size_t device_read(uint8_t param, uint8_t* data_buf); + + /** + * Writes the value of a parameter from a buffer into the device + * Helper function used to process a DEVICE_WRITE message. + * Arguments: + * param: The 0-indexed index of the parameter being written + * data_buf: Buffer containing the parameter value being written + * Returns: + * the size of the parameter that was written, or + * 0 on failure. + */ + virtual size_t device_write(uint8_t param, uint8_t* data_buf); + + /** + * Performs necessary setup for the device to operate. + * Called in the Device constructor + */ + virtual void device_enable(); + + /** + * Performs necessary cleanup to reset the device + * Called when we want to reset device to initial state when first plugged in + * and establishes a connection with dev_handler + */ + virtual void device_reset(); + + /** + * The "main" function of a device, performing continuously updating actions. + * This function SHOULD NOT block. It is called in each iteration of + * Device::loop(). + * It performs continuous actions that the device should do + * For example, the motor controller would move the motors + */ + virtual void device_actions(); + + protected: + Messenger* msngr; // Encodes/decodes and send/receive messages over serial + uint8_t enabled; + + private: + const static float MAX_SUB_INTERVAL_MS; // Maximum tolerable subscription delay, in ms + const static float MIN_SUB_INTERVAL_MS; // Minimum tolerable subscription delay, in ms + + StatusLED* led; // The LED on the Arduino + dev_id_t dev_id; // dev_id of this device determined when flashing + uint32_t params; // Bitmap of parameters subscribed to by dev handler + uint16_t sub_interval; // Time between sending new DEVICE_DATA messages + uint32_t timeout; // Maximum time (ms) we'll wait between PING messages from dev handler + uint64_t last_sent_data_time; // Timestamp of last time we sent DEVICE_DATA due to Subscription + uint64_t last_received_ping_time; // Timestamp of last time we received a PING + uint64_t curr_time; // The current time + message_t curr_msg; // current message being processed + + /** + * Builds a DEVICE_DATA message by reading all subscribed parameters. + * Arguments: + * msg: An empty message to be populated with parameter values ready for sending. + */ + void device_read_params(message_t* msg); + + /** + * Writes to device parameters given a DEVICE_WRITE message. + * Arguments: + * msg: A DEVICE_WRITE message containing parameters to write to the device. + */ + void device_write_params(message_t* msg); +}; + +#endif diff --git a/shepherd/lowcar/devices/Device/Messenger.cpp b/shepherd/lowcar/devices/Device/Messenger.cpp new file mode 100644 index 00000000..a710cf83 --- /dev/null +++ b/shepherd/lowcar/devices/Device/Messenger.cpp @@ -0,0 +1,257 @@ +#include "Messenger.h" + +// ************************ MESSENGER CLASS CONSTANTS *********************** // + +// Final encoded data sizes +const int Messenger::DELIMITER_BYTES = 1; // Bytes of the delimiter +const int Messenger::COBS_LEN_BYTES = 1; // Bytes indicating the size of the cobs encoded message + +// Message sizes +const int Messenger::MESSAGEID_BYTES = 1; // Bytes in message ID field of packet +const int Messenger::PAYLOAD_SIZE_BYTES = 1; // Bytes in payload size field of packet +const int Messenger::CHECKSUM_BYTES = 1; // Bytes in checksum field of packet + +// Sizes for ACKNOWLEDGEMENT message +const int Messenger::DEV_ID_TYPE_BYTES = 1; // Bytes in device type field of dev id +const int Messenger::DEV_ID_YEAR_BYTES = 1; // Bytes in year field of dev id +const int Messenger::DEV_ID_UID_BYTES = 8; // Bytes in uid field of dev id + +// ************************* MESSENGER CLASS METHODS ************************ // + +Messenger::Messenger() { + Serial.begin(115200); // open Serial (USB) connection + + // A queue initialized with room for 10 strings each of size MAX_PAYLOAD_SIZE + this->log_queue_max_size = 10; + this->log_queue = (char**) malloc(sizeof(char*) * this->log_queue_max_size); + for (int i = 0; i < this->log_queue_max_size; i++) { + this->log_queue[i] = (char*) malloc(sizeof(char) * MAX_PAYLOAD_SIZE); + } + this->num_logs = 0; +} + +Status Messenger::send_message(MessageID msg_id, message_t* msg, dev_id_t* dev_id) { + // Fill MessageID field + msg->message_id = msg_id; + delay(10); + /* + * Build the message + * All other Message Types (DEVICE_DATA, LOG) should already be built (if needed) + */ + if (msg_id == MessageID::ACKNOWLEDGEMENT) { + int status = 0; + status += append_payload(msg, (uint8_t*) &dev_id->type, Messenger::DEV_ID_TYPE_BYTES); + status += append_payload(msg, (uint8_t*) &dev_id->year, Messenger::DEV_ID_YEAR_BYTES); + status += append_payload(msg, (uint8_t*) &dev_id->uid, Messenger::DEV_ID_UID_BYTES); + + if (status != 0) { + return Status::PROCESS_ERROR; + } + } + + // Serialize the message into byte array + size_t msg_len = Messenger::MESSAGEID_BYTES + Messenger::PAYLOAD_SIZE_BYTES + msg->payload_length + Messenger::CHECKSUM_BYTES; + uint8_t data[msg_len]; + message_to_byte(data, msg); + data[msg_len - Messenger::CHECKSUM_BYTES] = checksum(data, msg_len - Messenger::CHECKSUM_BYTES); // put the checksum into data + + // Cobs encode the byte array + uint8_t cobs_buf[Messenger::DELIMITER_BYTES + Messenger::COBS_LEN_BYTES + msg_len + 1]; // Cobs encoding adds at most 1 byte overhead + cobs_buf[0] = 0x00; // Start with the delimiter + size_t cobs_len = cobs_encode(&cobs_buf[2], data, msg_len); + cobs_buf[1] = (byte) cobs_len; + + // Write to serial + uint8_t written = Serial.write(cobs_buf, Messenger::DELIMITER_BYTES + Messenger::COBS_LEN_BYTES + cobs_len); + + // Clear the message for the next send + msg->message_id = MessageID::NOP; + msg->payload_length = 0; + memset(msg->payload, 0, MAX_PAYLOAD_SIZE); + + return (written == Messenger::DELIMITER_BYTES + Messenger::COBS_LEN_BYTES + cobs_len) ? Status::SUCCESS : Status::PROCESS_ERROR; +} + +Status Messenger::read_message(message_t* msg) { + // Check if there's something to read + if (!Serial.available()) { + return Status::NO_DATA; + } + delay(10); + // Find the start of the packet (the delimiter) + int last_byte_read = -1; + while (Serial.available()) { + last_byte_read = Serial.read(); + if (last_byte_read == 0) { // Byte 0x00 is the delimiter + break; + } + } + + if (last_byte_read != 0) { // no start of packet found + return Status::MALFORMED_DATA; + } + if (Serial.available() == 0 || Serial.peek() == 0) { // no packet length found + return Status::MALFORMED_DATA; + } + + // Read the cobs len (how many bytes left in the message) + size_t cobs_len = Serial.read(); + + // Read the rest of the message into buffer + uint8_t cobs_buf[cobs_len]; + size_t read_len = Serial.readBytesUntil(0x00, (char*) cobs_buf, cobs_len); // Read cobs_len bytes or until the next delimiter (whichever is first) + if (cobs_len != read_len) { + return Status::PROCESS_ERROR; + } + + // Decode the cobs-encoded message into a buffer + uint8_t data[cobs_len]; // The decoded message will be smaller than COBS_LEN + cobs_decode(data, cobs_buf, cobs_len); + + uint8_t message_id = data[0]; + uint8_t payload_length = data[1]; + + // Verify that the received message has the correct checksum + uint8_t expected_chk = checksum(data, Messenger::MESSAGEID_BYTES + Messenger::PAYLOAD_SIZE_BYTES + payload_length); + uint8_t received_chk = data[Messenger::MESSAGEID_BYTES + Messenger::PAYLOAD_SIZE_BYTES + payload_length]; + if (received_chk != expected_chk) { + return Status::MALFORMED_DATA; + } + + // Populate MSG with received data + msg->message_id = (MessageID) message_id; + msg->payload_length = payload_length; + memcpy(msg->payload, &data[Messenger::MESSAGEID_BYTES + Messenger::PAYLOAD_SIZE_BYTES], payload_length); + return Status::SUCCESS; +} + +void Messenger::lowcar_printf(char* format, ...) { + // Double the queue size if it's full + if (this->num_logs == this->log_queue_max_size) { + this->log_queue = (char**) realloc(this->log_queue, sizeof(char*) * 2 * this->log_queue_max_size); + for (int i = this->log_queue_max_size; i < 2 * this->log_queue_max_size; i++) { + this->log_queue[i] = (char*) malloc(sizeof(char) * MAX_PAYLOAD_SIZE); + } + this->log_queue_max_size *= 2; + } + // Add the new formatted log to the queue + va_list args; + va_start(args, format); + vsprintf(this->log_queue[this->num_logs], format, args); + va_end(args); + // Increment the number of logs + this->num_logs++; +} + +void Messenger::lowcar_print_float(char* name, float val) { + char sign = (val < 0.0) ? '-' : ' '; // Holds sign of the value + if (val < 0.0) { + val *= -1.0; + } + int whole_val = (int) val; // Part of val to the left of the decimal point + int thousandths = (int) ((val - whole_val) * 1000); // The 3 digits to the right of the decimal point + + if (sign == '-') { + this->lowcar_printf("%s: %c%d.%03d", name, sign, whole_val, thousandths); + } else { + this->lowcar_printf("%s:%c%d.%03d", name, sign, whole_val, thousandths); + } +} + +void Messenger::lowcar_flush() { + // don't send anything if no logs + if (this->num_logs == 0) { + return; + } + + message_t log; + // For each log, send a new message + for (int i = 0; i < this->num_logs; i++) { + log.payload_length = strlen(this->log_queue[i]) + 1; // Null terminator character + + // Copy string into payload + memcpy((char*) log.payload, this->log_queue[i], (size_t) log.payload_length); + this->send_message(MessageID::LOG, &log); + } + // "Clear" the queue + this->num_logs = 0; +} + +// ***************************** HELPER METHODS ***************************** // + +int Messenger::append_payload(message_t* msg, uint8_t* data, uint8_t length) { + if (msg->payload_length + length > MAX_PAYLOAD_SIZE) { + return -1; + } + memcpy(&(msg->payload[msg->payload_length]), data, length); + msg->payload_length += length; + return 0; +} + +void Messenger::message_to_byte(uint8_t* data, message_t* msg) { + data[0] = (uint8_t) msg->message_id; + data[1] = msg->payload_length; + memcpy(&data[2], msg->payload, msg->payload_length); +} + +uint8_t Messenger::checksum(uint8_t* data, int length) { + uint8_t chk = data[0]; + for (int i = 1; i < length; i++) { + chk ^= data[i]; + } + return chk; +} + +// ***************************** COBS ENCODING ****************************** // + +#define finish_block() \ + { \ + *block_len_loc = block_len; \ + block_len_loc = dst++; \ + out_len++; \ + block_len = 0x01; \ + } + +size_t Messenger::cobs_encode(uint8_t* dst, const uint8_t* src, size_t src_len) { + const uint8_t* end = src + src_len; + uint8_t* block_len_loc = dst++; + uint8_t block_len = 0x01; + size_t out_len = 0; + + while (src < end) { + if (*src == 0) { + finish_block(); + } else { + *dst++ = *src; + block_len++; + out_len++; + if (block_len == 0xFF) { + finish_block(); + } + } + src++; + } + finish_block(); + return out_len; +} + +size_t Messenger::cobs_decode(uint8_t* dst, const uint8_t* src, size_t src_len) { + const uint8_t* end = src + src_len; + size_t out_len = 0; + + while (src < end) { + uint8_t code = *src++; + for (uint8_t i = 1; i < code; i++) { + *dst++ = *src++; + out_len++; + if (src > end) { // Bad packet + return 0; + } + } + if (code < 0xFF && src != end) { + *dst++ = 0; + out_len++; + } + } + return out_len; +} diff --git a/shepherd/lowcar/devices/Device/Messenger.h b/shepherd/lowcar/devices/Device/Messenger.h new file mode 100644 index 00000000..2b50c99e --- /dev/null +++ b/shepherd/lowcar/devices/Device/Messenger.h @@ -0,0 +1,141 @@ +/** + * A class used by each device to send and receive messages to and from the device handler + */ + +#ifndef MESSENGER_H +#define MESSENGER_H + +#include // va_start, va_list, va_arg, va_end +#include "defs.h" + +class Messenger { + public: + /* constructor; opens Serial connection */ + Messenger(); + + /** + * Handles any type of message and fills in appropriate parameters, then sends onto Serial port + * Clears all of MSG's fields before returning. + * Arguments: + * msg_id: The MessageID to populate msg->message_id + * msg: The message to send + * dev_id: The id of the device. Used for only ACKNOWLEDGEMENT messages + * Returns: + * a Status enum to report on success/failure + */ + Status send_message(MessageID msg_id, message_t* msg, dev_id_t* dev_id = NULL); + + /** + * Reads in data from serial port and puts results into msg + * Arguments: + * msg: A message to be populated based on read serial data. + * Returns: + * a Status enum to report on success/failure + */ + Status read_message(message_t* msg); + + // ****************************** LOGGING ******************************* // + + /** + * Queues a LOG message to be sent to dev handler (for runtime logger) + * Arguments: + * format: The string format to be sent (same usage as printf()) + */ + void lowcar_printf(char* format, ...); + + /** + * Workaround to print a float value. + * Use this function to print the value of a float instead of %f in lowcar_printf() + * This is needed because floats appear as question marks in the logger + * Sends a log with the string: ": " + * Where val is displayed up to the thousandths place + * Arguments: + * name: A string describing the value of the float (ex: the variable name) + * val: The float to be printed alongside NAME + */ + void lowcar_print_float(char* name, float val); + + /** + * Sends all currently queued logs to dev handler + * The log queue is then cleared. + */ + void lowcar_flush(); + + private: + // protocol constants + const static int DELIMITER_BYTES; // The size of a packet delimiter + const static int COBS_LEN_BYTES; // The size the cobs length in a packet + + const static int MESSAGEID_BYTES; // bytes in message ID field of packet + const static int PAYLOAD_SIZE_BYTES; // bytes in payload size field of packet + const static int CHECKSUM_BYTES; // bytes in checksum field of packet + + const static int DEV_ID_TYPE_BYTES; // bytes in device type field of dev_id + const static int DEV_ID_YEAR_BYTES; // bytes in year field of dev_id + const static int DEV_ID_UID_BYTES; // bytes in uid field of dev_id + + // private variables + uint8_t log_queue_max_size; // The size of the log queue in bytes + char** log_queue; // The log queue + uint8_t num_logs; // The number of logs in the log queue + + // *************************** HELPER METHODS *************************** // + + /** + * Appends data to the payload of a message + * The payload length field of the message is incremented accordingly. + * Arguments: + * msg: The message whose payload is to be appended to + * data: The buffer containing data to append to msg->payload + * length: The size of DATA + * Returns: + * 0 on success, or + * -1 if LENGTH is too large (DATA can't fit in the allocated payload) + */ + int append_payload(message_t* msg, uint8_t* data, uint8_t length); + + /** + * Serializes a message into a byte packet to be sent over serial. + * Arguments: + * data: The buffer to write the serialized message into + * msg: A populated message to be serialized. + */ + void message_to_byte(uint8_t* data, message_t* msg); + + /** + * Computes the checksum of a data buffer. + * The checksum is the bitwise XOR of each byte in the buffer. + * Arguments: + * data: The data buffer whose checksum is to be computed. + * length: the size of DATA + * Returns: + * the checksum + */ + uint8_t checksum(uint8_t* data, int length); + + // **************************** COBS ENCODING *************************** // + + /** + * Cobs encodes a byte array into a buffer + * Arguments: + * src: The byte array to be encoded + * dst: The buffer to write the encoded data into + * src_len: The size of SRC + * Returns: + * The size of the encoded data, DST + */ + size_t cobs_encode(uint8_t* dst, const uint8_t* src, size_t src_len); + + /** + * Cobs decodes a byte array into a buffer + * Arguments: + * src: The byte array to be decoded + * dst: The buffer to write the decoded data into + * src_len: The size of SRC + * Returns: + * The size of the decoded data, DST + */ + size_t cobs_decode(uint8_t* dst, const uint8_t* src, size_t src_len); +}; + +#endif diff --git a/shepherd/lowcar/devices/Device/StatusLED.cpp b/shepherd/lowcar/devices/Device/StatusLED.cpp new file mode 100644 index 00000000..7524dbff --- /dev/null +++ b/shepherd/lowcar/devices/Device/StatusLED.cpp @@ -0,0 +1,34 @@ +#include "StatusLED.h" + +#define QUICK_TIME 300 // Feel free to change this +// Morse code ratios +#define SLOW_TIME (QUICK_TIME * 3) +#define PAUSE_TIME QUICK_TIME + +StatusLED::StatusLED() { + pinMode(StatusLED::LED_PIN, OUTPUT); + digitalWrite(StatusLED::LED_PIN, LOW); + led_enabled = false; +} + +void StatusLED::toggle() { + digitalWrite(StatusLED::LED_PIN, led_enabled ? LOW : HIGH); + led_enabled = !led_enabled; +} + +void StatusLED::quick_blink(int num) { + this->blink(num, QUICK_TIME, PAUSE_TIME); +} + +void StatusLED::slow_blink(int num) { + this->blink(num, SLOW_TIME, PAUSE_TIME); +} + +void StatusLED::blink(int num, int ms, int space) { + for (int i = 0; i < num; i++) { + this->toggle(); + delay(ms); + this->toggle(); + delay(space); + } +} diff --git a/shepherd/lowcar/devices/Device/StatusLED.h b/shepherd/lowcar/devices/Device/StatusLED.h new file mode 100644 index 00000000..1b4b39b9 --- /dev/null +++ b/shepherd/lowcar/devices/Device/StatusLED.h @@ -0,0 +1,32 @@ +/** + * A class to handle whether to turn on or off the LED on the Arduino. + */ + +#ifndef STATUSLED_H +#define STATUSLED_H + +#include "defs.h" + +class StatusLED { + public: + // sets up the status LED and configures the pin + StatusLED(); + + // toggles LED between on and off + void toggle(); + + // Blinks quickly NUM times + void quick_blink(int num); + + // Blink slowly NUM times + void slow_blink(int num); + + private: + const static int LED_PIN = LED_BUILTIN; // pin to control the LED + bool led_enabled; // keeps track of whether LED is on or off + + // Blinks NUM times, waiting MS ms between the toggles, and SPACE ms between each blink + void blink(int num, int ms, int space); +}; + +#endif diff --git a/shepherd/lowcar/devices/Device/defs.h b/shepherd/lowcar/devices/Device/defs.h new file mode 100644 index 00000000..5ba6773f --- /dev/null +++ b/shepherd/lowcar/devices/Device/defs.h @@ -0,0 +1,90 @@ +#ifndef DEFS_H +#define DEFS_H + +#include +#include "Arduino.h" + +// The maximum number of parameters for a lowcar device +#define MAX_PARAMS 32 +// The size of the param bitmap used in various messages (8 bits in a byte) +#define PARAM_BITMAP_BYTES (MAX_PARAMS / 8) + +// Maximum size of a message payload +// achieved with a DEVICE_WRITE/DEVICE_DATA of MAX_PARAMS of all floats +#define MAX_PAYLOAD_SIZE (PARAM_BITMAP_BYTES + (MAX_PARAMS * sizeof(float))) + +// Use these with uint8_t instead of `bool` with `true` and `false` +// This makes device_read() and device_write() cleaner when parsing on C +#define TRUE 1 +#define FALSE 0 + +// identification for analog pins +enum class Analog : uint8_t { + IO0 = A0, + IO1 = A1, + IO2 = A2, + IO3 = A3, + IO4 = A4, + IO5 = A5 +}; + +// identification for digital pins +enum class Digital : uint8_t { + IO6 = 2, + IO7 = 3, + IO8 = 4, + IO9 = 5, + IO10 = 6, + IO11 = 7, + I012 = 8, + I013 = 9, + I014 = 10, + I015 = 11, + I016 = 12, + IO17 = 13 +}; + +/* The types of messages */ +enum class MessageID : uint8_t { + NOP = 0x00, // Dummy message + PING = 0x01, // To lowcar + ACKNOWLEDGEMENT = 0x02, // To dev handler + SUBSCRIPTION_REQUEST = 0x03, // To lowcar + DEVICE_WRITE = 0x04, // To lowcar + DEVICE_DATA = 0x05, // To dev handler + LOG = 0x06 // To dev handler +}; + +// identification for device types +enum class DeviceType : uint8_t { + DUMMY_DEVICE = 0x00, + ARDUINO1 = 0x01, + ARDUINO2 = 0x02, + ARDUINO3 = 0x03, + ARDUINO4 = 0x04, + // DISTANCE_SENSOR = 0x07 Uncomment when implemented +}; + +// identification for resulting status types +enum class Status { + SUCCESS, + PROCESS_ERROR, + MALFORMED_DATA, + NO_DATA +}; + +// decoded lowcar packet +typedef struct { + MessageID message_id; + uint8_t payload_length; + uint8_t payload[MAX_PAYLOAD_SIZE]; +} message_t; + +// unique id struct for a specific device +typedef struct { + DeviceType type; + uint8_t year; + uint64_t uid; +} dev_id_t; + +#endif diff --git a/shepherd/lowcar/devices/DummyDevice/DummyDevice.cpp b/shepherd/lowcar/devices/DummyDevice/DummyDevice.cpp new file mode 100644 index 00000000..1dcea026 --- /dev/null +++ b/shepherd/lowcar/devices/DummyDevice/DummyDevice.cpp @@ -0,0 +1,214 @@ +#include "DummyDevice.h" + +// The numbering of each parameter +typedef enum { + RUNTIME = 0, + SHEPHERD = 1, + DAWN = 2, + DEVOPS = 3, + ATLAS = 4, + INFRA = 5, + SENS = 6, + PDB = 7, + MECH = 8, + CPR = 9, + EDU = 10, + EXEC = 11, + PIEF = 12, + FUNTIME = 13, + SHEEP = 14, + DUSK = 15, +} param; + +DummyDevice::DummyDevice() : Device(DeviceType::DUMMY_DEVICE, 13) { + this->runtime = 1; + this->shepherd = 0.1; + this->dawn = TRUE; + + this->devops = 1; + this->atlas = 0.1; + this->infra = TRUE; + + this->sens = 0; + this->pdb = -0.1; + this->mech = FALSE; + + this->cpr = 0; + this->edu = 0.0; + this->exec = FALSE; + + this->pief = 0; + this->funtime = -0.1; + this->sheep = FALSE; + + this->dusk = 0; +} + +size_t DummyDevice::device_read(uint8_t param, uint8_t* data_buf) { + float* float_buf = (float*) data_buf; + int32_t* int_buf = (int32_t*) data_buf; + + switch (param) { + case RUNTIME: + int_buf[0] = this->runtime; + return sizeof(this->runtime); + + case SHEPHERD: + float_buf[0] = this->shepherd; + return sizeof(this->shepherd); + + case DAWN: + data_buf[0] = this->dawn; + return sizeof(this->dawn); + + case DEVOPS: + int_buf[0] = this->devops; + return sizeof(this->devops); + + case ATLAS: + float_buf[0] = this->atlas; + return sizeof(this->atlas); + + case INFRA: + data_buf[0] = this->infra; + return sizeof(this->infra); + + case SENS: + break; + + case PDB: + break; + + case MECH: + break; + + case CPR: + break; + + case EDU: + break; + + case EXEC: + break; + + case PIEF: + int_buf[0] = this->pief; + return sizeof(this->pief); + + case FUNTIME: + float_buf[0] = this->funtime; + return sizeof(this->funtime); + + case SHEEP: + data_buf[0] = this->sheep; + return sizeof(this->sheep); + + case DUSK: + int_buf[0] = this->dusk; + return sizeof(this->dusk); + } + return 0; +} + +size_t DummyDevice::device_write(uint8_t param, uint8_t* data_buf) { + switch (param) { + case RUNTIME: + break; + + case SHEPHERD: + break; + + case DAWN: + break; + + case DEVOPS: + break; + + case ATLAS: + break; + + case INFRA: + break; + + case SENS: + this->sens = ((int32_t*) data_buf)[0]; + return sizeof(this->sens); + + case PDB: + this->pdb = ((float*) data_buf)[0]; + return sizeof(this->pdb); + + case MECH: + this->mech = data_buf[0]; + return sizeof(this->mech); + + case CPR: + this->cpr = ((int32_t*) data_buf)[0]; + return sizeof(this->cpr); + + case EDU: + this->edu = ((float*) data_buf)[0]; + return sizeof(this->edu); + + case EXEC: + this->exec = data_buf[0]; + return sizeof(this->exec); + + case PIEF: + this->pief = ((int32_t*) data_buf)[0]; + return sizeof(this->pief); + + case FUNTIME: + this->funtime = ((float*) data_buf)[0]; + return sizeof(this->funtime); + + case SHEEP: + this->sheep = data_buf[0]; + return sizeof(this->sheep); + + case DUSK: + this->dusk = ((int32_t*) data_buf)[0]; + return sizeof(this->dusk); + } + return 0; +} + +void DummyDevice::device_enable() { + this->msngr->lowcar_printf("DUMMY DEVICE ENABLED"); +} + +void DummyDevice::device_disable() { + this->msngr->lowcar_printf("DUMMY DEVICE DISABLED"); +} + +void DummyDevice::device_actions() { + static uint64_t last_update_time = 0; + // static uint64_t last_count_time = 0; + uint64_t curr = millis(); + + // Simulate read-only params changing + if (curr - last_update_time > 500) { + this->runtime += 2; + this->shepherd += 1.9; + this->dawn = !this->dawn; + + this->devops++; + this->atlas += 0.9; + this->infra = TRUE; + + // this->msngr->lowcar_print_float("funtime", this->funtime); + // this->msngr->lowcar_print_float("atlas", this->atlas); + // this->msngr->lowcar_print_float("pdb", this->pdb); + + last_update_time = curr; + } + + // this->dusk++; + // + // //use dusk as a counter to see how fast this loop is going + // if (curr - last_count_time > 1000) { + // last_count_time = curr; + // this->msngr->lowcar_printf("loops in past second: %d", this->dusk); + // this->dusk = 0; + // } +} diff --git a/shepherd/lowcar/devices/DummyDevice/DummyDevice.h b/shepherd/lowcar/devices/DummyDevice/DummyDevice.h new file mode 100644 index 00000000..05b0dba3 --- /dev/null +++ b/shepherd/lowcar/devices/DummyDevice/DummyDevice.h @@ -0,0 +1,51 @@ +#ifndef DUMMY_H +#define DUMMY_H + +#include "Device.h" +#include "defs.h" + +/** + * DOES NOT REPRESENT ANY ACTUAL DEVICE + * Use to exercise the full extent of possible params and + * functionality of a lowcar device from runtime + * Flashable onto bare Arduino micro for testing + */ + +class DummyDevice : public Device { + public: + // construct a Dummy device + DummyDevice(); + + virtual size_t device_read(uint8_t param, uint8_t* data_buf); + virtual size_t device_write(uint8_t param, uint8_t* data_buf); + virtual void device_enable(); + virtual void device_disable(); + + // Changes several of the readable params for testing + virtual void device_actions(); + + private: + int32_t runtime; //param 0 + float shepherd; + uint8_t dawn; + + int32_t devops; //param 3 + float atlas; + uint8_t infra; + + int32_t sens; //param 6 + float pdb; + uint8_t mech; + + int32_t cpr; //param 9 + float edu; + uint8_t exec; + + int32_t pief; //param 12 + float funtime; + uint8_t sheep; + + int32_t dusk; //param 15 +}; + +#endif diff --git a/shepherd/lowcar/flash.sh b/shepherd/lowcar/flash.sh new file mode 100755 index 00000000..eab66936 --- /dev/null +++ b/shepherd/lowcar/flash.sh @@ -0,0 +1,348 @@ +#!/bin/bash + +# (I apologize to anyone who needs to edit this in the future.... bash is really stupid sometimes and cryptic) +# Some of the sequences of commands could be shortened or abbreviated but I tried to make it as easy to undersatnd as possible +# +# Useful links for understanding this file: +# arduino-cli: +# https://arduino.github.io/arduino-cli/getting-started/ +# awk: +# https://opensource.com/article/19/11/how-regular-expressions-awk +# https://linuxhint.com/20_awk_examples/ +# https://www.howtogeek.com/562941/how-to-use-the-awk-command-on-linux/ +# bash: +# https://linuxconfig.org/bash-scripting-tutorial +# https://tldp.org/LDP/abs/html/string-manipulation.html +# https://unix.stackexchange.com/questions/306111/what-is-the-difference-between-the-bash-operators-vs-vs-vs +# getopts: +# https://stackoverflow.com/questions/16483119/an-example-of-how-to-use-getopts-in-bash +# https://www.mkssoftware.com/docs/man1/getopts.1.asp +# od: +# https://serverfault.com/questions/283294/how-to-read-in-n-random-characters-from-dev-urandom +# sed: +# https://www.grymoire.com/Unix/Sed.html + +####################################### GLOBAL VARIABLES #################################### + +DEVICE_NAME="empty" # DummyDevice, YogiBear, etc. +DEVICE_PORT="empty" # port that board is connected on +DEVICE_FQBN="empty" # fully qualified board name, ex. arduino:avr:micro +DEVICE_CORE="empty" # core for connected board, ex. arduino:avr +LIB_INSTALL_DIR="empty" # installation directory for libraries +uid="empty" # UID of device (lowercase because capital UID is taken by system) +CLEAN=0 # whether or not the clean option was specified + +####################################### UTILITY FUNCTIONS ################################### + +# prints the usage of this flash script +function usage { + printf "Usage: flash [-cht] [-s UID] device_type\n" + printf "\t-c: short for \"Clean\"; removes the \"Device\" folder and all symbolic links\n" + printf "\t-h: short for \"Help\": displays this help message\n" + printf "\t-s: short for \"Set\"; manually set the UID for the device (input taken in hex, do not prefix \"0x\")\n" + printf "\t-t: short for \"Test\"; set the UID for the device to \"0x123456789ABCDEF\"\n\n" + printf "Examples:\n" + printf "\tTo flash a DummyDevice: flash DummyDevice\n" + printf "\tTo flash a DummyDevice specifying the UID \"ABCDEF\": flash -s ABCDEF DummyDevice\n" + printf "\tTo flash a DummyDevice with the test UID \"123456789ABCDEF\": flash -t DummyDevice\n" + printf "\tTo clean up directories without flashing anything: flash -c\n\n" + printf "Valid values for \"device type\" are listed below:\n\n" + + for type in $DEVICE_TYPES; do + printf "\t$type\n" + done + printf "\n" + + printf "Remember that you need to install arduino-cli first before you can use this flash script!\n\n" + exit 1 +} + +# checks whether a device is valid; if so, put it in DEVICE_NAME +function check_device { + # check if user specified a device type + if [[ $1 == "" ]]; then + printf "\nERROR: Must specify a device type\n\n" + usage + exit 1 + fi + + # check if device is valid + for device in ${DEVICE_TYPES}; do + if [[ $1 == $device ]]; then + DEVICE_NAME=$device + return 0 + fi + done + + printf "\nERROR: Invalid device type: $1\n\n" + usage + exit 1 +} + +# parse command-line options +function parse_opts { + # process the command-line arguments + while getopts "s:cth" opt; do + case $opt in + s) + # check to make sure that s flag was specified with hexadecimal digits + if [[ $OPTARG =~ [:xdigit:] ]]; then + printf "\nERROR: Specified invalid UID for -s flag: \"$uid\"\n\n" + usage + fi + uid="0x$OPTARG" + ;; + t) + uid="0x123456789ABCDEF" + ;; + c) + # check to make sure user didn't specify more arguments after -c flag + if (( $OPTIND <= $# )); then + printf "\nERROR: -c flag does not take any arguments\n\n" + usage + fi + CLEAN=1 + return 0 + ;; + *) + usage + ;; + esac + done + shift $(( $OPTIND - 1 )) # shift the command-line arguments so that $1 refers to the device type + + check_device $1 # check that we have a valid device + + return 0 +} + +# obtains the port, fqbn, and core of the attached arduino +function get_board_info { + local line_num=0 + + # read lines from board list and searches for the attached board + while read line; do + line_num=$(( line_num + 1 )) + + # if first line, or line contains "Unknown" or is blank, then skip + if [[ $line_num == 1 || $line == *"Unknown"* || $line == "" ]]; then + continue + fi + + # we found the board! now fill in the global variables + DEVICE_PORT=$(echo $line | awk '{ print $1 }') + DEVICE_FQBN=$(echo $line | awk '{ + for (i=1; i<=NF; i++) { + tmp=match($i, /arduino:(avr|samd):.*/) + if (tmp) { + print $i + } + } + }') + DEVICE_CORE=$(echo $line | awk '{ + for (i=1; i<=NF; i++) { + tmp=match($i, /arduino:[^:]*$/) + if (tmp) { + print $i + } + } + }') + + printf "\nFound board! \n\t Port = $DEVICE_PORT \n\t FQBN = $DEVICE_FQBN \n\t Core = $DEVICE_CORE\n\n" + + return 0 + + done <<< "$(arduino-cli board list)" + + # if we go through the entire output of arduino-cli board list, then no suitable board attached + printf "\nERROR: no board attached\n\n" + exit 1 +} + +# get the location that we need to symlink our code from so that arduino-cli can find them +function get_lib_install_dir { + + # read in config line by line until we find the "user: LIB_INSTALL_DIR" line + while read line; do + if [[ $line == "user"* ]]; then + LIB_INSTALL_DIR=$(echo $line | awk '{ print $2 }') + LIB_INSTALL_DIR="$LIB_INSTALL_DIR/libraries" # append /libraries to it + mkdir -p $LIB_INSTALL_DIR # Make library directory if it doesn't exist + printf "\nFound Library Installation Directory: $LIB_INSTALL_DIR\n" + return 1 + fi + done <<< "$(arduino-cli config dump)" + + # if we get here, we didn't find the library installation directory + printf "\nERROR: could not find Library Installation Directory\n\n" + exit 1 +} + +# create the symbolic links from the library installation directory to our code +# "$PWD" is set by the system and contains the output of the "pwd" command in the terminal +function symlink_libs { + + # first get names of folders to symlink in libs_to_install and devices_libs_to_install + local libs_to_install=$(ls lib) + local devices_to_install=$(ls devices) + printf "\nCreating symbolic links...\n\n" + + # process each element in libs_to_install + for lib in $libs_to_install; do + # now check if it already exists in LIB_INSTALL_DIR + if [[ -L "$LIB_INSTALL_DIR/$lib" ]]; then + continue + fi + + printf "Creating symbolic link for $lib\n" + ln -s "$PWD/lib/$lib" "$LIB_INSTALL_DIR/$lib" + done + + # process each element in devices_to_install + for lib in $devices_to_install; do + # now check if it already exists in LIB_INSTALL_DIR + if [[ -L "$LIB_INSTALL_DIR/$lib" ]]; then + continue + fi + + printf "Creating symbolic link for $lib\n" + ln -s "$PWD/devices/$lib" "$LIB_INSTALL_DIR/$lib" + done + + printf "\nFinished creating symbolic links to libraries!\n" +} + +# sets the UID of this device and generates the finished Arduino sketch code +function make_device_ino { + # if UID hasn't been set yet, generate a random one + if [[ $uid == "empty" ]]; then + printf "\nGenerating random UID\n" + + # get the random 64-bit UID + if [[ -e "/dev/urandom" ]]; then + uid=$(od -tu8 -An -N8 /dev/urandom) # get 8 bytes of random data from /dev/urandom + else + uid=$(od -tu8 -An -N8 /dev/random) # get 8 bytes of random data from /dev/random + fi + + # this trims the whitespace surrounding rand so that generated file looks nicer + uid="$(echo $uid | tr -d '[:space:]')" + fi + + printf "UID for this device is $uid\n" + printf "Generating Device/Device.ino\n" + + # if Device folder doesn't exist, make it + mkdir -p Device + + # create Device/Device.ino with proper header + echo "#include <$DEVICE_NAME.h>" > Device/Device.ino + # generates new Device_template file with UID_INSERTED and DEVICE replaced and appends to existing Device.ino + sed -e "s/UID_INSERTED/$uid/" -e "s/DEVICE/$DEVICE_NAME/" Device_template.cpp >> Device/Device.ino + + printf "Ready to attempt compiling!\n" +} + +# removes the Device folder and all symbolic links +function clean { + # get the location of library install + get_lib_install_dir + + # get the names of libraries to remove + local libs_to_clean="$(ls lib) $(ls devices)" + + # remove Device if it exists + if [[ -d "Device" ]]; then + printf "Removing Device\n" + rm -r Device + fi + + # process each element in libs_to_clean + for lib in $libs_to_clean; do + # now check if it already exists in LIB_INSTALL_DIR + if [[ -L "$LIB_INSTALL_DIR/$lib" ]]; then + printf "Removing symbolic link for $lib\n" + rm "$LIB_INSTALL_DIR/$lib" + fi + + done + + printf "Finished cleaning!\n\n" +} + +####################################### BEGIN SCRIPT ################################### + +# This script should only be called using `runtime flash` which will have its CWD be runtime/. We need to be in the lowcar/ folder. +# cd lowcar +# line above is commented out because script is already in the lowcar folder + +# Get all valid device types +DEVICE_TYPES=$(ls devices | awk '{ + for (i=1; i <= NF; i++) { + tmp=match($i, /^Device$/) + if (!tmp) { + print $i + } + } + }' +) + +# check if arduino-cli is installed +if [[ $(which arduino-cli) == "" ]]; then + printf "\nERROR: Could not find arduino-cli. Please install arduino-cli\n\n" + usage + exit 1 +fi + +# parse command-line options +parse_opts $@ + +# Clean symlinked libraries +clean + +# if CLEAN flag was specified, we are done +if [[ $CLEAN == 1 ]]; then + exit 1 +fi + +printf "\nAttempting to flash a $DEVICE_NAME!\n\n" + +# create the init file (see arduino-cli documentation) +arduino-cli config init + +# update the core index (see arduino-cli documentation) +arduino-cli core update-index + +# get the device port, board, fqbn, and core +get_board_info + +# install this device's core +arduino-cli core install $DEVICE_CORE + +# symlink libraries +get_lib_install_dir +symlink_libs + +# insert uid and requested device into Device_template, copy into Device/Device.ino +make_device_ino + +# compile the code +printf "\nCompiling code...\n\n" +if !(arduino-cli compile --fqbn $DEVICE_FQBN Device); then + printf "\nERROR: code did not compile correctly\n\n" + exit 1 +fi +printf "\nFinished compiling code!\n" + +# upload the code to board +printf "\nUploading code to board...\n\n" +if !(arduino-cli upload -p $DEVICE_PORT --fqbn $DEVICE_FQBN Device); then + printf "\nERROR: code did not upload correctly\n\n" + exit 1 +fi +printf "Finished uploading code!\n" + +# done! +printf "\nSuccessfully flashed a $DEVICE_NAME!\n\n" + +exit 0 diff --git a/shepherd/lowcar/test_color_linebreak_code.cpp b/shepherd/lowcar/test_color_linebreak_code.cpp new file mode 100644 index 00000000..8294ec42 --- /dev/null +++ b/shepherd/lowcar/test_color_linebreak_code.cpp @@ -0,0 +1,32 @@ +// TCS230 or TCS3200 pins wiring to Arduino +#define S0 2 +#define S1 3 +#define S2 4 +#define S3 5 +#define sensorOut 2 +// Stores frequency read by the photodiodes +int redFrequency = 0; +void setup() { + // Setting the outputs + pinMode(S0, OUTPUT); + pinMode(S1, OUTPUT); + pinMode(S2, OUTPUT); + pinMode(S3, OUTPUT); + // Setting the sensorOut as an input + pinMode(sensorOut, INPUT); + // Setting frequency scaling to 20% + digitalWrite(S0,HIGH); + digitalWrite(S1,LOW); + // Begins serial communication + Serial.begin(9600); +} +void loop() { + // Setting RED (R) filtered photodiodes to be read + digitalWrite(S2,LOW); + digitalWrite(S3,LOW); + // Reading the output frequency + redFrequency = pulseIn(sensorOut, LOW); + // Printing the RED (R) value + Serial.print(redFrequency); + delay(100); +} \ No newline at end of file diff --git a/shepherd/lowcar/test_linebreak_code.cpp b/shepherd/lowcar/test_linebreak_code.cpp new file mode 100644 index 00000000..237b3c98 --- /dev/null +++ b/shepherd/lowcar/test_linebreak_code.cpp @@ -0,0 +1,50 @@ +/* + IR Breakbeam sensor demo! +*/ + +#define LEDPIN 2 + // Pin 13: Arduino has an LED connected on pin 13 + // Pin 11: Teensy 2.0 has the LED on pin 11 + // Pin 6: Teensy++ 2.0 has the LED on pin 6 + // Pin 13: Teensy 3.0 has the LED on pin 13 + +#define SENSORPIN 13 + +// variables will change: +int sensorState = 0, lastState=0; // variable for reading the pushbutton status + +void setup() { + // initialize the LED pin as an output: + pinMode(LEDPIN, OUTPUT); + // initialize the sensor pin as an input: + pinMode(SENSORPIN, INPUT); + digitalWrite(SENSORPIN, HIGH); // turn on the pullup + + Serial.begin(9600); +} + +void loop(){ + // read the state of the pushbutton value: + sensorState = digitalRead(SENSORPIN); + + // check if the sensor beam is broken + // if it is, the sensorState is LOW: + if (sensorState == LOW) { + // turn LED on: + digitalWrite(LEDPIN, HIGH); + } + else { + // turn LED off: + digitalWrite(LEDPIN, LOW); + } + + if (sensorState && !lastState) { + Serial.println("Unbroken"); + } + + if (!sensorState && lastState) { + Serial.println("Broken"); + } + + lastState = sensorState; +} \ No newline at end of file diff --git a/shepherd/protos/device.proto b/shepherd/protos/device.proto new file mode 100644 index 00000000..85ecffe6 --- /dev/null +++ b/shepherd/protos/device.proto @@ -0,0 +1,29 @@ +/* + * Defines a message for communicating Device Data + */ + + syntax = "proto3"; + + option optimize_for = LITE_RUNTIME; + + //message for describing a single device parameter + message Param { + string name = 1; + oneof val { + float fval = 2; + int32 ival = 3; + bool bval = 4; + } + } + + //message for describing a single device + message Device { + string name = 1; + uint64 uid = 2; + uint32 type = 3; + repeated Param params = 4; //each device has some number of params + } + + message DevData { + repeated Device devices = 1; //this single field has information about all requested params of devices + } \ No newline at end of file diff --git a/shepherd/protos/device_pb2.py b/shepherd/protos/device_pb2.py new file mode 100644 index 00000000..0690472a --- /dev/null +++ b/shepherd/protos/device_pb2.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: device.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='device.proto', + package='', + syntax='proto3', + serialized_options=b'H\003', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x0c\x64\x65vice.proto\"L\n\x05Param\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x04\x66val\x18\x02 \x01(\x02H\x00\x12\x0e\n\x04ival\x18\x03 \x01(\x05H\x00\x12\x0e\n\x04\x62val\x18\x04 \x01(\x08H\x00\x42\x05\n\x03val\"I\n\x06\x44\x65vice\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03uid\x18\x02 \x01(\x04\x12\x0c\n\x04type\x18\x03 \x01(\r\x12\x16\n\x06params\x18\x04 \x03(\x0b\x32\x06.Param\"#\n\x07\x44\x65vData\x12\x18\n\x07\x64\x65vices\x18\x01 \x03(\x0b\x32\x07.DeviceB\x02H\x03\x62\x06proto3' +) + + + + +_PARAM = _descriptor.Descriptor( + name='Param', + full_name='Param', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='Param.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='fval', full_name='Param.fval', index=1, + number=2, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='ival', full_name='Param.ival', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='bval', full_name='Param.bval', index=3, + number=4, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='val', full_name='Param.val', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], + serialized_start=16, + serialized_end=92, +) + + +_DEVICE = _descriptor.Descriptor( + name='Device', + full_name='Device', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='Device.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uid', full_name='Device.uid', index=1, + number=2, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type', full_name='Device.type', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='params', full_name='Device.params', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=94, + serialized_end=167, +) + + +_DEVDATA = _descriptor.Descriptor( + name='DevData', + full_name='DevData', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='devices', full_name='DevData.devices', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=169, + serialized_end=204, +) + +_PARAM.oneofs_by_name['val'].fields.append( + _PARAM.fields_by_name['fval']) +_PARAM.fields_by_name['fval'].containing_oneof = _PARAM.oneofs_by_name['val'] +_PARAM.oneofs_by_name['val'].fields.append( + _PARAM.fields_by_name['ival']) +_PARAM.fields_by_name['ival'].containing_oneof = _PARAM.oneofs_by_name['val'] +_PARAM.oneofs_by_name['val'].fields.append( + _PARAM.fields_by_name['bval']) +_PARAM.fields_by_name['bval'].containing_oneof = _PARAM.oneofs_by_name['val'] +_DEVICE.fields_by_name['params'].message_type = _PARAM +_DEVDATA.fields_by_name['devices'].message_type = _DEVICE +DESCRIPTOR.message_types_by_name['Param'] = _PARAM +DESCRIPTOR.message_types_by_name['Device'] = _DEVICE +DESCRIPTOR.message_types_by_name['DevData'] = _DEVDATA +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Param = _reflection.GeneratedProtocolMessageType('Param', (_message.Message,), { + 'DESCRIPTOR' : _PARAM, + '__module__' : 'device_pb2' + # @@protoc_insertion_point(class_scope:Param) + }) +_sym_db.RegisterMessage(Param) + +Device = _reflection.GeneratedProtocolMessageType('Device', (_message.Message,), { + 'DESCRIPTOR' : _DEVICE, + '__module__' : 'device_pb2' + # @@protoc_insertion_point(class_scope:Device) + }) +_sym_db.RegisterMessage(Device) + +DevData = _reflection.GeneratedProtocolMessageType('DevData', (_message.Message,), { + 'DESCRIPTOR' : _DEVDATA, + '__module__' : 'device_pb2' + # @@protoc_insertion_point(class_scope:DevData) + }) +_sym_db.RegisterMessage(DevData) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/shepherd/protos/game_state.proto b/shepherd/protos/game_state.proto new file mode 100644 index 00000000..1a28fac0 --- /dev/null +++ b/shepherd/protos/game_state.proto @@ -0,0 +1,18 @@ +/* + * define an action runtime must take based on game state + */ + + syntax = "proto3"; + + option optimize_for = LITE_RUNTIME; + + enum State { + POISON_IVY = 0; + DEHYDRATION = 1; + HYPOTHERMIA_START = 2; + HYPOTHERMIA_END = 3; + } + + message GameState { + State state = 1; + } \ No newline at end of file diff --git a/shepherd/protos/game_state_pb2.py b/shepherd/protos/game_state_pb2.py new file mode 100644 index 00000000..b298da3f --- /dev/null +++ b/shepherd/protos/game_state_pb2.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: game_state.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='game_state.proto', + package='', + syntax='proto3', + serialized_options=b'H\003', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x10game_state.proto\"\"\n\tGameState\x12\x15\n\x05state\x18\x01 \x01(\x0e\x32\x06.State*T\n\x05State\x12\x0e\n\nPOISON_IVY\x10\x00\x12\x0f\n\x0b\x44\x45HYDRATION\x10\x01\x12\x15\n\x11HYPOTHERMIA_START\x10\x02\x12\x13\n\x0fHYPOTHERMIA_END\x10\x03\x42\x02H\x03\x62\x06proto3' +) + +_STATE = _descriptor.EnumDescriptor( + name='State', + full_name='State', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='POISON_IVY', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DEHYDRATION', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='HYPOTHERMIA_START', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='HYPOTHERMIA_END', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=56, + serialized_end=140, +) +_sym_db.RegisterEnumDescriptor(_STATE) + +State = enum_type_wrapper.EnumTypeWrapper(_STATE) +POISON_IVY = 0 +DEHYDRATION = 1 +HYPOTHERMIA_START = 2 +HYPOTHERMIA_END = 3 + + + +_GAMESTATE = _descriptor.Descriptor( + name='GameState', + full_name='GameState', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='state', full_name='GameState.state', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=20, + serialized_end=54, +) + +_GAMESTATE.fields_by_name['state'].enum_type = _STATE +DESCRIPTOR.message_types_by_name['GameState'] = _GAMESTATE +DESCRIPTOR.enum_types_by_name['State'] = _STATE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +GameState = _reflection.GeneratedProtocolMessageType('GameState', (_message.Message,), { + 'DESCRIPTOR' : _GAMESTATE, + '__module__' : 'game_state_pb2' + # @@protoc_insertion_point(class_scope:GameState) + }) +_sym_db.RegisterMessage(GameState) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/shepherd/protos/gamepad.proto b/shepherd/protos/gamepad.proto new file mode 100644 index 00000000..6cdccf6d --- /dev/null +++ b/shepherd/protos/gamepad.proto @@ -0,0 +1,13 @@ +/* + * Defines a message for communicating GamePad state + */ + +syntax = "proto3"; + +option optimize_for = LITE_RUNTIME; + +message GpState { + bool connected = 1; + fixed32 buttons = 2; + repeated float axes = 3; +} \ No newline at end of file diff --git a/shepherd/protos/gamepad_pb2.py b/shepherd/protos/gamepad_pb2.py new file mode 100644 index 00000000..f667a5b9 --- /dev/null +++ b/shepherd/protos/gamepad_pb2.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: gamepad.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='gamepad.proto', + package='', + syntax='proto3', + serialized_options=b'H\003', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\rgamepad.proto\";\n\x07GpState\x12\x11\n\tconnected\x18\x01 \x01(\x08\x12\x0f\n\x07\x62uttons\x18\x02 \x01(\x07\x12\x0c\n\x04\x61xes\x18\x03 \x03(\x02\x42\x02H\x03\x62\x06proto3' +) + + + + +_GPSTATE = _descriptor.Descriptor( + name='GpState', + full_name='GpState', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='connected', full_name='GpState.connected', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='buttons', full_name='GpState.buttons', index=1, + number=2, type=7, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='axes', full_name='GpState.axes', index=2, + number=3, type=2, cpp_type=6, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=17, + serialized_end=76, +) + +DESCRIPTOR.message_types_by_name['GpState'] = _GPSTATE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +GpState = _reflection.GeneratedProtocolMessageType('GpState', (_message.Message,), { + 'DESCRIPTOR' : _GPSTATE, + '__module__' : 'gamepad_pb2' + # @@protoc_insertion_point(class_scope:GpState) + }) +_sym_db.RegisterMessage(GpState) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/shepherd/protos/run_mode.proto b/shepherd/protos/run_mode.proto new file mode 100644 index 00000000..b73e868f --- /dev/null +++ b/shepherd/protos/run_mode.proto @@ -0,0 +1,19 @@ +/* + * Defines a message for communicating the run mode + */ + +syntax = "proto3"; + +option optimize_for = LITE_RUNTIME; + +enum Mode { + IDLE = 0; + AUTO = 1; + TELEOP = 2; + ESTOP = 3; + CHALLENGE = 4; +} + +message RunMode { + Mode mode = 1; +} \ No newline at end of file diff --git a/shepherd/protos/run_mode_pb2.py b/shepherd/protos/run_mode_pb2.py new file mode 100644 index 00000000..511eaff4 --- /dev/null +++ b/shepherd/protos/run_mode_pb2.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: run_mode.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='run_mode.proto', + package='', + syntax='proto3', + serialized_options=b'H\003', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x0erun_mode.proto\"\x1e\n\x07RunMode\x12\x13\n\x04mode\x18\x01 \x01(\x0e\x32\x05.Mode*@\n\x04Mode\x12\x08\n\x04IDLE\x10\x00\x12\x08\n\x04\x41UTO\x10\x01\x12\n\n\x06TELEOP\x10\x02\x12\t\n\x05\x45STOP\x10\x03\x12\r\n\tCHALLENGE\x10\x04\x42\x02H\x03\x62\x06proto3' +) + +_MODE = _descriptor.EnumDescriptor( + name='Mode', + full_name='Mode', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='IDLE', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='AUTO', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TELEOP', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ESTOP', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CHALLENGE', index=4, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=50, + serialized_end=114, +) +_sym_db.RegisterEnumDescriptor(_MODE) + +Mode = enum_type_wrapper.EnumTypeWrapper(_MODE) +IDLE = 0 +AUTO = 1 +TELEOP = 2 +ESTOP = 3 +CHALLENGE = 4 + + + +_RUNMODE = _descriptor.Descriptor( + name='RunMode', + full_name='RunMode', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='mode', full_name='RunMode.mode', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=18, + serialized_end=48, +) + +_RUNMODE.fields_by_name['mode'].enum_type = _MODE +DESCRIPTOR.message_types_by_name['RunMode'] = _RUNMODE +DESCRIPTOR.enum_types_by_name['Mode'] = _MODE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +RunMode = _reflection.GeneratedProtocolMessageType('RunMode', (_message.Message,), { + 'DESCRIPTOR' : _RUNMODE, + '__module__' : 'run_mode_pb2' + # @@protoc_insertion_point(class_scope:RunMode) + }) +_sym_db.RegisterMessage(RunMode) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/shepherd/protos/start_pos.proto b/shepherd/protos/start_pos.proto new file mode 100644 index 00000000..fc252370 --- /dev/null +++ b/shepherd/protos/start_pos.proto @@ -0,0 +1,14 @@ +//Message for communicating starting position + +syntax = "proto3"; + +option optimize_for = LITE_RUNTIME; + +enum Pos { //we can add more start positions in the future + LEFT = 0; + RIGHT = 1; +} + +message StartPos { + Pos pos = 1; +} \ No newline at end of file diff --git a/shepherd/protos/start_pos_pb2.py b/shepherd/protos/start_pos_pb2.py new file mode 100644 index 00000000..de087583 --- /dev/null +++ b/shepherd/protos/start_pos_pb2.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: start_pos.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='start_pos.proto', + package='', + syntax='proto3', + serialized_options=b'H\003', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x0fstart_pos.proto\"\x1d\n\x08StartPos\x12\x11\n\x03pos\x18\x01 \x01(\x0e\x32\x04.Pos*\x1a\n\x03Pos\x12\x08\n\x04LEFT\x10\x00\x12\t\n\x05RIGHT\x10\x01\x42\x02H\x03\x62\x06proto3' +) + +_POS = _descriptor.EnumDescriptor( + name='Pos', + full_name='Pos', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='LEFT', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='RIGHT', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=50, + serialized_end=76, +) +_sym_db.RegisterEnumDescriptor(_POS) + +Pos = enum_type_wrapper.EnumTypeWrapper(_POS) +LEFT = 0 +RIGHT = 1 + + + +_STARTPOS = _descriptor.Descriptor( + name='StartPos', + full_name='StartPos', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='pos', full_name='StartPos.pos', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=19, + serialized_end=48, +) + +_STARTPOS.fields_by_name['pos'].enum_type = _POS +DESCRIPTOR.message_types_by_name['StartPos'] = _STARTPOS +DESCRIPTOR.enum_types_by_name['Pos'] = _POS +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +StartPos = _reflection.GeneratedProtocolMessageType('StartPos', (_message.Message,), { + 'DESCRIPTOR' : _STARTPOS, + '__module__' : 'start_pos_pb2' + # @@protoc_insertion_point(class_scope:StartPos) + }) +_sym_db.RegisterMessage(StartPos) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/shepherd/protos/text.proto b/shepherd/protos/text.proto new file mode 100644 index 00000000..3c074c6f --- /dev/null +++ b/shepherd/protos/text.proto @@ -0,0 +1,12 @@ +/* + * Defines a message for communicating text data + */ + +syntax = "proto3"; + +option optimize_for = LITE_RUNTIME; + +message Text { + repeated string payload = 1; //CHALLENGE_DATA: initial values or results of challenges + //LOG: list of log lines +} \ No newline at end of file diff --git a/shepherd/protos/text_pb2.py b/shepherd/protos/text_pb2.py new file mode 100644 index 00000000..ee15b4e7 --- /dev/null +++ b/shepherd/protos/text_pb2.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: text.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='text.proto', + package='', + syntax='proto3', + serialized_options=b'H\003', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\ntext.proto\"\x17\n\x04Text\x12\x0f\n\x07payload\x18\x01 \x03(\tB\x02H\x03\x62\x06proto3' +) + + + + +_TEXT = _descriptor.Descriptor( + name='Text', + full_name='Text', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='payload', full_name='Text.payload', index=0, + number=1, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=14, + serialized_end=37, +) + +DESCRIPTOR.message_types_by_name['Text'] = _TEXT +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Text = _reflection.GeneratedProtocolMessageType('Text', (_message.Message,), { + 'DESCRIPTOR' : _TEXT, + '__module__' : 'text_pb2' + # @@protoc_insertion_point(class_scope:Text) + }) +_sym_db.RegisterMessage(Text) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/shepherd/readmefigures/PiE Sheep.png b/shepherd/readmefigures/PiE Sheep.png new file mode 100644 index 00000000..1e4f09f2 Binary files /dev/null and b/shepherd/readmefigures/PiE Sheep.png differ diff --git a/shepherd/robot.py b/shepherd/robot.py new file mode 100644 index 00000000..24edd1f9 --- /dev/null +++ b/shepherd/robot.py @@ -0,0 +1,35 @@ +from datetime import datetime +import random +import math +from utils import * + +# TODO: rewrite this whole class for evergreen + + +class Robot: + + def __init__(self, name: str, number: int): + self.name: str = name + self.number: int = number + self.coding_challenge = [False] * 8 + self.starting_position = None + + def reset(self): + self.coding_challenge = [False] * 8 + self.starting_position = None + + def set_from_dict(self, dic: dict): + self.name = dic["team_name"] + self.number = dic["team_num"] + self.starting_position = dic.get("starting_position", self.starting_position) + + def info_dict(self, robot_ip): + return { + "team_name": self.name, + "team_num": self.number, + "starting_position": self.starting_position, + "robot_ip": robot_ip + } + + def __str__(self): + return f"Robot({self.number} {self.name})" diff --git a/shepherd/runtimeclient.py b/shepherd/runtimeclient.py index 28f3304d..75d6954f 100644 --- a/shepherd/runtimeclient.py +++ b/shepherd/runtimeclient.py @@ -1,115 +1,197 @@ -import msgpackrpc #pylint: disable=import-error +import time +import threading +import socket +from protos import run_mode_pb2 +from protos import start_pos_pb2 +from protos import game_state_pb2 +from utils import YDL_TARGETS, PROTOBUF_TYPES, UI_HEADER +from ydl import ydl_send + +PORT_RASPI = 8101 class RuntimeClient: - def __init__(self, host, port): - self.host, self.port, self.client = host, port, None + """ + This is a client that connects to the server running on a Raspberry Pi. + One client is initialized per robot. + + Lifecycle: when the client is created, it makes a socket and a background thread + to listen to the socket. If the socket disconnects, it will attempt to + reconnect. If close_connection is called, the socket will close and the + thread will end. + """ + def __init__(self, ind, robot_ip): + self.ind = ind + self.robot_ip = robot_ip + self.sock = None + self.connected = False + self.manually_closed = False # whether Shepherd has manually closed this client + # note that the first connect_tcp has to be in main thread, + # because otherwise close_connection could be called before first connection + self.__connect_tcp(silent=(robot_ip=="")) + self.send_connection_status_to_ui() + if self.connected: + threading.Thread(target=self.__start_recv, daemon=True).start() + + def __repr__(self) -> str: + return f"RuntimeClient({self.ind}, {self.robot_ip})" + + def __send_msg(self, mode: int, protobuf_obj): + msg_str = protobuf_obj.SerializeToString() + msg = bytes(msg_str) + msglen = len(msg).to_bytes(2, "little") + if self.connected: + try: + self.sock.sendall(bytes([mode]) + msglen + msg) + except (ConnectionError, OSError) as ex: + print(f"Error while sending from {self}: {ex}") + else: + print(f"{self} is not connected. Could not send message {msg_str}") + + def send_mode(self, mode): + """ + Send the Run Mode to Runtime. Example: auto vs teleop + """ + p = run_mode_pb2.RunMode() + p.mode = mode + self.__send_msg(PROTOBUF_TYPES.RUN_MODE, p) - def connect(self): - self.client = msgpackrpc.Client(msgpackrpc.Address(self.host, self.port)) + def send_start_pos(self, pos): + """ + Send the start position of the robot to Runtime (left or right) + """ + p = start_pos_pb2.StartPos() + p.pos = pos + self.__send_msg(PROTOBUF_TYPES.START_POS, p) - def disconnect(self): - self.client.close() - self.client = None + def send_game_state(self, state): + """ + Tells Runtime to use the game state, e.g. poison ivy or dehyrdration + """ + p = game_state_pb2.GameState() + p.state = state + self.__send_msg(PROTOBUF_TYPES.GAME_STATE, p) - @property - def connected(self): - return self.client is not None - def set_mode(self, mode=True): - self.client.call('set_mode', mode) + def close_connection(self): + """ + Closes the connection if not already closed. Note that + sock.shutdown(socket.SHUT_RDWR) sends a fin/eof to the peer + regardless of how many processes have handles on this socket; + the other thread will get an OSError while receiving + """ + if not self.manually_closed: + self.manually_closed = True # on other thread, know this was deliberate + try: + self.sock.shutdown(socket.SHUT_RDWR) # see above docstring + except (ConnectionError, OSError) as ex: + print(f"Error shutting down {self}: {ex}") + self.sock.close() # deallocates - def set_alliance(self, alliance): - self.client.call('set_alliance', alliance) - def set_master(self, master): - self.client.call('set_master', master) + def send_connection_status_to_ui(self): + data = { + "ind": self.ind, + "connected": self.connected, + "robot_ip": self.robot_ip + } + ydl_send(YDL_TARGETS.UI, UI_HEADER.ROBOT_CONNECTION, data) - def set_starting_zone(self, zone): - self.client.call('set_starting_zone', zone) - def run_challenge(self, seed, timeout=1): - self.client.notify('run_challenge', int(seed), timeout) + def __connect_tcp(self, silent=False) -> bool: + """ + Attempts to connect to the Rasberry PI + sets self.connected to the connection status + """ + self.connected = False + try: + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.settimeout(2) + self.sock.connect((self.robot_ip, PORT_RASPI)) + self.connected = True + message = f"Successfully connected to {self}" + except (ConnectionError, OSError) as ex: + self.connected = False + message = f"Error connecting to {self}: {ex}" + if not silent: + print(message) + if self.connected: + self.sock.settimeout(None) + # send 0 byte so that Runtime knows it's Shepherd + self.sock.sendall(bytes([0])) + + + def __start_recv(self): + """ + Until the connection is manually close (controlled by RuntimeClientManager), + waits to receive an incoming message. If connection fails, + then tries to reconnect infinitely until self.manually_closed is True + sends connection status to UI when appropriate + """ + while True: + try: + received = self.sock.recv(1024) + print(f"Received message from {self}: ", received) + except (ConnectionError, OSError) as ex: + print(f"Error while reading from socket of {self}: {ex}") + received = False + if not received: + self.connected = False + self.send_connection_status_to_ui() + if self.manually_closed: + self.sock.close() # deallocate from this thread as well + return + + print(f"Connection lost to {self}. Trying again...") + while not self.manually_closed: + self.__connect_tcp(silent=True) + if self.connected: + print(f"Successfully reconnected to {self}") + self.send_connection_status_to_ui() + break + time.sleep(1) + else: + # This is where one would process received data, ideally using some function mapping + # similar to the way it is done in Shepherd. + pass - def get_challenge_solution(self): - return self.client.call('get_challenge_solution') class RuntimeClientManager: - def __init__(self, blue_alliance, gold_alliance, b1_custom_ip=None, b2_custom_ip=None, - g1_custom_ip=None, g2_custom_ip=None): - custom_ips = (b1_custom_ip, b2_custom_ip, g1_custom_ip, g2_custom_ip) - self.blue_alliance, self.gold_alliance = blue_alliance, gold_alliance - self.clients = {} - for i in range(len(self.blue_alliance+self.gold_alliance)): - if custom_ips[i]: - self.clients[(self.blue_alliance+self.gold_alliance)[i]] =\ - (RuntimeClient(f'{custom_ips[i]}', 6020)) - elif (self.blue_alliance + self.gold_alliance)[i] >= -100: - self.clients[(self.blue_alliance+self.gold_alliance)[i]] =\ - (RuntimeClient(f'192.168.128.{200 +(self.blue_alliance + self.gold_alliance)[i]}', 6020)) #pylint: disable=line-too-long - else: - self.clients[(self.blue_alliance+self.gold_alliance)[i]] = None - for client in self.clients.values(): - if client is not None: - print(client.host) - client.connect() - for team in self.blue_alliance: - client = self.clients[team] - if client: - client.set_alliance('blue') - for team in self.gold_alliance: - client = self.clients[team] - if client: - client.set_alliance('gold') - - def set_starting_zones(self, zones): - for team, zone in zip(self.blue_alliance + self.gold_alliance, zones): - if self.clients[team]: - self.clients[team].set_starting_zone(zone) - - def set_mode(self, mode): - for client in self.clients.values(): - if client: - print('Setting mode for client:', client.host) - client.set_mode(mode) - - def get_challenge_solutions(self): - return {team: (client.get_challenge_solution() if client else None) for team, - client in self.clients.items()} - - def set_master_robots(self, blue_team, gold_team): - """ - blue_master = self.clients[blue_team] - if blue_master: - blue_master.set_master() - gold_master = self.clients[gold_team] - if gold_master: - gold_master.set_master()""" - - -""" -client = RuntimeClient('192.168.128.107', 6020) -client.connect() -import time -from Code import decode -x = 1 -client.run_challenge(x) -time.sleep(0.2) -print('Robot answer:', client.get_challenge_solution()) -print('Authoritative answer:', decode(x)) -""" #pylint: disable=pointless-string-statement - -# client = RuntimeClient('192.168.128.115', 6020) -# client.connect() -# client.set_mode('idle') - -# print('OK!') -# client.set_alliance('blue') -# client.set_starting_zone('left') -# client.run_challenge(123) -# time.sleep(0.2) -# print(client.get_challenge_solution()) -# time.sleep(1) -# client.run_challenge(123) -# print(client.get_challenge_solution()) + def __init__(self): + self.clients = [RuntimeClient(i, "") for i in range(4)] + + def connect_client(self, ind: int, robot_ip: str): + """ + Makes a RuntimeClient at ind and connects it to the + given robot_ip, terminating any previous connection + """ + self.clients[ind].close_connection() + self.clients[ind] = RuntimeClient(ind, robot_ip) + + def reconnect_all(self): + """ + Reconnects all clients to their saved robot_ips + (terminates existing connections - no need for close_all before) + """ + for c in range(len(self.clients)): + self.connect_client(c, self.clients[c].robot_ip) + + def foreach(self, fun, *args, **kwargs): + for client in self.clients: + fun(client, *args, **kwargs) + + def send_connection_status_to_ui(self): + self.foreach(RuntimeClient.send_connection_status_to_ui) + + def send_mode(self, mode): + self.foreach(RuntimeClient.send_mode, mode) + + def send_start_pos(self, pos): + self.foreach(RuntimeClient.send_start_pos, pos) + + def send_game_state(self, state): + self.foreach(RuntimeClient.send_game_state, state) + + def close_all(self): + self.foreach(RuntimeClient.close_connection) diff --git a/shepherd/sensors.py b/shepherd/sensors.py new file mode 100644 index 00000000..a4d038f8 --- /dev/null +++ b/shepherd/sensors.py @@ -0,0 +1,304 @@ +#YDL -> TURN_ON_LIGHT, {"id": 4} +#YDL -> SET_TRAFFIC_LIGHT, {"id": ???, color : "green"} + +# START_BUTTON_PRESSED {} -> YDL + +# figure out three things: 1) which device talking to, 2) which param of device to modify 3) value + +# TURN_ON_LIGHT, {light : 7} + +#arduino 1: lights [0,1,2,5,6] +#arduino 2: lights [3,4,7], traffic_light + +# Dev Handler should get something in the form +# device 8, parameter 1, value 1.0 + +""" +YDL Messages to Sensors.py should follow this format: + +[HEADER, {args}] + +args: { + id : # + variables : values +} + +Details for each header will live in Utils.py +""" +from __future__ import annotations +from utils import SENSOR_HEADER, SHEPHERD_HEADER +import queue +import time + +################################################ +# Evergreen Methods +################################################ + +class LowcarMessage: + """ + Representation of a packet of data from or command to a lowcar device + Detailing which devices (by year, uid), parameters, and the parameters' values are affected + """ + def __init__(self, dev_ids, params): + """ + Arguments: + dev_ids: A list of device ids strings of the format '{device_type}_{device_uid}' + params: A list of dictionaries + params[i] is a dictionary of parameters for device dev_ids[i] + key: (str) parameter name + value: (int/bool/float) the value that the parameter should be set to + Ex: We want to write to device 0x123 ("Device A") and 0x456 ("Device B") + Lets say we want to turn on an LED ("LED_X") on Device A + Lets say we want to dim an LED ("LED_Y") on Device B to 20% brightness + Let's say Devices A and B have device type 50 + dev_ids = ["50_123", "50_456"] + params = [ + { + "LED_A": 1.0 + }, + { + "LED_B": 0.2 + } + ] + """ + self.dev_ids = dev_ids + self.params = params + + def get_dev_ids(self): + return self.dev_ids + + def get_params(self): + return self.params + + def __repr__(self): + return f"LowcarMessage(dev_ids={self.dev_ids}, params={self.params})" + +class Device: + """ + An Arduino Device is connected to a number of sensors (also known as Parameter). + It has a uuid (you get this when you flash it), type (assigned in the lowcar file), + and parameters (mentioned above) + SUPER IMPORTANT: make sure the sensors/shepherd_util.h header file has the same values!! + """ + def __init__(self, device_type, uuid, parameters: list): + self.uuid = uuid + self.type = device_type + self.params = { parameter.name: parameter for parameter in parameters } + self.polling_parameters = [] + for parameter in parameters: + parameter.device = self + if parameter.should_poll: + self.polling_parameters.append(parameter) + + def get_identifier(self): + return f"{self.uuid}_{self.type}" + + def get_param(self, param: str) -> Parameter: + """ + Returns Parameter with name `param`. + """ + if param not in self.params: + raise Exception("Parameter {param} not found in arduino {self}") + return self.params.get(param) + + def __str__(self): + return self.get_identifier() + +class Parameter: + """ + A parameter (sensor) consists of: + - a name + - this is the name of the sensor. Under the hood, this is the name of a variable being shared with the arduino. + - this needs to be the same as the name in shepherd_utils.c + - an identifier + - this is the number to identify this sensor in shepherd. This is used internally for abstraction with the rest of shepherd. + - this should be unique + - this can be optional + example: name light, identifier: 1 means light 1. + this corresponds to the "id" in the YDL header args: { id : 1, variables : values} + - a parent device (arduino) that owns this sensor + TODO: talk about debounce_threshold and debounce_sensitivity + """ + def __init__(self, name: str, should_poll: bool, identifier=None, debounce_threshold=None, debounce_sensitivity=0.7,ydl_header=None): + self.name = name + self.should_poll = should_poll + self.identifier = identifier + self.__device = None + self.debounce_threshold = debounce_threshold + self.debounce_sensitivity = debounce_sensitivity + self.ydl_header = ydl_header + + @property + def device(self): + if self.__device is None: + raise Exception("Arduino is none, but is being accessed :(") + return self.__device + + @device.setter + def device(self, d): + self.__device = d + + def value_from_header(self, header): + """ + Returns the value to write to this device. For example, + light on should correspond to a value of 1.0 + """ + raise Exception("override this function plz") + + def is_state_change_significant(self, value, previous_value: float): + raise Exception("override this function you sad little sheep") + + def ydl_message_from_state_change(self, value: float): + # feel free to add previous_value as a param if needed + raise Exception("override this function you sad little sheep") + + def __repr__(self): + return f"Parameter({self.name}, {self.identifier})" + +def parameter_from_header(header): + """ + Gets the parameter (sensor) that a header corresponds to. + """ + sensor_pool = HEADER_MAPPINGS[header[0]] + args = header[1] + for p in sensor_pool: + if p.identifier is None or args.get("id", None) == p.identifier: + return p + raise Exception(f'no device found associated with YDL header {header}') + +def translate_ydl_message(header): + """ + This method will decode three import pieces of information: + 1) the arduino and device to talk to + 2) the parameter of device to modify + 3) the value to modify it to + """ + parameter: Parameter = parameter_from_header(header) + device = f'{parameter.device.type}_{parameter.device.uuid}' + value = parameter.value_from_header(header) + message = LowcarMessage([device], [{parameter.name:value}]) + return message + +previous_debounced_value = {} # TODO: move this. + +################################################ +# Game Specific Variables +################################################ + +""" + +# This is what sensors looked like in Spring 2021 +# Feel free to take inspiration from this code +# or burn it with fire + +# Look in "sensors overview/underview" on the wiki for more info +# also look at comments in Parameter class and Device class above + +class Light(Parameter): + def value_from_header(self, header): + if (header[0] == SENSOR_HEADER.TURN_ON_LIGHT + or header[0] == SENSOR_HEADER.TURN_ON_FIRE_LIGHT + or header[0] == SENSOR_HEADER.TURN_ON_LASERS): + return True + elif (header[0] == SENSOR_HEADER.TURN_OFF_LIGHT + or header[0] == SENSOR_HEADER.TURN_OFF_FIRE_LIGHT + or header[0] == SENSOR_HEADER.TURN_OFF_LASERS): + return False + raise Exception(f"Attempting to get value of light, but header[0] is {header[0]}") + +class DehydrationButton(Parameter): + def is_state_change_significant(self, value: bool, previous_value: bool): + return value == True and previous_value == False + + def ydl_message_from_state_change(self, value: float): + # fire lever, traffic button + args = { + "button": self.identifier + } + return args + # self.identifier is which button + +class GenericButton(Parameter): + def is_state_change_significant(self, value: bool, previous_value: bool): + return value == True and previous_value == False + + def ydl_message_from_state_change(self, value: float): + # fire lever, traffic button, linebreak sensors + args = {} + return args + +class TrafficLight(Parameter): + # COLORS = {"red" : 0xFF0000, "green" : 0x00FF00, "yellow" : 0xFFFF00} + + def value_from_header(self, header): + # this relationship is documented in shepherd_utils.c + if header[0] == SENSOR_HEADER.TURN_OFF_TRAFFIC_LIGHT: + return 0 + elif header[0] == SENSOR_HEADER.SET_TRAFFIC_LIGHT: + color = header[1]["color"] + if color == "red": + return 1 + elif color == "green": + return 2 + else: + raise Exception("Traffic light can only be red or green.") + raise Exception(f"Attempting to get value of traffic light but header[0] is {header[0]}") + +linebreak_debounce_threshold = 10 # samples + +# how fast are we polling? 100 Hz + +city_linebreak = GenericButton(name="city_linebreak", should_poll=True, identifier=0, ydl_header=SHEPHERD_HEADER.CITY_LINEBREAK, debounce_threshold=linebreak_debounce_threshold) +traffic_linebreak = GenericButton(name="traffic_linebreak", should_poll=True, identifier=1, ydl_header=SHEPHERD_HEADER.STOPLIGHT_CROSS, debounce_threshold=linebreak_debounce_threshold) +desert_linebreak = GenericButton(name="desert_linebreak", should_poll=True, identifier=2, ydl_header=SHEPHERD_HEADER.DESERT_ENTRY, debounce_threshold=linebreak_debounce_threshold) +dehydration_linebreak = GenericButton(name="dehydration_linebreak", should_poll=True, identifier=3, ydl_header=SHEPHERD_HEADER.DEHYDRATION_ENTRY, debounce_threshold=linebreak_debounce_threshold) +hypothermia_linebreak = GenericButton(name="hypothermia_linebreak", should_poll=True, identifier=4, ydl_header=SHEPHERD_HEADER.HYPOTHERMIA_ENTRY, debounce_threshold=linebreak_debounce_threshold) +airport_linebreak = GenericButton(name="airport_linebreak", should_poll=True, identifier=5, ydl_header=SHEPHERD_HEADER.FINAL_ENTRY, debounce_threshold=linebreak_debounce_threshold) + +fire_lever = GenericButton(name="fire_lever", should_poll=True, ydl_header=SHEPHERD_HEADER.FIRE_LEVER) + +traffic_button = GenericButton(name="traffic_button", should_poll=True, ydl_header=SHEPHERD_HEADER.STOPLIGHT_BUTTON_PRESS) +traffic_light = TrafficLight(name="traffic_light", should_poll=False) + +num_dehydration_buttons = 7 + +dehydration_buttons = [DehydrationButton(name=f"button{i}", should_poll=True, identifier=i, ydl_header=SHEPHERD_HEADER.DEHYDRATION_BUTTON_PRESS) for i in range(num_dehydration_buttons)] +lights = [Light(name=f"light{i}", should_poll=False, identifier=i) for i in range(num_dehydration_buttons)] +fire_light = Light(name="fire_light", should_poll=False) + +lasers = Light(name="lasers", should_poll=False) + +arduino_1 = Device(1, 1, lights + dehydration_buttons) +arduino_2 = Device(2, 2, [desert_linebreak, dehydration_linebreak, hypothermia_linebreak, airport_linebreak, fire_lever, fire_light]) # fix NOW TODO +arduino_3 = Device(3, 3, [city_linebreak, traffic_linebreak, traffic_button, traffic_light]) # fix +arduino_4 = Device(4, 4, [lasers]) +""" + +################################################ +# Evergreen Variables (may still need to be updated) +################################################ + +HEADER_MAPPINGS = { + # SENSOR_HEADER.TURN_ON_LIGHT : lights, + # SENSOR_HEADER.TURN_OFF_LIGHT : lights, + # SENSOR_HEADER.SET_TRAFFIC_LIGHT : [traffic_light], + # SENSOR_HEADER.TURN_OFF_TRAFFIC_LIGHT : [traffic_light], + # SENSOR_HEADER.TURN_ON_FIRE_LIGHT : [fire_light], + # SENSOR_HEADER.TURN_OFF_FIRE_LIGHT : [fire_light], + # SENSOR_HEADER.TURN_OFF_LASERS: [lasers], + # SENSOR_HEADER.TURN_ON_LASERS: [lasers], +} + +#used to request values from lowcar (non-polled) +HEADER_COMMAND_MAPPINGS = { + +} + +arduinos = { + # arduino_1.get_identifier(): arduino_1, + # arduino_2.get_identifier(): arduino_2, + # arduino_3.get_identifier(): arduino_3, + # arduino_4.get_identifier(): arduino_4, +} + +################################################ diff --git a/shepherd/sensors/Makefile b/shepherd/sensors/Makefile new file mode 100644 index 00000000..c7483ae8 --- /dev/null +++ b/shepherd/sensors/Makefile @@ -0,0 +1,18 @@ +LIBFLAGS=-pthread -lrt -Wall +DEPENDENCIES=dev_handler.c message.c shepherd_util.c shm_wrapper.c +HEADERS=message.h shepherd_util.h shm_wrapper.h + +all: dev_handler shm_api.c + +dev_handler: $(DEPENDENCIES) $(HEADERS) + gcc $(DEPENDENCIES) -o dev_handler $(LIBFLAGS) + +shm_api.c: shm_api.pyx setup.py shm_wrapper.c shepherd_util.c + python3 setup.py build_ext -i + +clean: + rm -f ./dev_handler + rm -rf build/ + rm -f shm_api.c + rm -f shm_api*.so + rm -f /dev/shm/* diff --git a/.gitmodules b/shepherd/sensors/__init__.py similarity index 100% rename from .gitmodules rename to shepherd/sensors/__init__.py diff --git a/shepherd/sensors/debouncer.py b/shepherd/sensors/debouncer.py new file mode 100644 index 00000000..cbdf2d32 --- /dev/null +++ b/shepherd/sensors/debouncer.py @@ -0,0 +1,29 @@ +import sys +sys.path.insert(1, '../') +from typing import Union +from sensors import Parameter +class Debouncer: + def __init__(self): + self.previous_parameter_values = {} + + def debounce(self, value: Union[int, float, bool], param: Parameter): + """ + This method performs debouncing by TODO + updates state, be careful + If there is no debouncing, it returns the value. + """ + if param.debounce_threshold is not None: + # Begin Debouncing + if param not in self.previous_parameter_values: + self.previous_parameter_values[param] = [] + prev_values = self.previous_parameter_values[param] + prev_values.append(value) + if len(prev_values) > param.debounce_threshold: + prev_values.pop(0) + # ex: if 70% of samples greater than some value + for value in set(prev_values): + if len([v for v in prev_values if v == value]) / param.debounce_threshold > param.debounce_sensitivity: + return value + return None + + return value \ No newline at end of file diff --git a/shepherd/sensors/debouncer_tests.py b/shepherd/sensors/debouncer_tests.py new file mode 100644 index 00000000..c0e54813 --- /dev/null +++ b/shepherd/sensors/debouncer_tests.py @@ -0,0 +1,25 @@ +import unittest +import sys +sys.path.insert(1, '../') +from sensors import GenericButton +from debouncer import Debouncer +from utils import SHEPHERD_HEADER + +class TestDebouncer(unittest.TestCase): + + def test_noise(self): + linebreak_debounce_threshold = 10 + city_linebreak = GenericButton(name="city_linebreak", should_poll=True, identifier=0, ydl_header=SHEPHERD_HEADER.CITY_LINEBREAK, debounce_threshold=linebreak_debounce_threshold) + d = Debouncer() + for i in range(10): + d.debounce(0, city_linebreak) + for i in range(100): + value = d.debounce(i % 2, city_linebreak) + # 70% threshold should be exceeded in first 5 iterations + if i < 5: + self.assertEqual(value, 0) + else: + self.assertEqual(value, None) + +if __name__ == '__main__': + unittest.main() diff --git a/shepherd/sensors/dev_handler.c b/shepherd/sensors/dev_handler.c new file mode 100644 index 00000000..ddb3c7bb --- /dev/null +++ b/shepherd/sensors/dev_handler.c @@ -0,0 +1,792 @@ +/** + * Handles lowcar device connects/disconnects and + * acts as the interface between the devices and shared memory + */ + +#include // for POSIX terminal control definitions in serialport_open() + +#include "shepherd_util.h" +#include "shm_wrapper.h" +#include "message.h" + +// ********************************* CONFIG ********************************* // + +// The interval (ms) at which we want DEVICE_DATA messages for subscribed params +#define SUB_INTERVAL 1 + +// **************************** PRIVATE STRUCT ****************************** // + +/* A struct shared between SENDER, RECEIVER, and RELAYER threads communicating + * with the same device. + * Contains information about each thread, how to communicate with the device, + * and information about the device itself + * The RELAYER thread is responsible for using this struct to properly clean up + * when the device disconnects or times out + */ +typedef struct { + pthread_t sender; // Thread to build and send outgoing messages + pthread_t receiver; // Thread to receive and process all incoming messages + pthread_t relayer; // Thread to get ACKNOWLEDGEMENT and monitor disconnect/timeout + uint8_t port_num; // Where device is connected to "/" + int file_descriptor; // Obtained from opening port. Used to close port. + int shm_dev_idx; // The unique index assigned to the device by shm_wrapper for shared memory operations on device_connect() + dev_id_t dev_id; // set by relayer once ACKNOWLEDGEMENT is received + uint64_t last_received_msg_time; // set by receiver: Timestamp of the most recent message from the device + pthread_mutex_t relay_lock; // Mutex on relay->last_received_msg_time + pthread_cond_t start_cond; // Conditional variable for relayer to broadcast to sender and receiver to start work +} relay_t; + +// ************************** FUNCTION DECLARATIONS ************************* // + +// Main functions +void init(); +void stop(); +void poll_connected_devices(); + +// Polling Utility +int get_new_devices(uint32_t* bitmap); + +// Threads for communicating with devices +void communicate(uint8_t port_num); +void* relayer(void* relay_cast); +void relay_clean_up(relay_t* relay); +void* sender(void* relay_cast); +void* receiver(void* relay_cast); + +// Device communication +int send_message(relay_t* relay, message_t* msg); +int receive_message(relay_t* relay, message_t* msg); +int verify_lowcar(relay_t* relay); + +// Serial port or socket opening and closing +int connect_socket(const char* socket_name); +int serialport_open(const char* port_name); +int serialport_close(int fd); + +// Utility +void cleanup_handler(void* args); + +// **************************** GLOBAL VARIABLES **************************** // +// The file name prefix of where to find devices, preceeding the port number +char* port_prefix = "/dev/ttyACM"; + +// Bitmap indicating whether port "*" is being monitored by dev handler, where * is the *-th bit +// Bits are turned on in get_new_devices() and turned off on disconnect/timeout in relay_clean_up() +uint32_t used_ports = 0; +pthread_mutex_t used_ports_lock; // poll_connected_devices() and relay_clean_up() shouldn't access used_ports at the same time + +// ***************************** MAIN FUNCTIONS ***************************** // + +// Initialize logger, shm, and mutexes +void init() { + // Init shared memory + shm_init(); + + // Initialize lock on global variable USED_PORTS + if (pthread_mutex_init(&used_ports_lock, NULL) != 0) { + log_printf(FATAL, "init: Couldn't init USED_PORTS_LOCK"); + exit(1); + } +} + +// Disconnect devices from shared memory and destroy mutexes +void stop() { + log_printf(INFO, "Interrupt received, terminating dev_handler\n"); + // For each tracked lowcar device, disconnect from shared memory + uint32_t connected_devs = 0; + get_catalog(&connected_devs); + for (int i = 0; i < MAX_DEVICES; i++) { + if ((1 << i) & connected_devs) { + device_disconnect(i); + } + } + // Destroy locks + pthread_mutex_destroy(&used_ports_lock); + exit(0); +} + +/** + * Detects when devices are connected + * On Arduino device connect, + * connect to shared memory and spawn three threads to communicate with the device + */ +void poll_connected_devices() { + // Poll for newly connected devices and open threads for them + log_printf(DEBUG, "Polling now for %s*.\n", port_prefix); + uint32_t connected_devs = 0; + while (1) { + if (get_new_devices(&connected_devs) > 0) { + // If bit i of CONNECTED_DEVS is on, then it's a new device + for (int i = 0; (connected_devs >> i) > 0 && i < MAX_DEVICES; i++) { + if (connected_devs & (1 << i)) { + communicate(i); + } + } + } + connected_devs = 0; + // Save CPU usage by checking for new devices only every so often (defined in runtime_util.h) + usleep(POLL_INTERVAL); + } +} + +// **************************** POLLING UTILITY **************************** // + +/** + * Finds which Arduinos are newly connected since the last call to this function + * Arguments: + * bitmap: Bit i will be turned on if [i] is a newly connected device + * Returns: + * the number of devices that were found + */ +int get_new_devices(uint32_t* bitmap) { + char port_name[14]; + uint8_t num_devices_found = 0; + // Check every port + for (int i = 0; i < MAX_DEVICES; i++) { + pthread_mutex_lock(&used_ports_lock); + // Check if i-th bit of USED_PORTS is zero (indicating device wasn't connected in previous function call) + if (!(used_ports & (1 << i))) { + sprintf(port_name, "%s%d", port_prefix, i); + // If that port currently connected (file exists), it's a new device + if (access(port_name, F_OK) != -1) { + // Turn bit on in BITMAP + *bitmap |= (1 << i); + // Mark that we've taken care of this device + used_ports |= (1 << i); + num_devices_found++; + } + } + pthread_mutex_unlock(&used_ports_lock); + } + return num_devices_found; +} + +// ******************************** THREADS ********************************* // + +/** + * Opens threads for communication with a device + * Three threads will be opened: + * relayer: Verifies device is lowcar and cancels threads when device disconnects/timesout + * sender: Sends data to write to device and periodically sends PING + * receiver: Receives parameter data from the lowcar device and processes logs + * Arguments: + * port_num: The port number of the new device to connect to + */ +void communicate(uint8_t port_num) { + relay_t* relay = malloc(sizeof(relay_t)); + if (relay == NULL) { + log_printf(FATAL, "communicate: Failed to malloc"); + exit(1); + } + relay->port_num = port_num; + + char port_name[15]; // Template size + 2 indices for port_number + sprintf(port_name, "%s%d", port_prefix, relay->port_num); + if (strcmp(port_prefix, "/tmp/ttyACM") == 0) { // Bind to socket + relay->file_descriptor = connect_socket(port_name); + if (relay->file_descriptor == -1) { + // log_printf(ERROR, "communicate: Couldn't connect to socket %s\n", port_name); + relay_clean_up(relay); + return; + } + } else { // Open serial port + relay->file_descriptor = serialport_open(port_name); + if (relay->file_descriptor == -1) { + log_printf(ERROR, "communicate: Couldn't open port %s\n", port_name); + relay_clean_up(relay); + return; + } + } + + // Initialize the other relay values + relay->shm_dev_idx = -1; + relay->dev_id.type = -1; + relay->dev_id.year = -1; + relay->dev_id.uid = -1; + relay->last_received_msg_time = 0; + pthread_mutex_init(&relay->relay_lock, NULL); + pthread_cond_init(&relay->start_cond, NULL); + + // Open threads for sender, receiver, and relayer + if (pthread_create(&relay->sender, NULL, sender, relay) != 0) { + log_printf(ERROR, "communicate: Couldn't spawn thread for SENDER"); + } + if (pthread_create(&relay->receiver, NULL, receiver, relay) != 0) { + log_printf(ERROR, "communicate: Couldn't spawn thread for RECEIVER"); + } + if (pthread_create(&relay->relayer, NULL, relayer, relay) != 0) { + log_printf(ERROR, "communicate: Couldn't spawn thread for RELAYER"); + } +} + +/** + * Sends a PING to the device and waits for an ACKNOWLEDGEMENT + * If the ACKNOWLEDGEMENT takes too long, close the device and exit all threads + * Connects the device to shared memory and signals the sender and receiver to start + * Continuously checks if the device disconnected or timed out + * If so, it disconnects the device from shared memory, closes the device, and frees memory + * Arguments: + * relay_cast: uncasted relay_t struct containing device info + */ +void* relayer(void* relay_cast) { + relay_t* relay = relay_cast; + int ret; + + // Verify that the device is a lowcar device + sleep(2); + ret = verify_lowcar(relay); + if (ret != 0) { + log_printf(DEBUG, "/dev/ttyACM%d couldn't be verified to be a lowcar device", relay->port_num); + log_printf(ERROR, "A non-PiE device was recently plugged in. Please unplug immediately"); + relay_clean_up(relay); + return NULL; + } + + // At this point, the device is confirmed to be a lowcar device! + + // Connect the lowcar device to shared memory + device_connect(&relay->dev_id, &relay->shm_dev_idx); + if (relay->shm_dev_idx == -1) { + relay_clean_up(relay); + return NULL; + } + + // Broadcast to the sender and receiver to start work + pthread_cond_broadcast(&relay->start_cond); + + // If the device disconnects or times out, clean up + log_printf(DEBUG, "Monitoring %s (0x%016llX)", get_device_name(relay->dev_id.type), relay->dev_id.uid); + char port_name[14]; + sprintf(port_name, "%s%d", port_prefix, relay->port_num); + while (1) { + // If Arduino port file doesn't exist, it disconnected + if (access(port_name, F_OK) == -1) { + log_printf(INFO, "%s (0x%016llX) disconnected!", get_device_name(relay->dev_id.type), relay->dev_id.uid); + relay_clean_up(relay); + return NULL; + } + // If it took too long to receive a message, the device timed out + pthread_mutex_lock(&relay->relay_lock); + if ((millis() - relay->last_received_msg_time) >= TIMEOUT) { + pthread_mutex_unlock(&relay->relay_lock); + log_printf(WARN, "%s (0x%016llX) timed out!", get_device_name(relay->dev_id.type), relay->dev_id.uid); + relay_clean_up(relay); + return NULL; + } + pthread_mutex_unlock(&relay->relay_lock); + usleep(POLL_INTERVAL); + } +} + +/** + * Called by relayer to clean up after the device. + * Closes serialport, cancels threads, + * disconnects from shared memory, and frees the RELAY struct + * Arguments: + * relay: Struct containing device/thread info used to clean up + */ +void relay_clean_up(relay_t* relay) { + // If couldn't connect to device in the first place, just mark as unused + if (relay->file_descriptor == -1) { + used_ports &= ~(1 << relay->port_num); // Set bit to 0 to indicate unused + free(relay); + sleep(TIMEOUT / 1000); + return; + } + + int ret; + // Cancel the sender and receiver threads when ongoing transfers are completed + pthread_cancel(relay->sender); + pthread_cancel(relay->receiver); + if ((ret = pthread_join(relay->sender, NULL)) != 0) { + log_printf(ERROR, "relay_clean_up: pthread_join on sender failed -- error: %d", ret); + } + if ((ret = pthread_join(relay->receiver, NULL)) != 0) { + log_printf(ERROR, "relay_clean_up: pthread_join on receiver failed -- error: %d", ret); + } + + // Disconnect the device from shared memory if it's connected + if (relay->shm_dev_idx != -1) { + device_disconnect(relay->shm_dev_idx); + } + + /* 1) If lowcar device timed out, we sleep to give it time to reset and try to send an ACK again + * 2) If not lowcar device, we want to minimize time spent on multiple attempts to receive an ACK + */ + sleep(TIMEOUT / 1000); + // Close the device and mark that it disconnected + serialport_close(relay->file_descriptor); + if ((ret = pthread_mutex_lock(&used_ports_lock))) { + log_printf(ERROR, "relay_clean_up: used_ports_lock mutex lock failed with code %d", ret); + } + used_ports &= ~(1 << relay->port_num); // Set bit to 0 to indicate unused + pthread_mutex_unlock(&used_ports_lock); + pthread_mutex_destroy(&relay->relay_lock); + pthread_cond_destroy(&relay->start_cond); + if (relay->dev_id.uid == -1) { + log_printf(DEBUG, "Cleaned up bad device %s%d\n", port_prefix, relay->port_num); + } else { + log_printf(DEBUG, "Cleaned up %s (0x%016llX)", get_device_name(relay->dev_id.type), relay->dev_id.uid); + } + free(relay); +} + +/** + * Continuously sends PING and reads from shared memory to send DEVICE_WRITE/SUBSCRIPTION_REQUEST + * Arguments: + * relay_cast: Uncasted relay_t struct containing device info + */ +void* sender(void* relay_cast) { + relay_t* relay = relay_cast; + + // Wait until relayer gets an ACKNOWLEDGEMENT + pthread_mutex_lock(&relay->relay_lock); + pthread_cleanup_push(&cleanup_handler, (void*) relay); + pthread_cond_wait(&relay->start_cond, &relay->relay_lock); + pthread_cleanup_pop(1); + + // Cancel this thread only where pthread_testcancel() + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + + // Start doing work + uint32_t pmap[MAX_DEVICES + 1]; + param_val_t* params = malloc(MAX_PARAMS * sizeof(param_val_t)); // Array of params to be filled on device_read() + if (params == NULL) { + log_printf(FATAL, "sender: Failed to malloc"); + exit(1); + } + uint32_t sub_map[MAX_DEVICES + 1]; + message_t* msg; // Message to build + int ret; // Hold the value from send_message() + uint64_t last_sent_ping_time = millis(); + while (1) { + // Write to device if needed via a DEVICE_WRITE message + get_cmd_map(pmap); + if (pmap[0] & (1 << relay->shm_dev_idx)) { // If bit i in pmap[0] != 0, there are values to write to device i + // Read the new parameter values to write from shared memory as DEV_HANDLER from the COMMAND stream + device_read(relay->shm_dev_idx, DEV_HANDLER, COMMAND, pmap[1 + relay->shm_dev_idx], params); + // Serialize and bulk transfer a DeviceWrite packet with PARAMS to the device + msg = make_device_write(relay->dev_id.type, pmap[1 + relay->shm_dev_idx], params); + ret = send_message(relay, msg); + if (ret != 0) { + log_printf(WARN, "Couldn't send DEVICE_WRITE to %s (0x%016llX)", get_device_name(relay->dev_id.type), relay->dev_id.uid); + } + destroy_message(msg); + } + + // Send another PING every PING_FREQ milliseconds + if ((millis() - last_sent_ping_time) >= PING_FREQ) { + msg = make_ping(); + ret = send_message(relay, msg); + if (ret != 0) { + log_printf(WARN, "Couldn't send PING to %s (0x%016llX)", get_device_name(relay->dev_id.type), relay->dev_id.uid); + } + // Update the timestamp at which we sent a PING + last_sent_ping_time = millis(); + destroy_message(msg); + } + + // Send another SUBSCRIPTION_REQUEST if requested + get_sub_requests(sub_map, DEV_HANDLER); + if (sub_map[0] & (1 << relay->shm_dev_idx)) { // If bit i in sub_map[0] != 0, there is a new SUBSCRIPTION_REQUEST to be sent to device i + msg = make_subscription_request(relay->dev_id.type, sub_map[1 + relay->shm_dev_idx], SUB_INTERVAL); + ret = send_message(relay, msg); + if (ret != 0) { + log_printf(WARN, "Couldn't send SUBSCRIPTION_REQUEST to %s (0x%016llX)", get_device_name(relay->dev_id.type), relay->dev_id.uid); + } + destroy_message(msg); + } + + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + pthread_testcancel(); // Cancellation point + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + usleep(1000); + } + return NULL; +} + +/** + * Continuously attempts to parse incoming data over serial and send to shared memory + * Sets relay->last_received_msg_time upon receiving a message + * Arguments: + * relay_cast: uncasted relay_t struct containing device info + */ +void* receiver(void* relay_cast) { + relay_t* relay = relay_cast; + + // Wait until relayer gets an ACKNOWLEDGEMENT + pthread_mutex_lock(&relay->relay_lock); + pthread_cleanup_push(&cleanup_handler, (void*) relay); + pthread_cond_wait(&relay->start_cond, &relay->relay_lock); + pthread_cleanup_pop(1); + + // Cancel this thread only where pthread_testcancel() + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + + // Start doing work! + // An empty message to parse the received data into + message_t* msg = make_empty(MAX_PAYLOAD_SIZE); + // An array of empty parameter values to be populated from DeviceData message payloads and written to shared memory + param_val_t* vals = malloc(MAX_PARAMS * sizeof(param_val_t)); + if (vals == NULL) { + log_printf(FATAL, "receiver: Failed to malloc"); + exit(1); + } + while (1) { + // Try to read a message + if (receive_message(relay, msg) != 0) { + // Message was broken... try to read the next message + continue; + } + + if (msg->message_id == DEVICE_DATA || msg->message_id == LOG || msg->message_id == PING) { + // Update last received message time + pthread_mutex_lock(&relay->relay_lock); + relay->last_received_msg_time = millis(); + pthread_mutex_unlock(&relay->relay_lock); + // Handle message + if (msg->message_id == DEVICE_DATA) { + // If received DEVICE_DATA, write to shared memory + parse_device_data(relay->dev_id.type, msg, vals); // Get param values from payload + device_write(relay->shm_dev_idx, DEV_HANDLER, DATA, *((uint32_t*) msg->payload), vals); + } else if (msg->message_id == LOG) { + // If received LOG, send it to the logger + log_printf(DEBUG, "[%s (0x%016llX)]: %s", get_device_name(relay->dev_id.type), relay->dev_id.uid, msg->payload); + } + } else { // Invalid message type + log_printf(WARN, "Dropped bad message (type %d) from %s (0x%016llX)", msg->message_id, get_device_name(relay->dev_id.type), relay->dev_id.uid); + } + // Now that the message is taken care of, clear the message + msg->message_id = 0x0; + msg->payload_length = 0; + msg->max_payload_length = MAX_PAYLOAD_SIZE; + memset(msg->payload, 0, MAX_PAYLOAD_SIZE); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + pthread_testcancel(); // Cancellation point + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + } + return NULL; +} + +// ************************** DEVICE COMMUNICATION ************************** // + +/** + * Helper function for sender() + * Serializes, encodes, and sends a message + * Arguments: + * relay: Contains the file descriptor + * msg: The message to be sent + * Returns: + * 0 if successful + * -1 if couldn't write all the bytes + */ +int send_message(relay_t* relay, message_t* msg) { + int len = calc_max_cobs_msg_length(msg); + uint8_t* data = malloc(len); + if (data == NULL) { + log_printf(FATAL, "send_message: Failed to malloc"); + exit(1); + } + len = message_to_bytes(msg, data, len); + int transferred = writen(relay->file_descriptor, data, len); + if (transferred != len) { + log_printf(WARN, "Sent only %d out of %d bytes to %d (0x%016llX)\n", transferred, len, get_device_name(relay->dev_id.type), relay->dev_id.uid); + } + free(data); + return (transferred == len) ? 0 : -1; +} + +/** + * Helper function for receiver() + * Continuously reads from stream until reads the next message, then attempts to parse + * This function blocks until it reads a (possibly broken) message + * Arguments: + * relay: Contains the file descriptor and port number of the device + * msg: The message_t *to be populated with the parsed data (if successful) + * Returns: + * 0 on successful parse + * 1 on broken message + * 2 on incorrect checksum + * 3 on timeout + */ +int receive_message(relay_t* relay, message_t* msg) { + uint8_t last_byte_read = 0; // Variable to temporarily hold a read byte + int num_bytes_read = 0; + + if (relay->dev_id.uid == -1) { + /* Haven't verified device is lowcar yet + * read() is set to timeout while waiting for an ACK (see serialport_open())*/ + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + num_bytes_read = read(relay->file_descriptor, &last_byte_read, 1); + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + if (num_bytes_read == -1) { + log_printf(ERROR, "receive_message: Error on read() for ACK--%s", strerror(errno)); + return 3; + } else if (num_bytes_read == 0) { // read() returned due to timeout + log_printf(WARN, "Timed out when waiting for ACK from %s%d!", port_prefix, relay->port_num); + return 3; + } else if (last_byte_read != 0x00) { + // If the first thing received isn't a perfect ACK, we won't accept it + log_printf(WARN, "Attempting to read delimiter but got 0x%02X from %s%d\n", last_byte_read, port_prefix, relay->port_num); + return 1; + } + } else { // Receiving from a verified lowcar device + // Keep reading a byte until we get the delimiter byte + while (1) { + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + num_bytes_read = readn(relay->file_descriptor, &last_byte_read, 1); // Waiting for first byte can block + if (num_bytes_read == 0) { + // received EOF so sleep to make device disconnected + sleep(TIMEOUT / 1000 + 1); + return 1; + } else if (num_bytes_read == -1) { + log_printf(ERROR, "receive_message: error reading from file: %s", strerror(errno)); + return 1; + } + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + if (last_byte_read == 0x00) { + // Found start of a message + break; + } + // If we were able to read a byte but it wasn't the delimiter + log_printf(WARN, "Attempting to read delimiter but got 0x%02X from %s%d\n", last_byte_read, port_prefix, relay->port_num); + } + } + + // Read the next byte, which tells how many bytes left are in the message + uint8_t cobs_len; + num_bytes_read = readn(relay->file_descriptor, &cobs_len, 1); + if (num_bytes_read != 1) { + return 1; + } else if (cobs_len > (MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE + MAX_PAYLOAD_SIZE + CHECKSUM_SIZE + 1)) { // + 1 for cobs encoding overhead + // Got some weird message that is unusually long (longer than a valid message with the longest payload) + log_printf(WARN, "Received a cobs length that is too large"); + return 1; + } else if (cobs_len < (MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE + CHECKSUM_SIZE + 1)) { // + 1 for cobs encoding overhead + // Got some weird message that is unusually short (shorter than a PING with no payload) + log_printf(WARN, "Received a cobs length that is too small"); + return 1; + } + + // Allocate buffer to read message into + uint8_t* data = malloc(DELIMITER_SIZE + COBS_LENGTH_SIZE + cobs_len); + if (data == NULL) { + log_printf(FATAL, "receive_message: Failed to malloc"); + exit(1); + } + data[0] = 0x00; + data[1] = cobs_len; + + // Read the message + num_bytes_read = readn(relay->file_descriptor, &data[2], cobs_len); + if (num_bytes_read != cobs_len) { + log_printf(WARN, "Read only %d out of %d bytes from %s (0x%016llX)\n", num_bytes_read, cobs_len, get_device_name(relay->dev_id.type), relay->dev_id.uid); + free(data); + return 1; + } + + // Parse the message + int ret = parse_message(data, msg); + free(data); + if (ret != 0) { + log_printf(WARN, "Couldn't parse message from %s%d\n", port_prefix, relay->port_num); + return 2; + } + return 0; +} + +/** + * Sends a PING to the device and waits for an ACKNOWLEDGEMENT + * The first message received must be a perfectly constructed ACKNOWLEDGEMENT + * Arguments: + * relay: Struct containing all relevant port information. + * dev_id field will be populated on successful ACKNOWLEDGEMENT + * Returns: + * 0 if received ACKNOWLEDGEMENT. Sets relay->dev_id + * 1 if Ping message couldn't be sent + * 2 if ACKNOWLEDGEMENT wasn't received + */ +int verify_lowcar(relay_t* relay) { + // Send a Ping + message_t* ping = make_ping(); + int ret = send_message(relay, ping); + destroy_message(ping); + if (ret != 0) { + return 1; + } + + // Try to read an ACKNOWLEDGEMENT, which we expect from a lowcar device that receives a PING + message_t* ack = make_empty(MAX_PAYLOAD_SIZE); + ret = receive_message(relay, ack); + if (ret != 0) { + log_printf(DEBUG, "Didn't receive ACK"); + destroy_message(ack); + return 2; + } else if (ack->message_id != ACKNOWLEDGEMENT) { + log_printf(DEBUG, "Message is not an ACK, but of type %d", ack->message_id); + destroy_message(ack); + return 2; + } + + // We have a lowcar device! + + /* Set serial port options to allow read() to block indefinitely + * We expect the lowcar device to continuously send data + * In serialport_open(), we set read() to timeout specifically for waiting for ACK */ + if (strcmp(port_prefix, "/dev/ttyACM") == 0) { + struct termios toptions; + if (tcgetattr(relay->file_descriptor, &toptions) < 0) { // Get current options + log_printf(ERROR, "verify_lowcar: Couldn't get term attributes for %s (0x%016llX)", get_device_name(relay->dev_id.type), relay->dev_id.uid); + return -1; + } + toptions.c_cc[VMIN] = 1; // read() must read at least a byte before returning + // Save changes to TOPTIONS immediately using flag TCSANOW + tcsetattr(relay->file_descriptor, TCSANOW, &toptions); + if (tcsetattr(relay->file_descriptor, TCSAFLUSH, &toptions) < 0) { + log_printf(ERROR, "verify_lowcar: Couldn't set term attributes for %s (0x%016llX)", get_device_name(relay->dev_id.type), relay->dev_id.uid); + return -1; + } + } + + // Parse ACKNOWLEDGEMENT payload into relay->dev_id_t + memcpy(&relay->dev_id.type, &ack->payload[0], 1); + memcpy(&relay->dev_id.year, &ack->payload[1], 1); + memcpy(&relay->dev_id.uid, &ack->payload[2], 8); + log_printf(INFO, "Connected %s (0x%016llX) from year %d!", get_device_name(relay->dev_id.type), relay->dev_id.uid, relay->dev_id.year); + relay->last_received_msg_time = millis(); + destroy_message(ack); + return 0; +} + +// ************************* SOCKETS / SERIAL PORTS ************************* // + +/** + * Binds to a socket for reading and writing binary data + * Arguments: + * socket_name: THe name of the socket (ex: "/tmp/ttyACM0") + * Returns: + * A valid file_descriptor, or + * -1 on error + */ +int connect_socket(const char* socket_name) { + // Make a local socket for sending/receiving raw byte streams + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) { + log_printf(ERROR, "connect_socket: Couldn't create socket--%s", strerror(errno)); + return -1; + } + // Connect the socket to the found device's socket address + // https://www.man7.org/linux/man-pages/man7/unix.7.html + struct sockaddr_un dev_socket_addr = {0}; + dev_socket_addr.sun_family = AF_UNIX; + strcpy(dev_socket_addr.sun_path, socket_name); + if (connect(fd, (struct sockaddr*) &dev_socket_addr, sizeof(dev_socket_addr)) != 0) { + log_printf(ERROR, "connect_socket: Couldn't connect socket %s--%s", dev_socket_addr.sun_path, strerror(errno)); + remove(socket_name); + return -1; + } + + // Set read() to timeout for up to TIMEOUT milliseconds + struct timeval tv; + tv.tv_sec = TIMEOUT / 1000; + tv.tv_usec = 0; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*) &tv, sizeof(tv)); + return fd; +} + +/** + * Opens a serial port for reading and writing binary data + * Uses 8-N-1 serial port config and without special processing + * Also makes read() block for TIMEOUT milliseconds + * Used to timeout when waiting for an ACKNOWLEDGEMENT + * After receiving an ACKNOWLEDGEMENT, read() blocks until receiving at least a byte (set in verify_lowcar()) + * Arguments: + * port_name: The name of the port (ex: "/dev/ttyACM0", "/dev/tty.usbserial", "COM1") + * Returns: + * A valid file_descriptor, or + * -1 on error + */ +int serialport_open(const char* port_name) { + // Open the serialport for reading and writing + // Need to specify O_NOCTTY to prevent attaching devices from becoming controlling terminals; see wiki + int fd = open(port_name, O_RDWR | O_NOCTTY); + if (fd == -1) { + log_printf(ERROR, "serialport_open: Unable to open port %s", port_name); + return -1; + } + + // Get the current serialport options + struct termios toptions; + if (tcgetattr(fd, &toptions) < 0) { + log_printf(ERROR, "serialport_open: Couldn't get term attributes for port %s", port_name); + return -1; + } + + // Set the baudrate of communication to 115200 (same as on Arduino) + speed_t brate = B115200; + cfsetspeed(&toptions, brate); + + // Update serialport options: https://linux.die.net/man/3/cfsetspeed + // Set serialport config to 8-N-1, which is default for Arduino Serial.begin() + toptions.c_cflag &= ~CSIZE; // Reset character size + toptions.c_cflag |= CS8; // Set character size to 8 + toptions.c_cflag &= ~PARENB; // Disable parity generation on output and parity checking for input (N) + toptions.c_cflag &= ~CSTOPB; // Set only one stop bit (1) + + // Disables special processing of input and output bytes. See https://linux.die.net/man/3/cfsetspeed + cfmakeraw(&toptions); + + // Set options for read(fd, buffer, num_bytes_to_read) + // see: http://unixwiz.net/techtips/termios-vmin-vtime.html + toptions.c_cc[VMIN] = 0; // Until receiving ACK, do not block indefinitely (use timeout) + toptions.c_cc[VTIME] = TIMEOUT / 100; // Number of deciseconds to timeout + + // Save changes to TOPTIONS. (Flag TCSANOW saves immediately) + tcsetattr(fd, TCSANOW, &toptions); + if (tcsetattr(fd, TCSAFLUSH, &toptions) < 0) { + log_printf(ERROR, "serialport_open: Couldn't set term attributes for port %s", port_name); + return -1; + } + + return fd; +} + +/** + * Closes the serial port opened via serialport_open() + * Arguments: + * fd: File descriptor obtained from serialport_open() + * Returns: + * 0 on success + * -1 on error and sets errno + */ +int serialport_close(int fd) { + return close(fd); +} + +// ******************************** UTILITY ********************************* // + +/** + * If sender/receive is canceled during pthread_cond_wait(), pthread_cleanup_push() + * using this function guarantees that relay->relay_lock is unlocked before cancellation. + * Arguments: + * args: Uncasted relay_t containing mutex requiring unlocking + */ +void cleanup_handler(void* args) { + relay_t* relay = (relay_t*) args; + pthread_mutex_unlock(&relay->relay_lock); +} + +// ********************************** MAIN ********************************** // + +int main(int argc, char* argv[]) { + // If SIGINT (Ctrl+C) is received, call stop() to clean up + signal(SIGINT, stop); + init(); + // Passing argument "virtual" will search for fake devices in /tmp/ttyACM + if (argc == 2 && strcmp(argv[1], "virtual") == 0) { + port_prefix = "/tmp/ttyACM"; + } + log_printf(INFO, "DEV_HANDLER initialized."); + poll_connected_devices(); + return 0; +} diff --git a/shepherd/sensors/message.c b/shepherd/sensors/message.c new file mode 100644 index 00000000..8a503e7e --- /dev/null +++ b/shepherd/sensors/message.c @@ -0,0 +1,385 @@ +#include "message.h" + +// ******************************** Utility ********************************* // + +void print_bytes(uint8_t* data, size_t len) { + printf("0x"); + for (int i = 0; i < len; i++) { + printf("%02X ", data[i]); + } + printf("\n"); +} + +// *************************** PRIVATE FUNCTIONS **************************** // + +/** + * Private utility function to calculate the size of the payload needed + * for a DEVICE_WRITE message. + * Arguments: + * device_type: The type of device (refer to runtime_util) + * param_bitmap: A bitmap, the i-th bit indicates whether param i will be transmitted in the message + * Returns: + * The size of the payload + */ +static size_t device_write_payload_size(uint8_t device_type, uint32_t param_bitmap) { + size_t result = BITMAP_SIZE; + device_t* dev = get_device(device_type); + // Loop through each of the device's parameters and add the size of the parameter + for (int i = 0; ((param_bitmap >> i) > 0) && (i < MAX_PARAMS); i++) { + // Include parameter i if the i-th bit is on + if ((1 << i) & param_bitmap) { + switch (dev->params[i].type) { + case INT: + result += sizeof(int32_t); + break; + case FLOAT: + result += sizeof(float); + break; + case BOOL: + result += sizeof(uint8_t); + break; + } + } + } + return result; +} + +/** + * Appends data to the end of a message's payload + * Increments msg->payload_length accordingly + * Arguments: + * msg: The message whose payload is to be appended to + * data: The data to be appended to MSG's payload + * len: The size of data + * Returns: + * 0 if successful + * -1 if max_payload_length is too small + */ +static int append_payload(message_t* msg, uint8_t* data, size_t len) { + memcpy(&(msg->payload[msg->payload_length]), data, len); + msg->payload_length += len; + return (msg->payload_length > msg->max_payload_length) ? -1 : 0; +} + +/** + * Computes the checksum of input byte array by XOR-ing each byte + * Arguments: + * data: A byte array whose checksum will be computed + * len: the size of data + * Returns: + * The checksum of DATA + */ +static uint8_t checksum(uint8_t* data, size_t len) { + uint8_t chk = data[0]; + for (int i = 1; i < len; i++) { + chk ^= data[i]; + } + return chk; +} + +/** + * A macro to help with cobs_encode + */ +#define finish_block() \ + { \ + block[0] = (uint8_t) block_len; \ + block = dst; \ + dst++; \ + dst_len++; \ + block_len = 1; \ + }; + +/** + * Cobs encodes a byte array into a buffer + * Arguments: + * src: The byte array to be encoded + * dst: The buffer to write the encoded data into + * src_len: The size of SRC + * Returns: + * The size of the encoded data, DST + */ +static ssize_t cobs_encode(uint8_t* dst, const uint8_t* src, size_t src_len) { + const uint8_t* end = src + src_len; + uint8_t* block = dst; // Advancing pointer to start of each block + dst++; + size_t block_len = 1; + ssize_t dst_len = 0; + + /* Build the DST array in "blocks", copying bytes one-by-one from SRC + * until a block ends. + * A block ends when + * 1) Encountering a 0x00 byte in the source array, + * 2) Reaching the max length of 256, or + * 3) Source array is fully processed + * When a block ends, insert the block length in DST at the beginning of + * that block, then start a new block if there are still bytes in SRC to process + */ + while (src < end) { + if (*src == 0) { + finish_block(); + } else { + // Copy non-zero byte over without processing + *dst = *src; + dst++; + block_len++; + dst_len++; + if (block_len == 0xFF) { + // Reached max block length + finish_block(); + } + } + src++; + } + finish_block(); + return dst_len; +} + +/** + * Cobs decodes a byte array into a buffer + * Arguments: + * src: The byte array to be decoded + * dst: The buffer to write the decoded data into + * src_len: The size of SRC + * Returns: + * The size of the decoded data, DST + */ +static ssize_t cobs_decode(uint8_t* dst, const uint8_t* src, size_t src_len) { + // Pointer to end of source array + const uint8_t* end = src + src_len; + // Size counter of decoded array to return + ssize_t out_len = 0; + + while (src < end) { + int num_bytes_to_copy = *src; + src++; + for (int i = 1; i < num_bytes_to_copy; i++) { + *dst = *src; + dst++; + src++; + out_len++; + if (src > end) { // Bad packet + return 0; + } + } + if (src != end) { + // Start decoding a new block, putting back the zero + *dst++ = 0; + out_len++; + } + } + return out_len; +} + +// ************************* MESSAGE CONSTRUCTORS *************************** // + +message_t* make_empty(ssize_t payload_size) { + message_t* msg = malloc(sizeof(message_t)); + if (msg == NULL) { + log_printf(FATAL, "make_empty: Failed to malloc"); + exit(1); + } + msg->message_id = NOP; + msg->payload = malloc(payload_size); + if (msg->payload == NULL) { + log_printf(FATAL, "make_empty: Failed to malloc"); + exit(1); + } + msg->payload_length = 0; + msg->max_payload_length = payload_size; + return msg; +} + +message_t* make_ping() { + message_t* ping = malloc(sizeof(message_t)); + if (ping == NULL) { + log_printf(FATAL, "make_ping: Failed to malloc"); + exit(1); + } + ping->message_id = PING; + ping->payload = NULL; + ping->payload_length = 0; + ping->max_payload_length = 0; + return ping; +} + +message_t* make_subscription_request(uint8_t dev_type, uint32_t pmap, uint16_t interval) { + device_t* dev = get_device(dev_type); + // Don't read from non-existent params + pmap &= ((uint32_t) -1) >> (MAX_PARAMS - dev->num_params); // Set non-existent params to 0 + // Set non-read-able params to 0 + for (int i = 0; i < MAX_PARAMS; i++) { + if (dev->params[i].read == 0) { + pmap &= ~(1 << i); // Set bit i to 0 + } + } + message_t* sub_request = malloc(sizeof(message_t)); + if (sub_request == NULL) { + log_printf(FATAL, "make_subscription_request: Failed to malloc"); + exit(1); + } + sub_request->message_id = SUBSCRIPTION_REQUEST; + sub_request->payload = malloc(BITMAP_SIZE + INTERVAL_SIZE); + if (sub_request->payload == NULL) { + log_printf(FATAL, "make_subscription_request: Failed to malloc"); + exit(1); + } + sub_request->payload_length = 0; + sub_request->max_payload_length = BITMAP_SIZE + INTERVAL_SIZE; + + int status = 0; + status += append_payload(sub_request, (uint8_t*) &pmap, BITMAP_SIZE); + status += append_payload(sub_request, (uint8_t*) &interval, INTERVAL_SIZE); + return (status == 0) ? sub_request : NULL; +} + +message_t* make_device_write(uint8_t dev_type, uint32_t pmap, param_val_t param_values[]) { + device_t* dev = get_device(dev_type); + // Don't write to non-existent params + pmap &= ((uint32_t) -1) >> (MAX_PARAMS - dev->num_params); // Set non-existent params to 0 + // Set non-writeable params to 0 + for (int i = 0; ((pmap >> i) > 0) && (i < MAX_PARAMS); i++) { + if (dev->params[i].write == 0) { + pmap &= ~(1 << i); // Set bit i to 0 + } + } + // Build the message + message_t* dev_write = malloc(sizeof(message_t)); + if (dev_write == NULL) { + log_printf(FATAL, "make_device_write: Failed to malloc"); + exit(1); + } + dev_write->message_id = DEVICE_WRITE; + dev_write->payload_length = 0; + dev_write->max_payload_length = device_write_payload_size(dev_type, pmap); + dev_write->payload = malloc(dev_write->max_payload_length); + if (dev_write->payload == NULL) { + log_printf(FATAL, "make_device_write: Failed to malloc"); + exit(1); + } + int status = 0; + // Append the param bitmap + status += append_payload(dev_write, (uint8_t*) &pmap, BITMAP_SIZE); + // Append the param values to the payload + for (int i = 0; ((pmap >> i) > 0) && (i < MAX_PARAMS); i++) { + // If the parameter is on in the bitmap, include it + if ((1 << i) & pmap) { + switch (dev->params[i].type) { + case INT: + status += append_payload(dev_write, (uint8_t*) &(param_values[i].p_i), sizeof(int32_t)); + break; + case FLOAT: + status += append_payload(dev_write, (uint8_t*) &(param_values[i].p_f), sizeof(float)); + break; + case BOOL: + status += append_payload(dev_write, (uint8_t*) &(param_values[i].p_b), sizeof(uint8_t)); + break; + } + } + } + return (status == 0) ? dev_write : NULL; +} + +void destroy_message(message_t* message) { + free(message->payload); + free(message); +} + +// ********************* SERIALIZE AND PARSE MESSAGES *********************** // + +size_t calc_max_cobs_msg_length(message_t* msg) { + size_t required_packet_length = MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE + msg->payload_length + CHECKSUM_SIZE; + // Cobs encoding a length N message adds overhead of at most ceil(N/254) + size_t cobs_length = required_packet_length + (required_packet_length / 254) + 1; + /* Add 2 additional bytes to the buffer for use in message_to_bytes() + * 0th byte will be 0x0 indicating the start of a packet. + * 1st byte will hold the actual length from cobs encoding */ + return DELIMITER_SIZE + COBS_LENGTH_SIZE + cobs_length; +} + +ssize_t message_to_bytes(message_t* msg, uint8_t cobs_encoded[], size_t len) { + size_t required_length = calc_max_cobs_msg_length(msg); + if (len < required_length) { + return -1; + } + // Build an intermediate byte array to hold the serialized message to be encoded + uint8_t* data = malloc(MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE + msg->payload_length + CHECKSUM_SIZE); + if (data == NULL) { + log_printf(FATAL, "message_to_bytes: Failed to malloc"); + exit(1); + } + data[0] = msg->message_id; + data[1] = msg->payload_length; + for (int i = 0; i < msg->payload_length; i++) { + data[i + MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE] = msg->payload[i]; + } + data[MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE + msg->payload_length] = checksum(data, MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE + msg->payload_length); + + // Encode the intermediate byte array into output buffer + cobs_encoded[0] = 0x00; + int cobs_len = cobs_encode(&cobs_encoded[2], data, MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE + msg->payload_length + CHECKSUM_SIZE); + free(data); + cobs_encoded[1] = cobs_len; + return DELIMITER_SIZE + COBS_LENGTH_SIZE + cobs_len; +} + +int parse_message(uint8_t data[], message_t* msg_to_fill) { + uint8_t cobs_len = data[1]; + uint8_t* decoded = malloc(cobs_len); // Actual number of bytes populated will be a couple less due to overhead + if (decoded == NULL) { + log_printf(FATAL, "parse_message: Failed to malloc"); + exit(1); + } + int ret = cobs_decode(decoded, &data[2], cobs_len); + if (ret < (MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE + CHECKSUM_SIZE)) { + // Smaller than valid message + free(decoded); + return 3; + } else if (ret > (MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE + MAX_PAYLOAD_SIZE + CHECKSUM_SIZE)) { + // Larger than the largest valid message + free(decoded); + return 3; + } + msg_to_fill->message_id = decoded[0]; + msg_to_fill->payload_length = 0; + msg_to_fill->max_payload_length = decoded[MESSAGE_ID_SIZE]; + ret = append_payload(msg_to_fill, &decoded[MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE], msg_to_fill->max_payload_length); + if (ret != 0) { + log_printf(ERROR, "parse_message: Overwrote to payload\n"); + return 2; + } + uint8_t expected_checksum = checksum(decoded, MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE + msg_to_fill->payload_length); + uint8_t received_checksum = decoded[MESSAGE_ID_SIZE + PAYLOAD_LENGTH_SIZE + msg_to_fill->payload_length]; + if (expected_checksum != received_checksum) { + log_printf(ERROR, "parse_message: Expected checksum 0x%02X. Received 0x%02X\n", expected_checksum, received_checksum); + } + free(decoded); + return (expected_checksum != received_checksum) ? 1 : 0; +} + +void parse_device_data(uint8_t dev_type, message_t* dev_data, param_val_t vals[]) { + device_t* dev = get_device(dev_type); + // Bitmap is stored in the first 32 bits of the payload + uint32_t bitmap = *((uint32_t*) dev_data->payload); + /* Iterate through device's parameters. If bit is off, continue + * If bit is on, determine how much to read from the payload then put it in VALS in the appropriate field */ + uint8_t* payload_ptr = &(dev_data->payload[BITMAP_SIZE]); // Start the pointer at the beginning of the values (skip the bitmap) + for (int i = 0; ((bitmap >> i) > 0) && (i < MAX_PARAMS); i++) { + // If bit is on, parameter is included in the payload + if ((1 << i) & bitmap) { + switch (dev->params[i].type) { + case INT: + vals[i].p_i = *((int32_t*) payload_ptr); + payload_ptr += sizeof(int32_t) / sizeof(uint8_t); + break; + case FLOAT: + vals[i].p_f = *((float*) payload_ptr); + payload_ptr += sizeof(float) / sizeof(uint8_t); + break; + case BOOL: + vals[i].p_b = *payload_ptr; + payload_ptr += sizeof(uint8_t) / sizeof(uint8_t); + break; + } + } + } +} diff --git a/shepherd/sensors/message.h b/shepherd/sensors/message.h new file mode 100644 index 00000000..4813229d --- /dev/null +++ b/shepherd/sensors/message.h @@ -0,0 +1,178 @@ +/** + * Serves as a utility API for DEV_HANDLER + * Functions to build, encode, decode, serialize, and parse messages + * Each message, when serialized is in the following format: + * [delimiter][Length of cobs encoded message][Cobs encoded message] + * The cobs encoded message, when decoded is in the following format: + * [message id][payload length][payload][checksum] + */ + +#ifndef MESSAGE_H +#define MESSAGE_H + +#include "shepherd_util.h" + +/* The maximum number of milliseconds to wait between each PING from a device + * Waiting for this long will exit all threads for that device (doing cleanup as necessary) */ +#define TIMEOUT 1000 + +// The number of milliseconds between each PING sent to the device +#define PING_FREQ 250 + +// The size in bytes of the message delimiter +#define DELIMITER_SIZE 1 +// The size in bytes of the section specifying the length of the cobs encoded message +#define COBS_LENGTH_SIZE 1 +// The size in bytes of the section specifying the type of message being encoded +#define MESSAGE_ID_SIZE 1 +// The size in bytes of the section specifying the length of the payload in the message +#define PAYLOAD_LENGTH_SIZE 1 +// The size in bytes of the param bitmap in SUBSCRIPTION_REQUEST, DEVICE_WRITE and DEVICE_DATA payloads +#define BITMAP_SIZE (MAX_PARAMS / 8) +// The size in bytes of the section specifying the interval for SUBSCRIPTION_REQUEST +#define INTERVAL_SIZE 2 +// The size in bytes of the section specifying the device id for ACKNOWLEDGEMENT +#define DEVICE_ID_SIZE 10 +// The size in bytes of the section specifying the checksum of the message id, the payload length, and the payload itself +#define CHECKSUM_SIZE 1 +// The length of the largest payload in bytes, which may be reached for DEVICE_WRITE and DEVICE_DATA message types. +#define MAX_PAYLOAD_SIZE (BITMAP_SIZE + (MAX_PARAMS * sizeof(float))) // Bitmap + Each param (may be floats) + +// The types of messages +typedef enum { + NOP = 0x00, // Dummy message + PING = 0x01, // To lowcar + ACKNOWLEDGEMENT = 0x02, // To dev handler + SUBSCRIPTION_REQUEST = 0x03, // To lowcar + DEVICE_WRITE = 0x04, // To lowcar + DEVICE_DATA = 0x05, // To dev handler + LOG = 0x06 // To dev handler +} message_id_t; + +// A struct defining a message to be sent over serial +typedef struct { + message_id_t message_id; + uint8_t* payload; // Array of bytes + size_t payload_length; // The current number of bytes in payload + size_t max_payload_length; // The maximum length of the payload for the specific message_id +} message_t; + +// ******************************** Utility ********************************* // + +/** + * Prints a byte array byte by byte in hex + * Arguments: + * data: Byte array to be printed + * len: The length of the data array + */ +void print_bytes(uint8_t* data, size_t len); + +// ************************* MESSAGE CONSTRUCTORS *************************** // +// Messages built from these constructors MUST be decalloated with destroy_message() + +/** + * Utility function to allocate memory for an empty message + * To be used with parse_message() + * Arguments: + * payload_size: The size of memory to allocate for the payload + * Returns: + * A message of type NOP, payload_length 0, and max_payload_length PAYLOAD_SIZE + */ +message_t* make_empty(ssize_t payload_size); + +/** + * Builds a PING message + * Returns: + * A message of type PING + * payload_length 0 + * max_payload_length 0 + */ +message_t* make_ping(); + +/** + * Builds a SUBSCRIPTION_REQUEST + * Arguments: + * dev_type: The type of device. Used to verify params are readable + * pmap: param bitmap indicating which params should be subscribed to + * interval: The number of milliseconds to wait between each DEVICE_DATA + * Returns: + * A message of type SUBSCRIPTION_REQUEST + * Payload: bitmap, then interval + * payload_length: sizeof(pmap) + sizeof(interval) + * max_payload_length: same as above + */ +message_t* make_subscription_request(uint8_t dev_type, uint32_t pmap, uint16_t interval); + +/** + * Builds a DEVICE_WRITE + * Arguments: + * dev_type: The type of device. Used to verify params are writeable + * pmap: param bitmap indicating which parameters will be written to + * param_values: An array of the parameter values. + * The i-th bit in PMAP is on if and only if its value is in the i-th index of PARAM_VALUES + * Returns: + * A message of type DEVICE_WRITE + * Payload: pmap followed by, each of the param_values specified + * payload_length: sizeof(pmap) + sizeof(all the values in PARAM_VALUES) + * max_payload_length: same as above + */ +message_t* make_device_write(uint8_t dev_type, uint32_t pmap, param_val_t param_values[]); + +/** + * Frees the memory allocated for the message struct and its payload. + * Arguments: + * message: The message to have its memory deallocated. + */ +void destroy_message(message_t* message); + +// ********************* SERIALIZE AND PARSE MESSAGES *********************** // + +/** + * Calculates the largest length possible of a cobs-encoded msg + * Used when allocating a buffer to parse a message into via parse_message() + * Arguments: + * msg: The message to be serialized to a byte array + * Returns: + * The size of a byte array that should be allocated to serialize the message into + */ +size_t calc_max_cobs_msg_length(message_t* msg); + +/** + * Serializes then cobs encodes a message into a byte array + * Arguments: + * msg: the message to serialize + * cobs_encoded: empty buffer to be filled with the cobs-encoded message + * len: the length of COBS_ENCODED. Should be at least calc_max_cobs_msg_length(msg) + * Returns: + * The size of COBS_ENCODED that was actually populated + * -1 if len is too small (less than calc_max_cobs_msg_length) + */ +ssize_t message_to_bytes(message_t* msg, uint8_t cobs_encoded[], size_t len); + +/** + * Cobs decodes a byte array and populates the fields of input message + * Arguments: + * data: A byte array containing a cobs encoded message. + * data[0] should be the delimiter. data[1] should be cobs_len + * empty_msg: A message to be populated. + * Payload must be properly allocated memory. Use make_empty() + * Returns: + * 0 if successful parsing + * 1 if incorrect checksum + * 2 if max_payload_length is too small + * 3 if invalid message (decoded message has invalid length) + */ +int parse_message(uint8_t data[], message_t* empty_msg); + +/** + * Reads the parameter values from a DEVICE_DATA message into param_val_t[] + * Arguments: + * dev_type: The type of the device that the message was sent from + * dev_data: The DEVICE_DATA message to unpack + * vals: An array of param_val_t structs to be populated with the values from the message. + * NOTE: The length of vals MUST be at LEAST the number of params sent in the DEVICE_DATA message + * Allocate MAX_PARAMS param_val_t structs to guarantee this + */ +void parse_device_data(uint8_t dev_type, message_t* dev_data, param_val_t vals[]); + +#endif diff --git a/shepherd/sensors/sensor_interface.py b/shepherd/sensors/sensor_interface.py new file mode 100644 index 00000000..c66bbf14 --- /dev/null +++ b/shepherd/sensors/sensor_interface.py @@ -0,0 +1,176 @@ +""" +A process that is the interface between YDL and shared memory. +It parses device commands from YDL and writes it to shared memory. +It reads device data from shared memory and publishes it to YDL if necessary. +- Ex: Provide the device uid and param index of a button +- This will write to YDL if the button has been pressed. +""" +import threading +import time +import queue +# import pyximport +# pyximport.install() +import shm_api +import sys +sys.path.insert(1, '../') +from debouncer import Debouncer +from ydl import ( + ydl_send, + ydl_start_read +) +from typing import List, Union +from sensors import ( + arduinos, + translate_ydl_message, + HEADER_MAPPINGS, + HEADER_COMMAND_MAPPINGS, + Parameter, + previous_debounced_value, + LowcarMessage +) +from utils import ( + YDL_TARGETS, +) +#YDL -> TURN_ON_LIGHT, {light: 8} +#YDL -> SET_TRAFFIC_LIGHT, {color: "green"} + +# START_BUTTON_PRESSED {} -> YDL + +# figure out three things: 1) which device talking to, 2) which param of device to modify 3) value + +# TURN_ON_LIGHT, {light: 8, brightness: 10} + +#arduino 1: lights [1,2,3,6,7] +#arduino 2: lights [4,5,8], traffic_light + +############################# NON-EVERGREEN FUNCTIONS ############################# + + +""" +A dictionary containing which parameters to watch out for: +Ex: buttons + { + {dev_id}_{dev_type} : ["param_name", "param_name_2"] + } +""" + +PARAMS_TO_READ = { + arduino.get_identifier(): arduino.polling_parameters for arduino in arduinos.values() +} +print(f"PARAMS TO READ {PARAMS_TO_READ}") + +############################# START OF EVERGREEN FUNCTIONS ############################# + +def place_device_command(lowcar_message): + """ + Reads a Lowcar command and writes it to shared memory. + Arguments: + lowcar_message: An LowcarMessage that contains a command for lowcar + Returns: + None + """ + all_dev_ids = lowcar_message.get_dev_ids() + all_params = lowcar_message.get_params() + print(f"lowcar message is {lowcar_message}") + for i in range(len(all_dev_ids)): + dev_id = all_dev_ids[i] + params = all_params[i] + + for (name, val) in params.items(): + shm_api.set_value(dev_id, name, val) + +def read_device_data(dev_id, param_names): + """ + TODO: Update docs + Reads the current device data from shared memory. + Arguments: + dev_id: String of form {dev_type}_{dev_uid} + param_name: String corresponding to the name of the param type + Returns: + LowcarMessage: holds the param value + """ + param_vals = {} + for param_name in param_names: + param_vals[param_name] = shm_api.get_value(dev_id, param_name) + + return LowcarMessage([dev_id], [param_vals]) + +def thread_device_commander(): + """ + Thread function + Reads commands from YDL and writes it to shared memory. + """ + print("started commander thread") + events = queue.Queue() + ydl_start_read(YDL_TARGETS.SENSORS, events) + while True: + time.sleep(0.1) + payload = events.get(True) + print(payload) + if payload[0] in HEADER_MAPPINGS: + # TODO: remove this, after dealing with all exceptions properly + try: + lowcar_message = translate_ydl_message(payload) + place_device_command(lowcar_message) + except Exception as e: + print(f"Exception occured while translating {payload} to a LowcarMessage or placing command: {e}.") + elif payload[0] in HEADER_COMMAND_MAPPINGS: + HEADER_COMMAND_MAPPINGS[payload[0]](payload[1]) + +def thread_device_sentinel(params_to_read): + """ + Thread function + Polls shared memory for specified parameters. If they change, write to YDL. + Ex: Button is pressed + Arguments: + A dictionary containing which parameters to watch out for: + { + {dev_id}_{dev_type} : ["param_name", "param_name_2"] + } + Returns: + None + """ + print("started sentinel thread") + debouncer = Debouncer() + while (True): + # iterate through all devices + for device in params_to_read: + params: List[Parameter] = params_to_read[device] # list of parameters + dev_data: LowcarMessage = read_device_data(device, [param.name for param in params]) + + if not (len(dev_data.dev_ids) == 1) or not (len(dev_data.params) == 1): + raise Exception("LowcarMessage should only contain information about one device, because only one device is being queried.") + arduino = arduinos[device] + param_values = dev_data.params[0] # Dictionary: param_name -> value + for param_name, value in param_values.items(): + param = arduino.get_param(param_name) + # below logs are for testing buttons, slowly + # print(f"{param_name}: {value}") + # time.sleep(.2) + value = debouncer.debounce(value, param) + if value is None: + continue # unable to debounce anything meaningful + + if (param in previous_debounced_value and + param.is_state_change_significant(value, previous_debounced_value[param])): + args = param.ydl_message_from_state_change(value) + print(f"Sending YDL message with header {param.ydl_header} and args {args}") + ydl_send(YDL_TARGETS.SHEPHERD, param.ydl_header, args) + + previous_debounced_value[param] = value + +def main(): + try: + shm_api.dev_handler_connect() + device_thread = threading.Thread(target=thread_device_commander) + device_sentinel_thread = threading.Thread(target=thread_device_sentinel, args=(PARAMS_TO_READ,)) + device_thread.start() + device_sentinel_thread.start() + while True: + time.sleep(1) + except Exception as e: + print(f"Couldn't start device_commander() thread. Encountered exception: {e}") + raise e + +if __name__ == "__main__": + main() diff --git a/shepherd/sensors/setup.py b/shepherd/sensors/setup.py new file mode 100644 index 00000000..c1e6dd4e --- /dev/null +++ b/shepherd/sensors/setup.py @@ -0,0 +1,24 @@ +from setuptools import Extension, setup +from Cython.Build import cythonize +import sys + +sourcefiles = [ + "shm_wrapper.c", + "shepherd_util.c", + "shm_api.pyx" +] + +if sys.platform == 'linux': + libraries = ['rt'] +elif sys.platform == 'darwin': + libraries = [] +else: + raise NotImplementedError("Cython not implemented for OS other than Linux or MacOS") + +setup( + name="shm_api", + ext_modules = cythonize([ + Extension("shm_api", sources=sourcefiles, libraries=libraries) + ], compiler_directives={'language_level' : '3', 'boundscheck': False}), + zip_safe=False, +) diff --git a/shepherd/sensors/shepherd_util.c b/shepherd/sensors/shepherd_util.c new file mode 100644 index 00000000..15382932 --- /dev/null +++ b/shepherd/sensors/shepherd_util.c @@ -0,0 +1,338 @@ +#include "shepherd_util.h" + +// *************************** LOWCAR DEFINITIONS *************************** // + +device_t DummyDevice = { + .type = 0, + .name = "DummyDevice", + .num_params = 16, + .params = { + // Read-only + {.name = "RUNTIME", .type = INT, .read = 1, .write = 0}, + {.name = "SHEPHERD", .type = FLOAT, .read = 1, .write = 0}, + {.name = "DAWN", .type = BOOL, .read = 1, .write = 0}, + {.name = "DEVOPS", .type = INT, .read = 1, .write = 0}, + {.name = "ATLAS", .type = FLOAT, .read = 1, .write = 0}, + {.name = "INFRA", .type = BOOL, .read = 1, .write = 0}, + // Write-only + {.name = "SENS", .type = INT, .read = 0, .write = 1}, + {.name = "PDB", .type = FLOAT, .read = 0, .write = 1}, + {.name = "MECH", .type = BOOL, .read = 0, .write = 1}, + {.name = "CPR", .type = INT, .read = 0, .write = 1}, + {.name = "EDU", .type = FLOAT, .read = 0, .write = 1}, + {.name = "EXEC", .type = BOOL, .read = 0, .write = 1}, + // Read-able and write-able + {.name = "PIEF", .type = INT, .read = 1, .write = 1}, + {.name = "FUNTIME", .type = FLOAT, .read = 1, .write = 1}, + {.name = "SHEEP", .type = BOOL, .read = 1, .write = 1}, + {.name = "DUSK", .type = INT, .read = 1, .write = 1}, + }}; + +device_t Arduino1 = { + .type = 1, + .name = "Arduino1", + .num_params = 14, + .params = { + // Read-only + {.name = "button0", .type = BOOL, .read = 1, .write = 0}, + {.name = "button1", .type = BOOL, .read = 1, .write = 0}, + {.name = "button2", .type = BOOL, .read = 1, .write = 0}, + {.name = "button3", .type = BOOL, .read = 1, .write = 0}, + {.name = "button4", .type = BOOL, .read = 1, .write = 0}, + {.name = "button5", .type = BOOL, .read = 1, .write = 0}, + {.name = "button6", .type = BOOL, .read = 1, .write = 0}, + {.name = "light0", .type = BOOL, .read = 1, .write = 1}, + {.name = "light1", .type = BOOL, .read = 1, .write = 1}, + {.name = "light2", .type = BOOL, .read = 1, .write = 1}, + {.name = "light3", .type = BOOL, .read = 1, .write = 1}, + {.name = "light4", .type = BOOL, .read = 1, .write = 1}, + {.name = "light5", .type = BOOL, .read = 1, .write = 1}, + {.name = "light6", .type = BOOL, .read = 1, .write = 1}, + // Read-able and write-able + /* + {.name = "PIEF", .type = INT, .read = 1, .write = 1}, + {.name = "FUNTIME", .type = FLOAT, .read = 1, .write = 1}, + {.name = "SHEEP", .type = BOOL, .read = 1, .write = 1}, + {.name = "DUSK", .type = INT, .read = 1, .write = 1}, + */ + } +}; + +device_t Arduino2 = { + .type = 2, + .name = "Arduino2", + .num_params = 6, + .params = { + // temporarily disabled linebreak sensors + {.name = "desert_linebreak", .type = BOOL, .read = 1, .write = 0}, + {.name = "dehydration_linebreak", .type = BOOL, .read = 1, .write = 0}, + {.name = "hypothermia_linebreak", .type = BOOL, .read = 1, .write = 0}, + {.name = "airport_linebreak", .type = BOOL, .read = 1, .write = 0}, + + {.name = "fire_lever", .type = BOOL, .read = 1, .write = 0}, + + {.name = "fire_light", .type = BOOL, .read = 0, .write = 1}, + // Read-able and write-able + /* + {.name = "PIEF", .type = INT, .read = 1, .write = 1}, + {.name = "FUNTIME", .type = FLOAT, .read = 1, .write = 1}, + {.name = "SHEEP", .type = BOOL, .read = 1, .write = 1}, + {.name = "DUSK", .type = INT, .read = 1, .write = 1}, + */ + } +}; + +device_t Arduino3 = { + .type = 3, + .name = "Arduino3", + .num_params = 4, + .params = { + // disable linebreaks for now + {.name = "city_linebreak", .type = BOOL, .read = 1, .write = 0}, + {.name = "traffic_linebreak", .type = BOOL, .read = 1, .write = 0}, + {.name = "traffic_button", .type = BOOL, .read = 1, .write = 0}, + // 0 = off, 1 = red, 2 = green. + {.name = "traffic_light", .type = INT, .read = 0, .write = 1}, + } +}; + +device_t Arduino4 = { + .type = 4, + .name = "Arduino4", + .num_params = 1, + .params = { + // Read-only + {.name = "lasers", .type = BOOL, .read = 0, .write = 1}, + } +}; + +// *********************** VIRTUAL DEVICE DEFINITIONS *********************** // + +// A CustomDevice is unusual because the parameters are dynamic +// This is just here to avoid some errors when using get_device() on CustomDevice +// Used in niche situations (ex: UDP_TCP_CONVERTER_TEST)for Spring 2021 comp. +device_t CustomDevice = { + .type = MAX_DEVICES, // Also used this way in udp_conn.c + .name = "CustomDevice", + .num_params = 1, + .params = { + {.name = "time_ms", .type = INT, .read = 1, .write = 0}}}; + +device_t SoundDevice = { + .type = 59, + .name = "SoundDevice", + .num_params = 2, + .params = { + {.name = "SOCK_FD", .type = INT, .read = 0, .write = 0}, + {.name = "PITCH", .type = FLOAT, .read = 1, .write = 1}}}; + +device_t TimeTestDevice = { + .type = 60, + .name = "TimeTestDevice", + .num_params = 2, + .params = { + {.name = "GET_TIME", .type = BOOL, .read = 1, .write = 1}, + {.name = "TIMESTAMP", .type = INT, .read = 1, .write = 0}}}; + +device_t UnstableTestDevice = { + .type = 61, + .name = "UnstableTestDevice", + .num_params = 1, + .params = { + {.name = "NUM_ACTIONS", .type = INT, .read = 1, .write = 0}}}; + +device_t SimpleTestDevice = { + .type = 62, + .name = "SimpleTestDevice", + .num_params = 4, + .params = { + {.name = "INCREASING", .type = INT, .read = 1, .write = 0}, + {.name = "DOUBLING", .type = FLOAT, .read = 1, .write = 0}, + {.name = "FLIP_FLOP", .type = BOOL, .read = 1, .write = 0}, + {.name = "MY_INT", .type = INT, .read = 1, .write = 1}, + }}; + +device_t GeneralTestDevice = { + .type = 63, + .name = "GeneralTestDevice", + .num_params = 32, + .params = { + // Read-only + {.name = "INCREASING_ODD", .type = INT, .read = 1, .write = 0}, + {.name = "DECREASING_ODD", .type = INT, .read = 1, .write = 0}, + {.name = "INCREASING_EVEN", .type = INT, .read = 1, .write = 0}, + {.name = "DECREASING_EVEN", .type = INT, .read = 1, .write = 0}, + {.name = "INCREASING_FLIP", .type = INT, .read = 1, .write = 0}, + {.name = "ALWAYS_LEET", .type = INT, .read = 1, .write = 0}, + {.name = "DOUBLING", .type = FLOAT, .read = 1, .write = 0}, + {.name = "DOUBLING_NEG", .type = FLOAT, .read = 1, .write = 0}, + {.name = "HALFING", .type = FLOAT, .read = 1, .write = 0}, + {.name = "HALFING_NEG", .type = FLOAT, .read = 1, .write = 0}, + {.name = "EXP_ONE_PT_ONE", .type = FLOAT, .read = 1, .write = 0}, + {.name = "EXP_ONE_PT_TWO", .type = FLOAT, .read = 1, .write = 0}, + {.name = "ALWAYS_PI", .type = FLOAT, .read = 1, .write = 0}, + {.name = "FLIP_FLOP", .type = BOOL, .read = 1, .write = 0}, + {.name = "ALWAYS_TRUE", .type = BOOL, .read = 1, .write = 0}, + {.name = "ALWAYS_FALSE", .type = BOOL, .read = 1, .write = 0}, + // Read and Write + {.name = "RED_INT", .type = INT, .read = 1, .write = 1}, + {.name = "ORANGE_INT", .type = INT, .read = 1, .write = 1}, + {.name = "GREEN_INT", .type = INT, .read = 1, .write = 1}, + {.name = "BLUE_INT", .type = INT, .read = 1, .write = 1}, + {.name = "PURPLE_INT", .type = INT, .read = 1, .write = 1}, + {.name = "RED_FLOAT", .type = FLOAT, .read = 1, .write = 1}, + {.name = "ORANGE_FLOAT", .type = FLOAT, .read = 1, .write = 1}, + {.name = "GREEN_FLOAT", .type = FLOAT, .read = 1, .write = 1}, + {.name = "BLUE_FLOAT", .type = FLOAT, .read = 1, .write = 1}, + {.name = "PURPLE_FLOAT", .type = FLOAT, .read = 1, .write = 1}, + {.name = "RED_BOOL", .type = BOOL, .read = 1, .write = 1}, + {.name = "ORANGE_BOOL", .type = BOOL, .read = 1, .write = 1}, + {.name = "GREEN_BOOL", .type = BOOL, .read = 1, .write = 1}, + {.name = "BLUE_BOOL", .type = BOOL, .read = 1, .write = 1}, + {.name = "PURPLE_BOOL", .type = BOOL, .read = 1, .write = 1}, + {.name = "YELLOW_BOOL", .type = BOOL, .read = 1, .write = 1}}}; + +// ************************ DEVICE UTILITY FUNCTIONS ************************ // + +// An array that holds pointers to the structs of each lowcar device +device_t* DEVICES[DEVICES_LENGTH] = {0}; +// A hack to initialize DEVICES. https://stackoverflow.com/a/6991475 +__attribute__((constructor)) void devices_arr_init() { + DEVICES[DummyDevice.type] = &DummyDevice; + DEVICES[CustomDevice.type] = &CustomDevice; + DEVICES[SoundDevice.type] = &SoundDevice; + DEVICES[TimeTestDevice.type] = &TimeTestDevice; + DEVICES[UnstableTestDevice.type] = &UnstableTestDevice; + DEVICES[SimpleTestDevice.type] = &SimpleTestDevice; + DEVICES[GeneralTestDevice.type] = &GeneralTestDevice; + DEVICES[Arduino1.type] = &Arduino1; + DEVICES[Arduino2.type] = &Arduino2; + DEVICES[Arduino3.type] = &Arduino3; + DEVICES[Arduino4.type] = &Arduino4; +} + +device_t* get_device(uint8_t dev_type) { + if (0 <= dev_type && dev_type < DEVICES_LENGTH) { + return DEVICES[dev_type]; + } + return NULL; +} + +uint8_t device_name_to_type(char* dev_name) { + for (int i = 0; i < DEVICES_LENGTH; i++) { + if (DEVICES[i] != NULL && strcmp(DEVICES[i]->name, dev_name) == 0) { + return i; + } + } + return -1; +} + +char* get_device_name(uint8_t dev_type) { + device_t* device = get_device(dev_type); + if (device == NULL) { + return NULL; + } + return device->name; +} + +param_desc_t* get_param_desc(uint8_t dev_type, char* param_name) { + device_t* device = get_device(dev_type); + if (device == NULL) { + return NULL; + } + for (int i = 0; i < device->num_params; i++) { + if (strcmp(param_name, device->params[i].name) == 0) { + return &device->params[i]; + } + } + return NULL; +} + +int8_t get_param_idx(uint8_t dev_type, char* param_name) { + device_t* device = get_device(dev_type); + if (device == NULL) { + return -1; + } + for (int i = 0; i < device->num_params; i++) { + if (strcmp(param_name, device->params[i].name) == 0) { + return i; + } + } + return -1; +} + +// ********************************** TIME ********************************** // + +/* Returns the number of milliseconds since the Unix Epoch */ +uint64_t millis() { + struct timeval time; // Holds the current time in seconds + microseconds + gettimeofday(&time, NULL); + uint64_t s1 = (uint64_t)(time.tv_sec) * 1000; // Convert seconds to milliseconds + uint64_t s2 = (time.tv_usec / 1000); // Convert microseconds to milliseconds + return s1 + s2; +} + +// ********************* READ/WRITE TO FILE DESCRIPTOR ********************** // + +int readn(int fd, void* buf, uint16_t n) { + uint16_t n_remain = n; + uint16_t n_read; + char* curr = buf; + + while (n_remain > 0) { + if ((n_read = read(fd, curr, n_remain)) < 0) { + if (errno == EINTR) { // read interrupted by signal; read again + n_read = 0; + } else { + perror("read"); + return -1; + } + } else if (n_read == 0) { // received EOF + return 0; + } + n_remain -= n_read; + curr += n_read; + } + return (n - n_remain); +} + +int writen(int fd, void* buf, uint16_t n) { + uint16_t n_remain = n; + uint16_t n_written; + char* curr = buf; + + while (n_remain > 0) { + if ((n_written = write(fd, curr, n_remain)) <= 0) { + if (n_written < 0 && errno == EINTR) { // write interrupted by signal, write again + n_written = 0; + } else { + perror("write"); + return -1; + } + } + n_remain -= n_written; + curr += n_written; + } + return n; +} + + + +/************** LOG_PRINTF STUB ***********/ + +void log_printf(int level, char* format, ...) { + va_list args; + va_start(args, format); + + int length = strlen(format) + 1; + char buf[length + 1]; + strncpy(buf, format, length); + if (buf[length-2] != '\n') { + buf[length-1] = '\n'; + buf[length] = 0; + } + vprintf(buf, args); + va_end(args); +} + + diff --git a/shepherd/sensors/shepherd_util.h b/shepherd/sensors/shepherd_util.h new file mode 100644 index 00000000..77de0bcd --- /dev/null +++ b/shepherd/sensors/shepherd_util.h @@ -0,0 +1,189 @@ +#ifndef UTIL_H +#define UTIL_H + +// ************************* COMMON STANDARD HEADERS ************************ // + +#include // for various error numbers (EINTR, EAGAIN, EPIPE, etc.) and errno +#include // for file opening constants (O_CREAT, O_RDONLY, O_RDWR, etc.) +#include // for thread-functions: pthread_create, pthread_cancel, pthread_join, etc. +#include // for setting signal function (setting signal handler) +#include // for uint32_t, int16_t, uint8_t, etc. +#include // for printf, perror, fprintf, fopen, etc. +#include // for malloc, free +#include // for strcmp, strcpy, strlen, memset, etc. +#include // for bind, listen, accept, socket +#include // for various system-related types and functions (sem_t, mkfifo) +#include // for time-related structures and time functions +#include // for struct sockaddr_un +#include // for F_OK, R_OK, SEEK_SET, SEEK_END, access, ftruncate, read, write, etc. +#include // for va_start, va_end + + +// ***************************** DEFINED CONSTANTS ************************** // + +#define MAX_DEVICES 32 // Maximum number of connected devices +#define MAX_PARAMS 32 // Maximum number of parameters supported + +#define DEVICES_LENGTH 64 // The largest device type number + 1. + +#define NUM_DESC_FIELDS_PERM 6 // Number of permanent fields in the robot description +#define NUM_DESC_FIELDS_TEMP 3 // Number of temporary fields in the robot description (related to game) +#define NUM_DESC_FIELDS (NUM_DESC_FIELDS_PERM + NUM_DESC_FIELDS_TEMP) + +// The interval (microseconds) at which we wait between detecting connects/disconnects +#define POLL_INTERVAL 200000 + + +/******************** LOG_PRINTF STUB *******************/ + +// Copied from logger.h for Shepherd +typedef enum log_level { + DEBUG, + INFO, + WARN, + PYTHON, + ERROR, + FATAL +} log_level_t; + +void log_printf(int level, char* format, ...); + +// ***************************** CUSTOM TYPES ***************************** // + +// enumerated names of processes +typedef enum process { + DEV_HANDLER, + EXECUTOR, + NET_HANDLER, + SHM, + TEST +} process_t; + +// enumerated names for the data types device parameters can be +typedef enum param_type { + INT, + FLOAT, + BOOL +} param_type_t; + + +// hold a single param value. One-to-one mapping to param_val_t enum +typedef union { + int32_t p_i; // data if int + float p_f; // data if float + uint8_t p_b; // data if bool +} param_val_t; + +// holds the device identification information of a single device +typedef struct dev_id { + uint8_t type; // The device type (ex: 0 for DummyDevice) + uint8_t year; // The device year + uint64_t uid; // The unique id for a specific device assigned when flashed +} dev_id_t; + +// A struct defining the type and access level of a device parameter +typedef struct param_desc { + char* name; // Parameter name + param_type_t type; // Data type + uint8_t read; // Whether or not the param is readable + uint8_t write; // Whether or not the param is writable +} param_desc_t; + +// A struct defining a kind of device (ex: LimitSwitch, KoalaBear) +typedef struct device { + uint8_t type; // The type of device + char* name; // Device name (ex: "LimitSwitch") + uint8_t num_params; // Number of params the device has + param_desc_t params[MAX_PARAMS]; // Description of each parameter +} device_t; + +// ************************ DEVICE UTILITY FUNCTIONS ************************ // + +/** + * Returns a pointer to a device given its type. + * Arguments: + * dev_type: The device type + * Returns: + * A pointer to the device, or + * NULL if the device doesn't exist. + */ +device_t* get_device(uint8_t dev_type); + +/** + * Returns the device type of a device given its name. + * Arguments: + * dev_name: The name of the device + * Returns: + * The device type, or + * -1 if the device doesn't exist. + */ +uint8_t device_name_to_type(char* dev_name); + +/** + * Returns the name of the device given its type. + * Arguments: + * dev_type: The device type + * Returns: + * The device name, or + * NULL if the device doesn't exist. + */ +char* get_device_name(uint8_t dev_type); + +/** + * Returns a parameter descriptor. + * Arguments: + * dev_type: The device type with the parameter of interest + * param_name: The name of the parameter + * Returns: + * The parameter descriptor, or + * NULL if the device doesn't exist or the parameter doesn't exist + */ +param_desc_t* get_param_desc(uint8_t dev_type, char* param_name); + +/** + * Return the index of a parameter. + * Arguments: + * dev_type: The device type + * param_name: The name of the parameter + * Returns: + * The parameter's index for the device, or + * -1 if the device doesn't exist or the parameter doesn't exist. + */ +int8_t get_param_idx(uint8_t dev_type, char* param_name); + + +// ********************************** TIME ********************************** // + +/** + * Returns the number of milliseconds since the Unix Epoch. + */ +uint64_t millis(); + +// ********************* READ/WRITE TO FILE DESCRIPTOR ********************** // + +/** + * Read n bytes from fd into buf; return number of bytes read into buf (deals with interrupts and unbuffered reads) + * Arguments: + * fd: file descriptor to read from + * buf: pointer to location to copy read bytes into + * n: number of bytes to read + * Returns: + * > 0: number of bytes read into buf + * 0: read EOF on fd + * -1: read errored out + */ +int readn(int fd, void* buf, uint16_t n); + +/** + * Read n bytes from buf to fd; return number of bytes written to buf (deals with interrupts and unbuffered writes) + * Arguments: + * fd: file descriptor to write to + * buf: pointer to location to read from + * n: number of bytes to write + * Returns: + * >= 0: number of bytes written into buf + * -1: write errored out + */ +int writen(int fd, void* buf, uint16_t n); + +#endif diff --git a/shepherd/sensors/shm_api.pyx b/shepherd/sensors/shm_api.pyx new file mode 100644 index 00000000..ce89528b --- /dev/null +++ b/shepherd/sensors/shm_api.pyx @@ -0,0 +1,147 @@ +from libc.stdint cimport * +from cpython.mem cimport PyMem_Malloc, PyMem_Free + +cdef extern from "shepherd_util.h": + int MAX_DEVICES + int MAX_PARAMS + ctypedef enum process_t: + EXECUTOR + ctypedef struct device_t: + char* name + uint8_t type + uint8_t num_params + param_desc_t* params + ctypedef union param_val_t: + int32_t p_i + float p_f + uint8_t p_b + ctypedef enum param_type_t: + INT, FLOAT, BOOL + ctypedef struct param_desc_t: + char* name + param_type_t type + uint8_t read + uint8_t write + device_t* get_device(uint8_t dev_type) + +cdef extern from "shm_wrapper.h" nogil: + ctypedef enum stream_t: + DATA, COMMAND + void shm_connect() + int device_read_uid(uint64_t device_uid, process_t process, stream_t stream, uint32_t params_to_read, param_val_t *params) + int device_write_uid(uint64_t device_uid, process_t process, stream_t stream, uint32_t params_to_write, param_val_t *params) + int place_sub_request (uint64_t dev_uid, process_t process, uint32_t params_to_sub) + + +## Functions needed by Shepherd + +class DeviceError(Exception): + """An exception caused by using an invalid device. """ + + +cpdef get_value(str device_id, str param_name): + """ + Get a device value. + + Args: + device_id: string of the format '{device_type}_{device_uid}' where device_type is LowCar device ID and device_uid is 64-bit UID assigned by LowCar. + param_name: Name of param to get. + """ + # Convert Python string to C string + cdef bytes param = param_name.encode('utf-8') + + # Getting device identification info + splits = device_id.split('_') + if len(splits) != 2: + raise ValueError(f"First argument device_id must be of the form _") + cdef int device_type = int(splits[0]) + cdef uint64_t device_uid = int(splits[1]) + + # Getting parameter info from the name + cdef device_t* device = get_device(device_type) + if not device: + raise DeviceError(f"Device with uid {device_uid} has invalid type {device_type}") + cdef param_type_t param_type + cdef int8_t param_idx = -1 + for i in range(device.num_params): + if device.params[i].name == param: + param_idx = i + param_type = device.params[i].type + break + if param_idx == -1: + raise DeviceError(f"Invalid device parameter {param_name} for device type {device.name.decode('utf-8')}({device_type})") + + # Allocate memory for parameter + cdef param_val_t* param_value = PyMem_Malloc(sizeof(param_val_t) * MAX_PARAMS) + if not param_value: + raise MemoryError("Could not allocate memory to get device value.") + + # Read and return parameter + cdef int err = device_read_uid(device_uid, EXECUTOR, DATA, 1 << param_idx, param_value) + if err == -1: + PyMem_Free(param_value) + raise DeviceError(f"Device with type {device.name.decode('utf-8')}({device_type}) and uid {device_uid} isn't connected to the robot") + + if param_type == INT: + ret = param_value[param_idx].p_i + elif param_type == FLOAT: + ret = param_value[param_idx].p_f + elif param_type == BOOL: + ret = bool(param_value[param_idx].p_b) + PyMem_Free(param_value) + return ret + + +cpdef void set_value(str device_id, str param_name, value) except *: + """ + Set a device parameter. + + Args: + device_id: string of the format '{device_type}_{device_uid}' where device_type is LowCar device ID and device_uid is 64-bit UID assigned by LowCar. + param_name: Name of param to get. + value: Value to set for the param. + """ + # Convert Python string to C string + cdef bytes param = param_name.encode('utf-8') + + # Get device identification info + splits = device_id.split('_') + if len(splits) != 2: + raise ValueError(f"First argument device_id must be of the form _") + cdef int device_type = int(splits[0]) + cdef uint64_t device_uid = int(splits[1]) + + # Getting parameter info from the name + cdef device_t* device = get_device(device_type) + if not device: + raise DeviceError(f"Device with uid {device_uid} has invalid type {device_type}") + cdef param_type_t param_type + cdef int8_t param_idx = -1 + + for i in range(device.num_params): + if device.params[i].name == param: + param_idx = i + param_type = device.params[i].type + break + + if param_idx == -1: + raise DeviceError(f"Invalid device parameter {param_name} for device type {device.name.decode('utf-8')}({device_type})") + + # Allocating memory for parameter to write + cdef param_val_t* param_value = PyMem_Malloc(sizeof(param_val_t) * MAX_PARAMS) + if not param_value: + raise MemoryError("Could not allocate memory to get device value.") + + if param_type == INT: + param_value[param_idx].p_i = value + elif param_type == FLOAT: + param_value[param_idx].p_f = value + elif param_type == BOOL: + param_value[param_idx].p_b = int(value) + cdef int err = device_write_uid(device_uid, EXECUTOR, COMMAND, 1 << param_idx, param_value) + PyMem_Free(param_value) + if err == -1: + raise DeviceError(f"Device with type {device.name.decode('utf-8')}({device_type}) and uid {device_uid} isn't connected to the robot") + +cpdef void dev_handler_connect() except *: + shm_connect() \ No newline at end of file diff --git a/shepherd/sensors/shm_wrapper.c b/shepherd/sensors/shm_wrapper.c new file mode 100644 index 00000000..8e2f2f27 --- /dev/null +++ b/shepherd/sensors/shm_wrapper.c @@ -0,0 +1,705 @@ +#include "shm_wrapper.h" + +// *********************************** WRAPPER-SPECIFIC GLOBAL VARS **************************************** // + +dual_sem_t sems[MAX_DEVICES]; // array of semaphores, two for each possible device (one for data and one for commands) +dev_shm_t* dev_shm_ptr; // points to memory-mapped shared memory block for device data and commands +sem_t* catalog_sem; // semaphore used as a mutex on the catalog +sem_t* cmd_map_sem; // semaphore used as a mutex on the command bitmap +sem_t* sub_map_sem; // semaphore used as a mutex on the subscription bitmap + +// ****************************************** SEMAPHORE UTILITIES ***************************************** // + +/** + * Custom wrapper function for sem_wait. Prints out descriptive logging message on failure + * Arguments: + * sem: pointer to a semaphore to wait on + * sem_desc: string that describes the semaphore being waited on, displayed with error message + */ +static void my_sem_wait(sem_t* sem, char* sem_desc) { + if (sem_wait(sem) == -1) { + log_printf(ERROR, "sem_wait: %s. %s", sem_desc, strerror(errno)); + } +} + +/** + * Custom wrapper function for sem_post. Prints out descriptive logging message on failure + * Arguments: + * sem: pointer to a semaphore to post + * sem_desc: string that describes the semaphore being posted, displayed with error message + */ +static void my_sem_post(sem_t* sem, char* sem_desc) { + if (sem_post(sem) == -1) { + log_printf(ERROR, "sem_post: %s. %s", sem_desc, strerror(errno)); + } +} + +/** + * Custom wrapper function for sem_open with create flag. Prints out descriptive logging message on failure + * exits (not being able to create and open a semaphore is fatal). + * Arguments: + * sem_name: name of a semaphore to be created and opened + * sem_desc: string that describes the semaphore being created and opened, displayed with error message + * Returns a pointer to the semaphore that was created and opened. + */ +// my_sem_open_create from shm_start.c +static sem_t* my_sem_open(char* sem_name, char* sem_desc) { + sem_t* ret; + if ((ret = sem_open(sem_name, O_CREAT, 0660, 1)) == SEM_FAILED) { + log_printf(FATAL, "sem_open: %s. %s", sem_desc, strerror(errno)); + exit(1); + } + return ret; +} + +/** + * Custom wrapper function for sem_close. Prints out descriptive logging message on failure + * Arguments: + * sem: pointer to a semaphore to close + * sem_desc: string that describes the semaphore being closed, displayed with error message + */ +static void my_sem_close(sem_t* sem, char* sem_desc) { + if (sem_close(sem) == -1) { + log_printf(ERROR, "sem_close: %s. %s", sem_desc, strerror(errno)); + } +} + +/** + * Unlinks a semaphore (so it will be removed from the system once all processes call sem_close() on it) + * Arguments: + * sem_name: name of semaphore (ex. "/ct-mutex") + * sem_desc: description of semaphore, to be printed with descriptive error message should sem_unlink() fail + */ +static void my_sem_unlink(char* sem_name, char* sem_desc) { + if (sem_unlink(sem_name) == -1) { + log_printf(ERROR, "sem_unlink: %s. %s", sem_desc, strerror(errno)); + } +} + +/** + * Unlinks a block of shared memory (so it will be removed from the system once all processes call shm_close() on it) + * Arguments: + * shm_name: name of shared memory block (ex. "/dev-shm") + * shm_desc: description of shared memory block, to be printed with descriptive error message should shm_unlink() fail + */ +static void my_shm_unlink(char* shm_name, char* shm_desc) { + if (shm_unlink(shm_name) == -1) { + log_printf(ERROR, "shm_unlink: %s. %s", shm_desc, strerror(errno)); + } +} + +// ******************************************** HELPER FUNCTIONS ****************************************** // + +/** + * Function that does the actual reading into shared memory for device_read and device_read_uid + * Takes care of updating the param bitmap for fast transfer of commands from executor to device handler + * Arguments: + * dev_ix: device index of the device whose data is being requested + * process: the calling process, one of DEV_HANDLER, EXECUTOR, or NET_HANDLER + * stream: the requested block to read from, one of DATA, COMMAND + * params_to_read: bitmap representing which params to be read (nonexistent params should have corresponding bits set to 0) + * params: pointer to array of param_val_t's that is at least as long as highest requested param number + * device data will be read into the corresponding param_val_t's + */ +static void device_read_helper(int dev_ix, process_t process, stream_t stream, uint32_t params_to_read, param_val_t* params) { + // grab semaphore for the appropriate stream and device + if (stream == DATA) { + my_sem_wait(sems[dev_ix].data_sem, "data sem @device_read"); + } else { + my_sem_wait(sems[dev_ix].command_sem, "command sem @device_read"); + } + + // read all requested params + for (int i = 0; i < MAX_PARAMS; i++) { + if (params_to_read & (1 << i)) { + params[i] = dev_shm_ptr->params[stream][dev_ix][i]; + } + } + + // if the device handler has processed the command, then turn off the change + // if stream = downstream and process = dev_handler then also update params bitmap + if (process == DEV_HANDLER && stream == COMMAND) { + // wait on cmd_map_sem + my_sem_wait(cmd_map_sem, "cmd_map_sem @device_read"); + + dev_shm_ptr->cmd_map[0] &= (~(1 << dev_ix)); // turn off changed device bit in cmd_map[0] + dev_shm_ptr->cmd_map[dev_ix + 1] &= (~params_to_read); // turn off bits for params that were changed and then read in cmd_map[dev_ix + 1] + + // release cmd_map_sem + my_sem_post(cmd_map_sem, "cmd_map_sem @device_read"); + } + + // release semaphore for appropriate stream and device + if (stream == DATA) { + my_sem_post(sems[dev_ix].data_sem, "data sem @device_read"); + } else { + my_sem_post(sems[dev_ix].command_sem, "command sem @device_read"); + } +} + +/** + * Function that does the actual writing into shared memory for device_write and device_write_uid + * Takes care of updating the param bitmap for fast transfer of commands from executor to device handler + * Grabs either one or two semaphores depending on calling process and stream requested. + * Arguments: + * dev_ix: device index of the device whose data is being written + * process: the calling process, one of DEV_HANDLER, EXECUTOR, or NET_HANDLER + * stream: the requested block to write to, one of DATA, COMMAND + * params_to_read: bitmap representing which params to be written (nonexistent params should have corresponding bits set to 0) + * params: pointer to array of param_val_t's that is at least as long as highest requested param number + * device data will be written into the corresponding param_val_t's + */ +static void device_write_helper(int dev_ix, process_t process, stream_t stream, uint32_t params_to_write, param_val_t* params) { + // grab semaphore for the appropriate stream and device + if (stream == DATA) { + my_sem_wait(sems[dev_ix].data_sem, "data sem @device_write"); + } else { + my_sem_wait(sems[dev_ix].command_sem, "command sem @device_write"); + } + + // write all requested params + for (int i = 0; i < MAX_PARAMS; i++) { + if (params_to_write & (1 << i)) { + dev_shm_ptr->params[stream][dev_ix][i] = params[i]; + } + } + + // turn on flag if executor is sending a command to the device + // if stream = downstream and process = executor then also update params bitmap + if (process == EXECUTOR && stream == COMMAND) { + // wait on cmd_map_sem + my_sem_wait(cmd_map_sem, "cmd_map_sem @device_write"); + + dev_shm_ptr->cmd_map[0] |= (1 << dev_ix); // turn on changed device bit in cmd_map[0] + dev_shm_ptr->cmd_map[dev_ix + 1] |= params_to_write; // turn on bits for params that were written in cmd_map[dev_ix + 1] + + // release cmd_map_sem + my_sem_post(cmd_map_sem, "cmd_map_sem @device_write"); + } + + // release semaphore for appropriate stream and device + if (stream == DATA) { + my_sem_post(sems[dev_ix].data_sem, "data sem @device_write"); + } else { + my_sem_post(sems[dev_ix].command_sem, "command sem @device_write"); + } +} + +/** + * This function will be called when process that called shm_init() exits + * Closes all semaphores; unmaps all shared memory (but does not unlink anything) + */ +// From shm_stop.c +static void shm_close() { + // close all the semaphores + for (int i = 0; i < MAX_DEVICES; i++) { + my_sem_close(sems[i].data_sem, "data sem"); + my_sem_close(sems[i].command_sem, "command sem"); + } + my_sem_close(catalog_sem, "catalog sem"); + my_sem_close(cmd_map_sem, "cmd map sem"); + my_sem_close(sub_map_sem, "sub map sem"); + + // unmap all shared memory blocks + if (munmap(dev_shm_ptr, sizeof(dev_shm_t)) == -1) { + log_printf(ERROR, "munmap: dev_shm. %s", strerror(errno)); + } + + // unlink shared memory blocks + my_shm_unlink(DEV_SHM_NAME, "dev_shm"); + + // unlink all semaphores + my_sem_unlink(CATALOG_MUTEX_NAME, "catalog mutex"); + my_sem_unlink(CMDMAP_MUTEX_NAME, "cmd map mutex"); + my_sem_unlink(SUBMAP_MUTEX_NAME, "sub map mutex"); + + char sname[SNAME_SIZE]; + for (int i = 0; i < MAX_DEVICES; i++) { + generate_sem_name(DATA, i, sname); + if (sem_unlink((const char*) sname) == -1) { + log_printf(ERROR, "sem_unlink: data_sem for dev_ix %d. %s", i, strerror(errno)); + } + generate_sem_name(COMMAND, i, sname); + if (sem_unlink((const char*) sname) == -1) { + log_printf(ERROR, "sem_unlink: command_sem for dev_ix %d. %s", i, strerror(errno)); + } + } +} + +static void shm_disconnect() { + // close all the semaphores + for (int i = 0; i < MAX_DEVICES; i++) { + my_sem_close(sems[i].data_sem, "data sem"); + my_sem_close(sems[i].command_sem, "command sem"); + } + my_sem_close(catalog_sem, "catalog sem"); + my_sem_close(cmd_map_sem, "cmd map sem"); + my_sem_close(sub_map_sem, "sub map sem"); + + // unmap all shared memory blocks + if (munmap(dev_shm_ptr, sizeof(dev_shm_t)) == -1) { + log_printf(ERROR, "munmap: dev_shm. %s", strerror(errno)); + } +} + +// ************************************ PUBLIC WRAPPER FUNCTIONS ****************************************** // + +void generate_sem_name(stream_t stream, int dev_ix, char* name) { + if (stream == DATA) { + sprintf(name, "/data_sem_%d", dev_ix); + } else if (stream == COMMAND) { + sprintf(name, "/command_sem_%d", dev_ix); + } +} + +int get_dev_ix_from_uid(uint64_t dev_uid) { + int dev_ix = -1; + + for (int i = 0; i < MAX_DEVICES; i++) { + if ((dev_shm_ptr->catalog & (1 << i)) && (dev_shm_ptr->dev_ids[i].uid == dev_uid)) { + dev_ix = i; + break; + } + } + return dev_ix; +} + +// from shm_start.c +void shm_init() { + int fd_shm; // file descriptor of the memory-mapped shared memory + char sname[SNAME_SIZE]; // for holding semaphore names + + // open all the semaphores + catalog_sem = my_sem_open(CATALOG_MUTEX_NAME, "catalog mutex"); + cmd_map_sem = my_sem_open(CMDMAP_MUTEX_NAME, "cmd map mutex"); + sub_map_sem = my_sem_open(SUBMAP_MUTEX_NAME, "sub map mutex"); + for (int i = 0; i < MAX_DEVICES; i++) { + generate_sem_name(DATA, i, sname); // get the data name + if ((sems[i].data_sem = sem_open((const char*) sname, O_CREAT, 0660, 1)) == SEM_FAILED) { + log_printf(FATAL, "sem_open: data sem for dev_ix %d: %s", i, strerror(errno)); + exit(1); + } + generate_sem_name(COMMAND, i, sname); // get the command name + if ((sems[i].command_sem = sem_open((const char*) sname, O_CREAT, 0660, 1)) == SEM_FAILED) { + log_printf(FATAL, "sem_open: command sem for dev_ix %d: %s", i, strerror(errno)); + exit(1); + } + } + + // create device shm block + if ((fd_shm = shm_open(DEV_SHM_NAME, O_RDWR | O_CREAT, 0660)) == -1) { + log_printf(FATAL, "shm_open devices: %s", strerror(errno)); + exit(1); + } + if (ftruncate(fd_shm, sizeof(dev_shm_t)) == -1) { + log_printf(FATAL, "ftruncate devices: %s", strerror(errno)); + exit(1); + } + if ((dev_shm_ptr = mmap(NULL, sizeof(dev_shm_t), PROT_READ | PROT_WRITE, MAP_SHARED, fd_shm, 0)) == MAP_FAILED) { + log_printf(FATAL, "mmap devices: %s", strerror(errno)); + exit(1); + } + if (close(fd_shm) == -1) { + log_printf(ERROR, "close devices: %s", strerror(errno)); + } + + // initialize everything + dev_shm_ptr->catalog = 0; + for (int i = 0; i < MAX_DEVICES + 1; i++) { + dev_shm_ptr->cmd_map[i] = 0; + dev_shm_ptr->net_sub_map[i] = -1; // By default, subscribe to all parameters on all devices + dev_shm_ptr->exec_sub_map[i] = -1; + } + + atexit(shm_close); +} + +void shm_connect() { + int fd_shm; // file descriptor of the memory-mapped shared memory + char sname[SNAME_SIZE]; // for holding semaphore names + + // open all the semaphores + catalog_sem = my_sem_open(CATALOG_MUTEX_NAME, "catalog mutex"); + cmd_map_sem = my_sem_open(CMDMAP_MUTEX_NAME, "cmd map mutex"); + sub_map_sem = my_sem_open(SUBMAP_MUTEX_NAME, "sub map mutex"); + for (int i = 0; i < MAX_DEVICES; i++) { + generate_sem_name(DATA, i, sname); // get the data name + if ((sems[i].data_sem = sem_open((const char*) sname, 0, 0, 0)) == SEM_FAILED) { + log_printf(ERROR, "sem_open: data sem for dev_ix %d: %s", i, strerror(errno)); + } + generate_sem_name(COMMAND, i, sname); // get the command name + if ((sems[i].command_sem = sem_open((const char*) sname, 0, 0, 0)) == SEM_FAILED) { + log_printf(ERROR, "sem_open: command sem for dev_ix %d: %s", i, strerror(errno)); + } + } + + // open dev shm block and map to client process virtual memory + if ((fd_shm = shm_open(DEV_SHM_NAME, O_RDWR, 0)) == -1) { // no O_CREAT + log_printf(FATAL, "shm_open dev_shm: %s", strerror(errno)); + exit(1); + } + if ((dev_shm_ptr = mmap(NULL, sizeof(dev_shm_t), PROT_READ | PROT_WRITE, MAP_SHARED, fd_shm, 0)) == MAP_FAILED) { + log_printf(FATAL, "mmap dev_shm: %s", strerror(errno)); + exit(1); + } + if (close(fd_shm) == -1) { + log_printf(ERROR, "close dev_shm: %s", strerror(errno)); + } + + atexit(shm_disconnect); +} + +void device_connect(dev_id_t* dev_id, int* dev_ix) { + // wait on catalog_sem + my_sem_wait(catalog_sem, "catalog_sem"); + + // find a valid dev_ix + for (*dev_ix = 0; *dev_ix < MAX_DEVICES; (*dev_ix)++) { + if (!(dev_shm_ptr->catalog & (1 << *dev_ix))) { // if the spot at dev_ix is free + break; + } + } + if (*dev_ix == MAX_DEVICES) { + log_printf(ERROR, "device_connect: maximum device limit %d reached, connection refused", MAX_DEVICES); + my_sem_post(catalog_sem, "catalog_sem"); // release the catalog semaphore + *dev_ix = -1; + return; + } + + // wait on associated data and command sems + my_sem_wait(sems[*dev_ix].data_sem, "data_sem"); + my_sem_wait(sems[*dev_ix].command_sem, "command_sem"); + my_sem_wait(sub_map_sem, "sub_map_sem"); + + // fill in dev_id for that device with provided values + dev_shm_ptr->dev_ids[*dev_ix].type = dev_id->type; + dev_shm_ptr->dev_ids[*dev_ix].year = dev_id->year; + dev_shm_ptr->dev_ids[*dev_ix].uid = dev_id->uid; + + // update the catalog + dev_shm_ptr->catalog |= (1 << *dev_ix); + + // reset param values to 0 + for (int i = 0; i < MAX_PARAMS; i++) { + dev_shm_ptr->params[DATA][*dev_ix][i] = (const param_val_t){0}; + dev_shm_ptr->params[COMMAND][*dev_ix][i] = (const param_val_t){0}; + } + + // reset executor subscriptions to on + dev_shm_ptr->exec_sub_map[0] |= 1 << *dev_ix; + dev_shm_ptr->exec_sub_map[*dev_ix + 1] = -1; + + // reset net_handler subscriptions to on + dev_shm_ptr->net_sub_map[0] |= 1 << *dev_ix; + dev_shm_ptr->net_sub_map[*dev_ix + 1] = -1; + + my_sem_post(sub_map_sem, "sub_map_sem"); + // release associated data and command sems + my_sem_post(sems[*dev_ix].data_sem, "data_sem"); + my_sem_post(sems[*dev_ix].command_sem, "command_sem"); + + // release catalog_sem + my_sem_post(catalog_sem, "catalog_sem"); +} + +void device_disconnect(int dev_ix) { + // wait on catalog_sem + my_sem_wait(catalog_sem, "catalog_sem"); + + // wait on associated data and command sems + my_sem_wait(sems[dev_ix].data_sem, "data_sem"); + my_sem_wait(sems[dev_ix].command_sem, "command_sem"); + my_sem_wait(cmd_map_sem, "cmd_map_sem"); + + // update the catalog + dev_shm_ptr->catalog &= (~(1 << dev_ix)); + + // reset cmd bitmap values to 0 + dev_shm_ptr->cmd_map[0] &= (~(1 << dev_ix)); // reset the changed bit flag in cmd_map[0] + dev_shm_ptr->cmd_map[dev_ix + 1] = 0; // turn off all changed bits for the device + + my_sem_post(cmd_map_sem, "cmd_map_sem"); + // release associated upstream and downstream sems + my_sem_post(sems[dev_ix].data_sem, "data_sem"); + my_sem_post(sems[dev_ix].command_sem, "command_sem"); + + // release catalog_sem + my_sem_post(catalog_sem, "catalog_sem"); +} + +int device_read(int dev_ix, process_t process, stream_t stream, uint32_t params_to_read, param_val_t* params) { + // check catalog to see if dev_ix is valid, if not then return immediately + if (!(dev_shm_ptr->catalog & (1 << dev_ix))) { + log_printf(ERROR, "device_read: no device at dev_ix = %d, read failed", dev_ix); + return -1; + } + + // call the helper to do the actual reading + device_read_helper(dev_ix, process, stream, params_to_read, params); + return 0; +} + +int device_read_uid(uint64_t dev_uid, process_t process, stream_t stream, uint32_t params_to_read, param_val_t* params) { + int dev_ix; + + // if device doesn't exist, return immediately + if ((dev_ix = get_dev_ix_from_uid(dev_uid)) == -1) { + log_printf(ERROR, "device_read_uid: no device at dev_uid = %llu, read failed", dev_uid); + return -1; + } + + // call the helper to do the actual reading + device_read_helper(dev_ix, process, stream, params_to_read, params); + return 0; +} + +int device_write(int dev_ix, process_t process, stream_t stream, uint32_t params_to_write, param_val_t* params) { + // check catalog to see if dev_ix is valid, if not then return immediately + if (!(dev_shm_ptr->catalog & (1 << dev_ix))) { + log_printf(ERROR, "device_write: no device at dev_ix = %d, write failed", dev_ix); + return -1; + } + + // call the helper to do the actual reading + device_write_helper(dev_ix, process, stream, params_to_write, params); + return 0; +} + +int device_write_uid(uint64_t dev_uid, process_t process, stream_t stream, uint32_t params_to_write, param_val_t* params) { + int dev_ix; + + // if device doesn't exist, return immediately + if ((dev_ix = get_dev_ix_from_uid(dev_uid)) == -1) { + log_printf(ERROR, "device_write_uid: no device at dev_uid = %llu, write failed", dev_uid); + return -1; + } + + // call the helper to do the actual reading + device_write_helper(dev_ix, process, stream, params_to_write, params); + return 0; +} + +int place_sub_request(uint64_t dev_uid, process_t process, uint32_t params_to_sub) { + int dev_ix; // dev_ix that we're operating on + uint32_t* curr_sub_map; // sub map that we're operating on + + // validate request and obtain dev_ix, sub_map + if (process != NET_HANDLER && process != EXECUTOR) { + log_printf(ERROR, "place_sub_request: calling place_sub_request from incorrect process %u", process); + return -1; + } + if ((dev_ix = get_dev_ix_from_uid(dev_uid)) == -1) { + log_printf(ERROR, "place_sub_request: no device at dev_uid = %llu, sub request failed", dev_uid); + return -1; + } + curr_sub_map = (process == NET_HANDLER) ? dev_shm_ptr->net_sub_map : dev_shm_ptr->exec_sub_map; + + // wait on sub_map_sem + my_sem_wait(sub_map_sem, "sub_map_sem"); + + // only fill in params_to_sub if it's different from what's already there + if (curr_sub_map[dev_ix + 1] != params_to_sub) { + curr_sub_map[dev_ix + 1] = params_to_sub; + curr_sub_map[0] |= (1 << dev_ix); // turn on corresponding bit + } + + // release sub_map_sem + my_sem_post(sub_map_sem, "sub_map_sem"); + + return 0; +} + +void get_sub_requests(uint32_t sub_map[MAX_DEVICES + 1], process_t process) { + // wait on sub_map_sem + my_sem_wait(sub_map_sem, "sub_map_sem"); + + // bitwise OR the changes into sub_map[0]; if no changes then return + if (process == NET_HANDLER) { + sub_map[0] = dev_shm_ptr->net_sub_map[0]; + } else if (process == EXECUTOR) { + sub_map[0] = dev_shm_ptr->exec_sub_map[0]; + } else { + sub_map[0] = dev_shm_ptr->net_sub_map[0] | dev_shm_ptr->exec_sub_map[0]; + } + + if (sub_map[0] == 0 && process == DEV_HANDLER) { + my_sem_post(sub_map_sem, "sub_map_sem"); + return; + } + + // bitwise OR each valid element together into sub_map[i] + for (int i = 1; i < MAX_DEVICES + 1; i++) { + if (dev_shm_ptr->catalog & (1 << (i - 1))) { // if device exists + if (process == NET_HANDLER) { + sub_map[i] = dev_shm_ptr->net_sub_map[i]; + } else if (process == EXECUTOR) { + sub_map[i] = dev_shm_ptr->exec_sub_map[i]; + } else { + sub_map[i] = dev_shm_ptr->net_sub_map[i] | dev_shm_ptr->exec_sub_map[i]; + } + } else { + sub_map[0] &= ~(1 << (i - 1)); + sub_map[i] = 0; + } + } + + if (process == DEV_HANDLER) { + // reset the change indicator eleemnts in net_sub_map and exec_sub_map + dev_shm_ptr->net_sub_map[0] = dev_shm_ptr->exec_sub_map[0] = 0; + } + + // release sub_map_sem + my_sem_post(sub_map_sem, "sub_map_sem"); +} + +void get_cmd_map(uint32_t bitmap[MAX_DEVICES + 1]) { + // wait on cmd_map_sem + my_sem_wait(cmd_map_sem, "cmd_map_sem"); + + for (int i = 0; i < MAX_DEVICES + 1; i++) { + bitmap[i] = dev_shm_ptr->cmd_map[i]; + } + + // release cmd_map_sem + my_sem_post(cmd_map_sem, "cmd_map_sem"); +} + +void get_device_identifiers(dev_id_t dev_ids[MAX_DEVICES]) { + // wait on catalog_sem + my_sem_wait(catalog_sem, "catalog_sem"); + + for (int i = 0; i < MAX_DEVICES; i++) { + dev_ids[i] = dev_shm_ptr->dev_ids[i]; + } + + // release catalog_sem + my_sem_post(catalog_sem, "catalog_sem"); +} + +void get_catalog(uint32_t* catalog) { + // wait on catalog_sem + my_sem_wait(catalog_sem, "catalog_sem"); + + *catalog = dev_shm_ptr->catalog; + + // release catalog_sem + my_sem_post(catalog_sem, "catalog_sem"); +} + + + +/***************** PRINTING FUNCTIONS *******************/ + +/** + * Function that prints a bitmap in human-readable way (in binary, with literal 1s and 0s) + * Arguments: + * num_bits: number of bits to print (max 32 bitS, although that can be increased by changing the size of BITMAP) + * bitmap: bitmap to print + */ +static void print_bitmap(int num_bits, uint32_t bitmap) { + for (int i = 0; i < num_bits; i++) { + printf("%d", (bitmap & (1 << i)) ? 1 : 0); + } + printf("\n"); +} + +void print_cmd_map() { + uint32_t cmd_map[MAX_DEVICES + 1]; + get_cmd_map(cmd_map); + + printf("Changed devices: "); + print_bitmap(MAX_DEVICES, cmd_map[0]); + + printf("Changed params:\n"); + for (int i = 1; i < 33; i++) { + if (cmd_map[0] & (1 << (i - 1))) { + printf("\tDevice %d: ", i - 1); + print_bitmap(MAX_PARAMS, cmd_map[i]); + } + } +} + +void print_sub_map(process_t process) { + uint32_t sub_map[MAX_DEVICES + 1]; + get_sub_requests(sub_map, process); + + printf("Requested devices: "); + print_bitmap(MAX_DEVICES, sub_map[0]); + + printf("Requested params:\n"); + for (int i = 1; i < 33; i++) { + if (sub_map[0] & (1 << (i - 1))) { + printf("\tDevice %d: ", i - 1); + print_bitmap(MAX_PARAMS, sub_map[i]); + } + } +} + +void print_dev_ids() { + dev_id_t dev_ids[MAX_DEVICES]; + uint32_t catalog; + + get_device_identifiers(dev_ids); + get_catalog(&catalog); + + if (catalog == 0) { + printf("no connected devices\n"); + } else { + for (int i = 0; i < MAX_DEVICES; i++) { + if (catalog & (1 << i)) { + printf("dev_ix = %d: type = %d, year = %d, uid = %llu\n", i, dev_ids[i].type, dev_ids[i].year, dev_ids[i].uid); + } + } + } +} + +void print_catalog() { + uint32_t catalog; + + get_catalog(&catalog); + print_bitmap(MAX_DEVICES, catalog); +} + +void print_params(uint32_t devices) { + dev_id_t dev_ids[MAX_DEVICES]; + uint32_t catalog; + device_t* device; + + get_device_identifiers(dev_ids); + get_catalog(&catalog); + + for (int i = 0; i < MAX_DEVICES; i++) { + if ((catalog & (1 << i)) && (devices & (1 << i))) { + device = get_device(dev_ids[i].type); + if (device == NULL) { + printf("Device at index %d with type %d is invalid\n", i, dev_ids[i].type); + continue; + } + printf("dev_ix = %d: name = %s, type = %d, year = %d, uid = %llu\n", i, device->name, dev_ids[i].type, dev_ids[i].year, dev_ids[i].uid); + + for (int s = 0; s < 2; s++) { + //print out the stream header + if (s == 0) { + printf("\tDATA stream:\n"); + } else if (s == 1) { + printf("\tCOMMAND stream:\n"); + } + + //print all params for the device for that stream + for (int j = 0; j < device->num_params; j++) { + switch (device->params[j].type) { + case INT: + printf("\t\tparam_idx = %d, name = %s, value = %d\n", j, device->params[j].name, dev_shm_ptr->params[s][i][j].p_i); + break; + case FLOAT: + printf("\t\tparam_idx = %d, name = %s, value = %f\n", j, device->params[j].name, dev_shm_ptr->params[s][i][j].p_f); + break; + case BOOL: + printf("\t\tparam_idx = %d, name = %s, value = %s\n", j, device->params[j].name, (dev_shm_ptr->params[s][i][j].p_b) ? "True" : "False"); + break; + } + } + } + } + } +} diff --git a/shepherd/sensors/shm_wrapper.h b/shepherd/sensors/shm_wrapper.h new file mode 100644 index 00000000..9399caff --- /dev/null +++ b/shepherd/sensors/shm_wrapper.h @@ -0,0 +1,237 @@ +#ifndef SHM_WRAPPER_H +#define SHM_WRAPPER_H + +#include // for UCHAR_MAX +#include // for semaphores +#include // for posix shared memory + +#include "shepherd_util.h" // for runtime constants + +// names of various objects used in shm_wrapper; should not be used outside of shm_wrapper.c, shm_start.c, and shm_stop.c +#define DEV_SHM_NAME "/dev-shm" // name of shared memory block across devices +#define CATALOG_MUTEX_NAME "/cat-sem" // name of semaphore used as a mutex on the catalog +#define CMDMAP_MUTEX_NAME "/cmap-sem" // name of semaphore used as a mutex on the command bitmap +#define SUBMAP_MUTEX_NAME "/smap-sem" // name of semaphore used as a mutex on the various subcription bitmaps + +#define SNAME_SIZE 32 // size of buffers that hold semaphore names, in bytes + +// *********************************** SHM TYPEDEFS ****************************************************** // + +// enumerated names for the two associated blocks per device +typedef enum stream { + DATA, + COMMAND +} stream_t; + +// shared memory block that holds device information, data, and commands has this structure +typedef struct { + uint32_t catalog; // catalog of valid devices + uint32_t cmd_map[MAX_DEVICES + 1]; // bitmap is 33 32-bit integers (changed devices and changed params of device commands from executor to dev_handler) + uint32_t net_sub_map[MAX_DEVICES + 1]; // bitmap is 33 32-bit integers (changed devices and changed params in which data net_handler is subscribed to) + uint32_t exec_sub_map[MAX_DEVICES + 1]; // bitmap is 33 32-bit integers (changed devices and changed params in which data executor is subscribed to) + param_val_t params[2][MAX_DEVICES][MAX_PARAMS]; // all the device parameter info, data and commands + dev_id_t dev_ids[MAX_DEVICES]; // all the device identification info +} dev_shm_t; + +// two mutex semaphores for each device +typedef struct { + sem_t* data_sem; // semaphore on the data stream of a device + sem_t* command_sem; // semaphore on the command stream of a device +} dual_sem_t; + + +// *********************************** SHM EXTERNAL VARIABLES ******************************************** // + +// DO NOT USE THESE UNDER NORMAL CIRCUMSTANCES +// THESE ARE ONLY USED TO SIMPLIFY CODE IN SHM_START AND SHM_STOP + +extern dual_sem_t sems[MAX_DEVICES]; // array of semaphores, two for each possible device (one for data and one for commands) +extern dev_shm_t* dev_shm_ptr; // points to memory-mapped shared memory block for device data and commands +extern sem_t* catalog_sem; // semaphore used as a mutex on the catalog +extern sem_t* cmd_map_sem; // semaphore used as a mutex on the command bitmap +extern sem_t* sub_map_sem; // semaphore used as a mutex on the subscription bitmap + +// ******************************************* WRAPPER FUNCTIONS ****************************************** // + +/** + * Function that generates a semaphore name for the data and command streams + * Should only be externally called by shm_start and shm_stop programs + * Arguments: + * stream: stream for which the semaphore is created (one of DATA or COMMAND) + * dev_ix: index of device whose STREAM is being created + * name: pointer to a buffer of size SNAME_SIZE into which the name of the semaphore will be put + */ +void generate_sem_name(stream_t stream, int dev_ix, char* name); + +/** + * Returns the index in the SHM block of the specified device if it exists (-1 if it doesn't) + * Arguments: + * dev_uid: 64-bit unique ID of the device + * Returns: device index in shared memory of the specified device, -1 if specified device is not in shared memory + */ +int get_dev_ix_from_uid(uint64_t dev_uid); + +/** + * Call this function from every process that wants to use the shared memory wrapper + * And shared memory has NOT been created + * No return value (will exit on fatal errors). + * Will configure process to close all shared memory and semaphores on process exit. + */ +void shm_init(); + +/** + * Call this function from every process that wants to use the shared memory wrapper + * And shared memory HAS been created + * No return value (will exit on fatal errors). + * Will configure process to close all shared memory and semaphores on process exit. + */ +void shm_connect(); + +/** + * Should only be called from device handler + * Selects the next available device index and assigns the newly connected device to that location. + * Turns on the associated bit in the catalog. + * Arguments: + * dev_id: device identification info for the device being connected (type, year, uid) + * dev_ix: the index that the device was assigned will be put here + * Returns device index of connected device in dev_ix on success; sets *dev_ix = -1 on failure + */ +void device_connect(dev_id_t* dev_id, int* dev_ix); + +/** + * Should only be called from device handler + * Disconnects a device with a given index by turning off the associated bit in the catalog. + * Arguments + * dev_ix: index of device in catalog to be disconnected + */ +void device_disconnect(int dev_ix); + +/** + * Should be called from every process wanting to read the device data + * Takes care of updating the param bitmap for fast transfer of commands from executor to device handler + * Arguments: + * dev_ix: device index of the device whose data is being requested + * process: the calling process, one of DEV_HANDLER, EXECUTOR, or NET_HANDLER + * stream: the requested block to read from, one of DATA, COMMAND + * params_to_read: bitmap representing which params to be read (nonexistent params should have corresponding bits set to 0) + * params: pointer to array of param_val_t's that is at least as long as highest requested param number + * device data will be read into the corresponding param_val_t's + * Returns: + * 0 on success + * -1 on failure (specified device is not connected in shm) + */ +int device_read(int dev_ix, process_t process, stream_t stream, uint32_t params_to_read, param_val_t* params); + +/** + * This function is the exact same as the above function, but instead uses the 64-bit device UID to identify + * the device that should be read, rather than the device index. + */ +int device_read_uid(uint64_t dev_uid, process_t process, stream_t stream, uint32_t params_to_read, param_val_t* params); + +/** + * Should be called from every process wanting to write to the device data + * Takes care of updating the param bitmap for fast transfer of commands from executor to device handler + * Grabs either one or two semaphores depending on calling process and stream requested. + * Arguments: + * dev_ix: device index of the device whose data is being written + * process: the calling process, one of DEV_HANDLER, EXECUTOR, or NET_HANDLER + * stream: the requested block to write to, one of DATA, COMMAND + * params_to_read: bitmap representing which params to be written (nonexistent params should have corresponding bits set to 0) + * params: pointer to array of param_val_t's that is at least as long as highest requested param number + * device data will be written into the corresponding param_val_t's + * Returns: + * 0 on success + * -1 on failure (specified device is not connected in shm) + */ +int device_write(int dev_ix, process_t process, stream_t stream, uint32_t params_to_write, param_val_t* params); + +/** + * This function is the exact same as the above function, but instead uses the 64-bit device UID to identify + * the device that should be written, rather than the device index. + */ +int device_write_uid(uint64_t dev_uid, process_t process, stream_t stream, uint32_t params_to_write, param_val_t* params); + +/** + * Send a sub request to dev_handler for a particular device. Takes care of updating the changed bits. + * Should only be called by executor and net_handler + * Arguments: + * dev_uid: unique 64-bit identifier of the device + * process: the calling process (will error if not EXECUTOR or NET_HANDLER) + * params_to_sub: bitmap representing params to subscribe to (nonexistent params should have corresponding bits set to 0) + * Returns: + * 0 on success + * -1 on failure (unrecognized process, or device is not connect in shm) + */ +int place_sub_request(uint64_t dev_uid, process_t process, uint32_t params_to_sub); + +/** + * Get current subscription requests for all devices. + * Arguments: + * sub_map[MAX_DEVICES + 1]: bitwise OR of the executor and net_handler sub_maps that will be put into this provided buffer + * (expects an array of 33 elements, where the 0th index is a bitmap indicating which devices require a new sub request to be sent, + * and the remaining 32 elements indicate what the subscription request to each device should be if there are changes) + * process: If EXECUTOR, fills with executor subscriptions. If NET_HANDLER, fills with net_handler subscriptions. + * If neither EXECUTOR nor NET_HANDLER, fill with subscriptions of both EXECUTOR and NET_HANDLER. + * If DEV_HANDLER, also reset bitmap indicating which devices have new subscriptions. + */ +void get_sub_requests(uint32_t sub_map[MAX_DEVICES + 1], process_t process); + +/** + * Should be called from all processes that want to know current state of the command map (i.e. device handler) + * Blocks on the command bitmap semaphore for obvious reasons + * Arguments: + * bitmap[MAX_DEVICES + 1]: pointer to array of 33 32-bit integers to copy the bitmap into. See the README for a + * description for how this bitmap works. + */ +void get_cmd_map(uint32_t bitmap[MAX_DEVICES + 1]); + +/** + * Should be called from all processes that want to know device identifiers of all currently connected devices + * Blocks on catalog semaphore for obvious reasons + * Arguments: + * dev_id_t dev_ids[MAX_DEVICES]: pointer to array of dev_id_t's to copy the information into + */ +void get_device_identifiers(dev_id_t dev_ids[MAX_DEVICES]); + +/** + * Should be called from all processes that want to know which dev_ix's are valid + * Blocks on catalog semaphore for obvious reasons + * Arguments: + * catalog: pointer to 32-bit integer into which the current catalog will be read into + */ +void get_catalog(uint32_t* catalog); + + +/********************* PRINTING FUNCTIONS *******************/ + +/** + * Prints the current values in the command bitmap + */ +void print_cmd_map(); + +/** + * Prints the current values in the subscription bitmap + */ +void print_sub_map(); + +/** + * Prints the device identification info of the currently attached devices + */ +void print_dev_ids(); + +/** + * Prints the catalog (i.e. how many and at which indices devices are currently attached) + */ +void print_catalog(); + +/** + * Prints the params of the specified devices + * Arguments: + * devices: bitmap specifying which devices should have their parameters printed + * (specifying nonexistent devices will be ignored without error) + */ +void print_params(uint32_t devices); + + + +#endif diff --git a/shepherd/server.py b/shepherd/server.py index 6b60c1a6..c004402b 100644 --- a/shepherd/server.py +++ b/shepherd/server.py @@ -1,84 +1,84 @@ import json -import threading -import time import queue -import gevent # pylint: disable=import-error -from flask import Flask, render_template # pylint: disable=import-error -from flask_socketio import SocketIO, emit, join_room, leave_room, send # pylint: disable=import-error -from Utils import * -from LCM import * +import hashlib +from flask import Flask, render_template, request +from flask_socketio import SocketIO +from utils import YDL_TARGETS, UI_PAGES +from ydl import ydl_send, ydl_start_read -HOST_URL = "127.0.0.1" +HOST_URL = "0.0.0.0" PORT = 5000 app = Flask(__name__) app.config['SECRET_KEY'] = 'omegalul!' -socketio = SocketIO(app, async_mode="gevent") +app.config['TEMPLATES_AUTO_RELOAD'] = True +socketio = SocketIO(app, async_mode="gevent", cors_allowed_origins="*") + @app.route('/') def hello_world(): return 'Hello, World!' -@app.route('/score_adjustment.html/') -def score_adjustment(): - return render_template('score_adjustment.html') - -@app.route('/staff_gui.html/') -def staff_gui(): - return render_template('staff_gui.html') -@app.route('/stage_control.html/') -def stage_control(): - return render_template('stage_control.html') +""" +checks to make sure p is the correct password +if you want to change the password, just change the hash +""" +def password(p): + if p is None: + return False + m = hashlib.sha256() + m.update((p + "cheese").encode("utf-8")) + return m.hexdigest() == \ + "44590c963be2a79f52c07f7a7572b3907bf5bb180d993bd31aab510d29bbfbd3" + +""" +routing for all ui pages. Gives "page not found" if +the page isn't in UI_PAGES, or prompts for password +if the page is password protected +""" +@app.route('/') +def give_page(subpath): + if subpath[-1] == "/": + subpath = subpath[:-1] + if subpath in UI_PAGES: + passed = (not UI_PAGES[subpath]) or password(request.cookies.get('password')) + return render_template(subpath if passed else "password.html") + return "oops page not found" + +@socketio.event +def connect(): + print('Established socketio connection') @socketio.on('join') def handle_join(client_name): - print('confirmed join: ' + client_name) - lcm_send(LCM_TARGETS.SHEPHERD, SHEPHERD_HEADER.REQUEST_CONNECTIONS) - -#Score Adjustment -@socketio.on('ui-to-server-scores') -def ui_to_server_scores(scores): - lcm_send(LCM_TARGETS.SHEPHERD, SHEPHERD_HEADER.SCORE_ADJUST, json.loads(scores)) - -@socketio.on('ui-to-server-score-request') -def ui_to_server_score_request(): - lcm_send(LCM_TARGETS.SHEPHERD, SHEPHERD_HEADER.GET_SCORES) - -#Main GUI -@socketio.on('ui-to-server-teams-info-request') -def ui_to_server_match_info_request(match_num_dict): - lcm_send(LCM_TARGETS.SHEPHERD, SHEPHERD_HEADER.GET_MATCH_INFO, json.loads(match_num_dict)) - print(json.loads(match_num_dict)) + print(f'confirmed join: {client_name}') -@socketio.on('ui-to-server-setup-match') -def ui_to_server_setup_match(teams_info): - lcm_send(LCM_TARGETS.SHEPHERD, SHEPHERD_HEADER.SETUP_MATCH, json.loads(teams_info)) +@socketio.on('ui-to-server') +def ui_to_server(p, header, args=None): + if not password(p): + return + if args is None: + ydl_send(YDL_TARGETS.SHEPHERD, header) + else: + ydl_send(YDL_TARGETS.SHEPHERD, header, json.loads(args)) -@socketio.on('ui-to-server-start-next-stage') -def ui_to_server_start_next_stage(): - lcm_send(LCM_TARGETS.SHEPHERD, SHEPHERD_HEADER.START_NEXT_STAGE) - -@socketio.on('ui-to-server-reset-match') -def ui_to_server_reset_match(): - lcm_send(LCM_TARGETS.SHEPHERD, SHEPHERD_HEADER.RESET_MATCH) def receiver(): - events = gevent.queue.Queue() - lcm_start_read(str.encode(LCM_TARGETS.UI), events) - + events = queue.Queue() + ydl_start_read(YDL_TARGETS.UI, events) while True: - - if not events.empty(): - event = events.get_nowait() + while not events.empty(): + event = events.get() print("RECEIVED:", event) - if event[0] == UI_HEADER.TEAMS_INFO: - socketio.emit('server-to-ui-teamsinfo', json.dumps(event[1], ensure_ascii=False)) - elif event[0] == UI_HEADER.SCORES: - socketio.emit('server-to-ui-scores', json.dumps(event[1], ensure_ascii=False)) - elif event[0] == UI_HEADER.CONNECTIONS: - socketio.emit('server-to-ui-connections', json.dumps(event[1], ensure_ascii=False)) + socketio.emit(event[0], json.dumps(event[1], ensure_ascii=False)) socketio.sleep(0.1) +if __name__ == "__main__": + print("Hello, world!") + print(f"Running server on port {PORT}.", + f"Go to http://localhost:{PORT}/staff_gui.html", + f"or http://localhost:{PORT}/scoreboard.html") + socketio.start_background_task(receiver) socketio.run(app, host=HOST_URL, port=PORT) diff --git a/shepherd/sheet.py b/shepherd/sheet.py new file mode 100644 index 00000000..c7dd317c --- /dev/null +++ b/shepherd/sheet.py @@ -0,0 +1,113 @@ +# installation: +# pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib +# source: https://developers.google.com/sheets/api/quickstart/python + +import os +import csv +import threading +from googleapiclient.discovery import build +from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from ydl import ydl_send +from utils import YDL_TARGETS, SHEPHERD_HEADER, CONSTANTS + +# If modifying these scopes, delete your previously saved credentials +# at USER_TOKEN_FILE +SCOPES = ['https://www.googleapis.com/auth/spreadsheets'] +CLIENT_SECRET_FILE = 'sheets/client_secret.json' +USER_TOKEN_FILE = "sheets/user_token.json" # user token; do not upload to github (.gitignore it) + + +class Sheet: + @staticmethod + def get_match(match_number): + ''' + Given a match number, gets match info and sends it back to Shepherd through ydl. + First attempts to use online spreadsheet, and falls back to csv file if offline + ''' + def bg_thread_work(): + game_data = [[]] + try: + spreadsheet = Sheet.__get_authorized_sheet() + game_data = spreadsheet.values().get(spreadsheetId=CONSTANTS.SPREADSHEET_ID, + range="Match Database!A2:M").execute()['values'] + except: # pylint: disable=bare-except + print('[error!] Google API has changed yet again, please fix Sheet.py') + print("Fetching data from offline csv file") + with open(CONSTANTS.CSV_FILE_NAME) as csv_file: + game_data = list(csv.reader(csv_file, delimiter=','))[1:] + + return_len = 12 + lst = [""] * return_len + for row in game_data: + if len(row) > 0 and row[0].isdigit() and int(row[0]) == match_number: + lst = row[1:return_len+1] + [""]*(return_len+1-len(row)) + break + teams = [{},{},{},{}] + for a in range(4): + teams[a]["team_num"] = int(lst[3*a]) if lst[3*a].isdigit() else -1 + teams[a]["team_name"] = lst[3*a+1] + teams[a]["robot_ip"] = lst[3*a+2] + ydl_send(YDL_TARGETS.SHEPHERD, SHEPHERD_HEADER.SET_TEAMS_INFO, {"teams": teams}) + + threading.Thread(target=bg_thread_work).start() + + @staticmethod + def write_scores(match_number, blue_score, gold_score): + def bg_thread_work(): + try: + Sheet.__write_online_scores(match_number, blue_score, gold_score) + except: # pylint: disable=bare-except + print('[error!] Google API has changed yet again, please fix Sheet.py') + print("Unable to write to spreadsheet") + threading.Thread(target=bg_thread_work).start() + + @staticmethod + def __get_authorized_sheet(): + """ + Gets valid user credentials from storage. + + If nothing has been stored, or if the stored credentials are invalid, + the OAuth2 flow is completed to obtain the new credentials. + + returns a google spreadsheet service thingy + """ + creds = None + if os.path.exists(USER_TOKEN_FILE): + creds = Credentials.from_authorized_user_file(USER_TOKEN_FILE, SCOPES) + # If there are no (valid) credentials available, let the user log in. + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES) + creds = flow.run_local_server(port=0) + with open(USER_TOKEN_FILE, 'w') as token: + print(f"Storing google auth credentials to {USER_TOKEN_FILE}") + token.write(creds.to_json()) + service = build('sheets', 'v4', credentials=creds) + return service.spreadsheets() # pylint: disable=no-member + + + @staticmethod + def __write_online_scores(match_number, blue_score, gold_score): + """ + A method that writes the scores to the sheet + """ + spreadsheet = Sheet.__get_authorized_sheet() + game_data = spreadsheet.values().get(spreadsheetId=CONSTANTS.SPREADSHEET_ID, + range="Match Database!A2:A").execute()['values'] + + row_num = -1 # if this fails, it'll overwrite the header which is fine + for i, row in enumerate(game_data): + if len(row) > 0 and row[0].isdigit() and int(row[0]) == match_number: + row_num = i + break + + range_name = f"Match Database!N{row_num + 2}:O{row_num + 2}" + body = { + 'values': [[str(blue_score), str(gold_score)]] + } + spreadsheet.values().update(spreadsheetId=CONSTANTS.SPREADSHEET_ID, + range=range_name, body=body, valueInputOption="RAW").execute() diff --git a/shepherd/Sheets/PIE test sheet - qual_matches.csv b/shepherd/sheets/PIE test sheet - qual_matches.csv similarity index 100% rename from shepherd/Sheets/PIE test sheet - qual_matches.csv rename to shepherd/sheets/PIE test sheet - qual_matches.csv diff --git a/shepherd/sheets/Shepherd Evergreen Database - Match Database.csv b/shepherd/sheets/Shepherd Evergreen Database - Match Database.csv new file mode 100644 index 00000000..9258862e --- /dev/null +++ b/shepherd/sheets/Shepherd Evergreen Database - Match Database.csv @@ -0,0 +1,13 @@ +match_num,blue_1_num,blue_1_name,blue_1_ip,blue_2_num,blue_2_name,blue_2_ip,gold_1_num,gold_1_name,gold_1_ip,gold_2_num,gold_2_name,gold_2_ip,blue_score,gold_score, +1,5,cheese,127.0.0.1,6,cheese,,7,cheese,127.0.0.1,8,cheese,127.0.0.1,,, +2,5,cheese,127.0.0.1,6,"cheese, banana",,7,cheese,127.0.0.1,8,,,,, +not an int,,,,,,,,,,,,,,,random text +not_an_int,,,,,,,,,,,,,,, +,"This spreadsheet isn't well formatted, but ideally Shepherd should still work. +",,,,,,,"more random text +make sure to test both online and offline capabilities!",,,,,,, +,,,,,,,,,,,,,,, +3,,,,,,,37.5,,,not an int,,,12,15,random +4,,,,,,,,"cheese +cheese",,,,,,, +6,12,,0.8.8.8.8.8.8.8,13,"cheese, ""banana """"""banana",0.8.8.8.8.8.8.8,blue,,0.8.8.8.8.8.8.8,18,,0.8.8.8.8.8.8.8,1233333,1533333, \ No newline at end of file diff --git a/shepherd/sheets/client_secret.json b/shepherd/sheets/client_secret.json new file mode 100644 index 00000000..ccbbe2f7 --- /dev/null +++ b/shepherd/sheets/client_secret.json @@ -0,0 +1,12 @@ +{ + "installed": + { + "client_id":"906041237671-fkro33tfu1m79loaikheaft3v9r0b4v2.apps.googleusercontent.com", + "project_id":"ivory-being-194603", + "auth_uri":"https://accounts.google.com/o/oauth2/auth", + "token_uri":"https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs", + "client_secret":"qNzg1w7agAM2NZ4TR7fof78F", + "redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"] + } +} \ No newline at end of file diff --git a/shepherd/sheets/fc2019.csv b/shepherd/sheets/fc2019.csv new file mode 100644 index 00000000..a4cd3d02 --- /dev/null +++ b/shepherd/sheets/fc2019.csv @@ -0,0 +1,48 @@ +f,MatchTime,Blue1Number,Blue1Name,Blue2Number,Blue2Name,Gold1Number,Gold1Name,Gold2Number,Gold2Name,BlueScore,GoldScore,MatchCompleted,WinScore,WinTeam,Win1,Win2,LoseScore,Lose1,Lose2,Lose3,Lose4,TieScore,Tie1,Tie2,Tie3,Tie4,WinDifference,LoseDifference,Day,Time,VideoURL,MatchType +1,Sat 12:30 PM,37,CCPA,15,Pinole Valley,10,Skyline,1,Albany,99,192,1,192,g,10,1,99,37,15,,,FALSE,,,,,93,-93,Sat,12:30 PM,,Q +2,Sat 12:50 PM,31,Arroyo,26,OSA,8,Bishop O'Dowd,29,REALM,52,42,1,52,b,31,26,42,8,29,,,FALSE,,,,,10,-10,Sat,12:50 PM,,Q +3,Sat 1:10 PM,35,Hayward One,40,Salesian,17,ACLC,41,ARISE,132,80,1,132,b,35,40,80,17,41,,,FALSE,,,,,52,-52,Sat ,1:10:00 PM,,Q +4,Sat 1:30 PM,36,Miramonte,3,De Anza,42,John Henry,20,Encinal,-1,89,1,89,g,42,20,-1,36,3,,,FALSE,,,,,90,-90,Sat ,1:30:00 PM,,Q +5,Sat 1:50 PM,12,El Cerrito,9,Hercules,4,LPS Richmond,6,Middle College,84,32,1,84,b,12,9,32,4,6,,,FALSE,,,,,52,-52,Sat ,1:50:00 PM,,Q +6,Sat 2:10 PM,43,Hayward Two,36,Miramonte,20,Encinal,40,Salesian,104,4,1,104,b,43,36,4,20,40,,,FALSE,,,,,100,-100,Sat ,2:10:00 PM,,Q +7,Sat 2:30 PM,41,ARISE,35,Hayward One,8,Bishop O'Dowd,1,Albany,40,204,1,204,g,8,1,40,41,35,,,FALSE,,,,,164,-164,Sat ,2:30:00 PM,,Q +8,Sat 2:50 PM,12,El Cerrito,40,Salesian,43,Hayward Two,10,Skyline,119,84,1,119,b,12,40,84,43,10,,,FALSE,,,,,35,-35,Sat ,2:50:00 PM,,Q +9,Sat 3:10 PM,3,De Anza,20,Encinal,29,REALM,15,Pinole Valley,-20,174,1,174,g,29,15,-20,3,20,,,FALSE,,,,,194,-194,Sat ,3:10:00 PM,,Q +10,Sun 9:00 AM,6,Middle College,42,John Henry,37,CCPA,26,OSA,117,114,1,117,b,6,42,114,37,26,,,FALSE,,,,,3,-3,Sun ,9:00:00 AM,,Q +11,Sun 9:15 AM,36,Miramonte,9,Hercules,#N/A,(LPS Richmond absent),17,ACLC,174,107,1,174,b,36,9,107,#N/A,17,,,FALSE,,,,,67,-67,Sun ,9:15:00 AM,,Q +12,Sun 9:30 AM,31,Arroyo,29,REALM,10,SkyLine,8,Bishop O'Dowd,124,33,1,124,b,31,29,33,10,8,,,FALSE,,,,,91,-91,Sun ,9:30:00 AM,,Q +13,Sun 10:15 AM,41,ARISE,3,De Anza,26,OSA,17,ACLC,54,104,1,104,g,26,17,54,41,3,,,FALSE,,,,,50,-50,Sun,10:15:00 AM,,Q +14,Sun 10:30 AM,12,El Cerrito,4,LPS Richmond,15,Pinole Valley,9,Hercules,164,124,1,164,b,12,4,124,15,9,,,FALSE,,,,,40,-40,Sun ,10:30:00 AM,,Q +15,Sun 10:45 AM,37,CCPA,42,John Henry,6,Middle College,31,Arroyo,97,164,1,164,g,6,31,97,37,42,,,FALSE,,,,,67,-67,Sun ,10:45:00 AM,,Q +16,Sun 11:00 AM,4,LPS Richmond,35,Hayward One,1,Albany,43,Hayward Two,32,264,1,264,g,1,43,32,4,35,,,FALSE,,,,,232,-232,Sun ,11:00:00 AM,,Q +17,Sun 12:40 PM,9,Hercules,15,Pinole Valley,8,Bishop O'Dowd,20,Encinal,82,114,1,,,,,,,,,,,,,,,,,Sun,12:40:00 PM,,E +18,Sun 12:50 PM,15,Pinole Valley,10,Skyline,29,REALM,20,Encinal,154,-16,1,,,,,,,,,,,,,,,,,Sun,12:50:00 PM,,E +19,Sun 2:10 PM,9,Hercules,10,Skyline,29,REALM,8,Bishop O'Dowd,-6,124,1,,,,,,,,,,,,,,,,,Sun,2:10:00 PM,,E +20,Sun 2:20 PM,6,Middle College,41,ARISE,42,John Henry,4,LPS Richmond,254,42,1,,,,,,,,,,,,,,,,,Sun,2:20:00 PM,,E +21,Sun 2:30 PM,43,Hayward Two,6,Middle College,4,LPS Richmond,35,Hayward One,172,52,1,,,,,,,,,,,,,,,,,Sun,2:30:00 PM,,E +22,Sun 2:40 PM,3,De Anza,31,Arroyo,26,OSA,17,ACLC,154,134,1,,,,,,,,,,,,,,,,,Sun,2:40:00 PM,,E +23,Sun 2:50 PM,31,Arroyo,36,Miramonte,17,ACLC,37,CCPA,154,84,1,,,,,,,,,,,,,,,,,Sun,2:50:00 PM,,E +24,Sun 3:00 PM,12,El Cerrito,1,Albany,29,REALM,20,Encinal,249,92,1,,,,,,,,,,,,,,,,,Sun,3:00:00 PM,,E +25,Sun 3:10 PM,40,Salesian,1,Albany,8,Bishop O'Dowd,29,REALM,165,114,1,,,,,,,,,,,,,,,,,Sun,3:10:00 PM,,E +26,Sun 3:20 PM,31,Arroyo,36,Miramonte,43,Hayward Two,6,Middle College,99,239,1,,,,,,,,,,,,,,,,,Sun,3:20:00 PM,,E +27,Sun 3:30 PM,36,Miramonte,3,De Anza,43,Hayward Two,41,ARISE,169,144,1,,,,,,,,,,,,,,,,,Sun,3:30:00 PM,,E +28,Sun 3:40 PM,31,Arroyo,3,De Anza,41,ARISE,6,Middle College,139,204,1,,,,,,,3,,,,,,,,,,Sun,3:40:00 PM,,E +29,Sun 3:50 PM,1,Albany,12,El Cerrito,6,Middle College,41,ARISE,259,234,1,,,,,,,,,,,,,,,,,Sun,3:50:00 PM,,E +30,Sun 4:00 PM,1,Albany,40,Salesian,6,Middle College,43,Hayward Two,259,159,1,,,,,,,,,,,,,,,,,Sun,4:00:00 PM,,E +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,259,234,,,,,,,,,,,,,,,,,,,,, \ No newline at end of file diff --git a/shepherd/Sheets/schedule.csv b/shepherd/sheets/schedule.csv similarity index 100% rename from shepherd/Sheets/schedule.csv rename to shepherd/sheets/schedule.csv diff --git a/shepherd/sheets/user_token.json.enc b/shepherd/sheets/user_token.json.enc new file mode 100644 index 00000000..0595817f Binary files /dev/null and b/shepherd/sheets/user_token.json.enc differ diff --git a/shepherd/shepherd.py b/shepherd/shepherd.py new file mode 100644 index 00000000..c883f7ee --- /dev/null +++ b/shepherd/shepherd.py @@ -0,0 +1,370 @@ +import queue +from alliance import Alliance +from timer import Timer +from ydl import ydl_send, ydl_start_read +from utils import * +from runtimeclient import RuntimeClientManager +from protos.run_mode_pb2 import Mode, TELEOP +from protos.game_state_pb2 import State +from sheet import Sheet +from robot import Robot +from challenge_results import CHALLENGE_RESULTS + + + +########################################### +# Evergreen Variables +########################################### + +MATCH_NUMBER: int = -1 +GAME_STATE: str = STATE.END +GAME_TIMER = Timer(TIMER_TYPES.MATCH) + +ALLIANCES = { + ALLIANCE_COLOR.GOLD: Alliance(Robot("", -1), Robot("", -1)), + ALLIANCE_COLOR.BLUE: Alliance(Robot("", -1), Robot("", -1)), +} + +CLIENTS = RuntimeClientManager() + + +########################################### +# Game Specific Variables +########################################### + + +########################################### +# 2020 Game Specific Variables +########################################### + + +########################################### +# Evergreen Methods +########################################### + + + + +def start(): + ''' + Main loop which processes the event queue and calls the appropriate function + based on game state and the dictionary of available functions + ''' + events = queue.Queue() + ydl_start_read(YDL_TARGETS.SHEPHERD, events) + while True: + print("GAME STATE OUTSIDE: ", GAME_STATE) + payload = events.get(True) + print(payload) + + if GAME_STATE in FUNCTION_MAPPINGS: + func_list = FUNCTION_MAPPINGS.get(GAME_STATE) + func = func_list.get(payload[0]) or EVERYWHERE_FUNCTIONS.get(payload[0]) + if func is not None: + func(**payload[1]) #deconstructs dictionary into arguments + else: + print(f"Invalid Event in {GAME_STATE}") + else: + print(f"Invalid State: {GAME_STATE}") + + + + + + +def set_match_number(match_num): + ''' + Retrieves all match info based on match number and sends this information to the UI. + If not already cached, fetches info from the spreadsheet, and caches it. + Fetching info from spreadsheet is asynchronous, will send a ydl header back with results + ''' + global MATCH_NUMBER + if MATCH_NUMBER != match_num: + MATCH_NUMBER = match_num + Sheet.get_match(match_num) + else: + send_match_info_to_ui() + + +def set_teams_info(teams): + ''' + Caches info and sends it to any open UIs. + ''' + ALLIANCES[ALLIANCE_COLOR.BLUE].robot1.set_from_dict(teams[INDICES.BLUE_1]) + ALLIANCES[ALLIANCE_COLOR.BLUE].robot2.set_from_dict(teams[INDICES.BLUE_2]) + ALLIANCES[ALLIANCE_COLOR.GOLD].robot1.set_from_dict(teams[INDICES.GOLD_1]) + ALLIANCES[ALLIANCE_COLOR.GOLD].robot2.set_from_dict(teams[INDICES.GOLD_2]) + for i in range(4): + CLIENTS.connect_client(i, teams[i]["robot_ip"]) + # even if source of info is UI, needs to be forwarded to other open UIs + send_match_info_to_ui() + + +def to_setup(match_num, teams): + ''' + loads the match information for the upcoming match, then + calls reset_match() to move to setup state. + By the end, should be ready to start match. + ''' + global MATCH_NUMBER + MATCH_NUMBER = match_num + set_teams_info(teams) + # note that reset_match is what actually moves Shepherd into the setup state + reset_match() + + +def reset_match(): + ''' + Resets the current match, moving back to the setup stage but with the current teams loaded in. + Should reset all state being tracked by Shepherd. + ****THIS METHOD MIGHT NEED UPDATING EVERY YEAR BUT SHOULD ALWAYS EXIST**** + ''' + global GAME_STATE + GAME_STATE = STATE.SETUP + Timer.reset_all() + disable_robots() + CLIENTS.reconnect_all() + ALLIANCES[ALLIANCE_COLOR.BLUE].reset() + ALLIANCES[ALLIANCE_COLOR.GOLD].reset() + send_state_to_ui() + print("ENTERING SETUP STATE") + + +def to_auto(): + ''' + Move to the autonomous stage where robots should begin autonomously. + By the end, should be in autonomous state, allowing any function from this + stage to be called and autonomous match timer should have begun. + ''' + global GAME_STATE + GAME_STATE = STATE.AUTO + GAME_TIMER.start_timer(CONSTANTS.AUTO_TIME) + enable_robots(autonomous=True) + + # score for each alliance is sum of passed coding challenges + robots = [ + ALLIANCES[ALLIANCE_COLOR.BLUE].robot1, + ALLIANCES[ALLIANCE_COLOR.BLUE].robot2, + ALLIANCES[ALLIANCE_COLOR.GOLD].robot1, + ALLIANCES[ALLIANCE_COLOR.GOLD].robot2, + ] + for r in robots: + r.coding_challenge = CHALLENGE_RESULTS.get(r.number, r.coding_challenge) + for al in ALLIANCES.values(): + al.score = al.robot1.coding_challenge.count(True) \ + + al.robot2.coding_challenge.count(True) + + send_score_to_ui() + send_state_to_ui() + print("ENTERING AUTO STATE") + + +def to_teleop(): + global GAME_STATE + GAME_STATE = STATE.TELEOP + GAME_TIMER.start_timer(CONSTANTS.TELEOP_TIME) + enable_robots(autonomous=False) + send_state_to_ui() + print("ENTERING TELEOP STATE") + + +def to_end(): + ''' + Go to the end state, finishing the game and flushing scores to the spreadsheet. + ''' + global GAME_STATE + GAME_STATE = STATE.END + disable_robots() + CLIENTS.close_all() + GAME_TIMER.reset() + send_state_to_ui() + send_score_to_ui() + flush_scores() + print("ENTERING END STATE") + + +def go_to_state(state): + transitions = { + STATE.SETUP: reset_match, + STATE.AUTO: to_auto, + STATE.TELEOP: to_teleop, + STATE.END: to_end + } + if state in transitions: + transitions[state]() + else: + print(f"Sorry, {state} is not a valid state to move to.") + + + + +def set_robot_ip(ind, robot_ip): + ''' + Sets the given client ip, and attempts to connect to it + ''' + CLIENTS.connect_client(ind, robot_ip) + + +def score_adjust(blue_score=None, gold_score=None): + ''' + Allow for score to be changed based on referee decisions + ''' + if blue_score is not None: + ALLIANCES[ALLIANCE_COLOR.BLUE].set_score(blue_score) + if gold_score is not None: + ALLIANCES[ALLIANCE_COLOR.GOLD].set_score(gold_score) + send_score_to_ui() + flush_scores() + + +def flush_scores(): + ''' + Sends the most recent match score to the spreadsheet if connected to the internet + ''' + Sheet.write_scores( + MATCH_NUMBER, + ALLIANCES[ALLIANCE_COLOR.BLUE].score, + ALLIANCES[ALLIANCE_COLOR.GOLD].score + ) + + +def send_match_info_to_ui(): + ''' + Sends all match info to the UI + ''' + ydl_data = {"match_num": MATCH_NUMBER, "teams": [ + ALLIANCES[ALLIANCE_COLOR.BLUE].robot1.info_dict(CLIENTS.clients[INDICES.BLUE_1].robot_ip), + ALLIANCES[ALLIANCE_COLOR.BLUE].robot2.info_dict(CLIENTS.clients[INDICES.BLUE_2].robot_ip), + ALLIANCES[ALLIANCE_COLOR.GOLD].robot1.info_dict(CLIENTS.clients[INDICES.GOLD_1].robot_ip), + ALLIANCES[ALLIANCE_COLOR.GOLD].robot2.info_dict(CLIENTS.clients[INDICES.GOLD_2].robot_ip), + ]} + ydl_send(YDL_TARGETS.UI, UI_HEADER.TEAMS_INFO, ydl_data) + + +def send_score_to_ui(): + ''' + Sends the current score to the UI + ''' + data = { + "blue_score": ALLIANCES[ALLIANCE_COLOR.BLUE].score, + "gold_score": ALLIANCES[ALLIANCE_COLOR.GOLD].score, + } + ydl_send(YDL_TARGETS.UI, UI_HEADER.SCORES, data) + + +def send_state_to_ui(): + ''' + Sends the GAME_STATE to the UI + ''' + stage_times = { + STATE.AUTO: CONSTANTS.AUTO_TIME, + STATE.TELEOP: CONSTANTS.TELEOP_TIME + } + if GAME_STATE in stage_times: + st = (GAME_TIMER.end_time - stage_times.get(GAME_STATE)) * 1000 + ydl_send(YDL_TARGETS.UI, UI_HEADER.STATE, {"state": GAME_STATE, "start_time": st}) + else: + ydl_send(YDL_TARGETS.UI, UI_HEADER.STATE, {"state": GAME_STATE}) + + +def send_connection_status_to_ui(): + ''' + Sends the connection status of all runtime clients to the UI + ''' + CLIENTS.send_connection_status_to_ui() + + + + +########################################### +# Game Specific Methods +########################################### + + +def enable_robots(autonomous): + ''' + Sends message to Runtime to enable all robots. The argument should be a boolean + which is true if we are entering autonomous mode + ''' + CLIENTS.send_mode(Mode.AUTO if autonomous else Mode.TELEOP) + + +def disable_robots(): + ''' + Sends message to Runtime to disable all robots + ''' + CLIENTS.send_mode(Mode.IDLE) + + +def disable_robot(ind): + ''' + Send message to Runtime to disable the robot of team + ''' + CLIENTS.clients[ind].send_mode(Mode.IDLE) + + +def enable_robot(ind): + ''' + Send message to Runtime to enable the robot of team + ''' + mode = Mode.AUTO if GAME_STATE == STATE.AUTO else Mode.TELEOP + CLIENTS.clients[ind].send_mode(mode) + + + + + + +########################################### +# Spring 2022 Game +########################################### + + + + + + +########################################### +# Event to Function Mappings for each Stage +########################################### + +FUNCTION_MAPPINGS = { + STATE.SETUP: { + SHEPHERD_HEADER.SET_MATCH_NUMBER: set_match_number, + SHEPHERD_HEADER.SET_TEAMS_INFO: set_teams_info, + SHEPHERD_HEADER.SETUP_MATCH: to_setup, + SHEPHERD_HEADER.START_NEXT_STAGE: to_auto, + }, + STATE.AUTO: { + SHEPHERD_HEADER.STAGE_TIMER_END: to_teleop, + SHEPHERD_HEADER.RESET_CURRENT_STAGE: to_auto, + SHEPHERD_HEADER.START_NEXT_STAGE: to_teleop, + }, + STATE.TELEOP: { + SHEPHERD_HEADER.STAGE_TIMER_END: to_end, + SHEPHERD_HEADER.RESET_CURRENT_STAGE: to_teleop, + SHEPHERD_HEADER.START_NEXT_STAGE: to_end, + }, + STATE.END: { + SHEPHERD_HEADER.SET_MATCH_NUMBER: set_match_number, + SHEPHERD_HEADER.SET_TEAMS_INFO: set_teams_info, + SHEPHERD_HEADER.SETUP_MATCH: to_setup, + SHEPHERD_HEADER.SET_SCORES: score_adjust, + } +} + +EVERYWHERE_FUNCTIONS = { + SHEPHERD_HEADER.GET_MATCH_INFO: send_match_info_to_ui, + SHEPHERD_HEADER.GET_SCORES: send_score_to_ui, + SHEPHERD_HEADER.GET_STATE: send_state_to_ui, + SHEPHERD_HEADER.GET_CONNECTION_STATUS: send_connection_status_to_ui, + + SHEPHERD_HEADER.SET_STATE: go_to_state, + SHEPHERD_HEADER.ROBOT_OFF: disable_robot, + SHEPHERD_HEADER.ROBOT_ON: enable_robot, + SHEPHERD_HEADER.SET_ROBOT_IP: set_robot_ip, + SHEPHERD_HEADER.RESET_MATCH: reset_match, +} + +if __name__ == '__main__': + start() diff --git a/shepherd/static/1.png b/shepherd/static/1.png new file mode 100644 index 00000000..fecd7b1a Binary files /dev/null and b/shepherd/static/1.png differ diff --git a/shepherd/static/2.png b/shepherd/static/2.png new file mode 100644 index 00000000..3d9b79c8 Binary files /dev/null and b/shepherd/static/2.png differ diff --git a/shepherd/static/bootstrap.min.css b/shepherd/static/bootstrap.min.css new file mode 100644 index 00000000..92e3fe87 --- /dev/null +++ b/shepherd/static/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #dee2e6;border-bottom-right-radius:.3rem;border-bottom-left-radius:.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:0s .6s opacity}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/shepherd/favicon.ico b/shepherd/static/favicon.ico similarity index 100% rename from shepherd/favicon.ico rename to shepherd/static/favicon.ico diff --git a/shepherd/static/jquery-1.12.3.min.js b/shepherd/static/jquery-1.12.3.min.js new file mode 100644 index 00000000..7ae729d9 --- /dev/null +++ b/shepherd/static/jquery-1.12.3.min.js @@ -0,0 +1,5 @@ +/*! jQuery v1.12.3 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0; +}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML="
a",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:l.htmlSerialize?[0,"",""]:[1,"X
","
"]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?""!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("