Pytos Development Environment

From NESTFE Wiki

Jump to: navigation, search

Contents

Introduction

The pytos environment is an interactive environment into which the user can import a nescApplication, including the variables in each module, rpc functions, enums, types, and messages. This allows the user to get/set any variable on the mote and to call any rpc function on the mote over a multi-hop network. Importing enums, types, and message definitions directly from the nesc code allows the programmer to define all structs, values, and message formats only once and have them automatically update in the pc-side application as they change in the nesC code. Check out the Screenshots

Getting Started Quickly

1. First, install the python environment for your [platform].

2. Then, you must edit the compiler include path and invoke the nescDecls.extra and rpc.extra makefiles during compilation by adding the following lines to your ~/tinyos-1.x/tools/make/Makelocal (alternatively, you could add them to each application's Makefile individually).

GOALS+=nescDecls
GOALS+=rpc
CFLAGS += -I$(TOSDIR)/../tos/lib/Rpc
CFLAGS += -I$(TOSDIR)/../tos/lib/RamSymbols
CFLAGS += -I$(TOSDIR)/../tos/lib/Drip
CFLAGS += -I$(TOSDIR)/../tos/lib/Drain
CFLAGS += -I$(TOSDIR)/../contrib/nucleus/tos/lib/Nucleus/

3. Finally, each application must also include the RpcM.nc and RamSymbolsM.nc nesc libraries in your nesc application by adding the following line to a top-level configuration file:

components RpcC, RamSymbolsM;
Main.StdControl -> RpcC;

The "nescDecls" command above is the minimum requirement for Pytos. "Rpc" and "RamSymbols" add extra functionality to pytos but are not required. You can find more information in the individual "Setting Up" instructions at [Nesc Declarations], [Rpc] and [Ram Symbols].

4. Once you have completed this setup, you can simply run

 PytosShell.py telosb sf@localhost:9001

This should import the entire application from "build/telosb", connect to the node, and give you a pytos command promt. (If you are not using a TOSBase, set "tosbase=false" in the script before running). Type "app" at the python cmd prompt to begin playing around; you can also call help() on any object. Try some of the following:

  • type app.types, app.enums, or app.msgs to see more detail.
  • type app.ModuleName to see variables and functions for a particular nesc module, eg. app.DrainM
  • set a module's variables by calling app.module.variable.peek() or app.module.variable.poke(value).
  • The "dereference=True" parameter can be used to dereference a variable if it is a pointer, eg app.mod.var.poke(value,dereference=True)
  • The "arrayIndex" parameter can be used to index a variable if it is an array, eg app.mod.var.poke(value,arrayIndex=2)
  • You can call a function on a module eg app.mod.func(val1, val2, etc)

The following sections expain each feature of the environment in more detail.

Configuring NescApp Parameters

The NescApp imports all functions, variables, types, enums, modules, etc. from your nesc application into the python environment. In fact, all the PytosShell does is instantiate a NescApp object. This object takes four parameters:

  1. The build directory, e.g. "build/telos". NescApp will look in the build directory for the nescDecls.xml file, which is created at compile time (as long as you make nescDecls one of the GOALS in your makefile).
  2. Your MOTECOM string, e.g. sf@localhost:9001 or serial@/dev/ttyUSB0:telos. NescApp will connect to your nodes using this motecom string. Note: if you do not pass a motecom string, you will get a WARNING but you will still be able to access functions and variables through the resuling NescApp object. This can be useful for testing.
  3. The tosbase and localCommmOnly parameters, which indicate what kind of network you have. tosbase should be true if you have a TosBase node plugged into your machine. localCommOnly should be true if you do NOT want the nodes to use the DRAIN multihop routing algorithm to return data to the base station. These are boolean parameters, so there are four combinations of them:
  • localCommOnly=T, tosbase=T: all nodes are within one radio hop, and a TOSBase is plugged in
  • localCommOnly=T, tosbase=F: all nodes are plugged directly into a serial port, e.g. you have a wired testbed
  • localCommOnly=F, tosbase=T: some nodes are multiple hops away, and a TOSBase is plugged in
  • localCommOnly=F, tosbase=F: some nodes are multiple hops away, and the root of the tree is plugged into a serial port.


Importing nesC Applications into Python

The first thing you must do when starting python is to import your nesc App. To do this, first import the NescApp module and then create a new NescApp instance. The NescApp object requires a string parameter indicating where it can find the necsDecls.xml file, which you created at compile time. This can be

  • an absolute path to the correct build directory (eg "/home/kamin/tinyos-1.x/apps/Oscilloscope/build/telosb")
  • a relative path to the correct build directory (eg "../apps/Oscilloscope/build/telosb")
  • either of the above with the actual filename appended (eg. "..../nescDecls.xml")
  • if you are in the application directory, just the name of the platform (e.g "pc" or "telosb")

Example:

$ cd ~/tinyos-1.x/contrib/hood/apps/TestRpc/
kamin@kamin-mobile:~/tinyos-1.x/contrib/hood/apps/TestRpc/
$ python
Python 2.4.1 (#2, May  5 2005, 11:32:06)
[GCC 3.3.5 (Debian 1:3.3.5-12)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pytos.util.NescApp as NescApp
>>> app = NescApp.NescApp('telosb', port='sf@localhost:9001')

Now, your "app" object should contain all of the module variables, module rpc functions, enums, types, and messages that are defined in your nesc application. You can look at an overview with

>>> app
              Enums : 234
              Types : 79
           Messages : 6
      Rpc functions : 35
        Ram symbols : 91
            Modules : AMStandard
                      BusArbitrationM
                      CC2420ControlM
                      CC2420RadioM
                      DrainGroupManagerM
                      DrainLinkEstM
                      DrainM
                      DripM
                      DripStateM
                      FramerAckM
                      FramerM
                      GroupManagerM
                      HPLCC2420M
                      HPLUSART0M
                      HPLUSART1M
                      LedsC
                      MSP430DCOCalibM
                      RamSymbolsM
                      RandomLFSR
                      RpcM
                      TestRpcM
                      TimerJiffyAsyncM
                      TimerM
                      UARTM
                      WakeupCommM

or you can look at them individually through the sub-objects

>>> app.enums
>>> app.types
>>> app.msgs
>>> app.rpc
>>> app.ramSymbols
>>> app.DrainM
>>> app.RamSymbolsM
>>> app.TestRpcM

Note: if you are using Tossim or if your node is plugged directory into the serial/USB port, you must use the "tosbase=False" flag to indicate that your computer is not connected to a TOSBase. For example:

>>> app = NescApp.NescApp('telosb', port='sf@localhost:9001', tosbase=False)

Variables and Registry Attributes

If you included the RpcM and RamSymbolsM modules in your application and compiled with the "rpc" flag, the NescApp object will contain a field called app.ramSymbols, which lists all the modules in your nesc application and their variables. The variables are also available through module shortcuts, eg. app.moduleName.variableName

You can get and set the values of module variables through the poke and peek commands. If the variable is an array, it can be indexed into with the arrayIndex option. If the variable is a pointer, it can be dereferenced with the dereference option. Arrays of pointers can be indexed and then dereferenced. For example:

NOTE: Poke is a fairly dangerous operation because it is changing the value of a variable without allowing the module to do any sort of state-checking.

>>> app.ramSymbols
<class 'pytos.tools.RamSymbols.RamSymbols'> object at 0x4cf6446c:

           uint16_t : AMStandard.counter
           uint16_t : AMStandard.lastCount
               bool : AMStandard.state
            uint8_t : BusArbitrationM.busid
               bool : BusArbitrationM.isBusReleasedPending
            uint8_t : BusArbitrationM.state
       uint16_t[14] : CC2420ControlM.gCurrentParameters
           uint16_t : CC2420RadioM.LocalAddr
            TOS_Msg : CC2420RadioM.RxBuf
               bool : CC2420RadioM.bAckEnable
               bool : CC2420RadioM.bPacketReceiving
            uint8_t : CC2420RadioM.countRetry
            uint8_t : CC2420RadioM.currentDSN
            uint8_t : CC2420RadioM.stateRadio
            uint8_t : CC2420RadioM.stateTimer
            uint8_t : CC2420RadioM.txlength
            TOS_Msg : DrainGroupManagerM.msgBuf
               bool : DrainGroupManagerM.msgBufBusy
            TOS_Msg : DrainLinkEstM.msgBuf
               bool : DrainLinkEstM.msgBufBusy
 DrainRouteEntry[2] : DrainLinkEstM.routes
               bool : DrainLinkEstM.timerRunning
           uint16_t : DrainM.ackedPackets
            uint8_t : DrainM.backoff
               bool : DrainM.baseAcks
            uint8_t : DrainM.forwardDrops
           uint16_t : DrainM.forwardPackets
         TOS_Msg[3] : DrainM.fwdBuffers
               bool : DrainM.fwdQueueFull
            uint8_t : DrainM.fwdQueueIn
            uint8_t : DrainM.fwdQueueOut
               bool : DrainM.queuesBusy
            uint8_t : DrainM.sendDrops
           uint16_t : DrainM.sendPackets
               bool : DrainM.sendQueueFull
            uint8_t : DrainM.sendQueueIn
            uint8_t : DrainM.sendQueueOut
           uint16_t : DrainM.unackedPackets
            TOS_Msg : DripM.msgBuf
               bool : DripM.msgBufBusy
  DripCacheEntry[1] : DripStateM.dripCache
            uint8_t : FramerAckM.gTokenBuf
            uint8_t : FramerM.gFlags
         TOS_Msg[2] : FramerM.gMsgRcvBuf
            uint8_t : FramerM.gPrevTxState
           uint16_t : FramerM.gRxByteCnt
            uint8_t : FramerM.gRxHeadIndex
           uint16_t : FramerM.gRxRunningCRC
            uint8_t : FramerM.gRxState
            uint8_t : FramerM.gRxTailIndex
           uint16_t : FramerM.gTxByteCnt
            uint8_t : FramerM.gTxEscByte
           uint16_t : FramerM.gTxLength
           uint16_t : FramerM.gTxProto
           uint16_t : FramerM.gTxRunningCRC
            uint8_t : FramerM.gTxState
            uint8_t : FramerM.gTxTokenBuf
            uint8_t : FramerM.gTxUnknownBuf
           uint8_t* : FramerM.gpRxBuf
           uint8_t* : FramerM.gpTxBuf
        uint16_t[4] : GroupManagerM.forwardGroups
           uint16_t : HPLCC2420M.ramaddr
           uint8_t* : HPLCC2420M.rambuf
            uint8_t : HPLCC2420M.ramlen
           uint8_t* : HPLCC2420M.rxbuf
            uint8_t : HPLCC2420M.rxlen
           uint8_t* : HPLCC2420M.txbuf
            uint8_t : HPLCC2420M.txlen
           uint16_t : HPLUSART0M.l_br
            uint8_t : HPLUSART0M.l_ssel
           uint16_t : HPLUSART1M.l_br
            uint8_t : HPLUSART1M.l_mctl
            uint8_t : HPLUSART1M.l_ssel
            uint8_t : LedsC.ledsOn
           uint16_t : MSP430DCOCalibM.m_prev
        ramSymbol_t : RamSymbolsM.symbol
           uint16_t : RandomLFSR.initSeed
           uint16_t : RandomLFSR.mask
           uint16_t : RandomLFSR.shiftReg
            TOS_Msg : RpcM.cmdStore
           uint16_t : RpcM.cmdStoreLength
            TOS_Msg : RpcM.dripStore
           uint16_t : RpcM.dripStoreLength
               bool : RpcM.processingCommand
            TOS_Msg : RpcM.responseMsgBuf
               bool : RpcM.sendingResponse
      RpcCommandMsg : TestRpcM.m_rpcCommandMsg
            uint8_t : TestRpcM.test
           uint8_t* : TestRpcM.testPtr
               bool : TimerJiffyAsyncM.bSet
           uint32_t : TimerJiffyAsyncM.jiffy
            uint8_t : TimerM.m_head_long
            uint8_t : TimerM.m_head_short
           uint16_t : TimerM.m_hinow
         int32_t[5] : TimerM.m_period
               bool : TimerM.m_posted_checkShortTimers
               bool : UARTM.state
           uint16_t : WakeupCommM.address
               bool : WakeupCommM.busy
            uint8_t : WakeupCommM.id
            uint8_t : WakeupCommM.length
            uint8_t : WakeupCommM.sendCount
>>> app.DrainM.sendPackets.peek()
.
[<class 'pytos.util.nescDecls.TosMsg'> object at 0x4caed32c:

       TosMsg(am=5742) uint16_t,  nodeID=1:
               uint16_t value  : 2
]
>>> app.DrainM.sendPackets.peek()
.
[<class 'pytos.util.nescDecls.TosMsg'> object at 0x4caf138c:

       TosMsg(am=5742) uint16_t,  nodeID=1:
               uint16_t value  : 3
]
>>> app.DrainM.sendPackets.poke(150)
.
[<class 'pytos.util.nescDecls.TosMsg'> object at 0x4caf6c0c:

       TosMsg(am=5742) PokeResponseMsg,  nodeID=1:
               result_t value  : 1
]
>>> app.DrainM.sendPackets.peek()
.
[<class 'pytos.util.nescDecls.TosMsg'> object at 0x4cafaaac:

       TosMsg(am=5742) uint16_t,  nodeID=1:
               uint16_t value  : 151
]
>>> app.TimerM.m_period.peek(arrayIndex=0)
.
[<class 'pytos.util.nescDecls.TosMsg'> object at 0x4cafa0cc:

       TosMsg(am=5626) int32_t,  nodeID=1:
                int32_t value  : 32000
]
>>> app.TestRpcM.testPtr.peek()
.
[<class 'pytos.util.nescDecls.TosMsg'> object at 0x4cb26aac:

       TosMsg(am=5498) uint16_t,  nodeID=1:
               uint16_t value  : 4620
]
>>> app.TestRpcM.testPtr.peek(dereference=True)
.
[<class 'pytos.util.nescDecls.TosMsg'> object at 0x4cb0022c:

       TosMsg(am=5498) uint8_t,  nodeID=1:
                uint8_t value  : 0
]


Besides module variables, registry attributes are also available through the Rpc interface, and can be accessed through the RegistryC shortcut (To see how to declare a nesc Registry attribute in your nesc code, see Registry.)

cd ~/tinyos-1.x/contrib/hood/apps/TestRegistry
make telosb install.1
java net.tinyos.sf.SerialForwarder -comm serial@/dev/ttyUSB0:telos &
TestRpc.py telosb sf@localhost:9001
>>> app.RegistryC

You should see an Attribute interface available for each Registry attribute, through which you can get and set the value. The TestRegistry application sets the location.x and location.y values to TOS_LOCAL_ADDRESS and continuously sets the light value using the ADC.

RPC Functions

All functions and interfaces in an application that are declared "@rpc()" are imported into python and can be used to call functions on a mote. The rpc functions are available both through the app.rpc object and through the app.ModuleName shortcuts. To see how to declare a rpc function in your nesc code, see Rpc.

Once the rpc object is imported, it provides all rpc-able functions available as methods. Modules or interfaces with rpc functions can also be gotten as member fields. By default, rpc functions are blocking, wait 1 second, and return a list of all responses. A period is printed for each response that is received while the command is blocking.

To test rpc, you can do the following:

>>> print app.rpc
The following functions are available:
         ramSymbol_t RamSymbolsM.peek(  uint16_t memAddress, uint8_t length, bool dereference )
            uint16_t RamSymbolsM.poke(  ramSymbol_t symbol )
            result_t TestRpcM.StdControl.init( )
            result_t TestRpcM.StdControl.start( )
            result_t TestRpcM.StdControl.stop( )
                void TestRpcM.TestInterface1.testCommand1( )
            result_t TestRpcM.TestInterface1.testCommand3(  uint8_t something )
            result_t TestRpcM.TestInterface1.testCommand4(  uint8_t something, uint16_t data )
       RpcCommandMsg TestRpcM.TestInterface1.testCommand5(  RpcCommandMsg data )
       RpcCommandMsg TestRpcM.TestInterface1.testCommand6(  RpcCommandMsg data )
                void TestRpcM.TestInterface1.testCommand7(  RpcCommandMsg data )
       RpcCommandMsg TestRpcM.TestInterface1.testCommand8( )
                void TestRpcM.TestInterface2.testEvent1( )
            result_t TestRpcM.TestInterface2.testEvent3(  uint8_t something )
            result_t TestRpcM.TestInterface2.testEvent4(  uint8_t something, uint16_t data )
       RpcCommandMsg TestRpcM.TestInterface2.testEvent5(  RpcCommandMsg data )
       RpcCommandMsg TestRpcM.TestInterface2.testEvent6(  RpcCommandMsg data )
                void TestRpcM.TestInterface2.testEvent7(  RpcCommandMsg data )
       RpcCommandMsg TestRpcM.TestInterface2.testEvent8( )
                void TestRpcM.testCommand1( )
            result_t TestRpcM.testCommand3(  uint8_t something )
            result_t TestRpcM.testCommand4(  uint8_t something, uint16_t data )
       RpcCommandMsg TestRpcM.testCommand5(  RpcCommandMsg data )
       RpcCommandMsg TestRpcM.testCommand6(  RpcCommandMsg data )
                void TestRpcM.testCommand7(  RpcCommandMsg data )
       RpcCommandMsg TestRpcM.testCommand8( )
                void TestRpcM.testEvent1( )
            result_t TestRpcM.testEvent3(  uint8_t something )
            result_t TestRpcM.testEvent4(  uint8_t something, uint16_t data )
       RpcCommandMsg TestRpcM.testEvent5(  RpcCommandMsg data )
       RpcCommandMsg TestRpcM.testEvent6(  RpcCommandMsg data )
                void TestRpcM.testEvent7(  RpcCommandMsg data )
       RpcCommandMsg TestRpcM.testEvent8( )
>>> interface = module.TestInterface1
>>> print interface
        Interface TestRpcM.TestInterface1: 

           void testCommand1( )
       result_t testCommand3(  uint8_t something )
       result_t testCommand4(  uint8_t something, uint16_t data )
        ...
>>> r = rpc.TestRpcM.testCommand1(address=app.enums.TOS_BCAST_ADDR, returnAddress = app.enums.TOS_UART_ADDR)
    .
[ ...response is printed]


Function Parameters

Function parameters all default to 0 values. They can be set simply by passing them as arguments when the function is called, but they can also be set as fields on the rpc function.

In the following example, we send a rpc command to set the value of a cached value. We send it to the BCAST address and have it return to the UART address because we are assuming there is a not attached to the USB port.

First, pass the function parameter "data" as a call-time parameter. This does not effect the default function parameter values for subsequent calls of the same function.

>>> data = interface.testCommand7.data
>>> data.transactionID = 101
>>> data.commandID = 250
>>> r= rpc.TestRpcM.testCommand7(data, address=testRpc.enums.TOS_BCAST_ADDR,
          returnAddress=testRpc.enums.TOS_UART_ADDR)
    .
>>> print r[0]
TosMsg(am=22) void:
                    void value  :

>>> interface.testCommand7.data
RpcCommandMsg:
        uint16_t transactionID  : 0
            uint16_t commandID  : 0
        uint16_t returnAddress  : 0
          bool responseDesired  : 0
               bool dataLength  : 0
                  bool[0] data  :][

Now, set the function parameters as fields of the function. This does effect the default parameter values, and these values will be used in subsequent calls of the same function as well, unless they are overridden by new call-time parameters:

>>> interface.testCommand7.data.transactionID = 102
>>> interface.testCommand7.data.commandID = 251
>>> r = interface.testCommand7( address=testRpc.enums.TOS_BCAST_ADDR,
            returnAddress=testRpc.enums.TOS_UART_ADDR)
    .
>>> print r[0]
TosMsg(am=8) void:
                    void value  :
>>> print interface.testCommand7.data
RpcCommandMsg:
        uint16_t transactionID  : 102
            uint16_t commandID  : 251
        uint16_t returnAddress  : 0
          bool responseDesired  : 0
               bool dataLength  : 0
                  bool[0] data  :][

The command we just called sets a cached value on the node. Now, call testCommand8, which retrieves the cached value and we confirm that it is the same value we just sent:

>>> r = interface.testCommand8( address=testRpc.enums.TOS_BCAST_ADDR,
           returnAddress=testRpc.enums.TOS_UART_ADDR)
    .
>>> print r[0]
TosMsg(am=9) RpcCommandMsg:
        uint16_t transactionID  : 102
            uint16_t commandID  : 251
        uint16_t returnAddress  : 0
          bool responseDesired  : 0
               bool dataLength  : 0
                  bool[0] data  :][


HINT: rpc functions are subclasses of TosMsg objects, so you can check the values being sent with func.getBytes() or func.createMigMsg(). It also has a func.printCurrentValues() function for pretty-printing the default function parameters that are set on the rpc function.

Call Parameters

Call parameters are special named parameters to the rpc function that determine how it is called

  • the address is the default address that the rpc commands will be addressed to (e.g app.enums.TOS_BCAST_ADDR)
  • the returnAddress is the default address that the rpc response will be address to (eg. app.enums.TOS_UART_ADDR)
  • blocking indicates whether or not the functions should block and return the responses
  • timeout indicates how long the function should block
  • responseDesired indicates whether or not the node should send a rpc response

Call parameters can be set in four ways:

  1. as named parameters when the function is called (it applies only to this call)
  2. by setting the value on the rpc function (it applies only to this function)
  3. by setting the value on the rpc object (it applies to all functions in that object)
  4. as a constructor argument to the Rpc object

These are listed in order of precendence, ie. call-time arguments override function attribute values override rpc attribute values override constructor arguments. An example each of the four is shown below:

>>> rpc.TestRpcM.testCommand1(responseDesired = False)
>>> rpc.TestRpcM.testCommand1.responseDesired = False
>>> rpc.responseDesired = False
>>> rpc = Rpc.Rpc(app, 'telosb', comm, drain, responseDesired=False)

Enums

You can access enums as fields of the "enums" object

>>> addr = app.enums.TOS_BCAST_ADDR
>>> addr
65535

If you name your enum in the nescFile, you can access those enums as a group. For example, if you nesc code has the following declaration:

enum drainMsgs {
  AM_DRAINMSG = 4,
  AM_DRAINBEACONMSG = 7,
  AM_DRAINGROUPREGISTERMSG = 89,
};

you can acess these through the command

>>> drainMsgs = app.enums.drainMsgs
>>> drainMsgs

        AM_DRAINMSG = 4
        AM_DRAINBEACONMSG = 7
        AM_DRAINGROUPREGISTERMSG = 89

The Dynamic Typing System

The nescDecls package includes a dynamic typing system that mirrors the types in nesC applications. There are four kinds of types:

  • nescType: basic types like uint8_t, char, etc
  • nescArray: arrays of any of the four types
  • nescPointer: pointers to any of the four types
  • nescStruct: structs of any of the four types

All basic types, typedefs, and structs defined in the nesc application are imported through the app.types variable. A python variable can be made a certain nesC type simply by assigning a type to the variable.

>>> num = app.types.uint8_t
>>> msg = app.types.TOS_Msg

The typing system does import typedefs, ie. typedef names such as "bool" or "result_t" will be available through "app.types". One can also typedef a custom struct. However, the system does not import anonymous structs; if you typedef a struct, make sure the struct is not anonymous or it will not import. Finally, the system does not import sets of structs with circular pointers to each other (such types are not very useful for pc-mote communication anyway). To see the types that failed to import, call app.types.printSkippedTypes().

Important Note: Objects in python are passed by reference, not by value. If you pass a nesc-typed object and the value of the new reference changes, the value of the original object will change as well. To solve this problem use the "deepcopy()" function to create a new copy of the variable. The nescApp object always returns a deepcopy, ie a new instance for all types and messages.

The user can use the typing system to create new nesC types in python, as described in the next 4 sub-sections. The main reason to use the nescDecls typing package is that each typed variable has a getBytes() and setBytes() function, which correctly converts the value to a byte stream. It also does type-checking to make sure the value is appropriate for the type. If you are going to only use the types directly import from the nesc app, there is no reason to read the next four sections.

nescType

The basic nescType has a field called "value" which should be used to set it. nescType objects are type-safe, meaning that they give you an error when you assign a value they cannot take:

>>> variable = app.types.uint8_t
>>> variable
uint8_t
>>> variable
0
>>> variable.value = 255
>>> variable
255
>>> variable.getBytes()
'\xff'
>>> variable.setBytes('\x0f')

>>> variable
15
>>> variable.value = 256
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "/home/kamin/tinyos-1.x/contrib/python/pytos/util/nescDecls.py", line 105, in __setattr__
    pack(self._conversionString, value)
struct.error: ubyte format requires 0<=number<=255

nescArray

The nesCArray can be created dynamically from a size and another type, and they can be indexed through a [] operator:

>>> array = nescDecls.nescArray(10, variable)
>>> array
nescArray of type uint8_t[10]:
 0: 15
 1: 15
 2: 15
 3: 15
 4: 15
 5: 15
 6: 15
 7: 15
 8: 15
 9: 15

>>> array.getBytes()
'\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f'
>>> array[3] = 0
>>> array.getBytes()
'\x0f\x0f\x0f\x00\x0f\x0f\x0f\x0f\x0f\x0f'

nescPointer

A nescPointer can be constructed from any other nescTyped object. A pointer has a "value" field, through which the typed object pointed to should be set. The only difference between a pointer and the object it points to is that the pointer getBytes() command will only return 2 bytes.

>>> pointer = nescDecls.nescPointer(array)
>>> pointer
ptr-> nescArray of type uint8_t[10]:
 0: 15
 1: 15
 2: 15
 3: 0
 4: 15
 5: 15
 6: 15
 7: 15
 8: 15
 9: 15 

>>> pointer.value[1] = 101
>>> pointer
ptr-> nescArray of type uint8_t[10]:
 0: 15
 1: 101
 2: 15
 3: 0
 4: 15
 5: 15
 6: 15
 7: 15
 8: 15
 9: 15  

>>> pointer.getBytes()
'\x00\x00'
>>> pointer.value.getBytes()
'\x0fe\x0f\x00\x0f\x0f\x0f\x0f\x0f\x0f'
>>> array.getBytes()
'\x0fe\x0f\x00\x0f\x0f\x0f\x0f\x0f\x0f'

Notice that changing the pointer.value also changes the array, which the pointer is pointing to (the pointer.value field is simply a reference to the array).

nescStruct

A nescStruct is a list of nescTyped objects. Those structs imported directly from the nesc application are packed exactly as they are used in the nesc application. In other words, if the struct has bit-fields or if the compiler uses padded bytes to make things byte-aligned, the getBytes() and setBytes() commands on the corresponding struct objects will correctly handle these. However, nescStruct objects created manually from within python must be manually packed by the user.

nescStructs can be manually constructed in two different ways:

  1. a name followed by a series of (name, type) tupes
  2. another struct
>>> structA = nescDecls.nescStruct("myStruct", ("field1", variable), ("field2", array), ("field3", pointer))
>>> structA
myStruct:
                uint8_t field1  : 15
            uint8_t[10] field2  : [15, 101, 15, 0, 15, 15, 15, 15, 15, 15]
           uint8_t[10]* field3  : ptr-> [15, 101, 15, 0, 15, 15, 15, 15, 15, 15]

>>> structA.field1 = 169
>>> structA.field3.value[9] = 253
>>> structA
myStruct:
                uint8_t field1  : 169
            uint8_t[10] field2  : [15, 101, 15, 0, 15, 15, 15, 15, 15, 253]
           uint8_t[10]* field3  : ptr-> [15, 101, 15, 0, 15, 15, 15, 15, 15, 253] 

>>> structB = nescDecls.nescStruct(structA)
>>> structB
myStruct:
                uint8_t field1  : 169
            uint8_t[10] field2  : [15, 101, 15, 0, 15, 15, 15, 15, 15, 253]
           uint8_t[10]* field3  : ptr-> [15, 101, 15, 0, 15, 15, 15, 15, 15, 253]

Again, notice that changing the fields in the struct change the values of the original variables, because the struct fields are simply references to the original variables.

Messages

Any struct of type myName for which there is an enum AM_MYNAME will be considered a TOS message with amType AM_MYNAME (this is following the MIG convention that all messages have the amType declared in such an enum). All such structs are automatically cast as python TosMsg objects (described below) and made available through the "msgs" field of nescApp.

>>> print app.msgs
         211 : RpcCommandMsg
         212 : RpcResponseMsg
           4 : DrainMsg
           7 : DrainBeaconMsg
          89 : DrainGroupRegisterMsg
           3 : DripMsg

Sending and Receiving Messages

The Comm Stack

Pytos provides a communication stack for sending a receiving messages to/from the mote network. The comm stack has the following functions:

  • create comm stack instance to communicate w/network.
 import pytos.Comm as Comm
 comm = Comm.Comm()  
  • "connect or "disconnect" to a serial forwarder. These commands can take any number of MOTECOM specs. Python will assume any serialForwarders are already started and running independantly of this python/TOS program.
 comm.connect( "sf@localhost:9002") 
 comm.connect( "sf@localhost:9002", "sf@otherhost:9002", ... ) 

 comm.disconnect( "sf@localhost:9002") 
 comm.disconnect( "sf@localhost:9002", "sf@otherhost:9002", ... ) 
  • "send", "register" or "unregister" a message "msg." "msg" can either be a python TosMsg object (described below) or a java class that subclasses net.tinyos.message.Message classes, eg. a MIG-generated message. All three commands can take any number of optional MOTECOM specs to indicate which connections the function should operate on. Sending no MOTECOM specs indicates that the function should operate on all existing connections. If the desired MOTECOM specs are not already connected to, the functions will first connect to them.
    • On send, the parameter "addr" indicates what address should be put in the msg.
    • On "register" and "unregister", the parameter "queue" is the Comm.MessageQueue object (Described below
 comm.send(addr, msg)
 comm.send(addr, msg, "sf@localhost:9002") 
 comm.send(addr, msg, "sf@localhost:9002", "sf@otherhost:9002", ... )

 comm.register(msg, queue)
 comm.register(msg, queue, "sf@localhost:9002") 
 comm.register(msg, queue, "sf@localhost:9002", "sf@otherhost:9002", ... )

 comm.unregister(msg, queue)
 comm.unregister(msg, queue, "sf@localhost:9002") 
 comm.unregister(msg, queue, "sf@localhost:9002", "sf@otherhost:9002", ... )

Each comm stack is a collection of connections, and therefore each python program should create its own comm stack to avoid interference with each other, even if they are running in the same python environment (unless they explicitly want to share the same collection of connections). A program can also create multiple comm stacks for itself, if desired. The disavdantage of doing that is that each comm stack opens a new socket to the running serial forwarder.

The TosMsg Object

The nescDecls.TosMsg class is a subclass of the nescStruct object and has all of the same properties:

  1. it is correctly "packed" when imported directly from the nesc app
  2. it must be manually packed when created manually
  3. it has fields that hold its values
  4. it can be constructed from either a nescStruct or a name followed by types of field/type pairs

The TosMsg also has 2 more properties

  1. it has an amType, which must always be the first parameter in the constructor
  2. it can be converted to/from java Mig messages with the createMigMsg() and parseMigMsg() functions

When the parseMigMsg() or setBytes() commands are being called on a TosMsg and there are not enough bytes, it throws an error just like a nescStruct would. However, if there are too many bytes and the last field of the TosMsg is a nescArray of length 0, that array is expanded to be as large as necessary to hold the extra data. This behavior is designed to accomodate the TinyOS convention that nested packets are held within the "data" field of the wrapper header, which is usually an array of length 0. We will see an example of how this comes in handy in the next section. Important note: wrapper headers used as such must be "packed", otherwise nesc may not report the correct length of the packet and you may have errors.


The TosMsg is "hierarchical" because it has a parentMsg field, which is used when TosMsgs are nested. For example, if MyMsg is received over the Drain routing layer, mymsg.parentMsg would point to the original drain message, with the drain headers. If the drain message were received over another routing layer, myMsg.parentMsg.parentMsg would point to those headers. Any ancestor message can be gotten through the myMsg.getParentMsg() function, which takes either the ancestor message name or amType. The rpc function response message is a good example of a Hierarcical TosMsg... try the following on an rpc function response:

>>> print r[0].parentMsg
TosMsg(am=212) RpcResponseMsg:
        uint16_t transactionID  : 0
            uint16_t commandID  : 9
        uint16_t sourceAddress  : 0
            uint16_t errorCode  : 0
               bool dataLength  : 8
                  bool[8] data  : [102, 0, 251, 0, 0, 0, 0, 0]

>>> print r[0].parentMsg.parentMsg
TosMsg(am=4) DrainMsg:
                     bool type  : 212
                      bool ttl  : 15
               uint16_t source  : 0
                 uint16_t dest  : 126
                 bool[17] data  : [0, 0, 9, 0, 0, 0, 0, 0, 8, 102, 0, 251, ...]

We can see that the r[0] message is just a parsing of the RpcResponseMsg.data bytes based on the commandID=9. The RpcResponseMsg is just a parsing of the DrainMsg.data bytes based on the type=212.


The following examples show how to use and, if necessary, create your own TosMsg objects.

>>> msgA = app.msgs.DripMsg
>>> msgA.amType
3
>>> msgB = nescDecls.TosMsg(104, structA)
>>> msgB
TosMsg(am=104) myStruct:
                uint8_t field1  : 169
            uint8_t[10] field2  : [15, 15, 15, 0, 15, 15, 15, 15, 15, 254]
           uint8_t[10]* field3  : ptr-> [15, 101, 15, 0, 15, 15, 15, 15, 15, 253]

>>> migMsg = msgB.createMigMsg()
>>> migMsg
Message <BaseTOSMsg>
  [addr=0xfa9]
  [type=0xf]
  [group=0xf]
  [length=0x0]
  [data=0xf 0xf 0xf 0xf 0xfe 0x0 0x0 0x0
>>> msgC = nescDecls.nescStruct("myMsg", ("field1", variable), ("field2", array), ("field3", pointer))
>>> msgC
myMsg:
                uint8_t field1  : 0
            uint8_t[10] field2  : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
           uint8_t[10]* field3  : ptr-> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 

>>> msgC = nescDecls.TosMsg(99, "myMsg", ("field1", variable), ("field2", array), ("field3", pointer))
>>> msgC
TosMsg(am=99) myMsg:
                uint8_t field1  : 0
            uint8_t[10] field2  : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
           uint8_t[10]* field3  : ptr-> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

>>> msgC.parseMigMsg(migMsg)
>>> msgC
TosMsg(am=104) myMsg:
                uint8_t field1  : 169
            uint8_t[10] field2  : [15, 15, 15, 0, 15, 15, 15, 15, 15, 254]
           uint8_t[10]* field3  : ptr-> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Of course, the pointer to the array in field3 is neither packed nor unpacked, and therefore msgC.field3 does not have the same value as msgB.field3.

Message Queues

A MessageQueue is used to receive messages and can buffer at most a pre-specified number of messages. If the size of the queue grows too large, the oldest messages are discarded. A MessageQueue can be registered to receive a single type of message or multiple types of messages, and then a thread can loop over the queue to get the messages. Here is a simple example (warning: this will lock up your python session if you don't actually have messages arriving):

>>> import pytos.Comm as Comm
>>> comm = Comm.Comm()
>>> comm.connect('sf@locahost:9001')
>>> msg = app.msgs.DrainMsg
>>> msgQ = Comm.MessageQueue(5)
>>> comm.register(msg, msgQ)
>>> for i in range(100) : #receive 100 messages and print them
...   (addr, msg) = msgQ.get()
...   msg

One can also use the threading package to start a new thread to process the messages, as shown in the python/apps/Oscope.py file. When the thread is set to be a "daemon" thread, it will run indefinitely until all non-daemon threads (such as the main thread) are dead. Example:

   ....main...
       msgThread = threading.Thread(target=self.processMessages)
       msgThread.setDaemon(True)
       msgThread.start()

   def processMessages(self) :
       while True :
           (addr,msg) = self.msgQueue.get()
           msg

Listen

As a simple example of what PyTOS can do, run the Listen.py tool, which is equivalent to the C and Java Listen tools except that it automatically parses the message payloads.

Listen.py telosb sf@localhost:9001

This version of Listen allows the user to filter messages and set different levels of verbosity. See a Screenshots for basic operation with the OscilloscopeRF application.

GUIs

Gui material is still being developed. For now, look at a few examples in:

contrib/python/Oscope.py

contrib/nestfe/nesc/apps/TestDetectionEvent/TestDetectionGui.py

To run the oscope application, do the following:

cd tinyos-1.x/apps/Oscilloscope
make telosb install.1 nescDecls
Oscope.py telosb serial@/dev/ttyUSB0:telos

Alternatively, you can use OscopeAlternate.py simply by compiling the java tools in net.tinyos.oscope. This version shows how to use Mig objects and event-based message handling.

Advanced

Using Mig Messages Directly

Mig objects can be used directly through the jpype.jimport module.

from jpype import jimport
oscopeMsg = jimport.net.tinyos.oscope.OscopeMsg()
comm.send(65535, oscopeMsg)

Furthermore, mig objects can be received directly by using them in the register/unregister commands.

Event driven programming

Although the description of the comm stack API above indicates that the register/unregister functions take the Comm.MessageQueue object as a parameter, they can actually take any messageReceived function or object which has that function. The messageReceived functions must take two parameters: "addr" and "msg". This is a way to provide a callback function for message handling, ie. to perform event-driven message handling.

In fact event-driven programming in general is possible in python is a manner similar to that used in TinyOS when one uses the pytos.util.Timer object. To create a timer that will first pause for waitTime seconds, and then every period seconds invoke the function callbackFcn exactly numFirings times, do this

 t = Timer( callbackFcn , period , numFirings , waitTime )

This timer is then started with

 t.start()

and may be cancelled at any time with

 t.cancel()

Additionally, at any point, a timer may be restarted with

 t.start()

There several reasons, however, not event-driven programming in python, particularly for message handling:

  1. event-based code needs to be made thread safe since it can be called by multiple threads. Message handling code, for example, can be called by multiple serialForwarder threads simultaneously.
  2. With event-driven message handling, the message handling is being done on the data source's thread of control (e.g the serialForwarder thread). This means that the data souce (such as the serial forwarder) cannot process incoming packets while the user is handling messages
  3. python errors that happen on the java SerialForwarder thread of control are difficult to handle
  4. It is easier to write for-loops using threads than as event-driven loops

Inheriting from Java tools

Java tools can be used from the python command line simply by importing them with the jpype.jimport library. For example:

from jpype import jimport
drain = jimport.net.tinyos.drain.Drain()

However, one may want to add python-specific functions to the drain tool, such as the ability to register as a listener for a python TosMsg object instead of a java mig object. To do this, we create a new python object that inherits from the java object using the pytos.util.JavaInheritor object. The Drain.py object is a good example: it inherits from both Drain.java (which builds the drain tree) and DrainConnector.java (which listens for messages on the drain tree), and then adds new functions for registering as a listener of incoming TosMsg objects.

To use the JavaInheritor object, follow three steps:

  1. create a new object that inherits from the JavaInheritor
  2. in the constructor of that object, instantiate the java objects you want it to inherit from
  3. call the JavaInheritor constructor with those objects.

To see how the python version of the Drain tool does it:

from jpype import jimport  

drain = jimport.net.tinyos.drain

class Drain( JavaInheritor ) : 

    def __init__( self ) :
        drainObj = drain.Drain(spAddr, moteIF)
        drainConnectorObj = drain.DrainConnector(spAddr, moteIF)
        JavaInheritor.__init__(self, (drainObj, drainConnectorObj) )

Now, you can print an instance of the python Drain object to see what it inherits from the Java objects:

>>> drain = pytos.util.Drain.Drain()
>>> drain
Python object derived from java classes:
        net.tinyos.drain.Drain
        net.tinyos.drain.DrainConnector

The following java fields/methods are inherited:
        Drain.main()
        Drain.buildTree()
        Drain.sendTo()
        Drain.send()
        DrainConnector.registerListener()
        DrainConnector.deregisterListener()
        DrainConnector.setDebug()
        DrainConnector.messageReceived()

Adding a New Layer of Dispatch

This section is for pytos developers. If you are just a user of pytos, skip this section.

Unlike statically typed systems like MIG, the dynamic typing system described above allows multiple levels of dispatch. A new layer of message dispatch can be added by creating a new MessageListener which unpacks an incoming message, removes the data field, creates a new msg with the data, and passes the new message on to another MessageListener. See pytos.tools.Drain.py or pytos.tools.Rpc.py for an example.

There are three steps to creating multiple layers of dispatch:

  1. All message listeners must subclass the Comm.MessageListener class, which allows java hashing to work and therefore allows both the java registerListener and deregisterListener commands to work
  2. When the migMsg is received and the TosMsg.parseMigMsg() command is called at a low-level of dispatch, the migMsg data field will be longer than the TosMsg data field (because the low-level of dispatch does not yet know how to parse the rest of the data). The last field of the TosMsg must therefore be a nescArray of length 0 and the TosMsg will expand this field to be as long as necessary to hold all unparsable bytes from the mig Msg (as described above). This data array can then be used to construct the new packet for the next level of dispatch. Important note: wrapper headers used as such must be "packed", otherwise nesc may not report the correct length of the packet and you may have errors.
  3. the old message should be added as the parent of the new message

An example from the Drain.py file is included here:

class DrainMsgPeeler( Comm.MessageListener ) :

  def __init__(self, app, msg, callback) :
    self.drainMsg = nescDecls.TosMsg(app.enums.AM_DRAINMSG, app.types.DrainMsg)
    self.msg = msg
    Comm.MessageListener.__init__(self, callback )
    
  def messageReceived( self , addr , migMsg ) :
    drainMsg = deepcopy(self.drainMsg)
    drainMsg.parseMigMsg(migMsg)      #drainMsg is expanded as necessary to hold extra data
    msg = deepcopy(self.msg)
    bytes = drainMsg.data.getBytes()  
    msg.setBytes( bytes )             # a new message is created from the extra data
    msg.parentMsg = drainMsg          #the old message is set as the new msg's parent
    self.callback( addr, msg )