diff --git a/README.md b/README.md index a1cd373..35efb8f 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ themselves that are having issues. Call for maintainers -------------------- Hi, -This project is in need of maintenance and I (kmpm) do not have the time the project deserves. -Look at https://github.com/kmpm/nodemcu-uploader/issues/90 for more information on what to do about it. +This project is in need of maintenance and I (kmpm) do not have the time the +project deserves. Look at https://github.com/kmpm/nodemcu-uploader/issues/90 +for more information on what to do about it or email peter@kmpm.se Installation @@ -52,9 +53,11 @@ python easy_install pyserial Usage ----- +Download NodeMCU firmware from http://nodemcu-build.com/ . + Since version v0.4.0 of the tool you will need a recent (june/july 2016) version of the firmware for nodemcu. The default baudrate was changed in firmware from -9600 to 115200 and this tool was changed as well. Download from http://nodemcu-build.com/ . +9600 to 115200 and this tool was changed as well. If you are using an older firmware you MUST use the option `--start-baud 9600` to the device to be recognized. Otherwise you will get a diff --git a/nodemcu_uploader/exceptions.py b/nodemcu_uploader/exceptions.py index e643862..5e2980b 100644 --- a/nodemcu_uploader/exceptions.py +++ b/nodemcu_uploader/exceptions.py @@ -33,4 +33,10 @@ class VerificationError(Exception): class PathLengthException(Exception): - pass \ No newline at end of file + pass + + +class ValidationException(Exception): + def __init__(self, message, key, value): + message = "Validation Exception. {key} was {message}. '{value}'".format(message=message, key=key, value=value) + super(ValidationException, self).__init__(message) diff --git a/nodemcu_uploader/luacode.py b/nodemcu_uploader/luacode.py index a85d4ea..859647e 100644 --- a/nodemcu_uploader/luacode.py +++ b/nodemcu_uploader/luacode.py @@ -4,6 +4,7 @@ # Copyright (C) 2015-2019 Peter Magnusson # pylint: disable=C0301 +# flake8: noqa LUA_FUNCTIONS = ['recv_block', 'recv_name', 'recv', 'shafile', 'send_block', 'send_file', 'send'] @@ -12,29 +13,27 @@ PRINT_FILE = "file.open('{filename}') print('---{filename}---') print(file.read()) file.close() print('---')" +INFO_GROUP = "for key,value in pairs(node.info('{group}')) do k=tostring(key) print(k .. string.rep(' ', 20 - #k), tostring(value)) end" + LIST_FILES = 'for key,value in pairs(file.list()) do print(key,value) end' -#NUL = \000, ACK = \006 +# NUL = \000, ACK = \006 RECV_LUA = \ r""" -function recv_block(d) - if string.byte(d, 1) == 1 then - size = string.byte(d, 2) - uart.write(0,'\006') - if size > 0 then - file.write(string.sub(d, 3, 3+size-1)) - else - file.close() - uart.on('data') +function recv() + local on,w,ack,nack=uart.on,uart.write,'\6','\21' + local fd + local function recv_block(d) + local t,l = d:byte(1,2) + if t ~= 1 then w(0, nack); fd:close(); return on('data') end + if l >= 0 then fd:write(d:sub(3, l+2)); end + if l == 0 then fd:close(); w(0, ack); return on('data') else w(0, ack) end end - else - uart.write(0, '\021' .. d) - uart.on('data') + local function recv_name(d) d = d:gsub('%z.*', '') file.remove(d) fd=file.open(d, 'w') on('data', 130, recv_block, 0) w(0, ack) end + on('data', '\0', recv_name, 0) + w(0, 'C') end -end -function recv_name(d) d = d:gsub('%z.*', '') file.remove(d) file.open(d, 'w') uart.on('data', 130, recv_block, 0) uart.write(0, '\006') end -function recv() uart.on('data', '\000', recv_name, 0) uart.write(0, 'C') end function shafile(f) print(crypto.toHex(crypto.fhash('sha1', f))) end -""" +""" # noqa: E122 SEND_LUA = \ r""" @@ -49,3 +48,7 @@ """ UART_SETUP = 'uart.setup(0,{baud},8,0,1,1)' + +REMOVE_ALL_FILES = r""" +for key,value in pairs(file.list()) do file.remove(key) end +""" diff --git a/nodemcu_uploader/main.py b/nodemcu_uploader/main.py index bdfe6e2..38e4d4a 100644 --- a/nodemcu_uploader/main.py +++ b/nodemcu_uploader/main.py @@ -8,14 +8,16 @@ import argparse import logging import os +import sys import glob from .uploader import Uploader from .term import terminal from serial import VERSION as serialversion +from .version import __version__ -log = logging.getLogger(__name__) # pylint: disable=C0103 -from .version import __version__ +log = logging.getLogger(__name__) # pylint: disable=C0103 + def destination_from_source(sources, use_glob=True): """ @@ -47,14 +49,18 @@ def destination_from_source(sources, use_glob=True): def operation_upload(uploader, sources, verify, do_compile, do_file, do_restart): """The upload operation""" + if not isinstance(sources, list): + sources = [sources] sources, destinations = destination_from_source(sources) if len(destinations) == len(sources): if uploader.prepare(): for filename, dst in zip(sources, destinations): if do_compile: uploader.file_remove(os.path.splitext(dst)[0]+'.lc') + if not os.path.exists(filename) and not os.path.isfile(filename): + raise Exception("File does not exist. {filename}".format(filename=filename)) uploader.write_file(filename, dst, verify) - #init.lua is not allowed to be compiled + # init.lua is not allowed to be compiled if do_compile and dst != 'init.lua': uploader.file_compile(dst) uploader.file_remove(dst) @@ -70,27 +76,32 @@ def operation_upload(uploader, sources, verify, do_compile, do_file, do_restart) if do_restart: uploader.node_restart() log.info('All done!') + return destinations -def operation_download(uploader, sources): +def operation_download(uploader, sources, *args, **kwargs): """The download operation""" sources, destinations = destination_from_source(sources, False) - print('sources', sources) - print('destinations', destinations) + # print('sources', sources) + # print('destinations', destinations) + dest = kwargs.pop('dest', '') if len(destinations) == len(sources): if uploader.prepare(): for filename, dst in zip(sources, destinations): + dst = os.path.join(dest, dst) uploader.read_file(filename, dst) else: raise Exception('You must specify a destination filename for each file you want to download.') log.info('All done!') + def operation_list(uploader): """List file on target""" files = uploader.file_list() for f in files: log.info("{file:30s} {size}".format(file=f[0], size=f[1])) + def operation_file(uploader, cmd, filename=''): """File operations""" if cmd == 'list': @@ -106,7 +117,8 @@ def operation_file(uploader, cmd, filename=''): elif cmd == 'print': for path in filename: uploader.file_print(path) - + elif cmd == 'remove_all': + uploader.file_remove_all() def arg_auto_int(value): @@ -127,11 +139,20 @@ def main_func(): action='store_true', default=False) + parser.add_argument( + '--silent', + help='silent output. Errors and worse', + action='store_true', + default=False) + parser.add_argument( '--version', help='prints the version and exists', action='version', - version='%(prog)s {version} (serial {serialversion})'.format(version=__version__, serialversion=serialversion) + version='%(prog)s {version} (serial {serialversion}, python {pv})'.format( + version=__version__, + serialversion=serialversion, + pv=sys.version) ) parser.add_argument( @@ -173,7 +194,6 @@ def main_func(): help='Backup all the files on the nodemcu board') backup_parser.add_argument('path', help='Folder where to store the backup') - upload_parser = subparsers.add_parser( 'upload', help='Path to one or more files to be uploaded. Destination name will be the same as the file name.') @@ -224,19 +244,19 @@ def main_func(): 'download', help='Path to one or more files to be downloaded. Destination name will be the same as the file name.') - download_parser.add_argument('filename', + download_parser.add_argument( + 'filename', nargs='+', help='Lua file to download. Use colon to give alternate destination.') - file_parser = subparsers.add_parser( 'file', help='File functions') file_parser.add_argument( 'cmd', - choices=('list', 'do', 'format', 'remove', 'print'), - help="list=list files, do=dofile given path, format=formate file area, remove=remove given path") + choices=('list', 'do', 'format', 'remove', 'print', 'remove_all'), + help="list=list files, do=dofile given path, format=formate file area, remove=remove given path, remove_all=delete all files") file_parser.add_argument('filename', nargs='*', help='path for cmd') @@ -254,15 +274,17 @@ def main_func(): args = parser.parse_args() default_level = logging.INFO + if args.silent: + default_level = logging.ERROR if args.verbose: default_level = logging.DEBUG - #formatter = logging.Formatter('%(message)s') + # formatter = logging.Formatter('%(message)s') logging.basicConfig(level=default_level, format='%(message)s') if args.operation == 'terminal': - #uploader can not claim the port + # uploader can not claim the port terminal(args.port, str(args.start_baud)) return @@ -299,5 +321,5 @@ def main_func(): elif args.operation == 'backup': uploader.backup(args.path) - #no uploader related commands after this point + # no uploader related commands after this point uploader.close() diff --git a/nodemcu_uploader/uploader.py b/nodemcu_uploader/uploader.py index e55f59d..3523a99 100644 --- a/nodemcu_uploader/uploader.py +++ b/nodemcu_uploader/uploader.py @@ -16,12 +16,12 @@ from . import validate from .exceptions import CommunicationTimeout, DeviceNotFoundException, \ BadResponseException, VerificationError, NoAckException -from .utils import default_port, system, wrap, hexify, from_file, ENCODING +from .utils import default_port, system, hexify, from_file, ENCODING from .luacode import RECV_LUA, SEND_LUA, LUA_FUNCTIONS, \ - LIST_FILES, UART_SETUP, PRINT_FILE + LIST_FILES, UART_SETUP, PRINT_FILE, INFO_GROUP, REMOVE_ALL_FILES -log = logging.getLogger(__name__) # pylint: disable=C0103 +log = logging.getLogger(__name__) # pylint: disable=C0103 __all__ = ['Uploader', 'default_port'] @@ -29,9 +29,10 @@ MINIMAL_TIMEOUT = 0.001 -BLOCK_START = '\x01' -NUL = '\x00' -ACK = '\x06' +BLOCK_START = b'\x01' +NUL = b'\x00' +ACK = b'\x06' + class Uploader(object): """Uploader is the class for communicating with the nodemcu and @@ -53,14 +54,14 @@ def __init__(self, port=PORT, baud=BAUD, start_baud=START_BAUD, timeout=TIMEOUT, self._port = serial.Serial(port, start_baud, timeout=timeout) # black magic aka proxifying - self._port = wrap(self._port) + # self._port = wrap(self._port) self.start_baud = start_baud self.baud = baud self.autobaud_time = autobaud_time - # Keeps things working, if following conections are made: - ## RTS = CH_PD (i.e reset) - ## DTR = GPIO0 + # Keeps things working, if following connections are made: + # RTS = CH_PD (i.e reset) + # DTR = GPIO0 self._port.setRTS(False) self._port.setDTR(False) @@ -72,7 +73,7 @@ def __sync(): self.__writeln('UUUUUUUUUUUU') # Send enough characters for auto-baud self.__clear_buffers() time.sleep(self.autobaud_time) # Wait for autobaud timer to expire - self.__exchange(';') # Get a defined state + self.__exchange(';') # Get a defined state self.__writeln('print("%sync%");') self.__expect('%sync%\r\n> ') except CommunicationTimeout: @@ -113,30 +114,37 @@ def __clear_buffers(self): self._port.reset_input_buffer() self._port.reset_output_buffer() except AttributeError: - #pySerial 2.7 + # pySerial 2. self._port.flushInput() self._port.flushOutput() - def __expect(self, exp='> ', timeout=None): - """will wait for exp to be returned from nodemcu or timeout""" + """will wait for exp to be returned from nodemcu or timeout. + Will use utils.ENCODING for encoding if not bytes. + """ timeout_before = self._port.timeout timeout = timeout or self._timeout - #do NOT set timeout on Windows + # do NOT set timeout on Windows if SYSTEM != 'Windows': # Checking for new data every 100us is fast enough if self._port.timeout != MINIMAL_TIMEOUT: self._port.timeout = MINIMAL_TIMEOUT + t = type(exp) + if not isinstance(exp, bytes): + exp = bytes(exp, ENCODING) + end = time.time() + timeout # Finish as soon as either exp matches or we run out of time (work like dump, but faster on success) - data = '' + data = bytes() while not data.endswith(exp) and time.time() <= end: data += self._port.read() + # msg = data.decode(ENCODING, 'ignore') - log.debug('expect returned: `{0}`'.format(data)) - if time.time() > end: + now = time.time() + log.debug('expect returned: `{0}`. wants: {1}'.format(data, exp)) + if now > end: raise CommunicationTimeout('Timeout waiting for data', data) if not data.endswith(exp) and len(exp) > 0: @@ -145,15 +153,18 @@ def __expect(self, exp='> ', timeout=None): if SYSTEM != 'Windows': self._port.timeout = timeout_before - return data + return str(data, ENCODING) def __write(self, output, binary=False): - """write data on the nodemcu port. If 'binary' is True the debug log - will show the intended output as hex, otherwise as string""" + """write data on the nodemcu port. Strings will be converted to bytes using utils.ENCODING. + If 'binary' is True the debug log will show the intended output as hex, otherwise as string""" if not binary: log.debug('write: %s', output) else: log.debug('write binary: %s', hexify(output)) + if isinstance(output, str): + output = bytes(output, ENCODING) + self._port.write(output) self._port.flush() @@ -161,14 +172,15 @@ def __writeln(self, output): """write, with linefeed""" self.__write(output + '\n') - def __exchange(self, output, timeout=None): - """Write output to the port and wait for response""" + """Write output to the port and wait for response + Expects a str as input""" + if not isinstance(output, str): + raise TypeError("output should be a str") self.__writeln(output) self._port.flush() return self.__expect(timeout=timeout or self._timeout) - def close(self): """restores the nodemcu to default baudrate and then closes the port""" try: @@ -181,7 +193,6 @@ def close(self): log.debug('closing port') self._port.close() - def prepare(self): """ This uploads the protocol functions nessecary to do binary @@ -198,10 +209,10 @@ def prepare(self): return True functions = RECV_LUA + '\n' + SEND_LUA data = functions.format(baud=self._port.baudrate) - ##change any \r\n to just \n and split on that + # change any \r\n to just \n and split on that lines = data.replace('\r', '').split('\n') - - #remove some unneccesary spaces to conserve some bytes + # remove some unneccesary spaces to conserve some bytes + # TODO: a good minifier for lua for line in lines: line = line.strip().replace(', ', ',').replace(' = ', '=') @@ -209,41 +220,44 @@ def prepare(self): continue resp = self.__exchange(line) - #do some basic test of the result + # do some basic test of the result if ('unexpected' in resp) or ('stdin' in resp) or len(resp) > len(functions)+10: log.error('error when preparing "%s"', resp) return False return True def download_file(self, filename): - """Download a file from device to local filesystem""" + """Download a file from device to RAM + Return 'bytes' of the full content + """ validate.remotePath(filename) res = self.__exchange('send("{filename}")'.format(filename=filename)) if ('unexpected' in res) or ('stdin' in res): log.error('Unexpected error downloading file: %s', res) raise Exception('Unexpected error downloading file') - #tell device we are ready to receive + # tell device we are ready to receive self.__write('C') - #we should get a NUL terminated filename to start with + # we should get a NUL terminated filename to start with sent_filename = self.__expect(NUL).strip() log.info('receiveing ' + sent_filename) - #ACK to start download + # ACK to start download self.__write(ACK, True) - buf = '' + buf = bytes() - data = '' + data = bytes() chunk, buf = self.__read_chunk(buf) - #read chunks until we get an empty which is the end - while chunk != '': + # read chunks until we get an empty which is the end + while len(chunk) > 0: self.__write(ACK, True) data = data + chunk chunk, buf = self.__read_chunk(buf) return data def read_file(self, filename, destination=''): - """reading data from device into local file""" + """Downloading data from remote device into local file using the transfer protocol. + """ if not destination: destination = filename log.info('Transferring %s to %s', filename, destination) @@ -258,15 +272,19 @@ def read_file(self, filename, destination=''): except OSError as e: # Guard against race condition if e.errno != errno.EEXIST: raise - with open(destination, 'w') as fil: - fil.write(data) + with open(destination, 'wb') as fil: + try: + fil.write(data) + except Exception as e: + log.error("Unexpected error writing file", e) + raise def write_file(self, path, destination='', verify='none'): - """sends a file to the device using the transfer protocol""" + """Uploads a file to the remote device using the transfer protocol""" filename = os.path.basename(path) if not destination: destination = filename - + validate.remotePath(destination) log.info('Transferring %s as %s', path, destination) self.__writeln("recv()") @@ -301,8 +319,8 @@ def write_file(self, path, destination='', verify='none'): pos += chunk_size log.debug('sending zero block') - #zero size block - self.__write_chunk('') + # zero size block + self.__write_chunk() if verify != 'none': self.verify_file(path, destination, verify) @@ -321,12 +339,12 @@ def verify_file(self, path, destination, verify='none'): else: log.info('Verification successful. Contents are identical.') elif verify == 'sha1': - #Calculate SHA1 on remote file. Extract just hash from result + # Calculate SHA1 on remote file. Extract just hash from result data = self.__exchange('shafile("'+destination+'")').splitlines()[1] log.info('Remote SHA1: %s', data) - #Calculate hash of local data - filehashhex = hashlib.sha1(content.encode(ENCODING)).hexdigest() + # Calculate hash of local data + filehashhex = hashlib.sha1(content).hexdigest() log.info('Local SHA1: %s', filehashhex) if data != filehashhex: log.error('SHA1 verification failed.') @@ -359,8 +377,9 @@ def __got_ack(self): """Returns true if ACK is received""" log.debug('waiting for ack') res = self._port.read(1) - log.debug('ack read %s', hexify(res)) - return res == ACK + acked = res == ACK + log.debug('ack read %s, comparing with %s. %s', hexify(res), hexify(ACK), acked) + return acked def write_lines(self, data): """write lines, one by one, separated by \n to device""" @@ -368,15 +387,18 @@ def write_lines(self, data): for line in lines: self.__exchange(line) - def __write_chunk(self, chunk): - """formats and sends a chunk of data to the device according - to transfer protocol""" + def __write_chunk(self, chunk=bytes()): + """formats and sends a chunk of data to the device according to transfer protocol. + Return result of ack check""" + if not isinstance(chunk, bytes): + raise TypeError() log.debug('writing %d bytes chunk', len(chunk)) - data = BLOCK_START + chr(len(chunk)) + chunk + data = BLOCK_START + bytes([len(chunk)]) + chunk if len(chunk) < 128: padding = 128 - len(chunk) log.debug('pad with %d characters', padding) - data = data + (' ' * padding) + data = data + (b'\x00' * padding) + log.debug("packet size %d", len(data)) self.__write(data) self._port.flush() @@ -392,18 +414,24 @@ def __read_chunk(self, buf): self._port.timeout = MINIMAL_TIMEOUT end = time.time() + timeout_before - + if not isinstance(buf, bytes): + raise Exception('Buffer is not instance of "bytes"') while len(buf) < 130 and time.time() <= end: - buf = buf + self._port.read() + r = self._port.read() + if not isinstance(r, bytes): + raise Exception('r is not instance of "bytes" is {t}'.format(t=type(r).__name__)) + buf = buf + r - if buf[0] != BLOCK_START or len(buf) < 130: + if buf[0] != ord(BLOCK_START) or len(buf) < 130: log.debug('buffer binary: %s ', hexify(buf)) raise Exception('Bad blocksize or start byte') + # else: + # log.debug('buf binary: %s', hexify(buf)) if SYSTEM != 'Windows': self._port.timeout = timeout_before - chunk_size = ord(buf[1]) + chunk_size = buf[1] data = buf[2:chunk_size+2] buf = buf[130:] return (data, buf) @@ -444,6 +472,12 @@ def file_print(self, filename): log.info(res) return res + def file_remove_all(self): + log.info('Removing all files!!!') + res = self.__exchange(REMOVE_ALL_FILES) + log.info(res) + return res + def node_heap(self): """Show device heap size""" log.info('Heap') @@ -457,14 +491,21 @@ def node_restart(self): res = self.__exchange('node.restart()') log.info(res) return res - + def node_info(self): """Node info""" log.info('Node info') - res = self.__exchange('print(node.info())') + res = self.node_info_group('hw') + res += self.node_info_group('sw_version') + res += self.node_info_group('build_config') + return res + + def node_info_group(self, group): + log.info('Node info %s', group) + res = self.__exchange(INFO_GROUP.format(group=group)) log.info(res) return res - + def file_compile(self, path): """Compiles a file specified by path on the device""" log.info('Compile '+path) diff --git a/nodemcu_uploader/utils.py b/nodemcu_uploader/utils.py index c583bd7..8701450 100644 --- a/nodemcu_uploader/utils.py +++ b/nodemcu_uploader/utils.py @@ -4,13 +4,16 @@ from platform import system from os import environ -from wrapt import ObjectProxy +# from wrapt import ObjectProxy from sys import version_info -__all__ = ['default_port', 'system', 'hexify', 'from_file', 'wrap', 'PY2', 'ENCODING'] +__all__ = ['default_port', 'system', 'hexify', 'from_file', 'PY2', 'ENCODING'] PY2 = version_info.major == 2 +if PY2: + raise Exception("Python 2 is no longer supported") + ENCODING = 'latin1' @@ -24,28 +27,35 @@ def default_port(sysname=system()): def to_hex(x): + if isinstance(x, int): + return hex(x) return hex(ord(x)) def hexify(byte_arr): - return ':'.join((to_hex(x)[2:] for x in byte_arr)) + if isinstance(byte_arr, int): + return to_hex(byte_arr)[2:] + else: + return ':'.join((to_hex(x)[2:] for x in byte_arr)) def from_file(path): + """Returns content of file as 'bytes'. + """ with open(path, 'rb') as f: content = f.read() - return content if PY2 else content.decode(ENCODING) + return content -class DecoderWrapper(ObjectProxy): - def read(self, *args, **kwargs): - res = self.__wrapped__.read(*args, **kwargs) - return res if PY2 else res.decode(ENCODING) +# class DecoderWrapper(ObjectProxy): +# def read(self, *args, **kwargs): +# res = self.__wrapped__.read(*args, **kwargs) +# return res if PY2 else res.decode(ENCODING) - def write(self, data): - data = data if PY2 else data.encode(ENCODING) - return self.__wrapped__.write(data) +# def write(self, data): +# data = data if PY2 else data.encode(ENCODING) +# return self.__wrapped__.write(data) -def wrap(x): - return DecoderWrapper(x) +# def wrap(x): +# return DecoderWrapper(x) diff --git a/nodemcu_uploader/validate.py b/nodemcu_uploader/validate.py index 65e38ac..92a4e54 100644 --- a/nodemcu_uploader/validate.py +++ b/nodemcu_uploader/validate.py @@ -1,10 +1,12 @@ - +from .exceptions import PathLengthException MAX_FS_NAME_LEN = 31 -from .exceptions import PathLengthException def remotePath(path): + """Do various checks on the remote file name like max length. + Raises exception if not valid + """ if len(path) > MAX_FS_NAME_LEN: raise PathLengthException if len(path) < 1: diff --git a/nodemcu_uploader/version.py b/nodemcu_uploader/version.py index d4ee7a9..419a120 100644 --- a/nodemcu_uploader/version.py +++ b/nodemcu_uploader/version.py @@ -2,5 +2,5 @@ # Copyright (C) 2015-2019 Peter Magnusson """just keeper of current version""" -#TODO: remember to update tests when version changes -__version__ = '0.4.3' +# TODO: remember to update tests when version changes +__version__ = '1.0.0' diff --git a/setup.py b/setup.py index 7d936b8..595c5f4 100755 --- a/setup.py +++ b/setup.py @@ -5,16 +5,15 @@ from setuptools import setup -exec(open('nodemcu_uploader/version.py').read()) #pylint: disable=W0122 +exec(open('nodemcu_uploader/version.py').read()) # pylint: disable=W0122 setup(name='nodemcu-uploader', - version=__version__, #pylint: disable=E0602 + version=__version__, # noqa: F821 install_requires=[ - 'pyserial>=2.7', - 'wrapt>=1.10.10' + 'pyserial>=3.4' ], packages=['nodemcu_uploader'], - #package_dir={'nodemcu_uploader': 'lib'}, + # package_dir={'nodemcu_uploader': 'lib'}, url='https://github.com/kmpm/nodemcu-uploader', author='kmpm', author_email='peter@birchroad.net', @@ -33,4 +32,4 @@ ] }, zip_safe=False - ) + ) diff --git a/test_requirements.txt b/test_requirements.txt index ecbadb6..942f901 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,3 +1,3 @@ -pyserial==3.0.1 -coverage==4.0.3 -wrapt==1.10.10 +pyserial==3.4 +coverage==4.0.3 +flake8==3.7.9 diff --git a/tests/fixtures/led_blink.lua b/tests/fixtures/led_blink.lua new file mode 100644 index 0000000..35847db --- /dev/null +++ b/tests/fixtures/led_blink.lua @@ -0,0 +1,11 @@ +lighton=0 +tmr.alarm(0,1000,1,function() +if lighton==0 then + lighton=1 + led(512,512,512) + -- 512/1024, 50% duty cycle +else + lighton=0 + led(0,0,0) +end +end) diff --git a/tests/fixtures/riazzerawifi.lua b/tests/fixtures/riazzerawifi.lua new file mode 100644 index 0000000..94fa85a --- /dev/null +++ b/tests/fixtures/riazzerawifi.lua @@ -0,0 +1,17 @@ +--riazzerawifi.lua +-- Starts the portal to choose the wi-fi router to which we have +-- to associate +wifi.sta.disconnect() +wifi.setmode(wifi.STATIONAP) +--ESP SSID generated wiht its chipid +wifi.ap.config({ssid="Mynode-"..node.chipid() +, auth=wifi.OPEN}) +enduser_setup.manual(true) +enduser_setup.start( + function() + print("Connected to wifi as:" .. wifi.sta.getip()) + end, + function(err, str) + print("enduser_setup: Err #" .. err .. ": " .. str) + end +); diff --git a/tests/fixtures/signatur.tif b/tests/fixtures/signatur.tif new file mode 100644 index 0000000..f2f5fa5 Binary files /dev/null and b/tests/fixtures/signatur.tif differ diff --git a/tests/fixtures/webserver.lua b/tests/fixtures/webserver.lua new file mode 100644 index 0000000..3bea749 --- /dev/null +++ b/tests/fixtures/webserver.lua @@ -0,0 +1,35 @@ +-- webserver.lua +--webserver sample from the nodemcu github +if srv~=nil then + srv:close() + end + + gpio.mode(1, gpio.OUTPUT) + srv=net.createServer(net.TCP) + srv:listen(80,function(conn) + conn:on("receive", function(client,request) + local buf = "" + local _, _, method, path, vars = string.find(request, "([A-Z]+) (.+)?(.+) HTTP") + if(method == nil)then + _, _, method, path = string.find(request, "([A-Z]+) (.+) HTTP") + end + local _GET = {} + if (vars ~= nil)then + for k, v in string.gmatch(vars, "(%w+)=(%w+)&*") do + _GET[k] = v + end + end + buf = buf.."

Hello, NodeMcu.

Turn PIN1
" + client:send(buf) + end) + conn:on("sent", function (c) c:close() end) + end) diff --git a/tests/torture.py b/tests/torture.py new file mode 100644 index 0000000..b5eab1a --- /dev/null +++ b/tests/torture.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +import unittest +import logging +import time +import os +from nodemcu_uploader import Uploader +from nodemcu_uploader.main import operation_download, operation_upload +import shutil + +log = logging.getLogger(__name__) +logging.basicConfig( + filename='test.log', + level=logging.DEBUG, + format='%(asctime)s %(levelname)s %(module)s.%(funcName)s %(message)s') + +LOOPPORT = 'loop://' + +# on which port should the tests be performed +SERIALPORT = os.environ.get('SERIALPORT', LOOPPORT) + + +def is_real(): + if SERIALPORT.strip() == '': + return False + return str(SERIALPORT) != str(LOOPPORT) + + +@unittest.skipUnless(is_real(), 'Needs a configured SERIALPORT') +class TestTorture(unittest.TestCase): + uploader = None + + def setUp(self): + self.uploader = Uploader(SERIALPORT) + + def tearDown(self): + if is_real(): + self.uploader.node_restart() + self.uploader.close() + time.sleep(1) + + def task_upload_verify_compile(self): + print('upload-verify-compile') + self.assertTrue(self.uploader.prepare()) + pattern = os.path.join('tests', 'fixtures', '*.lua') + self.assertEqual(pattern, "tests\\fixtures\\*.lua") + dests = operation_upload(self.uploader, "tests/fixtures/*.lua", 'sha1', True, False, False) + return len(dests) + + def task_check_remote_files(self, wanted): + lst = self.uploader.file_list() + self.assertIsInstance(lst, type([])) + self.assertEqual(len(lst), wanted) + return lst + + def task_remove_all_files(self): + print('remove all files') + self.uploader.file_remove_all() + self.task_check_remote_files(0) + + def task_download_all_files(self, files): + print('download all files', files) + dest = os.path.join('.', 'tmp') + operation_download(self.uploader, files, dest=dest) + for f in files: + self.assertTrue(os.path.isfile(os.path.join(dest, f))) + + def task_remove_tmp(self): + dest = os.path.join('.', 'tmp') + if os.path.isdir(dest): + shutil.rmtree(dest) + + def test_for_long_time(self): + for x in range(20): + print('{x} amount of times'.format(x=x)) + self.task_remove_tmp() + self.task_remove_all_files() + time.sleep(0.5) + count = self.task_upload_verify_compile() + self.assertEqual(count, 3) + files = self.task_check_remote_files(count) + self.task_download_all_files(list(map(lambda x: x[0], files))) diff --git a/tests/uploader.py b/tests/uploader.py index 3d009be..c23c0ad 100644 --- a/tests/uploader.py +++ b/tests/uploader.py @@ -1,17 +1,19 @@ # -*- coding: utf-8 -*- # Copyright (C) 2015-2019 Peter Magnusson -#pylint: disable=C0111,R0904 +# pylint: disable=C0111,R0904 import unittest -import os, time +import time +import os from nodemcu_uploader import Uploader -from serial import VERSION as serialversion -from distutils.version import LooseVersion +# from serial import VERSION as serialversion +# from distutils.version import LooseVersion LOOPPORT = 'loop://' -#on which port should the tests be performed +# on which port should the tests be performed SERIALPORT = os.environ.get('SERIALPORT', LOOPPORT) + def is_real(): if SERIALPORT.strip() == '': return False @@ -23,9 +25,11 @@ def is_real(): # uploader = Uploader(SERIALPORT) # uploader.close() + @unittest.skipUnless(is_real(), 'Needs a configured SERIALPORT') class UploaderTestCase(unittest.TestCase): uploader = None + def setUp(self): self.uploader = Uploader(SERIALPORT) @@ -35,7 +39,6 @@ def tearDown(self): self.uploader.close() time.sleep(1) - def test_upload_and_verify_raw(self): self.uploader.prepare() self.uploader.write_file('tests/fixtures/big_file.txt', verify='raw') @@ -44,20 +47,21 @@ def test_upload_and_verify_sha1(self): self.uploader.prepare() self.uploader.write_file('tests/fixtures/big_file.txt', verify='sha1') - def test_upload_strange_file(self): self.uploader.prepare() self.uploader.write_file('tests/fixtures/testuploadfail.txt', verify='raw') - def test_file_list(self): lst = self.uploader.file_list() self.assertIsInstance(lst, type([])) self.assertGreaterEqual(len(lst), 1) self.assertLess(len(lst), 50) - def test_node_heap(self): size = self.uploader.node_heap() self.assertGreater(size, 20000) self.assertLess(size, 60000) + + def test_node_info(self): + result = self.uploader.node_info() + self.assertNotIn("deprecated", result) diff --git a/tox.ini b/tox.ini index 64277fd..b977bfb 100644 --- a/tox.ini +++ b/tox.ini @@ -6,3 +6,10 @@ deps = -rtest_requirements.txt commands = python -m unittest -v tests.get_tests setenv = SERIALPORT=/dev/ttyACM0 + +[flake8] +include = + nodemcu_uploader, + tests +# ignore = E501 +max-line-length = 120 \ No newline at end of file