Compare commits

..

27 commits

Author SHA1 Message Date
Joel Klimont
3ab9db95e0 small changes 2022-10-06 19:52:00 +02:00
Joel Klimont
beb7066cb0 changed version numbering to conform with .deb package policy 2022-10-06 19:46:22 +02:00
Joel Klimont
ea51e4911a more work on build 2022-10-06 18:33:27 +02:00
Joel Klimont
b718985293 more work on build 2022-10-06 18:24:19 +02:00
Joel Klimont
cf8586aa7b more work on build 2022-10-06 18:20:22 +02:00
Joel Klimont
8cb3626138 more work on build 2022-10-06 18:18:57 +02:00
Joel Klimont
c224411279 more work on build 2022-10-06 18:16:49 +02:00
Joel Klimont
aa39f44f71 added dependencies to server package build 2022-10-05 23:25:05 +02:00
Joel Klimont
f29c78514a (MINOR) something 2022-10-05 23:20:00 +02:00
Joel Klimont
71bb71587a MAJOR very important change 2022-10-05 23:11:30 +02:00
Joel Klimont
d9c504c4da (MAJOR) very important change 2022-10-05 23:05:51 +02:00
Joel Klimont
eb411a33b8 very unimportant change 2022-10-05 23:04:29 +02:00
Joel Klimont
e2349c237d test6 2022-10-05 23:03:43 +02:00
Joel Klimont
a4e5c73940 test5 2022-10-05 23:01:28 +02:00
Joel Klimont
cdb1a9aae0 (MINOR) test4 2022-10-05 22:59:47 +02:00
Joel Klimont
57c7596718 (MINOR) test3 2022-10-05 22:54:56 +02:00
Joel Klimont
e8497b94e1 (MINOR) test2 2022-10-05 22:50:36 +02:00
Joel Klimont
24bae797b2 (MINOR) test 2022-10-05 22:48:45 +02:00
Joel Klimont
7b6b46ab05 (MINOR) test 2022-10-05 22:45:45 +02:00
Joel Klimont
2152d6ac89 (minor) test 2022-10-05 22:44:32 +02:00
Joel Klimont
c65016f135 (minor) test 2022-10-05 22:39:45 +02:00
Joel Klimont
531c3286dc (minor) test 2022-10-05 22:36:44 +02:00
Joel Klimont
2c0bb51748 (minor) test 2022-10-05 22:30:23 +02:00
Joel Klimont
dc85519fa3 (minor) test 2022-10-05 22:28:49 +02:00
Joel Klimont
680a168435 (minor) test 2022-10-05 22:26:15 +02:00
Joel Klimont
646bddf303 (minor) test 2022-10-05 22:21:11 +02:00
Joel Klimont
0cfd021ef2 started working on automatic build of complib deb package 2022-10-05 22:16:13 +02:00
218 changed files with 6839 additions and 4005 deletions

View file

@ -3,11 +3,11 @@ name: Build comlib.deb package
on:
push:
branches:
- master
- build-test
jobs:
build-complib-deb:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
@ -20,7 +20,7 @@ jobs:
uses: paulhatch/semantic-version@v4.0.2
id: next_semantic_version
with:
branch: "master"
branch: "build-test"
tag_prefix: ""
major_pattern: "(MAJOR)"
minor_pattern: "(MINOR)"
@ -55,6 +55,6 @@ jobs:
source-directory: 'output'
target-directory: 'debs/complib/'
destination-github-username: 'F-WuTS'
destination-repository-name: 'compREP'
destination-repository-name: 'compREP-dev'
user-email: joel.klimont@comp-air.at
target-branch: master

312
.gitignore vendored
View file

@ -1,312 +0,0 @@
# Created by https://www.toptal.com/developers/gitignore/api/macos,python,pycharm
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,python,pycharm
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### PyCharm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PyCharm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# End of https://www.toptal.com/developers/gitignore/api/macos,python,pycharm

View file

@ -1,11 +1,7 @@
# compLIB
Rewrite for ROS is the live packaged version since 18.07.2023.
# Dependencies
TODO: document
## Building documentation
```
pip install sphinx-rtd-theme
@ -18,13 +14,6 @@ pip install sphinx-rtd-theme
[Inline documentation example](https://pythonhosted.org/an_example_pypi_project/sphinx.html#full-code-example)
[reStructured Text](https://pythonhosted.org/an_example_pypi_project/sphinx.html#restructured-text-rest-resources)
# ENV Variables
+ `DEBUG`, default="0", If set to != "0" (default), debug prints will be enabled
+ `API_URL`, default="http://localhost:5000/"
+ `API_FORCE`, default="", if set to !="" (default), it will replace the API_URL env variable
+ `FORCE_SEED`, default="-1", if set to !="-1" (default), the seeding seed supplied by the user will be ignored and this seed will be used instead
# Stream Video
```
@ -42,9 +31,3 @@ with ffmpeg, cpu friendly
```
ffmpeg -f v4l2 -framerate 30 -video_size 640x480 -i /dev/video0 -b:v 2M -f flv rtmp://10.20.86.88/live/stream
```
# Bullseye now only supports libcamera
https://www.raspberrypi.com/news/bullseye-camera-system/
(This can still be mitigated by enabling "old camera support" in the raspi-config settings. (This is done automatically in the postinstallscript)

View file

@ -4,6 +4,7 @@ mkdir output
DEB="empty"
cd client_s2
source build_deb.sh
echo "Ran build deb, created: $DEB"
mv $DEB ./output
mv $DEB ../output

View file

@ -1,38 +1,12 @@
# Created by https://www.toptal.com/developers/gitignore/api/macos,python,pycharm
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,python,pycharm
.idea
# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,code
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,code
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Code ###
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
*.code-workspace
### PyCharm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
@ -45,9 +19,6 @@ Temporary Items
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
@ -98,9 +69,6 @@ atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
@ -143,10 +111,6 @@ fabric.properties
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
@ -164,12 +128,11 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
@ -199,7 +162,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
cover/
pytestdebug.log
# Translations
*.mo
@ -220,9 +183,9 @@ instance/
# Sphinx documentation
docs/_build/
doc/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
@ -233,9 +196,7 @@ profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@ -244,22 +205,7 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
@ -277,6 +223,7 @@ venv/
ENV/
env.bak/
venv.bak/
pythonenv*
# Spyder project settings
.spyderproject
@ -299,14 +246,7 @@ dmypy.json
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# profiling data
.prof
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# End of https://www.toptal.com/developers/gitignore/api/macos,python,pycharm
# End of https://www.toptal.com/developers/gitignore/api/python,pycharm,code

47
client_s1/build_deb.sh Executable file
View file

@ -0,0 +1,47 @@
##!/usr/bin/zsh
export PYTHONDONTWRITEBYTECODE=1
# be sure to change version if needed!
fpm -s python --python-bin python3 --python-pip pip3 --python-package-name-prefix python3 \
-m '"Joel Klimont" <joel.klimont@gmail.com>' \
--license 'proprietary' \
--description 'Library for robot used in the competition' \
--after-install postinstall.sh \
--after-upgrade postinstall.sh \
--deb-generate-changes \
--deb-priority "optional" \
--deb-systemd "complib.service" \
-d "python3-pip" \
-d "nginx" \
-d "libnginx-mod-rtmp" \
-d "libsystemd-dev" \
-d "python3-systemd" \
-d "gstreamer1.0-tools" \
-d "gstreamer1.0-plugins-bad" \
-d "gstreamer1.0-plugins-base" \
-d "gstreamer1.0-plugins-good" \
-d "gstreamer1.0-omx-rpi" \
-d "gstreamer1.0-omx-rpi-config" \
-d "opencv-dev" \
-d "opencv-libs" \
-d "opencv-licenses" \
-d "opencv-main" \
-d "opencv-python" \
-d "opencv-scripts" \
-d "libatlas-base-dev" \
-d "pigpio" \
-d "python-pigpio" \
-d "python3-pigpio" \
-d "python3-numpy" \
-d "ffmpeg" \
-v 0.4.1-1 -t deb setup.py
# --deb-changelog changelog \
# --deb-upstream-changelog changelog \
# --deb-field "Distribution: stable" \
# --deb-dist "stable" \
#sudo apt purge python3-complib -y
#sudo apt install ./python3-*
#sudo apt search complib
#ar vx ./python3*

5
client_s1/changelog Normal file
View file

@ -0,0 +1,5 @@
python3-complib (0.0.2-4) stable; urgency=low
* Initial release.
-- Joel Klimont <joel.klimont@gmail.com> Fri, 15 Jan 2021 23:14:01 +0100

233
client_s1/compLib/Api.py Normal file
View file

@ -0,0 +1,233 @@
import json
import os
import time
from typing import Dict, Tuple, List
import requests
from compLib.LogstashLogging import Logging
API_URL = os.getenv("API_URL", "http://localhost:5000/") + "api/"
api_override = os.getenv("API_FORCE", "")
if api_override != "":
print(f"API_URL was set to {API_URL} but was overwritten with {api_override}")
API_URL = api_override
API_URL_GET_DELIVERY = API_URL + "getDelivery"
API_URL_GET_MATERIAL = API_URL + "getMaterial"
API_URL_GET_GARBAGE = API_URL + "getGarbage"
API_URL_GET_LIST_CARGO = API_URL + "listCargo"
API_URL_GET_CARGO = API_URL + "getCargo/"
API_URL_GET_ROBOT_STATE = API_URL + "getRobotState"
API_URL_GET_POS = API_URL + "getPos"
API_URL_GET_OP = API_URL + "getOp"
API_URL_GET_GOAL = API_URL + "getGoal"
API_URL_GET_ITEMS = API_URL + "getItems"
API_URL_GET_SCORES = API_URL + "getScores"
API_URL_GET_METEOROID = API_URL + "getMeteoroids"
class Seeding:
"""Class used for communicating with seeding api
"""
@staticmethod
def get_delivery() -> Tuple[Dict, int]:
"""Makes the /api/getDelivery call to the api.
:return: Json Object and status code as returned by the api.
:rtype: Tuple[Dict, int]
"""
res = requests.get(API_URL_GET_DELIVERY)
result = json.loads(res.content)
Logging.get_logger().debug(f"Seeding.get_delivery = {result}, status code = {res.status_code}")
return result, res.status_code
@staticmethod
def get_material() -> Tuple[Dict, int]:
"""Makes the /api/getMaterial call to the api.
:return: Json Object and status code as returned by the api.
:rtype: Tuple[Dict, int]
"""
res = requests.get(API_URL_GET_MATERIAL)
result = json.loads(res.content)
Logging.get_logger().debug(f"Seeding.get_material = {result}, status code = {res.status_code}")
return result, res.status_code
@staticmethod
def get_garbage() -> Tuple[Dict, int]:
"""Makes the /api/getGarbage call to the api.
:return: Json Object and status code as returned by the api.
:rtype: Tuple[Dict, int]
"""
res = requests.get(API_URL_GET_GARBAGE)
result = json.loads(res.content)
Logging.get_logger().debug(f"Seeding.get_garbage {result}, status code = {res.status_code}")
return result, res.status_code
@staticmethod
def list_cargo() -> Tuple[Dict, int]:
"""Makes the /api/listCargo call to the api.
:return: Json Object and status code as returned by the api.
:rtype: Tuple[Dict, int]
"""
res = requests.get(API_URL_GET_LIST_CARGO)
result = json.loads(res.content)
Logging.get_logger().debug(f"Seeding.list_cargo {result}, status code = {res.status_code}")
return result, res.status_code
@staticmethod
def get_cargo(color: str) -> Tuple[Dict, int]:
"""Makes the /api/getCargo call to the api.
:param color: Color parameter which specifies which cargo should be taken. (A string which is either "green", "red", "yellow" or "blue") The function only picks up one package.
:return: Json Object and status code as returned by the api.
:rtype: Tuple[Dict, int]
"""
res = requests.get(API_URL_GET_CARGO + color)
result = json.loads(res.content)
Logging.get_logger().debug(f"Seeding.get_cargo {result}, status code = {res.status_code}")
return result, res.status_code
@staticmethod
def get_robot_state() -> Tuple[Dict, int]:
res = requests.get(API_URL_GET_ROBOT_STATE)
result = json.loads(res.content)
Logging.get_logger().debug(f"Seeding.get_robot_state {result}, status code = {res.status_code}")
return result, res.status_code
class Position:
"""Datastructure for holding a position
"""
def __init__(self, x, y, degrees):
self.x = x
self.y = y
self.degrees = degrees
def __repr__(self):
return f"Position(x={self.x}, y={self.y}, degrees={self.degrees})"
def __str__(self):
return f"Position(x={round(self.x, 5)}, y={round(self.y, 5)}, degrees={round(self.degrees, 5)})"
class DoubleElim:
"""Class used for communicating with double elimination api
"""
@staticmethod
def get_position() -> Tuple[Position, int]:
"""Makes the /api/getPos call to the api.
:return: A Position object with robot position
:rtype: Tuple[Position, int]
"""
res = requests.get(API_URL_GET_POS)
if res.status_code == 408:
Logging.get_logger().error(f"DoubleElim.get_position timeout!")
time.sleep(0.01)
return DoubleElim.get_position()
response = json.loads(res.content)
Logging.get_logger().debug(f"DoubleElim.get_position = {response}, status code = {res.status_code}")
return Position(response["x"], response["y"], response["degrees"]), res.status_code
@staticmethod
def get_opponent() -> Tuple[Position, int]:
"""Makes the /api/getOp call to the api.
:return: A Position object with opponents robot position
:rtype: Tuple[Position, int]
"""
res = requests.get(API_URL_GET_OP)
if res.status_code == 408:
Logging.get_logger().error(f"DoubleElim.get_opponent timeout!")
time.sleep(0.01)
return DoubleElim.get_opponent()
response = json.loads(res.content)
Logging.get_logger().debug(f"DoubleElim.get_opponent = x:{response}, status code = {res.status_code}")
return Position(response["x"], response["y"], response["degrees"]), res.status_code
@staticmethod
def get_goal() -> Tuple[Position, int]:
"""Makes the /api/getGoal call to the api.
:return: A Position object with x and y coordinates of the goal, rotation is always -1
:rtype: Tuple[Position, int]
"""
res = requests.get(API_URL_GET_GOAL)
if res.status_code == 408:
Logging.get_logger().error(f"DoubleElim.get_goal timeout!")
time.sleep(0.01)
return DoubleElim.get_goal()
response = json.loads(res.content)
Logging.get_logger().debug(f"DoubleElim.get_goal = {response}, status code = {res.status_code}")
return Position(response["x"], response["y"], -1), res.status_code
@staticmethod
def get_items() -> Tuple[List[Dict], int]:
"""Makes the /api/getItems call to the api.
:return: A list will all items currently on the game field. Items are dictionaries that look like: {"id": 0, "x": 0, "y": 0}
:rtype: Tuple[List[Dict], int]
"""
res = requests.get(API_URL_GET_ITEMS)
if res.status_code == 408:
Logging.get_logger().error(f"DoubleElim.get_items timeout!")
time.sleep(0.01)
return DoubleElim.get_items()
elif res.status_code == 503:
return [], 503
response = json.loads(res.content)
Logging.get_logger().debug(f"DoubleElim.get_items = {response}, status code = {res.status_code}")
return response, res.status_code
@staticmethod
def get_scores() -> Tuple[Dict, int]:
"""Makes the /api/getScores call to the api.
:return: A dictionary with all scores included like: {"self":2,"opponent":0}
:rtype: Tuple[Dict, int]
"""
res = requests.get(API_URL_GET_SCORES)
if res.status_code == 408:
Logging.get_logger().error(f"DoubleElim.get_scores timeout!")
time.sleep(0.01)
return DoubleElim.get_scores()
elif res.status_code == 503:
return {"self": 0, "opponent": 0}, 503
response = json.loads(res.content)
Logging.get_logger().debug(f"DoubleElim.get_scores = {response}, status code = {res.status_code}")
return response, res.status_code
@staticmethod
def get_meteoroids() -> Tuple[List[Dict], int]:
"""Makes the /api/getMeteoroids call to the api.
:return: A list will all meteoroids currently on the game field. Meteoroids are dictionaries that look like: {"x": 0, "y": 0}
:rtype: Tuple[List[Dict], int]
"""
res = requests.get(API_URL_GET_METEOROID)
if res.status_code == 408:
Logging.get_logger().error(f"DoubleElim.get_meteoroids timeout!")
time.sleep(0.01)
return DoubleElim.get_meteoroids()
elif res.status_code == 503:
return [], 503
response = json.loads(res.content)
Logging.get_logger().debug(f"DoubleElim.get_meteoroids = {response}, status code = {res.status_code}")
return response, res.status_code

View file

@ -0,0 +1,49 @@
import string
from compLib.LogstashLogging import logstash_logger
from compLib.Spi import Spi, Register
LINE_COUNT = 4
CHARS_PER_LINE = 16
class Display(object):
"""Access the display on the robot.
The display is split into 4 Rows and 16 Columns. Each function call changes one line at a time.
"""
@staticmethod
def write(line: int, text: str):
"""Write a string of text to the integrated display.
:param line: Line to write. Between 1 and 4
:param text: Text to write. Up to 16 characters
:raises: IndexError
"""
if len(text) > CHARS_PER_LINE:
logstash_logger.error(f"Too many characters specified!")
return
if line <= 0 or line > LINE_COUNT:
raise IndexError("Invalid line number specified")
to_write = [0] * CHARS_PER_LINE
for i in range(len(text)):
to_write[i] = ord(text[i])
if line == 1:
Spi.write_array(Register.DISPLAY_LINE_1_C0, CHARS_PER_LINE, to_write)
elif line == 2:
Spi.write_array(Register.DISPLAY_LINE_2_C0, CHARS_PER_LINE, to_write)
elif line == 3:
Spi.write_array(Register.DISPLAY_LINE_3_C0, CHARS_PER_LINE, to_write)
elif line == 4:
Spi.write_array(Register.DISPLAY_LINE_4_C0, CHARS_PER_LINE, to_write)
@staticmethod
def clear():
"""Clear the display
"""
for i in range(1, LINE_COUNT + 1):
Display.write(i, "")

View file

@ -0,0 +1,127 @@
import atexit
from enum import Enum
from compLib.LogstashLogging import Logging
from compLib.Spi import Spi, Register
MOTOR_COUNT = 4
encoder_start_values = [0] * (MOTOR_COUNT + 1)
class Encoder(object):
"""Class used to read the encoders
"""
@staticmethod
def handle_wrap(raw_value, port):
"""Handle overflow and underflow of int for encoders.
:param raw_value: Raw value which was read on port
:param port: Port, which the motor is connected to. Between 1 and 4
:raises: IndexError
:return: Current encoder position
"""
if port <= 0 or port > MOTOR_COUNT:
raise IndexError("Invalid encoder port specified!")
diff = raw_value - encoder_start_values[port]
if diff > 2 ** 31:
diff -= 2 ** 32
elif diff < -2 ** 31:
diff += 2 ** 32
return diff
@staticmethod
def read_raw(port: int) -> int:
"""Read raw encoder from a specified port. Will not be reset to 0 at start.
:param port: Port, which the motor is connected to. Between 1 and 4
:raises: IndexError
:return: Current encoder position
"""
if port <= 0 or port > MOTOR_COUNT:
raise IndexError("Invalid encoder port specified!")
raw_value = 0
if port == 1:
raw_value = Spi.read(Register.MOTOR_1_POS_B3, 4)
elif port == 2:
raw_value = Spi.read(Register.MOTOR_2_POS_B3, 4)
elif port == 3:
raw_value = Spi.read(Register.MOTOR_3_POS_B3, 4)
elif port == 4:
raw_value = Spi.read(Register.MOTOR_4_POS_B3, 4)
return raw_value
@staticmethod
def read_all_raw():
"""Read all encoders at once.
This is faster than read_raw as it only uses one SPI call for all encoders!
:return: Tuple of all current raw encoder positions
"""
encoders = Spi.read_array(Register.MOTOR_1_POS_B3, 4 * 4)
encoder_1 = int.from_bytes(
encoders[0:4], byteorder='big', signed=False)
encoder_2 = int.from_bytes(
encoders[4:8], byteorder='big', signed=False)
encoder_3 = int.from_bytes(
encoders[8:12], byteorder='big', signed=False)
encoder_4 = int.from_bytes(
encoders[12:16], byteorder='big', signed=False)
return (encoder_1, encoder_2, encoder_3, encoder_4)
@staticmethod
def read_all():
"""Read all encoders at once.
This is faster than read as it only uses one SPI call for all encoders!
:return: Tuple of all current encoder positions
"""
encoders = Encoder.read_all_raw()
return (Encoder.handle_wrap(encoders[0], 1),
Encoder.handle_wrap(encoders[1], 2),
Encoder.handle_wrap(encoders[2], 3),
Encoder.handle_wrap(encoders[3], 4))
@staticmethod
def read(port: int) -> int:
"""Read encoder from a specified port
:param port: Port, which the motor is connected to. Between 1 and 4
:raises: IndexError
:return: Current encoder position
"""
if port <= 0 or port > MOTOR_COUNT:
raise IndexError("Invalid encoder port specified!")
return Encoder.handle_wrap(Encoder.read_raw(port), port)
@staticmethod
def clear(port: int):
"""Reset encoder position to 0
:param port: Port, which the motor is connected to. Between 1 and 4
:raises: IndexError
"""
if port <= 0 or port > MOTOR_COUNT:
raise IndexError("Invalid encoder port specified!")
encoder_start_values[port] = Encoder.read_raw(port)
@staticmethod
def clear_all():
"""Reset all encoder positions to 0
"""
for i in range(1, MOTOR_COUNT + 1):
encoder_start_values[i] = Encoder.read_raw(i)

View file

@ -0,0 +1,116 @@
import datetime
import os
import socket
import threading
import time
import systemd.daemon
from compLib.Lock import Lock
try:
from compLib.LogstashLogging import Logging
except Exception as e:
import logging
class Logger():
def __init__(self):
self.logger = logging.Logger('compApi background')
def get_logger(self):
return self.logger
Logging = Logger()
print(f"Could not import compLib.LogstashLogging: {str(e)}")
Logging.get_logger().error(f"Could not import compLib.LogstashLogging: {str(e)}")
print("after basic imports")
RUN_IP_CHECK = False
try:
from compLib.Display import Display
from compLib.Spi import Spi
from compLib import __version__
RUN_IP_CHECK = True
except Exception as e:
print(f"Could not import display or spi for ip output {str(e)}")
Logging.get_logger().warning(f"Could not import display or spi for ip output {str(e)}")
print(f"After display and Spi import")
__run = """raspivid -t 0 -b 5000000 -w 1280 -h 720 -fps 30 -n -o - | gst-launch-1.0 fdsrc ! video/x-h264,width=1280,height=720,framerate=30/1,noise-reduction=1,profile=high,stream-format=byte-stream ! h264parse ! queue ! flvmux streamable=true ! rtmpsink location=\"rtmp://localhost/live/stream\""""
STREAM_RASPI = False if os.getenv("STREAM_RASPI", "false") == "false" else True
IP_OUTPUT = False if os.getenv("IP_OUTPUT", "true") != "true" else True
def get_ip():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('10.255.255.255', 1))
IP = s.getsockname()[0]
except Exception:
IP = 'Not connected'
print(f"Error could not query ip: {e}")
finally:
s.close()
return IP
def write_ip_to_screen():
while os.getenv("IP_OUTPUT", "true") == "true":
try:
if not Lock.is_locked():
Lock.lock()
ip = str(get_ip())
print(f"writing {ip} to display")
Display.write(2, f"LIB: V{__version__}")
Display.write(3, f"FW: V{Spi.get_version()}")
Display.write(4, f"IP: {ip}")
Display.write(1, datetime.datetime.now().strftime("%b %d %H:%M:%S"))
Lock.unlock()
time.sleep(10)
except Exception as e:
print(f"Exception in write ip thread: {e}")
time.sleep(5)
if __name__ == '__main__':
try:
systemd.daemon.notify(systemd.daemon.Notification.READY)
except:
Logging.get_logger().warning("Warning, old systemd version detected")
systemd.daemon.notify('READY=1')
ip_output = None
if RUN_IP_CHECK and IP_OUTPUT:
print("starting ip output")
Logging.get_logger().info("starting ip output")
Lock.unlock()
try:
Spi.disable_health_check()
ip_output = threading.Thread(target=write_ip_to_screen)
ip_output.start()
print("starting ip output - DONE")
Logging.get_logger().info("starting ip output - DONE")
except Exception as e:
print(f"could not start ip output -> {str(e)}")
Logging.get_logger().error(f"could not start ip output -> {str(e)}")
if STREAM_RASPI:
print("starting gstreamer background process")
Logging.get_logger().info("starting gstreamer background process")
os.system(__run)
print("gstreamer stopped...")
Logging.get_logger().error("gstreamer stopped...")
else:
print("not starting gstreamer background process")
Logging.get_logger().info("not starting gstreamer background process")
if ip_output is not None:
ip_output.join()
else:
print("ip display output failed to initialize.. sleeping for a day, good night")
Logging.get_logger().info("ip display output failed to initialize.. sleeping for a day, good night")
time.sleep(60 * 60 * 24)

View file

@ -0,0 +1,81 @@
from compLib.LogstashLogging import Logging
from compLib.MetricsLogging import MetricsLogging
from compLib.Spi import Spi, Register
SENSOR_COUNT = 5
class IRSensor(object):
"""Access the different IR Sensors of the robot
"""
@staticmethod
def read(sensor: int) -> int:
"""Read one infrared sensor
:param sensor: Which sensor to read. Between 1 and 5
:raises: IndexError
:return: Sensor value. 10 bit accuracy
:rtype: int
"""
if sensor <= 0 or sensor > SENSOR_COUNT:
raise IndexError("Invalid sensor specified!")
result = 0
if sensor == 1:
result = Spi.read(Register.IR_1_H, 2)
elif sensor == 2:
result = Spi.read(Register.IR_2_H, 2)
elif sensor == 3:
result = Spi.read(Register.IR_3_H, 2)
elif sensor == 4:
result = Spi.read(Register.IR_4_H, 2)
elif sensor == 5:
result = Spi.read(Register.IR_5_H, 2)
MetricsLogging.put("Infrared", result, sensor)
return result
@staticmethod
def read_all():
"""Read all IR sensors at once.
This is faster than read as it only uses one SPI call for all sensors!
:return: Tuple of all current ir sensors
"""
sensors = Spi.read_array(Register.IR_1_H, 5 * 2)
sensor_1 = int.from_bytes(
sensors[0:2], byteorder='big', signed=False)
sensor_2 = int.from_bytes(
sensors[2:4], byteorder='big', signed=False)
sensor_3 = int.from_bytes(
sensors[4:6], byteorder='big', signed=False)
sensor_4 = int.from_bytes(
sensors[6:8], byteorder='big', signed=False)
sensor_5 = int.from_bytes(
sensors[8:10], byteorder='big', signed=False)
return (sensor_1, sensor_2, sensor_3, sensor_4, sensor_5)
@staticmethod
def set(sensor: int, on: bool):
"""Turn on / off a IR emitter
:param sensor: Which sensor to read. Between 1 and 5
:raises: IndexError
"""
if sensor <= 0 or sensor > SENSOR_COUNT:
raise IndexError("Invalid sensor specified!")
if sensor == 1:
Spi.write(Register.IR_1_LED, 1, on)
elif sensor == 2:
Spi.write(Register.IR_2_LED, 1, on)
elif sensor == 3:
Spi.write(Register.IR_3_LED, 1, on)
elif sensor == 4:
Spi.write(Register.IR_4_LED, 1, on)
elif sensor == 5:
Spi.write(Register.IR_5_LED, 1, on)

25
client_s1/compLib/Lock.py Normal file
View file

@ -0,0 +1,25 @@
from filelock import FileLock, Timeout
import atexit
FILELOCK_PATH = "/root/complib.lock"
global_lock = FileLock(FILELOCK_PATH, timeout=1)
class Lock(object):
@staticmethod
def lock():
global_lock.acquire()
@staticmethod
def unlock():
global_lock.release()
@staticmethod
def is_locked():
try:
global_lock.acquire()
global_lock.release()
return False
except Timeout:
return True
atexit.register(Lock.unlock)

View file

@ -0,0 +1,102 @@
import logging
import os
import sys
import requests
from logstash_async.transport import HttpTransport
from logstash_async.handler import AsynchronousLogstashHandler
EXTENSIVE_LOGGING = os.getenv("EXTENSIVE_LOGGING", "False")
if EXTENSIVE_LOGGING == "True":
EXTENSIVE_LOGGING = True
else:
EXTENSIVE_LOGGING = False
host = 'logstash.robo4you.at'
port = 443
logstash_logger = logging.getLogger('logstash')
logstash_logger.setLevel(logging.INFO)
transport = HttpTransport(
host,
port,
username="robo",
password="competition",
timeout=60.0,
)
asynchronousLogstashHandler = AsynchronousLogstashHandler(
host,
port,
transport=transport,
database_path='logs.db'
)
class StreamToLogger(object):
"""
Fake file-like stream object that redirects writes to a logger instance.
"""
def __init__(self, textio, log_level=logging.INFO):
self.logger = logging.getLogger('logstash')
self.console = textio
self.log_level = log_level
self.linebuf = ''
def write(self, buf):
self.console.write(buf)
temp_linebuf = self.linebuf + buf
for line in buf.splitlines(True):
self.linebuf += line
def flush(self):
if self.linebuf != '':
self.logger.log(self.log_level, self.linebuf.rstrip())
self.linebuf = ''
self.console.flush()
asynchronousLogstashHandler.flush()
if EXTENSIVE_LOGGING:
try:
r = requests.get(f"https://{host}:{port}")
if r.status_code == 401:
logstash_logger.addHandler(asynchronousLogstashHandler)
so = StreamToLogger(sys.stdout, logging.INFO)
sys.stdout = so
se = StreamToLogger(sys.stderr, logging.ERROR)
sys.stderr = se
else:
print(f"Could not connect to {host} -> ERROR CODE: {r.status_code}!")
except requests.exceptions.ConnectionError as identifier:
print(f"Could not connect to {host}!")
print(f"Error loading logger was -> {identifier}")
else:
print("Extensive logging is disabled! No logs will be sent over network...")
class Logging(object):
"""Can be used to manually use the logger or turn up the logging level used for debugging this library.
"""
@staticmethod
def get_logger() -> logging.Logger:
"""Get the logger object used to communicate with logstash
:return: Python logger
:rtype: logging.Logger
"""
return logstash_logger
@staticmethod
def set_debug():
"""Turns up the logging level of the library to debug. Should be used, when debugging with the
Competition organizers
"""
logstash_logger.setLevel(logging.DEBUG)

View file

@ -0,0 +1,97 @@
from influxdb_client import InfluxDBClient, Point, WritePrecision
import socket
import uuid
import os
import multiprocessing
import datetime
import requests
import time
CONCURRENCY = 1
HOSTNAME = socket.gethostname()
RUN_TRACE = str(uuid.uuid4())
TOKEN = "aCdxnntw-Zp3agci8Z32lQAR_ep6MhdmWOJG7ObnoikYqe7nKAhFYx1jVGBpipQuId79SC4Jl0J6IBYVqauJyw=="
ORG = "robo4you"
BUCKET = "compAIR"
INFLUX_HOST = "https://influxdb.comp-air.at"
EXTENSIVE_LOGGING = os.getenv("EXTENSIVE_LOGGING", "True")
if EXTENSIVE_LOGGING == "True":
EXTENSIVE_LOGGING = True
else:
EXTENSIVE_LOGGING = False
influx_client = InfluxDBClient(url=INFLUX_HOST, token=TOKEN)
write_api = influx_client.write_api()
point_queue = multiprocessing.Queue()
workers = []
class MetricsLogging():
"""Used to send metrics / points to a influxdb
"""
@staticmethod
def is_influx_reachable() -> bool:
"""Check if we can send metrics to the database
:return: Is reachable?
:rtype: bool
"""
try:
r = requests.get(INFLUX_HOST)
if r.status_code == 200:
return True
else:
print(f"Could not connect to {INFLUX_HOST} -> ERROR CODE: {r.status_code}!")
return False
except requests.exceptions.RequestException as identifier:
print(f"Could not connect to {INFLUX_HOST}!")
@staticmethod
def put(sensorType, value, port):
"""Put a datapoint into a time-series-database
:param sensorType: A key used to identify a datapoint
:param value: Value measured by the sensor
:param port: Port of the sensor which was read
"""
if EXTENSIVE_LOGGING:
point = Point(sensorType) \
.tag("host", HOSTNAME) \
.tag("runID", RUN_TRACE) \
.tag("port", port) \
.field("value", value) \
.time(datetime.datetime.utcnow(), WritePrecision.MS)
point_queue.put_nowait(point)
@staticmethod
def worker():
point = object()
for job in iter(point_queue.get, point):
try:
write_api.write(BUCKET, ORG, point)
time.sleep(0.1)
except Exception as e:
pass
finally:
pass
@staticmethod
def start_workers():
global EXTENSIVE_LOGGING
if EXTENSIVE_LOGGING:
if MetricsLogging.is_influx_reachable():
for i in range(CONCURRENCY):
worker = multiprocessing.Process(target=MetricsLogging.worker, daemon=True)
worker.start()
workers.append(worker)
else:
EXTENSIVE_LOGGING = False
MetricsLogging.start_workers()

226
client_s1/compLib/Motor.py Normal file
View file

@ -0,0 +1,226 @@
import atexit
from enum import IntEnum
from compLib.LogstashLogging import Logging
from compLib.MetricsLogging import MetricsLogging
from compLib.Spi import Spi, Register
MOTOR_COUNT = 4
MAX_MOTOR_SPEED = 65535
MOTOR_PERCENTAGE_MULT = MAX_MOTOR_SPEED / 100.0
NEW_MOTOR_CURVE = [0.0, 0.0, 0.5, 60.0, 199.83333333333334, 377.66666666666663, 990.3333333333333, 1860.6666666666665, 2587.0, 3091.6666666666665, 3489.0, 3860.5, 4197.333333333333, 4432.166666666667, 4647.166666666666, 4873.166666666666, 5054.333333333334, 5208.666666666667, 5353.0, 5466.5, 5604.0]
MOTOR_CURVE = [0.0, 0.0, 426.5, 692.0, 842.5, 953.5, 1032.5, 1090.5, 1135.5, 1171.0, 1203.5, 1230.0, 1249.5, 1268.0, 1283.0, 1298.5, 1308.0, 1320.0, 1332.0, 1339.5, 1352.5]
SPEED_LOCK = False
SPEED_MULT = 1.0
class MotorMode(IntEnum):
COAST = 0,
FORWARD = 1,
BACKWARD = 2,
BREAK = 3
class Motor(object):
"""Class used to control the motors
"""
@staticmethod
def pwm(port: int, pwm: int, mode: MotorMode):
"""Set specified motor to a specific pwm value and motor mode
:param port: Port, which the motor is connected to. 1-4 allowed
:param pwm: Raw PWM value which the motor should be set to. From 0 to 2^16 - 1
:param mode: Motor mode. See enum MotorMode for more info
:raises: IndexError
"""
if SPEED_LOCK:
return
if port <= 0 or port > MOTOR_COUNT:
raise IndexError("Invalid Motor port specified!")
if port == 1:
Spi.write(Register.MOTOR_1_PWM_H, 2, pwm)
Spi.write(Register.PWM_1_CTRL, 1, int(mode))
elif port == 2:
Spi.write(Register.MOTOR_2_PWM_H, 2, pwm)
Spi.write(Register.PWM_2_CTRL, 1, int(mode))
elif port == 3:
Spi.write(Register.MOTOR_3_PWM_H, 2, pwm)
Spi.write(Register.PWM_3_CTRL, 1, int(mode))
elif port == 4:
Spi.write(Register.MOTOR_4_PWM_H, 2, pwm)
Spi.write(Register.PWM_4_CTRL, 1, int(mode))
@staticmethod
def power_raw(port: int, percent: float, log_metric = True):
"""Set specified motor to percentage power
:param port: Port, which the motor is connected to. 1-4
:param percent: Percentage of max speed. between -100 and 100
:raises: IndexError
"""
if port <= 0 or port > MOTOR_COUNT:
raise IndexError("Invalid Motor port specified!")
if percent < -100 or percent > 100:
raise IndexError("Invalid Motor speed specified! Speed is between -100 and 100 percent!")
if log_metric:
MetricsLogging.put("MotorRaw", float(percent), port)
mode = MotorMode.COAST
if percent < 0:
percent = abs(percent)
mode = MotorMode.BACKWARD
elif percent > 0:
mode = MotorMode.FORWARD
percent *= SPEED_MULT
pwm = percent * MOTOR_PERCENTAGE_MULT
Motor.pwm(port, int(pwm), mode)
@staticmethod
def power(port: int, percent: float):
"""Set specified motor to percentage power, percentage is linearized
so that it's roughly proportional to the speed
:param port: Port, which the motor is connected to. 1-4
:param percent: Percentage of max speed. between -100 and 100
:raises: IndexError
"""
raw_power = raw_power = Motor.__linearizePower(MOTOR_CURVE, abs(percent))
if percent < 0:
raw_power *= -1
MetricsLogging.put("Motor", float(percent), port)
Motor.power_raw(port, raw_power, False)
@staticmethod
def all_off():
"""
Turns of all motors
"""
Logging.get_logger().debug(f"Motor.all_off")
for i in range(1, MOTOR_COUNT + 1):
Motor.active_break(i)
@staticmethod
def active_break(port: int):
"""
Actively break with a specific motor
:param port: Port, which the motor is connected to. 1-4
"""
Motor.pwm(port, 0, MotorMode.BREAK)
@staticmethod
def set_motor_curve(curve):
"""
Set the global motor curve, must be a float array with exactly 21 elements.
[0] = x ticks/s (0% power)
[1] = x ticks/s (5% power)
[2] = x ticks/s (10% power)
...
[20] = x ticks/s (100% power)
:param curve: float array with 21 elements
:raises: ValueError
"""
if (len(curve) != 21):
raise ValueError('The motor curve is invalid, check documentation for set_motor_curve()!')
global MOTOR_CURVE
MOTOR_CURVE = curve
@staticmethod
def get_motor_curve():
"""
Get the currently active motor curve. Check set_motor_curve() for more info.
:return: current motor curve
"""
return MOTOR_CURVE
@staticmethod
def __map(x, in_min, in_max, out_min, out_max):
"""
Linear interpolation. Check https://www.arduino.cc/reference/en/language/functions/math/map/
"""
x = float(x)
in_min = float(in_min)
in_max = float(in_max)
out_min = float(out_min)
out_max = float(out_max)
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
@staticmethod
def __interpolateSpeed(curve, speed):
"""
Interpolate a speed in the specified motor curve and return
the 'power percentage that is needed to reach that speed'
"""
if speed < min(curve) or speed > max(curve):
raise ValueError(f'Speed out of range: {str(speed)} ticks/s')
for index in range(len(curve) - 1):
if speed >= curve[index] and speed <= curve[index + 1] and curve[index] != curve[index + 1]:
return Motor.__map(speed, curve[index], curve[index + 1], index * 5, index * 5 + 5)
return 0
@staticmethod
def __linearizePower(curve, power):
"""
Takes raw power and returns it corrected so that the motor speed would be roughly linear
"""
if power < 0 or power > 100:
raise ValueError(f'Power out of range: {str(power)}%')
requiredSpeed = Motor.__map(power, 0, 100, min(curve), max(curve))
return Motor.__interpolateSpeed(curve, requiredSpeed)
@staticmethod
def robot_state_loop():
import time
from compLib.Api import Seeding
global SPEED_LOCK
global SPEED_MULT
while True:
try:
state, status = Seeding.get_robot_state()
SPEED_MULT = state["speed"] / 100.0
if state["left"] != -1 or state["right"] != -1:
Motor.power(1, state["right"])
Motor.power(4, state["left"])
SPEED_LOCK = True
time.sleep(state["time"])
SPEED_LOCK = False
Motor.power(1, 0)
Motor.power(4, 0)
else:
time.sleep(0.25)
except:
time.sleep(0.25)
pass
@staticmethod
def start_robot_state_loop():
from threading import Thread
robot_state_thread = Thread(target=Motor.robot_state_loop)
robot_state_thread.setDaemon(True)
robot_state_thread.start()
atexit.register(Motor.all_off)

101
client_s1/compLib/Odom.py Normal file
View file

@ -0,0 +1,101 @@
import time
import math
from compLib.Encoder import Encoder
from compLib.Robot import Robot
last_run = time.time()
last_enc_left = 0
last_enc_right = 0
pos_x = 0
pos_y = 0
orientation = 0
class Odometry():
"""DTO used for holding all odometry information. \n
Coordinate system: \n
X: + Forward; - Backwards \n
Y: + Right; - Left \n
Orientation: + Right; - Left
"""
def __init__(self, x, y, orientation):
self.x = x
self.y = y
self.orientation = orientation
def get_x(self) -> float:
"""Returns distance driven on x-axis in meters"""
return self.x
def get_y(self) -> float:
"""Returns distance driven on y-axis in meters"""
return self.y
def get_orientation(self) -> float:
"""Returns degrees turned in radians"""
return self.orientation
def __str__(self):
return f"X: {self.x} Y: {self.y} O: {self.orientation}"
class Odom(object):
"""Class used to track the movement of the robot in X, Y, Theta (Orientation)
"""
@staticmethod
def get_odom() -> Odometry:
"""
:return: Current orientation of the robot
"""
return Odometry(pos_x, pos_y, orientation)
@staticmethod
def clear() -> None:
"""
Clears the current odometry information and start from X, Y, Orientation set to 0
"""
global last_run, last_enc_left, last_enc_right, pos_x, pos_y, orientation
last_run = 0
last_enc_left = 0
last_enc_right = 0
pos_x = 0
pos_y = 0
orientation = 0
@staticmethod
def update() -> None:
"""
Updates the current odometry information of the robot, Should be called in a loop with at least 100HZ.
Do not clear encoder positions between updates! Must be cleared, when clearing encoders!
"""
global last_run, last_enc_left, last_enc_right, pos_x, pos_y, orientation
now = time.time()
if last_run + 0.01 < now:
last_run = now
encoder_positions = Encoder.read_all()
current_left = encoder_positions[Robot.LEFT_PORT - 1]
current_right = -encoder_positions[Robot.RIGHT_PORT - 1]
distance_left = (current_left - last_enc_left) / Robot.TICKS_PER_METER
distance_right = (current_right - last_enc_right) / Robot.TICKS_PER_METER
last_enc_left = current_left
last_enc_right = current_right
distance = (distance_left + distance_right) / 2.0
theta = (distance_left - distance_right) / Robot.ARBOR_LENGTH_M
if distance != 0:
distance_x = math.cos(theta) * distance
distance_y = -math.sin(theta) * distance
pos_x = pos_x + (math.cos(orientation) * distance_x -
math.sin(orientation) * distance_y)
pos_y = pos_y + (math.sin(orientation) * distance_x +
math.cos(orientation) * distance_y)
if theta != 0:
orientation += theta

View file

@ -0,0 +1,24 @@
import RPi.GPIO as GPIO
import time
GPIO.setwarnings(False)
RESET_PIN = 23
BOOT_PIN = 17
class Reset:
"""Reset the co-processor
"""
@staticmethod
def reset_bot():
"""Reset the co-processor
"""
GPIO.setmode(GPIO.BCM)
GPIO.setup(RESET_PIN, GPIO.OUT)
GPIO.setup(BOOT_PIN, GPIO.OUT)
GPIO.output(RESET_PIN, GPIO.LOW)
GPIO.output(BOOT_PIN, GPIO.LOW)
time.sleep(0.1)
GPIO.output(RESET_PIN, GPIO.HIGH)
time.sleep(1.5)

View file

@ -0,0 +1,24 @@
import math
class Robot(object):
WHEEL_CIRCUMFERENCE_MM = 71.0 * math.pi
"""Circumference of a wheel in millimeters"""
TICKS_PER_TURN = 27.7 * 100.0
"""Ticks per 360 degree turn of a wheel"""
ARBOR_LENGTH_MM = 139.0
"""Distance between the two wheels in millimeters"""
ARBOR_LENGTH_M = ARBOR_LENGTH_MM / 1000.0
"""Distance between the two wheels in meters"""
TICKS_PER_METER = 1000.0 / WHEEL_CIRCUMFERENCE_MM * TICKS_PER_TURN
"""Ticks after driving one meter"""
LEFT_PORT = 4
"""Motor port for the left motor"""
RIGHT_PORT = 1
"""Motor port for the right motor"""

View file

@ -0,0 +1,63 @@
from compLib.Spi import Spi, Register
SERVO_COUNT = 8
MIN_ANGLE = -90.0
MAX_ANGLE = 90.0
def map(x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
class Servo:
"""Control the servo ports on the robot
"""
@staticmethod
def set_position(port: int, angle: float, offset: float = 90.0):
"""Set position of servo connected to port
:param port: port between 1 and 8
:param angle: Angle of servo
"""
if port <= 0 or port > SERVO_COUNT:
raise IndexError("Invalid Servo port specified!")
angle = max(min(angle, MAX_ANGLE), MIN_ANGLE)
mapped_angle = int(map(angle, MIN_ANGLE, MAX_ANGLE, 0, 65535))
if port == 1:
Spi.write(Register.SERVO_1_PWM_H, 2, mapped_angle)
Spi.write(Register.PWM_1_CTRL, 1, 4)
elif port == 2:
Spi.write(Register.SERVO_2_PWM_H, 2, mapped_angle)
Spi.write(Register.PWM_1_CTRL, 1, 4)
elif port == 3:
Spi.write(Register.SERVO_3_PWM_H, 2, mapped_angle)
Spi.write(Register.PWM_2_CTRL, 1, 4)
elif port == 4:
Spi.write(Register.SERVO_4_PWM_H, 2, mapped_angle)
Spi.write(Register.PWM_2_CTRL, 1, 4)
elif port == 5:
Spi.write(Register.SERVO_5_PWM_H, 2, mapped_angle)
Spi.write(Register.PWM_3_CTRL, 1, 4)
elif port == 6:
Spi.write(Register.SERVO_6_PWM_H, 2, mapped_angle)
Spi.write(Register.PWM_3_CTRL, 1, 4)
elif port == 7:
Spi.write(Register.SERVO_7_PWM_H, 2, mapped_angle)
Spi.write(Register.PWM_4_CTRL, 1, 4)
elif port == 8:
Spi.write(Register.SERVO_8_PWM_H, 2, mapped_angle)
Spi.write(Register.PWM_4_CTRL, 1, 4)
@staticmethod
def setup_position():
"""Set position of servos to the position used during the setup process
"""
for i in range(1, SERVO_COUNT + 1):
Servo.set_position(i, 0)

222
client_s1/compLib/Spi.py Normal file
View file

@ -0,0 +1,222 @@
import importlib
from threading import Thread
import multiprocessing
from enum import IntEnum
import time
import sys
import random
from compLib.LogstashLogging import Logging
SPI_BUS = 1
SPI_DEVICE = 2
SPI_SPEED = 4000000
SPI_BUFFER_SIZE = 32
SPI_HEALTH = True
# For development purposes
spi_found = importlib.util.find_spec("spidev") is not None
spi = None
spi_lock = multiprocessing.Lock()
last_spi_call = time.time()
if spi_found:
import spidev
spi = spidev.SpiDev()
spi.open(SPI_BUS, SPI_DEVICE)
spi.max_speed_hz = SPI_SPEED
spi.mode = 0
spi.bits_per_word = 8
class Register(IntEnum):
IDENTIFICATION_MODEL_ID = 1,
IDENTIFICATION_MODEL_REV_MAJOR = 2,
IDENTIFICATION_MODEL_REV_MINOR = 3,
IDENTIFICATION_MODEL_REV_PATCH = 4,
# Motor encoder positions
MOTOR_1_POS_B3 = 10,
MOTOR_1_POS_B2 = 11,
MOTOR_1_POS_B1 = 12,
MOTOR_1_POS_B0 = 13,
MOTOR_2_POS_B3 = 14,
MOTOR_2_POS_B2 = 15,
MOTOR_2_POS_B1 = 16,
MOTOR_2_POS_B0 = 17,
MOTOR_3_POS_B3 = 18,
MOTOR_3_POS_B2 = 19,
MOTOR_3_POS_B1 = 20,
MOTOR_3_POS_B0 = 21,
MOTOR_4_POS_B3 = 22,
MOTOR_4_POS_B2 = 23,
MOTOR_4_POS_B1 = 24,
MOTOR_4_POS_B0 = 25,
# PWM Control Modes
PWM_1_CTRL = 26,
PWM_2_CTRL = 27,
PWM_3_CTRL = 28,
PWM_4_CTRL = 29,
# Motor pwm speed
MOTOR_1_PWM_H = 30,
MOTOR_1_PWM_L = 31,
MOTOR_2_PWM_H = 32,
MOTOR_2_PWM_L = 33,
MOTOR_3_PWM_H = 34,
MOTOR_3_PWM_L = 35,
MOTOR_4_PWM_H = 36,
MOTOR_4_PWM_L = 37,
# Servo goal position
SERVO_1_PWM_H = 38,
SERVO_1_PWM_L = 39,
SERVO_2_PWM_H = 40,
SERVO_2_PWM_L = 41,
SERVO_3_PWM_H = 42,
SERVO_3_PWM_L = 43,
SERVO_4_PWM_H = 44,
SERVO_4_PWM_L = 45,
SERVO_5_PWM_H = 46,
SERVO_5_PWM_L = 47,
SERVO_6_PWM_H = 48,
SERVO_6_PWM_L = 49,
SERVO_7_PWM_H = 50,
SERVO_7_PWM_L = 51,
SERVO_8_PWM_H = 52,
SERVO_8_PWM_L = 53,
# IR Sensor value
IR_1_H = 54,
IR_1_L = 55,
IR_2_H = 56,
IR_2_L = 57,
IR_3_H = 58,
IR_3_L = 59,
IR_4_H = 60,
IR_4_L = 61,
IR_5_H = 62,
IR_5_L = 63,
IR_1_LED = 64,
IR_2_LED = 65,
IR_3_LED = 66,
IR_4_LED = 67,
IR_5_LED = 68,
# Display registers
DISPLAY_LINE_1_C0 = 69,
DISPLAY_LINE_2_C0 = 85,
DISPLAY_LINE_3_C0 = 101,
DISPLAY_LINE_4_C0 = 117
class Spi(object):
@staticmethod
def transfer(tx_buffer: list):
if not spi_found:
return [] * SPI_BUFFER_SIZE
write_reg = tx_buffer[1]
tx_buffer_copy = tx_buffer.copy()
with spi_lock:
spi.xfer(tx_buffer)
rx_buffer = spi.xfer([0] * SPI_BUFFER_SIZE)
if rx_buffer[1] != write_reg:
Logging.get_logger().error(f"Warning! SPI error during read/write of register {write_reg}! Retrying automagically")
time.sleep(random.uniform(0.0, 0.1))
return Spi.transfer(tx_buffer_copy)
global last_spi_call
last_spi_call = time.time()
return rx_buffer
@staticmethod
def read(reg: int, length: int):
return int.from_bytes(Spi.read_array(reg, length), byteorder='big', signed=False)
@staticmethod
def read_array(reg: int, length: int):
if not type(reg) is int:
reg = int(reg)
tx_buf = [0] * SPI_BUFFER_SIZE
tx_buf[0] = 0
tx_buf[1] = reg
tx_buf[2] = length
rx_buf = Spi.transfer(tx_buf)
return rx_buf[2:2 + length]
@staticmethod
def write(reg: int, length: int, value: int):
if not type(reg) is int:
reg = int(reg)
tx_buf = [0] * SPI_BUFFER_SIZE
tx_buf[0] = 1
tx_buf[1] = reg
tx_buf[2] = length
pos = 3
for i in value.to_bytes(length, 'big'):
tx_buf[pos] = i
pos += 1
rx_buf = Spi.transfer(tx_buf)
return int.from_bytes(rx_buf[2:2 + length], byteorder='big', signed=False)
@staticmethod
def write_array(reg: int, length: int, values):
if not type(reg) is int:
reg = int(reg)
tx_buf = [0] * SPI_BUFFER_SIZE
tx_buf[0] = 1
tx_buf[1] = reg
tx_buf[2] = length
pos = 3
for i in values:
tx_buf[pos] = i
pos += 1
rx_buf = Spi.transfer(tx_buf)
return rx_buf
@staticmethod
def health_check():
if Spi.read(Register.IDENTIFICATION_MODEL_ID, 1) != 3:
Logging.get_logger().error(f"Unable to read Version! Make sure the mainboard is connected!")
print("Unable to read Version! Make sure the mainboard is connected!")
@staticmethod
def start_health_check_loop():
health_check_thread = Thread(target=Spi.health_check_loop)
health_check_thread.setDaemon(True)
health_check_thread.start()
@staticmethod
def disable_health_check():
global SPI_HEALTH
SPI_HEALTH = False
@staticmethod
def health_check_loop():
while SPI_HEALTH:
if last_spi_call + 0.5 < time.time():
Spi.health_check()
time.sleep(0.1)
@staticmethod
def get_version():
major = Spi.read(Register.IDENTIFICATION_MODEL_REV_MAJOR, 1)
minor = Spi.read(Register.IDENTIFICATION_MODEL_REV_MINOR, 1)
patch = Spi.read(Register.IDENTIFICATION_MODEL_REV_PATCH, 1)
return f"{major}.{minor}.{patch}"

207
client_s1/compLib/Vision.py Normal file
View file

@ -0,0 +1,207 @@
import os
import queue
import socket
import threading
import cv2
from flask import Flask, Response
from compLib.LogstashLogging import Logging
RTMP_SERVER = os.getenv("RTMP_SERVER", "rtmp://localhost/live/stream")
SERVE_VIDEO = os.getenv("SERVER_SRC", "/live")
BUILDING_DOCS = os.getenv("BUILDING_DOCS", "false")
app = Flask(__name__)
HTML = """
<html>
<head>
<title>Opencv Output</title>
</head>
<body>
<h1>Opencv Output</h1>
<img src="{{ VIDEO_DST }}">
</body>
</html>
"""
# it would be better to use jinja2 here, but I don't want to blow up the package dependencies...
HTML = HTML.replace("{{ VIDEO_DST }}", SERVE_VIDEO)
class __Streaming:
"""
Class that handles rtmp streaming for opencv.
DO NOT CREATE AN INSTANCE OF THIS CLASS YOURSELF!
This is automatically done when importing this module. Use Vision.Streaming which is
an instance of this class!
grab frames -> do your own processing -> publish frame -> view on http server
"""
class __NoBufferVideoCapture:
def __init__(self, cam):
self.cap = cv2.VideoCapture(cam)
self.cap.set(3, 640)
self.cap.set(4, 480)
self.q = queue.Queue(maxsize=5)
t = threading.Thread(target=self._reader)
t.daemon = True
t.start()
def _reader(self):
while True:
ret, frame = self.cap.read()
if not ret:
break
if not self.q.empty():
try:
self.q.get_nowait()
except queue.Empty:
pass
self.q.put(frame)
def read(self):
return self.q.get()
def __init__(self):
"""
Create instance of __Streaming class
This is done implicitly when importing the vision module and will only fail if you would
create an object of this class. (There can (SHOULD!) only be one VideCapture)
"""
Logging.get_logger().info("capturing rtmp stream is disabled in this version")
#self.__camera_stream = cv2.VideoCapture(RTMP_SERVER)
#self.__camera_stream = cv2.VideoCapture(0)
#self.__camera_stream.set(3, 640)
#self.__camera_stream.set(4, 480)
self.__camera_stream = self.__NoBufferVideoCapture(0)
self.__newest_frame = None
self.__lock = threading.Lock()
Logging.get_logger().info("Initialized vision")
def get_frame(self):
"""
Grab the newest frame from the rtmp stream.
:return: An opencv frame
"""
#ret, img16 = self.__camera_stream.read()
img16 = self.__camera_stream.read()
return img16
def publish_frame(self, image):
"""
Publish an opencv frame to the http webserver.
:param image: Opencv frame that will be published
:return: None
"""
with self.__lock:
if image is not None:
self.__newest_frame = image.copy()
def _newest_frame_generator(self):
"""
Private generator which is called directly from flask server.
:return: Yields image/jpeg encoded frames published from publish_frame function.
"""
while True:
# use a buffer frame to copy the newest frame with lock and then freeing it immediately
buffer_frame = None
with self.__lock:
if self.__newest_frame is None:
continue
buffer_frame = self.__newest_frame.copy()
# encode frame for jpeg stream
(flag, encoded_image) = cv2.imencode(".jpg", buffer_frame)
# if there was an error try again with the next frame
if not flag:
continue
# else yield encoded frame with mimetype image/jpeg
yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' +
bytearray(encoded_image) + b'\r\n')
Streaming = None
if BUILDING_DOCS == "false":
# instantiate private class __Streaming
Streaming = __Streaming()
Logging.get_logger().info("created instance of streaming class")
@app.route("/live")
def __video_feed():
"""
Define route for serving jpeg stream.
:return: Return the response generated along with the specific media.
"""
return Response(Streaming._newest_frame_generator(),
mimetype="multipart/x-mixed-replace; boundary=frame")
@app.route("/")
def __index():
"""
Define route for serving a static http site to view the stream.
:return: Static html page
"""
return HTML
def get_ip():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('10.255.255.255', 1))
IP = s.getsockname()[0]
except Exception as e:
IP = '127.0.0.1'
print(f"Error could not query ip: {e}")
finally:
s.close()
return IP
def __start_flask():
"""
Function for running flask server in a thread.
:return:
"""
Logging.get_logger().info("starting flask server")
app.run(host="0.0.0.0", port=9898, debug=True, threaded=True, use_reloader=False)
ip = get_ip()
Logging.get_logger().info(f"Vision stream started and can be viewed on: {ip}:9898")
print(f"\033[92mVision stream started and can be viewed on: {ip}:9898\033[0m")
if BUILDING_DOCS == "false":
# start flask service in the background
__webserver_thread = threading.Thread(target=__start_flask)
__webserver_thread.start()
# for debugging and testing start processing frames and detecting a 6 by 9 calibration chessboard
if __name__ == '__main__' and BUILDING_DOCS == "false":
while True:
frame = Streaming.get_frame()
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# processing
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# find the chessboard corners
ret, corners = cv2.findChessboardCorners(gray, (6, 9), None)
cv2.drawChessboardCorners(frame, (6, 9), corners, ret)
Streaming.publish_frame(frame)

View file

@ -0,0 +1,50 @@
__version__ = "0.4.1-1"
import sys
import importlib
import compLib.LogstashLogging
import compLib.MetricsLogging
from compLib.Lock import Lock
apt_found = importlib.util.find_spec("apt") is not None
spi_found = importlib.util.find_spec("spidev") is not None
if apt_found:
import apt
try:
__versions = apt.Cache()["python3-complib"].versions
if len(__versions) != 1:
print(f"Starting compLib! \033[91mVersion: {__version__} is outdated\033[0m\n"
f"\033[92m[!] run the command 'sudo apt update && sudo apt install python3-complib' to install the newest version\033[0m")
else:
print(f"Starting compLib! \033[92mVersion: {__version__} is up to date\033[0m")
except Exception as e:
compLib.LogstashLogging.Logging.get_logger().error(f"error during checking apt package version -> {str(e)}")
print(f"\033[91merror during checking apt package version -> {str(e)}\033[0m\n")
else:
print("apt is not installed! This is for local development only!")
if Lock.is_locked():
print("Another program using compLib is still running! Delete /root/complib.lock if this is an error. Exiting...")
sys.exit()
else:
Lock.lock()
if spi_found:
from compLib.Spi import Spi
from compLib.Reset import Reset
from compLib.Encoder import Encoder
from compLib.Motor import Motor
import logging
print(f"\033[34mInitializing chipmunk board...\033[0m")
Reset.reset_bot()
Spi.health_check()
Spi.start_health_check_loop()
Motor.start_robot_state_loop()
Encoder.clear_all()
print(f"\033[34mReady\033[0m\n")
else:
print("spidev is not installed! This is for local development only!")

11
client_s1/complib.service Normal file
View file

@ -0,0 +1,11 @@
[Unit]
Description=Monitoring service
[Service]
ExecStart=/usr/bin/python3 /usr/local/lib/python3.7/dist-packages/compLib/IPService.py
Environment="debug=False"
Restart=always
RestartSec=5
Type=notify
[Install]
Alias=complib
WantedBy=default.target

2
client_s1/docs/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
build
logs.db

View file

@ -22,7 +22,6 @@ os.environ["EXTENSIVE_LOGGING"] = "False"
project = 'CompLib'
copyright = '2022, Verein zur Förderung von Wissenschaft und Technik an Schulen (F-WuTS)'
author = 'robo4you'
autoclass_content = 'both'
# The full version, including alpha/beta/rc tags
release = '0.2.3'
@ -59,11 +58,3 @@ html_theme = 'sphinx_rtd_theme'
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_logo = "images/compair-logo-white.svg"
html_theme_options = {
'logo_only': True,
'display_version': False,
}
language = "de"

View file

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Before After
Before After

View file

@ -0,0 +1,19 @@
.. image:: images/compair-logo-white.svg
Competition Robot Library
#############################
.. toctree::
:maxdepth: 2
:caption: Contents:
Contents
*********
.. toctree::
:maxdepth: 5
:glob:
self
other/usage
lib/*

View file

@ -0,0 +1,107 @@
.. _lib_api:
Api
****
Seeding
========
.. autoclass:: compLib.Api.Seeding
:members:
Double Elimination
===================
.. autoclass:: compLib.Api.DoubleElim
:members:
Position
========
.. autoclass:: compLib.Api.Position
:members:
Examples
========
Calling Seeding API
---------------------
.. code-block:: python
from compLib.Api import Seeding
zones, code = Seeding.get_delivery()
if code == 403:
print(f"I am not in the correct zone to make that request!")
else:
print(f"First we need to go to zone {zone[0]}")
# put code here to follow line and drive to the zone
print(f"Now we need to go to zone {zone[1]}")
# put code here to follow line and drive to the next zone
print(f"Now we need to go to zone {zone[2]}")
# put code here to follow line and drive to the last zone
print(f"We delivered all packages, hopefully we scored some points!")
.. code-block:: python
from compLib.Api import Seeding
package, code = Seeding.get_cargo("yellow")
if code == 403:
print(f"I am not in the correct zone to make that request!")
elif code == 404:
print(f"I am in the correct zone, but there is no yellow package here.")
elif code == 413:
print(f"I am in the correct zone, but I already have two packages loaded.")
else code == 200:
print(f"The {package['color']} has been picked up!")
Calling Double Elimination API
----------------------------------
.. code-block:: python
from compLib.Api import DoubleElim
position, status = DoubleElim.get_position()
print(f"Position of my robot is: x={position.x}, y={position.y} and rotation is: {position.degrees}, the server responded with status code: {status}")
goal, status = DoubleElim.get_goal()
print(f"Goal is at: x={goal.x}, y={goal.y}, the server responded with status code: {status}")
.. code-block:: python
from compLib.Api import DoubleElim
import time
# function which waits for the game to be started (you should include this in your double elimination program)
def wait_for_start():
_, status = DoubleElim.get_position()
while status == 503:
time.sleep(0.1)
_, status = DoubleElim.get_position()
wait_for_start()
print(f"Game has started, lets score some points!!")
position, status = DoubleElim.get_position()
print(f"Position of my robot is: x={position.x}, y={position.y} and rotation is: {position.degrees}, the server responded with status code: {status}")
opponent_position, status = DoubleElim.get_opponent()
print(f"Position of the opponents robot is: x={opponent_position.x}, y={opponent_position.y} and rotation is: {opponent_position.degrees}, the server responded with status code: {status}")
goal, status = DoubleElim.get_goal()
print(f"Goal is at: x={goal.x}, y={goal.y}, the server responded with status code: {status}")
items, status = DoubleElim.get_items()
print(f"There are currently {len(items)} on the gameboard: {items}, the server responded with status code: {status}")
score, status = DoubleElim.get_score()
print(f"The current score of the game is {score}, the server responded with status code: {status}")
meteoroids, status = DoubleElim.get_meteoroids()
print(f"The current meteoroids in the game are {meteoroids}, the server responded with status code: {status}")
In this second example we wait until the game is started by the judges and then make all possible requests once. You should use the wait_for_start function in your double elimination program. If your robot starts too soon your run will not count!

View file

@ -0,0 +1,83 @@
.. _lib_vision:
Aruco
*******
Examples
=========
Recognizing ArUco tags
-------------------------
.. code-block:: python
import time
import cv2
from cv2 import aruco
from compLib import Vision
ARUCO_DICT = cv2.aruco.Dictionary_get(aruco.DICT_6X6_250)
ARUCO_PARAMETERS = aruco.DetectorParameters_create()
def getTagCenterFromFrame(id, frame):
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
corners, ids, rejectedImgPoints = aruco.detectMarkers(gray, ARUCO_DICT, parameters = ARUCO_PARAMETERS)
frame = aruco.drawDetectedMarkers(frame.copy(), corners, ids)
if ids is None:
return frame, None, None
for tag_id, corner in zip(ids, corners):
if (tag_id[0] == id):
x, y = 0, 0
for i in range(4):
x += corner[0][i][0] * 0.25
y += corner[0][i][1] * 0.25
return frame, x, y
return frame, None, None
# Get the center from the aruco tag with the specified id
# in pixel coordinates (0-640, 0-480)
def getTagPosition(id):
frame = Vision.Streaming.get_frame()
frame, x, y = getTagCenterFromFrame(id, frame)
Vision.Streaming.publish_frame(frame)
return x, y
# Get the normalized center coordinates from the aruco tag
# with the specified id
# left is -1, right +1
# bottom is -1, top +1
def getNormalizedTagPosition(id):
frame = Vision.Streaming.get_frame()
frame, x, y = getTagCenterFromFrame(id, frame)
Vision.Streaming.publish_frame(frame)
if x is None or y is None:
return None, None
height, width = frame.shape[:2]
x = x / width * 2.0 - 1.0
y = -(y / height * 2.0 - 1.0)
return x, y
if __name__ == '__main__':
desiredID = 11
while True:
x, y = getNormalizedTagPosition(desiredID)
if x is not None:
print("X Coordinate: ", x)
else:
print("Tag not found")
This example shows how to recognize ArUco tags based on their id and position.
You can specify an ID of the tag you want to use and if it's found, the coordinates of the center are returned.
With the normalized function this is very easy: The x-coordinate is -1 on the left, 1 on the right and 0 in the center of the screen, same for y.
This way it is quite simple to act on the position of the tag.

View file

@ -0,0 +1,23 @@
.. _lib_display:
Display
*******
Class Documentation
====================
.. autoclass:: compLib.Display.Display
:members:
Examples
=========
Write a line to the display
---------------------------
.. code-block:: python
import time
from compLib.Display import Display
Display.write(1, "Hello World!")

View file

@ -3,8 +3,8 @@
Encoder
*******
Dokumentation der Klasse
========================
Class Documentation
====================
.. autoclass:: compLib.Encoder.Encoder
:members:

View file

@ -0,0 +1,20 @@
.. _lib_irsensor:
Infrared Sensor
****************
.. autoclass:: compLib.IRSensor.IRSensor
:members:
Examples
=========
Testing analog sensors
-------------------------
.. code-block:: python
from compLib import IRSensor
while True:
print ("left: {} middle: {} right: {}".format(IRSensor.read(1), IRSensor.read(3), IRSensor.read(5)))

View file

@ -0,0 +1,79 @@
.. _lib_Linefollower:
Linefollower Examples
*********************
Simple Linefollower
-------------------------
.. code-block:: python
from compLib.Motor import Motor
from compLib.Display import Display
from compLib.IRSensor import IRSensor
from compLib.Encoder import Encoder
import time
IRSensor.set(1, True)
IRSensor.set(2, True)
IRSensor.set(3, True)
IRSensor.set(4, True)
IRSensor.set(5, True)
DRIVE_SPEED = 75
COLOR_BREAK = 900
KP = 10.0
KD = 0.0
def drive(leftSpeed, rightSpeed):
rightSpeed *= -0.906
Motor.power(1, min(max(-100, rightSpeed), 100))
Motor.power(4, min(max(-100, leftSpeed), 100))
def follow(sleepTime = 0.1):
lastError = 0
sensorsBlack = 0
while sensorsBlack < 3:
sensorsBlack = 0
for i in range(1, 6):
if IRSensor.read(i) > COLOR_BREAK:
sensorsBlack += 1
error = lastError
if IRSensor.read(3) > COLOR_BREAK:
error = 0
elif IRSensor.read(1) > COLOR_BREAK:
error = -1.5
elif IRSensor.read(5) > COLOR_BREAK:
error = 1.5
elif IRSensor.read(2) > COLOR_BREAK:
error = -1
elif IRSensor.read(4) > COLOR_BREAK:
error = 1
elif error == 1.5:
error = 3
elif error == -1.5:
error = -3
lastError = error
adjustment = KP * error + KD * (error - lastError)
leftSpeed = DRIVE_SPEED + adjustment
rightSpeed = DRIVE_SPEED - adjustment
print(f"{leftSpeed} {rightSpeed} {adjustment} {error}")
drive(leftSpeed, rightSpeed)
drive(0, 0)
time.sleep(sleepTime)
def main():
follow()
follow()
follow()
follow()
follow(0.2)
main()

View file

@ -0,0 +1,22 @@
.. _lib_logging:
Logging
*******
Class Documentation
====================
.. autoclass:: compLib.LogstashLogging.Logging
:members:
Examples
=========
Turn up the logging
--------------------
.. code-block:: python
from compLib.LogstashLogging import Logging
Logging.set_debug()

View file

@ -0,0 +1,23 @@
.. _lib_motor:
Motor
******
Class Documentation
====================
.. autoclass:: compLib.Motor.Motor
:members:
Examples
=========
Driving straight (maybe)
-------------------------
.. code-block:: python
from compLib.Motor import Motor
Motor.power(1, -50)
Motor.power(4, 50)

View file

@ -0,0 +1,50 @@
.. _lib_odom:
Odometry
********
Class Documentation
====================
.. autoclass:: compLib.Odom.Odometry
:members:
.. autoclass:: compLib.Odom.Odom
:members:
Examples
=========
Getting actual distance driven
------------------------------
.. code-block:: python
import time
import math
from compLib.Motor import Motor
from compLib.Encoder import Encoder
from compLib.Odom import Odom, Odometry
# distance in meters
# speed in % of max speed
def drive_example(distance, speed):
Odom.update()
odom = Odom.get_odom()
while abs(odom.get_x()) < distance:
Odom.update()
odom = Odom.get_odom()
Motor.power(4, speed)
Motor.power(1, -speed)
print(f" Forward: {odom.get_x()} m")
print(f" Right: {odom.get_y()} m")
print(f" Turned: {math.degrees(odom.get_orientation())} degrees")
Motor.active_break(1)
Motor.active_break(4)
time.sleep(0.1)
Encoder.clear_all()
Odom.clear()

View file

@ -0,0 +1,71 @@
.. _lib_qc:
Quality Control
###############
Infrared Test
-------------------------
.. code-block:: python
from compLib.IRSensor import IRSensor
import time
IRSensor.set(1, True)
IRSensor.set(2, True)
IRSensor.set(3, True)
IRSensor.set(4, True)
IRSensor.set(5, True)
while True:
t = time.time()
for i in range(1, 6):
print(f"{i}: {IRSensor.read(i)}")
print("")
time.sleep(0.2)
Motor Test
-------------------------
.. code-block:: python
from compLib.Motor import Motor
from compLib.Encoder import Encoder
import time
Motor.power(1, -50)
Motor.power(4, 50)
while True:
print(f"L:{Encoder.read(4)} R:{Encoder.read(1)}")
time.sleep(0.1)
Servo Test
-------------------------
.. code-block:: python
from compLib.Servo import Servo
import time
for i in range(1, 8 + 1):
Servo.set_position(i, 45)
print(f"{i}")
time.sleep(1)
Servo.setup_position()
time.sleep(10)
Vision Test
-------------------------
.. code-block:: python
import cv2
from compLib import Vision
from compLib.Servo import Servo
while True:
frame = Vision.Streaming.get_frame()
Vision.Streaming.publish_frame(frame)
Servo.set_position(3, -45)

View file

@ -0,0 +1,11 @@
.. _lib_robot:
Robot
******
Class Documentation
====================
.. autoclass:: compLib.Robot.Robot
:members:
:private-members:

View file

@ -0,0 +1,7 @@
.. _lib_servo:
Servo
******
.. autoclass:: compLib.Servo.Servo
:members:

View file

@ -0,0 +1,104 @@
.. _lib_vision:
Vision
*******
This module provides an interface for grabbing an rtmp stream and using the images to do some processing in opencv.
How do I use this module?
1. Get frames from the raspberry pi camera
2. -- here comes your own processing --
3. Publish the processed frames on an http server
4. You can view the http stream of your processed images in a web browser
Opencv Stream
==============
Because of the rtmp stream needing to buffer some frames and waiting for P-Frames, importing this module might take up
to 5 Seconds.
.. autoclass:: compLib.Vision.__Streaming
:members:
Examples
=========
Using the Vision Module
-------------------------
.. code-block:: python
import cv2
from compLib import Vision
while True:
# get newest opencv frame from camera
frame = Vision.Streaming.get_frame()
# do some processing with the frame.....
# publish frame to streaming server
Vision.Streaming.publish_frame(frame)
Connect the raspberry pi to your internet and view the stream at: "http://your_raspi_ip:9898/". This should display
your raspberry pi camera. Note: the stream will lag a little bit BUT the processing of the image will be done in
realtime.
The output on the website should show whatever your raspberry pi cam records:
.. image:: images/opencv_http_stream.png
:width: 680
:alt: Processed frames from opencv
Chessboard Detection
------------------------------------------
In this example we process the captured stream of images and want to detect chessboards. Run this example and
point your raspberry pi camera to a chessboard and it should be detected.
For testing you can point it at this image:
.. image:: images/chessboard.jpg
:width: 680
:alt: Chessboard for opencv processing
.. code-block:: python
import cv2
from compLib import Vision
while True:
# get newest opencv frame from camera
frame = Vision.Streaming.get_frame()
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# convert image to grayscale image
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# find the chessboard corners
ret, corners = cv2.findChessboardCorners(gray, (6, 9), None)
# draw detected chessboard position onto the image
cv2.drawChessboardCorners(frame, (6, 9), corners, ret)
# publish frame to streaming server
Vision.Streaming.publish_frame(frame)
Connect the raspberry pi to your internet and view the stream at: "http://your_raspi_ip:9898/".
The output image should look like this:
.. image:: images/chessboard_detected.jpg
:width: 680
:alt: Processed frames from opencv
Here is a screenshot of the stream website while viewing the chessboard in this documentation.
.. image:: images/opencv_processed.png
:width: 680
:alt: Processed frames from opencv

View file

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 391 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 356 KiB

After

Width:  |  Height:  |  Size: 356 KiB

Before After
Before After

View file

@ -0,0 +1,29 @@
.. _other_usage:
Usage
######
.. code-block:: python
import time
from compLib.Motor import *
def forward():
Motor.power(1, -30);
Motor.power(2, 30);
def backward():
Motor.power(1, 30);
Motor.power(2, -30);
def main():
print("hallo ich bin ein roboter beep buup")
forward()
time.sleep(1)
backward()
time.sleep(1)
if __name__ == '__main__':
main()

63
client_s1/postinstall.sh Normal file
View file

@ -0,0 +1,63 @@
grep -qxF "apt update" /etc/rc.local
if [ $? -ne 0 ]; then
echo "adding apt update to rc.local"
sed -i "2s/^/apt update\n/" /etc/rc.local
fi
install_package() {
echo "Installing package '$1' via pip3"
pip3 install "$1"
if [ $? -eq 0 ]; then
echo "Successfully installed pip3 package '$1'"
else
echo "Could not install pip3 package '$1'"
exit 1
fi
}
#install_package "smbus"
#install_package "requests"
#install_package "flask"
#install_package "python-logstash-async"
#install_package "RPi.GPIO"
#install_package "spidev"
#install_package "influxdb_client"
pip3 install smbus requests flask python-logstash-async RPi.GPIO spidev influxdb_client filelock
echo "Setting up opencv4"
pkg-config --modversion opencv4
echo "Setting up nginx rtmp server"
sudo /etc/init.d/nginx start
{
echo 'load_module "modules/ngx_rtmp_module.so";'
echo 'worker_processes auto;'
echo 'rtmp_auto_push on;'
echo 'events {}'
echo 'rtmp {'
echo ' server {'
echo ' listen 1935;'
echo ' listen [::]:1935 ipv6only=on;'
echo ' application live {'
echo ' live on;'
echo ' record off;'
echo ' }'
echo ' }'
echo '}'
} >| /etc/nginx/nginx.conf
echo "Starting pigpiod daemon"
sudo systemctl start pigpiod
sudo systemctl enable pigpiod
base64 -d << UPD
CiBfX19fX18gICAgIF9fX19fXyAgICAgX18gICAgX18gICAgIF9fX19fXyAgIF9fICAgICAgICAgX18gICAgIF9fX19fXyAgICAgICAgICAgICAgICAgIAovXCAgX19fXCAgIC9cICBfXyBcICAgL1wgIi0uLyAgXCAgIC9cICA9PSBcIC9cIFwgICAgICAgL1wgXCAgIC9cICA9PSBcICAgICAgICAgICAgICAgICAKXCBcIFxfX19fICBcIFwgXC9cIFwgIFwgXCBcLS4vXCBcICBcIFwgIF8tLyBcIFwgXF9fX18gIFwgXCBcICBcIFwgIF9fPCAgICAgICAgICAgICAgICAgCiBcIFxfX19fX1wgIFwgXF9fX19fXCAgXCBcX1wgXCBcX1wgIFwgXF9cICAgIFwgXF9fX19fXCAgXCBcX1wgIFwgXF9fX19fXCAgICAgICAgICAgICAgIAogIFwvX19fX18vICAgXC9fX19fXy8gICBcL18vICBcL18vICAgXC9fLyAgICAgXC9fX19fXy8gICBcL18vICAgXC9fX19fXy8gICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiBfXyAgX18gICAgIF9fX19fXyAgICAgIF9fX19fXyAgIF9fX19fXyAgICAgICAgX19fX18gICAgIF9fX19fXyAgICAgX19fX19fICAgX19fX19fICAgIAovXCBcL1wgXCAgIC9cICA9PSBcICAgIC9cX18gIF9cIC9cICBfXyBcICAgICAgL1wgIF9fLS4gIC9cICBfXyBcICAgL1xfXyAgX1wgL1wgIF9fX1wgICAKXCBcIFxfXCBcICBcIFwgIF8tLyAgICBcL18vXCBcLyBcIFwgXC9cIFwgICAgIFwgXCBcL1wgXCBcIFwgIF9fIFwgIFwvXy9cIFwvIFwgXCAgX19cICAgCiBcIFxfX19fX1wgIFwgXF9cICAgICAgICAgXCBcX1wgIFwgXF9fX19fXCAgICAgXCBcX19fXy0gIFwgXF9cIFxfXCAgICBcIFxfXCAgXCBcX19fX19cIAogIFwvX19fX18vICAgXC9fLyAgICAgICAgICBcL18vICAgXC9fX19fXy8gICAgICBcL19fX18vICAgXC9fL1wvXy8gICAgIFwvXy8gICBcL19fX19fLyA=
UPD
echo ""
echo ""
service complib restart
systemctl enable complib

37
client_s1/setup.py Normal file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env python3
import setuptools
import os
base_dir = os.path.dirname(__file__)
readme_path = os.path.join(base_dir, "README.md")
if os.path.exists(readme_path):
with open(readme_path) as stream:
long_description = stream.read()
else:
long_description = ""
setuptools.setup(
name="complib",
version="0.4.1-1",
author="F-WuTs",
author_email="--",
description="",
summary="Library for the competition",
platforms=["any"],
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/F-WuTS/compLIB",
packages=setuptools.find_packages(),
include_package_data=True,
classifiers=[
"Programming Language :: Python :: 3.7",
"License :: Other/Proprietary License",
"Operating System :: Unix"
],
license="proprietary"#,
#install_requires=[
# "smbus",
# "requests",
# "python-logstash-async"
#]
)

61
client_s1/test.py Normal file
View file

@ -0,0 +1,61 @@
import unittest
import os
os.environ["API_URL"] = "http://127.0.0.1:5000/"
from compLib.Api import Seeding
from compLib.Api import DoubleElim
class TestApiServer(unittest.TestCase):
def test_all(self):
ret, code = Seeding.get_garbage()
print(ret)
print(code)
ret, code = Seeding.get_delivery()
print(ret)
print(code)
ret, code = Seeding.get_material()
print(ret)
print(code)
ret, code = Seeding.list_cargo()
print(ret)
print(code)
ret, code = Seeding.get_cargo("green")
print(ret)
print(code)
ret, code = Seeding.get_cargo("yellow")
print(ret)
print(code)
ret, code = Seeding.get_cargo("blue")
print(ret)
print(code)
ret, code = Seeding.get_cargo("red")
print(ret)
print(code)
class TestApiServerDoubleElim(unittest.TestCase):
def test_all(self):
ret, code = DoubleElim.get_goal()
print(ret)
print(code)
ret, code = DoubleElim.get_items()
print(ret)
print(code)
ret, code = DoubleElim.get_scores()
print(ret)
print(code)
ret, code = DoubleElim.get_opponent()
print(ret)
print(code)
ret, code = DoubleElim.get_position()
print(ret)
print(code)
ret, code = DoubleElim.get_meteoroids()
print(ret)
print(code)
if __name__ == '__main__':
unittest.main()

3
client_s2/.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

8
client_s2/.idea/client_s2.iml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="list.__getitem__" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
client_s2/.idea/misc.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>

8
client_s2/.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/client_s2.iml" filepath="$PROJECT_DIR$/.idea/client_s2.iml" />
</modules>
</component>
</project>

15
client_s2/.idea/saveactions_settings.xml generated Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SaveActionSettings">
<option name="actions">
<set>
<option value="activate" />
<option value="activateOnShortcut" />
<option value="activateOnBatch" />
<option value="organizeImports" />
<option value="reformat" />
<option value="rearrange" />
</set>
</option>
</component>
</project>

6
client_s2/.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

1
client_s2/MANIFEST.in Normal file
View file

@ -0,0 +1 @@
include readme.md

View file

@ -6,48 +6,53 @@ export PYTHONDONTWRITEBYTECODE=1
export EXTRACT_PKG="0"
if [[ -z $VERSION ]]; then
echo "Warning, setting VERSION env var to default value 0.0.1-0"
export VERSION="0.0.1-0"
echo "Warning, setting VERSION env var to default value v1.0.0-1"
export VERSION="v1.0.0-0"
fi
echo "Building Package version: $VERSION"
# BE CAREFUL TO NOT BUILD IN A PYTHON VENV!
# BE CAREFUL TO NOT BUILD IN A PYTHON VENV! wasd
# be sure to change version if needed!
fpm -s python --python-bin python3 --python-package-name-prefix python3 \
fpm -s python --python-bin python3 --python-pip pip3 --python-package-name-prefix python3 \
-m '"Joel Klimont" <joel.klimont@comp-air.at>' \
--license 'proprietary' \
--description 'Library for robot used in the competition' \
--after-install postinstall.sh \
--after-upgrade postinstall.sh \
--deb-priority "optional" \
--architecture "aarch64" \
--deb-systemd "complib.service" \
-d "python3-pip" \
-d "nginx" \
-d "libnginx-mod-rtmp" \
-d "libatlas-base-dev" \
-d "python3-numpy" \
-d "libsystemd-dev" \
-d "python3-systemd" \
-d "gstreamer1.0-tools" \
-d "gstreamer1.0-plugins-bad" \
-d "gstreamer1.0-plugins-base" \
-d "gstreamer1.0-plugins-good" \
-d "gstreamer1.0-omx-rpi" \
-d "gstreamer1.0-omx-rpi-config" \
-d "opencv-dev" \
-d "opencv-libs" \
-d "opencv-licenses" \
-d "opencv-main" \
-d "opencv-python" \
-d "opencv-scripts" \
-d "libprotobuf23" \
-d "protobuf-compiler" \
-d "python3-protobuf" \
--python-install-lib "/usr/local/lib/python3.9/dist-packages" \
-d "libatlas-base-dev" \
-d "python3-numpy" \
-d "ffmpeg" \
-v $VERSION -t deb setup.py
if [ "$EXTRACT_PKG" == "1" ]; then
echo "Extracting deb package"
echo "Exracting deb package"
mkdir build_extract
mv python3-complib_"$VERSION"_arm64.deb build_extract
mv python3-complib_"$VERSION"_all.deb build_extract
cd build_extract
ar -xv python3-complib_"$VERSION"_arm64.deb
ar -xv python3-complib_"$VERSION"_all.deb
fi
export DEB=python3-complib_"$VERSION"_arm64.deb
export DEB=python3-complib_"$VERSION"_all.deb
echo "Created: $DEB"
# --deb-changelog changelog \
@ -58,4 +63,4 @@ echo "Created: $DEB"
# sudo apt purge python3-complib -y
# sudo apt install ./python3-*
# sudo apt search complib
# ar vx ./python3*
# ar vx ./python3*

View file

@ -5,7 +5,7 @@ from typing import Dict, Tuple, List
import requests
logger = logging.getLogger("complib-logger")
logger = logging.getLogger("seeding-api")
API_URL = os.getenv("API_URL", "http://localhost:5000/") + "api/"
CONF_URL = os.getenv("API_URL", "http://localhost:5000/") + "config/"
@ -23,14 +23,14 @@ API_URL_GET_ROBOT_STATE = API_URL + "getRobotState"
class Seeding:
"""Klasse welche mit der Seeding API Kommuniziert.
"""Class used for communicating with seeding api
"""
@staticmethod
def get_heuballen() -> int:
"""Macht den /api/getHeuballen request zur Seeding API.
"""Makes the /api/getHeuballen call to the api.
:return: hueballencode als int.
:return: hueballencode as int.
:rtype: int
"""
res = requests.get(API_URL_GET_HEU)
@ -40,9 +40,9 @@ class Seeding:
@staticmethod
def get_logistic_plan() -> List:
"""Macht den /api/getLogisticPlan zur Seeding API.
"""Makes the /api/getLogisticPlan call to the api.
:return: Liste an logistic-centern, welche vom roboter in genau der Reihenfolge beliefert werden sollten.
:return: Json Object and status code as returned by the api.
:rtype: List
"""
res = requests.get(API_URL_GET_LOGISTIC_PLAN)
@ -52,7 +52,7 @@ class Seeding:
@staticmethod
def get_material_deliveries() -> List:
"""Macht den /api/getMaterialDeliveries zur Seeding API.
"""Makes the /api/getMaterialDeliveries call to the api.
:return: Json Object and status code as returned by the api.
:rtype: List

View file

@ -58,39 +58,18 @@ message IRSensorsReadAllResponse {
repeated uint32 data = 3 [packed = true];
}
message MotorSetPowerRequest {
message MotorsSetPowerRequest {
Header header = 1;
uint32 port = 2;
double power = 3;
}
message MotorsSetPowerRequest {
Header header = 1;
repeated MotorSetPowerRequest requests = 2;
}
message MotorSetSpeedRequest {
message MotorsSetSpeedRequest {
Header header = 1;
uint32 port = 2;
double speed = 3;
}
message MotorsSetSpeedRequest {
Header header = 1;
repeated MotorSetSpeedRequest requests = 2;
}
message MotorSetPulseWidthRequest {
Header header = 1;
uint32 port = 2;
double percent = 3;
}
message MotorsSetPulseWidthRequest {
Header header = 1;
repeated MotorSetPulseWidthRequest requests = 2;
}
message OdometryReadRequest {
Header header = 1;
}
@ -119,8 +98,4 @@ message DriveRequest {
Header header = 1;
double linear_velocity_m_s = 2;
double angular_velocity_rad_s = 3;
}
message HealthUpdateRequest {
Header header = 1;
}

View file

@ -0,0 +1,45 @@
import socket
import compLib.CompLib_pb2 as CompLib_pb2
UNIX_SOCKET_PATH = "/tmp/compLib"
TCP_SOCKET_HOST = "192.168.0.151"
TCP_SOCKET_PORT = 9090
class CompLibClient(object):
@staticmethod
def send(data: bytes, size: int) -> bytes:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((TCP_SOCKET_HOST, TCP_SOCKET_PORT))
# with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
# sock.connect(UNIX_SOCKET_PATH)
sock.sendall(size.to_bytes(1, byteorder='big'))
sock.sendall(data)
response_size_bytes = sock.recv(1)
response_size = int.from_bytes(response_size_bytes, byteorder="big")
# print(response_size)
response_bytes = sock.recv(response_size)
# print(response_bytes.hex())
# print(len(response_bytes))
CompLibClient.check_response(response_bytes)
return response_bytes
@staticmethod
def check_response(response_bytes: bytes) -> bool:
print(f"{response_bytes}")
res = CompLib_pb2.GenericResponse()
res.ParseFromString(response_bytes)
if res.status.successful:
return True
# TODO: Log error message if unsuccessful
return False

View file

@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: CompLib.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
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_pool.Default().AddSerializedFile(b'\n\rCompLib.proto\x12\x07\x43ompLib\"\x1e\n\x06Header\x12\x14\n\x0cmessage_type\x18\x01 \x01(\t\"3\n\x06Status\x12\x12\n\nsuccessful\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\"1\n\x0eGenericRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\"S\n\x0fGenericResponse\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\x12\x1f\n\x06status\x18\x02 \x01(\x0b\x32\x0f.CompLib.Status\">\n\x1b\x45ncoderReadPositionsRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\"w\n\x1c\x45ncoderReadPositionsResponse\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\x12\x1f\n\x06status\x18\x02 \x01(\x0b\x32\x0f.CompLib.Status\x12\x15\n\tpositions\x18\x03 \x03(\x05\x42\x02\x10\x01\"?\n\x1c\x45ncoderReadVelocitiesRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\"y\n\x1d\x45ncoderReadVelocitiesResponse\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\x12\x1f\n\x06status\x18\x02 \x01(\x0b\x32\x0f.CompLib.Status\x12\x16\n\nvelocities\x18\x03 \x03(\x01\x42\x02\x10\x01\"9\n\x16IRSensorsEnableRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\":\n\x17IRSensorsDisableRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\":\n\x17IRSensorsReadAllRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\"n\n\x18IRSensorsReadAllResponse\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\x12\x1f\n\x06status\x18\x02 \x01(\x0b\x32\x0f.CompLib.Status\x12\x10\n\x04\x64\x61ta\x18\x03 \x03(\rB\x02\x10\x01\"U\n\x15MotorsSetPowerRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\x12\x0c\n\x04port\x18\x02 \x01(\r\x12\r\n\x05power\x18\x03 \x01(\x01\"U\n\x15MotorsSetSpeedRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\x12\x0c\n\x04port\x18\x02 \x01(\r\x12\r\n\x05speed\x18\x03 \x01(\x01\"6\n\x13OdometryReadRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\"\x95\x01\n\x14OdometryReadResponse\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\x12\x1f\n\x06status\x18\x02 \x01(\x0b\x32\x0f.CompLib.Status\x12\x12\n\nx_position\x18\x03 \x01(\x01\x12\x12\n\ny_position\x18\x04 \x01(\x01\x12\x13\n\x0borientation\x18\x05 \x01(\x01\"a\n\x14\x44riveDistanceRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\x12\x12\n\ndistance_m\x18\x02 \x01(\x01\x12\x14\n\x0cvelocity_m_s\x18\x03 \x01(\x01\"d\n\x12TurnDegreesRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\x12\x15\n\rangle_degrees\x18\x02 \x01(\x01\x12\x16\n\x0evelocity_rad_s\x18\x03 \x01(\x01\"l\n\x0c\x44riveRequest\x12\x1f\n\x06header\x18\x01 \x01(\x0b\x32\x0f.CompLib.Header\x12\x1b\n\x13linear_velocity_m_s\x18\x02 \x01(\x01\x12\x1e\n\x16\x61ngular_velocity_rad_s\x18\x03 \x01(\x01\x62\x06proto3')
_HEADER = DESCRIPTOR.message_types_by_name['Header']
_STATUS = DESCRIPTOR.message_types_by_name['Status']
_GENERICREQUEST = DESCRIPTOR.message_types_by_name['GenericRequest']
_GENERICRESPONSE = DESCRIPTOR.message_types_by_name['GenericResponse']
_ENCODERREADPOSITIONSREQUEST = DESCRIPTOR.message_types_by_name['EncoderReadPositionsRequest']
_ENCODERREADPOSITIONSRESPONSE = DESCRIPTOR.message_types_by_name['EncoderReadPositionsResponse']
_ENCODERREADVELOCITIESREQUEST = DESCRIPTOR.message_types_by_name['EncoderReadVelocitiesRequest']
_ENCODERREADVELOCITIESRESPONSE = DESCRIPTOR.message_types_by_name['EncoderReadVelocitiesResponse']
_IRSENSORSENABLEREQUEST = DESCRIPTOR.message_types_by_name['IRSensorsEnableRequest']
_IRSENSORSDISABLEREQUEST = DESCRIPTOR.message_types_by_name['IRSensorsDisableRequest']
_IRSENSORSREADALLREQUEST = DESCRIPTOR.message_types_by_name['IRSensorsReadAllRequest']
_IRSENSORSREADALLRESPONSE = DESCRIPTOR.message_types_by_name['IRSensorsReadAllResponse']
_MOTORSSETPOWERREQUEST = DESCRIPTOR.message_types_by_name['MotorsSetPowerRequest']
_MOTORSSETSPEEDREQUEST = DESCRIPTOR.message_types_by_name['MotorsSetSpeedRequest']
_ODOMETRYREADREQUEST = DESCRIPTOR.message_types_by_name['OdometryReadRequest']
_ODOMETRYREADRESPONSE = DESCRIPTOR.message_types_by_name['OdometryReadResponse']
_DRIVEDISTANCEREQUEST = DESCRIPTOR.message_types_by_name['DriveDistanceRequest']
_TURNDEGREESREQUEST = DESCRIPTOR.message_types_by_name['TurnDegreesRequest']
_DRIVEREQUEST = DESCRIPTOR.message_types_by_name['DriveRequest']
Header = _reflection.GeneratedProtocolMessageType('Header', (_message.Message,), {
'DESCRIPTOR' : _HEADER,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.Header)
})
_sym_db.RegisterMessage(Header)
Status = _reflection.GeneratedProtocolMessageType('Status', (_message.Message,), {
'DESCRIPTOR' : _STATUS,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.Status)
})
_sym_db.RegisterMessage(Status)
GenericRequest = _reflection.GeneratedProtocolMessageType('GenericRequest', (_message.Message,), {
'DESCRIPTOR' : _GENERICREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.GenericRequest)
})
_sym_db.RegisterMessage(GenericRequest)
GenericResponse = _reflection.GeneratedProtocolMessageType('GenericResponse', (_message.Message,), {
'DESCRIPTOR' : _GENERICRESPONSE,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.GenericResponse)
})
_sym_db.RegisterMessage(GenericResponse)
EncoderReadPositionsRequest = _reflection.GeneratedProtocolMessageType('EncoderReadPositionsRequest', (_message.Message,), {
'DESCRIPTOR' : _ENCODERREADPOSITIONSREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.EncoderReadPositionsRequest)
})
_sym_db.RegisterMessage(EncoderReadPositionsRequest)
EncoderReadPositionsResponse = _reflection.GeneratedProtocolMessageType('EncoderReadPositionsResponse', (_message.Message,), {
'DESCRIPTOR' : _ENCODERREADPOSITIONSRESPONSE,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.EncoderReadPositionsResponse)
})
_sym_db.RegisterMessage(EncoderReadPositionsResponse)
EncoderReadVelocitiesRequest = _reflection.GeneratedProtocolMessageType('EncoderReadVelocitiesRequest', (_message.Message,), {
'DESCRIPTOR' : _ENCODERREADVELOCITIESREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.EncoderReadVelocitiesRequest)
})
_sym_db.RegisterMessage(EncoderReadVelocitiesRequest)
EncoderReadVelocitiesResponse = _reflection.GeneratedProtocolMessageType('EncoderReadVelocitiesResponse', (_message.Message,), {
'DESCRIPTOR' : _ENCODERREADVELOCITIESRESPONSE,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.EncoderReadVelocitiesResponse)
})
_sym_db.RegisterMessage(EncoderReadVelocitiesResponse)
IRSensorsEnableRequest = _reflection.GeneratedProtocolMessageType('IRSensorsEnableRequest', (_message.Message,), {
'DESCRIPTOR' : _IRSENSORSENABLEREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.IRSensorsEnableRequest)
})
_sym_db.RegisterMessage(IRSensorsEnableRequest)
IRSensorsDisableRequest = _reflection.GeneratedProtocolMessageType('IRSensorsDisableRequest', (_message.Message,), {
'DESCRIPTOR' : _IRSENSORSDISABLEREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.IRSensorsDisableRequest)
})
_sym_db.RegisterMessage(IRSensorsDisableRequest)
IRSensorsReadAllRequest = _reflection.GeneratedProtocolMessageType('IRSensorsReadAllRequest', (_message.Message,), {
'DESCRIPTOR' : _IRSENSORSREADALLREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.IRSensorsReadAllRequest)
})
_sym_db.RegisterMessage(IRSensorsReadAllRequest)
IRSensorsReadAllResponse = _reflection.GeneratedProtocolMessageType('IRSensorsReadAllResponse', (_message.Message,), {
'DESCRIPTOR' : _IRSENSORSREADALLRESPONSE,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.IRSensorsReadAllResponse)
})
_sym_db.RegisterMessage(IRSensorsReadAllResponse)
MotorsSetPowerRequest = _reflection.GeneratedProtocolMessageType('MotorsSetPowerRequest', (_message.Message,), {
'DESCRIPTOR' : _MOTORSSETPOWERREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.MotorsSetPowerRequest)
})
_sym_db.RegisterMessage(MotorsSetPowerRequest)
MotorsSetSpeedRequest = _reflection.GeneratedProtocolMessageType('MotorsSetSpeedRequest', (_message.Message,), {
'DESCRIPTOR' : _MOTORSSETSPEEDREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.MotorsSetSpeedRequest)
})
_sym_db.RegisterMessage(MotorsSetSpeedRequest)
OdometryReadRequest = _reflection.GeneratedProtocolMessageType('OdometryReadRequest', (_message.Message,), {
'DESCRIPTOR' : _ODOMETRYREADREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.OdometryReadRequest)
})
_sym_db.RegisterMessage(OdometryReadRequest)
OdometryReadResponse = _reflection.GeneratedProtocolMessageType('OdometryReadResponse', (_message.Message,), {
'DESCRIPTOR' : _ODOMETRYREADRESPONSE,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.OdometryReadResponse)
})
_sym_db.RegisterMessage(OdometryReadResponse)
DriveDistanceRequest = _reflection.GeneratedProtocolMessageType('DriveDistanceRequest', (_message.Message,), {
'DESCRIPTOR' : _DRIVEDISTANCEREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.DriveDistanceRequest)
})
_sym_db.RegisterMessage(DriveDistanceRequest)
TurnDegreesRequest = _reflection.GeneratedProtocolMessageType('TurnDegreesRequest', (_message.Message,), {
'DESCRIPTOR' : _TURNDEGREESREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.TurnDegreesRequest)
})
_sym_db.RegisterMessage(TurnDegreesRequest)
DriveRequest = _reflection.GeneratedProtocolMessageType('DriveRequest', (_message.Message,), {
'DESCRIPTOR' : _DRIVEREQUEST,
'__module__' : 'CompLib_pb2'
# @@protoc_insertion_point(class_scope:CompLib.DriveRequest)
})
_sym_db.RegisterMessage(DriveRequest)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_ENCODERREADPOSITIONSRESPONSE.fields_by_name['positions']._options = None
_ENCODERREADPOSITIONSRESPONSE.fields_by_name['positions']._serialized_options = b'\020\001'
_ENCODERREADVELOCITIESRESPONSE.fields_by_name['velocities']._options = None
_ENCODERREADVELOCITIESRESPONSE.fields_by_name['velocities']._serialized_options = b'\020\001'
_IRSENSORSREADALLRESPONSE.fields_by_name['data']._options = None
_IRSENSORSREADALLRESPONSE.fields_by_name['data']._serialized_options = b'\020\001'
_HEADER._serialized_start=26
_HEADER._serialized_end=56
_STATUS._serialized_start=58
_STATUS._serialized_end=109
_GENERICREQUEST._serialized_start=111
_GENERICREQUEST._serialized_end=160
_GENERICRESPONSE._serialized_start=162
_GENERICRESPONSE._serialized_end=245
_ENCODERREADPOSITIONSREQUEST._serialized_start=247
_ENCODERREADPOSITIONSREQUEST._serialized_end=309
_ENCODERREADPOSITIONSRESPONSE._serialized_start=311
_ENCODERREADPOSITIONSRESPONSE._serialized_end=430
_ENCODERREADVELOCITIESREQUEST._serialized_start=432
_ENCODERREADVELOCITIESREQUEST._serialized_end=495
_ENCODERREADVELOCITIESRESPONSE._serialized_start=497
_ENCODERREADVELOCITIESRESPONSE._serialized_end=618
_IRSENSORSENABLEREQUEST._serialized_start=620
_IRSENSORSENABLEREQUEST._serialized_end=677
_IRSENSORSDISABLEREQUEST._serialized_start=679
_IRSENSORSDISABLEREQUEST._serialized_end=737
_IRSENSORSREADALLREQUEST._serialized_start=739
_IRSENSORSREADALLREQUEST._serialized_end=797
_IRSENSORSREADALLRESPONSE._serialized_start=799
_IRSENSORSREADALLRESPONSE._serialized_end=909
_MOTORSSETPOWERREQUEST._serialized_start=911
_MOTORSSETPOWERREQUEST._serialized_end=996
_MOTORSSETSPEEDREQUEST._serialized_start=998
_MOTORSSETSPEEDREQUEST._serialized_end=1083
_ODOMETRYREADREQUEST._serialized_start=1085
_ODOMETRYREADREQUEST._serialized_end=1139
_ODOMETRYREADRESPONSE._serialized_start=1142
_ODOMETRYREADRESPONSE._serialized_end=1291
_DRIVEDISTANCEREQUEST._serialized_start=1293
_DRIVEDISTANCEREQUEST._serialized_end=1390
_TURNDEGREESREQUEST._serialized_start=1392
_TURNDEGREESREQUEST._serialized_end=1492
_DRIVEREQUEST._serialized_start=1494
_DRIVEREQUEST._serialized_end=1602
# @@protoc_insertion_point(module_scope)

View file

@ -3,10 +3,16 @@ import os
import time
from typing import Tuple, List, Dict
import requests
import requests as requests
import logging
logger = logging.getLogger("complib-logger")
logger = logging.getLogger("seeding-api")
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
ch.setFormatter(formatter)
logger.addHandler(ch)
RETRY_TIMEOUT = 0.05
@ -16,7 +22,7 @@ API_URL = os.getenv("API_URL", "http://localhost:5000/") + "api/"
api_override = os.getenv("API_FORCE", "")
if api_override != "":
logger.warning(f"API_URL was set to {API_URL} but was overwritten with {api_override}")
print(f"API_URL was set to {API_URL} but was overwritten with {api_override}")
API_URL = api_override
API_URL_GET_ROBOT_STATE = API_URL + "getRobotState"
@ -29,12 +35,7 @@ API_URL_GET_SCORES = API_URL + "getScores"
class Position:
"""
Datenstruktur, welche eine Position representiert.
:ivar x: X Position in Centimeter
:ivar y: Y Position in Centimeter
:ivar degrees: Rotation in Grad von -180 bis 180
"""Datastructure for holding a position
"""
def __init__(self, x, y, degrees):
@ -62,14 +63,13 @@ class Position:
class DoubleElim:
"""Klasse für die Kommunikation mit Double Elimination Api
"""Class used for communicating with double elimination api
"""
@staticmethod
def get_pos() -> Tuple[Position, int]:
"""Führt den /api/getPos Aufruf an die API aus.
:return: Ein Objekt der Klasse :class:`.Position` mit der Position des Roboters und der Status Code
"""Makes the /api/getPos call to the api.
:return: A Position object with robot position
:rtype: Tuple[Position, int]
"""
res = requests.get(API_URL_GET_POS)
@ -86,9 +86,8 @@ class DoubleElim:
@staticmethod
def get_opponent() -> Tuple[Position, int]:
"""Führt den /api/getOp Aufruf an die API aus.
:return: Ein Objekt der Klasse :class:`.Position` mit der Position des gegnerischen Roboters relativ zum eigenen Roboter und der Status Code
"""Makes the /api/getOp call to the api.
:return: A Position object with opponents robot position
:rtype: Tuple[Position, int]
"""
res = requests.get(API_URL_GET_OP)
@ -105,9 +104,8 @@ class DoubleElim:
@staticmethod
def get_goal() -> Tuple[Position, int]:
"""Führt den /api/getGoal Aufruf an die API aus.
:return: Ein Objekt der Klasse :class:`.Position` mit der Position des Ziels relativ zum eigenen Roboter und der Status Code
"""Makes the /api/getGoal call to the api.
:return: A Position object with x and y coordinates of the goal, rotation is always -1
:rtype: Tuple[Position, int]
"""
res = requests.get(API_URL_GET_GOAL)
@ -124,9 +122,8 @@ class DoubleElim:
@staticmethod
def get_items() -> Tuple[List[Dict], int]:
"""Führt den /api/getItems Aufruf an die API aus.
:return: Eine Liste aller Items, die sich derzeit auf dem Spielfeld befinden. Items sind "dictionaries", die wie folgt aussehen: {"id": 0, "x": 0, "y": 0}
"""Makes the /api/getItems call to the api.
:return: A list will all items currently on the game field. Items are dictionaries that look like: {"id": 0, "x": 0, "y": 0}
:rtype: Tuple[List[Dict], int]
"""
res = requests.get(API_URL_GET_ITEMS)
@ -143,9 +140,8 @@ class DoubleElim:
@staticmethod
def get_scores() -> Tuple[Dict, int]:
"""Führt den /api/getScores Aufruf an die API aus.
:return: Ein "dictionary" mit dem eignen Score und dem des Gegners: {"self":2,"opponent":0}
"""Makes the /api/getScores call to the api.
:return: A dictionary with all scores included like: {"self":2,"opponent":0}
:rtype: Tuple[Dict, int]
"""
res = requests.get(API_URL_GET_SCORES)

View file

@ -3,35 +3,33 @@ from compLib.CompLibClient import CompLibClient
class Encoder(object):
"""Klasse zum Zugriff auf die Encoder der einzelnen Motoren
"""Class used to read the encoders
"""
@staticmethod
def read_all_positions():
"""Lesen aller absoluten Positionen der einzelnen Encoder
"""Read all encoder positions.
:return: Tupel mit allen aktuellen Encoderpositionen
:return: Tuple of all current encoder positions
"""
request = CompLib_pb2.EncoderReadPositionsRequest()
request.header.message_type = request.DESCRIPTOR.full_name
response = CompLib_pb2.EncoderReadPositionsResponse()
response.ParseFromString(CompLibClient.send(
request.SerializeToString(), request.ByteSize()))
response.ParseFromString(CompLibClient.send(request.SerializeToString(), request.ByteSize()))
return tuple(i for i in response.positions)
@staticmethod
def read_all_velocities():
"""Lesen der Geschwindigkeit aller angeschlossenen Motoren.
"""Read the velocity of all motors connected.
:return: Tupel aller aktuellen Motorgeschwindigkeiten in Radianten pro Sekunde
:return: Tuple of all current motor velocities
"""
request = CompLib_pb2.EncoderReadVelocitiesRequest()
request.header.message_type = request.DESCRIPTOR.full_name
response = CompLib_pb2.EncoderReadVelocitiesResponse()
response.ParseFromString(CompLibClient.send(
request.SerializeToString(), request.ByteSize()))
response.ParseFromString(CompLibClient.send(request.SerializeToString(), request.ByteSize()))
return tuple(i for i in response.velocities)

View file

@ -1,45 +1,39 @@
import time
import compLib.CompLib_pb2 as CompLib_pb2
from compLib.CompLibClient import CompLibClient
class IRSensor(object):
"""Ermöglicht den Zugriff auf die einzelnen IRSensoren des Roboters
"""Access the different IR Sensors of the robot
"""
@staticmethod
def read_all():
"""Auslesen aller Sensoren gleichzeitig
"""Read all IR sensors at once.
:return: Array aller Sensorwerte
:return: Tuple of all current ir sensors
"""
request = CompLib_pb2.IRSensorsReadAllRequest()
request.header.message_type = request.DESCRIPTOR.full_name
response = CompLib_pb2.IRSensorsReadAllResponse()
response.ParseFromString(CompLibClient.send(
request.SerializeToString(), request.ByteSize()))
response.ParseFromString(CompLibClient.send(request.SerializeToString(), request.ByteSize()))
return [i for i in response.data]
return tuple(i for i in response.data)
@staticmethod
def enable():
"""Aktivieren Infrarot-Sender. Muss bei jedem Programmstart ausgeführt werden.
"""Turn on all IR emitters
"""
request = CompLib_pb2.IRSensorsEnableRequest()
request.header.message_type = request.DESCRIPTOR.full_name
CompLibClient.send(request.SerializeToString(), request.ByteSize())
time.sleep(0.1) # IR sensor reading is async -> Wait a bit
@staticmethod
def disable():
"""Deaktivieren der Infrarot-Sender
"""Turn off all IR emitters
"""
request = CompLib_pb2.IRSensorsDisableRequest()
request.header.message_type = request.DESCRIPTOR.full_name
CompLibClient.send(request.SerializeToString(), request.ByteSize())
time.sleep(0.1) # IR sensor reading is async -> Wait a bit

View file

@ -0,0 +1,69 @@
import compLib.CompLib_pb2 as CompLib_pb2
from compLib.CompLibClient import CompLibClient
MOTOR_COUNT = 4
class Motor(object):
"""Class used to control the motors
"""
@staticmethod
def power(port: int, percent: float):
"""Set specified motor to percentage power
:param port: Port, which the motor is connected to. 0-3
:param percent: Percentage of max speed. between -100 and 100
:raises: IndexError
"""
if port < 0 or port >= MOTOR_COUNT:
raise IndexError("Invalid Motor port specified!")
if percent < -100 or percent > 100:
raise IndexError("Invalid Motor speed specified! Speed is between -100 and 100 percent!")
request = CompLib_pb2.MotorsSetPowerRequest()
request.header.message_type = request.DESCRIPTOR.full_name
request.port = port
request.power = percent
CompLibClient.send(request.SerializeToString(), request.ByteSize())
@staticmethod
def speed(port: int, speed: float):
"""Set specified motor to percentage power
:param port: Port, which the motor is connected to. 0-3
:param speed: Speed at which a motor should turn in RPM
:raises: IndexError
"""
if port < 0 or port >= MOTOR_COUNT:
raise IndexError("Invalid Motor port specified!")
request = CompLib_pb2.MotorsSetSpeedRequest()
request.header.message_type = request.DESCRIPTOR.full_name
request.port = port
request.speed = speed
CompLibClient.send(request.SerializeToString(), request.ByteSize())
# @staticmethod
# def all_off():
# """
# Turns of all motors
# """
# Logging.get_logger().debug(f"Motor.all_off")
#
# for i in range(1, MOTOR_COUNT + 1):
# Motor.active_break(i)
# @staticmethod
# def active_break(port: int):
# """
# Actively break with a specific motor
#
# :param port: Port, which the motor is connected to. 1-4
# """
# Motor.pwm(port, 0, MotorMode.BREAK)

View file

@ -1,24 +1,17 @@
import logging
import os
import numpy as np
# TODO: if set to competition mode, get the seed from the api
FORCE_SEED = int(os.getenv("FORCE_SEED", "-1"))
logger = logging.getLogger("complib-logger")
# TODO: if set to competition mode, get the seed from the api
def set_random_seed(seed: int):
np.random.seed(seed)
def get_random_number(min: int, max: int):
return np.random.randint(256 ** 4, dtype='<u4', size=1)[0] % (max - min + 1) + min
class Gamestate:
@staticmethod
def __set_random_seed(seed: int):
logger.debug(f"Seeding seed to: {seed}")
np.random.seed(seed)
@staticmethod
def __get_random_number(min: int, max: int):
return np.random.randint(256 ** 4, dtype='<u4', size=1)[0] % (max - min + 1) + min
def __str__(self) -> str:
return f"""Seed: {self.seed}
Heu Color: {self.heu_color}
@ -28,30 +21,18 @@ Logistic Plan: {self.logistic_plan}
Logistic Centers: {self.logistic_center}"""
def __init__(self, seed: int):
"""
Erstellt den Seeding "Gamestate" für den angegebenen Seed.
self.seed = seed
set_random_seed(seed)
:param seed: Seed welcher zum Erstellen des Gamestates benutzt werden soll.
"""
if FORCE_SEED == -1:
self.seed = seed
else:
print(f"Wettkampfmodus, zufälliger Seed wird verwendet: Seed={FORCE_SEED}")
self.seed = FORCE_SEED
logger.debug(f"Creating gamestate with seed: {self.seed}")
self.__set_random_seed(self.seed)
self.heu_color = self.__get_random_number(1, 2)
self.heu_color = get_random_number(1, 2)
self.materials = [0, 0, 0, 0]
self.material_pairs = []
for i in range(0, 4):
num1 = self.__get_random_number(0, 3)
num1 = get_random_number(0, 3)
self.material_pairs.append([num1, num1])
while self.material_pairs[i][1] == num1:
self.material_pairs[i][1] = self.__get_random_number(0, 3)
self.material_pairs[i][1] = get_random_number(0, 3)
flat = [item for sublist in self.material_pairs for item in sublist]
for i in range(0, 4):
@ -62,7 +43,7 @@ Logistic Centers: {self.logistic_center}"""
visited = [5, 5, 5, 5]
def __logistic_plan_generator(i: int):
drive_to = self.__get_random_number(0, 3)
drive_to = get_random_number(0, 3)
for j in range(0, 4):
drive_to = (drive_to + j) % 4
if visited[drive_to] <= 0 or drive_to == self.logistic_plan[i - 1]:
@ -97,28 +78,12 @@ Logistic Centers: {self.logistic_center}"""
self.logistic_center[self.logistic_plan[i]][self.logistic_plan[i + 1]] += 1
self.logistic_plan = [x + 10 for x in self.logistic_plan]
logger.debug(f"Created gamesate: {str(self)}")
def get_heuballen(self) -> int:
"""
Die Funktion gibt entweder die Zahl "1" oder "2" zurück. Wenn die Funktion "1" zurückgibt, dann liegen die Heuballen auf den gelben Linien. Wenn die Funktion "2" zurückgibt, dann liegen sie auf den blauen Flächen.
:return: Gibt entweder die Zahl 1 oder 2 zurück.
"""
return self.heu_color
def get_logistic_plan(self) -> []:
"""
Die Funktion gibt den "Logistik Plan" zurück. Also die Reihenfolge, in welcher der Roboter die Logistik Zonen Abfahren muss, um die Pakete welche dort liegen zu sortieren.
:return: Eine Liste an Zahlen zwischen 10 und 13.
"""
return self.logistic_plan
def get_material_deliveries(self) -> [[]]:
"""
Die Funktion gibt die einzelnen "Material Lieferungen" zurück. Da der Roboter immer zwei Paare an Materialien anliefern muss, gibt die Funktion eine Liste an Material Paaren zurück. Die Materialien werden dabei durch ihre Zonen-ID representiert. Also Holz ist z.B. "0" und die Ziegelsteine sind "3".
:return: Eine Liste and Material Paaren.
"""
return self.material_pairs

View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

11
client_s2/complib.service Normal file
View file

@ -0,0 +1,11 @@
[Unit]
Description=Monitoring service
[Service]
ExecStart=/usr/bin/python3 /usr/local/lib/python3.7/dist-packages/compLib/IPService.py
Environment="debug=False"
Restart=always
RestartSec=5
Type=notify
[Install]
Alias=complib
WantedBy=default.target

90
client_s2/main.py Normal file
View file

@ -0,0 +1,90 @@
SOCKET_PATH = "/tmp/compLib"
# def send(data, size):
# with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
# sock.connect(SOCKET_PATH)
# sock.sendall(size.to_bytes(1, byteorder='big'))
# sock.sendall(data)
#
# response_size_bytes = sock.recv(1)
# response_size = int.from_bytes(response_size_bytes, byteorder="big")
# # print(f"Response size: {response_size}")
#
# response_bytes = sock.recv(response_size)
# generic_response = CompLib_pb2.GenericResponse()
#
# generic_response.ParseFromString(response_bytes)
# # print(f"Response: {generic_response}")
#
# # reponseBytes =
def main():
# encoder_read_positions_request = CompLib_pb2.EncoderReadPositionsRequest()
# # readSensorsRequest.header = CompLib_pb2.Header()
# encoder_read_positions_request.header.message_type = encoder_read_positions_request.DESCRIPTOR.full_name
#
# start_time = time.time()
# for i in range(100000):
# send(encoder_read_positions_request.SerializeToString(), encoder_read_positions_request.ByteSize())
# print("--- %s seconds ---" % (time.time() - start_time))
# from compLib.IRSensor import IRSensor
# IRSensor.read_all()
#
# from compLib.Encoder import Encoder
# print(Encoder.read_all_positions())
# print(Encoder.read_all_velocities())
# from compLib.Motor import Motor
# Motor.speed(0, 50)
# Motor.speed(3, -50)
#
# import time
# time.sleep(2)
#
# Motor.speed(0, 0)
# Motor.speed(3, -0)
# Motor.power(0, 0)
# Motor.power(3, 0)
# import math
# from compLib.Movement import Movement
# Movement.turn_degrees(90, math.pi * 2)
# Movement.turn_degrees(-90, math.pi * 2)
#
# Movement.turn_degrees(90, math.pi * 2)
# Movement.turn_degrees(90, -math.pi * 2)
#
# Movement.turn_degrees(90, math.pi * 2)
# Movement.turn_degrees(-90, -math.pi * 2)
# from compLib.Movement import Movement
# Movement.drive_distance(0.1, 0.5)
# Movement.drive_distance(-0.1, 0.5)
#
# Movement.drive_distance(0.1, 0.5)
# Movement.drive_distance(0.1, -0.5)
#
# Movement.drive_distance(0.1, 0.5)
# Movement.drive_distance(-0.1, -0.5)
from compLib.Movement import Movement
import math
Movement.drive_distance(0.5, 0.5)
Movement.turn_degrees(90, math.pi * 2)
Movement.drive_distance(0.5, 0.5)
Movement.turn_degrees(90, math.pi * 2)
Movement.drive_distance(0.5, 0.5)
Movement.turn_degrees(90, math.pi * 2)
Movement.drive_distance(0.5, 0.5)
Movement.turn_degrees(90, math.pi * 2)
if __name__ == '__main__':
main()

41
postinstall.sh → client_s2/postinstall.sh Executable file → Normal file
View file

@ -1,22 +1,35 @@
#!/usr/bin/env bash
if [ "$(uname -m)" = "x86_64" ]; then
echo "Not running on RPi - Skipping postinstall"
exit 0
fi
grep -qxF "apt update" /etc/rc.local
if [ $? -ne 0 ]; then
echo "adding apt update to rc.local"
sed -i "2s/^/apt update\n/" /etc/rc.local
fi
pip3 install requests flask
install_package() {
echo "Installing package '$1' via pip3"
pip3 install "$1"
if [ $? -eq 0 ]; then
echo "Successfully installed pip3 package '$1'"
else
echo "Could not install pip3 package '$1'"
exit 1
fi
}
#echo "Setting up nginx rtmp server"
#sudo /etc/init.d/nginx start
#install_package "smbus"
#install_package "requests"
#install_package "flask"
#install_package "python-logstash-async"
#install_package "RPi.GPIO"
#install_package "spidev"
#install_package "influxdb_client"
sudo raspi-config nonint do_legacy 0 || echo "(WARNING) raspi-config not found, cannot enable legacy camera support"
pip3 install smbus requests flask python-logstash-async RPi.GPIO spidev influxdb_client filelock
echo "Setting up opencv4"
pkg-config --modversion opencv4
echo "Setting up nginx rtmp server"
sudo /etc/init.d/nginx start
{
echo 'load_module "modules/ngx_rtmp_module.so";'
@ -35,11 +48,11 @@ sudo raspi-config nonint do_legacy 0 || echo "(WARNING) raspi-config not found,
echo '}'
} >| /etc/nginx/nginx.conf
echo "Stopping nginx rtmp server as its not required anymore"
sudo /etc/init.d/nginx stop
base64 -d << UPD
CiBfX19fX18gICAgIF9fX19fXyAgICAgX18gICAgX18gICAgIF9fX19fXyAgIF9fICAgICAgICAgX18gICAgIF9fX19fXyAgICAgICAgICAgICAgICAgIAovXCAgX19fXCAgIC9cICBfXyBcICAgL1wgIi0uLyAgXCAgIC9cICA9PSBcIC9cIFwgICAgICAgL1wgXCAgIC9cICA9PSBcICAgICAgICAgICAgICAgICAKXCBcIFxfX19fICBcIFwgXC9cIFwgIFwgXCBcLS4vXCBcICBcIFwgIF8tLyBcIFwgXF9fX18gIFwgXCBcICBcIFwgIF9fPCAgICAgICAgICAgICAgICAgCiBcIFxfX19fX1wgIFwgXF9fX19fXCAgXCBcX1wgXCBcX1wgIFwgXF9cICAgIFwgXF9fX19fXCAgXCBcX1wgIFwgXF9fX19fXCAgICAgICAgICAgICAgIAogIFwvX19fX18vICAgXC9fX19fXy8gICBcL18vICBcL18vICAgXC9fLyAgICAgXC9fX19fXy8gICBcL18vICAgXC9fX19fXy8gICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiBfXyAgX18gICAgIF9fX19fXyAgICAgIF9fX19fXyAgIF9fX19fXyAgICAgICAgX19fX18gICAgIF9fX19fXyAgICAgX19fX19fICAgX19fX19fICAgIAovXCBcL1wgXCAgIC9cICA9PSBcICAgIC9cX18gIF9cIC9cICBfXyBcICAgICAgL1wgIF9fLS4gIC9cICBfXyBcICAgL1xfXyAgX1wgL1wgIF9fX1wgICAKXCBcIFxfXCBcICBcIFwgIF8tLyAgICBcL18vXCBcLyBcIFwgXC9cIFwgICAgIFwgXCBcL1wgXCBcIFwgIF9fIFwgIFwvXy9cIFwvIFwgXCAgX19cICAgCiBcIFxfX19fX1wgIFwgXF9cICAgICAgICAgXCBcX1wgIFwgXF9fX19fXCAgICAgXCBcX19fXy0gIFwgXF9cIFxfXCAgICBcIFxfXCAgXCBcX19fX19cIAogIFwvX19fX18vICAgXC9fLyAgICAgICAgICBcL18vICAgXC9fX19fXy8gICAgICBcL19fX18vICAgXC9fL1wvXy8gICAgIFwvXy8gICBcL19fX19fLyA=
UPD
echo ""
echo ""
service complib restart
systemctl enable complib

View file

@ -6,7 +6,7 @@ print("Using version: {str(os.environ['VERSION'])}")
setuptools.setup(
name="complib",
version=str(os.environ.get('VERSION', "")),
version=str(os.environ["VERSION"]),
author="F-WuTs",
author_email="joel.klimont@comp-air.at",
description="",

View file

@ -61,60 +61,6 @@ class SeedingApiTest(unittest.TestCase):
self.assertEqual(seeding_api.get_logistic_plan(), gamestate.get_logistic_plan())
self.assertEqual(seeding_api.get_material_deliveries(), gamestate.get_material_deliveries())
def test_gamestate(self):
seed = 42
gamestate = Seeding.Gamestate(seed)
print(gamestate)
print(gamestate.get_heuballen())
heu_color = gamestate.get_heuballen()
if heu_color == 1:
print("Heuballen liegen auf den gelben Linien")
# TODO: code um die über die gelben Linien zu fahren
elif heu_color == 2:
print("Heuballen liegen auf den blauen Linien")
# TODO: code um die über die blauen Linien zu fahren
materials = gamestate.get_material_deliveries()
print(materials)
for material_pair in materials:
print(f"Der Roboter sollte jetzt die beiden Materialien {material_pair} holen")
for material in material_pair:
if material == 0:
print(f"Der Roboter sollte jetzt Holz aufnehmen, Zone: {material}")
# TODO: code um in die Material Zone mit dem Holz zu fahren
elif material == 1:
print(f"Der Roboter sollte jetzt Stahl aufnehmen, Zone: {material}")
# TODO: code um in die Material Zone mit dem Holz zu fahren
elif material == 2:
print(f"Der Roboter sollte jetzt Beton aufnehmen, Zone: {material}")
# TODO: code um in die Material Zone mit dem Holz zu fahren
elif material == 3:
print(f"Der Roboter sollte jetzt Ziegelsteine aufnehmen, Zone: {material}")
# TODO: code um in die Material Zone mit dem Holz zu fahren
print("Der Roboter sollte jetzt die beiden Materialien zur Baustelle fahren")
# TODO: code um zur Baustelle zu fahren
logistic_plan = gamestate.get_logistic_plan()
print(logistic_plan)
for zone in logistic_plan:
if zone == 10:
print(f"Roboter sollte jetzt zur grünen Zone fahren: {zone}")
# TODO: code um in die grüne Zone zu fahren
elif zone == 11:
print(f"Roboter sollte jetzt zur roten Zone fahren: {zone}")
# TODO: code um in die rote Zone zu fahren
elif zone == 12:
print(f"Roboter sollte jetzt zur blauen Zone fahren: {zone}")
# TODO: code um in die blaue Zone zu fahren
elif zone == 13:
print(f"Roboter sollte jetzt zur gelben Zone fahren: {zone}")
# TODO: code um in die gelbe Zone zu fahren
class DeApiTest(unittest.TestCase):
def test_api_de(self):
@ -146,5 +92,6 @@ class DeApiTest(unittest.TestCase):
self.assertTrue(util_reset_state())
if __name__ == '__main__':
unittest.main()

View file

@ -1,55 +0,0 @@
cmake_minimum_required(VERSION 3.26)
project(comp_lib)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(std_msgs REQUIRED)
find_package(irobot_create_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)
# add_executable(talker src/publisher_member_function.cpp)
# ament_target_dependencies(talker rclcpp std_msgs irobot_create_msgs)
# add_executable(listener src/subscriber_member_function.cpp)
# ament_target_dependencies(listener rclcpp std_msgs irobot_create_msgs)
# add_executable(drive src/drive_action.cpp)
# ament_target_dependencies(drive rclcpp rclcpp_action std_msgs irobot_create_msgs)
# add_executable(set_vel src/set_speed.cpp)
# ament_target_dependencies(set_vel rclcpp rclcpp_action std_msgs irobot_create_msgs geometry_msgs)
add_executable(compLib_node src/main.cpp src/motor.cpp src/controls.cpp)
ament_target_dependencies(compLib_node rclcpp rclcpp_action std_msgs irobot_create_msgs geometry_msgs)
target_include_directories(compLib_node PRIVATE include)
install(TARGETS
#talker
#listener
#drive
#set_vel
compLib_node
DESTINATION lib/${PROJECT_NAME})
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# uncomment the line when a copyright and license is not present in all source files
#set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# uncomment the line when this package is not in a git repo
#set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
endif()
ament_package()

View file

@ -1,231 +0,0 @@
import sys
from typing import Any, Tuple, List
# build image is somehow different from raspberry image? opencv-python is installed to a directory which is not in the pythonpath by default....
sys.path.append("/usr/lib/python3.9/site-packages")
import logging
import os
import queue
import threading
import cv2
from flask import Flask, Response
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
SERVE_VIDEO = os.getenv("SERVER_SRC", "/live")
BUILDING_DOCS = os.getenv("BUILDING_DOCS", "false")
HTML = """
<html>
<head>
<title>Opencv Output</title>
</head>
<body>
<h1>Opencv Output</h1>
<img src="{{ VIDEO_DST }}">
</body>
</html>
"""
# it would be better to use jinja2 here, but I don't want to blow up the package dependencies...
HTML = HTML.replace("{{ VIDEO_DST }}", SERVE_VIDEO)
class Marker:
def __init__(self, id: int, x: float, y: float):
self.id: int = id
self.x: float = x
self.y: float = y
def __str__(self) -> str:
return f"Marker ID: {self.id}, position: {self.x} x, {self.y} y"
def __repr__(self) -> str:
return str({"id": self.id, "x": self.x, "y": self.y})
class Camera:
class __Webserver:
def __init__(self, camera):
self.app = Flask(__name__)
self.__camera = camera
self.__thread = threading.Thread(target=self.__start_flask, daemon=True)
self.__thread.start()
@self.app.route("/live")
def __video_feed():
"""
Define route for serving jpeg stream.
:return: Return the response generated along with the specific media.
"""
return Response(self.__camera._newest_frame_generator(),
mimetype="multipart/x-mixed-replace; boundary=frame")
@self.app.route("/")
def __index():
"""
Define route for serving a static http site to view the stream.
:return: Static html page where the video stream of Opencv can be viewed.
"""
return HTML
def __start_flask(self):
"""
Function for running flask server in a thread.
:return:
"""
logging.getLogger("complib-logger").info("starting flask server")
self.app.run(host="0.0.0.0", port=9898, debug=True, threaded=True, use_reloader=False)
class __NoBufferVideoCapture:
def __init__(self, cam):
self.cap = cv2.VideoCapture(cam)
self.cap.set(3, 640)
self.cap.set(4, 480)
self.q = queue.Queue(maxsize=3)
self.stopped = False
self.t = threading.Thread(target=self._reader, daemon=True)
self.t.start()
def _reader(self):
while not self.stopped:
ret, frame = self.cap.read()
if not ret:
continue
if self.q.full():
try:
self.q.get_nowait()
except queue.Empty:
pass
self.q.put(frame)
def read(self):
return self.q.get()
def stop(self):
self.stopped = True
self.t.join()
def __init__(self):
self.__logger = logging.getLogger("complib-logger")
self.__logger.info("capturing rtmp stream is disabled in this version")
self.__camera_stream = self.__NoBufferVideoCapture(-1)
self.__newest_frame = None
self.__lock = threading.Lock()
self.__webserver = self.__Webserver(self)
self.aruco_dict = cv2.aruco.Dictionary_get(cv2.aruco.DICT_6X6_50)
self.aruco_params = cv2.aruco.DetectorParameters_create()
self.__logger.info("Initialized vision")
def get_frame(self) -> Any:
"""
Die Funktion gibt das neuste Bild, welches die Kamera aufgenommen, hat zurück.
:return: Ein "opencv image frame"
"""
img16 = self.__camera_stream.read()
return img16
def detect_markers(self, image) -> Any:
"""
Funktion um die ArUco Marker in einem Bild zu erkennen.
:param image: Bild, welches die Kamera aufgenommen hat.
:return: Gibt drei Variablen zurueck. Erstens eine Liste an Postionen der "Ecken" der erkannten Markern. Zweitens eine Liste an IDs der erkannten Markern und dritten noch Debug Informationen (diese können ignoriert werden).
"""
return cv2.aruco.detectMarkers(image, self.aruco_dict, parameters=self.aruco_params)
def detect_markers_midpoint(self, image) -> Tuple[List[Marker], Any]:
"""
Funktion um die ArUco Marker in einem Bild zu erkennen, einzuzeichnen und den Mittelpunkt der Marker auszurechnen.
:param image: Bild, welches die Kamera aufgenommen hat.
:return: Gibt zwei Variablen zurueck. Erstens eine Liste an "Markern" und zweitens das Bild mit den eigezeichneten Marken.
:rtype: Tuple[List[Marker], Any]
"""
(corners, ids, rejected) = self.detect_markers(image)
self.draw_markers(image, corners, ids)
res = []
for i in range(0, len(corners)):
x = sum([point[0] for point in corners[i][0]]) / 4
y = sum([point[1] for point in corners[i][0]]) / 4
res.append(Marker(ids[i][0], x, y))
return res, image
def draw_markers(self, image, corners, ids) -> Any:
"""
Zeichnet die erkannten Markern mit ihren IDs in das Bild.
:param image: Original Bild, in dem die Marker erkannt wurden.
:param corners: List der Positionen der Ecken der erkannten Marker.
:param ids: IDs der erkannten Markern.
:return: Neues Bild mit den eigezeichneten Markern.
"""
return cv2.aruco.drawDetectedMarkers(image, corners, ids)
def publish_frame(self, image):
"""
Sendet das Bild, welches der Funktion übergeben wird, an den Webserver, damit es der Nutzer in seinem Browser ansehen kann.
:param image: Opencv Bild, welches dem Nutzer angezeigt werden soll.
:return: None
"""
with self.__lock:
if image is not None:
self.__newest_frame = image.copy()
def _newest_frame_generator(self):
"""
Private generator which is called directly from flask server.
:return: Yields image/jpeg encoded frames published from publish_frame function.
"""
while True:
# use a buffer frame to copy the newest frame with lock and then freeing it immediately
buffer_frame = None
with self.__lock:
if self.__newest_frame is None:
continue
buffer_frame = self.__newest_frame.copy()
# encode frame for jpeg stream
(flag, encoded_image) = cv2.imencode(".jpg", buffer_frame)
# if there was an error try again with the next frame
if not flag:
continue
# else yield encoded frame with mimetype image/jpeg
yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' +
bytearray(encoded_image) + b'\r\n')
# for debugging and testing start processing frames and detecting a 6 by 9 calibration chessboard
if __name__ == '__main__' and BUILDING_DOCS == "false":
camera = Camera()
while True:
image = camera.get_frame()
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# processing
# gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# find the chessboard corners
# ret, corners = cv2.findChessboardCorners(gray, (6, 9), None)
# cv2.drawChessboardCorners(frame, (6, 9), corners, ret)
markers, image = camera.detect_markers_midpoint(image)
print(markers)
print("-----------------")
camera.publish_frame(image)

View file

@ -1,70 +0,0 @@
import socket
from threading import Lock
import compLib.CompLib_pb2 as CompLib_pb2
class CompLibClient(object):
UNIX_SOCKET_PATH = "/tmp/compLib"
TCP_SOCKET_HOST = ""
TCP_SOCKET_PORT = 9090
SOCKET = None
LOCK = Lock()
@staticmethod
def use_unix_socket(socket_path="/tmp/compLib"):
CompLibClient.UNIX_SOCKET_PATH = socket_path
CompLibClient.SOCKET = socket.socket(
socket.AF_UNIX, socket.SOCK_STREAM)
CompLibClient.SOCKET.connect(CompLibClient.UNIX_SOCKET_PATH)
from compLib.HealthCheck import HealthUpdater
HealthUpdater.start()
@staticmethod
def use_tcp_socket(socket_host, socket_port=TCP_SOCKET_PORT):
CompLibClient.TCP_SOCKET_HOST = socket_host
CompLibClient.TCP_SOCKET_PORT = socket_port
CompLibClient.SOCKET = socket.socket(
socket.AF_INET, socket.SOCK_STREAM)
CompLibClient.SOCKET.connect(
(CompLibClient.TCP_SOCKET_HOST, CompLibClient.TCP_SOCKET_PORT))
from compLib.HealthCheck import HealthUpdater
HealthUpdater.start()
@staticmethod
def send(data: bytes, size: int) -> bytes:
with CompLibClient.LOCK:
if CompLibClient.SOCKET is None:
CompLibClient.use_unix_socket()
CompLibClient.SOCKET.sendall(size.to_bytes(1, byteorder='big'))
CompLibClient.SOCKET.sendall(data)
response_size_bytes = CompLibClient.SOCKET.recv(1)
response_size = int.from_bytes(
response_size_bytes, byteorder="big")
# print(response_size)
response_bytes = CompLibClient.SOCKET.recv(response_size)
# print(response_bytes.hex())
# print(len(response_bytes))
CompLibClient.check_response(response_bytes)
return response_bytes
@staticmethod
def check_response(response_bytes: bytes) -> bool:
# print(f"{response_bytes}")
res = CompLib_pb2.GenericResponse()
res.ParseFromString(response_bytes)
if res.status.successful:
return True
# TODO: Log error message if unsuccessful
return False

File diff suppressed because it is too large Load diff

View file

@ -1,23 +0,0 @@
import threading
import time
import compLib.CompLib_pb2 as CompLib_pb2
from compLib.CompLibClient import CompLibClient
class HealthUpdater(object):
started = False
@staticmethod
def start():
if not HealthUpdater.started:
threading.Thread(target=HealthUpdater.loop, daemon=True).start()
HealthUpdater.started = True
@staticmethod
def loop():
while True:
request = CompLib_pb2.HealthUpdateRequest()
request.header.message_type = request.DESCRIPTOR.full_name
CompLibClient.send(request.SerializeToString(), request.ByteSize())
time.sleep(0.25)

View file

@ -1,142 +0,0 @@
import compLib.CompLib_pb2 as CompLib_pb2
from compLib.CompLibClient import CompLibClient
MOTOR_COUNT = 4
class Motor(object):
"""Klasse zum Ansteuern der Motoren
"""
@staticmethod
def power(port: int, percent: float):
"""Motor auf eine prozentuale Leistung der Höchstgeschwindigkeit einstellen
:param port: Port, an welchen der Motor angesteckt ist. 0-3
:param percent: Prozentsatz der Höchstgeschwindigkeit. zwischen -100 und 100
:raises: IndexError
"""
if port < 0 or port >= MOTOR_COUNT:
raise IndexError("Invalid Motor port specified!")
if percent < -100 or percent > 100:
raise IndexError(
"Invalid Motor speed specified! Speed is between -100 and 100 percent!")
request = CompLib_pb2.MotorSetPowerRequest()
request.header.message_type = request.DESCRIPTOR.full_name
request.port = port
request.power = percent
CompLibClient.send(request.SerializeToString(), request.ByteSize())
@staticmethod
def multiple_power(*arguments: tuple[int, float]):
"""Mehrere Motoren auf eine prozentuale Leistung der Höchstgeschwindigkeit einstellen
:param arguments: tuple von port, percentage
:raises: IndexError
"""
request = CompLib_pb2.MotorsSetPowerRequest()
request.header.message_type = request.DESCRIPTOR.full_name
for port, percent in arguments:
if port < 0 or port >= MOTOR_COUNT:
raise IndexError("Invalid Motor port specified!")
if percent < -100 or percent > 100:
raise IndexError(
"Invalid Motor speed specified! Speed is between -100 and 100 percent!")
inner_request = CompLib_pb2.MotorSetPowerRequest()
inner_request.port = port
inner_request.power = percent
request.requests.append(inner_request)
CompLibClient.send(request.SerializeToString(), request.ByteSize())
@staticmethod
def speed(port: int, speed: float):
"""Geschwindigkeit des Motors einstellen
:param port: Port, an welchen der Motor angesteckt ist. 0-3
:param speed: Drehzahl, mit der sich ein Motor dreht, in Centimeter pro Sekunde (cm/s)
:raises: IndexError
"""
if port < 0 or port >= MOTOR_COUNT:
raise IndexError("Invalid Motor port specified!")
request = CompLib_pb2.MotorSetSpeedRequest()
request.header.message_type = request.DESCRIPTOR.full_name
request.port = port
request.speed = speed
CompLibClient.send(request.SerializeToString(), request.ByteSize())
@staticmethod
def multiple_speed(*arguments: tuple[int, float]):
"""Geschwindigkeit mehrerer Motoren einstellen
:param arguments: tuple von port, Geschwindigkeit in Radianten pro Sekunde (rad/s)
:raises: IndexError
"""
request = CompLib_pb2.MotorsSetSpeedRequest()
request.header.message_type = request.DESCRIPTOR.full_name
for port, speed in arguments:
if port < 0 or port >= MOTOR_COUNT:
raise IndexError("Invalid Motor port specified!")
inner_request = CompLib_pb2.MotorSetSpeedRequest()
inner_request.port = port
inner_request.speed = speed
request.requests.append(inner_request)
CompLibClient.send(request.SerializeToString(), request.ByteSize())
@staticmethod
def pulse_width(port: int, percent: float):
"""Setzen den Pulsbreite eines Motors in Prozent der Periode
:param port: Port, an welchen der Motor angesteckt ist. 0-3
:param percent: Prozent der Periode zwischen -100 und 100
:raises: IndexError
"""
if port < 0 or port >= MOTOR_COUNT:
raise IndexError("Invalid Motor port specified!")
request = CompLib_pb2.MotorSetPulseWidthRequest()
request.header.message_type = request.DESCRIPTOR.full_name
request.port = port
request.percent = percent
CompLibClient.send(request.SerializeToString(), request.ByteSize())
@staticmethod
def multiple_pulse_width(*arguments: tuple[int, float]):
"""Setzen den Pulsbreite mehrerer Motoren in Prozent der Periode
:param arguments: tuple von port, prozent
:raises: IndexError
"""
request = CompLib_pb2.MotorsSetPulseWidthRequest()
request.header.message_type = request.DESCRIPTOR.full_name
for port, percent in arguments:
if port < 0 or port >= MOTOR_COUNT:
raise IndexError("Invalid Motor port specified!")
inner_request = CompLib_pb2.MotorSetPulseWidthRequest()
inner_request.port = port
inner_request.percent = percent
request.requests.append(inner_request)
CompLibClient.send(request.SerializeToString(), request.ByteSize())

View file

@ -1,7 +0,0 @@
import logging
import os
if os.getenv("DEBUG", "0") != "0":
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG)
else:
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)

View file

@ -1,14 +0,0 @@
#ifndef ROS_NODE_H
#define ROS_NODE_H
#include "rclcpp/rclcpp.hpp"
class CompLibNode : public rclcpp::Node
{
public:
CompLibNode();
};
#endif

View file

@ -1,24 +0,0 @@
#ifndef CONTROLS_H
#define CONTROLS_H
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "irobot_create_msgs/msg/interface_buttons.hpp"
#include "irobot_create_msgs/msg/lightring_leds.hpp"
class ButtonPressNode : public rclcpp::Node
{
public:
ButtonPressNode();
void bt1_wait();
void bt2_wait();
void kill();
private:
void result_callback(const irobot_create_msgs::msg::InterfaceButtons::SharedPtr result);
rclcpp::Subscription<irobot_create_msgs::msg::InterfaceButtons>::SharedPtr interface_buttons_subscriber_;
bool button1{false};
bool button2{false};
};
#endif

View file

@ -1,67 +0,0 @@
#ifndef MOTOR_H
#define MOTOR_H
#include <thread>
#include <memory>
#include <geometry_msgs/msg/twist.hpp>
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "irobot_create_msgs/action/drive_distance.hpp"
#include "irobot_create_msgs/action/drive_arc.hpp"
#include "irobot_create_msgs/action/rotate_angle.hpp"
class DriveDistNode : public rclcpp::Node
{
public:
DriveDistNode();
void drive_dist(float meters, float velocity);
void kill();
private:
void result_callback(const rclcpp_action::ClientGoalHandle<irobot_create_msgs::action::DriveDistance>::WrappedResult & result);
rclcpp_action::Client<irobot_create_msgs::action::DriveDistance>::SharedPtr drive_dist_action_;
bool processing;
};
class SetSpeedNode : public rclcpp::Node
{
public:
SetSpeedNode();
void drive(float speed);
void stop();
void kill();
private:
void set_speed(float speed);
void drive_loop(float speed);
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr speed_publisher_;
bool run = true;
std::thread t;
};
class RotateAngleNode : public rclcpp::Node
{
public:
RotateAngleNode();
void rotate_angle(float angle, float velocity);
void kill();
private:
void result_callback(const rclcpp_action::ClientGoalHandle<irobot_create_msgs::action::RotateAngle>::WrappedResult & result);
rclcpp_action::Client<irobot_create_msgs::action::RotateAngle>::SharedPtr rotate_angle_action_;
bool processing;
};
class DriveArcNode : public rclcpp::Node
{
public:
DriveArcNode();
void drive_arc(float angle, float radius, float velocity, int direction=1);
void kill();
private:
void result_callback(const rclcpp_action::ClientGoalHandle<irobot_create_msgs::action::DriveArc>::WrappedResult & result);
rclcpp_action::Client<irobot_create_msgs::action::DriveArc>::SharedPtr drive_arc_action_;
bool processing;
};
#endif

View file

@ -1,11 +0,0 @@
#ifndef SEQUENCE_LOCK_H
#define SEQUENCE_LOCK_H
#include <mutex>
namespace SequenceLock
{
std::mutex m;
}
#endif

View file

@ -1,22 +0,0 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>comp_lib</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="matthias@todo.todo">matthias</maintainer>
<license>TODO: License declaration</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>rclcpp_action</depend>
<depend>std_msgs</depend>
<depend>irobot_create_msgs</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>

View file

@ -1,10 +0,0 @@
#include "ros_node.h"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
CompLibNode::CompLibNode()
: Node("CompLibNode")
{
}

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