added project files
This commit is contained in:
parent
74da7ee616
commit
76853cd66f
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 J0EK3R
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
21
pyproject.toml
Normal file
21
pyproject.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "mkconnect"
|
||||
version = "0.1.0"
|
||||
description = "MouldKing Bluetooth hub connector"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = {"" = "src"}
|
||||
include-package-data = true
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[project.scripts]
|
||||
mkconnect = "mkconnect.cli:main"
|
||||
BIN
src/.DS_Store
vendored
Normal file
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
149
src/mkconnect.egg-info/PKG-INFO
Normal file
149
src/mkconnect.egg-info/PKG-INFO
Normal file
@ -0,0 +1,149 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: mkconnect
|
||||
Version: 0.1.0
|
||||
Summary: MouldKing Bluetooth hub connector
|
||||
License: MIT License
|
||||
|
||||
Copyright (c) 2024 J0EK3R
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
Dynamic: license-file
|
||||
|
||||
# mkconnect-python
|
||||
...a bit of code to connect to MouldKing Bluetooth Hubs in python.
|
||||
|
||||
# MouldKing Hubs
|
||||
## MouldKing 6.0 Hub
|
||||
The MouldKing 6.0 Hub has two modes:
|
||||
* RC-Mode to be controlled with a MouldKing remotecontrol
|
||||
* Bluetooth-Mode to be controlled with an app
|
||||
|
||||
You can control a maximum of three MK6.0 Hubs at the same time with bluetooth.
|
||||
> (Currently this project can send only one advertising telegram the same time - so only one hub can be controlled, all others will go in timeout-mode till next telegram with their device address is sent.)
|
||||
|
||||
## Setting the address of the Hub
|
||||
To switch the Hub's device address to the next one (device 0, device 1, device 2) just press the button on the hub.
|
||||
|
||||
> i.E.: If the script runs with **mkcontrol(2,0,1)** (-> device2, channel0, full speed forward) and nothing is happening, you have to short-press the button, perhaps again...)
|
||||
|
||||
# usage
|
||||
Start the script [consoletest.py](https://github.com/J0EK3R/mkconnect-python/blob/main/consoletest.py) on your raspberry:
|
||||
```
|
||||
pi@devpi:~/dev/mkconnect-python $ sudo python -i consoletest.py
|
||||
```
|
||||
|
||||
## mkbtstop() - stop bluetooth advertising
|
||||
```
|
||||
Ready to execute commands
|
||||
|
||||
>>> mkbtstop()
|
||||
```
|
||||
|
||||
## mkconnect() - switch hubs in bluetooth mode
|
||||
If you power-on the hubs they will listen to telegrams to the **first device by default**.
|
||||
Call mkconnect() to switch all hubs in Bluetooth mode.
|
||||
By short-pressing the button on MK6.0 Hubs you can choose the hubId:
|
||||
* hubId=0 - one Led flash
|
||||
* hubId=1 - two Led flashs
|
||||
* hubId=2 - three Led flashs
|
||||
```
|
||||
Ready to execute commands
|
||||
|
||||
>>> mkconnect()
|
||||
```
|
||||
|
||||
## mkcontrol(deviceId, channel, power and powerAndDirection)
|
||||
i.E.: mkcontrol(0, 0, 1) - on first device (deviceId=0) run channel A (channel=0) with fullspeed (powerAndDirection=1)
|
||||
```
|
||||
Ready to execute commands
|
||||
|
||||
>>> mkcontrol(0, 0, 1)
|
||||
```
|
||||
|
||||
## mkstop(deviceId)
|
||||
Set all channels of device to zero
|
||||
```
|
||||
Ready to execute commands
|
||||
|
||||
>>> mkstop(0)
|
||||
```
|
||||
|
||||
---
|
||||
# old stuff
|
||||
|
||||
There is a testscript [consoletest.py](https://github.com/J0EK3R/mkconnect-python/blob/main/consoletest.py) where (on raspberry pi) **hcitool** is used to advertise telegrams over bluetooth.
|
||||
|
||||
Maybe you habe to **sudo** the command:
|
||||
```
|
||||
pi@devpi:~/dev/mkconnect-python $ sudo python -i consoletest.py
|
||||
|
||||
Ready to execute commands
|
||||
|
||||
For connecting: mkconnect(hubId) ex: mkconnect(0) or mkconnect(1) for the second hub
|
||||
|
||||
Available commands: mkconnect(hubId)
|
||||
mkstop(hubId)
|
||||
mkcontrol(deviceId, channel, powerAndDirection)
|
||||
|
||||
ex: mkcontrol(0, 0, 0.5) ; mkcontrol(0, 'B', -1)
|
||||
the minus sign - indicate reverse motor direction
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Just look in [main.py](https://github.com/J0EK3R/mkconnect-python/blob/main/main.py) for current usage...
|
||||
|
||||
Current output in [https://wokwi.com/projects/new/micropython-pi-pico](https://wokwi.com/projects/398314618803830785)
|
||||
...looks very good! :)
|
||||
```
|
||||
connect-telegram
|
||||
rawdata: 6d 7b a7 80 80 80 80 92
|
||||
crypted: 6d b6 43 cf 7e 8f 47 11 88 66 59 38 d1 7a aa 26 49 5e 13 14 15 16 17 18
|
||||
|
||||
stop-telegram
|
||||
rawdata: 61 7b a7 80 80 80 80 80 80 9e
|
||||
crypted: 6d b6 43 cf 7e 8f 47 11 84 66 59 38 d1 7a aa 34 67 4a 55 bf 15 16 17 18
|
||||
|
||||
C1: fullspeed forwards
|
||||
rawdata: 61 7b a7 ff 80 80 80 80 80 9e
|
||||
crypted: 6d b6 43 cf 7e 8f 47 11 84 66 59 47 d1 7a aa 34 67 4a ed b7 15 16 17 18
|
||||
|
||||
C1: halfspeed forwards
|
||||
rawdata: 61 7b a7 bf 80 80 80 80 80 9e
|
||||
crypted: 6d b6 43 cf 7e 8f 47 11 84 66 59 07 d1 7a aa 34 67 4a eb 70 15 16 17 18
|
||||
|
||||
C1: halfspeed backwards
|
||||
rawdata: 61 7b a7 40 80 80 80 80 80 9e
|
||||
crypted: 6d b6 43 cf 7e 8f 47 11 84 66 59 f8 d1 7a aa 34 67 4a 4e fe 15 16 17 18
|
||||
|
||||
C2: halfspeed backwards
|
||||
rawdata: 61 7b a7 40 40 80 80 80 80 9e
|
||||
crypted: 6d b6 43 cf 7e 8f 47 11 84 66 59 f8 11 7a aa 34 67 4a 3d f9 15 16 17 18
|
||||
MicroPython v1.22.0 on 2023-12-27; Raspberry Pi Pico with RP2040
|
||||
Type "help()" for more information.
|
||||
>>>
|
||||
raw REPL; CTRL-B to exit
|
||||
>
|
||||
```
|
||||
41
src/mkconnect.egg-info/SOURCES.txt
Normal file
41
src/mkconnect.egg-info/SOURCES.txt
Normal file
@ -0,0 +1,41 @@
|
||||
LICENSE
|
||||
README.md
|
||||
pyproject.toml
|
||||
src/mkconnect/__init__.py
|
||||
src/mkconnect/__main__.py
|
||||
src/mkconnect/cli.py
|
||||
src/mkconnect.egg-info/PKG-INFO
|
||||
src/mkconnect.egg-info/SOURCES.txt
|
||||
src/mkconnect.egg-info/dependency_links.txt
|
||||
src/mkconnect.egg-info/entry_points.txt
|
||||
src/mkconnect.egg-info/top_level.txt
|
||||
src/mkconnect/advertiser/Advertiser.py
|
||||
src/mkconnect/advertiser/AdvertiserBTMgmt.py
|
||||
src/mkconnect/advertiser/AdvertiserBTSocket.py
|
||||
src/mkconnect/advertiser/AdvertiserDummy.py
|
||||
src/mkconnect/advertiser/AdvertiserHCITool.py
|
||||
src/mkconnect/advertiser/AdvertiserMicroPython.py
|
||||
src/mkconnect/advertiser/AdvertisingDevice.py
|
||||
src/mkconnect/advertiser/IAdvertiser.py
|
||||
src/mkconnect/advertiser/IAdvertisingDevice.py
|
||||
src/mkconnect/advertiser/__init__.py
|
||||
src/mkconnect/btsocket/__init__.py
|
||||
src/mkconnect/btsocket/btmgmt_callback.py
|
||||
src/mkconnect/btsocket/btmgmt_protocol.py
|
||||
src/mkconnect/btsocket/btmgmt_socket.py
|
||||
src/mkconnect/btsocket/btmgmt_sync.py
|
||||
src/mkconnect/btsocket/tools.py
|
||||
src/mkconnect/examples/consoletest.py
|
||||
src/mkconnect/examples/main.py
|
||||
src/mkconnect/mouldking/MouldKing.py
|
||||
src/mkconnect/mouldking/MouldKingCrypt.py
|
||||
src/mkconnect/mouldking/MouldKingHub.py
|
||||
src/mkconnect/mouldking/MouldKingHub_Byte.py
|
||||
src/mkconnect/mouldking/MouldKingHub_Nibble.py
|
||||
src/mkconnect/mouldking/MouldKing_Hub_4.py
|
||||
src/mkconnect/mouldking/MouldKing_Hub_6.py
|
||||
src/mkconnect/mouldking/MouldKing_Hubs_4_12Ch.py
|
||||
src/mkconnect/mouldking/__init__.py
|
||||
src/mkconnect/tracer/Tracer.py
|
||||
src/mkconnect/tracer/TracerConsole.py
|
||||
src/mkconnect/tracer/__init__.py
|
||||
1
src/mkconnect.egg-info/dependency_links.txt
Normal file
1
src/mkconnect.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
||||
|
||||
2
src/mkconnect.egg-info/entry_points.txt
Normal file
2
src/mkconnect.egg-info/entry_points.txt
Normal file
@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
mkconnect = mkconnect.cli:main
|
||||
1
src/mkconnect.egg-info/top_level.txt
Normal file
1
src/mkconnect.egg-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
||||
mkconnect
|
||||
0
src/mkconnect/__init__.py
Normal file
0
src/mkconnect/__init__.py
Normal file
3
src/mkconnect/__main__.py
Normal file
3
src/mkconnect/__main__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .cli import main
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
BIN
src/mkconnect/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/mkconnect/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/mkconnect/__pycache__/cli.cpython-313.pyc
Normal file
BIN
src/mkconnect/__pycache__/cli.cpython-313.pyc
Normal file
Binary file not shown.
108
src/mkconnect/advertiser/Advertiser.py
Normal file
108
src/mkconnect/advertiser/Advertiser.py
Normal file
@ -0,0 +1,108 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
import sys
|
||||
|
||||
if (sys.platform == 'rp2'):
|
||||
import _thread as thread
|
||||
else:
|
||||
import threading as thread
|
||||
|
||||
from .IAdvertiser import IAdvertiser
|
||||
from .IAdvertisingDevice import IAdvertisingDevice
|
||||
|
||||
from ..tracer.Tracer import Tracer
|
||||
|
||||
class Advertiser(IAdvertiser) :
|
||||
"""
|
||||
This is the BaseClass for all Advertiser classes.
|
||||
It implements the interface IAdvertiser.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
initializes the object and defines the member fields
|
||||
"""
|
||||
self._tracer = None
|
||||
|
||||
# dictionary to administer the registered AdvertisingDevices.
|
||||
# * key is the instance of the AdvertisingDevice
|
||||
# * value is the AdvertisementIdentifier of the AdvertisingDevice
|
||||
self._registeredDeviceTable = dict()
|
||||
|
||||
if (sys.platform == 'rp2'):
|
||||
self._registeredDeviceTable_Lock = thread.allocate_lock()
|
||||
else:
|
||||
self._registeredDeviceTable_Lock = thread.Lock()
|
||||
return
|
||||
|
||||
|
||||
def SetTracer(self, tracer: Tracer) -> Tracer:
|
||||
"""
|
||||
set tracer object
|
||||
"""
|
||||
self._tracer = tracer
|
||||
return tracer
|
||||
|
||||
|
||||
def AdvertisementStop(self) -> None:
|
||||
"""
|
||||
stop bluetooth advertising for the Advertiser
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
def TryRegisterAdvertisingDevice(self, advertisingDevice: IAdvertisingDevice) -> bool:
|
||||
"""
|
||||
try to register the given AdvertisingDevice
|
||||
* returns True if the AdvertisingDevice was registered successfully
|
||||
* returns False if the AdvertisingDevice wasn't registered successfully (because it still was registered)
|
||||
"""
|
||||
if(advertisingDevice is None):
|
||||
return False
|
||||
|
||||
try:
|
||||
# acquire lock for table
|
||||
#self._registeredDeviceTable_Lock.acquire(blocking=True)
|
||||
self._registeredDeviceTable_Lock.acquire()
|
||||
|
||||
if(advertisingDevice in self._registeredDeviceTable):
|
||||
return False
|
||||
else:
|
||||
self._registeredDeviceTable[advertisingDevice] = advertisingDevice.GetAdvertisementIdentifier()
|
||||
return True
|
||||
finally:
|
||||
# release lock for table
|
||||
self._registeredDeviceTable_Lock.release()
|
||||
|
||||
|
||||
def TryUnregisterAdvertisingDevice(self, advertisingDevice: IAdvertisingDevice) -> bool:
|
||||
"""
|
||||
try to unregister the given AdvertisingDevice
|
||||
* returns True if the AdvertisingDevice was unregistered successfully
|
||||
* returns False if the AdvertisingDevice wasn't unregistered successfully
|
||||
"""
|
||||
if(advertisingDevice is None):
|
||||
return False
|
||||
|
||||
try:
|
||||
# acquire lock for table
|
||||
#self._registeredDeviceTable_Lock.acquire(blocking=True)
|
||||
self._registeredDeviceTable_Lock.acquire()
|
||||
|
||||
if(advertisingDevice in self._registeredDeviceTable):
|
||||
self._registeredDeviceTable.pop(advertisingDevice)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
finally:
|
||||
# release lock for table
|
||||
self._registeredDeviceTable_Lock.release()
|
||||
|
||||
|
||||
def AdvertisementDataSet(self, advertisementIdentifier: str, manufacturerId: bytes, rawdata: bytes) -> None:
|
||||
"""
|
||||
Sets Advertisement-Data for a specific AdvertisementIdentifier
|
||||
This Methode has to be overridden by the implementation of the AdvertisingDevice!
|
||||
"""
|
||||
return
|
||||
277
src/mkconnect/advertiser/AdvertiserBTMgmt.py
Normal file
277
src/mkconnect/advertiser/AdvertiserBTMgmt.py
Normal file
@ -0,0 +1,277 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from ..tracer.Tracer import Tracer
|
||||
from .IAdvertisingDevice import IAdvertisingDevice
|
||||
from .Advertiser import Advertiser
|
||||
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
class AdvertiserBTMgmt(Advertiser) :
|
||||
"""
|
||||
baseclass
|
||||
"""
|
||||
|
||||
# protected static field
|
||||
_BTMgmt_path = '/usr/bin/btmgmt'
|
||||
|
||||
# Number of repetitions per second
|
||||
_RepetitionsPerSecond = 4
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
initializes the object and defines the member fields
|
||||
"""
|
||||
|
||||
super().__init__() # call baseclass
|
||||
|
||||
self._advertisement_thread_Run = False
|
||||
self._advertisement_thread = None
|
||||
self._advertisement_thread_Lock = threading.Lock()
|
||||
|
||||
# Table
|
||||
# * key: AdvertisementIdentifier
|
||||
# * value: advertisement-command for the call of btmgmt tool
|
||||
self._advertisementTable_thread_Lock = threading.Lock()
|
||||
self._advertisementTable = dict()
|
||||
|
||||
self._lastSetAdvertisementCommand = None
|
||||
|
||||
return
|
||||
|
||||
|
||||
def TryRegisterAdvertisingDevice(self, advertisingDevice: IAdvertisingDevice) -> bool:
|
||||
"""
|
||||
try to register the given AdvertisingDevice
|
||||
* returns True if the AdvertisingDevice was registered successfully
|
||||
* returns False if the AdvertisingDevice wasn't registered successfully (because it still was registered)
|
||||
"""
|
||||
result = super().TryRegisterAdvertisingDevice(advertisingDevice)
|
||||
|
||||
# AdvertisingDevice was registered successfully in baseclass
|
||||
if(result):
|
||||
# register AdvertisindIdentifier -> only registered AdvertisindIdentifier will be sent
|
||||
advertisementIdentifier = advertisingDevice.GetAdvertisementIdentifier()
|
||||
self._RegisterAdvertisementIdentifier(advertisementIdentifier)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def TryUnregisterAdvertisingDevice(self, advertisingDevice: IAdvertisingDevice) -> bool:
|
||||
"""
|
||||
try to unregister the given AdvertisingDevice
|
||||
* returns True if the AdvertisingDevice was unregistered successfully
|
||||
* returns False if the AdvertisingDevice wasn't unregistered successfully
|
||||
"""
|
||||
result = super().TryUnregisterAdvertisingDevice(advertisingDevice)
|
||||
|
||||
# AdvertisingDevice was unregistered successfully in baseclass
|
||||
if(result):
|
||||
# unregister AdvertisementIdentifier to remove from publishing
|
||||
advertisementIdentifier = advertisingDevice.GetAdvertisementIdentifier()
|
||||
self._UnregisterAdvertisementIdentifier(advertisementIdentifier)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def AdvertisementStop(self) -> None:
|
||||
"""
|
||||
stop bluetooth advertising
|
||||
"""
|
||||
|
||||
# stop publishing thread
|
||||
self._advertisement_thread_Run = False
|
||||
if(self._advertisement_thread is not None):
|
||||
self._advertisement_thread.join()
|
||||
self._advertisement_thread = None
|
||||
|
||||
#self._advertisementTable.clear()
|
||||
|
||||
advertisementCommand = self._BTMgmt_path + ' rm-adv 1' + ' &> /dev/null'
|
||||
subprocess.run(advertisementCommand, shell=True, executable="/bin/bash")
|
||||
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo('AdvertiserBTMgmnt.AdvertisementStop')
|
||||
self._tracer.TraceInfo(advertisementCommand)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def _RegisterAdvertisementIdentifier(self, advertisementIdentifier: str) -> None:
|
||||
"""
|
||||
Register AdvertisementIdentifier
|
||||
"""
|
||||
|
||||
try:
|
||||
self._advertisementTable_thread_Lock.acquire(blocking=True)
|
||||
|
||||
if(not advertisementIdentifier in self._advertisementTable):
|
||||
self._advertisementTable[advertisementIdentifier] = None
|
||||
finally:
|
||||
self._advertisementTable_thread_Lock.release()
|
||||
|
||||
return
|
||||
|
||||
|
||||
def _UnregisterAdvertisementIdentifier(self, advertisementIdentifier: str) -> None:
|
||||
"""
|
||||
Unregister AdvertisementIdentifier
|
||||
"""
|
||||
try:
|
||||
self._registeredDeviceTable_Lock.acquire(blocking=True)
|
||||
|
||||
foundAdvertisementIdentifier = False
|
||||
|
||||
# there are devices wich share the same AdvertisementIdentifier
|
||||
# check if AdvertisementIdentifier is still present
|
||||
for currentAdvertisementIdentifier in self._registeredDeviceTable.values():
|
||||
if(currentAdvertisementIdentifier == advertisementIdentifier):
|
||||
foundAdvertisementIdentifier = True
|
||||
break
|
||||
|
||||
if(not foundAdvertisementIdentifier):
|
||||
self._RemoveAdvertisementIdentifier(advertisementIdentifier)
|
||||
|
||||
finally:
|
||||
self._registeredDeviceTable_Lock.release()
|
||||
return
|
||||
|
||||
def _RemoveAdvertisementIdentifier(self, advertisementIdentifier: str) -> None:
|
||||
"""
|
||||
Remove AdvertisementIdentifier
|
||||
"""
|
||||
|
||||
try:
|
||||
self._advertisementTable_thread_Lock.acquire(blocking=True)
|
||||
|
||||
if(advertisementIdentifier in self._advertisementTable):
|
||||
self._advertisementTable.pop(advertisementIdentifier)
|
||||
|
||||
finally:
|
||||
self._advertisementTable_thread_Lock.release()
|
||||
|
||||
if(len(self._advertisementTable) == 0):
|
||||
self.AdvertisementStop()
|
||||
|
||||
return
|
||||
|
||||
def AdvertisementDataSet(self, advertisementIdentifier: str, manufacturerId: bytes, rawdata: bytes) -> None:
|
||||
"""
|
||||
Set Advertisement data
|
||||
"""
|
||||
|
||||
try:
|
||||
self._advertisementTable_thread_Lock.acquire(blocking=True)
|
||||
|
||||
# only registered AdvertisementIdentifier are handled
|
||||
if(advertisementIdentifier in self._advertisementTable):
|
||||
advertisementCommand = self._BTMgmt_path + ' add-adv -d ' + self._CreateTelegramForBTMgmmt(manufacturerId, rawdata) + ' --general-discov 1' + ' &> /dev/null'
|
||||
self._advertisementTable[advertisementIdentifier] = advertisementCommand
|
||||
|
||||
# for quick change handle immediately
|
||||
timeSlot = self._CalcTimeSlot()
|
||||
self._Advertise(advertisementCommand, timeSlot)
|
||||
finally:
|
||||
self._advertisementTable_thread_Lock.release()
|
||||
|
||||
# start publish thread if necessary
|
||||
if(not self._advertisement_thread_Run):
|
||||
self._advertisement_thread = threading.Thread(target=self._publish)
|
||||
self._advertisement_thread.daemon = True
|
||||
self._advertisement_thread.start()
|
||||
self._advertisement_thread_Run = True
|
||||
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo('AdvertiserBTMgmnt.AdvertisementSet')
|
||||
|
||||
return
|
||||
|
||||
|
||||
def _publish(self) -> None:
|
||||
"""
|
||||
publishing loop
|
||||
"""
|
||||
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo('AdvertiserBTMgmnt._publish')
|
||||
|
||||
# loop while field is True
|
||||
while(self._advertisement_thread_Run):
|
||||
try:
|
||||
try:
|
||||
self._advertisementTable_thread_Lock.acquire(blocking=True)
|
||||
|
||||
# make a copy of the table to release the lock as quick as possible
|
||||
copy_of_advertisementTable = self._advertisementTable.copy()
|
||||
|
||||
# calc time for one publishing slot
|
||||
timeSlot = self._CalcTimeSlot()
|
||||
finally:
|
||||
self._advertisementTable_thread_Lock.release()
|
||||
|
||||
if(len(copy_of_advertisementTable) == 0):
|
||||
pass
|
||||
else:
|
||||
for key, advertisementCommand in copy_of_advertisementTable.items():
|
||||
# stop publishing?
|
||||
if(not self._advertisement_thread_Run):
|
||||
return
|
||||
|
||||
self._Advertise(advertisementCommand, timeSlot)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def _CalcTimeSlot(self) -> float:
|
||||
"""
|
||||
Calculates the timespan in seconds for each timeslot
|
||||
"""
|
||||
|
||||
# timeSlot = 1 second / repetitionsPerSecond / len(self._advertisementTable)
|
||||
timeSlot = 1 / self._RepetitionsPerSecond / max(1, len(self._advertisementTable))
|
||||
return timeSlot
|
||||
|
||||
|
||||
def _Advertise(self, advertisementCommand: str, timeSlot: float) -> None:
|
||||
"""
|
||||
calls the btmgmt tool as subprocess
|
||||
"""
|
||||
|
||||
try:
|
||||
self._advertisement_thread_Lock.acquire(blocking=True)
|
||||
timeStart = time.time()
|
||||
|
||||
if (self._lastSetAdvertisementCommand != advertisementCommand):
|
||||
self._lastSetAdvertisementCommand = advertisementCommand
|
||||
|
||||
subprocess.run(advertisementCommand, shell=True, executable="/bin/bash")
|
||||
|
||||
timeEnd = time.time()
|
||||
timeDelta = timeEnd - timeStart
|
||||
timeSlotRemain = max(0.001, timeSlot - timeDelta)
|
||||
|
||||
# stop publishing?
|
||||
if(self._advertisement_thread_Run):
|
||||
time.sleep(timeSlotRemain)
|
||||
finally:
|
||||
self._advertisement_thread_Lock.release()
|
||||
|
||||
return
|
||||
|
||||
def _CreateTelegramForBTMgmmt(self, manufacturerId: bytes, rawDataArray: bytes) -> str:
|
||||
"""
|
||||
Create input data for btmgmt
|
||||
"""
|
||||
rawDataArrayLen = len(rawDataArray)
|
||||
|
||||
resultArray = bytearray(4 + rawDataArrayLen)
|
||||
resultArray[0] = rawDataArrayLen + 3 # len
|
||||
resultArray[1] = 0xFF # type manufacturer specific
|
||||
resultArray[2] = manufacturerId[1] # companyId
|
||||
resultArray[3] = manufacturerId[0] # companyId
|
||||
for index in range(rawDataArrayLen):
|
||||
resultArray[index + 4] = rawDataArray[index]
|
||||
|
||||
return ''.join(f'{x:02x}' for x in resultArray)
|
||||
500
src/mkconnect/advertiser/AdvertiserBTSocket.py
Normal file
500
src/mkconnect/advertiser/AdvertiserBTSocket.py
Normal file
@ -0,0 +1,500 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
import sys
|
||||
import enum
|
||||
import threading
|
||||
import time
|
||||
|
||||
from ..btsocket import btmgmt_socket
|
||||
from ..btsocket import btmgmt_protocol
|
||||
|
||||
from ..tracer.Tracer import Tracer
|
||||
|
||||
from .Advertiser import Advertiser, IAdvertisingDevice
|
||||
|
||||
class AdvertiserBTSocket(Advertiser) :
|
||||
"""
|
||||
baseclass
|
||||
"""
|
||||
|
||||
class Flags(enum.IntEnum):
|
||||
CONNECTABLE = enum.auto()
|
||||
GENERAL_DISCOVERABLE = enum.auto()
|
||||
LIMITED_DISCOVERABLE = enum.auto()
|
||||
FLAGS_IN_ADV_DATA = enum.auto()
|
||||
TX_IN_ADV_DATA = enum.auto()
|
||||
APPEARANCE_IN_ADV_DATA = enum.auto()
|
||||
LOCAL_NAME_IN_ADV_DATA = enum.auto()
|
||||
PHY_LE_1M = enum.auto()
|
||||
PHY_LE_2M = enum.auto()
|
||||
PHY_LE_CODED = enum.auto()
|
||||
|
||||
# Number of repetitions per second
|
||||
_RepetitionsPerSecond = 4
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
initializes the object and defines the member fields
|
||||
"""
|
||||
super().__init__() # call baseclass
|
||||
|
||||
self._advertisement_thread_Run = False
|
||||
self._advertisement_thread = None
|
||||
self._advertisement_thread_Lock = threading.Lock()
|
||||
|
||||
# Table
|
||||
# * key: AdvertisementIdentifier
|
||||
# * value: advertisement-command for the call of btmgmt tool
|
||||
self._advertisementTable_thread_Lock = threading.Lock()
|
||||
self._advertisementTable = dict()
|
||||
|
||||
self._lastSetAdvertisementCommand = None
|
||||
|
||||
self.sock = btmgmt_socket.open()
|
||||
|
||||
return
|
||||
|
||||
|
||||
def TryRegisterAdvertisingDevice(self, advertisingDevice: IAdvertisingDevice) -> bool:
|
||||
"""
|
||||
try to register the given AdvertisingDevice
|
||||
* returns True if the AdvertisingDevice was registered successfully
|
||||
* returns False if the AdvertisingDevice wasn't registered successfully (because it still was registered)
|
||||
"""
|
||||
result = super().TryRegisterAdvertisingDevice(advertisingDevice)
|
||||
|
||||
# AdvertisingDevice was registered successfully in baseclass
|
||||
if(result):
|
||||
# register AdvertisindIdentifier -> only registered AdvertisindIdentifier will be sent
|
||||
advertisementIdentifier = advertisingDevice.GetAdvertisementIdentifier()
|
||||
self._RegisterAdvertisementIdentifier(advertisementIdentifier)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def TryUnregisterAdvertisingDevice(self, advertisingDevice: IAdvertisingDevice) -> bool:
|
||||
"""
|
||||
try to unregister the given AdvertisingDevice
|
||||
* returns True if the AdvertisingDevice was unregistered successfully
|
||||
* returns False if the AdvertisingDevice wasn't unregistered successfully
|
||||
"""
|
||||
result = super().TryUnregisterAdvertisingDevice(advertisingDevice)
|
||||
|
||||
# AdvertisingDevice was unregistered successfully in baseclass
|
||||
if(result):
|
||||
# unregister AdvertisementIdentifier to remove from publishing
|
||||
advertisementIdentifier = advertisingDevice.GetAdvertisementIdentifier()
|
||||
self._UnregisterAdvertisementIdentifier(advertisementIdentifier)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def AdvertisementStop(self) -> None:
|
||||
"""
|
||||
stop bluetooth advertising
|
||||
"""
|
||||
|
||||
# stop publishing thread
|
||||
self._advertisement_thread_Run = False
|
||||
if(self._advertisement_thread is not None):
|
||||
self._advertisement_thread.join()
|
||||
self._advertisement_thread = None
|
||||
|
||||
advertisementCommand = AdvertiserBTSocket._create_rm_advert_command(1)
|
||||
self.sock.send(advertisementCommand)
|
||||
|
||||
# if (self._tracer is not None):
|
||||
# self._tracer.TraceInfo('AdvertiserBTMgmnt.AdvertisementStop')
|
||||
# self._tracer.TraceInfo(advertisementCommand)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def _RegisterAdvertisementIdentifier(self, advertisementIdentifier: str) -> None:
|
||||
"""
|
||||
Register AdvertisementIdentifier
|
||||
"""
|
||||
|
||||
try:
|
||||
self._advertisementTable_thread_Lock.acquire(blocking=True)
|
||||
|
||||
if(not advertisementIdentifier in self._advertisementTable):
|
||||
self._advertisementTable[advertisementIdentifier] = None
|
||||
finally:
|
||||
self._advertisementTable_thread_Lock.release()
|
||||
|
||||
return
|
||||
|
||||
|
||||
def _UnregisterAdvertisementIdentifier(self, advertisementIdentifier: str) -> None:
|
||||
"""
|
||||
Unregister AdvertisementIdentifier
|
||||
"""
|
||||
try:
|
||||
self._registeredDeviceTable_Lock.acquire(blocking=True)
|
||||
|
||||
foundAdvertisementIdentifier = False
|
||||
|
||||
# there are devices wich share the same AdvertisementIdentifier
|
||||
# check if AdvertisementIdentifier is still present
|
||||
for currentAdvertisementIdentifier in self._registeredDeviceTable.values():
|
||||
if(currentAdvertisementIdentifier == advertisementIdentifier):
|
||||
foundAdvertisementIdentifier = True
|
||||
break
|
||||
|
||||
if(not foundAdvertisementIdentifier):
|
||||
self._RemoveAdvertisementIdentifier(advertisementIdentifier)
|
||||
|
||||
finally:
|
||||
self._registeredDeviceTable_Lock.release()
|
||||
return
|
||||
|
||||
def _RemoveAdvertisementIdentifier(self, advertisementIdentifier: str) -> None:
|
||||
"""
|
||||
Remove AdvertisementIdentifier
|
||||
"""
|
||||
|
||||
try:
|
||||
self._advertisementTable_thread_Lock.acquire(blocking=True)
|
||||
|
||||
if(advertisementIdentifier in self._advertisementTable):
|
||||
self._advertisementTable.pop(advertisementIdentifier)
|
||||
|
||||
finally:
|
||||
self._advertisementTable_thread_Lock.release()
|
||||
|
||||
if(len(self._advertisementTable) == 0):
|
||||
self.AdvertisementStop()
|
||||
|
||||
return
|
||||
|
||||
def AdvertisementDataSet(self, advertisementIdentifier: str, manufacturerId: bytes, rawdata: bytes) -> None:
|
||||
"""
|
||||
Set Advertisement data
|
||||
"""
|
||||
|
||||
try:
|
||||
self._advertisementTable_thread_Lock.acquire(blocking=True)
|
||||
|
||||
# only registered AdvertisementIdentifier are handled
|
||||
if(advertisementIdentifier in self._advertisementTable):
|
||||
# advertisementCommand = self._BTMgmt_path + ' add-adv -d ' + self._CreateTelegramForBTMgmmt(manufacturerId, rawdata) + ' --general-discov 1' + ' &> /dev/null'
|
||||
advertisingData = self._CreateAdvertisingDataString(manufacturerId, rawdata)
|
||||
|
||||
advertisementCommand = AdvertiserBTSocket._create_add_advert_command(
|
||||
instance_id=1,
|
||||
flags=AdvertiserBTSocket.Flags.GENERAL_DISCOVERABLE,
|
||||
duration=0x00, # zero means use default
|
||||
timeout=0x00, # zero means use default
|
||||
#adv_data='1bfff0ff6DB643CF7E8F471188665938D17AAA26495E131415161718',
|
||||
adv_data=advertisingData,
|
||||
scan_rsp='',
|
||||
)
|
||||
self._advertisementTable[advertisementIdentifier] = advertisementCommand
|
||||
|
||||
# for quick change handle immediately
|
||||
timeSlot = self._CalcTimeSlot()
|
||||
self._Advertise(advertisementCommand, timeSlot)
|
||||
finally:
|
||||
self._advertisementTable_thread_Lock.release()
|
||||
|
||||
# start publish thread if necessary
|
||||
if(not self._advertisement_thread_Run):
|
||||
self._advertisement_thread = threading.Thread(target=self._publish)
|
||||
self._advertisement_thread.daemon = True
|
||||
self._advertisement_thread.start()
|
||||
self._advertisement_thread_Run = True
|
||||
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo('AdvertiserBTMgmnt.AdvertisementSet')
|
||||
|
||||
return
|
||||
|
||||
|
||||
def _publish(self) -> None:
|
||||
"""
|
||||
publishing loop
|
||||
"""
|
||||
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo('AdvertiserBTMgmnt._publish')
|
||||
|
||||
# loop while field is True
|
||||
while(self._advertisement_thread_Run):
|
||||
try:
|
||||
try:
|
||||
self._advertisementTable_thread_Lock.acquire(blocking=True)
|
||||
|
||||
# make a copy of the table to release the lock as quick as possible
|
||||
copy_of_advertisementTable = self._advertisementTable.copy()
|
||||
|
||||
# calc time for one publishing slot
|
||||
timeSlot = self._CalcTimeSlot()
|
||||
finally:
|
||||
self._advertisementTable_thread_Lock.release()
|
||||
|
||||
if(len(copy_of_advertisementTable) == 0):
|
||||
pass
|
||||
else:
|
||||
for key, advertisementCommand in copy_of_advertisementTable.items():
|
||||
# stop publishing?
|
||||
if(not self._advertisement_thread_Run):
|
||||
return
|
||||
|
||||
self._Advertise(advertisementCommand, timeSlot)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def _CalcTimeSlot(self) -> float:
|
||||
"""
|
||||
Calculates the timespan in seconds for each timeslot
|
||||
"""
|
||||
|
||||
# timeSlot = 1 second / repetitionsPerSecond / len(self._advertisementTable)
|
||||
timeSlot = 1 / self._RepetitionsPerSecond / max(1, len(self._advertisementTable))
|
||||
return timeSlot
|
||||
|
||||
|
||||
def _Advertise(self, advertisementCommand: str, timeSlot: float) -> None:
|
||||
"""
|
||||
calls the btmgmt tool as subprocess
|
||||
"""
|
||||
|
||||
try:
|
||||
self._advertisement_thread_Lock.acquire(blocking=True)
|
||||
timeStart = time.time()
|
||||
|
||||
if (self._lastSetAdvertisementCommand != advertisementCommand):
|
||||
self._lastSetAdvertisementCommand = advertisementCommand
|
||||
|
||||
# self.loop.call_soon(self.sock.send, advertisementCommand)
|
||||
self.sock.send(advertisementCommand)
|
||||
|
||||
timeEnd = time.time()
|
||||
timeDelta = timeEnd - timeStart
|
||||
timeSlotRemain = max(0.001, timeSlot - timeDelta)
|
||||
|
||||
# stop publishing?
|
||||
if(self._advertisement_thread_Run):
|
||||
time.sleep(timeSlotRemain)
|
||||
finally:
|
||||
self._advertisement_thread_Lock.release()
|
||||
|
||||
return
|
||||
|
||||
def _CreateAdvertisingDataString(self, manufacturerId: bytes, rawDataArray: bytes) -> str:
|
||||
"""
|
||||
Create input data for btmgmt
|
||||
"""
|
||||
rawDataArrayLen = len(rawDataArray)
|
||||
|
||||
resultArray = bytearray(4 + rawDataArrayLen)
|
||||
resultArray[0] = rawDataArrayLen + 3 # len
|
||||
resultArray[1] = 0xFF # type manufacturer specific
|
||||
resultArray[2] = manufacturerId[1] # companyId
|
||||
resultArray[3] = manufacturerId[0] # companyId
|
||||
for index in range(rawDataArrayLen):
|
||||
resultArray[index + 4] = rawDataArray[index]
|
||||
|
||||
return ''.join(f'{x:02x}' for x in resultArray)
|
||||
|
||||
@staticmethod
|
||||
def _little_bytes(value, size_of):
|
||||
return int(value).to_bytes(size_of, byteorder='little')
|
||||
|
||||
@staticmethod
|
||||
def _create_add_advert_command(instance_id, flags, duration, timeout, adv_data, scan_rsp):
|
||||
""" Add Advertising Command
|
||||
|
||||
Command Code: 0x003e
|
||||
Controller Index: <controller id>
|
||||
Command Parameters: Instance (1 Octet)
|
||||
Flags (4 Octets)
|
||||
Duration (2 Octets)
|
||||
Timeout (2 Octets)
|
||||
Adv_Data_Len (1 Octet)
|
||||
Scan_Rsp_Len (1 Octet)
|
||||
Adv_Data (0-255 Octets)
|
||||
Scan_Rsp (0-255 Octets)
|
||||
Return Parameters: Instance (1 Octet)
|
||||
|
||||
This command is used to configure an advertising instance that
|
||||
can be used to switch a Bluetooth Low Energy controller into
|
||||
advertising mode.
|
||||
|
||||
Added advertising information with this command will not be visible
|
||||
immediately if advertising is enabled via the Set Advertising
|
||||
command. The usage of the Set Advertising command takes precedence
|
||||
over this command. Instance information is stored and will be
|
||||
advertised once advertising via Set Advertising has been disabled.
|
||||
|
||||
The Instance identifier is a value between 1 and the number of
|
||||
supported instances. The value 0 is reserved.
|
||||
|
||||
With the Flags value the type of advertising is controlled and
|
||||
the following flags are defined:
|
||||
|
||||
0 Switch into Connectable mode
|
||||
1 Advertise as Discoverable
|
||||
2 Advertise as Limited Discoverable
|
||||
3 Add Flags field to Adv_Data
|
||||
4 Add TX Power field to Adv_Data
|
||||
5 Add Appearance field to Scan_Rsp
|
||||
6 Add Local Name in Scan_Rsp
|
||||
7 Secondary Channel with LE 1M
|
||||
8 Secondary Channel with LE 2M
|
||||
9 Secondary Channel with LE Coded
|
||||
|
||||
When the connectable flag is set, then the controller will use
|
||||
undirected connectable advertising. The value of the connectable
|
||||
setting can be overwritten this way. This is useful to switch a
|
||||
controller into connectable mode only for LE operation. This is
|
||||
similar to the mode 0x02 from the Set Advertising command.
|
||||
|
||||
When the connectable flag is not set, then the controller will
|
||||
use advertising based on the connectable setting. When using
|
||||
non-connectable or scannable advertising, the controller will
|
||||
be programmed with a non-resolvable random address. When the
|
||||
system is connectable, then the identity address or resolvable
|
||||
private address will be used.
|
||||
|
||||
Using the connectable flag is useful for peripheral mode support
|
||||
where BR/EDR (and/or LE) is controlled by Add Device. This allows
|
||||
making the peripheral connectable without having to interfere
|
||||
with the global connectable setting.
|
||||
|
||||
If Scan_Rsp_Len is zero and connectable flag is not set and
|
||||
the global connectable setting is off, then non-connectable
|
||||
advertising is used. If Scan_Rsp_Len is larger than zero and
|
||||
connectable flag is not set and the global advertising is off,
|
||||
then scannable advertising is used. This small difference is
|
||||
supported to provide less air traffic for devices implementing
|
||||
broadcaster role.
|
||||
|
||||
Secondary channel flags can be used to advertise in secondary
|
||||
channel with the corresponding PHYs. These flag bits are mutually
|
||||
exclusive and setting multiple will result in Invalid Parameter
|
||||
error. Choosing either LE 1M or LE 2M will result in using
|
||||
extended advertising on the primary channel with LE 1M and the
|
||||
respectively LE 1M or LE 2M on the secondary channel. Choosing
|
||||
LE Coded will result in using extended advertising on the primary
|
||||
and secondary channels with LE Coded. Choosing none of these flags
|
||||
will result in legacy advertising.
|
||||
|
||||
The Duration parameter configures the length of an Instance. The
|
||||
value is in seconds.
|
||||
|
||||
A value of 0 indicates a default value is chosen for the
|
||||
Duration. The default is 2 seconds.
|
||||
|
||||
If only one advertising Instance has been added, then the Duration
|
||||
value will be ignored. It only applies for the case where multiple
|
||||
Instances are configured. In that case every Instance will be
|
||||
available for the Duration time and after that it switches to
|
||||
the next one. This is a simple round-robin based approach.
|
||||
|
||||
The Timeout parameter configures the life-time of an Instance. In
|
||||
case the value 0 is used it indicates no expiration time. If a
|
||||
timeout value is provided, then the advertising Instance will be
|
||||
automatically removed when the timeout passes. The value for the
|
||||
timeout is in seconds. Powering down a controller will invalidate
|
||||
all advertising Instances and it is not possible to add a new
|
||||
Instance with a timeout when the controller is powered down.
|
||||
|
||||
When a Timeout is provided, then the Duration subtracts from
|
||||
the actual Timeout value of that Instance. For example an Instance
|
||||
with Timeout of 5 and Duration of 2 will be scheduled exactly 3
|
||||
times, twice with 2 seconds and once with one second. Other
|
||||
Instances have no influence on the Timeout.
|
||||
|
||||
Re-adding an already existing instance (i.e. issuing the Add
|
||||
Advertising command with an Instance identifier of an existing
|
||||
instance) will update that instance's configuration.
|
||||
|
||||
An instance being added or changed while another instance is
|
||||
being advertised will not be visible immediately but only when
|
||||
the new/changed instance is being scheduled by the round robin
|
||||
advertising algorithm.
|
||||
|
||||
Changes to an instance that is currently being advertised will
|
||||
cancel that instance and switch to the next instance. The changes
|
||||
will be visible the next time the instance is scheduled for
|
||||
advertising. In case a single instance is active, this means
|
||||
that changes will be visible right away.
|
||||
|
||||
A pre-requisite is that LE is already enabled, otherwise this
|
||||
command will return a "rejected" response.
|
||||
|
||||
This command can be used when the controller is not powered and
|
||||
all settings will be programmed once powered.
|
||||
|
||||
This command generates a Command Complete event on success or a
|
||||
Command Status event on failure.
|
||||
|
||||
Possible errors: Failed
|
||||
Rejected
|
||||
Not Supported
|
||||
Invalid Parameters
|
||||
Invalid Index
|
||||
"""
|
||||
cmd = AdvertiserBTSocket._little_bytes(0x003e, 2)
|
||||
ctrl_idx = AdvertiserBTSocket._little_bytes(0x00, 2)
|
||||
instance = AdvertiserBTSocket._little_bytes(instance_id, 1) # (1 Octet)
|
||||
flags = AdvertiserBTSocket._little_bytes(flags, 4) # (4 Octets)
|
||||
duration = AdvertiserBTSocket._little_bytes(duration, 2) # (2 Octets)
|
||||
timeout = AdvertiserBTSocket._little_bytes(timeout, 2) # (2 Octets)
|
||||
adv_data = bytes.fromhex(adv_data) # (0-255 Octets)
|
||||
adv_data_len = AdvertiserBTSocket._little_bytes(len(adv_data), 1) # (1 Octet)
|
||||
scan_rsp = bytes.fromhex(scan_rsp) # (0-255 Octets)
|
||||
scan_rsp_len = AdvertiserBTSocket._little_bytes(len(scan_rsp), 1) # (1 Octet)
|
||||
params = instance + flags + duration + timeout + adv_data_len + scan_rsp_len + adv_data + scan_rsp
|
||||
param_len = AdvertiserBTSocket._little_bytes(len(params), 2)
|
||||
|
||||
return cmd + ctrl_idx + param_len + params
|
||||
|
||||
@staticmethod
|
||||
def _create_rm_advert_command(instance_id):
|
||||
""" Remove Advertising Command
|
||||
|
||||
Command Code: 0x003f
|
||||
Controller Index: <controller id>
|
||||
Command Parameters: Instance (1 Octet)
|
||||
Return Parameters: Instance (1 Octet)
|
||||
|
||||
This command is used to remove an advertising instance that
|
||||
can be used to switch a Bluetooth Low Energy controller into
|
||||
advertising mode.
|
||||
|
||||
When the Instance parameter is zero, then all previously added
|
||||
advertising Instances will be removed.
|
||||
|
||||
Removing advertising information with this command will not be
|
||||
visible as long as advertising is enabled via the Set Advertising
|
||||
command. The usage of the Set Advertising command takes precedence
|
||||
over this command. Changes to Instance information are stored and
|
||||
will be advertised once advertising via Set Advertising has been
|
||||
disabled.
|
||||
|
||||
Removing an instance while it is being advertised will immediately
|
||||
cancel the instance, even when it has been advertised less then its
|
||||
configured Timeout or Duration.
|
||||
|
||||
This command can be used when the controller is not powered and
|
||||
all settings will be programmed once powered.
|
||||
|
||||
This command generates a Command Complete event on success or
|
||||
a Command Status event on failure.
|
||||
|
||||
Possible errors: Invalid Parameters
|
||||
Invalid Index
|
||||
"""
|
||||
|
||||
cmd = AdvertiserBTSocket._little_bytes(0x003f, 2)
|
||||
ctrl_idx = AdvertiserBTSocket._little_bytes(0x00, 2)
|
||||
instance = AdvertiserBTSocket._little_bytes(instance_id, 1) # (1 Octet)
|
||||
params = instance
|
||||
param_len = AdvertiserBTSocket._little_bytes(len(params), 2)
|
||||
|
||||
return cmd + ctrl_idx + param_len + params
|
||||
35
src/mkconnect/advertiser/AdvertiserDummy.py
Normal file
35
src/mkconnect/advertiser/AdvertiserDummy.py
Normal file
@ -0,0 +1,35 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from ..tracer.Tracer import Tracer
|
||||
|
||||
from .Advertiser import Advertiser
|
||||
|
||||
|
||||
class AdvertiserDummy(Advertiser) :
|
||||
"""
|
||||
Dummy Advertiser
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
return
|
||||
|
||||
def AdvertisementStop(self) -> None:
|
||||
"""
|
||||
stop bluetooth advertising
|
||||
|
||||
"""
|
||||
|
||||
return
|
||||
|
||||
def AdvertisementDataSet(self, identifier: str, manufacturerId: bytes, rawdata: bytes) -> None:
|
||||
"""
|
||||
Set Advertisement data
|
||||
"""
|
||||
return
|
||||
145
src/mkconnect/advertiser/AdvertiserHCITool.py
Normal file
145
src/mkconnect/advertiser/AdvertiserHCITool.py
Normal file
@ -0,0 +1,145 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from ..tracer.Tracer import Tracer
|
||||
|
||||
from .Advertiser import Advertiser
|
||||
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
class AdvertiserHCITool(Advertiser) :
|
||||
"""
|
||||
baseclass
|
||||
"""
|
||||
|
||||
HCITool_path = '/usr/bin/hcitool'
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._isInitialized = False
|
||||
self._ad_thread_Run = False
|
||||
self._ad_thread = None
|
||||
self._ad_thread_Lock = threading.Lock()
|
||||
self._advertisementTable = dict()
|
||||
|
||||
return
|
||||
|
||||
def AdvertisementStop(self) -> None:
|
||||
"""
|
||||
stop bluetooth advertising
|
||||
"""
|
||||
|
||||
self._ad_thread_Run = False
|
||||
if(self._ad_thread is not None):
|
||||
self._ad_thread.join()
|
||||
self._ad_thread = None
|
||||
self._isInitialized = False
|
||||
|
||||
hcitool_args_0x08_0x000a = self.HCITool_path + ' -i hci0 cmd 0x08 0x000a 00'
|
||||
|
||||
subprocess.run(hcitool_args_0x08_0x000a + ' &> /dev/null', shell=True, executable="/bin/bash")
|
||||
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo('AdvertiserHCITool.AdvertisementStop')
|
||||
self._tracer.TraceInfo(hcitool_args_0x08_0x000a)
|
||||
|
||||
return
|
||||
|
||||
def AdvertisementDataSet(self, identifier: str, manufacturerId: bytes, rawdata: bytes) -> None:
|
||||
"""
|
||||
Set Advertisement data
|
||||
"""
|
||||
|
||||
advertisementCommand = self.HCITool_path + ' -i hci0 cmd 0x08 0x0008 ' + self._CreateTelegramForHCITool(manufacturerId, rawdata)
|
||||
self._ad_thread_Lock.acquire(blocking=True)
|
||||
self._advertisementTable[identifier] = advertisementCommand
|
||||
self._ad_thread_Lock.release()
|
||||
|
||||
if(not self._ad_thread_Run):
|
||||
self._ad_thread = threading.Thread(target=self._publish)
|
||||
self._ad_thread.daemon = True
|
||||
self._ad_thread.start()
|
||||
self._ad_thread_Run = True
|
||||
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo('AdvertiserHCITool.AdvertisementSet')
|
||||
|
||||
return
|
||||
|
||||
def _publish(self) -> None:
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo('AdvertiserHCITool._publish')
|
||||
|
||||
while(self._ad_thread_Run):
|
||||
try:
|
||||
self._ad_thread_Lock.acquire(blocking=True)
|
||||
copy_of_advertisementTable = self._advertisementTable.copy()
|
||||
self._ad_thread_Lock.release()
|
||||
|
||||
# We want to repeat each command
|
||||
repetitionsPerSecond = 4
|
||||
# timeSlot = 1 second / repetitionsPerSecond / len(copy_of_advertisementTable)
|
||||
timeSlot = 1 / repetitionsPerSecond / max(1, len(copy_of_advertisementTable))
|
||||
|
||||
for key, advertisementCommand in copy_of_advertisementTable.items():
|
||||
# stopp publishing?
|
||||
if(not self._ad_thread_Run):
|
||||
return
|
||||
|
||||
timeStart = time.time()
|
||||
subprocess.run(advertisementCommand + ' &> /dev/null', shell=True, executable="/bin/bash")
|
||||
|
||||
if(not self._isInitialized):
|
||||
hcitool_args_0x08_0x0006 = self.HCITool_path + ' -i hci0 cmd 0x08 0x0006 A0 00 A0 00 03 00 00 00 00 00 00 00 00 07 00'
|
||||
hcitool_args_0x08_0x000a = self.HCITool_path + ' -i hci0 cmd 0x08 0x000a 01'
|
||||
|
||||
subprocess.run(hcitool_args_0x08_0x0006 + ' &> /dev/null', shell=True, executable="/bin/bash")
|
||||
subprocess.run(hcitool_args_0x08_0x000a + ' &> /dev/null', shell=True, executable="/bin/bash")
|
||||
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo(str(hcitool_args_0x08_0x0006))
|
||||
self._tracer.TraceInfo(str(hcitool_args_0x08_0x000a))
|
||||
self._tracer.TraceInfo()
|
||||
|
||||
self._isInitialized = True
|
||||
|
||||
timeEnd = time.time()
|
||||
timeDelta = timeEnd - timeStart
|
||||
timeSlotRemain = max(0.001, timeSlot - timeDelta)
|
||||
|
||||
# if (self._tracer is not None):
|
||||
# self._tracer.TraceInfo(str(timeSlotRemain) + " " + str(key) + ": " + str(advertisement))
|
||||
|
||||
# if (self._tracer is not None):
|
||||
# self._tracer.TraceInfo(str(timeSlotRemain))
|
||||
|
||||
time.sleep(timeSlotRemain)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _CreateTelegramForHCITool(self, manufacturerId: bytes, rawDataArray: bytes) -> str:
|
||||
"""
|
||||
Create input data for hcitool
|
||||
"""
|
||||
rawDataArrayLen = len(rawDataArray)
|
||||
|
||||
resultArray = bytearray(8 + rawDataArrayLen)
|
||||
resultArray[0] = rawDataArrayLen + 7 # len
|
||||
resultArray[1] = 0x02 # flags
|
||||
resultArray[2] = 0x01
|
||||
resultArray[3] = 0x02
|
||||
resultArray[4] = rawDataArrayLen + 3 # len
|
||||
resultArray[5] = 0xFF # type manufacturer specific
|
||||
resultArray[6] = manufacturerId[1] # companyId
|
||||
resultArray[7] = manufacturerId[0] # companyId
|
||||
for index in range(rawDataArrayLen):
|
||||
resultArray[index + 8] = rawDataArray[index]
|
||||
|
||||
return ' '.join(f'{x:02x}' for x in resultArray)
|
||||
90
src/mkconnect/advertiser/AdvertiserMicroPython.py
Normal file
90
src/mkconnect/advertiser/AdvertiserMicroPython.py
Normal file
@ -0,0 +1,90 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from ..tracer.Tracer import Tracer
|
||||
|
||||
from .Advertiser import Advertiser
|
||||
|
||||
try:
|
||||
import bluetooth
|
||||
except ImportError as err:
|
||||
print("AdvertiserMicroPython: " + str(err))
|
||||
|
||||
class AdvertiserMicroPython(Advertiser) :
|
||||
"""
|
||||
Advertiser using bluetooth library from MicroPython
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
# Activate bluetooth
|
||||
try:
|
||||
self.ble = bluetooth.BLE()
|
||||
self.ble.active(True)
|
||||
except Exception as exception:
|
||||
self.ble = None
|
||||
print("AdvertiserMicroPython.init: " + str(exception))
|
||||
|
||||
return
|
||||
|
||||
|
||||
def AdvertisementStop(self) -> None:
|
||||
"""
|
||||
stop bluetooth advertising
|
||||
|
||||
"""
|
||||
|
||||
if(self.ble is not None):
|
||||
self.ble.gap_advertise(None)
|
||||
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo("AdvertisementSet")
|
||||
else:
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo("self.ble is None")
|
||||
|
||||
if (self._tracer is not None):
|
||||
pass
|
||||
|
||||
return
|
||||
|
||||
|
||||
def AdvertisementDataSet(self, identifier: str, manufacturerId: bytes, rawdata: bytes) -> None:
|
||||
"""
|
||||
Set Advertisement data
|
||||
"""
|
||||
data = self._CreateTelegramForPicoW(manufacturerId, rawdata)
|
||||
|
||||
if(self.ble is not None):
|
||||
self.ble.gap_advertise(100, data)
|
||||
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo("AdvertisementSet")
|
||||
else:
|
||||
if (self._tracer is not None):
|
||||
self._tracer.TraceInfo("self.ble is None")
|
||||
|
||||
return
|
||||
|
||||
|
||||
def _CreateTelegramForPicoW(self, manufacturerId: bytes, rawDataArray: bytes) -> bytes:
|
||||
"""
|
||||
Create input data for bluetooth lib for Pico W
|
||||
"""
|
||||
rawDataArrayLen = len(rawDataArray)
|
||||
|
||||
btdata = bytearray(2 + rawDataArrayLen)
|
||||
btdata[0] = 0x00
|
||||
btdata[1] = 0xFF
|
||||
for index in range(rawDataArrayLen):
|
||||
btdata[index + 2] = rawDataArray[index]
|
||||
|
||||
btcrypteddata = bytearray(b'\x02\x01\x02') + bytearray((len(btdata) + 1, 0xFF)) + btdata
|
||||
|
||||
return bytes(btcrypteddata)
|
||||
|
||||
99
src/mkconnect/advertiser/AdvertisingDevice.py
Normal file
99
src/mkconnect/advertiser/AdvertisingDevice.py
Normal file
@ -0,0 +1,99 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from ..tracer.Tracer import Tracer
|
||||
from .IAdvertisingDevice import IAdvertisingDevice
|
||||
from .Advertiser import Advertiser
|
||||
|
||||
class AdvertisingDevice(IAdvertisingDevice) :
|
||||
"""
|
||||
baseclass
|
||||
"""
|
||||
|
||||
def __init__(self, identifier: str):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
|
||||
self._connected = False
|
||||
self._advertiser = None
|
||||
self._advertiser_registered = False
|
||||
self._tracer = None
|
||||
self._identifier = identifier
|
||||
|
||||
|
||||
def SetAdvertiser(self, advertiser: Advertiser) -> Advertiser:
|
||||
"""
|
||||
set advertiser object
|
||||
"""
|
||||
if(self._advertiser == advertiser):
|
||||
return advertiser
|
||||
|
||||
reconnect = self._connected
|
||||
|
||||
# unregister
|
||||
if(self._advertiser is not None and self._advertiser_registered):
|
||||
self._advertiser_registered = not self._advertiser.TryUnregisterAdvertisingDevice(self)
|
||||
self._connected = False
|
||||
|
||||
self._advertiser = advertiser
|
||||
|
||||
# register
|
||||
if(self._advertiser is not None and reconnect):
|
||||
self._advertiser_registered = self._advertiser.TryRegisterAdvertisingDevice(self)
|
||||
|
||||
return advertiser
|
||||
|
||||
|
||||
def SetTracer(self, tracer: Tracer) -> Tracer:
|
||||
"""
|
||||
set tracer object
|
||||
"""
|
||||
self._tracer = tracer
|
||||
|
||||
return tracer
|
||||
|
||||
|
||||
def Connect(self):
|
||||
"""
|
||||
connects the device to the advertiser
|
||||
"""
|
||||
|
||||
if(self._advertiser is not None and not self._advertiser_registered):
|
||||
self._advertiser_registered = self._advertiser.TryRegisterAdvertisingDevice(self)
|
||||
|
||||
self._connected = True
|
||||
|
||||
return
|
||||
|
||||
|
||||
def Disconnect(self) -> None:
|
||||
"""
|
||||
disconnects the device from the advertiser
|
||||
"""
|
||||
|
||||
if(self._advertiser is not None and self._advertiser_registered):
|
||||
self._advertiser_registered = not self._advertiser.TryUnregisterAdvertisingDevice(self)
|
||||
|
||||
self._connected = False
|
||||
|
||||
return
|
||||
|
||||
|
||||
def Stop(self) -> bytes:
|
||||
"""
|
||||
stops the device
|
||||
"""
|
||||
|
||||
raise NotImplementedError # override this methode
|
||||
|
||||
|
||||
def AdvertisementSet(self, manufacturerId: bytes, rawdata: bytes) -> None:
|
||||
"""
|
||||
Set Advertisement data
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def GetAdvertisementIdentifier(self) -> str:
|
||||
return self._identifier
|
||||
9
src/mkconnect/advertiser/IAdvertiser.py
Normal file
9
src/mkconnect/advertiser/IAdvertiser.py
Normal file
@ -0,0 +1,9 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
class IAdvertiser :
|
||||
"""
|
||||
(kind of) interface for Advertiser
|
||||
This Type mustn't import any AdvertisingDevice stuff!
|
||||
To prevent cyclic imports caused by imports of Advertiser <--> AdvertisingDevice.
|
||||
"""
|
||||
48
src/mkconnect/advertiser/IAdvertisingDevice.py
Normal file
48
src/mkconnect/advertiser/IAdvertisingDevice.py
Normal file
@ -0,0 +1,48 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
class IAdvertisingDevice :
|
||||
"""
|
||||
(kind of interface) for AdvertisingDevice
|
||||
This Type mustn't import any Advertiser stuff!
|
||||
To prevent cyclic imports caused by imports of Advertiser <--> AdvertisingDevice.
|
||||
"""
|
||||
|
||||
def GetAdvertisementIdentifier(self) -> str:
|
||||
"""
|
||||
Returns the AdvertisementIdentifier to differentiate the Advertising-Data.
|
||||
The AdvertisementIdentifier is used to register the Advertising-Data object.
|
||||
This Methode has to be overridden by the implementation of the AdvertisingDevice!
|
||||
|
||||
Some AdvertisingDevices like MouldKing 4.0 Hub use only one Advertising telegram for all
|
||||
(three possible) devices. So each AdvertisingDevices returns the same AdvertisementIdentifier
|
||||
"""
|
||||
raise NotImplementedError # override this methode
|
||||
|
||||
|
||||
def Connect(self) -> None:
|
||||
"""
|
||||
connects the device to the advertiser
|
||||
"""
|
||||
raise NotImplementedError # override this methode
|
||||
|
||||
|
||||
def Disconnect(self) -> None:
|
||||
"""
|
||||
disconnects the device from the advertiser
|
||||
"""
|
||||
raise NotImplementedError # override this methode
|
||||
|
||||
|
||||
def Stop(self) -> bytes:
|
||||
"""
|
||||
stops the device
|
||||
"""
|
||||
raise NotImplementedError # override this methode
|
||||
|
||||
|
||||
def SetChannel(self, channelId: int, value: float) -> bytes:
|
||||
"""
|
||||
set internal stored value of channel with channelId to value and return the telegram
|
||||
"""
|
||||
raise NotImplementedError # override this methode
|
||||
0
src/mkconnect/advertiser/__init__.py
Normal file
0
src/mkconnect/advertiser/__init__.py
Normal file
BIN
src/mkconnect/advertiser/__pycache__/Advertiser.cpython-313.pyc
Normal file
BIN
src/mkconnect/advertiser/__pycache__/Advertiser.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/mkconnect/advertiser/__pycache__/IAdvertiser.cpython-313.pyc
Normal file
BIN
src/mkconnect/advertiser/__pycache__/IAdvertiser.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/mkconnect/advertiser/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/mkconnect/advertiser/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
0
src/mkconnect/btsocket/__init__.py
Normal file
0
src/mkconnect/btsocket/__init__.py
Normal file
123
src/mkconnect/btsocket/btmgmt_callback.py
Normal file
123
src/mkconnect/btsocket/btmgmt_callback.py
Normal file
@ -0,0 +1,123 @@
|
||||
"""
|
||||
Use callback-based programming style to read and write to BlueZ Management API
|
||||
"""
|
||||
import asyncio
|
||||
from collections import deque
|
||||
|
||||
from . import btmgmt_socket
|
||||
from . import btmgmt_protocol
|
||||
from . import tools
|
||||
|
||||
logger = tools.create_module_logger(__name__)
|
||||
|
||||
|
||||
class Mgmt:
|
||||
|
||||
def __init__(self):
|
||||
# Setup read and write sockets
|
||||
self.sock = btmgmt_socket.open()
|
||||
self.loop = asyncio.get_event_loop()
|
||||
# Store for event callbacks
|
||||
self._event_callbacks = dict()
|
||||
# Queue for commands to be written to BlueZ socket
|
||||
self.cmd_queue = deque()
|
||||
self.running = False
|
||||
|
||||
def add_event_callback(self, event, callback):
|
||||
"""
|
||||
Assign a callback to be called when a specific event happens.
|
||||
The callback should take two arguments.
|
||||
1) The response packet
|
||||
2) the AsyncMgmt() class instance object
|
||||
|
||||
:param event: An entry from the enum btmgmt.Events
|
||||
:param callback: A callback function
|
||||
"""
|
||||
self._event_callbacks[event] = callback
|
||||
|
||||
def reader(self):
|
||||
"""
|
||||
Read callback is called when data available on Bluetooth socket.
|
||||
Processes packet and hands-off to event callbacks that have subscribed
|
||||
to events.
|
||||
"""
|
||||
logger.debug('Reader callback')
|
||||
data = self.sock.recv(100)
|
||||
pkt = btmgmt_protocol.reader(data)
|
||||
logger.info('pkt: [%s]', pkt)
|
||||
if pkt.header.event_code in self._event_callbacks:
|
||||
self._event_callbacks[pkt.header.event_code](pkt, self)
|
||||
if not self.running:
|
||||
self.stop()
|
||||
|
||||
def writer(self):
|
||||
"""
|
||||
Write callback when Bluetooth socket is available for writing.
|
||||
Takes commands that are on the cmd_queue and sends.
|
||||
"""
|
||||
logger.debug('Writer callback')
|
||||
if len(self.cmd_queue) > 0:
|
||||
this_cmd = self.cmd_queue.popleft()
|
||||
logger.info('sending pkt [%s]', tools.format_pkt(this_cmd))
|
||||
self.sock.send(this_cmd)
|
||||
if not self.running and len(self.cmd_queue) == 0:
|
||||
self.loop.stop()
|
||||
# Do one more read to get the response from the last command
|
||||
self.reader()
|
||||
|
||||
@staticmethod
|
||||
def _as_packet(pkt_objs):
|
||||
"""Pack bytes together for sending"""
|
||||
full_pkt = b''
|
||||
for frame in pkt_objs:
|
||||
if frame:
|
||||
full_pkt += frame.octets
|
||||
return full_pkt
|
||||
|
||||
def send(self, cmd, ctrl_idx, *params):
|
||||
"""
|
||||
Add commands onto the queue ready to be sent.
|
||||
Basic structure of the command
|
||||
send(<command_name>, <adapter index>, <positional paramters>)
|
||||
|
||||
:param cmd: A value from btmgmt.Commands
|
||||
:param ctrl_idx: The index of the controller [0xFFFF is non-controller]
|
||||
:param params: 0 or more input parameters for command
|
||||
"""
|
||||
pkt_objs = btmgmt_protocol.command(cmd, ctrl_idx, *params)
|
||||
cmd_pkt = self._as_packet(pkt_objs)
|
||||
logger.debug('Queue command: %s', tools.format_pkt(cmd_pkt))
|
||||
self.cmd_queue.append(cmd_pkt)
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Once all commands have been sent, exit the event loop
|
||||
"""
|
||||
self.running = False
|
||||
|
||||
self.loop.remove_writer(self.sock)
|
||||
self.loop.remove_reader(self.sock)
|
||||
self.loop.stop()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Stop the event loop and close sockets etc.
|
||||
"""
|
||||
btmgmt_socket.close(self.sock)
|
||||
# Stop the event loop
|
||||
self.loop.close()
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
# Setup reader and writer for socket streams
|
||||
self.loop.add_reader(self.sock, self.reader)
|
||||
self.loop.add_writer(self.sock, self.writer)
|
||||
logger.debug('Starting event loop...')
|
||||
try:
|
||||
# Run the event loop
|
||||
self.loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
self.loop.stop()
|
||||
finally:
|
||||
# We are done. Close sockets and the event loop.
|
||||
self.close()
|
||||
1075
src/mkconnect/btsocket/btmgmt_protocol.py
Normal file
1075
src/mkconnect/btsocket/btmgmt_protocol.py
Normal file
File diff suppressed because it is too large
Load Diff
120
src/mkconnect/btsocket/btmgmt_socket.py
Normal file
120
src/mkconnect/btsocket/btmgmt_socket.py
Normal file
@ -0,0 +1,120 @@
|
||||
import asyncio
|
||||
import ctypes
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
AF_BLUETOOTH = 31
|
||||
PF_BLUETOOTH = AF_BLUETOOTH
|
||||
SOCK_RAW = 3
|
||||
BTPROTO_HCI = 1
|
||||
SOCK_CLOEXEC = 524288
|
||||
SOCK_NONBLOCK = 2048
|
||||
HCI_CHANNEL_CONTROL = 3
|
||||
HCI_DEV_NONE = 0xffff
|
||||
|
||||
|
||||
class BluetoothSocketError(BaseException):
|
||||
pass
|
||||
|
||||
|
||||
class BluetoothCommandError(BaseException):
|
||||
pass
|
||||
|
||||
|
||||
class SocketAddr(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("hci_family", ctypes.c_ushort),
|
||||
("hci_dev", ctypes.c_ushort),
|
||||
("hci_channel", ctypes.c_ushort),
|
||||
]
|
||||
|
||||
|
||||
def open():
|
||||
"""
|
||||
Because of the following issue with Python the Bluetooth User socket
|
||||
on linux needs to be done with lower level calls.
|
||||
https://bugs.python.org/issue36132
|
||||
Based on mgmt socket at:
|
||||
https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/mgmt-api.txt
|
||||
"""
|
||||
|
||||
sockaddr_hcip = ctypes.POINTER(SocketAddr)
|
||||
ctypes.cdll.LoadLibrary("libc.so.6")
|
||||
libc = ctypes.CDLL("libc.so.6")
|
||||
|
||||
libc_socket = libc.socket
|
||||
libc_socket.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.c_int)
|
||||
libc_socket.restype = ctypes.c_int
|
||||
|
||||
bind = libc.bind
|
||||
bind.argtypes = (ctypes.c_int, ctypes.POINTER(SocketAddr), ctypes.c_int)
|
||||
bind.restype = ctypes.c_int
|
||||
|
||||
# fd = libc_socket(PF_BLUETOOTH, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK,
|
||||
# BTPROTO_HCI)
|
||||
fd = libc_socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI)
|
||||
|
||||
if fd < 0:
|
||||
raise BluetoothSocketError("Unable to open PF_BLUETOOTH socket")
|
||||
|
||||
addr = SocketAddr()
|
||||
addr.hci_family = AF_BLUETOOTH # AF_BLUETOOTH
|
||||
addr.hci_dev = HCI_DEV_NONE # adapter index
|
||||
addr.hci_channel = HCI_CHANNEL_CONTROL # HCI_USER_CHANNEL
|
||||
r = bind(fd, sockaddr_hcip(addr), ctypes.sizeof(addr))
|
||||
if r < 0:
|
||||
raise BluetoothSocketError("Unable to bind %s", r)
|
||||
|
||||
sock_fd = socket.socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI, fileno=fd)
|
||||
return sock_fd
|
||||
|
||||
|
||||
def close(bt_socket):
|
||||
"""Close the open socket"""
|
||||
fd = bt_socket.detach()
|
||||
socket.close(fd)
|
||||
|
||||
|
||||
def test_asyncio_usage():
|
||||
sock = open()
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
loop = asyncio.get_event_loop()
|
||||
else:
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
def reader():
|
||||
data = sock.recv(100)
|
||||
print("Received:", data)
|
||||
|
||||
# We are done: unregister the file descriptor
|
||||
loop.remove_reader(sock)
|
||||
|
||||
# Stop the event loop
|
||||
loop.stop()
|
||||
|
||||
# Register the file descriptor for read event
|
||||
loop.add_reader(sock, reader)
|
||||
|
||||
# Write a command to the socket
|
||||
# Read Management Version Information Command
|
||||
# b'\x01\x00\xff\xff\x00\x00'
|
||||
loop.call_soon(sock.send, b'\x01\x00\xff\xff\x00\x00')
|
||||
|
||||
try:
|
||||
# Run the event loop
|
||||
loop.run_forever()
|
||||
finally:
|
||||
# We are done. Close sockets and the event loop.
|
||||
close(sock)
|
||||
loop.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_asyncio_usage()
|
||||
37
src/mkconnect/btsocket/btmgmt_sync.py
Normal file
37
src/mkconnect/btsocket/btmgmt_sync.py
Normal file
@ -0,0 +1,37 @@
|
||||
from . import btmgmt_protocol
|
||||
from . import btmgmt_socket
|
||||
from . import tools
|
||||
|
||||
logger = tools.create_module_logger(__name__)
|
||||
|
||||
def _as_packet(pkt_objs):
|
||||
full_pkt = b''
|
||||
for frame in pkt_objs:
|
||||
if frame:
|
||||
full_pkt += frame.octets
|
||||
return full_pkt
|
||||
|
||||
|
||||
def send(*args):
|
||||
response_recvd = False
|
||||
pkt_objs = btmgmt_protocol.command(*args)
|
||||
logger.debug('Sending btmgmt frames %s', pkt_objs)
|
||||
cmd_pkt = _as_packet(pkt_objs)
|
||||
# print('cmd pkt', [f'{octets:x}' for octets in cmd_pkt])
|
||||
sock = btmgmt_socket.open()
|
||||
logger.debug('Sending bytes: %s', cmd_pkt)
|
||||
sock.send(cmd_pkt)
|
||||
while not response_recvd:
|
||||
raw_data = sock.recv(100)
|
||||
logger.debug('Received: %s', raw_data)
|
||||
data = btmgmt_protocol.reader(raw_data)
|
||||
logger.debug('Received btmgmt frames: %s', data)
|
||||
if data.cmd_response_frame:
|
||||
response_recvd = True
|
||||
|
||||
btmgmt_socket.close(sock)
|
||||
if data.event_frame.status != btmgmt_protocol.ErrorCodes.Success:
|
||||
raise NameError(f'btmgmt Error: '
|
||||
f'{data.event_frame.command_opcode} '
|
||||
f'{data.event_frame.status.name}')
|
||||
return data
|
||||
1
src/mkconnect/btsocket/readme.md
Normal file
1
src/mkconnect/btsocket/readme.md
Normal file
@ -0,0 +1 @@
|
||||
The files are copied from https://github.com/ukBaz/python-btsocket
|
||||
18
src/mkconnect/btsocket/tools.py
Normal file
18
src/mkconnect/btsocket/tools.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""Location for tools/functions to be used across various files"""
|
||||
import logging
|
||||
|
||||
|
||||
def create_module_logger(module_name):
|
||||
"""helper function to create logger in modules"""
|
||||
logger = logging.getLogger(module_name)
|
||||
strm_hndlr = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
strm_hndlr.setFormatter(formatter)
|
||||
logger.addHandler(strm_hndlr)
|
||||
return logger
|
||||
|
||||
|
||||
def format_pkt(data):
|
||||
"""Put data packets in a human readable format"""
|
||||
return ', '.join([f'{bite:#04x}' for bite in data])
|
||||
24
src/mkconnect/cli.py
Normal file
24
src/mkconnect/cli.py
Normal file
@ -0,0 +1,24 @@
|
||||
import sys
|
||||
from .tracer.TracerConsole import TracerConsole
|
||||
from .mouldking.MouldKing import MouldKing
|
||||
|
||||
def main() -> int:
|
||||
tracer = TracerConsole()
|
||||
|
||||
if sys.platform == "linux":
|
||||
from .advertiser.AdvertiserBTSocket import AdvertiserBTSocket as Advertiser
|
||||
elif sys.platform == "rp2":
|
||||
from .advertiser.AdvertiserMicroPython import AdvertiserMicroPython as Advertiser
|
||||
elif sys.platform == "win32":
|
||||
from .advertiser.AdvertiserDummy import AdvertiserDummy as Advertiser
|
||||
else:
|
||||
raise SystemExit("unsupported platform")
|
||||
|
||||
advertiser = Advertiser()
|
||||
advertiser.SetTracer(tracer)
|
||||
|
||||
MouldKing.SetTracer(tracer)
|
||||
MouldKing.SetAdvertiser(advertiser)
|
||||
|
||||
tracer.TraceInfo("mkconnect CLI initialized")
|
||||
return 0
|
||||
179
src/mkconnect/examples/consoletest.py
Normal file
179
src/mkconnect/examples/consoletest.py
Normal file
@ -0,0 +1,179 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# to run: sudo python -i consoletest.py
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
print('Script: consoletest.py')
|
||||
print('Platform: ' + sys.platform)
|
||||
|
||||
from mkconnect.tracer.Tracer import Tracer
|
||||
from mkconnect.tracer.TracerConsole import TracerConsole
|
||||
|
||||
# uncomment to choose advertiser
|
||||
if (sys.platform == 'linux'):
|
||||
#from Advertiser.AdvertiserHCITool import AdvertiserHCITool as Advertiser
|
||||
#from Advertiser.AdvertiserBTMgmt import AdvertiserBTMgmt as Advertiser
|
||||
from mkconnect.advertiser.AdvertiserBTSocket import AdvertiserBTSocket as Advertiser
|
||||
pass
|
||||
elif (sys.platform == 'rp2'):
|
||||
from mkconnect.advertiser.AdvertiserMicroPython import AdvertiserMicroPython as Advertiser
|
||||
pass
|
||||
elif (sys.platform == 'win32'):
|
||||
from mkconnect.advertiser.AdvertiserDummy import AdvertiserDummy as Advertiser
|
||||
else:
|
||||
raise Exception('unsupported platform')
|
||||
|
||||
from mkconnect.mouldking.MouldKing import MouldKing
|
||||
|
||||
# instantiate Tracer
|
||||
tracer = TracerConsole()
|
||||
|
||||
# instantiate Advertiser
|
||||
advertiser = Advertiser()
|
||||
advertiser.SetTracer(tracer)
|
||||
|
||||
# Set Tracer for all MouldKing Hubs
|
||||
MouldKing.SetTracer(tracer)
|
||||
MouldKing.SetAdvertiser(advertiser)
|
||||
|
||||
# save pre-instantiated objects in local variables
|
||||
hub0 = MouldKing.Module6_0.Device0
|
||||
hub1 = MouldKing.Module6_0.Device1
|
||||
hub2 = MouldKing.Module6_0.Device2
|
||||
|
||||
hub3 = MouldKing.Module4_0.Device0
|
||||
hub4 = MouldKing.Module4_0.Device1
|
||||
hub5 = MouldKing.Module4_0.Device2
|
||||
|
||||
def _getChannelId(channel):
|
||||
switch={
|
||||
'A': 0,
|
||||
'B': 1,
|
||||
'C': 2,
|
||||
'D': 3,
|
||||
'E': 4,
|
||||
'F': 5,
|
||||
}
|
||||
return switch.get(channel,"")
|
||||
|
||||
def _getHubId(deviceId):
|
||||
# MK6
|
||||
if deviceId == 0:
|
||||
return hub0
|
||||
elif deviceId == 1:
|
||||
return hub1
|
||||
elif deviceId == 2:
|
||||
return hub2
|
||||
# MK4
|
||||
elif deviceId == 3:
|
||||
return hub3
|
||||
elif deviceId == 4:
|
||||
return hub4
|
||||
elif deviceId == 5:
|
||||
return hub5
|
||||
else:
|
||||
raise Exception("deviceId 0..5")
|
||||
|
||||
def _automate(deviceId: int, channel: int):
|
||||
userinput = input("\nDo you want to test channel "+ str(channel) +" ? enter y/n\n")
|
||||
|
||||
if (userinput != str("y")):
|
||||
return
|
||||
|
||||
tracer.TraceInfo("HUB: "+ str(deviceId) +", FORWARD : Power ramp up from 0 to 100% on channel :" + str(channel))
|
||||
for percent in range(0, 110, 10):
|
||||
tracer.TraceInfo("Power : " + str(percent) + "%")
|
||||
mkcontrol(deviceId,channel, percent/100)
|
||||
time.sleep(1)
|
||||
|
||||
mkstop(deviceId)
|
||||
|
||||
tracer.TraceInfo("HUB: "+ str(deviceId) +", REVERSE: Power ramp up from 0 to 100% on channel :" + str(channel))
|
||||
for percent in range(-0, -110, -10):
|
||||
tracer.TraceInfo("Power : " + str(percent) + "%")
|
||||
mkcontrol(deviceId,channel,percent/100)
|
||||
time.sleep(1)
|
||||
|
||||
mkstop(deviceId)
|
||||
|
||||
def mkbtstop():
|
||||
"""
|
||||
stop bluetooth advertising
|
||||
"""
|
||||
advertiser.AdvertisementStop()
|
||||
# hcitool_args1 = hcitool_path + ' -i hci0 cmd 0x08 0x000a 00' + ' &> /dev/null'
|
||||
|
||||
# if platform.system() == 'Linux':
|
||||
# subprocess.run(hcitool_args1, shell=True, executable="/bin/bash")
|
||||
# elif platform.system() == 'Windows':
|
||||
# print('Connect command :')
|
||||
# print(hcitool_args1)
|
||||
# else:
|
||||
# print('Unsupported OS')
|
||||
|
||||
return
|
||||
|
||||
def mkconnect(deviceId: int=0):
|
||||
"""
|
||||
send the bluetooth connect telegram to switch the MouldKing hubs in bluetooth mode
|
||||
press the button on the hub(s) and the flashing of status led should switch from blue-green to blue
|
||||
"""
|
||||
hub = _getHubId(deviceId)
|
||||
rawdata = hub.Connect()
|
||||
return
|
||||
|
||||
def mkstop(deviceId: int=0):
|
||||
hub = _getHubId(deviceId)
|
||||
rawdata = hub.Stop()
|
||||
return
|
||||
|
||||
def mkcontrol(deviceId: int=0, channel: int=0, powerAndDirection: float=0):
|
||||
hub = _getHubId(deviceId)
|
||||
rawdata = hub.SetChannel(channel, powerAndDirection)
|
||||
return
|
||||
|
||||
def test_hub(hubId: int=0):
|
||||
tracer.TraceInfo("HUB "+ str(hubId) +" connecting")
|
||||
mkconnect()
|
||||
time.sleep(1)
|
||||
|
||||
for index in range(6):
|
||||
_automate(hubId, index) # start channel 0 = A
|
||||
tracer.TraceInfo("Channel change requested")
|
||||
time.sleep(1)
|
||||
|
||||
def help():
|
||||
tracer.TraceInfo("Available commands:")
|
||||
tracer.TraceInfo(" help() : print available commands")
|
||||
tracer.TraceInfo(" hints() : print hints and examples")
|
||||
tracer.TraceInfo(" mkconnect() : Initiate hub control by sending bluetooth connect telegram")
|
||||
tracer.TraceInfo(" mkstop(hubId) : Stop ALL motors")
|
||||
tracer.TraceInfo(" mkcontrol(hubId, channel, powerAndDirection) : Control a specific hub, channel, power and motor direction")
|
||||
tracer.TraceInfo(" test_hub(hubId) : run automated tests on each channels")
|
||||
tracer.TraceInfo(" mkbtstop() : stop bluetooth advertising")
|
||||
|
||||
def hints():
|
||||
tracer.TraceInfo("HINTS:")
|
||||
tracer.TraceInfo("If run on windows, commands are shown but not executed (hcitool dependency)")
|
||||
tracer.TraceInfo()
|
||||
tracer.TraceInfo("For connecting:")
|
||||
tracer.TraceInfo(" Switch MK6.0 Hubs on - led is flashing green/blue")
|
||||
tracer.TraceInfo(" mkconnect() to send the bluetooth connect telegram. All hubs switch to bluetooth mode")
|
||||
tracer.TraceInfo(" by short-pressing the button on MK6.0 Hubs you can choose the hubId")
|
||||
tracer.TraceInfo(" hubId=0 - one Led flash")
|
||||
tracer.TraceInfo(" hubId=1 - two Led flashs")
|
||||
tracer.TraceInfo(" hubId=2 - three Led flashs")
|
||||
tracer.TraceInfo()
|
||||
tracer.TraceInfo("ex: test_hub(0), mkcontrol(0, 0, 0.5); mkcontrol(0, 1, -1, True)")
|
||||
tracer.TraceInfo(" the minus sign - indicate reverse motor direction")
|
||||
|
||||
##################################################################
|
||||
# Entry point when script is started by python -i consoletest.py
|
||||
help()
|
||||
tracer.TraceInfo()
|
||||
hints()
|
||||
|
||||
tracer.TraceInfo()
|
||||
tracer.TraceInfo("Ready to execute commands\n")
|
||||
157
src/mkconnect/examples/main.py
Normal file
157
src/mkconnect/examples/main.py
Normal file
@ -0,0 +1,157 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
print('Script: main.py')
|
||||
print('Platform: ' + sys.platform)
|
||||
|
||||
from mkconnect.tracer.Tracer import Tracer
|
||||
from mkconnect.tracer.TracerConsole import TracerConsole
|
||||
|
||||
# uncomment to choose advertiser
|
||||
if (sys.platform == 'linux'):
|
||||
#from Advertiser.AdvertiserHCITool import AdvertiserHCITool as Advertiser
|
||||
#from Advertiser.AdvertiserBTMgmt import AdvertiserBTMgmt as Advertiser
|
||||
from mkconnect.advertiser.AdvertiserBTSocket import AdvertiserBTSocket as Advertiser
|
||||
pass
|
||||
elif (sys.platform == 'rp2'):
|
||||
from mkconnect.advertiser.AdvertiserMicroPython import AdvertiserMicroPython as Advertiser
|
||||
pass
|
||||
elif (sys.platform == 'win32'):
|
||||
from mkconnect.advertiser.AdvertiserDummy import AdvertiserDummy as Advertiser
|
||||
else:
|
||||
raise Exception('unsupported platform')
|
||||
|
||||
from mkconnect.mouldking.MouldKing import MouldKing
|
||||
|
||||
# instantiate Tracer
|
||||
tracer = TracerConsole()
|
||||
|
||||
# instantiate Advertiser
|
||||
advertiser = Advertiser()
|
||||
advertiser.SetTracer(tracer)
|
||||
|
||||
# Set Tracer for all MouldKing Hubs
|
||||
MouldKing.SetTracer(tracer)
|
||||
MouldKing.SetAdvertiser(advertiser)
|
||||
|
||||
# save pre-instantiated objects in local variables
|
||||
hub0 = MouldKing.Module6_0.Device0
|
||||
hub1 = MouldKing.Module6_0.Device1
|
||||
#hub2 = MouldKing.Module6_0.Device2
|
||||
|
||||
#hub0 = MouldKing.Module4_0.Device0
|
||||
#hub1 = MouldKing.Module4_0.Device1
|
||||
hub2 = MouldKing.Module4_0.Device2
|
||||
|
||||
############################################################################
|
||||
# get uncrypted connect-telegram as bytearray
|
||||
title = "connect-telegram"
|
||||
tracer.TraceInfo("\n" + title)
|
||||
|
||||
hub0.Connect()
|
||||
hub1.Connect()
|
||||
hub2.Connect()
|
||||
time.sleep(5)
|
||||
|
||||
#tracer.TraceInfo("rawdata: " + ' '.join(f'{x:02x}' for x in rawdata))
|
||||
|
||||
#crypted = MouldKingCrypt.Crypt(rawdata) # get crypted data from rawdata
|
||||
#tracer.TraceInfo("crypted: " + ' '.join(f'{x:02x}' for x in crypted))
|
||||
|
||||
############################################################################
|
||||
# get uncrypted stop-telegram as bytearray
|
||||
title = "stop-telegram"
|
||||
tracer.TraceInfo("\n" + title)
|
||||
|
||||
rawdata = hub0.Stop()
|
||||
rawdata = hub1.Stop()
|
||||
time.sleep(1)
|
||||
|
||||
#tracer.TraceInfo("rawdata: " + ' '.join(f'{x:02x}' for x in rawdata))
|
||||
|
||||
#crypted = MouldKingCrypt.Crypt(rawdata) # get crypted data from rawdata
|
||||
#tracer.TraceInfo("crypted: " + ' '.join(f'{x:02x}' for x in crypted))
|
||||
|
||||
############################################################################
|
||||
# get uncrypted telegram with channel 1 (indexer 0) fullspeed forwards
|
||||
title = "C1: fullspeed forwards"
|
||||
tracer.TraceInfo("\n" + title)
|
||||
|
||||
rawdata = hub0.SetChannel(0, 1)
|
||||
time.sleep(1)
|
||||
|
||||
#tracer.TraceInfo("rawdata: " + ' '.join(f'{x:02x}' for x in rawdata))
|
||||
|
||||
#crypted = MouldKingCrypt.Crypt(rawdata) # get crypted data from rawdata
|
||||
#tracer.TraceInfo("crypted: " + ' '.join(f'{x:02x}' for x in crypted))
|
||||
|
||||
############################################################################
|
||||
# get uncrypted telegram with channel 1 (indexer 0) halfspeed forwards
|
||||
title = "C1: halfspeed forwards"
|
||||
tracer.TraceInfo("\n" + title)
|
||||
|
||||
rawdata = hub0.SetChannel(0, 0.5)
|
||||
time.sleep(1)
|
||||
|
||||
#tracer.TraceInfo("rawdata: " + ' '.join(f'{x:02x}' for x in rawdata))
|
||||
|
||||
#crypted = MouldKingCrypt.Crypt(rawdata) # get crypted data from rawdata
|
||||
#tracer.TraceInfo("crypted: " + ' '.join(f'{x:02x}' for x in crypted))
|
||||
|
||||
############################################################################
|
||||
# get uncrypted telegram with channel 1 (indexer 0) fullspeed forwards
|
||||
title = "C1: fullspeed forwards"
|
||||
tracer.TraceInfo("\n" + title)
|
||||
|
||||
rawdata = hub1.SetChannel(0, 1)
|
||||
time.sleep(2)
|
||||
|
||||
#tracer.TraceInfo("rawdata: " + ' '.join(f'{x:02x}' for x in rawdata))
|
||||
|
||||
#crypted = MouldKingCrypt.Crypt(rawdata) # get crypted data from rawdata
|
||||
#tracer.TraceInfo("crypted: " + ' '.join(f'{x:02x}' for x in crypted))
|
||||
|
||||
############################################################################
|
||||
# get uncrypted telegram with channel 1 (indexer 0) halfspeed backwards
|
||||
title = "C1: halfspeed backwards"
|
||||
tracer.TraceInfo("\n" + title)
|
||||
|
||||
rawdata = hub0.SetChannel(0, -0.5)
|
||||
time.sleep(1)
|
||||
|
||||
#tracer.TraceInfo("rawdata: " + ' '.join(f'{x:02x}' for x in rawdata))
|
||||
|
||||
#crypted = MouldKingCrypt.Crypt(rawdata) # get crypted data from rawdata
|
||||
#tracer.TraceInfo("crypted: " + ' '.join(f'{x:02x}' for x in crypted))
|
||||
|
||||
############################################################################
|
||||
# get uncrypted telegram with channel 1 (indexer 0) halfspeed backwards
|
||||
title = "C2: halfspeed backwards"
|
||||
tracer.TraceInfo("\n" + title)
|
||||
|
||||
rawdata = hub0.SetChannel(1, -0.5)
|
||||
time.sleep(1)
|
||||
|
||||
#tracer.TraceInfo("rawdata: " + ' '.join(f'{x:02x}' for x in rawdata))
|
||||
|
||||
#crypted = MouldKingCrypt.Crypt(rawdata) # get crypted data from rawdata
|
||||
#tracer.TraceInfo("crypted: " + ' '.join(f'{x:02x}' for x in crypted))
|
||||
|
||||
############################################################################
|
||||
# disconnect from advertiser
|
||||
title = "Disconnect from advertiser"
|
||||
tracer.TraceInfo("\n" + title)
|
||||
|
||||
hub0.Disconnect()
|
||||
hub1.Disconnect()
|
||||
hub2.Disconnect()
|
||||
time.sleep(1)
|
||||
|
||||
############################################################################
|
||||
# stop Advertisement
|
||||
title = "Advertisement stop"
|
||||
tracer.TraceInfo("\n" + title)
|
||||
advertiser.AdvertisementStop()
|
||||
time.sleep(1)
|
||||
119
src/mkconnect/mouldking/MouldKing.py
Normal file
119
src/mkconnect/mouldking/MouldKing.py
Normal file
@ -0,0 +1,119 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from ..tracer.Tracer import Tracer
|
||||
from ..advertiser.Advertiser import Advertiser
|
||||
|
||||
from .MouldKing_Hub_4 import MouldKing_Hub_4
|
||||
from .MouldKing_Hub_6 import MouldKing_Hub_6
|
||||
|
||||
class MouldKing :
|
||||
"""
|
||||
abstract class to store the static objects
|
||||
"""
|
||||
|
||||
class Module4_0 :
|
||||
"""
|
||||
abstract class to store the static objects
|
||||
"""
|
||||
|
||||
Device0 = MouldKing_Hub_4(0)
|
||||
"""
|
||||
MouldKing Hub 4.0 with address 1
|
||||
"""
|
||||
|
||||
Device1 = MouldKing_Hub_4(1)
|
||||
"""
|
||||
MouldKing Hub 4.0 with address 2
|
||||
"""
|
||||
|
||||
Device2 = MouldKing_Hub_4(2)
|
||||
"""
|
||||
MouldKing Hub 4.0 with address 3
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def SetAdvertiser(advertiser: Advertiser) -> Advertiser:
|
||||
"""
|
||||
Set Advertiser for all MouldKing 4.0 Hubs
|
||||
"""
|
||||
|
||||
# MouldKing_4_Hubs is the same instance for all MouldKing_4_Hub-Instances
|
||||
MouldKing.Module4_0.Device0._MouldKing_4_Hubs.SetAdvertiser(advertiser)
|
||||
# MouldKing.Module4_0.Device1.MouldKing_4_Hubs.SetAdvertiser(advertiser)
|
||||
# MouldKing.Module4_0.Device2.MouldKing_4_Hubs.SetAdvertiser(advertiser)
|
||||
return advertiser
|
||||
|
||||
@staticmethod
|
||||
def SetTracer(tracer: Tracer) -> Tracer:
|
||||
"""
|
||||
Set Tracer for all MouldKing 4.0 Hubs
|
||||
"""
|
||||
|
||||
MouldKing.Module4_0.Device0.SetTracer(tracer)
|
||||
MouldKing.Module4_0.Device1.SetTracer(tracer)
|
||||
MouldKing.Module4_0.Device2.SetTracer(tracer)
|
||||
return tracer
|
||||
|
||||
class Module6_0 :
|
||||
"""
|
||||
abstract class to store the static objects
|
||||
"""
|
||||
|
||||
Device0 = MouldKing_Hub_6(0)
|
||||
"""
|
||||
MouldKing Hub 6.0 with address 1
|
||||
"""
|
||||
|
||||
Device1 = MouldKing_Hub_6(1)
|
||||
"""
|
||||
MouldKing Hub 6.0 with address 2
|
||||
"""
|
||||
|
||||
Device2 = MouldKing_Hub_6(2)
|
||||
"""
|
||||
MouldKing Hub 6.0 with address 3
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def SetAdvertiser(advertiser: Advertiser) -> Advertiser:
|
||||
"""
|
||||
Set Advertiser for all MouldKing 6.0 Hubs
|
||||
"""
|
||||
|
||||
MouldKing.Module6_0.Device0.SetAdvertiser(advertiser)
|
||||
MouldKing.Module6_0.Device1.SetAdvertiser(advertiser)
|
||||
MouldKing.Module6_0.Device2.SetAdvertiser(advertiser)
|
||||
return advertiser
|
||||
|
||||
@staticmethod
|
||||
def SetTracer(tracer: Tracer) -> Tracer:
|
||||
"""
|
||||
Set Tracer for all MouldKing 6.0 Hubs
|
||||
"""
|
||||
|
||||
MouldKing.Module6_0.Device0.SetTracer(tracer)
|
||||
MouldKing.Module6_0.Device1.SetTracer(tracer)
|
||||
MouldKing.Module6_0.Device2.SetTracer(tracer)
|
||||
return tracer
|
||||
|
||||
@staticmethod
|
||||
def SetAdvertiser(advertiser: Advertiser) -> Advertiser:
|
||||
"""
|
||||
Set Advertiser for all MouldKing 4.0 Hubs
|
||||
"""
|
||||
|
||||
# MouldKing_4_Hubs is the same instance for all MouldKing_4_Hub-Instances
|
||||
MouldKing.Module4_0.SetAdvertiser(advertiser)
|
||||
MouldKing.Module6_0.SetAdvertiser(advertiser)
|
||||
return advertiser
|
||||
|
||||
@staticmethod
|
||||
def SetTracer(tracer: Tracer) -> Tracer:
|
||||
"""
|
||||
Set Tracer for all MouldKing 4.0 Hubs
|
||||
"""
|
||||
|
||||
MouldKing.Module4_0.SetTracer(tracer)
|
||||
MouldKing.Module6_0.SetTracer(tracer)
|
||||
return tracer
|
||||
159
src/mkconnect/mouldking/MouldKingCrypt.py
Normal file
159
src/mkconnect/mouldking/MouldKingCrypt.py
Normal file
@ -0,0 +1,159 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
class MouldKingCrypt :
|
||||
"""
|
||||
class with static methods to do MouldKing encryption
|
||||
"""
|
||||
|
||||
# static class variables
|
||||
__Array_C1C2C3C4C5 = bytes([0xC1, 0xC2, 0xC3, 0xC4, 0xC5])
|
||||
|
||||
@staticmethod
|
||||
def CreateTelegramForHCITool(manufacturerId: bytes, rawDataArray: bytes) -> bytes:
|
||||
"""
|
||||
Create input data for hcitool
|
||||
"""
|
||||
cryptedArray = MouldKingCrypt.Crypt(rawDataArray)
|
||||
cryptedArrayLen = len(cryptedArray)
|
||||
|
||||
resultArray = bytearray(8 + cryptedArrayLen)
|
||||
resultArray[0] = cryptedArrayLen + 7 # len
|
||||
resultArray[1] = 0x02 # flags
|
||||
resultArray[2] = 0x01
|
||||
resultArray[3] = 0x02
|
||||
resultArray[4] = cryptedArrayLen + 3 # len
|
||||
resultArray[5] = 0xFF # type manufacturer specific
|
||||
resultArray[6] = manufacturerId[1] # companyId
|
||||
resultArray[7] = manufacturerId[0] # companyId
|
||||
for index in range(cryptedArrayLen):
|
||||
resultArray[index + 8] = cryptedArray[index]
|
||||
|
||||
return ' '.join(f'{x:02x}' for x in resultArray)
|
||||
|
||||
@staticmethod
|
||||
def Crypt(rawDataArray: bytes) -> bytes:
|
||||
"""
|
||||
do the MouldKing encryption for the given byte-array and return the resulting byte-array
|
||||
"""
|
||||
|
||||
targetArrayLength = len(MouldKingCrypt.__Array_C1C2C3C4C5) + len(rawDataArray) + 20
|
||||
|
||||
targetArray = bytearray(targetArrayLength)
|
||||
targetArray[15] = 113 # 0x71
|
||||
targetArray[16] = 15 # 0x0f
|
||||
targetArray[17] = 85 # 0x55
|
||||
|
||||
# copy firstDataArray reverse into targetArray with offset 18
|
||||
for index in range(len(MouldKingCrypt.__Array_C1C2C3C4C5)):
|
||||
targetArray[index + 18] = MouldKingCrypt.__Array_C1C2C3C4C5[(len(MouldKingCrypt.__Array_C1C2C3C4C5) - index) - 1]
|
||||
|
||||
# copy rawDataArray into targetArray with offset 18 + len(MouldKingCrypt.__Array_C1C2C3C4C5)
|
||||
for index in range(len(rawDataArray)):
|
||||
targetArray[18 + len(MouldKingCrypt.__Array_C1C2C3C4C5) + index] = rawDataArray[index]
|
||||
|
||||
# crypt bytes from position 15 to 22
|
||||
for index in range(15, len(MouldKingCrypt.__Array_C1C2C3C4C5) + 18):
|
||||
targetArray[index] = MouldKingCrypt.__revert_bits_byte(targetArray[index])
|
||||
|
||||
# calc checksum und copy to array
|
||||
checksum = MouldKingCrypt.__calc_checksum_from_arrays(MouldKingCrypt.__Array_C1C2C3C4C5, rawDataArray)
|
||||
targetArray[len(MouldKingCrypt.__Array_C1C2C3C4C5) + 18 + len(rawDataArray) + 0] = (checksum & 255)
|
||||
targetArray[len(MouldKingCrypt.__Array_C1C2C3C4C5) + 18 + len(rawDataArray) + 1] = ((checksum >> 8) & 255)
|
||||
|
||||
# crypt bytes from offset 18 to the end with magicNumberArray_63
|
||||
magicNumberArray_63 = MouldKingCrypt.__create_magic_array(63, 7)
|
||||
tempArray = bytearray(targetArrayLength - 18)
|
||||
for index in range(len(tempArray)):
|
||||
tempArray[index] = targetArray[index + 18]
|
||||
|
||||
MouldKingCrypt.__crypt_array(tempArray, magicNumberArray_63)
|
||||
targetArray[18:] = tempArray
|
||||
|
||||
# crypt complete array with magicNumberArray_37
|
||||
magicNumberArray_37 = MouldKingCrypt.__create_magic_array(37, 7)
|
||||
MouldKingCrypt.__crypt_array(targetArray, magicNumberArray_37)
|
||||
|
||||
# resulting advertisement array has a length of constant 24 bytes
|
||||
telegramArray = bytearray(24)
|
||||
|
||||
lengthResultArray = len(MouldKingCrypt.__Array_C1C2C3C4C5) + len(rawDataArray) + 5
|
||||
telegramArray[:lengthResultArray] = targetArray[15:15 + lengthResultArray]
|
||||
|
||||
# fill rest of array
|
||||
for index in range(lengthResultArray, len(telegramArray)):
|
||||
telegramArray[index] = index + 1
|
||||
|
||||
return telegramArray
|
||||
|
||||
@staticmethod
|
||||
def __create_magic_array(magic_number: int, size: int) -> bytes:
|
||||
magic_array = [0] * size
|
||||
magic_array[0] = 1
|
||||
|
||||
for index in range(1, 7):
|
||||
magic_array[index] = (magic_number >> (6 - index)) & 1
|
||||
|
||||
return magic_array
|
||||
|
||||
@staticmethod
|
||||
def __revert_bits_byte(value: int) -> int:
|
||||
result = 0
|
||||
for index_bit in range(8):
|
||||
if ((1 << index_bit) & value) != 0:
|
||||
result = result | (1 << (7 - index_bit))
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def __revert_bits_int(value: int) -> int:
|
||||
result = 0
|
||||
for index_bit in range(16):
|
||||
if ((1 << index_bit) & value) != 0:
|
||||
result |= 1 << (15 - index_bit)
|
||||
return 65535 & result
|
||||
|
||||
@staticmethod
|
||||
def __crypt_array(byte_array: bytes, magic_number_array: bytes) -> bytes:
|
||||
# foreach byte of array
|
||||
for index_byte in range(len(byte_array)):
|
||||
current_byte = byte_array[index_byte]
|
||||
current_result = 0
|
||||
# foreach bit in byte
|
||||
for index_bit in range(8):
|
||||
current_result += (((current_byte >> index_bit) & 1) ^ MouldKingCrypt.__shift_magic_array(magic_number_array)) << index_bit
|
||||
byte_array[index_byte] = current_result & 255
|
||||
return byte_array
|
||||
|
||||
@staticmethod
|
||||
def __calc_checksum_from_arrays(first_array: bytes, second_array: bytes) -> int:
|
||||
result = 65535
|
||||
for first_array_index in range(len(first_array)):
|
||||
result = (result ^ (first_array[(len(first_array) - 1) - first_array_index] << 8)) & 65535
|
||||
for index_bit in range(8):
|
||||
current_result = result & 32768
|
||||
result <<= 1
|
||||
if current_result != 0:
|
||||
result ^= 4129
|
||||
|
||||
for current_byte in second_array:
|
||||
result = ((MouldKingCrypt.__revert_bits_byte(current_byte) << 8) ^ result) & 65535
|
||||
for index_bit in range(8):
|
||||
current_result = result & 32768
|
||||
result <<= 1
|
||||
if current_result != 0:
|
||||
result ^= 4129
|
||||
|
||||
return MouldKingCrypt.__revert_bits_int(result) ^ 65535
|
||||
|
||||
@staticmethod
|
||||
def __shift_magic_array(i_arr: bytes) -> bytes:
|
||||
r1 = i_arr[3] ^ i_arr[6]
|
||||
i_arr[3] = i_arr[2]
|
||||
i_arr[2] = i_arr[1]
|
||||
i_arr[1] = i_arr[0]
|
||||
i_arr[0] = i_arr[6]
|
||||
i_arr[6] = i_arr[5]
|
||||
i_arr[5] = i_arr[4]
|
||||
i_arr[4] = r1
|
||||
return i_arr[0]
|
||||
|
||||
117
src/mkconnect/mouldking/MouldKingHub.py
Normal file
117
src/mkconnect/mouldking/MouldKingHub.py
Normal file
@ -0,0 +1,117 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from ..tracer.Tracer import Tracer
|
||||
from ..advertiser.AdvertisingDevice import AdvertisingDevice
|
||||
|
||||
from .MouldKingCrypt import MouldKingCrypt
|
||||
|
||||
class MouldKingHub(AdvertisingDevice) :
|
||||
"""
|
||||
baseclass
|
||||
"""
|
||||
|
||||
ManufacturerID = bytes([0xFF, 0xF0])
|
||||
|
||||
|
||||
def __init__(self, identifier: str, numberOfChannels: int, channelStartOffset: int, channelEndOffset: int, telegram_connect: bytes, basetelegram: bytes):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
|
||||
super().__init__(identifier)
|
||||
|
||||
if telegram_connect is not None:
|
||||
maxArrayOffset = len(telegram_connect) - 1
|
||||
|
||||
if channelStartOffset > maxArrayOffset:
|
||||
raise Exception("max channelStartOffset:" + maxArrayOffset)
|
||||
|
||||
if channelEndOffset > (maxArrayOffset - 1):
|
||||
raise Exception("max channelEndOffset:" + maxArrayOffset)
|
||||
|
||||
self._NumberOfChannels = numberOfChannels
|
||||
self._ChannelStartOffset = channelStartOffset
|
||||
self._ChannelEndOffset = channelEndOffset
|
||||
|
||||
self._Telegram_connect = telegram_connect
|
||||
self._Basetelegram = basetelegram
|
||||
|
||||
# create array
|
||||
self._ChannelValueList = [float(0)] * self._NumberOfChannels
|
||||
|
||||
return
|
||||
|
||||
|
||||
def Connect(self) -> None:
|
||||
"""
|
||||
returns the telegram to switch the MouldKing Hubs in bluetooth mode
|
||||
"""
|
||||
|
||||
# call baseClass to register at Advertiser
|
||||
super().Connect()
|
||||
|
||||
self._Advertise(self._Telegram_connect)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def Disconnect(self) -> None:
|
||||
"""
|
||||
disconnects the device from the advertiser
|
||||
"""
|
||||
|
||||
self.Stop()
|
||||
|
||||
super().Disconnect()
|
||||
|
||||
return
|
||||
|
||||
|
||||
def Stop(self) -> bytes:
|
||||
"""
|
||||
set internal stored value of all channels to zero and return the telegram
|
||||
"""
|
||||
|
||||
# init channels
|
||||
for channelId in range(0, self._NumberOfChannels):
|
||||
self._ChannelValueList[channelId] = float(0)
|
||||
|
||||
return self.CreateTelegram()
|
||||
|
||||
|
||||
def SetChannel(self, channelId: int, value: float) -> bytes:
|
||||
"""
|
||||
set internal stored value of channel with channelId to value and return the telegram
|
||||
"""
|
||||
|
||||
if channelId > self._NumberOfChannels - 1:
|
||||
raise Exception("only channelId 0.." + int(self._NumberOfChannels - 1) + "are allowed")
|
||||
|
||||
self._ChannelValueList[channelId] = value
|
||||
|
||||
return self.CreateTelegram()
|
||||
|
||||
|
||||
def CreateTelegram(self) -> bytes:
|
||||
"""
|
||||
returns a telegram including the internal stored value from all channels
|
||||
"""
|
||||
|
||||
raise NotImplementedError # override this methode
|
||||
|
||||
|
||||
def _Advertise(self, rawdata: bytes) -> bytes:
|
||||
"""
|
||||
sends the data to the advertiser
|
||||
"""
|
||||
|
||||
if(self._tracer is not None):
|
||||
self._tracer.TraceInfo("Advertise")
|
||||
|
||||
if(self._advertiser is not None):
|
||||
cryptedData = MouldKingCrypt.Crypt(rawdata)
|
||||
self._advertiser.AdvertisementDataSet(self._identifier, self.ManufacturerID, cryptedData)
|
||||
|
||||
return self._Telegram_connect
|
||||
|
||||
50
src/mkconnect/mouldking/MouldKingHub_Byte.py
Normal file
50
src/mkconnect/mouldking/MouldKingHub_Byte.py
Normal file
@ -0,0 +1,50 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from .MouldKingHub import MouldKingHub
|
||||
|
||||
class MouldKingHub_Byte(MouldKingHub) :
|
||||
"""
|
||||
baseclass handling with byte channels
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, identifier: str, numberOfChannels, channelStartOffset, channelEndOffset, telegram_connect, basetelegram):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
|
||||
# call baseclass init and set number of channels
|
||||
super().__init__(identifier, numberOfChannels, channelStartOffset, channelEndOffset, telegram_connect, basetelegram)
|
||||
|
||||
|
||||
def CreateTelegram(self) -> bytes:
|
||||
"""
|
||||
returns the telegram including the internal stored value from all channels
|
||||
"""
|
||||
|
||||
# make a copy of the basetelegram
|
||||
currentTelegramData = bytearray(self._Basetelegram)
|
||||
|
||||
# calc the length to be used for channels
|
||||
channelDataLength = len(currentTelegramData) - self._ChannelEndOffset
|
||||
|
||||
# iterate channels
|
||||
for channelId in range(0, self._NumberOfChannels):
|
||||
currentChannelStartOffset = self._ChannelStartOffset + channelId
|
||||
|
||||
if self._NumberOfChannels >= (channelId + 1) and channelDataLength >= currentChannelStartOffset:
|
||||
channelValue = self._ChannelValueList[channelId]
|
||||
|
||||
if channelValue < 0:
|
||||
# Range [-1..0] -> 0x80 - [0x7F .. 0x00] = [0x01 .. 0x80]
|
||||
currentTelegramData[currentChannelStartOffset] = int(0x80 - min(-channelValue * 0x80, 0x80))
|
||||
elif channelValue > 0:
|
||||
# Range [0..1] -> 0x80 + [0x00 .. 0x7F] = [0x80 .. 0xFF]
|
||||
currentTelegramData[currentChannelStartOffset] = int(0x80 + min(channelValue * 0x7F, 0x7F))
|
||||
else:
|
||||
currentTelegramData[currentChannelStartOffset] = 0x80
|
||||
|
||||
self._Advertise(currentTelegramData)
|
||||
|
||||
return currentTelegramData
|
||||
71
src/mkconnect/mouldking/MouldKingHub_Nibble.py
Normal file
71
src/mkconnect/mouldking/MouldKingHub_Nibble.py
Normal file
@ -0,0 +1,71 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from .MouldKingHub import MouldKingHub
|
||||
|
||||
class MouldKingHub_Nibble(MouldKingHub) :
|
||||
"""
|
||||
baseclass handling with nibble channels
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, identifier: str, numberOfChannels, channelStartOffset, channelEndOffset, telegram_connect, basetelegram):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
|
||||
# call baseclass init and set number of channels
|
||||
super().__init__(identifier, numberOfChannels, channelStartOffset, channelEndOffset, telegram_connect, basetelegram)
|
||||
|
||||
|
||||
def CreateTelegram(self) -> bytes:
|
||||
"""
|
||||
returns the telegram including the internal stored value from all channels
|
||||
"""
|
||||
|
||||
# make a copy of the basetelegram
|
||||
currentTelegramData = bytearray(self._Basetelegram)
|
||||
|
||||
# calc the length to be used for channels
|
||||
channelDataLength = len(currentTelegramData) - self._ChannelEndOffset
|
||||
|
||||
# iterate channels
|
||||
byteOffset = 0
|
||||
for channelId in range(0, self._NumberOfChannels, 2):
|
||||
currentChannelStartOffset = self._ChannelStartOffset + byteOffset
|
||||
|
||||
highNibble = 0
|
||||
lowNibble = 0
|
||||
|
||||
if self._NumberOfChannels >= (channelId + 1) and channelDataLength >= currentChannelStartOffset:
|
||||
evenChannelValue = self._ChannelValueList[channelId]
|
||||
oddChannelValue2 = self._ChannelValueList[channelId + 1]
|
||||
|
||||
# even Channel -> highNibble
|
||||
if evenChannelValue < 0:
|
||||
# Range [-1..0] -> [0x07 .. 0x00] = [0x07 .. 0x00]
|
||||
highNibble = int(min(-evenChannelValue * 0x07, 0x07))
|
||||
elif evenChannelValue > 0:
|
||||
# Range [0..1] -> 0x80 + [0x00 .. 0x07] = [0x80 .. 0x0F]
|
||||
highNibble = int(0x08 + min(evenChannelValue * 0x07, 0x07))
|
||||
else:
|
||||
highNibble = 0x08
|
||||
|
||||
# odd Channel -> lowNibble
|
||||
if oddChannelValue2 < 0:
|
||||
# Range [-1..0] -> [0x07 .. 0x00] = [0x07 .. 0x00]
|
||||
lowNibble = int(min(-oddChannelValue2 * 0x07, 0x07))
|
||||
elif oddChannelValue2 > 0:
|
||||
# Range [0..1] -> 0x80 + [0x00 .. 0x07] = [0x80 .. 0x0F]
|
||||
lowNibble = int(0x08 + min(oddChannelValue2 * 0x07, 0x07))
|
||||
else:
|
||||
lowNibble = 0x08
|
||||
|
||||
currentTelegramData[currentChannelStartOffset] = (int)((highNibble << 4) + lowNibble)
|
||||
|
||||
# next byte
|
||||
byteOffset = byteOffset + 1
|
||||
|
||||
self._Advertise(currentTelegramData)
|
||||
|
||||
return currentTelegramData
|
||||
77
src/mkconnect/mouldking/MouldKing_Hub_4.py
Normal file
77
src/mkconnect/mouldking/MouldKing_Hub_4.py
Normal file
@ -0,0 +1,77 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from ..tracer.Tracer import Tracer
|
||||
|
||||
from ..advertiser.IAdvertisingDevice import IAdvertisingDevice
|
||||
|
||||
#from .MouldKingDevice import MouldKingDevice
|
||||
from .MouldKing_Hubs_4_12Ch import MouldKing_Hubs_4_12Ch
|
||||
|
||||
class MouldKing_Hub_4(IAdvertisingDevice) :
|
||||
"""
|
||||
class handling a MouldKing 4.0 Hub
|
||||
"""
|
||||
|
||||
# static fields/constants
|
||||
_MouldKing_4_Hubs = MouldKing_Hubs_4_12Ch()
|
||||
|
||||
|
||||
def __init__(self, deviceId: int):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
|
||||
if deviceId > 2:
|
||||
raise Exception('only deviceId 0..2 are allowed')
|
||||
|
||||
self._deviceId = deviceId
|
||||
self._NumberOfChannels = 4
|
||||
self._tracer = None
|
||||
|
||||
|
||||
def SetTracer(self, tracer: Tracer) -> Tracer:
|
||||
"""
|
||||
set tracer object
|
||||
"""
|
||||
self._tracer = tracer
|
||||
|
||||
return tracer
|
||||
|
||||
|
||||
def Connect(self) -> None:
|
||||
"""
|
||||
returns the telegram to switch the MouldKing Hubs in bluetooth mode
|
||||
"""
|
||||
MouldKing_Hub_4._MouldKing_4_Hubs.SubDevice_Register(self)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def Disconnect(self) -> None:
|
||||
"""
|
||||
disconnects the device from the advertiser
|
||||
"""
|
||||
MouldKing_Hub_4._MouldKing_4_Hubs.SubDevice_Unregister(self)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def Stop(self) -> bytes:
|
||||
"""
|
||||
set internal stored value of all channels to zero and return the telegram
|
||||
"""
|
||||
|
||||
return MouldKing_Hub_4._MouldKing_4_Hubs.SubDevice_Stop(self._deviceId, self._NumberOfChannels)
|
||||
|
||||
|
||||
def SetChannel(self, channelId: int, value: float) -> bytes:
|
||||
"""
|
||||
set internal stored value of channel with channelId to value and return the telegram
|
||||
"""
|
||||
|
||||
if channelId > self._NumberOfChannels - 1:
|
||||
raise Exception("only channelId 0.." + int(self._NumberOfChannels - 1) + "are allowed")
|
||||
|
||||
return MouldKing_Hub_4._MouldKing_4_Hubs.SubDevice_SetChannel(self._deviceId, self._NumberOfChannels, channelId, value)
|
||||
|
||||
38
src/mkconnect/mouldking/MouldKing_Hub_6.py
Normal file
38
src/mkconnect/mouldking/MouldKing_Hub_6.py
Normal file
@ -0,0 +1,38 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from .MouldKingHub_Byte import MouldKingHub_Byte
|
||||
|
||||
class MouldKing_Hub_6(MouldKingHub_Byte) :
|
||||
"""
|
||||
class handling the MouldKing 6.0 Hub
|
||||
"""
|
||||
|
||||
# static fields/constants
|
||||
__telegram_connect = bytes([0x6D, 0x7B, 0xA7, 0x80, 0x80, 0x80, 0x80, 0x92]) # Define a byte array for Telegram Connect
|
||||
|
||||
__telegram_base_device_a = bytes([0x61, 0x7B, 0xA7, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x9E]) # byte array for base Telegram
|
||||
__telegram_base_device_b = bytes([0x62, 0x7B, 0xA7, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x9D]) # byte array for base Telegram
|
||||
__telegram_base_device_c = bytes([0x63, 0x7B, 0xA7, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x9C]) # byte array for base Telegram
|
||||
|
||||
|
||||
def __init__(self, deviceId: int):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
|
||||
if deviceId == 0:
|
||||
basetelegram = MouldKing_Hub_6.__telegram_base_device_a
|
||||
elif deviceId == 1:
|
||||
basetelegram = MouldKing_Hub_6.__telegram_base_device_b
|
||||
elif deviceId == 2:
|
||||
basetelegram = MouldKing_Hub_6.__telegram_base_device_c
|
||||
else:
|
||||
raise Exception('only deviceId 0..2 are allowed')
|
||||
|
||||
# call baseclass init and set number of channels
|
||||
super().__init__("MK6_" + str(deviceId), 6, 3, 1, MouldKing_Hub_6.__telegram_connect, basetelegram)
|
||||
|
||||
|
||||
|
||||
|
||||
99
src/mkconnect/mouldking/MouldKing_Hubs_4_12Ch.py
Normal file
99
src/mkconnect/mouldking/MouldKing_Hubs_4_12Ch.py
Normal file
@ -0,0 +1,99 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from ..tracer.Tracer import Tracer
|
||||
from ..advertiser.IAdvertisingDevice import IAdvertisingDevice
|
||||
|
||||
from .MouldKingHub_Nibble import MouldKingHub_Nibble
|
||||
|
||||
class MouldKing_Hubs_4_12Ch(MouldKingHub_Nibble) :
|
||||
"""
|
||||
class handling 3 x MouldKing 4.0 Hubs
|
||||
Only one telegram addresses all possible 3 x MK4 hubs the same time
|
||||
"""
|
||||
|
||||
# static fields/constants
|
||||
__telegram_connect = bytes([0xAD, 0x7B, 0xA7, 0x80, 0x80, 0x80, 0x4F, 0x52]) # Define a byte array for Telegram Connect
|
||||
|
||||
__telegram_base = bytes([0x7D, 0x7B, 0xA7, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x82]) # byte array for base Telegram
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
# call baseclass init and set number of channels
|
||||
super().__init__("MK4", 12, 3, 1, MouldKing_Hubs_4_12Ch.__telegram_connect, MouldKing_Hubs_4_12Ch.__telegram_base)
|
||||
|
||||
self._connectedSubDevices = list()
|
||||
|
||||
|
||||
def SubDevice_Register(self, subDevice: IAdvertisingDevice) -> None:
|
||||
"""
|
||||
returns the telegram to switch the MouldKing Hubs in bluetooth mode
|
||||
"""
|
||||
connectedSubDevicesLen = len(self._connectedSubDevices)
|
||||
|
||||
if(not subDevice is None and not subDevice in self._connectedSubDevices):
|
||||
self._connectedSubDevices.append(subDevice)
|
||||
|
||||
# first subDevice was added
|
||||
if(connectedSubDevicesLen == 0):
|
||||
self.Connect()
|
||||
|
||||
return
|
||||
|
||||
|
||||
def SubDevice_Unregister(self, subDevice: IAdvertisingDevice) -> None:
|
||||
"""
|
||||
disconnects the device from the advertiser
|
||||
"""
|
||||
if(not subDevice is None and subDevice in self._connectedSubDevices):
|
||||
self._connectedSubDevices.remove(subDevice)
|
||||
|
||||
# last subDevice was removed
|
||||
if(len(self._connectedSubDevices) == 0):
|
||||
self.Disconnect()
|
||||
|
||||
return
|
||||
|
||||
|
||||
def SubDevice_Stop(self, hubDeviceId: int, hubNumberOfChannels: int) -> bytes:
|
||||
"""
|
||||
set internal stored value of all channels to zero and return the telegram
|
||||
"""
|
||||
# deviceId = 0
|
||||
# -> channelId 0..4
|
||||
# deviceId = 2
|
||||
# -> channelId 5..8
|
||||
# deviceId = 3
|
||||
# -> channelId 9..12
|
||||
channelIdHubs = hubDeviceId * hubNumberOfChannels
|
||||
|
||||
# init channels
|
||||
for channelId in range(channelIdHubs, hubNumberOfChannels):
|
||||
if channelId < self._NumberOfChannels:
|
||||
self._ChannelValueList[channelId] = float(0)
|
||||
|
||||
return self.CreateTelegram()
|
||||
|
||||
|
||||
def SubDevice_SetChannel(self, hubDeviceId: int, hubNumberOfChannels: int, hubChannelId: int, value: float) -> bytes:
|
||||
"""
|
||||
set internal stored value of channel with channelId to value and return the telegram
|
||||
"""
|
||||
|
||||
# deviceId = 0
|
||||
# -> channelId 0..4
|
||||
# deviceId = 2
|
||||
# -> channelId 5..8
|
||||
# deviceId = 3
|
||||
# -> channelId 9..12
|
||||
channelIdHubs = hubDeviceId * hubNumberOfChannels + hubChannelId
|
||||
|
||||
if channelIdHubs > self._NumberOfChannels - 1:
|
||||
raise Exception("only channelId 0.." + int(self._NumberOfChannels - 1) + "are allowed")
|
||||
|
||||
self._ChannelValueList[channelIdHubs] = value
|
||||
|
||||
return self.CreateTelegram()
|
||||
0
src/mkconnect/mouldking/__init__.py
Normal file
0
src/mkconnect/mouldking/__init__.py
Normal file
BIN
src/mkconnect/mouldking/__pycache__/MouldKing.cpython-313.pyc
Normal file
BIN
src/mkconnect/mouldking/__pycache__/MouldKing.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/mkconnect/mouldking/__pycache__/MouldKingHub.cpython-313.pyc
Normal file
BIN
src/mkconnect/mouldking/__pycache__/MouldKingHub.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/mkconnect/mouldking/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/mkconnect/mouldking/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
18
src/mkconnect/tracer/Tracer.py
Normal file
18
src/mkconnect/tracer/Tracer.py
Normal file
@ -0,0 +1,18 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
class Tracer :
|
||||
"""
|
||||
baseclass
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
pass
|
||||
|
||||
def TraceInfo(self, value: str):
|
||||
"""
|
||||
prints out
|
||||
"""
|
||||
pass
|
||||
24
src/mkconnect/tracer/TracerConsole.py
Normal file
24
src/mkconnect/tracer/TracerConsole.py
Normal file
@ -0,0 +1,24 @@
|
||||
__author__ = "J0EK3R"
|
||||
__version__ = "0.1"
|
||||
|
||||
from .Tracer import Tracer
|
||||
|
||||
class TracerConsole(Tracer) :
|
||||
"""
|
||||
baseclass
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
initializes the object and defines the fields
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
|
||||
def TraceInfo(self, value: str=""):
|
||||
"""
|
||||
prints out
|
||||
"""
|
||||
print(value)
|
||||
|
||||
0
src/mkconnect/tracer/__init__.py
Normal file
0
src/mkconnect/tracer/__init__.py
Normal file
BIN
src/mkconnect/tracer/__pycache__/Tracer.cpython-313.pyc
Normal file
BIN
src/mkconnect/tracer/__pycache__/Tracer.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/mkconnect/tracer/__pycache__/TracerConsole.cpython-313.pyc
Normal file
BIN
src/mkconnect/tracer/__pycache__/TracerConsole.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/mkconnect/tracer/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/mkconnect/tracer/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user