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/.style.yapf b/.style.yapf new file mode 100644 index 000000000..65fa0ee33 --- /dev/null +++ b/.style.yapf @@ -0,0 +1,3 @@ +[style] +based_on_style: google +indent_width: 2 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/pylintrc b/pylintrc new file mode 100644 index 000000000..987c6abf9 --- /dev/null +++ b/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=' ' 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/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 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 new file mode 100755 index 000000000..522586de6 --- /dev/null +++ b/tools/auto_analyse_raw_data.py @@ -0,0 +1,406 @@ +#!/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 RawIRMessage(object): + """Basic analyse functions & structure for raw IR messages.""" + + # pylint: disable=too-many-instance-attributes + + def __init__(self, margin, timings, output=sys.stdout, verbose=True): + 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 = [] + 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 self.timings: + count = count + 1 + 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) + + 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(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 "" + output.write(str(bit)) # This effectively displays in LSB first order. + return so_far + str(bit) # Storing it in MSB first order. + + +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] + 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.""" + defines = [] + function_code = [] + + # Parse the input. + rawdata = convert_rawdata(rawdata_str) + + output.write("Found %d timing entries.\n" % len(rawdata)) + + 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(message, defines, function_code, output) + if gen_code: + generate_irsend_code(defines, function_code, total_bits, output) + + +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 + + # 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") + state = "" + count = 1 + total_bits = "" + binary_value = add_bit("", "reset") + + 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) {", + " enableIROut(38); // A guess. Most common frequency.", + " for (uint16_t r = 0; r <= repeat; r++) {" + ]) + + 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: + message.display_binary(binary_value) + total_bits = total_bits + binary_value + output.write(state) + binary_value = add_bit(binary_value, "reset") + output.write("HDR_MARK+") + 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: + message.display_binary(binary_value) + total_bits = total_bits + binary_value + function_code.extend(message.add_data_code(binary_value)) + binary_value = add_bit(binary_value, "reset") + output.write("UNEXPECTED->") + state = "HS" + output.write("HDR_SPACE+") + 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)") + state = "BM" + elif message.is_zero_space(usec): + if state != "BM": + output.write("ZERO_SPACE(UNEXPECTED)") + state = "BS" + binary_value = add_bit(binary_value, 0, output) + elif message.is_one_space(usec): + if state != "BM": + output.write("ONE_SPACE(UNEXPECTED)") + state = "BS" + binary_value = add_bit(binary_value, 1, output) + elif message.is_gap(usec): + if state != "BM": + output.write("UNEXPECTED->") + 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)" % usec) + state = "UNK" + count = count + 1 + 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)) + defines.append("#define XYZ_BITS %dU" % len(total_bits)) + if len(total_bits) > 64: + defines.append("#define XYZ_STATE_LENGTH %dU" % (len(total_bits) / 8)) + return total_bits + + +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 defines: + 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))) + + +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)