Complètement refactorisé la partie WEB_IDE pour en faire une IDE_remote. Ainsi je peux ajouter un petit nouveau SOCKET_IDE ;-)

This commit is contained in:
Christian Zufferey
2019-10-20 11:39:24 +02:00
parent 6ab29bd11b
commit d1ffbc522c
28 changed files with 1654 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
# Nouvelle tentative de gestion de NodeMCU à distance
## Sources
Cette fois, je teste le pluggin https://atom.io/packages/nodemcu-thingy pour voir si je peux remplacer l'IDE local (connection USB) ESPlorer et travailler en remote via le WIFI.
Je suis parti de ce dépôt https://github.com/holtermp/nodemcu-thingy, que j'ai *forké* chez moi sur ce dépôt https://github.com/zuzu59/nodemcu-thingy.
La source vient de ce dépôt https://github.com/creationix/nodemcu-webide

View File

@@ -0,0 +1,148 @@
# nodemcu-thingy
**THIS PACKAGE IS CURRENTLY UNDER DEVOLOPMENT AND TO BE CONSIDERED ALPHA**
NodeMCU Thingy is an [atom](https://atom.io/) package for "over the air" development on the NodeMCU platform.
It uses a websocket connection to the esp8266 for communication, so in an ideal world the USB cable should be needed only to upload the basic firmware and the initial websocket server to the NodeMCU.
Features include:
* Upload of files to the NodeMCU
* Download of files from the NodeMCU
* Deletion of files on the NodeMCU
* Interactive Console
![demo](https://github.com/holtermp/nodemcu-thingy/blob/master/screencasts/console.gif)
**Setup**
*Get the firmware*
Flash your esp8266 with a current NodeMCU firmware. I recommend using a firmware built with Marcel Stoer's excellent online tool at https://nodemcu-build.com/
Select branch ```master``` and at least the following 9 modules:
* ```bit```
* ```crypto```
* ```file```
* ```gpio```
* ```net```
* ```node```
* ```tmr```
* ```uart```
* ```wifi```
Note that ```bit``` and ```crypto``` are not selected by default.
This creates two versions of the firmware: integer and float. If you do not know which one to use, the integer version is probably ok for you.
*Flash the firmware*
Download ```esptool.py``` from https://github.com/espressif/esptool.
Flash the firmware to the esp8266 with (use the appropriate ```--port``` parameter)
```
esptool.py --port /dev/ttyUSB0 erase_flash
esptool.py --port /dev/ttyUSB0 write_flash -fm dio 0x00000 nodemcu-master*.bin
```
which should produce an output like:
```
esptool.py v1.3-dev
Connecting...
Running Cesanta flasher stub...
Erasing flash (this may take a while)...
Erase took 6.6 seconds
esptool.py v1.3-dev
Connecting...
Auto-detected Flash size: 32m
Running Cesanta flasher stub...
Flash params set to 0x0240
Writing 401408 @ 0x0... 401408 (100 %)
Wrote 401408 bytes at 0x0 in 34.8 seconds (92.3 kbit/s)...
Leaving...
```
Give it a few seconds to reboot.
*Install websocket server*
Cd to ```.atom/packages/nodemcu-thingy/mcu/```.
Change ```init.lua``` to match your wireless LAN.
```
station_cfg.ssid="your ssid"
station_cfg.pwd="yout wifi password"
wifi.sta.sethostname("your hostname for the esp8266")
```
Edit ```upload.sh``` and check if the ```--port``` value matches your system and execute ```bash upload.sh```.
This takes some time and you should see a lot of lines like
```
->file.writeline([==[end]==]) -> ok
```
ending in
```
->file.flush() -> ok
->file.close() -> ok
--->>> All done <<<---
```
Restart the esp8266 by pressing the ```RST``` button on the esp8266.
After a few seconds you should be able to ping the esp8266 using the value from ```wifi.sta.sethostname()```
*Configure package nodemcu-thingy*
Open Atom's preferences screen (```Edit->Preferences``` or ```Ctrl-,```). Open ```Packages```. Find package ```nodemcu-thingy``` and click ```Settings```.
Enter the ip address or hostname of your esp8266 (value from ```wifi.sta.sethostname()```) into the ```host``` field.
Activate the package with ```Packages->nodemcu-thingy->Toggle```
Click ```Connect```.
If everything worked the red ```Disconnected``` changes to a green ```Connected``` after a few seconds.
![demo](https://github.com/holtermp/nodemcu-thingy/blob/master/screencasts/connect.gif)
You are now good to go.
Very brave programmers can now unhook the usb cable from the computer and run
the esp8266 on some other power source (probably a phone charger).
*Use package nodemcu-thingy*
You can upload, download, erase files on the esp8266.
The installation process has installed 3 files on the esp8266 which are protected from modification by this package:
* ```websocket.lc``` implements the websocket protocol
* ```main.lc``` implements the server side functions for this package
* ```init.lua``` called by the NodeMCU firmware on startup.
* sets up the wifi connection
* starts the websocket server
* calls ```userinit.lua``` if present.
(The original implementation of these 3 files is borrowed from [nodemcu-webide](https://github.com/creationix/nodemcu-webide))
To implement your own functionality upload a custom ```userinit.lua``` to start your own custom code.
![demo](https://github.com/holtermp/nodemcu-thingy/blob/master/screencasts/upload.gif)
See https://nodemcu.readthedocs.io/en/master/ for the all the good stuff you can use...
*Credits*
The Websocket server on the NodeMCU is a slightly modified version of the one in Creationix's
[nodemcu-webide](https://github.com/creationix/nodemcu-webide)
The Python luatool comes from 4ref0nt@gmail.com (http://esp8266.ru)

View File

@@ -0,0 +1,5 @@
{
"atom-workspace": {
"ctrl-alt-o": "nodemcu-thingy:toggle"
}
}

View File

@@ -0,0 +1,265 @@
'use babel';
var util=require( './util')
const ID_CONSOLE = 'ID_CONSOLE'
const ID_BTN_RESTART = 'ID_BTN_RESTART'
const ID_BTN_CONNECT = 'ID_BTN_CONNECT'
const ID_BTN_LIST = 'ID_BTN_LIST'
const ID_CMD_LINE = 'ID_CMD_LINE'
const ID_BADGE_CONNECTION_STATUS = 'ID_BADGE_CONNECTION_STATUS'
const ID_UL_FILE_LIST_ROOT = 'ID_UL_FILE_LIST_ROOT'
function createButton(label, id) {
var button = document.createElement('button');
button.classList.add('inline-block-tight');
button.classList.add('btn');
var txt= document.createTextNode(label);
button.appendChild(txt)
button.id= id
button.onclick=eventHandler
return button
}
var handlers=[];
function eventHandler(event) {
var id=event.target.id
console.log(id+" pressed...")
func=handlers[id];
func(event);
}
export default class NodemcuThingyView {
constructor(serializedState) {
// Create root element
this.element = document.createElement('div');
this.element.classList.add('nodemcu-thingy');
// Create message element
const message = document.createElement('div');
message.classList.add('message');
message.style.height="100%"
var div = document.createElement('div');
div.classList.add("block")
message.appendChild(div);
div.appendChild(createButton("Connect",ID_BTN_CONNECT));
div.appendChild(createButton("Restart",ID_BTN_RESTART));
div.appendChild(createButton("List Files",ID_BTN_LIST));
div.style.height="100%";
var span = document.createElement('span');
span.classList.add("badge");
span.classList.add("badge-error");
var txt= document.createTextNode("Disconnected");
span.id=ID_BADGE_CONNECTION_STATUS;
span.appendChild(txt)
div.appendChild(span);
var centerDiv = document.createElement('div');
centerDiv.classList.add("block")
centerDiv.style.height="80%";
centerDiv.style.width="100%";
div.appendChild(centerDiv);
var consoleDiv = document.createElement('div');
consoleDiv.classList.add('inline-block');
consoleDiv.style.height="100%";
consoleDiv.style.width="75%";
centerDiv.appendChild(consoleDiv);
textarea=document.createElement('textarea');
textarea.classList.add('input-textarea');
textarea.style.height="100%";
textarea.style.width="100%";
textarea.readOnly = true
textarea.id=ID_CONSOLE
//this is needed for cursor movement etc.
textarea.classList.add('native-key-bindings')
consoleDiv.appendChild(textarea);
var input =document.createElement('input');
input.classList.add('input-text');
input.classList.add('native-key-bindings');
input.type='text'
input.id=ID_CMD_LINE
input.onkeypress=eventHandler
consoleDiv.appendChild(input);
var fileDiv = document.createElement('div');
fileDiv.classList.add('inline-block');
fileDiv.style.height="100%"
centerDiv.appendChild(fileDiv);
this.createFileListView(fileDiv);
this.element.appendChild(message);
/*
// Wonder if we use that one day for automatic upload
const disposable = atom.project.onDidChangeFiles(events => {
for (const event of events) {
if (event.path.indexOf("/.atom")==-1) {
// "created", "modified", "deleted", or "renamed"
console.log(`Event action: ${event.action}`)
// absolute path to the filesystem entry that was touched
console.log(`Event path: ${event.path}`)
if (event.type === 'renamed') {
console.log(`.. renamed from: ${event.oldPath}`)
}
}
}
});
*/
}
createFileListView(parent) {
var ulRoot = document.createElement('ul');
ulRoot.classList.add("list-tree")
parent.appendChild(ulRoot);
var li = document.createElement('li');
li.classList.add("list-nested-item");
ulRoot.appendChild(li);
var liDiv= document.createElement('div');
liDiv.classList.add("list-item");
li.appendChild(liDiv);
var liSpan = document.createElement("span");
liSpan.classList.add("icon");
liSpan.classList.add("icon-file-directory") ;
liSpan.innerText="NodeMCU";
liDiv.appendChild(liSpan);
var fileListRoot = document.createElement("ul");
fileListRoot.classList.add("list-tree");
fileListRoot.id=ID_UL_FILE_LIST_ROOT;
liDiv.appendChild(fileListRoot);
}
registerConnectButtonHandler(func){
handlers[ID_BTN_CONNECT]=func;
}
registerRestartButtonHandler(func){
handlers[ID_BTN_RESTART]=func;
}
registerListButtonHandler(func){
handlers[ID_BTN_LIST]=func;
}
registerCmdLineHandler(func){
handlers[ID_CMD_LINE]=func;
}
// Returns an object that can be retrieved when package is activated
serialize() {}
// Tear down any state and detach
destroy() {
this.element.remove();
}
getElement() {
return this.element;
}
writeToConsole(text){
textarea=document.getElementById(ID_CONSOLE)
textarea.value+=text;
textarea.scrollTop = textarea.scrollHeight;
}
getCmdLineValue() {
input=document.getElementById(ID_CMD_LINE)
return input.value
}
setCmdLineValue(value) {
input=document.getElementById(ID_CMD_LINE)
input.value=value
}
setConnected(connected) {
var span=document.getElementById(ID_BADGE_CONNECTION_STATUS)
if (span!=null){
var showsConnected=span.classList.contains("badge-success");
if (connected && !showsConnected){
span.classList.add("badge-success");
span.classList.remove("badge-error");
span.innerHTML='Connected'
}
else if (!connected && showsConnected) {
span.classList.remove("badge-success");
span.classList.add("badge-error");
span.innerHTML='Disconnected'
}
}
}
setFileList(list) {
var root=document.getElementById(ID_UL_FILE_LIST_ROOT)
while (root.firstChild) {
root.removeChild(root.firstChild);
}
for (var i = 0; i < list.length; i++) {
var li = document.createElement('li');
li.classList.add("list-item");
root.appendChild(li);
var span = document.createElement('span');
span.classList.add("icon");
var iconType="text"
var filename=list[i].substring(0,list[i].indexOf(" ("));
if (util.isWriteprotected(filename)) {
iconType="zip"
span.classList.add('text-subtle');
}
else{
if (filename.endsWith(".lua")) {
iconType="code"
}
else
if (filename.endsWith(".lc")) {
iconType="binary"
}
}
span.classList.add("mcu-download");
span.classList.add("icon-file-"+iconType);
span.innerText=list[i];
li.appendChild(span);
}
}
getTitle() {
// Used by Atom for tab text
return 'NodeMCU Thingy';
}
getURI() {
// Used by Atom to identify the view when toggling.
return 'atom://nodemcu-thingy';
}
getDefaultLocation() {
// This location will be used if the user hasn't overridden it by dragging the item elsewhere.
// Valid values are "left", "right", "bottom", and "center" (the default).
return 'right';
}
getAllowedLocations() {
// The locations into which the item can be moved.
return ['left', 'right', 'bottom'];
}
}

View File

@@ -0,0 +1,342 @@
'use babel';
const pingInterval=3000
// how many ms to wait for ping reply
const pongOffset=200
var util=require( './util')
//find the position of the given byte in the byteArray
function findByte(byteArray, byte) {
for(var i=0;i<byteArray.length;i++) {
if (byteArray[i] == byte) {
return i;
}
}
return -1;
}
function parseFileList(arraybuffer) {
var byteArray = new Uint8Array(arraybuffer);
var dec= new TextDecoder('utf-8');
var list = []
var end=0;
while ( (end = findByte(byteArray,0))>=0) {
if (end==0){
byteArray=byteArray.slice(1);
}
else{
var strByte= byteArray.slice(0,end);
//filename
var str=dec.decode(strByte)
byteArray=byteArray.slice(end+1);
end=findByte(byteArray,0);
var strByte= byteArray.slice(0,end);
str = str+" ("+dec.decode(strByte)+")"
byteArray= byteArray.slice(end);
list.push(str);
}
}
return list
}
function getFileName(event) {
var str=event.target.innerText;
str = str.substring(0,str.indexOf(" "));
return str
}
import NodemcuThingyView from './nodemcu-thingy-view';
import { CompositeDisposable, Disposable } from 'atom';
export default {
config:{
mcu_hostname:{
title: 'Host',
description: 'Host name or ip of the nodemcu',
type: 'string',
default: '192.168.1.100',
},
download_dir:{
title: 'Download folder',
description: 'Defines a folder to receive the file, when you click '+
'``Download`` in the mcu file view.',
type: 'string',
default: 'mcu',
}
},
nodemcuThingyView: null,
subscriptions: null,
connection:null,
pongtime:null,
pongReceived:null,
lsRequested:null,
loadRequested:null,
downloadFileName:null,
activate(state) {
pongReceived=false;
lsRequested=false;
loadRequested=false;
var t=this;
this.startPing();
this.nodemcuThingyView = new NodemcuThingyView(state.nodemcuThingyViewState);
this.nodemcuThingyView.registerConnectButtonHandler(() => this.connectMCU());
this.nodemcuThingyView.registerRestartButtonHandler(() => this.restartMCU());
this.nodemcuThingyView.registerListButtonHandler(() => this.list());
this.nodemcuThingyView.registerCmdLineHandler((event) => this.evalCmd(event));
this.subscriptions = new CompositeDisposable(
// Add an opener for our view.
atom.workspace.addOpener(uri => {
if (uri === 'atom://nodemcu-thingy') {
return new NodemcuThingyView();
}
}),
// Register command that toggles this view
atom.commands.add('atom-workspace', {
'nodemcu-thingy:toggle': () => this.toggle(),
'nodemcu-thingy:upload': () => this.upload(),
'nodemcu-thingy:list': () => this.list(),
'nodemcu-thingy:sendTest': () => this.sendTest(),
'nodemcu-thingy:download': (event) => this.download(event),
'nodemcu-thingy:delete': (event) => this.delete(event),
'nodemcu-thingy:dofile': (event) => this.dofile(event)
}),
// Destroy any NodemcuThingyViews when the package is deactivated.
new Disposable(() => {
atom.workspace.getPaneItems().forEach(item => {
if (item instanceof NodemcuThingyView) {
item.destroy();
}
});
})
);
},
deactivate() {
this.subscriptions.dispose();
connection.close();
},
serialize() {
return {
nodemcuThingyViewState: this.nodemcuThingyView.serialize()
};
},
startPing(){
var t = this;
const intervalObj = setInterval(() => {
if (typeof connection !== 'undefined' && connection !== null && connection.readyState === WebSocket.OPEN ) {
connection.send("ping");
}
//expect reply after a certain time
setTimeout(() => {
t.checkPongReceived();
},pongOffset);
}, pingInterval);
},
getView(){
return this.nodemcuThingyView
},
checkConnectionAndRun(callback) {
if (typeof connection !== 'undefined' && connection !== null && connection.readyState === WebSocket.OPEN ) {
callback();
}
else {
this.createConnection(callback);
}
} ,
createConnection(callback) {
var mcuView=this.nodemcuThingyView;
mcuView.writeToConsole("connecting...\n");
connection = new WebSocket('ws://'+atom.config.get('nodemcu-thingy.mcu_hostname'));
connection.binaryType = "arraybuffer";
// Log errors
connection.onerror = function (error) {
console.log('WebSocket Error ' + error);
mcuView.writeToConsole('WebSocket Error ' + error);
};
connection.onopen = function (e) {
mcuView.writeToConsole("connected\n");
callback();
}
var fs = require('fs');
var t=this
// Log messages from the server
connection.onmessage = function (e) {
if (e.data instanceof ArrayBuffer) {
if (e.data.byteLength==4 ){
var dec= new TextDecoder('utf-8');
var str=dec.decode(e.data)
if (str == "pong") {
//register reception globally
pongReceived=true;
}
}
if (!pongReceived){
if(lsRequested) {
lsRequested=false;
var fileList =parseFileList(e.data)
t.getView().setFileList(fileList);
}
if(loadRequested) {
loadRequested=false;
var dec= new TextDecoder('utf-8');
var str=dec.decode(e.data)
var foldername =atom.config.get('nodemcu-thingy.download_dir')
var wstream = fs.createWriteStream(foldername+'/'+downloadFileName);
wstream.write(str);
t.getView().writeToConsole("Downloaded:\""+foldername+'/'+downloadFileName+"\"");
}
}
}
else {
t.getView().writeToConsole(e.data);
}
}
},
//registered menu methods
toggle() {
atom.workspace.toggle('atom://nodemcu-thingy');
},
sendTest() {
this.checkConnectionAndRun(() => {
enc= new TextEncoder('utf-8');
var bytes = enc.encode("print(\"testing:\"..node.heap())")
connection.send(bytes);
connection.send('eval:bla');
});
},
download(event) {
var mcuFilename=getFileName(event);
//store filename for websocket response
downloadFileName=mcuFilename;
loadRequested=true;
var foldername =atom.config.get('nodemcu-thingy.download_dir')
var fs = require('fs');
if (!fs.existsSync(foldername)) {
atom.confirm ({
message: "Folder \""+foldername+"\" does not exist, create?",
buttons: {"Yes":() => {
fs.mkdirSync(foldername);
connection.send('load:'+mcuFilename);
}, "No":() => {}}});
}
else{
connection.send('load:'+mcuFilename);
}
},
delete(event) {
var mcuFilename=getFileName(event);
if (util.isWriteprotected(mcuFilename)) {
atom.confirm ({
message: "Cannot delete writeprotected \""+mcuFilename+"\"!",
buttons: {
"OK":() => {}}});
return
}
var t = this;
atom.confirm ({
message: "Really delete \""+mcuFilename+"\"?",
buttons: {
"Yes":() => {
this.sendCmd("file.remove(\""+mcuFilename+"\");print(\"Removed "+mcuFilename+" \")");
//auto refresh file list TODO make eventdriven?
setTimeout(() => {
t.list();
},2000)
},
"No":() => {}}});
},
dofile(event) {
var mcuFilename=getFileName(event);
var mcuView=this.nodemcuThingyView;
mcuView.writeToConsole("running "+mcuFilename+"...\n");
this.sendCmd("dofile(\""+mcuFilename+"\")");
},
sendCmd(cmd) {
enc= new TextEncoder('utf-8');
var bytes = enc.encode(cmd)
connection.send(bytes);
connection.send('eval:cmd');
},
evalCmd(event) {
switch(event.key) {
case "ArrowUp":
//FIXME implement
break;
case "ArrowDown":
//FIXME implement
break;
case "Enter":
var t= this;
this.checkConnectionAndRun(() => {
var cmd = t.getView().getCmdLineValue();
t.sendCmd(cmd);
});
break;
}
},
connectMCU(){
connection=null;
var t = this;
this.checkConnectionAndRun(() => {
t.sendCmd('print("...")');
});
},
restartMCU(){
var t= this;
this.checkConnectionAndRun(() => {
var cmd = t.getView().getCmdLineValue();
t.sendCmd('print("Restarting...")');
t.sendCmd('node.restart()');
connection.close();
});
},
list(){
var t= this;
this.checkConnectionAndRun(() => {
lsRequested=true;
connection.send('ls');
});
},
upload() {
this.checkConnectionAndRun(() => {
const editor = atom.workspace.getActiveTextEditor();
const filePath=editor.getPath();
if (filePath != null) {
var fs = require('fs');
var path = require('path');
if (util.isWriteprotected(path.basename(filePath))) {
atom.confirm ({
message: "Cannot upload writeprotected \""+path.basename(filePath)+"\"!",
buttons: {
"OK":() => {}}});
return
}
var contents = fs.readFileSync(filePath, { encoding: 'utf-8'});
enc= new TextEncoder('utf-8');
var bytes = enc.encode(contents)
connection.send(bytes);
connection.send('save:'+path.basename(filePath));
}
});
},
checkPongReceived() {
var s="pongReceived:"+pongReceived;
//update status
this.getView().setConnected(pongReceived);
//reset for next ping
pongReceived=false;
}
};

View File

@@ -0,0 +1,11 @@
'use babel';
function isWriteprotected(filename) {
var protectedFiles=["init.lua","websocket.lc", "main.lc"];
for (var i = 0; i < protectedFiles.length; i++) {
if (protectedFiles[i] === filename) {
return true;
}
}
}
module.exports.isWriteprotected = isWriteprotected;

View File

@@ -0,0 +1,58 @@
local bootreasons={
[0]="power-on",
[1]="hardware watchdog reset",
[2]="exception reset",
[3]="software watchdog reset",
[4]="software restart",
[5]="wake from deep sleep",
[6]="external reset"
}
local ip = wifi.sta.getip()
if ip then
print("already got:"..ip)
else
print("Connecting...")
wifi.setmode(wifi.STATION)
ip_cfg={}
ip_cfg.ip = "192.168.6.90"
ip_cfg.netmask = "255.255.255.0"
ip_cfg.gateway = "192.168.6.1"
wifi.sta.setip(ip_cfg)
station_cfg={}
station_cfg.ssid="holternet"
station_cfg.pwd="nemosushi_sushinemo"
wifi.sta.sethostname("mybutton1")
wifi.sta.config(station_cfg)
wifi.sta.connect()
wifi.sta.autoconnect(1)
tmr.alarm(0, 1000, 1, function ()
local ip = wifi.sta.getip()
if ip then
tmr.stop(0)
print(ip)
end
end)
end
dofile("websocket.lc")
dofile("main.lc")
if file.exists("userinit.lua") then
--[[
0, power-on
1, hardware watchdog reset
2, exception reset
3, software watchdog reset
4, software restart
5, wake from deep sleep
6, external reset
]]
_ , reason = node.bootreason()
if (reason<1 or reason > 3) then
dofile("userinit.lua")
else
print ("Bootreason="..reason.." ("..bootreasons[reason].."), skipping userinit.lua")
end
else
print("userinit.lua not found")
end

View File

@@ -0,0 +1,213 @@
#!/usr/bin/env python2
#
# ESP8266 luatool
# Author e-mail: 4ref0nt@gmail.com
# Site: http://esp8266.ru
# Contributions from: https://github.com/sej7278
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
# Street, Fifth Floor, Boston, MA 02110-1301 USA.
import sys
import serial
from time import sleep
import argparse
from os.path import basename
version = "0.6.3"
def writeln(data, check=1):
if s.inWaiting() > 0:
s.flushInput()
if len(data) > 0:
sys.stdout.write("\r\n->")
sys.stdout.write(data.split("\r")[0])
s.write(data)
sleep(0.3)
if check > 0:
line = ''
char = ''
while char != chr(62): # '>'
char = s.read(1)
if char == '':
raise Exception('No proper answer from MCU')
if char == chr(13) or char == chr(10): # LF or CR
if line != '':
line = line.strip()
if line+'\r' == data:
sys.stdout.write(" -> ok")
else:
if line[:4] == "lua:":
sys.stdout.write("\r\n\r\nLua ERROR: %s" % line)
raise Exception('ERROR from Lua interpreter\r\n\r\n')
else:
data = data.split("\r")[0]
sys.stdout.write("\r\n\r\nERROR")
sys.stdout.write("\r\n send string : '%s'" % data)
sys.stdout.write("\r\n expected echo : '%s'" % data)
sys.stdout.write("\r\n but got answer : '%s'" % line)
sys.stdout.write("\r\n\r\n")
raise Exception('Error sending data to MCU\r\n\r\n')
line = ''
else:
line += char
else:
sys.stdout.write(" -> send without check")
def writer(data):
writeln("file.writeline([==[" + data + "]==])\r")
def openserial(args):
# Open the selected serial port
try:
s = serial.Serial(args.port, args.baud)
except:
sys.stderr.write("Could not open port %s\n" % (args.port))
sys.exit(1)
if args.verbose:
sys.stderr.write("Set timeout %s\r\n" % s.timeout)
s.timeout = 3
if args.verbose:
sys.stderr.write("Set interCharTimeout %s\r\n" % s.interCharTimeout)
s.interCharTimeout = 3
return s
if __name__ == '__main__':
# parse arguments or use defaults
parser = argparse.ArgumentParser(description='ESP8266 Lua script uploader.')
parser.add_argument('-p', '--port', default='/dev/ttyUSB0', help='Device name, default /dev/ttyUSB0')
# parser.add_argument('-b', '--baud', default=9600, help='Baudrate, default 9600')
parser.add_argument('-b', '--baud', default=115200, help='Baudrate, default 115200')
parser.add_argument('-f', '--src', default='main.lua', help='Source file on computer, default main.lua')
parser.add_argument('-t', '--dest', default=None, help='Destination file on MCU, default to source file name')
parser.add_argument('-c', '--compile', action='store_true', help='Compile lua to lc after upload')
parser.add_argument('-r', '--restart', action='store_true', help='Restart MCU after upload')
parser.add_argument('-d', '--dofile', action='store_true', help='Run the Lua script after upload')
parser.add_argument('-v', '--verbose', action='store_true', help="Show progress messages.")
parser.add_argument('-l', '--list', action='store_true', help='List files on device')
parser.add_argument('-w', '--wipe', action='store_true', help='Delete all lua/lc files on device.')
args = parser.parse_args()
if args.list:
s = openserial(args)
writeln("local l = file.list();for k,v in pairs(l) do print('name:'..k..', size:'..v)end\r", 0)
while True:
char = s.read(1)
if char == '' or char == chr(62):
break
sys.stdout.write(char)
sys.exit(0)
if args.wipe:
s = openserial(args)
writeln("local l = file.list();for k,v in pairs(l) do print(k)end\r", 0)
file_list = []
fn = ""
while True:
char = s.read(1)
if char == '' or char == chr(62):
break
if char not in ['\r', '\n']:
fn += char
else:
if fn:
file_list.append(fn.strip())
fn = ''
for fn in file_list[1:]: # first line is the list command sent to device
if args.verbose:
sys.stderr.write("Delete file {} from device.\r\n".format(fn))
writeln("file.remove(\"" + fn + "\")\r")
sys.exit(0)
if args.dest is None:
args.dest = basename(args.src)
# open source file for reading
try:
f = open(args.src, "rt")
except:
sys.stderr.write("Could not open input file \"%s\"\n" % args.src)
sys.exit(1)
# Verify the selected file will not exceed the size of the serial buffer.
# The size of the buffer is 256. This script does not accept files with
# lines longer than 230 characters to have some room for command overhead.
for ln in f:
if len(ln) > 230:
sys.stderr.write("File \"%s\" contains a line with more than 240 "
"characters. This exceeds the size of the serial buffer.\n"
% args.src)
f.close()
sys.exit(1)
# Go back to the beginning of the file after verifying it has the correct
# line length
f.seek(0)
# Open the selected serial port
s = openserial(args)
# set serial timeout
if args.verbose:
sys.stderr.write("Upload starting\r\n")
# remove existing file on device
if args.verbose:
sys.stderr.write("Stage 1. Deleting old file from flash memory")
writeln("file.open(\"" + args.dest + "\", \"w\")\r")
writeln("file.close()\r")
writeln("file.remove(\"" + args.dest + "\")\r")
# read source file line by line and write to device
if args.verbose:
sys.stderr.write("\r\nStage 2. Creating file in flash memory and write first line")
writeln("file.open(\"" + args.dest + "\", \"w+\")\r")
line = f.readline()
if args.verbose:
sys.stderr.write("\r\nStage 3. Start writing data to flash memory...")
while line != '':
writer(line.strip())
line = f.readline()
# close both files
f.close()
if args.verbose:
sys.stderr.write("\r\nStage 4. Flush data and closing file")
writeln("file.flush()\r")
writeln("file.close()\r")
# compile?
if args.compile:
if args.verbose:
sys.stderr.write("\r\nStage 5. Compiling")
writeln("node.compile(\"" + args.dest + "\")\r")
writeln("file.remove(\"" + args.dest + "\")\r")
# restart or dofile
if args.restart:
writeln("node.restart()\r")
if args.dofile: # never exec if restart=1
writeln("dofile(\"" + args.dest + "\")\r", 0)
# close serial port
s.flush()
s.close()
# flush screen
sys.stdout.flush()
sys.stderr.flush()
sys.stderr.write("\r\n--->>> All done <<<---\r\n")

View File

@@ -0,0 +1,63 @@
collectgarbage("setmemlimit", 4)
websocket.createServer(80, function (socket)
local data
node.output(function (msg)
return socket.send(msg, 1)
end, 1)
print("New websocket client connected")
function socket.onmessage(payload, opcode)
if opcode == 1 then
if payload == "ls" then
local list = file.list()
local lines = {}
for k, v in pairs(list) do
lines[#lines + 1] = k .. "\0" .. v
end
socket.send(table.concat(lines, "\0"), 2)
return
-- standard js websockets do not support ping/pong opcodes so we have to
-- fake it
elseif payload == "ping" then
socket.send("pong", 2)
return
end
local command, name = payload:match("^([a-z]+):(.*)$")
if command == "load" then
file.open(name, "r")
socket.send(file.read(), 2)
file.close()
elseif command == "save" then
file.open(name, "w")
file.write(data)
data = nil
file.close()
print("saved:"..name)
elseif command == "compile" then
node.compile(name)
print("compiled:"..name)
elseif command == "run" then
dofile(name)
elseif command == "eval" then
local fn, success, err
fn, err = loadstring(data, name)
if not fn then
fn = loadstring("print(" .. data .. ")", name)
end
data = nil
if fn then
success, err = pcall(fn)
end
if not success then
print(err)
end
else
print("Invalid command: " .. command)
end
elseif opcode == 2 then
data = payload
end
collectgarbage("setmemlimit", 4)
end
end)

View File

@@ -0,0 +1,5 @@
#!/bin/sh
chmod +x luatool.py
./luatool.py --port /dev/ttyUSB0 -f websocket.lua -c
./luatool.py --port /dev/ttyUSB0 -f main.lua -c
./luatool.py --port /dev/ttyUSB0 -f init.lua

View File

@@ -0,0 +1,141 @@
do
local websocket = {}
_G.websocket = websocket
local band = bit.band
local bor = bit.bor
local rshift = bit.rshift
local lshift = bit.lshift
local char = string.char
local byte = string.byte
local sub = string.sub
local applyMask = crypto.mask
local toBase64 = crypto.toBase64
local hash = crypto.hash
local function decode(chunk)
if #chunk < 2 then return end
local second = byte(chunk, 2)
local len = band(second, 0x7f)
local offset
if len == 126 then
if #chunk < 4 then return end
len = bor(
lshift(byte(chunk, 3), 8),
byte(chunk, 4))
offset = 4
elseif len == 127 then
if #chunk < 10 then return end
len = bor(
-- Ignore lengths longer than 32bit
lshift(byte(chunk, 7), 24),
lshift(byte(chunk, 8), 16),
lshift(byte(chunk, 9), 8),
byte(chunk, 10))
offset = 10
else
offset = 2
end
local mask = band(second, 0x80) > 0
if mask then
offset = offset + 4
end
if #chunk < offset + len then return end
local first = byte(chunk, 1)
local payload = sub(chunk, offset + 1, offset + len)
assert(#payload == len, "Length mismatch")
if mask then
payload = applyMask(payload, sub(chunk, offset - 3, offset))
end
local extra = sub(chunk, offset + len + 1)
local opcode = band(first, 0xf)
return extra, payload, opcode
end
local function encode(payload, opcode)
opcode = opcode or 2
assert(type(opcode) == "number", "opcode must be number")
assert(type(payload) == "string", "payload must be string")
local len = #payload
local head = char(
bor(0x80, opcode),
bor(len < 126 and len or len < 0x10000 and 126 or 127)
)
if len >= 0x10000 then
head = head .. char(
0,0,0,0, -- 32 bit length is plenty, assume zero for rest
band(rshift(len, 24), 0xff),
band(rshift(len, 16), 0xff),
band(rshift(len, 8), 0xff),
band(len, 0xff)
)
elseif len >= 126 then
head = head .. char(band(rshift(len, 8), 0xff), band(len, 0xff))
end
return head .. payload
end
local guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
local function acceptKey(key)
return toBase64(hash("sha1", key .. guid))
end
function websocket.createServer(port, callback)
net.createServer(net.TCP):listen(port, function(conn)
local buffer = false
local socket = {}
local queue = {}
local waiting = false
local function onSend()
if queue[1] then
local data = table.remove(queue, 1)
return conn:send(data, onSend)
end
waiting = false
end
function socket.send(...)
local data = encode(...)
if not waiting then
waiting = true
conn:send(data, onSend)
else
queue[#queue + 1] = data
end
end
conn:on("receive", function(_, chunk)
if buffer then
buffer = buffer .. chunk
while true do
local extra, payload, opcode = decode(buffer)
if not extra then return end
buffer = extra
socket.onmessage(payload, opcode)
end
end
local _, e, method = string.find(chunk, "([A-Z]+) /[^\r]* HTTP/%d%.%d\r\n")
local key, name, value
while true do
_, e, name, value = string.find(chunk, "([^ ]+): *([^\r]+)\r\n", e + 1)
if not e then break end
if string.lower(name) == "sec-websocket-key" then
key = value
end
end
if method == "GET" and key then
conn:send(
"HTTP/1.1 101 Switching Protocols\r\n" ..
"Upgrade: websocket\r\nConnection: Upgrade\r\n" ..
"Sec-WebSocket-Accept: " .. acceptKey(key) .. "\r\n\r\n",
function () callback(socket) end)
buffer = ""
else
conn:send(
"HTTP/1.1 404 Not Found\r\nConnection: Close\r\n\r\n",
conn.close)
end
end)
end)
end
end

View File

@@ -0,0 +1,44 @@
{
"context-menu": {
"atom-text-editor": [
{
"label": "Upload to NodeMCU",
"command": "nodemcu-thingy:upload"
}
],
".mcu-download": [
{
"label": "Download to Disk",
"command": "nodemcu-thingy:download"
},
{
"label": "Delete on NodeMCU",
"command": "nodemcu-thingy:delete"
},
{
"label": "Run on NodeMCU",
"command": "nodemcu-thingy:dofile"
}
]
},
"menu": [
{
"label": "Packages",
"submenu": [
{
"label": "nodemcu-thingy",
"submenu": [
{
"label": "Toggle",
"command": "nodemcu-thingy:toggle"
},
{
"label": "List MCU files",
"command": "nodemcu-thingy:list"
}
]
}
]
}
]
}

View File

@@ -0,0 +1,16 @@
{
"name": "nodemcu-thingy",
"main": "./lib/nodemcu-thingy",
"version": "0.0.3",
"description": "Provides \"over the air\" access to your NodeMCU device (work in progress)",
"keywords": [],
"activationCommands": {
"atom-workspace": "nodemcu-thingy:toggle"
},
"repository": "https://github.com/holtermp/nodemcu-thingy",
"license": "MIT",
"engines": {
"atom": ">=1.0.0 <2.0.0"
},
"dependencies": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

@@ -0,0 +1,73 @@
'use babel';
import NodemcuThingy from '../lib/nodemcu-thingy';
// Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs.
//
// To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit`
// or `fdescribe`). Remove the `f` to unfocus the block.
describe('NodemcuThingy', () => {
let workspaceElement, activationPromise;
beforeEach(() => {
workspaceElement = atom.views.getView(atom.workspace);
activationPromise = atom.packages.activatePackage('nodemcu-thingy');
});
describe('when the nodemcu-thingy:toggle event is triggered', () => {
it('hides and shows the modal panel', () => {
// Before the activation event the view is not on the DOM, and no panel
// has been created
expect(workspaceElement.querySelector('.nodemcu-thingy')).not.toExist();
// This is an activation event, triggering it will cause the package to be
// activated.
atom.commands.dispatch(workspaceElement, 'nodemcu-thingy:toggle');
waitsForPromise(() => {
return activationPromise;
});
runs(() => {
expect(workspaceElement.querySelector('.nodemcu-thingy')).toExist();
let nodemcuThingyElement = workspaceElement.querySelector('.nodemcu-thingy');
expect(nodemcuThingyElement).toExist();
let nodemcuThingyPanel = atom.workspace.panelForItem(nodemcuThingyElement);
expect(nodemcuThingyPanel.isVisible()).toBe(true);
atom.commands.dispatch(workspaceElement, 'nodemcu-thingy:toggle');
expect(nodemcuThingyPanel.isVisible()).toBe(false);
});
});
it('hides and shows the view', () => {
// This test shows you an integration test testing at the view level.
// Attaching the workspaceElement to the DOM is required to allow the
// `toBeVisible()` matchers to work. Anything testing visibility or focus
// requires that the workspaceElement is on the DOM. Tests that attach the
// workspaceElement to the DOM are generally slower than those off DOM.
jasmine.attachToDOM(workspaceElement);
expect(workspaceElement.querySelector('.nodemcu-thingy')).not.toExist();
// This is an activation event, triggering it causes the package to be
// activated.
atom.commands.dispatch(workspaceElement, 'nodemcu-thingy:toggle');
waitsForPromise(() => {
return activationPromise;
});
runs(() => {
// Now we can test for view visibility
let nodemcuThingyElement = workspaceElement.querySelector('.nodemcu-thingy');
expect(nodemcuThingyElement).toBeVisible();
atom.commands.dispatch(workspaceElement, 'nodemcu-thingy:toggle');
expect(nodemcuThingyElement).not.toBeVisible();
});
});
});
});

View File

@@ -0,0 +1,9 @@
'use babel';
import NodemcuThingyView from '../lib/nodemcu-thingy-view';
describe('NodemcuThingyView', () => {
it('has one valid test', () => {
expect('life').toBe('easy');
});
});

View File

@@ -0,0 +1,8 @@
// The ui-variables file is provided by base themes provided by Atom.
//
// See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less
// for a full listing of what's available.
@import "ui-variables";
.nodemcu-thingy {
}

View File

@@ -0,0 +1,13 @@
# Le WEB IDE de Matthieu Borgognon
## Sources
Il a modifié le WEB_IDE de https://github.com/joysfera/nodemcu-web-ide afin de lui ajouter la commande 'rename'
Son fichier tenu à jour se trouve dans son dépôt ici:
https://github.com/matbgn/NodeMCU/tree/master/lib/web-ide
Mais, j'en ai fait une petite copie locale ;-)
zf191020.1128

View File

@@ -0,0 +1,230 @@
local mPort = 88
local function editor(aceEnabled) -- feel free to disable the shiny Ajax.org Cloud Editor
local AceEnabled = aceEnabled == nil and true or aceEnabled
srv = net.createServer(net.TCP)
srv:listen(mPort, function(conn)
local rnrn = 0
local Status = 0
local DataToGet = 0
local method = ""
local url = ""
local vars = ""
conn:on("receive", function(sck, payload)
if Status == 0 then
_, _, method, url, vars = string.find(payload, "([A-Z]+) /([^?]*)%??(.*) HTTP")
end
if method == "POST" then
if Status == 0 then
_, _, DataToGet, payload = string.find(payload, "Content%-Length: (%d+)(.+)")
if DataToGet then
DataToGet = tonumber(DataToGet)
rnrn = 1
Status = 1
else
print("bad length")
end
end
if Status == 1 then
local payloadlen = string.len(payload)
local mark = "\r\n\r\n"
local i
for i=1, payloadlen do
if string.byte(mark, rnrn) == string.byte(payload, i) then
rnrn = rnrn + 1
if rnrn == 5 then
payload = string.sub(payload, i+1, payloadlen)
file.open(url, "w")
file.close()
Status = 2
break
end
else
rnrn = 1
end
end
if Status == 1 then
return
end
end
if Status == 2 then
if payload then
DataToGet = DataToGet - string.len(payload)
file.open(url, "a+")
file.write(payload)
file.close()
else
sck:send("HTTP/1.1 200 OK\r\n\r\nERROR")
Status = 0
end
if DataToGet == 0 then
sck:send("HTTP/1.1 200 OK\r\n\r\nOK")
Status = 0
end
end
return
end
-- end of POST method handling
DataToGet = -1
if url == "favicon.ico" then
sck:send("HTTP/1.1 404 file not found\r\nServer: NodeMCU IDE\r\nContent-Type: text/html\r\n\r\n<html><head><title>404 - File Not Found</title></head><body>Ya done goofed.</body></html>")
return
end
local sen = "HTTP/1.1 200 OK\r\nServer: NodeMCU IDE\r\nContent-Type: text/html\r\nPragma: no-cache\r\nCache-Control: no-cache\r\n\r\n"
-- it wants a file in particular
if url ~= "" and vars == "" then
DataToGet = 0
sck:send(sen)
return
end
sen = sen .. "<html><head><title>NodeMCU IDE</title><meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"><meta http-equiv=\"Expires\" content=\"-1\" />"
sen = sen .. "<style>a:link{color:white;} a:visited{color:white;} a:hover{color:yellow;} a:active{color:green;}</style></head>"
sen = sen .. "<body style=\"background-color:#333333;color:#dddddd\"><h1><a href='/'>NodeMCU IDE</a></h1>"
if vars == "edit" then
if AceEnabled then
local mode = 'ace/mode/'
if url:match(".css") then mode = mode .. 'css'
elseif url:match(".html") then mode = mode .. 'html'
elseif url:match(".json") then mode = mode .. 'json'
elseif url:match(".js") then mode = mode .. 'javascript'
else mode = mode .. 'lua'
end
sen = sen .. "<style type='text/css'>#editor{width: 100%; height: 80%}</style><div id='editor'></div><script src='//rawgit.com/ajaxorg/ace-builds/master/src-min-noconflict/ace.js'></script>"
.. "<script>var e=ace.edit('editor');e.setTheme('ace/theme/monokai');e.getSession().setMode('"..mode.."');function getSource(){return e.getValue();};function setSource(s){e.setValue(s);}</script>"
else
sen = sen .. "<textarea name=t cols=79 rows=17></textarea></br>"
.. "<script>function getSource() {return document.getElementsByName('t')[0].value;};function setSource(s) {document.getElementsByName('t')[0].value = s;};</script>"
end
sen = sen .. "<script>function tag(c){document.getElementsByTagName('w')[0].innerHTML=c};var x=new XMLHttpRequest();x.onreadystatechange=function(){if(x.readyState==4) setSource(x.responseText);};"
.. "x.open('GET',location.pathname);x.send()</script><button onclick=\"tag('Saving, wait!');x.open('POST',location.pathname);x.onreadystatechange=function(){console.log(x.readyState);"
.. "if(x.readyState==4) tag(x.responseText);};x.send(new Blob([getSource()],{type:'text/plain'}));\">Save</button> <a href='?run'>[Run File]</a> <a href=\"/\">[Main Page]</a> <w></w>"
elseif vars == "run" then
sen = sen .. "Output of the run:<hr><pre>"
function s_output(str) sen = sen .. str end
node.output(s_output, 0) -- re-direct output to function s_output.
local st, result = pcall(dofile, url)
-- delay the output capture by 1000 milliseconds to give some time to the user routine in pcall()
tmr.alarm(0, 1000, tmr.ALARM_SINGLE, function()
node.output(nil)
if result then
local outp = tostring(result):sub(1,1300) -- to fit in one send() packet
result = nil
sen = sen .. outp
end
sen = sen .. "</pre><hr><a href=\"?edit\">[Edit File]</a> <a href=\"?run\">[Run Again]</a> <a href=\"/\">[Main Page]</a></body></html>"
sck:send(sen)
end)
return
elseif vars == "rename" then
file.rename(url:match("(.+)\/"), url:match("\/(.+)"))
url = ""
elseif vars == "compile" then
collectgarbage()
node.compile(url)
url = ""
elseif vars == "delete" then
file.remove(url)
url = ""
elseif vars == "restart" then
node.restart()
return
end
local message = {}
message[#message + 1] = sen
sen = nil
if url == "" then
local l = file.list();
message[#message + 1] = "<table border=1 cellpadding=3><tr><th>Name</th><th>Size</th><th>Edit</th><th>Rename</th><th>Compile</th><th>Delete</th><th>Run</th></tr>\n"
for k,v in pairs(l) do
local line = "<tr><td><a href='" ..k.. "'>" ..k.. "</a></td><td>" ..v.. "</td><td>"
local editable = k:sub(-4, -1) == ".lua" or k:sub(-4, -1) == ".css" or k:sub(-5, -1) == ".html" or k:sub(-5, -1) == ".json" or k:sub(-4, -1) == ".txt" or k:sub(-4, -1) == ".csv"
if editable then
line = line .. "<a href='" ..k.. "?edit'>edit</a>"
end
line = line .. "</td><td><a href='#' onclick='v=prompt(\"Type the new filename\");if (v!=null) { this.href=\"/"..k.."/\"+v+\"?rename\"; return true;} else return false;'>rename</a></td><td>"
if k:sub(-4, -1) == ".lua" then
line = line .. "<a href='" ..k.. "?compile'>compile</a>"
end
line = line .. "</td><td><a href='#' onclick='v=prompt(\"Type YES to confirm file deletion!\");if (v==\"YES\") { this.href=\"/"..k.."?delete\"; return true;} else return false;'>delete</a></td><td>"
if ((k:sub(-4, -1) == ".lua") or (k:sub(-3, -1) == ".lc")) then
line = line .. "<a href='" ..k.. "?run'>run</a></td></tr>\n"
end
message[#message + 1] = line
end
remaining, used, total=file.fsinfo()
message[#message + 1] = "</table><br>Total: "..total.." Bytes <br> Used: "..used.." Bytes <br> Remaining: "..remaining.." Bytes <br><br><a href='#' onclick='v=prompt(\"Filename\");if (v!=null) { this.href=\"/\"+v+\"?edit\"; return true;} else return false;'>[New File]</a>&nbsp;"
remaining, used, total=nil
message[#message + 1] = "<a href='#' onclick='var x=new XMLHttpRequest();x.open(\"GET\",\"/?restart\");x.send();setTimeout(function(){location.href=\"/\"},5000);this.innerText=\"[Please wait...]\";return false'>[Restart]</a>"
end
message[#message + 1] = "</body></html>"
local function send_table(sk)
if #message > 0 then
sk:send(table.remove(message, 1))
else
sk:close()
message = nil
end
end
sck:on("sent", send_table)
send_table(sck)
end)
conn:on("sent", function(sck)
if DataToGet >= 0 and method == "GET" then
if file.open(url, "r") then
file.seek("set", DataToGet)
local chunkSize = 512
local line = file.read(chunkSize)
file.close()
if line then
sck:send(line)
DataToGet = DataToGet + chunkSize
if string.len(line) == chunkSize then return end
end
end
end
sck:close()
sck = nil
end)
end)
end
local t = tmr.create()
t:alarm(500, tmr.ALARM_AUTO, function()
if (wifi.sta.status() == wifi.STA_GOTIP) then
t:unregister()
t=nil
print("\n--- Web server started ---")
print("NodeMCU Web IDE running at http://"..wifi.sta.getip()..":"..mPort.."/")
editor()
end
end)