├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── demos ├── 01-simple_rx_scan.py ├── 02-rx_scan_with_factory.py ├── 03-pcap-demo.py ├── 04-serial-driver.py ├── demo-capture-1.pcap └── demo-capture-2.pcap ├── libAnt ├── __init__.py ├── constants.py ├── core.py ├── drivers │ ├── __init__.py │ ├── driver.py │ ├── pcap.py │ ├── serial.py │ └── usb.py ├── loggers │ ├── __init__.py │ ├── logger.py │ └── pcap.py ├── message.py ├── node.py └── profiles │ ├── __init__.py │ ├── factory.py │ ├── heartrate_profile.py │ ├── power_profile.py │ ├── profile.py │ └── speed_cadence_profile.py ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | .idea/ 91 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | python: true 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: 3 | directories: 4 | - "$HOME/.cache/pip" 5 | - "$HOME/.pyenv" 6 | python: 7 | - "3.5" 8 | - "nightly" 9 | - "pypy3" 10 | 11 | install: 12 | - pip install -r requirements.txt 13 | - python setup.py -q install 14 | 15 | matrix: 16 | fast_finish: true 17 | sudo: false 18 | notifications: 19 | email: false 20 | script: echo hello 21 | 22 | deploy: 23 | provider: pypi 24 | distributions: sdist bdist_wheel 25 | user: halftome 26 | password: 27 | secure: "zcIHP2xcT7WsDjXQxyqF2M0suh6gJqBHDn5iISBRf9tWlq8yVzNLg3eKP3jRjj79nDl5v/tDLJGuW6c8jMsRe3kNuoVwGTLj4VAu/PTj8qXdWMoyUIvWGEzJh7T4qglLsHe+S7Am8raKVGWMDNuJFzeeJd8zybr0ktnNVNHmP2/HH7et3v4OnBCQZfVJlfkeQKf+tHdVNAGYXyGIxs+LNx2SuEhpIx9GU6tpZP/uafb7dmj1K/Xlc5v6hVnWt+60dO3lW37drxVVOaBzQRO6/U6T136bn68yu6IlKU/3x0AiWJKOrNw1ULOzrUVQxl8Mg9i/lChR+WVHexRSYsZoWHzhMA01pEa6seILFBVyKtg7MYe1tFrSnJyVntct9rzHoDizR/2jwlPXZAsqQ2U9UFw3rS3fo3S3CADRnj4hwdkc+af+3t2NrhlOqgpmIw4XL9CZCKl0Gvk8rwgP8DXp78ATpQsuJbuiqWiwNPWiaj7XupnVZYd8Vsjc59si0XNbQ1G00ggyPgWrOaKsieGlggApXClq+yoshY8LbDRfZ3ZJ+z9Wxu8lNquOdXEfms1dwz1UppyF6OS4YrTtoUlH1jGcaVAE2gB6Cmpfpz9vtnQSsPjtO/xotXLDE4f36zFO0J7FnnfxnwWtjBlsSyjUqME6u2RJb+yWvYAO3bN6E3U=" 28 | on: 29 | branch: master 30 | 31 | # branches: 32 | # only: 33 | # - master 34 | # - "/([0-9].[0-9].[0-9])/" 35 | # tags: true 36 | # repo: half2me/libant 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | python3 setup.py install 3 | 4 | test: 5 | py.test tests 6 | 7 | .PHONY: init test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LibAnt 2 | 3 | [![Build Status](https://travis-ci.org/half2me/libant.svg?branch=master)](https://travis-ci.org/half2me/libant) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/half2me/libant/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/half2me/libant/?branch=master) 5 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/half2me/libant/master/LICENSE) 6 | [![Join the chat at https://gitter.im/libant/Lobby](https://badges.gitter.im/libant/Lobby.svg)](https://gitter.im/libant/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | A Python implementation of the ANT+ Protocol 8 | 9 | The goal of this project is to provide a clean, Python-only implementation of the [ANT+ Protocol](https://www.thisisant.com). Usage of the library should require little to no knowledge of the ANT+ Protocol internals. It should be easy to use, easy to read, and have proper error handling. 10 | 11 | This project was born when I decided to completely rewrite the [python-ant library](https://github.com/mvillalba/python-ant) from scratch, after not finding a fork that suited my needs. There were so many different forks of the original project, each with their own patches, but not a properly useable one. Because of this, there may be parts of the code which look similar to the python-ant library, as I have their code as a reference. 12 | 13 | ## Installing 14 | For the stable version: `pip3 install antlib` 15 | For the latest clone the repo and do `./setup.py install` under UNIX systems or `python setup.py install` on windows (Make sure to use python3) 16 | 17 | ## Usage 18 | See usage examples in the `demos` folder. 19 | If you want to use the serial driver under linux, either run the script as root, or add your user to the `dialout` group with: 20 | ```bash 21 | sudo adduser yourusername dialout 22 | ``` 23 | 24 | ## Known bugs 25 | The usb driver has a bug which requires you to replug your ANT+ stick every time you run a demo script. So until we get that fixed, I suggest you stick to the serial driver, which is stable. -------------------------------------------------------------------------------- /demos/01-simple_rx_scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from time import sleep 3 | 4 | from libAnt.drivers.serial import SerialDriver 5 | from libAnt.drivers.usb import USBDriver 6 | from libAnt.node import Node 7 | 8 | 9 | def callback(msg): 10 | print(msg) 11 | 12 | 13 | def eCallback(e): 14 | print(e) 15 | 16 | # for USB driver 17 | # with Node(USBDriver(vid=0x0FCF, pid=0x1008), 'MyNode') as n: 18 | 19 | # for serial driver 20 | with Node(SerialDriver("/dev/ttyUSB0"), 'MyNode') as n: 21 | n.enableRxScanMode() 22 | n.start(callback, eCallback) 23 | sleep(3) # Listen for 30sec 24 | -------------------------------------------------------------------------------- /demos/02-rx_scan_with_factory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from time import sleep 3 | 4 | from libAnt.drivers.usb import USBDriver 5 | from libAnt.loggers.pcap import PcapLogger 6 | from libAnt.node import Node 7 | from libAnt.profiles.factory import Factory 8 | 9 | 10 | def callback(msg): 11 | print(msg) 12 | 13 | 14 | def eCallback(e): 15 | raise (e) 16 | 17 | 18 | with Node(USBDriver(vid=0x0FCF, pid=0x1008, logger=PcapLogger(logFile='log.pcap')), 'MyNode') as n: 19 | f = Factory(callback) 20 | 21 | n.enableRxScanMode() 22 | n.start(f.parseMessage, eCallback) 23 | sleep(10) # Listen for 1min 24 | -------------------------------------------------------------------------------- /demos/03-pcap-demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from time import sleep 3 | 4 | from libAnt.drivers.pcap import PcapDriver 5 | from libAnt.node import Node 6 | from libAnt.profiles.factory import Factory 7 | 8 | 9 | def callback(msg): 10 | print(msg) 11 | 12 | 13 | def eCallback(e): 14 | print(e) 15 | 16 | 17 | with Node(PcapDriver('demo-capture-1.pcap'), 'PcapNode1') as n: 18 | # n.enableRxScanMode() # Pcap driver is read-only 19 | f = Factory(callback) 20 | n.start(f.parseMessage, eCallback) 21 | sleep(30) # Listen for 30sec 22 | -------------------------------------------------------------------------------- /demos/04-serial-driver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from time import sleep 3 | 4 | from libAnt.drivers.serial import SerialDriver 5 | from libAnt.node import Node 6 | from libAnt.profiles.factory import Factory 7 | 8 | 9 | def callback(msg): 10 | print(msg) 11 | 12 | 13 | def eCallback(e): 14 | raise e 15 | 16 | 17 | with Node(SerialDriver('/dev/ttyUSB0'), 'SerialNode1') as n: 18 | n.enableRxScanMode() 19 | f = Factory(callback) 20 | n.start(f.parseMessage, eCallback) 21 | sleep(30) # Listen for 30sec 22 | -------------------------------------------------------------------------------- /demos/demo-capture-1.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/half2me/libant/c449eb31d06473b8cdfeea753d3cdf01fe8489e5/demos/demo-capture-1.pcap -------------------------------------------------------------------------------- /demos/demo-capture-2.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/half2me/libant/c449eb31d06473b8cdfeea753d3cdf01fe8489e5/demos/demo-capture-2.pcap -------------------------------------------------------------------------------- /libAnt/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['message', 'node', 'driver', 'constants', 'profiles'] 2 | -------------------------------------------------------------------------------- /libAnt/constants.py: -------------------------------------------------------------------------------- 1 | MESSAGE_TX_SYNC = 0xA4 2 | MESSAGE_TX_SYNC_LEGACY = 0xA5 3 | 4 | # Configuration messages 5 | MESSAGE_CHANNEL_UNASSIGN = 0x41 # [Channel number] 6 | MESSAGE_CHANNEL_ASSIGN = 0x42 # [Channel number, Channel type, Network number, Extended assign't (optional)] 7 | MESSAGE_CHANNEL_ID = 0x51 # [Channel number, Device number (2bytes), Device type ID, Trans. type] 8 | MESSAGE_CHANNEL_PERIOD = 0x43 # [Channel number, Channel period (2bytes)] 9 | MESSAGE_CHANNEL_SEARCH_TIMEOUT = 0x44 # [Channel number, Search timeout] 10 | MESSAGE_CHANNEL_FREQUENCY = 0x45 # [Channel number, RF frequency] 11 | MESSAGE_NETWORK_KEY = 0x46 # [Network number, Network key (8bytes)] 12 | MESSAGE_TX_POWER = 0x47 # [0, TX Power] 13 | MESSAGE_SEARCH_WAVEFORM = 0x49 # [Channel number, Waveform (2bytes)] 14 | MESSAGE_ADD_CHANNEL_ID_TO_LIST = 0x59 # [Channel number, Device number, Device type ID, Trans. type, List index] 15 | MESSAGE_ADD_ENCRYPION_ID_TO_LIST = 0x59 # [Channel number, Encryption ID (4bytes), List index] 16 | MESSAGE_CONFIG_ID_LIST = 0x5A # [Channel number, List size, Exclude] 17 | MESSAGE_CHANNEL_TX_POWER = 0x60 # [Channel number, Transmit power] 18 | MESSAGE_LOW_PRIORITY_SEARCH_TIMEOUT = 0x63 # [Channel number, Search timeout] 19 | MESSAGE_PROXIMITY_SEARCH = 0x71 20 | MESSAGE_ENABLE_EXT_RX_MESSAGES = 0x66 # [0, enable (0 or 1)] 21 | MESSAGE_LIB_CONFIG = 0x6E # [0, libconfig] 22 | 23 | 24 | # Notification messages 25 | MESSAGE_STARTUP = 0x6F 26 | 27 | # Control messages 28 | MESSAGE_SYSTEM_RESET = 0x4A 29 | MESSAGE_CHANNEL_OPEN = 0x4B 30 | MESSAGE_CHANNEL_CLOSE = 0x4C 31 | MESSAGE_CHANNEL_REQUEST = 0x4D 32 | 33 | # Data messages 34 | MESSAGE_CHANNEL_BROADCAST_DATA = 0x4E 35 | MESSAGE_CHANNEL_ACKNOWLEDGED_DATA = 0x4F 36 | MESSAGE_CHANNEL_BURST_DATA = 0x50 37 | 38 | # Channel event messages 39 | MESSAGE_CHANNEL_EVENT = 0x40 40 | 41 | # Requested response messages 42 | MESSAGE_CHANNEL_STATUS = 0x52 43 | MESSAGE_VERSION = 0x3E 44 | MESSAGE_CAPABILITIES = 0x54 45 | MESSAGE_SERIAL_NUMBER = 0x61 46 | 47 | # Message parameters 48 | CHANNEL_TYPE_TWOWAY_RECEIVE = 0x00 49 | CHANNEL_TYPE_TWOWAY_TRANSMIT = 0x10 50 | CHANNEL_TYPE_SHARED_RECEIVE = 0x20 51 | CHANNEL_TYPE_SHARED_TRANSMIT = 0x30 52 | CHANNEL_TYPE_ONEWAY_RECEIVE = 0x40 53 | CHANNEL_TYPE_ONEWAY_TRANSMIT = 0x50 54 | RADIO_TX_POWER_MINUS20DB = 0x00 55 | RADIO_TX_POWER_MINUS10DB = 0x01 56 | RADIO_TX_POWER_0DB = 0x02 57 | RADIO_TX_POWER_PLUS4DB = 0x03 58 | 59 | # Message Codes 60 | RESPONSE_NO_ERROR = 0x00 61 | EVENT_RX_SEARCH_TIMEOUT = 0x01 62 | EVENT_RX_FAIL = 0x02 63 | EVENT_TX = 0x03 64 | EVENT_TRANSFER_RX_FAILED = 0x04 65 | EVENT_TRANSFER_TX_COMPLETED = 0x05 66 | EVENT_TRANSFER_TX_FAILED = 0x06 67 | EVENT_CHANNEL_CLOSED = 0x07 68 | EVENT_RX_FAIL_GO_TO_SEARCH = 0x08 69 | EVENT_CHANNEL_COLLISION = 0x09 70 | EVENT_TRANSFER_TX_START = 0x0A 71 | EVENT_TRANSFER_NEXT_DATA_BLOCK = 0x11 72 | CHANNEL_IN_WRONG_STATE = 0x15 73 | CHANNEL_NOT_OPENED = 0x16 74 | CHANNEL_ID_NOT_SET = 0x18 75 | CLOSE_ALL_CHANNELS = 0x19 76 | TRANSFER_IN_PROGRESS = 0x1F 77 | TRANSFER_SEQUENCE_NUMBER_ERROR = 0x20 78 | TRANSFER_IN_ERROR = 0x21 79 | MESSAGE_SIZE_EXCEEDS_LIMIT = 0x27 80 | INVALID_MESSAGE = 0x28 81 | INVALID_NETWORK_NUMBER = 0x29 82 | INVALID_LIST_ID = 0x30 83 | INVALID_SCAN_TX_CHANNEL = 0x31 84 | INVALID_PARAMETER_PROVIDED = 0x33 85 | EVENT_SERIAL_QUE_OVERFLOW = 0x34 86 | EVENT_QUEUE_OVERFLOW = 0x35 87 | ENCRYPT_NEGOTIATION_SUCCESS = 0x38 88 | ENCRYPT_NEGOTIATION_FAIL = 0x39 89 | NVM_FULL_ERROR = 0x40 90 | NVM_WRITE_ERROR = 0x41 91 | USB_STRING_WRITE_FAIL = 0x70 92 | MESG_SERIAL_ERROR_ID = 0xAE 93 | 94 | CHANNEL_STATE_UNASSIGNED = 0x00 95 | CHANNEL_STATE_ASSIGNED = 0x01 96 | CHANNEL_STATE_SEARCHING = 0x02 97 | CHANNEL_STATE_TRACKING = 0x03 98 | CAPABILITIES_NO_RECEIVE_CHANNELS = 0x01 99 | CAPABILITIES_NO_TRANSMIT_CHANNELS = 0x02 100 | CAPABILITIES_NO_RECEIVE_MESSAGES = 0x04 101 | CAPABILITIES_NO_TRANSMIT_MESSAGES = 0x08 102 | CAPABILITIES_NO_ACKNOWLEDGED_MESSAGES = 0x10 103 | CAPABILITIES_NO_BURST_MESSAGES = 0x20 104 | CAPABILITIES_NETWORK_ENABLED = 0x02 105 | CAPABILITIES_SERIAL_NUMBER_ENABLED = 0x08 106 | CAPABILITIES_PER_CHANNEL_TX_POWER_ENABLED = 0x10 107 | CAPABILITIES_LOW_PRIORITY_SEARCH_ENABLED = 0x20 108 | CAPABILITIES_SCRIPT_ENABLED = 0x40 109 | CAPABILITIES_SEARCH_LIST_ENABLED = 0x80 110 | CAPABILITIES_LED_ENABLED = 0x01 111 | CAPABILITIES_EXT_MESSAGE_ENABLED = 0x02 112 | CAPABILITIES_SCAN_MODE_ENABLED = 0x04 113 | CAPABILITIES_PROX_SEARCH_ENABLED = 0x10 114 | CAPABILITIES_EXT_ASSIGN_ENABLED = 0x20 115 | CAPABILITIES_FS_ANTFS_ENABLED = 0x40 116 | TIMEOUT_NEVER = 0xFF 117 | 118 | OPEN_RX_SCAN_MODE = 0x5B 119 | 120 | 121 | ANTPLUS_NETWORK_KEY = b'\xB9\xA5\x21\xFB\xBD\x72\xC3\x45' 122 | ANTFS_KEY = b'\xA8\xA4\x23\xB9\xF5\x5E\x63\xC1' 123 | PUBLIC_NETWORK_KEY = b'\xE8\xE4\x21\x3B\x55\x7A\x67\xC1' 124 | 125 | # Extended message flags 126 | EXT_FLAG_CHANNEL_ID = 0x80 127 | EXT_FLAG_RSSI = 0x40 128 | EXT_FLAG_TIMESTAMP = 0x20 -------------------------------------------------------------------------------- /libAnt/core.py: -------------------------------------------------------------------------------- 1 | def lazyproperty(fn): 2 | attr_name = '__' + fn.__name__ 3 | 4 | @property 5 | def _lazyprop(self): 6 | if not hasattr(self, attr_name): 7 | setattr(self, attr_name, fn(self)) 8 | return getattr(self, attr_name) 9 | 10 | return _lazyprop 11 | -------------------------------------------------------------------------------- /libAnt/drivers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/half2me/libant/c449eb31d06473b8cdfeea753d3cdf01fe8489e5/libAnt/drivers/__init__.py -------------------------------------------------------------------------------- /libAnt/drivers/driver.py: -------------------------------------------------------------------------------- 1 | import time 2 | from abc import abstractmethod 3 | from queue import Empty 4 | from threading import Lock 5 | 6 | from libAnt.constants import MESSAGE_TX_SYNC 7 | from libAnt.loggers.logger import Logger 8 | from libAnt.message import Message 9 | 10 | 11 | class DriverException(Exception): 12 | pass 13 | 14 | 15 | class Driver: 16 | """ 17 | The driver provides an interface to read and write raw data to and from an ANT+ capable hardware device 18 | """ 19 | 20 | def __init__(self, logger: Logger = None): 21 | self._lock = Lock() 22 | self._logger = logger 23 | self._openTime = None 24 | 25 | def __enter__(self): 26 | self.open() 27 | return self 28 | 29 | def __exit__(self, exc_type, exc_val, exc_tb): 30 | self.close() 31 | 32 | def isOpen(self) -> bool: 33 | with self._lock: 34 | return self._isOpen() 35 | 36 | def open(self) -> None: 37 | with self._lock: 38 | if not self._isOpen(): 39 | self._openTime = time.time() 40 | if self._logger is not None: 41 | self._logger.open() 42 | self._open() 43 | 44 | def close(self) -> None: 45 | with self._lock: 46 | if self._isOpen: 47 | self._close() 48 | if self._logger is not None: 49 | self._logger.close() 50 | 51 | def reOpen(self) -> None: 52 | with self._lock: 53 | if self._isOpen(): 54 | self._close() 55 | self._open() 56 | 57 | def read(self, timeout=None) -> Message: 58 | if not self.isOpen(): 59 | raise DriverException("Device is closed") 60 | 61 | with self._lock: 62 | while True: 63 | try: 64 | sync = self._read(1, timeout=timeout)[0] 65 | if sync is not MESSAGE_TX_SYNC: 66 | continue 67 | length = self._read(1, timeout=timeout)[0] 68 | type = self._read(1, timeout=timeout)[0] 69 | data = self._read(length, timeout=timeout) 70 | chk = self._read(1, timeout=timeout)[0] 71 | msg = Message(type, data) 72 | 73 | if self._logger: 74 | logMsg = bytearray([sync, length, type]) 75 | logMsg.extend(data) 76 | logMsg.append(chk) 77 | 78 | self._logger.log(bytes(logMsg)) 79 | 80 | if msg.checksum() == chk: 81 | return msg 82 | except IndexError: 83 | raise Empty 84 | 85 | def write(self, msg: Message) -> None: 86 | if not self.isOpen(): 87 | raise DriverException("Device is closed") 88 | 89 | with self._lock: 90 | self._write(msg.encode()) 91 | 92 | def abort(self) -> None: 93 | self._abort() 94 | 95 | @abstractmethod 96 | def _isOpen(self) -> bool: 97 | pass 98 | 99 | @abstractmethod 100 | def _open(self) -> None: 101 | pass 102 | 103 | @abstractmethod 104 | def _close(self) -> None: 105 | pass 106 | 107 | @abstractmethod 108 | def _read(self, count: int, timeout=None) -> bytes: 109 | pass 110 | 111 | @abstractmethod 112 | def _write(self, data: bytes) -> None: 113 | pass 114 | 115 | @abstractmethod 116 | def _abort(self) -> None: 117 | pass -------------------------------------------------------------------------------- /libAnt/drivers/pcap.py: -------------------------------------------------------------------------------- 1 | import time 2 | from queue import Queue 3 | from struct import unpack, error 4 | from threading import Thread, Event 5 | 6 | from libAnt.drivers.driver import Driver 7 | from libAnt.loggers.logger import Logger 8 | 9 | 10 | class PcapDriver(Driver): 11 | def __init__(self, pcap : str, logger: Logger = None): 12 | super().__init__(logger=logger) 13 | self._isopen = False 14 | self._pcap = pcap 15 | self._buffer = Queue() 16 | 17 | self._loop = None 18 | 19 | class PcapLoop(Thread): 20 | def __init__(self, pcap, buffer: Queue): 21 | super().__init__() 22 | self._stopper = Event() 23 | self._pcap = pcap 24 | self._buffer = buffer 25 | 26 | def stop(self) -> None: 27 | self._stopper.set() 28 | 29 | def run(self) -> None: 30 | self._pcapfile = open(self._pcap, 'rb') 31 | # move file pointer to first packet header 32 | global_header_length = 24 33 | self._pcapfile.seek(global_header_length, 0) 34 | 35 | first_ts = 0 36 | start_time = time.time() 37 | while not self._stopper.is_set(): 38 | try: 39 | ts_sec, = unpack('i', self._pcapfile.read(4)) 40 | except error: 41 | break 42 | ts_usec = unpack('i', self._pcapfile.read(4))[0] / 1000000 43 | 44 | if first_ts is 0: 45 | first_ts = ts_sec + ts_usec 46 | 47 | ts = ts_sec + ts_usec 48 | send_time = ts - first_ts 49 | elapsed_time = time.time() - start_time 50 | if send_time > (elapsed_time): 51 | sleep_time = send_time - elapsed_time 52 | time.sleep(sleep_time) 53 | 54 | packet_length = unpack('i', self._pcapfile.read(4))[0] 55 | self._pcapfile.seek(4, 1) 56 | for i in range(packet_length): 57 | self._buffer.put(self._pcapfile.read(1)) 58 | 59 | self._pcapfile.close() 60 | 61 | def _isOpen(self) -> bool: 62 | return self._isopen 63 | 64 | def _open(self) -> None: 65 | self._isopen = True 66 | self._loop = self.PcapLoop(self._pcap, self._buffer) 67 | self._loop.start() 68 | 69 | def _close(self) -> None: 70 | self._isopen = False 71 | if self._loop is not None: 72 | if self._loop.is_alive(): 73 | self._loop.stop() 74 | self._loop.join() 75 | self._loop = None 76 | 77 | def _read(self, count: int, timeout=None) -> bytes: 78 | result = bytearray() 79 | 80 | while len(result) < count: 81 | result += self._buffer.get(block=True, timeout=timeout) 82 | 83 | return bytes(result) 84 | 85 | def _write(self, data: bytes) -> None: 86 | pass 87 | -------------------------------------------------------------------------------- /libAnt/drivers/serial.py: -------------------------------------------------------------------------------- 1 | from queue import Queue 2 | from threading import Thread, Event 3 | 4 | from serial import Serial 5 | from serial import SerialTimeoutException, SerialException 6 | 7 | from libAnt.drivers.driver import Driver, DriverException 8 | from libAnt.loggers.logger import Logger 9 | 10 | 11 | class SerialDriver(Driver): 12 | """ 13 | An implementation of a serial ANT+ device driver 14 | """ 15 | 16 | def __init__(self, device: str, baudRate: int = 115200, logger: Logger = None): 17 | super().__init__(logger=logger) 18 | self._device = device 19 | self._baudRate = baudRate 20 | self._serial = None 21 | 22 | def __str__(self): 23 | if self.isOpen(): 24 | return self._device + " @ " + str(self._baudRate) 25 | return None 26 | 27 | def _isOpen(self) -> bool: 28 | return self._serial is not None 29 | 30 | def _open(self) -> None: 31 | try: 32 | self._serial = Serial(port=self._device, baudrate=self._baudRate) 33 | except SerialException as e: 34 | raise DriverException(str(e)) 35 | 36 | if not self._serial.isOpen(): 37 | raise DriverException("Could not open specified device") 38 | 39 | def _close(self) -> None: 40 | self._serial.close() 41 | self._serial = None 42 | 43 | def _read(self, count: int, timeout=None) -> bytes: 44 | return self._serial.read(count) 45 | 46 | def _write(self, data: bytes) -> None: 47 | try: 48 | self._serial.write(data) 49 | self._serial.flush() 50 | except SerialTimeoutException as e: 51 | raise DriverException(str(e)) 52 | 53 | def _abort(self) -> None: 54 | if self._serial is not None: 55 | self._serial.cancel_read() 56 | self._serial.cancel_write() 57 | self._serial.reset_input_buffer() 58 | self._serial.reset_output_buffer() 59 | -------------------------------------------------------------------------------- /libAnt/drivers/usb.py: -------------------------------------------------------------------------------- 1 | from queue import Queue 2 | from threading import Event, Thread 3 | 4 | from usb import USBError, ENDPOINT_OUT, ENDPOINT_IN 5 | from usb.control import get_interface 6 | from usb.core import find 7 | from usb.util import find_descriptor, endpoint_direction, claim_interface, dispose_resources 8 | 9 | from libAnt.drivers.driver import Driver, DriverException 10 | from libAnt.loggers.logger import Logger 11 | 12 | 13 | class USBDriver(Driver): 14 | """ 15 | An implementation of a USB ANT+ device driver 16 | """ 17 | 18 | def __init__(self, vid, pid, logger: Logger = None): 19 | super().__init__(logger=logger) 20 | self._idVendor = vid 21 | self._idProduct = pid 22 | self._dev = None 23 | self._epOut = None 24 | self._epIn = None 25 | self._interfaceNumber = None 26 | self._packetSize = 0x20 27 | self._queue = None 28 | self._loop = None 29 | self._driver_open = False 30 | 31 | def __str__(self): 32 | if self.isOpen(): 33 | return str(self._dev) 34 | return "Closed" 35 | 36 | class USBLoop(Thread): 37 | def __init__(self, ep, packetSize: int, queue: Queue): 38 | super().__init__() 39 | self._stopper = Event() 40 | self._ep = ep 41 | self._packetSize = packetSize 42 | self._queue = queue 43 | 44 | def stop(self) -> None: 45 | self._stopper.set() 46 | 47 | def run(self) -> None: 48 | while not self._stopper.is_set(): 49 | try: 50 | data = self._ep.read(self._packetSize, timeout=1000) 51 | for d in data: 52 | self._queue.put(d) 53 | except USBError as e: 54 | if e.errno not in (60, 110) and e.backend_error_code != -116: # Timout errors 55 | self._stopper.set() 56 | # We Put in an invalid byte so threads will realize the device is stopped 57 | self._queue.put(None) 58 | 59 | def _isOpen(self) -> bool: 60 | return self._driver_open 61 | 62 | def _open(self) -> None: 63 | print('USB OPEN START') 64 | try: 65 | # find the first USB device that matches the filter 66 | self._dev = find(idVendor=self._idVendor, idProduct=self._idProduct) 67 | 68 | if self._dev is None: 69 | raise DriverException("Could not open specified device") 70 | 71 | # Detach kernel driver 72 | try: 73 | if self._dev.is_kernel_driver_active(0): 74 | try: 75 | self._dev.detach_kernel_driver(0) 76 | except USBError as e: 77 | raise DriverException("Could not detach kernel driver") 78 | except NotImplementedError: 79 | pass # for non unix systems 80 | 81 | # set the active configuration. With no arguments, the first 82 | # configuration will be the active one 83 | self._dev.set_configuration() 84 | 85 | # get an endpoint instance 86 | cfg = self._dev.get_active_configuration() 87 | self._interfaceNumber = cfg[(0, 0)].bInterfaceNumber 88 | interface = find_descriptor(cfg, bInterfaceNumber=self._interfaceNumber, 89 | bAlternateSetting=get_interface(self._dev, 90 | self._interfaceNumber)) 91 | claim_interface(self._dev, self._interfaceNumber) 92 | 93 | self._epOut = find_descriptor(interface, custom_match=lambda e: endpoint_direction( 94 | e.bEndpointAddress) == ENDPOINT_OUT) 95 | 96 | self._epIn = find_descriptor(interface, custom_match=lambda e: endpoint_direction( 97 | e.bEndpointAddress) == ENDPOINT_IN) 98 | 99 | if self._epOut is None or self._epIn is None: 100 | raise DriverException("Could not initialize USB endpoint") 101 | 102 | self._queue = Queue() 103 | self._loop = self.USBLoop(self._epIn, self._packetSize, self._queue) 104 | self._loop.start() 105 | self._driver_open = True 106 | print('USB OPEN SUCCESS') 107 | except IOError as e: 108 | self._close() 109 | raise DriverException(str(e)) 110 | 111 | def _close(self) -> None: 112 | print('USB CLOSE START') 113 | if self._loop is not None: 114 | if self._loop.is_alive(): 115 | self._loop.stop() 116 | self._loop.join() 117 | self._loop = None 118 | try: 119 | self._dev.reset() 120 | dispose_resources(self._dev) 121 | except: 122 | pass 123 | self._dev = self._epOut = self._epIn = None 124 | self._driver_open = False 125 | print('USB CLOSE END') 126 | 127 | def _read(self, count: int, timeout=None) -> bytes: 128 | data = bytearray() 129 | for i in range(0, count): 130 | b = self._queue.get(timeout=timeout) 131 | if b is None: 132 | self._close() 133 | raise DriverException("Device is closed!") 134 | data.append(b) 135 | return bytes(data) 136 | 137 | def _write(self, data: bytes) -> None: 138 | return self._epOut.write(data) 139 | 140 | def _abort(self) -> None: 141 | pass # not implemented for USB driver, use timeouts instead 142 | -------------------------------------------------------------------------------- /libAnt/loggers/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['pcap', 'logger'] -------------------------------------------------------------------------------- /libAnt/loggers/logger.py: -------------------------------------------------------------------------------- 1 | class Logger: 2 | def __init__(self, logFile: str): 3 | self._logFile = logFile 4 | self._log = None 5 | 6 | def __enter__(self): 7 | self.open() 8 | return self 9 | 10 | def __exit__(self, exc_type, exc_val, exc_tb): 11 | self.close() 12 | 13 | def open(self): 14 | def validate(logFile: str) -> str: 15 | if '.' in logFile: 16 | name, ext = logFile.split('.', 2) 17 | ext = '.' + ext 18 | else: 19 | name = logFile 20 | ext = '' 21 | num = 0 22 | exists = True 23 | while(exists): 24 | logFile = name + '-' + str(num) + ext 25 | try: 26 | with open(logFile): 27 | pass 28 | except IOError: 29 | return logFile 30 | num += 1 31 | 32 | if self._log is not None: 33 | self.close() 34 | self._logFile = validate(self._logFile) 35 | self._log = open(self._logFile, 'wb') 36 | self.onOpen() 37 | 38 | def close(self): 39 | if self._log is not None: 40 | self.beforeClose() 41 | self._log.close() 42 | self.afterClose() 43 | 44 | def log(self, data: bytes): 45 | self._log.write(self.encodeData(data)) 46 | 47 | def onOpen(self): 48 | pass 49 | 50 | def beforeClose(self): 51 | pass 52 | 53 | def afterClose(self): 54 | pass 55 | 56 | def encodeData(self, data): 57 | return data -------------------------------------------------------------------------------- /libAnt/loggers/pcap.py: -------------------------------------------------------------------------------- 1 | from struct import Struct 2 | import time 3 | import math 4 | 5 | from libAnt.loggers.logger import Logger 6 | 7 | class PcapLogger(Logger): 8 | def onOpen(self): 9 | # write pcap global header 10 | magic_number = b'\xD4\xC3\xB2\xA1' 11 | version_major = 2 12 | version_minor = 4 13 | thiszone = b'\x00\x00\x00\x00' 14 | sigfigs = b'\x00\x00\x00\x00' 15 | snaplen = b'\xFF\x00\x00\x00' 16 | network = b'\x01\x00\x00\x00' 17 | pcap_global_header = Struct('<4shh4s4s4s4s') 18 | self._log.write( 19 | pcap_global_header.pack(magic_number, version_major, version_minor, thiszone, sigfigs, 20 | snaplen, network)) 21 | 22 | def encodeData(self, data): 23 | timestamp = time.time() 24 | frac, whole = math.modf(timestamp) 25 | 26 | ts_sec = int(whole).to_bytes(4, byteorder='little') 27 | ts_usec = int(frac * 1000 * 1000).to_bytes(4, byteorder='little') 28 | incl_len = len(data) 29 | orig_len = incl_len 30 | 31 | pcap_packet_header = Struct('<4s4sll').pack(ts_sec, ts_usec, incl_len, orig_len) 32 | return pcap_packet_header + data -------------------------------------------------------------------------------- /libAnt/message.py: -------------------------------------------------------------------------------- 1 | from libAnt.constants import * 2 | 3 | 4 | class Message: 5 | def __init__(self, type: int, content: bytes): 6 | self._type = type 7 | self._content = content 8 | 9 | def __len__(self): 10 | return len(self._content) 11 | 12 | def __iter__(self): 13 | return self._content 14 | 15 | def __str__(self): 16 | return '({:02X}): '.format(self._type) + ' '.join('{:02X}'.format(x) for x in self._content) 17 | 18 | def checksum(self) -> int: 19 | chk = MESSAGE_TX_SYNC ^ len(self) ^ self._type 20 | for b in self._content: 21 | chk ^= b 22 | return chk 23 | 24 | def encode(self) -> bytes: 25 | b = bytearray([MESSAGE_TX_SYNC, len(self), self._type]) 26 | b.extend(self._content) 27 | b.append(self.checksum()) 28 | return bytes(b) 29 | 30 | @property 31 | def type(self) -> int: 32 | return self._type 33 | 34 | @property 35 | def content(self) -> bytes: 36 | return self._content 37 | 38 | 39 | class BroadcastMessage(Message): 40 | def __init__(self, type: int, content: bytes): 41 | self.flag = None 42 | self.deviceNumber = self.deviceType = self.transType = None 43 | self.rssiMeasurementType = self.rssi = self._rssiThreshold = None 44 | self.rssi = None 45 | self.rssiThreshold = None 46 | self.rxTimestamp = None 47 | self.channel = None 48 | self.extendedContent = None 49 | 50 | super().__init__(type, content) 51 | 52 | def build(self, raw: bytes): 53 | self._type = MESSAGE_CHANNEL_BROADCAST_DATA 54 | self.channel = raw[0] 55 | self._content = raw[1:9] 56 | if len(raw) > 9: # Extended message 57 | self.flag = raw[9] 58 | self.extendedContent = raw[10:] 59 | offset = 0 60 | if self.flag & EXT_FLAG_CHANNEL_ID: 61 | self.deviceNumber = int.from_bytes(self.extendedContent[:2], byteorder='little', signed=False) 62 | self.deviceType = self.extendedContent[2] 63 | self.transType = self.extendedContent[3] 64 | offset += 4 65 | if self.flag & EXT_FLAG_RSSI: 66 | rssi = self.extendedContent[offset:(offset + 3)] 67 | self.rssiMeasurementType = rssi[0] 68 | self.rssi = rssi[1] 69 | self.rssiThreshold = rssi[2] 70 | offset += 3 71 | if self.flag & EXT_FLAG_TIMESTAMP: 72 | self.rxTimestamp = int.from_bytes(self.extendedContent[offset:], 73 | byteorder='little', signed=False) 74 | return self 75 | 76 | def checksum(self) -> int: 77 | pass 78 | 79 | def encode(self) -> bytes: 80 | pass 81 | 82 | 83 | class SystemResetMessage(Message): 84 | def __init__(self): 85 | super().__init__(MESSAGE_SYSTEM_RESET, b'0') 86 | 87 | 88 | class SetNetworkKeyMessage(Message): 89 | def __init__(self, channel: int, key: bytes = ANTPLUS_NETWORK_KEY): 90 | content = bytearray([channel]) 91 | content.extend(key) 92 | super().__init__(MESSAGE_NETWORK_KEY, bytes(content)) 93 | 94 | 95 | class AssignChannelMessage(Message): 96 | def __init__(self, channel: int, type: int, network: int = 0, extended: int = None): 97 | content = bytearray([channel, type, network]) 98 | if extended is not None: 99 | content.append(extended) 100 | super().__init__(MESSAGE_CHANNEL_ASSIGN, bytes(content)) 101 | 102 | 103 | class SetChannelIdMessage(Message): 104 | def __init__(self, channel: int, deviceNumber: int = 0, deviceType: int = 0, transType: int = 0): 105 | content = bytearray([channel]) 106 | content.extend(deviceNumber.to_bytes(2, byteorder='big')) 107 | content.append(deviceType) 108 | content.append(transType) 109 | super().__init__(MESSAGE_CHANNEL_ID, bytes(content)) 110 | 111 | 112 | class SetChannelRfFrequencyMessage(Message): 113 | def __init__(self, channel: int, frequency: int = 2457): 114 | content = bytes([channel, frequency - 2400]) 115 | super().__init__(MESSAGE_CHANNEL_FREQUENCY, content) 116 | 117 | 118 | class OpenRxScanModeMessage(Message): 119 | def __init__(self): 120 | super().__init__(OPEN_RX_SCAN_MODE, bytes([0])) 121 | 122 | 123 | class EnableExtendedMessagesMessage(Message): 124 | def __init__(self, enable: bool = True): 125 | content = bytes([0, int(enable)]) 126 | super().__init__(MESSAGE_ENABLE_EXT_RX_MESSAGES, content) 127 | 128 | 129 | class LibConfigMessage(Message): 130 | def __init__(self, rxTimestamp: bool = True, rssi: bool = True, channelId: bool = True): 131 | config = 0 132 | if rxTimestamp: 133 | config |= EXT_FLAG_TIMESTAMP 134 | if rssi: 135 | config |= EXT_FLAG_RSSI 136 | if channelId: 137 | config |= EXT_FLAG_CHANNEL_ID 138 | super().__init__(MESSAGE_LIB_CONFIG, bytes([0, config])) 139 | -------------------------------------------------------------------------------- /libAnt/node.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from queue import Queue, Empty 3 | from time import sleep 4 | 5 | from libAnt.drivers.driver import Driver 6 | from libAnt.message import * 7 | 8 | 9 | class Network: 10 | def __init__(self, key: bytes = b'\x00' * 8, name: str = None): 11 | self.key = key 12 | self.name = name 13 | self.number = 0 14 | 15 | def __str__(self): 16 | return self.name 17 | 18 | 19 | class Pump(threading.Thread): 20 | def __init__(self, driver: Driver, initMessages, out: Queue, onSucces, onFailure): 21 | super().__init__() 22 | self._stopper = threading.Event() 23 | self._driver = driver 24 | self._out = out 25 | self._initMessages = initMessages 26 | self._waiters = [] 27 | self._onSuccess = onSucces 28 | self._onFailure = onFailure 29 | 30 | def stop(self): 31 | self._driver.abort() 32 | self._stopper.set() 33 | 34 | def stopped(self): 35 | return self._stopper.isSet() 36 | 37 | def run(self): 38 | while not self.stopped(): 39 | try: 40 | with self._driver as d: 41 | # Startup 42 | rst = SystemResetMessage() 43 | self._waiters.append(rst) 44 | d.write(rst) 45 | for m in self._initMessages: 46 | self._waiters.append(m) 47 | d.write(m) 48 | 49 | while not self.stopped(): 50 | # Write 51 | try: 52 | outMsg = self._out.get(block=False) 53 | self._waiters.append(outMsg) 54 | d.write(outMsg) 55 | except Empty: 56 | pass 57 | 58 | # Read 59 | try: 60 | msg = d.read(timeout=1) 61 | if msg.type == MESSAGE_CHANNEL_EVENT: 62 | # This is a response to our outgoing message 63 | for w in self._waiters: 64 | if w.type == msg.content[1]: # ACK 65 | self._waiters.remove(w) 66 | # TODO: Call waiter callback from tuple (waiter, callback) 67 | break 68 | elif msg.type == MESSAGE_CHANNEL_BROADCAST_DATA: 69 | bmsg = BroadcastMessage(msg.type, msg.content).build(msg.content) 70 | self._onSuccess(bmsg) 71 | except Empty: 72 | pass 73 | except Exception as e: 74 | self._onFailure(e) 75 | except: 76 | pass 77 | self._waiters.clear() 78 | sleep(1) 79 | 80 | 81 | class Node: 82 | def __init__(self, driver: Driver, name: str = None): 83 | self._driver = driver 84 | self._name = name 85 | self._out = Queue() 86 | self._init = [] 87 | self._pump = None 88 | self._configMessages = Queue() 89 | 90 | def __enter__(self): 91 | return self 92 | 93 | def __exit__(self, exc_type, exc_val, exc_tb): 94 | self.stop() 95 | 96 | def start(self, onSuccess, onFailure): 97 | if not self.isRunning(): 98 | self._pump = Pump(self._driver, self._init, self._out, onSuccess, onFailure) 99 | self._pump.start() 100 | 101 | def enableRxScanMode(self, networkKey=ANTPLUS_NETWORK_KEY, channelType=CHANNEL_TYPE_ONEWAY_RECEIVE, 102 | frequency: int = 2457, rxTimestamp: bool = True, rssi: bool = True, channelId: bool = True): 103 | self._init.append(SystemResetMessage()) 104 | self._init.append(SetNetworkKeyMessage(0, networkKey)) 105 | self._init.append(AssignChannelMessage(0, channelType)) 106 | self._init.append(SetChannelIdMessage(0)) 107 | self._init.append(SetChannelRfFrequencyMessage(0, frequency)) 108 | self._init.append(EnableExtendedMessagesMessage()) 109 | self._init.append(LibConfigMessage(rxTimestamp, rssi, channelId)) 110 | self._init.append(OpenRxScanModeMessage()) 111 | 112 | def stop(self): 113 | if self.isRunning(): 114 | self._pump.stop() 115 | self._pump.join() 116 | 117 | def isRunning(self): 118 | if self._pump is None: 119 | return False 120 | return self._pump.is_alive() 121 | 122 | def getCapabilities(self): 123 | pass 124 | -------------------------------------------------------------------------------- /libAnt/profiles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/half2me/libant/c449eb31d06473b8cdfeea753d3cdf01fe8489e5/libAnt/profiles/__init__.py -------------------------------------------------------------------------------- /libAnt/profiles/factory.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | 3 | from libAnt.message import BroadcastMessage 4 | from libAnt.profiles.power_profile import PowerProfileMessage 5 | from libAnt.profiles.speed_cadence_profile import SpeedAndCadenceProfileMessage 6 | from libAnt.profiles.heartrate_profile import HeartRateProfileMessage 7 | 8 | 9 | class Factory: 10 | types = { 11 | 120: HeartRateProfileMessage, 12 | 121: SpeedAndCadenceProfileMessage, 13 | 11: PowerProfileMessage 14 | } 15 | 16 | def __init__(self, callback=None): 17 | self._filter = None 18 | self._lock = Lock() 19 | self._messages = {} 20 | self._callback = callback 21 | 22 | def enableFilter(self): 23 | with self._lock: 24 | if self._filter is None: 25 | self._filter = {} 26 | 27 | def disableFilter(self): 28 | with self._lock: 29 | if self._filter is not None: 30 | self._filter = None 31 | 32 | def clearFilter(self): 33 | with self._lock: 34 | if self._filter is not None: 35 | self._filter.clear() 36 | 37 | def addToFilter(self, deviceNumber: int): 38 | with self._lock: 39 | if self._filter is not None: 40 | self._filter[deviceNumber] = True 41 | 42 | def removeFromFilter(self, deviceNumber: int): 43 | with self._lock: 44 | if self._filter is not None: 45 | if deviceNumber in self._filter: 46 | del self._filter[deviceNumber] 47 | 48 | def parseMessage(self, msg: BroadcastMessage): 49 | with self._lock: 50 | if self._filter is not None: 51 | if msg.deviceNumber not in self._filter: 52 | return 53 | if msg.deviceType in Factory.types: 54 | num = msg.deviceNumber 55 | type = msg.deviceType 56 | if type == 11: # Quick patch to filter out power messages with non-power info 57 | if msg.content[0] != 16: 58 | return 59 | pmsg = self.types[type](msg, self._messages[(num, type)] if (num, type) in self._messages else None) 60 | self._messages[(num, type)] = pmsg 61 | if callable(self._callback): 62 | self._callback(pmsg) 63 | 64 | def reset(self): 65 | with self._lock: 66 | self._messages = {} -------------------------------------------------------------------------------- /libAnt/profiles/heartrate_profile.py: -------------------------------------------------------------------------------- 1 | from libAnt.core import lazyproperty 2 | from libAnt.profiles.profile import ProfileMessage 3 | 4 | 5 | class HeartRateProfileMessage(ProfileMessage): 6 | """ Message from Heart Rate Monitor """ 7 | 8 | def __init__(self, msg, previous): 9 | super().__init__(msg, previous) 10 | 11 | def __str__(self): 12 | return f'{self.heartrate}' 13 | 14 | @lazyproperty 15 | def heartrate(self): 16 | """ 17 | Instantaneous heart rate. This value is 18 | intended to be displayed by the display 19 | device without further interpretation. 20 | If Invalid set to 0x00 21 | """ 22 | return self.msg.content[7] -------------------------------------------------------------------------------- /libAnt/profiles/power_profile.py: -------------------------------------------------------------------------------- 1 | from libAnt.core import lazyproperty 2 | from libAnt.profiles.profile import ProfileMessage 3 | 4 | 5 | class PowerProfileMessage(ProfileMessage): 6 | """ Message from Power Meter """ 7 | 8 | maxAccumulatedPower = 65536 9 | maxEventCount = 256 10 | 11 | def __str__(self): 12 | return super().__str__() + ' Power: {0:.0f}W'.format(self.averagePower) 13 | 14 | @lazyproperty 15 | def dataPageNumber(self): 16 | """ 17 | :return: Data Page Number (int) 18 | """ 19 | return self.msg.content[0] 20 | 21 | @lazyproperty 22 | def eventCount(self): 23 | """ 24 | The update event count field is incremented each time the information in the message is updated. 25 | There are no invalid values for update event count. 26 | The update event count in this message refers to updates of the standard Power-Only main data page (0x10) 27 | :return: Power Event Count 28 | """ 29 | return self.msg.content[1] 30 | 31 | @lazyproperty 32 | def instantaneousCadence(self): 33 | """ 34 | The instantaneous cadence field is used to transmit the pedaling cadence recorded from the power sensor. 35 | This field is an instantaneous value only; it does not accumulate between messages. 36 | :return: Instantaneous Cadence (W) 37 | """ 38 | return self.msg.content[3] 39 | 40 | @lazyproperty 41 | def accumulatedPower(self): 42 | """ 43 | Accumulated power is the running sum of the instantaneous power data and is incremented at each update 44 | of the update event count. The accumulated power field rolls over at 65.535kW. 45 | :return: 46 | """ 47 | return (self.msg.content[5] << 8) | self.msg.content[4] 48 | 49 | @lazyproperty 50 | def instantaneousPower(self): 51 | """ Instantaneous power (W) """ 52 | return (self.msg.content[7] << 8) | self.msg.content[6] 53 | 54 | @lazyproperty 55 | def accumulatedPowerDiff(self): 56 | if self.previous is None: 57 | return None 58 | elif self.accumulatedPower < self.previous.accumulatedPower: 59 | # Rollover 60 | return (self.accumulatedPower - self.previous.accumulatedPower) + self.maxAccumulatedPower 61 | else: 62 | return self.accumulatedPower - self.previous.accumulatedPower 63 | 64 | @lazyproperty 65 | def eventCountDiff(self): 66 | if self.previous is None: 67 | return None 68 | elif self.eventCount < self.previous.eventCount: 69 | # Rollover 70 | return (self.eventCount - self.previous.eventCount) + self.maxEventCount 71 | else: 72 | return self.eventCount - self.previous.eventCount 73 | 74 | @lazyproperty 75 | def averagePower(self): 76 | """ 77 | Under normal conditions with complete RF reception, average power equals instantaneous power. 78 | In conditions where packets are lost, average power accurately calculates power over the interval 79 | between the received messages 80 | :return: Average power (Watts) 81 | """ 82 | if self.previous is None: 83 | return self.instantaneousPower 84 | if self.eventCount == self.previous.eventCount: 85 | return self.instantaneousPower 86 | return self.accumulatedPowerDiff / self.eventCountDiff 87 | -------------------------------------------------------------------------------- /libAnt/profiles/profile.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | import time 4 | 5 | from libAnt.message import BroadcastMessage 6 | 7 | 8 | class ProfileMessage: 9 | def __init__(self, msg, previous): 10 | self.previous = previous 11 | self.msg = deepcopy(msg) 12 | self.count = previous.count + 1 if previous is not None else 1 13 | self.timestamp = time.time() 14 | self.firstTimestamp = previous.firstTimestamp if previous is not None else self.timestamp 15 | 16 | def __str__(self): 17 | return str(self.msg.deviceNumber) 18 | 19 | @staticmethod 20 | def decode(cls, msg: BroadcastMessage): 21 | if msg.deviceType in cls.match: 22 | cls.match[msg.deviceType]() -------------------------------------------------------------------------------- /libAnt/profiles/speed_cadence_profile.py: -------------------------------------------------------------------------------- 1 | from libAnt.core import lazyproperty 2 | from libAnt.profiles.profile import ProfileMessage 3 | 4 | 5 | class SpeedAndCadenceProfileMessage(ProfileMessage): 6 | """ Message from Speed & Cadence sensor """ 7 | 8 | def __init__(self, msg, previous): 9 | super().__init__(msg, previous) 10 | self.staleSpeedCounter = previous.staleSpeedCounter if previous is not None else 0 11 | self.staleCadenceCounter = previous.staleCadenceCounter if previous is not None else 0 12 | self.totalRevolutions = previous.totalRevolutions + self.cadenceRevCountDiff if previous is not None else 0 13 | self.totalSpeedRevolutions = previous.totalSpeedRevolutions + self.speedRevCountDiff if previous is not None else 0 14 | 15 | if self.previous is not None: 16 | if self.speedEventTime == self.previous.speedEventTime: 17 | self.staleSpeedCounter += 1 18 | else: 19 | self.staleSpeedCounter = 0 20 | 21 | if self.cadenceEventTime == self.previous.cadenceEventTime: 22 | self.staleCadenceCounter += 1 23 | else: 24 | self.staleCadenceCounter = 0 25 | 26 | maxCadenceEventTime = 65536 27 | maxSpeedEventTime = 65536 28 | maxSpeedRevCount = 65536 29 | maxCadenceRevCount = 65536 30 | maxstaleSpeedCounter = 7 31 | maxstaleCadenceCounter = 7 32 | 33 | def __str__(self): 34 | ret = '{} Speed: {:.2f}m/s (avg: {:.2f}m/s)\n'.format(super().__str__(), self.speed(2096), 35 | self.averageSpeed(2096)) 36 | ret += '{} Cadence: {:.2f}rpm (avg: {:.2f}rpm)\n'.format(super().__str__(), self.cadence, self.averageCadence) 37 | ret += '{} Total Distance: {:.2f}m\n'.format(super().__str__(), self.totalDistance(2096)) 38 | ret += '{} Total Revolutions: {:d}'.format(super().__str__(), self.totalRevolutions) 39 | return ret 40 | 41 | @lazyproperty 42 | def cadenceEventTime(self): 43 | """ Represents the time of the last valid bike cadence event (1/1024 sec) """ 44 | return (self.msg.content[1] << 8) | self.msg.content[0] 45 | 46 | @lazyproperty 47 | def cumulativeCadenceRevolutionCount(self): 48 | """ Represents the total number of pedal revolutions """ 49 | return (self.msg.content[3] << 8) | self.msg.content[2] 50 | 51 | @lazyproperty 52 | def speedEventTime(self): 53 | """ Represents the time of the last valid bike speed event (1/1024 sec) """ 54 | return (self.msg.content[5] << 8) | self.msg.content[4] 55 | 56 | @lazyproperty 57 | def cumulativeSpeedRevolutionCount(self): 58 | """ Represents the total number of wheel revolutions """ 59 | return (self.msg.content[7] << 8) | self.msg.content[6] 60 | 61 | @lazyproperty 62 | def speedEventTimeDiff(self): 63 | if self.previous is None: 64 | return 0 65 | elif self.speedEventTime < self.previous.speedEventTime: 66 | # Rollover 67 | return (self.speedEventTime - self.previous.speedEventTime) + self.maxSpeedEventTime 68 | else: 69 | return self.speedEventTime - self.previous.speedEventTime 70 | 71 | @lazyproperty 72 | def cadenceEventTimeDiff(self): 73 | if self.previous is None: 74 | return 0 75 | elif self.cadenceEventTime < self.previous.cadenceEventTime: 76 | # Rollover 77 | return (self.cadenceEventTime - self.previous.cadenceEventTime) + self.maxCadenceEventTime 78 | else: 79 | return self.cadenceEventTime - self.previous.cadenceEventTime 80 | 81 | @lazyproperty 82 | def speedRevCountDiff(self): 83 | if self.previous is None: 84 | return 0 85 | elif self.cumulativeSpeedRevolutionCount < self.previous.cumulativeSpeedRevolutionCount: 86 | # Rollover 87 | return ( 88 | self.cumulativeSpeedRevolutionCount - self.previous.cumulativeSpeedRevolutionCount) + self.maxSpeedRevCount 89 | else: 90 | return self.cumulativeSpeedRevolutionCount - self.previous.cumulativeSpeedRevolutionCount 91 | 92 | @lazyproperty 93 | def cadenceRevCountDiff(self): 94 | if self.previous is None: 95 | return 0 96 | elif self.cumulativeCadenceRevolutionCount < self.previous.cumulativeCadenceRevolutionCount: 97 | # Rollover 98 | return ( 99 | self.cumulativeCadenceRevolutionCount - self.previous.cumulativeCadenceRevolutionCount) + self.maxCadenceRevCount 100 | else: 101 | return self.cumulativeCadenceRevolutionCount - self.previous.cumulativeCadenceRevolutionCount 102 | 103 | def speed(self, c): 104 | """ 105 | :param c: circumference of the wheel (mm) 106 | :return: The current speed (m/sec) 107 | """ 108 | if self.previous is None: 109 | return 0 110 | if self.speedEventTime == self.previous.speedEventTime: 111 | if self.staleSpeedCounter > self.maxstaleSpeedCounter: 112 | return 0 113 | return self.previous.speed(c) 114 | return self.speedRevCountDiff * 1.024 * c / self.speedEventTimeDiff 115 | 116 | def distance(self, c): 117 | """ 118 | :param c: circumference of the wheel (mm) 119 | :return: The distance since the last message (m) 120 | """ 121 | return self.speedRevCountDiff * c / 1000 122 | 123 | def totalDistance(self, c): 124 | """ 125 | :param c: circumference of the wheel (mm) 126 | :return: The total distance since the first message (m) 127 | """ 128 | return self.totalSpeedRevolutions * c / 1000 129 | 130 | @lazyproperty 131 | def cadence(self): 132 | """ 133 | :return: RPM 134 | """ 135 | if self.previous is None: 136 | return 0 137 | if self.cadenceEventTime == self.previous.cadenceEventTime: 138 | if self.staleCadenceCounter > self.maxstaleCadenceCounter: 139 | return 0 140 | return self.previous.cadence 141 | return self.cadenceRevCountDiff * 1024 * 60 / self.cadenceEventTimeDiff 142 | 143 | @lazyproperty 144 | def averageCadence(self): 145 | """ 146 | Returns the average cadence since the first message 147 | :return: RPM 148 | """ 149 | if self.firstTimestamp == self.timestamp: 150 | return self.cadence 151 | return self.totalRevolutions * 60 / (self.timestamp - self.firstTimestamp) 152 | 153 | def averageSpeed(self, c): 154 | """ 155 | Returns the average speed since the first message 156 | :param c: circumference of the wheel (mm) 157 | :return: m/s 158 | """ 159 | if self.firstTimestamp == self.timestamp: 160 | return self.speed(c) 161 | return self.totalDistance(c) / (self.timestamp - self.firstTimestamp) 162 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyusb>=1.0.0 2 | pyserial>=3.1.1 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='LibAnt', 7 | packages=['libAnt', 'libAnt.profiles', 'libAnt.drivers', 'libAnt.loggers'], 8 | version='0.1.4', 9 | description='Python Ant+ Lib', 10 | author='Benjamin Tamasi', 11 | author_email='h@lfto.me', 12 | url='https://github.com/half2me/libAnt', 13 | download_url='https://github.com/half2me/libAnt/tarball/0.1.3', 14 | keywords = ['ant', 'antplus', 'ant+', 'antfs', 'thisisant'], 15 | install_requires=['pyusb>=1.0.0', 'pyserial>=3.1.1'], 16 | ) 17 | --------------------------------------------------------------------------------