Tuesday 6 October 2009

Argot Meets Contiki

I've spent the last week getting a demo Argot application communicating with a Contiki device. Thankfully, I was able to dust off the code developed in 2005 and get it working reasonably painlessly. The end result is a java client and a Contiki host with a published method that can be called via UDP from a Java application.

The Contiki device has implemented a very simple interface as defined by the following Argot source file. It defines a “test” interface and a “doSomething” method which is attached to the “test” interface. The “doSomething” method takes a single 32-bit integer as a request and responds with a 32-bit integer. The method multiplies the input by three and sends back the result.


(library.entry
(library.definition meta.name:"test" meta.version:"1.3")
(remote.interface))

(library.entry
(library.relation #test meta.version:"1.3" u8utf8:"doSomething")
(remote.method u8ascii:"doSomething"
[ (remote.parameter #int32 u8ascii:"param" ) ]
[ (remote.parameter #int32 u8ascii:"ret" ) ]
[ ]
)
)

This Argot source file includes types like “remote.method” and “remote.interface” that were not defined by the meta dictionary in the last post. These other types are also defined in other argot files. For brevity, I haven't included them here. I might cover them in a post later.

The java client uses the meta information supplied by the Argot dictionary to allow a Java Interface to be bound to the meta data. The result is that calling the method is simple:
public void testApp() throws Exception
{
// Create the object reference
MetaObject objectReference = new MetaObject(
new SimpleLocation(2,"local"),
_client.getTypeLibrary().getTypeId(ITestClass.TYPENAME,"1.3") );

// Call the test method and check the result.
ITestClass test = (ITestClass) _client.getFront( objectReference );

// Call the remote method.
int result = test.doSomething(10);

assertEquals( result, 30 );
}
The first line creates the object reference. It consists of a location and a type. The location is an abstract data type meaning that different types of location specifiers can be used. In this case the location specifier is a simple Integer. The server contains a list of index based objects. For small devices this is appropriate, however, other devices may wish to use URLs or other location specifier. The second parameter is the identifier of the “test” interface.

The second line retrieves the interface to the “test” interface. In Java this has been implemented as the ITestClass. During setup the ITestClass is bound to the “Test” interface definition. All the methods are checked to ensure they are consistent with the methods that are defined in the Argot dictionary.

Finally, the method “doSomething” is called with the value 10. The end of the test asserts that the final result is 30.

On the Contiki side, I created a static dictionary structure of all the data types required by the interface. This included the full meta dictionary as defined in the last post, the “test” interface and method definition and everything in between. The end result is a dictionary with 60 entries and uses about 3kb of data. This can probably be halved with a few changes; I'll get to those later.

Argot uses a Type Resolution Protocol to create a tight data binding between client and server. The protocol uses only a few basic concepts. These are:
  • TYPE_META – The initial request sends a challenge to request the meta dictionary. As this is the base for all other types, the client and server must ensure these are the same on both sides.

  • TYPE_MAP – A type map request sends a data type dictionary location and definition and returns a type identifier.

  • TYPE_MAP_DEFAULT – Sends a type name and returns a location with version and definition. The client then checks its own dictionary for a match.

  • TYPE_RESERVE – In cases where the data type is self referencing the client must retrieve and identifier for the type to adequately define the type. This reserves an identifier before TYPE_MAP is called.

  • TYPE_REVERSE – In cases where the server sends a client an identifier that the client hasn't mapped, the client sends the identifier and is returned the location with version and definition of the data type.

  • TYPE_BASE – This is a type of boot strap discovery mechanism. The client is able to request the base type or interface to be found on the host.<.li>

  • TYPE_MSG – Finally, the last message is a user defined message and is for the application protocol being used.
These basic messages allow a client to discover from first principles all the data types required to communicate with a host. The following demonstrates the messages being sent/received between the java application and Contiki host.

Note: The very first message should be a TYPE_META message. This is yet to be implemented on Contiki as it needs to be changed from the previous method of making this call. Previously the client would send the full meta dictionary so that the server can perform a binary compare. To reduce size this will be reversed. The client will issue a meta dictionary challenge; the server will respond with either the full meta dictionary or just a meta dictionary version. Obviously on small devices the ability to remove the 35 types from the meta dictionary will help size greatly. To reduce size further the response to the meta dictionary challenge could be a URL of where to find the device dictionary. In this way the benefits of the Argot protocol could fit the smallest of devices.

The following shows the full conversation between Java client and Contiki host:
MAP DEFAULT: remote
-> 02 00 09 0b 00 06 r e m o t e
<- 02 00 00 00 23 0b 00 06 r e m o t e 00 01 05

MAP DEFAULT: rpc
-> 02 00 06 0b 23 03 r p c
<- 02 00 00 00 3d 0b 23 03 r p c 00 01 05

MAP DEFAULT: remote.rpc.request
-> 02 00 0a 0b 3d 07 r e q u e s t
<- 02 00 00 00 3e 0c 3d 07 r e q u e s t 01 03 00 23 0f 03 0e 08 l o c a t i o n 0d 25 0e 06 m e t h o d 0d 28 0e 04 d a t a 10 0d 01 0d B

MAP DEFAULT: remote.location
-> 02 00 0b 0b 23 08 l o c a t i o n
<- 02 00 00 00 25 0c 23 08 l o c a t i o n 01 03 00 02 07 00

MAP DEFAULT: uint16
-> 02 00 09 0b 00 06 u i n t 31 36
<- 02 00 00 00 28 0c 00 06 u i n t 31 36 01 03 00 09 13 10 10 04 16 10 17 18 19

MAP DEFAULT: meta.identified
-> 02 00 0d 0b 03 0a i d e n t i f i e d
<- 02 00 00 00 B 0c 03 0a i d e n t i f i e d 01 03 00 11 0f 01 0e 0b d e s c r i p t i o n 0d 08

MAP DEFAULT: remote.rpc.response
-> 02 00 0b 0b 3d 08 r e s p o n s e
<- 02 00 00 00 3f 0c 3d 08 r e s p o n s e 01 03 00 18 0f 02 0e 07 i n E r r o r 0d 3c 0e 04 d a t a 10 0d 01 0d B

MAP DEFAULT: bool
-> 02 00 07 0b 00 04 b o o l
<- 02 00 00 00 3c 0c 00 04 b o o l 01 03 00 02 0d 01

MAP DEFAULT: index (definition)
-> 02 00 08 0b 00 05 i n d e x
<- 02 00 00 00 27 0c 00 05 i n d e x 01 03 00 04 0f 01 0d 28

MAP: index (relation)
-> 01 00 08 0d 25 05 i n d e x 00 02 06 27
<- 01 00 00 00 26

MAP DEFAULT: remote.method
-> 02 00 09 0b 23 06 m e t h o d
<- 02 00 00 00 2e 0c 23 06 m e t h o d 01 03 00 33 0f 04 0e 04 n a m e 0d 2f 0e 07 r e q u e s t 10 0d 01 0d 30 0e 08 r e s p o n s e 10 0d 01 0d 30 0e 05 e r r o r 10 0d 01 0d 28

MAP DEFAULT: u8ascii
-> 02 00 0a 0b 00 07 u 38 a s c i i
<- 02 00 00 00 2f 0c 00 07 u 38 a s c i i 01 03 00 10 12 10 0d 01 0d 01 09 I S O 36 34 36 2d U S

MAP DEFAULT: remote.parameter
-> 02 00 0c 0b 23 09 p a r a m e t e r
<- 02 00 00 00 30 0c 23 09 p a r a m e t e r 01 03 00 12 0f 02 0e 04 t y p e 0d 28 0e 04 n a m e 0d 2f

MAP: remote.method (relation)
-> 01 00 10 0d 0b 0d r e m o t e 2e m e t h o d 00 02 06 2e
<- 01 00 00 00 2d

MAP_DEFAULT: remote.interface
-> 02 00 0c 0b 23 09 i n t e r f a c e
<- 02 00 00 00 2b 0c 23 09 i n t e r f a c e 01 03 00 07 0f 01 10 0d 01 0d 28

MAP: remote_interface remote.interface (relation)
-> 01 00 13 0d 0b 10 r e m o t e 2e i n t e r f a c e 00 02 06 2b
<- 01 00 00 00 2a

MAP: test (interface)
-> 01 00 09 0c 00 04 t e s t 01 03 00 02 2b 00
<- 01 00 00 00 29

MAP: int32
-> 01 00 0a 0c 00 05 i n t 33 32 01 03 00 09 13 20 20 04 16 20 17 18 19
<- 01 00 00 00 33

MAP: test.doSomething (method definition relation)
-> 01 00 0e 0d 29 0b d o S o m e t h i n g 00 20 2e 0b d o S o m e t h i n g 01 00 33 05 p a r a m 01 00 33 03 r e t 01 00 33
<- 01 00 00 00 32
All the above is purely related to the data type agreement required before actually making the real method call. Obviously this is expensive and would not be performed every time. The client would cache the mappings already performed so that they can be used later.

The most important aspect of the list of data types resolved above is that only the data types required to make the doSomething method call are resolved. Additional types may need to be resolved before calling another method. This allows the client to only resolve the parts of the interface it uses. There are numerous advantages to this which I'll cover in another post.

Finally the actual method call is made.

MSG:
-> 07 27 00 02 00 32 01 00 33 00 00 00 0a
<- 07 00 01 00 33 00 00 00 1e

The final message request can be broken down as the following. It is defined by the remote.rpc.request type:
 07 – Message Request
27 – Location identifier type 27 (index)
00 02 – Location index 2. This is currently uint16. Could be modified to uvint28 to reduce a byte.
00 32 – Method identifier. Could also be changed to use uvint28.
01 – Number of parameters.
00 33 – First parameter is a int32.
00 00 00 0a – The input value 10.
The result is a defined by remote.rpc.response and is broken down as follows:
 07 – Message response
00 – In Error flag. Boolean value. False.
01 – One parameter returned.
00 33 – First parameter is int32.
00 00 00 1e – The return value 30.
The remote.rpc protocol is very simple and is missing some elements that would be required for a UDP implementation. For instance an identifier so that requests could be matched up with correct responses would be required.

It should also be noted that remote.interface and remote.method define a style of RPC mechanism. Different meta data could be produced to define a REST like interfaces and protocols.

There's still plenty to be done to get the Internet Draft ready by the 19th of October. Hopefully the above provided a small insight to how Argot works and can be implemented on even the smallest of devices.

3 comments:

  1. From what I understand, you are using UDP for the RPC, which means that you will need to implement your own sequence numbers, round-trip time measurements, and retransmissions. Have you thought about using TCP instead? That will give you a reliable channel with a much lower overhead than a custom-TCP-over-UDP.

    ReplyDelete
  2. you're correct that I'm using UDP for the RPC. This was done as a proof of concept more than anything. In the past I've normally used TCP, Pipe or even HTTP (tunneled) transports.

    In the Smart Energy 2.0 and 6lowapp discussions various people have said that TCP would be too much overhead for a 6lowpan network. In those situations they suggest that UDP would be better. Do you think that's a fair assumption?

    ReplyDelete
  3. If you want reliability over UDP you typically end up recreating most of TCP anyway (but typically in a worse way, that hasn't been thoroughly debugged in the same way that TCP has), and you lose all the interoperability benefits of TCP.

    TCP has its share of problems over wireless links, but they mostly have to do with the performance of large bulk data transfers, which we are unlikely to have in low-power radio networks anyway.

    The TCP header overhead can be reduced through header compression instead of through defining custom last-hop protocols over UDP.

    ReplyDelete

Note: only a member of this blog may post a comment.