From a098e08e00820d0d10709bad19e82080b4546780 Mon Sep 17 00:00:00 2001 From: gepd Date: Mon, 6 Nov 2017 01:09:39 -0300 Subject: [PATCH 01/45] Fixed --help command --- commands/console_write.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/commands/console_write.py b/commands/console_write.py index 1a28957..cb7c45a 100644 --- a/commands/console_write.py +++ b/commands/console_write.py @@ -51,9 +51,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)) From 9613ad1041492c371df017389c6dae6f687d0abe Mon Sep 17 00:00:00 2001 From: gepd Date: Mon, 6 Nov 2017 01:13:51 -0300 Subject: [PATCH 02/45] Fix bug make not work all commands except for "sampy run" introduced in https://github.com/gepd/uPiotMicroPythonTool/commit/6ceba526b00f3812b4e64d9ae0f0187cbaaae9a2 --- tools/ampy/pyboard.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tools/ampy/pyboard.py b/tools/ampy/pyboard.py index 8e91efb..9f596c7 100644 --- a/tools/ampy/pyboard.py +++ b/tools/ampy/pyboard.py @@ -231,12 +231,12 @@ def exit_raw_repl(self): # ctrl-B: enter friendly REPL self.serial.write(b'\r\x02') - def eval(self, expression): + def eval(self, expression, quiet=False): ret = self.exec_('print({})'.format(expression)) ret = ret.strip() return ret - def exec_(self, command): + def exec_(self, command, quiet=True): if isinstance(command, bytes): command_bytes = command else: @@ -259,16 +259,21 @@ def exec_(self, command): 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() + out = self.receive_serial_data(quiet) # Receive data after use '\x03' - self.receive_serial_data() + self.receive_serial_data(quiet) + + return out - def receive_serial_data(self): + def receive_serial_data(self, quiet=True): + session_data = b'' data = b'' + # add to lines in the console + if(not quiet): + self.data_consumer('\n\n') + while(b'\x04' not in data): data += self.serial.read(1) @@ -276,13 +281,16 @@ def receive_serial_data(self): 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) + if(not quiet): + self.data_consumer(data) + session_data += data data = b'' + return session_data def execfile(self, filename): with open(filename, 'rb') as f: pyfile = f.read() - return self.exec_(pyfile) + return self.exec_(pyfile, quiet=False) def get_time(self): t = str(self.eval('pyb.RTC().datetime()'), From b3c7e0279ab4a03dc1a4dd1be3775b1068c21060 Mon Sep 17 00:00:00 2001 From: gepd Date: Mon, 6 Nov 2017 01:18:23 -0300 Subject: [PATCH 03/45] First implementation to show a color in the status bar when a serial connection is established in a port --- tools/sampy_manager.py | 5 +- tools/serial.py | 10 +++- tools/status_color.py | 122 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 tools/status_color.py diff --git a/tools/sampy_manager.py b/tools/sampy_manager.py index 6e5d42e..d54b286 100644 --- a/tools/sampy_manager.py +++ b/tools/sampy_manager.py @@ -26,7 +26,7 @@ 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 @@ -50,7 +50,7 @@ def start_sampy(): # close the current connection in open port if(port in serial.in_use): run_serial = serial.serial_dict[port] - run_serial.close() + run_serial.close(clean_color=False) # message printer txt = message.open(port) @@ -64,6 +64,7 @@ def start_sampy(): if('failed to access' in str(e)): txt.print(errors.serialError_noaccess) + status_color.remove() exit(0) diff --git a/tools/serial.py b/tools/serial.py index 3bd0586..4875e36 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 = {} @@ -147,6 +148,7 @@ def keep_listen(self, printer): data = self.readable() except pyserial.serialutil.SerialException: self.close() + status_color.set("error", 2000) printer("\n\nSerialError: device disconected") break @@ -164,7 +166,7 @@ def flush(self): self._serial.flushOutput() self._serial.flushInput() - def close(self): + def close(self, clean_color=True): """Close serial connection Closes the serial connection in the port selected. @@ -179,6 +181,9 @@ def close(self): in_use.remove(port) del serial_dict[port] + if(clean_color): + status_color.remove() + def establish_connection(port): """establish serial connection and listen @@ -206,8 +211,11 @@ def establish_connection(port): except pyserial.serialutil.SerialException as e: if('could not open port' in str(e)): txt.print(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..941f791 --- /dev/null +++ b/tools/status_color.py @@ -0,0 +1,122 @@ +# 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 + + if(path.exists(theme_path)): + remove_file(theme_path) + + +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) From b3698ab2e7585463117e521823cea6d829188475 Mon Sep 17 00:00:00 2001 From: gepd Date: Mon, 6 Nov 2017 01:22:28 -0300 Subject: [PATCH 04/45] When the console is closed, it will destroy/close the group panel if it's empty --- tools/message.py | 25 ++++++++++++++++--------- tools/serial.py | 5 +++-- upiot.py | 12 ++++++------ 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/tools/message.py b/tools/message.py index fc30373..b1ac2e8 100644 --- a/tools/message.py +++ b/tools/message.py @@ -26,7 +26,7 @@ # SOFTWARE. import sublime -from sublime_plugin import EventListener +import sublime_plugin import collections import threading @@ -39,7 +39,7 @@ class Message: - BLOCK_SIZE = 2 ** 14 + port = None text_queue = collections.deque() text_queue_lock = threading.Lock() @@ -168,9 +168,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 +181,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 +198,17 @@ 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): + serial.serial_dict[self.port].close() + close_panel = False diff --git a/tools/serial.py b/tools/serial.py index 4875e36..c20adc2 100644 --- a/tools/serial.py +++ b/tools/serial.py @@ -163,8 +163,9 @@ 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, clean_color=True): """Close serial connection diff --git a/upiot.py b/upiot.py index 8862532..79b1971 100644 --- a/upiot.py +++ b/upiot.py @@ -26,18 +26,18 @@ # 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 def plugin_loaded(): message_upgrade() -class uPiotListener(EventListener): +class uListener(EventListener): + + def on_pre_close(self, view): + 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() + message.session.on_close(view) From 4587defb0ade1b1aa161843f34a47b537e10dab9 Mon Sep 17 00:00:00 2001 From: gepd Date: Mon, 6 Nov 2017 01:23:54 -0300 Subject: [PATCH 05/45] bump to alpha v0.1.3 --- tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/__init__.py b/tools/__init__.py index 0154db7..a603f8f 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, 3, '-alpha') ACTIVE_VIEW = None SETTINGS_NAME = 'upiot.sublime-settings' From b274a9f4d19d36828ab3373ae6731f9fe48eaa8f Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:06:31 -0300 Subject: [PATCH 06/45] Renamed closed serial method to destroy, and add a new one called disconnect. disconnect will close the serial connection in the selected port, but will keep the serial object. The destroy method will remove the serial instance from the global dict var --- commands/burn_firmware.py | 2 +- tools/sampy_manager.py | 2 +- tools/serial.py | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/commands/burn_firmware.py b/commands/burn_firmware.py index 6e41778..9d71c89 100644 --- a/commands/burn_firmware.py +++ b/commands/burn_firmware.py @@ -123,7 +123,7 @@ def burn_firmware(self): return if(self.port in serial.in_use): - serial.serial_dict[self.port].close() + serial.serial_dict[self.port].disconnect() Command().run(options, port=self.port) diff --git a/tools/sampy_manager.py b/tools/sampy_manager.py index d54b286..be46ee6 100644 --- a/tools/sampy_manager.py +++ b/tools/sampy_manager.py @@ -50,7 +50,7 @@ def start_sampy(): # close the current connection in open port if(port in serial.in_use): run_serial = serial.serial_dict[port] - run_serial.close(clean_color=False) + run_serial.disconnect() # message printer txt = message.open(port) diff --git a/tools/serial.py b/tools/serial.py index c20adc2..31a6843 100644 --- a/tools/serial.py +++ b/tools/serial.py @@ -167,16 +167,17 @@ def flush(self): self._serial.flushOutput() self._serial.flushInput() - def close(self, clean_color=True): + 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) From 5394cf24786da5fecc9effa95eea7a23862d5886 Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:08:39 -0300 Subject: [PATCH 07/45] Displays the port disconnection changing the status bar color when the console window is closed --- tools/message.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/message.py b/tools/message.py index b1ac2e8..0b2bf26 100644 --- a/tools/message.py +++ b/tools/message.py @@ -32,6 +32,7 @@ import threading from .. import tools +from ..tools import status_color session = None close_panel = False @@ -208,6 +209,7 @@ def on_close(self, view): if(self.port in serial.in_use): serial.serial_dict[self.port].close() + status_color.set("error", 2000) close_panel = False From 9382781e18918c1103f598ce7399cf78f2a6c00b Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:09:51 -0300 Subject: [PATCH 08/45] try to close the console only when there is a session stored --- upiot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/upiot.py b/upiot.py index 79b1971..ee7e2f7 100644 --- a/upiot.py +++ b/upiot.py @@ -37,7 +37,9 @@ def plugin_loaded(): class uListener(EventListener): def on_pre_close(self, view): - message.session.on_pre_close(view) + if(message.session): + message.session.on_pre_close(view) def on_close(self, view): - message.session.on_close(view) + if(message.session): + message.session.on_close(view) From 029acbd17edb4385ceeffca89b6197cd27bb6892 Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:10:53 -0300 Subject: [PATCH 09/45] fixed --help command --- tools/sampy_manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/sampy_manager.py b/tools/sampy_manager.py index be46ee6..df88ad0 100644 --- a/tools/sampy_manager.py +++ b/tools/sampy_manager.py @@ -34,7 +34,7 @@ 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,7 +48,7 @@ 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.disconnect() @@ -56,7 +56,7 @@ def start_sampy(): 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: @@ -304,7 +304,7 @@ def help(): Displays the sampy command usage """ - start_sampy() + start_sampy(quiet=True) _help = """\n\nUsage: sampy COMMAND [ARGS]... @@ -330,3 +330,4 @@ def help(): """.replace(' ', '') txt.print(_help) + finished_action() From 261aa4bcb6773632f124301a55963ac94e317a91 Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:11:56 -0300 Subject: [PATCH 10/45] avoid to save serial instance when it's already saved --- tools/serial.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/serial.py b/tools/serial.py index 31a6843..44c80bb 100644 --- a/tools/serial.py +++ b/tools/serial.py @@ -62,8 +62,9 @@ def open(self): # 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 receive(self): """Receive data From 8297a5a5293efdc31856d328b66273a88026b1f8 Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:13:25 -0300 Subject: [PATCH 11/45] use global vars --- tools/serial.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/serial.py b/tools/serial.py index 44c80bb..5fb4598 100644 --- a/tools/serial.py +++ b/tools/serial.py @@ -57,6 +57,9 @@ 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 @@ -200,6 +203,8 @@ def establish_connection(port): from ..tools import message from threading import Thread + global serial_dict + try: link = serial_dict[port] except: From 5d7b9a8824b027cc0e986b220fe25b6a84e39217 Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:14:06 -0300 Subject: [PATCH 12/45] use new disconnect and destroy methods --- tools/serial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/serial.py b/tools/serial.py index 5fb4598..d884c75 100644 --- a/tools/serial.py +++ b/tools/serial.py @@ -151,8 +151,8 @@ def keep_listen(self, printer): try: data = self.readable() except pyserial.serialutil.SerialException: - self.close() - status_color.set("error", 2000) + self.disconnect() + self.destroy() printer("\n\nSerialError: device disconected") break From 091997b62d576a2c73e80d65b1d096e0d0e94620 Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:15:08 -0300 Subject: [PATCH 13/45] use _stop_task to avoid errors when keep_listen is stoping --- tools/serial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/serial.py b/tools/serial.py index d884c75..6e04e27 100644 --- a/tools/serial.py +++ b/tools/serial.py @@ -103,7 +103,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 @@ -147,7 +147,7 @@ def keep_listen(self, printer): self.flush() - while(self.is_running()): + while(not self._stop_task): try: data = self.readable() except pyserial.serialutil.SerialException: From cbdbe07e4fddc6e9d22a209768ec6dba621a4653 Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:16:18 -0300 Subject: [PATCH 14/45] Show a red color in the status bar when the serial instance is destroyed/closed --- tools/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/serial.py b/tools/serial.py index 6e04e27..f36ed92 100644 --- a/tools/serial.py +++ b/tools/serial.py @@ -188,7 +188,7 @@ def destroy(self, clean_color=True): del serial_dict[port] if(clean_color): - status_color.remove() + status_color.set("error", 2000) def establish_connection(port): From fad6cf297b3a2fcc20b1d8f018b37c67a2fea980 Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:17:07 -0300 Subject: [PATCH 15/45] fix print error call in a serial exception --- tools/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/serial.py b/tools/serial.py index f36ed92..162ef84 100644 --- a/tools/serial.py +++ b/tools/serial.py @@ -218,7 +218,7 @@ def establish_connection(port): link.open() except pyserial.serialutil.SerialException as e: if('could not open port' in str(e)): - txt.print(serialError_noaccess) + txt.print(errors.serialError_noaccess) status_color.remove() return From 93cc5e9a2a08e1d1aa2bfbfb96cbb627e7e1502a Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:17:32 -0300 Subject: [PATCH 16/45] new --close command --- commands/console_write.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/commands/console_write.py b/commands/console_write.py index cb7c45a..d707958 100644 --- a/commands/console_write.py +++ b/commands/console_write.py @@ -71,6 +71,18 @@ def callback(self, data): 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') From aaafdaa8bfd2b5dd067b9e515792d73d887c1ad0 Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:18:02 -0300 Subject: [PATCH 17/45] catch only the key errors --- commands/console_write.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/console_write.py b/commands/console_write.py index d707958..da840f2 100644 --- a/commands/console_write.py +++ b/commands/console_write.py @@ -118,7 +118,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)) From 591d5078ebf398942a3a2b6da381e8d7a25c388a Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:19:11 -0300 Subject: [PATCH 18/45] Workaround to remove the status bar color, when it wasn't closed before exit from ST --- tools/paths.py | 9 +++++++++ upiot.py | 9 +++++++++ 2 files changed, 18 insertions(+) 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/upiot.py b/upiot.py index ee7e2f7..715f605 100644 --- a/upiot.py +++ b/upiot.py @@ -28,11 +28,20 @@ from .commands import * from .tools import message_upgrade, message from sublime_plugin import EventListener +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 uListener(EventListener): From 3f3a0aa8665c52a3d81179990e7d467346c1cb19 Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 02:58:13 -0300 Subject: [PATCH 19/45] Linux serial fixes --- tools/pyserial/serialposix.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) 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: From 48598038aabff951a8e2125b6dee667e00dc5b2f Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 7 Nov 2017 03:01:16 -0300 Subject: [PATCH 20/45] fix destroying serial session when console window is closed --- tools/message.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/message.py b/tools/message.py index 0b2bf26..6a73896 100644 --- a/tools/message.py +++ b/tools/message.py @@ -208,7 +208,9 @@ def on_close(self, view): from ..tools import serial if(self.port in serial.in_use): - serial.serial_dict[self.port].close() + link = serial.serial_dict[self.port] + link.disconnect() + link.destroy() status_color.set("error", 2000) close_panel = False From 2baa3e2a15808bf84f19abd107b292e89cdc2cd1 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 12 Nov 2017 14:49:02 -0300 Subject: [PATCH 21/45] 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 --- tools/ampy/pyboard.py | 43 +++--------------------------------------- tools/sampy.py | 5 ++++- tools/sampy_manager.py | 18 +----------------- tools/serial.py | 21 +++++++++++++++++++++ 4 files changed, 29 insertions(+), 58 deletions(-) diff --git a/tools/ampy/pyboard.py b/tools/ampy/pyboard.py index 9f596c7..37b9693 100644 --- a/tools/ampy/pyboard.py +++ b/tools/ampy/pyboard.py @@ -119,51 +119,13 @@ class Pyboard: port = None data_consumer = None - def __init__(self, device, baudrate=115200, user='micro', password='python', wait=0, data_consumer=None): + def __init__(self, oserial, user='micro', password='python', 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('') + self.serial = oserial 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) @@ -191,6 +153,7 @@ def read_until(self, min_num_bytes, ending, timeout=10): def enter_raw_repl(self): # ctrl-C twice: interrupt any running program self.serial.write(b'\r\x03\x03') + self.serial.write(b'\x04') # flush input (without relying on serial.flushInput()) n = self.serial.inWaiting() diff --git a/tools/sampy.py b/tools/sampy.py index cfc8a3c..d000ec5 100644 --- a/tools/sampy.py +++ b/tools/sampy.py @@ -29,6 +29,7 @@ from sublime import platform, set_timeout_async from ..tools.ampy import files from ..tools.ampy import pyboard +from ..tools import serial _board = None @@ -55,7 +56,9 @@ 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) + + raw = serial.serial_dict[port].raw() + _board = pyboard.Pyboard(oserial=raw, data_consumer=data_consumer) def get(self, remote_file, local_file=None): """ diff --git a/tools/sampy_manager.py b/tools/sampy_manager.py index df88ad0..bc0dbbe 100644 --- a/tools/sampy_manager.py +++ b/tools/sampy_manager.py @@ -50,7 +50,7 @@ def start_sampy(quiet=False): # close the current connection in open port if(port in serial.in_use and not quiet): run_serial = serial.serial_dict[port] - run_serial.disconnect() + run_serial.stop_task() # message printer txt = message.open(port) @@ -103,8 +103,6 @@ def run_file(filepath): txt.print("\n[done]") - sampy.close() - finished_action() @@ -120,8 +118,6 @@ def list_files(): for filename in sampy.ls(): txt.print('\n' + filename) - sampy.close() - finished_action() @@ -145,8 +141,6 @@ def get_file(filename): txt.print('\n\n' + output) - sampy.close() - finished_action() @@ -174,8 +168,6 @@ def get_files(destination): txt.print("\n[done]") - sampy.close() - finished_action() if(check_sidebar_folder(destination)): @@ -218,8 +210,6 @@ def put_file(filepath): txt.print('\n\n' + output) - sampy.close() - finished_action() @@ -244,8 +234,6 @@ def remove_file(filepath): txt.print('\n\n' + output) - sampy.close() - finished_action() @@ -269,8 +257,6 @@ def make_folder(folder_name): txt.print('\n\n' + output) - sampy.close() - finished_action() @@ -294,8 +280,6 @@ def remove_folder(folder_name): txt.print('\n\n' + output) - sampy.close() - finished_action() diff --git a/tools/serial.py b/tools/serial.py index 162ef84..0ba7310 100644 --- a/tools/serial.py +++ b/tools/serial.py @@ -46,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 @@ -69,6 +72,24 @@ def open(self): 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 From f65c7b5693bb2abc36714ff8f60cf0b22b6ef5df Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 12 Nov 2017 14:53:42 -0300 Subject: [PATCH 22/45] make the write console command only available when there is a port connected removed stablish connection, no need to do this anymore --- commands/console_write.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/commands/console_write.py b/commands/console_write.py index da840f2..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 @@ -66,10 +69,6 @@ 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 From f648531556d4952b5e41f8602c2db0d93adeb18c Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 12 Nov 2017 15:46:28 -0300 Subject: [PATCH 23/45] Fix serial listener after run a sampy command --- tools/serial.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tools/serial.py b/tools/serial.py index 0ba7310..4c717da 100644 --- a/tools/serial.py +++ b/tools/serial.py @@ -167,6 +167,7 @@ def keep_listen(self, printer): sleep(1.5) self.flush() + self._stop_task = False while(not self._stop_task): try: @@ -231,19 +232,18 @@ def establish_connection(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(errors.serialError_noaccess) - status_color.remove() - return - status_color.set("success") + 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() From 4859f43b7b7603c26a8372473f1b45de8bf9ccaf Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 12 Nov 2017 15:55:17 -0300 Subject: [PATCH 24/45] bump to alpha v0.1.4 --- tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/__init__.py b/tools/__init__.py index a603f8f..e2e392d 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, 3, '-alpha') +VERSION = (0, 1, 4, '-alpha') ACTIVE_VIEW = None SETTINGS_NAME = 'upiot.sublime-settings' From b57295443873cf3ab313354bf014fcf36b628ace Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 12 Nov 2017 16:06:36 -0300 Subject: [PATCH 25/45] updated README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 35b4d6e..7e86462 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 From 8d9ce1c2775ca18bd3ad847da1b7ac7d48f2b54a Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 14:18:08 -0300 Subject: [PATCH 26/45] - 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. --- commands/make_folder.py | 2 +- commands/raw_serial.py | 4 +- commands/remove_folder.py | 2 +- tools/ampy/files.py | 68 +++++----- tools/ampy/pyboard.py | 261 -------------------------------------- tools/repl.py | 221 ++++++++++++++++++++++++++++++++ tools/sampy.py | 17 ++- tools/sampy_manager.py | 43 ++++--- tools/status_color.py | 7 +- 9 files changed, 306 insertions(+), 319 deletions(-) delete mode 100644 tools/ampy/pyboard.py create mode 100644 tools/repl.py diff --git a/commands/make_folder.py b/commands/make_folder.py index 36825d3..ef1bc51 100644 --- a/commands/make_folder.py +++ b/commands/make_folder.py @@ -36,7 +36,7 @@ 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(): 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_folder.py b/commands/remove_folder.py index 0cab02a..09edeeb 100644 --- a/commands/remove_folder.py +++ b/commands/remove_folder.py @@ -36,7 +36,7 @@ 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(): diff --git a/tools/ampy/files.py b/tools/ampy/files.py index 6d50743..41a4cc0 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,11 +19,10 @@ # 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 @@ -35,6 +34,10 @@ 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 +62,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 +70,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 +82,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 +99,13 @@ def ls(self, directory='/'): import os except ImportError: import uos as os - print({{f : uos.stat(f)[0] for f in uos.listdir('{0}')}}) + print(os.listdir('{0}')) """.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 +113,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 +131,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 +143,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 +163,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 +174,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 +190,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 +221,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 +232,13 @@ 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() + self._pyboard.enter_raw() self._pyboard.execfile(filename) - self._pyboard.exit_raw_repl() + self._pyboard.exit_raw() diff --git a/tools/ampy/pyboard.py b/tools/ampy/pyboard.py deleted file mode 100644 index 37b9693..0000000 --- a/tools/ampy/pyboard.py +++ /dev/null @@ -1,261 +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, oserial, user='micro', password='python', data_consumer=None): - self.data_consumer = data_consumer - self.serial = oserial - - def write(self, data): - self.serial.write(data) - - 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') - self.serial.write(b'\x04') - - # 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, quiet=False): - ret = self.exec_('print({})'.format(expression)) - ret = ret.strip() - return ret - - def exec_(self, command, quiet=True): - 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') - - # receive data from the serial port - out = self.receive_serial_data(quiet) - # Receive data after use '\x03' - self.receive_serial_data(quiet) - - return out - - def receive_serial_data(self, quiet=True): - session_data = b'' - data = b'' - - # add to lines in the console - if(not quiet): - self.data_consumer('\n\n') - - 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') - if(not quiet): - self.data_consumer(data) - session_data += data - data = b'' - return session_data - - def execfile(self, filename): - with open(filename, 'rb') as f: - pyfile = f.read() - return self.exec_(pyfile, quiet=False) - - 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/repl.py b/tools/repl.py new file mode 100644 index 0000000..af501fb --- /dev/null +++ b/tools/repl.py @@ -0,0 +1,221 @@ +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 + """ + + with open(filename, 'rb') as f: + pyfile = f.read() + 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 d000ec5..0c1942e 100644 --- a/tools/sampy.py +++ b/tools/sampy.py @@ -28,8 +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 @@ -57,8 +57,13 @@ def __init__(self, port, baudrate=115200, data_consumer=None): if platform() == 'windows': port = windows_full_port_name(port) - raw = serial.serial_dict[port].raw() - _board = pyboard.Pyboard(oserial=raw, 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): """ @@ -110,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 @@ -182,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 bc0dbbe..7698ef2 100644 --- a/tools/sampy_manager.py +++ b/tools/sampy_manager.py @@ -28,7 +28,7 @@ from ..tools import check_sidebar_folder, make_folder as mkfolder 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 @@ -59,7 +59,7 @@ def start_sampy(quiet=False): 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)): @@ -99,9 +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]") + try: + sampy.run(filepath) + txt.print("\n[done]") + except AttributeError as e: + txt.print("\n\nOpening the console...\nRun the command again.") finished_action() @@ -155,9 +157,11 @@ def get_files(destination): 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(): + txt.print('\nRetrieving ' + filename + ' ...') + filepath = path.normpath(path.join(destination, filename)) if(filename.endswith('/')): if(not path.exists(filepath)): @@ -166,14 +170,14 @@ def get_files(destination): with open(filepath, 'w') as file: file.write(sampy.get(filename)) - txt.print("\n[done]") + txt.print("\n\n[done]") finished_action() 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") @@ -199,16 +203,23 @@ 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) + file = path.basename(filepath) + txt.print('\n\n>> put {0}'.format(file)) - txt.print('\n\n' + output) + try: + sampy.put(path.normpath(filepath)) + output = '[done]' + except FileNotFoundError as e: + output = str(e) + except file.PyboardError as e: + txt.print("\n\nError putting the file.\nReason: " + str(e)) + return finished_action() + + txt.print('\n\n' + output) + + except TypeError as e: + txt.print("\n\n" + str(e)) finished_action() diff --git a/tools/status_color.py b/tools/status_color.py index 941f791..ecf4f91 100644 --- a/tools/status_color.py +++ b/tools/status_color.py @@ -93,8 +93,11 @@ def remove(remove_path=None): rmtree(remove_path) return - if(path.exists(theme_path)): - remove_file(theme_path) + try: + if(path.exists(theme_path)): + remove_file(theme_path) + except: + pass def check_folder_paths(): From af8f36ac02a8f73791d619423b7c669ee18a5dd8 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 14:23:46 -0300 Subject: [PATCH 27/45] make sure to always download the given file, even if it already exists --- tools/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/__init__.py b/tools/__init__.py index e2e392d..66335d0 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -101,10 +101,6 @@ def download_file(file_url, dst_path, callback=None): 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: req = requests.get(file_url, stream=True, headers=headers) From 71ca1a3fd4e2eb754eddd9ed5099a5d7d8bc5ed4 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 17:04:05 -0300 Subject: [PATCH 28/45] - Opens the console when a command is ran it's closed - Other minor syntax updates --- tools/sampy_manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/sampy_manager.py b/tools/sampy_manager.py index 7698ef2..80ba669 100644 --- a/tools/sampy_manager.py +++ b/tools/sampy_manager.py @@ -103,7 +103,7 @@ def run_file(filepath): sampy.run(filepath) txt.print("\n[done]") except AttributeError as e: - txt.print("\n\nOpening the console...\nRun the command again.") + txt.print("\n\nOpening console...\nRun the command again.") finished_action() @@ -212,10 +212,11 @@ def put_file(filepath): output = '[done]' except FileNotFoundError as e: output = str(e) - except file.PyboardError as e: - txt.print("\n\nError putting the file.\nReason: " + 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: From 00a06a326401f381fef57b879174e4165c5f20b4 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 17:08:04 -0300 Subject: [PATCH 29/45] Feature: runs selected code (Issue: https://github.com/gepd/uPiotMicroPythonTool/issues/4) --- commands/run_current_file.py | 5 +++++ tools/ampy/files.py | 9 +++++++++ tools/repl.py | 8 ++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/commands/run_current_file.py b/commands/run_current_file.py index 80f2f35..6f01494 100644 --- a/commands/run_current_file.py +++ b/commands/run_current_file.py @@ -29,6 +29,7 @@ from ..tools import message from threading import Thread from ..tools.serial import selected_port +from ..tools.ampy import files class upiotRunCurrentFileCommand(WindowCommand): @@ -47,4 +48,8 @@ def run(self): if(view.is_dirty()): view.run_command('save') + view = self.window.active_view() + selection = view.sel()[0] + files.SELECTED_TEXT = bytes(view.substr(selection), 'utf-8') + Thread(target=sampy_manager.run_file, args=(file,)).start() diff --git a/tools/ampy/files.py b/tools/ampy/files.py index 41a4cc0..dabdd18 100644 --- a/tools/ampy/files.py +++ b/tools/ampy/files.py @@ -29,6 +29,8 @@ # This is kept small because small chips and USB to serial # bridges usually have very small buffers. +SELECTED_TEXT = None + class DirectoryExistsError(Exception): pass @@ -239,6 +241,13 @@ def run(self, filename): 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 """ + global SELECTED_TEXT + + if(SELECTED_TEXT): + filename = SELECTED_TEXT + self._pyboard.enter_raw() self._pyboard.execfile(filename) self._pyboard.exit_raw() + + SELECTED_TEXT = None diff --git a/tools/repl.py b/tools/repl.py index af501fb..906f8fb 100644 --- a/tools/repl.py +++ b/tools/repl.py @@ -201,8 +201,12 @@ def execfile(self, filename): bytes -- file content """ - with open(filename, 'rb') as f: - pyfile = f.read() + try: + with open(filename, 'rb') as f: + pyfile = f.read() + except OSError as e: + pyfile = filename + return self.exec_(pyfile, quiet=False) From 47f9e2f770281683ab0412761180bbd121b9b142 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 18:21:23 -0300 Subject: [PATCH 30/45] Added feedback in the statusbar while the command is running --- commands/list_files.py | 7 +++- commands/make_folder.py | 9 +++-- commands/put_current_file.py | 9 ++--- commands/put_file.py | 9 ++--- commands/remove_file.py | 8 +++-- commands/remove_folder.py | 9 +++-- commands/retrieve_all_files.py | 7 +++- commands/run_current_file.py | 7 ++-- tools/command.py | 9 +++-- tools/thread_progress.py | 61 ++++++++++++++++++++++++++++++++++ 10 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 tools/thread_progress.py 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 ef1bc51..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): @@ -39,6 +41,7 @@ def run(self): 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..a8234e4 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): @@ -38,7 +40,6 @@ def run(self): file = self.window.active_view().file_name() - 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/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/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 09edeeb..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): @@ -39,6 +41,7 @@ def run(self): 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 6f01494..ef08a90 100644 --- a/commands/run_current_file.py +++ b/commands/run_current_file.py @@ -24,12 +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): @@ -52,4 +53,6 @@ def run(self): selection = view.sel()[0] files.SELECTED_TEXT = bytes(view.substr(selection), 'utf-8') - Thread(target=sampy_manager.run_file, args=(file,)).start() + th = Thread(target=sampy_manager.run_file, args=(file,)) + th.start() + ThreadProgress(th, '', '') 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/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) From d1766ac73178823f8d954b4a63013684ac875e3e Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 18:22:18 -0300 Subject: [PATCH 31/45] bump to alpha v0.1.5 --- tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/__init__.py b/tools/__init__.py index 66335d0..870f818 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, 4, '-alpha') +VERSION = (0, 1, 5, '-alpha') ACTIVE_VIEW = None SETTINGS_NAME = 'upiot.sublime-settings' From 29763f3f9d088c811e0067e3a94178ae60c5c241 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 18:43:49 -0300 Subject: [PATCH 32/45] Bug fix cleaning the retrieved line --- tools/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/repl.py b/tools/repl.py index 906f8fb..3a481d1 100644 --- a/tools/repl.py +++ b/tools/repl.py @@ -97,7 +97,7 @@ def receive_serial_data(self, quiet=True): if(not quiet): self.data_consumer(data) - data = b'' + data = b'' return session_data From ac84896fc0259868663be1892b3695749afdeb92 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 18:45:15 -0300 Subject: [PATCH 33/45] Minor code improvement writing code in a file --- tools/sampy_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/sampy_manager.py b/tools/sampy_manager.py index 80ba669..bdb7cb0 100644 --- a/tools/sampy_manager.py +++ b/tools/sampy_manager.py @@ -167,8 +167,8 @@ def get_files(destination): if(not path.exists(filepath)): mkdir(filepath) else: - with open(filepath, 'w') as file: - file.write(sampy.get(filename)) + with open(filepath, 'wb') as file: + sampy.get(filename, file) txt.print("\n\n[done]") From c663075113c1cfeeaddc81870c12ca2691324e53 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 18:56:16 -0300 Subject: [PATCH 34/45] Connect the console when a command is run and it's not connected --- tools/sampy_manager.py | 44 +++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/tools/sampy_manager.py b/tools/sampy_manager.py index bdb7cb0..716113f 100644 --- a/tools/sampy_manager.py +++ b/tools/sampy_manager.py @@ -117,8 +117,11 @@ def list_files(): txt.print('\n\n>> sampy ls\n') - for filename in sampy.ls(): - txt.print('\n' + filename) + 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() @@ -152,6 +155,7 @@ 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) @@ -159,21 +163,29 @@ def get_files(destination): txt.print('\n\n>> Storing in {0}\n'.format(destination)) - 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) + 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 - txt.print("\n\n[done]") + txt.print('\n\n' + output) finished_action() + if(error): + return + if(check_sidebar_folder(destination)): return @@ -243,6 +255,8 @@ 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) @@ -266,6 +280,8 @@ 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) @@ -289,6 +305,8 @@ 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) From 86e68023afd0ae364309d412a0103848cbb5c703 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 19:16:43 -0300 Subject: [PATCH 35/45] bump to alpha v0.1.6 --- tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/__init__.py b/tools/__init__.py index 870f818..a4cd9e5 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, 5, '-alpha') +VERSION = (0, 1, 6, '-alpha') ACTIVE_VIEW = None SETTINGS_NAME = 'upiot.sublime-settings' From 440a592eaf0c29138910d990d88dd25e6ca2e4a4 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 19:31:32 -0300 Subject: [PATCH 36/45] update changelog --- CHANGES.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) 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 From 9bbcb93b700b3e1a74fc69673491ced9f9a4578b Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 20:01:03 -0300 Subject: [PATCH 37/45] added close command in the README file --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7e86462..962016d 100644 --- a/README.md +++ b/README.md @@ -97,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) From 1077b84c6b81ce705579657806d2e9f99774f25f Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 20:02:34 -0300 Subject: [PATCH 38/45] saves the file before put it in the device --- commands/put_current_file.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commands/put_current_file.py b/commands/put_current_file.py index a8234e4..0b75e20 100644 --- a/commands/put_current_file.py +++ b/commands/put_current_file.py @@ -39,6 +39,10 @@ def run(self): return file = self.window.active_view().file_name() + view = self.window.active_view() + + if(view.is_dirty()): + view.run_command('save') th = Thread(target=sampy_manager.put_file, args=(file,)) th.start() From fd6f1cbbe2bbff2eb8f000c76f86feb2e0faf00f Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 21:29:08 -0300 Subject: [PATCH 39/45] updated esptool to v2.2 --- tools/esptool.py | 906 +++++++++++++++++++++-------------------------- 1 file changed, 403 insertions(+), 503 deletions(-) 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\ """))) From 1e75f8e96f262969f93d78e67cdcfa81fc25e6e5 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 21:46:40 -0300 Subject: [PATCH 40/45] Added compatibility for micropython loboris (https://github.com/loboris/MicroPython_ESP32_psRAM_LoBo/) --- boards/esp32_loboris.json | 10 ++++++++++ commands/burn_firmware.py | 22 ++++++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 boards/esp32_loboris.json 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 9d71c89..7d8c3fc 100644 --- a/commands/burn_firmware.py +++ b/commands/burn_firmware.py @@ -89,9 +89,18 @@ def firmware_list(self): """ firm_path = join(self.firmwares, '*') + # file names who shouldn't be displayed in the list + blacklist = [ + 'bootloader', + 'bootloader.bin', + 'partitions_mpy.bin', + 'phy_init_data.bin' + ] + for firmware in glob(firm_path): name = basename(firmware) - self.items.append(name) + if(name not in blacklist): + self.items.append(name) def burn_firmware(self): """Burn firmware @@ -103,7 +112,7 @@ def burn_firmware(self): filename = self.url.split('/')[-1] 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?" @@ -127,20 +136,16 @@ def burn_firmware(self): 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 = [] @@ -155,6 +160,7 @@ def get_board_options(board): options.append(option) wf = board_file['upload']['write_flash'] + wf = wf.format(self.firmwares) options.append('write_flash ' + wf) return options From 6d7c615974a727b24002dc7032bffc3256d68821 Mon Sep 17 00:00:00 2001 From: gepd Date: Sun, 21 Jan 2018 22:17:35 -0300 Subject: [PATCH 41/45] Adds "/" in the list of folders and files returned by the device --- tools/ampy/files.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/ampy/files.py b/tools/ampy/files.py index dabdd18..702266e 100644 --- a/tools/ampy/files.py +++ b/tools/ampy/files.py @@ -101,7 +101,17 @@ def ls(self, directory): import os except ImportError: import uos as os - print(os.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() From d3321736c8b5e134aaa54478a849ae917329b9ff Mon Sep 17 00:00:00 2001 From: gepd Date: Wed, 24 Jan 2018 22:29:18 -0300 Subject: [PATCH 42/45] exclude extra files for loboris firmware --- commands/burn_firmware.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/commands/burn_firmware.py b/commands/burn_firmware.py index 7d8c3fc..3ed90a4 100644 --- a/commands/burn_firmware.py +++ b/commands/burn_firmware.py @@ -91,9 +91,11 @@ def firmware_list(self): # file names who shouldn't be displayed in the list blacklist = [ + 'flash.sh', 'bootloader', 'bootloader.bin', 'partitions_mpy.bin', + 'partitions_mpy.csv', 'phy_init_data.bin' ] From 30a7f6bbec4454569f81108e090e5723cdcd68c9 Mon Sep 17 00:00:00 2001 From: gepd Date: Thu, 25 Jan 2018 20:41:36 -0300 Subject: [PATCH 43/45] extract the loboris firmware when it's in zip file and display the corresponding file in the list of firmwares. The file will be renamed based in the current date time data. --- commands/burn_firmware.py | 35 ++++++++++++++++---- commands/download_firmware.py | 61 ++++++++++++++++++++++++++++++----- tools/__init__.py | 5 ++- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/commands/burn_firmware.py b/commands/burn_firmware.py index 3ed90a4..70ec34b 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): @@ -91,6 +96,8 @@ def firmware_list(self): # file names who shouldn't be displayed in the list blacklist = [ + 'README.md', + 'sdkconfig', 'flash.sh', 'bootloader', 'bootloader.bin', @@ -99,10 +106,23 @@ def firmware_list(self): 'phy_init_data.bin' ] - for firmware in glob(firm_path): - name = basename(firmware) - if(name not in blacklist): - self.items.append(name) + # 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 @@ -112,7 +132,10 @@ 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() options.append(firmware) 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/tools/__init__.py b/tools/__init__.py index a4cd9e5..415d2eb 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -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,8 +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) + with open(dst_path, 'wb') as file: try: From ff9d7004e32cb318dd1448b4e75f69a4cbd3a8c4 Mon Sep 17 00:00:00 2001 From: gepd Date: Thu, 25 Jan 2018 23:41:58 -0300 Subject: [PATCH 44/45] update path when a loboris firmware is selected based in the latest changes --- commands/burn_firmware.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/commands/burn_firmware.py b/commands/burn_firmware.py index 70ec34b..3f11262 100644 --- a/commands/burn_firmware.py +++ b/commands/burn_firmware.py @@ -153,12 +153,12 @@ def burn_firmware(self): options.insert(0, "--port " + self.port) - if(not serial.check_port(self.port)): - return - if(self.port in serial.in_use): serial.serial_dict[self.port].disconnect() + if(not serial.check_port(self.port)): + return + Command().run(options, port=self.port) def get_board_options(self): @@ -184,8 +184,13 @@ def get_board_options(self): 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(self.firmwares) + wf = wf.format(replace_path) options.append('write_flash ' + wf) return options From 285bc9539d3d718413042d74570300b90597fce8 Mon Sep 17 00:00:00 2001 From: gepd Date: Tue, 20 Feb 2018 13:49:20 -0300 Subject: [PATCH 45/45] Fix package path to open uPiot settings (Issue: https://github.com/gepd/uPiotMicroPythonTool/issues/5) --- Main.sublime-menu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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" } },