Merge branch 'develop' into docs_test

This commit is contained in:
Bastian Schroll 2019-10-26 15:19:13 +02:00 committed by GitHub
commit 33cc717a22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
112 changed files with 3418 additions and 2318 deletions

32
.github/workflows/run_pytest.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: pytest
on: [push]
jobs:
build:
strategy:
max-parallel: 3
matrix:
os: [ubuntu-latest]
python-version: [3.5, 3.6, 3.7]
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{matrix.python-version}} at ${{matrix.os}}
uses: actions/setup-python@v1
with:
python-version: ${{matrix.python-version}}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
mkdir log/
- name: Test with pytest
run: |
pytest -c 'test/pytest.ini'
- name: Save artifacts
uses: actions/upload-artifact@master
with:
name: test.log
path: log/test.log

16
.gitignore vendored
View file

@ -6,12 +6,14 @@
# German BOS Information Script
# by Bastian Schroll
# virtual environment
\venv/
# generated files
stats_*
log/
_docu/
_bin/win/zadig/
_bin/win/rtl_sdr/
_bin/win/multimon_1_1_1/
_bin/win/kalibrate/
docu/docs/api/html
docu/site/
# Logo Photoshop file
*.psd
@ -22,6 +24,10 @@ _bin/win/kalibrate/
# Python precompiled
*.pyc
# Visual Studio Code
\.vscode/
\.pytest_cache/
# PyCharm IDE
\.cache/
\.idea/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,19 +0,0 @@
@echo off
echo " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " "
echo " ____ ____ ______ __ __ __ _____ "
echo " / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / "
echo " / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < "
echo " / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / "
echo " /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ "
echo " German BOS Information Script "
echo " by Bastian Schroll "
echo " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " "
echo.
echo Run 'count lines of code' ...
echo.
cd ..
_bin\win\cloc_1_72\cloc-1.72.exe . --exclude-lang=XML --exclude-dir=_docu,_config,_info,doxygen.ini --by-file-by-lang
echo.
echo.
pause

View file

@ -1,19 +0,0 @@
@echo off
echo " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " "
echo " ____ ____ ______ __ __ __ _____ "
echo " / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / "
echo " / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < "
echo " / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / "
echo " /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ "
echo " German BOS Information Script "
echo " by Bastian Schroll "
echo " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " "
echo.
echo Build Doxygen Documentation
echo.
cd ..
_bin\win\doxygen\doxygen.exe _gen/doxygen.ini
echo.
echo.
pause

View file

@ -1,45 +0,0 @@
@echo off
cd ..
if not exist "log/" mkdir log
echo " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " "
echo " ____ ____ ______ __ __ __ _____ "
echo " / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / "
echo " / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < "
echo " / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / "
echo " /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ "
echo " German BOS Information Script "
echo " by Bastian Schroll "
echo " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " "
echo.
echo BOSWatch 3 Unittest framework
echo.
echo Which test? [ENTER] for all
echo Or name a specific test test_[###].py
echo.
set /p test=Testcase:
if "%test%" == "" (
goto start
) else (
goto start_spec
)
:start
echo.
python -m pytest -c "_gen/pytest.ini"
echo.
echo --- Hit any key to repeat ---
pause
cls
goto start
:start_spec
echo.
python -m pytest test/test_%test%.py -c "_gen/pytest.ini"
echo.
echo --- Hit any key to repeat ---
pause
cls
goto start_spec

View file

@ -1,46 +0,0 @@
## Generatoren
Im Verzeichnis `_gen/` befinden sich Windows Batch-Scripte um bestimmt Aufgaben zu
vereinfachen und zu automatisieren.
---
### cloc.bat
`CLoC` steht für Count Lines of Code
Dieses Tool erstellt einen kurzen Report über die Anzahl der Dateien, Codezeilen,
Kommentare und Leerzeilen sortiert nach jeweiliger Programmiersprache
---
### doxygen.bat
Doxygen ist ein Dokumentationswerkzeug zum automatischen dokumentieren von Quellcode.
Nach dem ausführen wird der gesamte Quellcode geparst und eine HTML Dokumentation
im Verzeichnis `_docu/html/` angelegt.
Die Konfigurations Datei für Doxygen findet sich unter `_gen/doxygen.ini`
---
### pytest.bat
pytest ist ein Python Testframework
Damit ist es möglich, automatisierte Tests laufen zu lassen.
Vorher müssen die benötigten Plugins welche unter `_info/requirements.txt` gelistet sind
mittels `pip` installiert werden.
Nach dem Start kann man mit einem Druck auf `[ENTER]` direkt alle bekannten Tests
laufen lassen, oder durch die Eingabe eines spezifischen Tests auch nur diesen
ausführen. Die Tests werden dabei in zufälliger Reihenfolge abgearbeitet um auch
Fehler durch Abhängigkeiten voneinander zu erfassen.
Die Testfälle befinden sich im Unterordner `test/`
Einzeltests werden durch Eingabe des Namens ohne `test_` und `.py` aufgerufen.
Zum Beispiel `decoder` statt `test_decoder.py`
Zusätzlich werden alle Quellcode Dateien mittels eines PEP8 Parsers auf die Einhaltung
der Code-Sytel Vorgaben von Python hin untersucht und etwaige Fehler ausgegeben.
Vom Testverlauf wird ein Logfile erstellt welches in `log/test.log` befindet.
Die Konfigurationsdatei für pytest findet sich unter `_gen/pytest.ini`

View file

View file

@ -1,117 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice

View file

@ -1,20 +0,0 @@
Facts:
Dokumentation mittels Doxygen
Unittests mittels pytest (zufällige Reihenfolge durch pytest-randomly)
Codeabdeckung mittels pytest-cov
PEP8 Design Kontrolle mittels pytest-pep8
Logging mittels eigener Config-Datei einstellbar
Multi-Threading Server
Fuktionen:
Server- Client Anwendung
Verarbeitung von FMS, POCSAG und ZVEI
Komfortables Plugin System
Beschreibung aus CSV File
Konfigurationsdateien Modulweit teilbar über "Sharepoints"
Alarm-Daten Übermittlung per definierten bwPacket-Paket
Plugin Statistiken sowie Ausführungszeiten
Plugin Priorisierung
Funktionaler und dynamischer Pluginloader
Automatische Wildcard-Ersetzung in Plugins
Verschiedene Filterfunktionen (doubleFilter, )

View file

@ -1,289 +0,0 @@
## Format of the BOSWatch packets
<table>
<tr>
<td>field</td>
<td>fms</td>
<td>pocsag</td>
<td>zvei</td>
<td>msg</td>
<td>wildcard</td>
<td>description</td>
</tr>
<tr>
<td>serverName</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{SNAME}</td>
<td>name of the boswatch server instance</td>
</tr>
<tr>
<td>serverVersion</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{SVERS}</td>
<td>in case of new version, server can notify</td>
</tr>
<tr>
<td>serverBuildDate</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{SDATE}</td>
<td></td>
</tr>
<tr>
<td>serverBranch</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{SBRCH}</td>
<td></td>
</tr>
<tr>
<td>clientName</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{CNAME}</td>
<td>name of the boswatch client instance</td>
</tr>
<tr>
<td>clientIP</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{CIP}</td>
<td></td>
</tr>
<tr>
<td>clientVersion</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{CVERS}</td>
<td>in case of new version, server can notify</td>
</tr>
<tr>
<td>clientBuildDate</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{CDATE}</td>
<td></td>
</tr>
<tr>
<td>clientBranch</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{CBRCH}</td>
<td></td>
</tr>
<tr>
<td>inputSource</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{INSRC}</td>
<td>(stick, audio)</td>
</tr>
<tr>
<td>timestamp</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{TIMES}</td>
<td></td>
</tr>
<tr>
<td>frequency</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{FREQ}</td>
<td></td>
</tr>
<tr>
<td>mode</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td>{MODE}</td>
<td>(fms, pocsag, zvei, msg)</td>
</tr>
<tr>
<td>descriptionShort</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td></td>
<td>{DESCS}</td>
<td>loaded from optional CSV file</td>
</tr>
<tr>
<td>descriptionLong</td>
<td>X</td>
<td>X</td>
<td>X</td>
<td></td>
<td>{DESCL}</td>
<td>loaded from optional CSV file</td>
</tr>
<tr>
<td>bitrate</td>
<td></td>
<td>X</td>
<td></td>
<td></td>
<td>{BIT}</td>
<td></td>
</tr>
<tr>
<td>ric</td>
<td></td>
<td>X</td>
<td></td>
<td></td>
<td>{RIC}</td>
<td></td>
</tr>
<tr>
<td>subric</td>
<td></td>
<td>X</td>
<td></td>
<td></td>
<td>{SRIC}</td>
<td>(1, 2, 3, 4)</td>
</tr>
<tr>
<td>subricText</td>
<td></td>
<td>X</td>
<td></td>
<td></td>
<td>{SRICT}</td>
<td>(a, b, c, d)</td>
</tr>
<tr>
<td>message</td>
<td></td>
<td>X</td>
<td></td>
<td>X</td>
<td>{MSG}</td>
<td></td>
</tr>
<tr>
<td>tone</td>
<td></td>
<td></td>
<td>X</td>
<td></td>
<td>{TONE}</td>
<td>5-tone sequence</td>
</tr>
<tr>
<td>fms</td>
<td>X</td>
<td></td>
<td></td>
<td></td>
<td>{FMS}</td>
<td></td>
</tr>
<tr>
<td>service</td>
<td>X</td>
<td></td>
<td></td>
<td></td>
<td>{SERV}</td>
<td></td>
</tr>
<tr>
<td>country</td>
<td>X</td>
<td></td>
<td></td>
<td></td>
<td>{COUNT}</td>
<td></td>
</tr>
<tr>
<td>location</td>
<td>X</td>
<td></td>
<td></td>
<td></td>
<td>{LOC}</td>
<td></td>
</tr>
<tr>
<td>vehicle</td>
<td>X</td>
<td></td>
<td></td>
<td></td>
<td>{VEHC}</td>
<td></td>
</tr>
<tr>
<td>status</td>
<td>X</td>
<td></td>
<td></td>
<td></td>
<td>{STAT}</td>
<td></td>
</tr>
<tr>
<td>direction</td>
<td>X</td>
<td></td>
<td></td>
<td></td>
<td>{DIR}</td>
<td></td>
</tr>
<tr>
<td>dirextionText</td>
<td>X</td>
<td></td>
<td></td>
<td></td>
<td>{DIRT}</td>
<td>(Fhz-&gt;Lst, Lst-&gt;Fhz)</td>
</tr>
<tr>
<td>tacticalInfo</td>
<td>X</td>
<td></td>
<td></td>
<td></td>
<td>{TACI}</td>
<td>(I, II, III, IV)</td>
</tr>
</table>
<br><br>
### Other possible wildcards:
- {BR} - Line break (\\r\\n)
- {LPAR} - Left parenthesis (
- {RPAR} - Right parenthesis )
- {TIME} - Actual timestamp

View file

@ -1,131 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: config.py
@date: 25.12.2017
@author: Bastian Schroll
@description: Module for the configuration
"""
import logging
import configparser
logging.debug("- %s loaded", __name__)
class Config:
_sharePoints = {}
def __init__(self):
"""!Create a new config object and load the ini file directly"""
self._config = configparser.ConfigParser()
def loadConfigFile(self, configPath, sharePoint=""):
"""!loads a given configuration in the class wide config variable
@param configPath: Path to the config file
@param sharePoint: If you want to share the config set name here
@return True or False"""
logging.debug("load config file from: %s", configPath)
try:
self._config.read(configPath, "utf-8")
if sharePoint:
self._shareConfig(sharePoint)
return True
except: # pragma: no cover
logging.exception("cannot load config file")
return False
def _shareConfig(self, sharePoint):
"""!Shares the configuration
Shares the local _config to the class wide global _sharedConfig
@param sharePoint: Name of the global share point
@return True or False"""
if sharePoint in self._sharePoints:
logging.error("cannot share config - name is always in use: %s", sharePoint)
return False
else:
self._sharePoints[sharePoint] = self._config
logging.debug("add config sharePoint: %s", sharePoint)
return True
def getInt(self, section, key, sharePoint=""):
"""!Method to read a single config entry as integer
@param section: Section to read from
@param key: Value to read
@param sharePoint: Name of the global config share (empty is only local)
@return An Integer or None"""
value = self._get(section, key, sharePoint)
if value is None:
return None
return int(value)
def getBool(self, section, key, sharePoint=""):
"""!Method to read a single config entry as boolean
@param section: Section to read from
@param key: Value to read
@param sharePoint: Name of the global config share (empty is only local)
@return True or False"""
if self._get(section, key, sharePoint).lower() in ["1", "true", "yes"]:
return True
return False
def getStr(self, section, key, sharePoint=""):
"""!Method to read a single config entry as string
@param section: Section to read from
@param key: Value to read
@param sharePoint: Name of the global config share (empty is only local)
@return The value or None"""
value = self._get(section, key, sharePoint)
if value is None:
return None
return str(value)
def _get(self, section, key, sharePoint=""):
"""!Method to read a single config entry
@param section: Section to read from
@param key: Value to read
@param sharePoint: Name of the global config share (empty is only local)
@return The value or None"""
if sharePoint:
try:
return self._sharePoints[sharePoint].get(section, key)
except KeyError:
logging.error("no sharePoint named: %s", sharePoint)
except configparser.NoSectionError:
logging.warning("no shared config section: %s", section)
except configparser.NoOptionError:
logging.warning("no shared config option: %s", key)
except: # pragma: no cover
logging.exception("error while reading shared config")
return None
else:
try:
return self._config.get(section, key)
except configparser.NoSectionError:
logging.warning("no local config section: %s", section)
except configparser.NoOptionError:
logging.warning("no local config option: %s", key)
except: # pragma: no cover
logging.exception("error while reading local config")
return None
def getAllSharepoints(self):
"""!Return a python dict of all set sharepoints
@return Sharepoint dict"""
return self._sharePoints

77
boswatch/configYaml.py Normal file
View file

@ -0,0 +1,77 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: configYaml.py
@date: 27.02.2019
@author: Bastian Schroll
@description: Module for the configuration in YAML format
"""
import logging
import yaml
import yaml.parser
logging.debug("- %s loaded", __name__)
class ConfigYAML:
def __init__(self, config=None):
self._config = config
def __iter__(self):
for item in self._config:
if type(item) is list or type(item) is dict:
yield ConfigYAML(item)
else:
yield item
def __len__(self):
"""!returns the length of an config element"""
return len(self._config)
def __str__(self):
"""!Returns the string representation of the internal config dict"""
return str(self._config)
def loadConfigFile(self, configPath):
"""!loads a given configuration file
@param configPath: Path to the config file
@return True or False"""
logging.debug("load config file from: %s", configPath)
try:
with open(configPath) as file:
# use safe_load instead load
self._config = yaml.safe_load(file)
return True
except FileNotFoundError:
logging.error("config file not found: %s", configPath)
except yaml.parser.ParserError:
logging.exception("syntax error in config file: %s", configPath)
return False
def get(self, *args, default=None):
"""!Get a single value from the config
or a value set in a new configYAML class instance
@param *args: Config section (one ore more strings)
@param default: Default value if section not found (None)
@return: A single value, a value set in an configYAML instance, the default value"""
tmp = self._config
try:
for arg in args:
tmp = tmp.get(arg, default)
if type(tmp) is list or type(tmp) is dict:
return ConfigYAML(tmp)
else:
return tmp
except AttributeError: # pragma: no cover
return default

View file

@ -16,9 +16,9 @@
"""
import logging
from boswatch.decoder.fmsdecoder import FmsDecoder
from boswatch.decoder.pocsagdecoder import PocsagDecoder
from boswatch.decoder.zveidecoder import ZveiDecoder
from boswatch.decoder.fmsDecoder import FmsDecoder
from boswatch.decoder.pocsagDecoder import PocsagDecoder
from boswatch.decoder.zveiDecoder import ZveiDecoder
logging.debug("- %s loaded", __name__)
@ -32,6 +32,7 @@ class Decoder:
@param data: data to decode
@return bwPacket instance"""
logging.debug("search decoder")
data = str(data)
if "FMS" in data:
return FmsDecoder.decode(data)
elif "POCSAG" in data:
@ -39,5 +40,5 @@ class Decoder:
elif "ZVEI" in data:
return ZveiDecoder.decode(data)
else:
logging.error("no decoder found for: %s", data)
logging.warning("no decoder found for: %s", data)
return None

View file

@ -18,7 +18,7 @@
import logging
import re
from boswatch.packet import packet
from boswatch.packet import Packet
logging.debug("- %s loaded", __name__)
@ -50,7 +50,7 @@ class FmsDecoder:
if re.search("[0-9a-f]{8}[0-9a-f][01]", fms_id):
logging.debug("found valid FMS")
bwPacket = packet.Packet()
bwPacket = Packet()
bwPacket.set("mode", "fms")
bwPacket.set("fms", fms_id)
bwPacket.set("service", service)
@ -62,7 +62,6 @@ class FmsDecoder:
bwPacket.set("directionText", directionText)
bwPacket.set("tacticalInfo", tacticalInfo)
logging.debug(bwPacket)
return bwPacket
logging.warning("no valid data")

View file

@ -18,7 +18,7 @@
import logging
import re
from boswatch.packet import packet
from boswatch.packet import Packet
logging.debug("- %s loaded", __name__)
@ -48,7 +48,7 @@ class PocsagDecoder:
logging.debug("found valid POCSAG")
bwPacket = packet.Packet()
bwPacket = Packet()
bwPacket.set("mode", "pocsag")
bwPacket.set("bitrate", bitrate)
bwPacket.set("ric", ric)
@ -56,7 +56,6 @@ class PocsagDecoder:
bwPacket.set("subricText", subricText)
bwPacket.set("message", message)
logging.debug(bwPacket)
return bwPacket
logging.warning("no valid data")
@ -70,20 +69,20 @@ class PocsagDecoder:
@return bitrate
@return ric
@return subric"""
bitrate, ric, subric = 0, 0, 0
bitrate, ric, subric = "0", "0", "0"
if "POCSAG512:" in data:
bitrate = 512
bitrate = "512"
ric = data[20:27].replace(" ", "").zfill(7)
subric = str(int(data[39]) + 1)
elif "POCSAG1200:" in data:
bitrate = 1200
bitrate = "1200"
ric = data[21:28].replace(" ", "").zfill(7)
subric = str(int(data[40]) + 1)
elif "POCSAG2400:" in data:
bitrate = 2400
bitrate = "2400"
ric = data[21:28].replace(" ", "").zfill(7)
subric = str(int(data[40]) + 1)

View file

@ -18,7 +18,7 @@
import logging
import re
from boswatch.packet import packet
from boswatch.packet import Packet
logging.debug("- %s loaded", __name__)
@ -40,11 +40,10 @@ class ZveiDecoder:
if re.search("[0-9E]{5}", data[7:12]):
logging.debug("found valid ZVEI")
bwPacket = packet.Packet()
bwPacket = Packet()
bwPacket.set("mode", "zvei")
bwPacket.set("zvei", ZveiDecoder._solveDoubleTone(data[7:12]))
logging.debug(bwPacket)
return bwPacket
logging.warning("no valid data")

View file

@ -1,111 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: descriptor.py
@date: 07.01.2018
@author: Bastian Schroll
@description: Descriptor to load Descriptions from csv files
"""
import logging
import csv
import re
from boswatch.utils import paths
logging.debug("- %s loaded", __name__)
class Descriptor:
"""!CSV Descriptor class to load specific descriptions from CSV files, manage and serve them"""
def __init__(self):
"""!Initialise a private list for the DescriptionList objects"""
self._lists = {}
def loadDescription(self, csvType):
"""!Build a new description list from DescriptionList class
@param csvType: Name of the CSV file without `.csv`
@return True or False"""
bwDescriptionList = DescriptionList()
if bwDescriptionList.loadCSV(csvType):
self._lists[csvType] = bwDescriptionList
return True
return False
def addDescriptions(self, bwPacket):
"""!Add the short and long description to a bwPacket
@param bwPacket: bwPacket instance to add descriptions
@return True or False"""
logging.debug("add descriptions to bwPacket")
try:
bwPacket.set("shortDescription",
self._lists[bwPacket.get("mode")].getShortDescription(bwPacket.get(bwPacket.get("mode"))))
bwPacket.set("longDescription",
self._lists[bwPacket.get("mode")].getLongDescription(bwPacket.get(bwPacket.get("mode"))))
return True
except: # pragma: no cover
logging.exception("error while adding descriptions")
return False
class DescriptionList:
def __init__(self):
"""!Loads the given CSV file into internal list"""
logging.debug("create new descriptionList")
self._descriptionList = {}
def getShortDescription(self, checkId):
"""!Returns the short description of given id
@return short description or empty string"""
try:
return self._descriptionList[str(checkId)]["shortDescription"]
except:
return ""
def getLongDescription(self, checkId):
"""!Returns the long description of given id
@return long description or empty string"""
try:
return self._descriptionList[str(checkId)]["longDescription"]
except:
return ""
def loadCSV(self, csvType):
"""!Load descriptions from an csv file
@param csvType: Name of the CSV file without `.csv`
@return True or False"""
count = 0
logging.debug("loading csv file: %s", csvType)
csvPath = paths.CSV_PATH + csvType + ".csv"
try:
csvFile = open(csvPath, 'r', -1, 'utf-8')
reader = csv.DictReader(csvFile)
for line in reader:
if re.match("^[0-9]+[A-D]?$", line["id"], re.IGNORECASE):
self._descriptionList[line["id"]] = {"shortDescription": line["shortDescription"], "longDescription": line["longDescription"]}
logging.debug("- %s", line)
count += 1
logging.debug("%s entry's loaded", count)
return True
except FileNotFoundError:
logging.error("csv file not found: %s", csvPath)
return False
except: # pragma: no cover
logging.exception("error while loading descriptions")
return False

View file

@ -0,0 +1,167 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: broadcast.py
@date: 21.09.2018
@author: Bastian Schroll
@description: UDP broadcast server and client class
"""
import logging
import socket
import threading
logging.debug("- %s loaded", __name__)
class BroadcastClient:
"""!BroadcastClient class"""
def __init__(self, port=5000):
"""!Create an BroadcastClient instance
@param port: port to send broadcast packets (5000)"""
self._broadcastPort = port
self._serverIP = ""
self._serverPort = 0
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self._socket.settimeout(3)
def getConnInfo(self, retry=0):
"""!Get the connection info from server over udp broadcast
This function will send broadcast package(s)
to get connection info from the server.
- send the magic packet <BW-Request> on broadcast address.
- wait for a <BW-Result> magic packet.
- extract the connection data from the magic packet and return
@param retry: Count of retry - 0 is infinite (0)
@return True or False"""
sendPackages = 0
while sendPackages < retry or retry == 0:
try:
logging.debug("send magic <BW3-Request> as broadcast - Try: %d", sendPackages)
self._socket.sendto("<BW3-Request>".encode(), ('255.255.255.255', self._broadcastPort))
sendPackages += 1
payload, address = self._socket.recvfrom(1024)
payload = str(payload, "UTF-8")
if payload.startswith("<BW3-Result>"):
logging.debug("received magic <BW3-Result> from: %s", address[0])
self._serverIP = address[0]
self._serverPort = int(payload.split(";")[1])
logging.info("got connection info from server: %s:%d", self._serverIP, self._serverPort)
return True
except socket.timeout: # nothing received - retry
logging.debug("no magic packet received")
logging.warning("cannot fetch connection info after %d tries", sendPackages)
return False
@property
def serverIP(self):
"""!Property to get the server IP after successful broadcast"""
return self._serverIP
@property
def serverPort(self):
"""!Property to get the server Port after successful broadcast"""
return self._serverPort
class BroadcastServer:
"""!BroadcastServer class"""
def __init__(self, servePort=8080, listenPort=5000):
"""!Create an BroadcastServer instance
@param servePort: port to serve as connection info (8080)
@param listenPort: port to listen for broadcast packets (5000)"""
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self._socket.settimeout(2)
self._socket.bind(('', listenPort))
self._serverThread = None
self._serverShutdown = False
self._servePort = servePort
def __del__(self): # pragma: no cover
if self.isRunning:
self.stop()
while self.isRunning:
pass
def start(self):
"""!Start the broadcast server in a new thread
@return True or False"""
if not self.isRunning:
logging.debug("start udp broadcast server")
self._serverThread = threading.Thread(target=self._listen)
self._serverThread.name = "BroadServ"
self._serverThread.daemon = True
self._serverShutdown = False
self._serverThread.start()
return True
logging.warning("udp broadcast server always started")
return True
def stop(self):
"""!Stop the broadcast server
Due to the timeout of the socket,
stopping the thread can be delayed by two seconds.
But function returns immediately.
@return True or False"""
if self.isRunning:
logging.debug("stop udp broadcast server")
self._serverShutdown = True
return True
else:
logging.warning("udp broadcast server always stopped")
return True
def _listen(self):
"""!Broadcast server worker thread
This function listen for magic packets on broadcast
address and send the connection info to the clients.
- listen for the magic packet <BW-Request>
- send connection info in an <BW-Result> macig packet"""
logging.debug("start listening for magic")
while not self._serverShutdown:
try:
payload, address = self._socket.recvfrom(1024)
payload = str(payload, "UTF-8")
if payload == "<BW3-Request>":
logging.debug("received magic <BW3-Request> from: %s", address[0])
logging.info("send connection info in magic <BW3-Result> to: %s", address[0])
self._socket.sendto("<BW3-Result>;".encode() + str(self._servePort).encode(), address)
except socket.timeout:
continue # timeout is accepted (not block at recvfrom())
self._serverThread = None
logging.debug("udp broadcast server stopped")
@property
def isRunning(self):
"""!Property of broadcast server running state"""
if self._serverThread:
return True
return False

View file

@ -16,9 +16,12 @@
"""
import logging
import socket
import select
logging.debug("- %s loaded", __name__)
HEADERSIZE = 10
class TCPClient:
"""!TCP client class"""
@ -26,51 +29,43 @@ class TCPClient:
def __init__(self, timeout=3):
"""!Create a new instance
Create a new instance of an TCP Client.
And set the timeout"""
try:
self._sock = None
self._timeout = timeout
except: # pragma: no cover
logging.exception("cannot create a TCPClient")
@param timeout: timeout for the client in sec. (3)"""
socket.setdefaulttimeout(timeout)
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
def connect(self, host="localhost", port=8080):
"""!Connect to the server
@param host: Server IP address (localhost)
@param host: Server IP address ("localhost")
@param port: Server Port (8080)
@return True or False"""
try:
self._sock = socket
self._sock.setdefaulttimeout(self._timeout)
self._sock = socket.create_connection((host, port))
logging.debug("connected to %s:%s", host, port)
if not self.isConnected:
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.connect((host, port))
logging.debug("connected to %s:%s", host, port)
return True
logging.warning("client always connected")
return True
except ConnectionRefusedError:
logging.error("cannot connect to %s:%s - connection refused", host, port)
return False
except socket.timeout: # pragma: no cover
logging.warning("cannot connect to %s:%s - timeout after %s sec", host, port, self._timeout)
return False
except: # pragma: no cover
logging.exception("cannot connect to %s:%s", host, port)
return False
except socket.error as e:
logging.error(e)
return False
def disconnect(self):
"""!Disconnect from the server
@return True or False"""
try:
self._sock.close()
logging.debug("disconnected")
if self.isConnected:
self._sock.shutdown(socket.SHUT_RDWR)
self._sock.close()
logging.debug("disconnected")
return True
logging.warning("client always disconnected")
return True
except AttributeError:
logging.error("cannot disconnect - no connection established")
return False
except: # pragma: no cover
logging.exception("error while disconnecting")
return False
except socket.error as e:
logging.error(e)
return False
def transmit(self, data):
"""!Send a data packet to the server
@ -78,37 +73,55 @@ class TCPClient:
@param data: data to send to the server
@return True or False"""
try:
logging.debug("transmitting: %s", data)
self._sock.sendall(bytes(data + "\n", "utf-8"))
logging.debug("transmitting:\n%s", data)
data = data.encode("utf-8")
header = str(len(data)).ljust(HEADERSIZE).encode("utf-8")
self._sock.sendall(header + data)
logging.debug("transmitted...")
return True
except AttributeError:
logging.error("cannot transmit - no connection established")
return False
except ConnectionResetError:
logging.error("cannot transmit - host closed connection")
return False
except: # pragma: no cover
logging.exception("error while transmitting")
return False
except socket.error as e:
logging.error(e)
return False
def receive(self):
def receive(self, timeout=1):
"""!Receive data from the server
@param: timeout to wait for incoming data in seconds
@return received data"""
try:
received = str(self._sock.recv(1024), "utf-8")
logging.debug("received: %d", received)
read, _, _ = select.select([self._sock], [], [], timeout)
if not read: # check if there is something to read
return False
header = self._sock.recv(HEADERSIZE).decode("utf-8")
if not len(header): # check if there data
return False
length = int(header.strip())
received = self._sock.recv(length).decode("utf-8")
logging.debug("recv header: '%s'", header)
logging.debug("received %d bytes: %s", len(received), received)
return received
except AttributeError:
logging.error("cannot receive - no connection established")
except socket.error as e:
logging.error(e)
return False
@property
def isConnected(self):
"""!Property of client connected state"""
try:
if self._sock:
_, write, _ = select.select([], [self._sock], [], 0.1)
if write:
data = "<keep-alive>".encode("utf-8")
header = str(len(data)).ljust(HEADERSIZE).encode("utf-8")
self._sock.sendall(header + data)
return True
return False
except ConnectionResetError:
logging.error("cannot receive - host closed connection")
except socket.error as e:
if e.errno != 32:
logging.exception(e)
return False
except socket.timeout: # pragma: no cover
logging.warning("cannot receive - timeout after %s sec", self._timeout)
return False
except: # pragma: no cover
logging.exception("error while receiving")
except ValueError:
return False

View file

@ -0,0 +1,48 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: netCheck.py
@date: 21.09.2018
@author: Bastian Schroll
@description: Worker to check internet connection
"""
import logging
from urllib.request import urlopen
logging.debug("- %s loaded", __name__)
class NetCheck:
"""!Worker class to check internet connection"""
def __init__(self, hostname="https://www.google.com/", timeout=1):
"""!Create a new NetCheck instance
@param hostname: host against connection check is running ("https://www.google.com/")
@param timout: timout for connection check in sec. (1)"""
self._hostname = hostname
self._timeout = timeout
self.connectionState = False
self.checkConn() # initiate a first check
def checkConn(self):
"""!Check the connection
@return True or False"""
try:
urlopen(self._hostname, timeout=self._timeout)
logging.debug("%s is reachable", self._hostname)
self.connectionState = True
return True
except: # todo find right exception type
logging.warning("%s is not reachable", self._hostname)
self.connectionState = False
return False

View file

@ -15,70 +15,94 @@
@description: Class implementation for a threaded TCP socket server
"""
import logging
import socket
import socketserver
import threading
import time
import select
logging.debug("- %s loaded", __name__)
# module wide global list for received data sets
_dataPackets = []
_lockDataPackets = threading.Lock()
# module wide global list for all currently connected clients
_clients = {} # _clients[ThreadName] = {"address", "timestamp"}
_lockClients = threading.Lock()
HEADERSIZE = 10
class TCPHandler(socketserver.BaseRequestHandler):
"""!RequestHandler class for our TCPServer class."""
class _ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
"""!ThreadedTCPRequestHandler class for our TCPServer class."""
def handle(self):
"""!Handles the request from an single client in a own thread
Insert a request in the clients[] list and send a [ack]"""
with _lockClients:
_clients[threading.current_thread().name] = {"address": self.client_address[0], "timestamp": time.time()}
with self.server.clientsConnectedLock: # because our list is not threadsafe
self.server.clientsConnected[threading.current_thread().name] = {"address": self.client_address[0], "timestamp": time.time()}
logging.info("Client connected: %s", self.client_address[0])
data = 1 # to enter while loop
cur_thread = threading.current_thread().name
req_name = str(cur_thread) + " " + self.client_address[0]
try:
while data:
data = str(self.request.recv(1024).strip(), 'utf-8')
if data != "":
logging.debug("%s recv: %s", req_name, data)
while self.server.isActive:
read, _, _ = select.select([self.request], [], [], 0.5)
if not read:
continue # nothing to read on the socket
# add a new entry at first position (index 0) with client IP
# and the decoded data dict as an string in utf-8 and an timestamp
with _lockDataPackets:
_dataPackets.insert(0, (self.client_address[0], data, time.time())) # time() to calc time in queue
logging.debug("Add data to queue")
header = self.request.recv(HEADERSIZE).decode("utf-8")
if not len(header):
break # empty data -> socked closed
logging.debug("%s send: [ack]", req_name)
self.request.sendall(bytes("[ack]", "utf-8"))
self.request.close()
length = int(header.strip())
data = self.request.recv(length).decode("utf-8")
except (ConnectionResetError, ConnectionAbortedError): # pragma: no cover
logging.debug("%s connection closed", req_name)
except: # pragma: no cover
logging.exception("%s error while receiving", req_name)
if data == "<keep-alive>":
continue
logging.debug("%s recv header: '%s'", req_name, header)
logging.debug("%s recv %d bytes:\n%s", req_name, len(data), data)
# add a new entry and the decoded data dict as an string in utf-8 and an timestamp
self.server.alarmQueue.put_nowait((self.client_address[0], data, time.time())) # queue is threadsafe
logging.debug("Add data to queue")
logging.debug("%s send: [ack]", req_name)
data = "[ack]".encode("utf-8")
header = str(len(data)).ljust(HEADERSIZE).encode("utf-8")
self.request.sendall(header + data)
except socket.error as e:
logging.error(e)
return False
finally:
with _lockClients:
del _clients[threading.current_thread().name]
self.request.close()
del self.server.clientsConnected[threading.current_thread().name]
logging.info("Client disconnected: %s", self.client_address[0])
class TCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
class _ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
"""!ThreadedTCPServer class for our TCPServer class."""
pass
class TCPServer:
"""!TCP server class"""
def __init__(self, timeout=3):
"""!Create a new instance"""
def __init__(self, alarmQueue, timeout=3):
"""!Create a new instance
@param alarmQueue: python queue instance
@param timeout: server timeout in sec (3)
"""
self._server = None
self._server_thread = None
self._timeout = timeout
self._alarmQueue = alarmQueue
self._clientsConnectedLock = threading.Lock()
self._clientsConnected = {}
def __del__(self):
if self.isRunning:
self.stop()
def start(self, port=8080):
"""!Start a threaded TCP socket server
@ -90,78 +114,66 @@ class TCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
@param port: Server Port (8080)
@return True or False"""
try:
self._server = socketserver.ThreadingTCPServer(("", port), TCPHandler)
self._server.timeout = self._timeout
if not self.isRunning:
try:
socketserver.TCPServer.allow_reuse_address = True # because we can start two instances on same port elsewhere
self._server = _ThreadedTCPServer(("", port), _ThreadedTCPRequestHandler)
self._server.timeout = self._timeout
self._server.alarmQueue = self._alarmQueue
self._server.isActive = True
self.flushQueue()
self._server.clientsConnectedLock = self._clientsConnectedLock
self._server.clientsConnected = self._clientsConnected
self._server_thread = threading.Thread(target=self._server.serve_forever)
self._server_thread.name = "Thread-BWServer"
self._server_thread.daemon = True
self._server_thread.start()
logging.debug("TCPServer started in Thread: %s", self._server_thread.name)
self._server_thread = threading.Thread(target=self._server.serve_forever)
self._server_thread.name = "Thread-BWServer"
self._server_thread.daemon = True
self._server_thread.start()
logging.debug("TCPServer started in Thread: %s", self._server_thread.name)
return True
except socket.error as e:
logging.error(e)
return False
else:
logging.warning("server always started")
return True
except OSError:
logging.exception("server always running?")
except: # pragma: no cover
logging.exception("cannot start the server")
return False
def stop(self):
"""!Stops the TCP socket server
@return True or False"""
try:
if self.isRunning:
self._server.shutdown()
self._server.isActive = False
self._server.server_close()
self._server_thread.join()
self._server.socket.close()
self._server_thread = None
self._server = None
logging.debug("TCPServer stopped")
return True
except AttributeError:
logging.exception("cannot stop - server not started?")
return False
except: # pragma: no cover
logging.exception("cannot stop the server")
return False
logging.warning("server always stopped")
return True
@staticmethod
def countClientsConnected():
def countClientsConnected(self):
"""!Number of currently connected Clients
@return Connected clients"""
with _lockClients:
return len(_clients)
with self._clientsConnectedLock: # because our list is not threadsafe
return len(self._clientsConnected)
@staticmethod
def getClientsConnected():
# todo insert comment
def getClientsConnected(self):
"""!A list of all connected clients
with their IP address and last seen timestamp
_clients[ThreadName] = {"address", "timestamp"}
@return List of onnected clients"""
# todo return full list or write a print/debug method?
return _clients
with self._clientsConnectedLock: # because our list is not threadsafe
return self._clientsConnected
@staticmethod
def getDataFromQueue():
"""!Function to get the data packages from server
must be polled by main program
@return Next data packet.py from intern queue"""
if _dataPackets:
with _lockDataPackets:
message = _dataPackets.pop()
logging.debug("Get data from queue")
return message
return None
@staticmethod
def countPacketsInQueue():
"""!Get packets waiting in queue
@return Packets in queue"""
return len(_dataPackets) # no lock needed - only reading
@staticmethod
def flushQueue():
"""!To flush all existing data in queue"""
logging.debug("Flush data queue")
with _lockDataPackets:
_dataPackets.clear()
@property
def isRunning(self):
"""!Property of server running state"""
if self._server:
return True
return False

View file

@ -16,8 +16,6 @@
"""
import logging
import time
from boswatch.config import Config
from boswatch import version
logging.debug("- %s loaded", __name__)
@ -34,11 +32,7 @@ class Packet:
self._packet = {"timestamp": time.time()}
else:
logging.debug("create bwPacket from string")
try:
self._packet = eval(str(bwPacket.strip()))
except: # pragma: no cover
# todo can we repair the packet anyway?
logging.exception("error while create packet from string")
self._packet = eval(str(bwPacket.strip()))
def __str__(self):
"""!Return the intern _packet dict as string"""
@ -63,44 +57,9 @@ class Packet:
logging.warning("field not found: %s", fieldName)
return None
def addClientData(self):
"""!Add the client information to the decoded data
This function adds the following data to the bwPacket:
- clientName
- clientVersion
- clientBuildDate
- clientBranch
- inputSource
- frequency"""
config = Config()
logging.debug("add client data to bwPacket")
self.set("clientName", config.getStr("Client", "Name", "clientConfig"))
self.set("clientVersion", version.client)
self.set("clientBuildDate", version.date)
self.set("clientBranch", version.branch)
self.set("inputSource", config.getStr("Client", "InputSource", "clientConfig"))
self.set("frequency", config.getStr("Stick", "Frequency", "clientConfig"))
def addServerData(self):
"""!Add the server information to the decoded data
This function adds the following data to the bwPacket:
- serverName
- serverVersion
- serverBuildDate
- serverBranch"""
config = Config()
logging.debug("add server data to bwPacket")
self.set("serverName", config.getStr("Server", "Name", "serverConfig"))
self.set("serverVersion", version.server)
self.set("serverBuildDate", version.date)
self.set("serverBranch", version.branch)
def printInfo(self):
"""!Print a info message to the log on INFO level.
Contains the most useful info about this packet.
@todo not complete yet - must be edit to print nice formatted messages on console
@param bwPacket: BOSWatch packet instance"""
"""
logging.info("[%s]", self.get("mode"))

View file

@ -1,111 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: pluginManager.py
@date: 08.01.2018
@author: Bastian Schroll
@description: Plugin manager class to load and call the plugins
@todo must be mostly refactored
"""
import logging
import os
import time
import importlib
from boswatch.config import Config
from boswatch.utils import paths
logging.debug("- %s loaded", __name__)
class PluginManager:
"""!Plugin manager class to load, manage and call the plugins
@todo refactor the class and add documentation"""
def __init__(self):
"""!init comment"""
self._config = Config()
self._pluginList = []
def searchPluginDir(self):
logging.debug("search for plugins in: %s", paths.PLUGIN_PATH)
for name in os.listdir(paths.PLUGIN_PATH):
location = os.path.join(paths.PLUGIN_PATH, name)
# Skip if Path.isdir() or no File DIR_NAME.py is found
if not os.path.isdir(location) or not name + ".py" in os.listdir(location):
continue
pluginPriority = self._config.getInt("Plugins", name, "serverConfig")
if pluginPriority is None:
logging.warning("no entry in server config for plugin: %s", name)
continue
elif pluginPriority > 0:
self._pluginList.append({"pluginName": name, "pluginPriority": pluginPriority})
logging.debug("[ENABLED ] %s [%3d]", name, pluginPriority)
elif pluginPriority <= 0:
logging.debug("[DISABLED] %s ", name)
# sort pluginList on pluginPriority descending (High to Low)
self._pluginList.sort(key=lambda x: x['pluginPriority'], reverse=True)
def importAllPlugins(self):
logging.debug("importing all plugins")
for item in self._pluginList:
importPlugin = self._importPlugin(item["pluginName"])
if importPlugin:
item["pluginImport"] = importPlugin
@staticmethod
def _importPlugin(pluginName):
logging.debug("import plugin: %s", pluginName)
try:
return importlib.import_module("plugins." + pluginName + "." + pluginName)
except:
logging.exception("error while loading plugin: %s", pluginName)
return False
def loadAllPlugins(self):
logging.debug("loading all plugins")
for item in self._pluginList:
item["pluginObject"] = None # todo del or none ???
item["pluginObject"] = item["pluginImport"].BoswatchPlugin()
def runAllPlugins(self, bwPacket):
logging.info("ALARM - %0.3f sec. since radio reception", time.time() - bwPacket.get("timestamp"))
for item in self._pluginList:
item["pluginObject"]._run(bwPacket)
item["pluginStatistics"] = item["pluginObject"]._getStatistics()
self.printEndStats()
def unloadAllPlugins(self):
logging.debug("unload all plugins")
for item in self._pluginList:
# todo del or None ???
del item["pluginObject"] # delete plugin object to force __del__() running
def printEndStats(self):
logging.debug("Plugin run statistics:")
logging.debug("Plugin | runs | tRUN | tCUM | tSET | tALA | tTRD | eSET | eALA | eTRD")
for item in self._pluginList:
logging.debug("- %-12s | %4d | %0.2f | %6.1f | %0.2f | %0.2f | %0.2f | %4d | %4d | %4d",
item["pluginName"],
item["pluginStatistics"]["runCount"],
item["pluginStatistics"]["sumTime"],
item["pluginStatistics"]["cumTime"],
item["pluginStatistics"]["setupTime"],
item["pluginStatistics"]["alarmTime"],
item["pluginStatistics"]["teardownTime"],
item["pluginStatistics"]["setupErrorCount"],
item["pluginStatistics"]["alarmErrorCount"],
item["pluginStatistics"]["teardownErrorCount"])

144
boswatch/processManager.py Normal file
View file

@ -0,0 +1,144 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: processManager.py
@date: 04.03.2018
@author: Bastian Schroll
@description: Class for managing sub processes
"""
import logging
import subprocess
logging.debug("- %s loaded", __name__)
class ProcessManager:
"""!class to manage a extern sub process"""
def __init__(self, process, textMode=False):
logging.debug("create process instance %s - textMode: %s", process, textMode)
self._args = []
self._args.append(process)
self._stdin = None
self._stdout = subprocess.PIPE
self._stderr = subprocess.STDOUT
self._processHandle = None
self._textMode = textMode
def __del__(self):
self.stop()
def addArgument(self, arg):
"""!add a new argument
@param arg: argument to add as string"""
logging.debug("add argument to process: %s -> %s", self._args[0], arg)
for splitArg in arg.split():
self._args.append(splitArg)
def clearArguments(self):
"""!clear all arguments"""
self._args = self._args[0:1] # kept first element (process name)
def start(self):
"""!start the new process
@return: True or False"""
logging.debug("start new process: %s %s", self._args[0], self._args[1:])
try:
self._processHandle = subprocess.Popen(self._args,
stdin=self._stdin,
stdout=self._stdout,
stderr=self._stderr,
universal_newlines=self._textMode,
shell=False)
if not self.isRunning:
logging.error("cannot start process")
return False
logging.debug("process started with PID %d", self._processHandle.pid)
return True
except FileNotFoundError:
logging.error("File not found: %s", self._args[0])
return False
def stop(self):
"""!Stop the process by sending SIGTERM and wait for ending"""
logging.debug("stopping process: %s", self._args[0])
if self.isRunning:
self._processHandle.terminate()
while self.isRunning:
pass
logging.debug("process %s returned %d", self._args[0], self._processHandle.returncode)
def readline(self):
"""!Read one line from stdout stream
@return singe line or None"""
if self.isRunning and self._stdout is not None:
try:
line = self._processHandle.stdout.readline().strip()
except UnicodeDecodeError:
return None
return line
return None
def skipLines(self, lineCount=1):
"""!Skip given number of lines from the output
@param lineCount: number of lines to skip
"""
logging.debug("skip %d lines from output", lineCount)
while self.isRunning and lineCount:
self.readline()
lineCount -= 1
def skipLinesUntil(self, matchText):
"""!Skip lines from the output until the given string is in it
@param matchText: string to search for in output
"""
logging.debug("skip lines till '%s' from output", matchText)
if not self._textMode:
matchText = bytes(matchText, "utf-8")
while self.isRunning and matchText not in self.readline():
pass
def setStdin(self, stdin):
"""!Set the stdin stream instance"""
self._stdin = stdin
def setStdout(self, stdout):
"""!Set the stdout stream instance"""
self._stdout = stdout
def setStderr(self, stderr):
"""!Set the stderr stream instance"""
self._stderr = stderr
@property
def stdout(self):
"""!Property to get the stdout stream"""
return self._processHandle.stdout
@property
def stderr(self):
"""!Property to get the stderr stream"""
return self._processHandle.stderr
@property
def isRunning(self):
"""!Property to get process running state
@return True or False"""
if self._processHandle:
if self._processHandle.poll() is None:
return True
return False

36
boswatch/router/route.py Normal file
View file

@ -0,0 +1,36 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: route.py
@date: 04.03.2019
@author: Bastian Schroll
@description: Class for a single BOSWatch packet router route point
"""
import logging
logging.debug("- %s loaded", __name__)
class Route:
"""!Class for single routing points"""
def __init__(self, name, callback, statsCallback=None, cleanupCallback=None):
"""!Create a instance of an route point
@param name: name of the route point
@param callback: instance of the callback function
@param statsCallback: instance of the callback to get statistics (None)
@param cleanupCallback: instance of the callback to run a cleanup method (None)
"""
self.name = name
self.callback = callback
self.statistics = statsCallback
self.cleanup = cleanupCallback

89
boswatch/router/router.py Normal file
View file

@ -0,0 +1,89 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: router.py
@date: 01.03.2019
@author: Bastian Schroll
@description: Class for the BOSWatch packet router
"""
import logging
import copy
import time
logging.debug("- %s loaded", __name__)
class Router:
"""!Class for the Router"""
def __init__(self, name):
"""!Create a new router
@param name: name of the router"""
self.name = name
self.routeList = []
# for time counting
self._cumTime = 0
self._routerTime = 0
# for statistics
self._runCount = 0
logging.debug("[%s] add new router", self.name)
def addRoute(self, route):
"""!Adds a route point to the router
@param route: instance of the Route class
"""
logging.debug("[%s] add route: %s", self.name, route.name)
self.routeList.append(route)
def runRouter(self, bwPacket):
"""!Run the router
@param bwPacket: instance of Packet class
@return a instance of Packet class
"""
self._runCount += 1
tmpTime = time.time()
logging.debug("[%s] started", self.name)
for routeObject in self.routeList:
logging.debug("[%s] -> run route: %s", self.name, routeObject.name)
bwPacket_tmp = routeObject.callback(copy.deepcopy(bwPacket)) # copy bwPacket to prevent edit the original
if bwPacket_tmp is None: # returning None doesnt change the bwPacket
continue
if bwPacket_tmp is False: # returning False stops the router immediately
logging.debug("[%s] stopped", self.name)
break
bwPacket = bwPacket_tmp
logging.debug("[%s] bwPacket returned", self.name)
logging.debug("[%s] finished", self.name)
self._routerTime = time.time() - tmpTime
self._cumTime += self._routerTime
return bwPacket
def _getStatistics(self):
"""!Returns statistical information's from last router run
@return Statistics as pyton dict"""
stats = {"type": "router",
"runCount": self._runCount,
"cumTime": self._cumTime,
"moduleTime": self._routerTime}
return stats

View file

@ -0,0 +1,160 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: routerManager.py
@date: 04.03.2019
@author: Bastian Schroll
@description: Class for the BOSWatch packet router manager class
"""
# todo think about implement threading for routers and the plugin calls (THREAD SAFETY!!!)
import logging
import importlib
import time
from boswatch.configYaml import ConfigYAML
from boswatch.router.router import Router
from boswatch.router.route import Route
logging.debug("- %s loaded", __name__)
class RouterManager:
"""!Class to manage all routers"""
def __init__(self):
"""!Create new router"""
self._routerDict = {}
self._startTime = int(time.time())
# if there is an error, router list would be empty (see tmp variable)
def buildRouters(self, config):
"""!Initialize Routers from given config file
@param config: instance of ConfigYaml class
@return True or False"""
self._routerDict = {} # all routers and instances of modules/plugins would be destroyed
routerDict_tmp = {}
logging.debug("build routers")
# first we have to init all routers
# because a router can be a valid target and we need his reference
for router in config.get("router"):
if router.get("name") is None or router.get("route") is None:
logging.error("name or route not found in router: %s", router)
return False
if router.get("name") in self._routerDict:
logging.error("duplicated router name: %s", router.get("name"))
return False
routerDict_tmp[router.get("name")] = Router(router.get("name"))
for router in config.get("router"):
routerName = router.get("name")
for route in router.get("route"):
routeType = route.get("type")
routeRes = route.get("res")
routeName = route.get("name", default=routeRes)
routeConfig = route.get("config", default=ConfigYAML()) # if no config - build a empty
if routeType is None or routeRes is None:
logging.error("type or name not found in route: %s", route)
return False
try:
if routeType == "plugin":
importedFile = importlib.import_module(routeType + "." + routeRes)
loadedClass = importedFile.BoswatchPlugin(routeConfig)
routerDict_tmp[routerName].addRoute(Route(routeName,
loadedClass._run,
loadedClass._getStatistics,
loadedClass._cleanup))
elif routeType == "module":
importedFile = importlib.import_module(routeType + "." + routeRes)
loadedClass = importedFile.BoswatchModule(routeConfig)
routerDict_tmp[routerName].addRoute(Route(routeName,
loadedClass._run,
loadedClass._getStatistics,
loadedClass._cleanup))
elif routeType == "router":
routerDict_tmp[routerName].addRoute(Route(routeName, routerDict_tmp[routeName].runRouter))
else:
logging.error("unknown type '%s' in %s", routeType, route)
return False
# except ModuleNotFoundError: # only since Py3.6
except ImportError:
logging.error("%s not found: %s", route.get("type"), route.get("res"))
return False
logging.debug("finished building routers")
self._routerDict = routerDict_tmp
self._showRouterRoute()
return True
def runRouters(self, routerRunList, bwPacket):
"""!Run given Routers
@param routerRunList: string or list of router names in string form
@param bwPacket: instance of Packet class"""
if type(routerRunList) is str: # convert single string name to list
routerRunList = [routerRunList]
for routerName in routerRunList:
if routerName in self._routerDict:
self._routerDict[routerName].runRouter(bwPacket)
else:
logging.warning("unknown router: %s", routerName)
self._saveStats() # write stats to stats file
def cleanup(self):
"""!Run cleanup routines for all loaded route points"""
for name, routerObject in self._routerDict.items():
logging.debug("Start cleanup for %s", name)
for routePoint in routerObject.routeList:
if routePoint.cleanup:
routePoint.cleanup()
def _showRouterRoute(self):
"""!Show the routes of all routers"""
for name, routerObject in self._routerDict.items():
logging.debug("Route for %s", name)
counter = 0
for routePoint in routerObject.routeList:
counter += 1
logging.debug(" %d. %s", counter, routePoint.name)
def _saveStats(self):
"""!Save current statistics to file"""
lines = []
for name, routerObject in self._routerDict.items():
lines.append("[" + name + "]")
lines.append(" - Route points: " + str(len(routerObject.routeList)))
lines.append(" - Runs: " + str(routerObject._getStatistics()['runCount']))
for routePoint in routerObject.routeList:
lines.append("[+] " + routePoint.name)
if routePoint.statistics:
if routePoint.statistics()['type'] == "module":
lines.append(" - Runs: " + str(routePoint.statistics()['runCount']))
lines.append(" - Run errors: " + str(routePoint.statistics()['moduleErrorCount']))
elif routePoint.statistics()['type'] == "plugin":
lines.append(" - Runs: " + str(routePoint.statistics()['runCount']))
lines.append(" - Setup errors: " + str(routePoint.statistics()['setupErrorCount']))
lines.append(" - Alarm errors: " + str(routePoint.statistics()['alarmErrorCount']))
lines.append(" - Teardown errors: " + str(routePoint.statistics()['teardownErrorCount']))
lines.append("")
with open("stats_" + str(self._startTime) + ".txt", "w") as stats:
for line in lines:
stats.write(line + "\n")

116
boswatch/timer.py Normal file
View file

@ -0,0 +1,116 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: timer.py
@date: 21.09.2018
@author: Bastian Schroll
@description: Timer class for interval timed events
"""
import logging
import time
from threading import Thread, Event
logging.debug("- %s loaded", __name__)
class RepeatedTimer:
def __init__(self, interval, targetFunction, *args, **kwargs):
"""!Create a new instance of the RepeatedTimer
@param interval: interval in sec. to recall target function
@param targetFunction: function to call on timer event
@param *args: arguments for the called function
@param *kwargs: keyword arguments for the called function
"""
self._interval = interval
self._function = targetFunction
self._args = args
self._kwargs = kwargs
self._start = 0
self._overdueCount = 0
self._lostEvents = 0
self._isRunning = False
self._event = Event()
self._thread = None
def start(self):
"""!Start a new timer worker thread
@return True or False"""
if self._thread is None:
self._event.clear()
self._thread = Thread(target=self._target)
self._thread.name = "RepTim(" + str(self._interval) + ")"
self._thread.daemon = True # start as daemon (thread dies if main program ends)
self._thread.start()
logging.debug("start repeatedTimer: %s", self._thread.name)
return True
logging.debug("repeatedTimer always started")
return True
def stop(self):
"""!Stop the timer worker thread
@return True or False"""
self._event.set()
if self._thread is not None:
logging.debug("stop repeatedTimer: %s", self._thread.name)
self._thread.join()
return True
logging.warning("repeatedTimer always stopped")
return True
def _target(self):
"""!Runs the target function with his arguments in own thread"""
self._start = time.time()
while not self._event.wait(self.restTime):
logging.debug("work")
startTime = time.time()
try:
self._function(*self._args, **self._kwargs)
except: # pragma: no cover
logging.exception("target throws an exception")
runTime = time.time() - startTime
if runTime < self._interval:
logging.debug("ready after: %0.3f sec. - next call in: %0.3f sec.", runTime, self.restTime)
else:
lostEvents = int(runTime / self._interval)
logging.warning("timer overdue! interval: %0.3f sec. - runtime: %0.3f sec. - "
"%d events lost - next call in: %0.3f sec.", self._interval, runTime, lostEvents, self.restTime)
self._lostEvents += lostEvents
self._overdueCount += 1
logging.debug("repeatedTimer thread stopped: %s", self._thread.name)
self._thread = None # set to none after leave teh thread (running recognize)
@property
def isRunning(self):
"""!Property for repeatedTimer running state"""
if self._thread:
return True
return False
@property
def restTime(self):
"""!Property to get remaining time till next call"""
return self._interval - ((time.time() - self._start) % self._interval)
@property
def overdueCount(self):
"""!Property to get a count over all overdues"""
return self._overdueCount
@property
def lostEvents(self):
"""!Property to get a count over all lost events"""
return self._lostEvents

View file

@ -17,7 +17,7 @@
import logging
import platform # for python version nr
import boswatch.version
from boswatch.utils import version
logging.debug("- %s loaded", __name__)
@ -26,66 +26,39 @@ def logoToLog():
"""!Prints the BOSWatch logo to the log at debug level
@return True or False on error"""
try:
logging.debug(" ____ ____ ______ __ __ __ _____ ")
logging.debug(" / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / ")
logging.debug(" / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < ")
logging.debug(" / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / ")
logging.debug("/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ ")
logging.debug(" German BOS Information Script ")
logging.debug(" by Bastian Schroll ")
logging.debug("")
return True
except: # pragma: no cover
logging.exception("cannot display logo in log")
return False
logging.debug(" ____ ____ ______ __ __ __ _____ ")
logging.debug(" / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / ")
logging.debug(" / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < ")
logging.debug(" / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / ")
logging.debug("/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ ")
logging.debug(" German BOS Information Script ")
logging.debug(" by Bastian Schroll ")
logging.debug("")
return True
def infoToLog():
"""!Prints the BOSWatch and OS information to log at debug level
@return True or False on error"""
try:
logging.debug("BOSWatch and environment information")
logging.debug("- Client version: %d.%d.%d",
boswatch.version.client["major"],
boswatch.version.client["minor"],
boswatch.version.client["patch"])
logging.debug("- Server version: %d.%d.%d",
boswatch.version.server["major"],
boswatch.version.server["minor"],
boswatch.version.server["patch"])
logging.debug("- Branch: %s",
boswatch.version.branch)
logging.debug("- Release date: %02d.%02d.%4d",
boswatch.version.date["day"],
boswatch.version.date["month"],
boswatch.version.date["year"])
logging.debug("- Python version: %s", platform.python_version())
logging.debug("- Python build: %s", platform.python_build())
logging.debug("- System: %s", platform.system())
logging.debug("- OS Version: %s", platform.platform())
logging.debug("")
return True
except: # pragma: no cover
logging.exception("cannot display OS information")
return False
def logoToScreen():
"""!Prints the BOSWatch logo to the screen at debug level
@return True or False on error"""
try:
print(" ____ ____ ______ __ __ __ _____ ")
print(" / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / ")
print(" / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < ")
print(" / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / ")
print("/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ ")
print(" German BOS Information Script ")
print(" by Bastian Schroll ")
print("")
return True
except: # pragma: no cover
logging.exception("cannot display logo on screen")
return False
logging.debug("BOSWatch and environment information")
logging.debug("- Client version: %d.%d.%d",
version.client["major"],
version.client["minor"],
version.client["patch"])
logging.debug("- Server version: %d.%d.%d",
version.server["major"],
version.server["minor"],
version.server["patch"])
logging.debug("- Branch: %s",
version.branch)
logging.debug("- Release date: %02d.%02d.%4d",
version.date["day"],
version.date["month"],
version.date["year"])
logging.debug("- Python version: %s", platform.python_version())
logging.debug("- Python build: %s", platform.python_build())
logging.debug("- System: %s", platform.system())
logging.debug("- OS Version: %s", platform.platform())
logging.debug("")
return True

54
boswatch/utils/misc.py Normal file
View file

@ -0,0 +1,54 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: misc.py
@date: 11.03.2019
@author: Bastian Schroll
@description: Some misc functions
"""
import logging
from boswatch.utils import version
logging.debug("- %s loaded", __name__)
def addClientDataToPacket(bwPacket, config):
"""!Add the client information to the decoded data
This function adds the following data to the bwPacket:
- clientName
- clientVersion
- clientBuildDate
- clientBranch
- inputSource
- frequency"""
logging.debug("add client data to bwPacket")
bwPacket.set("clientName", config.get("client", "name"))
bwPacket.set("clientVersion", version.client)
bwPacket.set("clientBuildDate", version.date)
bwPacket.set("clientBranch", version.branch)
bwPacket.set("inputSource", config.get("client", "inputSource"))
bwPacket.set("frequency", config.get("inputSource", "sdr", "frequency"))
def addServerDataToPacket(bwPacket, config):
"""!Add the server information to the decoded data
This function adds the following data to the bwPacket:
- serverName
- serverVersion
- serverBuildDate
- serverBranch"""
logging.debug("add server data to bwPacket")
bwPacket.set("serverName", config.get("server", "name"))
bwPacket.set("serverVersion", version.server)
bwPacket.set("serverBuildDate", version.date)
bwPacket.set("serverBranch", version.branch)

View file

@ -20,12 +20,11 @@ import sys
logging.debug("- %s loaded", __name__)
# todo searching for root part is not a nice solution atm
# note searching for root part is not a nice solution atm
ROOT_PATH = os.path.dirname(sys.modules['boswatch'].__file__).replace("\\", "/") + "/../"
LOG_PATH = ROOT_PATH + "log/"
CONFIG_PATH = ROOT_PATH + "config/"
PLUGIN_PATH = ROOT_PATH + "plugins/"
CSV_PATH = ROOT_PATH + "csv/"
BIN_PATH = ROOT_PATH + "_bin/"
TEST_PATH = ROOT_PATH + "test/"
@ -39,11 +38,7 @@ def makeDirIfNotExist(dirPath):
@param dirPath: Path of the directory
@return Path of the directory or False"""
try:
if not os.path.exists(dirPath):
os.mkdir(dirPath)
logging.debug("directory created: %s", dirPath)
return dirPath
except: # pragma: no cover
logging.exception("error by creating a directory: %s", dirPath)
return False
if not os.path.exists(dirPath):
os.mkdir(dirPath)
logging.debug("directory created: %s", dirPath)
return dirPath

View file

@ -20,5 +20,5 @@ logging.debug("- %s loaded", __name__)
client = {"major": 3, "minor": 0, "patch": 0}
server = {"major": 3, "minor": 0, "patch": 0}
date = {"day": 1, "month": 1, "year": 2018}
date = {"day": 1, "month": 1, "year": 2019}
branch = "develop"

View file

@ -1,2 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

View file

@ -1,29 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: watchdog.py
@date: ##.##.2018
@author: Bastian Schroll
@description: Watchdog to _check if BOSWatch client, server, rtl_fm or multimon-ng is still running
"""
import logging
logging.debug("- %s loaded", __name__)
class Watchdog:
"""!Class for an Watchdog to observe,
if needed subprocess still running"""
def __init__(self):
"""!Create a new instance"""
pass

View file

@ -12,32 +12,43 @@
@file: wildcard.py
@date: 15.01.2018
@author: Bastian Schroll
@description: Little Helper to replace wildcards in stings
@todo not completed yet
@description: Functions to replace wildcards in stings
"""
import logging
import time
# from boswatch.module import file
logging.debug("- %s loaded", __name__)
# todo check function and document + write an test
_additionalWildcards = {}
def registerWildcard(wildcard, bwPacketField):
if wildcard in _additionalWildcards:
logging.error("wildcard always registered: %s", wildcard)
return
logging.debug("register new wildcard %s for field: %s", wildcard, bwPacketField)
_additionalWildcards[wildcard] = bwPacketField
def replaceWildcards(message, bwPacket):
_wildcards = {
# formatting wildcards
# todo check if br and par are needed - if not also change config
"{BR}": "\r\n",
"{LPAR}": "(",
"{RPAR}": ")",
"{TIME}": time.time(),
"{TIME}": time.strftime("%d.%m.%Y %H:%M:%S"),
# info wildcards
# server
"{SNAME}": bwPacket.getField("serverName"),
"{SVERS}": bwPacket.getField("serverVersion"),
"{SDATE}": bwPacket.getField("serverBuildDate"),
"{SBRCH}": bwPacket.getField("serverBranch"),
# client
"{CNAME}": bwPacket.getField("clientName"),
"{CIP}": bwPacket.getField("clientIP"),
"{CVERS}": bwPacket.getField("clientVersion"),
@ -49,8 +60,6 @@ def replaceWildcards(message, bwPacket):
"{TIMES}": bwPacket.getField("mode"),
"{FREQ}": bwPacket.getField("frequency"),
"{MODE}": bwPacket.getField("mode"),
"{DESCS}": bwPacket.getField("descriptionShort"),
"{DESCL}": bwPacket.getField("descriptionLong"),
# fms wildcards
"{FMS}": bwPacket.getField("fms"),
@ -75,10 +84,10 @@ def replaceWildcards(message, bwPacket):
# message for MSG packet is done in poc
}
for wildcard in _wildcards:
try:
message = message.replace(wildcard, _wildcards[wildcard])
except:
logging.exception("error in wildcard replacement")
for wildcard, field in _wildcards.items():
message = message.replace(wildcard, field)
for wildcard, field in _additionalWildcards.items():
message = message.replace(wildcard, bwPacket.getField(field))
return message

11
build_docu.sh Normal file
View file

@ -0,0 +1,11 @@
#!/bin/bash
echo "clean api documentation"
rm -r docu/docs/api/html
echo "generate api documentation"
doxygen docu/doxygen.ini
echo "build documentation"
source ./venv/bin/activate
python3 -m mkdocs build -f docu/mkdocs.yml
deactivate

View file

@ -14,99 +14,183 @@
@author: Bastian Schroll
@description: BOSWatch client application
"""
# pylint: disable=wrong-import-position
# pylint: disable=wrong-import-order
from boswatch.utils import paths
if not paths.makeDirIfNotExist(paths.LOG_PATH):
print("cannot find/create log directory: %s", paths.LOG_PATH)
exit(1)
try:
import logging
import logging.config
import logging.config
logging.config.fileConfig(paths.CONFIG_PATH + "logger_client.ini")
logging.debug("")
logging.debug("######################## NEW LOG ############################")
logging.debug("BOSWatch client has started ...")
logging.config.fileConfig(paths.CONFIG_PATH + "logger_client.ini")
logging.debug("")
logging.debug("######################## NEW LOG ############################")
logging.debug("BOSWatch client has started ...")
except Exception as e: # pragma: no cover
print("cannot load logger")
print(e)
logging.debug("Import python modules")
import argparse
logging.debug("- argparse")
import threading
logging.debug("- threading")
import queue
logging.debug("- queue")
import time
logging.debug("- time")
logging.debug("Import BOSWatch modules")
from boswatch.configYaml import ConfigYAML
from boswatch.network.client import TCPClient
from boswatch.network.broadcast import BroadcastClient
from boswatch.processManager import ProcessManager
from boswatch.decoder.decoder import Decoder
from boswatch.utils import header
from boswatch.utils import misc
header.logoToLog()
header.infoToLog()
# With -h or --help you get the Args help
parser = argparse.ArgumentParser(prog="bw_client.py",
description="""BOSWatch is a Python Script to receive and
decode german BOS information with rtl_fm and multimon-NG""",
epilog="""More options you can find in the extern client.ini
file in the folder /config""")
parser.add_argument("-c", "--config", help="Name to configuration File", required=True)
parser.add_argument("-t", "--test", help="Start Client with testdata-set", action="store_true")
args = parser.parse_args()
bwConfig = ConfigYAML()
if not bwConfig.loadConfigFile(paths.CONFIG_PATH + args.config):
logging.error("cannot load config file")
exit(1)
try:
logging.debug("Import python modules")
import argparse
logging.debug("- argparse")
import subprocess
logging.debug("- subprocess")
# following is temp for testing
import time
logging.debug("Import BOSWatch modules")
from boswatch.config import Config
from boswatch.network.client import TCPClient
from boswatch.decoder.decoder import Decoder
from boswatch.utils import header
except Exception as e: # pragma: no cover
logging.exception("cannot import modules")
print("cannot import modules")
print(e)
exit(1)
# ========== CLIENT CODE ==========
mmThread = None
bwClient = None
try:
header.logoToLog()
header.infoToLog()
header.logoToScreen()
ip = bwConfig.get("server", "ip", default="127.0.0.1")
port = bwConfig.get("server", "port", default="8080")
logging.debug("parse args")
# With -h or --help you get the Args help
parser = argparse.ArgumentParser(prog="bw_client.py",
description="""BOSWatch is a Python Script to receive and
decode german BOS information with rtl_fm and multimon-NG""",
epilog="""More options you can find in the extern client.ini
file in the folder /config""")
parser.add_argument("-c", "--config", help="Name to configuration File", required=True)
parser.add_argument("-t", "--test", help="Client will send some testdata", action="store_true") # todo implement testmode
args = parser.parse_args()
if bwConfig.get("client", "useBroadcast", default=False):
broadcastClient = BroadcastClient()
if broadcastClient.getConnInfo():
ip = broadcastClient.serverIP
port = broadcastClient.serverPort
bwConfig = Config()
if bwConfig.loadConfigFile(paths.CONFIG_PATH + args.config, "clientConfig") is False:
logging.exception("cannot load config file")
print("cannot load config file")
exit(1) # without config cannot _run
# ========== INPUT CODE ==========
def handleSDRInput(dataQueue, sdrConfig, decoderConfig): # todo exception handling inside
sdrProc = ProcessManager(str(sdrConfig.get("rtlPath", default="rtl_fm")))
sdrProc.addArgument("-d " + str(sdrConfig.get("device", default="0"))) # device id
sdrProc.addArgument("-f " + sdrConfig.get("frequency")) # frequencies
sdrProc.addArgument("-p " + str(sdrConfig.get("error", default="0"))) # frequency error in ppm
sdrProc.addArgument("-l " + str(sdrConfig.get("squelch", default="1"))) # squelch
sdrProc.addArgument("-g " + str(sdrConfig.get("gain", default="100"))) # gain
sdrProc.addArgument("-M fm") # set mode to fm
sdrProc.addArgument("-E DC") # set DC filter
sdrProc.addArgument("-s 22050") # bit rate of audio stream
sdrProc.setStderr(open(paths.LOG_PATH + "rtl_fm.log", "a"))
sdrProc.start()
# sdrProc.skipLinesUntil("Output at")
mmProc = ProcessManager(str(sdrConfig.get("mmPath", default="multimon-ng")), textMode=True)
if decoderConfig.get("fms", default=0):
mmProc.addArgument("-a FMSFSK")
if decoderConfig.get("zvei", default=0):
mmProc.addArgument("-a ZVEI1")
if decoderConfig.get("poc512", default=0):
mmProc.addArgument("-a POCSAG512")
if decoderConfig.get("poc1200", default=0):
mmProc.addArgument("-a POCSAG1200")
if decoderConfig.get("poc2400", default=0):
mmProc.addArgument("-a POCSAG2400")
mmProc.addArgument("-f alpha")
mmProc.addArgument("-t raw -")
mmProc.setStdin(sdrProc.stdout)
mmProc.setStderr(open(paths.LOG_PATH + "multimon-ng.log", "a"))
mmProc.start()
# mmProc.skipLinesUntil("Available demodulators:")
logging.info("start decoding")
while inputThreadRunning:
if not sdrProc.isRunning:
logging.warning("rtl_fm was down - try to restart")
sdrProc.start()
# sdrProc.skipLinesUntil("Output at") # last line form rtl_fm before data
elif not mmProc.isRunning:
logging.warning("multimon was down - try to restart")
mmProc.start()
# mmProc.skipLinesUntil("Available demodulators:") # last line from mm before data
elif sdrProc.isRunning and mmProc.isRunning:
line = mmProc.readline()
if line:
dataQueue.put_nowait((line, time.time()))
logging.debug("Add data to queue")
print(line)
logging.debug("stopping thread")
mmProc.stop()
sdrProc.stop()
# ========== INPUT CODE ==========
inputQueue = queue.Queue()
if not args.test:
inputThreadRunning = True
mmThread = threading.Thread(target=handleSDRInput, name="mmReader",
args=(inputQueue, bwConfig.get("inputSource", "sdr"), bwConfig.get("decoder")))
mmThread.daemon = True
mmThread.start()
else:
logging.warning("STARTING TESTMODE!")
logging.debug("reading testdata from file")
testFile = open("test/testdata.list", mode="r", encoding="utf-8")
for testData in testFile:
if (len(testData.rstrip(' \t\n\r')) > 1) and ("#" not in testData[0]):
logging.info("Testdata: %s", testData.rstrip(' \t\n\r'))
inputQueue.put_nowait((testData.rstrip(' \t\n\r'), time.time()))
logging.debug("finished reading testdata")
bwClient = TCPClient()
if bwClient.connect(bwConfig.getStr("Server", "IP"), bwConfig.getInt("Server", "PORT")):
bwClient.connect(ip, port)
while 1:
while 1:
for i in range(0, 5):
time.sleep(1)
print("Alarm Nr #" + str(i))
if not bwClient.isConnected:
reconnectDelay = bwConfig.get("client", "reconnectDelay", default="3")
logging.warning("connection to server lost - sleep %d seconds", reconnectDelay)
time.sleep(reconnectDelay)
bwClient.connect(ip, port)
data = "ZVEI1: 12345"
bwPacket = Decoder.decode(data)
elif not inputQueue.empty():
data = inputQueue.get()
logging.info("get data from queue (waited %0.3f sec.)", time.time() - data[1])
logging.debug("%s packet(s) still waiting in queue", inputQueue.qsize())
if bwPacket:
bwPacket.printInfo()
bwPacket.addClientData()
bwClient.transmit(str(bwPacket))
bwPacket = Decoder.decode(data[0])
inputQueue.task_done()
# todo should we do this in an thread, to not block receiving ???
# todo but then we should use transmit() and receive() with Lock()
failedTransmits = 0
while not bwClient.receive() == "[ack]": # wait for ack or timeout
if failedTransmits >= 3:
logging.error("cannot transmit after 5 retires")
break
failedTransmits += 1
logging.warning("attempt %d to resend packet", failedTransmits)
bwClient.transmit(str(bwPacket)) # try to resend
if bwPacket is None:
continue
bwPacket.printInfo()
misc.addClientDataToPacket(bwPacket, bwConfig)
for sendCnt in range(bwConfig.get("client", "sendTries", default="3")):
bwClient.transmit(str(bwPacket))
if bwClient.receive() == "[ack]":
logging.debug("ack ok")
break
sendDelay = bwConfig.get("client", "sendDelay", default="3")
logging.warning("cannot send packet - sleep %d seconds", sendDelay)
time.sleep(sendDelay)
else:
if args.test:
break
time.sleep(0.1) # reduce cpu load (wait 100ms)
# in worst case a packet have to wait 100ms until it will be processed
bwClient.disconnect()
break
# test for server ####################################
except KeyboardInterrupt: # pragma: no cover
logging.warning("Keyboard interrupt")
@ -114,5 +198,11 @@ except SystemExit: # pragma: no cover
logging.error("BOSWatch interrupted by an error")
except: # pragma: no cover
logging.exception("BOSWatch interrupted by an error")
finally: # pragma: no cover
logging.debug("BOSWatch has ended ...")
finally:
logging.debug("Starting shutdown routine")
if bwClient:
bwClient.disconnect()
inputThreadRunning = False
if mmThread:
mmThread.join()
logging.debug("BOSWatch client has stopped ...")

View file

@ -14,148 +14,95 @@
@author: Bastian Schroll
@description: BOSWatch server application
"""
# pylint: disable=wrong-import-position
# pylint: disable=wrong-import-order
from boswatch.utils import paths
if not paths.makeDirIfNotExist(paths.LOG_PATH):
print("cannot find/create log directory: %s", paths.LOG_PATH)
exit(1)
try:
import logging
import logging.config
print(paths.CONFIG_PATH + "logger_server.ini")
logging.config.fileConfig(paths.CONFIG_PATH + "logger_server.ini")
logging.debug("")
logging.debug("######################## NEW LOG ############################")
logging.debug("BOSWatch server has started ...")
except Exception as e: # pragma: no cover
print("cannot load logger")
print(e)
import logging.config
logging.config.fileConfig(paths.CONFIG_PATH + "logger_server.ini")
logging.debug("")
logging.debug("######################## NEW LOG ############################")
logging.debug("BOSWatch server has started ...")
logging.debug("Import python modules")
import argparse
logging.debug("- argparse")
import queue
logging.debug("- queue")
import time
logging.debug("- time")
logging.debug("Import BOSWatch modules")
from boswatch.configYaml import ConfigYAML
from boswatch.network.server import TCPServer
from boswatch.packet import Packet
from boswatch.utils import header
from boswatch.network.broadcast import BroadcastServer
from boswatch.router.routerManager import RouterManager
from boswatch.utils import misc
header.logoToLog()
header.infoToLog()
# With -h or --help you get the Args help
parser = argparse.ArgumentParser(prog="bw_server.py",
description="""BOSWatch is a Python Script to receive and
decode german BOS information with rtl_fm and multimon-NG""",
epilog="""More options you can find in the extern client.ini
file in the folder /config""")
parser.add_argument("-c", "--config", help="Name to configuration File", required=True)
args = parser.parse_args()
bwConfig = ConfigYAML()
if not bwConfig.loadConfigFile(paths.CONFIG_PATH + args.config):
logging.fatal("cannot load config file")
exit(1)
# ############################# begin server system
bwRoutMan = None
bwServer = None
bcServer = None
try:
logging.debug("Import python modules")
import argparse
logging.debug("- argparse")
# following is temp for testing
import time
import sys
import threading
import threading
bwRoutMan = RouterManager()
if not bwRoutMan.buildRouters(bwConfig):
logging.fatal("Error while building routers")
exit(1)
logging.debug("Import BOSWatch modules")
from boswatch.config import Config
from boswatch.network.server import TCPServer
from boswatch.packet.packet import Packet
from boswatch.plugin.pluginManager import PluginManager
from boswatch.descriptor.descriptor import Descriptor
from boswatch.filter.doubeFilter import DoubleFilter
from boswatch.utils import header
except Exception as e: # pragma: no cover
logging.exception("cannot import modules")
print("cannot import modules")
print(e)
exit(1)
bcServer = BroadcastServer()
if bwConfig.get("server", "useBroadcast", default=False):
bcServer.start()
try:
header.logoToLog()
header.infoToLog()
header.logoToScreen()
logging.debug("parse args")
# With -h or --help you get the Args help
parser = argparse.ArgumentParser(prog="bw_server.py",
description="""BOSWatch is a Python Script to receive and
decode german BOS information with rtl_fm and multimon-NG""",
epilog="""More options you can find in the extern client.ini
file in the folder /config""")
parser.add_argument("-c", "--config", help="Name to configuration File", required=True)
args = parser.parse_args()
bwConfig = Config()
if bwConfig.loadConfigFile(paths.CONFIG_PATH + args.config, "serverConfig") is False:
logging.exception("cannot load config file")
print("cannot load config file")
exit(1) # without config cannot run
bwPluginManager = PluginManager()
bwPluginManager.searchPluginDir()
bwPluginManager.importAllPlugins()
bwPluginManager.loadAllPlugins()
bwDescriptor = Descriptor()
if bwConfig.getBool("Description", "fms"):
bwDescriptor.loadDescription("fms")
if bwConfig.getBool("Description", "pocsag"):
bwDescriptor.loadDescription("pocsag")
if bwConfig.getBool("Description", "zvei"):
bwDescriptor.loadDescription("zvei")
bwDoubleFilter = DoubleFilter()
serverPaused = False # flag to pause alarming of server
serverStop = False # flag to stop the whole server application
# server flags test code
# ----------------------
# import threading
# def eins():
# global serverPaused
# serverPaused = True
# def zwei():
# global serverPaused
# serverPaused = False
def drei():
global serverStop
serverStop = True
#
# t1 = threading.Timer(1, eins)
# t2 = threading.Timer(5, zwei)
t3 = threading.Timer(15, drei)
# t1.start()
# t2.start()
t3.start()
bwServer = TCPServer(bwConfig.getInt("Server", "PORT"))
incomingQueue = queue.Queue()
bwServer = TCPServer(incomingQueue)
if bwServer.start():
while 1:
if serverPaused:
logging.warning("Server pause flag received ...")
logging.debug("But all received packages will be cached!")
packetsOld = 0
while serverPaused is True:
time.sleep(0.2) # reduce cpu load (run all 200ms)
packetsNew = bwServer.countPacketsInQueue()
if packetsNew is not packetsOld:
logging.debug("%s packet(s) waiting in queue", packetsNew)
packetsOld = packetsNew
logging.warning("Server resumed ...")
if incomingQueue.empty(): # pause only when no data
time.sleep(0.1) # reduce cpu load (wait 100ms)
# in worst case a packet have to wait 100ms until it will be processed
if serverStop:
logging.warning("Server stop flag received ...")
break
else:
data = incomingQueue.get()
if not bwServer.countPacketsInQueue(): # pause only when no data
time.sleep(0.1) # reduce cpu load (run all 100ms)
data = bwServer.getDataFromQueue()
if data is not None:
logging.info("get data from %s (waited in queue %0.3f sec.)", data[0], time.time() - data[2])
logging.debug("%s packet(s) waiting in queue", bwServer.countPacketsInQueue())
logging.debug("%s packet(s) still waiting in queue", incomingQueue.qsize())
bwPacket = Packet((data[1]))
if not bwDoubleFilter.filter(bwPacket):
continue
bwPacket.set("clientIP", data[0])
bwPacket.addServerData()
misc.addServerDataToPacket(bwPacket, bwConfig)
if bwConfig.getBool("Description", bwPacket.get("mode")):
bwDescriptor.addDescriptions(bwPacket)
logging.debug("[ --- ALARM --- ]")
bwRoutMan.runRouters(bwConfig.get("alarmRouter"), bwPacket)
logging.debug("[ --- END ALARM --- ]")
bwPluginManager.runAllPlugins(bwPacket)
# print(bwPacket.get("clientVersion")["major"])
incomingQueue.task_done()
except KeyboardInterrupt: # pragma: no cover
logging.warning("Keyboard interrupt")
@ -163,15 +110,12 @@ except SystemExit: # pragma: no cover
logging.error("BOSWatch interrupted by an error")
except: # pragma: no cover
logging.exception("BOSWatch interrupted by an error")
finally: # pragma: no cover
# try-except-blocks are necessary because there is a change that the vars
# bwServer or bwPluginManager are not defined in case of an early error
try:
finally:
logging.debug("Starting shutdown routine")
if bwRoutMan:
bwRoutMan.cleanup()
if bwServer:
bwServer.stop()
except: # pragma: no cover
pass
try:
bwPluginManager.unloadAllPlugins()
except: # pragma: no cover
pass
logging.debug("BOSWatch has ended ...")
if bcServer:
bcServer.stop()
logging.debug("BOSWatch server has stopped ...")

View file

@ -1,49 +0,0 @@
# -*- coding: utf-8 -*-
# ____ ____ ______ __ __ __ _____
# / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
# / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
# / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
#/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
# German BOS Information Script
# by Bastian Schroll
[Server]
# connection params to BOSWatch alarm server
IP = 127.0.0.1
PORT = 8080
# you can set multiple servers (one line for each)
# the alarm data are served to all server simultane
# for each server you can set one backup server that be used if master is not reachable
# {indv. name} = {Master}:{PORT};{Slave}:{PORT}
# Name Master Port|Slave Port
server1 = 127.0.0.1:8080
serverTest = 127.0.0.1:1234;192.168.178.1:1234
[Client]
# information about this BOSWatch client instance
Name = BW3 Client
# choose input source for multimon
# 'stick' or 'audio'
InputSource = stick
[Stick]
# configuration for your DVB-T stick
Device = 0
Frequency = 85.000M
PPMError = 0
Squelch = 0
Gain = 100
[Audio]
# configuration for audio input
[Decoder]
# here you can enable needed decoders
FMS = 0
ZVEI = 0
POC512 = 0
POC1200 = 0
POC2400 = 0

37
config/client.yaml Normal file
View file

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# ____ ____ ______ __ __ __ _____
# / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
# / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
# / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
#/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
# German BOS Information Script
# by Bastian Schroll
client:
name: BW3 Client # name of the BW3 Client instance
inputSource: sdr # atm only 'sdr' is possible
useBroadcast: no # use broadcast to find server automatically
reconnectDelay: 3 # time in seconds to delay reconnect try
sendTries: 3 # how often should tried to send a packet
sendDelay: 3 # time in seconds to delay the resend try
server: # only used if useBroadcast = no
ip: 127.0.0.1
port: 8080
inputSource:
sdr:
device: 0
frequency: 85M
error: 0
squelch: 1
gain: 100
rtlPath: /usr/bin/rtl_fm
mmPath: /opt/multimon/multimon-ng
decoder:
fms: yes
zvei: yes
poc512: yes
poc1200: yes
poc2400: yes

View file

@ -22,7 +22,7 @@ format=%(asctime)s,%(msecs)03d - %(module)-15s [%(levelname)-8s] %(message)s
datefmt=%d.%m.%Y %H:%M:%S
[formatter_complex]
format=%(asctime)s,%(msecs)03d - %(module)-15s %(funcName)-18s [%(levelname)-8s] %(message)s
format=%(asctime)s,%(msecs)03d - %(threadName)-15s %(module)-15s %(funcName)-18s [%(levelname)-8s] %(message)s
datefmt=%d.%m.%Y %H:%M:%S
[handlers]

View file

@ -22,7 +22,7 @@ format=%(asctime)s,%(msecs)03d - %(module)-15s [%(levelname)-8s] %(message)s
datefmt=%d.%m.%Y %H:%M:%S
[formatter_complex]
format=%(asctime)s,%(msecs)03d - %(module)-15s %(funcName)-18s [%(levelname)-8s] %(message)s
format=%(asctime)s,%(msecs)03d - %(threadName)-15s %(module)-15s %(funcName)-18s [%(levelname)-8s] %(message)s
datefmt=%d.%m.%Y %H:%M:%S
[handlers]

View file

@ -1,60 +0,0 @@
# -*- coding: utf-8 -*-
# ____ ____ ______ __ __ __ _____
# / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
# / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
# / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
#/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
# German BOS Information Script
# by Bastian Schroll
[Server]
PORT = 8080
Name = BW3 Server
[FMS]
UseLists =
Allowed =
Denied =
RegEx =
[POCSAG]
UseLists =
Allowed =
Denied =
Range =
RegEx =
[ZVEI]
UseLists =
Allowed =
Denied =
Range =
RegEx =
[Filter]
UseDoubleFilter = 0
UseRegexFilter = 0
[doubleFilter]
# max number of entrys to save for comparison
MaxEntry = 30
# time to ignore same alarm in seconds
IgnoreTime = 10
# include pocsag msg in comparison
CheckMsg = 0
[regexFilter]
[Description]
# load CSV description files with short and lon description
fms = 0
pocsag = 0
zvei = 0
[Plugins]
# here you can enable needed plugins
# 0 is disabled
# all greater than 0 enable the plugin
# the higher the number the earlier the plugin is called on alarm
# we call ist Plugin Prioority
template = 1

30
config/server.yaml Normal file
View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# ____ ____ ______ __ __ __ _____
# / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
# / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
# / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
#/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
# German BOS Information Script
# by Bastian Schroll
server:
port: 8080
name: BW3 Server # name of the BW3 Server instance
useBroadcast: no # serve server ip on broadcast request
alarmRouter:
- Router 1
router:
- name: Router 1
route:
- type: module
res: filter.modeFilter
name: Filter Fms/Zvei
config:
allowed:
- fms
- zvei
- type: plugin
name: test plugin
res: template_plugin

View file

@ -1,12 +0,0 @@
id,shortDescription,longDescription
#
# BOSWatch CSV file for describing FMS-Addresses
#
# For each FMS-Address you could set a description-text
# Use the structure: fms,"Description-Text"
#
# !!! DO NOT delete the first line !!!
#
12345678,"FMS testdata äöüß"
56487234,"Test Nummer 18",""
68734123,"Und noch einer ganz schnell",""
Can't render this file because it has a wrong number of fields in line 2.

View file

@ -1,18 +0,0 @@
id,shortDescription,longDescription
#
# BOSWatch CSV file for describing POCSAG-Addresses
#
# For each RIC-Address you could set a description-text
# Use the structure: ric,"Description-Text"
#
# You can even define specific subrics, therefore you
# 1. need to specify a main RIC: 1234567, "Unit One"
# 2. specify a certain subric: 1234567B, "Subunit Bravo"
# The result for 1234567B will be "Unit One Subunit Bravo"
# - Be sure having defined the main RIC (step one)! -
#
# !!! DO NOT delete the first line !!!
#
1234567,"POCSAG testdata äöüß"
4323424,"Test Nummer 18",""
6873423,"Und noch einer ganz schnell",""
Can't render this file because it has a wrong number of fields in line 2.

View file

@ -1,12 +0,0 @@
id,shortDescription,longDescription
#
# BOSWatch CSV file for describing ZVEI-Addresses
#
# For each ZVEI-Address you could set a description-text
# Use the structure: zvei,"Description-Text"
#
# !!! DO NOT delete the first line !!!
#
12345,"ZVEI testdata äöüß","Hier nochmal eine echt Lange Info die keinen Sinn macht"
56487,"Test Nummer 18",""
68734,"Und noch einer ganz schnell",""
Can't render this file because it has a wrong number of fields in line 2.

32
docu/docs/changelog.md Normal file
View file

@ -0,0 +1,32 @@
# <center>Changelog</center>
---
## Version [2.9.0] - date
Functions implemented in initial version:
- Multithreaded Server/Client infrastructure for alarm handling
- Client can auto fetch connection information over bradocast from server
- Easy configuration with YAML file for all components
- Simple module and plugin system to extend functionality
- Alarmpacket routing system for flexible chains of modules nd plugins
### Modules
- Mode filter to filter at specific packet types such as FMS, POCSAG, ZVEI or MSG packets
### Filter
---
Zum schreiben des Changelog's siehe: [http://keepachangelog.com/de/1.0.0/](http://keepachangelog.com/de/1.0.0/)
<!--
## Version [#.#.#] - date
### Added
### Changed
### Deprecated
### Removed
### Fixed
### Security
-->

133
docu/docs/config.md Normal file
View file

@ -0,0 +1,133 @@
# <center>Konfiguration</center>
Die Konfiguration von BOSWatch 3 ist im YAML Format abgelegt und wird nachfolgend beschrieben.
Immer wenn für eine Einstellung ein **Default** Wert angegeben ist, muss diese Einstellung nicht
zwingend in die Konfiguration eingetragen werden.
## Client
---
### `client:`
|Feld|Beschreibung|Default|
|----|------------|-------|
|name|Name zur Identifizierung der Client Instanz||
|inputSource|Art der zu nutzenden Input Quelle (aktuell nur `sdr`)|sdr|
|useBroadcast|Verbindungsdaten per [Broadcast](information/broadcast.md) beziehen|no|
|reconnectDelay|Verzögerung für erneuten Verbindungsversuch zum Server|3|
|sendTries|Anzahl der Sendeversuche eines Pakets|3|
|sendDelay|Verzögerung für einen erneuten Sendeversuch|3|
---
### `server:`
Der Abschnitt `server:` wird nur genutzt, wenn `useBroadcast: no` gesetzt ist.
Ansonsten wird versucht die Verbindungsdaten per Broadcast Paket direkt vom Server zu beziehen.
|Feld|Beschreibung|Default|
|----|------------|-------|
|ip|IP Adresse des Servers|127.0.0.1|
|port|Port des Sever|8080|
**Beispiel:**
```yaml
server:
ip: 10.10.10.2
port: 9123
```
---
### `inputSource:`
Aktuell gibt es nur `sdr:` als Input Quelle
#### `sdr:`
|Feld|Beschreibung|Default|
|----|------------|-------|
|device|rtl_fm Device ID|0|
|frequency|Zu empfangende Frequenz||
|error|Frequenz Abweichung in ppm|0|
|squelch|Einstellung der Rauschsperre|1|
|gain|Verstärkung des Eingangssignals|100|
|rtlPath|Pfad zur rtl_fm Binary|rtl_fm|
|mmPath|Pfad zur multimon-ng Binary|multimon-ng|
**Beispiel:**
```yaml
inputSource:
sdr:
device: 0
frequency: 85M
error: 0
squelch: 1
gain: 100
rtlPath: /usr/bin/rtl-fm
mmPath: /opt/multimon/multimon-ng
```
---
### `decoder:`
|Feld|Beschreibung|Default|
|----|------------|-------|
|fms|FMS Decoder|no|
|zvei|ZVEI Decoder|no|
|poc512|POCSAG Decoder (Bitrate 512)|no|
|poc1200|POCSAG Decoder (Bitrate 1200)|no|
|poc2400|POCSAG Decoder (Bitrate 2400)|no|
---
## Server
Nachfolgend alle Paramater der Server Konfiguration
### `server:`
|Feld|Beschreibung|Default|
|----|------------|-------|
|port|Port auf dem der Server lauscht|8080
|name|Name zur Identifizierung der Server Instanz||
|useBroadcast|Verbindungsdaten per Broadcast Server bereitstellen|no|
---
### `alarmRouter:`
Enthält eine Liste der Router Namen, welche bei einem Alarm direkt gestartet werden sollen.
**Beispiel:**
```yaml
alarmRouter:
- Name des Routers
- ein weiter Router
```
---
### `router:`
Mit den Routern kann der Verarbeitungsweg eines Alarm-Paketes festgelegt werden. Es können beliebig viele Router in Form einer Liste angegeben werden.
|Feld|Beschreibung|Default|
|----|------------|-------|
|name|Name des Routers||
|route|Definiten des Routenverlaufs
#### `route:`
Jeder Router kann eine beliebige Anzahl einzelner Routenpunkte enthalten. Diese werden innerhalb des Routers sequentiel abgearbeitet. Mögliche Typen der Routenpunkte sind dabei ein Modul, ein Plugin oder ein anderer Router. Sie werden ebenfalls in Form einer Liste definiert.
|Feld|Beschreibung|Default|
|----|------------|-------|
|type|Art des Routenpunktes (module, plugin, router)||
|res|Zu ladende Resource (Siehe entsprechende Kapitel)||
|name|Optionaler Name des Routenpunktes|gleich wie Resource|
|config|Konfigurationseinstellungen des Routenpunktes (Siehe entsprechende Kapitel)||
**Beispiel:**
```yaml
router:
- name: Router 1
route:
- type: module
res: filter.modeFilter
name: Filter Fms/Zvei
config:
allowed:
- fms
```
---
## Module/Plugins
Die möglichen Einstellungen der einzelnen Module und Plugins sind im jeweiligen Kapitel aufgelistet.

View file

@ -0,0 +1,133 @@
# <center>Eigenes Modul/Plugin schreiben</center>
Um ein eigenes Modul oder Plugin zu schreiben, sollte man sich am besten zuerst einmal das das `template` im entsprechenden Ordner ansehen. Dies kann als Vorlage für das eigene Modul oder Plugin genutzt werden.
---
## Allgemeine Informationen
Im ersten Schritt sollte eine Kopie des jeweiligen Templates (Modul oder Plugin) erstellt werden. Nun sollten im Dateikopf die Angaben angepasst werden.
---
## Benötigte Methoden überschreiben
### Modul
Die Modul Basisklasse bietet einige Methoden, welche vom Modul überschrieben werden können.
- `onLoad()` wird direkt beim Import des Moduls ausgeführt
- `doWork(bwPacket)` wird bei der Ausführung aufgerufen
- `onUnload()` wird beim Zerstören der Plugin Modul zum Programmende ausgeführt
### Plugin
Die Plugin Basisklasse bietet einige Methoden, welche vom Plugin überschrieben werden können.
- `onLoad()` wird direkt beim Import des Plugins ausgeführt
- `setup()` wird vor jeder Ausführung gerufen
- `fms(bwPacket)` wird bei einem FMS Paket ausgeführt
- `pocsag(bwPacket)` wird bei einem POCSAG Paket ausgeführt
- `zvei(bwPacket)` wird bei einem ZVEI Packet ausgeführt
- `msg(bwPacket)` wird bei einem Nachrichten Packet ausgeführt
- `teardown()` wird nach jeder Ausführung gerufen
- `onUnload()` wird beim Zerstören der Plugin Instanz zum Programmende ausgeführt
---
## Konfiguration
### Konfiguration anlegen
Jedes Modul oder Plugin wird in einem Router folgendermaßen deklariert:
```yaml
- type: module # oder 'plugin'
res: template_module # Name der Python Datei (ohne .py)
name: Mein Modul # optionaler Name
config: # config-Sektion
option1: value 1
option2:
underOption1: value 21
underOption2: value 22
list:
- list 1
- list 2
```
Eine entsprechende Dokumentation der Parameter **muss** in der Dokumentation des jeweiligen Moduls oder Plugins hinterleget werden.
### Konfiguration verwenden
Wird der Instanz eine Konfiguration übergeben wird diese in `self.config` abgelegt und kann wie folgt abgerufen werden:
(Dies Ergebnisse beziehen sich auf das Konfigurationsbeispiel oben)
- Einzelnes Feld auslesen
`self.config.get("option1")`
> liefert `value 1`
- Verschachteltes Feld auslesen (beliebige tiefe möglich)
`self.config.get("option2", "underOption1")`
> liefert `value 21`
- Es kann ein Default Wert angegeben werden (falls entsprechender Eintrag fehlt)
`self.config.get("notSet", default="defValue")`
> liefert `defValue`
- Über Listen kann einfach iteriert werden
`for item in self.config.get(FIELD):`
> liefert ein Element je Iteration - hier `list 1` und `list 2`
Wird ein End-Wert ausgelesen, wird dieser direkt zurück gegeben.
Sollten weitere Unterelemente oder eine Liste exisitieren wird erneut ein Objekt der Klasse `Config()` zurück gegeben, auf welches wiederum nach obigem Schema zugegriffen werden kann.
---
## Arbeiten mit dem bwPacket
An das Modul bzw. Plugin wird eine Instanz eines BOSWatch-Paket Objekts übergeben.
Aus dieser kann mittels `bwPacket.get(FIELDNAME)` das entsprechende Feld ausgelesen werden.
Mittels `bwPacket.set(FIELDNAME, VALUE)` kann ein Wert hinzugefügt oder modifiziert werden.
Eine Auflistung der bereitgestellten Informationen findet sich im entsprechenden [BOSWatch Paket](packet.md) Dokumentation.
**Bitte beachten:**
- Selbst vom Modul hinzugefügte Felder **müssen** in der Modul Dokumentation unter `Paket Modifikation` aufgeführt werden.
- Sollte ein Modul oder Plugin Felder benutzen, welche in einem anderen Modul erstellt werden, **muss** dies im Punkt `Abhänigkeiten` des jeweiligen Moduls oder Plugins dokumentiert werden.
### Rückgabewert bei Modulen
Module können Pakete beliebig verändern. Diese Änderungen werden im Router entsprechend weitergeleitet.
Mögliche Rückgabewerte eines Moduls:
- `return bwPacket` Gibt das modifizierte bwPacket an den Router zurück (Paket Modifikation)
- `return None` Der Router fährt mit dem unveränderten bwPacket fort (Input = Output)
- `return False` Der Router stopt sofort die Ausführung (zB. in Filtern verwendet)
### Rückgabewert bei Plugins
Plugins geben keine Pakete mehr zurück. Sie fungieren ausschließlich als Endpunkt.
Die Plugin Basisklasse liefert intern immer ein `None` an den Router zurück,
was zur weiteren Ausführung des Routers mit dem original Paket führt. Daher macht es in Plugins keinen Sinn ein Paket zu modifizieren.
---
## Nutzung der Wildcards
Es gibt einige vordefinierte Wildcards welche in der [BOSWatch Paket](packet.md) Dokumentation zu finden sind.
Außerdem sind die folgenden allgemeinen Wildcards definiert:
- `{BR}` - Zeilenumbruch `\r\n`
- `{LPAR}` - öffnende Klammer `(`
- `{RPAR}` - schließende Klammer `)`
- `{TIME}` - Aktueller Zeitstempel im Format `%d.%m.%Y %H:%M:%S`
### Wildcards registrieren [Module]
Module können zusätzliche Wildcards registrieren welche anschließend in den Plugins ebenfalls geparst werden können.
Dies kann über die interne Methode `self.registerWildcard(newWildcard, bwPacketField)` gemacht werden.
- `newWildcard` muss im folgenden Format angegeben werden: `{WILDCARD}`
- `bwPacketField` ist der Name des Feldes im bwPacket - gestezt per `bwPacket.set(FIELDNAME, VALUE)`
**Bitte beachten:**
- Selbst vom Modul registrierte Wildcards **müssen** in der Modul Dokumentation unter `Zusätzliche Wildcards` aufgeführt werden.
### Wildcards parsen [Plugins]
Das parsen der Wildcars funktioniert komfortabel über die interne Methode `msg = self.parseWildcards(msg)`.
- `msg` enstrpicht dabei dem String in welchem die Wildcards ersetzt werden sollen
Die Platzhalter der Wildcards findet man in der [BOSWatch Paket](packet.md) Dokumentation.
Sollten Module zusätzliche Wildcards registrieren, findet man Informationen dazu in der jeweiligen Plugin Dokumentation
---
## Richtiges Logging
tbd ...

View file

@ -0,0 +1,51 @@
# <center>BOSWatch Paket Format</center>
Ein BOSWatch Datenpaket wird in einem Python Dict abgebildet. In der nachfolgenden Tabelle sind die genutzten Felder abgebildet.
---
## Allgemeine Informationen
|Feldname|FMS|POCSAG|ZVEI|MSG|Wildcard|Beschreibung|
|--------|:-:|:----:|:--:|:-:|--------|------------|
|serverName|X|X|X|X|`{SNAME}`|Name der BOSWatch Server Instanz|
|serverVersion|X|X|X|X|`{SVERS}`||
|serverBuildDate|X|X|X|X|`{SDATE}`||
|serverBranch|X|X|X|X|`{SBRCH}`||
|clientName|X|X|X|X|`{CNAME}`|Name der BOSWatch Client Instanz|
|clientIP|X|X|X|X|`{CIP}`||
|clientVersion|X|X|X|X|`{CVERS}`||
|clientBuildDate|X|X|X|X|`{CDATE}`||
|clientBranch|X|X|X|X|`{CBRCH}`||
|inputSource|X|X|X|X|`{INSRC}`|(sdr, audio)|
|timestamp|X|X|X|X|`{TIMES}`||
|frequency|X|X|X|X|`{FREQ}`||
|mode|X|X|X|X|`{MODE}`|(fms, pocsag, zvei, msg)|
---
## Speziell für POCSAG
|Feldname|FMS|POCSAG|ZVEI|MSG|Wildcard|Beschreibung|
|--------|:-:|:----:|:--:|:-:|--------|------------|
|bitrate||X|||`{BIT}`||
|ric||X|||`{RIC}`||
|subric||X|||`{SRIC}`|(1, 2, 3, 4)|
|subricText||X|||`{SRICT}`|(a, b, c, d)|
|message||X||X|`{MSG}`|Kann außerdem für ein Message Paket genutzt werden|
---
## Speziell für ZVEI
|Feldname|FMS|POCSAG|ZVEI|MSG|Wildcard|Beschreibung|
|--------|:-:|:----:|:--:|:-:|--------|------------|
|tone|||X||`{TONE}`|5-Ton Sequenz nach ZVEI|
---
## Speziell für FMS
|Feldname|FMS|POCSAG|ZVEI|MSG|Wildcard|Beschreibung|
|--------|:-:|:----:|:--:|:-:|--------|------------|
|fms|X||||`{FMS}`||
|service|X||||`{SERV}`||
|country|X||||`{COUNT}`||
|location|X||||`{LOC}`||
|vehicle|X||||`{VEC}`||
|status|X||||`{STAT}`||
|direction|X||||`{DIR}`||
|dirextionText|X||||`{DIRT}`|(Fhz->Lst, Lst->Fhz)|
|tacticalInfo|X||||`{TACI}`|(I, II, III, IV)|

View file

@ -0,0 +1 @@
<mxfile modified="2019-09-20T09:41:55.464Z" host="www.draw.io" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36" etag="JWctItHCjYtE4wFsANak" version="11.2.9" pages="1"><diagram id="dOOvDc07wKykiP7z2FdT" name="Page-1">7Vpbb9owFP41PK6K49x4bGm3adrWakzaeJpMYsBdEiNjCuzXzybO1VBCExo2WlVqfGyf2N93bnbTg4No/YGh+ewLDXDYM41g3YO3PdMEwLPFHynZJBIHOolgykigBuWCIfmDldBQ0iUJ8KI0kFMacjIvC30ax9jnJRlijK7KwyY0LL91jqZYEwx9FOrSHyTgs0TqmW4u/4jJdJa+GTj9pCdC6WC1k8UMBXRVEMG7HhwwSnnyFK0HOJTgpbgk897v6c0WxnDM60zof3K/umN0jwfcur8fGeNvv27eKS1PKFyqDavF8k2KAKPLOMBSidGDN6sZ4Xg4R77sXQnOhWzGo1C0gHickDAc0JCy7Vw48Xzs+0K+4Iz+xoWesWdbtlSoFoAZx+u9OwMZXsLQMI0wZxsxRE2wHQWxsjFLbWKVEwYsNWRWIMtMhUgZyTRTneMoHhSUR8BqnhbWAGFvshNWx/fweNIOrMA8N1gdDdYhZmKPGrhi07yMYBmpmMa4AqsSoZBMY9H0BUhCMbyREBIRD65VR0SCQL5mJ2VlUpN3pmFjyyONuYpxEvmkrZYNWqCs6gm2VZOyUzHmaYwNiNzLG2G7faxzwqChcYMDkRFVkzI+o1Mao/Aul1ZQfFxG8xQz0cynfKZ0rnB9xJxv1CC05LTMPV4T/lNqu7JVa1TouV2rF20bm7QRi+0XJsnmqNiXT9u2NrU5TwCRKDxPsgCNLpmPn0u3KoJxxKaYPzewv9tsGA4RJ0/llbRuBEAPtAz7T9uqZkr8M/Xe3by1Gk+B7XTtn0BD/80/2/PP9Ihx0D+dTv1TL9sXYsl8Ob9g1zSMjl0TWDorOA4uL2pWixpgu11T099Hjbw3kNcTiKPLIajqO4KxegS5J0tr+gH59dPa4VRTSGLmUVmsfo46mHogbJh61NQHSuRBLHPavnEFCz+WVzIRCCvUJ+tUSorXTrresq25/bKiJClrirZmlG2vwVWMsa+WvUjXr8bm7l3f1IsZPRbEwbW8zJW4hmixIP7RnqtK1rRIHZXK190la5uFpdGwsCzQY+9gJ5U1DQIV64B2hfU9zqorciuKjHrhozWvr3ED255RGUcYVZZCQCl/5IeixhnkoDGadk1jLPyf4s0YGxijdwbFzX97Zjdh3dBqNSycmqU5qEWkizuzawfD7s/senU4ZhQFPlqc6z8pTsALtM7twG7qdylnUhTmMbGz/J1eNB2OeJ3mb7N6Q+e4L8vf2lWf41wBr5//Oq+bzm3NNgPC5McnpvF98NDLPkYhNL7cGGLCzmOI+5ox5B87A6Q3oofPAHaXMUS7qbSsl8WQajDSFJ04aEC9/PNRnMcK8SSjhRFTFiGpbYU2lxM8ALBL7GTBpGBl2amtYfAQzfyju4Te/NNFePcX</diagram></mxfile>

BIN
docu/docs/img/broadcast.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
docu/docs/img/bw3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -0,0 +1 @@
<mxfile host="www.draw.io" modified="2019-10-26T07:55:48.917Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/77.0.3865.90 Chrome/77.0.3865.90 Safari/537.36" etag="82d1pq0jwhTcNaaLB_Ck" version="12.1.7" type="device" pages="1"><diagram id="9aEBdlF2oZdVepulqs3T" name="Page-1">7Vjbcts2EP0aPdrDi0gpj5FsJ9Nxpm7VadMnD0iuSNQgwYJLU+rXFzeSoijJlyhOMq09o9EeAIvdPdhDQhN/mW8+CFJmn3gCbOI5yWbiX008bx5M5acCtgbwZ74BUkETA7k9sKL/gAUdi9Y0gWowETlnSMshGPOigBgHGBGCN8Npa86Gu5YkhRGwigkbo3/QBDObljfr8Y9A06zd2Q3fmZGctJNtJlVGEt7sQP71xF8KztF8yzdLYKp2bV3Mupsjo11gAgp8zoKmWa0XC//q4Sf8rYohvZn/vrqY29hw2yYMiczfmlxgxlNeEHbdowvB6yIB5dWRVj/nlvNSgq4E/wLErSWT1MgllGHO7ChsKH5Wyy/DmTX/1GZgrauNda6N7Y5xB4LmgCBarECx/bxr7HhSZu9KW62vcfFsPSteixhOVKw9hESkgCfmeWaeKufOBpaaD8BlFmIrJwhgBOnj8LgRe2rTbl5PrPxiuX0Bz9bvI2G13WlE/CMIpPLgv2c0LSSEis1Fi96SCNgdryhSrkYjjshzOYHtDcSyooqeBbF+OmDnBPAaGS1g2TWt4mRNGVtyxoWOx1/PY4hjiVco+APsjERSVQK1IhUkodL9FRXSjdm/UAeyW9X2rKeQjJQq13yTKqm6LAAbLh6qS+WF3yNvdJhHj4YqBWxOktmqnm15q3mhNZteQILAQNmudjjOV2I//CZd/uat6T2zNYPvqjW9A60ZMlQNwWUFvDBFXRWDRfuAmbTDbfh3zduBi0qz815OcKflph9svfwKMagMZdUyASRpvcpMDu0u4VEAkXgaqUpSPCtI/3SQpEjUuwTE8jVDx6w+E4JEd5dsLi8kuZKtIqrKYdwmhGFY9PUxCWT363yinrndFvS8aetkT8Y/LvQXZJTX8q0q58VFkZ7KaXQy9pRlqBtNRhFWJdF920jZHWrESNod/Td+GNws1f951NkNn5RnN3DeUp9dd1TGNxDo1wvt9NwCapfecarFzBIV7hHlunsEGKW3q/Y46MJ4PS3Tb6nMnwgtvm9ZXoFWYyPCssUfAPXtrMMqELIrVZXXHRbxqiEYZ52M6/HKXJjijCrxsVLu0KKs0aj7C2Xwxxep6fRpkZq9qUjN/hsvkcEzXyLProFfRE5wVKo6YfilBjl2UDH2eG0vSvFWXtISfSl6okUiw/Nt1AFSDlLN/s/mqnesl7q73LHb3xl6yZ8Gg16ajXspPNBK86/VSe8OkPX/Zdxexhu5nEFV3efy4ZCf5wCE+2IajMXUOXQCghefAGn2P+mZF5H+d1H/+l8=</diagram></mxfile>

BIN
docu/docs/img/client.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -0,0 +1 @@
<mxfile host="www.draw.io" modified="2019-10-26T07:56:37.560Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/77.0.3865.90 Chrome/77.0.3865.90 Safari/537.36" etag="JWxWn9rJVMwJN8QniWVz" version="12.1.7" type="device" pages="1"><diagram id="9aEBdlF2oZdVepulqs3T" name="Page-1">7VrbkuI2EP0aHpcCC3N5HGBmt1IzlcmSyu48pYQtbGVki5XlNeTr07Il3wGTZRYqE6gy0tHFLXX3UUuihxbB7qPAW/+Ju4T1rIG766Flz7Km9gieCthnwEjlFOAJ6mbQsABW9G+iwYFGY+qSqFJRcs4k3VZBh4chcWQFw0LwpFptw1n1rVvskQawcjBrol+oK309LGtS4J8I9Xzz5uF4lpUE2FTWI4l87PKkBKH7HloIzmWWCnYLwtTcmXnJ2j0cKM0FEySUXRokyWozn6Pl6y/y98gh3sP0j9UHpLv5jlmsR6yllXszBYLHoUtUL4Memic+lWS1xY4qTUDngPkyYJAbQjKSgr+SBWdcpK3ROP1AyYYyVsI3tvoqnIeyhGcfwDGjXggYIxsY31zLSYQku4MzMMznFeyR8IBIsYcqusFMa0Kb4nCi80mh2PFAY35ZqQbE2pi8vOtiviGhp/yM6bfsxmwTF+xPZ7mQPvd4iNl9gc6r+ijqPHK+1Vr4i0i5186EY8mrOoLpEvuvqn3fNtkX3V2aWe4qub3OZbIqAY8rAMbDY+GQIwPXrCCx8Ig8NkGjdo0KwrCk36uCtKknbXonBN6XKmw5DWVU6vlZAYWhjAd21VLsmmvV6g+nR+tDIpOgMJR8KP/edkYtnjtmUnsUpL3MbTJsXQeySiXLG3+LuSn4EKW2cwcVhqPtrig0vTxhGir9+YJg13QJw2h7NcCNt6/FaSTa4rCThKhNwiUFKqLrWJJUTpIaqvNKlN6V3Dz2/LwEcpKGnqJt4vg4pFFQkj4T5JS4jdFflEYH6adJow8L9b0MP9pWlSBNDyV+zDnz5/Dj8Dr8uKPyayn9UlAl5ApyVBnDjQWnVhi1INi351S7I6f+KKX+kEbtg6yVc8RvMYGyVvKo2QKEVFuVdPaMgsrFab9aZ7bxuM4B4AQvtZhfYwm9kEMOuIZQ1m5xwM3UIY5zGQcc1RwQNR1w3OJ/0zdzP3Td8ORqrjTr6EpD65q+NDsduytzpLCbudPBtFQ6mBv0Ea8Je+YRlZSr0jWXkgdQgdUKHJjB1L1MUJ4DJb3xzIEW+U7smLcc9i9PYJdC90sqoJvs/aEyo7yV2YhZCtEUEOw8tf/sh0QmXLxG/QSaMxJFfwawJQ0u45/T2vo47dvNFbJtB2Hiw8t76OT/BbKzVw+tjm5tX9OrjZTXCew/E4eoId50bF8WUj1dLLE63BFAXtZgzaMES0dF9w5TTKK0FLrKaGKVpukjBKNWODy+1QKO9xLv54dsNxPvm2O1Y0vaT+C3C3KO1ZVzLHRN0rGuSjpP3I1hrb5JrlmkjcELVbxjDbib/kBQQTcOlg0O0sciClmnxw3vkFdQ/fhs2jxobSWWyZvxCrqmeT+z2KPhbZr3fehu9WEm31SMuQ+/K6xW2YXqnqQrKBcp6YFW/Oy8jLDoPS6ddm3pvAETH//nls7OlwRX3YVb5x3Ed3bXVippMNRn2HWrFekwh3T2yktTyxMOsUeixno42DCeKNScwQdm+TdheoMv3wut5JH1zdAKagsM67QSunfqqh1yDsNRRJ1e63HBoG+ZM4KXUtGp84L+pHJLmR8Rvv2BQffg/QADldRmt2jNYOfdZrZcV1aMBqGaMWTj1I0Ke2j0M5qd6Cibh0ZH516votG08h5zMX9QLoSO1T/3ehWyxb8usurFX1fQ/T8=</diagram></mxfile>

BIN
docu/docs/img/server.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

18
docu/docs/index.md Normal file
View file

@ -0,0 +1,18 @@
# <center>BOSWatch 3</center>
---
<center>
![BOSWatch](img/bw3.png "BOSWatch 3 Logo")
Falls du uns unterstützen möchtest würden wir uns über eine Spende freuen.
Server, Hosting, Domain sowie Kaffee kosten leider Geld ;-)
[![](https://www.paypalobjects.com/de_DE/DE/i/btn/btn_donate_LG.gif)](https://www.paypal.me/BSchroll)
</center>
**Es wird darauf hingewiesen, dass für die Teilnahme am BOS-Funk nur nach den Technischen Richtlinien der BOS zugelassene Funkanlagen verwendet werden dürfen.**
**Der BOS-Funk ist ein nichtöffentlicher mobiler Landfunk. Privatpersonen gehören nicht zum Kreis der berechtigten Funkteilnehmer.** _(Quelle: TR-BOS)_
---
**The intercept of the German BOS radio is strictly prohibited and will be prosecuted. The use is only permitted for authorized personnel.**

View file

@ -0,0 +1,31 @@
# <center>Broadcast Service</center>
Durch den Broadcast Service haben CLients die Möglichkeit, automatisch den Server zu finden und sich mit diesem zu verbinden. Dazu stellt der Server die benötigten Verbinungsinformationen per Broadcast Service bereit.
**Hinweis:** *Server und Client müssen sich im selben Subnetz befinden.*
---
## Aufbau
Der Broadcast Service besteht aus 2 Teilen - einem Server und einem Clienten.
Nachfolgend soll der Ablauf einer Verbunding des Clienen zum Server mittels des Broadcast Services erklärt werden.
<center>![](../img/broadcast.png)</center>
---
## Ablauf
### Schritt 1 - Broadcast Server starten
Im ersten Schritt wird auf dem Server ein zusätzlicher Broadcast Server in einem seperaten Thread gestartet. Dieser lauscht auf einem festgelegten Port auf UDP Broadcast Pakete. Nun kann eine beliebige Anzahl von Clienten mittels des Broadcast Services die Verbinundgdaten des Server abfragen.
### Schritt 2 - Broadcast durch Clienten
Die Client Applikation startet nun zur Abfrage der Verbindungsdaten einen BC Clienten und sendet dort auf dem festgelegten Port ein Paket per UDP Boradcast. Der Inhalt des Paketes ist das Magic-Word `<BW3-Request>` und wird von allen im selben Subnetz vohandenen Gegenstellen empfangen. Nun wird auf eine Antwort des Broadcast Server mit den Verbindungsdaten gewartet.
### Schritt 3 - Verbindungsdaten senden
Wird nun ein Broadcast Paket empfangen, prüft der BC Server die Daten auf das Magic-Word `<BW3-Request>`. Wird dieses erkannt, liest der Server die Absender-IP-Addresse aus dem Paket aus und sendet eine Antwort direkt an diesen Clienten. Dieses Antwortpaket sieht folgendermaßen aus: `<BW3-Result>;8080` wobei die `8080` hier den normalen TCP Kommunikationsport des Server darstellt.
### Schritt 4 - Verbindungsdaten empfangen
Nachdem der Client das direkt an ihn gerichtete Paket mit den Verbindungsdaten vom Server empfangen hat, prüft er auf das Magic-Word `<BW3-Result>`. Ist dieses enthalten wird der Port für die TCP Verbundindung aus dem Paket extrahiert. Außerdem wird die IP-Addresse des Absenders aus dem Paket gelesen.
Anschließend stehen dem Clienten die Verbindungsdaten des Servers zur Verfügung und er kann sich per TCP über den angegebenen Port mit dem BOSWatch Server verbindden um seine Alarmierungs-Pakete zu versenden.
Da der Broadcast Server in einem eigenen Thread, unabhängig vom Hauptprogram läuft, können ganz einfach weitere Clienten per Broadcast Service die Verbindungsdaten des Servers abrufen.

View file

@ -0,0 +1,33 @@
# <center>Server/Client Prinzip</center>
BOSWatch 3 wurde als Server/Client Anwedung entwickelt.
Dies ermöglicht es, mehrere Empfangsstationen an einer Auswerte- und Verteilereinheit zu bündeln.
---
## BOSWatch Client
Der **BOSWatch Client** übernimmt den Empfang und die Dekodierung der Daten. Anschließend werden die Daten mittels der implemetierten
Dekoder ausgewertet und in ein sogenanntes bwPacket verpackt.
Dieses Paket wird anschließend in einer Sende-Queue abgelegt. Nun werden Pakete aus der Queue an den BOSWatch Server per TCP-Socket
gesendet. Der Ansatz, Pakete statt dem direkten versenden vorher in einer Queue zwischen zu speichern, verhindert den Verlust von
Paketen, sollte die Verbindung zum Server einmal abreisen. Nach einer erfolgreichen Wiederverbdingun können die wartenden Pakete nun
nachträglich an den Server übermittelt werden.
Dabei überwacht der Client selbstständig die benötigten Programme zum Empfang der Daten und startet diese bei einem Fehler ggf. neu.
<center>![](../img/client.png)</center>
---
## BOSWatch Server
Nachdem die Daten vom Clienten über die TCP-Socket Verbindung empfangen wurden, übernimmt der **BOSWatch Server** die weitere
Verarbeitung der Daten.
Auch hier werden die empfangenen Daten in From von bwPacket's in einer Queue abelegt um zu gewährleisten, das auch während einer länger
dauernden Plugin Ausführung alle Pakete korrekt empfangen werden können und es zu keinen Verlusten kommt.
Die Verarbeitung der Pakete geschieht anschließend in sogenannten Routern, welche aufgrund ihres Umfangs jedoch in einem eigenen Kapitel
erklärt werden. Diese steuern die Verteilung der Daten an die einzelnen Plugins.
<center>![](../img/server.png)</center>

View file

@ -0,0 +1,39 @@
# <center>Mode Filter</center>
---
## Beschreibung
Mit diesem Modul ist es möglich, die Pakete auf bestimmte Modes (FMS, POCSAG, ZVEI) zu Filtern. Je nach Konfiguration werden Pakete eines bestimmten Modes im aktuellen Router weitergeleitet oder verworfen.
## Resource
`filter.modeFilter`
## Konfiguration
|Feld|Beschreibung|Default|
|----|------------|-------|
|allowed|Liste der erlaubten Paket Typen `fms` `zvei` `pocsag` `msg`||
**Beispiel:**
```yaml
- type: module
res: filter.modeFilter
config:
allowed:
- fms
- pocsag
```
---
## Abhängigkeiten
- keine
---
## Paket Modifikationen
- keine
---
## Zusätzliche Wildcards
- keine

View file

@ -0,0 +1,70 @@
# <center>Regex Filter</center>
---
## Beschreibung
Mit diesem Modul ist es möglich, komplexe Filter basierend auf Regulären Ausdrücken (Regex) anzulegen.
Für einen Filter können beliebig viele Checks angelegt werden, welche Felder eines BOSWatch Pakets mittels Regex prüfen.
Folgendes gilt:
- Die Filter werden nacheinander abgearbeitet
- Innerhalb des Filters werden die Checks nacheinander abgearbeitet
- Sobald ein einzelner Check fehlschlägt ist der ganze Filter fehlgeschlagen
- Sobald ein Filter mit all seinen Checks besteht, wird mit der Ausführung des Routers fortgefahren
- Sollten alle Filter fehlschlagen wird die Ausführung des Routers beendet
## Resource
`filter.regexFilter`
## Konfiguration
|Feld|Beschreibung|Default|
|----|------------|-------|
|filter|Enthält eine Liste der einzelnen Filter||
#### `filter:`
|Feld|Beschreibung|Default|
|----|------------|-------|
|name|Beliebiger Name des Filters||
|checks|Liste der einzelnen Checks innerhalb des Filters||
#### `checks:`
|Feld|Beschreibung|Default|
|----|------------|-------|
|field|Name des Feldes innerhalb des BOSWatch Pakets welches untersucht werden soll||
|regex|Regulärer Ausdruck (Bei Sonderzeichen " " verwenden)||
**Beispiel:**
```yaml
- type: module
res: filter.regexFilter
config:
filter:
- name: "Zvei filter"
checks:
- field: zvei
regex: "65[0-9]{3}" # all zvei with starting 65
- name: "FMS Stat 3"
checks:
- field: mode
regex: "fms" # check if mode is fms
- field: status
regex: "3" # check if status is 3
```
---
## Abhängigkeiten
- keine
---
## Paket Modifikationen
- keine
---
## Zusätzliche Wildcards
- keine

4
docu/docs/tbd.md Normal file
View file

@ -0,0 +1,4 @@
# <center>To be done ...</center>
---
Hier existiert noch kein Inhalt, gerne kannst du uns aber Helfen die Dokumentation zu vervollständigen.

View file

@ -58,7 +58,7 @@ PROJECT_LOGO =
# entered, it will be relative to the location where doxygen was started. If
# left blank the current directory will be used.
OUTPUT_DIRECTORY = _docu
OUTPUT_DIRECTORY = docu/docs/api/
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
# directories (in 2 levels) under the output directory of each output format and
@ -92,6 +92,7 @@ ALLOW_UNICODE_NAMES = NO
# The default value is: English.
OUTPUT_LANGUAGE = English
#German
# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
# descriptions after the members that are listed in the file and class
@ -666,21 +667,21 @@ MAX_INITIALIZER_LINES = 30
# list will mention the files that were used to generate the documentation.
# The default value is: YES.
SHOW_USED_FILES = YES
SHOW_USED_FILES = NO
# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
# will remove the Files entry from the Quick Index and from the Folder Tree View
# (if specified).
# The default value is: YES.
SHOW_FILES = YES
SHOW_FILES = NO
# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
# page. This will remove the Namespaces entry from the Quick Index and from the
# Folder Tree View (if specified).
# The default value is: YES.
SHOW_NAMESPACES = YES
SHOW_NAMESPACES = NO
# The FILE_VERSION_FILTER tag can be used to specify a program or script that
# doxygen should invoke to get the current version for each file (typically from
@ -756,7 +757,7 @@ WARN_IF_DOC_ERROR = YES
# parameter documentation, but not about the absence of documentation.
# The default value is: NO.
WARN_NO_PARAMDOC = NO
WARN_NO_PARAMDOC = YES
# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
# a warning is encountered.
@ -875,7 +876,9 @@ RECURSIVE = YES
EXCLUDE = bw_client.py \
bw_server.py \
_info/FileHead.template.py
FileHead.template.py \
venv \
docu
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
# directories that are symbolic links (a Unix file system feature) are excluded

37
docu/mkdocs.yml Normal file
View file

@ -0,0 +1,37 @@
site_name: BOSWatch3 Core
site_author: Bastian Schroll & BW3 Dev team
repo_url: https://github.com/BOSWatch/BW3-Core
edit_uri: edit/develop/docu/docs/
nav:
# - BW3: index.md
- Quick Start:
- Installation: tbd.md
- Konfiguration: config.md
# - BOSWatch benutzen: tbd.md
# - Als Service einrichten: tbd.md
- Informationen:
- Server/Cient Prinzip: information/serverclient.md
- Broadcast Service: information/broadcast.md
# - Modul/Plugin Konzept: tbd.md
# - Routing Mechanismus: tbd.md
- Changelog: changelog.md
- Module:
- Mode Filter: modul/mode_filter.md
- Regex Filter: modul/regex_filter.md
- Plugins: tbd.md
- Entwickler:
- Eigenes Modul/Plugin schreiben: develop/ModulPlugin.md
- BOSWatch Alarmpaket Format: develop/packet.md
- BW3 Quellcode Dokumentation: api/html/index.html
use_directory_urls: false
theme:
name: mkdocs
highlightjs: true
hljs_style: github

View file

@ -13,23 +13,20 @@
@date: 15.01.2018
@author: Bastian Schroll
@description: Class to implement a filter for double alarms
@todo test, refactor and document
@todo check_msg is not implemented yet
@todo test, refactor and document / check_msg is not implemented yet
"""
import logging
import time
from boswatch.config import Config
logging.debug("- %s loaded", __name__)
class DoubleFilter:
"""!Double Filter Class"""
def __init__(self):
def __init__(self, config):
"""!init"""
self._config = Config()
self._config = config
self._filterLists = {}
def filter(self, bwPacket):
@ -58,14 +55,14 @@ class DoubleFilter:
# delete entries that are to old
counter = 0
for listPacket in self._filterLists[bwPacket.get("mode")][1:]: # [1:] skip first entry, thats the new one
if listPacket.get("timestamp") < (time.time() - self._config.getInt("doubleFilter", "IgnoreTime", "serverConfig")):
if listPacket.get("timestamp") < (time.time() - self._config["ignoreTime"]):
self._filterLists[bwPacket.get("mode")].remove(listPacket)
counter += 1
if counter:
logging.debug("%d old entry(s) removed", counter)
# delete last entry if list is to big
if len(self._filterLists[bwPacket.get("mode")]) > self._config.getInt("doubleFilter", "MaxEntry", "serverConfig"):
if len(self._filterLists[bwPacket.get("mode")]) > self._config["maxEntry"]:
logging.debug("MaxEntry reached - delete oldest")
self._filterLists[bwPacket.get("mode")].pop()

View file

@ -0,0 +1,52 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: modeFilter.py
@date: 09.03.2019
@author: Bastian Schroll
@description: Filter module for the packet type
"""
import logging
from module.module import Module
# ###################### #
# Custom plugin includes #
# ###################### #
logging.debug("- %s loaded", __name__)
class BoswatchModule(Module):
"""!Filter of specific bwPacket mode"""
def __init__(self, config):
"""!Do not change anything here!"""
super().__init__(__name__, config) # you can access the config class on 'self.config'
def onLoad(self):
"""!Called by import of the plugin"""
pass
def doWork(self, bwPacket):
"""!start an run of the module.
@param bwPacket: A BOSWatch packet instance"""
for mode in self.config.get("allowed", default=[]):
if bwPacket.get("mode") == mode:
logging.debug("mode is allowed: %s", bwPacket.get("mode"))
return None
logging.debug("mode is denied: %s", bwPacket.get("mode"))
return False
def onUnload(self):
"""!Called by destruction of the plugin"""
pass

View file

@ -0,0 +1,65 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: regexFilter.py
@date: 26.10.2019
@author: Bastian Schroll
@description: Regex filter module
"""
import logging
from module.module import Module
# ###################### #
# Custom plugin includes #
import re
# ###################### #
logging.debug("- %s loaded", __name__)
class BoswatchModule(Module):
"""!Description of the Module"""
def __init__(self, config):
"""!Do not change anything here!"""
super().__init__(__name__, config) # you can access the config class on 'self.config'
def onLoad(self):
"""!Called by import of the plugin"""
pass
def doWork(self, bwPacket):
"""!start an run of the module.
@param bwPacket: A BOSWatch packet instance"""
for filter in self.config.get("filter"):
checkFailed = False
logging.debug("try filter '%s' with %d check(s)", filter.get("name"), len(filter.get("checks")))
for check in filter.get("checks"):
fieldData = bwPacket.get(check.get("field"))
if not fieldData or not re.search(check.get("regex"), fieldData):
logging.debug("[-] field '%s' with regex '%s'", check.get("field"), check.get("regex"))
checkFailed = True
break # if one check failed we break this filter
else:
logging.debug("[+] field '%s' with regex '%s'", check.get("field"), check.get("regex"))
if not checkFailed:
logging.debug("[PASSED] filter '%s'", filter.get("name"))
return None # None -> Router will go on with this packet
logging.debug("[FAILED] filter '%s'", filter.get("name"))
return False # False -> Router will stop further processing
def onUnload(self):
"""!Called by destruction of the plugin"""
pass

116
module/module.py Normal file
View file

@ -0,0 +1,116 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: module.py
@date: 01.03.2019
@author: Bastian Schroll
@description: Module main class to inherit
"""
import logging
import time
from boswatch import wildcard
logging.debug("- %s loaded", __name__)
class Module:
"""!Main module class"""
_modulesActive = []
def __init__(self, moduleName, config):
"""!init preload some needed locals and then call onLoad() directly"""
self._moduleName = moduleName
self.config = config
self._modulesActive.append(self)
# for time counting
self._cumTime = 0
self._moduleTime = 0
# for statistics
self._runCount = 0
self._moduleErrorCount = 0
logging.debug("[%s] onLoad()", moduleName)
self.onLoad()
def _cleanup(self):
"""!Cleanup routine calls onUnload() directly"""
logging.debug("[%s] onUnload()", self._moduleName)
self._modulesActive.remove(self)
self.onUnload()
def _run(self, bwPacket):
"""!start an run of the module.
@param bwPacket: A BOSWatch packet instance
@return bwPacket or False"""
self._runCount += 1
logging.debug("[%s] run #%d", self._moduleName, self._runCount)
tmpTime = time.time()
try:
logging.debug("[%s] doWork()", self._moduleName)
bwPacket = self.doWork(bwPacket)
except:
self._moduleErrorCount += 1
logging.exception("[%s] alarm error", self._moduleName)
self._moduleTime = time.time() - tmpTime
self._cumTime += self._moduleTime
logging.debug("[%s] took %0.3f seconds", self._moduleName, self._moduleTime)
return bwPacket
def _getStatistics(self):
"""!Returns statistical information's from last module run
@return Statistics as pyton dict"""
stats = {"type": "module",
"runCount": self._runCount,
"cumTime": self._cumTime,
"moduleTime": self._moduleTime,
"moduleErrorCount": self._moduleErrorCount}
return stats
def onLoad(self):
"""!Called by import of the module
can be inherited"""
pass
def doWork(self, bwPacket):
"""!Called module run
can be inherited
@param bwPacket: bwPacket instance"""
logging.warning("no functionality in module %s", self._moduleName)
def onUnload(self):
"""!Called on shutdown of boswatch
can be inherited"""
pass
@staticmethod
def registerWildcard(newWildcard, bwPacketField):
"""!Register a new wildcard
@param newWildcard: wildcard where parser searching for
@param bwPacketField: field from bwPacket where holds replacement data"""
if not newWildcard.startswith("{") or not newWildcard.endswith("}"):
logging.error("wildcard not registered - false format: %s", newWildcard)
return
if bwPacketField == "":
logging.error("wildcard not registered - bwPacket field is empty")
return
wildcard.registerWildcard(newWildcard, bwPacketField)

55
module/template_module.py Normal file
View file

@ -0,0 +1,55 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: template_module.py
@date: 01.03.2019
@author: Bastian Schroll
@description: Template Module File
"""
import logging
from module.module import Module
# ###################### #
# Custom plugin includes #
# ###################### #
logging.debug("- %s loaded", __name__)
class BoswatchModule(Module):
"""!Description of the Module"""
def __init__(self, config):
"""!Do not change anything here!"""
super().__init__(__name__, config) # you can access the config class on 'self.config'
def onLoad(self):
"""!Called by import of the plugin"""
pass
def doWork(self, bwPacket):
"""!start an run of the module.
@param bwPacket: A BOSWatch packet instance"""
if bwPacket.get("mode") == "fms":
pass
elif bwPacket.get("mode") == "zvei":
pass
elif bwPacket.get("mode") == "pocsag":
pass
elif bwPacket.get("mode") == "msg":
pass
return bwPacket
def onUnload(self):
"""!Called by destruction of the plugin"""
pass

View file

@ -17,9 +17,7 @@
import logging
import time
from boswatch.utils import paths
from boswatch.config import Config
from boswatch.utils import wildcard
from boswatch import wildcard
logging.debug("- %s loaded", __name__)
@ -27,12 +25,13 @@ logging.debug("- %s loaded", __name__)
class Plugin:
"""!Main plugin class"""
_pluginsActive = 0
_pluginsActive = []
def __init__(self, pluginName):
def __init__(self, pluginName, config):
"""!init preload some needed locals and then call onLoad() directly"""
self._pluginName = pluginName
self._pluginsActive += 1
self.config = config
self._pluginsActive.append(self)
# to save the packet while alarm is running for other functions
self._bwPacket = None
@ -43,8 +42,6 @@ class Plugin:
self._setupTime = 0
self._alarmTime = 0
self._teardownTime = 0
self._endTime = 0
self._tmpTime = 0
# for statistics
self._runCount = 0
@ -52,19 +49,13 @@ class Plugin:
self._alarmErrorCount = 0
self._teardownErrorCount = 0
if paths.fileExist(paths.PLUGIN_PATH + pluginName + "/" + pluginName + ".ini"):
self.config = Config()
self.config.loadConfigFile(paths.PLUGIN_PATH + pluginName + "/" + pluginName + ".ini")
else:
logging.debug("no config for %s found", pluginName)
logging.debug("[%s] onLoad()", pluginName)
self.onLoad()
def __del__(self):
"""!Destructor calls onUnload() directly"""
def _cleanup(self):
"""!Cleanup routine calls onUnload() directly"""
logging.debug("[%s] onUnload()", self._pluginName)
self._pluginsActive -= 1
self._pluginsActive.remove(self)
self.onUnload()
def _run(self, bwPacket):
@ -78,7 +69,7 @@ class Plugin:
self._bwPacket = bwPacket
self._tmpTime = time.time()
tmpTime = time.time()
try:
logging.debug("[%s] setup()", self._pluginName)
self.setup()
@ -86,8 +77,8 @@ class Plugin:
self._setupErrorCount += 1
logging.exception("[%s] error in setup()", self._pluginName)
self._setupTime = time.time() - self._tmpTime
self._tmpTime = time.time()
self._setupTime = time.time() - tmpTime
tmpTime = time.time()
try:
if bwPacket.get("mode") == "fms":
@ -106,8 +97,8 @@ class Plugin:
self._alarmErrorCount += 1
logging.exception("[%s] alarm error", self._pluginName)
self._alarmTime = time.time() - self._tmpTime
self._tmpTime = time.time()
self._alarmTime = time.time() - tmpTime
tmpTime = time.time()
try:
logging.debug("[%s] teardown()", self._pluginName)
self.teardown()
@ -115,10 +106,9 @@ class Plugin:
self._teardownErrorCount += 1
logging.exception("[%s] error in teardown()", self._pluginName)
self._teardownTime = time.time() - self._tmpTime
self._teardownTime = time.time() - tmpTime
self._sumTime = self._setupTime + self._alarmTime + self._teardownTime
self._cumTime += self._sumTime
self._endTime = time.time()
self._bwPacket = None
@ -127,11 +117,14 @@ class Plugin:
# logging.debug("- alarm: %0.2f sec.", self._alarmTime)
# logging.debug("- teardown: %0.2f sec.", self._teardownTime)
return None
def _getStatistics(self):
"""!Returns statistical information's from last plugin run
@return Statistics as pyton dict"""
stats = {"runCount": self._runCount,
stats = {"type": "plugin",
"runCount": self._runCount,
"sumTime": self._sumTime,
"cumTime": self._cumTime,
"setupTime": self._setupTime,
@ -144,50 +137,50 @@ class Plugin:
def onLoad(self):
"""!Called by import of the plugin
Must be inherit"""
can be inherited"""
pass
def setup(self):
"""!Called before alarm
Must be inherit"""
can be inherited"""
pass
def fms(self, bwPacket):
"""!Called on FMS alarm
Must be inherit
can be inherited
@param bwPacket: bwPacket instance"""
logging.warning("ZVEI not implemented in %s", self._pluginName)
def pocsag(self, bwPacket):
"""!Called on POCSAG alarm
Must be inherit
can be inherited
@param bwPacket: bwPacket instance"""
logging.warning("POCSAG not implemented in %s", self._pluginName)
def zvei(self, bwPacket):
"""!Called on ZVEI alarm
Must be inherit
can be inherited
@param bwPacket: bwPacket instance"""
logging.warning("ZVEI not implemented in %s", self._pluginName)
def msg(self, bwPacket):
"""!Called on MSG packet
Must be inherit
can be inherited
@param bwPacket: bwPacket instance"""
logging.warning("MSG not implemented in %s", self._pluginName)
def teardown(self):
"""!Called after alarm
Must be inherit"""
can be inherited"""
pass
def onUnload(self):
"""!Called by destruction of the plugin
Must be inherit"""
"""!Called on shutdown of boswatch
can be inherited"""
pass
def parseWildcards(self, msg):

View file

@ -9,13 +9,13 @@
German BOS Information Script
by Bastian Schroll
@file: template.py
@file: template_module.py
@date: 14.01.2018
@author: Bastian Schroll
@description: Template Plugin File
"""
import logging
from boswatch.plugin.plugin import Plugin
from plugin.plugin import Plugin
# ###################### #
# Custom plugin includes #
@ -27,10 +27,9 @@ logging.debug("- %s loaded", __name__)
class BoswatchPlugin(Plugin):
"""!Description of the Plugin"""
def __init__(self):
"""!Do not change anything here except the PLUGIN NAME in the super() call"""
# PLEASE SET YOU PLUGIN NAME HERE !!!!
Plugin.__init__("template")
def __init__(self, config):
"""!Do not change anything here!"""
super().__init__(__name__, config) # you can access the config class on 'self.config'
def onLoad(self):
"""!Called by import of the plugin"""
@ -56,11 +55,13 @@ class BoswatchPlugin(Plugin):
"""!Called on ZVEI alarm
@param bwPacket: bwPacket instance"""
pass
def msg(self, bwPacket):
"""!Called on MSG packet
@param bwPacket: bwPacket instance"""
pass
def teardown(self):
"""!Called after alarm"""

View file

@ -1,41 +0,0 @@
## Eigene Plugins schreiben
Um ein eigenes Plugin zu schreiben, sollte man sich am besten zuerst einmal das Plugin `template` ansehen.
Dies kann als Vorlage für das eigene Plugin genutzt werden.
### 1.) Informationen anpassen
- Dateikopf anpassen
- Namen des Plugins vergeben in der `__init__` Methode `super().__init__("template")`
### 2.) Benötigte Methode überschreiben
Die Plugin Basisklasse bietet einige Methoden, welche vom Plugin überschrieben werden können.
- `onLoad()` wird direkt beim Import des Plugins ausgeführt
- `setup()` wird vor jeder Ausführung gerufen
- `fms(bwPacket)` wird bei einem FMS Paket ausgeführt
- `pocsag(bwPacket)` wird bei einem POCSAG Paket ausgeführt
- `zvei(bwPacket)` wird bei einem ZVEI Packet ausgeführt
- `msg(bwPacket)` wird bei einem Nachrichten Packet ausgeführt
- `teardown()` wird nach jeder Ausführung gerufen
- `onUnload()` wird beim Zerstören der Plugin Instanz zum Programmende ausgeführt
### 3.) Zugriff auf Config Datei
Wenn sich im Ordner des Plugins eine ini-Datei befindet,
welche exakt so wie das Plugin heißt, kann deren Inhalt
über die lokale Config-Reader Instanz
- `self.config.getBool(SECTION, KEY)`
- `self.config.getInt(SECTION, KEY)`
- `self.config.getStr(SECTION, KEY)`
abgerufen werden.
### 4.) Daten aus dem BOSWatch Paket lesen
An die Alarm Funktionen FMS, POCSAG und ZVEI wird eine Instanz eines
BOSWatch-Packet Objekts übergeben.
Aus dieser kann mittels `bwPacket.get(FELDNAME)` das entsprechende Feld
ausgelesen werden. Eine Auflistung der bereitgestellten Informationen
findet sich im entsprechenden BOSWatch-Packet Dokument.
### 5.) Wildcards parsen
Das parsen der Wildcars funktioniert komfortabel über die interne Methode `self.parseWildcards(MSG)`.
Die Platzhalter für die Wildcards findet man in `boswatch/utils/wildcard.py` oder in der `packet.md`-

View file

@ -1,4 +0,0 @@
[Example]
String = Hello World!
bool = 1
integer = 12

View file

@ -1,7 +1,14 @@
# for pip to install all needed
# called with 'pip -r requirements.txt'
pyyaml
# for documentation generating
mkdocs
# for develope only
pytest
pytest-cov
pytest-pep8
pytest-randomly
pytest-flakes
pytest-randomly

3
some_old_stuff.txt Normal file
View file

@ -0,0 +1,3 @@
python -m pytest -c "_gen/pytest.ini"
_bin\win\doxygen\doxygen.exe _gen/doxygen.ini
_bin\win\cloc_1_72\cloc-1.72.exe . --exclude-lang=XML --exclude-dir=_docu,_config,_info,doxygen.ini --by-file-by-lang

View file

@ -0,0 +1 @@
# coding=utf-8

View file

@ -0,0 +1,247 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: test_ServerClient.py
@date: 10.12.2017
@author: Bastian Schroll
@description: Unittests for BOSWatch. File have to run as "pytest" unittest
"""
# problem of the pytest fixtures
# pylint: disable=redefined-outer-name
import logging
import time
import queue
import pytest
from boswatch.network.server import TCPServer
from boswatch.network.client import TCPClient
import threading
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
@pytest.fixture
def getClient():
"""!Build and serve a TCPCLient"""
return TCPClient()
@pytest.fixture
def getServer():
"""!Build and serve a TCPServer"""
dataQueue = queue.Queue()
testServer = TCPServer(dataQueue)
return testServer
@pytest.fixture
def getRunningServer(getServer):
"""!Build and serve a still running TCPServer"""
logging.debug("start server")
assert getServer.start()
while not getServer.isRunning:
pass
yield getServer
logging.debug("stop server")
assert getServer.stop()
time.sleep(0.1) # wait for safe stopped
def test_clientConnectFailed(getClient):
"""!Connect to a non available server"""
assert not getClient.connect()
def test_clientDisconnectFailed(getClient):
"""!Disconnect while no connection is established"""
assert getClient.disconnect()
def test_clientTransmitFailed(getClient):
"""!Transmit while no connection is established"""
assert not getClient.transmit("test")
def test_clientReceiveFailed(getClient):
"""!Receive while no connection is established"""
assert not getClient.receive()
def test_clientConnect(getClient, getRunningServer):
"""!Connect to a server"""
assert getClient.connect()
assert getClient.disconnect()
def test_doubleConnect(getClient, getRunningServer):
"""!Connect to a server twice"""
assert getClient.connect()
assert getClient.connect()
assert getClient.disconnect()
def test_clientReconnect(getClient, getRunningServer):
"""!Try a reconnect after a established connection"""
assert getClient.connect()
assert getClient.disconnect()
assert getClient.connect()
assert getClient.disconnect()
def test_clientMultiConnect(getClient, getRunningServer):
"""!Connect with 2 clients to the server"""
assert getClient.connect()
testClient2 = TCPClient()
assert testClient2.connect()
time.sleep(0.1) # wait for all clients connected
# check connected clients
assert getRunningServer.countClientsConnected() == 2
# disconnect all
assert getClient.disconnect()
assert testClient2.disconnect()
def test_clientCommunicate(getClient, getRunningServer):
"""!Try to send data to the server and check on '[ack]'"""
assert getClient.connect()
assert getClient.transmit("test")
assert getClient.receive() == "[ack]"
assert getClient.disconnect()
@pytest.mark.skip("needs fixture for more than one client")
def test_clientMultiCommunicate(getServer):
"""!Try to send data to the server with 3 clients and check on '[ack]'"""
# connect all
testClient1 = TCPClient()
assert testClient1.connect()
testClient2 = TCPClient()
assert testClient2.connect()
testClient3 = TCPClient()
assert testClient3.connect()
# send all
assert testClient1.transmit("test")
assert testClient2.transmit("test")
assert testClient3.transmit("test")
# recv all
assert testClient3.receive() == "[ack]"
assert testClient2.receive() == "[ack]"
assert testClient1.receive() == "[ack]"
# check server msg queue
assert getRunningServer._alarmQueue.qsize() == 3
# disconnect all
assert testClient1.disconnect()
assert testClient2.disconnect()
assert testClient3.disconnect()
def test_serverRestart(getRunningServer):
"""!Test a stop and restart of the server"""
assert getRunningServer.stop()
assert getRunningServer.start()
assert getRunningServer.stop()
def test_serverStopFailed(getServer):
"""!Test to stop a stopped server"""
assert getServer.stop()
def test_serverDoubleStart(getServer):
"""!Test to start the server twice"""
assert getServer.start()
assert getServer.start()
assert getServer.stop()
def test_serverStartTwoInstances():
"""!Test to start two server different server instances"""
dataQueue = queue.Queue()
testServer1 = TCPServer(dataQueue)
testServer2 = TCPServer(dataQueue)
assert testServer1.start()
assert testServer1.isRunning
assert not testServer2.start()
assert testServer1.isRunning
assert not testServer2.isRunning
assert testServer1.stop()
assert testServer2.stop()
def test_serverStopsWhileConnected(getRunningServer, getClient):
"""!Shutdown server while client is connected"""
getClient.connect()
getRunningServer.stop()
timeout = 5
while getClient.isConnected:
time.sleep(0.1)
timeout = timeout - 1
if timeout == 0:
break
assert timeout
@pytest.mark.skip("needs fixture for more than one client")
def test_serverGetOutput(getRunningServer):
"""!Send data to server with 2 clients, check '[ack]' and data on server queue"""
# connect all
testClient1 = TCPClient()
assert testClient1.connect()
testClient2 = TCPClient()
assert testClient2.connect()
# send all
assert testClient1.transmit("test1")
time.sleep(0.1) # wait for recv to prevent fail of false order
assert testClient2.transmit("test2")
# recv all
assert testClient1.receive() == "[ack]"
assert testClient2.receive() == "[ack]"
# _check server output data
assert getRunningServer._alarmQueue.qsize() == 2
assert getRunningServer._alarmQueue.get(True, 1)[1] == "test1"
assert getRunningServer._alarmQueue.get(True, 1)[1] == "test2"
assert getRunningServer._alarmQueue.qsize() == 0 # Last _check must be None
# disconnect all
assert testClient1.disconnect()
assert testClient2.disconnect()
def test_serverHighLoad(getRunningServer):
"""!High load server test with 10 send threads each will send 100 msg with 324 bytes size"""
logging.debug("start sendThreads")
threads = []
for thr_id in range(10):
thr = threading.Thread(target=sendThread, name="sendThread-" + str(thr_id))
thr.daemon = True
thr.start()
threads.append(thr)
for thread in threads:
thread.join()
logging.debug("finished sendThreads")
assert getRunningServer._alarmQueue.qsize() == 1000
def sendThread():
client = TCPClient()
client.connect()
time.sleep(0.1)
for i in range(100):
# actually this string is 324 bytes long
client.transmit("HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-"
"HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-"
"HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-")
if not client.receive() == "[ack]":
logging.error("missing [ACK]")
time.sleep(0.1)
client.disconnect()

View file

@ -0,0 +1,77 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: test_broadcast.py
@date: 25.09.2018
@author: Bastian Schroll
@description: Unittests for BOSWatch. File have to run as "pytest" unittest
"""
# problem of the pytest fixtures
# pylint: disable=redefined-outer-name
import logging
import pytest
from boswatch.network.broadcast import BroadcastServer
from boswatch.network.broadcast import BroadcastClient
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
@pytest.fixture()
def broadcastServer():
"""!Server a BroadcastServer instance"""
broadcastServer = BroadcastServer()
yield broadcastServer
if broadcastServer.isRunning:
assert broadcastServer.stop()
while broadcastServer.isRunning:
pass
@pytest.fixture()
def broadcastClient():
"""!Server a BroadcastClient instance"""
return BroadcastClient()
def test_serverStartStop(broadcastServer):
"""!Start a BroadcastServer, check if running and stop it"""
assert broadcastServer.start()
assert broadcastServer.isRunning
assert broadcastServer.stop()
def test_serverDoubleStart(broadcastServer):
"""!Try to start a BroadcastServer twice"""
assert broadcastServer.start()
assert broadcastServer.start()
assert broadcastServer.stop()
def test_serverStopNotStarted(broadcastServer):
"""!Try to stop a BroadcastServer where is not running"""
assert broadcastServer.stop()
def test_clientWithoutServer(broadcastClient):
"""!Use BroadcastClient with no server"""
assert not broadcastClient.getConnInfo(1)
def test_serverClientFetchConnInfo(broadcastClient, broadcastServer):
"""!Fetch connection info from BroadcastServer"""
assert broadcastServer.start()
assert broadcastClient.getConnInfo()
assert broadcastServer.stop()
assert broadcastClient.serverIP
assert broadcastClient.serverPort

View file

@ -0,0 +1,105 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: test_config.py
@date: 08.01.2017
@author: Bastian Schroll
@description: Unittests for BOSWatch. File have to run as "pytest" unittest
"""
# problem of the pytest fixtures
# pylint: disable=redefined-outer-name
import logging
import pytest
from boswatch.utils import paths
from boswatch.configYaml import ConfigYAML
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
@pytest.fixture
def getConfig():
"""!Build a config object"""
return ConfigYAML()
@pytest.fixture
def getFilledConfig():
"""!Build a config object and fill it with the config data"""
filledConfig = ConfigYAML()
assert filledConfig.loadConfigFile(paths.TEST_PATH + "test_config.yaml") is True
return filledConfig
def test_loadConfigFile(getConfig):
"""!load a config file"""
assert getConfig.loadConfigFile(paths.TEST_PATH + "test_config.yaml") is True
def test_loadConfigFileFailed(getConfig):
"""!load a config file with syntax error"""
assert getConfig.loadConfigFile(paths.TEST_PATH + "test_configFailed.yaml") is False
def test_loadConfigFileNotFound(getConfig):
"""!load a config file where is not available"""
assert getConfig.loadConfigFile(paths.TEST_PATH + "test_configNotFound.yaml") is False
def test_getConfigAsString(getFilledConfig):
"""!Get the string representation of the config"""
assert type(str(getFilledConfig)) is str
logging.debug(getFilledConfig)
def test_getTypes(getFilledConfig):
"""!Get and check different data types in config"""
assert type(getFilledConfig.get("types")) is ConfigYAML
assert type(getFilledConfig.get("types", "string")) is str
assert type(getFilledConfig.get("types", "bool")) is bool
assert type(getFilledConfig.get("types", "integer")) is int
assert type(getFilledConfig.get("types", "float")) is float
def test_getDefaultValue(getFilledConfig):
"""!Get the default value of an not existent entry"""
assert getFilledConfig.get("notExistent", default="defaultValue") == "defaultValue"
def test_getNestedConfig(getFilledConfig):
"""!Work with nested sub-config elements"""
nestedConfig = getFilledConfig.get("types")
assert type(nestedConfig) is ConfigYAML
assert nestedConfig.get("string") == "Hello World"
def test_configIterationList(getFilledConfig):
"""!Try to iterate over a list in the config"""
counter = 0
for item in getFilledConfig.get("list"):
assert type(item) is str
counter += 1
assert counter == 3
def test_configIterationListWithNestedList(getFilledConfig):
"""!Try to iterate over a list in the config where its elements are lists itself"""
listCnt = 0
strCnt = 0
for item in getFilledConfig.get("list1"):
if type(item) is ConfigYAML:
listCnt += 1
if type(item) is str:
strCnt += 1
assert listCnt == 2
assert strCnt == 1

View file

@ -0,0 +1,104 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: test_Decoder.py
@date: 15.12.2017
@author: Bastian Schroll
@description: Unittests for BOSWatch. File have to run as "pytest" unittest
"""
# problem of the pytest fixtures
# pylint: disable=redefined-outer-name
import logging
from boswatch.decoder.decoder import Decoder
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
def test_decoderNoData():
"""!Test a empty string"""
assert Decoder.decode("") is None
def test_decoderZveiValid():
"""!Test valid ZVEI"""
assert not Decoder.decode("ZVEI1: 12345") is None
assert not Decoder.decode("ZVEI1: 12838") is None
assert not Decoder.decode("ZVEI1: 34675") is None
def test_decoderZveiDoubleTone():
"""!Test doubleTone included ZVEI"""
assert not Decoder.decode("ZVEI1: 6E789") is None
assert not Decoder.decode("ZVEI1: 975E7") is None
assert not Decoder.decode("ZVEI1: 2E87E") is None
def test_decoderZveiInvalid():
"""Test invalid ZVEI"""
assert Decoder.decode("ZVEI1: 1245A") is None
assert Decoder.decode("ZVEI1: 1245") is None
assert Decoder.decode("ZVEI1: 135") is None
assert Decoder.decode("ZVEI1: 54") is None
assert Decoder.decode("ZVEI1: 54") is None
def test_decoderPocsagValid():
"""!Test valid POCSAG"""
assert not Decoder.decode("POCSAG512: Address: 1000000 Function: 0") is None
assert not Decoder.decode("POCSAG512: Address: 1000001 Function: 1") is None
assert not Decoder.decode("POCSAG1200: Address: 1000002 Function: 2") is None
assert not Decoder.decode("POCSAG2400: Address: 1000003 Function: 3") is None
def test_decoderPocsagText():
"""!Test POCSAG with text"""
assert not Decoder.decode("POCSAG512: Address: 1000000 Function: 0 Alpha: test") is None
assert not Decoder.decode("POCSAG512: Address: 1000001 Function: 1 Alpha: test") is None
assert not Decoder.decode("POCSAG1200: Address: 1000002 Function: 2 Alpha: test") is None
assert not Decoder.decode("POCSAG2400: Address: 1000003 Function: 3 Alpha: test") is None
def test_decoderPocsagShortRic():
"""!Test short POCSAG"""
assert not Decoder.decode("POCSAG512: Address: 3 Function: 0 Alpha: test") is None
assert not Decoder.decode("POCSAG512: Address: 33 Function: 0 Alpha: test") is None
assert not Decoder.decode("POCSAG1200: Address: 333 Function: 0 Alpha: test") is None
assert not Decoder.decode("POCSAG1200: Address: 3333 Function: 0 Alpha: test") is None
assert not Decoder.decode("POCSAG2400: Address: 33333 Function: 0 Alpha: test") is None
assert not Decoder.decode("POCSAG2400: Address: 333333 Function: 0 Alpha: test") is None
assert not Decoder.decode("POCSAG2400: Address: 3333333 Function: 0 Alpha: test") is None
def test_decoderPocsagInvalid():
"""!Test invalid POCSAG"""
assert Decoder.decode("POCSAG512: Address: 333333F Function: 0 Alpha: invalid") is None
assert Decoder.decode("POCSAG512: Address: 333333F Function: 1 Alpha: invalid") is None
assert Decoder.decode("POCSAG512: Address: 3333333 Function: 4 Alpha: invalid") is None
def test_decoderFmsValid():
"""!Test valid FMS"""
assert not Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 0=FZG->LST 2=I (ohneNA,ohneSIGNAL)) CRC correct""") is None
assert not Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 1=LST->FZG 2=I (ohneNA,ohneSIGNAL)) CRC correct""") is None
assert not Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 0=FZG->LST 2=II (ohneNA,mit SIGNAL)) CRC correct""") is None
assert not Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 1=LST->FZG 2=III(mit NA,ohneSIGNAL)) CRC correct""") is None
assert not Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 0=FZG->LST 2=IV (mit NA,mit SIGNAL)) CRC correct""") is None
def test_decoderFmsInvalid():
"""!Test invalid FMS"""
assert Decoder.decode("""FMS: 14170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 1=LST->FZG 2=III(mit NA,ohneSIGNAL)) CRC correct""") is None
assert Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Sta 3=Einsatz Ab 0=FZG->LST 2=IV (mit NA,mit SIGNAL)) CRC correct""") is None
assert Decoder.decode("""FMS: 14170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 1=LST->FZG 2=III(mit NA,ohneSIGNAL)) CRC incorrect""") is None
assert Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Sta 3=Einsatz Ab 0=FZG->LST 2=IV (mit NA,mit SIGNAL)) CRC incorrect""") is None

View file

@ -12,27 +12,24 @@
@file: test_header.py
@date: 12.12.2017
@author: Bastian Schroll
@description: Unittests for BOSWatch. File must be _run as "pytest" unittest
@description: Unittests for BOSWatch. File have to run as "pytest" unittest
"""
# problem of the pytest fixtures
# pylint: disable=redefined-outer-name
import logging
from boswatch.utils import header
class Test_Header:
"""!Unittests for the header"""
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
def setup_method(self, method):
logging.debug("[TEST] %s.%s", type(self).__name__, method.__name__)
def test_logoToLog(self):
"""!Test logo to log"""
assert header.logoToLog()
def test_logoToLog():
"""!Test logo to log"""
assert header.logoToLog()
def test_infoToLog(self):
"""!Test info to log"""
assert header.infoToLog()
def test_logoToScreen(self):
"""!Test logo to screen"""
assert header.logoToScreen()
def test_infoToLog():
"""!Test info to log"""
assert header.infoToLog()

View file

@ -0,0 +1,60 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: test_packet.py
@date: 12.12.2017
@author: Bastian Schroll
@description: Unittests for BOSWatch. File have to run as "pytest" unittest
"""
# problem of the pytest fixtures
# pylint: disable=redefined-outer-name
import logging
import pytest
from boswatch.packet import Packet
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
@pytest.fixture()
def buildPacket():
"""!Build a BOSWatch packet and serve it to each test"""
return Packet()
def test_createPacket(buildPacket):
"""!Create a packet"""
assert buildPacket != ""
def test_copyPacket(buildPacket):
"""!Copy a packet to an new instance"""
bwCopyPacket = Packet(buildPacket.__str__())
assert bwCopyPacket != ""
def test_getPacketString(buildPacket):
"""!get the intern packet dict as string"""
assert type(buildPacket.__str__()) is str
assert buildPacket.__str__() != ""
def test_getNotSetField(buildPacket):
"""!try to get a not set field"""
assert not buildPacket.get("testfield")
def test_setGetField(buildPacket):
"""!set and get a field"""
buildPacket.set("testField", "test")
assert buildPacket.get("testField") == "test"

View file

@ -0,0 +1,49 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: test_paths.py
@date: 22.02.2017
@author: Bastian Schroll
@description: Unittests for BOSWatch. File have to run as "pytest" unittest
"""
# problem of the pytest fixtures
# pylint: disable=redefined-outer-name
import logging
import os
from boswatch.utils import paths
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
def test_fileExists():
"""!load a local config file"""
assert paths.fileExist("README.md")
def test_fileNotExists():
"""!load a local config file"""
assert not paths.fileExist("notFound.txt")
def test_makeDirNotExisting():
"""!load a local config file"""
assert paths.makeDirIfNotExist("UnItTeSt")
os.removedirs("UnItTeSt")
def test_makeDirExisting():
"""!load a local config file"""
paths.makeDirIfNotExist("UnItTeSt")
assert paths.makeDirIfNotExist("UnItTeSt")
os.removedirs("UnItTeSt")

109
test/boswatch/test_timer.py Normal file
View file

@ -0,0 +1,109 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: test_timer.py
@date: 21.09.2018
@author: Bastian Schroll
@description: Unittests for BOSWatch. File have to run as "pytest" unittest
"""
# problem of the pytest fixtures
# pylint: disable=redefined-outer-name
import logging
import time
import pytest
from boswatch.timer import RepeatedTimer
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
def testTargetFast():
"""!Fast worker thread"""
logging.debug("run testTargetFast")
def testTargetSlow():
"""!Slow worker thread"""
logging.debug("run testTargetSlow start")
time.sleep(0.51)
logging.debug("run testTargetSlow end")
@pytest.fixture()
def useTimerFast():
"""!Server a RepeatedTimer instance with fast worker"""
testTimer = RepeatedTimer(0.1, testTargetFast)
yield testTimer
if testTimer.isRunning:
assert testTimer.stop()
@pytest.fixture()
def useTimerSlow():
"""!Server a RepeatedTimer instance slow worker"""
testTimer = RepeatedTimer(0.1, testTargetSlow)
yield testTimer
if testTimer.isRunning:
assert testTimer.stop()
def test_timerStartStop(useTimerFast):
"""!Try to start and stop a timer"""
assert useTimerFast.start()
assert useTimerFast.stop()
def test_timerDoubleStart(useTimerFast):
"""!Try to start a timer twice"""
assert useTimerFast.start()
assert useTimerFast.start()
assert useTimerFast.stop()
def test_timerStopNotStarted(useTimerFast):
"""!Try to stop a timer where is not started"""
assert useTimerFast.stop()
def test_timerIsRunning(useTimerFast):
"""!Check if a timer is running"""
assert useTimerFast.start()
assert useTimerFast.isRunning
assert useTimerFast.stop()
def test_timerRun(useTimerFast):
"""!Run a timer and check overdue and lostEvents"""
assert useTimerFast.start()
time.sleep(0.2)
assert useTimerFast.stop()
assert useTimerFast.overdueCount == 0
assert useTimerFast.lostEvents == 0
def test_timerOverdue(useTimerSlow):
"""!Run a timer and check overdue and lostEvents"""
assert useTimerSlow.start()
time.sleep(0.2)
assert useTimerSlow.stop()
assert useTimerSlow.overdueCount == 1
assert useTimerSlow.lostEvents == 5
def test_timerOverdueLong(useTimerSlow):
"""!Run a timer and check overdue and lostEvents"""
assert useTimerSlow.start()
time.sleep(1)
assert useTimerSlow.stop()
assert useTimerSlow.overdueCount == 2
assert useTimerSlow.lostEvents == 10

1
test/module/__init__.py Normal file
View file

@ -0,0 +1 @@
# coding=utf-8

1
test/plugin/__init__.py Normal file
View file

@ -0,0 +1 @@
# coding=utf-8

View file

@ -8,7 +8,7 @@
# by Bastian Schroll
[pytest]
addopts = -v --pep8 --cov=boswatch/ --cov-report=term-missing --log-level=CRITICAL
addopts = -v --pep8 --flakes --cov=boswatch/ --cov-report=term-missing --log-level=CRITICAL
# classic or progress
console_output_style = progress
@ -19,6 +19,7 @@ log_file_format=%(asctime)s - %(module)-12s %(funcName)-15s [%(levelname)-8s] %(
log_file_date_format=%d.%m.%Y %H:%M:%S
#pep8 plugin
pep8ignore = E402, E501 # import not at top
pep8ignore = E402 E501
# E402 # import not at top
# E501 # line too long
# pep8maxlinelength = 99

View file

@ -1,12 +0,0 @@
[test]
one = 1
two = two
[testcase]
test = ok
[boolTest]
one = 1
two = True
three = 0
four = False

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