From 180d13316aad5e706a0eca7ebda55ea75b8e00e0 Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Sun, 23 Mar 2014 15:32:18 +0000 Subject: [PATCH 01/13] Added latest changes to README.rst --- BrickPython/__init__.py | 2 +- README.rst | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/BrickPython/__init__.py b/BrickPython/__init__.py index aaf396a..986d4f0 100644 --- a/BrickPython/__init__.py +++ b/BrickPython/__init__.py @@ -1,2 +1,2 @@ -# Change version numbers here and in docs/conf.py. Also edit CHANGES.txt. +# Change version numbers here and in docs/conf.py. Also edit CHANGES.txt, and move latest to README.rst __version__ = '0.4' diff --git a/README.rst b/README.rst index a543ac3..9d6d0e1 100644 --- a/README.rst +++ b/README.rst @@ -35,4 +35,13 @@ Features * Full unit test suite * Runs on other Linux environments (Mac etc) for unit tests and development. +Changes in v0.4 +--------------- +* Updated PID algorithm so it's independent of the work cycle time. + BACKWARDS COMPATIBILITY WARNING: One parameter to the constructor of + Motor.PIDSetting has changed. + +* Added specialized sensor classes: UltrasonicSensor, TouchSensor, LightSensor + +* Added useful script for cross-platform testing. From e3d2e153fa23c269daadd48397d0f758e00b79d3 Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Sun, 23 Mar 2014 21:31:49 +0000 Subject: [PATCH 02/13] Added explanation and warning to runbp --- runbp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/runbp b/runbp index 6bb1af4..4c6cd9a 100755 --- a/runbp +++ b/runbp @@ -1,14 +1,20 @@ #!/bin/ksh -# Runs a command remotely on the BrickPi, assuming default credentials and device name. -# Copies whole BrickPython directory structure to the same location on the raspberrypi +# Runs a command remotely on the Pi, assuming default credentials and device name. +# Copies a whole directory structure to the same location (relative to ~) on the Pi +# # E.g. # cd ExamplePrograms; ../runbp python SimpleApp.py - -# This script lives in the BrickPython project directory +# +# This script lives in the base directory of the content to be copied. +# DON'T PUT IT IN YOUR HOME DIRECTORY!! +# It deletes the corresponding directory on the Pi before copying the new structure over. +# # To make it executable: # chmod a+x runpb +# # To make ssh work without entering passwords, see # https://www.debian.org/devel/passwordlessssh +# # Note - The windows will appear on the BrickPi's screen, not locally to this machine. # Current working directory relative to home: From 582190612c0aedd8125200caa021d96bddc4b2e4 Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Fri, 28 Mar 2014 20:21:29 +0000 Subject: [PATCH 03/13] Made UltrasonicSensor default to MAX_VALUE, not zero. --- BrickPython/Sensor.py | 3 ++- test/TestSensor.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/BrickPython/Sensor.py b/BrickPython/Sensor.py index 4881ac6..6a8d011 100644 --- a/BrickPython/Sensor.py +++ b/BrickPython/Sensor.py @@ -109,10 +109,11 @@ class UltrasonicSensor(Sensor): SMOOTHING_RANGE=10 def __init__(self, port): - self.recentRawValues = [0] + self.recentRawValues = [255] Sensor.__init__(self, port, Sensor.ULTRASONIC_CONT) def cookValue(self, rawValue): + rawValue = 255 if rawValue == 0 else rawValue self.recentRawValues.append( rawValue ) if len(self.recentRawValues) > UltrasonicSensor.SMOOTHING_RANGE: del self.recentRawValues[0] diff --git a/test/TestSensor.py b/test/TestSensor.py index beddd70..ff8020e 100644 --- a/test/TestSensor.py +++ b/test/TestSensor.py @@ -65,12 +65,12 @@ def testUltrasonicSensor(self): sensor = UltrasonicSensor( '1' ) self.assertEquals(sensor.port, 0) self.assertEquals( sensor.idChar, '1' ) - for input, output in {0:0, 2:0, 3:5, 4:5, 9:10, 11:10, 14:15, 16:15, 22:20, 23:25, 26:25, + for input, output in {0:UltrasonicSensor.MAX_VALUE, 2:0, 3:5, 4:5, 9:10, 11:10, 14:15, 16:15, 22:20, 23:25, 26:25, 255: UltrasonicSensor.MAX_VALUE }.items(): - for i in xrange(0,100): + for i in xrange(0,UltrasonicSensor.SMOOTHING_RANGE+1): sensor.updateValue( input ) # Remove effects of smoothing. - self.assertEquals( sensor.value(), output ) + self.assertEquals( sensor.value(), output, "Failed with input %d: got %d" %(input,sensor.value()) ) def testUltrasonicSensorSmoothing(self): sensor = UltrasonicSensor( '1' ) From bf2b73fe86f67dc09af81961c055fd3e566fa4df Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Fri, 28 Mar 2014 20:44:32 +0000 Subject: [PATCH 04/13] Minor refactor of UltrasonicSensor --- BrickPython/Sensor.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/BrickPython/Sensor.py b/BrickPython/Sensor.py index 6a8d011..87a91e7 100644 --- a/BrickPython/Sensor.py +++ b/BrickPython/Sensor.py @@ -68,18 +68,18 @@ def waitForChange(self): yield def value(self): - 'Answers the latest sensor value received' + 'Answers the latest sensor value received (overridable)' return self.recentValue def cookValue(self, rawValue): - 'Answers the value to return for a given input sensor reading' + 'Answers the value to return for a given input sensor reading (overridable)' return rawValue def __repr__(self): return "%s %s: %r (%d)" % (self.__class__.__name__, self.idChar, self.displayValue(), self.rawValue) def displayValue(self): - 'Answers a good representation of the current value for display' + 'Answers a good representation of the current value for display (overridable)' return self.value() class TouchSensor(Sensor): @@ -109,11 +109,13 @@ class UltrasonicSensor(Sensor): SMOOTHING_RANGE=10 def __init__(self, port): - self.recentRawValues = [255] + self.recentRawValues = [] Sensor.__init__(self, port, Sensor.ULTRASONIC_CONT) + # Don't want to return 0 initially, so need to reset the defaults: + self.recentRawValues = [255] + self.recentValue = UltrasonicSensor.MAX_VALUE def cookValue(self, rawValue): - rawValue = 255 if rawValue == 0 else rawValue self.recentRawValues.append( rawValue ) if len(self.recentRawValues) > UltrasonicSensor.SMOOTHING_RANGE: del self.recentRawValues[0] From 5f12170e0a80535a5d927d0827bbf133ec5a251a Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Sun, 6 Apr 2014 22:27:56 +0100 Subject: [PATCH 05/13] cw: Reimplemented Scheduler to use threads beneath the generator coroutines. --- BrickPython/Scheduler.py | 94 ++++++++++++++++++++++++++++++---------- test/TestScheduler.py | 4 +- 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/BrickPython/Scheduler.py b/BrickPython/Scheduler.py index 6d82b56..eaeaba6 100644 --- a/BrickPython/Scheduler.py +++ b/BrickPython/Scheduler.py @@ -6,6 +6,7 @@ import datetime import logging import sys, traceback +import threading class StopCoroutineException( Exception ): '''Exception used to stop a coroutine''' @@ -13,6 +14,55 @@ class StopCoroutineException( Exception ): ProgramStartTime = datetime.datetime.now() +class Coroutine(): + def __init__(self): + #: Semaphore is one when blocked, 0 when running + self.semaphore = threading.Semaphore(0) + + def action(self): + pass + + +class GeneratorCoroutineWrapper(Coroutine): + + def __init__(self, scheduler, generator): + Coroutine.__init__(self) + self.scheduler = scheduler + self.stopEvent = threading.Event() + self.generator = generator + self.thread = threading.Thread(target=self.action) + self.thread.start() + + def action(self): + 'The thread entry function - executed within thread `thread`' + try: + self.semaphore.acquire() + while not self.stopEvent.is_set(): + self.generator.next() + self.scheduler.semaphore.release() + self.semaphore.acquire() + self.generator.throw(StopCoroutineException) + except (StopCoroutineException, StopIteration): + pass + except Exception as e: + self.scheduler.lastExceptionCaught = e + logging.info( "Scheduler - caught: %r" % (e) ) + exc_type, exc_value, exc_traceback = sys.exc_info() + trace = "".join(traceback.format_tb(exc_traceback)) + logging.debug( "Traceback (latest call first):\n %s" % trace ) + + self.scheduler.coroutines.remove( self ) + self.scheduler.semaphore.release() + + def next(self): + 'Runs a bit of processing (next on the generator) - executed from the scheduler thread - returns only when processing has completed' + self.semaphore.release() + self.scheduler.semaphore.acquire() + + def stop(self): + 'Causes the thread to stop' + self.stopEvent.set() + class Scheduler(): ''' This manages an arbitrary number of coroutines (implemented as generator functions), supporting @@ -34,9 +84,11 @@ def __init__(self, timeMillisBetweenWorkCalls = 50): Scheduler.timeMillisBetweenWorkCalls = timeMillisBetweenWorkCalls self.coroutines = [] self.timeOfLastCall = Scheduler.currentTimeMillis() - self.updateCoroutine = self.nullCoroutine() # for testing - usually replaced. + self.updateCoroutine = GeneratorCoroutineWrapper( self, self.nullCoroutine() ) # for testing - usually replaced. #: The most recent exception raised by a coroutine: self.lastExceptionCaught = Exception("None") + #: Semaphore is one when blocked (running a coroutine), 0 when active + self.semaphore = threading.Semaphore(0) def doWork(self): @@ -48,17 +100,7 @@ def doWork(self): self.timeOfLastCall = timeNow self.updateCoroutine.next() for coroutine in self.coroutines[:]: # Copy of coroutines, so it doesn't matter removing one - try: - coroutine.next() - except (StopIteration): - self.coroutines.remove( coroutine ) - except Exception as e: - self.lastExceptionCaught = e - logging.info( "Scheduler - caught: %r" % (e) ) - exc_type, exc_value, exc_traceback = sys.exc_info() - trace = "".join(traceback.format_tb(exc_traceback)) - logging.debug( "Traceback (latest call first):\n %s" % trace ) - self.coroutines.remove( coroutine ) + coroutine.next() self.updateCoroutine.next() @@ -71,31 +113,36 @@ def timeMillisToNextCall(self): def addSensorCoroutine(self, *coroutineList): '''Adds one or more new sensor/program coroutines to be scheduled, answering the last one to be added. Sensor coroutines are scheduled *before* Action coroutines''' - self.coroutines[0:0] = coroutineList - return coroutineList[-1] + for generatorFunction in coroutineList: + latestAdded = GeneratorCoroutineWrapper(self, generatorFunction) + self.coroutines[0:0] = [ latestAdded ] + return generatorFunction def addActionCoroutine(self, *coroutineList): '''Adds one or more new motor control coroutines to be scheduled, answering the last coroutine to be added. Action coroutines are scheduled *after* Sensor coroutines''' - self.coroutines.extend( coroutineList ) - return coroutineList[-1] + for generatorFunction in coroutineList: + latestAdded = GeneratorCoroutineWrapper(self, generatorFunction) + self.coroutines.extend( [ latestAdded ] ) + return generatorFunction def setUpdateCoroutine(self, coroutine): # Private - set the coroutine that manages the interaction with the BrickPi. # The coroutine will be invoked once at the start and once at the end of each doWork call. - self.updateCoroutine = coroutine + self.updateCoroutine = GeneratorCoroutineWrapper(self, coroutine) + + def findCoroutineForGenerator(self, generator): + return [c for c in self.coroutines if c.generator == generator][0] def stopCoroutine( self, *coroutineList ): 'Terminates the given one or more coroutines' - for coroutine in coroutineList: - try: - coroutine.throw(StopCoroutineException) - except (StopCoroutineException,StopIteration): # If the coroutine doesn't catch the exception to tidy up, it comes back here. - self.coroutines.remove( coroutine ) + for generator in coroutineList: + coroutine = self.findCoroutineForGenerator(generator) + coroutine.stop() def stopAllCoroutines(self): 'Terminates all coroutines (except the updater one) - rather drastic!' - self.stopCoroutine(*self.coroutines[:]) # Makes a copy of the list - don't want to be changing it. + self.stopCoroutine(*[c.generator for c in self.coroutines]) # Makes a copy of the list - don't want to be changing it. def numCoroutines( self ): 'Answers the number of active coroutines' @@ -155,3 +202,4 @@ def waitFor(function, *args ): 'Coroutine that waits until the given function (with optional parameters) returns True.' while not function(*args): yield + diff --git a/test/TestScheduler.py b/test/TestScheduler.py index ba771f1..132249a 100644 --- a/test/TestScheduler.py +++ b/test/TestScheduler.py @@ -98,7 +98,7 @@ def testCoroutinesCanBeTerminated(self): self.scheduler.stopCoroutine( coroutine ) self.scheduler.doWork() # It has completed early - assert( TestScheduler.coroutineCalls[-1] == 1 ) + self.assertEquals( TestScheduler.coroutineCalls[-1], 1 ) TestScheduler.checkCoroutineFinished( coroutine ) def testAllCoroutinesCanBeTerminated(self): @@ -111,7 +111,7 @@ def testAllCoroutinesCanBeTerminated(self): self.scheduler.doWork() self.scheduler.doWork() # They all terminate - assert( TestScheduler.coroutineCalls == [1 , 1] ) + self.assertEquals(TestScheduler.coroutineCalls, [1 , 1] ) def testCoroutineThatThrowsException(self): # When we have a coroutine that throws an exception: From 7f04783770fc4831f015d3045995306b003b9189 Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Mon, 7 Apr 2014 07:19:57 +0100 Subject: [PATCH 06/13] cw: Fixed problem of process not exiting. --- BrickPython/Scheduler.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/BrickPython/Scheduler.py b/BrickPython/Scheduler.py index eaeaba6..d537cfd 100644 --- a/BrickPython/Scheduler.py +++ b/BrickPython/Scheduler.py @@ -24,13 +24,17 @@ def action(self): class GeneratorCoroutineWrapper(Coroutine): + '''Internal: Wraps a generator-style coroutine with a thread''' def __init__(self, scheduler, generator): + '''`scheduler` - the main Scheduler object + `generator` - the generator object created by calling the generator function''' Coroutine.__init__(self) self.scheduler = scheduler self.stopEvent = threading.Event() self.generator = generator self.thread = threading.Thread(target=self.action) + self.thread.setDaemon(True) # Daemon threads don't prevent the process from exiting. self.thread.start() def action(self): @@ -60,7 +64,7 @@ def next(self): self.scheduler.semaphore.acquire() def stop(self): - 'Causes the thread to stop' + 'Causes the thread to stop - executed from the scheduler thread' self.stopEvent.set() @@ -115,7 +119,7 @@ def addSensorCoroutine(self, *coroutineList): Sensor coroutines are scheduled *before* Action coroutines''' for generatorFunction in coroutineList: latestAdded = GeneratorCoroutineWrapper(self, generatorFunction) - self.coroutines[0:0] = [ latestAdded ] + self.coroutines.insert(0, latestAdded) return generatorFunction def addActionCoroutine(self, *coroutineList): @@ -123,7 +127,7 @@ def addActionCoroutine(self, *coroutineList): Action coroutines are scheduled *after* Sensor coroutines''' for generatorFunction in coroutineList: latestAdded = GeneratorCoroutineWrapper(self, generatorFunction) - self.coroutines.extend( [ latestAdded ] ) + self.coroutines.append(latestAdded) return generatorFunction def setUpdateCoroutine(self, coroutine): @@ -132,7 +136,7 @@ def setUpdateCoroutine(self, coroutine): self.updateCoroutine = GeneratorCoroutineWrapper(self, coroutine) def findCoroutineForGenerator(self, generator): - return [c for c in self.coroutines if c.generator == generator][0] + return (c for c in self.coroutines if c.generator == generator).next() def stopCoroutine( self, *coroutineList ): 'Terminates the given one or more coroutines' From e0020235fa6a149651d1d6e480d2bcf5f285fbcb Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Sun, 20 Apr 2014 09:47:15 +0100 Subject: [PATCH 07/13] cw: new Coroutine class --- BrickPython/Coroutine.py | 129 +++++++++++++++++++ test/TestCoroutine.py | 263 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 392 insertions(+) create mode 100644 BrickPython/Coroutine.py create mode 100644 test/TestCoroutine.py diff --git a/BrickPython/Coroutine.py b/BrickPython/Coroutine.py new file mode 100644 index 0000000..1281d4f --- /dev/null +++ b/BrickPython/Coroutine.py @@ -0,0 +1,129 @@ +# Scheduler +# Support for coroutines using Python generator functions. +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + +import logging +import sys, traceback +import threading +import datetime + +class StopCoroutineException( Exception ): + '''Exception used to stop a coroutine''' + pass + +ProgramStartTime = datetime.datetime.now() + +class Coroutine( threading.Thread ): + def __init__(self, func, *args, **kwargs): + threading.Thread.__init__(self) + self.args = args + self.kwargs = kwargs + self.logger = logging + self.mySemaphore = threading.Semaphore(0) + self.callerSemaphore = threading.Semaphore(0) + self.stopEvent = threading.Event() + self.setDaemon(True) # Daemon threads don't prevent the process from exiting. + self.func = func + self.lastExceptionCaught = None + self.start() + + @staticmethod + def currentTimeMillis(): + 'Answers the time in floating point milliseconds since program start.' + global ProgramStartTime + c = datetime.datetime.now() - ProgramStartTime + return c.days * (3600.0 * 1000 * 24) + c.seconds * 1000.0 + c.microseconds / 1000.0 + + def run(self): + try: + self.mySemaphore.acquire() + self.func(*self.args,**self.kwargs) + except (StopCoroutineException, StopIteration): + pass + except Exception as e: + self.lastExceptionCaught = e + self.logger.info( "Coroutine - caught exception: %r" % (e) ) + exc_type, exc_value, exc_traceback = sys.exc_info() + trace = "".join(traceback.format_tb(exc_traceback)) + self.logger.debug( "Traceback (latest call first):\n %s" % trace ) + self.callerSemaphore.release() + threading.Thread.run(self) # Does some cleanup. + + def call(self): + 'Executed from the caller thread. Runs the coroutine until it calls wait. Does nothing if the thread has terminated.' + if self.isAlive(): + self.mySemaphore.release() + self.callerSemaphore.acquire() + + def stop(self): + 'Executed from the caller thread. Stops the coroutine, causing thread to terminate.' + self.stopEvent.set() + self.call() + + @staticmethod + def wait(): + 'Called from within the coroutine to hand back control to the caller thread' + self=threading.currentThread() + self.callerSemaphore.release() + self.mySemaphore.acquire() + if (self.stopEvent.isSet()): + raise StopCoroutineException() + + @staticmethod + def waitMilliseconds(timeMillis): + 'Called from within the coroutine to wait the given time' + startTime = Coroutine.currentTimeMillis() + while Coroutine.currentTimeMillis() - startTime < timeMillis: + Coroutine.wait() + +# while not self.stopEvent.is_set(): + +# +# self.scheduler.coroutines.remove( self ) +# self.scheduler.semaphore.release() + +class GeneratorCoroutineWrapper(Coroutine): + '''Internal: Wraps a generator-style coroutine with a thread''' + + def __init__(self, scheduler, generator): + '''`scheduler` - the main Scheduler object + `generator` - the generator object created by calling the generator function''' + Coroutine.__init__(self) + self.scheduler = scheduler + self.stopEvent = threading.Event() + self.generator = generator + self.thread = threading.Thread(target=self.action) + self.thread.setDaemon(True) # Daemon threads don't prevent the process from exiting. + self.thread.start() + + def action(self): + 'The thread entry function - executed within thread `thread`' + try: + self.semaphore.acquire() + while not self.stopEvent.is_set(): + self.generator.next() + self.scheduler.semaphore.release() + self.semaphore.acquire() + self.generator.throw(StopCoroutineException) + except (StopCoroutineException, StopIteration): + pass + except Exception as e: + self.scheduler.lastExceptionCaught = e + logging.info( "Scheduler - caught: %r" % (e) ) + exc_type, exc_value, exc_traceback = sys.exc_info() + trace = "".join(traceback.format_tb(exc_traceback)) + logging.debug( "Traceback (latest call first):\n %s" % trace ) + + self.scheduler.coroutines.remove( self ) + self.scheduler.semaphore.release() + + def next(self): + 'Runs a bit of processing (next on the generator) - executed from the scheduler thread - returns only when processing has completed' + self.semaphore.release() + self.scheduler.semaphore.acquire() + + def stop(self): + 'Causes the thread to stop - executed from the scheduler thread' + self.stopEvent.set() + diff --git a/test/TestCoroutine.py b/test/TestCoroutine.py new file mode 100644 index 0000000..20740ba --- /dev/null +++ b/test/TestCoroutine.py @@ -0,0 +1,263 @@ +# Tests for Scheduler +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + +# Run tests as +# python TestCoroutine.py +# or, if you've got it installed: +# nosetests + + + +from BrickPython.Coroutine import * +import unittest +import logging +from mock import * + +class TestCoroutine(unittest.TestCase): + ''' Tests for the Scheduler class, its built-in coroutines, and its coroutine handling. + ''' + coroutineCalls = [] + @staticmethod + def dummyCoroutineFunc(): + for i in range(1, 5): + TestCoroutine.coroutineCalls.append(i) + Coroutine.wait(); + + @staticmethod + def dummyCoroutineFuncThatDoesCleanup(): + for i in range(1, 6): + TestCoroutine.coroutineCalls.append(i) + try: + Coroutine.wait() + finally: + TestCoroutine.coroutineCalls.append( -1 ) + + @staticmethod + def dummyCoroutineFuncThatThrowsException(): + raise Exception("Hello") + + def setUp(self): + TestCoroutine.coroutineCalls = [] + + def tearDown(self): + pass + + def testCoroutinesGetCalledUntilDone(self): + # When we start a coroutine + f = TestCoroutine.dummyCoroutineFunc + coroutine = Coroutine( f ) + # It's a daemon thread + self.assertTrue( coroutine.isDaemon()) + # It doesn't run until we call it. + self.assertEqual(TestCoroutine.coroutineCalls, [] ) + # Each call gets one iteration + coroutine.call() + self.assertEqual(TestCoroutine.coroutineCalls, [1] ) + # And when we run it until finished + for i in range(0,10): + coroutine.call() + # It has completed + self.assertEqual(TestCoroutine.coroutineCalls, [1,2,3,4] ) + self.assertFalse( coroutine.isAlive() ) + + def testCoroutinesGetStoppedAndCleanedUp(self): + # When we start a coroutine + coroutine = Coroutine(TestCoroutine.dummyCoroutineFuncThatDoesCleanup) + # run it for a bit then stop it + coroutine.call() + coroutine.stop() + # It has stopped and cleaned up + self.assertFalse( coroutine.isAlive()) + self.assertEquals( TestCoroutine.coroutineCalls, [1,-1] ) + + def testCoroutineExceptionLogging(self): + coroutine = Coroutine(TestCoroutine.dummyCoroutineFuncThatThrowsException) + coroutine.logger = Mock() + coroutine.call() + self.assertTrue(coroutine.logger.info.called) + firstParam = coroutine.logger.info.call_args[0][0] + self.assertRegexpMatches(firstParam, "Coroutine - caught exception: .*Exception.*") + self.assertRegexpMatches(coroutine.logger.debug.call_args[0][0], "Traceback.*") + + + def testCoroutineWaitMilliseconds(self): + def dummyCoroutineFuncWaiting1Sec(): + Coroutine.waitMilliseconds(1000) + + coroutine = Coroutine(dummyCoroutineFuncWaiting1Sec) + # Doesn't matter not restoring this; tests never use real time: + Coroutine.currentTimeMillis = Mock(side_effect = [1,10,500,1200]) + for i in range(1,3): + coroutine.call() + self.assertTrue(coroutine.is_alive(),"Coroutine dead at call %d" % i) + coroutine.call() + self.assertFalse(coroutine.is_alive()) + + def testCoroutineCanHaveParameters(self): + def func(*args, **kwargs): + print "In coroutine: %r %r" % (args, kwargs) + self.assertEquals(args, (1)) + self.assertEquals(kwargs, {"extra": 2}) + coroutine = Coroutine(func, 1, extra=2) + coroutine.call() + + pass + + def testWaitCanPassAndReceiveParameters(self): + pass + + def testRunCoroutinesUntilFirstCompletes(self): + pass + + def testRunCoroutinesUntilAllComplete(self): + pass + + + +# def testWaitMilliseconds(self): +# # If we wait for 10 ms +# for i in self.scheduler.waitMilliseconds(10): +# pass +# # that's about the time that will have passed: +# assert( self.scheduler.currentTimeMillis() in range(10,12) ) +# +# def testRunTillFirstCompletes(self): +# # When we run three coroutines using runTillFirstCompletes: +# for i in self.scheduler.runTillFirstCompletes(TestCoroutine.dummyCoroutine(1,9), +# TestCoroutine.dummyCoroutine(1,2), +# TestCoroutine.dummyCoroutine(1,9) ): +# pass +# # the first to complete stops the others: +# self.assertEquals( TestCoroutine.coroutineCalls, [1,1,1,2] ) +# self.assertEquals( self.scheduler.numCoroutines(), 0) +# +# def testRunTillAllComplete( self ): +# # When we run three coroutines using runTillAllComplete: +# for i in self.scheduler.runTillAllComplete( *[TestCoroutine.dummyCoroutine(1,i) for i in [2,3,4]] ): +# pass +# # they all run to completion: +# print TestCoroutine.coroutineCalls +# assert( TestCoroutine.coroutineCalls == [1,1,1,2,2,3] ) +# assert( self.scheduler.numCoroutines() == 0) +# +# def testWithTimeout(self): +# # When we run a coroutine with a timeout: +# for i in self.scheduler.withTimeout(10, TestCoroutine.dummyCoroutineThatDoesCleanup(1,99) ): +# pass +# # It completes at around the timeout, and does cleanup: +# print TestCoroutine.coroutineCalls +# self.assertTrue( 0 < TestCoroutine.coroutineCalls[-2] <= 10) # N.b. currentTimeMillis is called more than once per doWork call. +# self.assertEquals( TestCoroutine.coroutineCalls[-1], -1 ) +# +# def testTimeMillisToNextCall(self): +# # Given a mock timer, and a different scheduler set up with a known time interval +# scheduler = Scheduler(20) +# # when we have just coroutines that take no time +# scheduler.addActionCoroutine( TestCoroutine.dummyCoroutine() ) +# # then the time to next tick is the default less a bit for the timer check calls: +# scheduler.doWork() +# ttnt = scheduler.timeMillisToNextCall() +# assert( ttnt in range(17,20) ) +# # when we have an additional coroutine that takes time +# scheduler.addSensorCoroutine( TestCoroutine.dummyCoroutineThatTakesTime() ) +# # then the time to next tick is less by the amount of time taken by the coroutine: +# scheduler.doWork() +# ttnt = scheduler.timeMillisToNextCall() +# assert( ttnt in range(7,10) ) +# # but when the coroutines take more time than the time interval available +# for i in range(0,2): +# scheduler.addSensorCoroutine( TestCoroutine.dummyCoroutineThatTakesTime() ) +# # the time to next tick never gets less than zero +# scheduler.doWork() +# ttnt = scheduler.timeMillisToNextCall() +# assert( ttnt == 0 ) +# # and incidentally, we should have all the coroutines still running +# assert( scheduler.numCoroutines() == 4 ) +# +# def timeCheckerCoroutine(self): +# # Helper coroutine for testEachCallToACoroutineGetsADifferentTime +# # Checks that each call gets a different time value. +# time = Scheduler.currentTimeMillis() +# while True: +# yield +# newTime = Scheduler.currentTimeMillis() +# self.assertNotEquals( newTime, time ) +# time = newTime +# +# def testEachCallToACoroutineGetsADifferentTime(self): +# scheduler = Scheduler() +# Scheduler.currentTimeMillis = Mock( side_effect = [0,0,0,0,0,0,0,0,0,0,1,2,3,4,5] ) +# # For any coroutine, +# scheduler.setUpdateCoroutine( self.timeCheckerCoroutine() ) +# # We can guarantee that the timer always increments between calls (for speed calculations etc). +# for i in range(1,10): +# scheduler.doWork() +# +# def testTheWaitForCoroutine(self): +# scheduler = Scheduler() +# arrayParameter = [] +# # When we create a WaitFor coroutine with a function that takes one parameter (actually an array) +# coroutine = scheduler.waitFor( lambda ap: len(ap) > 0, arrayParameter ) +# # It runs +# for i in range(0,5): +# coroutine.next() +# # Until the function returns true. +# arrayParameter.append(1) +# TestCoroutine.checkCoroutineFinished( coroutine ) +# +# @staticmethod +# def throwingCoroutine(): +# yield +# raise Exception("Hello") +# +# def testExceptionThrownFromCoroutine(self): +# scheduler = Scheduler() +# self.assertIsNotNone(scheduler.lastExceptionCaught) +# scheduler.addActionCoroutine(self.throwingCoroutine()) +# for i in range(1,3): +# scheduler.doWork() +# self.assertEquals(scheduler.lastExceptionCaught.message, "Hello") +# +# def testRunTillFirstCompletesWithException(self): +# # When we run three coroutines using runTillFirstCompletes: +# self.scheduler.addActionCoroutine(self.scheduler.runTillFirstCompletes(self.throwingCoroutine(), +# TestCoroutine.dummyCoroutine(1,2), +# TestCoroutine.dummyCoroutine(1,9) )) +# for i in range(1,10): +# self.scheduler.doWork() +# # the first to complete stops the others: +# self.assertEquals( TestCoroutine.coroutineCalls, [1,1] ) +# self.assertEquals( self.scheduler.numCoroutines(), 0) +# # and the exception is caught by the Scheduler: +# self.assertEquals(self.scheduler.lastExceptionCaught.message, "Hello") +# +# def testRunTillAllCompleteWithException( self ): +# # When we run three coroutines using runTillAllComplete: +# self.scheduler.addActionCoroutine(self.scheduler.runTillAllComplete(self.throwingCoroutine(), +# TestCoroutine.dummyCoroutine(1,2))) +# for i in range(1,10): +# self.scheduler.doWork() +# # the first to complete stops the others: +# self.assertEquals( TestCoroutine.coroutineCalls, [1] ) +# self.assertEquals( self.scheduler.numCoroutines(), 0) +# # and the exception is caught by the Scheduler: +# self.assertEquals(self.scheduler.lastExceptionCaught.message, "Hello") +# +# def testCanCatchExceptionWithinNestedCoroutines(self): +# self.caught = 0 +# def outerCoroutine(self): +# try: +# for i in self.throwingCoroutine(): +# yield +# except: +# self.caught = 1 +# for i in outerCoroutine(self): +# pass +# self.assertEquals(self.caught, 1) + + +if __name__ == '__main__': + logging.basicConfig(format='%(message)s', level=logging.DEBUG) # Logging is a simple print + unittest.main() + From 84555db80eefaa1794ba97993aa2cc519987b4f0 Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Sun, 20 Apr 2014 14:05:28 +0100 Subject: [PATCH 08/13] Improved Coroutine implementation. --- BrickPython/Coroutine.py | 23 ++++++++++++++++++----- test/TestCoroutine.py | 36 ++++++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/BrickPython/Coroutine.py b/BrickPython/Coroutine.py index 1281d4f..59c1cb5 100644 --- a/BrickPython/Coroutine.py +++ b/BrickPython/Coroutine.py @@ -36,6 +36,7 @@ def currentTimeMillis(): return c.days * (3600.0 * 1000 * 24) + c.seconds * 1000.0 + c.microseconds / 1000.0 def run(self): + self.callResult = None try: self.mySemaphore.acquire() self.func(*self.args,**self.kwargs) @@ -50,25 +51,37 @@ def run(self): self.callerSemaphore.release() threading.Thread.run(self) # Does some cleanup. - def call(self): - 'Executed from the caller thread. Runs the coroutine until it calls wait. Does nothing if the thread has terminated.' + def call(self, param = None): + '''Executed from the caller thread. Runs the coroutine until it calls wait. + Does nothing if the thread has terminated. + If a parameter is passed, it is returned from the Coroutine.wait() function in the coroutine thread.''' if self.isAlive(): + self.callParam = param self.mySemaphore.release() self.callerSemaphore.acquire() + return self.callResult def stop(self): - 'Executed from the caller thread. Stops the coroutine, causing thread to terminate.' + '''Executed from the caller thread. Stops the coroutine, causing its thread to terminate. + On completion the thread has terminated: is_active() is false. + To support this, a coroutine mustn't catch the StopCoroutineException (unless it re-raises it). + ''' self.stopEvent.set() self.call() + self.join() @staticmethod - def wait(): - 'Called from within the coroutine to hand back control to the caller thread' + def wait(param = None): + '''Called from within the coroutine to hand back control to the caller thread. + If a parameter is passed, it will be returned from Coroutine.call in the caller thread. + ''' self=threading.currentThread() + self.callResult = param self.callerSemaphore.release() self.mySemaphore.acquire() if (self.stopEvent.isSet()): raise StopCoroutineException() + return self.callParam @staticmethod def waitMilliseconds(timeMillis): diff --git a/test/TestCoroutine.py b/test/TestCoroutine.py index 20740ba..d2085f1 100644 --- a/test/TestCoroutine.py +++ b/test/TestCoroutine.py @@ -32,11 +32,7 @@ def dummyCoroutineFuncThatDoesCleanup(): Coroutine.wait() finally: TestCoroutine.coroutineCalls.append( -1 ) - - @staticmethod - def dummyCoroutineFuncThatThrowsException(): - raise Exception("Hello") - + def setUp(self): TestCoroutine.coroutineCalls = [] @@ -54,7 +50,7 @@ def testCoroutinesGetCalledUntilDone(self): # Each call gets one iteration coroutine.call() self.assertEqual(TestCoroutine.coroutineCalls, [1] ) - # And when we run it until finished + # And when we run it until finished... for i in range(0,10): coroutine.call() # It has completed @@ -72,7 +68,9 @@ def testCoroutinesGetStoppedAndCleanedUp(self): self.assertEquals( TestCoroutine.coroutineCalls, [1,-1] ) def testCoroutineExceptionLogging(self): - coroutine = Coroutine(TestCoroutine.dummyCoroutineFuncThatThrowsException) + def dummyCoroutineFuncThatThrowsException(): + raise Exception("Hello") + coroutine = Coroutine(dummyCoroutineFuncThatThrowsException) coroutine.logger = Mock() coroutine.call() self.assertTrue(coroutine.logger.info.called) @@ -91,23 +89,37 @@ def dummyCoroutineFuncWaiting1Sec(): for i in range(1,3): coroutine.call() self.assertTrue(coroutine.is_alive(),"Coroutine dead at call %d" % i) + # Don't understand why this is failing! coroutine.call() - self.assertFalse(coroutine.is_alive()) + self.assertFalse(coroutine.is_alive(), "Coroutine should have finished") def testCoroutineCanHaveParameters(self): def func(*args, **kwargs): - print "In coroutine: %r %r" % (args, kwargs) self.assertEquals(args, (1)) self.assertEquals(kwargs, {"extra": 2}) coroutine = Coroutine(func, 1, extra=2) coroutine.call() - pass - def testWaitCanPassAndReceiveParameters(self): - pass + def cofunc(): + result = Coroutine.wait(1) + # the parameter passed in was right. + self.assertEquals(result, 2) + coroutine = Coroutine(cofunc) + #After we start the coroutine + coroutine.call() + #The next call returns the result we're expecting + self.assertEquals(coroutine.call(2), 1) + #And running it has caused the result parameter to be checked correctly before the coroutine terminated + self.assertFalse(coroutine.is_alive()) def testRunCoroutinesUntilFirstCompletes(self): + def cofunc1(): + Coroutine.wait() + def cofunc2(): + Coroutine.wait() + Coroutine.wait() +# Coroutine.runTillFirstCompletes pass def testRunCoroutinesUntilAllComplete(self): From c616647c99630c2949ea90d8575a8373df7c4ce9 Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Sun, 20 Apr 2014 15:34:58 +0100 Subject: [PATCH 09/13] cw: Got runTillFirstCompletes working for new Coroutine implementation. --- BrickPython/Coroutine.py | 25 ++++++++++++++++++++++--- test/TestCoroutine.py | 29 +++++++++++++++-------------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/BrickPython/Coroutine.py b/BrickPython/Coroutine.py index 59c1cb5..a119552 100644 --- a/BrickPython/Coroutine.py +++ b/BrickPython/Coroutine.py @@ -16,6 +16,7 @@ class StopCoroutineException( Exception ): class Coroutine( threading.Thread ): def __init__(self, func, *args, **kwargs): + print "Coroutine: %r %r" % (args, kwargs) threading.Thread.__init__(self) self.args = args self.kwargs = kwargs @@ -48,6 +49,7 @@ def run(self): exc_type, exc_value, exc_traceback = sys.exc_info() trace = "".join(traceback.format_tb(exc_traceback)) self.logger.debug( "Traceback (latest call first):\n %s" % trace ) + self.stopEvent.set() # Need to tell caller to do a join. self.callerSemaphore.release() threading.Thread.run(self) # Does some cleanup. @@ -55,10 +57,12 @@ def call(self, param = None): '''Executed from the caller thread. Runs the coroutine until it calls wait. Does nothing if the thread has terminated. If a parameter is passed, it is returned from the Coroutine.wait() function in the coroutine thread.''' - if self.isAlive(): + if self.is_alive(): self.callParam = param self.mySemaphore.release() self.callerSemaphore.acquire() + if self.stopEvent.is_set(): + self.join() # Ensure that is_alive is false on exit. return self.callResult def stop(self): @@ -85,12 +89,27 @@ def wait(param = None): @staticmethod def waitMilliseconds(timeMillis): - 'Called from within the coroutine to wait the given time' + '''Called from within the coroutine to wait the given time. + I.e. Invocations of the coroutine using call() will do nothing until then. ''' startTime = Coroutine.currentTimeMillis() while Coroutine.currentTimeMillis() - startTime < timeMillis: Coroutine.wait() -# while not self.stopEvent.is_set(): + @staticmethod + def runTillFirstCompletes(*coroutines): + def runTillFirstCompletesFunc(*coroutineList): + while True: + for c in coroutineList: + c.call() + if not c.is_alive(): + break + Coroutine.wait() + for c in coroutineList: + if c.is_alive(): + c.stop() + + result = Coroutine(runTillFirstCompletesFunc, *coroutines) + return result # # self.scheduler.coroutines.remove( self ) diff --git a/test/TestCoroutine.py b/test/TestCoroutine.py index d2085f1..e532797 100644 --- a/test/TestCoroutine.py +++ b/test/TestCoroutine.py @@ -14,13 +14,16 @@ import logging from mock import * +logging.basicConfig(format='%(message)s', level=logging.DEBUG) # Logging is a simple print + class TestCoroutine(unittest.TestCase): ''' Tests for the Scheduler class, its built-in coroutines, and its coroutine handling. ''' coroutineCalls = [] @staticmethod - def dummyCoroutineFunc(): - for i in range(1, 5): + def dummyCoroutineFunc(start=1, end=5): + logging.debug( "in dummyCoroutineFunc %d %d" %(start, end) ) + for i in range(start, end): TestCoroutine.coroutineCalls.append(i) Coroutine.wait(); @@ -32,7 +35,7 @@ def dummyCoroutineFuncThatDoesCleanup(): Coroutine.wait() finally: TestCoroutine.coroutineCalls.append( -1 ) - + def setUp(self): TestCoroutine.coroutineCalls = [] @@ -45,7 +48,9 @@ def testCoroutinesGetCalledUntilDone(self): coroutine = Coroutine( f ) # It's a daemon thread self.assertTrue( coroutine.isDaemon()) - # It doesn't run until we call it. + # It's alive + self.assertTrue(coroutine.is_alive()) + # But it doesn't run until we call it. self.assertEqual(TestCoroutine.coroutineCalls, [] ) # Each call gets one iteration coroutine.call() @@ -113,17 +118,13 @@ def cofunc(): #And running it has caused the result parameter to be checked correctly before the coroutine terminated self.assertFalse(coroutine.is_alive()) - def testRunCoroutinesUntilFirstCompletes(self): - def cofunc1(): - Coroutine.wait() - def cofunc2(): - Coroutine.wait() - Coroutine.wait() -# Coroutine.runTillFirstCompletes - pass + def testRunCoroutinesUntilFirstCompletesOrAllComplete(self): + coroutine = Coroutine.runTillFirstCompletes(Coroutine(TestCoroutine.dummyCoroutineFunc,1,3), + Coroutine(TestCoroutine.dummyCoroutineFunc,1,6)) + for i in range(1,10): + coroutine.call() + self.assertEquals(TestCoroutine.coroutineCalls, [1,1,2,2]) - def testRunCoroutinesUntilAllComplete(self): - pass From 2cf0c87389236588659593fda6b4168eb60e2fb9 Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Sun, 20 Apr 2014 15:58:35 +0100 Subject: [PATCH 10/13] cw: Added withTimeout to Coroutine --- BrickPython/Coroutine.py | 22 ++++++++++++++++++++-- test/TestCoroutine.py | 19 ++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/BrickPython/Coroutine.py b/BrickPython/Coroutine.py index a119552..a469691 100644 --- a/BrickPython/Coroutine.py +++ b/BrickPython/Coroutine.py @@ -16,7 +16,6 @@ class StopCoroutineException( Exception ): class Coroutine( threading.Thread ): def __init__(self, func, *args, **kwargs): - print "Coroutine: %r %r" % (args, kwargs) threading.Thread.__init__(self) self.args = args self.kwargs = kwargs @@ -98,7 +97,7 @@ def waitMilliseconds(timeMillis): @staticmethod def runTillFirstCompletes(*coroutines): def runTillFirstCompletesFunc(*coroutineList): - while True: + while all(c.is_alive() for c in coroutineList): for c in coroutineList: c.call() if not c.is_alive(): @@ -111,6 +110,25 @@ def runTillFirstCompletesFunc(*coroutineList): result = Coroutine(runTillFirstCompletesFunc, *coroutines) return result + @staticmethod + def runTillAllComplete(*coroutines): + def runTillAllCompleteFunc(*coroutineList): + while any(c.is_alive() for c in coroutineList): + for c in coroutineList: + c.call() + Coroutine.wait() + + result = Coroutine(runTillAllCompleteFunc, *coroutines) + return result + + def withTimeout(self, timeoutMillis): + '''Answers this coroutine, decorated with a timeout that stops it if called after timeoutMillis has elapsed. + ''' + def timeoutFunc(timeoutMillis): + Coroutine.waitMilliseconds(timeoutMillis) + result = Coroutine.runTillFirstCompletes(self, Coroutine(timeoutFunc, timeoutMillis)) + return result + # # self.scheduler.coroutines.remove( self ) # self.scheduler.semaphore.release() diff --git a/test/TestCoroutine.py b/test/TestCoroutine.py index e532797..3d3ca0f 100644 --- a/test/TestCoroutine.py +++ b/test/TestCoroutine.py @@ -22,7 +22,6 @@ class TestCoroutine(unittest.TestCase): coroutineCalls = [] @staticmethod def dummyCoroutineFunc(start=1, end=5): - logging.debug( "in dummyCoroutineFunc %d %d" %(start, end) ) for i in range(start, end): TestCoroutine.coroutineCalls.append(i) Coroutine.wait(); @@ -100,7 +99,7 @@ def dummyCoroutineFuncWaiting1Sec(): def testCoroutineCanHaveParameters(self): def func(*args, **kwargs): - self.assertEquals(args, (1)) + self.assertEquals(args, (1,)) self.assertEquals(kwargs, {"extra": 2}) coroutine = Coroutine(func, 1, extra=2) coroutine.call() @@ -118,13 +117,27 @@ def cofunc(): #And running it has caused the result parameter to be checked correctly before the coroutine terminated self.assertFalse(coroutine.is_alive()) - def testRunCoroutinesUntilFirstCompletesOrAllComplete(self): + def testRunCoroutinesUntilFirstCompletes(self): coroutine = Coroutine.runTillFirstCompletes(Coroutine(TestCoroutine.dummyCoroutineFunc,1,3), Coroutine(TestCoroutine.dummyCoroutineFunc,1,6)) for i in range(1,10): coroutine.call() self.assertEquals(TestCoroutine.coroutineCalls, [1,1,2,2]) + def testRunCoroutinesUntilAllComplete(self): + coroutine = Coroutine.runTillAllComplete(Coroutine(TestCoroutine.dummyCoroutineFunc,1,3), + Coroutine(TestCoroutine.dummyCoroutineFunc,1,6)) + for i in range(1,10): + coroutine.call() + self.assertEquals(TestCoroutine.coroutineCalls, [1,1,2,2,3,4,5]) + + def testWithTimeout(self): + Coroutine.currentTimeMillis = Mock(side_effect = [1,10,500,1200]) + coroutine = Coroutine(TestCoroutine.dummyCoroutineFunc,1,20).withTimeout(1000) + for i in range(1,20): + coroutine.call() + self.assertEquals(TestCoroutine.coroutineCalls, [1,2,3]) + From e6491846f51ff98283697343f93e15344e51027d Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Sun, 20 Apr 2014 16:45:30 +0100 Subject: [PATCH 11/13] cw: Got tests running (though issues with directories) --- BrickPython/Coroutine.py | 48 -------------------- BrickPython/Scheduler.py | 96 ++++++++++++---------------------------- test/TestScheduler.py | 11 ++--- 3 files changed, 32 insertions(+), 123 deletions(-) diff --git a/BrickPython/Coroutine.py b/BrickPython/Coroutine.py index a469691..4b17740 100644 --- a/BrickPython/Coroutine.py +++ b/BrickPython/Coroutine.py @@ -129,51 +129,3 @@ def timeoutFunc(timeoutMillis): result = Coroutine.runTillFirstCompletes(self, Coroutine(timeoutFunc, timeoutMillis)) return result -# -# self.scheduler.coroutines.remove( self ) -# self.scheduler.semaphore.release() - -class GeneratorCoroutineWrapper(Coroutine): - '''Internal: Wraps a generator-style coroutine with a thread''' - - def __init__(self, scheduler, generator): - '''`scheduler` - the main Scheduler object - `generator` - the generator object created by calling the generator function''' - Coroutine.__init__(self) - self.scheduler = scheduler - self.stopEvent = threading.Event() - self.generator = generator - self.thread = threading.Thread(target=self.action) - self.thread.setDaemon(True) # Daemon threads don't prevent the process from exiting. - self.thread.start() - - def action(self): - 'The thread entry function - executed within thread `thread`' - try: - self.semaphore.acquire() - while not self.stopEvent.is_set(): - self.generator.next() - self.scheduler.semaphore.release() - self.semaphore.acquire() - self.generator.throw(StopCoroutineException) - except (StopCoroutineException, StopIteration): - pass - except Exception as e: - self.scheduler.lastExceptionCaught = e - logging.info( "Scheduler - caught: %r" % (e) ) - exc_type, exc_value, exc_traceback = sys.exc_info() - trace = "".join(traceback.format_tb(exc_traceback)) - logging.debug( "Traceback (latest call first):\n %s" % trace ) - - self.scheduler.coroutines.remove( self ) - self.scheduler.semaphore.release() - - def next(self): - 'Runs a bit of processing (next on the generator) - executed from the scheduler thread - returns only when processing has completed' - self.semaphore.release() - self.scheduler.semaphore.acquire() - - def stop(self): - 'Causes the thread to stop - executed from the scheduler thread' - self.stopEvent.set() - diff --git a/BrickPython/Scheduler.py b/BrickPython/Scheduler.py index d537cfd..c78fc62 100644 --- a/BrickPython/Scheduler.py +++ b/BrickPython/Scheduler.py @@ -1,75 +1,35 @@ # Scheduler -# Support for coroutines using Python generator functions. +# Support for coroutines using either Python generator functions or thread-based coroutines. # # Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. -import datetime -import logging -import sys, traceback import threading - -class StopCoroutineException( Exception ): - '''Exception used to stop a coroutine''' - pass - -ProgramStartTime = datetime.datetime.now() - -class Coroutine(): - def __init__(self): - #: Semaphore is one when blocked, 0 when running - self.semaphore = threading.Semaphore(0) - - def action(self): - pass - +from Coroutine import Coroutine, StopCoroutineException class GeneratorCoroutineWrapper(Coroutine): '''Internal: Wraps a generator-style coroutine with a thread''' - def __init__(self, scheduler, generator): + def __init__(self, generator): '''`scheduler` - the main Scheduler object `generator` - the generator object created by calling the generator function''' - Coroutine.__init__(self) - self.scheduler = scheduler - self.stopEvent = threading.Event() + Coroutine.__init__(self, self.action) self.generator = generator - self.thread = threading.Thread(target=self.action) - self.thread.setDaemon(True) # Daemon threads don't prevent the process from exiting. - self.thread.start() def action(self): 'The thread entry function - executed within thread `thread`' - try: - self.semaphore.acquire() - while not self.stopEvent.is_set(): - self.generator.next() - self.scheduler.semaphore.release() - self.semaphore.acquire() - self.generator.throw(StopCoroutineException) - except (StopCoroutineException, StopIteration): - pass - except Exception as e: - self.scheduler.lastExceptionCaught = e - logging.info( "Scheduler - caught: %r" % (e) ) - exc_type, exc_value, exc_traceback = sys.exc_info() - trace = "".join(traceback.format_tb(exc_traceback)) - logging.debug( "Traceback (latest call first):\n %s" % trace ) - - self.scheduler.coroutines.remove( self ) - self.scheduler.semaphore.release() - - def next(self): - 'Runs a bit of processing (next on the generator) - executed from the scheduler thread - returns only when processing has completed' - self.semaphore.release() - self.scheduler.semaphore.acquire() + for _ in self.generator: + Coroutine.wait() def stop(self): - 'Causes the thread to stop - executed from the scheduler thread' - self.stopEvent.set() + try: + self.generator.throw(StopCoroutineException()) + except StopCoroutineException: + pass + Coroutine.stop(self) class Scheduler(): - ''' This manages an arbitrary number of coroutines (implemented as generator functions), supporting + ''' This manages an arbitrary number of coroutines (including generator functions), supporting invoking each every *timeMillisBetweenWorkCalls*, and detecting when each has completed. It supports one special coroutine - the updatorCoroutine, which is invoked before and after all the other ones. @@ -77,23 +37,22 @@ class Scheduler(): timeMillisBetweenWorkCalls = 50 + @staticmethod + def makeCoroutine(coroutineOrGenerator): + return coroutineOrGenerator if coroutineOrGenerator is Coroutine else GeneratorCoroutineWrapper(coroutineOrGenerator) + + @staticmethod def currentTimeMillis(): - 'Answers the time in floating point milliseconds since program start.' - global ProgramStartTime - c = datetime.datetime.now() - ProgramStartTime - return c.days * (3600.0 * 1000 * 24) + c.seconds * 1000.0 + c.microseconds / 1000.0 + return Coroutine.currentTimeMillis() def __init__(self, timeMillisBetweenWorkCalls = 50): Scheduler.timeMillisBetweenWorkCalls = timeMillisBetweenWorkCalls self.coroutines = [] self.timeOfLastCall = Scheduler.currentTimeMillis() - self.updateCoroutine = GeneratorCoroutineWrapper( self, self.nullCoroutine() ) # for testing - usually replaced. + self.updateCoroutine = Scheduler.makeCoroutine( self.nullCoroutine() ) # for testing - usually replaced. #: The most recent exception raised by a coroutine: self.lastExceptionCaught = Exception("None") - #: Semaphore is one when blocked (running a coroutine), 0 when active - self.semaphore = threading.Semaphore(0) - def doWork(self): 'Executes all the coroutines, handling exceptions' @@ -102,11 +61,14 @@ def doWork(self): if timeNow == self.timeOfLastCall: # Ensure each call gets a different timer value. return self.timeOfLastCall = timeNow - self.updateCoroutine.next() + self.updateCoroutine.call() for coroutine in self.coroutines[:]: # Copy of coroutines, so it doesn't matter removing one - coroutine.next() + coroutine.call() + if not coroutine.is_alive(): + self.coroutines.remove(coroutine) + self.lastExceptionCaught = coroutine.lastExceptionCaught - self.updateCoroutine.next() + self.updateCoroutine.call() def timeMillisToNextCall(self): 'Wait time before the next doWork call should be called.' @@ -118,7 +80,7 @@ def addSensorCoroutine(self, *coroutineList): '''Adds one or more new sensor/program coroutines to be scheduled, answering the last one to be added. Sensor coroutines are scheduled *before* Action coroutines''' for generatorFunction in coroutineList: - latestAdded = GeneratorCoroutineWrapper(self, generatorFunction) + latestAdded = Scheduler.makeCoroutine(generatorFunction) self.coroutines.insert(0, latestAdded) return generatorFunction @@ -126,14 +88,14 @@ def addActionCoroutine(self, *coroutineList): '''Adds one or more new motor control coroutines to be scheduled, answering the last coroutine to be added. Action coroutines are scheduled *after* Sensor coroutines''' for generatorFunction in coroutineList: - latestAdded = GeneratorCoroutineWrapper(self, generatorFunction) + latestAdded = Scheduler.makeCoroutine(generatorFunction) self.coroutines.append(latestAdded) return generatorFunction def setUpdateCoroutine(self, coroutine): # Private - set the coroutine that manages the interaction with the BrickPi. # The coroutine will be invoked once at the start and once at the end of each doWork call. - self.updateCoroutine = GeneratorCoroutineWrapper(self, coroutine) + self.updateCoroutine = Scheduler.makeCoroutine(coroutine) def findCoroutineForGenerator(self, generator): return (c for c in self.coroutines if c.generator == generator).next() @@ -157,7 +119,7 @@ def stillRunning( self, *coroutineList ): return any( c in self.coroutines for c in coroutineList ) ############################################################################################# - # Coroutines + # Generator-based coroutines. Kept for backward compatibility. ############################################################################################# @staticmethod diff --git a/test/TestScheduler.py b/test/TestScheduler.py index 132249a..a0f54d2 100644 --- a/test/TestScheduler.py +++ b/test/TestScheduler.py @@ -9,7 +9,8 @@ -from BrickPython.BrickPiWrapper import * +from BrickPython.BrickPiWrapper import BrickPiWrapper +from BrickPython.Scheduler import Scheduler import unittest import logging from mock import * @@ -58,15 +59,9 @@ def checkCoroutineFinished(coroutine): def setUp(self): TestScheduler.coroutineCalls = [] self.scheduler = Scheduler() - # Yes - we can use @Patch to do the following for specific tests, but that fails horribly when - # tests are run as python setup.py test - TestScheduler.saveCurrentTimeMillis = Scheduler.currentTimeMillis + # Don't need to replace this afterward - tests never use real time. Scheduler.currentTimeMillis = Mock( side_effect = xrange(0,10000) ) # Each call answers the next integer - def tearDown(self): - pass -# Scheduler.currentTimeMillis = TestScheduler.saveCurrentTimeMillis - def testCoroutinesGetCalledUntilDone(self): # When we start a motor coroutine coroutine = TestScheduler.dummyCoroutine() From 98c3f3c933215646ee43f78185681112266927d2 Mon Sep 17 00:00:00 2001 From: Charles Weir Date: Sun, 20 Apr 2014 17:20:17 +0100 Subject: [PATCH 12/13] cw: Got all tests running correctly. --- BrickPython/Coroutine.py | 3 +++ test/TestCoroutine.py | 7 +++---- test/TestMotor.py | 6 ++++++ test/TestScheduler.py | 17 +++++++++++------ 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/BrickPython/Coroutine.py b/BrickPython/Coroutine.py index 4b17740..5465a1a 100644 --- a/BrickPython/Coroutine.py +++ b/BrickPython/Coroutine.py @@ -62,6 +62,9 @@ def call(self, param = None): self.callerSemaphore.acquire() if self.stopEvent.is_set(): self.join() # Ensure that is_alive is false on exit. + # For testing - assertions within coroutines must be passed back to main thread. + if self.lastExceptionCaught != None and isinstance(self.lastExceptionCaught, AssertionError): + raise self.lastExceptionCaught return self.callResult def stop(self): diff --git a/test/TestCoroutine.py b/test/TestCoroutine.py index 3d3ca0f..67c4ca8 100644 --- a/test/TestCoroutine.py +++ b/test/TestCoroutine.py @@ -37,9 +37,11 @@ def dummyCoroutineFuncThatDoesCleanup(): def setUp(self): TestCoroutine.coroutineCalls = [] + TestCoroutine.oldCurrentTimeMillis = Coroutine.currentTimeMillis + Coroutine.currentTimeMillis = Mock(side_effect = [1,10,500,1200]) def tearDown(self): - pass + Coroutine.currentTimeMillis = TestCoroutine.oldCurrentTimeMillis def testCoroutinesGetCalledUntilDone(self): # When we start a coroutine @@ -88,8 +90,6 @@ def dummyCoroutineFuncWaiting1Sec(): Coroutine.waitMilliseconds(1000) coroutine = Coroutine(dummyCoroutineFuncWaiting1Sec) - # Doesn't matter not restoring this; tests never use real time: - Coroutine.currentTimeMillis = Mock(side_effect = [1,10,500,1200]) for i in range(1,3): coroutine.call() self.assertTrue(coroutine.is_alive(),"Coroutine dead at call %d" % i) @@ -132,7 +132,6 @@ def testRunCoroutinesUntilAllComplete(self): self.assertEquals(TestCoroutine.coroutineCalls, [1,1,2,2,3,4,5]) def testWithTimeout(self): - Coroutine.currentTimeMillis = Mock(side_effect = [1,10,500,1200]) coroutine = Coroutine(TestCoroutine.dummyCoroutineFunc,1,20).withTimeout(1000) for i in range(1,20): coroutine.call() diff --git a/test/TestMotor.py b/test/TestMotor.py index 76ce01f..3471824 100644 --- a/test/TestMotor.py +++ b/test/TestMotor.py @@ -12,12 +12,18 @@ class TestMotor(unittest.TestCase): ''' Tests for the Motor class, especially for PID Servo Motor functionality''' def setUp(self): + self.saveTime = Scheduler.currentTimeMillis + Scheduler.currentTimeMillis = Mock(side_effect = xrange(1,10000)) self.bp = BrickPiWrapper() motor = self.motor = self.bp.motor( 'A' ) #motor.position = Mock() motor.timeMillis = Mock() motor.timeMillis.side_effect = range(0,9999) + def tearDown(self): + Scheduler.currentTimeMillis = self.saveTime + unittest.TestCase.tearDown(self) + def testZeroPosition( self ): motor = self.motor # When the motor is zeroed at absolute position 1 diff --git a/test/TestScheduler.py b/test/TestScheduler.py index a0f54d2..17bcb5b 100644 --- a/test/TestScheduler.py +++ b/test/TestScheduler.py @@ -43,7 +43,7 @@ def dummyCoroutineThatTakesTime(): @staticmethod def dummyCoroutineThatThrowsException(): - raise Exception() + raise Exception("Hello") yield @staticmethod @@ -58,9 +58,13 @@ def checkCoroutineFinished(coroutine): def setUp(self): TestScheduler.coroutineCalls = [] - self.scheduler = Scheduler() - # Don't need to replace this afterward - tests never use real time. + TestScheduler.oldCurrentTimeMillis = Scheduler.currentTimeMillis Scheduler.currentTimeMillis = Mock( side_effect = xrange(0,10000) ) # Each call answers the next integer + self.scheduler = Scheduler() + + def tearDown(self): + Scheduler.currentTimeMillis = TestScheduler.oldCurrentTimeMillis + def testCoroutinesGetCalledUntilDone(self): # When we start a motor coroutine @@ -162,7 +166,8 @@ def testWaitMilliseconds(self): for i in self.scheduler.waitMilliseconds(10): pass # that's about the time that will have passed: - assert( self.scheduler.currentTimeMillis() in range(10,12) ) + timeNow = self.scheduler.currentTimeMillis() + assert( timeNow in range(10,12), "Wrong time " ) def testRunTillFirstCompletes(self): # When we run three coroutines using runTillFirstCompletes: @@ -224,12 +229,12 @@ def timeCheckerCoroutine(self): while True: yield newTime = Scheduler.currentTimeMillis() - self.assertNotEquals( newTime, time ) + self.assertNotEquals( newTime, time, "Time same for two scheduler calls" ) time = newTime def testEachCallToACoroutineGetsADifferentTime(self): - scheduler = Scheduler() Scheduler.currentTimeMillis = Mock( side_effect = [0,0,0,0,0,0,0,0,0,0,1,2,3,4,5] ) + scheduler = Scheduler() # For any coroutine, scheduler.setUpdateCoroutine( self.timeCheckerCoroutine() ) # We can guarantee that the timer always increments between calls (for speed calculations etc). From dde30ed08784ee954679a85ab095e16dd1ada3f0 Mon Sep 17 00:00:00 2001 From: Python3pkg Date: Wed, 17 May 2017 23:18:28 -0700 Subject: [PATCH 13/13] Convert to python3 --- BrickPython/BrickPi.py | 6 +- BrickPython/BrickPi.py.bak | 546 ++++++++++++++++++++++ BrickPython/BrickPiWrapper.py | 20 +- BrickPython/BrickPiWrapper.py.bak | 76 +++ BrickPython/CommandLineApplication.py | 2 +- BrickPython/CommandLineApplication.py.bak | 24 + BrickPython/Motor.py | 2 +- BrickPython/Motor.py.bak | 180 +++++++ BrickPython/Scheduler.py | 8 +- BrickPython/Scheduler.py.bak | 171 +++++++ BrickPython/Sensor.py | 2 +- BrickPython/Sensor.py.bak | 151 ++++++ BrickPython/TkApplication.py | 4 +- BrickPython/TkApplication.py.bak | 60 +++ ExamplePrograms/SimpleApp.py | 4 +- ExamplePrograms/SimpleApp.py.bak | 29 ++ ExamplePrograms/TrialApp.py | 4 +- ExamplePrograms/TrialApp.py.bak | 43 ++ docs/conf.py | 16 +- docs/conf.py.bak | 279 +++++++++++ test/TestMotor.py | 38 +- test/TestMotor.py.bak | 188 ++++++++ test/TestScheduler.py | 10 +- test/TestScheduler.py.bak | 310 ++++++++++++ test/TestSensor.py | 18 +- test/TestSensor.py.bak | 95 ++++ 26 files changed, 2219 insertions(+), 67 deletions(-) create mode 100644 BrickPython/BrickPi.py.bak create mode 100644 BrickPython/BrickPiWrapper.py.bak create mode 100644 BrickPython/CommandLineApplication.py.bak create mode 100644 BrickPython/Motor.py.bak create mode 100644 BrickPython/Scheduler.py.bak create mode 100644 BrickPython/Sensor.py.bak create mode 100644 BrickPython/TkApplication.py.bak create mode 100644 ExamplePrograms/SimpleApp.py.bak create mode 100644 ExamplePrograms/TrialApp.py.bak create mode 100644 docs/conf.py.bak create mode 100644 test/TestMotor.py.bak create mode 100644 test/TestScheduler.py.bak create mode 100644 test/TestSensor.py.bak diff --git a/BrickPython/BrickPi.py b/BrickPython/BrickPi.py index fd6ee7c..a131080 100644 --- a/BrickPython/BrickPi.py +++ b/BrickPython/BrickPi.py @@ -205,9 +205,9 @@ def upd(self,I2C_PORT): #Show button values def show_val(self): - print "ljb","rjb","d","c","b","a","l2","r2","l1","r1","tri","cir","cro","sqr","ljx","ljy","rjx","rjy" - print self.ljb," ",self.rjb," ",self.d,self.c,self.b,self.a,self.l2,"",self.r2,"",self.l1,"",self.r1,"",self.tri," ",self.cir," ",self.cro," ",self.sqr," ",self.ljx," ",self.ljy," ",self.rjx," ",self.rjy - print "" + print("ljb","rjb","d","c","b","a","l2","r2","l1","r1","tri","cir","cro","sqr","ljx","ljy","rjx","rjy") + print(self.ljb," ",self.rjb," ",self.d,self.c,self.b,self.a,self.l2,"",self.r2,"",self.l1,"",self.r1,"",self.tri," ",self.cir," ",self.cro," ",self.sqr," ",self.ljx," ",self.ljy," ",self.rjx," ",self.rjy) + print("") def BrickPiChangeAddress(OldAddr, NewAddr): diff --git a/BrickPython/BrickPi.py.bak b/BrickPython/BrickPi.py.bak new file mode 100644 index 0000000..fd6ee7c --- /dev/null +++ b/BrickPython/BrickPi.py.bak @@ -0,0 +1,546 @@ +# From project brickpi_python, with small amendments to support BrickPython. + +# Jaikrishna +# Karan Nayan +# John Cole +# Initial Date: June 24, 2013 +# Last Updated: Nov 4, 2013 +# http://www.dexterindustries.com/ +# +# These files have been made available online through a Creative Commons Attribution-ShareAlike 3.0 license. +# (http://creativecommons.org/licenses/by-sa/3.0/) +# +# Ported from Matthew Richardson's BrickPi library for C +# This library can be used in RaspberryPi to communicate with BrickPi +# Major Changes from C code: +# - The timeout parameter for BrickPiRx is in seconds expressed as a floating value +# - Instead of Call by Reference in BrickPiRx, multiple values are returned and then copied to the main Array appropriately +# - BrickPiStruct Variables are assigned to None and then modified to avoid appending which may lead to errors + +################################################################################################################## +# Debugging: +# - NOTE THAT DEBUGGING ERROR MESSAGES ARE TURNED OFF BY DEFAULT. To debug, just take the comment out of Line 29. +# +# If you #define DEBUG in the program, the BrickPi.h drivers will print debug messages to the terminal. One common message is +# "BrickPiRx error: -2", in function BrickPiUpdateValues(). This is caused by an error in the communication with one of the +# microcontrollers on the BrickPi. When this happens, the drivers automatically re-try the communication several times before the +# function gives up and returns -1 (unsuccessful) to the user-program. + +# Function BrickPiUpdateValues() will either return 0 (success), or -1 (error that could not automatically be resolved, even after +# re-trying several times). We have rarely had BrickPiUpdateValues() retry more than once before the communication was successful. +# A known cause for "BrickPiRx error: -2" is the RPi splitting the UART message. Sometimes the RPi will send e.g. 3 bytes, wait a +# while, and then send 4 more, when it should have just sent 7 at once. During the pause between the packs of bytes, the BrickPi +# microcontrollers will think the transmission is complete, realize the message doesn't make sense, throw it away, and not return +# a message to the RPi. The RPi will then fail to receive a message in the specified amount of time, timeout, and then retry the +# communication. + +# If a function returns 0, it completed successfully. If it returns -1, there was an error (most likely a communications error). + +# Function BrickPiRx() (background function that receives UART messages from the BrickPi) can return 0 (success), -1 (undefined error that shouldn't have happened, e.g. a filesystem error), -2 (timeout: the RPi didn't receive any UART communication from the BrickPi within the specified time), -4 (the message was too short to even contain a valid header), -5 (communication checksum error), or -6 (the number of bytes received was less than specified by the length byte). + + +import time +import logging +import os +if os.uname()[4].startswith("arm"): # If we're on a Raspberry Pi + from serial import * + ser = Serial() +else: # Mock out the serial port - it seems to work. + from mock import * + ser = Mock() + +#import serial +ser.port='/dev/ttyAMA0' +ser.baudrate = 500000 + +# ser.writeTimeout = 0.0005 +# ser.timeout = 0.0001 + +# DEBUG = 1 # Remove to hide errors + +PORT_A = 0 +PORT_B = 1 +PORT_C = 2 +PORT_D = 3 + +PORT_1 = 0 +PORT_2 = 1 +PORT_3 = 2 +PORT_4 = 3 + +MASK_D0_M = 0x01 +MASK_D1_M = 0x02 +MASK_9V = 0x04 +MASK_D0_S = 0x08 +MASK_D1_S = 0x10 + +BYTE_MSG_TYPE = 0 # MSG_TYPE is the first byte. +MSG_TYPE_CHANGE_ADDR = 1 # Change the UART address. +MSG_TYPE_SENSOR_TYPE = 2 # Change/set the sensor type. +MSG_TYPE_VALUES = 3 # Set the motor speed and direction, and return the sesnors and encoders. +MSG_TYPE_E_STOP = 4 # Float motors immidately +MSG_TYPE_TIMEOUT_SETTINGS = 5 # Set the timeout +# New UART address (MSG_TYPE_CHANGE_ADDR) +BYTE_NEW_ADDRESS = 1 + +# Sensor setup (MSG_TYPE_SENSOR_TYPE) +BYTE_SENSOR_1_TYPE = 1 +BYTE_SENSOR_2_TYPE = 2 + +BYTE_TIMEOUT=1 + +TYPE_MOTOR_PWM = 0 +TYPE_MOTOR_SPEED = 1 +TYPE_MOTOR_POSITION = 2 + +TYPE_SENSOR_RAW = 0 # - 31 +TYPE_SENSOR_LIGHT_OFF = 0 +TYPE_SENSOR_LIGHT_ON = (MASK_D0_M | MASK_D0_S) +TYPE_SENSOR_TOUCH = 32 +TYPE_SENSOR_ULTRASONIC_CONT = 33 +TYPE_SENSOR_ULTRASONIC_SS = 34 +TYPE_SENSOR_RCX_LIGHT = 35 # tested minimally +TYPE_SENSOR_COLOR_FULL = 36 +TYPE_SENSOR_COLOR_RED = 37 +TYPE_SENSOR_COLOR_GREEN = 38 +TYPE_SENSOR_COLOR_BLUE = 39 +TYPE_SENSOR_COLOR_NONE = 40 +TYPE_SENSOR_I2C = 41 +TYPE_SENSOR_I2C_9V = 42 + +BIT_I2C_MID = 0x01 # Do one of those funny clock pulses between writing and reading. defined for each device. +BIT_I2C_SAME = 0x02 # The transmit data, and the number of bytes to read and write isn't going to change. defined for each device. + +INDEX_RED = 0 +INDEX_GREEN = 1 +INDEX_BLUE = 2 +INDEX_BLANK = 3 + +Array = [0] * 256 +BytesReceived = None +Bit_Offset = 0 +Retried = 0 + +class BrickPiStruct: + Address = [ 1, 2 ] + MotorSpeed = [0] * 4 + + MotorEnable = [0] * 4 + + EncoderOffset = [None] * 4 + Encoder = [None] * 4 + + Sensor = [None] * 4 + SensorArray = [ [None] * 4 for i in range(4) ] + SensorType = [0] * 4 + SensorSettings = [ [None] * 8 for i in range(4) ] + + SensorI2CDevices = [None] * 4 + SensorI2CSpeed = [None] * 4 + SensorI2CAddr = [ [None] * 8 for i in range(4) ] + SensorI2CWrite = [ [None] * 8 for i in range(4) ] + SensorI2CRead = [ [None] * 8 for i in range(4) ] + SensorI2COut = [ [ [None] * 16 for i in range(8) ] for i in range(4) ] + SensorI2CIn = [ [ [None] * 16 for i in range(8) ] for i in range(4) ] + Timeout = 0 +BrickPi = BrickPiStruct() + +#PSP Mindsensors class +class button: + #Initialize all the buttons to 0 + def init(self): + self.l1=0 + self.l2=0 + self.r1=0 + self.r2=0 + self.a=0 + self.b=0 + self.c=0 + self.d=0 + self.tri=0 + self.sqr=0 + self.cir=0 + self.cro=0 + self.ljb=0 + self.ljx=0 + self.ljy=0 + self.rjx=0 + rjy=0 + + #Update all the buttons + def upd(self,I2C_PORT): + #For all buttons: + #0: Unpressed + #1: Pressed + # + #Left and right joystick: -127 to 127 + self.ljb=~(BrickPi.SensorI2CIn[I2C_PORT][0][0]>>1)&1 + self.rjb=~(BrickPi.SensorI2CIn[I2C_PORT][0][0]>>2)&1 + + #For buttons a,b,c,d + self.d=~(BrickPi.SensorI2CIn[I2C_PORT][0][0]>>4)&1 + self.c=~(BrickPi.SensorI2CIn[I2C_PORT][0][0]>>5)&1 + self.b=~(BrickPi.SensorI2CIn[I2C_PORT][0][0]>>6)&1 + self.a=~(BrickPi.SensorI2CIn[I2C_PORT][0][0]>>7)&1 + + #For buttons l1,l2,r1,r2 + self.l2=~(BrickPi.SensorI2CIn[I2C_PORT][0][1])&1 + self.r2=~(BrickPi.SensorI2CIn[I2C_PORT][0][1]>>1)&1 + self.l1=~(BrickPi.SensorI2CIn[I2C_PORT][0][1]>>2)&1 + self.r1=~(BrickPi.SensorI2CIn[I2C_PORT][0][1]>>3)&1 + + #For buttons square,triangle,cross,circle + self.tri=~(BrickPi.SensorI2CIn[I2C_PORT][0][1]>>4)&1 + self.cir=~(BrickPi.SensorI2CIn[I2C_PORT][0][1]>>5)&1 + self.cro=~(BrickPi.SensorI2CIn[I2C_PORT][0][1]>>6)&1 + self.sqr=~(BrickPi.SensorI2CIn[I2C_PORT][0][1]>>7)&1 + + #Left joystick x and y , -127 to 127 + self.ljx=BrickPi.SensorI2CIn[I2C_PORT][0][2]-128 + self.ljy=~BrickPi.SensorI2CIn[I2C_PORT][0][3]+129 + + #Right joystick x and y , -127 to 127 + self.rjx=BrickPi.SensorI2CIn[I2C_PORT][0][4]-128 + self.rjy=~BrickPi.SensorI2CIn[I2C_PORT][0][5]+129 + + #Show button values + def show_val(self): + print "ljb","rjb","d","c","b","a","l2","r2","l1","r1","tri","cir","cro","sqr","ljx","ljy","rjx","rjy" + print self.ljb," ",self.rjb," ",self.d,self.c,self.b,self.a,self.l2,"",self.r2,"",self.l1,"",self.r1,"",self.tri," ",self.cir," ",self.cro," ",self.sqr," ",self.ljx," ",self.ljy," ",self.rjx," ",self.rjy + print "" + + +def BrickPiChangeAddress(OldAddr, NewAddr): + Array[BYTE_MSG_TYPE] = MSG_TYPE_CHANGE_ADDR; + Array[BYTE_NEW_ADDRESS] = NewAddr; + BrickPiTx(OldAddr, 2, Array) + res, BytesReceived, InArray = BrickPiRx(0.005000) + if res : + return -1 + for i in range(len(InArray)): + Array[i] = InArray[i] + if not (BytesReceived == 1 and Array[BYTE_MSG_TYPE] == MSG_TYPE_CHANGE_ADDR): + return -1 + return 0 + +def BrickPiSetTimeout(): + for i in range(2): + Array[BYTE_MSG_TYPE] = MSG_TYPE_TIMEOUT_SETTINGS + Array[BYTE_TIMEOUT] = BrickPi.Timeout&0xFF + Array[BYTE_TIMEOUT + 1] = (BrickPi.Timeout / 256 ) & 0xFF + Array[BYTE_TIMEOUT + 2] = (BrickPi.Timeout / 65536 ) & 0xFF + Array[BYTE_TIMEOUT + 3] = (BrickPi.Timeout / 16777216) & 0xFF + BrickPiTx(BrickPi.Address[i], 5, Array) + res, BytesReceived, InArray = BrickPiRx(0.002500) + if res : + return -1 + for j in range(len(InArray)): + Array[j] = InArray[j] + if not (BytesReceived == 1 and Array[BYTE_MSG_TYPE] == MSG_TYPE_TIMEOUT_SETTINGS): + return -1 + i+=1 + return 0 + +def motorRotateDegree(power,deg,port,sampling_time=.1): + """Rotate the selected motors by specified degre + + Args: + power : an array of the power values at which to rotate the motors (0-255) + deg : an array of the angle's (in degrees) by which to rotate each of the motor + port : an array of the port's on which the motor is connected + sampling_time : (optional) the rate(in seconds) at which to read the data in the encoders + + Returns: + 0 on success + + Usage: + Pass the arguments in a list. if a single motor has to be controlled then the arguments should be + passed like elements of an array,e.g, motorRotateDegree([255],[360],[PORT_A]) or + motorRotateDegree([255,255],[360,360],[PORT_A,PORT_B]) + """ + + num_motor=len(power) #Number of motors being used + init_val=[0]*num_motor + final_val=[0]*num_motor + BrickPiUpdateValues() + for i in range(num_motor): + BrickPi.MotorEnable[port[i]] = 1 #Enable the Motors + power[i]=abs(power[i]) + BrickPi.MotorSpeed[port[i]] = power[i] if deg[i]>0 else -power[i] #For running clockwise and anticlockwise + init_val[i]=BrickPi.Encoder[port[i]] #Initial reading of the encoder + final_val[i]=init_val[i]+(deg[i]*2) #Final value when the motor has to be stopped;One encoder value counts for 0.5 degrees + run_stat=[0]*num_motor + while True: + result = BrickPiUpdateValues() #Ask BrickPi to update values for sensors/motors + if not result : + for i in range(num_motor): #Do for each of the motors + if run_stat[i]==1: + continue + if(deg[i]>0 and final_val[i]>init_val[i]) or (deg[i]<0 and final_val[i]0 else power[i] #Run the motors in reverse direction to stop instantly + BrickPiUpdateValues() + time.sleep(.04) + BrickPi.MotorEnable[port[i]] = 0 + BrickPiUpdateValues() + time.sleep(sampling_time) #sleep for the sampling time given (default:100 ms) + if(all(e==1 for e in run_stat)): #If all the motors have already completed their rotation, then stop + break + return 0 + +def GetBits( byte_offset, bit_offset, bits): + global Bit_Offset + result = 0 + i = bits + while i: + result *= 2 + result |= ((Array[(byte_offset + ((bit_offset + Bit_Offset + (i-1)) / 8))] >> ((bit_offset + Bit_Offset + (i-1)) % 8)) & 0x01) + i -= 1 + Bit_Offset += bits + return result + + +def BitsNeeded(value): + for i in range(32): + if not value: + return i + value /= 2 + return 31 + + +def AddBits(byte_offset, bit_offset, bits, value): + global Bit_Offset + for i in range(bits): + if(value & 0x01): + Array[(byte_offset + ((bit_offset + Bit_Offset + i)/ 8))] |= (0x01 << ((bit_offset + Bit_Offset + i) % 8)); + value /=2 + Bit_Offset += bits + + +def BrickPiSetupSensors(): + global Array + global Bit_Offset + global BytesReceived + for i in range(2): + Array = [0] * 256 + Bit_Offset = 0 + Array[BYTE_MSG_TYPE] = MSG_TYPE_SENSOR_TYPE + Array[BYTE_SENSOR_1_TYPE] = BrickPi.SensorType[PORT_1 + i*2 ] + Array[BYTE_SENSOR_2_TYPE] = BrickPi.SensorType[PORT_2 + i*2 ] + for ii in range(2): + port = i*2 + ii + if(Array[BYTE_SENSOR_1_TYPE + ii] == TYPE_SENSOR_I2C or Array[BYTE_SENSOR_1_TYPE + ii] == TYPE_SENSOR_I2C_9V ): + AddBits(3,0,8,BrickPi.SensorI2CSpeed[port]) + + if(BrickPi.SensorI2CDevices[port] > 8): + BrickPi.SensorI2CDevices[port] = 8 + + if(BrickPi.SensorI2CDevices[port] == 0): + BrickPi.SensorI2CDevices[port] = 1 + + AddBits(3,0,3, (BrickPi.SensorI2CDevices[port] - 1)) + + for device in range(BrickPi.SensorI2CDevices[port]): + AddBits(3,0,7, (BrickPi.SensorI2CAddr[port][device] >> 1)) + AddBits(3,0,2, BrickPi.SensorSettings[port][device]) + if(BrickPi.SensorSettings[port][device] & BIT_I2C_SAME): + AddBits(3,0,4, BrickPi.SensorI2CWrite[port][device]) + AddBits(3,0,4, BrickPi.SensorI2CRead[port][device]) + + for out_byte in range(BrickPi.SensorI2CWrite[port][device]): + AddBits(3,0,8, BrickPi.SensorI2COut[port][device][out_byte]) + + tx_bytes = (((Bit_Offset + 7) / 8) + 3) #eq to UART_TX_BYTES + BrickPiTx(BrickPi.Address[i], tx_bytes , Array) + res, BytesReceived, InArray = BrickPiRx(0.500000) + if res : + return -1 + for i in range(len(InArray)): + Array[i]=InArray[i] + if not (BytesReceived ==1 and Array[BYTE_MSG_TYPE] == MSG_TYPE_SENSOR_TYPE) : + return -1 + return 0 + + +def BrickPiUpdateValues(): + global Array + global Bit_Offset + global Retried + ret = False + i = 0 + while i < 2 : + if not ret: + Retried = 0 + #Retry Communication from here, if failed + + Array = [0] * 256 + Array[BYTE_MSG_TYPE] = MSG_TYPE_VALUES + Bit_Offset = 0 + + for ii in range(2): + port = (i * 2) + ii + if(BrickPi.EncoderOffset[port]): + Temp_Value = BrickPi.EncoderOffset[port] + AddBits(1,0,1,1) + if Temp_Value < 0 : + Temp_ENC_DIR = 1 + Temp_Value *= -1 + Temp_BitsNeeded = BitsNeeded(Temp_Value) + 1 + AddBits(1,0,5, Temp_BitsNeeded) + Temp_Value *= 2 + Temp_Value |= Temp_ENC_DIR + AddBits(1,0, Temp_BitsNeeded, Temp_Value) + else: + AddBits(1,0,1,0) + + + for ii in range(2): + port = (i *2) + ii + speed = BrickPi.MotorSpeed[port] + direc = 0 + if speed<0 : + direc = 1 + speed *= -1 + if speed>255: + speed = 255 + AddBits(1,0,10,((((speed & 0xFF) << 2) | (direc << 1) | (BrickPi.MotorEnable[port] & 0x01)) & 0x3FF)) + + + for ii in range(2): + port = (i * 2) + ii + if(BrickPi.SensorType[port] == TYPE_SENSOR_I2C or BrickPi.SensorType[port] == TYPE_SENSOR_I2C_9V): + for device in range(BrickPi.SensorI2CDevices[port]): + if not (BrickPi.SensorSettings[port][device] & BIT_I2C_SAME): + AddBits(1,0,4, BrickPi.SensorI2CWrite[port][device]) + AddBits(1,0,4, BrickPi.SensorI2CRead[port][device]) + for out_byte in range(BrickPi.SensorI2CWrite[port][device]): + AddBits(1,0,8, BrickPi.SensorI2COut[port][device][out_byte]) + device += 1 + + + tx_bytes = (((Bit_Offset + 7) / 8 ) + 1) #eq to UART_TX_BYTES + BrickPiTx(BrickPi.Address[i], tx_bytes, Array) + + result, BytesReceived, InArray = BrickPiRx(0.007500) #check timeout + for j in range(len(InArray)): + Array[j]=InArray[j] + + if result != -2 : + BrickPi.EncoderOffset[(i * 2) + PORT_A] = 0 + BrickPi.EncoderOffset[(i * 2) + PORT_B] = 0 + + if (result or (Array[BYTE_MSG_TYPE] != MSG_TYPE_VALUES)): + logging.debug( "BrickPiRxError: %d" % result ) + + if Retried < 2 : + ret = True + Retried += 1 + #print "Retry", Retried + continue + else: + logging.debug("BrickPiRx - all retried failed") + return -1 + + + ret = False + Bit_Offset = 0 + + Temp_BitsUsed = [] + Temp_BitsUsed.append(GetBits(1,0,5)) + Temp_BitsUsed.append(GetBits(1,0,5)) + + for ii in range(2): + Temp_EncoderVal = GetBits(1,0, Temp_BitsUsed[ii]) + if Temp_EncoderVal & 0x01 : + Temp_EncoderVal /= 2 + BrickPi.Encoder[ii + i*2] = Temp_EncoderVal*(-1) + else: + BrickPi.Encoder[ii + i*2] = Temp_EncoderVal / 2 + + + for ii in range(2): + port = ii + (i * 2) + if BrickPi.SensorType[port] == TYPE_SENSOR_TOUCH : + BrickPi.Sensor[port] = GetBits(1,0,1) + elif BrickPi.SensorType[port] == TYPE_SENSOR_ULTRASONIC_CONT or BrickPi.SensorType[port] == TYPE_SENSOR_ULTRASONIC_SS : + BrickPi.Sensor[port] = GetBits(1,0,8) + elif BrickPi.SensorType[port] == TYPE_SENSOR_COLOR_FULL: + BrickPi.Sensor[port] = GetBits(1,0,3) + BrickPi.SensorArray[port][INDEX_BLANK] = GetBits(1,0,10) + BrickPi.SensorArray[port][INDEX_RED] = GetBits(1,0,10) + BrickPi.SensorArray[port][INDEX_GREEN] = GetBits(1,0,10) + BrickPi.SensorArray[port][INDEX_BLUE] = GetBits(1,0,10) + elif BrickPi.SensorType[port] == TYPE_SENSOR_I2C or BrickPi.SensorType[port] == TYPE_SENSOR_I2C_9V : + BrickPi.Sensor[port] = GetBits(1,0, BrickPi.SensorI2CDevices[port]) + for device in range(BrickPi.SensorI2CDevices[port]): + if (BrickPi.Sensor[port] & ( 0x01 << device)) : + for in_byte in range(BrickPi.SensorI2CRead[port][device]): + BrickPi.SensorI2CIn[port][device][in_byte] = GetBits(1,0,8) + else: #For all the light, color and raw sensors + BrickPi.Sensor[ii + (i * 2)] = GetBits(1,0,10) + + i += 1 + return 0 + + +def BrickPiSetup(): + if ser.isOpen(): + return -1 + ser.open() + if not ser.isOpen(): + return -1 + return 0 + + +def BrickPiTx(dest, ByteCount, OutArray): + tx_buffer = '' + tx_buffer+=chr(dest) + tx_buffer+=chr((dest+ByteCount+sum(OutArray[:ByteCount]))%256) + tx_buffer+=chr(ByteCount) + for i in OutArray[:ByteCount]: + tx_buffer+=chr(i) + ser.write(tx_buffer) + + +def BrickPiRx(timeout): + rx_buffer = '' + ser.timeout=0 + ot = time.time() + + while( ser.inWaiting() <= 0): + if time.time() - ot >= timeout : + return -2, 0 , [] + + if not ser.isOpen(): + return -1, 0 , [] + + try: + while ser.inWaiting(): + rx_buffer += ( ser.read(ser.inWaiting()) ) + #time.sleep(.000075) + except: + return -1, 0 , [] + + RxBytes=len(rx_buffer) + + if RxBytes < 2 : + return -4, 0 , [] + + if RxBytes < ord(rx_buffer[1])+2 : + return -6, 0 , [] + + CheckSum = 0 + for i in rx_buffer[1:]: + CheckSum += ord(i) + + InArray = [] + for i in rx_buffer[2:]: + InArray.append(ord(i)) + if (CheckSum % 256) != ord(rx_buffer[0]) : #Checksum equals sum(InArray)+len(InArray) + return -5, 0 , [] + + InBytes = RxBytes - 2 + + return 0, InBytes, InArray diff --git a/BrickPython/BrickPiWrapper.py b/BrickPython/BrickPiWrapper.py index 8a297ac..578c4c0 100644 --- a/BrickPython/BrickPiWrapper.py +++ b/BrickPython/BrickPiWrapper.py @@ -2,10 +2,10 @@ # # Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. -from Motor import Motor -from Sensor import Sensor -import BrickPi as BP -from Scheduler import Scheduler +from .Motor import Motor +from .Sensor import Sensor +from . import BrickPi as BP +from .Scheduler import Scheduler class BrickPiWrapper(Scheduler): ''' @@ -22,7 +22,7 @@ def __init__(self, portTypes = {} ): self.sensors = { } BP.BrickPiSetup() # setup the serial port for communication - for port, sensorType in portTypes.items(): + for port, sensorType in list(portTypes.items()): if isinstance(sensorType, int): sensor = Sensor(port, sensorType) else: @@ -46,7 +46,7 @@ def sensor( self, which ): def update(self): # Communicates with the BrickPi processor, sending current motor settings, and receiving sensor values. global BrickPi - for motor in self.motors.values(): + for motor in list(self.motors.values()): BP.BrickPi.MotorEnable[motor.port] = int(motor.enabled()) BP.BrickPi.MotorSpeed[motor.port] = motor.power() @@ -54,15 +54,15 @@ def update(self): # Takes about 6ms. BP.BrickPiUpdateValues() - for motor in self.motors.values(): + for motor in list(self.motors.values()): position = BP.BrickPi.Encoder[motor.port] - if not isinstance( position, ( int, long ) ): # For mac + if not isinstance( position, int ): # For mac position = 0 motor.updatePosition( position ) - for sensor in self.sensors.values(): + for sensor in list(self.sensors.values()): value = BP.BrickPi.Sensor[sensor.port] - if not isinstance( value, ( int, long ) ): # For mac + if not isinstance( value, int ): # For mac value = 0 sensor.updateValue( value ) diff --git a/BrickPython/BrickPiWrapper.py.bak b/BrickPython/BrickPiWrapper.py.bak new file mode 100644 index 0000000..8a297ac --- /dev/null +++ b/BrickPython/BrickPiWrapper.py.bak @@ -0,0 +1,76 @@ +# Wrapper class for the BrickPi() structure provided with the installation. +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + +from Motor import Motor +from Sensor import Sensor +import BrickPi as BP +from Scheduler import Scheduler + +class BrickPiWrapper(Scheduler): + ''' + This extends the Scheduler with functionality specific to the BrickPi + + The constructor takes a map giving the class for the sensor connected to each port: 1 through 5. + E.g. BrickPiWrapper( {'1': TouchSensor, '2': UltrasonicSensor } ) + + Motors and sensors are identified by their port names: motors are A to D; sensors 1 to 5. + ''' + def __init__(self, portTypes = {} ): + Scheduler.__init__(self) + self.motors = { 'A': Motor(BP.PORT_A, self), 'B': Motor(BP.PORT_B, self), 'C': Motor(BP.PORT_C, self), 'D': Motor(BP.PORT_D, self) } + self.sensors = { } + BP.BrickPiSetup() # setup the serial port for communication + + for port, sensorType in portTypes.items(): + if isinstance(sensorType, int): + sensor = Sensor(port, sensorType) + else: + sensor = sensorType(port) + self.sensors[sensor.idChar] = sensor + BP.BrickPi.SensorType[sensor.port] = sensor.type + BP.BrickPiSetupSensors() #Send the properties of sensors to BrickPi + + self.setUpdateCoroutine( self.updaterCoroutine() ) + + def motor( self, which ): + '''Answers the corresponding motor, e.g. motor('A') + ''' + return self.motors[which] + + def sensor( self, which ): + '''Answers the corresponding sensor, e.g. sensor('1') + ''' + return self.sensors[which] + + def update(self): + # Communicates with the BrickPi processor, sending current motor settings, and receiving sensor values. + global BrickPi + for motor in self.motors.values(): + BP.BrickPi.MotorEnable[motor.port] = int(motor.enabled()) + BP.BrickPi.MotorSpeed[motor.port] = motor.power() + + # Updates sensor readings, motor locations, and motor power settings. + # Takes about 6ms. + BP.BrickPiUpdateValues() + + for motor in self.motors.values(): + position = BP.BrickPi.Encoder[motor.port] + if not isinstance( position, ( int, long ) ): # For mac + position = 0 + motor.updatePosition( position ) + + for sensor in self.sensors.values(): + value = BP.BrickPi.Sensor[sensor.port] + if not isinstance( value, ( int, long ) ): # For mac + value = 0 + sensor.updateValue( value ) + + + def updaterCoroutine(self): + # Coroutine to call the update function. + while True: + self.update() + yield + + diff --git a/BrickPython/CommandLineApplication.py b/BrickPython/CommandLineApplication.py index ea5811d..39ff86e 100644 --- a/BrickPython/CommandLineApplication.py +++ b/BrickPython/CommandLineApplication.py @@ -4,7 +4,7 @@ # Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. -from BrickPiWrapper import BrickPiWrapper +from .BrickPiWrapper import BrickPiWrapper import time class CommandLineApplication(BrickPiWrapper): diff --git a/BrickPython/CommandLineApplication.py.bak b/BrickPython/CommandLineApplication.py.bak new file mode 100644 index 0000000..ea5811d --- /dev/null +++ b/BrickPython/CommandLineApplication.py.bak @@ -0,0 +1,24 @@ +# CommandLineApplication class. Provides a dummy scheduler for BrickPiWrapper. +# Applications using the BrickPi derive from this, implementing appropriate functionality. +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + + +from BrickPiWrapper import BrickPiWrapper +import time + +class CommandLineApplication(BrickPiWrapper): + ''' + Main application class for command-line only apps. Doesn't support user input. + ''' + + def __init__(self, sensorConfiguration={}): + '''Initialization: *sensorConfiguration* is a map as passed to BrickPiWrapper''' + BrickPiWrapper.__init__(self, sensorConfiguration ) + + def mainloop(self): + 'The main loop for the application - call this after initialization. Never returns.' + while True: + self.doWork() + time.sleep(self.timeMillisToNextCall() / 1000.0) + diff --git a/BrickPython/Motor.py b/BrickPython/Motor.py index 44952f8..888884d 100644 --- a/BrickPython/Motor.py +++ b/BrickPython/Motor.py @@ -2,7 +2,7 @@ # # Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. -from Scheduler import Scheduler +from .Scheduler import Scheduler import logging class PIDSetting(): diff --git a/BrickPython/Motor.py.bak b/BrickPython/Motor.py.bak new file mode 100644 index 0000000..44952f8 --- /dev/null +++ b/BrickPython/Motor.py.bak @@ -0,0 +1,180 @@ +# Motor and associated classes, representing a motor attached to one of the BrickPi ports. +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + +from Scheduler import Scheduler +import logging + +class PIDSetting(): + '''Settings for the PID servo algorithm. These may differ between motors. The default values are here. + + Distances are in clicks, times in ms, motor power between -255 and +255, the sum of distance is added 20 times/sec. + Speeds in clicks per second. + ''' + def __init__(self,distanceMultiplier=0.9,speedMultiplier=0.23,integratedDistanceMultiplier=(0.05/20.0), + closeEnoughPosition=4, closeEnoughSpeed=10.0): + #: Factor for motor power as function of distance - in power units per click distance. + self.distanceMultiplier = distanceMultiplier + #: Factor for motor power as function of speed - in power units per (click per millisecond) + self.speedMultiplier = speedMultiplier + #: Factor for motor power as a function of the integrated distance - in power units per (click-millisecond) + self.integratedDistanceMultiplier = integratedDistanceMultiplier + #: Distance in clicks from the target that we'll accept as got there. + self.closeEnoughPosition = closeEnoughPosition + #: Speed that we'll consider as close enough to zero. + self.closeEnoughSpeed = closeEnoughSpeed + + + def __repr__(self): + return "PID Setting (distanceMultiplier=%3.4f, speedMultiplier=%3.4f, integratedDistanceMultiplier=%3.4f)" % (self.distanceMultiplier, + self.speedMultiplier, self.integratedDistanceMultiplier) + + +class TimePosition(): + "Represents a motor position at a specific time. Time in milliseconds, position in clicks." + def __init__(self,time=0.0,position=0): + #: Time created + self.time = float(time) + #: Position at that time + self.position = int(position) + + def __repr__(self): + return "TimeDist (%.3f, %d)" % (self.time, self.position) + + def averageSpeedFrom(self, other): + 'Answers the speed in clicks per second coming to this point from other' + result = 0.0 + if self.time != other.time: + result = 1000.0 * (self.position - other.position) / (self.time - other.time) + return result + +class Motor(): + '''An NXT motor connected to a BrickPi port. + + A motor is identified by its idChar ('A' through 'D'). + It has a current position, relative to the basePosition it has set, and a current speed. + + It also defines coroutines to position it using the standard PID servo motor algorithm, and + to run at a specific speed. + ''' + def timeMillis(self): + # Answers the current time - member function so we can mock it easily for testing. + return Scheduler.currentTimeMillis() + + def __init__(self, port, scheduler = None): + self.port = port + #: Identifier for the motor + self.idChar = chr(port + ord('A')) + self._enabled = False + self._position = 0 + self._power = 0 + self.pidSetting = PIDSetting() + self.currentTP = self.previousTP = TimePosition(0, self.timeMillis()) + self.scheduler = scheduler + self.basePosition = 0 + + def setPIDSetting( self, pidSetting ): + 'Sets the parameters for the PID servo motor algorithm' + self.pidSetting = pidSetting + def setPower(self, p): + 'Sets the power to be sent to the motor' + self._power = int(p) + def power(self): + 'Answers the current power setting' + return self._power + def position(self): + 'Answers the current position' + return self.currentTP.position + def enabled(self): + 'Answers true if the motor is enabled' + return self._enabled + def enable(self, whether): + 'Sets whether the motor is enabled' + self._enabled = whether + + def zeroPosition(self): + 'Resets the motor base for its position to the current position.' + self.basePosition += self.position() + + def speed(self): + 'Answers the current speed calculated from the latest two position readings' + return self.currentTP.averageSpeedFrom( self.previousTP ) + + def __repr__(self): + return "Motor %s (location=%d, speed=%f)" % (self.idChar, self.position(), self.speed()) + + def updatePosition(self, newPosition): + # Called by the framework when the BrickPi provides a new motor position. + self.previousTP = self.currentTP + self.currentTP = TimePosition( self.timeMillis(), newPosition - self.basePosition ) + + def stopAndDisable(self): + 'Stops and disables the motor' + self.setPower(0) + self.enable(False) + logging.info("Motor %s stopped" % (self.idChar)) + + def moveTo( self, *args, **kwargs ): + 'Alternative name for coroutine positionUsingPIDAlgorithm' + return self.positionUsingPIDAlgorithm( *args, **kwargs ) + + def positionUsingPIDAlgorithm( self, target, timeoutMillis = 3000 ): + 'Coroutine to move the motor to position *target*, stopping after *timeoutMillis* if it hasnt reached it yet' + return self.scheduler.withTimeout( timeoutMillis, self.positionUsingPIDAlgorithmWithoutTimeout( target ) ) + + def positionUsingPIDAlgorithmWithoutTimeout( self, target ): + 'Coroutine to move the motor to position *target*, using the PID algorithm with the current PIDSettings' + distanceIntegratedOverTime = 0 # I bit of PID. + self.enable(True) + logging.info( "Motor %s moving to %d" % (self.idChar, target) ) + try: + while True: + delta = (target - self.currentTP.position) + distanceIntegratedOverTime += delta * (self.currentTP.time - self.previousTP.time) + speed = self.speed() + + if abs(delta) <= self.pidSetting.closeEnoughPosition and abs(speed) < self.pidSetting.closeEnoughSpeed: + break # Near enough - finish. + + power = (self.pidSetting.distanceMultiplier * delta + - self.pidSetting.speedMultiplier * speed + + self.pidSetting.integratedDistanceMultiplier * distanceIntegratedOverTime ) + self.setPower( power ) + + yield + + finally: + self.stopAndDisable() + + + def setSpeed( self, targetSpeedInClicksPerSecond, timeoutMillis = 3000 ): + 'Coroutine to run the motor at constant speed *targetSpeedInClicksPerSecond* for time *timeoutMillis*' + return self.scheduler.withTimeout( timeoutMillis, self.runAtConstantSpeed( targetSpeedInClicksPerSecond ) ) + + def runAtConstantSpeed( self, targetSpeedInClicksPerSecond ): + '''Coroutine to run the motor at constant speed *targetSpeedInClicksPerSecond* + ''' + # TODO: Algorithm needs work. + # Seems to hunt rather than stick at a fixed speed, and magic numbers should be configurable. + speedErrorMargin = 10 + power = 20.0 + if targetSpeedInClicksPerSecond < 0: + power = -power + self.enable(True) + logging.info( "Motor %s moving at constant speed %.3f" % (self.idChar, targetSpeedInClicksPerSecond) ) + try: + while abs(targetSpeedInClicksPerSecond - 0.0) > speedErrorMargin: # Don't move if the target speed is zero. + speed = self.speed() + if abs(speed - targetSpeedInClicksPerSecond) < speedErrorMargin: + pass + elif abs(speed) < abs(targetSpeedInClicksPerSecond): + power *= 1.1 + elif abs(speed) > abs(targetSpeedInClicksPerSecond): + power /= 1.1 + self.setPower( power ) + + yield + + finally: + self.stopAndDisable() + diff --git a/BrickPython/Scheduler.py b/BrickPython/Scheduler.py index c78fc62..6b63d4d 100644 --- a/BrickPython/Scheduler.py +++ b/BrickPython/Scheduler.py @@ -4,7 +4,7 @@ # Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. import threading -from Coroutine import Coroutine, StopCoroutineException +from .Coroutine import Coroutine, StopCoroutineException class GeneratorCoroutineWrapper(Coroutine): '''Internal: Wraps a generator-style coroutine with a thread''' @@ -98,7 +98,7 @@ def setUpdateCoroutine(self, coroutine): self.updateCoroutine = Scheduler.makeCoroutine(coroutine) def findCoroutineForGenerator(self, generator): - return (c for c in self.coroutines if c.generator == generator).next() + return next((c for c in self.coroutines if c.generator == generator)) def stopCoroutine( self, *coroutineList ): 'Terminates the given one or more coroutines' @@ -134,7 +134,7 @@ def runTillFirstCompletes( *coroutineList ): while True: for coroutine in coroutineList: try: - coroutine.next() + next(coroutine) except (StopIteration, StopCoroutineException): return # CW - I don't understand it, but we don't seem to need to terminate the others explicitly. yield @@ -146,7 +146,7 @@ def runTillAllComplete(*coroutineList ): while coroutines != []: for coroutine in coroutines: try: - coroutine.next() + next(coroutine) except (StopIteration, StopCoroutineException): coroutines.remove( coroutine ) yield diff --git a/BrickPython/Scheduler.py.bak b/BrickPython/Scheduler.py.bak new file mode 100644 index 0000000..c78fc62 --- /dev/null +++ b/BrickPython/Scheduler.py.bak @@ -0,0 +1,171 @@ +# Scheduler +# Support for coroutines using either Python generator functions or thread-based coroutines. +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + +import threading +from Coroutine import Coroutine, StopCoroutineException + +class GeneratorCoroutineWrapper(Coroutine): + '''Internal: Wraps a generator-style coroutine with a thread''' + + def __init__(self, generator): + '''`scheduler` - the main Scheduler object + `generator` - the generator object created by calling the generator function''' + Coroutine.__init__(self, self.action) + self.generator = generator + + def action(self): + 'The thread entry function - executed within thread `thread`' + for _ in self.generator: + Coroutine.wait() + + def stop(self): + try: + self.generator.throw(StopCoroutineException()) + except StopCoroutineException: + pass + Coroutine.stop(self) + + +class Scheduler(): + ''' This manages an arbitrary number of coroutines (including generator functions), supporting + invoking each every *timeMillisBetweenWorkCalls*, and detecting when each has completed. + + It supports one special coroutine - the updatorCoroutine, which is invoked before and after all the other ones. + ''' + + timeMillisBetweenWorkCalls = 50 + + @staticmethod + def makeCoroutine(coroutineOrGenerator): + return coroutineOrGenerator if coroutineOrGenerator is Coroutine else GeneratorCoroutineWrapper(coroutineOrGenerator) + + + @staticmethod + def currentTimeMillis(): + return Coroutine.currentTimeMillis() + + def __init__(self, timeMillisBetweenWorkCalls = 50): + Scheduler.timeMillisBetweenWorkCalls = timeMillisBetweenWorkCalls + self.coroutines = [] + self.timeOfLastCall = Scheduler.currentTimeMillis() + self.updateCoroutine = Scheduler.makeCoroutine( self.nullCoroutine() ) # for testing - usually replaced. + #: The most recent exception raised by a coroutine: + self.lastExceptionCaught = Exception("None") + + def doWork(self): + 'Executes all the coroutines, handling exceptions' + + timeNow = Scheduler.currentTimeMillis() + if timeNow == self.timeOfLastCall: # Ensure each call gets a different timer value. + return + self.timeOfLastCall = timeNow + self.updateCoroutine.call() + for coroutine in self.coroutines[:]: # Copy of coroutines, so it doesn't matter removing one + coroutine.call() + if not coroutine.is_alive(): + self.coroutines.remove(coroutine) + self.lastExceptionCaught = coroutine.lastExceptionCaught + + self.updateCoroutine.call() + + def timeMillisToNextCall(self): + 'Wait time before the next doWork call should be called.' + timeRequired = self.timeMillisBetweenWorkCalls + self.timeOfLastCall - Scheduler.currentTimeMillis() + return max( timeRequired, 0 ) + + + def addSensorCoroutine(self, *coroutineList): + '''Adds one or more new sensor/program coroutines to be scheduled, answering the last one to be added. + Sensor coroutines are scheduled *before* Action coroutines''' + for generatorFunction in coroutineList: + latestAdded = Scheduler.makeCoroutine(generatorFunction) + self.coroutines.insert(0, latestAdded) + return generatorFunction + + def addActionCoroutine(self, *coroutineList): + '''Adds one or more new motor control coroutines to be scheduled, answering the last coroutine to be added. + Action coroutines are scheduled *after* Sensor coroutines''' + for generatorFunction in coroutineList: + latestAdded = Scheduler.makeCoroutine(generatorFunction) + self.coroutines.append(latestAdded) + return generatorFunction + + def setUpdateCoroutine(self, coroutine): + # Private - set the coroutine that manages the interaction with the BrickPi. + # The coroutine will be invoked once at the start and once at the end of each doWork call. + self.updateCoroutine = Scheduler.makeCoroutine(coroutine) + + def findCoroutineForGenerator(self, generator): + return (c for c in self.coroutines if c.generator == generator).next() + + def stopCoroutine( self, *coroutineList ): + 'Terminates the given one or more coroutines' + for generator in coroutineList: + coroutine = self.findCoroutineForGenerator(generator) + coroutine.stop() + + def stopAllCoroutines(self): + 'Terminates all coroutines (except the updater one) - rather drastic!' + self.stopCoroutine(*[c.generator for c in self.coroutines]) # Makes a copy of the list - don't want to be changing it. + + def numCoroutines( self ): + 'Answers the number of active coroutines' + return len(self.coroutines) + + def stillRunning( self, *coroutineList ): + 'Answers whether any of the given coroutines are still executing' + return any( c in self.coroutines for c in coroutineList ) + + ############################################################################################# + # Generator-based coroutines. Kept for backward compatibility. + ############################################################################################# + + @staticmethod + def nullCoroutine(): + 'Null coroutine - runs forever and does nothing' + while True: + yield + + @staticmethod + def runTillFirstCompletes( *coroutineList ): + 'Coroutine that executes the given coroutines until the first completes, then stops the others and finishes.' + while True: + for coroutine in coroutineList: + try: + coroutine.next() + except (StopIteration, StopCoroutineException): + return # CW - I don't understand it, but we don't seem to need to terminate the others explicitly. + yield + + @staticmethod + def runTillAllComplete(*coroutineList ): + 'Coroutine that executes the given coroutines until all have completed or one throws an exception.' + coroutines = list( coroutineList ) + while coroutines != []: + for coroutine in coroutines: + try: + coroutine.next() + except (StopIteration, StopCoroutineException): + coroutines.remove( coroutine ) + yield + + @staticmethod + def waitMilliseconds( timeMillis ): + 'Coroutine that waits for timeMillis, then finishes.' + t = Scheduler.currentTimeMillis() + while Scheduler.currentTimeMillis() - t < timeMillis: + yield + + @staticmethod + def withTimeout( timeoutMillis, *coroutineList ): + 'Coroutine that wraps the given coroutine(s) with a timeout' + return Scheduler.runTillFirstCompletes( Scheduler.waitMilliseconds( timeoutMillis ), *coroutineList ) + + @staticmethod + def waitFor(function, *args ): + 'Coroutine that waits until the given function (with optional parameters) returns True.' + while not function(*args): + yield + diff --git a/BrickPython/Sensor.py b/BrickPython/Sensor.py index 87a91e7..a312f06 100644 --- a/BrickPython/Sensor.py +++ b/BrickPython/Sensor.py @@ -2,7 +2,7 @@ # # Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. -import BrickPi +from . import BrickPi class Sensor(): '''Sensor, representing a sensor attached to one of the BrickPi ports. diff --git a/BrickPython/Sensor.py.bak b/BrickPython/Sensor.py.bak new file mode 100644 index 0000000..87a91e7 --- /dev/null +++ b/BrickPython/Sensor.py.bak @@ -0,0 +1,151 @@ +# Sensor - represents a single value sensor supported by the BrickPython library +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + +import BrickPi + +class Sensor(): + '''Sensor, representing a sensor attached to one of the BrickPi ports. + Parameter *port* may be either a value (BrickPi.PORT_1) or an integer '1'-'5' + + There are class attributes with the types defined in the BrickPi module, e.g. Sensor.ULTRASONIC_CONT + You can configure the sensor type for each port in the initialization parameters to BrickPiWrapper (and derived classes) + + Used both as class in its own right, and as superclass for other sensor types. + ''' + RAW = BrickPi.TYPE_SENSOR_RAW + LIGHT_OFF = BrickPi.TYPE_SENSOR_LIGHT_OFF + LIGHT_ON = BrickPi.TYPE_SENSOR_LIGHT_ON + TOUCH = BrickPi.TYPE_SENSOR_TOUCH + ULTRASONIC_CONT = BrickPi.TYPE_SENSOR_ULTRASONIC_CONT + ULTRASONIC_SS = BrickPi.TYPE_SENSOR_ULTRASONIC_SS + RCX_LIGHT = BrickPi.TYPE_SENSOR_RCX_LIGHT + COLOR_FULL = BrickPi.TYPE_SENSOR_COLOR_FULL + COLOR_RED = BrickPi.TYPE_SENSOR_COLOR_RED + COLOR_GREEN = BrickPi.TYPE_SENSOR_COLOR_GREEN + COLOR_BLUE = BrickPi.TYPE_SENSOR_COLOR_BLUE + COLOR_NONE = BrickPi.TYPE_SENSOR_COLOR_NONE + I2C = BrickPi.TYPE_SENSOR_I2C + I2C_9V = BrickPi.TYPE_SENSOR_I2C_9V + + @staticmethod + def portNumFromId(portNumOrIdChar): + # Answers the port number given either port number or the ID Char. + if isinstance(portNumOrIdChar, int): + result = portNumOrIdChar + else: + result = int(portNumOrIdChar) - 1 + assert( result in range(0,4)) # Yes, there are 5 sensor ports, but brickpi_python doesn't support #5 + return result + + def __init__(self, port, sensorType=RAW): + self.port = Sensor.portNumFromId(port) + self.type = sensorType + #: Character identifying the sensor: 1 through 5. + self.idChar = chr(self.port + ord('1')) + #: The most recent value to return + self.recentValue = self.cookValue(0) + #: The most recent raw value received from the BrickPi + self.rawValue = 0 + #: Function that gets called with new value as parameter when the value changes - default, none. + self.callbackFunction = lambda x: 0 + + def updateValue(self, newValue): + # Called by the framework to set the new value for the sensor. + # We ignore zero values - probably means a comms failure. + if newValue == 0: + return + self.rawValue = newValue + previousValue = self.recentValue + self.recentValue = self.cookValue(newValue) + if self.recentValue != previousValue: + self.callbackFunction(self.recentValue) + + def waitForChange(self): + 'Coroutine that completes when the sensor value changes' + previousValue = self.recentValue + while self.recentValue == previousValue: + yield + + def value(self): + 'Answers the latest sensor value received (overridable)' + return self.recentValue + + def cookValue(self, rawValue): + 'Answers the value to return for a given input sensor reading (overridable)' + return rawValue + + def __repr__(self): + return "%s %s: %r (%d)" % (self.__class__.__name__, self.idChar, self.displayValue(), self.rawValue) + + def displayValue(self): + 'Answers a good representation of the current value for display (overridable)' + return self.value() + +class TouchSensor(Sensor): + '''TouchSensor, representing an NXT touch sensor attached to one of the BrickPi ports. + Parameter *port* may be either a value (BrickPi.PORT_1) or an integer '1'-'5' + + value() is True if the button is pressed; False otherwise. + ''' + def __init__(self, port): + # Just using the BrickPi TYPE_SENSOR_TOUCH didn't work for me; hence raw. + Sensor.__init__(self, port, Sensor.RAW) + + def cookValue(self, rawValue): + return True if rawValue < 500 else False + +class UltrasonicSensor(Sensor): + '''Represents an NXT ultrasonic sensor attached to one of the BrickPi ports. + Parameter *port* may be either a value (BrickPi.PORT_1) or an integer '1'-'5' + + value() is distance to the nearest 5 cm, with a maximum of MAX_VALUE + ''' + #: The reading when no object is in sight: + MAX_VALUE = 30 + #: Round readings to nearest centimeters. + ROUND_TO = 5 + #: How many readings to smooth over. + SMOOTHING_RANGE=10 + + def __init__(self, port): + self.recentRawValues = [] + Sensor.__init__(self, port, Sensor.ULTRASONIC_CONT) + # Don't want to return 0 initially, so need to reset the defaults: + self.recentRawValues = [255] + self.recentValue = UltrasonicSensor.MAX_VALUE + + def cookValue(self, rawValue): + self.recentRawValues.append( rawValue ) + if len(self.recentRawValues) > UltrasonicSensor.SMOOTHING_RANGE: + del self.recentRawValues[0] + smoothedValue = min(self.recentRawValues) + result = int(self.ROUND_TO * round(float(smoothedValue)/self.ROUND_TO)) # Round to nearest 5 + return min(result, UltrasonicSensor.MAX_VALUE) + +# def displayValue(self): +# return self.recentRawValues + +class LightSensor(Sensor): + '''Represents my NXT color sensor. + The BrickPi_Python COLOR_FULL setting didn't work for me at all - always has value 1. + (though it did light up a red LED on the device). + + But in RAW mode the sensor does seem to detect the difference between light and dark backgrounds. + + value() is either LIGHT or DARK + ''' + #: Detected a light background: + LIGHT = 1 + #: Detected a dark background: + DARK = 0 + + def __init__(self, port): + Sensor.__init__(self, port, Sensor.RAW) + + def cookValue(self, rawValue): + return LightSensor.LIGHT if rawValue < 740 else LightSensor.DARK + + def displayValue(self): + return ("Dark","Light")[self.value()] + diff --git a/BrickPython/TkApplication.py b/BrickPython/TkApplication.py index 4523fb2..23e9556 100644 --- a/BrickPython/TkApplication.py +++ b/BrickPython/TkApplication.py @@ -4,9 +4,9 @@ # # Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. -import Tkinter as tk +import tkinter as tk -from BrickPiWrapper import BrickPiWrapper +from .BrickPiWrapper import BrickPiWrapper import logging class TkApplication(BrickPiWrapper): diff --git a/BrickPython/TkApplication.py.bak b/BrickPython/TkApplication.py.bak new file mode 100644 index 0000000..4523fb2 --- /dev/null +++ b/BrickPython/TkApplication.py.bak @@ -0,0 +1,60 @@ +# TkApplication class. Superclass for applications using the Tkinter framework +# +# Applications using the BrickPi derive from this, implementing appropriate functionality. +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + +import Tkinter as tk + +from BrickPiWrapper import BrickPiWrapper +import logging + +class TkApplication(BrickPiWrapper): + ''' + Main application class using the Tk toolkit. Implements the regular calls required by the scheduler. + + The default implementation creates a simple small window which + exits when it receives the 'Q' key, but this can be changed by overriding the doInitialization() method. + + ''' + + def __init__(self, sensorConfiguration={}): + '''Initialization: *sensorConfiguration* is a map as passed to BrickPiWrapper''' + BrickPiWrapper.__init__(self, sensorConfiguration ) + self.root = tk.Tk() + + self.doInitialization() + + self.timerTick() + + def doInitialization(self): + 'Default initialization function with a simple window - override if you want something different' + self.root.geometry('300x200') + self.label = tk.Label(text="BrickPi") + self.label.pack() + self.root.bind('', self.onKeyPress) + + + def mainloop(self): + 'The main loop for the application - call this after initialization. Returns on exit.' + self.root.mainloop() + + def timerTick(self): + # Private: Does all the coroutine processing, every 20ms or so. + self.doWork() + self.root.after(int(self.timeMillisToNextCall()), self.timerTick) + + def onKeyPress(self, event): + '''Default key press handling - answers True if it's handled the key. + Override this function to add extra keypress handling. ''' + char = event.char + if char == "": # Key such as shift or control... + pass + elif(char=='q'): + logging.info( "Application terminated") + self.root.destroy() + else: + return False + return True + + diff --git a/ExamplePrograms/SimpleApp.py b/ExamplePrograms/SimpleApp.py index 0277ac2..58670e6 100644 --- a/ExamplePrograms/SimpleApp.py +++ b/ExamplePrograms/SimpleApp.py @@ -18,10 +18,10 @@ def doActivity(self): motorA = self.motor('A') motorA.zeroPosition() while True: - print 'Moving forward' + print('Moving forward') for i in motorA.moveTo( 2*90 ): yield - print 'Moving back' + print('Moving back') for i in motorA.moveTo( 0 ): yield diff --git a/ExamplePrograms/SimpleApp.py.bak b/ExamplePrograms/SimpleApp.py.bak new file mode 100644 index 0000000..0277ac2 --- /dev/null +++ b/ExamplePrograms/SimpleApp.py.bak @@ -0,0 +1,29 @@ +# Very simple example application for BrickPython. +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + +import sys, os # Python path kludge - omit these 2 lines if BrickPython is installed. +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))) + +from BrickPython.CommandLineApplication import CommandLineApplication + +class SimpleApp(CommandLineApplication): + 'Simple command line example application' + def __init__(self): + CommandLineApplication.__init__(self) + self.addSensorCoroutine( self.doActivity() ) + + def doActivity(self): + 'Coroutine to rotate a motor forward and backward' + motorA = self.motor('A') + motorA.zeroPosition() + while True: + print 'Moving forward' + for i in motorA.moveTo( 2*90 ): + yield + print 'Moving back' + for i in motorA.moveTo( 0 ): + yield + +if __name__ == "__main__": + SimpleApp().mainloop() \ No newline at end of file diff --git a/ExamplePrograms/TrialApp.py b/ExamplePrograms/TrialApp.py index bb71ff2..92c70f0 100644 --- a/ExamplePrograms/TrialApp.py +++ b/ExamplePrograms/TrialApp.py @@ -28,13 +28,13 @@ def showChanges(self, sensorId): sensor = self.sensor(sensorId) while True: for i in sensor.waitForChange(): yield - print sensor + print(sensor) def showSensorValues(self, sensorId): sensor = self.sensor(sensorId) while True: for i in self.waitMilliseconds(1000): yield - print sensor + print(sensor) if __name__ == "__main__": logging.basicConfig(format='%(message)s', level=logging.DEBUG) # All log messages printed to console. diff --git a/ExamplePrograms/TrialApp.py.bak b/ExamplePrograms/TrialApp.py.bak new file mode 100644 index 0000000..bb71ff2 --- /dev/null +++ b/ExamplePrograms/TrialApp.py.bak @@ -0,0 +1,43 @@ +# App +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + +import sys, os # Python path kludge - omit these 2 lines if BrickPython is installed. +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))) + +from BrickPython.TkApplication import TkApplication +from BrickPython.Sensor import Sensor, TouchSensor, LightSensor,\ + UltrasonicSensor +import logging + +class App(TkApplication): + '''Application to do stuff. + ''' + + def __init__(self): + settings = {'1': LightSensor, '2': TouchSensor, '3': UltrasonicSensor } + TkApplication.__init__(self, settings) + self.root.wm_title("Trial running") + for c in "ABCD": + self.motor(c).zeroPosition() + self.addActionCoroutine(self.motor(c).runAtConstantSpeed(180)) + for c in settings: + self.addSensorCoroutine(self.showChanges(c)) + + def showChanges(self, sensorId): + sensor = self.sensor(sensorId) + while True: + for i in sensor.waitForChange(): yield + print sensor + + def showSensorValues(self, sensorId): + sensor = self.sensor(sensorId) + while True: + for i in self.waitMilliseconds(1000): yield + print sensor + +if __name__ == "__main__": + logging.basicConfig(format='%(message)s', level=logging.DEBUG) # All log messages printed to console. + logging.info( "Starting" ) + app = App() + app.mainloop() diff --git a/docs/conf.py b/docs/conf.py index ed63b15..2566aa3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,8 +49,8 @@ master_doc = 'index' # General information about the project. -project = u'BrickPython' -copyright = u'2014, Charles Weir' +project = 'BrickPython' +copyright = '2014, Charles Weir' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -217,8 +217,8 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'BrickPython.tex', u'BrickPython Documentation', - u'Charles Weir', 'manual'), + ('index', 'BrickPython.tex', 'BrickPython Documentation', + 'Charles Weir', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -247,8 +247,8 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'brickpython', u'BrickPython Documentation', - [u'Charles Weir'], 1) + ('index', 'brickpython', 'BrickPython Documentation', + ['Charles Weir'], 1) ] # If true, show URL addresses after external links. @@ -261,8 +261,8 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'BrickPython', u'BrickPython Documentation', - u'Charles Weir', 'BrickPython', 'One line description of project.', + ('index', 'BrickPython', 'BrickPython Documentation', + 'Charles Weir', 'BrickPython', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docs/conf.py.bak b/docs/conf.py.bak new file mode 100644 index 0000000..ed63b15 --- /dev/null +++ b/docs/conf.py.bak @@ -0,0 +1,279 @@ +# Changes Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. +# -*- coding: utf-8 -*- +# +# BrickPython documentation build configuration file, created by +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath('../BrickPython')) +sys.path.insert(0, os.path.abspath('../ExamplePrograms')) + + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'BrickPython' +copyright = u'2014, Charles Weir' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.4' +# The full version, including alpha/beta/rc tags. +release = '0.4beta' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build', 'generated'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'BrickPythondoc' + +autodoc_default_flags = ['members', + #'undoc-members','private-members', 'special-members', + 'show-inheritance'] + +# def autodoc_skip_member(app, what, name, obj, skip, options): +# exclusions = ('__weakref__', # special-members +# '__doc__', '__module__', '__dict__', # undoc-members +# ) +# if (obj.__doc__): +# print "hello: " + obj.__doc__ +# exclude = name in exclusions +# return skip or exclude +# +# def setup(app): +# app.connect('autodoc-skip-member', autodoc_skip_member) + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'BrickPython.tex', u'BrickPython Documentation', + u'Charles Weir', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'brickpython', u'BrickPython Documentation', + [u'Charles Weir'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'BrickPython', u'BrickPython Documentation', + u'Charles Weir', 'BrickPython', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/test/TestMotor.py b/test/TestMotor.py index 3471824..90692fa 100644 --- a/test/TestMotor.py +++ b/test/TestMotor.py @@ -13,12 +13,12 @@ class TestMotor(unittest.TestCase): ''' Tests for the Motor class, especially for PID Servo Motor functionality''' def setUp(self): self.saveTime = Scheduler.currentTimeMillis - Scheduler.currentTimeMillis = Mock(side_effect = xrange(1,10000)) + Scheduler.currentTimeMillis = Mock(side_effect = range(1,10000)) self.bp = BrickPiWrapper() motor = self.motor = self.bp.motor( 'A' ) #motor.position = Mock() motor.timeMillis = Mock() - motor.timeMillis.side_effect = range(0,9999) + motor.timeMillis.side_effect = list(range(0,9999)) def tearDown(self): Scheduler.currentTimeMillis = self.saveTime @@ -44,7 +44,7 @@ def testSpeedCalculation(self): motor.updatePosition(1) motor.updatePosition(2) # The speed is 1000 clicks per second. - print motor.speed() + print(motor.speed()) assert( int(motor.speed()) == 1000) # Tests for positionUsingPIDAlgorithm: @@ -56,7 +56,7 @@ def testGeneratorFunctionWorks(self): generator = motor.positionUsingPIDAlgorithmWithoutTimeout( 10 ) # it keeps going for i in generator: - motor.updatePosition( positions.next() ) + motor.updatePosition( next(positions) ) assert( motor.enabled() ) # until it reaches the target position assert( motor.position() == 10 ) @@ -70,7 +70,7 @@ def testFinishesIfNearEnough(self): positions = [0,99,99,99].__iter__() generator = motor.positionUsingPIDAlgorithmWithoutTimeout( 100 ) for i in generator: - motor.updatePosition( positions.next() ) + motor.updatePosition( next(positions) ) # it still completes : self.assertEquals( motor.position(), 99 ) self.assertFalse( motor.enabled() ) @@ -81,8 +81,8 @@ def testChecksSpeedOnFinishing(self): positions = [0,50,100,150].__iter__() co = motor.positionUsingPIDAlgorithm( 100 ) for i in range(3): - motor.updatePosition( positions.next() ) - co.next() + motor.updatePosition( next(positions) ) + next(co) # it doesn't stop (no StopIteration exception has been thrown) def testPowerIsFunctionOfDistance(self): @@ -90,9 +90,9 @@ def testPowerIsFunctionOfDistance(self): # When it's some distance from the target, but not moving co = motor.positionUsingPIDAlgorithmWithoutTimeout( 100 ) motor.updatePosition( 0 ) - co.next() + next(co) motor.updatePosition( 0 ) - co.next() + next(co) # There's power to the motor in the right direction. assert( motor.power() > 0 ) @@ -102,9 +102,9 @@ def testPowerIsFunctionOfSpeed(self): co = motor.positionUsingPIDAlgorithmWithoutTimeout( 100 ) # when it reaches the target motor.updatePosition( 0 ) - co.next() + next(co) motor.updatePosition( 100 ) - co.next() + next(co) # It doesn't finish and the power is in reverse. assert( motor.power() < 0 ) @@ -113,13 +113,13 @@ def testPowerIsFunctionOfSumDistance(self): # When the motor isn't moving and is some distance from the target co = motor.positionUsingPIDAlgorithmWithoutTimeout( 100 ) # And we have quite long work cycles: - motor.timeMillis.side_effect = range(0,990,20) + motor.timeMillis.side_effect = list(range(0,990,20)) # The motor power increases with time motor.updatePosition( 0 ) - co.next() + next(co) p1 = motor.power() motor.updatePosition( 0 ) - co.next() + next(co) p2 = motor.power() self.assertGreater( p2, p1 ) @@ -127,12 +127,12 @@ def testPowerIsFunctionOfSumDistance(self): # with with longer work cycles and a different motor motorB = self.bp.motor( 'B' ) motorB.timeMillis = Mock() - motorB.timeMillis.side_effect = range(0,990,40) + motorB.timeMillis.side_effect = list(range(0,990,40)) co = motorB.positionUsingPIDAlgorithmWithoutTimeout( 100 ) motorB.updatePosition( 0 ) - co.next() + next(co) motorB.updatePosition( 0 ) - co.next() + next(co) # then we get a larger power reading than before self.assertGreater( motorB.power(), p2 ) @@ -141,7 +141,7 @@ def testTimesOutIfNeverReachesTarget(self): motor = self.motor co = motor.positionUsingPIDAlgorithm( 100 ) # If the motor never reaches the target in 6 seconds: - for i in xrange(0,6000/50): + for i in range(0,6000/50): self.bp.doWork() # It terminates self.assertFalse( self.bp.stillRunning( co ) ) @@ -164,7 +164,7 @@ def testCanCancelOperation(self): motor = self.motor # if we start the motor co = motor.positionUsingPIDAlgorithmWithoutTimeout( 100 ) - co.next() + next(co) assert( motor.enabled() ) # then stop the coroutine try: diff --git a/test/TestMotor.py.bak b/test/TestMotor.py.bak new file mode 100644 index 0000000..3471824 --- /dev/null +++ b/test/TestMotor.py.bak @@ -0,0 +1,188 @@ +# Tests for Motor +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + + +from BrickPython.BrickPiWrapper import BrickPiWrapper +from BrickPython.Scheduler import Scheduler, StopCoroutineException +import unittest +from mock import Mock + + +class TestMotor(unittest.TestCase): + ''' Tests for the Motor class, especially for PID Servo Motor functionality''' + def setUp(self): + self.saveTime = Scheduler.currentTimeMillis + Scheduler.currentTimeMillis = Mock(side_effect = xrange(1,10000)) + self.bp = BrickPiWrapper() + motor = self.motor = self.bp.motor( 'A' ) + #motor.position = Mock() + motor.timeMillis = Mock() + motor.timeMillis.side_effect = range(0,9999) + + def tearDown(self): + Scheduler.currentTimeMillis = self.saveTime + unittest.TestCase.tearDown(self) + + def testZeroPosition( self ): + motor = self.motor + # When the motor is zeroed at absolute position 1 + motor.updatePosition(1) + motor.zeroPosition() + # then absolute position 2 is read as position 1 + motor.updatePosition(2) + self.assertEquals( motor.position(), 1 ) + # When the motor is zeroed again at absolute position 2 + motor.zeroPosition() + # then absolute position 3 is read as position 1 + motor.updatePosition(3) + self.assertEquals( motor.position(), 1 ) + + def testSpeedCalculation(self): + motor = self.motor + # When the motor is moving at 1 click per call (every millisecond) + motor.updatePosition(1) + motor.updatePosition(2) + # The speed is 1000 clicks per second. + print motor.speed() + assert( int(motor.speed()) == 1000) + + # Tests for positionUsingPIDAlgorithm: + + def testGeneratorFunctionWorks(self): + motor = self.motor + # When we try to reposition the motor + positions = [0,5,11,10,10,10].__iter__() + generator = motor.positionUsingPIDAlgorithmWithoutTimeout( 10 ) + # it keeps going + for i in generator: + motor.updatePosition( positions.next() ) + assert( motor.enabled() ) + # until it reaches the target position + assert( motor.position() == 10 ) + # then switches off + assert( not motor.enabled() ) + assert( motor.power() == 0 ) + + def testFinishesIfNearEnough(self): + motor = self.motor + # When the motor gets near enough to the target + positions = [0,99,99,99].__iter__() + generator = motor.positionUsingPIDAlgorithmWithoutTimeout( 100 ) + for i in generator: + motor.updatePosition( positions.next() ) + # it still completes : + self.assertEquals( motor.position(), 99 ) + self.assertFalse( motor.enabled() ) + + def testChecksSpeedOnFinishing(self): + motor = self.motor + # When the motor gets to the right position, but is still going fast: + positions = [0,50,100,150].__iter__() + co = motor.positionUsingPIDAlgorithm( 100 ) + for i in range(3): + motor.updatePosition( positions.next() ) + co.next() + # it doesn't stop (no StopIteration exception has been thrown) + + def testPowerIsFunctionOfDistance(self): + motor = self.motor + # When it's some distance from the target, but not moving + co = motor.positionUsingPIDAlgorithmWithoutTimeout( 100 ) + motor.updatePosition( 0 ) + co.next() + motor.updatePosition( 0 ) + co.next() + # There's power to the motor in the right direction. + assert( motor.power() > 0 ) + + def testPowerIsFunctionOfSpeed(self): + motor = self.motor + # When the motor is going fast + co = motor.positionUsingPIDAlgorithmWithoutTimeout( 100 ) + # when it reaches the target + motor.updatePosition( 0 ) + co.next() + motor.updatePosition( 100 ) + co.next() + # It doesn't finish and the power is in reverse. + assert( motor.power() < 0 ) + + def testPowerIsFunctionOfSumDistance(self): + motor = self.motor + # When the motor isn't moving and is some distance from the target + co = motor.positionUsingPIDAlgorithmWithoutTimeout( 100 ) + # And we have quite long work cycles: + motor.timeMillis.side_effect = range(0,990,20) + # The motor power increases with time + motor.updatePosition( 0 ) + co.next() + p1 = motor.power() + motor.updatePosition( 0 ) + co.next() + p2 = motor.power() + self.assertGreater( p2, p1 ) + + # And the motor power depends on time between readings, so if we do it all again + # with with longer work cycles and a different motor + motorB = self.bp.motor( 'B' ) + motorB.timeMillis = Mock() + motorB.timeMillis.side_effect = range(0,990,40) + co = motorB.positionUsingPIDAlgorithmWithoutTimeout( 100 ) + motorB.updatePosition( 0 ) + co.next() + motorB.updatePosition( 0 ) + co.next() + # then we get a larger power reading than before + self.assertGreater( motorB.power(), p2 ) + + + def testTimesOutIfNeverReachesTarget(self): + motor = self.motor + co = motor.positionUsingPIDAlgorithm( 100 ) + # If the motor never reaches the target in 6 seconds: + for i in xrange(0,6000/50): + self.bp.doWork() + # It terminates + self.assertFalse( self.bp.stillRunning( co ) ) + + def testAlternateNameAndUseOfScheduler(self): + motor = self.motor + # When we use the other name for the operation + generator = motor.moveTo( 10 ) + # and invoke the scheduler - disabling normal update processing - while the motor moves to the position + self.bp.setUpdateCoroutine(Scheduler.nullCoroutine()) + positions = [0,5,11,10,10,10] + for i in range(len(positions)): + motor.updatePosition( positions[i] ) + self.bp.doWork() + # Then we complete correctly + self.assertFalse( self.bp.stillRunning( generator )) + self.assertEquals( motor.position(), 10 ) + + def testCanCancelOperation(self): + motor = self.motor + # if we start the motor + co = motor.positionUsingPIDAlgorithmWithoutTimeout( 100 ) + co.next() + assert( motor.enabled() ) + # then stop the coroutine + try: + co.throw(StopCoroutineException) + # the coroutine stops + assert( False ) + except (StopCoroutineException, StopIteration): # The exception should stop the coroutine + pass + # and switch off the motor. + assert( not motor.enabled() ) + assert( motor.power() == 0 ) + + def testMotorTextRepresentation(self): + self.assertRegexpMatches( repr(self.motor), 'Motor.*location=.*speed=.*') + + def testPIDIntegratedDistanceMultiplierBackwardCompatibility(self): + pass + +if __name__ == '__main__': + unittest.main() + diff --git a/test/TestScheduler.py b/test/TestScheduler.py index 17bcb5b..02de80e 100644 --- a/test/TestScheduler.py +++ b/test/TestScheduler.py @@ -50,7 +50,7 @@ def dummyCoroutineThatThrowsException(): def checkCoroutineFinished(coroutine): # Fails the test if the given coroutine hasn't finished. try: - coroutine.next() + next(coroutine) assert(False) except StopIteration: pass @@ -59,7 +59,7 @@ def checkCoroutineFinished(coroutine): def setUp(self): TestScheduler.coroutineCalls = [] TestScheduler.oldCurrentTimeMillis = Scheduler.currentTimeMillis - Scheduler.currentTimeMillis = Mock( side_effect = xrange(0,10000) ) # Each call answers the next integer + Scheduler.currentTimeMillis = Mock( side_effect = range(0,10000) ) # Each call answers the next integer self.scheduler = Scheduler() def tearDown(self): @@ -184,7 +184,7 @@ def testRunTillAllComplete( self ): for i in self.scheduler.runTillAllComplete( *[TestScheduler.dummyCoroutine(1,i) for i in [2,3,4]] ): pass # they all run to completion: - print TestScheduler.coroutineCalls + print(TestScheduler.coroutineCalls) assert( TestScheduler.coroutineCalls == [1,1,1,2,2,3] ) assert( self.scheduler.numCoroutines() == 0) @@ -193,7 +193,7 @@ def testWithTimeout(self): for i in self.scheduler.withTimeout(10, TestScheduler.dummyCoroutineThatDoesCleanup(1,99) ): pass # It completes at around the timeout, and does cleanup: - print TestScheduler.coroutineCalls + print(TestScheduler.coroutineCalls) self.assertTrue( 0 < TestScheduler.coroutineCalls[-2] <= 10) # N.b. currentTimeMillis is called more than once per doWork call. self.assertEquals( TestScheduler.coroutineCalls[-1], -1 ) @@ -248,7 +248,7 @@ def testTheWaitForCoroutine(self): coroutine = scheduler.waitFor( lambda ap: len(ap) > 0, arrayParameter ) # It runs for i in range(0,5): - coroutine.next() + next(coroutine) # Until the function returns true. arrayParameter.append(1) TestScheduler.checkCoroutineFinished( coroutine ) diff --git a/test/TestScheduler.py.bak b/test/TestScheduler.py.bak new file mode 100644 index 0000000..17bcb5b --- /dev/null +++ b/test/TestScheduler.py.bak @@ -0,0 +1,310 @@ +# Tests for Scheduler +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + +# Run tests as +# python TestScheduler.py +# or, if you've got it installed: +# nosetests + + + +from BrickPython.BrickPiWrapper import BrickPiWrapper +from BrickPython.Scheduler import Scheduler +import unittest +import logging +from mock import * + +class TestScheduler(unittest.TestCase): + ''' Tests for the Scheduler class, its built-in coroutines, and its coroutine handling. + ''' + coroutineCalls = [] + @staticmethod + def dummyCoroutine(start = 1, finish = 5): + for i in range(start, finish): + TestScheduler.coroutineCalls.append(i) + yield + + @staticmethod + def dummyCoroutineThatDoesCleanup(start = 1, finish = 2): + for i in range(start, finish): + TestScheduler.coroutineCalls.append(i) + try: + yield + finally: + TestScheduler.coroutineCalls.append( -1 ) + + @staticmethod + def dummyCoroutineThatTakesTime(): + for i in range(0,2): + for j in range (0,10): # Takes 10 ms + Scheduler.currentTimeMillis() # Increments the mock time. + yield + + @staticmethod + def dummyCoroutineThatThrowsException(): + raise Exception("Hello") + yield + + @staticmethod + def checkCoroutineFinished(coroutine): + # Fails the test if the given coroutine hasn't finished. + try: + coroutine.next() + assert(False) + except StopIteration: + pass + + + def setUp(self): + TestScheduler.coroutineCalls = [] + TestScheduler.oldCurrentTimeMillis = Scheduler.currentTimeMillis + Scheduler.currentTimeMillis = Mock( side_effect = xrange(0,10000) ) # Each call answers the next integer + self.scheduler = Scheduler() + + def tearDown(self): + Scheduler.currentTimeMillis = TestScheduler.oldCurrentTimeMillis + + + def testCoroutinesGetCalledUntilDone(self): + # When we start a motor coroutine + coroutine = TestScheduler.dummyCoroutine() + self.scheduler.addActionCoroutine(coroutine) + # and run it until complete + for i in range(0,10): + self.scheduler.doWork() + # It has completed and no coroutines are running + assert( TestScheduler.coroutineCalls[-1] == 4 ) + TestScheduler.checkCoroutineFinished( coroutine ) + assert( self.scheduler.numCoroutines() == 0) + + def testCoroutinesGetCleanedUp(self): + # When we start a motor coroutine + coroutine = TestScheduler.dummyCoroutineThatDoesCleanup() + self.scheduler.addActionCoroutine(coroutine) + # and run it until complete + for i in range(0,10): + self.scheduler.doWork() + # It has cleaned up + self.assertEquals( TestScheduler.coroutineCalls[-1], -1 ) + + def testCoroutinesCanBeTerminated(self): + # When we start a motor coroutine + coroutine = TestScheduler.dummyCoroutine() + self.scheduler.addActionCoroutine(coroutine) + # run it a bit, then terminate it + self.scheduler.doWork() + self.scheduler.stopCoroutine( coroutine ) + self.scheduler.doWork() + # It has completed early + self.assertEquals( TestScheduler.coroutineCalls[-1], 1 ) + TestScheduler.checkCoroutineFinished( coroutine ) + + def testAllCoroutinesCanBeTerminated(self): + # When we start two coroutines + for i in range(2): + self.scheduler.addActionCoroutine(TestScheduler.dummyCoroutine()) + # run them, then terminate them all + self.scheduler.doWork() + self.scheduler.stopAllCoroutines() + self.scheduler.doWork() + self.scheduler.doWork() + # They all terminate + self.assertEquals(TestScheduler.coroutineCalls, [1 , 1] ) + + def testCoroutineThatThrowsException(self): + # When we have a coroutine that throws an exception: + self.scheduler.addActionCoroutine(TestScheduler.dummyCoroutineThatThrowsException()) + # then the scheduler will remove it from the work schedule + self.scheduler.doWork() + self.scheduler.doWork() # CW - can't quite figure out why we need two calls here... + self.assertEquals( self.scheduler.numCoroutines(), 0 ) + + def testCoroutinesCanCleanupWhenTerminated(self): + # When we start a motor coroutine + coroutine = TestScheduler.dummyCoroutineThatDoesCleanup() + self.scheduler.addActionCoroutine(coroutine) + # run it a bit, then terminate it + self.scheduler.doWork() + self.scheduler.stopCoroutine( coroutine ) + self.scheduler.doWork() + # It has completed and cleaned up + assert( TestScheduler.coroutineCalls[-1] == -1 ) + TestScheduler.checkCoroutineFinished( coroutine ) + + def testSensorCoroutinesWork(self): + # When we start a sensor coroutine + self.scheduler.addSensorCoroutine(TestScheduler.dummyCoroutine()) + # it gets executed + self.scheduler.doWork() + assert( TestScheduler.coroutineCalls[-1] == 1 ) + + def testSensorCoroutinesCanBeAccessedAndGetDoneFirst(self): + # When we start a motor coroutine (starts at 2) and a sensor one (starts at 1) + sensorCo = TestScheduler.dummyCoroutine() + motorCo = TestScheduler.dummyCoroutine(2) + motorCoReturned = self.scheduler.addActionCoroutine(motorCo) + sensorCoReturned = self.scheduler.addSensorCoroutine(sensorCo) + # Then the 'latest coroutine' will be returned correctly: + self.assertEquals( motorCoReturned, motorCo ) + self.assertEquals( sensorCoReturned, sensorCo ) + # and the motor coroutine will update the status last: + self.scheduler.doWork() + self.assertEquals( TestScheduler.coroutineCalls[-1], 2 ) + + def testUpdateCoroutineGetsCalledBothBeforeAndAfterTheOtherCoroutines(self): + # When we start a motor and sensor coroutines (start at 1,4) with an update one (starts at 10) + self.scheduler.setUpdateCoroutine(TestScheduler.dummyCoroutine(10,20)) + self.scheduler.addSensorCoroutine(TestScheduler.dummyCoroutine(4,8)) + self.scheduler.addActionCoroutine(TestScheduler.dummyCoroutine()) + # then the update coroutine will be invoked before and after the others: + self.scheduler.doWork() + assert( TestScheduler.coroutineCalls == [10,4,1,11] ) + + def testWaitMilliseconds(self): + # If we wait for 10 ms + for i in self.scheduler.waitMilliseconds(10): + pass + # that's about the time that will have passed: + timeNow = self.scheduler.currentTimeMillis() + assert( timeNow in range(10,12), "Wrong time " ) + + def testRunTillFirstCompletes(self): + # When we run three coroutines using runTillFirstCompletes: + for i in self.scheduler.runTillFirstCompletes(TestScheduler.dummyCoroutine(1,9), + TestScheduler.dummyCoroutine(1,2), + TestScheduler.dummyCoroutine(1,9) ): + pass + # the first to complete stops the others: + self.assertEquals( TestScheduler.coroutineCalls, [1,1,1,2] ) + self.assertEquals( self.scheduler.numCoroutines(), 0) + + def testRunTillAllComplete( self ): + # When we run three coroutines using runTillAllComplete: + for i in self.scheduler.runTillAllComplete( *[TestScheduler.dummyCoroutine(1,i) for i in [2,3,4]] ): + pass + # they all run to completion: + print TestScheduler.coroutineCalls + assert( TestScheduler.coroutineCalls == [1,1,1,2,2,3] ) + assert( self.scheduler.numCoroutines() == 0) + + def testWithTimeout(self): + # When we run a coroutine with a timeout: + for i in self.scheduler.withTimeout(10, TestScheduler.dummyCoroutineThatDoesCleanup(1,99) ): + pass + # It completes at around the timeout, and does cleanup: + print TestScheduler.coroutineCalls + self.assertTrue( 0 < TestScheduler.coroutineCalls[-2] <= 10) # N.b. currentTimeMillis is called more than once per doWork call. + self.assertEquals( TestScheduler.coroutineCalls[-1], -1 ) + + def testTimeMillisToNextCall(self): + # Given a mock timer, and a different scheduler set up with a known time interval + scheduler = Scheduler(20) + # when we have just coroutines that take no time + scheduler.addActionCoroutine( TestScheduler.dummyCoroutine() ) + # then the time to next tick is the default less a bit for the timer check calls: + scheduler.doWork() + ttnt = scheduler.timeMillisToNextCall() + assert( ttnt in range(17,20) ) + # when we have an additional coroutine that takes time + scheduler.addSensorCoroutine( TestScheduler.dummyCoroutineThatTakesTime() ) + # then the time to next tick is less by the amount of time taken by the coroutine: + scheduler.doWork() + ttnt = scheduler.timeMillisToNextCall() + assert( ttnt in range(7,10) ) + # but when the coroutines take more time than the time interval available + for i in range(0,2): + scheduler.addSensorCoroutine( TestScheduler.dummyCoroutineThatTakesTime() ) + # the time to next tick never gets less than zero + scheduler.doWork() + ttnt = scheduler.timeMillisToNextCall() + assert( ttnt == 0 ) + # and incidentally, we should have all the coroutines still running + assert( scheduler.numCoroutines() == 4 ) + + def timeCheckerCoroutine(self): + # Helper coroutine for testEachCallToACoroutineGetsADifferentTime + # Checks that each call gets a different time value. + time = Scheduler.currentTimeMillis() + while True: + yield + newTime = Scheduler.currentTimeMillis() + self.assertNotEquals( newTime, time, "Time same for two scheduler calls" ) + time = newTime + + def testEachCallToACoroutineGetsADifferentTime(self): + Scheduler.currentTimeMillis = Mock( side_effect = [0,0,0,0,0,0,0,0,0,0,1,2,3,4,5] ) + scheduler = Scheduler() + # For any coroutine, + scheduler.setUpdateCoroutine( self.timeCheckerCoroutine() ) + # We can guarantee that the timer always increments between calls (for speed calculations etc). + for i in range(1,10): + scheduler.doWork() + + def testTheWaitForCoroutine(self): + scheduler = Scheduler() + arrayParameter = [] + # When we create a WaitFor coroutine with a function that takes one parameter (actually an array) + coroutine = scheduler.waitFor( lambda ap: len(ap) > 0, arrayParameter ) + # It runs + for i in range(0,5): + coroutine.next() + # Until the function returns true. + arrayParameter.append(1) + TestScheduler.checkCoroutineFinished( coroutine ) + + @staticmethod + def throwingCoroutine(): + yield + raise Exception("Hello") + + def testExceptionThrownFromCoroutine(self): + scheduler = Scheduler() + self.assertIsNotNone(scheduler.lastExceptionCaught) + scheduler.addActionCoroutine(self.throwingCoroutine()) + for i in range(1,3): + scheduler.doWork() + self.assertEquals(scheduler.lastExceptionCaught.message, "Hello") + + def testRunTillFirstCompletesWithException(self): + # When we run three coroutines using runTillFirstCompletes: + self.scheduler.addActionCoroutine(self.scheduler.runTillFirstCompletes(self.throwingCoroutine(), + TestScheduler.dummyCoroutine(1,2), + TestScheduler.dummyCoroutine(1,9) )) + for i in range(1,10): + self.scheduler.doWork() + # the first to complete stops the others: + self.assertEquals( TestScheduler.coroutineCalls, [1,1] ) + self.assertEquals( self.scheduler.numCoroutines(), 0) + # and the exception is caught by the Scheduler: + self.assertEquals(self.scheduler.lastExceptionCaught.message, "Hello") + + def testRunTillAllCompleteWithException( self ): + # When we run three coroutines using runTillAllComplete: + self.scheduler.addActionCoroutine(self.scheduler.runTillAllComplete(self.throwingCoroutine(), + TestScheduler.dummyCoroutine(1,2))) + for i in range(1,10): + self.scheduler.doWork() + # the first to complete stops the others: + self.assertEquals( TestScheduler.coroutineCalls, [1] ) + self.assertEquals( self.scheduler.numCoroutines(), 0) + # and the exception is caught by the Scheduler: + self.assertEquals(self.scheduler.lastExceptionCaught.message, "Hello") + + def testCanCatchExceptionWithinNestedCoroutines(self): + self.caught = 0 + def outerCoroutine(self): + try: + for i in self.throwingCoroutine(): + yield + except: + self.caught = 1 + for i in outerCoroutine(self): + pass + self.assertEquals(self.caught, 1) + + +if __name__ == '__main__': + logging.basicConfig(format='%(message)s', level=logging.DEBUG) # Logging is a simple print + unittest.main() + diff --git a/test/TestSensor.py b/test/TestSensor.py index ff8020e..90f453f 100644 --- a/test/TestSensor.py +++ b/test/TestSensor.py @@ -5,7 +5,7 @@ import unittest from BrickPython.BrickPi import PORT_1 from BrickPython.Sensor import Sensor, TouchSensor, UltrasonicSensor, LightSensor -import TestScheduler +from . import TestScheduler class TestSensor(unittest.TestCase): 'Tests for the Sensor classes' @@ -56,8 +56,8 @@ def callbackFunc(x): def testCoroutineWaitingForChange(self): sensor = TouchSensor( '1' ) coroutine = sensor.waitForChange() - coroutine.next() - coroutine.next() + next(coroutine) + next(coroutine) sensor.updateValue( 1000 ) TestScheduler.TestScheduler.checkCoroutineFinished( coroutine ) @@ -65,10 +65,10 @@ def testUltrasonicSensor(self): sensor = UltrasonicSensor( '1' ) self.assertEquals(sensor.port, 0) self.assertEquals( sensor.idChar, '1' ) - for input, output in {0:UltrasonicSensor.MAX_VALUE, 2:0, 3:5, 4:5, 9:10, 11:10, 14:15, 16:15, 22:20, 23:25, 26:25, + for input, output in list({0:UltrasonicSensor.MAX_VALUE, 2:0, 3:5, 4:5, 9:10, 11:10, 14:15, 16:15, 22:20, 23:25, 26:25, 255: UltrasonicSensor.MAX_VALUE - }.items(): - for i in xrange(0,UltrasonicSensor.SMOOTHING_RANGE+1): + }.items()): + for i in range(0,UltrasonicSensor.SMOOTHING_RANGE+1): sensor.updateValue( input ) # Remove effects of smoothing. self.assertEquals( sensor.value(), output, "Failed with input %d: got %d" %(input,sensor.value()) ) @@ -76,7 +76,7 @@ def testUltrasonicSensorSmoothing(self): sensor = UltrasonicSensor( '1' ) for input in [ 24,14,10,8,10,10,50,10,50,18,50]: sensor.updateValue( input ) - print sensor + print(sensor) self.assertEquals( sensor.value(), 10 ) def testLightSensor(self): @@ -84,8 +84,8 @@ def testLightSensor(self): sensor = LightSensor('4') self.assertEquals(sensor.port, 3) self.assertEquals( sensor.idChar, '4' ) - for input, output in { 680: LightSensor.LIGHT, 800: LightSensor.DARK - }.items(): + for input, output in list({ 680: LightSensor.LIGHT, 800: LightSensor.DARK + }.items()): sensor.updateValue( input ) self.assertEquals( sensor.value(), output ) self.assertEquals( repr(sensor), "LightSensor 4: 'Dark' (800)") diff --git a/test/TestSensor.py.bak b/test/TestSensor.py.bak new file mode 100644 index 0000000..ff8020e --- /dev/null +++ b/test/TestSensor.py.bak @@ -0,0 +1,95 @@ +# Tests for Sensor +# +# Copyright (c) 2014 Charles Weir. Shared under the MIT Licence. + +import unittest +from BrickPython.BrickPi import PORT_1 +from BrickPython.Sensor import Sensor, TouchSensor, UltrasonicSensor, LightSensor +import TestScheduler + +class TestSensor(unittest.TestCase): + 'Tests for the Sensor classes' + + def testSensor(self): + sensor = Sensor( PORT_1 ) + self.assertEquals(sensor.port, PORT_1) + assert( sensor.idChar == '1' ) + assert( sensor.value() == 0 ) + sensor.updateValue( 3 ) + assert( sensor.value() == 3 ) + + def testSensorTextRepresentation(self): + self.assertEquals( repr(Sensor( PORT_1 ) ), 'Sensor 1: 0 (0)') + + def testDifferentWaysToInitialize(self): + self.assertEquals( repr(Sensor( '1' ) ), 'Sensor 1: 0 (0)') + self.assertEquals( repr(Sensor( '1', Sensor.COLOR_NONE ) ), 'Sensor 1: 0 (0)') + + def testTouchSensor(self): + sensor = TouchSensor( '1' ) + self.assertEquals(sensor.port, 0) + self.assertEquals( sensor.idChar, '1' ) + self.assertEquals( sensor.value(), True ) # Pressed in + sensor.updateValue( 1000 ) + self.assertEquals( sensor.value(), False ) + + def testTouchSensorTextRepresentation(self): + self.assertEquals( repr(TouchSensor( '1' ) ), 'TouchSensor 1: True (0)') + + def testCallbackWhenChanged(self): + result = [True] + def callbackFunc(x): + result[0] = x + sensor = TouchSensor( '1' ) + sensor.callbackFunction = callbackFunc + sensor.updateValue( 1000 ) + self.assertEquals( result[0], False ) + # And no call when it doesn't change + result[0] = True + sensor.updateValue( 1000 ) + self.assertEquals( result[0], True ) + # But does get a call when it changes back + result[0] = False + sensor.updateValue( 20 ) + self.assertEquals( result[0], True ) + + def testCoroutineWaitingForChange(self): + sensor = TouchSensor( '1' ) + coroutine = sensor.waitForChange() + coroutine.next() + coroutine.next() + sensor.updateValue( 1000 ) + TestScheduler.TestScheduler.checkCoroutineFinished( coroutine ) + + def testUltrasonicSensor(self): + sensor = UltrasonicSensor( '1' ) + self.assertEquals(sensor.port, 0) + self.assertEquals( sensor.idChar, '1' ) + for input, output in {0:UltrasonicSensor.MAX_VALUE, 2:0, 3:5, 4:5, 9:10, 11:10, 14:15, 16:15, 22:20, 23:25, 26:25, + 255: UltrasonicSensor.MAX_VALUE + }.items(): + for i in xrange(0,UltrasonicSensor.SMOOTHING_RANGE+1): + sensor.updateValue( input ) # Remove effects of smoothing. + self.assertEquals( sensor.value(), output, "Failed with input %d: got %d" %(input,sensor.value()) ) + + def testUltrasonicSensorSmoothing(self): + sensor = UltrasonicSensor( '1' ) + for input in [ 24,14,10,8,10,10,50,10,50,18,50]: + sensor.updateValue( input ) + print sensor + self.assertEquals( sensor.value(), 10 ) + + def testLightSensor(self): + #Light is 680, dark about 800 + sensor = LightSensor('4') + self.assertEquals(sensor.port, 3) + self.assertEquals( sensor.idChar, '4' ) + for input, output in { 680: LightSensor.LIGHT, 800: LightSensor.DARK + }.items(): + sensor.updateValue( input ) + self.assertEquals( sensor.value(), output ) + self.assertEquals( repr(sensor), "LightSensor 4: 'Dark' (800)") + +if __name__ == '__main__': + unittest.main() +