From e1c2059e94864c8b8baa793f633b0cb4a8f63f31 Mon Sep 17 00:00:00 2001 From: crankyoldgit Date: Wed, 9 May 2018 01:32:50 +1000 Subject: [PATCH 1/5] Port AutoAnalyseRawData script to Python. Mostly a direct port from the Bash shell code to python Some minor features were dropped. Some new ones added. --- tools/AutoAnalyseRawData.py | 277 ++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100755 tools/AutoAnalyseRawData.py diff --git a/tools/AutoAnalyseRawData.py b/tools/AutoAnalyseRawData.py new file mode 100755 index 000000000..fcbf28e3e --- /dev/null +++ b/tools/AutoAnalyseRawData.py @@ -0,0 +1,277 @@ +#!/usr/bin/python +# Attempt an automatic analysis of IRremoteESP8266's Raw data output. +# Makes suggestions on key values and tried to break down the message +# into likely chuncks. +# +# Copyright 2018 David Conran +import argparse +import sys + +def usecCompare(seen, expected, margin): + return (seen <= expected and seen > expected - margin) + +def usecCompares(usecs, expecteds, margin): + for expected in expecteds: + if usecCompare(usecs, expected, margin): + return True + return False + +def addBit(so_far, bit): + global binary_value + if bit == "reset": + return "" + sys.stdout.write(str(bit)) # This effectively displays in LSB first order. + return so_far + str(bit) # Storing it in MSB first order. + +def isOdd(num): + return num % 2 + +def displayBinaryValue(binary_str, output=sys.stdout): + num = int(binary_str, 2) + bits = len(binary_str) + rev_binary_str = binary_str[::-1] + rev_num = int(rev_binary_str, 2) + output.write("\n Bits: %d\n" % bits) + output.write(" Hex: %s (MSB first)\n" % "0x{0:0{1}X}".format(num, bits / 4)) + output.write(" %s (LSB first)\n" % "0x{0:0{1}X}".format(rev_num, + bits / 4)) + output.write(" Dec: %s (MSB first)\n" % num) + output.write(" %s (LSB first)\n" % rev_num) + output.write(" Bin: 0b%s (MSB first)\n" % binary_str) + output.write(" 0b%s (LSB first)\n" % rev_binary_str) + +def reduceList(items, margin): + result = [] + last = -1 + for item in sorted(items, reverse=True): + if last == -1 or item < last - margin: + result.append(item) + last = item + return result + +def ParseAndReport(rawdata_str, margin, gen_code=False, output=sys.stdout): + marks = [] + spaces = [] + codeDefines = [] + code64Bit = [] + # Clean up the input + start = rawdata_str.find('{') + end = rawdata_str.find('}') + rawdata_str = rawdata_str[start + 1:end] + rawData = map(lambda x: int(x.strip()), rawdata_str.split(',')) + + # Parse the input. + count = 0 + for usecs in rawData: + count = count + 1 + if isOdd(count): + marks.append(usecs) + else: + spaces.append(usecs) + + mark_candidates = reduceList(marks, margin) + space_candidates = reduceList(spaces, margin) + output.write("Potential Mark Candidates (using a margin of %d):\n" % margin) + output.write(str(mark_candidates)) + output.write("\n") + output.write("Potential Space Candidates (using a margin of %d):\n" % margin) + output.write(str(space_candidates)) + output.write("\n\nGuessing encoding type:\n") + if len(space_candidates) > len(mark_candidates): + output.write("Looks like it uses space encoding. Yay!\n\n") + output.write("Guessing key value:\n") + + # Largest mark is likely the HDR_MARK + hdr_mark = mark_candidates[0] + output.write("HDR_MARK = %d\n" % hdr_mark) + codeDefines.append("#define HDR_MARK %dU" % hdr_mark) + # The mark bit is likely to be the smallest. + bit_mark = mark_candidates[-1] + output.write("BIT_MARK = %d\n" % bit_mark) + codeDefines.append("#define BIT_MARK %dU" % bit_mark) + + gap = 0 + gap_candidates = [] + while len(space_candidates) > 3: + # We probably (still) have a gap in the protocol. + gap = gap + 1 + space = space_candidates.remove() + gap_candidates.append(space) + output.write("SPACE_GAP%d = %d\n" % (gap, space)) + codeDefines.append("#define SPACE_GAP%d = %dU" % (gap, space)) + + + # We should have 3 space candidates left. + # They should be zero_space (smallest), one_space, & hdr_space (largest) + zero_space = space_candidates.pop() + one_space = space_candidates.pop() + hdr_space = space_candidates.pop() + output.write("HDR_SPACE = %d\n" % hdr_space) + codeDefines.append("#define HDR_SPACE %dU" % hdr_space) + output.write("ONE_SPACE = %d\n" % one_space) + codeDefines.append("#define ONE_SPACE %dU" % one_space) + output.write("ZERO_SPACE = %d\n" % zero_space) + codeDefines.append("#define ZERO_SPACE %dU" % zero_space) + else: + output.write("Sorry, it looks like it is Mark encoded. " + "I can't do that yet. Exiting.\n") + sys.exit(1) + + + # Now we have likely candidates for the key values, go through the original + # sequence and break it up and indicate accordingly. + + output.write("\nDecoding protocol based on analysis so far:\n\n") + last = "" + count = 1 + data_count = 1 + last_binary_value = "" + bits = 0 + total_bits = "" + binary_value = addBit("", "reset") + + code64Bit.append("// Function should be safe upto 64 bits.") + code64Bit.append("void IRsend::sendXYZ(const uint64_t data, const uint16_t" + " nbits, const uint16_t repeat) {") + code64Bit.append(" enableIROut(38000); // A guess. Most common frequency.") + code64Bit.append(" for (uint16_t r = 0; r <= repeat; r++) {") + + for usecs in rawData: + if (usecCompare(usecs, hdr_mark, margin) and + isOdd(count) and not usecCompare(usecs, bit_mark, margin)): + last = "HM" + if len(binary_value) != 0: + displayBinaryValue(binary_value) + total_bits = total_bits + binary_value + output.write(last) + binary_value = addBit(binary_value, "reset") + output.write("HDR_MARK+") + code64Bit.append(" // Header") + code64Bit.append(" mark(HDR_MARK);") + elif (usecCompare(usecs, hdr_space, margin) and + not usecCompare(usecs, one_space, margin)): + if last != "HM": + if len(binary_value) != 0: + displayBinaryValue(binary_value) + total_bits = total_bits + binary_value + code64Bit.extend(addDataCode(binary_value)) + binary_value = addBit(binary_value, "reset") + output.write("UNEXPECTED->") + last = "HS" + output.write("HDR_SPACE+") + code64Bit.append(" space(HDR_SPACE);") + elif (usecCompare(usecs, bit_mark, margin) and isOdd(count)): + if last != "HS" and last != "BS": + output.write("BIT_MARK(UNEXPECTED)") + last = "BM" + elif usecCompare(usecs, zero_space, margin): + if last != "BM": + output.write("ZERO_SPACE(UNEXPECTED)") + last= "BS" + binary_value = addBit(binary_value, 0) + elif usecCompare(usecs, one_space, margin): + if last != "BM": + output.write("ONE_SPACE(UNEXPECTED)") + last = "BS" + binary_value = addBit(binary_value, 1) + elif usecCompares(usecs, gap_candidates, margin): + if last != "BM": + output.write("UNEXPECTED->") + last = "GS" + output.write(" GAP(%d)" % usecs) + displayBinaryValue(binary_value) + code64Bit.extend(addDataCode(binary_value)) + code64Bit.append(" space(SPACE_GAP);") + total_bits = total_bits + binary_value + binary_value = addBit(binary_value, "reset") + else: + output.write("UNKNOWN(%d)" % usecs) + last = "UNK" + count = count + 1 + displayBinaryValue(binary_value) + code64Bit.extend(addDataCode(binary_value)) + code64Bit.append(" space(100000); // A 100% made up guess of the gap" + " between messages.") + code64Bit.append(" }") + code64Bit.append("}") + + total_bits = total_bits + binary_value + output.write("Total Nr. of suspected bits: %d\n" % len(total_bits)) + codeDefines.append("#define XYZ_BITS %dU" % len(total_bits)) + if len(total_bits) > 64: + codeDefines.append("#define XYZ_STATE_LENGTH %dU" % (len(total_bits) / 8)) + + if gen_code: + output.write("\nGenerating a VERY rough code outline:\n\n" + "// WARNING: This probably isn't directly usable." + " It's a guide only.\n") + for line in codeDefines: + output.write("%s\n" % line) + + if len(total_bits) > 64: # Will it fit in a uint64_t? + output.write("// DANGER: More than 64 bits detected. A uint64_t for " + "'data' won't work!\n") + # Display the "normal" version's code incase there are some oddities in it. + for line in code64Bit: + output.write("%s\n" % line) + + if len(total_bits) > 64: # Will it fit in a uint64_t? + output.write("\n\n// Alternative >64 bit Function\n" + "void IRsend::sendXYZ(uint8_t data[], uint16_t nbytes," + " uint16_t repeat) {\n" + " // nbytes should typically be XYZ_STATE_LENGTH\n" + " // data should typically be:\n" + " // uint8_t data[XYZ_STATE_LENGTH] = {0x%s};\n" + " // data[] is assumed to be in MSB order for this code.\n" + " for (uint16_t r = 0; r <= repeat; r++) {\n" + " sendGeneric(HDR_MARK, HDR_SPACE,\n" + " BIT_MARK, ONE_SPACE,\n" + " BIT_MARK, ZERO_SPACE,\n" + " BIT_MARK\n" + " 100000, // 100%% made-up guess at the" + " message gap.\n" + " data, nbytes,\n" + " 38000, // Complete guess of the modulation" + " frequency.\n" + " true, 0, 50);\n" + "}\n" % ", 0x".join("%02X" % int(total_bits[i:i + 8], 2) + for i in range(0, len(total_bits), 8))) + +def addDataCode(bin_str): + code = [] + code.append(" // Data") + code.append(" // e.g. data = 0x%X, nbits = %d" % (int(bin_str, 2), + len(bin_str))) + code.append(" sendData(BIT_MARK, ONE_SPACE, BIT_MARK, ZERO_SPACE, data, " + "nbits, true);") + code.append(" // Footer") + code.append(" mark(BIT_MARK);") + return code + +# Main program + +class Args: + pass + +options = Args() +parser = argparse.ArgumentParser( + description="Read an IRremoteESP8266 rawData declaration and tries to " + "analyse it.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) +parser.add_argument("-r", "--range", type=int, + help="Max number of micro-seconds difference between values to consider it " + "the same value.", + dest="margin", + default=200) +parser.add_argument("rawData", + help="A rawData line from IRrecvDumpV2. e.g. 'uint16_t rawbuf[37] = {7930, " + "3952, 494, 1482, 520, 1482, 494, 1508, 494, 520, 494, 1482, 494, 520," + " 494, 1482, 494, 1482, 494, 3978, 494, 520, 494, 520, 494, 520, 494, " + "520, 520, 520, 494, 520, 494, 520, 494, 520, 494};'") +parser.add_argument("--code", action="store_true", default=False, + dest="gen_code", + help="Produce a C++ code outline to aid making an IRsend " + "function.") +parser.parse_args(namespace=options) + +ParseAndReport(options.rawData, options.margin, options.gen_code) From 73280d64558ef182b91fb4d1de05568c00dfeb5e Mon Sep 17 00:00:00 2001 From: David Conran Date: Thu, 10 May 2018 20:04:09 +1000 Subject: [PATCH 2/5] Python style & lint improvements for Analyse script. Rename python analysis script to snake_case format. --- tools/.style.yapf | 3 + tools/AutoAnalyseRawData.py | 277 ---------------------------- tools/auto_analyse_raw_data.py | 328 +++++++++++++++++++++++++++++++++ tools/pylintrc | 12 ++ 4 files changed, 343 insertions(+), 277 deletions(-) create mode 100644 tools/.style.yapf delete mode 100755 tools/AutoAnalyseRawData.py create mode 100755 tools/auto_analyse_raw_data.py create mode 100644 tools/pylintrc diff --git a/tools/.style.yapf b/tools/.style.yapf new file mode 100644 index 000000000..65fa0ee33 --- /dev/null +++ b/tools/.style.yapf @@ -0,0 +1,3 @@ +[style] +based_on_style: google +indent_width: 2 diff --git a/tools/AutoAnalyseRawData.py b/tools/AutoAnalyseRawData.py deleted file mode 100755 index fcbf28e3e..000000000 --- a/tools/AutoAnalyseRawData.py +++ /dev/null @@ -1,277 +0,0 @@ -#!/usr/bin/python -# Attempt an automatic analysis of IRremoteESP8266's Raw data output. -# Makes suggestions on key values and tried to break down the message -# into likely chuncks. -# -# Copyright 2018 David Conran -import argparse -import sys - -def usecCompare(seen, expected, margin): - return (seen <= expected and seen > expected - margin) - -def usecCompares(usecs, expecteds, margin): - for expected in expecteds: - if usecCompare(usecs, expected, margin): - return True - return False - -def addBit(so_far, bit): - global binary_value - if bit == "reset": - return "" - sys.stdout.write(str(bit)) # This effectively displays in LSB first order. - return so_far + str(bit) # Storing it in MSB first order. - -def isOdd(num): - return num % 2 - -def displayBinaryValue(binary_str, output=sys.stdout): - num = int(binary_str, 2) - bits = len(binary_str) - rev_binary_str = binary_str[::-1] - rev_num = int(rev_binary_str, 2) - output.write("\n Bits: %d\n" % bits) - output.write(" Hex: %s (MSB first)\n" % "0x{0:0{1}X}".format(num, bits / 4)) - output.write(" %s (LSB first)\n" % "0x{0:0{1}X}".format(rev_num, - bits / 4)) - output.write(" Dec: %s (MSB first)\n" % num) - output.write(" %s (LSB first)\n" % rev_num) - output.write(" Bin: 0b%s (MSB first)\n" % binary_str) - output.write(" 0b%s (LSB first)\n" % rev_binary_str) - -def reduceList(items, margin): - result = [] - last = -1 - for item in sorted(items, reverse=True): - if last == -1 or item < last - margin: - result.append(item) - last = item - return result - -def ParseAndReport(rawdata_str, margin, gen_code=False, output=sys.stdout): - marks = [] - spaces = [] - codeDefines = [] - code64Bit = [] - # Clean up the input - start = rawdata_str.find('{') - end = rawdata_str.find('}') - rawdata_str = rawdata_str[start + 1:end] - rawData = map(lambda x: int(x.strip()), rawdata_str.split(',')) - - # Parse the input. - count = 0 - for usecs in rawData: - count = count + 1 - if isOdd(count): - marks.append(usecs) - else: - spaces.append(usecs) - - mark_candidates = reduceList(marks, margin) - space_candidates = reduceList(spaces, margin) - output.write("Potential Mark Candidates (using a margin of %d):\n" % margin) - output.write(str(mark_candidates)) - output.write("\n") - output.write("Potential Space Candidates (using a margin of %d):\n" % margin) - output.write(str(space_candidates)) - output.write("\n\nGuessing encoding type:\n") - if len(space_candidates) > len(mark_candidates): - output.write("Looks like it uses space encoding. Yay!\n\n") - output.write("Guessing key value:\n") - - # Largest mark is likely the HDR_MARK - hdr_mark = mark_candidates[0] - output.write("HDR_MARK = %d\n" % hdr_mark) - codeDefines.append("#define HDR_MARK %dU" % hdr_mark) - # The mark bit is likely to be the smallest. - bit_mark = mark_candidates[-1] - output.write("BIT_MARK = %d\n" % bit_mark) - codeDefines.append("#define BIT_MARK %dU" % bit_mark) - - gap = 0 - gap_candidates = [] - while len(space_candidates) > 3: - # We probably (still) have a gap in the protocol. - gap = gap + 1 - space = space_candidates.remove() - gap_candidates.append(space) - output.write("SPACE_GAP%d = %d\n" % (gap, space)) - codeDefines.append("#define SPACE_GAP%d = %dU" % (gap, space)) - - - # We should have 3 space candidates left. - # They should be zero_space (smallest), one_space, & hdr_space (largest) - zero_space = space_candidates.pop() - one_space = space_candidates.pop() - hdr_space = space_candidates.pop() - output.write("HDR_SPACE = %d\n" % hdr_space) - codeDefines.append("#define HDR_SPACE %dU" % hdr_space) - output.write("ONE_SPACE = %d\n" % one_space) - codeDefines.append("#define ONE_SPACE %dU" % one_space) - output.write("ZERO_SPACE = %d\n" % zero_space) - codeDefines.append("#define ZERO_SPACE %dU" % zero_space) - else: - output.write("Sorry, it looks like it is Mark encoded. " - "I can't do that yet. Exiting.\n") - sys.exit(1) - - - # Now we have likely candidates for the key values, go through the original - # sequence and break it up and indicate accordingly. - - output.write("\nDecoding protocol based on analysis so far:\n\n") - last = "" - count = 1 - data_count = 1 - last_binary_value = "" - bits = 0 - total_bits = "" - binary_value = addBit("", "reset") - - code64Bit.append("// Function should be safe upto 64 bits.") - code64Bit.append("void IRsend::sendXYZ(const uint64_t data, const uint16_t" - " nbits, const uint16_t repeat) {") - code64Bit.append(" enableIROut(38000); // A guess. Most common frequency.") - code64Bit.append(" for (uint16_t r = 0; r <= repeat; r++) {") - - for usecs in rawData: - if (usecCompare(usecs, hdr_mark, margin) and - isOdd(count) and not usecCompare(usecs, bit_mark, margin)): - last = "HM" - if len(binary_value) != 0: - displayBinaryValue(binary_value) - total_bits = total_bits + binary_value - output.write(last) - binary_value = addBit(binary_value, "reset") - output.write("HDR_MARK+") - code64Bit.append(" // Header") - code64Bit.append(" mark(HDR_MARK);") - elif (usecCompare(usecs, hdr_space, margin) and - not usecCompare(usecs, one_space, margin)): - if last != "HM": - if len(binary_value) != 0: - displayBinaryValue(binary_value) - total_bits = total_bits + binary_value - code64Bit.extend(addDataCode(binary_value)) - binary_value = addBit(binary_value, "reset") - output.write("UNEXPECTED->") - last = "HS" - output.write("HDR_SPACE+") - code64Bit.append(" space(HDR_SPACE);") - elif (usecCompare(usecs, bit_mark, margin) and isOdd(count)): - if last != "HS" and last != "BS": - output.write("BIT_MARK(UNEXPECTED)") - last = "BM" - elif usecCompare(usecs, zero_space, margin): - if last != "BM": - output.write("ZERO_SPACE(UNEXPECTED)") - last= "BS" - binary_value = addBit(binary_value, 0) - elif usecCompare(usecs, one_space, margin): - if last != "BM": - output.write("ONE_SPACE(UNEXPECTED)") - last = "BS" - binary_value = addBit(binary_value, 1) - elif usecCompares(usecs, gap_candidates, margin): - if last != "BM": - output.write("UNEXPECTED->") - last = "GS" - output.write(" GAP(%d)" % usecs) - displayBinaryValue(binary_value) - code64Bit.extend(addDataCode(binary_value)) - code64Bit.append(" space(SPACE_GAP);") - total_bits = total_bits + binary_value - binary_value = addBit(binary_value, "reset") - else: - output.write("UNKNOWN(%d)" % usecs) - last = "UNK" - count = count + 1 - displayBinaryValue(binary_value) - code64Bit.extend(addDataCode(binary_value)) - code64Bit.append(" space(100000); // A 100% made up guess of the gap" - " between messages.") - code64Bit.append(" }") - code64Bit.append("}") - - total_bits = total_bits + binary_value - output.write("Total Nr. of suspected bits: %d\n" % len(total_bits)) - codeDefines.append("#define XYZ_BITS %dU" % len(total_bits)) - if len(total_bits) > 64: - codeDefines.append("#define XYZ_STATE_LENGTH %dU" % (len(total_bits) / 8)) - - if gen_code: - output.write("\nGenerating a VERY rough code outline:\n\n" - "// WARNING: This probably isn't directly usable." - " It's a guide only.\n") - for line in codeDefines: - output.write("%s\n" % line) - - if len(total_bits) > 64: # Will it fit in a uint64_t? - output.write("// DANGER: More than 64 bits detected. A uint64_t for " - "'data' won't work!\n") - # Display the "normal" version's code incase there are some oddities in it. - for line in code64Bit: - output.write("%s\n" % line) - - if len(total_bits) > 64: # Will it fit in a uint64_t? - output.write("\n\n// Alternative >64 bit Function\n" - "void IRsend::sendXYZ(uint8_t data[], uint16_t nbytes," - " uint16_t repeat) {\n" - " // nbytes should typically be XYZ_STATE_LENGTH\n" - " // data should typically be:\n" - " // uint8_t data[XYZ_STATE_LENGTH] = {0x%s};\n" - " // data[] is assumed to be in MSB order for this code.\n" - " for (uint16_t r = 0; r <= repeat; r++) {\n" - " sendGeneric(HDR_MARK, HDR_SPACE,\n" - " BIT_MARK, ONE_SPACE,\n" - " BIT_MARK, ZERO_SPACE,\n" - " BIT_MARK\n" - " 100000, // 100%% made-up guess at the" - " message gap.\n" - " data, nbytes,\n" - " 38000, // Complete guess of the modulation" - " frequency.\n" - " true, 0, 50);\n" - "}\n" % ", 0x".join("%02X" % int(total_bits[i:i + 8], 2) - for i in range(0, len(total_bits), 8))) - -def addDataCode(bin_str): - code = [] - code.append(" // Data") - code.append(" // e.g. data = 0x%X, nbits = %d" % (int(bin_str, 2), - len(bin_str))) - code.append(" sendData(BIT_MARK, ONE_SPACE, BIT_MARK, ZERO_SPACE, data, " - "nbits, true);") - code.append(" // Footer") - code.append(" mark(BIT_MARK);") - return code - -# Main program - -class Args: - pass - -options = Args() -parser = argparse.ArgumentParser( - description="Read an IRremoteESP8266 rawData declaration and tries to " - "analyse it.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) -parser.add_argument("-r", "--range", type=int, - help="Max number of micro-seconds difference between values to consider it " - "the same value.", - dest="margin", - default=200) -parser.add_argument("rawData", - help="A rawData line from IRrecvDumpV2. e.g. 'uint16_t rawbuf[37] = {7930, " - "3952, 494, 1482, 520, 1482, 494, 1508, 494, 520, 494, 1482, 494, 520," - " 494, 1482, 494, 1482, 494, 3978, 494, 520, 494, 520, 494, 520, 494, " - "520, 520, 520, 494, 520, 494, 520, 494, 520, 494};'") -parser.add_argument("--code", action="store_true", default=False, - dest="gen_code", - help="Produce a C++ code outline to aid making an IRsend " - "function.") -parser.parse_args(namespace=options) - -ParseAndReport(options.rawData, options.margin, options.gen_code) diff --git a/tools/auto_analyse_raw_data.py b/tools/auto_analyse_raw_data.py new file mode 100755 index 000000000..0a68ba1fe --- /dev/null +++ b/tools/auto_analyse_raw_data.py @@ -0,0 +1,328 @@ +#!/usr/bin/python +"""Attempt an automatic analysis of IRremoteESP8266's Raw data output. + Makes suggestions on key values and tried to break down the message + into likely chuncks.""" +# +# Copyright 2018 David Conran +import argparse +import sys + + +class Defs(object): + """House the parameters for a message.""" + # pylint: disable=too-many-instance-attributes + + def __init__(self, margin): + self.hdr_mark = None + self.hdr_space = None + self.bit_mark = None + self.zero_space = None + self.one_space = None + self.gaps = [] + self.margin = margin + self.marks = [] + self.spaces = [] + + def process_data(self, data): + """Determine the values from the given data.""" + count = 0 + for usecs in data: + count = count + 1 + if is_odd(count): + self.marks.append(usecs) + else: + self.spaces.append(usecs) + self.marks = self.reduce_list(self.marks) + self.spaces = self.reduce_list(self.spaces) + + def reduce_list(self, items): + """Reduce the list of numbers into buckets that are atleast margin apart.""" + result = [] + last = -1 + for item in sorted(items, reverse=True): + if last == -1 or item < last - self.margin: + result.append(item) + last = item + return result + + +def usec_compare(seen, expected, margin): + """Compare two usec values and see if they match within a + subtrative margin.""" + return seen <= expected and seen > expected - margin + + +def usec_compares(usecs, expecteds, margin): + """Compare a usec value to a list of values and return True + if they are within a subtractive margin.""" + for expected in expecteds: + if usec_compare(usecs, expected, margin): + return True + return False + + +def add_bit(so_far, bit): + """Add a bit to the end of the bits collected so far. """ + if bit == "reset": + return "" + sys.stdout.write(str(bit)) # This effectively displays in LSB first order. + return so_far + str(bit) # Storing it in MSB first order. + + +def is_odd(num): + """Is the num odd? i.e. Not even.""" + return num % 2 + + +def display_bin_value(binary_str, output=sys.stdout): + """Display common representations of the suppied binary string.""" + num = int(binary_str, 2) + bits = len(binary_str) + rev_binary_str = binary_str[::-1] + rev_num = int(rev_binary_str, 2) + output.write("\n Bits: %d\n" % bits) + output.write(" Hex: %s (MSB first)\n" % "0x{0:0{1}X}".format(num, bits / 4)) + output.write( + " %s (LSB first)\n" % "0x{0:0{1}X}".format(rev_num, bits / 4)) + output.write(" Dec: %s (MSB first)\n" % num) + output.write(" %s (LSB first)\n" % rev_num) + output.write(" Bin: 0b%s (MSB first)\n" % binary_str) + output.write(" 0b%s (LSB first)\n" % rev_binary_str) + + +def add_data_code(bin_str): + """Add the common "data" sequence of code to send the bulk of a message.""" + code = [] + code.append(" // Data") + code.append(" // e.g. data = 0x%X, nbits = %d" % (int(bin_str, 2), + len(bin_str))) + code.append(" sendData(BIT_MARK, ONE_SPACE, BIT_MARK, ZERO_SPACE, data, " + "nbits, true);") + code.append(" // Footer") + code.append(" mark(BIT_MARK);") + return code + + +def convert_rawdata(data_str): + """Parse a C++ rawdata declaration into a list of values.""" + start = data_str.find('{') + end = data_str.find('}') + data_str = data_str[start + 1:end] + return [int(x.strip()) for x in data_str.split(',')] + + +def parse_and_report(rawdata_str, margin, gen_code=False, output=sys.stdout): + """Analyse the rawdata c++ definition of a IR message.""" + code_defs = [] + code_64bit = [] + defs = Defs(margin) + + # Parse the input. + rawdata = convert_rawdata(rawdata_str) + + defs.process_data(rawdata) + output.write("Potential Mark Candidates:\n" + "%s\n" + "Potential Space Candidates:\n" + "%s\n" % (str(defs.marks), str(defs.spaces))) + output.write("\n\nGuessing encoding type:\n") + if len(defs.spaces) > len(defs.marks): + output.write("Looks like it uses space encoding. Yay!\n\n" + "Guessing key value:\n") + + # Largest mark is likely the HDR_MARK + defs.hdr_mark = defs.marks[0] + output.write("HDR_MARK = %d\n" % defs.hdr_mark) + code_defs.append("#define HDR_MARK %dU" % defs.hdr_mark) + # The mark bit is likely to be the smallest. + defs.bit_mark = defs.marks[-1] + output.write("BIT_MARK = %d\n" % defs.bit_mark) + code_defs.append("#define BIT_MARK %dU" % defs.bit_mark) + + gap = 0 + defs.gaps = [] + while len(defs.spaces) > 3: + # We probably (still) have a gap in the protocol. + gap = gap + 1 + space = defs.spaces.remove() + defs.gaps.append(space) + output.write("SPACE_GAP%d = %d\n" % (gap, space)) + code_defs.append("#define SPACE_GAP%d = %dU" % (gap, space)) + + # We should have 3 space candidates left. + # They should be zero_space (smallest), one_space, & hdr_space (largest) + defs.zero_space = defs.spaces.pop() + defs.one_space = defs.spaces.pop() + defs.hdr_space = defs.spaces.pop() + code_defs.append("#define HDR_SPACE %dU" % defs.hdr_space) + code_defs.append("#define ONE_SPACE %dU" % defs.one_space) + code_defs.append("#define ZERO_SPACE %dU" % defs.zero_space) + output.write("HDR_SPACE = %d\n" + "ONE_SPACE = %d\n" + "ZERO_SPACE = %d\n" % (defs.hdr_space, defs.one_space, + defs.zero_space)) + else: + output.write("Sorry, it looks like it is Mark encoded. " + "I can't do that yet. Exiting.\n") + sys.exit(1) + total_bits = decode_data(rawdata, defs, code_defs, code_64bit, output) + if gen_code: + generate_code(code_defs, code_64bit, total_bits, output) + + +def decode_data(data, defs, code_defs, code_64bit, output=sys.stdout): + """Decode the data sequence with the given values in mind.""" + # pylint: disable=too-many-branches,too-many-statements + + # Now we have likely candidates for the key values, go through the original + # sequence and break it up and indicate accordingly. + + output.write("\nDecoding protocol based on analysis so far:\n\n") + last = "" + count = 1 + total_bits = "" + binary_value = add_bit("", "reset") + + code_64bit.extend([ + "// Function should be safe up to 64 bits.", + "void IRsend::sendXYZ(const uint64_t data, const uint16_t" + " nbits, const uint16_t repeat) {", + " enableIROut(38); // A guess. Most common frequency.", + " for (uint16_t r = 0; r <= repeat; r++) {" + ]) + + for usecs in data: + if (usec_compare(usecs, defs.hdr_mark, defs.margin) and is_odd(count) and + not usec_compare(usecs, defs.bit_mark, defs.margin)): + last = "HM" + if binary_value: + display_bin_value(binary_value) + total_bits = total_bits + binary_value + output.write(last) + binary_value = add_bit(binary_value, "reset") + output.write("HDR_MARK+") + code_64bit.extend([" // Header", " mark(HDR_MARK);"]) + elif (usec_compare(usecs, defs.hdr_space, defs.margin) and + not usec_compare(usecs, defs.one_space, defs.margin)): + if last != "HM": + if binary_value: + display_bin_value(binary_value) + total_bits = total_bits + binary_value + code_64bit.extend(add_data_code(binary_value)) + binary_value = add_bit(binary_value, "reset") + output.write("UNEXPECTED->") + last = "HS" + output.write("HDR_SPACE+") + code_64bit.append(" space(HDR_SPACE);") + elif usec_compare(usecs, defs.bit_mark, defs.margin) and is_odd(count): + if last != "HS" and last != "BS": + output.write("BIT_MARK(UNEXPECTED)") + last = "BM" + elif usec_compare(usecs, defs.zero_space, defs.margin): + if last != "BM": + output.write("ZERO_SPACE(UNEXPECTED)") + last = "BS" + binary_value = add_bit(binary_value, 0) + elif usec_compare(usecs, defs.one_space, defs.margin): + if last != "BM": + output.write("ONE_SPACE(UNEXPECTED)") + last = "BS" + binary_value = add_bit(binary_value, 1) + elif usec_compares(usecs, defs.gaps, defs.margin): + if last != "BM": + output.write("UNEXPECTED->") + last = "GS" + output.write(" GAP(%d)" % usecs) + display_bin_value(binary_value) + code_64bit.extend(add_data_code(binary_value)) + code_64bit.append(" space(SPACE_GAP);") + total_bits = total_bits + binary_value + binary_value = add_bit(binary_value, "reset") + else: + output.write("UNKNOWN(%d)" % usecs) + last = "UNK" + count = count + 1 + display_bin_value(binary_value) + code_64bit.extend(add_data_code(binary_value)) + code_64bit.extend([ + " space(100000); // A 100% made up guess of the gap" + " between messages.", " }", "}" + ]) + + total_bits = total_bits + binary_value + output.write("Total Nr. of suspected bits: %d\n" % len(total_bits)) + code_defs.append("#define XYZ_BITS %dU" % len(total_bits)) + if len(total_bits) > 64: + code_defs.append("#define XYZ_STATE_LENGTH %dU" % (len(total_bits) / 8)) + return total_bits + + +def generate_code(defs, normal, bits_str, output=sys.stdout): + """Output the estimated C++ code to reproduce the IR message.""" + output.write("\nGenerating a VERY rough code outline:\n\n" + "// WARNING: This probably isn't directly usable." + " It's a guide only.\n") + for line in defs: + output.write("%s\n" % line) + + if len(bits_str) > 64: # Will it fit in a uint64_t? + output.write("// DANGER: More than 64 bits detected. A uint64_t for " + "'data' won't work!\n") + # Display the "normal" version's code incase there are some + # oddities in it. + for line in normal: + output.write("%s\n" % line) + + if len(bits_str) > 64: # Will it fit in a uint64_t? + output.write("\n\n// Alternative >64 bit Function\n" + "void IRsend::sendXYZ(uint8_t data[], uint16_t nbytes," + " uint16_t repeat) {\n" + " // nbytes should typically be XYZ_STATE_LENGTH\n" + " // data should typically be:\n" + " // uint8_t data[XYZ_STATE_LENGTH] = {0x%s};\n" + " // data[] is assumed to be in MSB order for this code.\n" + " for (uint16_t r = 0; r <= repeat; r++) {\n" + " sendGeneric(HDR_MARK, HDR_SPACE,\n" + " BIT_MARK, ONE_SPACE,\n" + " BIT_MARK, ZERO_SPACE,\n" + " BIT_MARK\n" + " 100000, // 100%% made-up guess at the" + " message gap.\n" + " data, nbytes,\n" + " 38000, // Complete guess of the modulation" + " frequency.\n" + " true, 0, 50);\n" + "}\n" % ", 0x".join("%02X" % int(bits_str[i:i + 8], 2) + for i in range(0, len(bits_str), 8))) + + +# Main program + +ARG_PARSER = argparse.ArgumentParser( + description="Read an IRremoteESP8266 rawData declaration and tries to " + "analyse it.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) +ARG_PARSER.add_argument( + "-g", + "--code", + action="store_true", + default=False, + dest="gen_code", + help="Generate a C++ code outline to aid making an IRsend function.") +ARG_PARSER.add_argument( + "-r", + "--range", + type=int, + help="Max number of micro-seconds difference between values to consider it " + "the same value.", + dest="margin", + default=200) +ARG_PARSER.add_argument( + "rawdata", + help="A rawData line from IRrecvDumpV2. e.g. 'uint16_t rawbuf[37] = {7930, " + "3952, 494, 1482, 520, 1482, 494, 1508, 494, 520, 494, 1482, 494, 520," + " 494, 1482, 494, 1482, 494, 3978, 494, 520, 494, 520, 494, 520, 494, " + "520, 520, 520, 494, 520, 494, 520, 494, 520, 494};'") +ARG_OPTIONS = ARG_PARSER.parse_args() + +parse_and_report(ARG_OPTIONS.rawdata, ARG_OPTIONS.margin, ARG_OPTIONS.gen_code) diff --git a/tools/pylintrc b/tools/pylintrc new file mode 100644 index 000000000..987c6abf9 --- /dev/null +++ b/tools/pylintrc @@ -0,0 +1,12 @@ +[REPORTS] + +# Tells whether to display a full report or only the messages +reports=no + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# String used as indentation unit. +indent-string=' ' From ede0ee7ddab75d53dd32faf545d1fbe9f86acc20 Mon Sep 17 00:00:00 2001 From: crankyoldgit Date: Fri, 11 May 2018 02:07:12 +1000 Subject: [PATCH 3/5] Add options to read the data from stdin or from a file. --- tools/auto_analyse_raw_data.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tools/auto_analyse_raw_data.py b/tools/auto_analyse_raw_data.py index 0a68ba1fe..e51a3b921 100755 --- a/tools/auto_analyse_raw_data.py +++ b/tools/auto_analyse_raw_data.py @@ -10,6 +10,7 @@ class Defs(object): """House the parameters for a message.""" + # pylint: disable=too-many-instance-attributes def __init__(self, margin): @@ -309,6 +310,16 @@ def generate_code(defs, normal, bits_str, output=sys.stdout): default=False, dest="gen_code", help="Generate a C++ code outline to aid making an IRsend function.") +ARG_GROUP = ARG_PARSER.add_mutually_exclusive_group(required=True) +ARG_GROUP.add_argument( + "rawdata", + help="A rawData line from IRrecvDumpV2. e.g. 'uint16_t rawbuf[37] = {7930, " + "3952, 494, 1482, 520, 1482, 494, 1508, 494, 520, 494, 1482, 494, 520," + " 494, 1482, 494, 1482, 494, 3978, 494, 520, 494, 520, 494, 520, 494, " + "520, 520, 520, 494, 520, 494, 520, 494, 520, 494};'", + nargs="?") +ARG_GROUP.add_argument( + "-f", "--file", help="Read in a rawData line from the file.") ARG_PARSER.add_argument( "-r", "--range", @@ -317,12 +328,18 @@ def generate_code(defs, normal, bits_str, output=sys.stdout): "the same value.", dest="margin", default=200) -ARG_PARSER.add_argument( - "rawdata", - help="A rawData line from IRrecvDumpV2. e.g. 'uint16_t rawbuf[37] = {7930, " - "3952, 494, 1482, 520, 1482, 494, 1508, 494, 520, 494, 1482, 494, 520," - " 494, 1482, 494, 1482, 494, 3978, 494, 520, 494, 520, 494, 520, 494, " - "520, 520, 520, 494, 520, 494, 520, 494, 520, 494};'") +ARG_GROUP.add_argument( + "--stdin", + help="Read in a rawData line from STDIN.", + action="store_true", + default=False) ARG_OPTIONS = ARG_PARSER.parse_args() -parse_and_report(ARG_OPTIONS.rawdata, ARG_OPTIONS.margin, ARG_OPTIONS.gen_code) +if ARG_OPTIONS.stdin: + DATA = sys.stdin.read() +elif ARG_OPTIONS.file: + with open(ARG_OPTIONS.file) as f: + DATA = f.read() +else: + DATA = ARG_OPTIONS.rawdata +parse_and_report(DATA, ARG_OPTIONS.margin, ARG_OPTIONS.gen_code) From 5a3882309303431ab5a4446746f0c23825066b76 Mon Sep 17 00:00:00 2001 From: crankyoldgit Date: Fri, 11 May 2018 02:10:14 +1000 Subject: [PATCH 4/5] Remove old analyse script. --- tools/AutoAnalyseRawData.sh | 388 ------------------------------------ 1 file changed, 388 deletions(-) delete mode 100755 tools/AutoAnalyseRawData.sh diff --git a/tools/AutoAnalyseRawData.sh b/tools/AutoAnalyseRawData.sh deleted file mode 100755 index 3a66f652e..000000000 --- a/tools/AutoAnalyseRawData.sh +++ /dev/null @@ -1,388 +0,0 @@ -#!/bin/bash -# Attempt an automatic analysis of IRremoteESP8266's Raw data output. -# Makes suggestions on key values and tried to break down the message -# into likely chuncks. -# -# Copyright 2017 David Conran - -function isDigits() -{ - [[ "$1" =~ ^[0-9]+$ ]] -} - -function maxFromList() -{ - max=-1 - for i in $*; do - if [[ $max -lt $i ]]; then - max=$i - fi - done - echo $max -} - -function cullList() -{ - high_mark=$1 - shift - for i in $*; do - if [[ $i -lt $high_mark ]]; then - echo $i - fi - done -} - -function reduceList() -{ - list=$* - max=$(maxFromList $*) - while [[ $max -gt 0 ]]; do - echo "$max" - list=$(cullList $((max - RANGE)) $list) - max=$(maxFromList $list) - done -} - -function listLength() -{ - echo $# -} - -function isHdrMark() -{ - [[ $1 -le $HDR_MARK && $1 -gt $((HDR_MARK - RANGE)) ]] -} - -function isBitMark() -{ - [[ $1 -le $BIT_MARK && $1 -gt $((BIT_MARK - RANGE)) ]] -} - -function isHdrSpace() -{ - [[ $1 -le $HDR_SPACE && $1 -gt $((HDR_SPACE - RANGE)) ]] -} - -function isZeroSpace() -{ - [[ $1 -le $ZERO_SPACE && $1 -gt $((ZERO_SPACE - RANGE)) ]] -} - -function isOneSpace() -{ - [[ $1 -le $ONE_SPACE && $1 -gt $((ONE_SPACE - RANGE)) ]] -} - -function isGap() -{ - for i in $GAP_LIST; do - if [[ $1 -le $i && $1 -gt $((i - RANGE)) ]]; then - return 0 - fi - done - return 1 -} - -function addBit() -{ - if [[ ${1} == "reset" ]]; then - binary_value="" - bits=0 - return - fi - echo -n "${1}" # This effectively displays in LSB first order. - bits=$((bits + 1)) - total_bits=$((total_bits + 1)) - binary_value="${binary_value}${1}" # Storing it in MSB first order. -} - -function isOdd() -{ - [[ $(($1 % 2)) -eq 1 ]] -} - -function usage() -{ - cat >&2 << EOF -Usage: $0 [-r grouping_range] [-g] - Reads an IRremoteESP8266 rawData declaration from STDIN and tries to - analyse it. - - Args: - -r grouping_range - Max number of milli-seconds difference between values - to consider it the same value. (Default: ${RANGE}) - -g - Produce a C++ code outline to aid making an IRsend function. - - Example input: - uint16_t rawbuf[37] = { - 7930, 3952, 494, 1482, 520, 1482, 494, 1508, - 494, 520, 494, 1482, 494, 520, 494, 1482, - 494, 1482, 494, 3978, 494, 520, 494, 520, - 494, 520, 494, 520, 520, 520, 494, 520, - 494, 520, 494, 520, 494}; -EOF - exit 1 -} - -function binToBase() -{ - bc -q << EOF -obase=${2} -ibase=2 -${1} -EOF -} - -function displayBinaryValue() -{ - [[ -z ${1} ]] && return # Nothing to display - reversed=$(echo ${1} | rev) # Convert the binary value to LSB first - echo " Bits: ${bits}" - echo " Hex: 0x$(binToBase ${1} 16) (MSB first)" - echo " 0x$(binToBase ${reversed} 16) (LSB first)" - echo " Dec: $(binToBase ${1} 10) (MSB first)" - echo " $(binToBase ${reversed} 10) (LSB first)" - echo " Bin: ${1} (MSB first)" - echo " ${reversed} (LSB first)" - if [[ "${1}" == "${last_binary_value}" ]]; then - echo " Note: Value is the same as the last one. Could be a repeated message." - fi -} - -function addCode() { - CODE=$(echo "${CODE}"; echo "${*}") -} - -function addDataCode() { - addCode " // Data #${data_count}" - if [[ "${binary_value}" == "${last_binary_value}" ]]; then - addCode " // CAUTION: data value appears to be a duplicate." - addCode " // This could be a repeated message." - fi - addCode " // e.g. data = 0x$(binToBase ${binary_value} 16), nbits = ${bits}" - addCode "$(bitSizeWarning ${bits} ' ')" - addCode " sendData(BIT_MARK, ONE_SPACE, BIT_MARK, ZERO_SPACE, data, nbits, true);" - addCode " // Footer #${data_count}" - addCode " mark(BIT_MARK);" - data_count=$((data_count + 1)) - last_binary_value=$binary_value -} - -function bitSizeWarning() { - # $1 is the nr of bits. $2 is what to indent with. - if [[ ${1} -gt 64 ]]; then - echo "${2}// DANGER: More than 64 bits detected. A uint64_t for data won't work!" - echo "${2}// DANGER: Try using alternative AirCon version below!" - fi -} - -# Main program - -RANGE=200 -OUTPUT_CODE="" - -while getopts "r:g" opt; do - case $opt in - r) - if isDigits $OPTARG; then - RANGE=$OPTARG - else - echo "Error: grouping_range is not a positive integer." >&2 - usage - fi - ;; - g) - DISPLAY_CODE="yes" - ;; - *) - usage - ;; - esac -done -shift $((OPTIND-1)) - -if [[ $# -ne 0 ]]; then - usage -fi - -if ! which bc &> /dev/null ; then - cat << EOF -'bc' program not found. Exiting. -Suggestion: sudo apt-get install bc -EOF - exit 2 -fi - -# Parse the input. -count=1 -while read line; do - # Quick and Dirty Removal of any array declaration syntax, and any commas. - line="$(echo ${line} | sed 's/^.*uint.*{//i' | sed 's/,/ /g' | sed 's/};.*//g')" - for msecs in ${line}; do - if isDigits "${msecs}"; then - orig="${orig} ${msecs}" - if isOdd $count; then - marks="${marks} ${msecs}" - else - spaces="${spaces} ${msecs}" - fi - count=$((count + 1)) - fi - done -done - -echo "Potential Mark Candidates (using a range of $RANGE):" -reduceList $marks -nr_mark_candidates=$(listLength $(reduceList $marks)) -echo -echo "Potential Space Candidates (using a range of $RANGE):" -reduceList $spaces -nr_space_candidates=$(listLength $(reduceList $spaces)) -echo -echo "Guessing encoding type:" -if [[ $nr_space_candidates -ge $nr_mark_candidates ]]; then - echo "Looks like it uses space encoding. Yay!" - echo - echo "Guessing key value:" - - # Largest mark is likely the HDR_MARK - HDR_MARK=$(reduceList $marks | head -1) - echo HDR_MARK = $HDR_MARK - addCode "#define HDR_MARK ${HDR_MARK}U" - # The mark bit is likely to be the smallest. - BIT_MARK=$(reduceList $marks | tail -1) - echo BIT_MARK = $BIT_MARK - addCode "#define BIT_MARK ${BIT_MARK}U" - - - left=$nr_space_candidates - gap_num=0 - GAP_LIST="" - while [[ $left -gt 3 ]]; do - # We probably (still) have a gap in the protocol. - gap=$((gap + 1)) - SPACE_GAP=$(reduceList $spaces | head -$gap | tail -1) - GAP_LIST="$GAP_LIST $SPACE_GAP" - left=$((left - 1)) - echo SPACE_GAP${gap} = $SPACE_GAP - addCode "#define SPACE_GAP${gap} ${SPACE_GAP}U" - done - # We should have 3 space candidates left. - # They should be ZERO_SPACE (smallest), ONE_SPACE, & HDR_SPACE (largest) - ZERO_SPACE=$(reduceList $spaces | tail -1) - ONE_SPACE=$(reduceList $spaces | tail -2 | head -1) - HDR_SPACE=$(reduceList $spaces | tail -3 | head -1) - echo HDR_SPACE = $HDR_SPACE - addCode "#define HDR_SPACE ${HDR_SPACE}U" - echo ONE_SPACE = $ONE_SPACE - addCode "#define ONE_SPACE ${ONE_SPACE}U" - echo ZERO_SPACE = $ZERO_SPACE - addCode "#define ZERO_SPACE ${ZERO_SPACE}U" -else - echo "Sorry, it looks like it is Mark encoded. I can't do that yet. Exiting." - exit 1 -fi - -# Now we have likely candidates for the key values, go through the original -# sequence and break it up and indicate accordingly. - -echo -echo "Decoding protocol based on analysis so far:" -echo -last="" -count=1 -data_count=1 -last_binary_value="" -total_bits=0 -addBit reset - -addCode "// Function" -addCode "void IRsend::sendXYZ(const uint64_t data, const uint16_t nbits, const uint16_t repeat) {" -addCode " for (uint16_t r = 0; r <= repeat; r++) {" - -for msecs in $orig; do - if isHdrMark $msecs && isOdd $count && ! isBitMark $msecs; then - last="HM" - if [[ $bits -ne 0 ]]; then - echo - displayBinaryValue ${binary_value} - echo -n $last - fi - addBit reset - echo -n "HDR_MARK+" - addCode " // Header #${data_count}" - addCode " mark(HDR_MARK);" - elif isHdrSpace $msecs && ! isOneSpace $msecs; then - if [[ $last != "HM" ]]; then - if [[ $bits -ne 0 ]]; then - echo - displayBinaryValue ${binary_value} - fi - addBit reset - echo -n "UNEXPECTED->" - fi - last="HS" - echo -n "HDR_SPACE+" - addCode " space(HDR_SPACE);" - elif isBitMark $msecs && isOdd $count; then - if [[ $last != "HS" && $last != "BS" ]]; then - echo -n "BIT_MARK(UNEXPECTED)" - fi - last="BM" - elif isZeroSpace $msecs; then - if [[ $last != "BM" ]] ; then - echo -n "ZERO_SPACE(UNEXPECTED)" - fi - last="BS" - addBit 0 - elif isOneSpace $msecs; then - if [[ $last != "BM" ]] ; then - echo -n "ONE_SPACE(UNEXPECTED)" - fi - last="BS" - addBit 1 - elif isGap $msecs; then - if [[ $last != "BM" ]] ; then - echo -n "UNEXPECTED->" - fi - last="GS" - echo " GAP($msecs)" - displayBinaryValue ${binary_value} - addDataCode - addCode " space($msecs);" - addBit reset - else - echo -n "UNKNOWN($msecs)" - last="UNK" - fi - count=$((count + 1)) -done -echo -displayBinaryValue ${binary_value} -if [[ "$DISPLAY_CODE" == "yes" ]]; then - echo - echo "Generating a VERY rough code outline:" - echo - echo "// WARNING: This probably isn't directly usable. It's a guide only." - bitSizeWarning ${total_bits} - addDataCode - addCode " delay(100); // A 100% made up guess of the gap between messages." - addCode " }" - addCode "}" - if [[ ${total_bits} -gt 64 ]]; then - addCode "Alternative (aircon code):" - addCode "// Alternative Function AirCon mode" - addCode "void IRsend::sendXYZ(uint8_t data[], uint16_t nbytes, uint16_t repeat) {" - addCode " // nbytes should typically be $(($total_bits / 8))" - addCode " // data should typically be of a type: uint8_t data[$(($total_bits / 8))] = {};" - addCode " // data[] is assumed to be in MSB order." - addCode " for (uint16_t r = 0; r <= repeat; r++) {" - addCode " sendGeneric(HDR_MARK, HDR_SPACE, BIT_MARK, ONE_SPACE, BIT_MARK, ZERO_SPACE, BIT_MARK" - addCode " 100, data, nbytes, 38, true, 0, 50);" - addCode "}" - fi - - echo "$CODE" -fi From 314811c599c4d2c0aab3174618ff4792fca1bf6e Mon Sep 17 00:00:00 2001 From: crankyoldgit Date: Fri, 11 May 2018 09:57:34 +1000 Subject: [PATCH 5/5] Improve auto_analyse_raw_data and add lint/unit tests. * Make analyse script code unittest-able. * Improve raw data parsing for analyse script. * Add some unit tests for analyse script. * Update Makefile(s) to use 'run_tests' to run their tests. * Add python unit & lint tests into Travis --- .gitignore | 3 + tools/.style.yapf => .style.yapf | 0 .travis.yml | 3 + tools/pylintrc => pylintrc | 0 test/Makefile | 2 + tools/Makefile | 14 +- tools/auto_analyse_raw_data.py | 459 ++++++++++++++++------------ tools/auto_analyse_raw_data_test.py | 288 +++++++++++++++++ 8 files changed, 569 insertions(+), 200 deletions(-) rename tools/.style.yapf => .style.yapf (100%) rename tools/pylintrc => pylintrc (100%) create mode 100755 tools/auto_analyse_raw_data_test.py diff --git a/.gitignore b/.gitignore index 6d57ebaab..c9dfea771 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ lib/googletest/**/* # GCC pre-compiled headers. **/*.gch +# Python compiled files +**/*.pyc + # Unit Test builds test/*.o test/*.a diff --git a/tools/.style.yapf b/.style.yapf similarity index 100% rename from tools/.style.yapf rename to .style.yapf diff --git a/.travis.yml b/.travis.yml index adfd968d8..4331425e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ install: - arduino --board $BD --save-prefs - arduino --pref "compiler.warning_level=all" --save-prefs - sudo apt-get install jq + - sudo pip install pylint script: # Check that everything compiles. - arduino --verify --board $BD $PWD/examples/IRrecvDemo/IRrecvDemo.ino @@ -44,9 +45,11 @@ script: # Check for lint issues. - shopt -s nullglob - python cpplint.py --extensions=c,cc,cpp,ino --headers=h,hpp {src,test,tools}/*.{h,c,cc,cpp,hpp,ino} examples/*/*.{h,c,cc,cpp,hpp,ino} + - pylint {src,test,tools}/*.py - shopt -u nullglob # Build and run the unit tests. - (cd test; make run) + - (cd tools; make run_tests) # Check the version numbers match. - LIB_VERSION=$(egrep "^#define\s+_IRREMOTEESP8266_VERSION_\s+" src/IRremoteESP8266.h | cut -d\" -f2) - test ${LIB_VERSION} == "$(jq -r .version library.json)" diff --git a/tools/pylintrc b/pylintrc similarity index 100% rename from tools/pylintrc rename to pylintrc diff --git a/test/Makefile b/test/Makefile index 259683bf9..929432efc 100644 --- a/test/Makefile +++ b/test/Makefile @@ -60,6 +60,8 @@ run : all echo "PASS: \o/ \o/ All unit tests passed. \o/ \o/"; \ fi +run_tests : run + install-googletest : git clone https://github.com/google/googletest.git ../lib/googletest diff --git a/tools/Makefile b/tools/Makefile index 696748bf4..9253fe05b 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -24,8 +24,20 @@ CXXFLAGS += -g -Wall -Wextra -pthread all : gc_decode +run_tests : all + failed=""; \ + for py_unittest in *_test.py; do \ + echo "RUNNING: $${py_unittest}"; \ + python ./$${py_unittest} || failed="$${failed} $${py_unittest}"; \ + done; \ + if [ -n "$${failed}" ]; then \ + echo "FAIL: :-( :-( Unit test(s)$${failed} failed! :-( :-("; exit 1; \ + else \ + echo "PASS: \o/ \o/ All unit tests passed. \o/ \o/"; \ + fi + clean : - rm -f *.o gc_decode + rm -f *.o *.pyc gc_decode # All the IR protocol object files. diff --git a/tools/auto_analyse_raw_data.py b/tools/auto_analyse_raw_data.py index e51a3b921..522586de6 100755 --- a/tools/auto_analyse_raw_data.py +++ b/tools/auto_analyse_raw_data.py @@ -8,12 +8,12 @@ import sys -class Defs(object): - """House the parameters for a message.""" +class RawIRMessage(object): + """Basic analyse functions & structure for raw IR messages.""" # pylint: disable=too-many-instance-attributes - def __init__(self, margin): + def __init__(self, margin, timings, output=sys.stdout, verbose=True): self.hdr_mark = None self.hdr_space = None self.bit_mark = None @@ -23,20 +23,27 @@ def __init__(self, margin): self.margin = margin self.marks = [] self.spaces = [] - - def process_data(self, data): - """Determine the values from the given data.""" + self.output = output + self.verbose = verbose + if len(timings) <= 3: + raise ValueError("Too few message timings supplied.") + self.timings = timings + self._generate_timing_candidates() + self._calc_values() + + def _generate_timing_candidates(self): + """Determine the likely values from the given data.""" count = 0 - for usecs in data: + for usecs in self.timings: count = count + 1 - if is_odd(count): + if count % 2: self.marks.append(usecs) else: self.spaces.append(usecs) - self.marks = self.reduce_list(self.marks) - self.spaces = self.reduce_list(self.spaces) + self.marks = self._reduce_list(self.marks) + self.spaces = self._reduce_list(self.spaces) - def reduce_list(self, items): + def _reduce_list(self, items): """Reduce the list of numbers into buckets that are atleast margin apart.""" result = [] last = -1 @@ -46,132 +53,183 @@ def reduce_list(self, items): last = item return result - -def usec_compare(seen, expected, margin): - """Compare two usec values and see if they match within a - subtrative margin.""" - return seen <= expected and seen > expected - margin - - -def usec_compares(usecs, expecteds, margin): - """Compare a usec value to a list of values and return True - if they are within a subtractive margin.""" - for expected in expecteds: - if usec_compare(usecs, expected, margin): - return True - return False - - -def add_bit(so_far, bit): + def _usec_compare(self, seen, expected): + """Compare two usec values and see if they match within a + subtrative margin.""" + return seen <= expected and seen > expected - self.margin + + def _usec_compares(self, usecs, expecteds): + """Compare a usec value to a list of values and return True + if they are within a subtractive margin.""" + for expected in expecteds: + if self._usec_compare(usecs, expected): + return True + return False + + def display_binary(self, binary_str): + """Display common representations of the suppied binary string.""" + num = int(binary_str, 2) + bits = len(binary_str) + rev_binary_str = binary_str[::-1] + rev_num = int(rev_binary_str, 2) + self.output.write("\n Bits: %d\n" + " Hex: %s (MSB first)\n" + " %s (LSB first)\n" + " Dec: %s (MSB first)\n" + " %s (LSB first)\n" + " Bin: 0b%s (MSB first)\n" + " 0b%s (LSB first)\n" % + (bits, "0x{0:0{1}X}".format(num, bits / 4), + "0x{0:0{1}X}".format(rev_num, bits / 4), num, rev_num, + binary_str, rev_binary_str)) + + def add_data_code(self, bin_str): + """Add the common "data" sequence of code to send the bulk of a message.""" + # pylint: disable=no-self-use + code = [] + code.append(" // Data") + code.append(" // e.g. data = 0x%X, nbits = %d" % (int(bin_str, 2), + len(bin_str))) + code.append(" sendData(BIT_MARK, ONE_SPACE, BIT_MARK, ZERO_SPACE, data, " + "nbits, true);") + code.append(" // Footer") + code.append(" mark(BIT_MARK);") + return code + + def _calc_values(self): + """Calculate the values which describe the standard timings + for the protocol.""" + if self.verbose: + self.output.write("Potential Mark Candidates:\n" + "%s\n" + "Potential Space Candidates:\n" + "%s\n" % (str(self.marks), str(self.spaces))) + # Largest mark is likely the HDR_MARK + self.hdr_mark = self.marks[0] + # The bit mark is likely to be the smallest mark. + self.bit_mark = self.marks[-1] + + if self.is_space_encoded() and len(self.spaces) >= 3: + if self.verbose and len(self.marks) > 2: + self.output.write("DANGER: Unexpected and unused mark timings!") + # We should have 3 space candidates at least. + # They should be: zero_space (smallest), one_space, & hdr_space (largest) + spaces = list(self.spaces) + self.zero_space = spaces.pop() + self.one_space = spaces.pop() + self.hdr_space = spaces.pop() + # Rest are probably message gaps + self.gaps = spaces + + def is_space_encoded(self): + """Make an educated guess if the message is space encoded.""" + return len(self.spaces) > len(self.marks) + + def is_hdr_mark(self, usec): + """Is usec the header mark?""" + return self._usec_compare(usec, self.hdr_mark) + + def is_hdr_space(self, usec): + """Is usec the header space?""" + return self._usec_compare(usec, self.hdr_space) + + def is_bit_mark(self, usec): + """Is usec the bit mark?""" + return self._usec_compare(usec, self.bit_mark) + + def is_one_space(self, usec): + """Is usec the one space?""" + return self._usec_compare(usec, self.one_space) + + def is_zero_space(self, usec): + """Is usec the zero_space?""" + return self._usec_compare(usec, self.zero_space) + + def is_gap(self, usec): + """Is usec the a space gap?""" + return self._usec_compares(usec, self.gaps) + + +def add_bit(so_far, bit, output=sys.stdout): """Add a bit to the end of the bits collected so far. """ if bit == "reset": return "" - sys.stdout.write(str(bit)) # This effectively displays in LSB first order. + output.write(str(bit)) # This effectively displays in LSB first order. return so_far + str(bit) # Storing it in MSB first order. -def is_odd(num): - """Is the num odd? i.e. Not even.""" - return num % 2 - - -def display_bin_value(binary_str, output=sys.stdout): - """Display common representations of the suppied binary string.""" - num = int(binary_str, 2) - bits = len(binary_str) - rev_binary_str = binary_str[::-1] - rev_num = int(rev_binary_str, 2) - output.write("\n Bits: %d\n" % bits) - output.write(" Hex: %s (MSB first)\n" % "0x{0:0{1}X}".format(num, bits / 4)) - output.write( - " %s (LSB first)\n" % "0x{0:0{1}X}".format(rev_num, bits / 4)) - output.write(" Dec: %s (MSB first)\n" % num) - output.write(" %s (LSB first)\n" % rev_num) - output.write(" Bin: 0b%s (MSB first)\n" % binary_str) - output.write(" 0b%s (LSB first)\n" % rev_binary_str) - - -def add_data_code(bin_str): - """Add the common "data" sequence of code to send the bulk of a message.""" - code = [] - code.append(" // Data") - code.append(" // e.g. data = 0x%X, nbits = %d" % (int(bin_str, 2), - len(bin_str))) - code.append(" sendData(BIT_MARK, ONE_SPACE, BIT_MARK, ZERO_SPACE, data, " - "nbits, true);") - code.append(" // Footer") - code.append(" mark(BIT_MARK);") - return code - - def convert_rawdata(data_str): """Parse a C++ rawdata declaration into a list of values.""" start = data_str.find('{') end = data_str.find('}') + if end == -1: + end = len(data_str) + if start > end: + raise ValueError("Raw Data not parsible due to parentheses placement.") data_str = data_str[start + 1:end] - return [int(x.strip()) for x in data_str.split(',')] + results = [] + for timing in [x.strip() for x in data_str.split(',')]: + try: + results.append(int(timing)) + except ValueError: + raise ValueError( + "Raw Data contains a non-numeric value of '%s'." % timing) + return results + + +def dump_constants(message, defines, output=sys.stdout): + """Dump the key constants and generate the C++ #defines.""" + output.write("Guessing key value:\n" + "HDR_MARK = %d\n" + "HDR_SPACE = %d\n" + "BIT_MARK = %d\n" + "ONE_SPACE = %d\n" + "ZERO_SPACE = %d\n" % + (message.hdr_mark, message.hdr_space, message.bit_mark, + message.one_space, message.zero_space)) + defines.append("#define HDR_MARK %dU" % message.hdr_mark) + defines.append("#define BIT_MARK %dU" % message.bit_mark) + defines.append("#define HDR_SPACE %dU" % message.hdr_space) + defines.append("#define ONE_SPACE %dU" % message.one_space) + defines.append("#define ZERO_SPACE %dU" % message.zero_space) + + if len(message.gaps) == 1: + output.write("SPACE_GAP = %d\n" % message.gaps[0]) + defines.append("#define SPACE_GAP = %dU" % message.gaps[0]) + else: + count = 0 + for gap in message.gaps: + # We probably (still) have a gap in the protocol. + count = count + 1 + output.write("SPACE_GAP%d = %d\n" % (count, gap)) + defines.append("#define SPACE_GAP%d = %dU" % (count, gap)) def parse_and_report(rawdata_str, margin, gen_code=False, output=sys.stdout): """Analyse the rawdata c++ definition of a IR message.""" - code_defs = [] - code_64bit = [] - defs = Defs(margin) + defines = [] + function_code = [] # Parse the input. rawdata = convert_rawdata(rawdata_str) - defs.process_data(rawdata) - output.write("Potential Mark Candidates:\n" - "%s\n" - "Potential Space Candidates:\n" - "%s\n" % (str(defs.marks), str(defs.spaces))) - output.write("\n\nGuessing encoding type:\n") - if len(defs.spaces) > len(defs.marks): - output.write("Looks like it uses space encoding. Yay!\n\n" - "Guessing key value:\n") + output.write("Found %d timing entries.\n" % len(rawdata)) - # Largest mark is likely the HDR_MARK - defs.hdr_mark = defs.marks[0] - output.write("HDR_MARK = %d\n" % defs.hdr_mark) - code_defs.append("#define HDR_MARK %dU" % defs.hdr_mark) - # The mark bit is likely to be the smallest. - defs.bit_mark = defs.marks[-1] - output.write("BIT_MARK = %d\n" % defs.bit_mark) - code_defs.append("#define BIT_MARK %dU" % defs.bit_mark) - - gap = 0 - defs.gaps = [] - while len(defs.spaces) > 3: - # We probably (still) have a gap in the protocol. - gap = gap + 1 - space = defs.spaces.remove() - defs.gaps.append(space) - output.write("SPACE_GAP%d = %d\n" % (gap, space)) - code_defs.append("#define SPACE_GAP%d = %dU" % (gap, space)) - - # We should have 3 space candidates left. - # They should be zero_space (smallest), one_space, & hdr_space (largest) - defs.zero_space = defs.spaces.pop() - defs.one_space = defs.spaces.pop() - defs.hdr_space = defs.spaces.pop() - code_defs.append("#define HDR_SPACE %dU" % defs.hdr_space) - code_defs.append("#define ONE_SPACE %dU" % defs.one_space) - code_defs.append("#define ZERO_SPACE %dU" % defs.zero_space) - output.write("HDR_SPACE = %d\n" - "ONE_SPACE = %d\n" - "ZERO_SPACE = %d\n" % (defs.hdr_space, defs.one_space, - defs.zero_space)) + message = RawIRMessage(margin, rawdata, output) + output.write("\nGuessing encoding type:\n") + if message.is_space_encoded(): + output.write("Looks like it uses space encoding. Yay!\n\n") + dump_constants(message, defines, output) else: output.write("Sorry, it looks like it is Mark encoded. " "I can't do that yet. Exiting.\n") sys.exit(1) - total_bits = decode_data(rawdata, defs, code_defs, code_64bit, output) + total_bits = decode_data(message, defines, function_code, output) if gen_code: - generate_code(code_defs, code_64bit, total_bits, output) + generate_irsend_code(defines, function_code, total_bits, output) -def decode_data(data, defs, code_defs, code_64bit, output=sys.stdout): +def decode_data(message, defines, function_code, output=sys.stdout): """Decode the data sequence with the given values in mind.""" # pylint: disable=too-many-branches,too-many-statements @@ -179,12 +237,12 @@ def decode_data(data, defs, code_defs, code_64bit, output=sys.stdout): # sequence and break it up and indicate accordingly. output.write("\nDecoding protocol based on analysis so far:\n\n") - last = "" + state = "" count = 1 total_bits = "" binary_value = add_bit("", "reset") - code_64bit.extend([ + function_code.extend([ "// Function should be safe up to 64 bits.", "void IRsend::sendXYZ(const uint64_t data, const uint16_t" " nbits, const uint16_t repeat) {", @@ -192,78 +250,77 @@ def decode_data(data, defs, code_defs, code_64bit, output=sys.stdout): " for (uint16_t r = 0; r <= repeat; r++) {" ]) - for usecs in data: - if (usec_compare(usecs, defs.hdr_mark, defs.margin) and is_odd(count) and - not usec_compare(usecs, defs.bit_mark, defs.margin)): - last = "HM" + for usec in message.timings: + if (message.is_hdr_mark(usec) and count % 2 and + not message.is_bit_mark(usec)): + state = "HM" if binary_value: - display_bin_value(binary_value) + message.display_binary(binary_value) total_bits = total_bits + binary_value - output.write(last) + output.write(state) binary_value = add_bit(binary_value, "reset") output.write("HDR_MARK+") - code_64bit.extend([" // Header", " mark(HDR_MARK);"]) - elif (usec_compare(usecs, defs.hdr_space, defs.margin) and - not usec_compare(usecs, defs.one_space, defs.margin)): - if last != "HM": + function_code.extend([" // Header", " mark(HDR_MARK);"]) + elif (message.is_hdr_space(usec) and not message.is_one_space(usec)): + if state != "HM": if binary_value: - display_bin_value(binary_value) + message.display_binary(binary_value) total_bits = total_bits + binary_value - code_64bit.extend(add_data_code(binary_value)) + function_code.extend(message.add_data_code(binary_value)) binary_value = add_bit(binary_value, "reset") output.write("UNEXPECTED->") - last = "HS" + state = "HS" output.write("HDR_SPACE+") - code_64bit.append(" space(HDR_SPACE);") - elif usec_compare(usecs, defs.bit_mark, defs.margin) and is_odd(count): - if last != "HS" and last != "BS": + function_code.append(" space(HDR_SPACE);") + elif message.is_bit_mark(usec) and count % 2: + if state != "HS" and state != "BS": output.write("BIT_MARK(UNEXPECTED)") - last = "BM" - elif usec_compare(usecs, defs.zero_space, defs.margin): - if last != "BM": + state = "BM" + elif message.is_zero_space(usec): + if state != "BM": output.write("ZERO_SPACE(UNEXPECTED)") - last = "BS" - binary_value = add_bit(binary_value, 0) - elif usec_compare(usecs, defs.one_space, defs.margin): - if last != "BM": + state = "BS" + binary_value = add_bit(binary_value, 0, output) + elif message.is_one_space(usec): + if state != "BM": output.write("ONE_SPACE(UNEXPECTED)") - last = "BS" - binary_value = add_bit(binary_value, 1) - elif usec_compares(usecs, defs.gaps, defs.margin): - if last != "BM": + state = "BS" + binary_value = add_bit(binary_value, 1, output) + elif message.is_gap(usec): + if state != "BM": output.write("UNEXPECTED->") - last = "GS" - output.write(" GAP(%d)" % usecs) - display_bin_value(binary_value) - code_64bit.extend(add_data_code(binary_value)) - code_64bit.append(" space(SPACE_GAP);") + state = "GS" + output.write(" GAP(%d)" % usec) + message.display_binary(binary_value) + function_code.extend(message.add_data_code(binary_value)) + function_code.append(" space(SPACE_GAP);") total_bits = total_bits + binary_value binary_value = add_bit(binary_value, "reset") else: - output.write("UNKNOWN(%d)" % usecs) - last = "UNK" + output.write("UNKNOWN(%d)" % usec) + state = "UNK" count = count + 1 - display_bin_value(binary_value) - code_64bit.extend(add_data_code(binary_value)) - code_64bit.extend([ + message.display_binary(binary_value) + function_code.extend(message.add_data_code(binary_value)) + function_code.extend([ " space(100000); // A 100% made up guess of the gap" " between messages.", " }", "}" ]) total_bits = total_bits + binary_value output.write("Total Nr. of suspected bits: %d\n" % len(total_bits)) - code_defs.append("#define XYZ_BITS %dU" % len(total_bits)) + defines.append("#define XYZ_BITS %dU" % len(total_bits)) if len(total_bits) > 64: - code_defs.append("#define XYZ_STATE_LENGTH %dU" % (len(total_bits) / 8)) + defines.append("#define XYZ_STATE_LENGTH %dU" % (len(total_bits) / 8)) return total_bits -def generate_code(defs, normal, bits_str, output=sys.stdout): +def generate_irsend_code(defines, normal, bits_str, output=sys.stdout): """Output the estimated C++ code to reproduce the IR message.""" output.write("\nGenerating a VERY rough code outline:\n\n" "// WARNING: This probably isn't directly usable." " It's a guide only.\n") - for line in defs: + for line in defines: output.write("%s\n" % line) if len(bits_str) > 64: # Will it fit in a uint64_t? @@ -297,49 +354,53 @@ def generate_code(defs, normal, bits_str, output=sys.stdout): for i in range(0, len(bits_str), 8))) -# Main program - -ARG_PARSER = argparse.ArgumentParser( - description="Read an IRremoteESP8266 rawData declaration and tries to " - "analyse it.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) -ARG_PARSER.add_argument( - "-g", - "--code", - action="store_true", - default=False, - dest="gen_code", - help="Generate a C++ code outline to aid making an IRsend function.") -ARG_GROUP = ARG_PARSER.add_mutually_exclusive_group(required=True) -ARG_GROUP.add_argument( - "rawdata", - help="A rawData line from IRrecvDumpV2. e.g. 'uint16_t rawbuf[37] = {7930, " - "3952, 494, 1482, 520, 1482, 494, 1508, 494, 520, 494, 1482, 494, 520," - " 494, 1482, 494, 1482, 494, 3978, 494, 520, 494, 520, 494, 520, 494, " - "520, 520, 520, 494, 520, 494, 520, 494, 520, 494};'", - nargs="?") -ARG_GROUP.add_argument( - "-f", "--file", help="Read in a rawData line from the file.") -ARG_PARSER.add_argument( - "-r", - "--range", - type=int, - help="Max number of micro-seconds difference between values to consider it " - "the same value.", - dest="margin", - default=200) -ARG_GROUP.add_argument( - "--stdin", - help="Read in a rawData line from STDIN.", - action="store_true", - default=False) -ARG_OPTIONS = ARG_PARSER.parse_args() - -if ARG_OPTIONS.stdin: - DATA = sys.stdin.read() -elif ARG_OPTIONS.file: - with open(ARG_OPTIONS.file) as f: - DATA = f.read() -else: - DATA = ARG_OPTIONS.rawdata -parse_and_report(DATA, ARG_OPTIONS.margin, ARG_OPTIONS.gen_code) +def main(): + """Parse the commandline arguments and call the method.""" + arg_parser = argparse.ArgumentParser( + description="Read an IRremoteESP8266 rawData declaration and tries to " + "analyse it.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + arg_parser.add_argument( + "-g", + "--code", + action="store_true", + default=False, + dest="gen_code", + help="Generate a C++ code outline to aid making an IRsend function.") + arg_group = arg_parser.add_mutually_exclusive_group(required=True) + arg_group.add_argument( + "rawdata", + help="A rawData line from IRrecvDumpV2. e.g. 'uint16_t rawbuf[37] = {" + "7930, 3952, 494, 1482, 520, 1482, 494, 1508, 494, 520, 494, 1482, 494, " + "520, 494, 1482, 494, 1482, 494, 3978, 494, 520, 494, 520, 494, 520, " + "494, 520, 520, 520, 494, 520, 494, 520, 494, 520, 494};'", + nargs="?") + arg_group.add_argument( + "-f", "--file", help="Read in a rawData line from the file.") + arg_parser.add_argument( + "-r", + "--range", + type=int, + help="Max number of micro-seconds difference between values to consider" + " it the same value.", + dest="margin", + default=200) + arg_group.add_argument( + "--stdin", + help="Read in a rawData line from STDIN.", + action="store_true", + default=False) + arg_options = arg_parser.parse_args() + + if arg_options.stdin: + data = sys.stdin.read() + elif arg_options.file: + with open(arg_options.file) as input_file: + data = input_file.read() + else: + data = arg_options.rawdata + parse_and_report(data, arg_options.margin, arg_options.gen_code) + + +if __name__ == '__main__': + main() diff --git a/tools/auto_analyse_raw_data_test.py b/tools/auto_analyse_raw_data_test.py new file mode 100755 index 000000000..ac84cab22 --- /dev/null +++ b/tools/auto_analyse_raw_data_test.py @@ -0,0 +1,288 @@ +#!/usr/bin/python +"""Unit tests for auto_analyse_raw_data.py""" +import StringIO +import unittest +import auto_analyse_raw_data as analyse + + +class TestRawIRMessage(unittest.TestCase): + """Unit tests for the RawIRMessage class.""" + + def test_display_binary(self): + """Test the display_binary() method.""" + output = StringIO.StringIO() + message = analyse.RawIRMessage(100, [8000, 4000, 500, 500, 500], output, + False) + self.assertEqual(output.getvalue(), '') + message.display_binary("10101010") + message.display_binary("0000000000000000") + message.display_binary("00010010001101000101011001111000") + self.assertEqual(output.getvalue(), '\n' + ' Bits: 8\n' + ' Hex: 0xAA (MSB first)\n' + ' 0x55 (LSB first)\n' + ' Dec: 170 (MSB first)\n' + ' 85 (LSB first)\n' + ' Bin: 0b10101010 (MSB first)\n' + ' 0b01010101 (LSB first)\n' + '\n' + ' Bits: 16\n' + ' Hex: 0x0000 (MSB first)\n' + ' 0x0000 (LSB first)\n' + ' Dec: 0 (MSB first)\n' + ' 0 (LSB first)\n' + ' Bin: 0b0000000000000000 (MSB first)\n' + ' 0b0000000000000000 (LSB first)\n' + '\n' + ' Bits: 32\n' + ' Hex: 0x12345678 (MSB first)\n' + ' 0x1E6A2C48 (LSB first)\n' + ' Dec: 305419896 (MSB first)\n' + ' 510274632 (LSB first)\n' + ' Bin: 0b00010010001101000101011001111000 (MSB first)\n' + ' 0b00011110011010100010110001001000 (LSB first)\n') + + +class TestAutoAnalyseRawData(unittest.TestCase): + """Unit tests for the functions in AutoAnalyseRawData.""" + + def test_dump_constants_simple(self): + """Simple tests for the dump_constants() function.""" + ignore = StringIO.StringIO() + output = StringIO.StringIO() + defs = [] + message = analyse.RawIRMessage(200, [ + 7930, 3952, 494, 1482, 520, 1482, 494, 1508, 494, 520, 494, 1482, 494, + 520, 494, 1482, 494, 1482, 494, 3978, 494, 520, 494, 520, 494, 520, 494, + 520, 520, 520, 494, 520, 494, 520, 494, 1482, 494 + ], ignore) + analyse.dump_constants(message, defs, output) + self.assertEqual(defs, [ + '#define HDR_MARK 7930U', '#define BIT_MARK 520U', + '#define HDR_SPACE 3978U', '#define ONE_SPACE 1508U', + '#define ZERO_SPACE 520U' + ]) + self.assertEqual(output.getvalue(), 'Guessing key value:\n' + 'HDR_MARK = 7930\n' + 'HDR_SPACE = 3978\n' + 'BIT_MARK = 520\n' + 'ONE_SPACE = 1508\n' + 'ZERO_SPACE = 520\n') + + def test_dump_constants_aircon(self): + """More complex tests for the dump_constants() function.""" + ignore = StringIO.StringIO() + output = StringIO.StringIO() + defs = [] + message = analyse.RawIRMessage(200, [ + 9008, 4496, 644, 1660, 676, 530, 648, 558, 672, 1636, 646, 1660, 644, + 556, 650, 584, 626, 560, 644, 580, 628, 1680, 624, 560, 648, 1662, 644, + 582, 648, 536, 674, 530, 646, 580, 628, 560, 670, 532, 646, 562, 644, + 556, 672, 536, 648, 1662, 646, 1660, 652, 554, 644, 558, 672, 538, 644, + 560, 668, 560, 648, 1638, 668, 536, 644, 1660, 668, 532, 648, 560, 648, + 1660, 674, 554, 622, 19990, 646, 580, 624, 1660, 648, 556, 648, 558, + 674, 556, 622, 560, 644, 564, 668, 536, 646, 1662, 646, 1658, 672, 534, + 648, 558, 644, 562, 648, 1662, 644, 584, 622, 558, 648, 562, 668, 534, + 670, 536, 670, 532, 672, 536, 646, 560, 646, 558, 648, 558, 670, 534, + 650, 558, 646, 560, 646, 560, 668, 1638, 646, 1662, 646, 1660, 646, + 1660, 648 + ], ignore) + analyse.dump_constants(message, defs, output) + self.assertEqual(defs, [ + '#define HDR_MARK 9008U', '#define BIT_MARK 676U', + '#define HDR_SPACE 4496U', '#define ONE_SPACE 1680U', + '#define ZERO_SPACE 584U', '#define SPACE_GAP = 19990U' + ]) + self.assertEqual(output.getvalue(), 'Guessing key value:\n' + 'HDR_MARK = 9008\n' + 'HDR_SPACE = 4496\n' + 'BIT_MARK = 676\n' + 'ONE_SPACE = 1680\n' + 'ZERO_SPACE = 584\n' + 'SPACE_GAP = 19990\n') + + def test_convert_rawdata(self): + """Tests for the convert_rawdata() function.""" + # trivial cases + self.assertEqual(analyse.convert_rawdata("0"), [0]) + with self.assertRaises(ValueError) as context: + analyse.convert_rawdata("") + self.assertEqual(context.exception.message, + "Raw Data contains a non-numeric value of ''.") + + # Single parenthesis + self.assertEqual(analyse.convert_rawdata("foo {10"), [10]) + self.assertEqual(analyse.convert_rawdata("20} bar"), [20]) + + # No parentheses + self.assertEqual(analyse.convert_rawdata("10,20 , 30"), [10, 20, 30]) + + # Dual parentheses + self.assertEqual(analyse.convert_rawdata("{10,20 , 30}"), [10, 20, 30]) + self.assertEqual(analyse.convert_rawdata("foo{10,20}bar"), [10, 20]) + + # Many parentheses + self.assertEqual(analyse.convert_rawdata("foo{10,20}{bar}"), [10, 20]) + self.assertEqual(analyse.convert_rawdata("foo{10,20}{bar}}{"), [10, 20]) + + # Bad parentheses + with self.assertRaises(ValueError) as context: + analyse.convert_rawdata("}10{") + self.assertEqual(context.exception.message, + "Raw Data not parsible due to parentheses placement.") + + # Non base-10 values + with self.assertRaises(ValueError) as context: + analyse.convert_rawdata("10, 20, foo, bar, 30") + self.assertEqual(context.exception.message, + "Raw Data contains a non-numeric value of 'foo'.") + + # A messy usual "good" case. + input_str = """uint16_t rawbuf[6] = { + 9008, 4496, 644, + 1660, 676, + + 530} + ;""" + self.assertEqual( + analyse.convert_rawdata(input_str), [9008, 4496, 644, 1660, 676, 530]) + + def test_parse_and_report(self): + """Tests for the parse_and_report() function.""" + + # Without code generation. + output = StringIO.StringIO() + input_str = """ + uint16_t rawbuf[139] = {9008, 4496, 644, 1660, 676, 530, 648, 558, 672, + 1636, 646, 1660, 644, 556, 650, 584, 626, 560, 644, 580, 628, 1680, + 624, 560, 648, 1662, 644, 582, 648, 536, 674, 530, 646, 580, 628, + 560, 670, 532, 646, 562, 644, 556, 672, 536, 648, 1662, 646, 1660, + 652, 554, 644, 558, 672, 538, 644, 560, 668, 560, 648, 1638, 668, + 536, 644, 1660, 668, 532, 648, 560, 648, 1660, 674, 554, 622, 19990, + 646, 580, 624, 1660, 648, 556, 648, 558, 674, 556, 622, 560, 644, + 564, 668, 536, 646, 1662, 646, 1658, 672, 534, 648, 558, 644, 562, + 648, 1662, 644, 584, 622, 558, 648, 562, 668, 534, 670, 536, 670, + 532, 672, 536, 646, 560, 646, 558, 648, 558, 670, 534, 650, 558, + 646, 560, 646, 560, 668, 1638, 646, 1662, 646, 1660, 646, 1660, + 648};""" + analyse.parse_and_report(input_str, 200, False, output) + self.assertEqual( + output.getvalue(), 'Found 139 timing entries.\n' + 'Potential Mark Candidates:\n' + '[9008, 676]\n' + 'Potential Space Candidates:\n' + '[19990, 4496, 1680, 584]\n' + '\n' + 'Guessing encoding type:\n' + 'Looks like it uses space encoding. Yay!\n' + '\n' + 'Guessing key value:\n' + 'HDR_MARK = 9008\n' + 'HDR_SPACE = 4496\n' + 'BIT_MARK = 676\n' + 'ONE_SPACE = 1680\n' + 'ZERO_SPACE = 584\n' + 'SPACE_GAP = 19990\n' + '\n' + 'Decoding protocol based on analysis so far:\n' + '\n' + 'HDR_MARK+HDR_SPACE+10011000010100000000011000001010010 GAP(19990)\n' + ' Bits: 35\n' + ' Hex: 0x4C2803052 (MSB first)\n' + ' 0x250600A19 (LSB first)\n' + ' Dec: 20443050066 (MSB first)\n' + ' 9938405913 (LSB first)\n' + ' Bin: 0b10011000010100000000011000001010010 (MSB first)\n' + ' 0b01001010000011000000000101000011001 (LSB first)\n' + 'BIT_MARK(UNEXPECTED)01000000110001000000000000001111\n' + ' Bits: 32\n' + ' Hex: 0x40C4000F (MSB first)\n' + ' 0xF0002302 (LSB first)\n' + ' Dec: 1086586895 (MSB first)\n' + ' 4026540802 (LSB first)\n' + ' Bin: 0b01000000110001000000000000001111 (MSB first)\n' + ' 0b11110000000000000010001100000010 (LSB first)\n' + 'Total Nr. of suspected bits: 67\n') + + # With code generation. + output = StringIO.StringIO() + input_str = """ + uint16_t rawbuf[37] = {7930, 3952, 494, 1482, 520, 1482, 494, + 1508, 494, 520, 494, 1482, 494, 520, 494, 1482, 494, 1482, 494, + 3978, 494, 520, 494, 520, 494, 520, 494, 520, 520, 520, 494, 520, + 494, 520, 494, 1482, 494};""" + analyse.parse_and_report(input_str, 200, True, output) + self.assertEqual( + output.getvalue(), 'Found 37 timing entries.\n' + 'Potential Mark Candidates:\n' + '[7930, 520]\n' + 'Potential Space Candidates:\n' + '[3978, 1508, 520]\n' + '\n' + 'Guessing encoding type:\n' + 'Looks like it uses space encoding. Yay!\n' + '\n' + 'Guessing key value:\n' + 'HDR_MARK = 7930\n' + 'HDR_SPACE = 3978\n' + 'BIT_MARK = 520\n' + 'ONE_SPACE = 1508\n' + 'ZERO_SPACE = 520\n' + '\n' + 'Decoding protocol based on analysis so far:\n' + '\n' + 'HDR_MARK+HDR_SPACE+11101011\n' + ' Bits: 8\n' + ' Hex: 0xEB (MSB first)\n' + ' 0xD7 (LSB first)\n' + ' Dec: 235 (MSB first)\n' + ' 215 (LSB first)\n' + ' Bin: 0b11101011 (MSB first)\n' + ' 0b11010111 (LSB first)\n' + 'UNEXPECTED->HDR_SPACE+00000001\n' + ' Bits: 8\n Hex: 0x01 (MSB first)\n' + ' 0x80 (LSB first)\n' + ' Dec: 1 (MSB first)\n' + ' 128 (LSB first)\n' + ' Bin: 0b00000001 (MSB first)\n' + ' 0b10000000 (LSB first)\n' + 'Total Nr. of suspected bits: 16\n' + '\n' + 'Generating a VERY rough code outline:\n' + '\n' + "// WARNING: This probably isn't directly usable. It's a guide only.\n" + '#define HDR_MARK 7930U\n' + '#define BIT_MARK 520U\n' + '#define HDR_SPACE 3978U\n' + '#define ONE_SPACE 1508U\n' + '#define ZERO_SPACE 520U\n' + '#define XYZ_BITS 16U\n' + '// Function should be safe up to 64 bits.\n' + 'void IRsend::sendXYZ(const uint64_t data, const uint16_t nbits,' + ' const uint16_t repeat) {\n' + ' enableIROut(38); // A guess. Most common frequency.\n' + ' for (uint16_t r = 0; r <= repeat; r++) {\n' + ' // Header\n' + ' mark(HDR_MARK);\n' + ' space(HDR_SPACE);\n' + ' // Data\n' + ' // e.g. data = 0xEB, nbits = 8\n' + ' sendData(BIT_MARK, ONE_SPACE, BIT_MARK, ZERO_SPACE, data, nbits,' + ' true);\n' + ' // Footer\n' + ' mark(BIT_MARK);\n' + ' space(HDR_SPACE);\n' + ' // Data\n' + ' // e.g. data = 0x1, nbits = 8\n' + ' sendData(BIT_MARK, ONE_SPACE, BIT_MARK, ZERO_SPACE, data, nbits,' + ' true);\n' + ' // Footer\n' + ' mark(BIT_MARK);\n' + ' space(100000); // A 100% made up guess of the gap between' + ' messages.\n' + ' }\n' + '}\n') + + +if __name__ == '__main__': + unittest.main(verbosity=2)