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

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# 

19 

20# a parser for ALP commands wrapped in serial interface frames 

21import binascii 

22 

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 

29 

30 

31 

32class MessageType(enum.IntEnum): 

33 ALP_DATA = 1 

34 PING_REQUEST = 2 

35 PING_RESPONSE = 3 

36 LOGGING = 4 

37 REBOOTED = 5 

38 

39class Parser(object): 

40 

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) 

48 

49 def shift_buffer(self, start): 

50 self.buffer = self.buffer[start:] 

51 return self 

52 

53 def parse(self, msg): 

54 self.buffer.extend(msg) 

55 return self.parse_buffer() 

56 

57 

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 

71 

72 

73 def parse_buffer(self): 

74 parsed = 0 

75 cmds = [] 

76 errors = [] 

77 message_types = [] 

78 

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) 

86 

87 info["parsed"] = parsed 

88 info["errors"] = errors 

89 return (message_types, cmds, info) 

90 

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 

109 

110 cmd = s.read("bytes:" + str(cmd_length)) 

111 else: 

112 cmd = self.alp_parser.parse(s, cmd_length) 

113 

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 }) 

126 

127 info = { 

128 "parsed" : bits_parsed, 

129 "buffer" : len(self.buffer) * 8, 

130 "errors" : errors 

131 } 

132 return (message_type, cmd, info) 

133 

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 

147 

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])) 

174 

175 return cmd_len, message_type 

176