Compare commits
194 commits
compair-s0
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
36d4379ddc | ||
|
c593059fb1 | ||
|
ed04957d94 | ||
|
27962d16a7 | ||
|
9c40daae88 | ||
|
de112d98e4 | ||
|
fa4cc1a0ad | ||
|
7e2977a653 | ||
|
cf415d38e8 | ||
|
f09d20ade2 | ||
|
671292a6cf | ||
|
b95b9bf518 | ||
|
dcef53712f | ||
|
c02cfcd71c | ||
|
4c24717278 | ||
|
19d98f1f04 | ||
|
13eefdceb1 | ||
|
f03df1b3b3 | ||
|
ee4f6a516d | ||
|
9e589fd681 | ||
|
be9a5c9f19 | ||
|
765336231b | ||
|
f2724e79f7 | ||
|
a5274c2232 | ||
|
67947116ec | ||
|
369692b619 | ||
|
de9b671f29 | ||
|
b87e830ac3 | ||
|
ff2c529d3a | ||
|
e4592e7963 | ||
|
285996f467 | ||
|
bd0f14a83b | ||
|
1ee4a1e1ef | ||
|
002db9d650 | ||
|
f6e45ac25d | ||
|
af3aaf7998 | ||
|
e1a17808f7 | ||
|
6245d1308a | ||
|
d7a62cfbf5 | ||
|
a84183375b | ||
|
60cfb2669c | ||
|
b6c351dd0b | ||
|
d4a7d8c0c0 | ||
|
c16a7172e7 | ||
|
afe5e9c766 | ||
|
c62b22aa0a | ||
|
807ea8e561 | ||
|
8151173d2a | ||
|
16c8f18c06 | ||
|
03daabaa37 | ||
|
b2281e2a31 | ||
|
8d19e876ab | ||
|
41e88c9fd5 | ||
|
587a7503a2 | ||
|
7a9c4e9f3a | ||
|
077937f5c6 | ||
|
1117f6efa7 | ||
|
1d91792c56 | ||
|
d1e385a2a1 | ||
|
222d274ea7 | ||
|
2dc55fe8e2 | ||
|
cc79faf13f | ||
|
5b2b7e4ea4 | ||
|
8ad2d3da8e | ||
|
e35b992865 | ||
|
f6b27971c4 | ||
|
c325a2c7c2 | ||
|
6946ce9957 | ||
|
f03b9d51b5 | ||
|
52c22995d3 | ||
|
72c5af8b56 | ||
|
d4c49a0a51 | ||
|
2c5f283a46 | ||
80ed5e5bf9 | |||
b3eb01ad6e | |||
4a9367629f | |||
|
7fe160211f | ||
|
37447298a4 | ||
|
4ee0b8045f | ||
|
9000316350 | ||
|
4be754c1db | ||
|
92d42be8b8 | ||
|
6a1ac72912 | ||
|
a484bc2137 | ||
|
1a033c8b03 | ||
|
0bef6035ae | ||
|
e9ae1a320a | ||
|
4d5c26d10c | ||
|
1fc400c695 | ||
|
5d8df998ae | ||
|
1a488065ac | ||
|
8537e8504b | ||
|
5ecf92426a | ||
|
ddc550ef1a | ||
|
50da718472 | ||
|
9b567b8c6c | ||
|
e277a83c5f | ||
|
a6af166fd1 | ||
|
5d6a4acdf7 | ||
|
98fc45e97a | ||
|
e1e03933d2 | ||
|
6687d1afba | ||
|
0e1b767f26 | ||
|
01cb854f50 | ||
|
e7f0f95d66 | ||
|
34e68e2a9e | ||
|
2c0f05dd77 | ||
|
4b96a1a4a7 | ||
|
a7de603298 | ||
|
67d047f521 | ||
|
2d63fcc58f | ||
|
df4c672701 | ||
|
a5b0933eff | ||
|
ed9a22b1c7 | ||
|
543be6cdab | ||
|
0d15ea1654 | ||
|
6ba6ab3ae1 | ||
|
2748290e30 | ||
|
d22c91cc4c | ||
|
fc64706b65 | ||
|
2b3f6f86c5 | ||
|
e7c82dbc47 | ||
|
d4d69daabd | ||
|
62f0b96d72 | ||
|
bbcbfa210f | ||
|
866fac9848 | ||
|
c800b30e31 | ||
|
c70ac8fd03 | ||
|
9b3c8e5b7f | ||
|
9e89123e92 | ||
|
d3a26a9539 | ||
|
0db6ed23b2 | ||
|
7f76813df1 | ||
|
c5dd14fb1f | ||
|
b2d025002e | ||
|
210580d3f1 | ||
|
4c4da544c2 | ||
|
107ba9c667 | ||
|
592311d3ee | ||
|
f7a2a8a261 | ||
|
222aa46123 | ||
|
97b08497d8 | ||
|
42952eb9e1 | ||
|
d81eb73b46 | ||
|
910c5edaee | ||
|
9f09ca3382 | ||
|
3f0651abbc | ||
|
ce7094b5cf | ||
|
74db0ca676 | ||
|
0f95d4207a | ||
|
eafad7ccdb | ||
|
881f8ddbf8 | ||
|
436ef35d46 | ||
|
89d681d10f | ||
|
f5cc142019 | ||
|
7228af5e40 | ||
|
b4eae06eb6 | ||
|
8218e8228e | ||
|
3faee0fb88 | ||
|
d1a86d2c0e | ||
|
3eab71aaf5 | ||
|
d58a41da69 | ||
|
c515c0062b | ||
|
d06e6fa32d | ||
|
023ccb416b | ||
|
bb4483b141 | ||
|
a81fd4204b | ||
|
7f00196680 | ||
|
6eef4e7aa4 | ||
|
31eb91d51b | ||
|
1c0310b9c7 | ||
|
3ef0fff256 | ||
|
54320096fb | ||
|
f5488f7b47 | ||
|
cf4e88fd35 | ||
|
8f2944da71 | ||
|
cbe3a379bd | ||
|
aac99a11ba | ||
|
3fe3139961 | ||
|
60f252c37a | ||
|
e0600d75c4 | ||
|
bdb7d687bb | ||
|
a2425c7010 | ||
|
50709c96e0 | ||
|
7aa415f860 | ||
|
74f0f42d2f | ||
|
ce748d8321 | ||
|
3e7df14da8 | ||
|
9e61023e67 | ||
|
91884bd433 | ||
|
4f564ca759 | ||
|
0f103cb34d | ||
|
63b8f868c9 | ||
|
530ddd8be7 |
60
.github/workflows/complib-package.yml
vendored
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
name: Build comlib.deb package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-complib-deb:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Bump version number
|
||||||
|
uses: paulhatch/semantic-version@v4.0.2
|
||||||
|
id: next_semantic_version
|
||||||
|
with:
|
||||||
|
branch: "master"
|
||||||
|
tag_prefix: ""
|
||||||
|
major_pattern: "(MAJOR)"
|
||||||
|
minor_pattern: "(MINOR)"
|
||||||
|
format: "${major}.${minor}.${patch}-${increment}"
|
||||||
|
bump_each_commit: true
|
||||||
|
- name: Print Version Numbers
|
||||||
|
run: |
|
||||||
|
echo ${{join(steps.next_semantic_version.outputs.*, ' - ')}}
|
||||||
|
echo "Next version: ${{steps.next_semantic_version.outputs.version}}"
|
||||||
|
echo "Next version tag: ${{steps.next_semantic_version.outputs.version_tag}}"
|
||||||
|
- name: Tag Commit
|
||||||
|
uses: actions/github-script@v5
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.git.createRef({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
ref: 'refs/tags/${{steps.next_semantic_version.outputs.version}}',
|
||||||
|
sha: context.sha
|
||||||
|
})
|
||||||
|
- name: Install fpm
|
||||||
|
run: sudo apt install ruby -y && sudo gem install fpm
|
||||||
|
- name: Build complib deb Package
|
||||||
|
env:
|
||||||
|
VERSION: ${{steps.next_semantic_version.outputs.version}}
|
||||||
|
run: bash ./build.sh # creates packages in "output" directory
|
||||||
|
- name: Pushes to another repository
|
||||||
|
uses: cpina/github-action-push-to-another-repository@main
|
||||||
|
env:
|
||||||
|
API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }}
|
||||||
|
with:
|
||||||
|
source-directory: 'output'
|
||||||
|
target-directory: 'debs/complib/'
|
||||||
|
destination-github-username: 'F-WuTS'
|
||||||
|
destination-repository-name: 'compREP'
|
||||||
|
user-email: joel.klimont@comp-air.at
|
||||||
|
target-branch: master
|
94
.gitignore
vendored
|
@ -1,12 +1,38 @@
|
||||||
.idea
|
# Created by https://www.toptal.com/developers/gitignore/api/macos,python,pycharm
|
||||||
# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,code
|
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,python,pycharm
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,code
|
|
||||||
|
|
||||||
### Code ###
|
### macOS ###
|
||||||
.vscode/*
|
# General
|
||||||
!.vscode/tasks.json
|
.DS_Store
|
||||||
!.vscode/launch.json
|
.AppleDouble
|
||||||
*.code-workspace
|
.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 ###
|
### PyCharm ###
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
@ -19,6 +45,9 @@
|
||||||
.idea/**/dictionaries
|
.idea/**/dictionaries
|
||||||
.idea/**/shelf
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
# Generated files
|
# Generated files
|
||||||
.idea/**/contentModel.xml
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
@ -69,6 +98,9 @@ atlassian-ide-plugin.xml
|
||||||
# Cursive Clojure plugin
|
# Cursive Clojure plugin
|
||||||
.idea/replstate.xml
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
com_crashlytics_export_strings.xml
|
com_crashlytics_export_strings.xml
|
||||||
crashlytics.properties
|
crashlytics.properties
|
||||||
|
@ -111,6 +143,10 @@ fabric.properties
|
||||||
# https://plugins.jetbrains.com/plugin/12206-codestream
|
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||||
.idea/codestream.xml
|
.idea/codestream.xml
|
||||||
|
|
||||||
|
# Azure Toolkit for IntelliJ plugin
|
||||||
|
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
|
||||||
|
.idea/**/azureSettings.xml
|
||||||
|
|
||||||
### Python ###
|
### Python ###
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
@ -128,11 +164,12 @@ dist/
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
wheels/
|
wheels/
|
||||||
pip-wheel-metadata/
|
|
||||||
share/python-wheels/
|
share/python-wheels/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
|
@ -162,7 +199,7 @@ coverage.xml
|
||||||
*.py,cover
|
*.py,cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
pytestdebug.log
|
cover/
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
|
@ -183,9 +220,9 @@ instance/
|
||||||
|
|
||||||
# Sphinx documentation
|
# Sphinx documentation
|
||||||
docs/_build/
|
docs/_build/
|
||||||
doc/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
target/
|
target/
|
||||||
|
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
|
@ -196,7 +233,9 @@ profile_default/
|
||||||
ipython_config.py
|
ipython_config.py
|
||||||
|
|
||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
# 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
|
# pipenv
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
@ -205,7 +244,22 @@ ipython_config.py
|
||||||
# install all needed dependencies.
|
# install all needed dependencies.
|
||||||
#Pipfile.lock
|
#Pipfile.lock
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
# 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__/
|
__pypackages__/
|
||||||
|
|
||||||
# Celery stuff
|
# Celery stuff
|
||||||
|
@ -223,7 +277,6 @@ venv/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
pythonenv*
|
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
|
@ -246,7 +299,14 @@ dmypy.json
|
||||||
# pytype static type analyzer
|
# pytype static type analyzer
|
||||||
.pytype/
|
.pytype/
|
||||||
|
|
||||||
# profiling data
|
# Cython debug symbols
|
||||||
.prof
|
cython_debug/
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/python,pycharm,code
|
# 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
|
||||||
|
|
24
README.md
|
@ -1,14 +1,11 @@
|
||||||
# compLIB
|
# compLIB
|
||||||
|
|
||||||
## TODOs
|
Rewrite for ROS is the live packaged version since 18.07.2023.
|
||||||
|
|
||||||
### LED control is not implemented
|
|
||||||
|
|
||||||
Need dependencies:
|
|
||||||
https://github.com/Freenove/Freenove_RPI_WS281x_Python
|
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
|
TODO: document
|
||||||
|
|
||||||
## Building documentation
|
## Building documentation
|
||||||
```
|
```
|
||||||
pip install sphinx-rtd-theme
|
pip install sphinx-rtd-theme
|
||||||
|
@ -21,13 +18,20 @@ pip install sphinx-rtd-theme
|
||||||
[Inline documentation example](https://pythonhosted.org/an_example_pypi_project/sphinx.html#full-code-example)
|
[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)
|
[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
|
# Stream Video
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo raspivid -t 0 -b 10000000 -w 1920 -h 1080 -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://10.1.1.68/live/stream\"```
|
sudo raspivid -t 0 -b 10000000 -w 1920 -h 1080 -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://10.1.1.68/live/stream\"```
|
||||||
```
|
```
|
||||||
|
|
||||||
# Stream video from generic USB camera
|
## Stream video from generic USB camera
|
||||||
|
|
||||||
with gst, very cpu heavy
|
with gst, very cpu heavy
|
||||||
```
|
```
|
||||||
|
@ -38,3 +42,9 @@ 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
|
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)
|
||||||
|
|
9
build.sh
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
mkdir output
|
||||||
|
|
||||||
|
DEB="empty"
|
||||||
|
|
||||||
|
source build_deb.sh
|
||||||
|
echo "Ran build deb, created: $DEB"
|
||||||
|
mv $DEB ./output
|
62
build_deb.sh
|
@ -1,47 +1,61 @@
|
||||||
##!/usr/bin/zsh
|
#!/usr/bin/bash
|
||||||
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
export PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
# set to 1 for debugging
|
||||||
|
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"
|
||||||
|
fi
|
||||||
|
echo "Building Package version: $VERSION"
|
||||||
|
|
||||||
|
# BE CAREFUL TO NOT BUILD IN A PYTHON VENV!
|
||||||
|
|
||||||
# be sure to change version if needed!
|
# be sure to change version if needed!
|
||||||
fpm -s python --python-bin python3 --python-pip pip3 --python-package-name-prefix python3 \
|
fpm -s python --python-bin python3 --python-package-name-prefix python3 \
|
||||||
-m '"Joel Klimont" <joel.klimont@gmail.com>' \
|
-m '"Joel Klimont" <joel.klimont@comp-air.at>' \
|
||||||
--license 'proprietary' \
|
--license 'proprietary' \
|
||||||
--description 'Library for robot used in the competition' \
|
--description 'Library for robot used in the competition' \
|
||||||
--after-install postinstall.sh \
|
--after-install postinstall.sh \
|
||||||
--after-upgrade postinstall.sh \
|
--after-upgrade postinstall.sh \
|
||||||
--deb-generate-changes \
|
|
||||||
--deb-priority "optional" \
|
--deb-priority "optional" \
|
||||||
--deb-systemd "complib.service" \
|
--architecture "aarch64" \
|
||||||
-d "python3-pip" \
|
-d "python3-pip" \
|
||||||
-d "nginx" \
|
-d "nginx" \
|
||||||
-d "libnginx-mod-rtmp" \
|
-d "libnginx-mod-rtmp" \
|
||||||
-d "libsystemd-dev" \
|
-d "libatlas-base-dev" \
|
||||||
-d "python3-systemd" \
|
-d "python3-numpy" \
|
||||||
-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-dev" \
|
||||||
-d "opencv-libs" \
|
-d "opencv-libs" \
|
||||||
-d "opencv-licenses" \
|
-d "opencv-licenses" \
|
||||||
-d "opencv-main" \
|
-d "opencv-main" \
|
||||||
-d "opencv-python" \
|
-d "opencv-python" \
|
||||||
-d "opencv-scripts" \
|
-d "opencv-scripts" \
|
||||||
-d "libatlas-base-dev" \
|
-d "libprotobuf23" \
|
||||||
-d "pigpio" \
|
-d "protobuf-compiler" \
|
||||||
-d "python-pigpio" \
|
-d "python3-protobuf" \
|
||||||
-d "python3-pigpio" \
|
--python-install-lib "/usr/local/lib/python3.9/dist-packages" \
|
||||||
-d "python3-numpy" \
|
-v $VERSION -t deb setup.py
|
||||||
-d "ffmpeg" \
|
|
||||||
-v 0.1.5-1 -t deb setup.py
|
if [ "$EXTRACT_PKG" == "1" ]; then
|
||||||
|
echo "Extracting deb package"
|
||||||
|
mkdir build_extract
|
||||||
|
mv python3-complib_"$VERSION"_arm64.deb build_extract
|
||||||
|
cd build_extract
|
||||||
|
ar -xv python3-complib_"$VERSION"_arm64.deb
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DEB=python3-complib_"$VERSION"_arm64.deb
|
||||||
|
echo "Created: $DEB"
|
||||||
|
|
||||||
# --deb-changelog changelog \
|
# --deb-changelog changelog \
|
||||||
# --deb-upstream-changelog changelog \
|
# --deb-upstream-changelog changelog \
|
||||||
# --deb-field "Distribution: stable" \
|
# --deb-field "Distribution: stable" \
|
||||||
# --deb-dist "stable" \
|
# --deb-dist "stable" \
|
||||||
|
|
||||||
#sudo apt purge python3-complib -y
|
# sudo apt purge python3-complib -y
|
||||||
#sudo apt install ./python3-*
|
# sudo apt install ./python3-*
|
||||||
#sudo apt search complib
|
# sudo apt search complib
|
||||||
#ar vx ./python3*
|
# ar vx ./python3*
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
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
|
|
312
compLib/.gitignore
vendored
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
# 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
|
|
@ -1,38 +0,0 @@
|
||||||
import smbus
|
|
||||||
|
|
||||||
from compLib.LogstashLogging import Logging
|
|
||||||
|
|
||||||
SINGLE_ENDED = 0x84
|
|
||||||
ADDRESS = 0x48
|
|
||||||
|
|
||||||
bus = smbus.SMBus(1)
|
|
||||||
|
|
||||||
|
|
||||||
class ADC:
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def read(channel) -> float:
|
|
||||||
"""
|
|
||||||
Read from adc channel
|
|
||||||
0 -> Left IR Sensor
|
|
||||||
1 -> Right IR Sensor
|
|
||||||
2 -> Battery voltage / 3
|
|
||||||
:param channel: Channel between 0 and 2
|
|
||||||
:return: voltage
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""Select the Command data from the given provided value above"""
|
|
||||||
command = SINGLE_ENDED | ((((channel << 2) | (channel >> 1)) & 0x07) << 4)
|
|
||||||
bus.write_byte(ADDRESS, command)
|
|
||||||
|
|
||||||
while "Koka is great":
|
|
||||||
value1 = bus.read_byte(ADDRESS)
|
|
||||||
value2 = bus.read_byte(ADDRESS)
|
|
||||||
if value1 == value2:
|
|
||||||
break
|
|
||||||
|
|
||||||
voltage = value1 / 255.0 * 3.3 # calculate the voltage value
|
|
||||||
voltage = round(voltage, 2)
|
|
||||||
|
|
||||||
Logging.get_logger().debug(f"ADC.read {channel} = {voltage}")
|
|
||||||
return voltage
|
|
188
compLib/Api.py
|
@ -1,164 +1,70 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
from typing import Dict, Tuple, List
|
from typing import Dict, Tuple, List
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
|
|
||||||
from compLib.LogstashLogging import Logging
|
logger = logging.getLogger("complib-logger")
|
||||||
|
|
||||||
API_URL = os.getenv("API_URL", "http://localhost:5000/") + "api/"
|
API_URL = os.getenv("API_URL", "http://localhost:5000/") + "api/"
|
||||||
API_URL_GET_POS = API_URL + "getPos"
|
CONF_URL = os.getenv("API_URL", "http://localhost:5000/") + "config/"
|
||||||
API_URL_GET_OP = API_URL + "getOp"
|
|
||||||
API_URL_GET_GOAL = API_URL + "getGoal"
|
api_override = os.getenv("API_FORCE", "")
|
||||||
API_URL_GET_ITEMS = API_URL + "getItems"
|
|
||||||
API_URL_GET_PARK = API_URL + "getPark"
|
if api_override != "":
|
||||||
API_URL_PAY_PARK = API_URL + "payPark"
|
print(f"API_URL was set to {API_URL} but was overwritten with {api_override}")
|
||||||
API_URL_SIMON_SAYS = API_URL + "simonSays"
|
API_URL = api_override
|
||||||
API_URL_GET_SCORES = API_URL + "getScores"
|
|
||||||
|
API_URL_GET_HEU = API_URL + "getHeuballen"
|
||||||
|
API_URL_GET_LOGISTIC_PLAN = API_URL + "getLogisticPlan"
|
||||||
|
API_URL_GET_MATERIAL_DELIVERIES = API_URL + "getMaterialDeliveries"
|
||||||
|
API_URL_GET_ROBOT_STATE = API_URL + "getRobotState"
|
||||||
|
|
||||||
|
|
||||||
class Seeding:
|
class Seeding:
|
||||||
"""Class used for communicating with seeding api
|
"""Klasse welche mit der Seeding API Kommuniziert.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_park() -> Tuple[Dict, int]:
|
def get_heuballen() -> int:
|
||||||
"""Get a parkingsapce from the api.
|
"""Macht den /api/getHeuballen request zur Seeding API.
|
||||||
|
|
||||||
:return: Json Object and status code as returned by the api.
|
:return: hueballencode als int.
|
||||||
:rtype: Tuple[Dict, int]
|
|
||||||
"""
|
|
||||||
res = requests.get(API_URL_GET_PARK)
|
|
||||||
result = json.loads(res.content)
|
|
||||||
Logging.get_logger().debug(f"Seeding.get_park = {result}, status code = {res.status_code}")
|
|
||||||
return result, res.status_code
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def pay_park() -> int:
|
|
||||||
"""Pay for parking.
|
|
||||||
|
|
||||||
:return: Status code as returned by the api.
|
|
||||||
:rtype: int
|
:rtype: int
|
||||||
"""
|
"""
|
||||||
result = requests.get(API_URL_PAY_PARK).status_code
|
res = requests.get(API_URL_GET_HEU)
|
||||||
Logging.get_logger().debug(f"Seeding.pay_park = {result}")
|
result = json.loads(res.content)
|
||||||
|
logger.debug(f"Seeding.get_heuballen = {result}, status code = {res.status_code}")
|
||||||
|
return result["heuballen"]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_logistic_plan() -> List:
|
||||||
|
"""Macht den /api/getLogisticPlan zur Seeding API.
|
||||||
|
|
||||||
|
:return: Liste an logistic-centern, welche vom roboter in genau der Reihenfolge beliefert werden sollten.
|
||||||
|
:rtype: List
|
||||||
|
"""
|
||||||
|
res = requests.get(API_URL_GET_LOGISTIC_PLAN)
|
||||||
|
result = json.loads(res.content)
|
||||||
|
logger.debug(f"Seeding.get_logistic_plan = {result}, status code = {res.status_code}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def simon_says() -> Tuple[Dict, int]:
|
def get_material_deliveries() -> List:
|
||||||
"""Get next simon says zone from the api.
|
"""Macht den /api/getMaterialDeliveries zur Seeding API.
|
||||||
|
|
||||||
:return: Json Object and status code as returned by the api.
|
:return: Json Object and status code as returned by the api.
|
||||||
:rtype: Tuple[Dict, int]
|
:rtype: List
|
||||||
"""
|
"""
|
||||||
res = requests.get(API_URL_SIMON_SAYS)
|
res = requests.get(API_URL_GET_MATERIAL_DELIVERIES)
|
||||||
result = json.loads(res.content)
|
result = json.loads(res.content)
|
||||||
Logging.get_logger().debug(f"Seeding.simon_says = {result}, status code = {res.status_code}")
|
logger.debug(f"Seeding.get_material_deliveries = {result}, status code = {res.status_code}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_robot_state() -> Tuple[Dict, int]:
|
||||||
|
res = requests.get(API_URL_GET_ROBOT_STATE)
|
||||||
|
result = json.loads(res.content)
|
||||||
|
logger.debug(f"Seeding.get_robot_state {result}, status code = {res.status_code}")
|
||||||
return result, 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]:
|
|
||||||
"""Get position of the robot
|
|
||||||
|
|
||||||
: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]:
|
|
||||||
"""Get position of the opponents robot
|
|
||||||
|
|
||||||
: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]:
|
|
||||||
"""Get position of the goal
|
|
||||||
|
|
||||||
: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]:
|
|
||||||
"""Get a list with all current items
|
|
||||||
|
|
||||||
: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()
|
|
||||||
|
|
||||||
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]:
|
|
||||||
"""Get the current scores
|
|
||||||
|
|
||||||
: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()
|
|
||||||
|
|
||||||
response = json.loads(res.content)
|
|
||||||
Logging.get_logger().debug(f"DoubleElim.get_scores = {response}, status code = {res.status_code}")
|
|
||||||
return response, res.status_code
|
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
from compLib.ADC import ADC
|
|
||||||
from compLib.LogstashLogging import Logging
|
|
||||||
|
|
||||||
BATTERY_CHANNEL = 2
|
|
||||||
BATTERY_COUNT = 2
|
|
||||||
BATTERY_MULTIPLIER = 3
|
|
||||||
BATTERY_MIN_VOLTAGE = 3.4
|
|
||||||
BATTERY_MAX_VOLTAGE = 4.2
|
|
||||||
|
|
||||||
adc = ADC()
|
|
||||||
|
|
||||||
|
|
||||||
class Battery(object):
|
|
||||||
"""Used to interact with the battery
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def percent() -> int:
|
|
||||||
"""Get battery percentage
|
|
||||||
|
|
||||||
:return: Percentage between 0 and 100
|
|
||||||
:rtype: int
|
|
||||||
"""
|
|
||||||
voltage = adc.read(BATTERY_CHANNEL) * BATTERY_MULTIPLIER
|
|
||||||
result = int(((voltage / BATTERY_COUNT) - BATTERY_MIN_VOLTAGE) / (BATTERY_MAX_VOLTAGE - BATTERY_MIN_VOLTAGE) * 100)
|
|
||||||
Logging.get_logger().debug(f"Battery.percent = {result}")
|
|
||||||
return result
|
|
|
@ -1,31 +0,0 @@
|
||||||
import atexit
|
|
||||||
import RPi.GPIO as GPIO
|
|
||||||
|
|
||||||
from compLib.LogstashLogging import Logging
|
|
||||||
|
|
||||||
GPIO.setwarnings(False)
|
|
||||||
Buzzer_Pin = 17
|
|
||||||
GPIO.setmode(GPIO.BCM)
|
|
||||||
GPIO.setup(Buzzer_Pin, GPIO.OUT)
|
|
||||||
|
|
||||||
|
|
||||||
class Buzzer:
|
|
||||||
"""Used to interact with the buzzer
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set(on: bool):
|
|
||||||
"""Turn the buzzer on / off
|
|
||||||
|
|
||||||
:param on: True if on, False if off
|
|
||||||
"""
|
|
||||||
Logging.get_logger().debug(f"Buzzer.set {on}")
|
|
||||||
|
|
||||||
GPIO.output(Buzzer_Pin, on)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def exit():
|
|
||||||
Buzzer.set(False)
|
|
||||||
|
|
||||||
|
|
||||||
atexit.register(Buzzer.exit)
|
|
55
compLib/CMakeLists.txt
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
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()
|
231
compLib/Camera.py
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
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)
|
126
compLib/CompLib.proto
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package CompLib;
|
||||||
|
|
||||||
|
message Header {
|
||||||
|
string message_type = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Status {
|
||||||
|
bool successful = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GenericRequest {
|
||||||
|
Header header = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GenericResponse {
|
||||||
|
Header header = 1;
|
||||||
|
Status status = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EncoderReadPositionsRequest {
|
||||||
|
Header header = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EncoderReadPositionsResponse {
|
||||||
|
Header header = 1;
|
||||||
|
Status status = 2;
|
||||||
|
repeated int32 positions = 3 [packed = true];
|
||||||
|
}
|
||||||
|
|
||||||
|
message EncoderReadVelocitiesRequest {
|
||||||
|
Header header = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EncoderReadVelocitiesResponse {
|
||||||
|
Header header = 1;
|
||||||
|
Status status = 2;
|
||||||
|
repeated double velocities = 3 [packed = true];
|
||||||
|
}
|
||||||
|
|
||||||
|
message IRSensorsEnableRequest {
|
||||||
|
Header header = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IRSensorsDisableRequest {
|
||||||
|
Header header = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IRSensorsReadAllRequest {
|
||||||
|
Header header = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IRSensorsReadAllResponse {
|
||||||
|
Header header = 1;
|
||||||
|
Status status = 2;
|
||||||
|
repeated uint32 data = 3 [packed = true];
|
||||||
|
}
|
||||||
|
|
||||||
|
message MotorSetPowerRequest {
|
||||||
|
Header header = 1;
|
||||||
|
uint32 port = 2;
|
||||||
|
double power = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MotorsSetPowerRequest {
|
||||||
|
Header header = 1;
|
||||||
|
repeated MotorSetPowerRequest requests = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MotorSetSpeedRequest {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OdometryReadResponse {
|
||||||
|
Header header = 1;
|
||||||
|
Status status = 2;
|
||||||
|
double x_position = 3;
|
||||||
|
double y_position = 4;
|
||||||
|
double orientation = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DriveDistanceRequest {
|
||||||
|
Header header = 1;
|
||||||
|
double distance_m = 2;
|
||||||
|
double velocity_m_s = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TurnDegreesRequest {
|
||||||
|
Header header = 1;
|
||||||
|
double angle_degrees = 2;
|
||||||
|
double velocity_rad_s = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DriveRequest {
|
||||||
|
Header header = 1;
|
||||||
|
double linear_velocity_m_s = 2;
|
||||||
|
double angular_velocity_rad_s = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HealthUpdateRequest {
|
||||||
|
Header header = 1;
|
||||||
|
}
|
70
compLib/CompLibClient.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
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
|
1212
compLib/CompLib_pb2.py
Normal file
161
compLib/DoubleElimination.py
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from typing import Tuple, List, Dict
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("complib-logger")
|
||||||
|
|
||||||
|
RETRY_TIMEOUT = 0.05
|
||||||
|
|
||||||
|
# TODO: rethink how the api url is read
|
||||||
|
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}")
|
||||||
|
API_URL = api_override
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, degrees):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.degrees = degrees
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "{x=%s, y=%s, degrees=%s}" % (self.x, self.y, self.degrees)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Position(x={round(self.x, 5)}, y={round(self.y, 5)}, degrees={round(self.degrees, 5)})"
|
||||||
|
|
||||||
|
def __eq__(self, o: object) -> bool:
|
||||||
|
if isinstance(o, Position):
|
||||||
|
return self.x == o.x and self.y == o.y and self.degrees == o.degrees
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __ne__(self, o: object) -> bool:
|
||||||
|
return not self.__eq__(o)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def position_from_json(json_str: Dict):
|
||||||
|
return Position(json_str["x"], json_str["y"], json_str["degrees"])
|
||||||
|
|
||||||
|
|
||||||
|
class DoubleElim:
|
||||||
|
"""Klasse für die Kommunikation mit 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
|
||||||
|
:rtype: Tuple[Position, int]
|
||||||
|
"""
|
||||||
|
res = requests.get(API_URL_GET_POS)
|
||||||
|
if res.status_code == 408:
|
||||||
|
logger.error(f"DoubleElim.get_position timeout. API={API_URL_GET_POS}")
|
||||||
|
time.sleep(RETRY_TIMEOUT)
|
||||||
|
return DoubleElim.get_pos()
|
||||||
|
elif res.status_code == 503:
|
||||||
|
return Position(0, 0, -1), 503
|
||||||
|
|
||||||
|
response = json.loads(res.content)
|
||||||
|
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]:
|
||||||
|
"""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
|
||||||
|
:rtype: Tuple[Position, int]
|
||||||
|
"""
|
||||||
|
res = requests.get(API_URL_GET_OP)
|
||||||
|
if res.status_code == 408:
|
||||||
|
logger.error(f"DoubleElim.get_opponent timeout. API={API_URL_GET_OP}")
|
||||||
|
time.sleep(RETRY_TIMEOUT)
|
||||||
|
return DoubleElim.get_opponent()
|
||||||
|
elif res.status_code == 503:
|
||||||
|
return Position(0, 0, -1), 503
|
||||||
|
|
||||||
|
response = json.loads(res.content)
|
||||||
|
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]:
|
||||||
|
"""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
|
||||||
|
:rtype: Tuple[Position, int]
|
||||||
|
"""
|
||||||
|
res = requests.get(API_URL_GET_GOAL)
|
||||||
|
if res.status_code == 408:
|
||||||
|
logger.error(f"DoubleElim.get_goal timeout. API={API_URL_GET_GOAL}")
|
||||||
|
time.sleep(RETRY_TIMEOUT)
|
||||||
|
return DoubleElim.get_goal()
|
||||||
|
elif res.status_code == 503:
|
||||||
|
return Position(0, 0, -1), 503
|
||||||
|
|
||||||
|
response = json.loads(res.content)
|
||||||
|
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]:
|
||||||
|
"""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}
|
||||||
|
:rtype: Tuple[List[Dict], int]
|
||||||
|
"""
|
||||||
|
res = requests.get(API_URL_GET_ITEMS)
|
||||||
|
if res.status_code == 408:
|
||||||
|
logger.error(f"DoubleElim.get_items timeout. API={API_URL_GET_ITEMS}")
|
||||||
|
time.sleep(RETRY_TIMEOUT)
|
||||||
|
return DoubleElim.get_items()
|
||||||
|
elif res.status_code == 503:
|
||||||
|
return [], 503
|
||||||
|
|
||||||
|
response = json.loads(res.content)
|
||||||
|
logger.debug(f"DoubleElim.get_items = {response}, status code = {res.status_code}")
|
||||||
|
return response, res.status_code
|
||||||
|
|
||||||
|
@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}
|
||||||
|
:rtype: Tuple[Dict, int]
|
||||||
|
"""
|
||||||
|
res = requests.get(API_URL_GET_SCORES)
|
||||||
|
if res.status_code == 408:
|
||||||
|
logger.error(f"DoubleElim.get_scores timeout. API={API_URL_GET_SCORES}")
|
||||||
|
time.sleep(RETRY_TIMEOUT)
|
||||||
|
return DoubleElim.get_scores()
|
||||||
|
elif res.status_code == 503:
|
||||||
|
return {"self": 0, "opponent": 0}, 503
|
||||||
|
|
||||||
|
response = json.loads(res.content)
|
||||||
|
logger.debug(f"DoubleElim.get_scores = {response}, status code = {res.status_code}")
|
||||||
|
return response, res.status_code
|
37
compLib/Encoder.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import compLib.CompLib_pb2 as CompLib_pb2
|
||||||
|
from compLib.CompLibClient import CompLibClient
|
||||||
|
|
||||||
|
|
||||||
|
class Encoder(object):
|
||||||
|
"""Klasse zum Zugriff auf die Encoder der einzelnen Motoren
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def read_all_positions():
|
||||||
|
"""Lesen aller absoluten Positionen der einzelnen Encoder
|
||||||
|
|
||||||
|
:return: Tupel mit allen aktuellen Encoderpositionen
|
||||||
|
"""
|
||||||
|
request = CompLib_pb2.EncoderReadPositionsRequest()
|
||||||
|
request.header.message_type = request.DESCRIPTOR.full_name
|
||||||
|
|
||||||
|
response = CompLib_pb2.EncoderReadPositionsResponse()
|
||||||
|
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.
|
||||||
|
|
||||||
|
:return: Tupel aller aktuellen Motorgeschwindigkeiten in Radianten pro Sekunde
|
||||||
|
"""
|
||||||
|
request = CompLib_pb2.EncoderReadVelocitiesRequest()
|
||||||
|
request.header.message_type = request.DESCRIPTOR.full_name
|
||||||
|
|
||||||
|
response = CompLib_pb2.EncoderReadVelocitiesResponse()
|
||||||
|
response.ParseFromString(CompLibClient.send(
|
||||||
|
request.SerializeToString(), request.ByteSize()))
|
||||||
|
|
||||||
|
return tuple(i for i in response.velocities)
|
23
compLib/HealthCheck.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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)
|
|
@ -1,157 +1,45 @@
|
||||||
import pigpio
|
|
||||||
|
|
||||||
from compLib.ADC import ADC
|
|
||||||
from compLib.LogstashLogging import Logging
|
|
||||||
import spidev
|
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
TOP_LEFT_CHANNEL = 0
|
import compLib.CompLib_pb2 as CompLib_pb2
|
||||||
TOP_RIGHT_CHANNEL = 1
|
|
||||||
|
|
||||||
TOP_IR_MIN_VOLTAGE = 0.0
|
from compLib.CompLibClient import CompLibClient
|
||||||
TOP_IR_MAX_VOLTAGE = 3.3
|
|
||||||
|
|
||||||
BOTTOM_LEFT_PIN = 14
|
|
||||||
BOTTOM_MIDDLE_PIN = 15
|
|
||||||
BOTTOM_RIGHT_PIN = 23
|
|
||||||
|
|
||||||
BOTTOM_LEFT_CHANNEL = 2
|
|
||||||
BOTTOM_MIDDLE_CHANNEL = 1
|
|
||||||
BOTTOM_RIGHT_CHANNEL = 0
|
|
||||||
|
|
||||||
adc = ADC()
|
|
||||||
spi = spidev.SpiDev()
|
|
||||||
spi.open(0, 0)
|
|
||||||
spi.max_speed_hz = 1000000
|
|
||||||
|
|
||||||
# GPIO.setmode(GPIO.BCM)
|
|
||||||
|
|
||||||
GPIO = pigpio.pi()
|
|
||||||
|
|
||||||
GPIO.set_mode(BOTTOM_LEFT_PIN, pigpio.INPUT)
|
|
||||||
GPIO.set_mode(BOTTOM_MIDDLE_PIN, pigpio.INPUT)
|
|
||||||
GPIO.set_mode(BOTTOM_RIGHT_PIN, pigpio.INPUT)
|
|
||||||
|
|
||||||
states = {
|
|
||||||
str(BOTTOM_LEFT_PIN): GPIO.read(BOTTOM_LEFT_PIN),
|
|
||||||
str(BOTTOM_MIDDLE_PIN): GPIO.read(BOTTOM_MIDDLE_PIN),
|
|
||||||
str(BOTTOM_RIGHT_PIN): GPIO.read(BOTTOM_RIGHT_PIN)}
|
|
||||||
|
|
||||||
analog_states = {
|
|
||||||
str(BOTTOM_LEFT_CHANNEL): 0,
|
|
||||||
str(BOTTOM_MIDDLE_CHANNEL): 0,
|
|
||||||
str(BOTTOM_RIGHT_CHANNEL): 0}
|
|
||||||
|
|
||||||
|
|
||||||
def gpio_callback(gpio, level, tick):
|
|
||||||
states[str(gpio)] = level
|
|
||||||
|
|
||||||
|
|
||||||
GPIO.callback(BOTTOM_LEFT_PIN, pigpio.EITHER_EDGE, gpio_callback)
|
|
||||||
GPIO.callback(BOTTOM_MIDDLE_PIN, pigpio.EITHER_EDGE, gpio_callback)
|
|
||||||
GPIO.callback(BOTTOM_RIGHT_PIN, pigpio.EITHER_EDGE, gpio_callback)
|
|
||||||
|
|
||||||
|
|
||||||
def read_analog_channel(channel):
|
|
||||||
spi.writebytes([channel << 3, 0])
|
|
||||||
|
|
||||||
time.sleep(0.0033) # 300HZ
|
|
||||||
|
|
||||||
bytes = spi.readbytes(2)
|
|
||||||
return (bytes[0] * 256 + bytes[1]) >> 2
|
|
||||||
|
|
||||||
|
|
||||||
def sensor_thread():
|
|
||||||
while True:
|
|
||||||
analog_states[str(BOTTOM_LEFT_CHANNEL)] = read_analog_channel(BOTTOM_LEFT_CHANNEL)
|
|
||||||
analog_states[str(BOTTOM_MIDDLE_CHANNEL)] = read_analog_channel(BOTTOM_MIDDLE_CHANNEL)
|
|
||||||
analog_states[str(BOTTOM_RIGHT_CHANNEL)] = read_analog_channel(BOTTOM_RIGHT_CHANNEL)
|
|
||||||
|
|
||||||
|
|
||||||
analog_thread = threading.Thread(target=sensor_thread, daemon=True)
|
|
||||||
analog_thread.start()
|
|
||||||
time.sleep(0.25)
|
|
||||||
|
|
||||||
|
|
||||||
class IRSensor(object):
|
class IRSensor(object):
|
||||||
"""Access the different IR Sensors at top / bottom of the robot
|
"""Ermöglicht den Zugriff auf die einzelnen IRSensoren des Roboters
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def top_left_percent() -> int:
|
def read_all():
|
||||||
"""Get top left infrared sensor percentage
|
"""Auslesen aller Sensoren gleichzeitig
|
||||||
|
|
||||||
:return: Percentage between 0 and 100
|
:return: Array aller Sensorwerte
|
||||||
:rtype: int
|
|
||||||
"""
|
"""
|
||||||
voltage = adc.read(TOP_LEFT_CHANNEL)
|
request = CompLib_pb2.IRSensorsReadAllRequest()
|
||||||
return int((voltage - TOP_IR_MIN_VOLTAGE) / TOP_IR_MAX_VOLTAGE * 100)
|
request.header.message_type = request.DESCRIPTOR.full_name
|
||||||
|
|
||||||
|
response = CompLib_pb2.IRSensorsReadAllResponse()
|
||||||
|
response.ParseFromString(CompLibClient.send(
|
||||||
|
request.SerializeToString(), request.ByteSize()))
|
||||||
|
|
||||||
|
return [i for i in response.data]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def top_right_percent() -> int:
|
def enable():
|
||||||
"""Get top right infrared sensor percentage
|
"""Aktivieren Infrarot-Sender. Muss bei jedem Programmstart ausgeführt werden.
|
||||||
|
|
||||||
:return: Percentage between 0 and 100
|
|
||||||
:rtype: int
|
|
||||||
"""
|
"""
|
||||||
voltage = adc.read(TOP_RIGHT_CHANNEL)
|
request = CompLib_pb2.IRSensorsEnableRequest()
|
||||||
return int((voltage - TOP_IR_MIN_VOLTAGE) / TOP_IR_MAX_VOLTAGE * 100)
|
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
|
@staticmethod
|
||||||
def bottom_left() -> bool:
|
def disable():
|
||||||
"""Get bottom left infrared sensor status
|
"""Deaktivieren der Infrarot-Sender
|
||||||
|
|
||||||
:return: True, if sensor detects IR signals
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
"""
|
||||||
return states[str(BOTTOM_LEFT_PIN)]
|
request = CompLib_pb2.IRSensorsDisableRequest()
|
||||||
# return GPIO.read(BOTTOM_LEFT_PIN)
|
request.header.message_type = request.DESCRIPTOR.full_name
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bottom_middle() -> bool:
|
|
||||||
"""Get bottom middle infrared sensor status
|
|
||||||
|
|
||||||
:return: True, if sensor detects IR signals
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
return states[str(BOTTOM_MIDDLE_PIN)]
|
|
||||||
# return GPIO.read(BOTTOM_MIDDLE_PIN)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bottom_right() -> bool:
|
|
||||||
"""Get bottom right infrared sensor status
|
|
||||||
|
|
||||||
:return: True, if sensor detects IR signals
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
return states[str(BOTTOM_RIGHT_PIN)]
|
|
||||||
# return GPIO.read(BOTTOM_RIGHT_PIN)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bottom_left_analog() -> int:
|
|
||||||
"""Get bottom left infrared sensor value: ranges from 0 to 1023
|
|
||||||
|
|
||||||
:return: 10-bit brightness value
|
|
||||||
:rtype: int
|
|
||||||
"""
|
|
||||||
return analog_states[str(BOTTOM_LEFT_CHANNEL)]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bottom_middle_analog() -> int:
|
|
||||||
"""Get bottom middle infrared sensor value: ranges from 0 to 1023
|
|
||||||
|
|
||||||
:return: 10-bit brightness value
|
|
||||||
:rtype: int
|
|
||||||
"""
|
|
||||||
return analog_states[str(BOTTOM_MIDDLE_CHANNEL)]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bottom_right_analog() -> int:
|
|
||||||
"""Get bottom right infrared sensor value: ranges from 0 to 1023
|
|
||||||
|
|
||||||
:return: 10-bit brightness value
|
|
||||||
:rtype: int
|
|
||||||
"""
|
|
||||||
return analog_states[str(BOTTOM_RIGHT_CHANNEL)]
|
|
||||||
|
|
||||||
|
CompLibClient.send(request.SerializeToString(), request.ByteSize())
|
||||||
|
time.sleep(0.1) # IR sensor reading is async -> Wait a bit
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
from compLib.IRSensor import IRSensor
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
ir = IRSensor()
|
|
||||||
|
|
||||||
MIN_VALUE = 0.0
|
|
||||||
MAX_VALUE = 0.0
|
|
||||||
MOVING_AVERAGE_SIZE = 30.0
|
|
||||||
|
|
||||||
|
|
||||||
def approximate_rolling_average(avg, value):
|
|
||||||
avg -= avg / MOVING_AVERAGE_SIZE
|
|
||||||
avg += value / MOVING_AVERAGE_SIZE
|
|
||||||
return avg
|
|
||||||
|
|
||||||
|
|
||||||
class IRWrapper(object):
|
|
||||||
"""Wrapper around the IRSensor to enable calibration of the sensor
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def calibrate():
|
|
||||||
"""Calibrate the black and white values of the IRSensors
|
|
||||||
This is done by putting the bot on black line with the middle sensor
|
|
||||||
Afterwards, all sensors are read for 2 seconds and filtered.
|
|
||||||
The minimum value is used for white, maximum is black.
|
|
||||||
"""
|
|
||||||
global MIN_VALUE
|
|
||||||
global MAX_VALUE
|
|
||||||
|
|
||||||
left_avg = ir.bottom_left_analog()
|
|
||||||
middle_avg = ir.bottom_middle_analog()
|
|
||||||
right_avg = ir.bottom_right_analog()
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
while time.time() - start_time < 2:
|
|
||||||
left_avg = approximate_rolling_average(left_avg, ir.bottom_left_analog())
|
|
||||||
middle_avg = approximate_rolling_average(middle_avg, ir.bottom_middle_analog())
|
|
||||||
right_avg = approximate_rolling_average(right_avg, ir.bottom_right_analog())
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
MIN_VALUE = min([left_avg, middle_avg, right_avg])
|
|
||||||
MAX_VALUE = max([left_avg, middle_avg, right_avg])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def adjust_for_calibration(raw_value: float) -> float:
|
|
||||||
"""Adjust a raw sensor value to 0 to 1000
|
|
||||||
|
|
||||||
:return: Value between 0 and 1000, White and Black
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
x = (raw_value - MIN_VALUE) * 1000.0 / (MAX_VALUE - MIN_VALUE)
|
|
||||||
return max(min(1000.0, x), 0.0)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bottom_left_calibrated() -> float:
|
|
||||||
"""Returns calibrated value of the bottom left analog sensor
|
|
||||||
|
|
||||||
:return: Value between 0 and 1000, White and Black
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
return IRWrapper.adjust_for_calibration(ir.bottom_left_analog())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bottom_middle_calibrated() -> float:
|
|
||||||
"""Returns calibrated value of the bottom middle analog sensor
|
|
||||||
|
|
||||||
:return: Value between 0 and 1000, White and Black
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
return IRWrapper.adjust_for_calibration(ir.bottom_middle_analog())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bottom_right_calibrated() -> float:
|
|
||||||
"""Returns calibrated value of the bottom right analog sensor
|
|
||||||
|
|
||||||
:return: Value between 0 and 1000, White and Black
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
return IRWrapper.adjust_for_calibration(ir.bottom_right_analog())
|
|
|
@ -1,102 +0,0 @@
|
||||||
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", "True")
|
|
||||||
|
|
||||||
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)
|
|
153
compLib/Motor.py
|
@ -1,63 +1,142 @@
|
||||||
import atexit
|
import compLib.CompLib_pb2 as CompLib_pb2
|
||||||
|
from compLib.CompLibClient import CompLibClient
|
||||||
from compLib.PCA9685 import PCA9685
|
|
||||||
from compLib.LogstashLogging import Logging
|
|
||||||
|
|
||||||
pwm = PCA9685(0x40, debug=True)
|
|
||||||
pwm.setPWMFreq(50)
|
|
||||||
|
|
||||||
MOTOR_COUNT = 4
|
MOTOR_COUNT = 4
|
||||||
MAX_MOTOR_SPEED = 4095.0
|
|
||||||
MOTOR_PERCENTAGE_MULT = MAX_MOTOR_SPEED / 100.0
|
|
||||||
|
|
||||||
|
|
||||||
class Motor(object):
|
class Motor(object):
|
||||||
"""Class used to control the motors
|
"""Klasse zum Ansteuern der Motoren
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def power(port: int, percent: float):
|
def power(port: int, percent: float):
|
||||||
"""Set specified motor to percentage power
|
"""Motor auf eine prozentuale Leistung der Höchstgeschwindigkeit einstellen
|
||||||
|
|
||||||
:param port: Port, which the motor is connected to. 0-3, 0 -> top left, 3 -> top right
|
:param port: Port, an welchen der Motor angesteckt ist. 0-3
|
||||||
:param percent: Percentage of max speed. between -100 and 100
|
:param percent: Prozentsatz der Höchstgeschwindigkeit. zwischen -100 und 100
|
||||||
:raises: IndexError
|
:raises: IndexError
|
||||||
"""
|
"""
|
||||||
Logging.get_logger().debug(f"Motor.power {port} {percent}")
|
|
||||||
|
|
||||||
if port < 0 or port >= MOTOR_COUNT:
|
if port < 0 or port >= MOTOR_COUNT:
|
||||||
raise IndexError("Invalid Motor port specified!")
|
raise IndexError("Invalid Motor port specified!")
|
||||||
|
|
||||||
if percent < -100 or percent > 100:
|
if percent < -100 or percent > 100:
|
||||||
raise IndexError("Invalid Motor speed specified! Speed is between -100 and 100 percent!")
|
raise IndexError(
|
||||||
|
"Invalid Motor speed specified! Speed is between -100 and 100 percent!")
|
||||||
|
|
||||||
forward = True
|
request = CompLib_pb2.MotorSetPowerRequest()
|
||||||
if percent < 0:
|
request.header.message_type = request.DESCRIPTOR.full_name
|
||||||
percent = abs(percent)
|
request.port = port
|
||||||
forward = False
|
request.power = percent
|
||||||
|
|
||||||
# bottom left motor is inverted - REEEEEEEEEEEE
|
CompLibClient.send(request.SerializeToString(), request.ByteSize())
|
||||||
if port == 1:
|
|
||||||
forward = not forward
|
|
||||||
|
|
||||||
adjusted_speed = int(min(max(0.0, percent), 100.0) * MOTOR_PERCENTAGE_MULT)
|
|
||||||
|
|
||||||
if forward:
|
|
||||||
pwm.setMotorPwm(port * 2, adjusted_speed)
|
|
||||||
pwm.setMotorPwm(port * 2 + 1, 0)
|
|
||||||
else:
|
|
||||||
pwm.setMotorPwm(port * 2, 0)
|
|
||||||
pwm.setMotorPwm(port * 2 + 1, adjusted_speed)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def all_off():
|
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
|
||||||
"""
|
"""
|
||||||
Turns of all motors
|
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
|
||||||
"""
|
"""
|
||||||
Logging.get_logger().debug(f"Motor.all_off")
|
|
||||||
|
|
||||||
for i in range(0, MOTOR_COUNT):
|
if port < 0 or port >= MOTOR_COUNT:
|
||||||
Motor.power(i, 0)
|
raise IndexError("Invalid Motor port specified!")
|
||||||
|
|
||||||
|
request = CompLib_pb2.MotorSetSpeedRequest()
|
||||||
|
request.header.message_type = request.DESCRIPTOR.full_name
|
||||||
|
request.port = port
|
||||||
|
request.speed = speed
|
||||||
|
|
||||||
atexit.register(Motor.all_off)
|
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())
|
||||||
|
|
57
compLib/Movement.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import compLib.CompLib_pb2 as CompLib_pb2
|
||||||
|
from compLib.CompLibClient import CompLibClient
|
||||||
|
|
||||||
|
|
||||||
|
class Movement(object):
|
||||||
|
"""High level class to control movement of the robot
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def drive_distance(distance: float, speed: float):
|
||||||
|
"""
|
||||||
|
Drive a given distance with a certain speed.
|
||||||
|
Positive distance and speed with result in forward motion. Everything else will move backwards.
|
||||||
|
:param distance: Distance in meters
|
||||||
|
:param speed: Speed in meters per second
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
request = CompLib_pb2.DriveDistanceRequest()
|
||||||
|
request.header.message_type = request.DESCRIPTOR.full_name
|
||||||
|
request.distance_m = distance
|
||||||
|
request.velocity_m_s = speed
|
||||||
|
|
||||||
|
response = CompLib_pb2.GenericResponse()
|
||||||
|
response.ParseFromString(CompLibClient.send(request.SerializeToString(), request.ByteSize()))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def turn_degrees(degrees: float, speed: float):
|
||||||
|
"""
|
||||||
|
Turn specified degrees with a given speed.
|
||||||
|
Positive degrees and speed with result in counter-clockwise motion. Everything else will be clockwise
|
||||||
|
:param degrees: Degrees between -180 and 180
|
||||||
|
:param speed: Speed in radians per second
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
request = CompLib_pb2.TurnDegreesRequest()
|
||||||
|
request.header.message_type = request.DESCRIPTOR.full_name
|
||||||
|
request.angle_degrees = degrees
|
||||||
|
request.velocity_rad_s = speed
|
||||||
|
|
||||||
|
response = CompLib_pb2.GenericResponse()
|
||||||
|
response.ParseFromString(CompLibClient.send(request.SerializeToString(), request.ByteSize()))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def drive(linear: float, angular: float):
|
||||||
|
"""
|
||||||
|
Non-blocking way to perform a linear and angular motion at the same time.
|
||||||
|
:param linear: Linear speed in meters per second
|
||||||
|
:param angular: Angular speed in radians per second
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
request = CompLib_pb2.DriveDistanceRequest()
|
||||||
|
request.header.message_type = request.DESCRIPTOR.full_name
|
||||||
|
request.linear_velocity_m_s = linear
|
||||||
|
request.angular_velocity_rad_s = angular
|
||||||
|
|
||||||
|
response = CompLib_pb2.GenericResponse()
|
||||||
|
response.ParseFromString(CompLibClient.send(request.SerializeToString(), request.ByteSize()))
|
|
@ -1,76 +0,0 @@
|
||||||
#!/usr/bin/python
|
|
||||||
|
|
||||||
import math
|
|
||||||
import time
|
|
||||||
|
|
||||||
import smbus
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Raspi PCA9685 16-Channel PWM Servo Driver
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class PCA9685:
|
|
||||||
# Registers/etc.
|
|
||||||
__SUBADR1 = 0x02
|
|
||||||
__SUBADR2 = 0x03
|
|
||||||
__SUBADR3 = 0x04
|
|
||||||
__MODE1 = 0x00
|
|
||||||
__PRESCALE = 0xFE
|
|
||||||
__LED0_ON_L = 0x06
|
|
||||||
__LED0_ON_H = 0x07
|
|
||||||
__LED0_OFF_L = 0x08
|
|
||||||
__LED0_OFF_H = 0x09
|
|
||||||
__ALLLED_ON_L = 0xFA
|
|
||||||
__ALLLED_ON_H = 0xFB
|
|
||||||
__ALLLED_OFF_L = 0xFC
|
|
||||||
__ALLLED_OFF_H = 0xFD
|
|
||||||
|
|
||||||
def __init__(self, address=0x40, debug=False):
|
|
||||||
self.bus = smbus.SMBus(1)
|
|
||||||
self.address = address
|
|
||||||
self.debug = debug
|
|
||||||
self.write(self.__MODE1, 0x00)
|
|
||||||
|
|
||||||
def write(self, reg, value):
|
|
||||||
"Writes an 8-bit value to the specified register/address"
|
|
||||||
self.bus.write_byte_data(self.address, reg, value)
|
|
||||||
|
|
||||||
def read(self, reg):
|
|
||||||
"Read an unsigned byte from the I2C device"
|
|
||||||
result = self.bus.read_byte_data(self.address, reg)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def setPWMFreq(self, freq):
|
|
||||||
"Sets the PWM frequency"
|
|
||||||
prescaleval = 25000000.0 # 25MHz
|
|
||||||
prescaleval /= 4096.0 # 12-bit
|
|
||||||
prescaleval /= float(freq)
|
|
||||||
prescaleval -= 1.0
|
|
||||||
|
|
||||||
oldmode = self.read(self.__MODE1)
|
|
||||||
newmode = (oldmode & 0x7F) | 0x10 # sleep
|
|
||||||
self.write(self.__MODE1, newmode) # go to sleep
|
|
||||||
self.write(self.__PRESCALE, int(round(prescaleval)))
|
|
||||||
self.write(self.__MODE1, oldmode)
|
|
||||||
time.sleep(0.005)
|
|
||||||
self.write(self.__MODE1, oldmode | 0x80)
|
|
||||||
|
|
||||||
def setPWM(self, channel, on, off):
|
|
||||||
"Sets a single PWM channel"
|
|
||||||
self.write(self.__LED0_ON_L + 4 * channel, on & 0xFF)
|
|
||||||
self.write(self.__LED0_ON_H + 4 * channel, on >> 8)
|
|
||||||
self.write(self.__LED0_OFF_L + 4 * channel, off & 0xFF)
|
|
||||||
self.write(self.__LED0_OFF_H + 4 * channel, off >> 8)
|
|
||||||
|
|
||||||
def setMotorPwm(self, channel, duty):
|
|
||||||
self.setPWM(channel, 0, duty)
|
|
||||||
|
|
||||||
def setServoPulse(self, channel, pulse):
|
|
||||||
"Sets the Servo Pulse,The PWM frequency must be 50HZ"
|
|
||||||
pulse = float(pulse) * (4096.0 / 20000.0) # PWM frequency is 50HZ,the period is 20000us
|
|
||||||
self.setPWM(channel, 0, int(pulse))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
pass
|
|
124
compLib/Seeding.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
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}
|
||||||
|
Material Pairs: {self.material_pairs}
|
||||||
|
Material Zones: {self.materials}
|
||||||
|
Logistic Plan: {self.logistic_plan}
|
||||||
|
Logistic Centers: {self.logistic_center}"""
|
||||||
|
|
||||||
|
def __init__(self, seed: int):
|
||||||
|
"""
|
||||||
|
Erstellt den Seeding "Gamestate" für den angegebenen 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.materials = [0, 0, 0, 0]
|
||||||
|
self.material_pairs = []
|
||||||
|
for i in range(0, 4):
|
||||||
|
num1 = self.__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)
|
||||||
|
|
||||||
|
flat = [item for sublist in self.material_pairs for item in sublist]
|
||||||
|
for i in range(0, 4):
|
||||||
|
self.materials[i] = flat.count(i)
|
||||||
|
|
||||||
|
self.logistic_plan = [0 for i in range(0, 21)]
|
||||||
|
self.logistic_center = [[0, 0, 0, 0] for i in range(0, 4)]
|
||||||
|
visited = [5, 5, 5, 5]
|
||||||
|
|
||||||
|
def __logistic_plan_generator(i: int):
|
||||||
|
drive_to = self.__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]:
|
||||||
|
continue
|
||||||
|
self.logistic_plan[i] = drive_to
|
||||||
|
|
||||||
|
visited[drive_to] -= 1
|
||||||
|
finished = True
|
||||||
|
for k in visited:
|
||||||
|
if k != 0:
|
||||||
|
finished = False
|
||||||
|
|
||||||
|
if finished and drive_to == 2:
|
||||||
|
visited[drive_to] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if finished:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if i < len(self.logistic_plan):
|
||||||
|
if __logistic_plan_generator(i + 1):
|
||||||
|
return True
|
||||||
|
visited[drive_to] += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.logistic_plan[0] = 2
|
||||||
|
visited[2] -= 1
|
||||||
|
_ = __logistic_plan_generator(1)
|
||||||
|
|
||||||
|
self.logistic_plan[-1] = 2
|
||||||
|
for i in range(0, len(self.logistic_plan) - 1):
|
||||||
|
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
|
|
@ -1,36 +0,0 @@
|
||||||
from compLib.PCA9685 import PCA9685
|
|
||||||
|
|
||||||
SERVO_COUNT = 10
|
|
||||||
|
|
||||||
pwm = PCA9685(0x40, debug=True)
|
|
||||||
pwm.setPWMFreq(50)
|
|
||||||
|
|
||||||
MIN_ANGLE = -90.0
|
|
||||||
MAX_ANGLE = 90.0
|
|
||||||
|
|
||||||
|
|
||||||
class Servo:
|
|
||||||
"""Control the servo ports on the robot
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_position(channel: int, angle: int, offset: float =90):
|
|
||||||
"""Set position of servo connected to port
|
|
||||||
|
|
||||||
:param channel: channel between 0 and 7
|
|
||||||
:param angle: Angle of servo
|
|
||||||
"""
|
|
||||||
if channel < 0 or channel >= SERVO_COUNT:
|
|
||||||
raise IndexError("Invalid Servo channel specified!")
|
|
||||||
|
|
||||||
angle = max(min(angle, MAX_ANGLE), MIN_ANGLE)
|
|
||||||
|
|
||||||
pwm.setServoPulse(channel + 8, 500+int((angle+offset)/0.09))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def setup_position():
|
|
||||||
"""Set position of servos to the position used during the setup process
|
|
||||||
"""
|
|
||||||
|
|
||||||
Servo.set_position(0, 0)
|
|
||||||
Servo.set_position(1, 0)
|
|
|
@ -1,107 +0,0 @@
|
||||||
import time
|
|
||||||
import RPi.GPIO as GPIO
|
|
||||||
|
|
||||||
trigger_pin = 27
|
|
||||||
echo_pin = 22
|
|
||||||
speed_of_sound_mps = 340 # in m/s
|
|
||||||
|
|
||||||
rising_found = False
|
|
||||||
rising_time = 0
|
|
||||||
falling_found = False
|
|
||||||
falling_time = 0
|
|
||||||
|
|
||||||
class Ultrasonic:
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __send_trigger():
|
|
||||||
GPIO.setup(trigger_pin, GPIO.OUT)
|
|
||||||
GPIO.output(trigger_pin, False) # Ensure a clean transition
|
|
||||||
time.sleep(0.000005)
|
|
||||||
GPIO.output(trigger_pin, True) # Trigger now
|
|
||||||
time.sleep(0.000010)
|
|
||||||
GPIO.output(trigger_pin, False)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __signal_start_callback(channel):
|
|
||||||
global rising_found
|
|
||||||
global rising_time
|
|
||||||
rising_found = True
|
|
||||||
rising_time = time.process_time_ns()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __signal_end_callback(channel):
|
|
||||||
global falling_found
|
|
||||||
global falling_time
|
|
||||||
falling_found = True
|
|
||||||
falling_time = time.process_time_ns()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __measure_pulse(timeout = 1000000) -> int:
|
|
||||||
""" Measure the length of a pulse in us. When either the pulse doesn't start in [timeout] us
|
|
||||||
or the pulse is longer than [timeout] us, 0 is returned.
|
|
||||||
|
|
||||||
:param timeout: Timeout in us
|
|
||||||
:return: The length of the pulse or 0 on timeout
|
|
||||||
:rtype: int
|
|
||||||
"""
|
|
||||||
|
|
||||||
global rising_found
|
|
||||||
global rising_time
|
|
||||||
global falling_found
|
|
||||||
global falling_time
|
|
||||||
|
|
||||||
# Setup pin
|
|
||||||
GPIO.setup(echo_pin, GPIO.IN)
|
|
||||||
|
|
||||||
# Wait for the rising edge
|
|
||||||
rising_found = False
|
|
||||||
GPIO.add_event_detect(echo_pin, GPIO.RISING, callback = Ultrasonic.__signal_start_callback)
|
|
||||||
start = time.process_time_ns()
|
|
||||||
while (time.process_time_ns() < start + timeout * 1000 and not rising_found):
|
|
||||||
pass
|
|
||||||
|
|
||||||
GPIO.remove_event_detect(echo_pin)
|
|
||||||
if (not rising_found):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Wait for the falling edge
|
|
||||||
falling_found = False
|
|
||||||
GPIO.add_event_detect(echo_pin, GPIO.FALLING, callback = Ultrasonic.__signal_end_callback)
|
|
||||||
start = time.process_time_ns()
|
|
||||||
while (time.process_time_ns() < start + timeout * 1000 and not falling_found):
|
|
||||||
pass
|
|
||||||
|
|
||||||
GPIO.remove_event_detect(echo_pin)
|
|
||||||
if (not falling_found):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Finally, return the time difference
|
|
||||||
return int((falling_time - rising_time) / 1000)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_distance() -> float:
|
|
||||||
""" Get the distance from the Ultrasonic sensor in cm.
|
|
||||||
Returns 'NaN' when the result is invalid.
|
|
||||||
|
|
||||||
:return: The distance in cm or 'NaN'
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
GPIO.setmode(GPIO.BCM)
|
|
||||||
|
|
||||||
# Trigger the measurement
|
|
||||||
Ultrasonic.__send_trigger()
|
|
||||||
|
|
||||||
# The pulse length in us
|
|
||||||
pulse = Ultrasonic.__measure_pulse()
|
|
||||||
|
|
||||||
if (pulse == 0):
|
|
||||||
print("invalid")
|
|
||||||
return float('NaN')
|
|
||||||
|
|
||||||
# Convert the speed of sound from m/s to cm/us for convenience
|
|
||||||
speed_of_sound_cmpus = speed_of_sound_mps / 10000
|
|
||||||
|
|
||||||
# Convert us time to cm distance
|
|
||||||
distance = (pulse / 2) * speed_of_sound_cmpus
|
|
||||||
|
|
||||||
return distance
|
|
|
@ -1,192 +0,0 @@
|
||||||
import os
|
|
||||||
import queue
|
|
||||||
from queue import Queue
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import logging
|
|
||||||
import threading
|
|
||||||
from compLib.LogstashLogging import Logging
|
|
||||||
|
|
||||||
from flask import Flask, Response
|
|
||||||
|
|
||||||
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 __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)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
|
@ -1,66 +0,0 @@
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
import systemd.daemon
|
|
||||||
import LogstashLogging
|
|
||||||
from LogstashLogging import logstash_logger
|
|
||||||
from threading import Thread
|
|
||||||
import logging
|
|
||||||
|
|
||||||
RUN_CHECK = False
|
|
||||||
try:
|
|
||||||
from Battery import Battery
|
|
||||||
from Buzzer import Buzzer
|
|
||||||
RUN_CHECK = True
|
|
||||||
except Exception as e:
|
|
||||||
logstash_logger.error(f"unable to import battery or buzzer in daemon -> {str(e)}")
|
|
||||||
|
|
||||||
__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
|
|
||||||
|
|
||||||
|
|
||||||
def check_battery():
|
|
||||||
while True:
|
|
||||||
time.sleep(2)
|
|
||||||
battery = 0
|
|
||||||
try:
|
|
||||||
battery = Battery.percent()
|
|
||||||
except Exception as e:
|
|
||||||
logstash_logger.error(f"could not check battery -> {str(e)}")
|
|
||||||
if -100 <= battery <= 15:
|
|
||||||
logstash_logger.warning(f"LOW BATTERY DETECTED: '{battery}'")
|
|
||||||
Buzzer.set(True)
|
|
||||||
time.sleep(1)
|
|
||||||
Buzzer.set(False)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
systemd.daemon.notify(systemd.daemon.Notification.READY)
|
|
||||||
except:
|
|
||||||
logstash_logger.warning("Warning, old systemd version detected")
|
|
||||||
systemd.daemon.notify('READY=1')
|
|
||||||
|
|
||||||
logstash_logger.info("starting battery checker")
|
|
||||||
battery_checker = None
|
|
||||||
if RUN_CHECK:
|
|
||||||
try:
|
|
||||||
battery_checker = threading.Thread(target=check_battery)
|
|
||||||
battery_checker.start()
|
|
||||||
logstash_logger.info("starting battery checker - DONE")
|
|
||||||
except Exception as e:
|
|
||||||
logstash_logger.error(f"could not start battery checker -> {str(e)}")
|
|
||||||
|
|
||||||
if STREAM_RASPI:
|
|
||||||
logstash_logger.info("starting gstreamer background process")
|
|
||||||
os.system(__run)
|
|
||||||
logstash_logger.error("gstreamer stopped...")
|
|
||||||
else:
|
|
||||||
logstash_logger.info("not starting gstreamer background process and only checking for battery")
|
|
||||||
if battery_checker is not None:
|
|
||||||
battery_checker.join()
|
|
||||||
else:
|
|
||||||
logstash_logger.info("battery checker failed to initialize.. sleeping for a day, good night")
|
|
||||||
time.sleep(60*60*24)
|
|
|
@ -1,16 +1,7 @@
|
||||||
__version__ = "0.1.5-1"
|
|
||||||
|
|
||||||
import compLib.LogstashLogging
|
|
||||||
import logging
|
import logging
|
||||||
import apt
|
import os
|
||||||
|
|
||||||
try:
|
if os.getenv("DEBUG", "0") != "0":
|
||||||
__versions = apt.Cache()["python3-complib"].versions
|
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG)
|
||||||
if len(__versions) != 1:
|
else:
|
||||||
print(f"Starting compLib! \033[91mVersion: {__version__} is outdated\033[0m\n"
|
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
|
||||||
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")
|
|
||||||
|
|
14
compLib/include/comp_lib_node.h
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#ifndef ROS_NODE_H
|
||||||
|
#define ROS_NODE_H
|
||||||
|
|
||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
|
||||||
|
class CompLibNode : public rclcpp::Node
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CompLibNode();
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
24
compLib/include/controls.h
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
#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
|
67
compLib/include/motor.h
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
#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
|
11
compLib/include/sequence_lock.h
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
#ifndef SEQUENCE_LOCK_H
|
||||||
|
#define SEQUENCE_LOCK_H
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace SequenceLock
|
||||||
|
{
|
||||||
|
std::mutex m;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
22
compLib/package.xml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?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>
|
10
compLib/src/comp_lib_node.cpp
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#include "ros_node.h"
|
||||||
|
|
||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
#include "rclcpp_action/rclcpp_action.hpp"
|
||||||
|
|
||||||
|
CompLibNode::CompLibNode()
|
||||||
|
: Node("CompLibNode")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
53
compLib/src/controls.cpp
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
#include "rclcpp_action/rclcpp_action.hpp"
|
||||||
|
|
||||||
|
#include "irobot_create_msgs/msg/interface_buttons.hpp"
|
||||||
|
|
||||||
|
#include "controls.h"
|
||||||
|
|
||||||
|
ButtonPressNode::ButtonPressNode()
|
||||||
|
: Node("button_press_node")
|
||||||
|
{
|
||||||
|
interface_buttons_subscriber_ = this->create_subscription<irobot_create_msgs::msg::InterfaceButtons>(
|
||||||
|
"/interface_buttons",
|
||||||
|
rclcpp::SensorDataQoS(),
|
||||||
|
std::bind(&ButtonPressNode::result_callback, this, std::placeholders::_1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonPressNode::bt1_wait()
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Wait for button 1...");
|
||||||
|
button1 = false;
|
||||||
|
while (!button1) {}
|
||||||
|
button1 = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonPressNode::bt2_wait()
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Wait for button 2...");
|
||||||
|
button2 = false;
|
||||||
|
while (!button2) {}
|
||||||
|
button2 = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonPressNode::result_callback(const irobot_create_msgs::msg::InterfaceButtons::SharedPtr result)
|
||||||
|
{
|
||||||
|
if (result->button_1.is_pressed) {
|
||||||
|
button1 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result->button_2.is_pressed) {
|
||||||
|
button2 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result->button_power.is_pressed) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonPressNode::kill()
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "ButtonPressNode killed");
|
||||||
|
rclcpp::shutdown();
|
||||||
|
}
|
93
compLib/src/main.cpp
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
#include <thread>
|
||||||
|
#include <memory>
|
||||||
|
#include <chrono>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "motor.h"
|
||||||
|
#include "controls.h"
|
||||||
|
|
||||||
|
std::mutex action_mutex;
|
||||||
|
|
||||||
|
// #lazyness
|
||||||
|
void run_node(std::shared_ptr<DriveDistNode> node)
|
||||||
|
{
|
||||||
|
rclcpp::spin(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void run_node1(std::shared_ptr<SetSpeedNode> node)
|
||||||
|
{
|
||||||
|
rclcpp::spin(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void run_node2(std::shared_ptr<RotateAngleNode> node)
|
||||||
|
{
|
||||||
|
rclcpp::spin(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void run_node3(std::shared_ptr<DriveArcNode> node)
|
||||||
|
{
|
||||||
|
rclcpp::spin(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void run_node4(std::shared_ptr<ButtonPressNode> node)
|
||||||
|
{
|
||||||
|
rclcpp::spin(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char * argv[])
|
||||||
|
{
|
||||||
|
rclcpp::init(argc, argv);
|
||||||
|
|
||||||
|
auto ddn = std::make_shared<DriveDistNode>();
|
||||||
|
auto ssn = std::make_shared<SetSpeedNode>();
|
||||||
|
auto ran = std::make_shared<RotateAngleNode>();
|
||||||
|
auto dan = std::make_shared<DriveArcNode>();
|
||||||
|
auto bpn = std::make_shared<ButtonPressNode>();
|
||||||
|
|
||||||
|
std::thread t;
|
||||||
|
std::thread t1;
|
||||||
|
std::thread t2;
|
||||||
|
std::thread t3;
|
||||||
|
std::thread t4;
|
||||||
|
|
||||||
|
t = std::thread(run_node, ddn);
|
||||||
|
t1 = std::thread(run_node1, ssn);
|
||||||
|
t2 = std::thread(run_node2, ran);
|
||||||
|
t3 = std::thread(run_node3, dan);
|
||||||
|
t4 = std::thread(run_node4, bpn);
|
||||||
|
|
||||||
|
bpn->bt1_wait();
|
||||||
|
bpn->bt2_wait();
|
||||||
|
bpn->bt1_wait();
|
||||||
|
|
||||||
|
ssn->drive(0.3);
|
||||||
|
|
||||||
|
std::this_thread::sleep_for (std::chrono::milliseconds(2000));
|
||||||
|
|
||||||
|
ssn->stop();
|
||||||
|
|
||||||
|
// std::this_thread::sleep_for (std::chrono::milliseconds(1000));
|
||||||
|
|
||||||
|
ran->rotate_angle(-45, 0.5);
|
||||||
|
// std::this_thread::sleep_for (std::chrono::milliseconds(5000));
|
||||||
|
|
||||||
|
ddn->drive_dist(0.2, 0.3);
|
||||||
|
|
||||||
|
// std::this_thread::sleep_for (std::chrono::milliseconds(5000));
|
||||||
|
|
||||||
|
dan->drive_arc(90, 0.5, 0.5);
|
||||||
|
|
||||||
|
ddn->kill();
|
||||||
|
ssn->kill();
|
||||||
|
ddn->kill();
|
||||||
|
dan->kill();
|
||||||
|
bpn->kill();
|
||||||
|
|
||||||
|
t.join();
|
||||||
|
t1.join();
|
||||||
|
t2.join();
|
||||||
|
t3.join();
|
||||||
|
t4.join();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
225
compLib/src/motor.cpp
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
#include <mutex>
|
||||||
|
#include <future>
|
||||||
|
#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"
|
||||||
|
|
||||||
|
#include "motor.h"
|
||||||
|
#include "sequence_lock.h"
|
||||||
|
|
||||||
|
double pi = 2 * acos(0.0);
|
||||||
|
|
||||||
|
DriveDistNode::DriveDistNode()
|
||||||
|
: Node("drive_dist_node")
|
||||||
|
{
|
||||||
|
drive_dist_action_ = rclcpp_action::create_client<irobot_create_msgs::action::DriveDistance>(
|
||||||
|
this,
|
||||||
|
"/drive_distance"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DriveDistNode::drive_dist(float meters, float velocity)
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "drive dist");
|
||||||
|
processing = true;
|
||||||
|
auto data = irobot_create_msgs::action::DriveDistance::Goal();
|
||||||
|
auto send_goal_options = rclcpp_action::Client<irobot_create_msgs::action::DriveDistance>::SendGoalOptions();
|
||||||
|
send_goal_options.result_callback =
|
||||||
|
std::bind(&DriveDistNode::result_callback, this, std::placeholders::_1);
|
||||||
|
|
||||||
|
data.distance = meters;
|
||||||
|
data.max_translation_speed = velocity;
|
||||||
|
drive_dist_action_->async_send_goal(data, send_goal_options);
|
||||||
|
|
||||||
|
while (processing) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DriveDistNode::result_callback(const rclcpp_action::ClientGoalHandle<irobot_create_msgs::action::DriveDistance>::WrappedResult & result)
|
||||||
|
{
|
||||||
|
processing = false;
|
||||||
|
switch (result.code) {
|
||||||
|
case rclcpp_action::ResultCode::SUCCEEDED:
|
||||||
|
RCLCPP_INFO(this->get_logger(), "finished dist");
|
||||||
|
return;
|
||||||
|
case rclcpp_action::ResultCode::ABORTED:
|
||||||
|
RCLCPP_ERROR(this->get_logger(), "Goal was aborted");
|
||||||
|
return;
|
||||||
|
case rclcpp_action::ResultCode::CANCELED:
|
||||||
|
RCLCPP_ERROR(this->get_logger(), "Goal was canceled");
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
RCLCPP_ERROR(this->get_logger(), "Unknown result code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DriveDistNode::kill()
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "DriveDistNode killed");
|
||||||
|
rclcpp::shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
SetSpeedNode::SetSpeedNode()
|
||||||
|
: Node("set_speed_node")
|
||||||
|
{
|
||||||
|
speed_publisher_ = this->create_publisher<geometry_msgs::msg::Twist>(
|
||||||
|
"/cmd_vel",
|
||||||
|
rclcpp::SensorDataQoS()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetSpeedNode::drive_loop(float speed)
|
||||||
|
{
|
||||||
|
while (run)
|
||||||
|
{
|
||||||
|
set_speed(speed);
|
||||||
|
// sleep set as described at http://wiki.ros.org/Robots/TIAGo/Tutorials/motions/cmd_vel
|
||||||
|
std::this_thread::sleep_for (std::chrono::milliseconds(333));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetSpeedNode::set_speed(float speed)
|
||||||
|
{
|
||||||
|
auto data = geometry_msgs::msg::Twist();
|
||||||
|
|
||||||
|
data.linear.x = speed;
|
||||||
|
data.linear.y = 0;
|
||||||
|
data.linear.z = 0;
|
||||||
|
data.angular.x = 0;
|
||||||
|
data.angular.x = 0;
|
||||||
|
data.angular.x = 0;
|
||||||
|
speed_publisher_->publish(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetSpeedNode::drive(float speed)
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Start drive");
|
||||||
|
run = true;
|
||||||
|
t = std::thread(&SetSpeedNode::drive_loop, this, speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetSpeedNode::stop()
|
||||||
|
{
|
||||||
|
run = false;
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Stop drive");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetSpeedNode::kill()
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "SetSpeedNode killed");
|
||||||
|
rclcpp::shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
RotateAngleNode::RotateAngleNode()
|
||||||
|
: Node("rotate_angle_node")
|
||||||
|
{
|
||||||
|
rotate_angle_action_ = rclcpp_action::create_client<irobot_create_msgs::action::RotateAngle>(
|
||||||
|
this,
|
||||||
|
"rotate_angle"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RotateAngleNode::rotate_angle(float angle, float velocity)
|
||||||
|
{
|
||||||
|
processing = true;
|
||||||
|
angle *= pi / 180;
|
||||||
|
auto data = irobot_create_msgs::action::RotateAngle::Goal();
|
||||||
|
auto send_goal_options = rclcpp_action::Client<irobot_create_msgs::action::RotateAngle>::SendGoalOptions();
|
||||||
|
send_goal_options.result_callback =
|
||||||
|
std::bind(&RotateAngleNode::result_callback, this, std::placeholders::_1);
|
||||||
|
|
||||||
|
|
||||||
|
data.angle = angle;
|
||||||
|
data.max_rotation_speed = velocity;
|
||||||
|
rotate_angle_action_->async_send_goal(data, send_goal_options);
|
||||||
|
|
||||||
|
while (processing) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RotateAngleNode::result_callback(const rclcpp_action::ClientGoalHandle<irobot_create_msgs::action::RotateAngle>::WrappedResult & result)
|
||||||
|
{
|
||||||
|
processing = false;
|
||||||
|
switch (result.code) {
|
||||||
|
case rclcpp_action::ResultCode::SUCCEEDED:
|
||||||
|
RCLCPP_INFO(this->get_logger(), "finished rotation");
|
||||||
|
return;
|
||||||
|
case rclcpp_action::ResultCode::ABORTED:
|
||||||
|
RCLCPP_ERROR(this->get_logger(), "Goal was aborted");
|
||||||
|
return;
|
||||||
|
case rclcpp_action::ResultCode::CANCELED:
|
||||||
|
RCLCPP_ERROR(this->get_logger(), "Goal was canceled");
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
RCLCPP_ERROR(this->get_logger(), "Unknown result code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RotateAngleNode::kill()
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "RotateAngleNode killed");
|
||||||
|
rclcpp::shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
DriveArcNode::DriveArcNode()
|
||||||
|
: Node("drive_arc_node")
|
||||||
|
{
|
||||||
|
drive_arc_action_ = rclcpp_action::create_client<irobot_create_msgs::action::DriveArc>(
|
||||||
|
this,
|
||||||
|
"/drive_arc"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DriveArcNode::drive_arc(float angle, float radius, float velocity, int direction)
|
||||||
|
{
|
||||||
|
processing = true;
|
||||||
|
angle *= pi / 180;
|
||||||
|
auto data = irobot_create_msgs::action::DriveArc::Goal();
|
||||||
|
auto send_goal_options = rclcpp_action::Client<irobot_create_msgs::action::DriveArc>::SendGoalOptions();
|
||||||
|
send_goal_options.result_callback =
|
||||||
|
std::bind(&DriveArcNode::result_callback, this, std::placeholders::_1);
|
||||||
|
|
||||||
|
|
||||||
|
data.angle = angle;
|
||||||
|
data.radius = radius;
|
||||||
|
data.translate_direction = direction;
|
||||||
|
data.max_translation_speed = velocity;
|
||||||
|
drive_arc_action_->async_send_goal(data, send_goal_options);
|
||||||
|
|
||||||
|
while (processing) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DriveArcNode::result_callback(const rclcpp_action::ClientGoalHandle<irobot_create_msgs::action::DriveArc>::WrappedResult & result)
|
||||||
|
{
|
||||||
|
processing = false;
|
||||||
|
switch (result.code) {
|
||||||
|
case rclcpp_action::ResultCode::SUCCEEDED:
|
||||||
|
RCLCPP_INFO(this->get_logger(), "finished arc");
|
||||||
|
return;
|
||||||
|
case rclcpp_action::ResultCode::ABORTED:
|
||||||
|
RCLCPP_ERROR(this->get_logger(), "Goal was aborted");
|
||||||
|
return;
|
||||||
|
case rclcpp_action::ResultCode::CANCELED:
|
||||||
|
RCLCPP_ERROR(this->get_logger(), "Goal was canceled");
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
RCLCPP_ERROR(this->get_logger(), "Unknown result code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DriveArcNode::kill()
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "DriveArcNode killed");
|
||||||
|
rclcpp::shutdown();
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Monitoring service
|
|
||||||
[Service]
|
|
||||||
ExecStart=/usr/bin/python3 /usr/local/lib/python3.7/dist-packages/compLib/VisionDaemon.py
|
|
||||||
Environment="debug=False"
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
Type=notify
|
|
||||||
[Install]
|
|
||||||
Alias=complib
|
|
||||||
WantedBy=default.target
|
|
3
docs/.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
build
|
build
|
||||||
logs.db
|
logs.db
|
||||||
|
!lib
|
|
@ -15,15 +15,17 @@ import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
sys.setrecursionlimit(1500)
|
sys.setrecursionlimit(1500)
|
||||||
|
os.environ["EXTENSIVE_LOGGING"] = "False"
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'CompLib'
|
project = 'CompLib'
|
||||||
copyright = '2021, robo4you'
|
copyright = '2022, Verein zur Förderung von Wissenschaft und Technik an Schulen (F-WuTS)'
|
||||||
author = 'robo4you'
|
author = 'robo4you'
|
||||||
|
autoclass_content = 'both'
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags
|
# The full version, including alpha/beta/rc tags
|
||||||
release = '0.0.2'
|
release = '0.2.3'
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
@ -35,7 +37,8 @@ extensions = [
|
||||||
'sphinx_rtd_theme'
|
'sphinx_rtd_theme'
|
||||||
]
|
]
|
||||||
|
|
||||||
autodoc_mock_imports = ["smbus", "compLib.PCA9685", "RPi", "pigpio", "flask", "apt", "spidev"]
|
autodoc_mock_imports = ["smbus", "compLib.PCA9685", "RPi",
|
||||||
|
"pigpio", "flask", "apt", "influxdb_client"]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
|
@ -56,3 +59,11 @@ html_theme = 'sphinx_rtd_theme'
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
html_logo = "images/compair-logo-white.svg"
|
||||||
|
html_theme_options = {
|
||||||
|
'logo_only': True,
|
||||||
|
'display_version': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
language = "de"
|
||||||
|
|
32
docs/source/faq.rst
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
FAQ
|
||||||
|
###
|
||||||
|
|
||||||
|
Was ist das Passwort für die Entwicklungsumgebung?
|
||||||
|
--------------------------------------------------
|
||||||
|
``compair``
|
||||||
|
|
||||||
|
Wie verbinde ich mich zur Entwicklungsumgebung?
|
||||||
|
-----------------------------------------------
|
||||||
|
|
||||||
|
See :ref:`gettingstarted_codeserver`
|
||||||
|
|
||||||
|
Was ist der Benutzername und das Passwort für den Raspberry Pi?
|
||||||
|
---------------------------------------------------------------
|
||||||
|
``compair`` ``compair``
|
||||||
|
|
||||||
|
Wie aktualisiere ich meine Software?
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sudo apt update
|
||||||
|
sudo apt upgrade
|
||||||
|
sudo update-firmware
|
||||||
|
|
||||||
|
Wie kann ich die SD-Karte neu beschreiben?
|
||||||
|
------------------------------------------
|
||||||
|
`SD-Karten Image <https://drive.google.com/drive/folders/16lMe-yGphk947L4WPjd4oD8ndY9R1WbA?usp=share_link>`_
|
||||||
|
|
||||||
|
Software zum Schreiben der SD-Karte `balenaEtcher <https://www.balena.io/etcher/>`_
|
||||||
|
|
||||||
|
|
11
docs/source/gettingStarted/codeServer.rst
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.. _gettingstarted_codeserver:
|
||||||
|
|
||||||
|
Programmierumgebung
|
||||||
|
###################
|
||||||
|
|
||||||
|
Als Umgebung zur Programmierung des Roboters wird `code-server <https://github.com/coder/code-server>`_ eingesetzt, welche bereits am Roboter vorinstalliert ist.
|
||||||
|
|
||||||
|
Verbindung zur Entwicklungsumgebung herstellen
|
||||||
|
----------------------------------------------
|
||||||
|
Am Roboter wird die IP-Adresse des Raspberry Pi angezeigt. Um nun die Verbindung herzustellen, muss man in einem Web-Browser einfach ``<roboter_ip>:8080`` eingeben.
|
||||||
|
Das Passwort für Visual Studio Code im Browser ist ``compair``!
|
26
docs/source/gettingStarted/firstProgram.rst
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
Mein erstes Programm
|
||||||
|
####################
|
||||||
|
|
||||||
|
Um mit der Programmierung zu beginnen, müssen wir zunächst einen neuen Ordner erstellen, in dem alle unsere Python-Dateien gespeichert werden.
|
||||||
|
|codeServerFolder|
|
||||||
|
|
||||||
|
Sie können diesen Ordner nennen, wie Sie wollen, für dieses Beispiel heißt er ``compAIR``.
|
||||||
|
Im nächsten Schritt erstellen wir unsere Datei ``main.py``.
|
||||||
|
|codeServerFile|
|
||||||
|
|
||||||
|
Dann können wir beginnen, unseren Code in diese Datei zu schreiben.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
print("Hallo Welt")
|
||||||
|
|
||||||
|
Praktischerweise können wir die Datei auch über die VS-Code-Plattform ausführen.
|
||||||
|
|codeServerRun|
|
||||||
|
|
||||||
|
Dann öffnet sich ein Terminal, der die Ausgabe unseres Programms anzeigt.
|
||||||
|
|codeServerTerminal|
|
||||||
|
|
||||||
|
.. |codeServerFolder| image:: images/06_codeServerFolder.png
|
||||||
|
.. |codeServerFile| image:: images/03_codeServerFile.png
|
||||||
|
.. |codeServerRun| image:: images/04_codeServerRun.png
|
||||||
|
.. |codeServerTerminal| image:: images/05_codeServerTerminal.png
|
BIN
docs/source/gettingStarted/images/01_boot.png
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
docs/source/gettingStarted/images/02_psk.png
Normal file
After Width: | Height: | Size: 239 KiB |
BIN
docs/source/gettingStarted/images/03_codeServerFile.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
docs/source/gettingStarted/images/04_codeServerRun.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
docs/source/gettingStarted/images/05_codeServerTerminal.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
docs/source/gettingStarted/images/06_codeServerFolder.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
docs/source/gettingStarted/images/07_irSensor.webp
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
docs/source/gettingStarted/images/08_notepad.png
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
docs/source/gettingStarted/images/09_update.png
Normal file
After Width: | Height: | Size: 148 KiB |
12
docs/source/gettingStarted/index.rst
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
Erste Schritte
|
||||||
|
##############
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 5
|
||||||
|
|
||||||
|
wifi.rst
|
||||||
|
codeServer.rst
|
||||||
|
firstProgram.rst
|
||||||
|
update.rst
|
||||||
|
secondProgram.rst
|
||||||
|
thridProgram.rst
|
313
docs/source/gettingStarted/secondProgram.rst
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
Mein zweites Programm
|
||||||
|
#####################
|
||||||
|
|
||||||
|
Motoren ansteuern
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Um die Motoren des Roboters zu steuern, müssen wir zunächst das entsprechende Python-Modul am Anfang der Datei importieren. Dann können wir Motor.power(port, power) verwenden, um den Motor zu steuern.
|
||||||
|
Dies ist auch ein guter Punkt, um sich mit der Dokumentation vertraut zu machen: Besuchen wir https://lib.comp-air.at/lib/Motor.html#compLib.Motor.Motor.power. Hier werden die beiden relevanten Parameter beschrieben.
|
||||||
|
|
||||||
|
Als Beispiel wollen wir den rechten Motor für fünf Sekunden auf volle Geschwindigkeit setzen:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
# motor.py
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
Motor.power(0, 100)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
Gerade fahren
|
||||||
|
-------------
|
||||||
|
Um geradeaus zu fahren, müssen wir beide Motoren auf dieselbe Geschwindigkeit einstellen.
|
||||||
|
Aber Achtung! Der rechte Motor muss umgedreht werden! Das liegt daran, dass einer nach rechts und einer nach links zeigt, sie sind also technisch gesehen gespiegelt.
|
||||||
|
Wenn wir nun diesen Code ausführen, wird der Roboter 5 Sekunden lang vorwärts fahren:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
# motor.py
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
Motor.power(0, -100)
|
||||||
|
Motor.power(3, 100)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
**Erläuterung**
|
||||||
|
|
||||||
|
| In ``Zeile 2`` wird das python-Paket ``time`` importiert. Wir brauchen es später, um auf die Fahrt des Roboters zu warten. Z.B.: ``time.sleep(5)``
|
||||||
|
| In ``Zeile 3`` importieren wir die notwendigen Funktionen aus dem ``Motor``-Modul der compLib.
|
||||||
|
| In ``Zeile 5`` stellen wir den ``rechten`` Motor so ein, dass er vorwärts fährt. Da der Motor rückwärts eingebaut ist, müssen wir den Wert auf ``-100`` setzen.
|
||||||
|
| In ``Zeile 6`` stellen wir den ``linken`` Motor auf Vorwärtsfahrt ein. Hier können wir den Wert ``100`` verwenden, da der Motor in der richtigen Richtung eingebaut ist.
|
||||||
|
| In ``Zeile 7`` müssen wir warten, bis der Roboter die Fahrbefehle tatsächlich ausführt. In diesem Fall warten wir ``5`` Sekunden lang.
|
||||||
|
|
||||||
|
Danach wird das Programm beendet und der Roboter bleibt stehen.
|
||||||
|
|
||||||
|
Mehr fahren
|
||||||
|
+++++++++++
|
||||||
|
|
||||||
|
Jetzt ist es Zeit für einige komplexere Bewegungen. Um unseren Code modular und leicht lesbar zu halten, werden wir jede Aktion in eine eigene Funktion packen.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
def driveForward():
|
||||||
|
Motor.power(0, -100)
|
||||||
|
Motor.power(3, 100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
In ``Zeile 4`` definieren wir die Funktion ``driveForward()``, die den Roboter mit voller Geschwindigkeit zwei Sekunden vorwärts bewegt.
|
||||||
|
|
||||||
|
Jetzt werden wir eine Funktion für das Rückwärtsfahren definieren:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
def driveForward():
|
||||||
|
Motor.power(0, -100)
|
||||||
|
Motor.power(3, 100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def driveBackward():
|
||||||
|
Motor.power(0, 100)
|
||||||
|
Motor.power(3, -100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
In ``Zeile 9`` haben wir die Funktion ``driveBackward()`` definiert, die den Roboter zwei Sekunden lang rückwärts fahren lässt.
|
||||||
|
|
||||||
|
Jetzt können wir diese beiden Funktionen aufrufen und vorwärts und dann wieder rückwärts fahren:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
def driveForward():
|
||||||
|
Motor.power(0, -100)
|
||||||
|
Motor.power(3, 100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def driveBackward():
|
||||||
|
Motor.power(0, 100)
|
||||||
|
Motor.power(3, -100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
driveForward()
|
||||||
|
driveBackward()
|
||||||
|
|
||||||
|
Wenn wir diesen Code ausführen, sollte der Roboter zunächst zwei Sekunden vorwärts und dann wieder zwei Sekunden rückwärts fahren und ungefähr an der gleichen Position wie beim Start anhalten.
|
||||||
|
|
||||||
|
Zwischen den Zeilen ``14`` und ``15`` brauchen wir kein ``time.sleep(2)``, da der sleep-Befehl bereits in den Funktionen integriert ist.
|
||||||
|
|
||||||
|
Jetzt wollen wir, dass der Roboter erst vorwärts fährt, dann zwei Sekunden stillsteht und dann wieder rückwärts in seine Ausgangsposition fährt.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
def driveForward():
|
||||||
|
Motor.power(0, -100)
|
||||||
|
Motor.power(3, 100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def driveBackward():
|
||||||
|
Motor.power(0, 100)
|
||||||
|
Motor.power(3, -100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
driveForward()
|
||||||
|
time.sleep(2)
|
||||||
|
driveBackward()
|
||||||
|
|
||||||
|
Wenn wir den obigen Code ausführen, bleibt der Roboter nicht zwei Sekunden lang stehen, sondern fährt nach der Funktion ``driveForward()`` noch zwei Sekunden lang weiter. Warum passiert das? Um das zu verstehen, müssen wir wie der Roboter denken!
|
||||||
|
|
||||||
|
**Erläuterung**
|
||||||
|
|
||||||
|
| 1. (``Zeile 14``) Die Funktion Vorwärtsfahrt wird aufgerufen
|
||||||
|
| (``Zeile 5``) Motor 1 wird auf -100 gesetzt
|
||||||
|
| (``Zeile 6``) Motor 4 wird auf 100 gesetzt
|
||||||
|
| (``Zeile 7``) Zwei Sekunden warten und Motor 1 mit der Geschwindigkeit -100 und Motor 4 mit der Geschwindigkeit 100 bewegen (z.B. vorwärts fahren)
|
||||||
|
|
||||||
|
| 2. (``Zeile 15``) Zwei Sekunden warten, die Motoren sind immer noch auf -100 und 100 eingestellt, also fahren wir weiter vorwärts
|
||||||
|
|
||||||
|
| 3. (``Zeile 16``) Die Funktion Rückwärtsfahren wird aufgerufen
|
||||||
|
| (``Zeile 5``) Motor 1 wird auf 100 gesetzt
|
||||||
|
| (``Zeile 6``) Motor 4 wird auf -100 gesetzt
|
||||||
|
| (``Zeile 7``) Warte zwei Sekunden und bewege Motor 1 mit der Geschwindigkeit 100 und Motor 4 mit der Geschwindigkeit -100 (z.B. Rückwärtsfahren)
|
||||||
|
|
||||||
|
| 4. Das Programm ist beendet, und alle Motordrehzahlen werden auf 0 gesetzt.
|
||||||
|
|
||||||
|
Wir sehen also, dass wir die Motoren nach der Vorwärts- oder Rückwärtsfunktion wieder auf Geschwindigkeit ``0`` setzen müssen, wenn wir den Roboter anhalten wollen. Für diesen Anwendungsfall können wir eine neue Funktion ``stopMotors()`` schreiben, die die Geschwindigkeit für Motor ``0`` und ``3`` auf ``0`` setzt:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
def driveForward():
|
||||||
|
Motor.power(0, -100)
|
||||||
|
Motor.power(3, 100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def driveBackward():
|
||||||
|
Motor.power(0, 100)
|
||||||
|
Motor.power(3, -100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def stopMotors():
|
||||||
|
Motor.power(0, 0)
|
||||||
|
Motor.power(3, 0)
|
||||||
|
|
||||||
|
Wenn wir nun vorwärts fahren, dann zwei Sekunden warten und dann wieder rückwärts fahren wollen, können wir die Funktionen wie folgt aufrufen:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
def driveForward():
|
||||||
|
Motor.power(0, -100)
|
||||||
|
Motor.power(3, 100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def driveBackward():
|
||||||
|
Motor.power(0, 100)
|
||||||
|
Motor.power(3, -100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def stopMotors():
|
||||||
|
Motor.power(0, 0)
|
||||||
|
Motor.power(3, 0)
|
||||||
|
|
||||||
|
driveForward()
|
||||||
|
stopMotors()
|
||||||
|
time.sleep(2)
|
||||||
|
driveBackward()
|
||||||
|
|
||||||
|
Und endlich bekommen wir die Bewegung, die wir uns wünschen.
|
||||||
|
|
||||||
|
**More Optimizations**
|
||||||
|
|
||||||
|
Während der Code für sehr einfache Bewegungen funktioniert, wollen wir normalerweise nicht, dass unsere Funktionen entscheiden, wie lange wir vorwärts fahren. Vielleicht müssen wir manchmal vier Sekunden vorwärts fahren, und manchmal nur eine Sekunde.
|
||||||
|
|
||||||
|
Nehmen wir an, wir wollen vier Sekunden vorwärts fahren. Wir wissen, dass ``driveForward()`` den Roboter zwei Sekunden vorwärts bewegen wird. Also können wir die Funktion einfach zwei Mal aufrufen!
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
def driveForward():
|
||||||
|
Motor.power(0, -100)
|
||||||
|
Motor.power(3, 100)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
driveForward()
|
||||||
|
driveForward()
|
||||||
|
|
||||||
|
Was aber, wenn wir uns nur eine Sekunde vorwärts bewegen wollen? Oder vielleicht drei Sekunden? Mit der Funktion ``driveForward()`` können wir das im Moment nicht machen.
|
||||||
|
|
||||||
|
Stattdessen werden wir die Funktion so umschreiben, dass sie einen Parameter akzeptiert, der die Zeit angibt.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
def driveForward(seconds):
|
||||||
|
Motor.power(0, -100)
|
||||||
|
Motor.power(3, 100)
|
||||||
|
time.sleep(seconds)
|
||||||
|
|
||||||
|
driveForward(3)
|
||||||
|
|
||||||
|
Und mit dieser neuen Funktion können wir drei Sekunden lang vorwärts fahren.
|
||||||
|
Wie funktioniert das nun?
|
||||||
|
|
||||||
|
In ``Zeile 4`` definieren wir die Funktion ``driveForward`` und sagen, dass sie einen Parameter ``seconds`` benötigt. Dieser Parameter ist im Grunde eine Variable, die wir uns zum Zeitpunkt der Definition wie einen Platzhalter vorstellen können. Wenn wir die Funktion definieren, wissen wir noch nicht, welchen Wert ``seconds`` haben wird.
|
||||||
|
|
||||||
|
Später in ``Zeile 9``, wenn wir die Funktion aufrufen, übergeben wir den Wert ``3`` an die Funktion und unser Platzhalter ``seconds`` wird den Wert ``3`` haben. Der Roboter wird also drei Sekunden vorwärts fahren.
|
||||||
|
|
||||||
|
Vielleicht wollen wir auch, dass der Roboter mit verschiedenen Geschwindigkeiten fahren kann. Wir können also einen weiteren Parameter mit dem Namen ``speed`` anlegen. Dann werden wir ein Programm schreiben, das den Roboter drei Sekunden mit voller Geschwindigkeit und dann fünf Sekunden mit halber Geschwindigkeit fahren lässt.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
def driveForward(seconds, speed):
|
||||||
|
Motor.power(0, -speed)
|
||||||
|
Motor.power(3, speed)
|
||||||
|
time.sleep(seconds)
|
||||||
|
|
||||||
|
driveForward(3, 100)
|
||||||
|
driveForward(5, 50)
|
||||||
|
|
||||||
|
In ``Zeile 9`` wird der Platzhalter ``seconds`` auf ``3`` und die ``Geschwindigkeit`` auf ``100`` gesetzt.
|
||||||
|
In ``Zeile 10`` wird der Platzhalter ``seconds`` auf ``5`` und die ``Geschwindigkeit`` auf ``50`` gesetzt.
|
||||||
|
|
||||||
|
**Bewährte Praktiken**
|
||||||
|
Nun werden wir uns einige weitere Optimierungen und bewährte Verfahren ansehen.
|
||||||
|
|
||||||
|
**1. Wir sollten den Schlafbefehl nicht in die Fahrfunktion einbauen.**
|
||||||
|
|
||||||
|
Wir haben das bis jetzt getan, um ein Gefühl dafür zu bekommen, wie Funktionen funktionieren, und der Einfachheit halber. Später, wenn Sie anfangen, komplexere Programme zu schreiben, sollten Sie dies vermeiden.
|
||||||
|
|
||||||
|
Das Beispiel von oben, in dem wir vorwärts und rückwärts gefahren sind und zwei Sekunden gewartet haben, sollte also wie folgt aussehen:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
|
||||||
|
def driveForward(speed):
|
||||||
|
Motor.power(0, -speed)
|
||||||
|
Motor.power(3, speed)
|
||||||
|
|
||||||
|
def driveBackward(speed):
|
||||||
|
Motor.power(0, speed)
|
||||||
|
Motor.power(3, -speed)
|
||||||
|
|
||||||
|
def stopMotors():
|
||||||
|
Motor.power(0, 0)
|
||||||
|
Motor.power(3, 0)
|
||||||
|
|
||||||
|
driveForward(100) # Set the motors to forward
|
||||||
|
time.sleep(2) # Let the robot drive for 2 seconds
|
||||||
|
stopMotors() # Now stop the robot
|
||||||
|
|
||||||
|
time.sleep(2) # Wait another 2 seconds, robot is not moving
|
||||||
|
|
||||||
|
driveBackward(100) # Now set the motors to a backwards speed
|
||||||
|
time.sleep(2) # Let the robot continue driving for 2 seconds
|
||||||
|
stopMotors() # And finally stop it again
|
||||||
|
|
||||||
|
**Warum ist das so wichtig?**
|
||||||
|
|
||||||
|
Normalerweise schlafen wir nicht sehr viel und führen in dieser Zeit andere Verarbeitungen durch. Zum Beispiel könnten wir ein Bild von der Kamera verarbeiten oder die IR-Sensoren auslesen. Wenn wir also eine Funktion wie ``driveForward()`` aufrufen, können wir davon ausgehen, dass sie im Hintergrund abläuft und wir andere Aufgaben erledigen, während sich der Roboter bewegt, anstatt nur darauf zu warten, dass er fertig wird.
|
||||||
|
|
||||||
|
**2. Fahren Sie nicht zu langsam.**
|
||||||
|
|
||||||
|
Wenn du die Fahrgeschwindigkeit auf eine sehr kleine Zahl einstellst, kann es sein, dass sich der Roboter gar nicht mehr bewegt, weil die Motoren eine bestimmte Menge an Energie benötigen, um den Roboter überhaupt zu bewegen.
|
||||||
|
|
||||||
|
**3. Fahren Sie nicht zu schnell.**
|
||||||
|
|
||||||
|
Wenn du die Fahrgeschwindigkeit auf eine sehr hohe Zahl einstellst (z. B. ``100``), könnte dein Roboter zu schnell für seine Sensoren sein. Dies wird später wichtig sein, wenn wir versuchen, eine schwarze Linie zu erkennen, aber zu schnell über sie fahren.
|
70
docs/source/gettingStarted/thridProgram.rst
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
Mein drittes Programm
|
||||||
|
#####################
|
||||||
|
|
||||||
|
Der offizielle compAIR-Bot ist mit einer Reihe von Sensoren ausgestattet. Die wichtigsten sind die Infrarotsensoren und -sender, die an der Vorderseite des Roboters angebracht sind. Insgesamt gibt es fünf IR-Sensoren.
|
||||||
|
|
||||||
|
Um loszulegen, muss man zunächst das entsprechende Modul wie folgt importieren:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
from compLib.IRSensor import IRSensor
|
||||||
|
|
||||||
|
|irSensor|
|
||||||
|
|
||||||
|
|
||||||
|
Wie im obigen Diagramm zu sehen ist, verfügt jeder Sensor auch über einen entsprechenden IR-Sender / Emitter. Dieser Sender kann mit ``IRSensor.set(port, enable)`` aktiviert werden.
|
||||||
|
|
||||||
|
Schalten wir nun alle fünf Sender ein:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
from compLib.IRSensor import IRSensor
|
||||||
|
|
||||||
|
IRSensor.enable()
|
||||||
|
|
||||||
|
Diese fünf verschiedenen Sensoren befinden sich an der Vorderseite des Roboters und sind wichtig, um schwarze Linien zu erkennen.
|
||||||
|
|
||||||
|
Es ist sehr einfach, den Wert der Sensoren abzulesen:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
from compLib.IRSensor import IRSensor
|
||||||
|
|
||||||
|
IRSensor.enable()
|
||||||
|
|
||||||
|
if IRSensor.read_all()[0] > 500:
|
||||||
|
print("high")
|
||||||
|
else:
|
||||||
|
print("low")
|
||||||
|
|
||||||
|
**Erkennen einer schwarzen Linie**
|
||||||
|
Um den IR-Sensor zu testen, kannst du deinen Roboter auf eine schwarze Linie stellen. Der Sensor in der Mitte sollte auf der schwarzen Linie liegen.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
from compLib.IRSensor import IRSensor
|
||||||
|
|
||||||
|
IRSensor.enable()
|
||||||
|
COLOR_BREAK = 900
|
||||||
|
|
||||||
|
if IRSensor.read_all()[2] > COLOR_BREAK:
|
||||||
|
print("Robot is standing on a black line")
|
||||||
|
else:
|
||||||
|
print("Robot is NOT standing on a black line")
|
||||||
|
|
||||||
|
Wenn das Programm ausgeführt wird, zeigt es an, dass der Roboter auf einer schwarzen Linie steht, wenn sich der mittlere IR-Sensor des Roboters über einer schwarzen Linie befindet, und es zeigt an, dass der Roboter NICHT auf einer schwarzen Linie steht, wenn sich der mittlere IR-Sensor nicht über einer Linie befindet.
|
||||||
|
|
||||||
|
| In ``Zeile 1`` importieren wir das ``IRSensor``-Modul, das zur Kommunikation mit dem IR-Sensor-Board verwendet werden kann.
|
||||||
|
| In ``Zeile 3`` wird der Sensor mit der Nummer ``3`` aktiviert. Wenn wir einen Sensor nicht aktivieren, können wir ihn nicht in unserem Programm verwenden.
|
||||||
|
| In ``Zeile 4`` stellen wir einen Farbschwellenwert von ``900`` ein, mit dem wir später prüfen werden, ob der Sensorwert unter oder über diesem Schwellenwert liegt. Unterhalb bedeutet, dass sich eine helle Farbe unter dem IR-Sensor befindet und ein höherer Wert als ``900`` bedeutet, dass sich eine dunkle Farbe unter dem IR-Sensor befindet.
|
||||||
|
|
||||||
|
| In ``Zeile 6`` lesen wir den Sensor Nummer ``2`` aus und prüfen, ob der Wert über dem von uns definierten Schwellenwert von ``900`` liegt. Wenn das der Fall ist, hat der IR-Sensor eine schwarze Linie erkannt.
|
||||||
|
|
||||||
|
Wir werden nun das Programm so ändern, dass es alle ``0.1`` Sekunden prüft, ob sich eine schwarze Linie unter dem Roboter befindet, und wenn dies der Fall ist, eine Meldung ausgibt.
|
||||||
|
|
||||||
|
|
||||||
|
.. |irSensor| image:: images/07_irSensor.webp
|
13
docs/source/gettingStarted/update.rst
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Software Updaten
|
||||||
|
#################
|
||||||
|
|
||||||
|
Da wir die ``compLib``, und die andere Software, welche auf dem Roboter läuft, laufend weiterentwickeln, solltet ihr immer wieder euren Roboter auf die neuste Version updaten. Dazu müsst ihr einfach den Roboter mit dem Internet verbinden und dann diesen Befehl in der Kommandozeile des Roboters eingeben:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sudo apt update && sudo apt upgrade
|
||||||
|
|
||||||
|
Am einfachsten kann das über die Webseite gemacht werden, auf der ihr auch euren Code schreibt. Dazu müsst ihr einfach nur das Terminal (= Konsole) öffnen, dann den Befehl dort hineinkopieren und Enter drücken.
|
||||||
|
|updatePic|
|
||||||
|
|
||||||
|
.. |updatePic| image:: images/09_update.png
|
86
docs/source/gettingStarted/wifi.rst
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
.. _gettingStarted_wifi:
|
||||||
|
|
||||||
|
WLAN-Verbindung herstellen
|
||||||
|
##########################
|
||||||
|
|
||||||
|
Schritt für Schritt - macOS
|
||||||
|
---------------------------
|
||||||
|
1. SD-Karte aus dem Raspberry Pi bzw. Roboter entnehmen.
|
||||||
|
2. Einstecken der SD-Karte in den Computer
|
||||||
|
3. Öffnen der SD-Karte mit dem Namen "boot" |bootImage|
|
||||||
|
4. Generieren des PSK auf `https://www.wireshark.org/tools/wpa-psk.html <https://www.wireshark.org/tools/wpa-psk.html>`_ |pskImage|
|
||||||
|
5. Öffnen der Datei "wpa_supplicant.conf" auf der SD-Karte
|
||||||
|
6. Einfügen der Konfiguration. Dabei muss die SSID und der vorher generierte PSK eingesetzt werden ::
|
||||||
|
|
||||||
|
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||||
|
update_config=1
|
||||||
|
country=AT
|
||||||
|
|
||||||
|
network={
|
||||||
|
ssid="EinTollerNameFürDasWlan"
|
||||||
|
psk="98117b165a48f25cbe36f288ddf597729a40feeea93054c19bfa8e5eab238541"
|
||||||
|
}
|
||||||
|
|
||||||
|
7. Speichern, Auswerfen und wieder in den Raspberry Pi einbauen
|
||||||
|
8. Starten des Roboters
|
||||||
|
9. Die IP-Adresse sollte nun am Roboter angezeigt werden
|
||||||
|
|
||||||
|
.. |bootImage| image:: images/01_boot.png
|
||||||
|
.. |pskImage| image:: images/02_psk.png
|
||||||
|
|
||||||
|
Weitere Informationen
|
||||||
|
---------------------
|
||||||
|
Die "wpa_supplicant.conf" Datei wird beim Start des Rpasberry Pi automatisch an den richtigen Ort kopiert, damit sich der Roboter zum Wlan verbindet.
|
||||||
|
Eine genauere Anleitung wird vom Hersteller des Raspberry Pi `hier <https://www.raspberrypi.com/documentation/computers/configuration.html#configuring-networking-2>`_ bereitgestellt.
|
||||||
|
|
||||||
|
Windows......
|
||||||
|
-------------
|
||||||
|
Je nach Betriebssystem und Editor, mit dem Sie die Datei erstellen, könnte die Datei falsche Zeilenumbrüche oder eine falsche Dateierweiterung haben; stellen Sie also sicher, dass Sie einen Editor verwenden, der dies berücksichtigt. Linux erwartet das Zeilenumbruchzeichen LF (Line Feed).
|
||||||
|
Beispielsweise kann `Notepad++ <https://notepad-plus-plus.org/downloads/>`_ verwendet werden, um die Datei richtig zu speichern.
|
||||||
|
|notepadImage|
|
||||||
|
|
||||||
|
.. |notepadImage| image:: images/08_notepad.png
|
||||||
|
|
||||||
|
|
||||||
|
Fehlerbehandlung
|
||||||
|
----------------
|
||||||
|
Sollte es dazu kommen, dass der Roboter nicht automatisch die Verbindung mit dem Netzwerk herstellt, kann eine Kabelgebundene Verbindung zur Diagnose von Fehlern genutzt werden.
|
||||||
|
Dabei wird automatisch die IP-Adresse der Verbindung "eth" am Roboter angezeigt. Nach der erfolgreichen Verbindung zum Roboter mittels SSH kann die "wpa_cli" zur Fehlerbehandlung verwendet werden:
|
||||||
|
::
|
||||||
|
|
||||||
|
> wpa_cli
|
||||||
|
wpa_cli v2.9
|
||||||
|
Copyright (c) 2004-2019, Jouni Malinen <j@w1.fi> and contributors
|
||||||
|
|
||||||
|
This software may be distributed under the terms of the BSD license.
|
||||||
|
See README for more details.
|
||||||
|
|
||||||
|
|
||||||
|
Selected interface 'p2p-dev-wlan0'
|
||||||
|
|
||||||
|
Interactive mode
|
||||||
|
|
||||||
|
> interface wlan0
|
||||||
|
Connected to interface 'wlan0.
|
||||||
|
> scan
|
||||||
|
OK
|
||||||
|
<3>CTRL-EVENT-SCAN-STARTED
|
||||||
|
<3>CTRL-EVENT-SCAN-RESULTS
|
||||||
|
> scan_result
|
||||||
|
bssid / frequency / signal level / flags / ssid
|
||||||
|
68:02:b8:0c:d7:47 2462 -66 [WPA2-PSK-CCMP][ESS] WG
|
||||||
|
68:02:b8:0c:d7:40 5220 -63 [WPA2-PSK-CCMP][ESS] WG
|
||||||
|
34:2c:c4:da:dd:b9 5200 -65 [WPA-PSK-TKIP][WPA2-PSK-CCMP][WPS][ESS] WLAN10573403
|
||||||
|
98:da:c4:e5:21:d0 2437 -57 [WPA2-PSK-CCMP][ESS] WG
|
||||||
|
34:2c:c4:da:dd:c6 2412 -52 [WPA-PSK-][WPA2-PSK-CCMP+TKIP][WPS][ESS] WLAN10573403
|
||||||
|
20:83:f8:07:5b:90 2467 -67 [WPA2-PSK-CCMP][WPS][ESS] A1-075b8c
|
||||||
|
7c:39:53:94:49:82 5280 -77 [WPA2-PSK-CCMP][WPS][ESS] A1-944980-5G
|
||||||
|
7c:39:53:94:49:81 2427 -68 [WPA2-PSK-CCMP][WPS][ESS] A1-944980
|
||||||
|
90:fd:73:ac:d3:27 2452 -72 [WPA2-PSK-CCMP][WPS][ESS] Drei_H288A_24G_eKy5
|
||||||
|
50:e0:39:3c:e5:80 5180 -82 [WPA2-PSK-CCMP][WPS][ESS] A1-393CE57F
|
||||||
|
90:fd:73:ac:d3:28 5500 -83 [WPA2-PSK-CCMP][WPS][ESS] Drei_H288A_5G_eKy5
|
||||||
|
68:02:b8:41:42:f9 5180 -84 [WPA-PSK-TKIP][WPA2-PSK-CCMP][WPS][ESS] WLAN18792472
|
||||||
|
34:2c:c4:30:3c:65 5180 -89 [WPA-PSK-TKIP][WPA2-PSK-CCMP][WPS][ESS] witt
|
||||||
|
fa:0d:ac:d3:40:d4 2467 -80 [WPA2-PSK-CCMP][WPS][ESS][P2P] DIRECT-d4-HP M28 LaserJet
|
||||||
|
0e:84:dc:14:ac:27 2467 -85 [WPA2-PSK-CCMP][WPS][ESS][P2P] DIRECT-wo-BRAVIA
|
||||||
|
>
|
189
docs/source/images/compair-logo-white.svg
Normal file
After Width: | Height: | Size: 188 KiB |
|
@ -1,21 +1,20 @@
|
||||||
Robo4you Competition Library
|
Dokumentation des Roboters
|
||||||
#############################
|
##########################
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
|
|
||||||
|
Inhalt
|
||||||
CompLib is the library used in the robo4you competition 2021.
|
******
|
||||||
|
|
||||||
|
|
||||||
Contents
|
|
||||||
*********
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 5
|
:maxdepth: 1
|
||||||
:glob:
|
:glob:
|
||||||
|
|
||||||
self
|
gettingStarted/index.rst
|
||||||
|
software/installation.rst
|
||||||
|
faq.rst
|
||||||
other/usage
|
other/usage
|
||||||
lib/*
|
lib/index.rst
|
||||||
|
other/hardware.rst
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
.. _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
|
|
||||||
|
|
||||||
park, code = Seeding.get_park()
|
|
||||||
if code == 403:
|
|
||||||
print(f"I am not in the correct zone to make that request!")
|
|
||||||
else:
|
|
||||||
park = park["id"]
|
|
||||||
print(f"I should move to parking position: {park}")
|
|
||||||
|
|
||||||
if park == 0:
|
|
||||||
print(f"I can't move to this position yet :(")
|
|
||||||
elif park == 1:
|
|
||||||
print(f"Moving to position 1!")
|
|
||||||
# drive to parking position using Motors module...
|
|
||||||
print(f"Now hopefully at position 1")
|
|
||||||
# drive back using Motors module...
|
|
||||||
elif park == 2:
|
|
||||||
# do something similar to park == 1..
|
|
||||||
elif park == 3:
|
|
||||||
# do something similar to park == 1..
|
|
||||||
|
|
||||||
success = Seeding.pay_park()
|
|
||||||
# check which code the api returned
|
|
||||||
if success == 204:
|
|
||||||
print(f"We scored some points!")
|
|
||||||
else:
|
|
||||||
print(f"We failed :(")
|
|
||||||
|
|
||||||
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}")
|
|
|
@ -1,83 +0,0 @@
|
||||||
.. _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.
|
|
|
@ -1,23 +0,0 @@
|
||||||
.. _lib_battery:
|
|
||||||
|
|
||||||
Battery
|
|
||||||
********
|
|
||||||
|
|
||||||
Class Documentation
|
|
||||||
====================
|
|
||||||
|
|
||||||
.. autoclass:: compLib.Battery.Battery
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
Examples
|
|
||||||
=========
|
|
||||||
|
|
||||||
Printing percentage
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from compLib.Battery import Battery
|
|
||||||
|
|
||||||
print(Battery.percent())
|
|
|
@ -1,25 +0,0 @@
|
||||||
.. _lib_buzzer:
|
|
||||||
|
|
||||||
Buzzer
|
|
||||||
*******
|
|
||||||
|
|
||||||
Class Documentation
|
|
||||||
====================
|
|
||||||
|
|
||||||
.. autoclass:: compLib.Buzzer.Buzzer
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Examples
|
|
||||||
=========
|
|
||||||
|
|
||||||
Turning buzzer on and off
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import time
|
|
||||||
from compLib.Buzzer import Buzzer
|
|
||||||
|
|
||||||
Buzzer.set(True)
|
|
||||||
time.sleep(1)
|
|
||||||
Buzzer.set(False)
|
|
|
@ -1,22 +0,0 @@
|
||||||
.. _lib_irsensor:
|
|
||||||
|
|
||||||
Infrared Sensor
|
|
||||||
****************
|
|
||||||
|
|
||||||
.. autoclass:: compLib.IRSensor.IRSensor
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Examples
|
|
||||||
=========
|
|
||||||
|
|
||||||
Testing analog sensors
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from compLib import IRSensor
|
|
||||||
|
|
||||||
ir = IRSensor.IRSensor()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
print ("left: {} middle: {} right: {}".format(ir.bottom_left_analog(), ir.bottom_middle_analog(), ir.bottom_right_analog()))
|
|
|
@ -1,22 +0,0 @@
|
||||||
.. _lib_irwrapper:
|
|
||||||
|
|
||||||
Infrared Wrapper
|
|
||||||
****************
|
|
||||||
|
|
||||||
.. autoclass:: compLib.IRWrapper.IRWrapper
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Examples
|
|
||||||
=========
|
|
||||||
|
|
||||||
Calibrating analog sensors
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from compLib import IRWrapper
|
|
||||||
|
|
||||||
ir = IRWrapper.IRWrapper()
|
|
||||||
|
|
||||||
ir.calibrate()
|
|
||||||
print("left {} middle {} right {}".format(ir.bottom_left_calibrated(), ir.bottom_middle_calibrated(), ir.bottom_right_calibrated()))
|
|
|
@ -1,89 +0,0 @@
|
||||||
.. _lib_Linefollower:
|
|
||||||
|
|
||||||
Linefollower Examples
|
|
||||||
*********************
|
|
||||||
|
|
||||||
Simple Linefollower
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
from compLib.IRWrapper import IRWrapper
|
|
||||||
from compLib.Motor import Motor
|
|
||||||
|
|
||||||
STRAIGHT_SPEED = 40.0
|
|
||||||
TURN_SPEED = 40.0
|
|
||||||
|
|
||||||
COLOR_BREAK = 500
|
|
||||||
|
|
||||||
|
|
||||||
def drive(left, right, active_break=False, back_mult=0.0):
|
|
||||||
left = max(min(left, 100), -100)
|
|
||||||
right = max(min(right, 100), -100)
|
|
||||||
|
|
||||||
print("left {} right {}".format(left, right))
|
|
||||||
|
|
||||||
Motor.power(0, left)
|
|
||||||
Motor.power(1, left * back_mult)
|
|
||||||
Motor.power(2, right * back_mult)
|
|
||||||
Motor.power(3, right)
|
|
||||||
|
|
||||||
if active_break:
|
|
||||||
time.sleep(0.001)
|
|
||||||
|
|
||||||
Motor.power(0, 10)
|
|
||||||
Motor.power(1, 10)
|
|
||||||
Motor.power(2, 10)
|
|
||||||
Motor.power(3, 10)
|
|
||||||
|
|
||||||
time.sleep(0.001)
|
|
||||||
|
|
||||||
|
|
||||||
def follow_till_line():
|
|
||||||
while True:
|
|
||||||
print("left: {:.2f} middle: {:.2f} right: {:.2f}".format(IRWrapper.bottom_left_calibrated(),
|
|
||||||
IRWrapper.bottom_middle_calibrated(),
|
|
||||||
IRWrapper.bottom_right_calibrated()))
|
|
||||||
|
|
||||||
left = IRWrapper.bottom_left_calibrated()
|
|
||||||
right = IRWrapper.bottom_right_calibrated()
|
|
||||||
|
|
||||||
if left > COLOR_BREAK:
|
|
||||||
drive(-TURN_SPEED, TURN_SPEED, back_mult=1.25)
|
|
||||||
while IRWrapper.bottom_middle_calibrated() < COLOR_BREAK and \
|
|
||||||
IRWrapper.bottom_right_calibrated() < COLOR_BREAK:
|
|
||||||
pass
|
|
||||||
drive(0, 0)
|
|
||||||
elif right > COLOR_BREAK:
|
|
||||||
drive(TURN_SPEED, -TURN_SPEED, back_mult=1.25)
|
|
||||||
while IRWrapper.bottom_middle_calibrated() < COLOR_BREAK and \
|
|
||||||
IRWrapper.bottom_left_calibrated() < COLOR_BREAK:
|
|
||||||
pass
|
|
||||||
drive(0, 0)
|
|
||||||
else:
|
|
||||||
drive(STRAIGHT_SPEED, STRAIGHT_SPEED, True)
|
|
||||||
|
|
||||||
if IRWrapper.bottom_left_calibrated() > 750 and IRWrapper.bottom_right_calibrated() > 750:
|
|
||||||
break
|
|
||||||
|
|
||||||
print("left: {} middle: {} right: {}".format(IRWrapper.bottom_left_calibrated(),
|
|
||||||
IRWrapper.bottom_middle_calibrated(),
|
|
||||||
IRWrapper.bottom_right_calibrated()))
|
|
||||||
|
|
||||||
drive(0, 0)
|
|
||||||
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Put bot with middle sensor onto black line
|
|
||||||
IRWrapper.calibrate()
|
|
||||||
|
|
||||||
# Initialize Motors, so the bot doesn't speed off instantly
|
|
||||||
drive(1, 1)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Follow line, till the left and right sensor are on black
|
|
||||||
follow_till_line()
|
|
|
@ -1,22 +0,0 @@
|
||||||
.. _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()
|
|
|
@ -1,23 +0,0 @@
|
||||||
.. _lib_motor:
|
|
||||||
|
|
||||||
Motor
|
|
||||||
******
|
|
||||||
|
|
||||||
Class Documentation
|
|
||||||
====================
|
|
||||||
|
|
||||||
.. autoclass:: compLib.Motor.Motor
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Examples
|
|
||||||
=========
|
|
||||||
|
|
||||||
Driving straight (maybe)
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from compLib.Motor import Motor
|
|
||||||
|
|
||||||
for port in range(0, 4):
|
|
||||||
Motor.power(port, 50)
|
|
|
@ -1,7 +0,0 @@
|
||||||
.. _lib_servo:
|
|
||||||
|
|
||||||
Servo
|
|
||||||
******
|
|
||||||
|
|
||||||
.. autoclass:: compLib.Servo.Servo
|
|
||||||
:members:
|
|
|
@ -1,104 +0,0 @@
|
||||||
.. _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
|
|
||||||
|
|
||||||
|
|
13
docs/source/lib/classes/DoubleElimination.rst
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.. _lib_doubleElim:
|
||||||
|
|
||||||
|
Double Elimination
|
||||||
|
*******************
|
||||||
|
|
||||||
|
Dokumentation des Double Elimination Moduls
|
||||||
|
============================================
|
||||||
|
|
||||||
|
.. autoclass:: compLib.DoubleElimination.Position
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: compLib.DoubleElimination.DoubleElim
|
||||||
|
:members:
|
10
docs/source/lib/classes/Encoder.rst
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.. _lib_encoder:
|
||||||
|
|
||||||
|
Encoder
|
||||||
|
*******
|
||||||
|
|
||||||
|
Dokumentation der Klasse
|
||||||
|
========================
|
||||||
|
|
||||||
|
.. autoclass:: compLib.Encoder.Encoder
|
||||||
|
:members:
|
10
docs/source/lib/classes/IRSensor.rst
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.. _lib_irsensor:
|
||||||
|
|
||||||
|
Infrarot Sensoren
|
||||||
|
*****************
|
||||||
|
|
||||||
|
Dokumentation der Klasse
|
||||||
|
========================
|
||||||
|
|
||||||
|
.. autoclass:: compLib.IRSensor.IRSensor
|
||||||
|
:members:
|
50
docs/source/lib/classes/Motor.rst
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
.. _lib_motor:
|
||||||
|
|
||||||
|
Motoren
|
||||||
|
********
|
||||||
|
|
||||||
|
Dokumentation der Klasse
|
||||||
|
========================
|
||||||
|
|
||||||
|
.. autoclass:: compLib.Motor.Motor
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Genauere Informationen
|
||||||
|
======================
|
||||||
|
|
||||||
|
Power vs Speed vs PulseWidth
|
||||||
|
-----------------------------
|
||||||
|
Zur ansteuerung der Motoren kann entweder ``Motor.power(...)``, ``Motor.speed(...)`` oder ``Motor.pulse_width(...)``` verwendet werden.
|
||||||
|
Der Unterschied der 3 Funktionen liegt dabei in der Einheit des 2. Parameters.
|
||||||
|
|
||||||
|
| Bei ``Motor.power()`` wird dabei ein Wert zwischen -100% und 100% der maximalen Geschwindigkeit angegeben.
|
||||||
|
| ``Motor.speed()`` verwendet die Encoder um die Geschwindigkeit der Motoren mittels closed-loop zu steuern. Diese Funktion sollte nur verwendet werden, wenn ``Motor.power()`` nicht zur Ansteuerung ausreicht.
|
||||||
|
| ``Motor.pulse_width()`` stellt die Geschwindigkeit des Motors mittels der Pulsbreite der PWM-Steuerung des Motors ein. Diese Funktion ist so nah an der Hardware wie möglich und sollte auch nur verwendet werden, wenn es einen expliziten Grund dafür gibt.
|
||||||
|
|
||||||
|
Normal vs Multiple
|
||||||
|
------------------
|
||||||
|
Der Aufruf der funktionen kann entweder über ``Motor.power(port, percent)`` oder ``Motor.power((port, percent), (port, percent), ..)`` erfolgen.
|
||||||
|
Der zweite Aufruf ermöglicht dem Entwickler dabei beide Motoren in einem Aufruf anzusteuern und bringt einen kleinen Vorteil in der Leistungsfähigkeit der Software.
|
||||||
|
|
||||||
|
|
||||||
|
Beispiele
|
||||||
|
=========
|
||||||
|
|
||||||
|
Vorwärts fahren
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Mit folgenden Programm drehen sich beide Motoren mit 50% ihrer maximalen Geschwindigkeit.
|
||||||
|
Dabei ist zu beachten, dass ein Motor in die entgegengesetzte Richtung zum aneren Motor gedreht werden muss, da diese spiegelverkehrt montiert sind.
|
||||||
|
|
||||||
|
Zusätzlich ist ein ``time.sleep(5)`` notwendig, welches das Programm für 5 Sekunden pausiert. Diese Pause wird benötigt, da der Roboter automatisch alle Motoren beim Ende des Progammes deaktiviert.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
import time
|
||||||
|
|
||||||
|
Motor.power(0, -50)
|
||||||
|
Motor.power(3, 50)
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
79
docs/source/lib/classes/Opencv.rst
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
.. _lib_camera:
|
||||||
|
|
||||||
|
Camera und OpenCV
|
||||||
|
*******************
|
||||||
|
|
||||||
|
Dokumentation des Camera Moduls
|
||||||
|
================================
|
||||||
|
|
||||||
|
.. autoclass:: compLib.Camera.Marker
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: compLib.Camera.Camera
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Beispiele
|
||||||
|
=========
|
||||||
|
|
||||||
|
Bild Anzeigen
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Das folgende Programm fragt Bilder von der Kamera ab und schickt sie an den Webserver, der im Hintergrund läuft. Der Benutzer kann dann auf die Webseite: http://raspi_ip:9898 gehen und die Ausgabe der Kamera sehen.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from compLib.Camera import *
|
||||||
|
|
||||||
|
camera = Camera()
|
||||||
|
while True:
|
||||||
|
image = camera.get_frame()
|
||||||
|
camera.publish_frame(image)
|
||||||
|
|
||||||
|
ArUco Marker Erkennen
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
In diesem Programm werden die ArUco Marker, die sich am Spielfeld befinden, erkannt. Diese "QR-Code" ähnlichen Marker finden sich in den Logistikzonen und können dazu verwendet werden zu erkennen, wo der Roboter hinfahren sollt etc.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from compLib.Camera import *
|
||||||
|
|
||||||
|
camera = Camera()
|
||||||
|
while True:
|
||||||
|
image = camera.get_frame()
|
||||||
|
|
||||||
|
markers, image = camera.detect_markers_midpoint(image)
|
||||||
|
print(markers)
|
||||||
|
print("-----------------")
|
||||||
|
|
||||||
|
camera.publish_frame(image)
|
||||||
|
|
||||||
|
Hier ist z.B. der ArUco Marker mit der ID 0. Führe das Programm aus und lass den Roboter auf den Bildschirm schauen. Das Programm sollte die 2D Position ausgeben, welcher der ArUco Marker (genauer sein Mittelpunkt) im Camera Bild hat.
|
||||||
|
|
||||||
|
|ArucoExample|
|
||||||
|
|
||||||
|
.. |ArucoExample| image:: images/6x6_1000-0.png
|
||||||
|
|
||||||
|
Um die Positionen zu verarbeiten, muss dann nur noch das "markers" array durchgegangen werden. Das könnte z.B. so gemacht werden:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from compLib.Camera import *
|
||||||
|
|
||||||
|
camera = Camera()
|
||||||
|
while True:
|
||||||
|
image = camera.get_frame()
|
||||||
|
|
||||||
|
markers, image = camera.detect_markers_midpoint(image)
|
||||||
|
print(markers)
|
||||||
|
print("-----------------")
|
||||||
|
|
||||||
|
for marker in markers:
|
||||||
|
print(f"Marker mit der id: {marker.id}")
|
||||||
|
print(f"Ist auf der X Position: {marker.x}")
|
||||||
|
print(f"und auf der Y Position: {marker.y}")
|
||||||
|
print("-----------------")
|
||||||
|
|
||||||
|
camera.publish_frame(image)
|
||||||
|
|
||||||
|
Wichtig ist noch zu beachten, dass die X und Y Positionen ihren Ursprung in der linken oberen Ecke des Bildes haben. D.h. die Position (0,0) ist im oberen linken Bildrand.
|
36
docs/source/lib/classes/Seeding.rst
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
.. _lib_seeding:
|
||||||
|
|
||||||
|
Seeding
|
||||||
|
*******
|
||||||
|
|
||||||
|
Dokumentation des Seeding Moduls
|
||||||
|
================================
|
||||||
|
|
||||||
|
.. autoclass:: compLib.Seeding.Gamestate
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Beispiele
|
||||||
|
----------
|
||||||
|
|
||||||
|
| In ``Zeile 1`` wird das Seeding Modul importiert.
|
||||||
|
| In ``Zeile 2`` definieren wir dann eine Variable, in der wir den "Seed" des Gamestates den wir erstellen wollten speichern.
|
||||||
|
| In ``Zeile 3`` erstellen wir dann einen neuen Gamestate mit dem Seed und speichern ihn in die Variable ``gamestate``.
|
||||||
|
| In ``Zeile 4`` geben wir dann den Gamestate aus, damit wir ihn auf der Konsole ansehen können.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import compLib.Seeding as Seeding
|
||||||
|
seed = 42
|
||||||
|
gamestate = Seeding.Gamestate(seed)
|
||||||
|
print(gamestate)
|
||||||
|
|
||||||
|
In der Ausgabe des Print Statements sehen wir den generierten Gamestate.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
Seed: 42
|
||||||
|
Heu Color: 1
|
||||||
|
Material Pairs: [[3, 0], [2, 3], [0, 2], [1, 2]]
|
||||||
|
Material Zones: [2, 1, 3, 2]
|
||||||
|
Logistic Plan: [12, 13, 12, 13, 10, 11, 13, 10, 13, 12, 11, 10, 11, 13, 10, 11, 12, 11, 12, 10, 12]
|
||||||
|
Logistic Centers: [[0, 3, 1, 1], [1, 0, 2, 2], [1, 2, 0, 2], [3, 0, 2, 0]]
|
BIN
docs/source/lib/classes/images/6x6_1000-0.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 391 KiB |
Before Width: | Height: | Size: 356 KiB After Width: | Height: | Size: 356 KiB |
12
docs/source/lib/index.rst
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
compLib
|
||||||
|
#######
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 5
|
||||||
|
|
||||||
|
classes/Motor
|
||||||
|
classes/Encoder
|
||||||
|
classes/IRSensor
|
||||||
|
classes/Seeding
|
||||||
|
classes/DoubleElimination
|
||||||
|
classes/Opencv
|
64
docs/source/other/hardware.rst
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
.. _other_bardware:
|
||||||
|
|
||||||
|
Hardware
|
||||||
|
########
|
||||||
|
|
||||||
|
Sensorarray
|
||||||
|
***********
|
||||||
|
|
||||||
|
|SensorarrayImage|
|
||||||
|
|
||||||
|
.. |SensorarrayImage| image:: images/Sensorarray.png
|
||||||
|
|
||||||
|
Specs V4
|
||||||
|
--------
|
||||||
|
|
||||||
|
| **Processor:** `STM32G030F6P6 <https://mou.sr/3UxW49B>`_ - 32-bit ARM Cortex M0 CPU @ 64 MHz
|
||||||
|
| **I/O:** 1x I2C, 1x SWD
|
||||||
|
| **Sensors:** 5x `QRE1113GR <https://mou.sr/3TWGYdI>`_
|
||||||
|
|
||||||
|
Specs V2
|
||||||
|
--------
|
||||||
|
|
||||||
|
| **Processor:** `ATMEGA328P-AU <https://mou.sr/3FxhPC5>`_ - 8-bit CPU @ 16 MHz
|
||||||
|
| **I/O:** 1x I2C, 1x UART, 1x ISP
|
||||||
|
| **Sensors:** 5x `QRE1113GR <https://mou.sr/3TWGYdI>`_
|
||||||
|
|
||||||
|
Details
|
||||||
|
-------
|
||||||
|
|
||||||
|
Das Sensorarray wird verwendet um Linienen vor dem Roboter zu erkennen. Es agiert als I2C Slave und muss dementsprechend aktiv gepollt werden.
|
||||||
|
Zusätzlich besteht die möglichkeit alle Emitter zu deaktiviern um einen eventuellen Messfehler durch Sonneneinstralung oder andere Störquellen zu erkennen.
|
||||||
|
|
||||||
|
Version 4 unterscheidet sich zu Version 2 im Mikroprozessor, da es zu Lieferengpässen des ATMEGA gekommen ist.
|
||||||
|
Zusätzlich wurde die möglichkeit alle Emitter einzeln an bzw. auszuschalten entfernt, da diese keinen signifikanten Mehrwert brachte.
|
||||||
|
|
||||||
|
Motorboard
|
||||||
|
**********
|
||||||
|
|
||||||
|
|MainboardImage|
|
||||||
|
|
||||||
|
.. |MainboardImage| image:: images/Mainboard.png
|
||||||
|
|
||||||
|
Specs
|
||||||
|
-----
|
||||||
|
**Motor-Treiber:** `LV8548MC-AH <https://mou.sr/3TXbFzu>`_
|
||||||
|
|
||||||
|
Details
|
||||||
|
-------
|
||||||
|
Das Motorboard kann an einen der 4 Ports am Roboter angesteckt werden und ermöglicht das Ansteuern von Motoren und auslesen von Encodern.
|
||||||
|
|
||||||
|
Mainboard
|
||||||
|
*********
|
||||||
|
|
||||||
|
Specs
|
||||||
|
-----
|
||||||
|
|
||||||
|
| **Processor:** `STM32L051C8T6TR <https://mou.sr/3fuaAQv>`_ - 32-bit ARM Cortex M0 @ 32MHz
|
||||||
|
| **I/O:** 4x I2C (3x Bus 1, 1x Bus 2), 1x 40 Pin GPIO Header, 2x SPI (Verbunden mit GPIO), 4x Motor-/Servo-connector, 1x SWD, 1x USB-C
|
||||||
|
|
||||||
|
Details
|
||||||
|
-------
|
||||||
|
|
||||||
|
Das Mainboard wird auf den GPIO-Header eines Raspberry Pi gesteckt und ermöglicht die Steuerung eines Roboters mittels 4 Motor- bzw. Servo-Ports. Der RaspberryPi kommuniziert dabei mittels SPI mit dem Mainboard und steuert die einzelnen Sensoren oder Module an.
|
||||||
|
Zusätzlich befinden sich auf der Unterseite des Mainboards Lötstellen, welche direkt mit der Stromversorgung der Motoren verbunden sind und geben so die möglichkeit Motoren mit mehr als 5V anzusteuern.
|
BIN
docs/source/other/images/Mainboard.png
Normal file
After Width: | Height: | Size: 278 KiB |
BIN
docs/source/other/images/Sensorarray.png
Normal file
After Width: | Height: | Size: 254 KiB |
|
@ -1,7 +1,10 @@
|
||||||
.. _other_usage:
|
.. _other_usage:
|
||||||
|
|
||||||
Usage
|
Beispiele
|
||||||
######
|
#########
|
||||||
|
|
||||||
|
Vorwärts und rückwärts fahren
|
||||||
|
*****************************
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -9,12 +12,13 @@ Usage
|
||||||
from compLib.Motor import *
|
from compLib.Motor import *
|
||||||
|
|
||||||
def forward():
|
def forward():
|
||||||
for port in range(0, 4):
|
Motor.power(0, -30)
|
||||||
Motor.power(port, 30);
|
Motor.power(3, 30)
|
||||||
|
|
||||||
|
|
||||||
def backward():
|
def backward():
|
||||||
for port in range(0, 4):
|
Motor.power(0, 30)
|
||||||
Motor.power(port, -30);
|
Motor.power(3, -30)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("hallo ich bin ein roboter beep buup")
|
print("hallo ich bin ein roboter beep buup")
|
||||||
|
@ -25,4 +29,138 @@ Usage
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
Eine Linie verfolgen
|
||||||
|
********************
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
from compLib.Encoder import Encoder
|
||||||
|
from compLib.IRSensor import IRSensor
|
||||||
|
|
||||||
|
COLOR_BREAK = 850
|
||||||
|
DRIVE_SPEED = 35
|
||||||
|
|
||||||
|
IRSensor.enable()
|
||||||
|
|
||||||
|
def drive(left, right):
|
||||||
|
right *= -1
|
||||||
|
Motor.multiple_power((0, right), (3, left))
|
||||||
|
print(f"{left} {right}")
|
||||||
|
|
||||||
|
def follow():
|
||||||
|
while True:
|
||||||
|
sensors = IRSensor.read_all()
|
||||||
|
|
||||||
|
if sensors[0] > COLOR_BREAK:
|
||||||
|
# turn left
|
||||||
|
drive(-DRIVE_SPEED, DRIVE_SPEED)
|
||||||
|
elif sensors[4] > COLOR_BREAK:
|
||||||
|
# turn right
|
||||||
|
drive(DRIVE_SPEED, -DRIVE_SPEED)
|
||||||
|
else:
|
||||||
|
# straight
|
||||||
|
drive(DRIVE_SPEED, DRIVE_SPEED)
|
||||||
|
|
||||||
|
if sensors[0] > COLOR_BREAK and sensors[4] > COLOR_BREAK:
|
||||||
|
break
|
||||||
|
|
||||||
|
drive(0, 0)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
follow()
|
||||||
|
|
||||||
|
drive(DRIVE_SPEED, DRIVE_SPEED)
|
||||||
|
time.sleep(0.5)
|
||||||
|
follow()
|
||||||
|
|
||||||
|
drive(DRIVE_SPEED, DRIVE_SPEED)
|
||||||
|
time.sleep(0.5)
|
||||||
|
follow()
|
||||||
|
|
||||||
|
drive(DRIVE_SPEED, DRIVE_SPEED)
|
||||||
|
time.sleep(0.5)
|
||||||
|
follow()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
Funktionalität des Roboters überprüfen
|
||||||
|
**************************************
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import time
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
from compLib.Encoder import Encoder
|
||||||
|
from compLib.IRSensor import IRSensor
|
||||||
|
|
||||||
|
|
||||||
|
def testIR():
|
||||||
|
print("Enabling Infrared Sensor")
|
||||||
|
IRSensor.enable()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print("Writing sensor values...")
|
||||||
|
for i in range(0, 50):
|
||||||
|
print(IRSensor.read_all())
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
print("Disabling Infrared Sensor")
|
||||||
|
IRSensor.disable()
|
||||||
|
|
||||||
|
def testEncoders():
|
||||||
|
Motor.multiple_pulse_width((0, 50), (3, -50))
|
||||||
|
|
||||||
|
print("Writing encoder positions...")
|
||||||
|
for i in range(0, 50):
|
||||||
|
print(Encoder.read_all_positions())
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
print("Writing encoder velocities...")
|
||||||
|
for i in range(0, 50):
|
||||||
|
print(Encoder.read_all_velocities())
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
Motor.multiple_pulse_width((0, 0), (3, 0))
|
||||||
|
|
||||||
|
|
||||||
|
def testMotors():
|
||||||
|
print("Setting pulse_with")
|
||||||
|
Motor.multiple_pulse_width((0, 50), (3, -50))
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
print("Setting power")
|
||||||
|
Motor.multiple_power((0, 50), (3, -50))
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
print("Setting pulse_with")
|
||||||
|
Motor.multiple_speed((0, 5), (3, -5))
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
for i in range(0, 100):
|
||||||
|
Motor.multiple_power((0, i), (3, -i))
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Make sure robot is turned on it's back!")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("----------------- Testing Infrared Sensor -----------------")
|
||||||
|
testIR()
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("----------------- Testing Encoder -----------------")
|
||||||
|
testEncoders()
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("----------------- Testing Motors -----------------")
|
||||||
|
testMotors()
|
59
docs/source/software/installation.rst
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
.. _software_installation:
|
||||||
|
|
||||||
|
Installationsanweisungen
|
||||||
|
########################
|
||||||
|
|
||||||
|
Diese Anleitung dient dazu die Software auf dem Roboter neu aufzusetzen.
|
||||||
|
|
||||||
|
**Im normalen Gebraucht sollte dies jedoch nicht notwendig sein.**
|
||||||
|
|
||||||
|
Betriebssystem aufsetzen
|
||||||
|
========================
|
||||||
|
|
||||||
|
Als Basis wird für den Roboter Raspberry Pi OS (64-bit) verwendet. Das 32-Bit Betriebssystem wird nicht unterstützt, da die Software-Komponenten nur für aarch64 bzw. arm64/v8 kompiliert werden.
|
||||||
|
Genauere Informationen sind `hier <https://www.raspberrypi.com/software/operating-systems/>`_ zu finden.
|
||||||
|
|
||||||
|
Bearbeiten der boot-Partition
|
||||||
|
=============================
|
||||||
|
|
||||||
|
1. ``cmdline.txt``
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
console=tty1 root=PARTUUID=21e60f8c-02 rootfstype=ext4 fsck.repair=yes rootwait quiet init=/usr/lib/raspi-config/init_resize.sh
|
||||||
|
|
||||||
|
|
||||||
|
Stellen Sie sicher, dass die folgenden Einstellungen in der ``config.txt`` korrekt gesetzt sind
|
||||||
|
|
||||||
|
2. ``config.txt``
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
# SPI
|
||||||
|
dtparam=spi=on
|
||||||
|
dtoverlay=spi1-3cs
|
||||||
|
|
||||||
|
# Run in 64-bit mode
|
||||||
|
arm_64bit=1
|
||||||
|
|
||||||
|
[all]
|
||||||
|
|
||||||
|
[pi4]
|
||||||
|
# Run as fast as firmware / board allows
|
||||||
|
arm_boost=1
|
||||||
|
|
||||||
|
[all]
|
||||||
|
start_x=1
|
||||||
|
gpu_mem=128
|
||||||
|
|
||||||
|
dtoverlay=pi3-disable-bt
|
||||||
|
enable_uart=1
|
||||||
|
|
||||||
|
3. Erstellen der leeren Datei ``ssh``, damit ssh beim nächsten Start aktiviert wird
|
||||||
|
4. Hinzufügen der ``userconf.txt``
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
compair:$6$eh2pkHm18RgYtwiG$PoeabVCH8llbyIio66OefPGXZ2r2BRI2hPHIdkNTBjmiR0lGXsozGyLx0uViOx3bi998syXjSDXkwt0t3x8Bq.
|
||||||
|
|
||||||
|
5. Wlan Verbindung einrichten
|
34
motorLinearityTest.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from compLib.Motor import Motor
|
||||||
|
from compLib.Encoder import Encoder
|
||||||
|
from compLib.IRSensor import IRSensor
|
||||||
|
from compLib.CompLibClient import CompLibClient
|
||||||
|
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
|
||||||
|
CompLibClient.use_unix_socket()
|
||||||
|
|
||||||
|
sign = 1
|
||||||
|
|
||||||
|
for pwr in (18, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 35, 40, 45, 50, 60, 70, 80 , 90, 100):
|
||||||
|
|
||||||
|
# Motor.multiple_pulse_width((0, pwr * sign), (3, -pwr * sign))
|
||||||
|
Motor.multiple_power((0, pwr * sign), (3, -pwr * sign))
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < 3:
|
||||||
|
Encoder.read_all_velocities()
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
avg = 0.0
|
||||||
|
for i in range(0, 20):
|
||||||
|
vels = Encoder.read_all_velocities()
|
||||||
|
avg += ((abs(vels[0]) + abs(vels[3])) / 2.0)
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
avg = avg / 10
|
||||||
|
|
||||||
|
vel = str(avg).replace(".", ",")
|
||||||
|
print(f"{pwr} {vel}")
|
||||||
|
|
||||||
|
sign *= -1
|
38
postinstall.sh
Normal file → Executable file
|
@ -1,32 +1,22 @@
|
||||||
|
#!/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
|
grep -qxF "apt update" /etc/rc.local
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "adding apt update to rc.local"
|
echo "adding apt update to rc.local"
|
||||||
sed -i "2s/^/apt update\n/" /etc/rc.local
|
sed -i "2s/^/apt update\n/" /etc/rc.local
|
||||||
fi
|
fi
|
||||||
|
|
||||||
install_package() {
|
pip3 install requests flask
|
||||||
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"
|
#echo "Setting up nginx rtmp server"
|
||||||
install_package "requests"
|
#sudo /etc/init.d/nginx start
|
||||||
install_package "flask"
|
|
||||||
install_package "python-logstash-async"
|
|
||||||
install_package "RPi.GPIO"
|
|
||||||
install_package "spidev"
|
|
||||||
|
|
||||||
echo "Setting up opencv4"
|
sudo raspi-config nonint do_legacy 0 || echo "(WARNING) raspi-config not found, cannot enable legacy camera support"
|
||||||
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 'load_module "modules/ngx_rtmp_module.so";'
|
||||||
|
@ -45,10 +35,8 @@ sudo /etc/init.d/nginx start
|
||||||
echo '}'
|
echo '}'
|
||||||
} >| /etc/nginx/nginx.conf
|
} >| /etc/nginx/nginx.conf
|
||||||
|
|
||||||
echo "Starting pigpiod daemon"
|
echo "Stopping nginx rtmp server as its not required anymore"
|
||||||
|
sudo /etc/init.d/nginx stop
|
||||||
sudo systemctl start pigpiod
|
|
||||||
sudo systemctl enable pigpiod
|
|
||||||
|
|
||||||
base64 -d << UPD
|
base64 -d << UPD
|
||||||
CiBfX19fX18gICAgIF9fX19fXyAgICAgX18gICAgX18gICAgIF9fX19fXyAgIF9fICAgICAgICAgX18gICAgIF9fX19fXyAgICAgICAgICAgICAgICAgIAovXCAgX19fXCAgIC9cICBfXyBcICAgL1wgIi0uLyAgXCAgIC9cICA9PSBcIC9cIFwgICAgICAgL1wgXCAgIC9cICA9PSBcICAgICAgICAgICAgICAgICAKXCBcIFxfX19fICBcIFwgXC9cIFwgIFwgXCBcLS4vXCBcICBcIFwgIF8tLyBcIFwgXF9fX18gIFwgXCBcICBcIFwgIF9fPCAgICAgICAgICAgICAgICAgCiBcIFxfX19fX1wgIFwgXF9fX19fXCAgXCBcX1wgXCBcX1wgIFwgXF9cICAgIFwgXF9fX19fXCAgXCBcX1wgIFwgXF9fX19fXCAgICAgICAgICAgICAgIAogIFwvX19fX18vICAgXC9fX19fXy8gICBcL18vICBcL18vICAgXC9fLyAgICAgXC9fX19fXy8gICBcL18vICAgXC9fX19fXy8gICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiBfXyAgX18gICAgIF9fX19fXyAgICAgIF9fX19fXyAgIF9fX19fXyAgICAgICAgX19fX18gICAgIF9fX19fXyAgICAgX19fX19fICAgX19fX19fICAgIAovXCBcL1wgXCAgIC9cICA9PSBcICAgIC9cX18gIF9cIC9cICBfXyBcICAgICAgL1wgIF9fLS4gIC9cICBfXyBcICAgL1xfXyAgX1wgL1wgIF9fX1wgICAKXCBcIFxfXCBcICBcIFwgIF8tLyAgICBcL18vXCBcLyBcIFwgXC9cIFwgICAgIFwgXCBcL1wgXCBcIFwgIF9fIFwgIFwvXy9cIFwvIFwgXCAgX19cICAgCiBcIFxfX19fX1wgIFwgXF9cICAgICAgICAgXCBcX1wgIFwgXF9fX19fXCAgICAgXCBcX19fXy0gIFwgXF9cIFxfXCAgICBcIFxfXCAgXCBcX19fX19cIAogIFwvX19fX18vICAgXC9fLyAgICAgICAgICBcL18vICAgXC9fX19fXy8gICAgICBcL19fX18vICAgXC9fL1wvXy8gICAgIFwvXy8gICBcL19fX19fLyA=
|
CiBfX19fX18gICAgIF9fX19fXyAgICAgX18gICAgX18gICAgIF9fX19fXyAgIF9fICAgICAgICAgX18gICAgIF9fX19fXyAgICAgICAgICAgICAgICAgIAovXCAgX19fXCAgIC9cICBfXyBcICAgL1wgIi0uLyAgXCAgIC9cICA9PSBcIC9cIFwgICAgICAgL1wgXCAgIC9cICA9PSBcICAgICAgICAgICAgICAgICAKXCBcIFxfX19fICBcIFwgXC9cIFwgIFwgXCBcLS4vXCBcICBcIFwgIF8tLyBcIFwgXF9fX18gIFwgXCBcICBcIFwgIF9fPCAgICAgICAgICAgICAgICAgCiBcIFxfX19fX1wgIFwgXF9fX19fXCAgXCBcX1wgXCBcX1wgIFwgXF9cICAgIFwgXF9fX19fXCAgXCBcX1wgIFwgXF9fX19fXCAgICAgICAgICAgICAgIAogIFwvX19fX18vICAgXC9fX19fXy8gICBcL18vICBcL18vICAgXC9fLyAgICAgXC9fX19fXy8gICBcL18vICAgXC9fX19fXy8gICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiBfXyAgX18gICAgIF9fX19fXyAgICAgIF9fX19fXyAgIF9fX19fXyAgICAgICAgX19fX18gICAgIF9fX19fXyAgICAgX19fX19fICAgX19fX19fICAgIAovXCBcL1wgXCAgIC9cICA9PSBcICAgIC9cX18gIF9cIC9cICBfXyBcICAgICAgL1wgIF9fLS4gIC9cICBfXyBcICAgL1xfXyAgX1wgL1wgIF9fX1wgICAKXCBcIFxfXCBcICBcIFwgIF8tLyAgICBcL18vXCBcLyBcIFwgXC9cIFwgICAgIFwgXCBcL1wgXCBcIFwgIF9fIFwgIFwvXy9cIFwvIFwgXCAgX19cICAgCiBcIFxfX19fX1wgIFwgXF9cICAgICAgICAgXCBcX1wgIFwgXF9fX19fXCAgICAgXCBcX19fXy0gIFwgXF9cIFxfXCAgICBcIFxfXCAgXCBcX19fX19cIAogIFwvX19fX18vICAgXC9fLyAgICAgICAgICBcL18vICAgXC9fX19fXy8gICAgICBcL19fX18vICAgXC9fL1wvXy8gICAgIFwvXy8gICBcL19fX19fLyA=
|
||||||
|
|
27
setup.py
|
@ -1,37 +1,24 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/python3
|
||||||
import setuptools
|
import setuptools
|
||||||
import os
|
import os
|
||||||
|
|
||||||
base_dir = os.path.dirname(__file__)
|
print("Using version: {str(os.environ['VERSION'])}")
|
||||||
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(
|
setuptools.setup(
|
||||||
name="complib",
|
name="complib",
|
||||||
version="0.1.5-1",
|
version=str(os.environ.get('VERSION', "")),
|
||||||
author="F-WuTs",
|
author="F-WuTs",
|
||||||
author_email="--",
|
author_email="joel.klimont@comp-air.at",
|
||||||
description="",
|
description="",
|
||||||
summary="Library for the competition",
|
summary="Robot client library for the compAIR competition",
|
||||||
platforms=["any"],
|
platforms=["any"],
|
||||||
long_description=long_description,
|
|
||||||
long_description_content_type="text/markdown",
|
|
||||||
url="https://github.com/F-WuTS/compLIB",
|
url="https://github.com/F-WuTS/compLIB",
|
||||||
packages=setuptools.find_packages(),
|
packages=setuptools.find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.9",
|
||||||
"License :: Other/Proprietary License",
|
"License :: Other/Proprietary License",
|
||||||
"Operating System :: Unix"
|
"Operating System :: Unix"
|
||||||
],
|
],
|
||||||
license="proprietary"#,
|
license="proprietary"
|
||||||
#install_requires=[
|
|
||||||
# "smbus",
|
|
||||||
# "requests",
|
|
||||||
# "python-logstash-async"
|
|
||||||
#]
|
|
||||||
)
|
)
|
||||||
|
|
358
test.py
|
@ -1,285 +1,149 @@
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import unittest
|
import unittest
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# compapi is a dependency for this test
|
import compLib.Seeding as Seeding
|
||||||
try:
|
import compLib.Api as SeedingApi
|
||||||
from compapi import server
|
import compLib.DoubleElimination as De
|
||||||
|
|
||||||
START_SERVER = True
|
|
||||||
except ImportError:
|
|
||||||
print("[!] error could not import server module from compapi, assuming server is running")
|
|
||||||
START_SERVER = False
|
|
||||||
|
|
||||||
from multiprocessing import Process
|
|
||||||
|
|
||||||
from compLib import LogstashLogging
|
|
||||||
from compLib import Api
|
|
||||||
|
|
||||||
|
|
||||||
def change_api_state(park_id: int = 0, in_get_park: bool = False, was_in_park: bool = False,
|
class SeedingTest(unittest.TestCase):
|
||||||
simon_says_ids: list = [0, 0, 0, 0, 0],
|
def test_basic_seed(self):
|
||||||
in_simon_zone: bool = False):
|
gamestate = Seeding.Gamestate(0)
|
||||||
data = {
|
self.assertEqual(gamestate.seed, 0)
|
||||||
"parkId": park_id,
|
self.assertEqual(gamestate.heu_color, 1)
|
||||||
"inGetPark": in_get_park,
|
self.assertEqual(gamestate.get_heuballen(), 1)
|
||||||
"wasInPark": was_in_park,
|
self.assertEqual(gamestate.get_logistic_plan(),
|
||||||
"simonSaysIDs": simon_says_ids,
|
[12, 13, 10, 13, 12, 10, 11, 10, 12, 11, 12, 13, 10, 12, 10, 11, 13, 11, 13, 11, 12])
|
||||||
"inSimonZone": in_simon_zone
|
self.assertEqual(gamestate.get_material_deliveries(), [[3, 1], [0, 3], [3, 1], [3, 1]])
|
||||||
}
|
|
||||||
|
|
||||||
requests.post("http://localhost:5000/api/api/updateState", json=data)
|
|
||||||
|
|
||||||
|
|
||||||
def change_double_elim_api_state(started: bool = False, goal: list = [0, 0], position: list = [0, 0, 0],
|
def util_get_info():
|
||||||
op_position: list = [0, 0, 0], items: list = []):
|
res = requests.get(SeedingApi.CONF_URL + "getInfo")
|
||||||
data = {
|
return json.loads(res.text)
|
||||||
"started": started,
|
|
||||||
"goal": goal,
|
|
||||||
"position": position,
|
|
||||||
"opPosition": op_position,
|
|
||||||
"items": items
|
|
||||||
}
|
|
||||||
|
|
||||||
requests.post("http://localhost:5000/api/api/updateDoubleElimState", json=data)
|
|
||||||
|
|
||||||
|
|
||||||
class TestApiServer(unittest.TestCase):
|
def util_set_seeding():
|
||||||
def test_get_park(self):
|
res = requests.get(SeedingApi.CONF_URL + "setToSeeding")
|
||||||
ret, code = Api.Seeding.get_park()
|
return res.status_code == 200
|
||||||
id = ret["id"]
|
|
||||||
assert type(ret) is dict
|
|
||||||
assert type(id) is int
|
|
||||||
assert id == -1
|
|
||||||
assert type(code) is int
|
|
||||||
assert code == 403
|
|
||||||
|
|
||||||
change_api_state(in_get_park=True)
|
|
||||||
|
|
||||||
ret, code = Api.Seeding.get_park()
|
def util_set_de():
|
||||||
id = ret["id"]
|
res = requests.get(SeedingApi.CONF_URL + "setToDoubleElim")
|
||||||
assert type(ret) is dict
|
return res.status_code == 200
|
||||||
assert type(id) is int
|
|
||||||
assert 0 <= id < 4
|
|
||||||
assert type(code) is int
|
|
||||||
assert code == 200
|
|
||||||
|
|
||||||
def test_pay_park(self):
|
|
||||||
ret = Api.Seeding.pay_park()
|
|
||||||
assert type(ret) is int
|
|
||||||
assert ret == 402
|
|
||||||
|
|
||||||
change_api_state(was_in_park=True)
|
def util_start_match():
|
||||||
|
res = requests.get(SeedingApi.CONF_URL + "startMatch")
|
||||||
|
return res.status_code == 200
|
||||||
|
|
||||||
ret = Api.Seeding.pay_park()
|
|
||||||
assert type(ret) is int
|
|
||||||
assert ret == 402
|
|
||||||
|
|
||||||
change_api_state(was_in_park=True, in_get_park=True)
|
def util_reset_state():
|
||||||
|
res = requests.get(SeedingApi.CONF_URL + "resetState")
|
||||||
|
return res.status_code == 200
|
||||||
|
|
||||||
ret = Api.Seeding.pay_park()
|
|
||||||
assert type(ret) is int
|
|
||||||
assert ret == 204
|
|
||||||
|
|
||||||
def test_simon_says(self):
|
def util_set_seed(seed):
|
||||||
change_api_state(in_simon_zone=False)
|
res = requests.get(SeedingApi.CONF_URL + "resetState", params={"seed": seed})
|
||||||
ret, code = Api.Seeding.simon_says()
|
return res.status_code == 200
|
||||||
id = ret["id"]
|
|
||||||
assert type(ret) is dict
|
|
||||||
assert type(id) is int
|
|
||||||
assert type(code) is int
|
|
||||||
assert code == 403
|
|
||||||
assert id == -1
|
|
||||||
|
|
||||||
got = []
|
|
||||||
while len(got) != 5:
|
|
||||||
change_api_state(in_simon_zone=True)
|
|
||||||
ret, code = Api.Seeding.simon_says()
|
|
||||||
id = ret["id"]
|
|
||||||
assert type(ret) is dict
|
|
||||||
assert type(id) is int
|
|
||||||
assert type(code) is int
|
|
||||||
assert code == 200
|
|
||||||
assert id <= 0 < 4
|
|
||||||
got.append(ret)
|
|
||||||
print(got)
|
|
||||||
|
|
||||||
change_api_state(in_simon_zone=True)
|
class SeedingApiTest(unittest.TestCase):
|
||||||
ret, code = Api.Seeding.simon_says()
|
def test_api_seeding_extensive(self):
|
||||||
id = ret["id"]
|
self.assertTrue(util_set_seeding())
|
||||||
assert type(ret) is dict
|
self.assertTrue(util_get_info()["is_seeding"])
|
||||||
assert type(id) is int
|
for seed in range(0, 256):
|
||||||
assert type(code) is int
|
print(f"Testing seed: {seed}")
|
||||||
assert code == 200
|
gamestate = Seeding.Gamestate(seed)
|
||||||
assert id == -1
|
self.assertTrue(util_set_seed(seed))
|
||||||
|
seeding_api = SeedingApi.Seeding()
|
||||||
|
self.assertEqual(seeding_api.get_heuballen(), gamestate.get_heuballen())
|
||||||
|
self.assertEqual(seeding_api.get_logistic_plan(), gamestate.get_logistic_plan())
|
||||||
|
self.assertEqual(seeding_api.get_material_deliveries(), gamestate.get_material_deliveries())
|
||||||
|
|
||||||
change_api_state(in_simon_zone=False)
|
def test_gamestate(self):
|
||||||
ret, code = Api.Seeding.simon_says()
|
seed = 42
|
||||||
id = ret["id"]
|
gamestate = Seeding.Gamestate(seed)
|
||||||
assert type(ret) is dict
|
print(gamestate)
|
||||||
assert type(id) is int
|
|
||||||
assert type(code) is int
|
|
||||||
assert code == 403
|
|
||||||
assert id == -1
|
|
||||||
|
|
||||||
def test_simon_says_iterations(self):
|
print(gamestate.get_heuballen())
|
||||||
for i in range(0, 5):
|
heu_color = gamestate.get_heuballen()
|
||||||
change_api_state(in_simon_zone=True)
|
if heu_color == 1:
|
||||||
ret, status = Api.Seeding.simon_says()
|
print("Heuballen liegen auf den gelben Linien")
|
||||||
assert type(ret) is dict
|
# TODO: code um die über die gelben Linien zu fahren
|
||||||
assert type(status) is int
|
elif heu_color == 2:
|
||||||
assert type(ret["id"]) is int
|
print("Heuballen liegen auf den blauen Linien")
|
||||||
assert 0 <= ret["id"] < 4
|
# TODO: code um die über die blauen Linien zu fahren
|
||||||
|
|
||||||
# after 4 iterations the api should only return -1 from now
|
materials = gamestate.get_material_deliveries()
|
||||||
for i in range(0, 10):
|
print(materials)
|
||||||
assert Api.Seeding.simon_says()[0]["id"] == -1
|
|
||||||
|
|
||||||
# after api reset this test should work again
|
for material_pair in materials:
|
||||||
self.resetApi()
|
print(f"Der Roboter sollte jetzt die beiden Materialien {material_pair} holen")
|
||||||
|
|
||||||
for i in range(0, 4):
|
for material in material_pair:
|
||||||
change_api_state(in_simon_zone=True)
|
if material == 0:
|
||||||
ret, status = Api.Seeding.simon_says()
|
print(f"Der Roboter sollte jetzt Holz aufnehmen, Zone: {material}")
|
||||||
assert type(ret) is dict
|
# TODO: code um in die Material Zone mit dem Holz zu fahren
|
||||||
assert type(status) is int
|
elif material == 1:
|
||||||
assert type(ret["id"]) is int
|
print(f"Der Roboter sollte jetzt Stahl aufnehmen, Zone: {material}")
|
||||||
assert 0 <= ret["id"] < 4
|
# 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
|
||||||
|
|
||||||
for i in range(0, 10):
|
print("Der Roboter sollte jetzt die beiden Materialien zur Baustelle fahren")
|
||||||
assert Api.Seeding.simon_says()[0]["id"] == -1
|
# TODO: code um zur Baustelle zu fahren
|
||||||
|
|
||||||
def test_simon_says_non_repeat(self):
|
logistic_plan = gamestate.get_logistic_plan()
|
||||||
"""
|
print(logistic_plan)
|
||||||
Note: this test will fail if testing against the local api as the
|
|
||||||
configuration is to return zeros if not otherwise specified.
|
|
||||||
|
|
||||||
Checks if simons says does not send the robot to the
|
for zone in logistic_plan:
|
||||||
same position again.
|
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
|
||||||
|
|
||||||
last_pos, status = Api.Seeding.simon_says()
|
|
||||||
last_pos = last_pos["id"]
|
|
||||||
for i in range(0, 100):
|
|
||||||
change_api_state(in_simon_zone=True)
|
|
||||||
next_pos, status = Api.Seeding.simon_says()
|
|
||||||
next_pos = next_pos["id"]
|
|
||||||
if next_pos == -1:
|
|
||||||
last_pos = -1 # state is reset, so reset here as well
|
|
||||||
self.resetApi()
|
|
||||||
continue
|
|
||||||
assert last_pos != next_pos
|
|
||||||
last_pos = next_pos
|
|
||||||
|
|
||||||
def test_get_position(self):
|
class DeApiTest(unittest.TestCase):
|
||||||
response, status = Api.DoubleElim.get_position()
|
def test_api_de(self):
|
||||||
assert type(status) is int
|
self.assertTrue(util_set_de())
|
||||||
assert status == 503
|
self.assertTrue(util_reset_state())
|
||||||
assert type(response) == Api.Position
|
self.assertFalse(util_get_info()["is_seeding"])
|
||||||
assert 0 == response.x
|
|
||||||
assert 0 == response.y
|
|
||||||
assert 0 == response.degrees
|
|
||||||
|
|
||||||
change_double_elim_api_state(started=True, position=[50, 75, 90])
|
de = De.DoubleElim()
|
||||||
|
self.assertEqual(de.get_pos(), (De.Position(0, 0, -1), 503))
|
||||||
|
self.assertEqual(de.get_opponent(), (De.Position(0, 0, -1), 503))
|
||||||
|
self.assertEqual(de.get_goal(), (De.Position(0, 0, -1), 503))
|
||||||
|
self.assertEqual(de.get_items(), ([], 503))
|
||||||
|
self.assertEqual(de.get_scores(), ({"self": 0, "opponent": 0}, 503))
|
||||||
|
|
||||||
response, status = Api.DoubleElim.get_position()
|
self.assertTrue(util_start_match())
|
||||||
assert type(status) is int
|
self.assertLessEqual(util_get_info()["timeleft"], 120)
|
||||||
assert status == 200
|
|
||||||
assert type(response) == Api.Position
|
|
||||||
assert 50 == response.x
|
|
||||||
assert 75 == response.y
|
|
||||||
assert 90 == response.degrees
|
|
||||||
|
|
||||||
def test_get_opponent(self):
|
self.assertEqual(de.get_pos()[1], 200)
|
||||||
response, status = Api.DoubleElim.get_opponent()
|
self.assertEqual(de.get_opponent()[1], 200)
|
||||||
assert type(status) is int
|
self.assertEqual(de.get_goal()[1], 200)
|
||||||
assert status == 503
|
self.assertEqual(de.get_items()[1], 200)
|
||||||
assert type(response) == Api.Position
|
self.assertEqual(de.get_scores()[1], 200)
|
||||||
assert 0 == response.x
|
|
||||||
assert 0 == response.y
|
|
||||||
assert 0 == response.degrees
|
|
||||||
|
|
||||||
change_double_elim_api_state(started=True, op_position=[50, 75, 90])
|
self.assertTrue(0 <= de.get_pos()[0].x <= 250)
|
||||||
|
self.assertTrue(0 <= de.get_pos()[0].y <= 250)
|
||||||
|
self.assertTrue(0 <= de.get_pos()[0].degrees <= 360)
|
||||||
|
self.assertEqual(de.get_items(), ([], 200))
|
||||||
|
|
||||||
response, status = Api.DoubleElim.get_opponent()
|
self.assertTrue(util_reset_state())
|
||||||
assert type(status) is int
|
|
||||||
assert status == 200
|
|
||||||
assert type(response) == Api.Position
|
|
||||||
assert 50 == response.x
|
|
||||||
assert 75 == response.y
|
|
||||||
assert 90 == response.degrees
|
|
||||||
|
|
||||||
def test_get_goal(self):
|
|
||||||
response, status = Api.DoubleElim.get_goal()
|
|
||||||
assert type(status) is int
|
|
||||||
assert status == 503
|
|
||||||
assert type(response) == Api.Position
|
|
||||||
assert 0 == response.x
|
|
||||||
assert 0 == response.y
|
|
||||||
assert response.degrees == -1
|
|
||||||
|
|
||||||
change_double_elim_api_state(started=True, goal=[50, 75])
|
|
||||||
|
|
||||||
response, status = Api.DoubleElim.get_goal()
|
|
||||||
assert type(status) is int
|
|
||||||
assert status == 200
|
|
||||||
assert type(response) == Api.Position
|
|
||||||
assert 50 == response.x
|
|
||||||
assert 75 == response.y
|
|
||||||
assert response.degrees == -1
|
|
||||||
|
|
||||||
def test_get_items(self):
|
|
||||||
response, status = Api.DoubleElim.get_items()
|
|
||||||
assert type(status) is int
|
|
||||||
assert status == 503
|
|
||||||
assert type(response) == list
|
|
||||||
assert response == []
|
|
||||||
|
|
||||||
change_double_elim_api_state(started=True)
|
|
||||||
|
|
||||||
response, status = Api.DoubleElim.get_items()
|
|
||||||
assert type(status) is int
|
|
||||||
assert status == 200
|
|
||||||
assert type(response) == list
|
|
||||||
assert response == []
|
|
||||||
|
|
||||||
change_double_elim_api_state(started=True, items=[{"id": 0, "x": 50, "y": 75}])
|
|
||||||
|
|
||||||
response, status = Api.DoubleElim.get_items()
|
|
||||||
assert type(status) is int
|
|
||||||
assert status == 200
|
|
||||||
assert type(response) == list
|
|
||||||
assert len(response) == 1
|
|
||||||
assert response == [{"id": 0, "x": 50, "y": 75}]
|
|
||||||
|
|
||||||
change_double_elim_api_state(started=True, items=[{"id": 0, "x": 50, "y": 75}, {"id": 2, "x": -50, "y": -75}])
|
|
||||||
|
|
||||||
response, status = Api.DoubleElim.get_items()
|
|
||||||
assert type(status) is int
|
|
||||||
assert status == 200
|
|
||||||
assert type(response) == list
|
|
||||||
assert len(response) == 2
|
|
||||||
assert response == [{"id": 0, "x": 50, "y": 75}, {"id": 2, "x": -50, "y": -75}]
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
if START_SERVER:
|
|
||||||
self.server = Process(target=server.app.run, kwargs={"host": "0.0.0.0", "port": "5000"})
|
|
||||||
self.server.start()
|
|
||||||
time.sleep(0.25)
|
|
||||||
else:
|
|
||||||
self.resetApi()
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
if START_SERVER:
|
|
||||||
self.server.terminate()
|
|
||||||
self.server.join()
|
|
||||||
|
|
||||||
def resetApi(self):
|
|
||||||
assert requests.get(Api.API_URL + "resetState").status_code == 200
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|