Skip to content

Commit

Permalink
Add state machine to decode a packet stream, return payloads.
Browse files Browse the repository at this point in the history
  • Loading branch information
cibomahto committed Apr 12, 2012
1 parent 2b9a36d commit 2ea4c5c
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 3 deletions.
26 changes: 26 additions & 0 deletions doc/PacketStreamDecoder.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// vim:ai:et:fenc=utf-8:ff=unix:sw=4:ts=4:

digraph
{
"READY" -> "VALID HEADER?" [label="receive byte"];
"VALID HEADER?" [shape=diamond];
"VALID HEADER?" -> "READY" [label="no"];
"VALID HEADER?" -> "WAIT FOR LENGTH" [label="yes"];
"WAIT FOR LENGTH" -> "READY" [label="timeout"];
"WAIT FOR LENGTH" -> "VALID LENGTH?" [label="receive byte"];
"VALID LENGTH?" [shape=diamond];
"VALID LENGTH?" -> "READY" [label="no"];
"VALID LENGTH?" -> "WAIT FOR DATA" [label="yes"];
"WAIT FOR DATA" -> "READY" [label="timeout"];
"WAIT FOR DATA" -> "ENOUGH DATA?" [label="receive byte"];
"ENOUGH DATA?" [shape=diamond];
"ENOUGH DATA?" -> "WAIT FOR DATA" [label="no"];
"ENOUGH DATA?" -> "WAIT FOR CRC" [label="yes"];
"WAIT FOR CRC" -> "READY" [label="timeout"];
"WAIT FOR CRC" -> "CALCULATE CRC" [label="receive byte"];
"CALCULATE CRC" -> "VALID CRC?";
"VALID CRC?" [shape=diamond];
"VALID CRC?" -> "READY" [label="no"];
"VALID CRC?" -> "EXTRACT PAYLOAD" [label="yes"];
"EXTRACT PAYLOAD" -> "READY";
}
Binary file added doc/PacketStreamDecoder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 48 additions & 1 deletion s3g.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
}

header = 0xD5
maximum_payload_length = 32

class PacketError(Exception):
def __init__(self, value):
Expand Down Expand Up @@ -90,6 +91,9 @@ def EncodePacket(payload):
@param payload Command payload, 1 - n bytes describing the command to send
@return bytearray containing the packet
"""
if len(payload) > maximum_payload_length:
raise PacketLengthError(len(payload), maximum_payload_length)

packet = bytearray()
packet.append(header)
packet.append(len(payload))
Expand Down Expand Up @@ -123,8 +127,51 @@ def DecodePacket(packet):
return packet[2:(len(packet)-1)]


class PacketStreamDecoder:
"""
A state machine that accepts bytes from an s3g packet stream, checks the validity of
each packet, then extracts and returns the payload.
"""
def __init__(self):
self.state = "READY"
self.payload = bytearray()
self.expected_length = 0

def ReceiveByte(self, byte):
"""
Entry point, call for each byte added to the stream.
@param byte Byte to add to the stream
@return s3g payload if a full packet was received, None otherwise.
"""
if self.state == "READY":
if byte != header:
raise PacketHeaderError(byte, header)

self.state = "WAIT_FOR_LENGTH"

elif self.state == "WAIT_FOR_LENGTH":
if byte > maximum_payload_length:
raise PacketLengthFieldError(byte, maximum_payload_length)

self.expected_length = byte
self.state = "WAIT_FOR_DATA"

elif self.state == "WAIT_FOR_DATA":
self.payload.append(byte)
if len(self.payload) == self.expected_length:
self.state = "WAIT_FOR_CRC"

elif self.state == "WAIT_FOR_CRC":
if CalculateCRC(self.payload) != byte:
raise PacketCRCError(byte, CalculateCRC(self.payload))

self.state = "READY"
return self.payload


class Replicator:
stream = None
def __init__(self):
self.stream = None

def Move(self, position, rate):
"""
Expand Down
72 changes: 70 additions & 2 deletions s3g_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def test_cases(self):
for case in cases:
assert s3g.CalculateCRC(case[0]) == case[1]


class EncodeTests(unittest.TestCase):
def test_encode_int32(self):
cases = [
Expand All @@ -31,7 +32,14 @@ def test_encode_uint32(self):
for case in cases:
assert s3g.EncodeUint32(case[0]) == case[1]


class PacketEncodeTests(unittest.TestCase):
def test_reject_oversize_payload(self):
payload = bytearray()
for i in range (0, s3g.maximum_payload_length + 1):
payload.append(i)
self.assertRaises(s3g.PacketLengthError,s3g.EncodePacket,payload)

def test_packet_length(self):
payload = 'abcd'
packet = s3g.EncodePacket(payload)
Expand All @@ -53,6 +61,7 @@ def test_packet_crc(self):
packet = s3g.EncodePacket(payload)
assert packet[6] == s3g.CalculateCRC(payload);


class PacketDecodeTests(unittest.TestCase):
def test_undersize_packet(self):
packet = bytearray('abc')
Expand All @@ -74,7 +83,7 @@ def test_bad_crc(self):
packet.append(s3g.header)
packet.append(1)
packet.extend('a')
packet.append(0xFF)
packet.append(s3g.CalculateCRC('a')+1)
self.assertRaises(s3g.PacketCRCError,s3g.DecodePacket,packet)

def test_got_payload(self):
Expand All @@ -89,6 +98,64 @@ def test_got_payload(self):
payload = s3g.DecodePacket(packet)
assert payload == expected_payload


class PacketStreamDecoderTests(unittest.TestCase):
def setUp(self):
self.s = s3g.PacketStreamDecoder()

def tearDown(self):
self.s = None

def test_starts_in_ready_mode(self):
assert(self.s.state == "READY")
assert(len(self.s.payload) == 0)
assert(self.s.expected_length == 0)

def test_reject_bad_header(self):
self.assertRaises(s3g.PacketHeaderError,self.s.ReceiveByte,0x00)
assert(self.s.state == "READY")

def test_accept_header(self):
self.s.ReceiveByte(s3g.header)
assert(self.s.state == "WAIT_FOR_LENGTH")

def test_reject_bad_size(self):
self.s.ReceiveByte(s3g.header)
self.assertRaises(s3g.PacketLengthFieldError,self.s.ReceiveByte,s3g.maximum_payload_length+1)

def test_accept_size(self):
self.s.ReceiveByte(s3g.header)
self.s.ReceiveByte(s3g.maximum_payload_length)
assert(self.s.state == "WAIT_FOR_DATA")
assert(self.s.expected_length == s3g.maximum_payload_length)

def test_accepts_data(self):
self.s.ReceiveByte(s3g.header)
self.s.ReceiveByte(s3g.maximum_payload_length)
for i in range (0, s3g.maximum_payload_length):
self.s.ReceiveByte(i)

assert(self.s.expected_length == s3g.maximum_payload_length)
for i in range (0, s3g.maximum_payload_length):
assert(self.s.payload[i] == i)

def test_reject_bad_crc(self):
payload = 'abcde'
self.s.ReceiveByte(s3g.header)
self.s.ReceiveByte(len(payload))
for i in range (0, len(payload)):
self.s.ReceiveByte(payload[i])
self.assertRaises(s3g.PacketCRCError,self.s.ReceiveByte,s3g.CalculateCRC(payload)+1)

def test_returns_payload(self):
payload = 'abcde'
self.s.ReceiveByte(s3g.header)
self.s.ReceiveByte(len(payload))
for i in range (0, len(payload)):
self.s.ReceiveByte(payload[i])
assert(self.s.ReceiveByte(s3g.CalculateCRC(payload)) == payload)


class ReplicatorTests(unittest.TestCase):
def setUp(self):
self.r = s3g.Replicator()
Expand All @@ -99,7 +166,7 @@ def tearDown(self):
self.r = None
self.stream = None

def test_move(self):
def test_queue_extended_point(self):
expected_target = [1,2,3,4,5]
expected_velocity = 6
self.r.Move(expected_target, expected_velocity)
Expand All @@ -111,6 +178,7 @@ def test_move(self):
for i in range(0, 5):
assert s3g.EncodeInt32(expected_target[i]) == payload[(i*4+1):(i*4+5)]


if __name__ == "__main__":
unittest.main()

0 comments on commit 2ea4c5c

Please sign in to comment.