Build basic working structure of NDN-HOME

Change-Id: I56cdbac58407192ed9e79ec55f2196c20c2e952d
diff --git a/README.md b/README.md
index f6cf57b..4e76140 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
 # NDN-HOME
-Bootstrap procedure for NDN home.
+Smart home via NDN.
diff --git a/access_control_manager.py b/access_control_manager.py
new file mode 100644
index 0000000..53ed12d
--- /dev/null
+++ b/access_control_manager.py
@@ -0,0 +1,26 @@
+# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
+#
+# Copyright (C) 2014 Regents of the University of California.
+# Author: Teng Liang <philoliang2011@gmail.com>
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# A copy of the GNU General Public License is in the file COPYING.
+
+"""
+This module uses access token method to sign, verfy, encrypt and decrypt data/interest. Besides, it generates and updates seed/command access token/access token. 
+
+"""
+
+class AccessControlManager(object):
+    pass
diff --git a/base_node.py b/base_node.py
index 2393eea..b93bc5d 100644
--- a/base_node.py
+++ b/base_node.py
@@ -17,33 +17,32 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # A copy of the GNU General Public License is in the file COPYING.
 
+"""
+This module defines BaseNode class, which contains methods/attributes common to both end device and controller.
+"""
+
 import logging
 import time
 import sys
-
 import random
 
-from pyndn import Name, Face, Interest, Data, ThreadsafeFace
-from pyndn.security.policy import ConfigPolicyManager
+from pyndn import Name, Face, Interest, Data
+from pyndn.threadsafe_face import ThreadsafeFace
 from pyndn.security import KeyChain
 from pyndn.security.identity import IdentityManager, BasicIdentityStorage
+from pyndn.security.policy import NoVerifyPolicyManager, ConfigPolicyManager
 from pyndn.security.security_exception import SecurityException
-    
 
-from collections import namedtuple
+from access_control_manager import AccessControlManager    
 
 try:
     import asyncio
 except ImportError:
     import trollius as asyncio
 
-#Command = namedtuple('Command', ['suffix', 'function', 'keywords', 'isSigned'])
 
 class BaseNode(object):
-    """
-    This class contains methods/attributes common to both end device and controller.
-    
-    """
+
     def __init__(self,configFileName):
         """
         Initialize the network and security classes for the node
@@ -53,9 +52,12 @@
 	
         self._identityStorage = BasicIdentityStorage()
         self._identityManager = IdentityManager(self._identityStorage)
-        self._policyManager = ConfigPolicyManager(configFileName)
 
-        # hopefully there is some private/public key pair available
+        if configFileName:
+            self._policyManager = ConfigPolicyManager(configFileName)
+        else:
+            self._policyManager = NoVerifyPolicyManager()
+
         self._keyChain = KeyChain(self._identityManager,self._policyManager)
 
         self._registrationFailures = 0
@@ -64,10 +66,11 @@
         self._setupComplete = False
         self._instanceSerial = None
 
-        # waiting devices register this prefix and respond to discovery
-        # or configuration interest
         self._bootstrapPrefix = '/home/controller/bootstrap'
-
+        self._serviceProfileList = []
+        self._commmandList = []
+        self._protocolList = []
+ 
     def getSerial(self):
         """
          Since you may wish to run two nodes on a Raspberry Pi, each
@@ -82,9 +85,99 @@
             self._instanceSerial = prefix.encode('hex')
         return self._instanceSerial
 
-##
-# Logging
-##
+    def addServiceProfiles(self,profiles):
+        pass
+
+    def addServices(self,services):
+        pass
+
+    def addProtocols(self,protocols):
+        pass
+
+
+    def beforeLoopStart(self):
+        """
+        Called before the event loop starts.
+        """
+        pass
+    
+
+    def start(self):
+        """
+        Begins the event loop. After this, the node's Face is set up and it can
+        send/receive interests+data
+        """
+        self.log.info("Starting up")
+        self.loop = asyncio.get_event_loop()
+        self.face = ThreadsafeFace(self.loop, 'localhost')
+       
+        self._keyChain.setFace(self.face)
+        self.beforeLoopStart()
+
+        self.face.setCommandSigningInfo(self._keyChain, self._keyChain.getDefaultCertificateName())
+
+        try:
+            self.loop.run_forever()
+        except Exception as e:
+            self.log.exception(e, exc_info=True)
+
+    def createACK(self):
+        pass
+  
+    def createNACK(self):
+        pass
+
+    def signData(self, data):
+        """
+        Sign the data with our network certificate
+        :param pyndn.Data data: The data to sign
+        """
+	pass
+
+    def sendData(self, data, transport, sign=True):
+        """
+        Reply to an interest with a data packet, optionally signing it.
+        :param pyndn.Data data: The response data packet
+        :param pyndn.Transport transport: The transport to send the data through. This is 
+            obtained from an incoming interest handler
+        :param boolean sign: (optional, default=True) Whether the response must be signed. 
+        """
+        if sign:
+            self.signData(data)
+        transport.send(data.wireEncode().buf())
+
+    """
+    Failure
+
+    """
+
+    def onRegisterFailed(self, prefix):
+        """
+        Called when the node cannot register its name with the forwarder
+        :param pyndn.Name prefix: The network name that failed registration
+        """
+        if self._registrationFailures < 5:
+            self._registrationFailures += 1
+            self.log.warn("Could not register {}, retry: {}/{}".format(prefix.toUri(), self._registrationFailures, 5)) 
+            self.face.registerPrefix(self.prefix, self._onCommandReceived, self.onRegisterFailed)
+        else:
+            self.log.critical("Could not register device prefix, ABORTING")
+            self._isStopped = True
+
+    def verificationFailed(self, dataOrInterest):
+        """
+        Called when verification of a data packet or command interest fails.
+        :param pyndn.Data or pyndn.Interest: The packet that could not be verified
+        """
+        self.log.info("Received invalid" + dataOrInterest.getName().toUri())
+
+
+
+
+    """
+    Logging
+    """
+    
     def _prepareLogging(self):
         self.log = logging.getLogger(str(self.__class__))
         self.log.setLevel(logging.DEBUG)
@@ -110,117 +203,7 @@
         """
         return self.log
 
-###
-# Startup and shutdown
-###
-    def beforeLoopStart(self):
-        """
-        Called before the event loop starts.
-        """
-        pass
-    
-    def getKeyChain(self):
-        return self._keyChain
 
-    def getDefaultIdentity(self):
-        try:
-            defaultIdentity = self._identityManager.getDefaultIdentity()
-        except SecurityException:
-            defaultIdentity = ""
-
-        return defaultIdentity
-
-
-    def getDefaultCertificateName(self):
-        #exception - no certficate, return ''
-
-        try:
-            certName = self._identityStorage.getDefaultCertificateNameForIdentity( 
-            self._identityManager.getDefaultIdentity())
-        except SecurityException:
-            certName = ""
-
-        return certName
-
-    def start(self):
-        """
-        Begins the event loop. After this, the node's Face is set up and it can
-        send/receive interests+data
-        """
-        self.log.info("Starting up")
-        self.loop = asyncio.get_event_loop()
-        self.face = ThreadsafeFace(self.loop, '')
-       
-        self._keyChain.setFace(self.face)
-
-        self._isStopped = False
-        self.face.stopWhen(lambda:self._isStopped)
-        
-        self.beforeLoopStart()
-
-        self.face.setCommandSigningInfo(self._keyChain, self.getDefaultCertificateName())
-
-        try:
-            self.loop.run_forever()
-        except KeyboardInterrupt:
-            pass
-        except Exception as e:
-            self.log.exception(e, exc_info=True)
-        finally:
-            self._isStopped = True
-
-    def stop(self):
-        """
-        Stops the node, taking it off the network
-        """
-        self.log.info("Shutting down")
-        self._isStopped = True 
-        
-###
-# Data handling
-###
-    def signData(self, data):
-        """
-        Sign the data with our network certificate
-        :param pyndn.Data data: The data to sign
-        """
-        self._keyChain.sign(data, self.getDefaultCertificateName())
-
-    def sendData(self, data, transport, sign=True):
-        """
-        Reply to an interest with a data packet, optionally signing it.
-        :param pyndn.Data data: The response data packet
-        :param pyndn.Transport transport: The transport to send the data through. This is 
-            obtained from an incoming interest handler
-        :param boolean sign: (optional, default=True) Whether the response must be signed. 
-        """
-        if sign:
-            self.signData(data)
-        transport.send(data.wireEncode().buf())
-
-###
-# 
-# 
-##
-    def onRegisterFailed(self, prefix):
-        """
-        Called when the node cannot register its name with the forwarder
-        :param pyndn.Name prefix: The network name that failed registration
-        """
-        if self._registrationFailures < 5:
-            self._registrationFailures += 1
-            self.log.warn("Could not register {}, retry: {}/{}".format(prefix.toUri(), self._registrationFailures, 5)) 
-            self.face.registerPrefix(self.prefix, self._onCommandReceived, self.onRegisterFailed)
-        else:
-            self.log.critical("Could not register device prefix, ABORTING")
-            self._isStopped = True
-
-    def verificationFailed(self, dataOrInterest):
-        """
-        Called when verification of a data packet or command interest fails.
-        :param pyndn.Data or pyndn.Interest: The packet that could not be verified
-        """
-        self.log.info("Received invalid" + dataOrInterest.getName().toUri())
 
     @staticmethod
     def getDeviceSerial():
@@ -233,6 +216,4 @@
         with open('/proc/cpuinfo') as f:
             for line in f:
                 if line.startswith('Serial'):
-                    return line.split(':')[1].strip()
-
-
+                    return line.split(':')[1].strip()
\ No newline at end of file
diff --git a/controller.py b/controller.py
index 7043466..17e2afb 100644
--- a/controller.py
+++ b/controller.py
@@ -37,7 +37,7 @@
     print(result)
 
 class Controller(BaseNode):
-    def __init__(self,configFileName):
+    def __init__(self,configFileName=None):
         super(Controller, self).__init__(configFileName=configFileName)
         self._responseCount = 0
         self._symmetricKey = "symmetricKeyForBootstrapping"
@@ -166,7 +166,7 @@
             self._keyChain.createIdentityAndCertificate(identityName)
        	    self._identityManager.setDefaultIdentity(identityName)
 
-	self.face.setCommandSigningInfo(self._keyChain, self.getDefaultCertificateName())
+	self.face.setCommandSigningInfo(self._keyChain, self._keyChain.getDefaultCertificateName())
         self.face.registerPrefix(self._prefix, self.onInterest, self.onRegisterFailed)
         
         
diff --git a/device.py b/device.py
new file mode 100644
index 0000000..4fa3540
--- /dev/null
+++ b/device.py
@@ -0,0 +1,177 @@
+# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
+#
+# Copyright (C) 2014 Regents of the University of California.
+# Author: Teng Liang <philoliang2011@gmail.com>
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# A copy of the GNU General Public License is in the file COPYING.
+
+"""
+This module defines Device class, which contains core modules of end device, including device discovery protocol, syncronization protocol, and access control manager.
+"""
+
+
+import time
+import json
+from pyndn import Name, Face, Interest, Data
+from pyndn.threadsafe_face import ThreadsafeFace
+from pyndn import KeyLocator, KeyLocatorType
+from base_node import BaseNode
+from hmac_helper import HmacHelper 
+from pyndn.security.security_exception import SecurityException
+from pyndn.util import Blob
+try:
+    import asyncio
+except ImportError:
+    import trollius as asyncio
+
+def dump(*list):
+    result = ""
+    for element in list:
+        result += (element if type(element) is str else repr(element)) + " "
+    print(result)
+
+class Device(BaseNode):
+    def __init__(self,configFileName=None):
+        super(Device, self).__init__(configFileName)
+        
+        self._deviceSerial = self.getSerial()
+        self._callbackCount = 0
+        self._symKey = "symmetricKeyForBootstrapping"
+        self._category = "sensors"
+        self._id = "T9273659"
+        self._hmacHelper = HmacHelper(self._symKey)
+
+
+    def expressBootstrapInterest(self):
+        
+        #generate bootstrap name /home/controller/bootstrap/<device-parameters>
+        bootstrapName = Name(self._bootstrapPrefix)
+
+        deviceParameters = {}
+        deviceParameters["category"] = self._category
+        deviceParameters["id"] = self._id
+        bootstrapName.append(json.dumps(deviceParameters))
+
+        bootstrapInterest = Interest(bootstrapName)
+        bootstrapInterest.setInterestLifetimeMilliseconds(3000)
+        self._hmacHelper.signInterest(bootstrapInterest)
+
+        dump("Express bootstrap interest : ",bootstrapInterest.toUri())
+        self.face.expressInterest(bootstrapInterest, self.onBootstrapData, self.onTimeout)
+    
+    def onBootstrapData(self, interest, data):
+        dump("Bootstrap data received.")
+
+        if (self._hmacHelper.verifyData(data)):
+            self.log.info("Bootstrap data is verified")
+            content = json.loads(data.getContent().toRawStr(), encoding="latin-1")
+            deviceNewIdentity = Name(content["deviceNewIdentity"])
+            controllerIdentity = Name(content["controllerIdentity"])
+            controllerPublicKeyInfo = content["controllerPublicKey"]
+
+            self.face.registerPrefix(content["deviceNewIdentity"],self.onInterest,self.onRegisterFailed)
+            #set new identity as default and generate default key-pair with KSK Certificate
+            self._identityStorage.addIdentity(deviceNewIdentity)
+            self._identityManager.setDefaultIdentity(deviceNewIdentity)
+            try:
+                self._identityManager.getDefaultKeyNameForIdentity(deviceNewIdentity)
+            except SecurityException:
+                #generate new key-pair and certificate for new identity
+                dump("Install new identity as default\nGenerate new key-pair and self signed certificate")
+                newKey = self._identityManager.generateRSAKeyPairAsDefault(Name(deviceNewIdentity), isKsk=True)
+                newCert = self._identityManager.selfSign(newKey)
+                self._identityManager.addCertificateAsIdentityDefault(newCert)
+            
+            #add controller's identity and public key
+            keyType = controllerPublicKeyInfo["keyType"]
+            keyName = Name(controllerPublicKeyInfo["keyName"])
+            keyDer = Blob().fromRawStr(controllerPublicKeyInfo["publicKeyDer"])
+            dump("Controller's KeyType: ",keyType)
+            dump("Controller's keyName: ",keyName)
+            dump("Controller public key der : ",keyDer)
+
+            self._identityStorage.addIdentity(controllerIdentity)
+            try:
+                self._identityStorage.addKey(keyName, keyType, keyDer)
+                dump("Controller's identity, key and certificate installled.")
+            except SecurityException:
+                dump("Controller's identity, key, certificate already exists.")
+
+            #express an certificate request interest
+            defaultKeyName = self._identityManager.getDefaultKeyNameForIdentity(self._keyChain.getDefaultIdentity() )
+            self.requestCertificate(defaultKeyName)
+        else:
+            self.log.info("Bootstrap data is not verified")
+        
+        
+
+
+    def beforeLoopStart(self):
+        #self.face.registerPrefix('/home', self.onInterest, self.onRegisterFailed)
+        self.expressBootstrapInterest()
+        
+    def onTimeout(self, interest):
+        self._callbackCount += 1
+        dump("Time out for interest", interest.getName().toUri())
+
+    def onInterest():
+        pass
+    def onRegisterFailed():
+	pass
+
+#    def requestCertificate(self, keyIdentity):
+        """
+        We compose a command interest with our public key info so the controller
+        can sign us a certificate that can be used with other nodes in the network.
+        Name format : /home/<device-category>/KEY/<device-id>/<key-id>/<publickey>/ID-CERT/<version-number>
+        """
+"""        certificateRequestName = self._keyChain.getDefaultIdentity()
+        deviceIdComponent = certificateRequestName.get(-1)
+        keyIdComponent = keyIdentity.get(-1)
+
+        certificateRequestName = certificateRequestName
+        certificateRequestName.append("KEY")
+        #certificateRequestName.append(deviceIdComponent)
+        certificateRequestName.append(keyIdComponent)
+
+        key = self._identityManager.getPublicKey(keyIdentity)
+        keyInfo = {}
+        keyInfo["keyType"] = key.getKeyType()
+        keyInfo["keyDer"] = key.getKeyDer().toRawStr()
+
+        certificateRequestName.append(json.dumps(keyInfo, encoding="latin-1"))
+
+        certificateRequestName.append("ID-CERT")
+
+        certificateRequest = Interest(certificateRequestName)
+        certificateRequest.setInterestLifetimeMilliseconds(5000)
+        self._hmacHelper.signInterest(certificateRequest)
+        
+        dump("Sending certificate request : ",certificateRequestName)
+
+        self.face.expressInterest(certificateRequest, self.onCertificateData, self.onTimeout)
+        #TODO use symmetric key to sign
+        
+    def onCertificateData(self, interest, data):
+        dump("OnCertificateData : ",data)
+"""        
+
+if __name__ == '__main__':
+
+    device = Device()
+    device.start()
+    
+    
+
diff --git a/device_profile.py b/device_profile.py
new file mode 100644
index 0000000..e63f01a
--- /dev/null
+++ b/device_profile.py
@@ -0,0 +1,26 @@
+# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
+#
+# Copyright (C) 2014 Regents of the University of California.
+# Author: Teng Liang <philoliang2011@gmail.com>
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# A copy of the GNU General Public License is in the file COPYING.
+
+"""
+This module defines the DeviceProfile class with holds descriptors of device
+"""
+
+class DeviceProfile(object):
+    pass    
+	
diff --git a/examples/DHT22.py b/examples/DHT22.py
new file mode 100644
index 0000000..49da4cd
--- /dev/null
+++ b/examples/DHT22.py
@@ -0,0 +1,22 @@
+# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
+#
+# Copyright (C) 2014 Regents of the University of California.
+# Author: Teng Liang <philoliang2011@gmail.com>
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# A copy of the GNU General Public License is in the file COPYING.
+
+"""
+This module gives an example of DHT22 instance
+"""
diff --git a/examples/LED.py b/examples/LED.py
new file mode 100644
index 0000000..9b7118c
--- /dev/null
+++ b/examples/LED.py
@@ -0,0 +1,22 @@
+# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
+#
+# Copyright (C) 2014 Regents of the University of California.
+# Author: Teng Liang <philoliang2011@gmail.com>
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# A copy of the GNU General Public License is in the file COPYING.
+
+"""
+This module gives an example of LED instance
+"""
diff --git a/test_get_async_threadsafe.py b/test_get_async_threadsafe.py
deleted file mode 100644
index fd1602e..0000000
--- a/test_get_async_threadsafe.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
-#
-# Copyright (C) 2014-2015 Regents of the University of California.
-# Author: Jeff Thompson <jefft0@remap.ucla.edu>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-# A copy of the GNU Lesser General Public License is in the file COPYING.
-
-try:
-    # Use builtin asyncio on Python 3.4+, or Tulip on Python 3.3
-    import asyncio
-except ImportError:
-    # Use Trollius on Python <= 3.2
-    import trollius as asyncio
-from pyndn import Name
-from pyndn import ThreadsafeFace
-
-def dump(*list):
-    result = ""
-    for element in list:
-        result += (element if type(element) is str else repr(element)) + " "
-    print(result)
-
-class Counter(object):
-    def __init__(self):
-        self._callbackCount = 0
-
-    def onData(self, interest, data):
-        self._callbackCount += 1
-        dump("Got data packet with name", data.getName().toUri())
-        # Use join to convert each byte to chr.
-        dump(data.getContent().toRawStr())
-
-    def onTimeout(self, interest):
-        self._callbackCount += 1
-        dump("Time out for interest", interest.getName().toUri())
-    
-    def onInterest(self,prefix,interest,face,interestFilterId,filter):
-        self._callbackCount+=1
-	dump("Receive interest", interest.toUri())
-
-    def onRegisterFailed(self,prefix):
-        dump("Register failed for prefix")
-
-def main():
-    loop = asyncio.get_event_loop()
-    face = ThreadsafeFace(loop, "aleph.ndn.ucla.edu")
-
-    counter = Counter()
-    face.stopWhen(lambda: counter._callbackCount >= 10)
-	
-    face.registerPrefix("/home",counter.onInterest,counter.onRegisterFailed)
-    #name1 = Name("/")
-    #dump("Express name ", name1.toUri())
-    # This call to exressIinterest is thread safe because face is a ThreadsafeFace.
-    #face.expressInterest(name1, counter.onData, counter.onTimeout)
-
-    # Run until stopWhen stops the loop.
-    loop.run_forever()
-    face.shutdown()
-
-main()
diff --git a/tests/test_timer.py b/tests/test_timer.py
new file mode 100644
index 0000000..becb79e
--- /dev/null
+++ b/tests/test_timer.py
@@ -0,0 +1,12 @@
+import threading
+import time
+
+def f():
+    print("HHHHH")
+    # call f() again in 60 seconds
+    threading.Timer(3, f).start()
+
+# start calling f now and every 60 sec thereafter
+
+if __name__ == '__main__':
+    f()
\ No newline at end of file