diff --git a/CHANGES.md b/CHANGES.md index e015770..e8cc525 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,68 @@ # uPiot Release Notes +## Version 0.1.6-alpha | 21 Jan 2018 + +#### Improvements + +* Connect the console when a command is run and it's not connected (in missing commands) +* Minor code improvement writing code in a file + +#### Bugs + +* Bug fix cleaning a retrieved line + +## Version 0.1.5-alpha | 21 Jan 2018 + +#### New + +* Added feedback in the statusbar while the command is running +* Runs selected code (Issue: https://github.com/gepd/uPiotMicroPythonTool/issues/4) + +#### Improvements + +* Opens the console when a command is ran it's closed +* Make sure to always download the given file, even if it already exists +* pyboard has been renamed and refactorized to repl to organize and improve the way to work with the device +* Absolute path in make and remove folder was removed to improve the compatibility with more devices and firmwares. +* Added more feedback when there is a problem opening the console or a problem running a command. +* The sync functions shows the name of the file retrieving in realtime. + +## Version 0.1.4-alpha | 12 Nov 2017 + +#### New + +* New --close command + +#### Improvements + +* make the write console command only available when there is a port connected removed stablish connection. +* The main serial instance is passed to sampy to avoid multiples connect and disconnect, it may solve a problem in Linux dtr and rts are disabled before make the connection in the serial port to fix a problem with linux +* Workaround to remove the status bar color, when it wasn't closed before exit from ST +* Show a red color in the status bar when the serial instance is destroyed/closed + +#### Bugs + +* Fix serial listener after run a sampy command +* Fix destroying serial session when console window is closed +* Linux serial fixes +* Fixed --help command +* Other minor bug fixes + +## Version 0.1.3-alpha | 06 Nov 2017 + +#### New + +* First implementation to show a color in the status bar when a serial connection is established in a port + +#### Improvements + +* When the console is closed, it will destroy/close the group panel if it's empty + +#### Bugs + +* Fix bug make not work all commands except for "sampy run" introduced in https://github.com/gepd/uPiotMicroPythonTool/commit/6ceba526b00f3812b4e64d9ae0f0187cbaaae9a2 +* Fixed --help command + ## Version 0.1.2-alpha | 04 Nov 2017 #### New diff --git a/Main.sublime-menu b/Main.sublime-menu index 94766e3..ceace91 100644 --- a/Main.sublime-menu +++ b/Main.sublime-menu @@ -19,7 +19,7 @@ "caption": "Settings", "command": "edit_settings", "args": { - "base_file": "${packages}/upiot/Default.sublime-settings", + "base_file": "${packages}/uPIOT MicroPython Tool/Default.sublime-settings", "default": "[\n\t$0\n]\n" } }, @@ -28,7 +28,7 @@ "caption": "Key Bindings", "command": "edit_settings", "args": { - "base_file": "${packages}/upiot/Default ($platform).sublime-keymap", + "base_file": "${packages}/uPIOT MicroPython Tool/Default ($platform).sublime-keymap", "default": "[\n\t$0\n]\n" } }, diff --git a/README.md b/README.md index 35b4d6e..962016d 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,13 @@ > **uPiot** is an experiment to make the work with uPython (micropython) a little bit easier. It uses an modified version of [ampy](https://github.com/adafruit/ampy/) called `sampy` (Sublime + ampy) to comunicate with the device and [pyserial](https://github.com/pyserial/pyserial) to listen for changes. You can also burn the firmware direct from Sublime Text without any extra tool. > -**This plugin is in alpha state, so you may find some bugs hanging around. Help me to improving it opening a issue with your repport or with a PR.** +**THIS PLUGIN IS IN ALPHA STATE, SO YOU MAY FIND SOME BUGS HANGING AROUND. HELP ME TO IMPROVING IT OPENING A ISSUE WITH YOUR REPPORT OR WITH A PR.** +# Features + +* Built-in fuction to burn MicroPython firmware. +* Manage your files in your device (get, list, run, make-remove, folders/files) +* Realtime output, even with loops # Setup @@ -92,6 +97,7 @@ Usage: `sampy COMMAND [ARGS]` |**mkdir** folder_name|Create a directory on the board| |**rmdir** folder_name|Forcefully remove a folder and all its content from board| |**reset**|Perform soft reset/reboot of the board| +|**--close**|Closes the connection between the console and the serial port| |**--help**|Shows this information| > Note that if you don't write prefix 'sampy' in the console the string will be sent as a raw text to the device (with the `\r\n` ending) diff --git a/boards/esp32_loboris.json b/boards/esp32_loboris.json new file mode 100644 index 0000000..2a93063 --- /dev/null +++ b/boards/esp32_loboris.json @@ -0,0 +1,10 @@ +{ + "id": "esp32-loboris", + "upload": { + "--chip": "esp32", + "--baud": "921600", + "--before": "default_reset", + "--after": "no_reset", + "write_flash": "-z --flash_mode dio --flash_freq 40m --flash_size detect 0x1000 {0}/bootloader/bootloader.bin 0xf000 {0}/phy_init_data.bin 0x8000 {0}/partitions_mpy.bin 0x10000" + } +} \ No newline at end of file diff --git a/commands/burn_firmware.py b/commands/burn_firmware.py index 6e41778..3f11262 100644 --- a/commands/burn_firmware.py +++ b/commands/burn_firmware.py @@ -30,7 +30,7 @@ from glob import glob from json import loads -from os.path import join, basename +from os.path import join, basename, dirname, isfile from .. import tools from ..tools import paths @@ -45,6 +45,7 @@ class upiotBurnFirmwareCommand(WindowCommand): items = None firmwares = None url = None + subfolder = None def run(self, selected=None): # only continue if a device is available @@ -80,6 +81,10 @@ def callback_selection(self, selection): return self.url = self.items[selection] + + if(self.url == 'No firmware(s)'): + return + sublime.set_timeout_async(self.burn_firmware, 0) def firmware_list(self): @@ -89,9 +94,35 @@ def firmware_list(self): """ firm_path = join(self.firmwares, '*') - for firmware in glob(firm_path): - name = basename(firmware) - self.items.append(name) + # file names who shouldn't be displayed in the list + blacklist = [ + 'README.md', + 'sdkconfig', + 'flash.sh', + 'bootloader', + 'bootloader.bin', + 'partitions_mpy.bin', + 'partitions_mpy.csv', + 'phy_init_data.bin' + ] + + # search for firmware files including a sub-folder + for files in glob(firm_path): + if(isfile(files)): + name = basename(files) + if(name not in blacklist): + self.items.append(name) + else: + mfiles = join(files, '*') + for more in glob(mfiles): + name = basename(more) + if(name not in blacklist): + self.subfolder = basename(dirname(more)) + name = self.subfolder + '/' + name + self.items.append(name) + + if(not self.items): + self.items.append('No firmware(s)') def burn_firmware(self): """Burn firmware @@ -101,9 +132,12 @@ def burn_firmware(self): from ..tools.command import Command filename = self.url.split('/')[-1] - firmware = join(self.firmwares, filename) + if(self.subfolder): + firmware = join(self.firmwares, self.subfolder, filename) + else: + firmware = join(self.firmwares, filename) - options = self.get_board_options(self.board) + options = self.get_board_options() options.append(firmware) caption = "Do you want to erase the flash memory?" @@ -119,28 +153,24 @@ def burn_firmware(self): options.insert(0, "--port " + self.port) + if(self.port in serial.in_use): + serial.serial_dict[self.port].disconnect() + if(not serial.check_port(self.port)): return - if(self.port in serial.in_use): - serial.serial_dict[self.port].close() - Command().run(options, port=self.port) - @staticmethod - def get_board_options(board): + def get_board_options(self): """get board option get the options defined in the json board file - Arguments: - board {str} -- board selected - Returns: list -- board options """ board_folder = paths.boards_folder() - filename = board + '.json' + filename = self.board + '.json' board_path = join(board_folder, filename) board_file = [] @@ -154,7 +184,13 @@ def get_board_options(board): option = "{0}{1}{2}".format(key, separator, value) options.append(option) + if(self.subfolder): + replace_path = join(self.firmwares, self.subfolder) + else: + replace_path = self.firmwares + wf = board_file['upload']['write_flash'] + wf = wf.format(replace_path) options.append('write_flash ' + wf) return options diff --git a/commands/console_write.py b/commands/console_write.py index 1a28957..d39d8e0 100644 --- a/commands/console_write.py +++ b/commands/console_write.py @@ -35,6 +35,9 @@ class upiotConsoleWriteCommand(WindowCommand): def run(self): self.window.show_input_panel('>>>', '', self.callback, None, None) + def is_eable(self): + return bool(serial.in_use) + def callback(self, data): """Console write callback @@ -51,9 +54,13 @@ def callback(self, data): return # run sampy commands - if(data.startswith('sampy')): + if(data.startswith(('sampy', '--help'))): data = data.split() - cmd = data[1] + try: + cmd = data[1] + except IndexError: + cmd = data[0] + arg = data[2] if(len(data) > 2) else None th = Thread(target=self.sampy_commands, args=(cmd, arg)) @@ -62,11 +69,19 @@ def callback(self, data): self.window.run_command('upiot_console_write') return - # establish a connection if it doesn't exists - if(self.port not in serial.in_use): - serial.establish_connection(self.port) - link = serial.serial_dict[self.port] + + # destroy connection + if(data == '--close'): + from ..tools import message + + link.disconnect() + link.destroy() + + txt = message.open(self.port) + txt.print("\n\nConnection to port {0} closed.".format(self.port)) + return + link.writable(data) self.window.run_command('upiot_console_write') @@ -102,7 +117,7 @@ def sampy_commands(self, option, arg=None): except: try: commands[option](arg) - except: + except KeyError: from ..tools import message txt = message.open(self.port) txt.print('\n\n>> CommandError: "{}" not found'.format(option)) diff --git a/commands/download_firmware.py b/commands/download_firmware.py index d93c7dd..b4b4c7b 100644 --- a/commands/download_firmware.py +++ b/commands/download_firmware.py @@ -23,7 +23,10 @@ # SOFTWARE. import sublime +from time import time from sublime_plugin import WindowCommand +from os import path, remove, rename +from datetime import datetime from ..tools import paths from .. import tools @@ -61,23 +64,65 @@ def download_firmware(self): If the file isn't in the firmwares folder, it download the file and put it in a folder corresponding to the board selection """ + tools.ACTIVE_VIEW = self.window.active_view() + tools.set_status('Preparing download') + settings = sublime.load_settings(tools.SETTINGS_NAME) board = settings.get('board', None) folder = paths.firmware_folder(board) tools.make_folder(folder) - tools.ACTIVE_VIEW = self.window.active_view() - out = tools.download_file(self.url, folder, callback=tools.set_status) + filename = self.url.split('/')[-1] + destination = path.join(folder, filename) + + out = tools.download_file(self.url, destination, callback=tools.set_status) if(out): tools.set_status('Download success') + self.extract_file(destination, folder) else: - from os import path, remove - - filename = self.url.split('/')[-1] - dst_path = path.join(folder, filename) - remove(dst_path) - + remove(destination) tools.set_status('Error downloading') sublime.set_timeout_async(tools.clean_status, 2000) + + + def extract_file(self, filepath, destination): + tools.set_status('Extracting file...') + + cur_datetime = datetime.now().strftime("%y%m%d-%H%M") + extension = filepath.split(".")[-1] + zip_name = path.basename(filepath).split(".")[0] + + if(extension not in ['zip', 'gz', 'bz2']): + return + + if(extension in ['gz', 'bz2']): + import tarfile + + tar = tarfile.open(filepath, 'r:' + extension) + for item in tar: + tar.extract(item, destination) + + if(extension in ['zip']): + import zipfile + + with zipfile.ZipFile(filepath, 'r') as file: + file.extractall(destination) + + # new name based in the current time to avoid collition with futures downloads + extracted_folder = path.join(destination, 'esp32') + if(path.isdir(extracted_folder)): + rename(extracted_folder, path.join(destination, zip_name)) + + # rename MicroPython.bin to datetime based name + micropython = path.join(destination, zip_name, 'MicroPython.bin') + new_name = path.join(destination, zip_name, cur_datetime + '.bin') + if(path.isfile(micropython)): + rename(micropython, new_name) + + remove(filepath) + + + + \ No newline at end of file diff --git a/commands/list_files.py b/commands/list_files.py index d1f322c..c3b827f 100644 --- a/commands/list_files.py +++ b/commands/list_files.py @@ -24,9 +24,11 @@ import sublime from sublime_plugin import WindowCommand +from threading import Thread from ..tools import sampy_manager from ..tools.serial import selected_port +from ..tools.thread_progress import ThreadProgress class upiotListFilesCommand(WindowCommand): @@ -36,4 +38,7 @@ def run(self): if(not port): return - sublime.set_timeout_async(sampy_manager.list_files, 0) + th = Thread(target=sampy_manager.list_files) + th.start() + + ThreadProgress(th, '', '') diff --git a/commands/make_folder.py b/commands/make_folder.py index 36825d3..0a4c874 100644 --- a/commands/make_folder.py +++ b/commands/make_folder.py @@ -24,9 +24,11 @@ import sublime from sublime_plugin import WindowCommand +from threading import Thread from ..tools import sampy_manager from ..tools.serial import selected_port +from ..tools.thread_progress import ThreadProgress class upiotMakeFolderCommand(WindowCommand): @@ -36,9 +38,10 @@ def run(self): if(not port): return - self.window.show_input_panel('Name', '/', self.callback, None, None) + self.window.show_input_panel('Name', '', self.callback, None, None) def callback(self, folder_name): - def make_folder(): - sampy_manager.make_folder(folder_name) - sublime.set_timeout_async(make_folder, 0) + th = Thread(target=sampy_manager.make_folder, args=(folder_name,)) + th.start() + + ThreadProgress(th, '', '') diff --git a/commands/put_current_file.py b/commands/put_current_file.py index a820eb9..0b75e20 100644 --- a/commands/put_current_file.py +++ b/commands/put_current_file.py @@ -24,9 +24,11 @@ import sublime from sublime_plugin import WindowCommand +from threading import Thread from ..tools import sampy_manager from ..tools.serial import selected_port +from ..tools.thread_progress import ThreadProgress class upiotPutCurrentFileCommand(WindowCommand): @@ -37,8 +39,11 @@ def run(self): return file = self.window.active_view().file_name() + view = self.window.active_view() - def put_file(): - sampy_manager.put_file(file) + if(view.is_dirty()): + view.run_command('save') - sublime.set_timeout_async(put_file, 0) + th = Thread(target=sampy_manager.put_file, args=(file,)) + th.start() + ThreadProgress(th, '', '') diff --git a/commands/put_file.py b/commands/put_file.py index 8850a87..b64e238 100644 --- a/commands/put_file.py +++ b/commands/put_file.py @@ -24,9 +24,11 @@ import sublime from sublime_plugin import WindowCommand +from threading import Thread from ..tools import sampy_manager from ..tools.serial import selected_port +from ..tools.thread_progress import ThreadProgress class upiotPutFileCommand(WindowCommand): @@ -39,7 +41,6 @@ def run(self): self.window.show_input_panel('Path', '', self.callback, None, None) def callback(self, file): - def put_file(): - sampy_manager.put_file(file) - - sublime.set_timeout_async(put_file, 0) + th = Thread(target=sampy_manager.put_file, args=(file,)) + th.start() + ThreadProgress(th, '', '') diff --git a/commands/raw_serial.py b/commands/raw_serial.py index e1b080a..7e68517 100644 --- a/commands/raw_serial.py +++ b/commands/raw_serial.py @@ -26,7 +26,7 @@ from sublime_plugin import WindowCommand from ..tools import serial -from ..tools.ampy import pyboard +from ..tools.ampy import files from ..tools import str_cmd_serial @@ -39,5 +39,5 @@ def run(self, data): sserial = serial.serial_dict[port] sserial.write(str_cmd_serial(data)) except: - sserial = pyboard.serial_dict[port] + sserial = files.serial_dict[port] sserial.write(str_cmd_serial(data)) diff --git a/commands/remove_file.py b/commands/remove_file.py index 84c12e1..ac4d980 100644 --- a/commands/remove_file.py +++ b/commands/remove_file.py @@ -24,9 +24,11 @@ import sublime from sublime_plugin import WindowCommand +from threading import Thread from ..tools import sampy_manager from ..tools.serial import selected_port +from ..tools.thread_progress import ThreadProgress class upiotRemoveFileCommand(WindowCommand): @@ -39,7 +41,7 @@ def run(self): self.window.show_input_panel('Path', '', self.callback, None, None) def callback(self, file): - def remove_file(): - sampy_manager.remove_file(file) + th = Thread(target=sampy_manager.remove_file, args=(file,)) + th.start() - sublime.set_timeout_async(remove_file, 0) + ThreadProgress(th, '', '') diff --git a/commands/remove_folder.py b/commands/remove_folder.py index 0cab02a..f2cbd68 100644 --- a/commands/remove_folder.py +++ b/commands/remove_folder.py @@ -24,9 +24,11 @@ import sublime from sublime_plugin import WindowCommand +from threading import Thread from ..tools import sampy_manager from ..tools.serial import selected_port +from ..tools.thread_progress import ThreadProgress class upiotRemoveFolderCommand(WindowCommand): @@ -36,9 +38,10 @@ def run(self): if(not port): return - self.window.show_input_panel('Name', '/', self.callback, None, None) + self.window.show_input_panel('Name', '', self.callback, None, None) def callback(self, folder): - def remove_folder(): - sampy_manager.remove_folder(folder) - sublime.set_timeout_async(remove_folder, 0) + th = Thread(target=sampy_manager.remove_folder, args=(folder,)) + th.start() + + ThreadProgress(th, '', '') diff --git a/commands/retrieve_all_files.py b/commands/retrieve_all_files.py index 2e69747..dacef99 100644 --- a/commands/retrieve_all_files.py +++ b/commands/retrieve_all_files.py @@ -25,8 +25,10 @@ import sublime from sublime_plugin import WindowCommand from threading import Thread + from ..tools import sampy_manager from ..tools.serial import selected_port +from ..tools.thread_progress import ThreadProgress class upiotRetrieveAllFilesCommand(WindowCommand): @@ -40,4 +42,7 @@ def run(self): 'Destination:', '', self.callback, None, None) def callback(self, path): - Thread(target=sampy_manager.get_files, args=(path,)).start() + th = Thread(target=sampy_manager.get_files, args=(path,)) + th.start() + + ThreadProgress(th, '', '') diff --git a/commands/run_current_file.py b/commands/run_current_file.py index 80f2f35..ef08a90 100644 --- a/commands/run_current_file.py +++ b/commands/run_current_file.py @@ -24,11 +24,13 @@ import sublime from sublime_plugin import WindowCommand +from threading import Thread from ..tools import sampy_manager from ..tools import message -from threading import Thread from ..tools.serial import selected_port +from ..tools.ampy import files +from ..tools.thread_progress import ThreadProgress class upiotRunCurrentFileCommand(WindowCommand): @@ -47,4 +49,10 @@ def run(self): if(view.is_dirty()): view.run_command('save') - Thread(target=sampy_manager.run_file, args=(file,)).start() + view = self.window.active_view() + selection = view.sel()[0] + files.SELECTED_TEXT = bytes(view.substr(selection), 'utf-8') + + th = Thread(target=sampy_manager.run_file, args=(file,)) + th.start() + ThreadProgress(th, '', '') diff --git a/tools/__init__.py b/tools/__init__.py index 0154db7..415d2eb 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -35,7 +35,7 @@ from ..tools.boards import boards_list from ..tools.quick_panel import quick_panel -VERSION = (0, 1, 2, '-alpha') +VERSION = (0, 1, 6, '-alpha') ACTIVE_VIEW = None SETTINGS_NAME = 'upiot.sublime-settings' @@ -89,7 +89,7 @@ def download_file(file_url, dst_path, callback=None): Arguments: file_url {str} -- url with the file to download - dst_path {str} -- where file will be stored + dst_path {str} -- where file will be stored (including file name) callback {obj} -- callback to show the progress of the download Returns: @@ -98,12 +98,7 @@ def download_file(file_url, dst_path, callback=None): downloaded = 0 progress_qty = 5 # numbers of symbols to show when it downloading (total) headers = get_headers() - filename = file_url.split('/')[-1] - dst_path = path.join(dst_path, filename) - - # stop if the file already exits - if(path.exists(dst_path)): - return True + with open(dst_path, 'wb') as file: try: diff --git a/tools/ampy/files.py b/tools/ampy/files.py index 6d50743..702266e 100644 --- a/tools/ampy/files.py +++ b/tools/ampy/files.py @@ -9,8 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, @@ -19,22 +19,27 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. + import ast import textwrap -from .pyboard import PyboardError - # Amount of data to read or write to the serial port at a time. BUFFER_SIZE = 32 # This is kept small because small chips and USB to serial # bridges usually have very small buffers. +SELECTED_TEXT = None + class DirectoryExistsError(Exception): pass +class PyboardError(BaseException): + pass + + class Files(object): """Class to interact with a MicroPython board files over a serial connection. Provides functions for listing, uploading, and downloading files from the @@ -59,6 +64,7 @@ def get(self, filename): # expects string data. command = """ import sys + file = b'' with open('{0}', 'rb') as infile: while True: result = infile.read({1}) @@ -66,7 +72,9 @@ def get(self, filename): break len = sys.stdout.write(result) """.format(filename, BUFFER_SIZE) - self._pyboard.enter_raw_repl() + + self._pyboard.enter_raw() + try: out = self._pyboard.exec_(textwrap.dedent(command)) except PyboardError as ex: @@ -76,10 +84,12 @@ def get(self, filename): raise RuntimeError('No such file: {0}'.format(filename)) else: raise ex - self._pyboard.exit_raw_repl() + + self._pyboard.exit_raw() + return out - def ls(self, directory='/'): + def ls(self, directory): """List the contents of the specified directory (or root if none is specified). Returns a list of strings with the names of files in the specified directory. @@ -91,11 +101,23 @@ def ls(self, directory='/'): import os except ImportError: import uos as os - print({{f : uos.stat(f)[0] for f in uos.listdir('{0}')}}) + + ls = [] + for item in os.listdir('{0}'): + try: + os.chdir(item) + os.chdir("..") + item += '/' + except: + pass + ls.append(item) + print(ls) """.format(directory) - self._pyboard.enter_raw_repl() + + self._pyboard.enter_raw() + try: - name2stat = self._pyboard.exec_(textwrap.dedent(command)) + out = self._pyboard.exec_(textwrap.dedent(command)) except PyboardError as ex: # Check if this is an OSError #2, i.e. directory doesn't exist and # rethrow it as something more descriptive. @@ -103,20 +125,15 @@ def ls(self, directory='/'): raise RuntimeError('No such directory: {0}'.format(directory)) else: raise ex - out = [] - for f_name, f_stat in sorted( - ast.literal_eval(name2stat.decode('utf-8')).items()): - if stat.S_ISDIR(f_stat): - out.append(f_name + '/') - else: - out.append(f_name) - self._pyboard.exit_raw_repl() + + self._pyboard.exit_raw() + # Parse the result list and return it. - return out + return ast.literal_eval(out.decode('utf-8')) def mkdir(self, directory): """Create the specified directory. Note this cannot create a recursive - hierarchy of directories, instead each one should be created separately. + hierarchy of directories, instead each one should be created separately """ # Execute os.mkdir command on the board. command = """ @@ -126,7 +143,9 @@ def mkdir(self, directory): import uos as os os.mkdir('{0}') """.format(directory) - self._pyboard.enter_raw_repl() + + self._pyboard.enter_raw() + try: out = self._pyboard.exec_(textwrap.dedent(command)) except PyboardError as ex: @@ -136,13 +155,14 @@ def mkdir(self, directory): 'Directory already exists: {0}'.format(directory)) else: raise ex - self._pyboard.exit_raw_repl() + + self._pyboard.exit_raw() def put(self, filename, data): """Create or update the specified file with the provided data. """ # Open the file for writing on the board and write chunks of data. - self._pyboard.enter_raw_repl() + self._pyboard.enter_raw() self._pyboard.exec_("f = open('{0}', 'wb')".format(filename)) size = len(data) # Loop through and write a buffer size chunk of data at a time. @@ -155,7 +175,7 @@ def put(self, filename, data): chunk = 'b' + chunk self._pyboard.exec_("f.write({0})".format(chunk)) self._pyboard.exec_('f.close()') - self._pyboard.exit_raw_repl() + self._pyboard.exit_raw() def rm(self, filename): """Remove the specified file or directory.""" @@ -166,7 +186,7 @@ def rm(self, filename): import uos as os os.remove('{0}') """.format(filename) - self._pyboard.enter_raw_repl() + self._pyboard.enter_raw() try: out = self._pyboard.exec_(textwrap.dedent(command)) except PyboardError as ex: @@ -182,7 +202,7 @@ def rm(self, filename): 'Directory is not empty: {0}'.format(filename)) else: raise ex - self._pyboard.exit_raw_repl() + self._pyboard.exit_raw() def rmdir(self, directory): """Forcefully remove the specified directory and all its children.""" @@ -213,7 +233,7 @@ def rmdir(directory): os.rmdir(directory) rmdir('{0}') """.format(directory) - self._pyboard.enter_raw_repl() + self._pyboard.enter_raw() try: out = self._pyboard.exec_(textwrap.dedent(command)) except PyboardError as ex: @@ -224,13 +244,20 @@ def rmdir(directory): raise RuntimeError('No such directory: {0}'.format(directory)) else: raise ex - self._pyboard.exit_raw_repl() + self._pyboard.exit_raw() def run(self, filename): """Run the provided script and show the output in realtime. If a print callback was provided in the pyboard module, it will be used to print the output instead of print() used by the ST console """ - self._pyboard.enter_raw_repl() + global SELECTED_TEXT + + if(SELECTED_TEXT): + filename = SELECTED_TEXT + + self._pyboard.enter_raw() self._pyboard.execfile(filename) - self._pyboard.exit_raw_repl() + self._pyboard.exit_raw() + + SELECTED_TEXT = None diff --git a/tools/ampy/pyboard.py b/tools/ampy/pyboard.py deleted file mode 100644 index 8e91efb..0000000 --- a/tools/ampy/pyboard.py +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/env python - -""" -pyboard interface - -This module provides the Pyboard class, used to communicate with and -control the pyboard over a serial USB connection. - -Example usage: - - import pyboard - pyb = pyboard.Pyboard('/dev/ttyACM0') - -Or: - - pyb = pyboard.Pyboard('192.168.1.1') - -Then: - - pyb.enter_raw_repl() - pyb.exec('pyb.LED(1).on()') - pyb.exit_raw_repl() - -Note: if using Python2 then pyb.exec must be written as pyb.exec_. -To run a script from the local machine on the board and print out the results: - - import pyboard - pyboard.execfile('test.py', device='/dev/ttyACM0') - -This script can also be run directly. To execute a local script, use: - - ./pyboard.py test.py - -Or: - - python pyboard.py test.py - -""" - -import sys -import time - -in_use = [] -serial_dict = {} - - -class PyboardError(BaseException): - pass - - -class TelnetToSerial: - - def __init__(self, ip, user, password, read_timeout=None): - import telnetlib - self.tn = telnetlib.Telnet(ip, timeout=15) - self.read_timeout = read_timeout - if b'Login as:' in self.tn.read_until(b'Login as:', timeout=read_timeout): - self.tn.write(bytes(user, 'ascii') + b"\r\n") - - if b'Password:' in self.tn.read_until(b'Password:', timeout=read_timeout): - # needed because of internal implementation details of the - # telnet server - time.sleep(0.2) - self.tn.write(bytes(password, 'ascii') + b"\r\n") - - if b'for more information.' in self.tn.read_until(b'Type "help()" for more information.', timeout=read_timeout): - # login succesful - from collections import deque - self.fifo = deque() - return - - raise PyboardError( - 'Failed to establish a telnet connection with the board') - - def __del__(self): - self.close() - - def close(self): - self.data_consumer("closed") - try: - self.tn.close() - except: - # the telnet object might not exist yet, so ignore this one - pass - - def read(self, size=1): - while len(self.fifo) < size: - timeout_count = 0 - data = self.tn.read_eager() - if len(data): - self.fifo.extend(data) - timeout_count = 0 - else: - time.sleep(0.25) - if self.read_timeout is not None and timeout_count > 4 * self.read_timeout: - break - timeout_count += 1 - - data = b'' - while len(data) < size and len(self.fifo) > 0: - data += bytes([self.fifo.popleft()]) - return data - - def write(self, data): - self.tn.write(data) - return len(data) - - def inWaiting(self): - n_waiting = len(self.fifo) - if not n_waiting: - data = self.tn.read_eager() - self.fifo.extend(data) - return len(data) - else: - return n_waiting - - -class Pyboard: - port = None - data_consumer = None - - def __init__(self, device, baudrate=115200, user='micro', password='python', wait=0, data_consumer=None): - self.data_consumer = data_consumer - if device and device[0].isdigit() and device[-1].isdigit() and device.count('.') == 3: - # device looks like an IP address - self.serial = TelnetToSerial( - device, user, password, read_timeout=10) - else: - from .. import pyserial as serial - delayed = False - for attempt in range(wait + 1): - try: - self.serial = serial.Serial( - device, baudrate=baudrate, interCharTimeout=1) - - in_use.append(device) - serial_dict[device] = self - - self.port = device - break - except (OSError, IOError): # Py2 and Py3 have different errors - if wait == 0: - continue - if attempt == 0: - sys.stdout.write( - 'Waiting {} seconds for pyboard '.format(wait)) - delayed = True - time.sleep(1) - sys.stdout.write('.') - sys.stdout.flush() - else: - if delayed: - print('') - raise PyboardError('failed to access ' + device) - if delayed: - print('') - - def write(self, data): - self.serial.write(data) - - def close(self): - in_use.remove(self.port) - del serial_dict[self.port] - - self.serial.close() - - def read_until(self, min_num_bytes, ending, timeout=10): - frepl = b'Type "help()" for more information' - data = self.serial.read(min_num_bytes) - - timeout_count = 0 - while True: - if data.endswith(ending): - break - if data.endswith(frepl): - data = b'' - # ctrl-A: enter raw REPL - self.serial.write(b'\r\x01') - elif self.serial.inWaiting() > 0: - new_data = self.serial.read(1) - data = data + new_data - timeout_count = 0 - else: - timeout_count += 1 - if timeout is not None and timeout_count >= 100 * timeout: - break - time.sleep(0.01) - - return data - - def enter_raw_repl(self): - # ctrl-C twice: interrupt any running program - self.serial.write(b'\r\x03\x03') - - # flush input (without relying on serial.flushInput()) - n = self.serial.inWaiting() - while n > 0: - self.serial.read(n) - n = self.serial.inWaiting() - - self.serial.write(b'\r\x01') # ctrl-A: enter raw REPL - data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n>') - if not data.endswith(b'raw REPL; CTRL-B to exit\r\n>'): - print(data) - raise PyboardError('could not enter raw repl') - - self.serial.write(b'\x04') # ctrl-D: soft reset - data = self.read_until(1, b'soft reboot\r\n') - if not data.endswith(b'soft reboot\r\n'): - print(data) - raise PyboardError('could not enter raw repl') - - # By splitting this into 2 reads, it allows boot.py to print stuff, - # which will show up after the soft reboot and before the raw REPL. - # Modification from original pyboard.py below: - # Add a small delay and send Ctrl-C twice after soft reboot to ensure - # any main program loop in main.py is interrupted. - - time.sleep(0.5) - - # interrupt any running program - self.serial.write(b'\x03\x03') - - # End modification above. - data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n') - if not data.endswith(b'raw REPL; CTRL-B to exit\r\n'): - print(data) - raise PyboardError('could not enter raw repl') - - def exit_raw_repl(self): - # ctrl-B: enter friendly REPL - self.serial.write(b'\r\x02') - - def eval(self, expression): - ret = self.exec_('print({})'.format(expression)) - ret = ret.strip() - return ret - - def exec_(self, command): - if isinstance(command, bytes): - command_bytes = command - else: - command_bytes = bytes(command, encoding='utf8') - - # check we have a prompt - data = self.read_until(1, b'>') - if not data.endswith(b'>'): - raise PyboardError('could not enter raw repl') - - # write command - for i in range(0, len(command_bytes), 256): - self.serial.write( - command_bytes[i:min(i + 256, len(command_bytes))]) - time.sleep(0.01) - self.serial.write(b'\x04') - - # check if we could exec command - data = self.serial.read(2) - if data != b'OK': - raise PyboardError('could not exec command') - - # add to lines in the console - self.data_consumer('\n\n') - # receive data from the serial port - self.receive_serial_data() - # Receive data after use '\x03' - self.receive_serial_data() - - def receive_serial_data(self): - data = b'' - - while(b'\x04' not in data): - data += self.serial.read(1) - - # avoid to print '\x04' char - if(data.endswith(b'\r\n') and b'\x04' not in data): - # normalizes end of line for ST - data = data.replace(b'\r\n', b'\n') - self.data_consumer(data) - data = b'' - - def execfile(self, filename): - with open(filename, 'rb') as f: - pyfile = f.read() - return self.exec_(pyfile) - - def get_time(self): - t = str(self.eval('pyb.RTC().datetime()'), - encoding='utf8')[1:-1].split(', ') - return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6]) diff --git a/tools/command.py b/tools/command.py index fdc0da3..bacb8a1 100644 --- a/tools/command.py +++ b/tools/command.py @@ -31,9 +31,11 @@ from sys import platform from subprocess import Popen, PIPE from functools import partial -from ..tools import message, paths from collections import deque +from ..tools import message, paths +from ..tools.thread_progress import ThreadProgress + _COMMAND_QUEUE = deque() _BUSY = False @@ -53,7 +55,10 @@ def __init__(self, cmd, listener): shell=True) if(self.proc.stdout): - threading.Thread(target=self.read_stdout).start() + th = threading.Thread(target=self.read_stdout) + th.start() + + ThreadProgress(th, '', '') if(self.proc.stderr): threading.Thread(target=self.read_stderr).start() diff --git a/tools/esptool.py b/tools/esptool.py index e804b43..84b5417 100644 --- a/tools/esptool.py +++ b/tools/esptool.py @@ -16,31 +16,43 @@ # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin # Street, Fifth Floor, Boston, MA 02110-1301 USA. -from __future__ import print_function, division +from __future__ import division, print_function import argparse +import base64 +import copy import hashlib import inspect +import io import os -import pyserial as serial +import shlex import struct import sys import time -import base64 import zlib -import shlex -import copy -import io -__version__ = "2.2-dev" +import pyserial as serial + +__version__ = "2.2" MAX_UINT32 = 0xffffffff MAX_UINT24 = 0xffffff -DEFAULT_TIMEOUT = 3 # timeout for most flash operations -START_FLASH_TIMEOUT = 20 # timeout for starting flash (may perform erase) -CHIP_ERASE_TIMEOUT = 120 # timeout for full chip erase -SYNC_TIMEOUT = 0.1 # timeout for syncing with bootloader +DEFAULT_TIMEOUT = 3 # timeout for most flash operations +START_FLASH_TIMEOUT = 20 # timeout for starting flash (may perform erase) +CHIP_ERASE_TIMEOUT = 120 # timeout for full chip erase +MAX_TIMEOUT = CHIP_ERASE_TIMEOUT * 2 # longest any command can run +SYNC_TIMEOUT = 0.1 # timeout for syncing with bootloader +MD5_TIMEOUT_PER_MB = 8 # timeout (per megabyte) for calculating md5sum +ERASE_REGION_TIMEOUT_PER_MB = 30 # timeout (per megabyte) for erasing a region + + +def timeout_per_mb(seconds_per_mb, size_bytes): + """ Scales timeouts which are size-specific """ + result = seconds_per_mb * (size_bytes / 1e6) + if result < DEFAULT_TIMEOUT: + return DEFAULT_TIMEOUT + return result DETECTED_FLASH_SIZES = {0x12: '256KB', 0x13: '512KB', 0x14: '1MB', @@ -109,23 +121,23 @@ class ESPLoader(object): # Commands supported by ESP8266 ROM bootloader ESP_FLASH_BEGIN = 0x02 - ESP_FLASH_DATA = 0x03 - ESP_FLASH_END = 0x04 - ESP_MEM_BEGIN = 0x05 - ESP_MEM_END = 0x06 - ESP_MEM_DATA = 0x07 - ESP_SYNC = 0x08 - ESP_WRITE_REG = 0x09 - ESP_READ_REG = 0x0a + ESP_FLASH_DATA = 0x03 + ESP_FLASH_END = 0x04 + ESP_MEM_BEGIN = 0x05 + ESP_MEM_END = 0x06 + ESP_MEM_DATA = 0x07 + ESP_SYNC = 0x08 + ESP_WRITE_REG = 0x09 + ESP_READ_REG = 0x0a # Some comands supported by ESP32 ROM bootloader (or -8266 w/ stub) ESP_SPI_SET_PARAMS = 0x0B - ESP_SPI_ATTACH = 0x0D + ESP_SPI_ATTACH = 0x0D ESP_CHANGE_BAUDRATE = 0x0F ESP_FLASH_DEFL_BEGIN = 0x10 - ESP_FLASH_DEFL_DATA = 0x11 - ESP_FLASH_DEFL_END = 0x12 - ESP_SPI_FLASH_MD5 = 0x13 + ESP_FLASH_DEFL_DATA = 0x11 + ESP_FLASH_DEFL_END = 0x12 + ESP_SPI_FLASH_MD5 = 0x13 # Some commands supported by stub only ESP_ERASE_FLASH = 0xD0 @@ -134,13 +146,12 @@ class ESPLoader(object): ESP_RUN_USER_CODE = 0xD3 # Maximum block sized for RAM and Flash writes, respectively. - ESP_RAM_BLOCK = 0x1800 + ESP_RAM_BLOCK = 0x1800 FLASH_WRITE_SIZE = 0x400 - # Default baudrate. The ROM auto-bauds, so we can use more or less - # whatever we want. - ESP_ROM_BAUD = 115200 + # Default baudrate. The ROM auto-bauds, so we can use more or less whatever we want. + ESP_ROM_BAUD = 115200 # First byte of the application image ESP_IMAGE_MAGIC = 0xe9 @@ -160,7 +171,7 @@ class ESPLoader(object): # The number of bytes in the UART response that signify command status STATUS_BYTES_LENGTH = 2 - def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD): + def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False): """Base constructor for ESPLoader bootloader interaction Don't call this constructor, either instantiate ESP8266ROM @@ -176,22 +187,22 @@ def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD): self._port = port else: self._port = serial.serial_for_url(port) - self._slip_reader = slip_reader(self._port) + self._slip_reader = slip_reader(self._port, self.trace) # setting baud rate in a separate step is a workaround for # CH341 driver on some Linux versions (this opens at 9600 then # sets), shouldn't matter for other platforms/drivers. See # https://github.com/espressif/esptool/issues/44#issuecomment-107094446 self._set_port_baudrate(baud) + self._trace_enabled = trace_enabled def _set_port_baudrate(self, baud): try: self._port.baudrate = baud except IOError: - raise FatalError( - "Failed to set baud rate %d. The driver may not support this rate." % baud) + raise FatalError("Failed to set baud rate %d. The driver may not support this rate." % baud) @staticmethod - def detect_chip(port=DEFAULT_PORT, baud=ESP_ROM_BAUD, connect_mode='default_reset'): + def detect_chip(port=DEFAULT_PORT, baud=ESP_ROM_BAUD, connect_mode='default_reset', trace_enabled=False): """ Use serial access to detect the chip type. We use the UART's datecode register for this, it's mapped at @@ -202,7 +213,7 @@ def detect_chip(port=DEFAULT_PORT, baud=ESP_ROM_BAUD, connect_mode='default_rese This routine automatically performs ESPLoader.connect() (passing connect_mode parameter) as part of querying the chip. """ - detect_port = ESPLoader(port, baud) + detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled) detect_port.connect(connect_mode) print('Detecting chip type...', end='') sys.stdout.flush() @@ -211,26 +222,36 @@ def detect_chip(port=DEFAULT_PORT, baud=ESP_ROM_BAUD, connect_mode='default_rese for cls in [ESP8266ROM, ESP32ROM]: if date_reg == cls.DATE_REG_VALUE: # don't connect a second time - inst = cls(detect_port._port, baud) + inst = cls(detect_port._port, baud, trace_enabled=trace_enabled) print(' %s' % inst.CHIP_NAME) return inst print('') - raise FatalError( - "Unexpected UART datecode value 0x%08x. Failed to autodetect chip type." % date_reg) + raise FatalError("Unexpected UART datecode value 0x%08x. Failed to autodetect chip type." % date_reg) """ Read a SLIP packet from the serial port """ - def read(self): return next(self._slip_reader) """ Write bytes to the serial port while performing SLIP escaping """ - def write(self, packet): buf = b'\xc0' \ - + (packet.replace(b'\xdb', b'\xdb\xdd').replace(b'\xc0', b'\xdb\xdc')) \ + + (packet.replace(b'\xdb',b'\xdb\xdd').replace(b'\xc0',b'\xdb\xdc')) \ + b'\xc0' + self.trace("Write %d bytes: %r", len(buf), buf) self._port.write(buf) + def trace(self, message, *format_args): + if self._trace_enabled: + now = time.time() + try: + + delta = now - self._last_trace + except AttributeError: + delta = 0.0 + self._last_trace = now + prefix = "TRACE +%.3f " % delta + print(prefix + (message % format_args)) + """ Calculate checksum of a blob, as it is defined by the ROM """ @staticmethod def checksum(data, state=ESP_CHECKSUM_MAGIC): @@ -243,70 +264,71 @@ def checksum(data, state=ESP_CHECKSUM_MAGIC): return state """ Send a request and read the response """ - - def command(self, op=None, data=b"", chk=0, wait_response=True): + def command(self, op=None, data=b"", chk=0, wait_response=True, timeout=DEFAULT_TIMEOUT): if op is not None: + self.trace("command op=0x%02x data len=%s wait_response=%d timeout=%.3f data=%r", + op, len(data), 1 if wait_response else 0, timeout, data) pkt = struct.pack(b' self.STATUS_BYTES_LENGTH: return data[:-self.STATUS_BYTES_LENGTH] - # otherwise, just return the 'val' field which comes from the reply - # header (this is used by read_reg) - else: + else: # otherwise, just return the 'val' field which comes from the reply header (this is used by read_reg) return val def flush_input(self): self._port.flushInput() - self._slip_reader = slip_reader(self._port) + self._slip_reader = slip_reader(self._port, self.trace) def sync(self): - self.command(self.ESP_SYNC, b'\x07\x07\x12\x20' + 32 * b'\x55') + self.command(self.ESP_SYNC, b'\x07\x07\x12\x20' + 32 * b'\x55', + timeout=SYNC_TIMEOUT) for i in range(7): self.command() @@ -348,13 +370,11 @@ def _connect_attempt(self, mode='default_reset', esp32r0_delay=False): time.sleep(0.05) self._port.setDTR(False) # IO0=HIGH, done - self._port.timeout = SYNC_TIMEOUT for _ in range(5): try: self.flush_input() self._port.flushOutput() self.sync() - self._port.timeout = DEFAULT_TIMEOUT return None except FatalError as e: if esp32r0_delay: @@ -374,53 +394,43 @@ def connect(self, mode='default_reset'): try: for _ in range(10): - last_error = self._connect_attempt( - mode=mode, esp32r0_delay=False) + last_error = self._connect_attempt(mode=mode, esp32r0_delay=False) if last_error is None: return - last_error = self._connect_attempt( - mode=mode, esp32r0_delay=True) + last_error = self._connect_attempt(mode=mode, esp32r0_delay=True) if last_error is None: return finally: print('') # end 'Connecting...' line - raise FatalError('Failed to connect to %s: %s' % - (self.CHIP_NAME, last_error)) + raise FatalError('Failed to connect to %s: %s' % (self.CHIP_NAME, last_error)) """ Read memory address in target """ - def read_reg(self, addr): # we don't call check_command here because read_reg() function is called # when detecting chip type, and the way we check for success (STATUS_BYTES_LENGTH) is different # for different chip types (!) val, data = self.command(self.ESP_READ_REG, struct.pack(' 32: - raise FatalError( - "Reading more than 32 bits back from a SPI flash operation is unsupported") + raise FatalError("Reading more than 32 bits back from a SPI flash operation is unsupported") if len(data) > 64: - raise FatalError( - "Writing more than 64 bytes of data with one SPI command is unsupported") + raise FatalError("Writing more than 64 bytes of data with one SPI command is unsupported") data_bits = len(data) * 8 old_spi_usr = self.read_reg(SPI_USR_REG) @@ -739,8 +733,7 @@ def set_data_lengths(mosi_bits, miso_bits): self.write_reg(SPI_USR2_REG, (7 << SPI_USR2_DLEN_SHIFT) | spiflash_command) if data_bits == 0: - # clear data register before we read it - self.write_reg(SPI_W0_REG, 0) + self.write_reg(SPI_W0_REG, 0) # clear data register before we read it else: data = pad_to(data, 4, b'\00') # pad to 32-bit multiple words = struct.unpack("I" * (len(data) // 4), data) @@ -770,7 +763,7 @@ def read_status(self, num_bytes=2): Not all SPI flash supports all three commands. The upper 1 or 2 bytes may be 0xFF. """ - SPIFLASH_RDSR = 0x05 + SPIFLASH_RDSR = 0x05 SPIFLASH_RDSR2 = 0x35 SPIFLASH_RDSR3 = 0x15 @@ -808,11 +801,9 @@ def write_status(self, new_status, num_bytes=2, set_non_volatile=False): # this may be redundant, but shouldn't hurt if num_bytes == 2: self.run_spiflash_command(enable_cmd) - self.run_spiflash_command( - SPIFLASH_WRSR, struct.pack(" 16: - raise FatalError( - 'Invalid firmware image magic=%d segments=%d' % (magic, segments)) - return segments + if magic != expected_magic or segments > 16: + raise FatalError('Invalid firmware image magic=%d segments=%d' % (magic, segments)) + return segments def load_segment(self, f, is_irom_segment=False): """ Load the next segment from the image file """ @@ -1156,8 +1141,7 @@ def load_segment(self, f, is_irom_segment=False): self.warn_if_unusual_segment(offset, size, is_irom_segment) segment_data = f.read(size) if len(segment_data) < size: - raise FatalError('End of file reading segment 0x%x, length %d (actual length %d)' % ( - offset, size, len(segment_data))) + raise FatalError('End of file reading segment 0x%x, length %d (actual length %d)' % (offset, size, len(segment_data))) segment = ImageSegment(offset, segment_data, file_offs) self.segments.append(segment) return segment @@ -1165,8 +1149,7 @@ def load_segment(self, f, is_irom_segment=False): def warn_if_unusual_segment(self, offset, size, is_irom_segment): if not is_irom_segment: if offset > 0x40200000 or offset < 0x3ffe0000 or size > 65536: - print('WARNING: Suspicious segment 0x%x, length %d' % - (offset, size)) + print('WARNING: Suspicious segment 0x%x, length %d' % (offset, size)) def save_segment(self, f, segment, checksum=None): """ Save the next segment to the image file, return next checksum value if provided """ @@ -1208,13 +1191,12 @@ def is_irom_addr(self, addr): return ESP8266ROM.IROM_MAP_START <= addr < ESP8266ROM.IROM_MAP_END def get_irom_segment(self): - irom_segments = [s for s in self.segments if self.is_irom_addr(s.addr)] - if len(irom_segments) > 0: - if len(irom_segments) != 1: - raise FatalError( - 'Found %d segments that could be irom0. Bad ELF file?' % len(irom_segments)) - return irom_segments[0] - return None + irom_segments = [s for s in self.segments if self.is_irom_addr(s.addr)] + if len(irom_segments) > 0: + if len(irom_segments) != 1: + raise FatalError('Found %d segments that could be irom0. Bad ELF file?' % len(irom_segments)) + return irom_segments[0] + return None def get_non_irom_segments(self): irom_segment = self.get_irom_segment() @@ -1233,8 +1215,7 @@ def __init__(self, load_file=None): self.version = 1 if load_file is not None: - segments = self.load_common_header( - load_file, ESPLoader.ESP_IMAGE_MAGIC) + segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) for _ in range(segments): self.load_segment(load_file) @@ -1273,21 +1254,17 @@ def __init__(self, load_file=None): super(OTAFirmwareImage, self).__init__() self.version = 2 if load_file is not None: - segments = self.load_common_header( - load_file, ESPBOOTLOADER.IMAGE_V2_MAGIC) + segments = self.load_common_header(load_file, ESPBOOTLOADER.IMAGE_V2_MAGIC) if segments != ESPBOOTLOADER.IMAGE_V2_SEGMENT: - # segment count is not really segment count here, but we expect - # to see '4' - print( - 'Warning: V2 header has unexpected "segment" count %d (usually 4)' % segments) + # segment count is not really segment count here, but we expect to see '4' + print('Warning: V2 header has unexpected "segment" count %d (usually 4)' % segments) # irom segment comes before the second header # # the file is saved in the image with a zero load address # in the header, so we need to calculate a load address irom_segment = self.load_segment(load_file, True) - # for actual mapped addr, add ESP8266ROM.IROM_MAP_START + - # flashing_Addr + 8 + # for actual mapped addr, add ESP8266ROM.IROM_MAP_START + flashing_Addr + 8 irom_segment.addr = 0 irom_segment.include_in_checksum = False @@ -1296,8 +1273,7 @@ def __init__(self, load_file=None): first_entrypoint = self.entrypoint # load the second header - segments = self.load_common_header( - load_file, ESPLoader.ESP_IMAGE_MAGIC) + segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) if first_flash_mode != self.flash_mode: print('WARNING: Flash mode value in first header (0x%02x) disagrees with second (0x%02x). Using second value.' @@ -1381,8 +1357,7 @@ def __init__(self, load_file=None): if load_file is not None: start = load_file.tell() - segments = self.load_common_header( - load_file, ESPLoader.ESP_IMAGE_MAGIC) + segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) self.load_extended_header(load_file) for _ in range(segments): @@ -1419,12 +1394,9 @@ def save(self, filename): checksum = ESPLoader.ESP_CHECKSUM_MAGIC - # split segments into flash-mapped vs ram-loaded, and take copies - # so we can mutate them - flash_segments = [copy.deepcopy(s) for s in sorted( - self.segments, key=lambda s:s.addr) if self.is_flash_addr(s.addr)] - ram_segments = [copy.deepcopy(s) for s in sorted( - self.segments, key=lambda s:s.addr) if not self.is_flash_addr(s.addr)] + # split segments into flash-mapped vs ram-loaded, and take copies so we can mutate them + flash_segments = [copy.deepcopy(s) for s in sorted(self.segments, key=lambda s:s.addr) if self.is_flash_addr(s.addr)] + ram_segments = [copy.deepcopy(s) for s in sorted(self.segments, key=lambda s:s.addr) if not self.is_flash_addr(s.addr)] IROM_ALIGN = 65536 @@ -1452,8 +1424,7 @@ def get_alignment_data_needed(segment): if pad_len == 0 or pad_len == IROM_ALIGN: return 0 # already aligned - # subtract SEG_HEADER_LEN a second time, as the padding block - # has a header as well + # subtract SEG_HEADER_LEN a second time, as the padding block has a header as well pad_len -= self.SEG_HEADER_LEN if pad_len < 0: pad_len += IROM_ALIGN @@ -1470,8 +1441,7 @@ def get_alignment_data_needed(segment): if len(ram_segments[0].data) == 0: ram_segments.pop(0) else: - pad_segment = ImageSegment( - 0, b'\x00' * pad_len, f.tell()) + pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) checksum = self.save_segment(f, pad_segment, checksum) total_segments += 1 else: @@ -1511,8 +1481,7 @@ def load_extended_header(self, load_file): def split_byte(n): return (n & 0x0F, (n >> 4) & 0x0F) - fields = list(struct.unpack( - self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))) + fields = list(struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))) self.wp_pin = fields[0] @@ -1524,15 +1493,14 @@ def split_byte(n): if fields[15] in [0, 1]: self.append_digest = (fields[15] == 1) else: - raise RuntimeError( - "Invalid value for append_digest field (0x%02x). Should be 0 or 1.", fields[15]) + raise RuntimeError("Invalid value for append_digest field (0x%02x). Should be 0 or 1.", fields[15]) # remaining fields in the middle should all be zero if any(f for f in fields[4:15] if f != 0): print("Warning: some reserved header fields have non-zero values. This image may be from a newer esptool.py?") def save_extended_header(self, save_file): - def join_byte(ln, hn): + def join_byte(ln,hn): return (ln & 0x0F) + ((hn & 0x0F) << 4) append_digest = 1 if self.append_digest else 0 @@ -1570,22 +1538,19 @@ def _read_elf_file(self, f): # read the ELF file header LEN_FILE_HEADER = 0x34 try: - (ident, _type, machine, _version, - self.entrypoint, _phoff, shoff, _flags, - _ehsize, _phentsize, _phnum, shentsize, - shnum, shstrndx) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER)) + (ident,_type,machine,_version, + self.entrypoint,_phoff,shoff,_flags, + _ehsize, _phentsize,_phnum, shentsize, + shnum,shstrndx) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER)) except struct.error as e: - raise FatalError( - "Failed to read a valid ELF header from %s: %s" % (self.name, e)) + raise FatalError("Failed to read a valid ELF header from %s: %s" % (self.name, e)) if byte(ident, 0) != 0x7f or ident[1:4] != b'ELF': raise FatalError("%s has invalid ELF magic header" % self.name) if machine != 0x5e: - raise FatalError("%s does not appear to be an Xtensa ELF file. e_machine=%04x" % ( - self.name, machine)) + raise FatalError("%s does not appear to be an Xtensa ELF file. e_machine=%04x" % (self.name, machine)) if shentsize != self.LEN_SEC_HEADER: - raise FatalError("%s has unexpected section header entry size 0x%x (not 0x28)" % ( - self.name, shentsize, self.LEN_SEC_HEADER)) + raise FatalError("%s has unexpected section header entry size 0x%x (not 0x28)" % (self.name, shentsize, self.LEN_SEC_HEADER)) if shnum == 0: raise FatalError("%s has 0 section headers" % (self.name)) self._read_sections(f, shoff, shnum, shstrndx) @@ -1595,44 +1560,35 @@ def _read_sections(self, f, section_header_offs, section_header_count, shstrndx) len_bytes = section_header_count * self.LEN_SEC_HEADER section_header = f.read(len_bytes) if len(section_header) == 0: - raise FatalError( - "No section header found at offset %04x in ELF file." % section_header_offs) + raise FatalError("No section header found at offset %04x in ELF file." % section_header_offs) if len(section_header) != (len_bytes): - raise FatalError("Only read 0x%x bytes from section header (expected 0x%x.) Truncated ELF file?" % ( - len(section_header), len_bytes)) + raise FatalError("Only read 0x%x bytes from section header (expected 0x%x.) Truncated ELF file?" % (len(section_header), len_bytes)) # walk through the section header and extract all sections - section_header_offsets = range( - 0, len(section_header), self.LEN_SEC_HEADER) + section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER) def read_section_header(offs): - name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from( - " 0: @@ -1805,8 +1762,7 @@ def read_mem(esp, args): def write_mem(esp, args): esp.write_reg(args.address, args.value, args.mask, 0) - print('Wrote %08x, mask %08x to %08x' % - (args.value, args.mask, args.address)) + print('Wrote %08x, mask %08x to %08x' % (args.value, args.mask, args.address)) def dump_mem(esp, args): @@ -1828,8 +1784,7 @@ def detect_flash_size(esp, args): size_id = flash_id >> 16 args.flash_size = DETECTED_FLASH_SIZES.get(size_id) if args.flash_size is None: - print('Warning: Could not auto-detect Flash size (FlashID=0x%x, SizeID=0x%x), defaulting to 4MB' % - (flash_id, size_id)) + print('Warning: Could not auto-detect Flash size (FlashID=0x%x, SizeID=0x%x), defaulting to 4MB' % (flash_id, size_id)) args.flash_size = '4MB' else: print('Auto-detected Flash size:', args.flash_size) @@ -1846,13 +1801,11 @@ def _update_image_flash_params(esp, address, args, image): return image # not flashing a bootloader, so don't modify this if args.flash_mode != 'keep': - flash_mode = {'qio': 0, 'qout': 1, - 'dio': 2, 'dout': 3}[args.flash_mode] + flash_mode = {'qio':0, 'qout':1, 'dio':2, 'dout': 3}[args.flash_mode] flash_freq = flash_size_freq & 0x0F if args.flash_freq != 'keep': - flash_freq = {'40m': 0, '26m': 1, - '20m': 2, '80m': 0xf}[args.flash_freq] + flash_freq = {'40m':0, '26m':1, '20m':2, '80m': 0xf}[args.flash_freq] flash_size = flash_size_freq & 0xF0 if args.flash_size != 'keep': @@ -1875,10 +1828,10 @@ def write_flash(esp, args): # verify file sizes fit in flash flash_end = flash_size_bytes(args.flash_size) for address, argfile in args.addr_filename: - argfile.seek(0, 2) # seek to end + argfile.seek(0,2) # seek to end if address + argfile.tell() > flash_end: raise FatalError(("File %s (length %d) at offset %d will not fit in %d bytes of flash. " + - "Use --flash-size argument, or change flashing address.") + "Use --flash-size argument, or change flashing address.") % (argfile.name, argfile.tell(), address, flash_end)) argfile.seek(0) @@ -1901,15 +1854,12 @@ def write_flash(esp, args): seq = 0 written = 0 t = time.time() - esp._port.timeout = min(DEFAULT_TIMEOUT * ratio, - CHIP_ERASE_TIMEOUT * 2) while len(image) > 0: - print('\rWriting at 0x%08x... (%d %%)' % (address + seq * - esp.FLASH_WRITE_SIZE, 100 * (seq + 1) // blocks), end='') + print('\rWriting at 0x%08x... (%d %%)' % (address + seq * esp.FLASH_WRITE_SIZE, 100 * (seq + 1) // blocks), end='') sys.stdout.flush() block = image[0:esp.FLASH_WRITE_SIZE] if args.compress: - esp.flash_defl_block(block, seq) + esp.flash_defl_block(block, seq, timeout=DEFAULT_TIMEOUT * ratio) else: # Pad the last block block = block + b'\xff' * (esp.FLASH_WRITE_SIZE - len(block)) @@ -1921,28 +1871,23 @@ def write_flash(esp, args): speed_msg = "" if args.compress: if t > 0.0: - speed_msg = " (effective %.1f kbit/s)" % (uncsize / - t * 8 / 1000) - print('\rWrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s...' % ( - uncsize, written, address, t, speed_msg)) + speed_msg = " (effective %.1f kbit/s)" % (uncsize / t * 8 / 1000) + print('\rWrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s...' % (uncsize, written, address, t, speed_msg)) else: if t > 0.0: speed_msg = " (%.1f kbit/s)" % (written / t * 8 / 1000) - print('\rWrote %d bytes at 0x%08x in %.1f seconds%s...' % - (written, address, t, speed_msg)) + print('\rWrote %d bytes at 0x%08x in %.1f seconds%s...' % (written, address, t, speed_msg)) try: res = esp.flash_md5sum(address, uncsize) if res != calcmd5: print('File md5: %s' % calcmd5) print('Flash md5: %s' % res) - print('MD5 of 0xFF is %s' % - (hashlib.md5(b'\xFF' * uncsize).hexdigest())) + print('MD5 of 0xFF is %s' % (hashlib.md5(b'\xFF' * uncsize).hexdigest())) raise FatalError("MD5 of file does not match data in flash!") else: print('Hash of data verified.') except NotImplementedInROMError: pass - esp._port.timeout = DEFAULT_TIMEOUT print('\nLeaving...') @@ -1964,8 +1909,7 @@ def write_flash(esp, args): def image_info(args): image = LoadFirmwareImage(args.chip, args.filename) print('Image version: %d' % image.version) - print('Entry point: %08x' % - image.entrypoint if image.entrypoint != 0 else 'Entry point not set') + print('Entry point: %08x' % image.entrypoint if image.entrypoint != 0 else 'Entry point not set') print('%d segments' % len(image.segments)) print idx = 0 @@ -1991,8 +1935,7 @@ def make_image(args): if len(args.segfile) == 0: raise FatalError('No segments specified') if len(args.segfile) != len(args.segaddr): - raise FatalError( - 'Number of specified files does not match number of specified addresses') + raise FatalError('Number of specified files does not match number of specified addresses') for (seg, addr) in zip(args.segfile, args.segaddr): data = open(seg, 'rb').read() image.segments.append(ImageSegment(addr, data)) @@ -2014,11 +1957,9 @@ def elf2image(args): image = OTAFirmwareImage() image.entrypoint = e.entrypoint image.segments = e.sections # ELFSection is a subclass of ImageSegment - image.flash_mode = {'qio': 0, 'qout': 1, - 'dio': 2, 'dout': 3}[args.flash_mode] + image.flash_mode = {'qio':0, 'qout':1, 'dio':2, 'dout': 3}[args.flash_mode] image.flash_size_freq = image.ROM_LOADER.FLASH_SIZES[args.flash_size] - image.flash_size_freq += {'40m': 0, '26m': 1, - '20m': 2, '80m': 0xf}[args.flash_freq] + image.flash_size_freq += {'40m':0, '26m':1, '20m':2, '80m': 0xf}[args.flash_freq] if args.output is None: args.output = image.default_output_name(args.input) @@ -2061,8 +2002,7 @@ def flash_id(esp, args): print('Manufacturer: %02x' % (flash_id & 0xff)) flid_lowbyte = (flash_id >> 16) & 0xFF print('Device: %02x%02x' % ((flash_id >> 8) & 0xff, flid_lowbyte)) - print('Detected flash size: %s' % - (DETECTED_FLASH_SIZES.get(flid_lowbyte, "Unknown"))) + print('Detected flash size: %s' % (DETECTED_FLASH_SIZES.get(flid_lowbyte, "Unknown"))) def read_flash(esp, args): @@ -2094,8 +2034,7 @@ def verify_flash(esp, args): image = _update_image_flash_params(esp, address, args, image) image_size = len(image) - print('Verifying 0x%x (%d) bytes @ 0x%08x in flash against %s...' % - (image_size, image_size, address, argfile.name)) + print('Verifying 0x%x (%d) bytes @ 0x%08x in flash against %s...' % (image_size, image_size, address, argfile.name)) # Try digest first, only read if there are differences. digest = esp.flash_md5sum(address, image_size) expected_digest = hashlib.md5(image).hexdigest() @@ -2111,8 +2050,7 @@ def verify_flash(esp, args): flash = esp.read_flash(address, image_size) assert flash != image diff = [i for i in range(image_size) if flash[i] != image[i]] - print('-- verify FAILED: %d differences, first @ 0x%08x' % - (len(diff), address + diff[0])) + print('-- verify FAILED: %d differences, first @ 0x%08x' % (len(diff), address + diff[0])) for d in diff: flash_byte = flash[d] image_byte = image[d] @@ -2146,8 +2084,7 @@ def version(args): def main(): - parser = argparse.ArgumentParser( - description='esptool.py v%s - ESP8266 ROM Bootloader Utility' % __version__, prog='esptool') + parser = argparse.ArgumentParser(description='esptool.py v%s - ESP8266 ROM Bootloader Utility' % __version__, prog='esptool') parser.add_argument('--chip', '-c', help='Target chip type', @@ -2182,6 +2119,11 @@ def main(): help="Disable launching the flasher stub, only talk to ROM bootloader. Some features will not be available.", action='store_true') + parser.add_argument( + '--trace', '-t', + help="Enable trace-level output of esptool.py interactions.", + action='store_true') + subparsers = parser.add_subparsers( dest='operation', help='Run esptool {command} -h for additional help') @@ -2199,26 +2141,21 @@ def add_spi_connection_arg(parent): parser_dump_mem = subparsers.add_parser( 'dump_mem', help='Dump arbitrary memory to disk') - parser_dump_mem.add_argument( - 'address', help='Base address', type=arg_auto_int) - parser_dump_mem.add_argument( - 'size', help='Size of region to dump', type=arg_auto_int) + parser_dump_mem.add_argument('address', help='Base address', type=arg_auto_int) + parser_dump_mem.add_argument('size', help='Size of region to dump', type=arg_auto_int) parser_dump_mem.add_argument('filename', help='Name of binary dump') parser_read_mem = subparsers.add_parser( 'read_mem', help='Read arbitrary memory location') - parser_read_mem.add_argument( - 'address', help='Address to read', type=arg_auto_int) + parser_read_mem.add_argument('address', help='Address to read', type=arg_auto_int) parser_write_mem = subparsers.add_parser( 'write_mem', help='Read-modify-write to arbitrary memory location') - parser_write_mem.add_argument( - 'address', help='Address to write', type=arg_auto_int) + parser_write_mem.add_argument('address', help='Address to write', type=arg_auto_int) parser_write_mem.add_argument('value', help='Value', type=arg_auto_int) - parser_write_mem.add_argument( - 'mask', help='Mask of bits to write', type=arg_auto_int) + parser_write_mem.add_argument('mask', help='Mask of bits to write', type=arg_auto_int) def add_spi_flash_subparsers(parent, is_elf2image): """ Add common parser arguments for SPI flash properties """ @@ -2226,12 +2163,10 @@ def add_spi_flash_subparsers(parent, is_elf2image): auto_detect = not is_elf2image parent.add_argument('--flash_freq', '-ff', help='SPI Flash frequency', - choices=extra_keep_args + - ['40m', '26m', '20m', '80m'], + choices=extra_keep_args + ['40m', '26m', '20m', '80m'], default=os.environ.get('ESPTOOL_FF', '40m' if is_elf2image else 'keep')) parent.add_argument('--flash_mode', '-fm', help='SPI Flash mode', - choices=extra_keep_args + - ['qio', 'qout', 'dio', 'dout'], + choices=extra_keep_args + ['qio', 'qout', 'dio', 'dout'], default=os.environ.get('ESPTOOL_FM', 'qio' if is_elf2image else 'keep')) parent.add_argument('--flash_size', '-fs', help='SPI Flash size in MegaBytes (1MB, 2MB, 4MB, 8MB, 16M)' ' plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)', @@ -2245,16 +2180,12 @@ def add_spi_flash_subparsers(parent, is_elf2image): parser_write_flash.add_argument('addr_filename', metavar='
', help='Address followed by binary filename, separated by space', action=AddrFilenamePairAction) add_spi_flash_subparsers(parser_write_flash, is_elf2image=False) - parser_write_flash.add_argument( - '--no-progress', '-p', help='Suppress progress output', action="store_true") + parser_write_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") parser_write_flash.add_argument('--verify', help='Verify just-written data on flash ' + '(mostly superfluous, data is read back during flashing)', action='store_true') - compress_args = parser_write_flash.add_mutually_exclusive_group( - required=False) - compress_args.add_argument( - '--compress', '-z', help='Compress data in transfer (default unless --no-stub is specified)', action="store_true", default=None) - compress_args.add_argument( - '--no-compress', '-u', help='Disable data compression during transfer (default if --no-stub is specified)', action="store_true") + compress_args = parser_write_flash.add_mutually_exclusive_group(required=False) + compress_args.add_argument('--compress', '-z', help='Compress data in transfer (default unless --no-stub is specified)',action="store_true", default=None) + compress_args.add_argument('--no-compress', '-u', help='Disable data compression during transfer (default if --no-stub is specified)',action="store_true") subparsers.add_parser( 'run', @@ -2269,21 +2200,16 @@ def add_spi_flash_subparsers(parent, is_elf2image): 'make_image', help='Create an application image from binary files') parser_make_image.add_argument('output', help='Output image file') - parser_make_image.add_argument( - '--segfile', '-f', action='append', help='Segment input file') - parser_make_image.add_argument( - '--segaddr', '-a', action='append', help='Segment base address', type=arg_auto_int) - parser_make_image.add_argument( - '--entrypoint', '-e', help='Address of entry point', type=arg_auto_int, default=0) + parser_make_image.add_argument('--segfile', '-f', action='append', help='Segment input file') + parser_make_image.add_argument('--segaddr', '-a', action='append', help='Segment base address', type=arg_auto_int) + parser_make_image.add_argument('--entrypoint', '-e', help='Address of entry point', type=arg_auto_int, default=0) parser_elf2image = subparsers.add_parser( 'elf2image', help='Create an application image from ELF file') parser_elf2image.add_argument('input', help='Input ELF file') - parser_elf2image.add_argument( - '--output', '-o', help='Output filename prefix (for version 1 image), or filename (for version 2 single image)', type=str) - parser_elf2image.add_argument( - '--version', '-e', help='Output image version', choices=['1', '2'], default='1') + parser_elf2image.add_argument('--output', '-o', help='Output filename prefix (for version 1 image), or filename (for version 2 single image)', type=str) + parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1','2'], default='1') add_spi_flash_subparsers(parser_elf2image, is_elf2image=True) @@ -2305,32 +2231,25 @@ def add_spi_flash_subparsers(parent, is_elf2image): help='Read SPI flash status register') add_spi_connection_arg(parser_read_status) - parser_read_status.add_argument( - '--bytes', help='Number of bytes to read (1-3)', type=int, choices=[1, 2, 3], default=2) + parser_read_status.add_argument('--bytes', help='Number of bytes to read (1-3)', type=int, choices=[1,2,3], default=2) parser_write_status = subparsers.add_parser( 'write_flash_status', help='Write SPI flash status register') add_spi_connection_arg(parser_write_status) - parser_write_status.add_argument( - '--non-volatile', help='Write non-volatile bits (use with caution)', action='store_true') - parser_write_status.add_argument( - '--bytes', help='Number of status bytes to write (1-3)', type=int, choices=[1, 2, 3], default=2) - parser_write_status.add_argument( - 'value', help='New value', type=arg_auto_int) + parser_write_status.add_argument('--non-volatile', help='Write non-volatile bits (use with caution)', action='store_true') + parser_write_status.add_argument('--bytes', help='Number of status bytes to write (1-3)', type=int, choices=[1,2,3], default=2) + parser_write_status.add_argument('value', help='New value', type=arg_auto_int) parser_read_flash = subparsers.add_parser( 'read_flash', help='Read SPI flash content') add_spi_connection_arg(parser_read_flash) - parser_read_flash.add_argument( - 'address', help='Start address', type=arg_auto_int) - parser_read_flash.add_argument( - 'size', help='Size of region to dump', type=arg_auto_int) + parser_read_flash.add_argument('address', help='Start address', type=arg_auto_int) + parser_read_flash.add_argument('size', help='Size of region to dump', type=arg_auto_int) parser_read_flash.add_argument('filename', help='Name of binary dump') - parser_read_flash.add_argument( - '--no-progress', '-p', help='Suppress progress output', action="store_true") + parser_read_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") parser_verify_flash = subparsers.add_parser( 'verify_flash', @@ -2350,16 +2269,13 @@ def add_spi_flash_subparsers(parent, is_elf2image): 'erase_region', help='Erase a region of the flash') add_spi_connection_arg(parser_erase_region) - parser_erase_region.add_argument( - 'address', help='Start address (must be multiple of 4096)', type=arg_auto_int) - parser_erase_region.add_argument( - 'size', help='Size of region to erase (must be multiple of 4096)', type=arg_auto_int) + parser_erase_region.add_argument('address', help='Start address (must be multiple of 4096)', type=arg_auto_int) + parser_erase_region.add_argument('size', help='Size of region to erase (must be multiple of 4096)', type=arg_auto_int) subparsers.add_parser( 'version', help='Print esptool version') - # internal sanity check - every operation matches a module function of the - # same name + # internal sanity check - every operation matches a module function of the same name for operation in subparsers.choices.keys(): assert operation in globals(), "%s should be a module function" % operation @@ -2384,18 +2300,16 @@ def add_spi_flash_subparsers(parent, is_elf2image): else: operation_args = inspect.getfullargspec(operation_func).args - # operation function takes an ESPLoader connection object - if operation_args[0] == 'esp': - # don't sync faster than the default baud rate - initial_baud = min(ESPLoader.ESP_ROM_BAUD, args.baud) + if operation_args[0] == 'esp': # operation function takes an ESPLoader connection object + initial_baud = min(ESPLoader.ESP_ROM_BAUD, args.baud) # don't sync faster than the default baud rate if args.chip == 'auto': - esp = ESPLoader.detect_chip(args.port, initial_baud, args.before) + esp = ESPLoader.detect_chip(args.port, initial_baud, args.before, args.trace) else: chip_class = { 'esp8266': ESP8266ROM, 'esp32': ESP32ROM, }[args.chip] - esp = chip_class(args.port, initial_baud) + esp = chip_class(args.port, initial_baud, args.trace) esp.connect(args.before) print("Chip is %s" % (esp.get_chip_description())) @@ -2407,14 +2321,12 @@ def add_spi_flash_subparsers(parent, is_elf2image): try: esp.change_baud(args.baud) except NotImplementedInROMError: - print( - "WARNING: ROM doesn't support changing baud rate. Keeping initial baud rate %d" % initial_baud) + print("WARNING: ROM doesn't support changing baud rate. Keeping initial baud rate %d" % initial_baud) # override common SPI flash parameter stuff if configured to do so if hasattr(args, "spi_connection") and args.spi_connection is not None: if esp.CHIP_NAME != "ESP32": - raise FatalError( - "Chip %s does not support --spi-connection option." % esp.CHIP_NAME) + raise FatalError("Chip %s does not support --spi-connection option." % esp.CHIP_NAME) print("Configuring SPI flash mode...") esp.flash_spi_attach(args.spi_connection) elif args.no_stub: @@ -2456,7 +2368,7 @@ def expand_file_arguments(): for arg in sys.argv: if arg.startswith("@"): expanded = True - with open(arg[1:], "r") as f: + with open(arg[1:],"r") as f: for line in f.readlines(): new_args += shlex.split(line) else: @@ -2471,10 +2383,8 @@ class FlashSizeAction(argparse.Action): (At next major relase, remove deprecated sizes and this can become a 'normal' choices= argument again.) """ - def __init__(self, option_strings, dest, nargs=1, auto_detect=False, **kwargs): - super(FlashSizeAction, self).__init__( - option_strings, dest, nargs, **kwargs) + super(FlashSizeAction, self).__init__(option_strings, dest, nargs, **kwargs) self._auto_detect = auto_detect def __call__(self, parser, namespace, values, option_string=None): @@ -2488,8 +2398,7 @@ def __call__(self, parser, namespace, values, option_string=None): '16m-c1': '2MB-c1', '32m-c1': '4MB-c1', }[values[0]] - print("WARNING: Flash size arguments in megabits like '%s' are deprecated." % ( - values[0])) + print("WARNING: Flash size arguments in megabits like '%s' are deprecated." % (values[0])) print("Please use the equivalent size '%s'." % (value)) print("Megabit arguments may be removed in a future release.") except KeyError: @@ -2500,15 +2409,13 @@ def __call__(self, parser, namespace, values, option_string=None): if self._auto_detect: known_sizes['detect'] = 'detect' if value not in known_sizes: - raise argparse.ArgumentError(self, '%s is not a known flash size. Known sizes: %s' % ( - value, ", ".join(known_sizes.keys()))) + raise argparse.ArgumentError(self, '%s is not a known flash size. Known sizes: %s' % (value, ", ".join(known_sizes.keys()))) setattr(namespace, self.dest, value) class SpiConnectionAction(argparse.Action): """ Custom action to parse 'spi connection' override. Values are SPI, HSPI, or a sequence of 5 pin numbers separated by commas. """ - def __call__(self, parser, namespace, value, option_string=None): if value.upper() == "SPI": value = 0 @@ -2517,19 +2424,16 @@ def __call__(self, parser, namespace, value, option_string=None): elif "," in value: values = value.split(",") if len(values) != 5: - raise argparse.ArgumentError( - self, '%s is not a valid list of comma-separate pin numbers. Must be 5 numbers - CLK,Q,D,HD,CS.' % value) + raise argparse.ArgumentError(self, '%s is not a valid list of comma-separate pin numbers. Must be 5 numbers - CLK,Q,D,HD,CS.' % value) try: - values = tuple(int(v, 0) for v in values) + values = tuple(int(v,0) for v in values) except ValueError: - raise argparse.ArgumentError( - self, '%s is not a valid argument. All pins must be numeric values' % values) + raise argparse.ArgumentError(self, '%s is not a valid argument. All pins must be numeric values' % values) if any([v for v in values if v > 33 or v < 0]): - raise argparse.ArgumentError( - self, 'Pin numbers must be in the range 0-33.') + raise argparse.ArgumentError(self, 'Pin numbers must be in the range 0-33.') # encode the pin numbers as a 32-bit integer with packed 6-bit values, the same way ESP32 ROM takes them # TODO: make this less ESP32 ROM specific somehow... - clk, q, d, hd, cs = values + clk,q,d,hd,cs = values value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk else: raise argparse.ArgumentError(self, '%s is not a valid spi-connection value. ' + @@ -2539,41 +2443,35 @@ def __call__(self, parser, namespace, value, option_string=None): class AddrFilenamePairAction(argparse.Action): """ Custom parser class for the address/filename pairs passed as arguments """ - def __init__(self, option_strings, dest, nargs='+', **kwargs): - super(AddrFilenamePairAction, self).__init__( - option_strings, dest, nargs, **kwargs) + super(AddrFilenamePairAction, self).__init__(option_strings, dest, nargs, **kwargs) def __call__(self, parser, namespace, values, option_string=None): # validate pair arguments pairs = [] - for i in range(0, len(values), 2): + for i in range(0,len(values),2): try: - address = int(values[i], 0) + address = int(values[i],0) except ValueError as e: - raise argparse.ArgumentError( - self, 'Address "%s" must be a number' % values[i]) + raise argparse.ArgumentError(self,'Address "%s" must be a number' % values[i]) try: argfile = open(values[i + 1], 'rb') except IOError as e: raise argparse.ArgumentError(self, e) except IndexError: - raise argparse.ArgumentError( - self, 'Must be pairs of an address and the binary filename to write there') + raise argparse.ArgumentError(self,'Must be pairs of an address and the binary filename to write there') pairs.append((address, argfile)) # Sort the addresses and check for overlapping end = 0 for address, argfile in sorted(pairs): - argfile.seek(0, 2) # seek to end + argfile.seek(0,2) # seek to end size = argfile.tell() argfile.seek(0) sector_start = address & ~(ESPLoader.FLASH_SECTOR_SIZE - 1) - sector_end = ((address + size + ESPLoader.FLASH_SECTOR_SIZE - 1) - & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) - 1 + sector_end = ((address + size + ESPLoader.FLASH_SECTOR_SIZE - 1) & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) - 1 if sector_start < end: - message = 'Detected overlap at address: 0x%x for file: %s' % ( - address, argfile.name) + message = 'Detected overlap at address: 0x%x for file: %s' % (address, argfile.name) raise argparse.ArgumentError(self, message) end = sector_end setattr(namespace, self.dest, pairs) @@ -2581,102 +2479,104 @@ def __call__(self, parser, namespace, values, option_string=None): # Binary stub code (see flasher_stub dir for source & details) ESP8266ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" -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\ +neIoDdsQM/v3/wLWvxs5\ """))) ESP32ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" -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\ +S0yomEyqQut//QdAk8Ky\ """))) diff --git a/tools/message.py b/tools/message.py index fc30373..6a73896 100644 --- a/tools/message.py +++ b/tools/message.py @@ -26,12 +26,13 @@ # SOFTWARE. import sublime -from sublime_plugin import EventListener +import sublime_plugin import collections import threading from .. import tools +from ..tools import status_color session = None close_panel = False @@ -39,7 +40,7 @@ class Message: - BLOCK_SIZE = 2 ** 14 + port = None text_queue = collections.deque() text_queue_lock = threading.Lock() @@ -168,9 +169,6 @@ def check_message_group(port): if(view and viewer_name in view.name()): group_index = window.get_view_index(view)[0] - -class CloseConsole(EventListener): - def on_pre_close(self, view): """Check console panel @@ -184,7 +182,11 @@ def on_pre_close(self, view): global close_panel if(viewer_name in view.name()): - close_panel = True + window = view.window() + group = window.get_view_index(view)[0] + close_panel = [window, group] + + self.port = view.name().split(' | ')[1] def on_close(self, view): """Close console close @@ -197,11 +199,20 @@ def on_close(self, view): global close_panel if(close_panel): - window = sublime.active_window() - active_group = window.active_group() + window = close_panel[0] - if len(window.views_in_group(active_group)) == 0: + if(len(window.views_in_group(close_panel[1])) == 0): window.run_command("destroy_pane", args={"direction": "self"}) + + # closes serial connection + from ..tools import serial + + if(self.port in serial.in_use): + link = serial.serial_dict[self.port] + link.disconnect() + link.destroy() + status_color.set("error", 2000) + close_panel = False diff --git a/tools/paths.py b/tools/paths.py index c36ff02..5cc13af 100644 --- a/tools/paths.py +++ b/tools/paths.py @@ -43,6 +43,15 @@ def plugin_folder(): return plugin_path +def status_color_folder(): + """ + Packages/User/Status Color/ + """ + plugin = path.dirname(plugin_folder()) + status_color = path.join(plugin, 'User', 'Status Color') + return status_color + + def plugin_name(): """ Get the plugin folder name diff --git a/tools/pyserial/serialposix.py b/tools/pyserial/serialposix.py index f8906a9..fe651c7 100644 --- a/tools/pyserial/serialposix.py +++ b/tools/pyserial/serialposix.py @@ -35,6 +35,7 @@ import sys import termios +from .serialutil import * from .serialutil import SerialBase, SerialException, to_bytes, \ portNotOpenError, writeTimeoutError, Timeout @@ -370,27 +371,27 @@ def _reconfigure_port(self, force_update=False): else: raise ValueError('Invalid char len: {!r}'.format(self._bytesize)) # setup stop bits - if self._stopbits == serial.STOPBITS_ONE: + if self._stopbits == STOPBITS_ONE: cflag &= ~(termios.CSTOPB) - elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE: + elif self._stopbits == STOPBITS_ONE_POINT_FIVE: cflag |= (termios.CSTOPB) # XXX same as TWO.. there is no POSIX support for 1.5 - elif self._stopbits == serial.STOPBITS_TWO: + elif self._stopbits == STOPBITS_TWO: cflag |= (termios.CSTOPB) else: raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits)) # setup parity iflag &= ~(termios.INPCK | termios.ISTRIP) - if self._parity == serial.PARITY_NONE: + if self._parity == PARITY_NONE: cflag &= ~(termios.PARENB | termios.PARODD | CMSPAR) - elif self._parity == serial.PARITY_EVEN: + elif self._parity == PARITY_EVEN: cflag &= ~(termios.PARODD | CMSPAR) cflag |= (termios.PARENB) - elif self._parity == serial.PARITY_ODD: + elif self._parity == PARITY_ODD: cflag &= ~CMSPAR cflag |= (termios.PARENB | termios.PARODD) - elif self._parity == serial.PARITY_MARK and CMSPAR: + elif self._parity == PARITY_MARK and CMSPAR: cflag |= (termios.PARENB | CMSPAR | termios.PARODD) - elif self._parity == serial.PARITY_SPACE and CMSPAR: + elif self._parity == PARITY_SPACE and CMSPAR: cflag |= (termios.PARENB | CMSPAR) cflag &= ~(termios.PARODD) else: diff --git a/tools/repl.py b/tools/repl.py new file mode 100644 index 0000000..3a481d1 --- /dev/null +++ b/tools/repl.py @@ -0,0 +1,225 @@ +import time +import sublime + +from . import serial +from threading import Thread + + +class Repl: + + def __init__(self, serial, data_consumer): + self.data_consumer = data_consumer + self.serial = serial + + def enter_raw(self): + """Enters in raw repl mode + + Enters in the raw repl, to do it, it first checks send two cancel + commands and then reads the data in the serial port. It's made to + avoid run flushIn(). If there is not data after 450ms, the raw REPL + command will be send once, and then, the loop will be waiting for the + "raw REPL; CTRL-B to exit" text. it will confirm the raw REPL mode. + """ + output_start = getMillTime() + current_time = getMillTime() + cmd_repl = False + data = b'' + + # ctrl-C ctrl-C + self.serial.write(b'\r\x03\x03') + + while(True): + current_time = getMillTime() + + # if there no data for 450ms send raw repl command + if(current_time - output_start > 450 and not cmd_repl): + self.exit_raw() + # ctrl-A raw REPL + self.serial.write(b'\r\x01') + cmd_repl = True + + if(self.serial.inWaiting() > 0): + # reset counter + output_start = getMillTime() + # read + r = self.serial.readline() + if(r.endswith(b'raw REPL; CTRL-B to exit\r\n')): + break + + # if end of file char detected break loop + if(r.endswith(b'\x04')): + break + + def exit_raw(self): + """Close raw repl mode + + Sends the \x02 command to enter in the friendly REPL mode + """ + # ctrl-B: enter friendly REPL + self.serial.write(b'\r\x02') + + def receive_serial_data(self, quiet=True): + """Read serial data comming + + Reads the serial data comming throug the serial port. + The data_consumer method is used to print the data in the + console in realtime. When the loop receives the \x04 char + it will end and the session data will be returned + + Keyword Arguments: + quiet {bool} -- When it's false no data is printed in the console + (default: {True}) + + Returns: + byte str -- all data received while the loop was running + """ + data = b'' + session_data = b'' + + # add to lines in the console + if(not quiet): + self.data_consumer('\n\n') + + while(True): + + if(self.serial.inWaiting() > 0): + data += self.serial.read(1) + + if(data.endswith(b'\x04')): + break + + if(data.endswith(b'\r\n')): + # normalizes end of line for ST + data = data.replace(b'\r\r\n', b'\n') + data = data.replace(b'\r\n', b'\n') + + session_data += data + + if(not quiet): + self.data_consumer(data) + data = b'' + + return session_data + + def read_until(self, min_bytes, ending, timeout=10, quiet=True): + """Read until givin string + + Reads until find the given string otherwise, wait until the + timeout is met. + + Arguments: + min_bytes {int} -- minimum bytes to read each time in serial.read + ending {str} -- char o string to search + + Keyword Arguments: + timeout {number} -- time to stop the execution if + there not match (default: {10}) + quiet {bool} -- When it's false displays the output in the + data_consumer function (default: {True}) + + Returns: + [str] -- All data received by the serial while the method runs + """ + data = self.serial.read(1) + + if(not quiet): + self.data_consumer(data) + + time_count = 0 + + while True: + if(data.endswith(ending)): + break + elif(self.serial.inWaiting() > 0): + new_data = self.serial.read(1) + data += new_data + if(not quiet): + self.data_consumer(new_data) + time_count = 0 + else: + time_count += 1 + if(timeout is not None and time_count >= 100 * timeout): + break + time.sleep(0.01) + return data + + def exec_(self, command, quiet=True): + """Executes a command + + Convert the command in bytes and execute it in the + remote device, after that the output will be printed + and/or received + + Arguments: + command {str/byte} -- command to run in the device + + Keyword Arguments: + quiet {bool} -- When it's false, no data is printed in the console + (default: {True}) + + Returns: + byte str -- data received after sends the commands + + Raises: + ReplError -- error when command confirmation is not get + """ + if isinstance(command, bytes): + cmd_bytes = command + else: + cmd_bytes = bytes(command, encoding='utf8') + + # check prompt + data = self.read_until(1, b'>') + if(not data.endswith(b'>')): + raise PyboardError('could not enter raw repl') + + # write command + for i in range(0, len(cmd_bytes), 256): + self.serial.write(cmd_bytes[i:min(i + 256, len(cmd_bytes))]) + time.sleep(0.01) + self.serial.write(b'\x04') + + # check if we could exec command + data = self.serial.read(2) + if b'OK' not in data: + raise ReplError('could not exec command') + + # receive data from the serial port + out = self.receive_serial_data(quiet) + + return out + + def execfile(self, filename): + """Execute file + + Opens and return the conten of a local file + + Arguments: + filename {str} -- path of the file to open + + Returns: + bytes -- file content + """ + + try: + with open(filename, 'rb') as f: + pyfile = f.read() + except OSError as e: + pyfile = filename + + return self.exec_(pyfile, quiet=False) + + +class ReplError(BaseException): + pass + + +def getMillTime(): + """Current time + + Current time in milliseconds + + Returns: + int -- current time + """ + return int(round(time.time() * 1000)) diff --git a/tools/sampy.py b/tools/sampy.py index cfc8a3c..0c1942e 100644 --- a/tools/sampy.py +++ b/tools/sampy.py @@ -28,7 +28,8 @@ from sublime import platform, set_timeout_async from ..tools.ampy import files -from ..tools.ampy import pyboard +from ..tools import serial +from ..tools.repl import Repl _board = None @@ -55,7 +56,14 @@ def __init__(self, port, baudrate=115200, data_consumer=None): # in windows_full_port_name function). if platform() == 'windows': port = windows_full_port_name(port) - _board = pyboard.Pyboard(port, baudrate, data_consumer=data_consumer) + + try: + raw = serial.serial_dict[port].raw() + except KeyError as e: + print("console not connected") + return + + _board = Repl(serial=raw, data_consumer=data_consumer) def get(self, remote_file, local_file=None): """ @@ -107,7 +115,7 @@ def mkdir(self, directory): board_files = files.Files(_board) board_files.mkdir(directory) - def ls(self, directory='/'): + def ls(self, directory=''): """List contents of a directory on the board. Can pass an optional argument which is the path to the directory. The @@ -179,10 +187,10 @@ def put(self, local, remote=None): board_files.mkdir(remote_parent) # Loop through all the files and put them on the board too. for filename in child_files: - with open(os.path.join(parent, filename), 'rb') as infile: + with open(os.path.join(parent, filename), 'rb') as f: remote_filename = posixpath.join( remote_parent, filename) - board_files.put(remote_filename, infile.read()) + board_files.put(remote_filename, f.read()) except files.DirectoryExistsError: # Ignore errors for directories that already exist. pass diff --git a/tools/sampy_manager.py b/tools/sampy_manager.py index 6e5d42e..716113f 100644 --- a/tools/sampy_manager.py +++ b/tools/sampy_manager.py @@ -26,15 +26,15 @@ from os import path, mkdir from ..tools import check_sidebar_folder, make_folder as mkfolder -from ..tools import message, serial, errors +from ..tools import message, serial, errors, status_color from ..tools.sampy import Sampy -from ..tools.ampy import files, pyboard +from ..tools.ampy import files txt = None port = None -def start_sampy(): +def start_sampy(quiet=False): """ Opens the sampy connection in the selected port. If already is a serial connection running it will look into it and close it. @@ -48,22 +48,23 @@ def start_sampy(): port = serial.selected_port() # close the current connection in open port - if(port in serial.in_use): + if(port in serial.in_use and not quiet): run_serial = serial.serial_dict[port] - run_serial.close() + run_serial.stop_task() # message printer txt = message.open(port) txt.set_focus() - if(port): + if(port and not quiet): try: return Sampy(port, data_consumer=txt.print) - except pyboard.PyboardError as e: + except file.PyboardError as e: from sys import exit if('failed to access' in str(e)): txt.print(errors.serialError_noaccess) + status_color.remove() exit(0) @@ -98,11 +99,11 @@ def run_file(filepath): head = '\n\n>> Run {0}\n\n"ctrl+shift+c" to stop the script.\n---' txt.print(head.format(file)) - sampy.run(filepath) - - txt.print("\n[done]") - - sampy.close() + try: + sampy.run(filepath) + txt.print("\n[done]") + except AttributeError as e: + txt.print("\n\nOpening console...\nRun the command again.") finished_action() @@ -116,10 +117,11 @@ def list_files(): txt.print('\n\n>> sampy ls\n') - for filename in sampy.ls(): - txt.print('\n' + filename) - - sampy.close() + try: + for filename in sampy.ls(): + txt.print('\n' + filename) + except AttributeError as e: + txt.print("\n\nOpening console...\nRun the command again.") finished_action() @@ -144,8 +146,6 @@ def get_file(filename): txt.print('\n\n' + output) - sampy.close() - finished_action() @@ -155,32 +155,41 @@ def get_files(destination): Gets all the files in the device and stored in the selected destination path. """ + error = False sampy = start_sampy() destination = path.normpath(destination) mkfolder(destination) - txt.print('\n\n>> get from device to {0}'.format(destination)) + txt.print('\n\n>> Storing in {0}\n'.format(destination)) - for filename in sampy.ls(): - filepath = path.normpath(path.join(destination, filename)) - if(filename.endswith('/')): - if(not path.exists(filepath)): - mkdir(filepath) - else: - with open(filepath, 'w') as file: - file.write(sampy.get(filename)) - - txt.print("\n[done]") + try: + for filename in sampy.ls(): + txt.print('\nRetrieving ' + filename + ' ...') + + filepath = path.normpath(path.join(destination, filename)) + if(filename.endswith('/')): + if(not path.exists(filepath)): + mkdir(filepath) + else: + with open(filepath, 'wb') as file: + sampy.get(filename, file) + output = '[done]' + except AttributeError as e: + output = 'Opening console...\nRun the command again.' + error = True - sampy.close() + txt.print('\n\n' + output) finished_action() + if(error): + return + if(check_sidebar_folder(destination)): return - caption = "files retrieved, would you like to" \ + caption = "files retrieved, would you like to " \ "add the folder to your current proyect?" answer = sublime.yes_no_cancel_dialog(caption, "Add", "Append") @@ -206,18 +215,24 @@ def put_file(filepath): """ sampy = start_sampy() - file = path.basename(filepath) - txt.print('\n\n>> put {0}'.format(file)) - try: - sampy.put(path.normpath(filepath)) - output = '[done]' - except FileNotFoundError as e: - output = str(e) - - txt.print('\n\n' + output) + file = path.basename(filepath) + txt.print('\n\n>> put {0}'.format(file)) - sampy.close() + try: + sampy.put(path.normpath(filepath)) + output = '[done]' + except FileNotFoundError as e: + output = str(e) + except files.PyboardError as e: + txt.print('\n\nError putting the file.\nReason: ' + str(e)) + return finished_action() + except AttributeError as e: + output = 'Opening console...\nRun the command again.' + txt.print('\n\n' + output) + + except TypeError as e: + txt.print("\n\n" + str(e)) finished_action() @@ -240,11 +255,11 @@ def remove_file(filepath): output = '[done]' except RuntimeError as e: output = str(e) + except AttributeError as e: + output = 'Opening console...\nRun the command again.' txt.print('\n\n' + output) - sampy.close() - finished_action() @@ -265,11 +280,11 @@ def make_folder(folder_name): output = '[done]' except files.DirectoryExistsError as e: output = str(e) + except AttributeError as e: + output = 'Opening console...\nRun the command again.' txt.print('\n\n' + output) - sampy.close() - finished_action() @@ -290,11 +305,11 @@ def remove_folder(folder_name): output = '[done]' except RuntimeError as e: output = str(e) + except AttributeError as e: + output = 'Opening console...\nRun the command again.' txt.print('\n\n' + output) - sampy.close() - finished_action() @@ -303,7 +318,7 @@ def help(): Displays the sampy command usage """ - start_sampy() + start_sampy(quiet=True) _help = """\n\nUsage: sampy COMMAND [ARGS]... @@ -329,3 +344,4 @@ def help(): """.replace(' ', '') txt.print(_help) + finished_action() diff --git a/tools/serial.py b/tools/serial.py index 3bd0586..4c717da 100644 --- a/tools/serial.py +++ b/tools/serial.py @@ -32,6 +32,7 @@ from ..tools import SETTINGS_NAME from .pyserial.tools import list_ports from ..tools import errors +from ..tools import status_color in_use = [] serial_dict = {} @@ -45,6 +46,9 @@ def __init__(self, port, baudrate=115200, timeout=1): self._serial.port = port self._serial.baudrate = baudrate self._serial.timeout = timeout + self._serial.dtr = False + self._serial.rts = False + self._serial.interCharTimeout = 1 self._stop_task = True @@ -56,13 +60,35 @@ def open(self): in_use: list of port already open serial_dict: dictionary with the serial object """ + global in_use + global serial_dict + self._serial.open() self._stop_task = False # store port used port = self._serial.port - in_use.append(port) - serial_dict[port] = self + if(port not in in_use): + in_use.append(port) + serial_dict[port] = self + + def raw(self): + """Return Serial object + + Returns the Serial object from pyserial instead of this serial class + + Returns: + Serial -- serial object + """ + return self._serial + + def stop_task(self): + """Stop keep listen + + Stops the keep_listen loop and flush the in and out serial data + """ + self._stop_task = True + self.flush() def receive(self): """Receive data @@ -98,7 +124,7 @@ def is_running(self): Returns: bool -- True if the port is running (open) false if not """ - return not self._stop_task + return self._serial.is_open def write(self, data): """Write bytedata to the port @@ -141,12 +167,14 @@ def keep_listen(self, printer): sleep(1.5) self.flush() + self._stop_task = False - while(self.is_running()): + while(not self._stop_task): try: data = self.readable() except pyserial.serialutil.SerialException: - self.close() + self.disconnect() + self.destroy() printer("\n\nSerialError: device disconected") break @@ -161,24 +189,29 @@ def flush(self): Cleans the input and output in the connected serial port """ - self._serial.flushOutput() - self._serial.flushInput() + if(self.is_running): + self._serial.flushOutput() + self._serial.flushInput() - def close(self): + def disconnect(self): + self._stop_task = True + self._serial.close() + + def destroy(self, clean_color=True): """Close serial connection Closes the serial connection in the port selected. _stop_task will be updated before close the port to avoid the overlaped error, """ - self._stop_task = True - self._serial.close() - port = self._serial.port in_use.remove(port) del serial_dict[port] + if(clean_color): + status_color.set("error", 2000) + def establish_connection(port): """establish serial connection and listen @@ -192,21 +225,25 @@ def establish_connection(port): from ..tools import message from threading import Thread + global serial_dict + try: link = serial_dict[port] except: link = Serial(port) - if(link.is_running()): - return - txt = message.open(port) - try: - link.open() - except pyserial.serialutil.SerialException as e: - if('could not open port' in str(e)): - txt.print(serialError_noaccess) - return + + if(not link.is_running()): + try: + link.open() + except pyserial.serialutil.SerialException as e: + if('could not open port' in str(e)): + txt.print(errors.serialError_noaccess) + status_color.remove() + return + + status_color.set("success") Thread(target=link.keep_listen, args=(txt.print,)).start() diff --git a/tools/status_color.py b/tools/status_color.py new file mode 100644 index 0000000..ecf4f91 --- /dev/null +++ b/tools/status_color.py @@ -0,0 +1,125 @@ +# MIT License +# +# Copyright (c) 2017 GEPD +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# HOW TO USE +# +# import status_color +# +# // sets a color. Possibles values ("error", "success", "warning") +# status_color.set("error") +# +# // removes the color +# status_color.remove() +# +# // sets a color and remove it after 5 seconds +# status_color.set("error", 5000) + + +import sublime +from glob import glob +from errno import EEXIST +from os import path, makedirs, remove as remove_file + +# color definition +# edit this var to remove or add your own colors +# [[backgrond_color], [text_color]] +colors = { + 'error': [[200, 50, 60], [230, 230, 230]], + 'success': [[50, 170, 60], [230, 230, 230]], + 'warning': [[200, 140, 40], [230, 230, 230]] +} + +theme_path = None + + +def set(status=False, timeout=0): + """Set color in the status status + + Sets the given action as color in the status bar + + Keyword Arguments: + status {bool} -- sets a color in the status bar based on the actions + defined in the 'colors' global var. (default: {False}) + timeout {number} -- removes the status bar color after the given delay + in milliseconds. If not defined the color will keep + forever (default: {0}) + """ + global theme_path + + check_folder_paths() + + resource = [] + # background color + resource.append({"class": "status_bar", "layer0.tint": colors[status][0]}) + # text color + resource.append({"class": "label_control", "color": colors[status][1]}) + resource = sublime.encode_value(resource) + + # save file + with open(theme_path, 'w') as file: + file.write(resource) + + if(timeout > 0): + sublime.set_timeout_async(remove, timeout) + + +def remove(remove_path=None): + """Remove status bar color + + Removes the file that overrides the theme status bar color + """ + if(remove_path): + from shutil import rmtree + if(path.exists(remove_path)): + rmtree(remove_path) + return + + try: + if(path.exists(theme_path)): + remove_file(theme_path) + except: + pass + + +def check_folder_paths(): + """Check folder and theme path + + Checks the status_color folder exitence and sets the full path + of the curren theme + """ + global theme_path + + setting = sublime.load_settings("Preferences.sublime-settings") + theme_name = setting.get("theme") + + packages = sublime.packages_path() + user_path = path.join(packages, 'User') + theme_folder = path.join(user_path, 'Status Color') + + # create folder + if(not path.exists(theme_folder)): + try: + makedirs(theme_folder) + except OSError as exc: + pass + + theme_path = path.join(theme_folder, theme_name) diff --git a/tools/thread_progress.py b/tools/thread_progress.py new file mode 100644 index 0000000..99203a5 --- /dev/null +++ b/tools/thread_progress.py @@ -0,0 +1,61 @@ +import sublime + + +class ThreadProgress(): + + """ + Animates an indicator, [= ], in the status area while a thread runs + + :param thread: + The thread to track for activity + + :param message: + The message to display next to the activity indicator + + :param success_message: + The message to display once the thread is complete + """ + + def __init__(self, thread, message, success_message): + self.thread = thread + self.message = message + self.success_message = success_message + self.addend = 1 + self.size = 8 + self.last_view = None + self.window = None + sublime.set_timeout(lambda: self.run(0), 100) + + def run(self, i): + if self.window is None: + self.window = sublime.active_window() + active_view = self.window.active_view() + + if self.last_view is not None and active_view != self.last_view: + self.last_view.erase_status('_package_control') + self.last_view = None + + if not self.thread.is_alive(): + def cleanup(): + active_view.erase_status('_package_control') + if hasattr(self.thread, 'result') and not self.thread.result: + cleanup() + return + active_view.set_status('_package_control', self.success_message) + sublime.set_timeout(cleanup, 1000) + return + + before = i % self.size + after = (self.size - 1) - before + + active_view.set_status('_package_control', '%s [%s=%s]' % (self.message, ' ' * before, ' ' * after)) + if self.last_view is None: + self.last_view = active_view + + if not after: + self.addend = -1 + if not before: + self.addend = 1 + i += self.addend + + sublime.set_timeout(lambda: self.run(i), 100) diff --git a/upiot.py b/upiot.py index 8862532..715f605 100644 --- a/upiot.py +++ b/upiot.py @@ -26,18 +26,29 @@ # SOFTWARE. from .commands import * +from .tools import message_upgrade, message from sublime_plugin import EventListener -from .tools.message import CloseConsole -from .tools import serial, message_upgrade +from shutil import rmtree +from .tools.paths import status_color_folder def plugin_loaded(): message_upgrade() +# plugin_unload is not working so if the status bar color +# folder is present when ST starts, it will remove it. +try: + rmtree(status_color_folder()) +except: + pass -class uPiotListener(EventListener): + +class uListener(EventListener): + + def on_pre_close(self, view): + if(message.session): + message.session.on_pre_close(view) def on_close(self, view): - port = serial.selected_port() - if(port in serial.in_use): - serial.serial_dict[port].close() + if(message.session): + message.session.on_close(view)