You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
webrepl: support access from LAN, without internet
Host everything on the device itself, rather than redirecting to https://micropython.org/webrepl/ because that doesn't work when there is not internet, including when the device is in Access Point mode. This was a bit slow, because of the many files and being pretty large, but the inline_minify_webrepl.py makes this much better and brings it down to around 1s to load the page, versus 20 seconds. The minification also reduces the size from around 160KB to 80KB.
This commit is contained in:
+4
-1
@@ -28,8 +28,11 @@ __pycache__/
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
# these get created:
|
||||
# these get created by the build system, don't know why:
|
||||
c_mpos/c_mpos
|
||||
|
||||
# build files
|
||||
*.bin
|
||||
|
||||
# auto created by inline_minify_webrepl.py
|
||||
internal_filesystem/builtin/html/webrepl_inlined_minified.html
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
This folder will be filled by the inline_minify_webrepl.py script.
|
||||
@@ -223,7 +223,8 @@ TaskManager.create_task(asyncio_repl()) # only gets started after TaskManager.st
|
||||
|
||||
try:
|
||||
import webrepl
|
||||
webrepl.start(port=7890,password="MPOSweb26") # password is max 9 characters
|
||||
from mpos.webserver import accept_handler as webrepl_accept_handler
|
||||
webrepl.start(port=7890, password="MPOSweb26", accept_handler=webrepl_accept_handler) # password is max 9 characters
|
||||
except Exception as e:
|
||||
print(f"Could not start webrepl - this is normal on desktop systems: {e}")
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Web server helpers for MicroPythonOS."""
|
||||
|
||||
from .webrepl_http import accept_handler
|
||||
|
||||
__all__ = ["accept_handler"]
|
||||
@@ -0,0 +1,136 @@
|
||||
import os
|
||||
import socket
|
||||
import uio
|
||||
|
||||
import _webrepl
|
||||
import webrepl
|
||||
import websocket
|
||||
|
||||
WEBREPL_HTML_PATH = "builtin/html/webrepl_inlined_minified.html"
|
||||
'''
|
||||
# Unused as these files are minified and inlined:
|
||||
#WEBREPL_HTML_PATH = "/builtin/html/webrepl.html"
|
||||
WEBREPL_CONTENT_PATH = "/builtin/html/webrepl.js"
|
||||
WEBREPL_TERM_PATH = "/builtin/html/term.js"
|
||||
WEBREPL_CSS_PATH = "/builtin/html/webrepl.css"
|
||||
WEBREPL_FILE_SAVER_PATH = "/builtin/html/FileSaver.js"
|
||||
'''
|
||||
|
||||
WEBREPL_ASSETS = {
|
||||
b"/": (WEBREPL_HTML_PATH, b"text/html"),
|
||||
b"/index.html": (WEBREPL_HTML_PATH, b"text/html"),
|
||||
#b"/webrepl.css": (WEBREPL_CSS_PATH, b"text/css"),
|
||||
#b"/webrepl.js": (WEBREPL_CONTENT_PATH, b"application/javascript"),
|
||||
#b"/term.js": (WEBREPL_TERM_PATH, b"application/javascript"),
|
||||
#b"/FileSaver.js": (WEBREPL_FILE_SAVER_PATH, b"application/javascript"),
|
||||
}
|
||||
|
||||
|
||||
class _MakefileSocket:
|
||||
def __init__(self, sock, raw_request):
|
||||
self._sock = sock
|
||||
self._raw_request = raw_request
|
||||
|
||||
def makefile(self, *args, **kwargs):
|
||||
return uio.BytesIO(self._raw_request)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._sock, name)
|
||||
|
||||
|
||||
def _read_http_request(cl):
|
||||
req = cl.makefile("rwb", 0)
|
||||
first_line = req.readline()
|
||||
if not first_line:
|
||||
return None, None, b""
|
||||
|
||||
raw_request = first_line
|
||||
headers = {}
|
||||
while True:
|
||||
line = req.readline()
|
||||
if not line:
|
||||
break
|
||||
raw_request += line
|
||||
if line == b"\r\n":
|
||||
break
|
||||
if b":" in line:
|
||||
key, value = line.split(b":", 1)
|
||||
headers[key.strip().lower()] = value.strip().lower()
|
||||
|
||||
parts = first_line.split()
|
||||
path = parts[1] if len(parts) >= 2 else b"/"
|
||||
if b"?" in path:
|
||||
path = path.split(b"?", 1)[0]
|
||||
|
||||
return path, headers, raw_request
|
||||
|
||||
|
||||
def _is_websocket_request(headers):
|
||||
connection = headers.get(b"connection", b"")
|
||||
upgrade = headers.get(b"upgrade", b"")
|
||||
return b"upgrade" in connection and upgrade == b"websocket"
|
||||
|
||||
|
||||
def _send_response(cl, status, content_type, body):
|
||||
cl.send(b"HTTP/1.0 " + status + b"\r\n")
|
||||
cl.send(b"Server: MicroPythonOS\r\n")
|
||||
cl.send(b"Content-Type: " + content_type + b"\r\n")
|
||||
cl.send(b"Content-Length: %d\r\n\r\n" % len(body))
|
||||
cl.send(body)
|
||||
cl.close()
|
||||
|
||||
|
||||
def _send_file_response(cl, path, content_type):
|
||||
try:
|
||||
with open(path, "rb") as handle:
|
||||
body = handle.read()
|
||||
except OSError:
|
||||
_send_response(cl, b"404 Not Found", b"text/plain", b"Not Found")
|
||||
return False
|
||||
|
||||
_send_response(cl, b"200 OK", content_type, body)
|
||||
return False
|
||||
|
||||
|
||||
def _start_webrepl_session(cl, remote_addr):
|
||||
print("\nWebREPL connection from:", remote_addr)
|
||||
webrepl.client_s = cl
|
||||
|
||||
ws = websocket.websocket(cl, True)
|
||||
ws = _webrepl._webrepl(ws)
|
||||
cl.setblocking(False)
|
||||
if hasattr(os, "dupterm_notify"):
|
||||
cl.setsockopt(socket.SOL_SOCKET, 20, os.dupterm_notify)
|
||||
os.dupterm(ws)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def accept_handler(listen_sock):
|
||||
cl, remote_addr = listen_sock.accept()
|
||||
print("\webrepl_http connection from:", remote_addr)
|
||||
try:
|
||||
path, headers, raw_request = _read_http_request(cl)
|
||||
if not path:
|
||||
cl.close()
|
||||
return False
|
||||
|
||||
if _is_websocket_request(headers):
|
||||
if not webrepl.server_handshake(_MakefileSocket(cl, raw_request)):
|
||||
cl.close()
|
||||
return False
|
||||
return _start_webrepl_session(cl, remote_addr)
|
||||
|
||||
if path in WEBREPL_ASSETS:
|
||||
asset_path, content_type = WEBREPL_ASSETS[path]
|
||||
return _send_file_response(cl, asset_path, content_type)
|
||||
|
||||
_send_response(cl, b"404 Not Found", b"text/plain", b"Not Found")
|
||||
return False
|
||||
except Exception as exc:
|
||||
print("webrepl_http: error handling connection:", exc)
|
||||
try:
|
||||
cl.close()
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
@@ -0,0 +1,188 @@
|
||||
/* FileSaver.js
|
||||
* A saveAs() FileSaver implementation.
|
||||
* 1.3.2
|
||||
* 2016-06-16 18:25:19
|
||||
*
|
||||
* By Eli Grey, http://eligrey.com
|
||||
* License: MIT
|
||||
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/*global self */
|
||||
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
|
||||
|
||||
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
|
||||
|
||||
var saveAs = saveAs || (function(view) {
|
||||
"use strict";
|
||||
// IE <10 is explicitly unsupported
|
||||
if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
|
||||
return;
|
||||
}
|
||||
var
|
||||
doc = view.document
|
||||
// only get URL when necessary in case Blob.js hasn't overridden it yet
|
||||
, get_URL = function() {
|
||||
return view.URL || view.webkitURL || view;
|
||||
}
|
||||
, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
|
||||
, can_use_save_link = "download" in save_link
|
||||
, click = function(node) {
|
||||
var event = new MouseEvent("click");
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
, is_safari = /constructor/i.test(view.HTMLElement)
|
||||
, is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent)
|
||||
, throw_outside = function(ex) {
|
||||
(view.setImmediate || view.setTimeout)(function() {
|
||||
throw ex;
|
||||
}, 0);
|
||||
}
|
||||
, force_saveable_type = "application/octet-stream"
|
||||
// the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to
|
||||
, arbitrary_revoke_timeout = 1000 * 40 // in ms
|
||||
, revoke = function(file) {
|
||||
var revoker = function() {
|
||||
if (typeof file === "string") { // file is an object URL
|
||||
get_URL().revokeObjectURL(file);
|
||||
} else { // file is a File
|
||||
file.remove();
|
||||
}
|
||||
};
|
||||
setTimeout(revoker, arbitrary_revoke_timeout);
|
||||
}
|
||||
, dispatch = function(filesaver, event_types, event) {
|
||||
event_types = [].concat(event_types);
|
||||
var i = event_types.length;
|
||||
while (i--) {
|
||||
var listener = filesaver["on" + event_types[i]];
|
||||
if (typeof listener === "function") {
|
||||
try {
|
||||
listener.call(filesaver, event || filesaver);
|
||||
} catch (ex) {
|
||||
throw_outside(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
, auto_bom = function(blob) {
|
||||
// prepend BOM for UTF-8 XML and text/* types (including HTML)
|
||||
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
|
||||
if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
|
||||
return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type});
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
, FileSaver = function(blob, name, no_auto_bom) {
|
||||
if (!no_auto_bom) {
|
||||
blob = auto_bom(blob);
|
||||
}
|
||||
// First try a.download, then web filesystem, then object URLs
|
||||
var
|
||||
filesaver = this
|
||||
, type = blob.type
|
||||
, force = type === force_saveable_type
|
||||
, object_url
|
||||
, dispatch_all = function() {
|
||||
dispatch(filesaver, "writestart progress write writeend".split(" "));
|
||||
}
|
||||
// on any filesys errors revert to saving with object URLs
|
||||
, fs_error = function() {
|
||||
if ((is_chrome_ios || (force && is_safari)) && view.FileReader) {
|
||||
// Safari doesn't allow downloading of blob urls
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = function() {
|
||||
var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;');
|
||||
var popup = view.open(url, '_blank');
|
||||
if(!popup) view.location.href = url;
|
||||
url=undefined; // release reference before dispatching
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
dispatch_all();
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
filesaver.readyState = filesaver.INIT;
|
||||
return;
|
||||
}
|
||||
// don't create more object URLs than needed
|
||||
if (!object_url) {
|
||||
object_url = get_URL().createObjectURL(blob);
|
||||
}
|
||||
if (force) {
|
||||
view.location.href = object_url;
|
||||
} else {
|
||||
var opened = view.open(object_url, "_blank");
|
||||
if (!opened) {
|
||||
// Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html
|
||||
view.location.href = object_url;
|
||||
}
|
||||
}
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
dispatch_all();
|
||||
revoke(object_url);
|
||||
}
|
||||
;
|
||||
filesaver.readyState = filesaver.INIT;
|
||||
|
||||
if (can_use_save_link) {
|
||||
object_url = get_URL().createObjectURL(blob);
|
||||
setTimeout(function() {
|
||||
save_link.href = object_url;
|
||||
save_link.download = name;
|
||||
click(save_link);
|
||||
dispatch_all();
|
||||
revoke(object_url);
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
fs_error();
|
||||
}
|
||||
, FS_proto = FileSaver.prototype
|
||||
, saveAs = function(blob, name, no_auto_bom) {
|
||||
return new FileSaver(blob, name || blob.name || "download", no_auto_bom);
|
||||
}
|
||||
;
|
||||
// IE 10+ (native saveAs)
|
||||
if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
|
||||
return function(blob, name, no_auto_bom) {
|
||||
name = name || blob.name || "download";
|
||||
|
||||
if (!no_auto_bom) {
|
||||
blob = auto_bom(blob);
|
||||
}
|
||||
return navigator.msSaveOrOpenBlob(blob, name);
|
||||
};
|
||||
}
|
||||
|
||||
FS_proto.abort = function(){};
|
||||
FS_proto.readyState = FS_proto.INIT = 0;
|
||||
FS_proto.WRITING = 1;
|
||||
FS_proto.DONE = 2;
|
||||
|
||||
FS_proto.error =
|
||||
FS_proto.onwritestart =
|
||||
FS_proto.onprogress =
|
||||
FS_proto.onwrite =
|
||||
FS_proto.onabort =
|
||||
FS_proto.onerror =
|
||||
FS_proto.onwriteend =
|
||||
null;
|
||||
|
||||
return saveAs;
|
||||
}(
|
||||
typeof self !== "undefined" && self
|
||||
|| typeof window !== "undefined" && window
|
||||
|| this.content
|
||||
));
|
||||
// `self` is undefined in Firefox for Android content script context
|
||||
// while `this` is nsIContentFrameMessageManager
|
||||
// with an attribute `content` that corresponds to the window
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports.saveAs = saveAs;
|
||||
} else if ((typeof define !== "undefined" && define !== null) && (define.amd !== null)) {
|
||||
define([], function() {
|
||||
return saveAs;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# WebREPL content
|
||||
|
||||
These files were sourced from commit `fff7b87` of https://github.com/micropython/webrepl.
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minify and inline WebREPL assets into webrepl_inlined_minified.html."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _is_alphanum(ch: str) -> bool:
|
||||
return ch.isalnum() or ch in "_$\\"
|
||||
|
||||
|
||||
def jsmin(js: str) -> str:
|
||||
"""Minify JavaScript by stripping comments and collapsing whitespace safely."""
|
||||
out: list[str] = []
|
||||
i = 0
|
||||
length = len(js)
|
||||
state = "code"
|
||||
quote = ""
|
||||
|
||||
def peek(offset: int = 1) -> str:
|
||||
idx = i + offset
|
||||
if idx >= length:
|
||||
return ""
|
||||
return js[idx]
|
||||
|
||||
def push_char(ch: str) -> None:
|
||||
out.append(ch)
|
||||
|
||||
while i < length:
|
||||
ch = js[i]
|
||||
nxt = peek(1)
|
||||
|
||||
if state == "code":
|
||||
if ch == "/" and nxt == "/":
|
||||
state = "line_comment"
|
||||
i += 2
|
||||
continue
|
||||
if ch == "/" and nxt == "*":
|
||||
state = "block_comment"
|
||||
i += 2
|
||||
continue
|
||||
if ch in ("'", '"'):
|
||||
state = "string"
|
||||
quote = ch
|
||||
push_char(ch)
|
||||
i += 1
|
||||
continue
|
||||
if ch == "`":
|
||||
state = "template"
|
||||
push_char(ch)
|
||||
i += 1
|
||||
continue
|
||||
if ch.isspace():
|
||||
if out:
|
||||
last = out[-1]
|
||||
if last in "{}[]();,":
|
||||
i += 1
|
||||
continue
|
||||
if last != " ":
|
||||
push_char(" ")
|
||||
i += 1
|
||||
continue
|
||||
if ch in "{}[]();,":
|
||||
if out and out[-1] == " ":
|
||||
out.pop()
|
||||
push_char(ch)
|
||||
i += 1
|
||||
continue
|
||||
push_char(ch)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if state == "line_comment":
|
||||
if ch in ("\n", "\r"):
|
||||
if out and out[-1] != " ":
|
||||
out.append(" ")
|
||||
state = "code"
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if state == "block_comment":
|
||||
if ch == "*" and nxt == "/":
|
||||
state = "code"
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if state == "string":
|
||||
push_char(ch)
|
||||
if ch == "\\" and nxt:
|
||||
push_char(nxt)
|
||||
i += 2
|
||||
continue
|
||||
if ch == quote:
|
||||
state = "code"
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if state == "template":
|
||||
push_char(ch)
|
||||
if ch == "\\" and nxt:
|
||||
push_char(nxt)
|
||||
i += 2
|
||||
continue
|
||||
if ch == "`":
|
||||
state = "code"
|
||||
i += 1
|
||||
continue
|
||||
|
||||
return "".join(out).strip()
|
||||
|
||||
|
||||
def cssmin(css: str) -> str:
|
||||
css = re.sub(r"/\*.*?\*/", "", css, flags=re.DOTALL)
|
||||
css = re.sub(r"\s+", " ", css)
|
||||
css = re.sub(r"\s*([{}:;,>])\s*", r"\1", css)
|
||||
return css.strip()
|
||||
|
||||
|
||||
def inline_assets() -> None:
|
||||
base_dir = Path(__file__).parent
|
||||
html_path = base_dir / "webrepl.html"
|
||||
out_path = base_dir / "webrepl_inlined_minified.html"
|
||||
|
||||
html = html_path.read_text(encoding="utf-8")
|
||||
css = cssmin((base_dir / "webrepl.css").read_text(encoding="utf-8"))
|
||||
term_js = jsmin((base_dir / "term.js").read_text(encoding="utf-8"))
|
||||
file_saver_js = jsmin((base_dir / "FileSaver.js").read_text(encoding="utf-8"))
|
||||
webrepl_js = jsmin((base_dir / "webrepl.js").read_text(encoding="utf-8"))
|
||||
|
||||
replacements = [
|
||||
(r"<link\s+rel=\"stylesheet\"\s+href=\"webrepl\.css\"\s*/?>", f"<style>{css}</style>"),
|
||||
(r"<script\s+src=\"term\.js\"\s*>\s*</script>", f"<script>{term_js}</script>"),
|
||||
(r"<script\s+src=\"FileSaver\.js\"\s*>\s*</script>", f"<script>{file_saver_js}</script>"),
|
||||
(r"<script\s+src=\"webrepl\.js\"\s*>\s*</script>", f"<script>{webrepl_js}</script>"),
|
||||
]
|
||||
|
||||
for pattern, replacement in replacements:
|
||||
new_html, count = re.subn(
|
||||
pattern,
|
||||
lambda _match, rep=replacement: rep,
|
||||
html,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if count != 1:
|
||||
raise RuntimeError(
|
||||
f"Expected to replace exactly one tag for pattern: {pattern}; replaced {count}"
|
||||
)
|
||||
html = new_html
|
||||
|
||||
out_path.write_text(html, encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
inline_assets()
|
||||
+6010
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
html {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
font: 20px/1.5 sans-serif;
|
||||
}
|
||||
.file-box {
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
background: #888;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>MicroPython WebREPL</title>
|
||||
<link rel="stylesheet" href="webrepl.css">
|
||||
<script src="term.js"></script>
|
||||
<script src="FileSaver.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div style="display:inline-block; vertical-align:top;">
|
||||
<form>
|
||||
<input type="text" name="webrepl_url" id="url" value="" />
|
||||
<input type="submit" id="button" value="Connect" onclick="button_click(); return false;" />
|
||||
</form>
|
||||
<div id="term">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="file-boxes" style="display:inline-block; vertical-align:top; width:230px;">
|
||||
|
||||
<div class="file-box">
|
||||
<strong>Send a file</strong>
|
||||
<input type="file" id="put-file-select" />
|
||||
<div id="put-file-list"></div>
|
||||
<input type="button" value="Send to device" id="put-file-button" onclick="put_file(); return false;" />
|
||||
</div>
|
||||
|
||||
<div class="file-box">
|
||||
<strong>Get a file</strong>
|
||||
<input type="text" name="get_filename" id="get_filename" value="" size="13" />
|
||||
<input type="button" value="Get from device" onclick="get_file(); return false;" />
|
||||
</div>
|
||||
|
||||
<div class="file-box" id="file-status"><span style="color:#707070">(file operation status)</span></div>
|
||||
|
||||
</div>
|
||||
|
||||
<br clear="both" />
|
||||
<i>Terminal widget should be focused (text cursor visible) to accept input. Click on it if not.</i><br/>
|
||||
<i>To paste, press Ctrl+A, then Ctrl+V</i>
|
||||
</body>
|
||||
|
||||
<script src="webrepl.js"></script>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,288 @@
|
||||
var term;
|
||||
var ws;
|
||||
var connected = false;
|
||||
var binary_state = 0;
|
||||
var put_file_name = null;
|
||||
var put_file_data = null;
|
||||
var get_file_name = null;
|
||||
var get_file_data = null;
|
||||
|
||||
function calculate_size(win) {
|
||||
var cols = Math.max(80, Math.min(150, (win.innerWidth - 280) / 7)) | 0;
|
||||
var rows = Math.max(24, Math.min(80, (win.innerHeight - 180) / 12)) | 0;
|
||||
return [cols, rows];
|
||||
}
|
||||
|
||||
(function() {
|
||||
window.onload = function() {
|
||||
var url = window.location.hash.substring(1);
|
||||
if (!url) {
|
||||
// pre-populate the url based on the host that served this page.
|
||||
url = document.location.host;
|
||||
}
|
||||
document.getElementById('url').value = 'ws://' + url;
|
||||
var size = calculate_size(self);
|
||||
term = new Terminal({
|
||||
cols: size[0],
|
||||
rows: size[1],
|
||||
useStyle: true,
|
||||
screenKeys: true,
|
||||
cursorBlink: false
|
||||
});
|
||||
term.open(document.getElementById("term"));
|
||||
show_https_warning();
|
||||
};
|
||||
window.addEventListener('resize', function() {
|
||||
var size = calculate_size(self);
|
||||
term.resize(size[0], size[1]);
|
||||
});
|
||||
}).call(this);
|
||||
|
||||
function show_https_warning() {
|
||||
if (window.location.protocol == 'https:') {
|
||||
var warningDiv = document.createElement('div');
|
||||
warningDiv.style.cssText = 'background:#f99;padding:5px;margin-bottom:10px;line-height:1.5em;text-align:center';
|
||||
warningDiv.innerHTML = [
|
||||
'The WebREPL client cannot be accessed over HTTPS connections.',
|
||||
'Load the WebREPL client from the device instead.'
|
||||
].join('<br>');
|
||||
document.body.insertBefore(warningDiv, document.body.childNodes[0]);
|
||||
term.resize(term.cols, term.rows - 7);
|
||||
}
|
||||
}
|
||||
|
||||
function button_click() {
|
||||
if (connected) {
|
||||
ws.close();
|
||||
} else {
|
||||
document.getElementById('url').disabled = true;
|
||||
document.getElementById('button').value = "Disconnect";
|
||||
connected = true;
|
||||
connect(document.getElementById('url').value);
|
||||
}
|
||||
}
|
||||
|
||||
function prepare_for_connect() {
|
||||
document.getElementById('url').disabled = false;
|
||||
document.getElementById('button').value = "Connect";
|
||||
}
|
||||
|
||||
function update_file_status(s) {
|
||||
document.getElementById('file-status').innerHTML = s;
|
||||
}
|
||||
|
||||
function connect(url) {
|
||||
var hostport = url.substring(5);
|
||||
if (hostport === document.location.host) {
|
||||
hostport = '';
|
||||
}
|
||||
|
||||
window.location.hash = hostport;
|
||||
ws = new WebSocket(url);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = function() {
|
||||
term.removeAllListeners('data');
|
||||
term.on('data', function(data) {
|
||||
// Pasted data from clipboard will likely contain
|
||||
// LF as EOL chars.
|
||||
data = data.replace(/\n/g, "\r");
|
||||
ws.send(data);
|
||||
});
|
||||
|
||||
term.on('title', function(title) {
|
||||
document.title = title;
|
||||
});
|
||||
|
||||
term.focus();
|
||||
term.element.focus();
|
||||
term.write('\x1b[31mWelcome to MicroPython!\x1b[m\r\n');
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
var data = new Uint8Array(event.data);
|
||||
switch (binary_state) {
|
||||
case 11:
|
||||
// first response for put
|
||||
if (decode_resp(data) == 0) {
|
||||
// send file data in chunks
|
||||
for (var offset = 0; offset < put_file_data.length; offset += 1024) {
|
||||
ws.send(put_file_data.slice(offset, offset + 1024));
|
||||
}
|
||||
binary_state = 12;
|
||||
}
|
||||
break;
|
||||
case 12:
|
||||
// final response for put
|
||||
if (decode_resp(data) == 0) {
|
||||
update_file_status('Sent ' + put_file_name + ', ' + put_file_data.length + ' bytes');
|
||||
} else {
|
||||
update_file_status('Failed sending ' + put_file_name);
|
||||
}
|
||||
binary_state = 0;
|
||||
break;
|
||||
|
||||
case 21:
|
||||
// first response for get
|
||||
if (decode_resp(data) == 0) {
|
||||
binary_state = 22;
|
||||
var rec = new Uint8Array(1);
|
||||
rec[0] = 0;
|
||||
ws.send(rec);
|
||||
}
|
||||
break;
|
||||
case 22: {
|
||||
// file data
|
||||
var sz = data[0] | (data[1] << 8);
|
||||
if (data.length == 2 + sz) {
|
||||
// we assume that the data comes in single chunks
|
||||
if (sz == 0) {
|
||||
// end of file
|
||||
binary_state = 23;
|
||||
} else {
|
||||
// accumulate incoming data to get_file_data
|
||||
var new_buf = new Uint8Array(get_file_data.length + sz);
|
||||
new_buf.set(get_file_data);
|
||||
new_buf.set(data.slice(2), get_file_data.length);
|
||||
get_file_data = new_buf;
|
||||
update_file_status('Getting ' + get_file_name + ', ' + get_file_data.length + ' bytes');
|
||||
|
||||
var rec = new Uint8Array(1);
|
||||
rec[0] = 0;
|
||||
ws.send(rec);
|
||||
}
|
||||
} else {
|
||||
binary_state = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 23:
|
||||
// final response
|
||||
if (decode_resp(data) == 0) {
|
||||
update_file_status('Got ' + get_file_name + ', ' + get_file_data.length + ' bytes');
|
||||
saveAs(new Blob([get_file_data], {type: "application/octet-stream"}), get_file_name);
|
||||
} else {
|
||||
update_file_status('Failed getting ' + get_file_name);
|
||||
}
|
||||
binary_state = 0;
|
||||
break;
|
||||
case 31:
|
||||
// first (and last) response for GET_VER
|
||||
console.log('GET_VER', data);
|
||||
binary_state = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
term.write(event.data);
|
||||
};
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
connected = false;
|
||||
if (term) {
|
||||
term.write('\x1b[31mDisconnected\x1b[m\r\n');
|
||||
}
|
||||
term.off('data');
|
||||
prepare_for_connect();
|
||||
}
|
||||
}
|
||||
|
||||
function decode_resp(data) {
|
||||
if (data[0] == 'W'.charCodeAt(0) && data[1] == 'B'.charCodeAt(0)) {
|
||||
var code = data[2] | (data[3] << 8);
|
||||
return code;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function put_file() {
|
||||
var dest_fname = put_file_name;
|
||||
var dest_fsize = put_file_data.length;
|
||||
|
||||
// WEBREPL_FILE = "<2sBBQLH64s"
|
||||
var rec = new Uint8Array(2 + 1 + 1 + 8 + 4 + 2 + 64);
|
||||
rec[0] = 'W'.charCodeAt(0);
|
||||
rec[1] = 'A'.charCodeAt(0);
|
||||
rec[2] = 1; // put
|
||||
rec[3] = 0;
|
||||
rec[4] = 0; rec[5] = 0; rec[6] = 0; rec[7] = 0; rec[8] = 0; rec[9] = 0; rec[10] = 0; rec[11] = 0;
|
||||
rec[12] = dest_fsize & 0xff; rec[13] = (dest_fsize >> 8) & 0xff; rec[14] = (dest_fsize >> 16) & 0xff; rec[15] = (dest_fsize >> 24) & 0xff;
|
||||
rec[16] = dest_fname.length & 0xff; rec[17] = (dest_fname.length >> 8) & 0xff;
|
||||
for (var i = 0; i < 64; ++i) {
|
||||
if (i < dest_fname.length) {
|
||||
rec[18 + i] = dest_fname.charCodeAt(i);
|
||||
} else {
|
||||
rec[18 + i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// initiate put
|
||||
binary_state = 11;
|
||||
update_file_status('Sending ' + put_file_name + '...');
|
||||
ws.send(rec);
|
||||
}
|
||||
|
||||
function get_file() {
|
||||
var src_fname = document.getElementById('get_filename').value;
|
||||
|
||||
// WEBREPL_FILE = "<2sBBQLH64s"
|
||||
var rec = new Uint8Array(2 + 1 + 1 + 8 + 4 + 2 + 64);
|
||||
rec[0] = 'W'.charCodeAt(0);
|
||||
rec[1] = 'A'.charCodeAt(0);
|
||||
rec[2] = 2; // get
|
||||
rec[3] = 0;
|
||||
rec[4] = 0; rec[5] = 0; rec[6] = 0; rec[7] = 0; rec[8] = 0; rec[9] = 0; rec[10] = 0; rec[11] = 0;
|
||||
rec[12] = 0; rec[13] = 0; rec[14] = 0; rec[15] = 0;
|
||||
rec[16] = src_fname.length & 0xff; rec[17] = (src_fname.length >> 8) & 0xff;
|
||||
for (var i = 0; i < 64; ++i) {
|
||||
if (i < src_fname.length) {
|
||||
rec[18 + i] = src_fname.charCodeAt(i);
|
||||
} else {
|
||||
rec[18 + i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// initiate get
|
||||
binary_state = 21;
|
||||
get_file_name = src_fname;
|
||||
get_file_data = new Uint8Array(0);
|
||||
update_file_status('Getting ' + get_file_name + '...');
|
||||
ws.send(rec);
|
||||
}
|
||||
|
||||
function get_ver() {
|
||||
// WEBREPL_REQ_S = "<2sBBQLH64s"
|
||||
var rec = new Uint8Array(2 + 1 + 1 + 8 + 4 + 2 + 64);
|
||||
rec[0] = 'W'.charCodeAt(0);
|
||||
rec[1] = 'A'.charCodeAt(0);
|
||||
rec[2] = 3; // GET_VER
|
||||
// rest of "rec" is zero
|
||||
|
||||
// initiate GET_VER
|
||||
binary_state = 31;
|
||||
ws.send(rec);
|
||||
}
|
||||
|
||||
function handle_put_file_select(evt) {
|
||||
// The event holds a FileList object which is a list of File objects,
|
||||
// but we only support single file selection at the moment.
|
||||
var files = evt.target.files;
|
||||
|
||||
// Get the file info and load its data.
|
||||
var f = files[0];
|
||||
put_file_name = f.name;
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
put_file_data = new Uint8Array(e.target.result);
|
||||
document.getElementById('put-file-list').innerHTML = '' + escape(put_file_name) + ' - ' + put_file_data.length + ' bytes';
|
||||
document.getElementById('put-file-button').disabled = false;
|
||||
};
|
||||
reader.readAsArrayBuffer(f);
|
||||
}
|
||||
|
||||
document.getElementById('put-file-select').addEventListener('click', function(){
|
||||
this.value = null;
|
||||
}, false);
|
||||
|
||||
document.getElementById('put-file-select').addEventListener('change', handle_put_file_select, false);
|
||||
document.getElementById('put-file-button').disabled = true;
|
||||
Reference in New Issue
Block a user