Tag Entry/Exit Server Protocol Source Code
The AngleID software distribution includes:
- Code written in C# to implement the tag entry/exit protocol and to show how to write a simple command-line client.
- A tree view showing recipes and the tags that match them.
- An SQL Server client that maintains a table of recipe/tag matches.
You are free to use this code to:
- Get a better understanding of the entry/exit protocol so that you can implement it yourself in another language, or
- Take the DLL containing the protocol implementation and use it directly in your projects as a .NET API to AngleID, or
- Treat the client programs as a debugging tool for your recipe definitions, or as a starting point for your applications, or
-
Do anything else you like, for any purpose, as long as you accept the following standard disclaimer for our protection.
Disclaimer
Writing a Tag Entry/Exit Server
AngleID is designed to make it a very simple job to write a tag entry/exit server. In particular, the AngleID reader takes responsibility for managing the connection, detecting connection failure, re-establishing the connection, and re-establishing the state of the remote side. The device simply has to implement a server that accepts a TCP connection on the chosen port and implement the AngleID protocol over the resulting byte stream, falling back to accept a new connection if the protocol's byte stream fails.
Tag Entry/Exit Protocol
The tag/entry exit protocol is very simple. This section describes the essential parts to help you to understand the code so that you can insert it into your environment, or translate it into another language. You can find the code in the files IByteStream.cs and TagEntryExitProtocol.cs in the code distribution.
Socket Abstraction
The protocol is defined using an abstract byte stream that provides functions for reading into a buffer and writing out of it. These are available in any kind of socket interface, though in C the buffer, offset pair would usually be replaced by a character pointer.
The usual way to implement this (and the way it has been implemented in the example code) is to use a non-blocking socket, wait on a select call to block until the socket is readable, and then fail if the socket is no longer connected or if it turns out that there are no bytes to be read (although the select call said that the socket was readable).
/// <summary> /// An abstraction of a network socket which supports /// reading or writing into or out of a buffer of bytes /// </summary> public interface IByteStream { /// <summary> /// Read a specified number of bytes into a buffer /// </summary> /// <param name="buffer">The buffer to read to</param> /// <param name="offset">The buffer offset to start writing the results</param> /// <param name="bytes">The number of bytes to read</param> void Read(byte[] buffer, int offset, int bytes); /// <summary> /// Write a specified number of bytes from a buffer /// </summary> /// <param name="buffer">The buffer to read from</param> /// <param name="offset">The buffer offset to read the message from</param> /// <param name="bytes">The number of bytes to write</param> void Write(byte[] buffer, int offset, int bytes); }
Message Buffering and Sizing
The entry/exit protocol is guaranteed to fit every message into a 256-byte array, so you can implement it easily without requiring a big chunk of buffer. All messages have the same format:
- Three ASCII characters representing the length of the whole message in bytes (in decimal).
- One ASCII character representing the message type (in decimal).
- Optionally, five ASCII characters representing the recipe's Id 1 (in decimal).
- Optionally, five ASCII characters representing the recipe's Id 2 (in decimal).
- Between zero and fifteen blocks of sixteen ASCII characters, each block representing a 64bit tag id (in hex).
- A terminating zero byte.
#region AngleID tag entry/exit protocol message storage and section lengths // Messages are guaranteed to be less than 256 bytes long
byte[] Buffer = new byte[256]; // The first three bytes of each message are used to encode the length // of the message without its subsequent tail
const int MessageLengthBytes = 3; // The next byte of the message is used to encode the message type
const int MessageTypeBytes = 1; // The (optional) next ten bytes of the message are used to encode the // two message ids, in two blocks of five
const int MessageRecipeIdBytes = 5; // Subsequent bytes of the message (up to 240 bytes) are used to encode // up to 15 tag ids.
const int MessageTagIdBytes = 16; // The tail of the message is a single byte containing the zero ASCII character
const int MessageTailBytes = 1; #endregion
Message Types
There are six message types represented by the decimal numbers 0 - 5:
- 0: a tag exit message
- 1: a tag entry message
- 2: a keepalive message
- 3: a 'session start' message
- 4: a 'session stop' message
- 5: an 'ack' message
Types 0 - 4 are sent by the reader side, and type 5 is sent by the other side. If the protocol is extended in future releases, protocol versioning information will be dealt with by adding extra data on the end of the 'session start' and 'ack' messages (and maintaining the property that the first three bytes encode the length of the whole message); this means that the protocol is extensible but we do not have to explicitly include versioning handshakes in this release of the protocol.
#region AngleID tag entry/exit protocol message type codes // The code of a tag exit message, indicating that the specified recipe // is now disabled for a given list of tags
const int ExitMessageCode = 0; // The code of a tag entry message, indicating that the specified recipe // is now enabled for a given list of tags
const int EntryMessageCode = 1; // The code for a protocol keepalive message
const int KeepaliveMessageCode = 2; // The code for a session start message code, indicating that a new protocol // session has started
const int SessionStartMessageCode = 3; // The code for a session stop message code, indicating that the protocol // session is stopping. This will normally not be received in cases where // we lose network connectivity to the sensor, so application code should // not rely on this message in order to guarantee correctness.
const int SessionStopMessageCode = 4; #endregion
Protocol Logic
In English, the protocol logic works as follows:
- Read a message by getting the initial three byte length section, decoding it, using it to read the rest of the message into the buffer.
- Decode the message type byte into decimal.
-
If the message type is 'exit' or 'entry', decode the rest of the message:
- Decode the five-byte recipe Id 1.
- Decode the five-byte recipe Id 2.
- Repeatedly decode all sixteen-byte tag ids that fit in the message (up to fifteen, depending on the message length).
- Raise a protocol event using the appropriate handler, according to the message type.
- If the message type is 'keepalive', 'session start', or 'session stop', no more decoding is required so raise a protocol event using the appropriate handler, according to the message type.
- Send an ack message.
- If the 'session stop' message was received, raise an exception to the server so it can shut down.
- Go back to step 1.
#region AngleID tag entry/exit protocol logic /// <summary> /// Read a protocol message into the buffer /// </summary> /// <param name="socket">The socket to read from</param> /// <returns>The length of the protocol message</returns>; int ReadMessage(IByteStream socket) { socket.Read(Buffer, 0, MessageLengthBytes); int length = DecodeDecimal(0, MessageLengthBytes); socket.Read(Buffer, MessageLengthBytes, length - MessageLengthBytes + MessageTailBytes); return length; } // The ack message sent to acknowledge every AngleID message const string AckMessage = "0045"; /// <summary> /// Send a protocol ack message via the buffer /// </summary> /// <param name="socket">The socket to use to send the message</param>; void WriteAck(IByteStream socket) { ASCIIEncoding.ASCII.GetBytes(AckMessage, 0, 4, Buffer, 0); Buffer[4] = 0; socket.Write(Buffer, 0, 5); } /// <summary> /// Decode an entry of exit message and invoke a given handler /// </summary> /// <param name="handler">The event handler to invoke</param>; /// <param name="length">The message length</param>; void HandleEntryOrExitMessage(ProtocolEntryOrExitHandler handler, int length) { // Decode the first id int offset = MessageLengthBytes + MessageTypeBytes; ushort id1 = (ushort)DecodeDecimal(offset, MessageRecipeIdBytes); // Decode the second id offset += MessageRecipeIdBytes; ushort id2 = (ushort)DecodeDecimal(offset, MessageRecipeIdBytes); // Decode the list of tags offset += MessageRecipeIdBytes; List<ulong> tags = new List<ulong>(); while (offset < length) { tags.Add(DecodeTag(offset)); offset += MessageTagIdBytes; } handler(id1, id2, tags); } /// <summary> /// Handle a single protocol message, invoking appropriate protocol /// events and sending back an acknowledgement /// </summary> /// <param name="socket">The protocol socket</param> internal void HandleProtocolMessage(IByteStream socket) { // Read the message into the buffer and retrieve its length int length = ReadMessage(socket); // Call the generic handler (this would normally be used for debugging) if (OnMessage != null) OnMessage(ASCIIEncoding.ASCII.GetString(Buffer, 0, length)); bool session_terminated = false; // Decode the message type and dispatch to the appropriate handler // or throw an exception if the message type is not recognized switch (DecodeDecimal(MessageLengthBytes, MessageTypeBytes)) { case ExitMessageCode: if (OnExit != null) HandleEntryOrExitMessage(OnExit, length); break; case EntryMessageCode: if (OnEntry != null) HandleEntryOrExitMessage(OnEntry, length); break; case KeepaliveMessageCode: if (OnKeepalive != null) OnKeepalive(); break; case SessionStartMessageCode: if (OnSessionStart != null) OnSessionStart(); break; case SessionStopMessageCode: if (OnSessionStop != null) OnSessionStop(); session_terminated = true; break; default: throw new Exception("unrecognized message: " + ASCIIEncoding.ASCII.GetString(Buffer,0,length)); } // Acknowledge the message WriteAck(socket); if (session_terminated) throw new Exception("session stop received, connection is closing"); } #endregion
Protocol Events
If you are not familiar with C#, the delegate keyword just defines the signature of a function, and the event keyword provides a point for the user to register a function with the specified signature to be called when the event is invoked. So in C this would be done by registering a callback.
#region AngleID tag entry/exit protocol events /// <summary> /// A handler for undecoded AngleID protocol messages /// </summary> /// <param name="message">The entire undecoded message</param> public delegate void ProtocolMessageHandler(string message); /// <summary> /// A handler for AngleID entry or exit messages /// </summary> /// <param name="id1">The value in the recipe of Id 1</param> /// <param name="id2">The value in the recipe of Id 2</param> /// <param name="tags">The list of tags for which the recipe is activated or deactivated</param> public delegate void ProtocolEntryOrExitHandler(ushort id1, ushort id2, List<ulong> tags); /// <summary> /// A handler for AngleID keepalive, session start or session stop events /// </summary> public delegate void ProtocolEventHandler(); /// <summary> /// This event is invoked whenever an individual message is detected. /// It is called with the entire ASCII test of the undecoded message, /// so it would normally be used to aid debugging. /// </summary> public event ProtocolMessageHandler OnMessage; /// <summary> /// This event is invoked whenever a recipe is activated for some tags /// </summary> public event ProtocolEntryOrExitHandler OnEntry; /// <summary> /// This event is invoked whenever a recipe is deactivated for some tags /// </summary> public event ProtocolEntryOrExitHandler OnExit; /// <summary> /// This event is invoked whenever a keepalive is received /// </summary> public event ProtocolEventHandler OnKeepalive; /// <summary> /// This event is invoked whenever a session is started. Application /// code can rely on raising this event when a connection is opened /// from the AngleID reader to the server /// </summary> public event ProtocolEventHandler OnSessionStart; /// <summary> /// This event may be invoked when a session ends, but the if session /// ends due to any kind of connection failure or other interruption /// this event will not fire, so higher level application code should /// ensure that it will maintain correctness if this event is not raised /// </summary> public event ProtocolEventHandler OnSessionStop; #endregion
Decoding ASCII Text
For completeness, here are the functions that we use to decode parts of the buffer into decimal numbers and tag ids.
#region Functions to decode decimal numbers and hex tag ids from the ASCII buffer // The byte that represents the ASCII character '0' byte ASCII_0 = Convert.ToByte('0'); /// <summary> /// Decode a decimal number from the buffer /// </summary> /// <param name="offset">The offset from the start of the buffer of the first digit</param> /// <param name="digits">The number of digits in the number to decode</param> /// <returns>The decoded number</returns> int DecodeDecimal(int offset, int digits) { int result = 0; for (int i = 0; i < digits; ++i) { result *= 10; result += (int)Buffer[offset + i] - ASCII_0; } return result; } // The bytes that represent the ASCII characters '9' and 'A' byte ASCII_9 = Convert.ToByte('9'); byte ASCII_A = Convert.ToByte('A'); /// <summary> /// Decode a tag id from the buffer /// </summary> /// <param name="offset">The offset from the start of the buffer of the first digit</param> /// <returns>The decoded tag id</returns> ulong DecodeTag(int offset) { ulong result = 0; for (int i = offset; i < offset + MessageTagIdBytes; ++i) { result *= 16; result += (ulong)(Buffer[i] > ASCII_9 ? Buffer[i] - ASCII_A + 10 : Buffer[i] - ASCII_0); } return result; } #endregion
|