From: Bart de Waal Date: Mon, 13 Jul 2015 21:59:55 +0000 (+0200) Subject: Add protocol decoder for Modbus RTU. X-Git-Tag: libsigrokdecode-0.4.0~105 X-Git-Url: https://sigrok.org/gitweb/?p=libsigrokdecode.git;a=commitdiff_plain;h=db858a048288645d00eeb643dc43006ef9875280 Add protocol decoder for Modbus RTU. --- diff --git a/decoders/modbus/__init__.py b/decoders/modbus/__init__.py new file mode 100644 index 0000000..e9f0deb --- /dev/null +++ b/decoders/modbus/__init__.py @@ -0,0 +1,29 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2014 Bart de Waal +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +## + +''' +This decoder stacks on top of the 'uart' PD and decodes Modbus RTU, +a protocol with a single a client and one or more servers. + +The RX channel will be checked for both client->server and server->client +communication, the TX channel only for client->server. +''' + +from .pd import Decoder diff --git a/decoders/modbus/pd.py b/decoders/modbus/pd.py new file mode 100644 index 0000000..d427844 --- /dev/null +++ b/decoders/modbus/pd.py @@ -0,0 +1,931 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2015 Bart de Waal +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see . +## + +import sigrokdecode as srd +from math import ceil + +RX = 0 +TX = 1 + +class No_more_data(Exception): + '''This exception is a signal that we should stop parsing an ADU as there + is no more data to parse.''' + pass + +class Data: + '''The Data class is used to hold the bytes from the serial decode.''' + def __init__(self, start, end, data): + self.start = start + self.end = end + self.data = data + +class Modbus_ADU: + '''An Application Data Unit is what Modbus calls one message. + Protocol decoders are supposed to keep track of state and then provide + decoded data to the backend as it reads it. In Modbus' case, the state is + the ADU up to that point. This class represents the state and writes the + messages to the backend. + This class is for the common infrastructure between CS and SC. It should + not be used directly, only inhereted from.''' + + def __init__(self, parent, start, write_channel, annotation_prefix): + self.data = [] # List of all the data received up to now + self.parent = parent # Reference to the decoder object + self.start = start + self.last_read = start # The last moment parsed by this ADU object + self.write_channel = write_channel + self.last_byte_put = -1 + self.annotation_prefix = annotation_prefix + # Any Modbus message needs to be at least 4 bytes long. The Modbus + # function may make this longer. + self.minimum_length = 4 + + # This variable is used by an external function to determine when the + # next frame should be started. + self.startNewFrame = False + + # If there is an error in a frame, we'd like to highlight it. Keep + # track of errors. + self.hasError = False + + def add_data(self, start, end, data): + '''Let the frame handle another piece of data. + start: start of this data + end: end of this data + data: data as received from the UART decoder''' + ptype, rxtx, pdata = data + self.last_read = end + if ptype == 'DATA': + self.data.append(Data(start, end, pdata[0])) + self.parse() # parse() is defined in the specific type of ADU. + + def puti(self, byte_to_put, annotation, message): + '''This class keeps track of how much of the data has already been + annotated. This function tells the parent class to write message, but + only if it hasn't written about this bit before. + byte_to_put: Only write if it hasn't yet written byte_to_put. It will + write from the start of self.last_byte_put+1 to the end + of byte_to_put. + annotation: Annotation to write to, without annotation_prefix. + message: Message to write.''' + if byte_to_put > len(self.data) - 1: + # If the byte_to_put hasn't been read yet. + raise No_more_data + + if annotation == 'error': + self.hasError = True + + if byte_to_put > self.last_byte_put: + self.parent.puta( + self.data[self.last_byte_put + 1].start, + self.data[byte_to_put].end, + self.annotation_prefix + annotation, + message) + self.last_byte_put = byte_to_put + raise No_more_data + + def putl(self, annotation, message, maximum=None): + '''Puts the last byte on the stack with message. The contents of the + last byte will be applied to message using format.''' + last_byte_address = len(self.data) - 1 + if maximum is not None and last_byte_address > maximum: + return + self.puti(last_byte_address, annotation, + message.format(self.data[-1].data)) + + def close(self, message_overflow): + '''Function to be called when next message is started. As there is + always space between one message and the next, we can use that space + for errors at the end.''' + # TODO: Figure out how to make this happen for last message. + data = self.data + if len(data) < self.minimum_length: + if len(data) == 0: + # Sometimes happens with noise, safe to ignore. + return + self.parent.puta( + data[self.last_byte_put].end, message_overflow, + self.annotation_prefix + 'error', + 'Message too short or not finished') + self.hasError = True + if self.hasError and self.parent.options['channel'] == 'RX': + # If we are on RX mode (so client->server and server->client + # messages can be seperated) we like to mark blocks containing + # errors. We don't do this in TX mode, because then we interpret + # each frame as both a client->server and server->client frame, and + # one of those is bound to contain an error, making highlighting + # frames useless. + self.parent.puta(data[0].start, data[-1].end, + 'error-indication', 'Frame contains error') + if len(data) > 256: + try: + self.puti(len(data) - 1, self.annotation_prefix + 'error', + 'Modbus data frames are limited to 256 bytes') + except No_more_data: + pass + + def check_crc(self, byte_to_put): + '''Check the CRC code, data[byte_to_put] is the 2nd byte of the CRC.''' + crc_byte1, crc_byte2 = self.calc_crc(byte_to_put) + data = self.data + if data[-2].data == crc_byte1 and data[-1].data == crc_byte2: + self.puti(byte_to_put, 'crc', 'CRC correct') + else: + self.puti(byte_to_put, 'error', + 'CRC should be {} {}'.format(crc_byte1, crc_byte2)) + + def half_word(self, start): + '''Return the half word (16 bit) value starting at start bytes in. If + it goes out of range it raises the usual errors.''' + if (start + 1) > (len(self.data) - 1): + # If there isn't enough length to access data[start + 1]. + raise No_more_data + return self.data[start].data * 0x100 + self.data[start + 1].data + + def calc_crc(self, last_byte): + '''Calculate the CRC, as described in the spec. + The last byte of the CRC should be data[last_byte].''' + if last_byte < 3: + # Every Modbus ADU should be as least 4 long, so we should never + # have to calculate a CRC on something shorter. + raise Exception('Could not calculate CRC: message too short') + + result = 0xFFFF + magic_number = 0xA001 # As defined in the modbus specification. + for byte in self.data[:last_byte - 1]: + result = result ^ byte.data + for i in range(8): + LSB = result & 1 + result = result >> 1 + if (LSB): # If the LSB is true. + result = result ^ magic_number + byte1 = result & 0xFF + byte2 = (result & 0xFF00) >> 8 + return (byte1, byte2) + + def parse_write_single_coil(self): + '''Parse function 5, write single coil.''' + self.minimum_length = 8 + + self.puti(1, 'function', 'Function 5: Write Single Coil') + + address = self.half_word(2) + self.puti(3, 'address', + 'Address 0x{:X} / {:d}'.format(address, address + 10000)) + + raw_value = self.half_word(4) + value = 'Invalid Coil Value' + if raw_value == 0x0000: + value = 'Coil Value OFF' + elif raw_value == 0xFF00: + value = 'Coil Value ON' + self.puti(5, 'data', value) + + self.check_crc(7) + + def parse_write_single_register(self): + '''Parse function 6, write single register.''' + self.minimum_length = 8 + + self.puti(1, 'function', 'Function 6: Write Single Register') + + address = self.half_word(2) + self.puti(3, 'address', + 'Address 0x{:X} / {:d}'.format(address, address + 30000)) + + value = self.half_word(4) + value_formatted = 'Register Value 0x{0:X} / {0:d}'.format(value) + self.puti(5, 'data', value_formatted) + + self.check_crc(7) + + def parse_diagnostics(self): + '''Parse function 8, diagnostics. This function has many subfunctions, + but they are all more or less the same.''' + self.minimum_length = 8 + + self.puti(1, 'function', 'Function 8: Diagnostics') + + diag_subfunction = { + 0: 'Return Query data', + 1: 'Restart Communications Option', + 2: 'Return Diagnostics Register', + 3: 'Change ASCII Input Delimiter', + 4: 'Force Listen Only Mode', + 10: 'Clear Counters and Diagnostic Register', + 11: 'Return Bus Message Count', + 12: 'Return Bus Communication Error Count', + 13: 'Return Bus Exception Error Count', + 14: 'Return Slave Message Count', + 15: 'Return Slave No Response Count', + 16: 'Return Slave NAK Count', + 17: 'Return Slave Busy Count', + 18: 'Return Bus Character Overrun Count', + 20: 'Return Overrun Counter and Flag', + } + subfunction = self.half_word(2) + subfunction_name = diag_subfunction.get(subfunction, + 'Reserved subfunction') + self.puti(3, 'data', + 'Subfunction {}: {}'.format(subfunction, subfunction_name)) + + diagnostic_data = self.half_word(4) + self.puti(5, 'data', + 'Data Field: {0} / 0x{0:04X}'.format(diagnostic_data)) + + self.check_crc(7) + + def parse_mask_write_register(self): + '''Parse function 22, Mask Write Register.''' + self.minimum_length = 10 + data = self.data + + self.puti(1, 'function', 'Function 22: Mask Write Register') + + address = self.half_word(2) + self.puti(3, 'address', + 'Address 0x{:X} / {:d}'.format(address, address + 30001)) + + self.half_word(4) # To make sure we don't oveflow data. + and_mask_1 = data[4].data + and_mask_2 = data[5].data + self.puti(5, 'data', + 'AND mask: {:08b} {:08b}'.format(and_mask_1, and_mask_2)) + + self.half_word(6) # To make sure we don't oveflow data. + or_mask_1 = data[6].data + or_mask_2 = data[7].data + self.puti(7, 'data', + 'OR mask: {:08b} {:08b}'.format(or_mask_1, or_mask_2)) + + self.check_crc(9) + + def parse_not_implemented(self): + '''Explicitly mark certain functions as legal functions, but not + implemented in this parser. This is due to the author not being able to + find anything (hardware or software) that supports these functions.''' + # TODO: Implement these functions. + + # Mentioning what function it is is no problem. + function = self.data[1].data + functionname = { + 20: 'Read File Record', + 21: 'Write File Record', + 24: 'Read FIFO Queue', + 43: 'Read Device Identification/Encapsulated Interface Transport', + }[function] + self.puti(1, 'function', + 'Function {}: {} (not supported)'.format(function, functionname)) + + # From there on out we can keep marking it unsupported. + self.putl('data', 'This function is not currently supported') + +class Modbus_ADU_SC(Modbus_ADU): + '''SC stands for Server -> Client.''' + def parse(self): + '''Select which specific Modbus function we should parse.''' + data = self.data + + # This try-catch is being used as flow control. + try: + server_id = data[0].data + if 1 <= server_id <= 247: + message = 'Slave ID: {}'.format(server_id) + else: + message = 'Slave ID {} is invalid' + self.puti(0, 'server-id', message) + + function = data[1].data + if function == 1 or function == 2: + self.parse_read_bits() + elif function == 3 or function == 4 or function == 23: + self.parse_read_registers() + elif function == 5: + self.parse_write_single_coil() + elif function == 6: + self.parse_write_single_register() + elif function == 7: + self.parse_read_exception_status() + elif function == 8: + self.parse_diagnostics() + elif function == 11: + self.parse_get_comm_event_counter() + elif function == 12: + self.parse_get_comm_event_log() + elif function == 15 or function == 16: + self.parse_write_multiple() + elif function == 17: + self.parse_report_server_id() + elif function == 22: + self.parse_mask_write_register() + elif function in {21, 21, 24, 43}: + self.parse_not_implemented() + elif function > 0x80: + self.parse_error() + else: + self.puti(1, 'error', + 'Unknown function: {}'.format(data[1].data)) + self.putl('error', 'Unknown function') + + # If the message gets here without raising an exception, the + # message goes on longer than it should. + self.putl('error', 'Message too long') + + except No_more_data: + # Just a message saying we don't need to parse anymore this round. + pass + + def parse_read_bits(self): + self.mimumum_length = 5 + + data = self.data + function = data[1].data + + if function == 1: + self.puti(1, 'function', 'Function 1: Read Coils') + else: + self.puti(1, 'function', 'Function 2: Read Discrete Inputs') + + bytecount = self.data[2].data + self.minimum_length = 5 + bytecount # 3 before data, 2 CRC. + self.puti(2, 'length', 'Byte count: {}'.format(bytecount)) + + # From here on out, we expect registers on 3 and 4, 5 and 6 etc. + # So registers never start when the length is even. + self.putl('data', '{:08b}', bytecount + 2) + self.check_crc(bytecount + 4) + + def parse_read_registers(self): + self.mimumum_length = 5 + + data = self.data + + function = data[1].data + if function == 3: + self.puti(1, 'function', 'Function 3: Read Holding Registers') + elif function == 4: + self.puti(1, 'function', 'Function 4: Read Input Registers') + elif function == 23: + self.puti(1, 'function', 'Function 23: Read/Write Multiple Registers') + + bytecount = self.data[2].data + self.minimum_length = 5 + bytecount # 3 before data, 2 CRC. + if bytecount % 2 == 0: + self.puti(2, 'length', 'Byte count: {}'.format(bytecount)) + else: + self.puti(2, 'error', + 'Error: Odd byte count ({})'.format(bytecount)) + + # From here on out, we expect registers on 3 and 4, 5 and 6 etc. + # So registers never start when the length is even. + if len(data) % 2 == 1: + register_value = self.half_word(-2) + self.putl('data', '0x{0:04X} / {0}'.format(register_value), + bytecount + 2) + else: + raise No_more_data + + self.check_crc(bytecount + 4) + + def parse_read_exception_status(self): + self.mimumum_length = 5 + + self.puti(1, 'function', 'Function 7: Read Exception Status') + exception_status = self.data[2].data + self.puti(2, 'data', + 'Exception status: {:08b}'.format(exception_status)) + self.check_crc(4) + + def parse_get_comm_event_counter(self): + self.mimumum_length = 8 + + self.puti(1, 'function', 'Function 11: Get Comm Event Counter') + + status = self.half_word(2) + if status == 0x0000: + self.puti(3, 'data', 'Status: not busy') + elif status == 0xFFFF: + self.puti(3, 'data', 'Status: busy') + else: + self.puti(3, 'error', 'Bad status: 0x{:04X}'.format(status)) + + count = self.half_word(4) + self.puti(5, 'data', 'Event Count: {}'.format(count)) + self.check_crc(7) + + def parse_get_comm_event_log(self): + self.mimumum_length = 11 + self.puti(1, 'function', 'Function 12: Get Comm Event Log') + + data = self.data + + bytecount = data[2].data + self.puti(2, 'length', 'Bytecount: {}'.format(bytecount)) + # The bytecount is the length of everything except the slaveID, + # function code, bytecount and CRC. + self.mimumum_length = 5 + bytecount + + status = self.half_word(3) + if status == 0x0000: + self.puti(4, 'data', 'Status: not busy') + elif status == 0xFFFF: + self.puti(4, 'data', 'Status: busy') + else: + self.puti(4, 'error', 'Bad status: 0x{:04X}'.format(status)) + + event_count = self.half_word(5) + self.puti(6, 'data', 'Event Count: {}'.format(event_count)) + + message_count = self.half_word(7) + self.puti(8, 'data', 'Message Count: {}'.format(message_count)) + + self.putl('data', 'Event: 0x{:02X}'.format(data[-1].data), + bytecount + 2) + + self.check_crc(bytecount + 4) + + def parse_write_multiple(self): + '''Function 15 and 16 are almost the same, so we can parse them both + using one function.''' + self.mimumum_length = 8 + + function = self.data[1].data + if function == 15: + data_unit = 'Coils' + max_outputs = 0x07B0 + long_address_offset = 10001 + elif function == 16: + data_unit = 'Registers' + max_outputs = 0x007B + long_address_offset = 30001 + + self.puti(1, 'function', + 'Function {}: Write Multiple {}'.format(function, data_unit)) + + starting_address = self.half_word(2) + # Some instruction manuals use a long form name for addresses, this is + # listed here for convienience. + address_name = long_address_offset + starting_address + self.puti(3, 'address', + 'Start at address 0x{:X} / {:d}'.format(starting_address, + address_name)) + + quantity_of_outputs = self.half_word(4) + if quantity_of_outputs <= max_outputs: + self.puti(5, 'data', + 'Write {} {}'.format(quantity_of_outputs, data_unit)) + else: + self.puti(5, 'error', + 'Bad value: {} {}. Max is {}'.format(quantity_of_outputs, + data_unit, max_outputs)) + + self.check_crc(7) + + def parse_report_server_id(self): + # Buildup of this function: + # 1 byte serverID + # 1 byte function (17) + # 1 byte bytecount + # 1 byte serverID (counts for bytecount) + # 1 byte Run Indicator Status (counts for bytecount) + # bytecount - 2 bytes of device specific data (counts for bytecount) + # 2 bytes of CRC + self.mimumum_length = 7 + data = self.data + self.puti(1, 'function', 'Function 17: Report Server ID') + + bytecount = data[2].data + self.puti(2, 'length', 'Data is {} bytes long'.format(bytecount)) + + self.puti(3, 'data', 'serverID: {}'.format(data[3].data)) + + run_indicator_status = data[4].data + if run_indicator_status == 0x00: + self.puti(4, 'data', 'Run Indicator status: Off') + elif run_indicator_status == 0xFF: + self.puti(4, 'data', 'Run Indicator status: On') + else: + self.puti(4, 'error', + 'Bad Run Indicator status: 0x{:X}'.format(run_indicator_status)) + + self.putl('data', 'Device specific data: {}, "{}"'.format(data[-1].data, + chr(data[-1].data)), 2 + bytecount) + + self.check_crc(4 + bytecount) + + def parse_error(self): + '''Parse a Modbus error message.''' + self.mimumum_length = 5 + # The function code of an error is always 0x80 above the function call + # that caused it. + functioncode = self.data[1].data - 0x80 + + functions = { + 1: 'Read Coils', + 2: 'Read Discrete Inputs', + 3: 'Read Holding Registers', + 4: 'Read Input Registers', + 5: 'Write Single Coil', + 6: 'Write Single Register', + 7: 'Read Exception Status', + 8: 'Diagnostic', + 11: 'Get Com Event Counter', + 12: 'Get Com Event Log', + 15: 'Write Multiple Coils', + 16: 'Write Multiple Registers', + 17: 'Report Slave ID', + 20: 'Read File Record', + 21: 'Write File Record', + 22: 'Mask Write Register', + 23: 'Read/Write Multiple Registers', + 24: 'Read FIFO Queue', + 43: 'Read Device Identification/Encapsulated Interface Transport', + } + functionname = '{}: {}'.format(functioncode, + functions.get(functioncode, 'Unknown function')) + self.puti(1, 'function', + 'Error for function {}'.format(functionname)) + + error = self.data[2].data + errorcodes = { + 1: 'Illegal Function', + 2: 'Illegal Data Address', + 3: 'Illegal Data Value', + 4: 'Slave Device Failure', + 5: 'Acknowledge', + 6: 'Slave Device Busy', + 8: 'Memory Parity Error', + 10: 'Gateway Path Unavailable', + 11: 'Gateway Target Device failed to respond', + } + errorname = '{}: {}'.format(error, errorcodes.get(error, 'Unknown')) + self.puti(2, 'data', 'Error {}'.format(errorname)) + self.check_crc(4) + +class Modbus_ADU_CS(Modbus_ADU): + '''CS stands for Client -> Server.''' + def parse(self): + '''Select which specific Modbus function we should parse.''' + data = self.data + + # This try-catch is being used as flow control. + try: + server_id = data[0].data + message = '' + if server_id == 0: + message = 'Broadcast message' + elif 1 <= server_id <= 247: + message = 'Slave ID: {}'.format(server_id) + elif 248 <= server_id <= 255: + message = 'Slave ID: {} (reserved address)'.format(server_id) + self.puti(0, 'server-id', message) + + function = data[1].data + if function >= 1 and function <= 4: + self.parse_read_data_command() + if function == 5: + self.parse_write_single_coil() + if function == 6: + self.parse_write_single_register() + if function in {7, 11, 12, 17}: + self.parse_single_byte_request() + elif function == 8: + self.parse_diagnostics() + if function in {15, 16}: + self.parse_write_multiple() + elif function == 22: + self.parse_mask_write_register() + elif function == 23: + self.parse_read_write_registers() + elif function in {21, 21, 24, 43}: + self.parse_not_implemented() + else: + self.puti(1, 'error', + 'Unknown function: {}'.format(data[1].data)) + self.putl('error', 'Unknown function') + + # If the message gets here without raising an exception, the + # message goes on longer than it should. + self.putl('error', 'Message too long') + + except No_more_data: + # Just a message saying we don't need to parse anymore this round. + pass + + def parse_read_data_command(self): + '''Interpret a command to read x units of data starting at address, ie + functions 1, 2, 3 and 4, and write the result to the annotations.''' + data = self.data + self.minimum_length = 8 + + function = data[1].data + functionname = {1: 'Read Coils', + 2: 'Read Discrete Inputs', + 3: 'Read Holding Registers', + 4: 'Read Input Registers', + }[function] + + self.puti(1, 'function', + 'Function {}: {}'.format(function, functionname)) + + starting_address = self.half_word(2) + # Some instruction manuals use a long form name for addresses, this is + # listed here for convienience. + # Example: holding register 60 becomes 30061. + address_name = 10000 * function + 1 + starting_address + self.puti(3, 'address', + 'Start at address 0x{:X} / {:d}'.format(starting_address, + address_name)) + + self.puti(5, 'length', + 'Read {:d} units of data'.format(self.half_word(4))) + self.check_crc(7) + + def parse_single_byte_request(self): + '''Some Modbus functions have no arguments, this parses those.''' + function = self.data[1].data + function_name = {7: 'Read Exception Status', + 11: 'Get Comm Event Counter', + 12: 'Get Comm Event Log', + 17: 'Report Slave ID', + }[function] + self.puti(1, 'function', + 'Function {}: {}'.format(function, function_name)) + + self.check_crc(3) + + def parse_write_multiple(self): + '''Function 15 and 16 are almost the same, so we can parse them both + using one function.''' + self.mimumum_length = 9 + + function = self.data[1].data + if function == 15: + data_unit = 'Coils' + max_outputs = 0x07B0 + ratio_bytes_data = 1/8 + long_address_offset = 10001 + elif function == 16: + data_unit = 'Registers' + max_outputs = 0x007B + ratio_bytes_data = 2 + long_address_offset = 30001 + + self.puti(1, 'function', + 'Function {}: Write Multiple {}'.format(function, data_unit)) + + starting_address = self.half_word(2) + # Some instruction manuals use a long form name for addresses, this is + # listed here for convienience. + address_name = long_address_offset + starting_address + self.puti(3, 'address', + 'Start at address 0x{:X} / {:d}'.format(starting_address, + address_name)) + + quantity_of_outputs = self.half_word(4) + if quantity_of_outputs <= max_outputs: + self.puti(5, 'length', + 'Write {} {}'.format(quantity_of_outputs, data_unit)) + else: + self.puti(5, 'error', + 'Bad value: {} {}. Max is {}'.format(quantity_of_outputs, + data_unit, max_outputs)) + proper_bytecount = ceil(quantity_of_outputs * ratio_bytes_data) + + bytecount = self.data[6].data + if bytecount == proper_bytecount: + self.puti(6, 'length', 'Byte count: {}'.format(bytecount)) + else: + self.puti(6, 'error', + 'Bad byte count, is {}, should be {}'.format(bytecount, + proper_bytecount)) + self.mimumum_length = bytecount + 9 + + self.putl('data', 'Value 0x{:X}', 6 + bytecount) + + self.check_crc(bytecount + 8) + + def parse_read_file_record(self): + self.puti(1, 'function', 'Function 20: Read file records') + + data = self.data + + bytecount = data[2].data + + self.minimum_length = 5 + bytecount + # 1 for serverID, 1 for function, 1 for bytecount, 2 for CRC. + + if 0x07 <= bytecount <= 0xF5: + self.puti(2, 'length', 'Request is {} bytes long'.format(bytecount)) + else: + self.puti(2, 'error', + 'Request claims to be {} bytes long, legal values are between' + ' 7 and 247'.format(bytecount)) + + current_byte = len(data) - 1 + # Function 20 is a number of sub-requests, the first starting at 3, + # the total length of the sub-requests is bytecount. + if current_byte <= bytecount + 2: + step = (current_byte - 3) % 7 + if step == 0: + if data[current_byte].data == 6: + self.puti(current_byte, 'data', 'Start sub-request') + else: + self.puti(current_byte, 'error', + 'First byte of subrequest should be 0x06') + elif step == 1: + raise No_more_data + elif step == 2: + file_number = self.half_word(current_byte - 1) + self.puti(current_byte, 'data', + 'Read File number {}'.format(file_number)) + elif step == 3: + raise No_more_data + elif step == 4: + record_number = self.half_word(current_byte - 1) + self.puti(current_byte, 'address', + 'Read from record number {}'.format(record_number)) + # TODO: Check if within range. + elif step == 5: + raise No_more_data + elif step == 6: + records_to_read = self.half_word(current_byte - 1) + self.puti(current_byte, 'length', + 'Read {} records'.format(records_to_read)) + self.check_crc(4 + bytecount) + + def parse_read_write_registers(self): + '''Parse function 23: Read/Write multiple registers.''' + self.minimum_length = 13 + + self.puti(1, 'function', 'Function 23: Read/Write Multiple Registers') + + starting_address = self.half_word(2) + # Some instruction manuals use a long form name for addresses, this is + # listed here for convienience. + # Example: holding register 60 becomes 30061. + address_name = 30001 + starting_address + self.puti(3, 'address', + 'Read starting at address 0x{:X} / {:d}'.format(starting_address, + address_name)) + + self.puti(5, 'length', 'Read {:d} units of data'.format(self.half_word(4))) + + starting_address = self.half_word(6) + self.puti(7, 'address', + 'Write starting at address 0x{:X} / {:d}'.format(starting_address, + address_name)) + + quantity_of_outputs = self.half_word(8) + self.puti(9, 'length', + 'Write {} registers'.format(quantity_of_outputs)) + proper_bytecount = quantity_of_outputs * 2 + + bytecount = self.data[10].data + if bytecount == proper_bytecount: + self.puti(10, 'length', 'Byte count: {}'.format(bytecount)) + else: + self.puti(10, 'error', + 'Bad byte count, is {}, should be {}'.format(bytecount, + proper_bytecount)) + self.mimumum_length = bytecount + 13 + + self.putl('data', 'Data, value 0x{:02X}', 10 + bytecount) + + self.check_crc(bytecount + 12) + +class Decoder(srd.Decoder): + api_version = 2 + id = 'modbus' + name = 'Modbus' + longname = 'Modbus RTU over RS232/RS485' + desc = 'Modbus RTU protocol for industrial applications.' + license = 'gplv2+' + inputs = ['uart'] + outputs = ['modbus'] + annotations = ( + ('sc-server-id', ''), + ('sc-function', ''), + ('sc-crc', ''), + ('sc-address', ''), + ('sc-data', ''), + ('sc-length', ''), + ('sc-error', ''), + ('cs-server-id', ''), + ('cs-function', ''), + ('cs-crc', ''), + ('cs-address', ''), + ('cs-data', ''), + ('cs-length', ''), + ('cs-error', ''), + ('error-indication', ''), + ) + annotation_rows = ( + ('sc', 'Server->client', (0, 1, 2, 3, 4, 5, 6)), + ('cs', 'Client->server', (7, 8, 9, 10, 11, 12, 13)), + ('error-indicator', 'Errors in frame', (14,)), + ) + options = ( + {'id': 'channel', 'desc': 'Server -> client channel', 'default': 'RX', + 'values': ('RX', 'TX')}, + ) + + def __init__(self, **kwargs): + self.ADUSc = None # Start off with empty slave -> client ADU. + self.ADUCs = None # Start off with empty client -> slave ADU. + + # The reason we have both (despite not supporting full duplex comms) is + # because we want to be able to decode the message as both client -> + # server and server -> client, and let the user see which of the two + # the ADU was. + + self.bitlength = None # We will later test how long a bit is. + + def start(self): + self.out_ann = self.register(srd.OUTPUT_ANN) + + def puta(self, start, end, ann_str, message): + '''Put an annotation from start to end, with ann as a + string. This means you don't have to know the ann's + number to write annotations to it.''' + ann = [s[0] for s in self.annotations].index(ann_str) + self.put(start, end, self.out_ann, [ann, [message]]) + + def decode_adu(self, ss, es, data, direction): + '''Decode the next byte or bit (depending on type) in the ADU. + ss: Start time of the data + es: End time of the data + data: Data as passed from the UART decoder + direction: Is this data for the Cs (client -> server) or Sc (server -> + client) being decoded right now?''' + ptype, rxtx, pdata = data + + # We don't have a nice way to get the baud rate from UART, so we have + # to figure out how long a bit lasts. We do this by looking at the + # length of (probably) the startbit. + if self.bitlength is None: + if ptype == 'STARTBIT' or ptype == 'STOPBIT': + self.bitlength = es - ss + else: + # If we don't know the bitlength yet, we can't start decoding. + return + + # Select the ADU, create the ADU if needed. + # We set ADU.startNewFrame = True when we know the old one is over. + if direction == 'Sc': + if (self.ADUSc is None) or self.ADUSc.startNewFrame: + self.ADUSc = Modbus_ADU_SC(self, ss, TX, 'sc-') + ADU = self.ADUSc + if direction == 'Cs': + if self.ADUCs is None or self.ADUCs.startNewFrame: + self.ADUCs = Modbus_ADU_CS(self, ss, TX, 'cs-') + ADU = self.ADUCs + + # We need to determine if the last ADU is over. + # According to the Modbus spec, there should be 3.5 characters worth of + # space between each message. But if within a message there is a length + # of more than 1.5 character, that's an error. For our purposes + # somewhere between seems fine. + # A character is 11 bits long, so (3.5 + 1.5)/2 * 11 ~= 28 + # TODO: Display error for too short or too long. + if (ss - ADU.last_read) <= self.bitlength * 28: + ADU.add_data(ss, es, data) + else: + # It's been too long since the last part of the ADU! + # If there is any data in the ADU we need to show it to the user + if len(ADU.data) > 0: + # Extend errors for 3 bits after last byte, we can guarantee + # space. + ADU.close(ADU.data[-1].end + self.bitlength * 3) + + ADU.startNewFrame = True + # Restart this function, it will make a new ADU for us. + self.decode_adu(ss, es, data, direction) + + def decode(self, ss, es, data): + ptype, rxtx, pdata = data + + # Decide what ADU(s) we need this packet to go to. + # Note that it's possible to go to both ADUs. + if rxtx == TX: + self.decode_adu(ss, es, data, 'Cs') + if rxtx == TX and self.options['channel'] == 'TX': + self.decode_adu(ss, es, data, 'Sc') + if rxtx == RX and self.options['channel'] == 'RX': + self.decode_adu(ss, es, data, 'Sc')