├── .gitignore ├── CHANGES.md ├── DEVELOP.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── bootstrap ├── buildout.cfg ├── demos └── ant.core │ ├── 01-reset.py │ ├── 02-capabilities-USB.py │ ├── 02-capabilities.py │ ├── 03-basicchannel.py │ ├── 04-processevents.py │ ├── 05-rawmessage.py │ ├── 06-rawmessage2.py │ ├── 07-rawmessage3.py │ ├── 07-rawmessage3_SparkFun.py │ ├── 08-rawmessage4.py │ ├── 09-logreader.py │ └── config.py ├── docs ├── Makefile └── source │ ├── conf.py │ └── index.rst ├── setup.py └── src └── ant ├── __init__.py ├── core ├── __init__.py ├── constants.py ├── driver.py ├── event.py ├── exceptions.py ├── log.py ├── message.py ├── node.py └── tests │ ├── driver_tests.py │ ├── event_tests.py │ ├── log_tests.py │ ├── message_tests.py │ └── node_tests.py ├── fs └── __init__.py └── plus └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.swp 4 | *.ant 5 | *.swo 6 | .installed.cfg 7 | virtualenv.py 8 | bootstrap.py 9 | src/.coverage 10 | docs/build 11 | docs/source/.buildinfo 12 | dist 13 | build 14 | bin 15 | develop-eggs 16 | eggs 17 | downloads 18 | parts 19 | include 20 | lib 21 | lib64 22 | src/*.egg-info 23 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | 0.1.0 5 | ----- 6 | 7 | * Implement log module. 8 | * Implement message module. 9 | * Implement event module. 10 | * Implement node module. 11 | * Write various demos. 12 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | Introduction 5 | ------------ 6 | This project uses a combination of virtualenv and zc.buildout to setup a self 7 | contained development environment. It's designed so I can basically go to any 8 | of my computers, clone the upstream repository, source the bootstrap script, 9 | and begin working. 10 | 11 | Also, git-flow is being used to manage the general workflow of the repository, 12 | so please install it and stick with it. 13 | 14 | 15 | Bootstrapping 16 | ------------- 17 | With virtualenv and zc.buildout: 18 | % source bootstrap 19 | % buildout 20 | 21 | With zc.buildout only: 22 | % python bootstrap.py -d 23 | % bin/buildout 24 | 25 | 26 | Web Resources 27 | ------------- 28 | Project website, bug database, and GIT repository is at: 29 | http://github.com/mvillalba/python-ant 30 | 31 | Project documentation (Sphinx) is at: 32 | NOT-HOSTED-ANYWHERE-YET 33 | 34 | Backup GIT repositories: 35 | https://gitorious.org/python-ant/python-ant 36 | http://repo.or.cz/w/python-ant.git 37 | 38 | 39 | Release Checklist 40 | ----------------- 41 | * Deactivate virtualenv environment, if active 42 | * Clone upstream to clean directory (setup all remotes!) 43 | * Bootstrap environment 44 | * Start release branch (git-flow) 45 | * Set/check version number (setup.py, project's __init__.py, docs) 46 | * Run pylint 47 | * Run importchecker 48 | % importchecker src 49 | * Run pep8 50 | % pep8 -r src --count --statistics 51 | * Run test suite and check test coverage 52 | % nosetests --with-coverage --cover-inclusive --cover-erase 53 | * Freeze dependencies' version numbers in buildout.cfg and setup.py 54 | * Check bug database for open issues/bugs 55 | * Build documentation 56 | * Check documentation (coverage, grammar, contents, etc) 57 | * Update CHANGES.md 58 | * Update copyright statements if new year 59 | * Update setup.py 60 | * Create distribution bundles 61 | % buildout setup . sdist bdist_egg 62 | * Check dist/* files (no nuclear launch codes, all files present, do they work 63 | in a separate virtualenv with pip?) 64 | * Upload to PyPI 65 | % buildout setup . sdist bdist_egg register upload 66 | * Check package page in PyPI (readme, download links) 67 | * If first release, delete dummy "develop" version from PyPI 68 | * Re-test release in a clean environment, installing from the cheeseshop 69 | * Finish git-flow release and add release tag and commit release 70 | * Push upstream (GitHub master, gitorious backup, odin backup) 71 | % git push --all all && git push --tags all 72 | * Close old feature branches (GitHub) 73 | % git push origin :feature/{NAME-HERE} 74 | * Upload dist files to GitHub and download them to check integrity 75 | * Unfreeze version numbers from setup.py and buildout.cfg 76 | * Set version number to "develop" (setup.py, project's __init__.py, docs) 77 | * Go back to old development repo and update everything 78 | % git checkout develop && git pull origin develop 79 | % git checkout master && git pull origin master 80 | * Upload built documentation 81 | * Make public announcement, if necessary 82 | 83 | 84 | Release Notes 85 | ------------- 86 | If releasing anything but a final version, skip registering and uploading to 87 | the cheeseshop. 88 | 89 | If releasing a .devX version, some steps may be skipped from the release 90 | checklist. In particular, .devX versions should be treated mostly as an 91 | internal thing and thus, should generally not be published nor uploaded to 92 | GitHub. 93 | 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Martín Raúl Villalba 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include demos/*/*.py 2 | include src/ant/*/tests/*.py 3 | include LICENSE 4 | include README.md 5 | include CHANGES.md 6 | include DEVELOP.md 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ANT 2 | === 3 | 4 | Introduction 5 | ------------ 6 | Python implementation of the ANT, ANT+, and ANT-FS protocols. For more 7 | information about ANT, see http://www.thisisant.com/. 8 | 9 | Can be used to communicate with ANT nodes using an ANT stick (USB). 10 | 11 | This project came to be when I tried to download data for analysis from my 12 | ANT+/ANT-FS enabled running watch under GNU/Linux. This eventually lead me to 13 | attempting to port ANT_LIB and ANT_DLL (by Dynastream) to Linux. However, I 14 | didn't quite like the library, the protocol is well documented and trivial to 15 | implement, and I was going to have to write a ctypes-based wrapper afterwards 16 | since I was only going to use the library from Python. Thus, I decided to 17 | write a pure Python implementation. 18 | 19 | 20 | Contact 21 | ------- 22 | You can reach me via e-Mail and Google Talk/Jabber at: 23 | martin at NOSPAM martinvillalba dot com 24 | 25 | Documentation 26 | ------------- 27 | Documentation will be a bit scarse for the time being, but everything public 28 | should have at least a docstring by the time I make the first stable release. 29 | 30 | 31 | License 32 | ------- 33 | Released under the MIT/X11 license. See LICENSE for the full text. 34 | 35 | 36 | Install 37 | ------- 38 | % python setup.py install 39 | 40 | 41 | Develop 42 | ------- 43 | See DEVELOP.md for details. 44 | -------------------------------------------------------------------------------- /bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Take out the trash 3 | echo "Taking out the trash..." 4 | rm -rf virtualenv.py bootstrap.py bin develop-eggs eggs include lib parts .installed.cfg dist build downloads lib64 src/ant.egg-info docs/build docs/source/.buildinfo src/.coverage 5 | 6 | # Fetch bootstrap files 7 | echo "Fetching bootstrap scripts..." 8 | wget -q https://raw.github.com/pypa/virtualenv/develop/virtualenv.py 9 | wget -q http://svn.zope.org/repos/main/zc.buildout/trunk/bootstrap/bootstrap.py 10 | 11 | # Create and activate isolated Python environment 12 | echo "Creating isolated Python environment..." 13 | python virtualenv.py --no-site-packages --distribute --prompt=\[ant\] . 14 | rm -f *.tar.* 15 | source bin/activate 16 | 17 | # Let zc.buildout do it's thing 18 | echo "Initializing buildout..." 19 | python bootstrap.py -d 20 | 21 | 22 | # Done 23 | echo "All done. Go ahead and run buildout." 24 | -------------------------------------------------------------------------------- /buildout.cfg: -------------------------------------------------------------------------------- 1 | [buildout] 2 | index = http://mirrors.martinvillalba.com/pypi 3 | parts = 4 | mpython 5 | nosetests 6 | pylint 7 | pep8 8 | importchecker 9 | sphinx 10 | pyusb 11 | develop = . 12 | eggs = ant 13 | 14 | [mpython] 15 | recipe = zc.recipe.egg 16 | interpreter = mpython 17 | eggs = ${buildout:eggs} 18 | extra-paths = 19 | ${buildout:directory}/demos/ant.core 20 | ${buildout:directory}/demos/ant.fs 21 | ${buildout:directory}/demos/ant.plus 22 | 23 | [nosetests] 24 | recipe = pbp.recipe.noserunner 25 | eggs = 26 | ${buildout:eggs} 27 | coverage 28 | working-directory = src 29 | 30 | [pylint] 31 | recipe = zc.recipe.egg 32 | eggs = 33 | ${buildout:eggs} 34 | pylint 35 | scripts = pylint 36 | entry-points = pylint=pylint.lint:Run 37 | arguments = sys.argv[1:] 38 | 39 | [pep8] 40 | recipe = zc.recipe.egg 41 | eggs = pep8 42 | 43 | [importchecker] 44 | recipe = zc.recipe.egg 45 | eggs = importchecker 46 | 47 | [sphinx] 48 | recipe = zc.recipe.egg 49 | eggs = 50 | ${buildout:eggs} 51 | sphinx 52 | 53 | [pyusb] 54 | recipe = zc.recipe.egg 55 | eggs = 56 | ${buildout:eggs} 57 | pyusb 58 | -------------------------------------------------------------------------------- /demos/ant.core/01-reset.py: -------------------------------------------------------------------------------- 1 | """ 2 | Perform basic node initialization and shutdown cleanly. 3 | 4 | """ 5 | 6 | import sys 7 | 8 | from ant.core import driver 9 | from ant.core import node 10 | 11 | from config import * 12 | 13 | # Initialize and configure our ANT stick's driver 14 | stick = driver.USB1Driver(SERIAL, log=LOG, debug=DEBUG) 15 | 16 | # Now create an ANT node, and pass it our driver so it can talk to the stick 17 | antnode = node.Node(stick) 18 | 19 | # Open driver if closed, start event listener, reset internal settings, and 20 | # send a system reset command to the ANT stick (blocks). 21 | try: 22 | antnode.start() 23 | except driver.DriverError, e: 24 | print e 25 | sys.exit() 26 | 27 | # At any point in our node's life, we could manually call reset() to re- 28 | # initialize the stick and Node. Like this: 29 | #antnode.reset() 30 | 31 | # Stop the ANT node. This should close all open channels, and do final system 32 | # reset on the stick. However, considering we just did a reset, we explicitly 33 | # tell our node to skip the reset. This call will also automatically release 34 | # the stick by calling close() on the driver. 35 | antnode.stop(reset=False) 36 | -------------------------------------------------------------------------------- /demos/ant.core/02-capabilities-USB.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interrogate stick for supported capabilities. 3 | 4 | """ 5 | 6 | import sys 7 | 8 | from ant.core import driver 9 | from ant.core import node 10 | 11 | from config import * 12 | 13 | # Initialize 14 | stick = driver.USB2Driver(SERIAL, log=LOG, debug=DEBUG) 15 | antnode = node.Node(stick) 16 | antnode.start() 17 | 18 | # Interrogate stick 19 | # Note: This method will return immediately, as the stick's capabilities are 20 | # interrogated on node initialization (node.start()) in order to set proper 21 | # internal Node instance state. 22 | capabilities = antnode.getCapabilities() 23 | 24 | print 'Maximum channels:', capabilities[0] 25 | print 'Maximum network keys:', capabilities[1] 26 | print 'Standard options: %X' % capabilities[2][0] 27 | print 'Advanced options: %X' % capabilities[2][1] 28 | 29 | # Shutdown 30 | antnode.stop() 31 | -------------------------------------------------------------------------------- /demos/ant.core/02-capabilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interrogate stick for supported capabilities. 3 | 4 | """ 5 | 6 | import sys 7 | 8 | from ant.core import driver 9 | from ant.core import node 10 | 11 | from config import * 12 | 13 | # Initialize 14 | stick = driver.USB1Driver(SERIAL, log=LOG, debug=DEBUG) 15 | antnode = node.Node(stick) 16 | antnode.start() 17 | 18 | # Interrogate stick 19 | # Note: This method will return immediately, as the stick's capabilities are 20 | # interrogated on node initialization (node.start()) in order to set proper 21 | # internal Node instance state. 22 | capabilities = antnode.getCapabilities() 23 | 24 | print 'Maximum channels:', capabilities[0] 25 | print 'Maximum network keys:', capabilities[1] 26 | print 'Standard options: %X' % capabilities[2][0] 27 | print 'Advanced options: %X' % capabilities[2][1] 28 | 29 | # Shutdown 30 | antnode.stop() 31 | -------------------------------------------------------------------------------- /demos/ant.core/03-basicchannel.py: -------------------------------------------------------------------------------- 1 | """ 2 | Initialize a basic broadcast slave channel for listening to 3 | an ANT+ HR monitor. 4 | 5 | """ 6 | 7 | import sys 8 | import time 9 | 10 | from ant.core import driver 11 | from ant.core import node 12 | from ant.core.constants import * 13 | 14 | from config import * 15 | 16 | NETKEY = '\xB9\xA5\x21\xFB\xBD\x72\xC3\x45' 17 | 18 | # Initialize 19 | stick = driver.USB1Driver(SERIAL, log=LOG, debug=DEBUG) 20 | antnode = node.Node(stick) 21 | antnode.start() 22 | 23 | # Set network key 24 | key = node.NetworkKey('N:ANT+', NETKEY) 25 | antnode.setNetworkKey(0, key) 26 | 27 | # Get the first unused channel. Returns an instance of the node.Channel class. 28 | channel = antnode.getFreeChannel() 29 | 30 | # Let's give our channel a nickname 31 | channel.name = 'C:HRM' 32 | 33 | # Initialize it as a receiving channel using our network key 34 | channel.assign('N:ANT+', CHANNEL_TYPE_TWOWAY_RECEIVE) 35 | 36 | # Now set the channel id for pairing with an ANT+ HR monitor 37 | channel.setID(120, 0, 0) 38 | 39 | # Listen forever and ever (not really, but for a long time) 40 | channel.setSearchTimeout(TIMEOUT_NEVER) 41 | 42 | # We want a ~4.06 Hz transmission period 43 | channel.setPeriod(8070) 44 | 45 | # And ANT frequency 57 46 | channel.setFrequency(57) 47 | 48 | # Time to go live 49 | channel.open() 50 | 51 | print "Listening for HR monitor events (120 seconds)..." 52 | time.sleep(120) 53 | 54 | # Shutdown channel 55 | channel.close() 56 | channel.unassign() 57 | 58 | # Shutdown 59 | antnode.stop() 60 | -------------------------------------------------------------------------------- /demos/ant.core/04-processevents.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extending on demo-03, implements an event callback we can use to process the 3 | incoming data. 4 | 5 | """ 6 | 7 | import sys 8 | import time 9 | 10 | from ant.core import driver 11 | from ant.core import node 12 | from ant.core import event 13 | from ant.core import message 14 | from ant.core.constants import * 15 | 16 | from config import * 17 | 18 | NETKEY = '\xB9\xA5\x21\xFB\xBD\x72\xC3\x45' 19 | 20 | # A run-the-mill event listener 21 | class HRMListener(event.EventCallback): 22 | def process(self, msg): 23 | if isinstance(msg, message.ChannelBroadcastDataMessage): 24 | print 'Heart Rate:', ord(msg.payload[-1]) 25 | 26 | # Initialize 27 | stick = driver.USB2Driver(SERIAL, log=LOG, debug=DEBUG) 28 | antnode = node.Node(stick) 29 | antnode.start() 30 | 31 | # Setup channel 32 | key = node.NetworkKey('N:ANT+', NETKEY) 33 | antnode.setNetworkKey(0, key) 34 | channel = antnode.getFreeChannel() 35 | channel.name = 'C:HRM' 36 | channel.assign('N:ANT+', CHANNEL_TYPE_TWOWAY_RECEIVE) 37 | channel.setID(120, 0, 0) 38 | channel.setSearchTimeout(TIMEOUT_NEVER) 39 | channel.setPeriod(8070) 40 | channel.setFrequency(57) 41 | channel.open() 42 | 43 | # Setup callback 44 | # Note: We could also register an event listener for non-channel events by 45 | # calling registerEventListener() on antnode rather than channel. 46 | channel.registerCallback(HRMListener()) 47 | 48 | # Wait 49 | print "Listening for HR monitor events (120 seconds)..." 50 | time.sleep(120) 51 | 52 | # Shutdown 53 | channel.close() 54 | channel.unassign() 55 | antnode.stop() 56 | -------------------------------------------------------------------------------- /demos/ant.core/05-rawmessage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Do a system reset using raw messages. 3 | 4 | """ 5 | 6 | import sys 7 | import time 8 | 9 | from ant.core import driver 10 | from ant.core import message 11 | from ant.core.constants import * 12 | 13 | from config import * 14 | 15 | # Initialize 16 | stick = driver.USB1Driver(SERIAL, log=LOG, debug=DEBUG) 17 | stick.open() 18 | 19 | # Prepare system reset message 20 | msg = message.Message() 21 | msg.setType(MESSAGE_SYSTEM_RESET) 22 | msg.setPayload('\x00') 23 | 24 | # Send 25 | stick.write(msg.encode()) 26 | 27 | # Wait for reset to complete 28 | time.sleep(1) 29 | 30 | # Alternatively, we could have done this: 31 | msg = message.SystemResetMessage() 32 | stick.write(msg.encode()) 33 | time.sleep(1) 34 | 35 | # Shutdown 36 | stick.close() 37 | -------------------------------------------------------------------------------- /demos/ant.core/06-rawmessage2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extending on demo 05, request stick capabilities using raw messages. 3 | 4 | """ 5 | 6 | import sys 7 | import time 8 | 9 | from ant.core import driver 10 | from ant.core import message 11 | from ant.core.constants import * 12 | 13 | from config import * 14 | 15 | # Initialize 16 | stick = driver.USB1Driver(SERIAL, log=LOG, debug=DEBUG) 17 | stick.open() 18 | 19 | # Reset stick 20 | msg = message.SystemResetMessage() 21 | stick.write(msg.encode()) 22 | time.sleep(1) 23 | 24 | # Request stick capabilities 25 | msg = message.ChannelRequestMessage() 26 | msg.setMessageID(MESSAGE_CAPABILITIES) 27 | stick.write(msg.encode()) 28 | 29 | # Read response 30 | hdlfinder = message.Message() 31 | capmsg = hdlfinder.getHandler(stick.read(8)) 32 | 33 | print 'Std Options:', capmsg.getStdOptions() 34 | print 'Adv Options:', capmsg.getAdvOptions() 35 | print 'Adv Options 2:', capmsg.getAdvOptions2() 36 | print 'Max Channels:', capmsg.getMaxChannels() 37 | print 'Max Networks:', capmsg.getMaxNetworks() 38 | 39 | # Shutdown 40 | stick.close() 41 | -------------------------------------------------------------------------------- /demos/ant.core/07-rawmessage3.py: -------------------------------------------------------------------------------- 1 | """ 2 | Initialize a basic broadcast slave channel for listening to 3 | an ANT+ Bicycle cadence and speed senser, using raw messages 4 | and event handlers. 5 | 6 | """ 7 | 8 | import sys 9 | import time 10 | 11 | from ant.core import driver 12 | from ant.core import event 13 | from ant.core.constants import * 14 | from ant.core.message import * 15 | 16 | from config import * 17 | 18 | NETKEY = '\xB9\xA5\x21\xFB\xBD\x72\xC3\x45' 19 | 20 | # Event callback 21 | class MyCallback(event.EventCallback): 22 | def process(self, msg): 23 | print msg 24 | 25 | # Initialize driver 26 | stick = driver.USB1Driver(SERIAL, log=LOG, debug=DEBUG) 27 | stick.open() 28 | 29 | # Initialize event machine 30 | evm = event.EventMachine(stick) 31 | evm.registerCallback(MyCallback()) 32 | evm.start() 33 | 34 | # Reset 35 | msg = SystemResetMessage() 36 | stick.write(msg.encode()) 37 | time.sleep(1) 38 | 39 | # Set network key 40 | msg = NetworkKeyMessage(key=NETKEY) 41 | stick.write(msg.encode()) 42 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 43 | sys.exit() 44 | 45 | # Initialize it as a receiving channel using our network key 46 | msg = ChannelAssignMessage() 47 | stick.write(msg.encode()) 48 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 49 | sys.exit() 50 | 51 | # Now set the channel id for pairing with an ANT+ bike cadence/speed sensor 52 | msg = ChannelIDMessage(device_type=121) 53 | stick.write(msg.encode()) 54 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 55 | sys.exit() 56 | 57 | # Listen forever and ever (not really, but for a long time) 58 | msg = ChannelSearchTimeoutMessage(timeout=255) 59 | stick.write(msg.encode()) 60 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 61 | sys.exit() 62 | 63 | # We want a ~4.05 Hz transmission period 64 | msg = ChannelPeriodMessage(period=8085) 65 | stick.write(msg.encode()) 66 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 67 | sys.exit() 68 | 69 | # And ANT frequency 57, of course 70 | msg = ChannelFrequencyMessage(frequency=57) 71 | stick.write(msg.encode()) 72 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 73 | sys.exit() 74 | 75 | # Time to go live 76 | msg = ChannelOpenMessage() 77 | stick.write(msg.encode()) 78 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 79 | sys.exit() 80 | 81 | print "Listening for ANT events (120 seconds)..." 82 | time.sleep(120) 83 | 84 | # Shutdown 85 | msg = SystemResetMessage() 86 | stick.write(msg.encode()) 87 | time.sleep(1) 88 | 89 | evm.stop() 90 | stick.close() 91 | -------------------------------------------------------------------------------- /demos/ant.core/07-rawmessage3_SparkFun.py: -------------------------------------------------------------------------------- 1 | """ 2 | Initialize a basic broadcast slave channel for listening to 3 | an ANT+ Bicycle cadence and speed senser, using raw messages 4 | and event handlers. 5 | 6 | """ 7 | 8 | import sys 9 | import time 10 | 11 | from ant.core import driver 12 | from ant.core import event 13 | from ant.core.constants import * 14 | from ant.core.message import * 15 | 16 | from config import * 17 | 18 | NETKEY = '\xB9\xA5\x21\xFB\xBD\x72\xC3\x45' 19 | 20 | # Event callback 21 | class MyCallback(event.EventCallback): 22 | def process(self, msg): 23 | print msg 24 | 25 | # Initialize driver 26 | stick = driver.USB1Driver(SERIAL, log=LOG, debug=DEBUG,baud_rate=4800) 27 | stick.open() 28 | 29 | # Initialize event machine 30 | evm = event.EventMachine(stick) 31 | evm.registerCallback(MyCallback()) 32 | evm.start() 33 | 34 | # Reset 35 | msg = SystemResetMessage() 36 | stick.write(msg.encode()) 37 | time.sleep(1) 38 | 39 | # Set network key 40 | msg = NetworkKeyMessage(key=NETKEY) 41 | stick.write(msg.encode()) 42 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 43 | sys.exit() 44 | 45 | # Initialize it as a receiving channel using our network key 46 | msg = ChannelAssignMessage() 47 | stick.write(msg.encode()) 48 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 49 | sys.exit() 50 | 51 | # Now set the channel id for pairing with an ANT+ bike cadence/speed sensor 52 | msg = ChannelIDMessage(device_type=121) 53 | stick.write(msg.encode()) 54 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 55 | sys.exit() 56 | 57 | # Listen forever and ever (not really, but for a long time) 58 | msg = ChannelSearchTimeoutMessage(timeout=255) 59 | stick.write(msg.encode()) 60 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 61 | sys.exit() 62 | 63 | # We want a ~4.05 Hz transmission period 64 | msg = ChannelPeriodMessage(period=8085) 65 | stick.write(msg.encode()) 66 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 67 | sys.exit() 68 | 69 | # And ANT frequency 57, of course 70 | msg = ChannelFrequencyMessage(frequency=57) 71 | stick.write(msg.encode()) 72 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 73 | sys.exit() 74 | 75 | # Time to go live 76 | msg = ChannelOpenMessage() 77 | stick.write(msg.encode()) 78 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 79 | sys.exit() 80 | 81 | print "Listening for ANT events (120 seconds)..." 82 | time.sleep(120) 83 | 84 | # Shutdown 85 | msg = SystemResetMessage() 86 | stick.write(msg.encode()) 87 | time.sleep(1) 88 | 89 | evm.stop() 90 | stick.close() 91 | -------------------------------------------------------------------------------- /demos/ant.core/08-rawmessage4.py: -------------------------------------------------------------------------------- 1 | """ 2 | Initialize a basic broadcast slave channel for listening to 3 | an ANT+ HRM monitor, using raw messages. 4 | 5 | """ 6 | 7 | import sys 8 | import time 9 | 10 | from ant.core import driver 11 | from ant.core import event 12 | from ant.core.constants import * 13 | from ant.core.message import * 14 | 15 | from config import * 16 | 17 | NETKEY = '\xB9\xA5\x21\xFB\xBD\x72\xC3\x45' 18 | 19 | # Event callback 20 | class MyCallback(event.EventCallback): 21 | def process(self, msg): 22 | print msg 23 | if isinstance(msg, ChannelBroadcastDataMessage): 24 | print 'Beat Count:', ord(msg.getPayload()[7]) 25 | print 'Heart Rate:', ord(msg.getPayload()[8]) 26 | 27 | # Initialize driver 28 | stick = driver.USB1Driver(SERIAL, log=LOG) # No debug, too much data 29 | stick.open() 30 | 31 | # Initialize event machine 32 | evm = event.EventMachine(stick) 33 | evm.registerCallback(MyCallback()) 34 | evm.start() 35 | 36 | # Reset 37 | msg = SystemResetMessage() 38 | stick.write(msg.encode()) 39 | time.sleep(1) 40 | 41 | # Set network key 42 | msg = NetworkKeyMessage(key=NETKEY) 43 | stick.write(msg.encode()) 44 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 45 | sys.exit() 46 | 47 | # Initialize it as a receiving channel using our network key 48 | msg = ChannelAssignMessage() 49 | stick.write(msg.encode()) 50 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 51 | sys.exit() 52 | 53 | # Now set the channel id for pairing with an ANT+ heart rate monitor 54 | msg = ChannelIDMessage(device_type=120) 55 | stick.write(msg.encode()) 56 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 57 | sys.exit() 58 | 59 | # Listen forever and ever (not really, but for a long time) 60 | msg = ChannelSearchTimeoutMessage(timeout=255) 61 | stick.write(msg.encode()) 62 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 63 | sys.exit() 64 | 65 | # We want a ~4.06 Hz transmission period 66 | msg = ChannelPeriodMessage(period=8070) 67 | stick.write(msg.encode()) 68 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 69 | sys.exit() 70 | 71 | # And ANT frequency 57, of course 72 | msg = ChannelFrequencyMessage(frequency=57) 73 | stick.write(msg.encode()) 74 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 75 | sys.exit() 76 | 77 | # Time to go live 78 | msg = ChannelOpenMessage() 79 | stick.write(msg.encode()) 80 | if evm.waitForAck(msg) != RESPONSE_NO_ERROR: 81 | sys.exit() 82 | 83 | print "Listening for ANT events (120 seconds)..." 84 | time.sleep(120) 85 | 86 | # Shutdown 87 | msg = SystemResetMessage() 88 | stick.write(msg.encode()) 89 | time.sleep(1) 90 | 91 | evm.stop() 92 | stick.close() 93 | -------------------------------------------------------------------------------- /demos/ant.core/09-logreader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read an ANT-LOG file. 3 | 4 | """ 5 | 6 | import sys 7 | 8 | from ant.core import log 9 | 10 | from config import * 11 | 12 | # Open log 13 | if len(sys.argv) != 2: 14 | print "Usage: {0} file.ant".format(sys.argv[0]) 15 | sys.exit() 16 | 17 | lr = log.LogReader(sys.argv[1]) 18 | 19 | event = lr.read() 20 | while (event != None): 21 | if event[0] == log.EVENT_OPEN: 22 | title = 'EVENT_OPEN' 23 | elif event[0] == log.EVENT_CLOSE: 24 | title = 'EVENT_CLOSE' 25 | elif event[0] == log.EVENT_READ: 26 | title = 'EVENT_READ' 27 | elif event[0] == log.EVENT_WRITE: 28 | title = 'EVENT_WRITE' 29 | 30 | print '========== [{0}:{1}] =========='.format(title, event[1]) 31 | if event[0] == log.EVENT_READ or event[0] == log.EVENT_WRITE: 32 | length = 8 33 | line = 0 34 | data = event[2] 35 | while data: 36 | row = data[:length] 37 | data = data[length:] 38 | hex_data = ['%02X' % ord(byte) for byte in row] 39 | print '%04X' % line, ' '.join(hex_data) 40 | 41 | print '' 42 | event = lr.read() 43 | -------------------------------------------------------------------------------- /demos/ant.core/config.py: -------------------------------------------------------------------------------- 1 | from ant.core import log 2 | 3 | # USB1 ANT stick interface. Running `dmesg | tail -n 25` after plugging the 4 | # stick on a USB port should tell you the exact interface. 5 | SERIAL = '/dev/ttyUSB0' 6 | 7 | # If set to True, the stick's driver will dump everything it reads/writes 8 | # from/to the stick. 9 | # Some demos depend on this setting being True, so unless you know what you 10 | # are doing, leave it as is. 11 | DEBUG = True 12 | 13 | # Set to None to disable logging 14 | #LOG = None 15 | LOG = log.LogWriter() 16 | 17 | # ========== DO NOT CHANGE ANYTHING BELOW THIS LINE ========== 18 | print "Using log file:", LOG.filename 19 | print "" 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ant.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ant.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/ant" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ant" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ant documentation build configuration file, created by 4 | # sphinx-quickstart on Thu May 26 09:57:48 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing 7 | # dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | import ant 19 | 20 | # -- General configuration ---------------------------------------------------- 21 | 22 | # If your documentation needs a minimal Sphinx version, state it here. 23 | needs_sphinx = '1.0' 24 | 25 | # Add any Sphinx extension module names here, as strings. They can be 26 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 27 | extensions = [ 28 | 'sphinx.ext.autodoc', 29 | 'sphinx.ext.todo', 30 | 'sphinx.ext.coverage', 31 | 'sphinx.ext.viewcode' 32 | ] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = '.rst' 39 | 40 | # The encoding of source files. 41 | source_encoding = 'utf-8-sig' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = u'ant' 48 | copyright = u'2011, Martín Raúl Villalba' 49 | 50 | # The version info for the project you're documenting, acts as replacement for 51 | # |version| and |release|, also used in various other places throughout the 52 | # built documents. 53 | # 54 | # The short X.Y version. 55 | version = ant.__version__ 56 | # The full version, including alpha/beta/rc tags. 57 | release = ant.__version__ 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | language = 'en' 62 | 63 | # There are two options for replacing |today|: either, you set today to some 64 | # non-false value, then it is used: 65 | today = '' 66 | # Else, today_fmt is used as the format for a strftime call. 67 | today_fmt = '%Y-%m-%d' 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | exclude_patterns = [] 72 | 73 | # The reST default role (used for this markup: `text`) to use for all 74 | # documents. 75 | default_role = None 76 | 77 | # If true, '()' will be appended to :func: etc. cross-reference text. 78 | add_function_parentheses = True 79 | 80 | # If true, the current module name will be prepended to all description 81 | # unit titles (such as .. function::). 82 | add_module_names = True 83 | 84 | # If true, sectionauthor and moduleauthor directives will be shown in the 85 | # output. They are ignored by default. 86 | show_authors = True 87 | 88 | # The name of the Pygments (syntax highlighting) style to use. 89 | pygments_style = 'sphinx' 90 | 91 | # A list of ignored prefixes for module index sorting. 92 | modindex_common_prefix = [] 93 | 94 | 95 | # -- Options for HTML output -------------------------------------------------- 96 | 97 | # The theme to use for HTML and HTML Help pages. See the documentation for 98 | # a list of builtin themes. 99 | html_theme = 'nature' 100 | 101 | # Theme options are theme-specific and customize the look and feel of a theme 102 | # further. For a list of options available for each theme, see the 103 | # documentation. 104 | html_theme_options = {} 105 | 106 | # Add any paths that contain custom themes here, relative to this directory. 107 | html_theme_path = [] 108 | 109 | # The name for this set of Sphinx documents. If None, it defaults to 110 | # " v documentation". 111 | html_title = 'Documentation for ant, version {0}' \ 112 | .format(ant.__version__) 113 | 114 | # A shorter title for the navigation bar. Default is the same as html_title. 115 | html_short_title = html_title 116 | 117 | # The name of an image file (relative to this directory) to place at the top 118 | # of the sidebar. 119 | html_logo = None 120 | 121 | # The name of an image file (within the static path) to use as favicon of the 122 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 123 | # pixels large. 124 | html_favicon = None 125 | 126 | # Add any paths that contain custom static files (such as style sheets) here, 127 | # relative to this directory. They are copied after the builtin static files, 128 | # so a file named "default.css" will overwrite the builtin "default.css". 129 | html_static_path = ['_static'] 130 | 131 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 132 | # using the given strftime format. 133 | html_last_updated_fmt = '%Y-%m-%d' 134 | 135 | # If true, SmartyPants will be used to convert quotes and dashes to 136 | # typographically correct entities. 137 | html_use_smartypants = True 138 | 139 | # Custom sidebar templates, maps document names to template names. 140 | html_sidebars = {} 141 | 142 | # Additional templates that should be rendered to pages, maps page names to 143 | # template names. 144 | html_additional_pages = {} 145 | 146 | # If false, no module index is generated. 147 | html_domain_indices = True 148 | 149 | # If false, no index is generated. 150 | html_use_index = True 151 | 152 | # If true, the index is split into individual pages for each letter. 153 | html_split_index = False 154 | 155 | # If true, links to the reST sources are added to the pages. 156 | html_show_sourcelink = True 157 | 158 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 159 | html_show_sphinx = True 160 | 161 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 162 | html_show_copyright = True 163 | 164 | # If true, an OpenSearch description file will be output, and all pages will 165 | # contain a tag referring to it. The value of this option must be the 166 | # base URL from which the finished HTML is served. 167 | #html_use_opensearch = '' 168 | 169 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 170 | html_file_suffix = None 171 | 172 | # Output file base name for HTML help builder. 173 | htmlhelp_basename = 'ant_doc' 174 | 175 | 176 | # -- Options for LaTeX output ------------------------------------------------- 177 | 178 | # The paper size ('letter' or 'a4'). 179 | #latex_paper_size = 'letter' 180 | 181 | # The font size ('10pt', '11pt' or '12pt'). 182 | #latex_font_size = '10pt' 183 | 184 | # Grouping the document tree into LaTeX files. List of tuples 185 | # (source start file, target name, title, author, documentclass 186 | # [howto/manual]). 187 | latex_documents = [ 188 | ('index', 'ant.tex', u'Documentation for ant', 189 | u'Martín Raúl Villalba', 'manual'), 190 | ] 191 | 192 | # The name of an image file (relative to this directory) to place at the top of 193 | # the title page. 194 | #latex_logo = None 195 | 196 | # For "manual" documents, if this is true, then toplevel headings are parts, 197 | # not chapters. 198 | #latex_use_parts = False 199 | 200 | # If true, show page references after internal links. 201 | #latex_show_pagerefs = False 202 | 203 | # If true, show URL addresses after external links. 204 | #latex_show_urls = False 205 | 206 | # Additional stuff for the LaTeX preamble. 207 | #latex_preamble = '' 208 | 209 | # Documents to append as an appendix to all manuals. 210 | #latex_appendices = [] 211 | 212 | # If false, no module index is generated. 213 | #latex_domain_indices = True 214 | 215 | 216 | # -- Options for manual page output ------------------------------------------- 217 | 218 | # One entry per manual page. List of tuples 219 | # (source start file, name, description, authors, manual section). 220 | man_pages = [ 221 | ('index', 'ant', u'Documentation for ant', 222 | [u'Martín Raúl Villalba'], 1) 223 | ] 224 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. ant documentation master file, created by 2 | sphinx-quickstart on Thu May 26 09:57:48 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to ant's documentation! 7 | =============================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | Indices and tables 15 | ================== 16 | 17 | * :ref:`genindex` 18 | * :ref:`modindex` 19 | * :ref:`search` 20 | 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | import os 27 | 28 | from setuptools import setup, find_packages 29 | 30 | 31 | def read(fname): 32 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 33 | 34 | setup( 35 | name='ant', 36 | version='0.0.1', 37 | url='http://www.github.com/mvillalba/python-ant', 38 | license='MIT', 39 | description='Python implementation of the ANT, ANT+, and ANT-FS ' \ 40 | 'protocols (http://www.thisisant.com/).', 41 | author=u'Mart\u00EDn Ra\u00FAl Villalba', 42 | author_email='martin@martinvillalba.com', 43 | long_description=read('README.md'), 44 | classifiers=[ 45 | "Development Status :: 2 - Pre-Alpha", 46 | "Intended Audience :: Developers", 47 | "License :: OSI Approved :: MIT License", 48 | "Operating System :: OS Independent", 49 | "Programming Language :: Python", 50 | "Programming Language :: Python :: 2.6", 51 | "Programming Language :: Python :: 2.7", 52 | "Topic :: Communications", 53 | "Topic :: Communications :: File Sharing", 54 | "Topic :: Software Development :: Libraries :: Python Modules", 55 | ], 56 | packages=find_packages('src'), 57 | package_dir={'': 'src'}, 58 | install_requires=[ 59 | 'distribute', 60 | 'pyserial', 61 | 'pyusb', 62 | 'msgpack-python' 63 | ], 64 | ) 65 | -------------------------------------------------------------------------------- /src/ant/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | __version__ = 'develop' 27 | -------------------------------------------------------------------------------- /src/ant/core/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | __all__ = ['exceptions', 'driver', 'message'] 27 | -------------------------------------------------------------------------------- /src/ant/core/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | MESSAGE_TX_SYNC = 0xA4 27 | 28 | # Configuration messages 29 | MESSAGE_CHANNEL_UNASSIGN = 0x41 30 | MESSAGE_CHANNEL_ASSIGN = 0x42 31 | MESSAGE_CHANNEL_ID = 0x51 32 | MESSAGE_CHANNEL_PERIOD = 0x43 33 | MESSAGE_CHANNEL_SEARCH_TIMEOUT = 0x44 34 | MESSAGE_CHANNEL_FREQUENCY = 0x45 35 | MESSAGE_CHANNEL_TX_POWER = 0x60 36 | MESSAGE_NETWORK_KEY = 0x46 37 | MESSAGE_TX_POWER = 0x47 38 | MESSAGE_PROXIMITY_SEARCH = 0x71 39 | 40 | # Notification messages 41 | MESSAGE_STARTUP = 0x6F 42 | 43 | # Control messages 44 | MESSAGE_SYSTEM_RESET = 0x4A 45 | MESSAGE_CHANNEL_OPEN = 0x4B 46 | MESSAGE_CHANNEL_CLOSE = 0x4C 47 | MESSAGE_CHANNEL_REQUEST = 0x4D 48 | 49 | # Data messages 50 | MESSAGE_CHANNEL_BROADCAST_DATA = 0x4E 51 | MESSAGE_CHANNEL_ACKNOWLEDGED_DATA = 0x4F 52 | MESSAGE_CHANNEL_BURST_DATA = 0x50 53 | 54 | # Channel event messages 55 | MESSAGE_CHANNEL_EVENT = 0x40 56 | 57 | # Requested response messages 58 | MESSAGE_CHANNEL_STATUS = 0x52 59 | #MESSAGE_CHANNEL_ID = 0x51 60 | MESSAGE_VERSION = 0x3E 61 | MESSAGE_CAPABILITIES = 0x54 62 | MESSAGE_SERIAL_NUMBER = 0x61 63 | 64 | # Message parameters 65 | CHANNEL_TYPE_TWOWAY_RECEIVE = 0x00 66 | CHANNEL_TYPE_TWOWAY_TRANSMIT = 0x10 67 | CHANNEL_TYPE_SHARED_RECEIVE = 0x20 68 | CHANNEL_TYPE_SHARED_TRANSMIT = 0x30 69 | CHANNEL_TYPE_ONEWAY_RECEIVE = 0x40 70 | CHANNEL_TYPE_ONEWAY_TRANSMIT = 0x50 71 | RADIO_TX_POWER_MINUS20DB = 0x00 72 | RADIO_TX_POWER_MINUS10DB = 0x01 73 | RADIO_TX_POWER_0DB = 0x02 74 | RADIO_TX_POWER_PLUS4DB = 0x03 75 | RESPONSE_NO_ERROR = 0x00 76 | EVENT_RX_SEARCH_TIMEOUT = 0x01 77 | EVENT_RX_FAIL = 0x02 78 | EVENT_TX = 0x03 79 | EVENT_TRANSFER_RX_FAILED = 0x04 80 | EVENT_TRANSFER_TX_COMPLETED = 0x05 81 | EVENT_TRANSFER_TX_FAILED = 0x06 82 | EVENT_CHANNEL_CLOSED = 0x07 83 | EVENT_RX_FAIL_GO_TO_SEARCH = 0x08 84 | EVENT_CHANNEL_COLLISION = 0x09 85 | EVENT_TRANSFER_TX_START = 0x0A 86 | CHANNEL_IN_WRONG_STATE = 0x15 87 | CHANNEL_NOT_OPENED = 0x16 88 | CHANNEL_ID_NOT_SET = 0x18 89 | CLOSE_ALL_CHANNELS = 0x19 90 | TRANSFER_IN_PROGRESS = 0x1F 91 | TRANSFER_SEQUENCE_NUMBER_ERROR = 0x20 92 | TRANSFER_IN_ERROR = 0x21 93 | MESSAGE_SIZE_EXCEEDS_LIMIT = 0x27 94 | INVALID_MESSAGE = 0x28 95 | INVALID_NETWORK_NUMBER = 0x29 96 | INVALID_LIST_ID = 0x30 97 | INVALID_SCAN_TX_CHANNEL = 0x31 98 | INVALID_PARAMETER_PROVIDED = 0x33 99 | EVENT_QUEUE_OVERFLOW = 0x35 100 | USB_STRING_WRITE_FAIL = 0x70 101 | CHANNEL_STATE_UNASSIGNED = 0x00 102 | CHANNEL_STATE_ASSIGNED = 0x01 103 | CHANNEL_STATE_SEARCHING = 0x02 104 | CHANNEL_STATE_TRACKING = 0x03 105 | CAPABILITIES_NO_RECEIVE_CHANNELS = 0x01 106 | CAPABILITIES_NO_TRANSMIT_CHANNELS = 0x02 107 | CAPABILITIES_NO_RECEIVE_MESSAGES = 0x04 108 | CAPABILITIES_NO_TRANSMIT_MESSAGES = 0x08 109 | CAPABILITIES_NO_ACKNOWLEDGED_MESSAGES = 0x10 110 | CAPABILITIES_NO_BURST_MESSAGES = 0x20 111 | CAPABILITIES_NETWORK_ENABLED = 0x02 112 | CAPABILITIES_SERIAL_NUMBER_ENABLED = 0x08 113 | CAPABILITIES_PER_CHANNEL_TX_POWER_ENABLED = 0x10 114 | CAPABILITIES_LOW_PRIORITY_SEARCH_ENABLED = 0x20 115 | CAPABILITIES_SCRIPT_ENABLED = 0x40 116 | CAPABILITIES_SEARCH_LIST_ENABLED = 0x80 117 | CAPABILITIES_LED_ENABLED = 0x01 118 | CAPABILITIES_EXT_MESSAGE_ENABLED = 0x02 119 | CAPABILITIES_SCAN_MODE_ENABLED = 0x04 120 | CAPABILITIES_PROX_SEARCH_ENABLED = 0x10 121 | CAPABILITIES_EXT_ASSIGN_ENABLED = 0x20 122 | CAPABILITIES_FS_ANTFS_ENABLED = 0x40 123 | TIMEOUT_NEVER = 0xFF 124 | -------------------------------------------------------------------------------- /src/ant/core/driver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | import thread 27 | 28 | # USB1 driver uses a USB<->Serial bridge 29 | import serial 30 | # USB2 driver uses direct USB connection. Requires PyUSB 31 | import usb.core 32 | import usb.util 33 | import usb.control 34 | 35 | from ant.core.exceptions import DriverError 36 | 37 | from array import * 38 | 39 | 40 | class Driver(object): 41 | _lock = thread.allocate_lock() 42 | 43 | def __init__(self, device, log=None, debug=False): 44 | self.device = device 45 | self.debug = debug 46 | self.log = log 47 | self.is_open = False 48 | 49 | def isOpen(self): 50 | self._lock.acquire() 51 | io = self.is_open 52 | self._lock.release() 53 | return io 54 | 55 | def open(self): 56 | self._lock.acquire() 57 | 58 | try: 59 | if self.is_open: 60 | raise DriverError("Could not open device (already open).") 61 | 62 | self._open() 63 | self.is_open = True 64 | if self.log: 65 | self.log.logOpen() 66 | finally: 67 | self._lock.release() 68 | 69 | def close(self): 70 | self._lock.acquire() 71 | 72 | try: 73 | if not self.is_open: 74 | raise DriverError("Could not close device (not open).") 75 | 76 | self._close() 77 | self.is_open = False 78 | if self.log: 79 | self.log.logClose() 80 | finally: 81 | self._lock.release() 82 | 83 | def read(self, count): 84 | self._lock.acquire() 85 | 86 | try: 87 | if not self.is_open: 88 | raise DriverError("Could not read from device (not open).") 89 | if count <= 0: 90 | raise DriverError("Could not read from device (zero request).") 91 | 92 | data = self._read(count) 93 | if self.log: 94 | self.log.logRead(data) 95 | 96 | if self.debug: 97 | self._dump(data, 'READ') 98 | finally: 99 | self._lock.release() 100 | 101 | return data 102 | 103 | def write(self, data): 104 | self._lock.acquire() 105 | 106 | try: 107 | if not self.is_open: 108 | raise DriverError("Could not write to device (not open).") 109 | if len(data) <= 0: 110 | raise DriverError("Could not write to device (no data).") 111 | 112 | if self.debug: 113 | self._dump(data, 'WRITE') 114 | 115 | ret = self._write(data) 116 | if self.log: 117 | self.log.logWrite(data[0:ret]) 118 | finally: 119 | self._lock.release() 120 | 121 | return ret 122 | 123 | def _dump(self, data, title): 124 | if len(data) == 0: 125 | return 126 | 127 | print('========== [{0}] =========='.format(title)) 128 | 129 | length = 8 130 | line = 0 131 | while data: 132 | row = data[:length] 133 | data = data[length:] 134 | hex_data = ['%02X' % ord(byte) for byte in row] 135 | print '%04X' % line, ' '.join(hex_data) 136 | 137 | print '' 138 | 139 | def _open(self): 140 | raise DriverError("Not Implemented") 141 | 142 | def _close(self): 143 | raise DriverError("Not Implemented") 144 | 145 | def _read(self, count): 146 | raise DriverError("Not Implemented") 147 | 148 | def _write(self, data): 149 | raise DriverError("Not Implemented") 150 | 151 | 152 | class USB1Driver(Driver): 153 | def __init__(self, device, baud_rate=115200, log=None, debug=False): 154 | Driver.__init__(self, device, log, debug) 155 | self.baud = baud_rate 156 | 157 | def _open(self): 158 | try: 159 | dev = serial.Serial(self.device, self.baud) 160 | except serial.SerialException, e: 161 | raise DriverError(str(e)) 162 | 163 | if not dev.isOpen(): 164 | raise DriverError('Could not open device') 165 | 166 | self._serial = dev 167 | self._serial.timeout = 0.01 168 | 169 | def _close(self): 170 | self._serial.close() 171 | 172 | def _read(self, count): 173 | return self._serial.read(count) 174 | 175 | def _write(self, data): 176 | try: 177 | count = self._serial.write(data) 178 | self._serial.flush() 179 | except serial.SerialTimeoutException, e: 180 | raise DriverError(str(e)) 181 | 182 | return count 183 | 184 | 185 | class USB2Driver(Driver): 186 | def _open(self): 187 | # Most of this is straight from the PyUSB example documentation 188 | dev = usb.core.find(idVendor=0x0fcf, idProduct=0x1008) 189 | 190 | if dev is None: 191 | raise DriverError('Could not open device (not found)') 192 | 193 | # make sure the kernel driver is not active 194 | if dev.is_kernel_driver_active(0): 195 | try: 196 | dev.detach_kernel_driver(0) 197 | except usb.core.USBError as e: 198 | sys.exit("could not detach kernel driver: {}".format(e)) 199 | 200 | dev.set_configuration() 201 | cfg = dev.get_active_configuration() 202 | interface_number = cfg[(0,0)].bInterfaceNumber 203 | alternate_setting = usb.control.get_interface(dev, interface_number) 204 | intf = usb.util.find_descriptor( 205 | cfg, bInterfaceNumber = interface_number, 206 | bAlternateSetting = alternate_setting 207 | ) 208 | usb.util.claim_interface(dev, interface_number) 209 | ep_out = usb.util.find_descriptor( 210 | intf, 211 | custom_match = \ 212 | lambda e: \ 213 | usb.util.endpoint_direction(e.bEndpointAddress) == \ 214 | usb.util.ENDPOINT_OUT 215 | ) 216 | assert ep_out is not None 217 | ep_in = usb.util.find_descriptor( 218 | intf, 219 | custom_match = \ 220 | lambda e: \ 221 | usb.util.endpoint_direction(e.bEndpointAddress) == \ 222 | usb.util.ENDPOINT_IN 223 | ) 224 | assert ep_in is not None 225 | self._ep_out = ep_out 226 | self._ep_in = ep_in 227 | self._dev = dev 228 | self._int = interface_number 229 | 230 | def _close(self): 231 | usb.util.release_interface(self._dev, self._int) 232 | 233 | def _read(self, count): 234 | arr_inp = array('B') 235 | try: 236 | arr_inp = self._ep_in.read(count) 237 | except usb.core.USBError: 238 | # Timeout errors seem to occasionally be expected 239 | pass 240 | 241 | return arr_inp.tostring() 242 | 243 | def _write(self, data): 244 | count = self._ep_out.write(data) 245 | 246 | return count 247 | -------------------------------------------------------------------------------- /src/ant/core/event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | # 27 | # Beware s/he who enters: uncommented, non unit-tested, 28 | # don't-fix-it-if-it-ain't-broken kind of threaded code ahead. 29 | # 30 | 31 | MAX_ACK_QUEUE = 25 32 | MAX_MSG_QUEUE = 25 33 | 34 | import thread 35 | import time 36 | 37 | from ant.core.constants import * 38 | from ant.core.message import Message, ChannelEventMessage 39 | from ant.core.exceptions import MessageError 40 | 41 | 42 | def ProcessBuffer(buffer_): 43 | messages = [] 44 | 45 | while True: 46 | hf = Message() 47 | try: 48 | msg = hf.getHandler(buffer_) 49 | buffer_ = buffer_[len(msg.getPayload()) + 4:] 50 | messages.append(msg) 51 | except MessageError, e: 52 | if e.internal == "CHECKSUM": 53 | buffer_ = buffer_[ord(buffer_[1]) + 4:] 54 | else: 55 | break 56 | 57 | return (buffer_, messages,) 58 | 59 | 60 | def EventPump(evm): 61 | evm.pump_lock.acquire() 62 | evm.pump = True 63 | evm.pump_lock.release() 64 | 65 | go = True 66 | 67 | while go: 68 | buffer_ = '' 69 | evm.running_lock.acquire() 70 | if not evm.running: 71 | go = False 72 | evm.running_lock.release() 73 | 74 | buffer_ += evm.driver.read(20) 75 | if len(buffer_) == 0: 76 | continue 77 | buffer_, messages = ProcessBuffer(buffer_) 78 | 79 | evm.callbacks_lock.acquire() 80 | for message in messages: 81 | for callback in evm.callbacks: 82 | try: 83 | callback.process(message) 84 | except Exception, e: 85 | pass 86 | 87 | evm.callbacks_lock.release() 88 | 89 | time.sleep(0.002) 90 | 91 | evm.pump_lock.acquire() 92 | evm.pump = False 93 | evm.pump_lock.release() 94 | 95 | 96 | class EventCallback(object): 97 | def process(self, msg): 98 | pass 99 | 100 | 101 | class AckCallback(EventCallback): 102 | def __init__(self, evm): 103 | self.evm = evm 104 | 105 | def process(self, msg): 106 | if isinstance(msg, ChannelEventMessage): 107 | self.evm.ack_lock.acquire() 108 | self.evm.ack.append(msg) 109 | if len(self.evm.ack) > MAX_ACK_QUEUE: 110 | self.evm.ack = self.evm.ack[-MAX_ACK_QUEUE:] 111 | self.evm.ack_lock.release() 112 | 113 | 114 | class MsgCallback(EventCallback): 115 | def __init__(self, evm): 116 | self.evm = evm 117 | 118 | def process(self, msg): 119 | self.evm.msg_lock.acquire() 120 | self.evm.msg.append(msg) 121 | if len(self.evm.msg) > MAX_MSG_QUEUE: 122 | self.evm.msg = self.evm.msg[-MAX_MSG_QUEUE:] 123 | self.evm.msg_lock.release() 124 | 125 | 126 | class EventMachine(object): 127 | callbacks_lock = thread.allocate_lock() 128 | running_lock = thread.allocate_lock() 129 | pump_lock = thread.allocate_lock() 130 | ack_lock = thread.allocate_lock() 131 | msg_lock = thread.allocate_lock() 132 | 133 | def __init__(self, driver): 134 | self.driver = driver 135 | self.callbacks = [] 136 | self.running = False 137 | self.pump = False 138 | self.ack = [] 139 | self.msg = [] 140 | self.registerCallback(AckCallback(self)) 141 | self.registerCallback(MsgCallback(self)) 142 | 143 | def registerCallback(self, callback): 144 | self.callbacks_lock.acquire() 145 | if callback not in self.callbacks: 146 | self.callbacks.append(callback) 147 | self.callbacks_lock.release() 148 | 149 | def removeCallback(self, callback): 150 | self.callbacks_lock.acquire() 151 | if callback in self.callbacks: 152 | self.callbacks.remove(callback) 153 | self.callbacks_lock.release() 154 | 155 | def waitForAck(self, msg): 156 | while True: 157 | self.ack_lock.acquire() 158 | for emsg in self.ack: 159 | if msg.getType() != emsg.getMessageID(): 160 | continue 161 | self.ack.remove(emsg) 162 | self.ack_lock.release() 163 | return emsg.getMessageCode() 164 | self.ack_lock.release() 165 | time.sleep(0.002) 166 | 167 | def waitForMessage(self, class_): 168 | while True: 169 | self.msg_lock.acquire() 170 | for emsg in self.msg: 171 | if not isinstance(emsg, class_): 172 | continue 173 | self.msg.remove(emsg) 174 | self.msg_lock.release() 175 | return emsg 176 | self.msg_lock.release() 177 | time.sleep(0.002) 178 | 179 | def start(self, driver=None): 180 | self.running_lock.acquire() 181 | 182 | if self.running: 183 | self.running_lock.release() 184 | return 185 | self.running = True 186 | if driver is not None: 187 | self.driver = driver 188 | 189 | thread.start_new_thread(EventPump, (self,)) 190 | while True: 191 | self.pump_lock.acquire() 192 | if self.pump: 193 | self.pump_lock.release() 194 | break 195 | self.pump_lock.release() 196 | time.sleep(0.001) 197 | 198 | self.running_lock.release() 199 | 200 | def stop(self): 201 | self.running_lock.acquire() 202 | 203 | if not self.running: 204 | self.running_lock.release() 205 | return 206 | self.running = False 207 | self.running_lock.release() 208 | 209 | while True: 210 | self.pump_lock.acquire() 211 | if not self.pump: 212 | self.pump_lock.release() 213 | break 214 | self.pump_lock.release() 215 | time.sleep(0.001) 216 | -------------------------------------------------------------------------------- /src/ant/core/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | 27 | class ANTException(Exception): 28 | pass 29 | 30 | 31 | class DriverError(ANTException): 32 | pass 33 | 34 | 35 | class MessageError(ANTException): 36 | def __init__(self, msg, internal=''): 37 | Exception.__init__(self, msg) 38 | self.internal = internal 39 | 40 | 41 | class NodeError(ANTException): 42 | pass 43 | 44 | 45 | class ChannelError(ANTException): 46 | pass 47 | -------------------------------------------------------------------------------- /src/ant/core/log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | import time 27 | import datetime 28 | 29 | import msgpack 30 | 31 | EVENT_OPEN = 0x01 32 | EVENT_CLOSE = 0x02 33 | EVENT_READ = 0x03 34 | EVENT_WRITE = 0x04 35 | 36 | 37 | class LogReader(object): 38 | def __init__(self, filename): 39 | self.is_open = False 40 | self.open(filename) 41 | 42 | def __del__(self): 43 | if self.is_open: 44 | self.fd.close() 45 | 46 | def open(self, filename): 47 | if self.is_open == True: 48 | self.close() 49 | 50 | self.fd = open(filename, 'r') 51 | self.is_open = True 52 | self.unpacker = msgpack.Unpacker() 53 | 54 | # Here be dragons 55 | self.unpacker.feed(self.fd.read()) 56 | self.fd.close() 57 | 58 | header = self.unpacker.unpack() 59 | if len(header) != 2 or header[0] != 'ANT-LOG' or header[1] != 0x01: 60 | raise IOError('Could not open log file (unknown format).') 61 | 62 | def close(self): 63 | if self.is_open: 64 | self.fd.close() 65 | self.is_open = False 66 | 67 | def read(self): 68 | try: 69 | return self.unpacker.unpack() 70 | except StopIteration: 71 | return None 72 | 73 | 74 | class LogWriter(object): 75 | def __init__(self, filename=''): 76 | self.packer = msgpack.Packer() 77 | self.is_open = False 78 | self.open(filename) 79 | 80 | def __del__(self): 81 | if self.is_open: 82 | self.fd.close() 83 | 84 | def open(self, filename=''): 85 | if filename == '': 86 | filename = datetime.datetime.now().isoformat() + '.ant' 87 | self.filename = filename 88 | 89 | if self.is_open == True: 90 | self.close() 91 | 92 | self.fd = open(filename, 'w') 93 | self.is_open = True 94 | self.packer = msgpack.Packer() 95 | 96 | header = ['ANT-LOG', 0x01] # [MAGIC, VERSION] 97 | self.fd.write(self.packer.pack(header)) 98 | 99 | def close(self): 100 | if self.is_open: 101 | self.fd.close() 102 | self.is_open = False 103 | 104 | def _logEvent(self, event, data=None): 105 | ev = [event, int(time.time()), data] 106 | 107 | if data is None: 108 | ev = ev[0:-1] 109 | elif len(data) == 0: 110 | return 111 | 112 | self.fd.write(self.packer.pack(ev)) 113 | 114 | def logOpen(self): 115 | self._logEvent(EVENT_OPEN) 116 | 117 | def logClose(self): 118 | self._logEvent(EVENT_CLOSE) 119 | 120 | def logRead(self, data): 121 | self._logEvent(EVENT_READ, data) 122 | 123 | def logWrite(self, data): 124 | self._logEvent(EVENT_WRITE, data) 125 | -------------------------------------------------------------------------------- /src/ant/core/message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | import struct 27 | 28 | from ant.core.exceptions import MessageError 29 | from ant.core.constants import * 30 | 31 | 32 | class Message(object): 33 | def __init__(self, type_=0x00, payload=''): 34 | self.setType(type_) 35 | self.setPayload(payload) 36 | 37 | def getPayload(self): 38 | return ''.join(self.payload) 39 | 40 | def setPayload(self, payload): 41 | if len(payload) > 9: 42 | raise MessageError( 43 | 'Could not set payload (payload too long).') 44 | 45 | self.payload = [] 46 | for byte in payload: 47 | self.payload += byte 48 | 49 | def getType(self): 50 | return self.type_ 51 | 52 | def setType(self, type_): 53 | if (type_ > 0xFF) or (type_ < 0x00): 54 | raise MessageError('Could not set type (type out of range).') 55 | 56 | self.type_ = type_ 57 | 58 | def getChecksum(self): 59 | data = chr(len(self.getPayload())) 60 | data += chr(self.getType()) 61 | data += self.getPayload() 62 | 63 | checksum = MESSAGE_TX_SYNC 64 | for byte in data: 65 | checksum = (checksum ^ ord(byte)) % 0xFF 66 | 67 | return checksum 68 | 69 | def getSize(self): 70 | return len(self.getPayload()) + 4 71 | 72 | def encode(self): 73 | raw = struct.pack('BBB', 74 | MESSAGE_TX_SYNC, 75 | len(self.getPayload()), 76 | self.getType()) 77 | raw += self.getPayload() 78 | raw += chr(self.getChecksum()) 79 | 80 | return raw 81 | 82 | def decode(self, raw): 83 | if len(raw) < 5: 84 | raise MessageError('Could not decode (message is incomplete).') 85 | 86 | sync, length, type_ = struct.unpack('BBB', raw[:3]) 87 | 88 | if sync != MESSAGE_TX_SYNC: 89 | raise MessageError('Could not decode (expected TX sync).') 90 | if length > 9: 91 | raise MessageError('Could not decode (payload too long).') 92 | if len(raw) < (length + 4): 93 | raise MessageError('Could not decode (message is incomplete).') 94 | 95 | self.setType(type_) 96 | self.setPayload(raw[3:length + 3]) 97 | 98 | if self.getChecksum() != ord(raw[length + 3]): 99 | raise MessageError('Could not decode (bad checksum).', 100 | internal='CHECKSUM') 101 | 102 | return self.getSize() 103 | 104 | def getHandler(self, raw=None): 105 | if raw: 106 | self.decode(raw) 107 | 108 | msg = None 109 | if self.type_ == MESSAGE_CHANNEL_UNASSIGN: 110 | msg = ChannelUnassignMessage() 111 | elif self.type_ == MESSAGE_CHANNEL_ASSIGN: 112 | msg = ChannelAssignMessage() 113 | elif self.type_ == MESSAGE_CHANNEL_ID: 114 | msg = ChannelIDMessage() 115 | elif self.type_ == MESSAGE_CHANNEL_PERIOD: 116 | msg = ChannelPeriodMessage() 117 | elif self.type_ == MESSAGE_CHANNEL_SEARCH_TIMEOUT: 118 | msg = ChannelSearchTimeoutMessage() 119 | elif self.type_ == MESSAGE_CHANNEL_FREQUENCY: 120 | msg = ChannelFrequencyMessage() 121 | elif self.type_ == MESSAGE_CHANNEL_TX_POWER: 122 | msg = ChannelTXPowerMessage() 123 | elif self.type_ == MESSAGE_NETWORK_KEY: 124 | msg = NetworkKeyMessage() 125 | elif self.type_ == MESSAGE_TX_POWER: 126 | msg = TXPowerMessage() 127 | elif self.type_ == MESSAGE_SYSTEM_RESET: 128 | msg = SystemResetMessage() 129 | elif self.type_ == MESSAGE_CHANNEL_OPEN: 130 | msg = ChannelOpenMessage() 131 | elif self.type_ == MESSAGE_CHANNEL_CLOSE: 132 | msg = ChannelCloseMessage() 133 | elif self.type_ == MESSAGE_CHANNEL_REQUEST: 134 | msg = ChannelRequestMessage() 135 | elif self.type_ == MESSAGE_CHANNEL_BROADCAST_DATA: 136 | msg = ChannelBroadcastDataMessage() 137 | elif self.type_ == MESSAGE_CHANNEL_ACKNOWLEDGED_DATA: 138 | msg = ChannelAcknowledgedDataMessage() 139 | elif self.type_ == MESSAGE_CHANNEL_BURST_DATA: 140 | msg = ChannelBurstDataMessage() 141 | elif self.type_ == MESSAGE_CHANNEL_EVENT: 142 | msg = ChannelEventMessage() 143 | elif self.type_ == MESSAGE_CHANNEL_STATUS: 144 | msg = ChannelStatusMessage() 145 | elif self.type_ == MESSAGE_VERSION: 146 | msg = VersionMessage() 147 | elif self.type_ == MESSAGE_CAPABILITIES: 148 | msg = CapabilitiesMessage() 149 | elif self.type_ == MESSAGE_SERIAL_NUMBER: 150 | msg = SerialNumberMessage() 151 | else: 152 | raise MessageError('Could not find message handler ' \ 153 | '(unknown message type).') 154 | 155 | msg.setPayload(self.getPayload()) 156 | return msg 157 | 158 | 159 | class ChannelMessage(Message): 160 | def __init__(self, type_, payload='', number=0x00): 161 | Message.__init__(self, type_, '\x00' + payload) 162 | self.setChannelNumber(number) 163 | 164 | def getChannelNumber(self): 165 | return ord(self.payload[0]) 166 | 167 | def setChannelNumber(self, number): 168 | if (number > 0xFF) or (number < 0x00): 169 | raise MessageError('Could not set channel number ' \ 170 | '(out of range).') 171 | 172 | self.payload[0] = chr(number) 173 | 174 | 175 | # Config messages 176 | class ChannelUnassignMessage(ChannelMessage): 177 | def __init__(self, number=0x00): 178 | ChannelMessage.__init__(self, type_=MESSAGE_CHANNEL_UNASSIGN, 179 | number=number) 180 | 181 | 182 | class ChannelAssignMessage(ChannelMessage): 183 | def __init__(self, number=0x00, type_=0x00, network=0x00): 184 | payload = struct.pack('BB', type_, network) 185 | ChannelMessage.__init__(self, type_=MESSAGE_CHANNEL_ASSIGN, 186 | payload=payload, number=number) 187 | 188 | def getChannelType(self): 189 | return ord(self.payload[1]) 190 | 191 | def setChannelType(self, type_): 192 | self.payload[1] = chr(type_) 193 | 194 | def getNetworkNumber(self): 195 | return ord(self.payload[2]) 196 | 197 | def setNetworkNumber(self, number): 198 | self.payload[2] = chr(number) 199 | 200 | 201 | class ChannelIDMessage(ChannelMessage): 202 | def __init__(self, number=0x00, device_number=0x0000, device_type=0x00, 203 | trans_type=0x00): 204 | ChannelMessage.__init__(self, type_=MESSAGE_CHANNEL_ID, 205 | payload='\x00' * 4, number=number) 206 | self.setDeviceNumber(device_number) 207 | self.setDeviceType(device_type) 208 | self.setTransmissionType(trans_type) 209 | 210 | def getDeviceNumber(self): 211 | return struct.unpack(' 0xFF) or (message_id < 0x00): 340 | raise MessageError('Could not set message ID ' \ 341 | '(out of range).') 342 | 343 | self.payload[1] = chr(message_id) 344 | 345 | 346 | class RequestMessage(ChannelRequestMessage): 347 | pass 348 | 349 | 350 | # Data messages 351 | class ChannelBroadcastDataMessage(ChannelMessage): 352 | def __init__(self, number=0x00, data='\x00' * 7): 353 | ChannelMessage.__init__(self, type_=MESSAGE_CHANNEL_BROADCAST_DATA, 354 | payload=data, number=number) 355 | 356 | 357 | class ChannelAcknowledgedDataMessage(ChannelMessage): 358 | def __init__(self, number=0x00, data='\x00' * 7): 359 | ChannelMessage.__init__(self, type_=MESSAGE_CHANNEL_ACKNOWLEDGED_DATA, 360 | payload=data, number=number) 361 | 362 | 363 | class ChannelBurstDataMessage(ChannelMessage): 364 | def __init__(self, number=0x00, data='\x00' * 7): 365 | ChannelMessage.__init__(self, type_=MESSAGE_CHANNEL_BURST_DATA, 366 | payload=data, number=number) 367 | 368 | 369 | # Channel event messages 370 | class ChannelEventMessage(ChannelMessage): 371 | def __init__(self, number=0x00, message_id=0x00, message_code=0x00): 372 | ChannelMessage.__init__(self, type_=MESSAGE_CHANNEL_EVENT, 373 | number=number, payload='\x00\x00') 374 | self.setMessageID(message_id) 375 | self.setMessageCode(message_code) 376 | 377 | def getMessageID(self): 378 | return ord(self.payload[1]) 379 | 380 | def setMessageID(self, message_id): 381 | if (message_id > 0xFF) or (message_id < 0x00): 382 | raise MessageError('Could not set message ID ' \ 383 | '(out of range).') 384 | 385 | self.payload[1] = chr(message_id) 386 | 387 | def getMessageCode(self): 388 | return ord(self.payload[2]) 389 | 390 | def setMessageCode(self, message_code): 391 | if (message_code > 0xFF) or (message_code < 0x00): 392 | raise MessageError('Could not set message code ' \ 393 | '(out of range).') 394 | 395 | self.payload[2] = chr(message_code) 396 | 397 | 398 | # Requested response messages 399 | class ChannelStatusMessage(ChannelMessage): 400 | def __init__(self, number=0x00, status=0x00): 401 | ChannelMessage.__init__(self, type_=MESSAGE_CHANNEL_STATUS, 402 | payload='\x00', number=number) 403 | self.setStatus(status) 404 | 405 | def getStatus(self): 406 | return ord(self.payload[1]) 407 | 408 | def setStatus(self, status): 409 | if (status > 0xFF) or (status < 0x00): 410 | raise MessageError('Could not set channel status ' \ 411 | '(out of range).') 412 | 413 | self.payload[1] = chr(status) 414 | 415 | #class ChannelIDMessage(ChannelMessage): 416 | 417 | 418 | class VersionMessage(Message): 419 | def __init__(self, version='\x00' * 9): 420 | Message.__init__(self, type_=MESSAGE_VERSION, payload='\x00' * 9) 421 | self.setVersion(version) 422 | 423 | def getVersion(self): 424 | return self.getPayload() 425 | 426 | def setVersion(self, version): 427 | if (len(version) != 9): 428 | raise MessageError('Could not set ANT version ' \ 429 | '(expected 9 bytes).') 430 | 431 | self.setPayload(version) 432 | 433 | 434 | class CapabilitiesMessage(Message): 435 | def __init__(self, max_channels=0x00, max_nets=0x00, std_opts=0x00, 436 | adv_opts=0x00, adv_opts2=0x00): 437 | Message.__init__(self, type_=MESSAGE_CAPABILITIES, payload='\x00' * 4) 438 | self.setMaxChannels(max_channels) 439 | self.setMaxNetworks(max_nets) 440 | self.setStdOptions(std_opts) 441 | self.setAdvOptions(adv_opts) 442 | if adv_opts2 is not None: 443 | self.setAdvOptions2(adv_opts2) 444 | 445 | def getMaxChannels(self): 446 | return ord(self.payload[0]) 447 | 448 | def getMaxNetworks(self): 449 | return ord(self.payload[1]) 450 | 451 | def getStdOptions(self): 452 | return ord(self.payload[2]) 453 | 454 | def getAdvOptions(self): 455 | return ord(self.payload[3]) 456 | 457 | def getAdvOptions2(self): 458 | return ord(self.payload[4]) if len(self.payload) == 5 else 0x00 459 | 460 | def setMaxChannels(self, num): 461 | if (num > 0xFF) or (num < 0x00): 462 | raise MessageError('Could not set max channels ' \ 463 | '(out of range).') 464 | 465 | self.payload[0] = chr(num) 466 | 467 | def setMaxNetworks(self, num): 468 | if (num > 0xFF) or (num < 0x00): 469 | raise MessageError('Could not set max networks ' \ 470 | '(out of range).') 471 | 472 | self.payload[1] = chr(num) 473 | 474 | def setStdOptions(self, num): 475 | if (num > 0xFF) or (num < 0x00): 476 | raise MessageError('Could not set std options ' \ 477 | '(out of range).') 478 | 479 | self.payload[2] = chr(num) 480 | 481 | def setAdvOptions(self, num): 482 | if (num > 0xFF) or (num < 0x00): 483 | raise MessageError('Could not set adv options ' \ 484 | '(out of range).') 485 | 486 | self.payload[3] = chr(num) 487 | 488 | def setAdvOptions2(self, num): 489 | if (num > 0xFF) or (num < 0x00): 490 | raise MessageError('Could not set adv options 2 ' \ 491 | '(out of range).') 492 | 493 | if len(self.payload) == 4: 494 | self.payload.append('\x00') 495 | self.payload[4] = chr(num) 496 | 497 | 498 | class SerialNumberMessage(Message): 499 | def __init__(self, serial='\x00' * 4): 500 | Message.__init__(self, type_=MESSAGE_SERIAL_NUMBER) 501 | self.setSerialNumber(serial) 502 | 503 | def getSerialNumber(self): 504 | return self.getPayload() 505 | 506 | def setSerialNumber(self, serial): 507 | if (len(serial) != 4): 508 | raise MessageError('Could not set serial number ' \ 509 | '(expected 4 bytes).') 510 | 511 | self.setPayload(serial) 512 | -------------------------------------------------------------------------------- /src/ant/core/node.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | import time 27 | import thread 28 | import uuid 29 | 30 | from ant.core.constants import * 31 | from ant.core.exceptions import * 32 | from ant.core import message 33 | from ant.core import event 34 | 35 | 36 | class NetworkKey(object): 37 | def __init__(self, name=None, key='\x00' * 8): 38 | self.key = key 39 | if name: 40 | self.name = name 41 | else: 42 | self.name = str(uuid.uuid4()) 43 | self.number = 0 44 | 45 | 46 | class Channel(event.EventCallback): 47 | cb_lock = thread.allocate_lock() 48 | 49 | def __init__(self, node): 50 | self.node = node 51 | self.is_free = True 52 | self.name = str(uuid.uuid4()) 53 | self.number = 0 54 | self.cb = [] 55 | self.node.evm.registerCallback(self) 56 | 57 | def __del__(self): 58 | self.node.evm.removeCallback(self) 59 | 60 | def assign(self, net_key, ch_type): 61 | msg = message.ChannelAssignMessage(number=self.number) 62 | msg.setNetworkNumber(self.node.getNetworkKey(net_key).number) 63 | msg.setChannelType(ch_type) 64 | self.node.driver.write(msg.encode()) 65 | if self.node.evm.waitForAck(msg) != RESPONSE_NO_ERROR: 66 | raise ChannelError('Could not assign channel.') 67 | self.is_free = False 68 | 69 | def setID(self, dev_type, dev_num, trans_type): 70 | msg = message.ChannelIDMessage(number=self.number) 71 | msg.setDeviceType(dev_type) 72 | msg.setDeviceNumber(dev_num) 73 | msg.setTransmissionType(trans_type) 74 | self.node.driver.write(msg.encode()) 75 | if self.node.evm.waitForAck(msg) != RESPONSE_NO_ERROR: 76 | raise ChannelError('Could not set channel ID.') 77 | 78 | def setSearchTimeout(self, timeout): 79 | msg = message.ChannelSearchTimeoutMessage(number=self.number) 80 | msg.setTimeout(timeout) 81 | self.node.driver.write(msg.encode()) 82 | if self.node.evm.waitForAck(msg) != RESPONSE_NO_ERROR: 83 | raise ChannelError('Could not set channel search timeout.') 84 | 85 | def setPeriod(self, counts): 86 | msg = message.ChannelPeriodMessage(number=self.number) 87 | msg.setChannelPeriod(counts) 88 | self.node.driver.write(msg.encode()) 89 | if self.node.evm.waitForAck(msg) != RESPONSE_NO_ERROR: 90 | raise ChannelError('Could not set channel period.') 91 | 92 | def setFrequency(self, frequency): 93 | msg = message.ChannelFrequencyMessage(number=self.number) 94 | msg.setFrequency(frequency) 95 | self.node.driver.write(msg.encode()) 96 | if self.node.evm.waitForAck(msg) != RESPONSE_NO_ERROR: 97 | raise ChannelError('Could not set channel frequency.') 98 | 99 | def open(self): 100 | msg = message.ChannelOpenMessage(number=self.number) 101 | self.node.driver.write(msg.encode()) 102 | if self.node.evm.waitForAck(msg) != RESPONSE_NO_ERROR: 103 | raise ChannelError('Could not open channel.') 104 | 105 | def close(self): 106 | msg = message.ChannelCloseMessage(number=self.number) 107 | self.node.driver.write(msg.encode()) 108 | if self.node.evm.waitForAck(msg) != RESPONSE_NO_ERROR: 109 | raise ChannelError('Could not close channel.') 110 | 111 | while True: 112 | msg = self.node.evm.waitForMessage(message.ChannelEventMessage) 113 | if msg.getMessageCode() == EVENT_CHANNEL_CLOSED: 114 | break 115 | 116 | def unassign(self): 117 | msg = message.ChannelUnassignMessage(number=self.number) 118 | self.node.driver.write(msg.encode()) 119 | if self.node.evm.waitForAck(msg) != RESPONSE_NO_ERROR: 120 | raise ChannelError('Could not unassign channel.') 121 | self.is_free = True 122 | 123 | def registerCallback(self, callback): 124 | self.cb_lock.acquire() 125 | if callback not in self.cb: 126 | self.cb.append(callback) 127 | self.cb_lock.release() 128 | 129 | def process(self, msg): 130 | self.cb_lock.acquire() 131 | if isinstance(msg, message.ChannelMessage) and \ 132 | msg.getChannelNumber() == self.number: 133 | for callback in self.cb: 134 | try: 135 | callback.process(msg) 136 | except: 137 | pass # Who cares? 138 | self.cb_lock.release() 139 | 140 | 141 | class Node(event.EventCallback): 142 | node_lock = thread.allocate_lock() 143 | 144 | def __init__(self, driver): 145 | self.driver = driver 146 | self.evm = event.EventMachine(self.driver) 147 | self.evm.registerCallback(self) 148 | self.networks = [] 149 | self.channels = [] 150 | self.running = False 151 | self.options = [0x00, 0x00, 0x00] 152 | 153 | def start(self): 154 | if self.running: 155 | raise NodeError('Could not start ANT node (already started).') 156 | 157 | if not self.driver.isOpen(): 158 | self.driver.open() 159 | 160 | self.reset() 161 | self.evm.start() 162 | self.running = True 163 | self.init() 164 | 165 | def stop(self, reset=True): 166 | if not self.running: 167 | raise NodeError('Could not stop ANT node (not started).') 168 | 169 | if reset: 170 | self.reset() 171 | self.evm.stop() 172 | self.running = False 173 | self.driver.close() 174 | 175 | def reset(self): 176 | msg = message.SystemResetMessage() 177 | self.driver.write(msg.encode()) 178 | time.sleep(1) 179 | 180 | def init(self): 181 | if not self.running: 182 | raise NodeError('Could not reset ANT node (not started).') 183 | 184 | msg = message.ChannelRequestMessage() 185 | msg.setMessageID(MESSAGE_CAPABILITIES) 186 | self.driver.write(msg.encode()) 187 | 188 | caps = self.evm.waitForMessage(message.CapabilitiesMessage) 189 | 190 | self.networks = [] 191 | for i in range(0, caps.getMaxNetworks()): 192 | self.networks.append(NetworkKey()) 193 | self.setNetworkKey(i) 194 | self.channels = [] 195 | for i in range(0, caps.getMaxChannels()): 196 | self.channels.append(Channel(self)) 197 | self.channels[i].number = i 198 | self.options = (caps.getStdOptions(), 199 | caps.getAdvOptions(), 200 | caps.getAdvOptions2(),) 201 | 202 | def getCapabilities(self): 203 | return (len(self.channels), 204 | len(self.networks), 205 | self.options,) 206 | 207 | def setNetworkKey(self, number, key=None): 208 | if key: 209 | self.networks[number] = key 210 | 211 | msg = message.NetworkKeyMessage() 212 | msg.setNumber(number) 213 | msg.setKey(self.networks[number].key) 214 | self.driver.write(msg.encode()) 215 | self.evm.waitForAck(msg) 216 | self.networks[number].number = number 217 | 218 | def getNetworkKey(self, name): 219 | for netkey in self.networks: 220 | if netkey.name == name: 221 | return netkey 222 | raise NodeError('Could not find network key with the supplied name.') 223 | 224 | def getFreeChannel(self): 225 | for channel in self.channels: 226 | if channel.is_free: 227 | return channel 228 | raise NodeError('Could not find free channel.') 229 | 230 | def registerEventListener(self, callback): 231 | self.evm.registerCallback(callback) 232 | 233 | def process(self, msg): 234 | pass 235 | -------------------------------------------------------------------------------- /src/ant/core/tests/driver_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | import unittest 27 | 28 | from ant.core.driver import * 29 | 30 | 31 | class DummyDriver(Driver): 32 | def _open(self): 33 | pass 34 | 35 | def _close(self): 36 | pass 37 | 38 | def _read(self, count): 39 | return '\x00' * count 40 | 41 | def _write(self, data): 42 | return len(data) 43 | 44 | 45 | class DriverTest(unittest.TestCase): 46 | def setUp(self): 47 | self.driver = DummyDriver('superdrive') 48 | 49 | def tearDown(self): 50 | pass 51 | 52 | def test_isOpen(self): 53 | self.assertFalse(self.driver.isOpen()) 54 | self.driver.open() 55 | self.assertTrue(self.driver.isOpen()) 56 | self.driver.close() 57 | self.assertFalse(self.driver.isOpen()) 58 | 59 | def test_open(self): 60 | self.driver.open() 61 | self.assertRaises(DriverError, self.driver.open) 62 | self.driver.close() 63 | 64 | def test_close(self): 65 | pass # Nothing to test for 66 | 67 | def test_read(self): 68 | self.assertFalse(self.driver.isOpen()) 69 | self.assertRaises(DriverError, self.driver.read, 1) 70 | self.driver.open() 71 | self.assertEqual(len(self.driver.read(5)), 5) 72 | self.assertRaises(DriverError, self.driver.read, -1) 73 | self.assertRaises(DriverError, self.driver.read, 0) 74 | self.driver.close() 75 | 76 | def test_write(self): 77 | self.assertRaises(DriverError, self.driver.write, '\xFF') 78 | self.driver.open() 79 | self.assertRaises(DriverError, self.driver.write, '') 80 | self.assertEquals(self.driver.write('\xFF' * 10), 10) 81 | self.driver.close() 82 | 83 | 84 | # How do you even test this without hardware? 85 | class USB1DriverTest(unittest.TestCase): 86 | def _open(self): 87 | pass 88 | 89 | def _close(self): 90 | pass 91 | 92 | def _read(self): 93 | pass 94 | 95 | def _write(self): 96 | pass 97 | -------------------------------------------------------------------------------- /src/ant/core/tests/event_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | import unittest 27 | 28 | from ant.core.event import * 29 | 30 | #TODO: How exactly do you properly test threaded code? 31 | -------------------------------------------------------------------------------- /src/ant/core/tests/log_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | LOG_LOCATION = '/tmp/python-ant.logtest.ant' 27 | 28 | import unittest 29 | 30 | from ant.core.log import * 31 | 32 | 33 | class LogReaderTest(unittest.TestCase): 34 | def setUp(self): 35 | lw = LogWriter(LOG_LOCATION) 36 | lw.logOpen() 37 | lw.logRead('\x01') 38 | lw.logWrite('\x00') 39 | lw.logRead('TEST') 40 | lw.logClose() 41 | lw.close() 42 | 43 | self.log = LogReader(LOG_LOCATION) 44 | 45 | def test_open_close(self): 46 | self.assertTrue(self.log.is_open) 47 | self.log.close() 48 | self.assertFalse(self.log.is_open) 49 | self.log.open(LOG_LOCATION) 50 | self.assertTrue(self.log.is_open) 51 | 52 | def test_read(self): 53 | t1 = self.log.read() 54 | t2 = self.log.read() 55 | t3 = self.log.read() 56 | t4 = self.log.read() 57 | t5 = self.log.read() 58 | 59 | self.assertEquals(self.log.read(), None) 60 | 61 | self.assertEquals(t1[0], EVENT_OPEN) 62 | self.assertTrue(isinstance(t1[1], int)) 63 | self.assertEquals(len(t1), 2) 64 | 65 | self.assertEquals(t2[0], EVENT_READ) 66 | self.assertTrue(isinstance(t1[1], int)) 67 | self.assertEquals(len(t2), 3) 68 | self.assertEquals(t2[2], '\x01') 69 | 70 | self.assertEquals(t3[0], EVENT_WRITE) 71 | self.assertTrue(isinstance(t1[1], int)) 72 | self.assertEquals(len(t3), 3) 73 | self.assertEquals(t3[2], '\x00') 74 | 75 | self.assertEquals(t4[0], EVENT_READ) 76 | self.assertEquals(t4[2], 'TEST') 77 | 78 | self.assertEquals(t5[0], EVENT_CLOSE) 79 | self.assertTrue(isinstance(t1[1], int)) 80 | self.assertEquals(len(t5), 2) 81 | 82 | 83 | class LogWriterTest(unittest.TestCase): 84 | def setUp(self): 85 | self.log = LogWriter(LOG_LOCATION) 86 | 87 | def test_open_close(self): 88 | self.assertTrue(self.log.is_open) 89 | self.log.close() 90 | self.assertFalse(self.log.is_open) 91 | self.log.open(LOG_LOCATION) 92 | self.assertTrue(self.log.is_open) 93 | 94 | def test_log(self): 95 | # Redundant, any error in log* methods will cause the LogReader test 96 | # suite to fail. 97 | pass 98 | -------------------------------------------------------------------------------- /src/ant/core/tests/message_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | import unittest 27 | 28 | from ant.core.message import * 29 | 30 | 31 | class MessageTest(unittest.TestCase): 32 | def setUp(self): 33 | self.message = Message() 34 | 35 | def test_get_setPayload(self): 36 | self.assertRaises(MessageError, self.message.setPayload, 37 | '\xFF' * 15) 38 | self.message.setPayload('\x11' * 5) 39 | self.assertEquals(self.message.getPayload(), '\x11' * 5) 40 | 41 | def test_get_setType(self): 42 | self.assertRaises(MessageError, self.message.setType, -1) 43 | self.assertRaises(MessageError, self.message.setType, 300) 44 | self.message.setType(0x23) 45 | self.assertEquals(self.message.getType(), 0x23) 46 | 47 | def test_getChecksum(self): 48 | self.message = Message(type_=MESSAGE_SYSTEM_RESET, payload='\x00') 49 | self.assertEquals(self.message.getChecksum(), 0xEF) 50 | self.message = Message(type_=MESSAGE_CHANNEL_ASSIGN, 51 | payload='\x00' * 3) 52 | self.assertEquals(self.message.getChecksum(), 0xE5) 53 | 54 | def test_getSize(self): 55 | self.message.setPayload('\x11' * 7) 56 | self.assertEquals(self.message.getSize(), 11) 57 | 58 | def test_encode(self): 59 | self.message = Message(type_=MESSAGE_CHANNEL_ASSIGN, 60 | payload='\x00' * 3) 61 | self.assertEqual(self.message.encode(), 62 | '\xA4\x03\x42\x00\x00\x00\xE5') 63 | 64 | def test_decode(self): 65 | self.assertRaises(MessageError, self.message.decode, 66 | '\xA5\x03\x42\x00\x00\x00\xE5') 67 | self.assertRaises(MessageError, self.message.decode, 68 | '\xA4\x14\x42' + ('\x00' * 20) + '\xE5') 69 | self.assertRaises(MessageError, self.message.decode, 70 | '\xA4\x03\x42\x01\x02\xF3\xE5') 71 | self.assertEqual(self.message.decode('\xA4\x03\x42\x00\x00\x00\xE5'), 72 | 7) 73 | self.assertEqual(self.message.getType(), MESSAGE_CHANNEL_ASSIGN) 74 | self.assertEqual(self.message.getPayload(), '\x00' * 3) 75 | self.assertEqual(self.message.getChecksum(), 0xE5) 76 | 77 | def test_getHandler(self): 78 | handler = self.message.getHandler('\xA4\x03\x42\x00\x00\x00\xE5') 79 | self.assertTrue(isinstance(handler, ChannelAssignMessage)) 80 | self.assertRaises(MessageError, self.message.getHandler, 81 | '\xA4\x03\xFF\x00\x00\x00\xE5') 82 | self.assertRaises(MessageError, self.message.getHandler, 83 | '\xA4\x03\x42') 84 | self.assertRaises(MessageError, self.message.getHandler, 85 | '\xA4\x05\x42\x00\x00\x00\x00') 86 | 87 | 88 | class ChannelMessageTest(unittest.TestCase): 89 | def setUp(self): 90 | self.message = ChannelMessage(type_=MESSAGE_SYSTEM_RESET) 91 | 92 | def test_get_setChannelNumber(self): 93 | self.assertEquals(self.message.getChannelNumber(), 0) 94 | self.message.setChannelNumber(3) 95 | self.assertEquals(self.message.getChannelNumber(), 3) 96 | 97 | 98 | class ChannelUnassignMessageTest(unittest.TestCase): 99 | # No currently defined methods need testing 100 | pass 101 | 102 | 103 | class ChannelAssignMessageTest(unittest.TestCase): 104 | def setUp(self): 105 | self.message = ChannelAssignMessage() 106 | 107 | def test_get_setChannelType(self): 108 | self.message.setChannelType(0x10) 109 | self.assertEquals(self.message.getChannelType(), 0x10) 110 | 111 | def test_get_setNetworkNumber(self): 112 | self.message.setNetworkNumber(0x11) 113 | self.assertEquals(self.message.getNetworkNumber(), 0x11) 114 | 115 | def test_payload(self): 116 | self.message.setChannelNumber(0x01) 117 | self.message.setChannelType(0x02) 118 | self.message.setNetworkNumber(0x03) 119 | self.assertEquals(self.message.getPayload(), '\x01\x02\x03') 120 | 121 | 122 | class ChannelIDMessageTest(unittest.TestCase): 123 | def setUp(self): 124 | self.message = ChannelIDMessage() 125 | 126 | def test_get_setDeviceNumber(self): 127 | self.message.setDeviceNumber(0x10FA) 128 | self.assertEquals(self.message.getDeviceNumber(), 0x10FA) 129 | 130 | def test_get_setDeviceType(self): 131 | self.message.setDeviceType(0x10) 132 | self.assertEquals(self.message.getDeviceType(), 0x10) 133 | 134 | def test_get_setTransmissionType(self): 135 | self.message.setTransmissionType(0x11) 136 | self.assertEquals(self.message.getTransmissionType(), 0x11) 137 | 138 | def test_payload(self): 139 | self.message.setChannelNumber(0x01) 140 | self.message.setDeviceNumber(0x0302) 141 | self.message.setDeviceType(0x04) 142 | self.message.setTransmissionType(0x05) 143 | self.assertEquals(self.message.getPayload(), '\x01\x02\x03\x04\x05') 144 | 145 | 146 | class ChannelPeriodMessageTest(unittest.TestCase): 147 | def setUp(self): 148 | self.message = ChannelPeriodMessage() 149 | 150 | def test_get_setChannelPeriod(self): 151 | self.message.setChannelPeriod(0x10FA) 152 | self.assertEquals(self.message.getChannelPeriod(), 0x10FA) 153 | 154 | def test_payload(self): 155 | self.message.setChannelNumber(0x01) 156 | self.message.setChannelPeriod(0x0302) 157 | self.assertEquals(self.message.getPayload(), '\x01\x02\x03') 158 | 159 | 160 | class ChannelSearchTimeoutMessageTest(unittest.TestCase): 161 | def setUp(self): 162 | self.message = ChannelSearchTimeoutMessage() 163 | 164 | def test_get_setTimeout(self): 165 | self.message.setTimeout(0x10) 166 | self.assertEquals(self.message.getTimeout(), 0x10) 167 | 168 | def test_payload(self): 169 | self.message.setChannelNumber(0x01) 170 | self.message.setTimeout(0x02) 171 | self.assertEquals(self.message.getPayload(), '\x01\x02') 172 | 173 | 174 | class ChannelFrequencyMessageTest(unittest.TestCase): 175 | def setUp(self): 176 | self.message = ChannelFrequencyMessage() 177 | 178 | def test_get_setFrequency(self): 179 | self.message.setFrequency(22) 180 | self.assertEquals(self.message.getFrequency(), 22) 181 | 182 | def test_payload(self): 183 | self.message.setChannelNumber(0x01) 184 | self.message.setFrequency(0x02) 185 | self.assertEquals(self.message.getPayload(), '\x01\x02') 186 | 187 | 188 | class ChannelTXPowerMessageTest(unittest.TestCase): 189 | def setUp(self): 190 | self.message = ChannelTXPowerMessage() 191 | 192 | def test_get_setPower(self): 193 | self.message.setPower(0xFA) 194 | self.assertEquals(self.message.getPower(), 0xFA) 195 | 196 | def test_payload(self): 197 | self.message.setChannelNumber(0x01) 198 | self.message.setPower(0x02) 199 | self.assertEquals(self.message.getPayload(), '\x01\x02') 200 | 201 | 202 | class NetworkKeyMessageTest(unittest.TestCase): 203 | def setUp(self): 204 | self.message = NetworkKeyMessage() 205 | 206 | def test_get_setNumber(self): 207 | self.message.setNumber(0xFA) 208 | self.assertEquals(self.message.getNumber(), 0xFA) 209 | 210 | def test_get_setKey(self): 211 | self.message.setKey('\xFD' * 8) 212 | self.assertEquals(self.message.getKey(), '\xFD' * 8) 213 | 214 | def test_payload(self): 215 | self.message.setNumber(0x01) 216 | self.message.setKey('\x02\x03\x04\x05\x06\x07\x08\x09') 217 | self.assertEquals(self.message.getPayload(), 218 | '\x01\x02\x03\x04\x05\x06\x07\x08\x09') 219 | 220 | 221 | class TXPowerMessageTest(unittest.TestCase): 222 | def setUp(self): 223 | self.message = TXPowerMessage() 224 | 225 | def test_get_setPower(self): 226 | self.message.setPower(0xFA) 227 | self.assertEquals(self.message.getPower(), 0xFA) 228 | 229 | def test_payload(self): 230 | self.message.setPower(0x01) 231 | self.assertEquals(self.message.getPayload(), '\x00\x01') 232 | 233 | 234 | class SystemResetMessageTest(unittest.TestCase): 235 | # No currently defined methods need testing 236 | pass 237 | 238 | 239 | class ChannelOpenMessageTest(unittest.TestCase): 240 | # No currently defined methods need testing 241 | pass 242 | 243 | 244 | class ChannelCloseMessageTest(unittest.TestCase): 245 | # No currently defined methods need testing 246 | pass 247 | 248 | 249 | class ChannelRequestMessageTest(unittest.TestCase): 250 | def setUp(self): 251 | self.message = ChannelRequestMessage() 252 | 253 | def test_get_setMessageID(self): 254 | self.message.setMessageID(0xFA) 255 | self.assertEquals(self.message.getMessageID(), 0xFA) 256 | self.assertRaises(MessageError, self.message.setMessageID, 0xFFFF) 257 | 258 | def test_payload(self): 259 | self.message.setChannelNumber(0x01) 260 | self.message.setMessageID(0x02) 261 | self.assertEquals(self.message.getPayload(), '\x01\x02') 262 | 263 | 264 | class ChannelBroadcastDataMessageTest(unittest.TestCase): 265 | # No currently defined methods need testing 266 | pass 267 | 268 | 269 | class ChannelAcknowledgedDataMessageTest(unittest.TestCase): 270 | # No currently defined methods need testing 271 | pass 272 | 273 | 274 | class ChannelBurstDataMessageTest(unittest.TestCase): 275 | # No currently defined methods need testing 276 | pass 277 | 278 | 279 | class ChannelEventMessageTest(unittest.TestCase): 280 | def setUp(self): 281 | self.message = ChannelEventMessage() 282 | 283 | def test_get_setMessageID(self): 284 | self.message.setMessageID(0xFA) 285 | self.assertEquals(self.message.getMessageID(), 0xFA) 286 | self.assertRaises(MessageError, self.message.setMessageID, 0xFFFF) 287 | 288 | def test_get_setMessageCode(self): 289 | self.message.setMessageCode(0xFA) 290 | self.assertEquals(self.message.getMessageCode(), 0xFA) 291 | self.assertRaises(MessageError, self.message.setMessageCode, 0xFFFF) 292 | 293 | def test_payload(self): 294 | self.message.setChannelNumber(0x01) 295 | self.message.setMessageID(0x02) 296 | self.message.setMessageCode(0x03) 297 | self.assertEquals(self.message.getPayload(), '\x01\x02\x03') 298 | 299 | 300 | class ChannelStatusMessageTest(unittest.TestCase): 301 | def setUp(self): 302 | self.message = ChannelStatusMessage() 303 | 304 | def test_get_setStatus(self): 305 | self.message.setStatus(0xFA) 306 | self.assertEquals(self.message.getStatus(), 0xFA) 307 | self.assertRaises(MessageError, self.message.setStatus, 0xFFFF) 308 | 309 | def test_payload(self): 310 | self.message.setChannelNumber(0x01) 311 | self.message.setStatus(0x02) 312 | self.assertEquals(self.message.getPayload(), '\x01\x02') 313 | 314 | 315 | class VersionMessageTest(unittest.TestCase): 316 | def setUp(self): 317 | self.message = VersionMessage() 318 | 319 | def test_get_setVersion(self): 320 | self.message.setVersion('\xAB' * 9) 321 | self.assertEquals(self.message.getVersion(), '\xAB' * 9) 322 | self.assertRaises(MessageError, self.message.setVersion, '1234') 323 | 324 | def test_payload(self): 325 | self.message.setVersion('\x01' * 9) 326 | self.assertEquals(self.message.getPayload(), '\x01' * 9) 327 | 328 | 329 | class CapabilitiesMessageTest(unittest.TestCase): 330 | def setUp(self): 331 | self.message = CapabilitiesMessage() 332 | 333 | def test_get_setMaxChannels(self): 334 | self.message.setMaxChannels(0xFA) 335 | self.assertEquals(self.message.getMaxChannels(), 0xFA) 336 | self.assertRaises(MessageError, self.message.setMaxChannels, 0xFFFF) 337 | 338 | def test_get_setMaxNetworks(self): 339 | self.message.setMaxNetworks(0xFA) 340 | self.assertEquals(self.message.getMaxNetworks(), 0xFA) 341 | self.assertRaises(MessageError, self.message.setMaxNetworks, 0xFFFF) 342 | 343 | def test_get_setStdOptions(self): 344 | self.message.setStdOptions(0xFA) 345 | self.assertEquals(self.message.getStdOptions(), 0xFA) 346 | self.assertRaises(MessageError, self.message.setStdOptions, 0xFFFF) 347 | 348 | def test_get_setAdvOptions(self): 349 | self.message.setAdvOptions(0xFA) 350 | self.assertEquals(self.message.getAdvOptions(), 0xFA) 351 | self.assertRaises(MessageError, self.message.setAdvOptions, 0xFFFF) 352 | 353 | def test_get_setAdvOptions2(self): 354 | self.message.setAdvOptions2(0xFA) 355 | self.assertEquals(self.message.getAdvOptions2(), 0xFA) 356 | self.assertRaises(MessageError, self.message.setAdvOptions2, 0xFFFF) 357 | self.message = CapabilitiesMessage(adv_opts2=None) 358 | self.assertEquals(len(self.message.payload), 4) 359 | 360 | def test_payload(self): 361 | self.message.setMaxChannels(0x01) 362 | self.message.setMaxNetworks(0x02) 363 | self.message.setStdOptions(0x03) 364 | self.message.setAdvOptions(0x04) 365 | self.message.setAdvOptions2(0x05) 366 | self.assertEquals(self.message.getPayload(), '\x01\x02\x03\x04\x05') 367 | 368 | 369 | class SerialNumberMessageTest(unittest.TestCase): 370 | def setUp(self): 371 | self.message = SerialNumberMessage() 372 | 373 | def test_get_setSerialNumber(self): 374 | self.message.setSerialNumber('\xFA\xFB\xFC\xFD') 375 | self.assertEquals(self.message.getSerialNumber(), '\xFA\xFB\xFC\xFD') 376 | self.assertRaises(MessageError, self.message.setSerialNumber, 377 | '\xFF' * 8) 378 | 379 | def test_payload(self): 380 | self.message.setSerialNumber('\x01\x02\x03\x04') 381 | self.assertEquals(self.message.getPayload(), '\x01\x02\x03\x04') 382 | -------------------------------------------------------------------------------- /src/ant/core/tests/node_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | import unittest 27 | 28 | from ant.core.node import * 29 | 30 | #TODO 31 | -------------------------------------------------------------------------------- /src/ant/fs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | __all__ = [] 27 | -------------------------------------------------------------------------------- /src/ant/plus/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2011, Martín Raúl Villalba 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################## 25 | 26 | __all__ = [] 27 | --------------------------------------------------------------------------------