Coverage for d7a/serial_modem_interface/parser.py: 82%
119 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-24 08:03 +0200
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-24 08:03 +0200
1#
2# Copyright (c) 2015-2021 University of Antwerp, Aloxy NV.
3#
4# This file is part of pyd7a.
5# See https://github.com/Sub-IoT/pyd7a for further info.
6#
7# Licensed under the Apache License, Version 2.0 (the "License");
8# you may not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS,
15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18#
20# a parser for ALP commands wrapped in serial interface frames
21import binascii
23from bitstring import ConstBitStream, ReadError
24from d7a.alp.parser import Parser as AlpParser
25from d7a.parse_error import ParseError
26import enum
27from d7a.support.Crc import calculate_crc
28from pprint import pprint
32class MessageType(enum.IntEnum):
33 ALP_DATA = 1
34 PING_REQUEST = 2
35 PING_RESPONSE = 3
36 LOGGING = 4
37 REBOOTED = 5
39class Parser(object):
41 def __init__(self, skip_alp_parsing=False, custom_files_class=None):
42 self.buffer = bytearray()
43 self.skip_alp_parsing = skip_alp_parsing
44 self.up_counter = 0
45 self.down_counter = 0
46 self.error_counter = 0
47 self.alp_parser = AlpParser(custom_files_class)
49 def shift_buffer(self, start):
50 self.buffer = self.buffer[start:]
51 return self
53 def parse(self, msg):
54 self.buffer.extend(msg)
55 return self.parse_buffer()
58#|sync|sync|counter|message type|length|crc1|crc2|
59 def build_serial_frame(self,command):
60 buffer = bytearray([ 0xC0, 0])
61 alp_command_bytes = bytearray(command)
62 buffer.append(self.up_counter)
63 buffer.append(MessageType.ALP_DATA.value)
64 buffer.append(len(alp_command_bytes))
65 crc = calculate_crc(bytes(alp_command_bytes))
66 buffer = buffer + bytes(bytearray(crc)) + alp_command_bytes
67 self.up_counter = self.up_counter + 1
68 if self.up_counter > 255:
69 self.up_counter = 0
70 return buffer
73 def parse_buffer(self):
74 parsed = 0
75 cmds = []
76 errors = []
77 message_types = []
79 while True:
80 (message_type, cmd, info) = self.parse_one_command_from_buffer()
81 errors.extend(info["errors"])
82 if cmd is None: break
83 parsed += info["parsed"]
84 message_types.append(message_type)
85 cmds.append(cmd)
87 info["parsed"] = parsed
88 info["errors"] = errors
89 return (message_types, cmds, info)
91 def parse_one_command_from_buffer(self):
92 retry = True # until we have one or don't have enough
93 errors = []
94 cmd = None
95 message_type = None
96 bits_parsed = 0
97 while retry and len(self.buffer) > 0:
98 try:
99 s = ConstBitStream(bytes=self.buffer)
100 cmd_length, message_type = self.parse_serial_interface_header(s)
101 if message_type == MessageType.REBOOTED.value:
102 cmd = s.read("uint:8")
103 elif message_type == MessageType.LOGGING.value:
104 cmd = (s.readlist('bytes:b', b=cmd_length)[0])
105 else:
106 if self.skip_alp_parsing:
107 if s.length < cmd_length:
108 raise ReadError
110 cmd = s.read("bytes:" + str(cmd_length))
111 else:
112 cmd = self.alp_parser.parse(s, cmd_length)
114 bits_parsed = s.pos
115 self.shift_buffer(int(bits_parsed/8))
116 retry = False # got one, carry on
117 except ReadError: # not enough to read, carry on and wait for more
118 retry = False
119 except Exception as e: # actual problem with current buffer, need to skip
120 errors.append({
121 "error" : e.args[0],
122 "buffer" : " ".join([format(b, "02x") for b in self.buffer]),
123 "pos" : s.pos,
124 "skipped" : self.skip_bad_buffer_content()
125 })
127 info = {
128 "parsed" : bits_parsed,
129 "buffer" : len(self.buffer) * 8,
130 "errors" : errors
131 }
132 return (message_type, cmd, info)
134 def skip_bad_buffer_content(self):
135 # skip until we find 0xc0, which might be a valid starting point
136 try:
137 self.buffer.pop(0) # first might be 0xc0
138 pos = self.buffer.index(b'\xc0')
139 self.buffer = self.buffer[pos:]
140 return pos + 1
141 except IndexError: # empty buffer
142 return 0
143 except ValueError: # empty buffer, reported by .index
144 skipped = len(self.buffer) + 1 # popped first item already
145 self.buffer = bytearray()
146 return skipped
148 # |sync|sync|counter|message type|length|crc1|crc2|
149 def parse_serial_interface_header(self, s):
150 b = s.read("uint:8")
151 if b != 0xC0:
152 raise ParseError("expected 0xC0, found {0}".format(b))
153 version = s.read("uint:8")
154 if version != 0:
155 raise ParseError("Expected version 0, found {0}".format(version))
156 counter = s.read("uint:8")
157 message_type = s.read("uint:8") #TODO different handler?
158 cmd_len = s.read("uint:8")
159 crc1 = s.read("uint:8")
160 crc2 = s.read("uint:8")
161 if len(self.buffer) - s.bytepos < cmd_len:
162 raise ReadError("ALP command not complete yet, expected {0} bytes, got {1}".format(cmd_len, s.len - s.bytepos))
163 payload = s.peeklist('bytes:b', b=cmd_len)[0]
164 crc = calculate_crc(bytes(payload))
165 self.down_counter = self.down_counter + 1
166 if self.down_counter > 255:
167 self.down_counter = 0
168 if counter != self.down_counter:
169 self.error_counter += 1
170 pprint("counters not equal") #TODO consequence?
171 self.down_counter = counter #reset counter
172 if crc[0] != crc1 or crc[1] != crc2:
173 raise ParseError("CRC is incorrect found {} {} and expected {} {}".format(crc1, crc2, crc[0], crc[1]))
175 return cmd_len, message_type