Convert AutoAnalyseRawData script to Python (#454)

* Port AutoAnalyseRawData script to Python.
* Rename python analysis script to snake_case format.
* Add options to read the data from stdin or from a file.
* Remove old analyse script.
* 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
This commit is contained in:
David Conran
2018-05-18 13:28:01 +10:00
committed by GitHub
parent 7c06012e37
commit 531dc66033
9 changed files with 730 additions and 389 deletions

3
.gitignore vendored
View File

@@ -23,6 +23,9 @@ lib/googletest/**/*
# GCC pre-compiled headers. # GCC pre-compiled headers.
**/*.gch **/*.gch
# Python compiled files
**/*.pyc
# Unit Test builds # Unit Test builds
test/*.o test/*.o
test/*.a test/*.a

3
.style.yapf Normal file
View File

@@ -0,0 +1,3 @@
[style]
based_on_style: google
indent_width: 2

View File

@@ -20,6 +20,7 @@ install:
- arduino --board $BD --save-prefs - arduino --board $BD --save-prefs
- arduino --pref "compiler.warning_level=all" --save-prefs - arduino --pref "compiler.warning_level=all" --save-prefs
- sudo apt-get install jq - sudo apt-get install jq
- sudo pip install pylint
script: script:
# Check that everything compiles. # Check that everything compiles.
- arduino --verify --board $BD $PWD/examples/IRrecvDemo/IRrecvDemo.ino - arduino --verify --board $BD $PWD/examples/IRrecvDemo/IRrecvDemo.ino
@@ -44,9 +45,11 @@ script:
# Check for lint issues. # Check for lint issues.
- shopt -s nullglob - 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} - 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 - shopt -u nullglob
# Build and run the unit tests. # Build and run the unit tests.
- (cd test; make run) - (cd test; make run)
- (cd tools; make run_tests)
# Check the version numbers match. # Check the version numbers match.
- LIB_VERSION=$(egrep "^#define\s+_IRREMOTEESP8266_VERSION_\s+" src/IRremoteESP8266.h | cut -d\" -f2) - LIB_VERSION=$(egrep "^#define\s+_IRREMOTEESP8266_VERSION_\s+" src/IRremoteESP8266.h | cut -d\" -f2)
- test ${LIB_VERSION} == "$(jq -r .version library.json)" - test ${LIB_VERSION} == "$(jq -r .version library.json)"

12
pylintrc Normal file
View File

@@ -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=' '

View File

@@ -60,6 +60,8 @@ run : all
echo "PASS: \o/ \o/ All unit tests passed. \o/ \o/"; \ echo "PASS: \o/ \o/ All unit tests passed. \o/ \o/"; \
fi fi
run_tests : run
install-googletest : install-googletest :
git clone https://github.com/google/googletest.git ../lib/googletest git clone https://github.com/google/googletest.git ../lib/googletest

View File

@@ -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

View File

@@ -24,8 +24,20 @@ CXXFLAGS += -g -Wall -Wextra -pthread
all : gc_decode 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 : clean :
rm -f *.o gc_decode rm -f *.o *.pyc gc_decode
# All the IR protocol object files. # All the IR protocol object files.

406
tools/auto_analyse_raw_data.py Executable file
View File

@@ -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()

View File

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