├── data ├── favicon.ico ├── js │ ├── reconn-ws.js.gz │ ├── terminal.js.gz │ ├── mousewheel.js.gz │ └── jquery-1.12.3.js.gz ├── css │ └── terminal.css.gz ├── default.txt ├── startup.ini ├── rn2483-ttn-otaa.txt ├── rn2483.txt ├── index.htm └── edit.htm ├── webdev ├── favicon.ico ├── default.txt ├── startup.ini ├── rn2483-ttn-otaa.txt ├── js │ ├── mousewheel.js │ ├── reconn-ws.js │ └── terminal.js ├── rn2483.txt ├── create_spiffs.js ├── css │ └── terminal.css ├── web_server.js ├── index.htm └── edit.htm ├── .gitattributes ├── .gitignore ├── rn2483.h ├── pushbutton.h ├── pushbutton.cpp ├── rn2483.cpp ├── WebSocketToSerial.h ├── README.md └── WebSocketToSerial.ino /data/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hallard/WebSocketToSerial/HEAD/data/favicon.ico -------------------------------------------------------------------------------- /webdev/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hallard/WebSocketToSerial/HEAD/webdev/favicon.ico -------------------------------------------------------------------------------- /data/js/reconn-ws.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hallard/WebSocketToSerial/HEAD/data/js/reconn-ws.js.gz -------------------------------------------------------------------------------- /data/js/terminal.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hallard/WebSocketToSerial/HEAD/data/js/terminal.js.gz -------------------------------------------------------------------------------- /data/css/terminal.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hallard/WebSocketToSerial/HEAD/data/css/terminal.css.gz -------------------------------------------------------------------------------- /data/js/mousewheel.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hallard/WebSocketToSerial/HEAD/data/js/mousewheel.js.gz -------------------------------------------------------------------------------- /data/js/jquery-1.12.3.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hallard/WebSocketToSerial/HEAD/data/js/jquery-1.12.3.js.gz -------------------------------------------------------------------------------- /data/default.txt: -------------------------------------------------------------------------------- 1 | # Startup config file for classic 115200 serial 2 | # commands prefixed by ! or $ are executed by ESP all others passed to serial module 3 | # command starting with $ wait until device return \n 4 | # !delay or any $ are not executed when connected via browser web terminal (websocket) 5 | 6 | # Set ESP Module serial speed 7 | !baud 115200 8 | !delay 100 9 | 10 | -------------------------------------------------------------------------------- /webdev/default.txt: -------------------------------------------------------------------------------- 1 | # Startup config file for classic 115200 serial 2 | # commands prefixed by ! or $ are executed by ESP all others passed to serial module 3 | # command starting with $ wait until device return \n 4 | # !delay or any $ are not executed when connected via browser web terminal (websocket) 5 | 6 | # Set ESP Module serial speed 7 | !baud 115200 8 | !delay 100 9 | 10 | -------------------------------------------------------------------------------- /data/startup.ini: -------------------------------------------------------------------------------- 1 | # Startup config file executed once in setup() 2 | # commands prefixed by ! are executed by ESP 3 | # all others passed to serial module 4 | 5 | # Set RGB LED luminosity (0=off 100=full light) 6 | !rgb 20 7 | 8 | # Microchip Lora rn2483 configuration 9 | #!read /rn2483.txt 10 | 11 | # Join ttn in otaa mode 12 | #!read /rn2483-ttn-otaa.txt 13 | 14 | # For a classic command line 15 | !read default.txt 16 | 17 | -------------------------------------------------------------------------------- /webdev/startup.ini: -------------------------------------------------------------------------------- 1 | # Startup config file executed once in setup() 2 | # commands prefixed by ! are executed by ESP 3 | # all others passed to serial module 4 | 5 | # Set RGB LED luminosity (0=off 100=full light) 6 | !rgb 20 7 | 8 | # Microchip Lora rn2483 configuration 9 | #!read /rn2483.txt 10 | 11 | # Join ttn in otaa mode 12 | #!read /rn2483-ttn-otaa.txt 13 | 14 | # For a classic command line 15 | !read default.txt 16 | 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ######################################################## 2 | ## hallard web dev environement located on sketch folder 3 | ######################################################## 4 | node_modules/ 5 | 6 | 7 | ############# 8 | ## Windows detritus 9 | ############# 10 | 11 | # Windows image file caches 12 | Thumbs.db 13 | ehthumbs.db 14 | 15 | # Folder config file 16 | Desktop.ini 17 | 18 | # Recycle Bin used on file shares 19 | $RECYCLE.BIN/ 20 | 21 | # Mac crap 22 | .DS_Store 23 | 24 | 25 | -------------------------------------------------------------------------------- /data/rn2483-ttn-otaa.txt: -------------------------------------------------------------------------------- 1 | # Startup config file for Microchip RN2483 join TTN in otaa 2 | # commands prefixed by ! or $ are executed by ESP all others passed to serial module 3 | # command starting with $ wait until device return \n 4 | # RN2483 always return string followed by "\r\n" on each command (ex "ok\r\n") 5 | # so $ wait a response (good or not) before sending next command 6 | # !delay or any $ are not executed when connected via browser web terminal (websocket) 7 | # See schematics here https://github.com/hallard/WeMos-RN2483 8 | 9 | # Start LoraWan 10 | $mac resume 11 | 12 | # Of course you should have setup your key at 1st setup 13 | # and saved with "mac save", if not put them there 14 | # see help https://github.com/lukastheiler/ttn_moteino/blob/master/Readme.md 15 | #$mac set appeui YOUR-APP-EUI 16 | #$mac set appkey YOUR-APP_KEY 17 | 18 | # Join Network 19 | $mac join otaa 20 | 21 | -------------------------------------------------------------------------------- /webdev/rn2483-ttn-otaa.txt: -------------------------------------------------------------------------------- 1 | # Startup config file for Microchip RN2483 join TTN in otaa 2 | # commands prefixed by ! or $ are executed by ESP all others passed to serial module 3 | # command starting with $ wait until device return \n 4 | # RN2483 always return string followed by "\r\n" on each command (ex "ok\r\n") 5 | # so $ wait a response (good or not) before sending next command 6 | # !delay or any $ are not executed when connected via browser web terminal (websocket) 7 | # See schematics here https://github.com/hallard/WeMos-RN2483 8 | 9 | # Start LoraWan 10 | $mac resume 11 | 12 | # Of course you should have setup your key at 1st setup 13 | # and saved with "mac save", if not put them there 14 | # see help https://github.com/lukastheiler/ttn_moteino/blob/master/Readme.md 15 | #$mac set appeui YOUR-APP-EUI 16 | #$mac set appkey YOUR-APP_KEY 17 | 18 | # Join Network 19 | $mac join otaa 20 | 21 | -------------------------------------------------------------------------------- /webdev/js/mousewheel.js: -------------------------------------------------------------------------------- 1 | (function(c){function g(a){var b=a||window.event,i=[].slice.call(arguments,1),e=0,h=0,f=0;a=c.event.fix(b);a.type="mousewheel";if(b.wheelDelta)e=b.wheelDelta/120;if(b.detail)e=-b.detail/3;f=e;if(b.axis!==undefined&&b.axis===b.HORIZONTAL_AXIS){f=0;h=-1*e}if(b.wheelDeltaY!==undefined)f=b.wheelDeltaY/120;if(b.wheelDeltaX!==undefined)h=-1*b.wheelDeltaX/120;i.unshift(a,e,h,f);return(c.event.dispatch||c.event.handle).apply(this,i)}var d=["DOMMouseScroll","mousewheel"];if(c.event.fixHooks)for(var j=d.length;j;)c.event.fixHooks[d[--j]]= 2 | c.event.mouseHooks;c.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var a=d.length;a;)this.addEventListener(d[--a],g,false);else this.onmousewheel=g},teardown:function(){if(this.removeEventListener)for(var a=d.length;a;)this.removeEventListener(d[--a],g,false);else this.onmousewheel=null}};c.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})})(jQuery); 3 | -------------------------------------------------------------------------------- /rn2483.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ifndef _RN2483_H 4 | #define _RN2483_H 5 | 6 | #define RN2483_RESET_PIN 15 7 | 8 | // The radio state machine when talking with RN2483 9 | typedef enum { 10 | RADIO_IDLE, 11 | RADIO_SENDING, 12 | RADIO_LISTENING, 13 | RADIO_RECEIVED_DATA, 14 | RADIO_WAIT_OK, /* just Wait Ok */ 15 | RADIO_WAIT_OK_SEND, /* Wait Ok and after we sent a packet */ 16 | RADIO_WAIT_OK_LISTENING, 17 | RADIO_ERROR 18 | } 19 | radio_state_e; 20 | 21 | // The application state machine 22 | typedef enum { 23 | APP_IDLE, 24 | APP_CONTINUOUS_LISTEN, 25 | APP_CONTINUOUS_SEND, 26 | } 27 | app_state_e; 28 | 29 | 30 | // exported vars 31 | extern app_state_e app_state ; 32 | 33 | // exported functions 34 | void radioExec( char * cmd); 35 | void radioExec( PGM_P cmd); 36 | void radioInit(uint32_t baudrate); 37 | void radioIdle(void); 38 | bool radioSend(uint32_t value); 39 | void radioListen(void) ; 40 | bool radioResponse(String inputString) ; 41 | void radioManageState(btn_action_e btn) ; 42 | radio_state_e radioState(void); 43 | 44 | #endif -------------------------------------------------------------------------------- /data/rn2483.txt: -------------------------------------------------------------------------------- 1 | # Startup config file for Microchip RN2483 2 | # commands prefixed by ! or $ are executed by ESP all others passed to serial module 3 | # command starting with $ wait until device return \n 4 | # RN2483 always return string followed by "\r\n" on each command (ex "ok\r\n") 5 | # so $ wait a response (good or not) before sending next command 6 | # !delay or any $ are not executed when connected via browser web terminal (websocket) 7 | # See schematics here https://github.com/hallard/WeMos-RN2483 8 | 9 | # Set ESP Module serial speed (RN2483 is 57600) 10 | # as reminder, it's now done in sketch 11 | #!baud 57600 12 | #!delay 50 13 | 14 | # reset RN2483 module (reset pin connected to ESP GPIO15) 15 | $reset 15 16 | 17 | # Wired GPIO to output 18 | $sys set pinmode GPIO0 digout 19 | $sys set pinmode GPIO1 digout 20 | $sys set pinmode GPIO11 digout 21 | $sys set pinmode GPIO10 digout 22 | 23 | # Light on the LED on all GPIO 24 | $sys set pindig GPIO0 1 25 | $sys set pindig GPIO1 1 26 | $sys set pindig GPIO10 1 27 | $sys set pindig GPIO11 1 28 | 29 | # Stop LoraWan 30 | $mac pause 31 | 32 | # Set Power Max 33 | $radio set pwr 15 34 | 35 | # Set watchdog to 5 min 36 | $radio set wdt 300000 37 | -------------------------------------------------------------------------------- /webdev/rn2483.txt: -------------------------------------------------------------------------------- 1 | # Startup config file for Microchip RN2483 2 | # commands prefixed by ! or $ are executed by ESP all others passed to serial module 3 | # command starting with $ wait until device return \n 4 | # RN2483 always return string followed by "\r\n" on each command (ex "ok\r\n") 5 | # so $ wait a response (good or not) before sending next command 6 | # !delay or any $ are not executed when connected via browser web terminal (websocket) 7 | # See schematics here https://github.com/hallard/WeMos-RN2483 8 | 9 | # Set ESP Module serial speed (RN2483 is 57600) 10 | # as reminder, it's now done in sketch 11 | #!baud 57600 12 | #!delay 50 13 | 14 | # reset RN2483 module (reset pin connected to ESP GPIO15) 15 | $reset 15 16 | 17 | # Wired GPIO to output 18 | $sys set pinmode GPIO0 digout 19 | $sys set pinmode GPIO1 digout 20 | $sys set pinmode GPIO11 digout 21 | $sys set pinmode GPIO10 digout 22 | 23 | # Light on the LED on all GPIO 24 | $sys set pindig GPIO0 1 25 | $sys set pindig GPIO1 1 26 | $sys set pindig GPIO10 1 27 | $sys set pindig GPIO11 1 28 | 29 | # Stop LoraWan 30 | $mac pause 31 | 32 | # Set Power Max 33 | $radio set pwr 15 34 | 35 | # Set watchdog to 5 min 36 | $radio set wdt 300000 37 | -------------------------------------------------------------------------------- /pushbutton.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ifndef _PUSHBUTTON_H 4 | #define _PUSHBUTTON_H 5 | 6 | // Switch button 7 | #define BTN_GPIO 14 8 | #define BTN_DEBOUNCE_DELAY 100 9 | #define BTN_LONG_PUSH_DELAY 1000 10 | 11 | // Button pressed set pin port to LOW 12 | #define BTN_PRESSED LOW 13 | #define BTN_RELEASED HIGH 14 | 15 | // The button state machine when pressed 16 | typedef enum { 17 | BTN_WAIT_PUSH, 18 | BTN_WAIT_RELEASE, 19 | BTN_WAIT_DEBOUNCE, 20 | BTN_WAIT_LONG_RELEASE 21 | } 22 | btn_state_e; 23 | 24 | // The actions possibe with different button press 25 | typedef enum { 26 | BTN_NONE, // do nothing. 27 | BTN_BAD_PRESS, // button pressed lower than debounce time 28 | BTN_QUICK_PRESS, // button pressed with debounce OK 29 | BTN_PRESSED_12, // pressed between 1 and 2 seconds 30 | BTN_PRESSED_23, // pressed between 2 and 3 seconds 31 | BTN_PRESSED_34, // pressed between 3 and 4 seconds 32 | BTN_PRESSED_45, // pressed between 4 and 5 seconds 33 | BTN_PRESSED_56, // pressed between 5 and 6 seconds 34 | BTN_PRESSED_67, // pressed between 6 and 7 seconds 35 | BTN_TIMEOUT // Long press timeout 36 | } 37 | btn_action_e; 38 | 39 | 40 | // These variables manages button feature 41 | extern btn_state_e _btn_State; // Button management state machine 42 | extern btn_action_e _btn_Action; // button action after press 43 | extern bool _btn_LongPress;// indicate a long press on button 44 | extern unsigned long _btn_StartTime;// when push started 45 | 46 | // Exported functions 47 | btn_state_e buttonManageState(uint8_t buttonLevel); 48 | 49 | #endif -------------------------------------------------------------------------------- /webdev/create_spiffs.js: -------------------------------------------------------------------------------- 1 | // ====================================================================== 2 | // ESP8266 create SPIFFS WEB server files script 3 | // ====================================================================== 4 | // This file is not part of web server, it's just used as ESP8266 SPIFFS 5 | // WEB server files preparation tool 6 | // Please install dependencies with 7 | // npm install zlib 8 | // after all is installed just start by typing on command line 9 | // node create_spiffs.js 10 | // once all is fine, you can upload data tiles with Arduino IDE 11 | // ====================================================================== 12 | 13 | var zlib = require('zlib'); 14 | var fs = require('fs'); 15 | var exec = require('child_process').exec, child; 16 | 17 | function copyCreate(file, compress) { 18 | var dest = "../data/"+file 19 | if (compress) dest += '.gz' 20 | var inp = fs.createReadStream(file); 21 | var out = fs.createWriteStream(dest); 22 | 23 | if (compress) { 24 | var gzip = zlib.createGzip(); 25 | console.log('Compressing '+file+' to '+dest); 26 | inp.pipe(gzip).pipe(out); 27 | } else { 28 | console.log('Copying '+file+' to '+dest); 29 | inp.pipe(out); 30 | } 31 | } 32 | 33 | copyCreate('js/mousewheel.js',true); 34 | copyCreate('js/terminal.js',true); 35 | copyCreate('js/jquery-1.12.3.js',true); 36 | copyCreate('js/reconn-ws.js',true); 37 | copyCreate('css/terminal.css',true); 38 | copyCreate('index.htm'); 39 | copyCreate('edit.htm'); 40 | copyCreate('favicon.ico'); 41 | copyCreate('startup.ini'); 42 | copyCreate('rn2483.txt'); 43 | console.log('finished!, upload to target from Arduino IDE'); 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /webdev/js/reconn-ws.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a}); 2 | -------------------------------------------------------------------------------- /webdev/css/terminal.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * __ _____ ________ __ 3 | * / // _ /__ __ _____ ___ __ _/__ ___/__ ___ ______ __ __ __ ___ / / 4 | * __ / // // // // // _ // _// // / / // _ // _// // // \/ // _ \/ / 5 | * / / // // // // // ___// / / // / / // ___// / / / / // // /\ // // / /__ 6 | * \___//____ \\___//____//_/ _\_ / /_//____//_/ /_/ /_//_//_/ /_/ \__\_\___/ 7 | * \/ /____/ version 0.10.3 8 | * http://terminal.jcubic.pl 9 | * 10 | * This file is part of jQuery Terminal. 11 | * 12 | * Copyright (c) 2011-2016 Jakub Jankiewicz 13 | * Released under the MIT license 14 | * 15 | * Date: Mon, 02 May 2016 15:26:58 +0000 16 | */.terminal .terminal-output .format,.cmd .format,.cmd .prompt,.cmd .prompt div,.terminal .terminal-output div div{display:inline-block}.terminal h1,.terminal h2,.terminal h3,.terminal h4,.terminal h5,.terminal h6,.terminal pre,.cmd{margin:0}.terminal h1,.terminal h2,.terminal h3,.terminal h4,.terminal h5,.terminal h6{line-height:1.2em}.cmd .clipboard{position:absolute;height:16px;left:-6px;width:5px;background:transparent;border:0;color:transparent;outline:0;padding:0;resize:none;z-index:0;overflow:hidden}.terminal .error{color:red}.terminal{padding:10px;position:relative;overflow:auto}.cmd{padding:0;height:1.3em;position:relative}.terminal .inverted,.cmd .inverted,.cmd .cursor.blink{background-color:#aaa;color:#000}.cmd .cursor.blink{-webkit-animation:terminal-blink 1s infinite steps(1,start);-moz-animation:terminal-blink 1s infinite steps(1,start);-ms-animation:terminal-blink 1s infinite steps(1,start);animation:terminal-blink 1s infinite steps(1,start)}@-webkit-keyframes terminal-blink{0,100%{background-color:#000;color:#aaa}50%{background-color:#bbb;color:#000}}@-ms-keyframes terminal-blink{0,100%{background-color:#000;color:#aaa}50%{background-color:#bbb;color:#000}}@-moz-keyframes terminal-blink{0,100%{background-color:#000;color:#aaa}50%{background-color:#bbb;color:#000}}@keyframes terminal-blink{0,100%{background-color:#000;color:#aaa}50%{background-color:#bbb;color:#000}}.terminal .terminal-output div div,.cmd .prompt{display:block;line-height:14px;height:auto}.cmd .prompt{float:left}.terminal,.cmd{font-family:monospace;color:#aaa;background-color:#000;font-size:12px;line-height:14px}.terminal-output>div{min-height:14px}.terminal .terminal-output div span{display:inline-block}.cmd span{float:left}.terminal-output span,.terminal-output a,.cmd div,.cmd span,.terminal td,.terminal pre,.terminal h1,.terminal h2,.terminal h3,.terminal h4,.terminal h5,.terminal h6{-webkit-touch-callout:initial;-webkit-user-select:initial;-khtml-user-select:initial;-moz-user-select:initial;-ms-user-select:initial;user-select:initial}.terminal,.terminal-output,.terminal-output div{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}@-moz-document url-prefix(){.terminal,.terminal-output,.terminal-output div{-webkit-touch-callout:initial;-webkit-user-select:initial;-khtml-user-select:initial;-moz-user-select:initial;-ms-user-select:initial;user-select:initial}}.terminal table{border-collapse:collapse}.terminal td{border:1px solid #aaa}.terminal h1::-moz-selection,.terminal h2::-moz-selection,.terminal h3::-moz-selection,.terminal h4::-moz-selection,.terminal h5::-moz-selection,.terminal h6::-moz-selection,.terminal pre::-moz-selection,.terminal td::-moz-selection,.terminal .terminal-output div div::-moz-selection,.terminal .terminal-output div span::-moz-selection,.terminal .terminal-output div div a::-moz-selection,.cmd div::-moz-selection,.cmd>span::-moz-selection,.cmd .prompt span::-moz-selection{background-color:#aaa;color:#000}.terminal h1::selection,.terminal h2::selection,.terminal h3::selection,.terminal h4::selection,.terminal h5::selection,.terminal h6::selection,.terminal pre::selection,.terminal td::selection,.terminal .terminal-output div div::selection,.terminal .terminal-output div div a::selection,.terminal .terminal-output div span::selection,.cmd div::selection,.cmd>span::selection,.cmd .prompt span::selection{background-color:#aaa;color:#000}.terminal .terminal-output div.error,.terminal .terminal-output div.error div{color:red}.tilda{position:fixed;top:0;left:0;width:100%;z-index:1100}.clear{clear:both}.terminal a{color:#0f60ff}.terminal a:hover{color:red} -------------------------------------------------------------------------------- /pushbutton.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "WebSocketToSerial.h" 3 | 4 | // These variables manages button feature 5 | btn_state_e _btn_State; // Button management state machine 6 | btn_action_e _btn_Action; // button action after press 7 | bool _btn_LongPress;// indicate a long press on button 8 | unsigned long _btn_StartTime;// when push started 9 | 10 | 11 | /* ====================================================================== 12 | Function: buttonManageState 13 | Purpose : manage button states (longpress actions/simple press/debounce) 14 | Input : pin button state (LOW or HIGH) we read just before 15 | Output : current state machine 16 | Comments: need to be called after push until state machine = BTN_WAIT_PUSH 17 | code inspired from one button library 18 | ====================================================================== */ 19 | btn_state_e buttonManageState(uint8_t buttonLevel) 20 | { 21 | // get current time in msecs 22 | unsigned long now = millis(); 23 | 24 | // Implementation of the state machine 25 | // waiting for menu pin being pressed. 26 | if (_btn_State == BTN_WAIT_PUSH) { 27 | // Putton pushed ? 28 | if (buttonLevel == BTN_PRESSED) { 29 | // now wait for release 30 | _btn_State = BTN_WAIT_RELEASE; 31 | // Set starting time 32 | _btn_StartTime = now; 33 | } 34 | 35 | // we're waiting for button released. 36 | } else if (_btn_State == BTN_WAIT_RELEASE) { 37 | // effectivly released ? 38 | if (buttonLevel == BTN_RELEASED) { 39 | // next step is to check debounce 40 | _btn_State = BTN_WAIT_DEBOUNCE; 41 | // still pressed, is it a Long press that is now starting ? 42 | } else if ((buttonLevel == BTN_PRESSED) && (now > _btn_StartTime + BTN_LONG_PUSH_DELAY)) { 43 | // Set long press state 44 | _btn_LongPress = true; 45 | 46 | // step to waiting long press release 47 | _btn_State = BTN_WAIT_LONG_RELEASE; 48 | } 49 | 50 | // waiting for being pressed timeout. 51 | } else if (_btn_State == BTN_WAIT_DEBOUNCE) { 52 | // do we got debounce time reached when released ? 53 | if (now > _btn_StartTime + BTN_DEBOUNCE_DELAY) 54 | _btn_Action = BTN_QUICK_PRESS; 55 | else 56 | _btn_Action = BTN_BAD_PRESS; 57 | 58 | // restart state machine 59 | _btn_State = BTN_WAIT_PUSH; 60 | 61 | // waiting for menu pin being release after long press. 62 | } else if (_btn_State == BTN_WAIT_LONG_RELEASE) { 63 | // are we released the long press ? 64 | if (buttonLevel == BTN_RELEASED) { 65 | // we're not anymore in a long press 66 | _btn_LongPress = false; 67 | 68 | // be sure to light off the blinking RGB led 69 | LedRGBOFF(); 70 | 71 | // We done, restart state machine 72 | _btn_State = BTN_WAIT_PUSH; 73 | } else { 74 | uint8_t sec_press; 75 | 76 | // for how long we have been pressed (in s) 77 | sec_press = ((now - _btn_StartTime)/1000L); 78 | 79 | // we're still in a long press 80 | _btn_LongPress = true; 81 | 82 | // We pressed button more than 7 sec 83 | if (sec_press >= 7 ) { 84 | // Led will be off 85 | _btn_Action = BTN_TIMEOUT; 86 | // We pressed button between 6 and 7 sec 87 | } else if (sec_press >= 6 ) { 88 | _btn_Action = BTN_PRESSED_67; 89 | // Prepare LED color 90 | LedRGBON(COLOR_RED); 91 | // We pressed button between 5 and 6 sec 92 | } else if (sec_press >= 5 ) { 93 | _btn_Action = BTN_PRESSED_56; 94 | LedRGBON(COLOR_YELLOW); 95 | // We pressed button between 4 and 5 sec 96 | } else if (sec_press >= 4 ) { 97 | _btn_Action = BTN_PRESSED_45; 98 | LedRGBON(COLOR_GREEN); 99 | // We pressed button between 3 and 4 sec 100 | } else if (sec_press >= 3 ) { 101 | _btn_Action = BTN_PRESSED_34; 102 | LedRGBON(COLOR_BLUE); 103 | // We pressed button between 2 and 3 sec 104 | } else if (sec_press >= 2 ) { 105 | _btn_Action = BTN_PRESSED_23; 106 | LedRGBON(COLOR_MAGENTA); 107 | // We pressed button between 1 and 2 sec 108 | } else if (sec_press >= 1 ) { 109 | _btn_Action = BTN_PRESSED_12; 110 | LedRGBON(COLOR_CYAN); 111 | } 112 | 113 | // manage the fast blinking 114 | // 20ms ON / 80ms OFF 115 | if (millis() % 100 < 50 ) { 116 | // Set Color 117 | #ifdef RGB_LED_PIN 118 | rgb_led.SetPixelColor(0, rgb_led_color); 119 | rgb_led.Show(); 120 | #endif 121 | } else { 122 | LedRGBOFF(); 123 | } 124 | } 125 | } 126 | 127 | // return the state machine we're in 128 | return (_btn_State); 129 | } 130 | 131 | 132 | -------------------------------------------------------------------------------- /rn2483.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "WebSocketToSerial.h" 3 | 4 | radio_state_e _radio_state; 5 | bool radio_continuous_send = false; 6 | 7 | void radioInit(uint32_t baudrate) { 8 | /* 9 | // Close proper 10 | SERIAL_DEVICE.flush(); 11 | //SERIAL_DEVICE.end(); 12 | 13 | // enable auto baud rate (see RN2483 datasheet) 14 | SERIAL_DEVICE.begin(baudrate); 15 | SERIAL_DEVICE.write((byte) 0x00); 16 | SERIAL_DEVICE.flush(); 17 | SERIAL_DEVICE.write(0x55); 18 | SERIAL_DEVICE.flush(); 19 | SERIAL_DEVICE.write(0x0A); 20 | SERIAL_DEVICE.write(0x0D); 21 | */ 22 | _radio_state = RADIO_IDLE; 23 | } 24 | 25 | // Send a radio command 26 | void radioExec( char * cmd) { 27 | // Set to blue immediatly 28 | LedRGBON(COLOR_BLUE, true); 29 | ws.textAll(cmd); 30 | execCommand(NULL, cmd); 31 | _radio_state = RADIO_WAIT_OK; 32 | } 33 | 34 | // Send a radio command 35 | void radioExec( PGM_P cmd) { 36 | String str = cmd; 37 | radioExec( str.c_str()); 38 | } 39 | 40 | // Send data 41 | bool radioSend(uint32_t value) { 42 | 43 | if (_radio_state != RADIO_IDLE) 44 | return false; 45 | 46 | // Set to BLUE immediatly 47 | LedRGBON(COLOR_BLUE, true); 48 | char cmd[32]; 49 | sprintf_P(cmd, PSTR("radio tx %08X"), value); 50 | ws.textAll(cmd); 51 | execCommand(NULL, cmd); 52 | _radio_state = RADIO_WAIT_OK_SEND; 53 | return true; 54 | } 55 | 56 | // Put RN2483 into listening mode 57 | void radioListen(void) { 58 | 59 | // Idle or previously listening ? 60 | if ( _radio_state==RADIO_IDLE || _radio_state==RADIO_LISTENING ) { 61 | char cmd[32]; 62 | 63 | // Set receive Watchdog to 1H 64 | //sprintf_P( cmd, PSTR("radio set wdt 3600000")); 65 | //ws.textAll(cmd); 66 | //execCommand(NULL, cmd); 67 | //delay(250); 68 | 69 | // Enable Receive mode 70 | sprintf_P( cmd, PSTR("radio rx 0")); 71 | ws.textAll(cmd); 72 | execCommand(NULL, cmd); 73 | 74 | // Wait ok listenning 75 | _radio_state = RADIO_WAIT_OK_LISTENING; 76 | } 77 | } 78 | 79 | // we received a RN2483 serial response 80 | bool radioResponse(String inputString) { 81 | // got OK 82 | if (inputString == "ok") { 83 | if (_radio_state == RADIO_WAIT_OK_LISTENING) { 84 | _radio_state = RADIO_LISTENING; 85 | } else if (_radio_state == RADIO_WAIT_OK_SEND) { 86 | _radio_state = RADIO_SENDING; 87 | } else { 88 | _radio_state = RADIO_IDLE; 89 | } 90 | 91 | // "radio_tx_ok" 92 | } else if (inputString == "radio_tx_ok") { 93 | _radio_state = RADIO_IDLE; 94 | 95 | // Set to GREEN immediatly 96 | LedRGBON(COLOR_GREEN, true); 97 | 98 | // "radio_rx data" 99 | } else if (inputString.startsWith("radio_rx ")) { 100 | // Stop Animation for being sure to start a full one 101 | LedRGBOFF(); 102 | // Set to GREEN immediatly 103 | LedRGBON(COLOR_GREEN, true); 104 | 105 | _radio_state = RADIO_RECEIVED_DATA; 106 | 107 | // got something to do 108 | return (true); 109 | 110 | // radio_err 111 | } else if (inputString == "radio_err") { 112 | // We were listening, restart listen (timed out) 113 | // not really an error 114 | if ( _radio_state == RADIO_LISTENING ) { 115 | radioListen(); 116 | } else { 117 | _radio_state = RADIO_ERROR; 118 | 119 | // Set to RED immediatly 120 | LedRGBON(COLOR_RED, true); 121 | } 122 | } 123 | return false; 124 | } 125 | 126 | // Manage radio state machine 127 | void radioManageState(btn_action_e btn) { 128 | static radio_state_e old_radio_state = _radio_state; 129 | 130 | // Action due to push button? 131 | if (btn != BTN_NONE) { 132 | if ( btn == BTN_QUICK_PRESS) { 133 | radioSend(millis()/1000); 134 | } else if ( btn == BTN_PRESSED_12) { 135 | LedRGBON(COLOR_CYAN, true); 136 | radioListen(); 137 | } else if ( btn == BTN_PRESSED_23) { 138 | if (!radio_continuous_send) { 139 | LedRGBON(COLOR_MAGENTA, true); 140 | radio_continuous_send = true; 141 | } else { 142 | LedRGBOFF(); 143 | radio_continuous_send = false; 144 | } 145 | } 146 | } 147 | 148 | // Check radio state changed ? 149 | if (_radio_state != old_radio_state) { 150 | old_radio_state = _radio_state; 151 | 152 | // Set Breathing to cyan when listening 153 | if (_radio_state == RADIO_LISTENING ) { 154 | LedRGBON(COLOR_CYAN); 155 | 156 | } else if (_radio_state == RADIO_RECEIVED_DATA) { 157 | _radio_state=RADIO_IDLE; 158 | // Listen back 159 | radioListen(); 160 | } else if (_radio_state == RADIO_SENDING) { 161 | LedRGBON(COLOR_RED, true); 162 | 163 | } else if (_radio_state == RADIO_IDLE) { 164 | //LedRGBOFF(); 165 | } 166 | } // Radio state changed 167 | } 168 | 169 | // get radio state machine 170 | radio_state_e radioState(void) { 171 | return _radio_state; 172 | } -------------------------------------------------------------------------------- /WebSocketToSerial.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef _WEBSOCKETTOSERIAL_H 3 | #define _WEBSOCKETTOSERIAL_H 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "pushbutton.h" 10 | #include "rn2483.h" 11 | 12 | // Define here the target serial you connected to this app 13 | // ------------------------------------------------------- 14 | // Classic terminal connected to RX/TX of ESP8266 15 | // using Arduino IDE or other terminal software 16 | #define MOD_TERMINAL 17 | 18 | // LoraWan Microchip RN2483, see hardware here 19 | // https://github.com/hallard/WeMos-RN2483 20 | //#define MOD_RN2483 21 | 22 | // Uncomment 3 lines below if you have an WS1812B RGB LED 23 | // like shield here https://github.com/hallard/WeMos-RN2483 24 | #ifdef MOD_RN2483 25 | #define SERIAL_DEVICE Serial 26 | #define SERIAL_DEBUG Serial1 27 | #define RGB_LED_PIN 0 28 | #define RGB_LED_COUNT 1 29 | #define RGBW_LED 30 | #endif 31 | 32 | // Uncomment 3 lines below if you have an WS1812B RGB LED 33 | // like shield here https://github.com/hallard/WeMos-RN2483 34 | #ifdef MOD_TERMINAL 35 | #define SERIAL_DEVICE Serial 36 | #define SERIAL_DEBUG Serial1 37 | #endif 38 | 39 | #ifdef RGB_LED_PIN 40 | #include 41 | #include 42 | 43 | #ifdef RGBW_LED 44 | typedef NeoPixelBus MyPixelBus; 45 | 46 | // what is stored for state is specific to the need, in this case, the colors. 47 | // basically what ever you need inside the animation update function 48 | struct MyAnimationState { 49 | RgbwColor RgbStartingColor; 50 | RgbwColor RgbEndingColor; 51 | uint8_t IndexPixel; // general purpose variable used to store pixel index 52 | }; 53 | #else 54 | typedef NeoPixelBus MyPixelBus; 55 | 56 | // what is stored for state is specific to the need, in this case, the colors. 57 | // basically what ever you need inside the animation update function 58 | struct MyAnimationState { 59 | RgbColor RgbStartingColor; 60 | RgbColor RgbEndingColor; 61 | uint8_t IndexPixel; // general purpose variable used to store pixel index 62 | }; 63 | #endif 64 | #endif 65 | 66 | // The RGB animation state machine 67 | typedef enum { 68 | RGB_ANIM_NONE, 69 | RGB_ANIM_FADE_IN, 70 | RGB_ANIM_FADE_OUT, 71 | RGB_ANIM_BLINK_ON, 72 | RGB_ANIM_BLINK_OFF, 73 | } 74 | rgb_anim_state_e; 75 | 76 | // value for HSL color 77 | // see http://www.workwithcolor.com/blue-color-hue-range-01.htm 78 | #define COLOR_RED 0 79 | #define COLOR_ORANGE 30 80 | #define COLOR_ORANGE_YELLOW 45 81 | #define COLOR_YELLOW 60 82 | #define COLOR_YELLOW_GREEN 90 83 | #define COLOR_GREEN 120 84 | #define COLOR_GREEN_CYAN 165 85 | #define COLOR_CYAN 180 86 | #define COLOR_CYAN_BLUE 210 87 | #define COLOR_BLUE 240 88 | #define COLOR_BLUE_MAGENTA 275 89 | #define COLOR_MAGENTA 300 90 | #define COLOR_PINK 350 91 | 92 | 93 | // Maximum number of simultaned clients connected (WebSocket) 94 | #define MAX_WS_CLIENT 5 95 | 96 | // Client state machine 97 | #define CLIENT_NONE 0 98 | #define CLIENT_ACTIVE 1 99 | 100 | #define HELP_TEXT "[[b;green;]WebSocket2Serial HELP]\n" \ 101 | "---------------------\n" \ 102 | "[[b;cyan;]?] or [[b;cyan;]help] show this help\n" \ 103 | "[[b;cyan;]swap] swap serial UART pin to GPIO15/GPIO13\n" \ 104 | "[[b;cyan;]ping] send ping command\n" \ 105 | "[[b;cyan;]heap] show free RAM\n" \ 106 | "[[b;cyan;]whoami] show client # we are\n" \ 107 | "[[b;cyan;]who] show all clients connected\n" \ 108 | "[[b;cyan;]fw] show firmware date/time\n" \ 109 | "[[b;cyan;]baud] show current serial baud rate\n" \ 110 | "[[b;cyan;]baud n] set serial baud rate to n\n" \ 111 | "[[b;cyan;]rgb l] set RGB Led luminosity l (0..100)\n" \ 112 | "[[b;cyan;]reset p] do a reset pulse on gpio pin number p\n" \ 113 | "[[b;cyan;]hostname] show network hostname of device\n" \ 114 | "[[b;cyan;]ls] list SPIFFS files\n" \ 115 | "[[b;cyan;]debug] show debug information\n" \ 116 | "[[b;cyan;]cat file] display content of file\n" \ 117 | "[[b;cyan;]read file] send SPIFFS file to serial (read)" 118 | 119 | // Web Socket client state 120 | typedef struct { 121 | uint32_t id; 122 | uint8_t state; 123 | } _ws_client; 124 | 125 | 126 | // Exported vars 127 | extern AsyncWebSocket ws; 128 | 129 | // RGB Led exported vars 130 | #ifdef RGB_LED_PIN 131 | #ifdef RGBW_LED 132 | extern RgbwColor rgb_led_color; // RGBW Led color 133 | #else 134 | extern RgbColor rgb_led_color; // RGB Led color 135 | #endif 136 | 137 | extern MyPixelBus rgb_led; 138 | extern uint8_t rgb_led_effect_state; 139 | extern MyAnimationState animationState[]; 140 | #endif 141 | 142 | #ifdef MOD_RN2483 143 | extern SoftwareSerial SoftSer; 144 | #endif 145 | 146 | 147 | // Exported function 148 | #ifdef RGB_LED_PIN 149 | void LedRGBFadeAnimUpdate(const AnimationParam& param); 150 | #else 151 | void LedRGBFadeAnimUpdate(void * param); 152 | #endif 153 | 154 | void LedRGBAnimate(uint16_t param); 155 | void LedRGBON (uint16_t hue, bool now=false); 156 | void LedRGBOFF(void); 157 | 158 | void execCommand(AsyncWebSocketClient * client, char * msg) ; 159 | 160 | #endif 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /webdev/web_server.js: -------------------------------------------------------------------------------- 1 | // ====================================================================== 2 | // ESP8266 WEB Server Simulator 3 | // ====================================================================== 4 | // This file is not part of web server, it's just used as ESP8266 5 | // Simulator to check HTLM / JQuery and all web stuff without needing 6 | // to flash ESP8266 target, you'll need nodejs to run it 7 | // Please install dependencies with 8 | // npm install mime httpdispatcher websocket 9 | // after all is installed just start by typing on command line 10 | // node web_server.js 11 | // once all is fine, just run the script create_spiffs.js to publish 12 | // files to data folder, then you can upload data tiles with Arduino IDE 13 | // ====================================================================== 14 | 15 | // Dependencies 16 | var http = require('http'); 17 | var fs = require('fs'); 18 | var path = require('path'); 19 | var url = require('url'); 20 | var mime = require('mime'); 21 | var util = require('util'); 22 | var os = require('os'); 23 | var Httpdispatcher = require('httpdispatcher'); 24 | var dispatcher = new Httpdispatcher(); 25 | var ws = require('websocket').server; 26 | 27 | var clientid=0; 28 | 29 | 30 | function list() { 31 | return[ {"type":"file","name":"css/terminal.css.gz"}, 32 | {"type":"file","name":"favicon.ico"}, 33 | {"type":"file","name":"js/jquery-1.12.3.js.gz"}, 34 | {"type":"file","name":"js/mousewheel.js.gz"}, 35 | {"type":"file","name":"js/terminal.js.gz"}, 36 | {"type":"file","name":"edit.htm"}, 37 | {"type":"file","name":"index.htm"}, 38 | {"type":"file","name":"rn2483.txt"}, 39 | {"type":"file","name":"startup.ini"}] 40 | } 41 | 42 | //Lets use our dispatcher 43 | function handleRequest(req, res) { 44 | try { 45 | console.log(req.url); 46 | dispatcher.dispatch(req, res); 47 | } 48 | catch(err) { 49 | console.log(err); 50 | } 51 | } 52 | 53 | // Quick format RAM size to human 54 | function humanSize(bytes) { 55 | var units = ['kB','MB','GB','TB','PB','EB','ZB','YB'] 56 | var thresh = 1024; 57 | if(Math.abs(bytes) < thresh) 58 | return bytes + ' B'; 59 | 60 | var u = -1; 61 | do { 62 | bytes /= thresh; 63 | ++u; 64 | } 65 | while(Math.abs(bytes) >= thresh && u < units.length - 1); 66 | return bytes.toFixed(1)+' '+units[u]; 67 | } 68 | 69 | // Return RAM usage 70 | dispatcher.onGet("/heap", function(req, res) { 71 | res.writeHead(200, {"Content-Type": "text/html"}); 72 | res.end(humanSize(os.freemem())); 73 | }); 74 | 75 | dispatcher.onGet("/list", function(req, res) { 76 | res.writeHead(200, {"Content-Type": "text/json"}); 77 | res.end(JSON.stringify(list())); 78 | }); 79 | 80 | 81 | // Not found, try to read file from disk and send to client 82 | dispatcher.onError(function(req, res) { 83 | var uri = url.parse(req.url).pathname; 84 | var filePath = '.' + uri; 85 | var extname = path.extname(filePath); 86 | 87 | if (filePath == './') filePath = './index.htm'; 88 | if (filePath == './edit') filePath = './edit.htm'; 89 | 90 | var contentType = mime.lookup(filePath); 91 | 92 | console.log('File='+filePath+' Type='+contentType); 93 | 94 | // Read file O 95 | fs.readFile(filePath, function(error, content) { 96 | if (error) { 97 | // File not found on local disk 98 | if(error.code == 'ENOENT') { 99 | fs.readFile('./404.html', function(error, content) { 100 | res.writeHead(200, { 'Content-Type': contentType }); 101 | res.end(content, 'utf-8'); 102 | console.log("Error ENOENT when serving file '"+filePath+ "' => "+contentType); 103 | }); 104 | } else { 105 | // WTF ? 106 | res.writeHead(500); 107 | res.end('Sorry, check with the site admin for error: '+error.code+' ..\n'); 108 | res.end(); 109 | console.log("Error "+filePath+ ' => '+contentType); 110 | } 111 | } else { 112 | // No error, serve the file 113 | res.writeHead(200, { 'Content-Type': contentType }); 114 | res.end(content, 'utf-8'); 115 | console.log("Sent "+filePath+ ' => '+contentType); 116 | } 117 | }); 118 | }); 119 | 120 | // Create HTTP server + WebSocket 121 | var server = http.createServer(handleRequest); 122 | var wsSrv = new ws({ httpServer: server }); 123 | 124 | // WebSocket Handler 125 | wsSrv.on('request', function(request) { 126 | var connection = request.accept('', request.origin); 127 | console.log("+++ Websocket client connected !"); 128 | connection.sendUTF("Hello client :)"); 129 | 130 | // We received a message 131 | connection.on('message', function(message) { 132 | //console.log(connection, 'ws message ' + util.inspect(request, false, null)); 133 | 134 | // Text message 135 | if (message.type === 'utf8') { 136 | var msg = message.utf8Data.split(':'); 137 | var value = msg[1]; 138 | msg = msg[0] 139 | console.log(' msg="'+msg+'" value="'+value+'"'); 140 | 141 | if (msg==='ping') { 142 | // Send pong 143 | connection.sendUTF('[[b;cyan;]pong]'); 144 | } else if (msg=='heap'){ 145 | var free = humanSize(os.freemem()); 146 | connection.sendUTF("Free RAM [[b;green;]"+free+"]"); 147 | } else if (msg=='whoami'){ 148 | // dummy 149 | ++clientid; 150 | connection.sendUTF("You are [[b;green;]"+clientid+"]"); 151 | } else { 152 | // Send back what we received 153 | msg = msg.replace(/\]/g, "]"); 154 | connection.sendUTF("I've received [[b;green;]"+msg+"]") 155 | } 156 | } 157 | else if (message.type === 'binary') { 158 | console.log('Received Binary Message of [[b;green;]"'+message.binaryData.length+'] bytes'); 159 | connection.sendUTF("I've received your binary data here it back"); 160 | connection.sendBytes(message.binaryData); 161 | } 162 | }); 163 | 164 | connection.on('close', function(reasonCode, description) { 165 | console.log((new Date()) +' Peer '+connection.remoteAddress+' disconnected.'); 166 | }); 167 | }); 168 | 169 | //Lets start our server 170 | server.listen(8080, function() { 171 | //Callback triggered when server is successfully listening. Hurray! 172 | console.log("Server listening on: http://localhost:%s", 8080); 173 | }); 174 | -------------------------------------------------------------------------------- /data/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ESP8266 WebSocket2Serial Proxy 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 166 | 167 | -------------------------------------------------------------------------------- /webdev/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ESP8266 WebSocket2Serial Proxy 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 166 | 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Transparent TCP Network to Serial Proxy using WebSocket for ESP8266 2 | =================================================================== 3 | 4 | This is a pure transparent bridge between Wifi and serial using any ESP8266 device. It's very useful for debugging or talking to remote serial device that have no network connection. 5 | 6 | I'm using it on WeMos target, you can find more information on WeMos on their [site][1], it's really well documented. 7 | I now use WeMos boards instead of NodeMCU's one because they're just smaller, features remains the same, but I also suspect WeMos regulator far better quality than the one used on NodeMCU that are just fake of originals AMS117 3V3. 8 | 9 | This project is mainly based on excellent @me-no-dev [ESPAsyncWebServer][4] library and great [JQuery Terminal][3] done by Jakub Jankiewicz. 10 | 11 | Documentation 12 | ============= 13 | 14 | Once uploaded SPIFFS data (web page) you can connect with a browser to http://ip_of_esp8266 and start playing with it. 15 | The main `index.htm` web page include a full javascript terminal so you can type command and receive response. 16 | 17 | The main web page can also be hosted anywhere and it's not mandatory to have it on the device (except if device and your computer have no access on Internet). I've published the fully fonctionnal WEB page from github so you can access it from [here][9] and then connect to your device on wich you flashed the firmware. 18 | 19 | Some commands will be interpreted by the target (ESP8266) and not passed to serial, so you can interact with ESP8266 doing some variable stuff. 20 | 21 | Test web page without ESP8266 22 | ----------------------------- 23 | 24 | You need to have [nodejs][7] and some dependencies installed. 25 | 26 | [webdev][10] folder is the development folder to test and validate web pages. It's used to avoid flashing the device on each modification. 27 | All source files are located in this folder the ESP8266 `data` folder (containing web pages) is filled by a nodejs script launched from [webdev][10] folder. This repo contain in [data][13] lasted files so if you don't change any file, you can upload to SPIFFS as is. 28 | 29 | To test web pages, go to a command line, go into [webdev][10] folder and issue a: 30 | `node web_server.js` 31 | then connect your browser to htpp://localhost:8080 you can them modidy and test source files such index.htm 32 | 33 | Once all is okay issue a: 34 | `node create_spiffs.js` 35 | this will gzip file and put them into [data][13] folder, after that you can upload from Arduino IDE to device SPIFFS 36 | 37 | See comments in both [create_spiffs.js][11] and [web_server.js][11] files, it's also indicated dependencies needed by nodejs. 38 | 39 | Terminal Commands: 40 | ------------------ 41 | - connect : connect do target device 42 | - help : show help 43 | 44 | Commands once connected to remote device: 45 | ----------------------------------------- 46 | - `!close` or CTRL-D : close connection 47 | - `swap` swap ESP8266 UART pin between GPIO1/GPIO3 with GPIO15/GPIO13 48 | - `ping` typing ping on terminal and ESP8266 will send back pong 49 | - `?` or `help` show help 50 | - `heap` show ESP8266 free RAM 51 | - `whoami` show WebSocket client # we are 52 | - `who` show all WebSocket clients connected 53 | - `fw` show firmware date/time 54 | - `baud n` set ESP8266 serial baud rate to n (to be compatble with device driven) 55 | - `reset p` reset gpio pin number p 56 | - `ls` list SPIFFS files 57 | - `read file` execute SPIFFS file command 58 | 59 | 60 | Every command in file `startup.ini` are executed in setup() you can chain with other files. 61 | 62 | I'm using this sketch to drive Microchip RN2483 Lora module to test LoraWan, see the [boards][8] I used. 63 | 64 | For example my `startup.ini` file contains command to read microchip RN2483 config file named `rn2483.txt` 65 | 66 | `startup.ini` 67 | ```sh 68 | # Startup config file executed once in setup() 69 | # commands prefixed by ! are executed by ESP 70 | # all others passed to serial module 71 | 72 | # Chain with Microchip Lora rn2483 configuration 73 | !read /rn2483.txt 74 | 75 | ``` 76 | 77 | rn2483 configuration file for my [WeMos shield][8] `rn2483.txt` 78 | ```shell 79 | # Startup config file for Microchip RN2483 80 | # commands prefixed by ! are executed by ESP all others passed to serial module 81 | # !delay is not executed when connected via browser web terminal (websocket) 82 | # See schematics here https://github.com/hallard/WeMos-RN2483 83 | 84 | # Set ESP Module serial speed (RN2483 is 57600) 85 | !baud 57600 86 | !delay 100 87 | 88 | # reset RN2483 module (reset pin connected to ESP GPIO15) 89 | !reset 15 90 | !delay 1000 91 | 92 | # Light on the LED on GPIO0 93 | sys set pindig GPIO0 1 94 | !delay 250 95 | 96 | # Light on the LED on GPIO10 97 | sys set pindig GPIO10 1 98 | !delay 250 99 | ``` 100 | 101 | By the way I integrated the excellent @me-no-dev SPIFFS Web editor so you can direct edit configuration files of SPIFFS going to 102 | `http://ESP_IP/edit` 103 | Your computer need to be connected to Internet (so may be your ESP8266 device) and authenticated for this feature, default login/pass are in the sketch (admin/admin) 104 | 105 | See all in action 106 | http://cdn.rawgit.com/hallard/WebSocketToSerial/master/webdev/index.htm 107 | 108 | Known Issues/Missing Features: 109 | ------------------------------ 110 | - More configuration features 111 | - Configuration file for SSID/PASSWORD and login/pass for http admin access 112 | 113 | Dependencies 114 | ------------ 115 | - Arduino [ESP8266][6] 116 | - @me-no-dev [ESPAsyncWebServer][4] library 117 | - @me-no-dev [ESPAsyncTCP][5] library 118 | - [nodejs][7] for web pages development test 119 | 120 | Misc 121 | ---- 122 | See news and other projects on my [blog][2] 123 | 124 | [1]: http://www.wemos.cc/ 125 | [2]: https://hallard.me 126 | [3]: http://terminal.jcubic.pl/ 127 | [4]: https://github.com/me-no-dev/ESPAsyncWebServer 128 | [5]: https://github.com/me-no-dev/ESPAsyncTCP 129 | [6]: https://github.com/esp8266/Arduino/blob/master/README.md 130 | [7]: https://nodejs.org/ 131 | [8]: https://github.com/hallard/WeMos-RN2483/blob/master/README.md 132 | [9]: http://cdn.rawgit.com/hallard/WebSocketToSerial/master/webdev/index.htm 133 | [10]: https://github.com/hallard/WebSocketToSerial/tree/master/webdev 134 | [11]: https://github.com/hallard/WebSocketToSerial/blob/master/webdev/create_spiffs.js 135 | [12]: https://github.com/hallard/WebSocketToSerial/blob/master/webdev/web_server.js 136 | [13]: https://github.com/hallard/WebSocketToSerial/tree/master/data 137 | -------------------------------------------------------------------------------- /data/edit.htm: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | ESP8266 SPIFFS File Editor 10 | 11 | 12 | 13 | 34 | 406 | 407 | 408 | 409 |
410 |
411 |
412 | 413 | 414 | 415 | 416 | -------------------------------------------------------------------------------- /webdev/edit.htm: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | ESP8266 SPIFFS File Editor 10 | 11 | 12 | 13 | 34 | 406 | 407 | 408 | 409 |
410 |
411 |
412 | 413 | 414 | 415 | 416 | -------------------------------------------------------------------------------- /WebSocketToSerial.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // Main project include file 14 | #include "WebSocketToSerial.h" 15 | 16 | // RGB Led 17 | #ifdef RGB_LED_PIN 18 | 19 | #ifdef RGBW_LED 20 | MyPixelBus rgb_led(RGB_LED_COUNT, RGB_LED_PIN); 21 | RgbwColor rgb_led_color; // RGBW Led color 22 | #else 23 | MyPixelBus rgb_led(RGB_LED_COUNT, RGB_LED_PIN); 24 | RgbColor rgb_led_color; // RGB Led color 25 | #endif 26 | 27 | // general purpose variable used to store effect state 28 | rgb_anim_state_e rgb_anim_state; 29 | uint8_t rgb_luminosity = 20 ; // Luminosity from 0 to 100% 30 | 31 | // one entry per pixel to match the animation timing manager 32 | NeoPixelAnimator animations(1); 33 | MyAnimationState animationState[1]; 34 | 35 | #endif 36 | 37 | #ifdef MOD_RN2483 38 | // If you want to use soft serial, not super reliable with AsyncTCP 39 | //SoftwareSerial SoftSer(12, 13, false, 128); 40 | app_state_e app_state ; 41 | #endif 42 | 43 | const char* ssid = "*******"; 44 | const char* password = "*******"; 45 | const char* http_username = "admin"; 46 | const char* http_password = "admin"; 47 | char thishost[17]; 48 | 49 | String inputString = ""; 50 | bool serialSwapped = false; 51 | 52 | // SKETCH BEGIN 53 | AsyncWebServer server(80); 54 | AsyncWebSocket ws("/ws"); 55 | 56 | // State Machine for WebSocket Client; 57 | _ws_client ws_client[MAX_WS_CLIENT]; 58 | 59 | #ifndef RGB_LED_PIN 60 | void LedRGBFadeAnimUpdate(void * param) {} 61 | void LedRGBAnimate(uint16_t param) {} 62 | void LedRGBON (uint16_t hue, bool now) {} 63 | void LedRGBOFF(void) {} 64 | #else 65 | /* ====================================================================== 66 | Function: LedRGBBlinkAnimUpdate 67 | Purpose : Blink Anim update for RGB Led 68 | Input : - 69 | Output : - 70 | Comments: grabbed from NeoPixelBus library examples 71 | ====================================================================== */ 72 | void LedRGBBlinkAnimUpdate(const AnimationParam& param) 73 | { 74 | // this gets called for each animation on every time step 75 | // progress will start at 0.0 and end at 1.0 76 | rgb_led.SetPixelColor(0, RgbColor(0)); 77 | 78 | // 25% on so 75% off 79 | if (param.progress < 0.25f) { 80 | rgb_led.SetPixelColor(0, rgb_led_color); 81 | } 82 | } 83 | 84 | /* ====================================================================== 85 | Function: LedRGBFadeAnimUpdate 86 | Purpose : Fade in and out effect for RGB Led 87 | Input : - 88 | Output : - 89 | Comments: grabbed from NeoPixelBus library examples 90 | ====================================================================== */ 91 | void LedRGBFadeAnimUpdate(const AnimationParam& param) 92 | { 93 | // this gets called for each animation on every time step 94 | // progress will start at 0.0 and end at 1.0 95 | // apply a exponential curve to both front and back 96 | float progress = NeoEase::QuadraticInOut(param.progress) ; 97 | 98 | // we use the blend function on the RgbColor to mix 99 | // color based on the progress given to us in the animation 100 | #ifdef RGBW_LED 101 | RgbwColor updatedColor = RgbwColor::LinearBlend( 102 | animationState[param.index].RgbStartingColor, 103 | animationState[param.index].RgbEndingColor, 104 | progress); 105 | rgb_led.SetPixelColor(0, updatedColor); 106 | #else 107 | RgbColor updatedColor = RgbColor::LinearBlend( 108 | animationState[param.index].RgbStartingColor, 109 | animationState[param.index].RgbEndingColor, 110 | progress); 111 | rgb_led.SetPixelColor(0, updatedColor); 112 | #endif 113 | } 114 | 115 | 116 | /* ====================================================================== 117 | Function: LedRGBAnimate 118 | Purpose : Manage RGBLed Animations 119 | Input : parameter (here animation time in ms) 120 | Output : - 121 | Comments: 122 | ====================================================================== */ 123 | void LedRGBAnimate(uint16_t param) 124 | { 125 | if ( animations.IsAnimating() ) { 126 | // the normal loop just needs these two to run the active animations 127 | animations.UpdateAnimations(); 128 | rgb_led.Show(); 129 | 130 | } else { 131 | 132 | // We start animation with current led color 133 | animationState[0].RgbStartingColor = rgb_led.GetPixelColor(0); 134 | 135 | // Node default fade in 136 | if ( rgb_anim_state==RGB_ANIM_NONE ) 137 | rgb_anim_state=RGB_ANIM_FADE_IN ; 138 | 139 | // Fade in 140 | if ( rgb_anim_state==RGB_ANIM_NONE || rgb_anim_state==RGB_ANIM_FADE_IN ) { 141 | animationState[0].RgbEndingColor = rgb_led_color; // selected color 142 | rgb_anim_state=RGB_ANIM_FADE_OUT; // Next 143 | animations.StartAnimation(0, param, LedRGBFadeAnimUpdate); 144 | 145 | // Fade out 146 | } else if ( rgb_anim_state==RGB_ANIM_FADE_OUT ) { 147 | animationState[0].RgbEndingColor = RgbColor(0); // off 148 | rgb_anim_state=RGB_ANIM_FADE_IN; // Next 149 | animations.StartAnimation(0, param, LedRGBFadeAnimUpdate); 150 | 151 | // Blink ON 152 | } else if ( rgb_anim_state==RGB_ANIM_BLINK_ON ) { 153 | rgb_anim_state=RGB_ANIM_BLINK_OFF; // Next 154 | animations.StartAnimation(0, param, LedRGBBlinkAnimUpdate); 155 | 156 | // Blink OFF 157 | } else if ( rgb_anim_state==RGB_ANIM_BLINK_OFF ) { 158 | rgb_anim_state=RGB_ANIM_BLINK_ON; // Next 159 | animations.StartAnimation(0, param, LedRGBBlinkAnimUpdate); 160 | } 161 | } 162 | } 163 | 164 | /* ====================================================================== 165 | Function: LedRGBON 166 | Purpose : Set RGB LED strip color, but does not lit it 167 | Input : Hue (0..360) 168 | if led should be lit immediatly 169 | Output : - 170 | Comments: 171 | ====================================================================== */ 172 | void LedRGBON (uint16_t hue, bool doitnow) 173 | { 174 | // Convert to neoPixel API values 175 | // H (is color from 0..360) should be between 0.0 and 1.0 176 | // S is saturation keep it to 1 177 | // L is brightness should be between 0.0 and 0.5 178 | // rgb_luminosity is between 0 and 100 (percent) 179 | RgbColor target = HslColor( hue / 360.0f, 1.0f, 0.005f * rgb_luminosity); 180 | rgb_led_color = target; 181 | 182 | // do it now ? 183 | if (doitnow) { 184 | // Stop animation 185 | animations.StopAnimation(0); 186 | animationState[0].RgbStartingColor = target; 187 | animationState[0].RgbEndingColor = RgbColor(0); 188 | 189 | // set the strip 190 | rgb_led.SetPixelColor(0, target); 191 | rgb_led.Show(); 192 | } 193 | } 194 | 195 | /* ====================================================================== 196 | Function: LedRGBOFF 197 | Purpose : light off the RGB LED strip 198 | Input : - 199 | Output : - 200 | Comments: - 201 | ====================================================================== */ 202 | void LedRGBOFF(void) 203 | { 204 | // stop animation, reset params 205 | animations.StopAnimation(0); 206 | animationState[0].RgbStartingColor = RgbColor(0); 207 | animationState[0].RgbEndingColor = RgbColor(0); 208 | rgb_led_color = RgbColor(0); 209 | rgb_anim_state=RGB_ANIM_NONE; 210 | 211 | // clear the strip 212 | rgb_led.SetPixelColor(0, RgbColor(0)); 213 | rgb_led.Show(); 214 | } 215 | #endif 216 | 217 | 218 | /* ====================================================================== 219 | Function: execCommand 220 | Purpose : translate and execute command received from serial/websocket 221 | Input : client if coming from Websocket 222 | command received 223 | Output : - 224 | Comments: - 225 | ====================================================================== */ 226 | void execCommand(AsyncWebSocketClient * client, char * msg) { 227 | uint16_t l = strlen(msg); 228 | uint8_t index=MAX_WS_CLIENT; 229 | 230 | 231 | // Search if w're known client 232 | if (client) { 233 | for (index=0; indexid() ) 236 | break; 237 | } // for all clients 238 | } 239 | 240 | //if (client) 241 | // client->printf_P(PSTR("command[%d]='%s'"), l, msg); 242 | // Display on debug 243 | SERIAL_DEBUG.printf(" -> \"%s\"\r\n", msg); 244 | 245 | // Custom command to talk to device 246 | if (!strcmp_P(msg,PSTR("ping"))) { 247 | if (client) 248 | client->printf_P(PSTR("received your [[b;cyan;]ping], here is my [[b;cyan;]pong]")); 249 | 250 | } else if (!strcmp_P(msg,PSTR("swap"))) { 251 | Serial.swap(); 252 | serialSwapped =! serialSwapped; 253 | if (client) 254 | client->printf_P(PSTR("Swapped UART pins, now using [[b;green;]RX-GPIO%d TX-GPIO%d]"), 255 | serialSwapped?13:3,serialSwapped?15:1); 256 | 257 | // Debug information 258 | } else if ( !strncmp_P(msg,PSTR("debug"), 5) ) { 259 | int br = SERIAL_DEVICE.baudRate(); 260 | if (client) { 261 | client->printf_P(PSTR("Baud Rate : [[b;green;]%d] bps"), br); 262 | #ifdef MOD_RN2483 263 | client->printf_P(PSTR("States : appli=[[b;green;]%d] radio=[[b;green;]%d]"), app_state, radioState() ); 264 | #endif 265 | } 266 | 267 | // baud only display current Serial Speed 268 | } else if ( client && l==4 && !strncmp_P(msg,PSTR("baud"), 4) ) { 269 | client->printf_P(PSTR("Current Baud Rate is [[b;green;]%d] bps"), SERIAL_DEVICE.baudRate()); 270 | 271 | // baud speed only display current Serial Speed 272 | } else if (l>=6 && !strncmp_P(msg,PSTR("baud "), 5) ) { 273 | uint32_t br = atoi(&msg[5]); 274 | if ( br==115200 || br==57600 || br==19200 || br==9600 ) { 275 | #ifdef MOD_RN2483 276 | radioInit(br); 277 | #elif defined (MOD_TERMINAL) 278 | SERIAL_DEVICE.begin(br); 279 | #endif 280 | if (client) 281 | client->printf_P(PSTR("Serial Baud Rate is now [[b;green;]%d] bps"), br); 282 | } else { 283 | if (client) { 284 | client->printf_P(PSTR("[[b;red;]Error: Invalid Baud Rate %d]"), br); 285 | client->printf_P(PSTR("Valid baud rate are 115200, 57600, 19200 or 9600")); 286 | } 287 | } 288 | 289 | #ifdef RGB_LED_PIN 290 | // rgb led current luminosity 291 | } else if ( client && l==3 && !strncmp_P(msg,PSTR("rgb"), 3) ) { 292 | client->printf_P(PSTR("Current RGB Led Luminosity is [[b;green;]%d%%]"), rgb_luminosity); 293 | 294 | // rgb led luminosity 295 | } else if (l>=5 && !strncmp_P(msg,PSTR("rgb "), 4) ) { 296 | uint8_t lum = atoi(&msg[4]); 297 | if ( lum>=0 && lum<=100) { 298 | rgb_luminosity = lum; 299 | if (client) 300 | client->printf_P(PSTR("RGB Led Luminosity is now [[b;green;]%d]"), lum); 301 | } else { 302 | if (client) { 303 | client->printf_P(PSTR("[[b;red;]Error: Invalid RGB Led Luminosity value %d]"), lum); 304 | client->printf_P(PSTR("Valid is from 0 (off) to 100 (full)")); 305 | } 306 | } 307 | #endif 308 | 309 | } else if (client && !strcmp_P(msg,PSTR("hostname")) ) { 310 | client->printf_P(PSTR("[[b;green;]%s]"), thishost); 311 | 312 | // Dir files on SPIFFS system 313 | // -------------------------- 314 | } else if (!strcmp_P(msg,PSTR("ls")) ) { 315 | Dir dir = SPIFFS.openDir("/"); 316 | uint16_t cnt = 0; 317 | String out = PSTR("SPIFFS Files\r\n Size Name"); 318 | char buff[16]; 319 | 320 | while ( dir.next() ) { 321 | cnt++; 322 | File entry = dir.openFile("r"); 323 | sprintf_P(buff, "\r\n%6d ", entry.size()); 324 | //client->printf_P(PSTR("%5d %s"), entry.size(), entry.name()); 325 | out += buff; 326 | out += String(entry.name()).substring(1); 327 | entry.close(); 328 | } 329 | if (client) 330 | client->printf_P(PSTR("%s\r\nFound %d files"), out.c_str(), cnt); 331 | 332 | // read file and send to serial 333 | // ---------------------------- 334 | } else if (l>=6 && !strncmp_P(msg,PSTR("read "), 5) ) { 335 | char * pfname = &msg[5]; 336 | 337 | if ( *pfname != '/' ) 338 | *--pfname = '/'; 339 | 340 | // file exists 341 | if (SPIFFS.exists(pfname)) { 342 | // open file for reading. 343 | File ofile = SPIFFS.open(pfname, "r"); 344 | if (ofile) { 345 | char c; 346 | String str=""; 347 | if (client) 348 | client->printf_P(PSTR("Reading file %s"), pfname); 349 | // Read until end 350 | while (ofile.available()) 351 | { 352 | // Read all chars 353 | c = ofile.read(); 354 | if (c=='\r') { 355 | // Nothing to do 356 | } else if (c == '\n') { 357 | str.trim(); 358 | if (str!="") { 359 | char c = str.charAt(0); 360 | // Not a comment 361 | if ( c != '#' ) { 362 | // internal command for us ? 363 | if ( c=='!' || c=='$' ) { 364 | // Call ourselve to execute internal, command 365 | execCommand(client, (char*) (str.c_str())+1); 366 | 367 | // Don't read serial in async (websocket) 368 | if (c=='$' && !client) { 369 | String ret = SERIAL_DEVICE.readStringUntil('\n'); 370 | ret.trim(); 371 | SERIAL_DEBUG.printf(" <- \"%s\"\r\n", ret.c_str()); 372 | } 373 | } else { 374 | // send content to Serial (passtrough) 375 | SERIAL_DEVICE.print(str); 376 | SERIAL_DEVICE.print("\r\n"); 377 | 378 | // and to do connected client 379 | if (client) 380 | client->printf_P(PSTR("[[b;green;]%s]"), str.c_str()); 381 | } 382 | } else { 383 | // and to do connected client 384 | if (client) 385 | client->printf_P(PSTR("[[b;grey;]%s]"), str.c_str()); 386 | } 387 | } 388 | str = ""; 389 | } else { 390 | // prepare line 391 | str += c; 392 | } 393 | } 394 | ofile.close(); 395 | } else { 396 | if (client) 397 | client->printf_P(PSTR("[[b;red;]Error: opening file %s]"), pfname); 398 | } 399 | } else { 400 | if (client) 401 | client->printf_P(PSTR("[[b;red;]Error: file %s not found]"), pfname); 402 | } 403 | 404 | // show file content 405 | // ----------------- 406 | } else if (l>=6 && !strncmp_P(msg,PSTR("cat "), 4) ) { 407 | char * pfname = &msg[4]; 408 | 409 | if ( *pfname != '/' ) 410 | *--pfname = '/'; 411 | 412 | // file exists 413 | if (SPIFFS.exists(pfname)) { 414 | // open file for reading. 415 | File ofile = SPIFFS.open(pfname, "r"); 416 | if (ofile) { 417 | 418 | size_t size = ofile.size(); 419 | size_t chunk; 420 | char * p; 421 | 422 | client->printf_P(PSTR("content of file %s size %u bytes"), pfname, size); 423 | 424 | // calculate chunk size (max 1Kb) 425 | chunk = size>=1024?1024:size; 426 | 427 | // Allocate a buffer to store contents of the file + \0 428 | p = (char *) malloc( chunk+1 ); 429 | 430 | while (p && size) { 431 | ofile.readBytes(p, chunk); 432 | *(p+chunk) = '\0'; 433 | 434 | if (client) 435 | client->text(p); 436 | 437 | // This is done 438 | size -= chunk; 439 | 440 | // Last chunk 441 | if (sizeprintf_P(PSTR("[[b;red;]Error: opening file %s]"), pfname); 452 | } 453 | } else { 454 | if (client) 455 | client->printf_P(PSTR("[[b;red;]Error: file %s not found]"), pfname); 456 | } 457 | 458 | // no delay in client (websocket) 459 | // ---------------------------- 460 | } else if (client==NULL && l>=7 && !strncmp_P(msg,PSTR("delay "), 6) ) { 461 | uint16_t v= atoi(&msg[6]); 462 | if (v>=0 && v<=60000 ) { 463 | while(v--) { 464 | LedRGBAnimate(500); 465 | delay(1); 466 | } 467 | } 468 | 469 | // ---------------------------- 470 | } else if (l>=7 && !strncmp_P(msg,PSTR("reset "), 6) ) { 471 | int v= atoi(&msg[6]); 472 | if (v>=0 && v<=16) { 473 | pinMode(v, OUTPUT); 474 | digitalWrite(v, HIGH); 475 | if (client) 476 | client->printf_P(PSTR("[[b;orange;]Reseting pin %d]"), v); 477 | digitalWrite(v, LOW); 478 | delay(50); 479 | digitalWrite(v, HIGH); 480 | } else { 481 | if (client) { 482 | client->printf_P(PSTR("[[b;red;]Error: Invalid pin number %d]"), v); 483 | client->printf_P(PSTR("Valid pin number are 0 to 16")); 484 | } 485 | } 486 | 487 | } else if (client && !strcmp_P(msg,PSTR("fw"))) { 488 | client->printf_P(PSTR("Firmware version [[b;green;]%s %s]"), __DATE__, __TIME__); 489 | 490 | } else if (client && !strcmp_P(msg,PSTR("whoami"))) { 491 | client->printf_P(PSTR("[[b;green;]You are client #%u at index[%d]]"),client->id(), index ); 492 | 493 | } else if (client && !strcmp_P(msg,PSTR("who"))) { 494 | uint8_t cnt = 0; 495 | // Count client 496 | for (uint8_t i=0; iprintf_P(PSTR("[[b;green;]Current client total %d/%d possible]"), cnt, MAX_WS_CLIENT); 503 | for (uint8_t i=0; iprintf_P(PSTR("index[[[b;green;]%d]]; client [[b;green;]#%d]"), i, ws_client[i].id ); 506 | } 507 | } 508 | 509 | } else if (client && !strcmp_P(msg,PSTR("heap"))) { 510 | client->printf_P(PSTR("Free Heap [[b;green;]%ld] bytes"), ESP.getFreeHeap()); 511 | 512 | } else if (client && (*msg=='?' || !strcmp_P(msg,PSTR("help")))) { 513 | client->printf_P(PSTR(HELP_TEXT)); 514 | 515 | // all other to serial Proxy 516 | } else if (*msg) { 517 | //SERIAL_DEBUG.printf("Text '%s'", msg); 518 | // Send text to serial 519 | SERIAL_DEVICE.print(msg); 520 | SERIAL_DEVICE.print("\r\n"); 521 | } 522 | } 523 | 524 | /* ====================================================================== 525 | Function: execCommand 526 | Purpose : translate and execute command received from serial/websocket 527 | Input : client if coming from Websocket 528 | command from Flash 529 | Output : - 530 | Comments: - 531 | ====================================================================== */ 532 | void execCommand(AsyncWebSocketClient * client, PGM_P msg) { 533 | size_t n = strlen_P(msg); 534 | char * cmd = (char*) malloc(n+1); 535 | if( cmd) { 536 | for(size_t b=0; burl(), client->id()); 555 | 556 | for (index=0; indexid(); 559 | ws_client[index].state = CLIENT_ACTIVE; 560 | SERIAL_DEBUG.printf("added #%u at index[%d]\n", client->id(), index); 561 | client->printf("[[b;green;]Hello Client #%u, added you at index %d]", client->id(), index); 562 | client->ping(); 563 | break; // Exit for loop 564 | } 565 | } 566 | if (index>=MAX_WS_CLIENT) { 567 | SERIAL_DEBUG.printf("not added, table is full"); 568 | client->printf("[[b;red;]Sorry client #%u, Max client limit %d reached]", client->id(), MAX_WS_CLIENT); 569 | client->ping(); 570 | } 571 | 572 | } else if(type == WS_EVT_DISCONNECT){ 573 | SERIAL_DEBUG.printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id()); 574 | for (uint8_t i=0; iid() ) { 576 | ws_client[i].id = 0; 577 | ws_client[i].state = CLIENT_NONE; 578 | SERIAL_DEBUG.printf("freed[%d]\n", i); 579 | break; // Exit for loop 580 | } 581 | } 582 | } else if(type == WS_EVT_ERROR){ 583 | SERIAL_DEBUG.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data); 584 | } else if(type == WS_EVT_PONG){ 585 | SERIAL_DEBUG.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:""); 586 | } else if(type == WS_EVT_DATA){ 587 | //data packet 588 | AwsFrameInfo * info = (AwsFrameInfo*) arg; 589 | char * msg = NULL; 590 | size_t n = info->len; 591 | uint8_t index; 592 | 593 | // Size of buffer needed 594 | // String same size +1 for \0 595 | // Hex size*3+1 for \0 (hex displayed as "FF AA BB ...") 596 | n = info->opcode == WS_TEXT ? n+1 : n*3+1; 597 | 598 | msg = (char*) calloc(n, sizeof(char)); 599 | if (msg) { 600 | // Grab all data 601 | for(size_t i=0; i < info->len; i++) { 602 | if (info->opcode == WS_TEXT ) { 603 | msg[i] = (char) data[i]; 604 | } else { 605 | sprintf_P(msg+i*3, PSTR("%02x "), (uint8_t) data[i]); 606 | } 607 | } 608 | } 609 | 610 | SERIAL_DEBUG.printf("ws[%s][%u] message %s\n", server->url(), client->id(), msg); 611 | 612 | // Search if it's a known client 613 | for (index=0; indexid() ) { 615 | SERIAL_DEBUG.printf("known[%d] '%s'\n", index, msg); 616 | SERIAL_DEBUG.printf("client #%d info state=%d\n", client->id(), ws_client[index].state); 617 | 618 | // Received text message 619 | if (info->opcode == WS_TEXT) { 620 | execCommand(client, msg); 621 | } else { 622 | SERIAL_DEBUG.printf("Binary 0x:%s", msg); 623 | } 624 | // Exit for loop 625 | break; 626 | } // if known client 627 | } // for all clients 628 | 629 | // Free up allocated buffer 630 | if (msg) 631 | free(msg); 632 | 633 | } // EVT_DATA 634 | } 635 | 636 | /* ====================================================================== 637 | Function: setup 638 | Purpose : Setup I/O and other one time startup stuff 639 | Input : - 640 | Output : - 641 | Comments: - 642 | ====================================================================== */ 643 | void setup() { 644 | // Set Hostname for OTA and network (add only 2 last bytes of last MAC Address) 645 | // You can't have _ or . in hostname 646 | sprintf_P(thishost, PSTR("WS2Serial-%04X"), ESP.getChipId() & 0xFFFF); 647 | 648 | #ifdef MOD_RN2483 649 | SERIAL_DEVICE.begin(57600); 650 | #else 651 | SERIAL_DEVICE.begin(115200); 652 | #endif 653 | 654 | SERIAL_DEBUG.begin(115200); 655 | SERIAL_DEBUG.print(F("\r\nBooting on ")); 656 | SERIAL_DEBUG.println(ARDUINO_BOARD); 657 | SPIFFS.begin(); 658 | WiFi.mode(WIFI_STA); 659 | 660 | 661 | // No empty sketch SSID, try connect 662 | if (*ssid!='*' && *password!='*' ) { 663 | SERIAL_DEBUG.printf("connecting to %s with psk %s\r\n", ssid, password ); 664 | WiFi.begin(ssid, password); 665 | } else { 666 | // empty sketch SSID, try autoconnect with SDK saved credentials 667 | SERIAL_DEBUG.println(F("No SSID/PSK defined in sketch\r\nConnecting with SDK ones if any")); 668 | } 669 | 670 | // Loop until connected 671 | while ( WiFi.status() !=WL_CONNECTED ) { 672 | LedRGBON(COLOR_ORANGE_YELLOW); 673 | LedRGBAnimate(500); 674 | yield(); 675 | } 676 | 677 | SERIAL_DEBUG.print(F("I'm network device named ")); 678 | SERIAL_DEBUG.println(thishost); 679 | 680 | ArduinoOTA.setHostname(thishost); 681 | 682 | // OTA callbacks 683 | ArduinoOTA.onStart([]() { 684 | // Clean SPIFFS 685 | SPIFFS.end(); 686 | 687 | // Light of the LED, stop animation 688 | LedRGBOFF(); 689 | 690 | ws.textAll("OTA Update Started"); 691 | ws.enable(false); 692 | ws.closeAll(); 693 | 694 | }); 695 | 696 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 697 | uint8_t percent = progress / (total / 100); 698 | // hue from 0.0 to 1.0 (rainbow) with 33% (of 0.5f) luminosity 699 | #ifdef RGB_LED_PIN 700 | rgb_led.SetPixelColor(0, HslColor(percent * 0.01f , 1.0f, 0.17f )); 701 | rgb_led.Show(); 702 | #endif 703 | }); 704 | 705 | ArduinoOTA.onEnd([]() { 706 | #ifdef RGB_LED_PIN 707 | rgb_led.SetPixelColor(0, HslColor(COLOR_GREEN/360.0f, 1.0f, 0.25f)); 708 | rgb_led.Show(); 709 | #endif 710 | }); 711 | 712 | ArduinoOTA.onError([](ota_error_t error) { 713 | #ifdef RGB_LED_PIN 714 | rgb_led.SetPixelColor(0, HslColor(COLOR_RED/360.0f, 1.0f, 0.25f)); 715 | rgb_led.Show(); 716 | #endif 717 | ESP.restart(); 718 | }); 719 | 720 | ArduinoOTA.begin(); 721 | MDNS.addService("http","tcp",80); 722 | 723 | 724 | // Enable and start websockets 725 | ws.onEvent(onEvent); 726 | server.addHandler(&ws); 727 | 728 | server.addHandler(new SPIFFSEditor(http_username,http_password)); 729 | 730 | server.on("/heap", HTTP_GET, [](AsyncWebServerRequest *request){ 731 | request->send(200, "text/plain", String(ESP.getFreeHeap())); 732 | }); 733 | 734 | server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.htm"); 735 | 736 | 737 | server.onNotFound([](AsyncWebServerRequest *request){ 738 | SERIAL_DEBUG.printf("NOT_FOUND: "); 739 | if(request->method() == HTTP_GET) 740 | SERIAL_DEBUG.printf("GET"); 741 | else if(request->method() == HTTP_POST) 742 | SERIAL_DEBUG.printf("POST"); 743 | else if(request->method() == HTTP_DELETE) 744 | SERIAL_DEBUG.printf("DELETE"); 745 | else if(request->method() == HTTP_PUT) 746 | SERIAL_DEBUG.printf("PUT"); 747 | else if(request->method() == HTTP_PATCH) 748 | SERIAL_DEBUG.printf("PATCH"); 749 | else if(request->method() == HTTP_HEAD) 750 | SERIAL_DEBUG.printf("HEAD"); 751 | else if(request->method() == HTTP_OPTIONS) 752 | SERIAL_DEBUG.printf("OPTIONS"); 753 | else 754 | SERIAL_DEBUG.printf("UNKNOWN"); 755 | 756 | SERIAL_DEBUG.printf(" http://%s%s\n", request->host().c_str(), request->url().c_str()); 757 | 758 | if(request->contentLength()){ 759 | SERIAL_DEBUG.printf("_CONTENT_TYPE: %s\n", request->contentType().c_str()); 760 | SERIAL_DEBUG.printf("_CONTENT_LENGTH: %u\n", request->contentLength()); 761 | } 762 | 763 | int headers = request->headers(); 764 | int i; 765 | for(i=0;igetHeader(i); 767 | SERIAL_DEBUG.printf("_HEADER[%s]: %s\n", h->name().c_str(), h->value().c_str()); 768 | } 769 | 770 | int params = request->params(); 771 | for(i=0;igetParam(i); 773 | if(p->isFile()){ 774 | SERIAL_DEBUG.printf("_FILE[%s]: %s, size: %u\n", p->name().c_str(), p->value().c_str(), p->size()); 775 | } else if(p->isPost()){ 776 | SERIAL_DEBUG.printf("_POST[%s]: %s\n", p->name().c_str(), p->value().c_str()); 777 | } else { 778 | SERIAL_DEBUG.printf("_GET[%s]: %s\n", p->name().c_str(), p->value().c_str()); 779 | } 780 | } 781 | 782 | request->send(404); 783 | }); 784 | 785 | server.onFileUpload([](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){ 786 | if(!index) 787 | SERIAL_DEBUG.printf("UploadStart: %s\n", filename.c_str()); 788 | SERIAL_DEBUG.printf("%s", (const char*)data); 789 | if(final) 790 | SERIAL_DEBUG.printf("UploadEnd: %s (%u)\n", filename.c_str(), index+len); 791 | }); 792 | server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){ 793 | if(!index) 794 | SERIAL_DEBUG.printf("BodyStart: %u\n", total); 795 | SERIAL_DEBUG.printf("%s", (const char*)data); 796 | if(index + len == total) 797 | SERIAL_DEBUG.printf("BodyEnd: %u\n", total); 798 | }); 799 | 800 | // Set on board led GPIO to outout if not GPIO2 and Debug Serial1 801 | #if (SERIAL_DEBUG != Serial1) && (BUILTIN_LED != 2) 802 | pinMode(BUILTIN_LED, OUTPUT); 803 | #endif 804 | 805 | #if defined (BTN_GPIO) 806 | pinMode(BTN_GPIO, INPUT); 807 | #endif 808 | 809 | // Start Server 810 | WiFiMode_t con_type = WiFi.getMode(); 811 | uint16_t lcolor = 0; 812 | server.begin(); 813 | SERIAL_DEBUG.print(F("Started ")); 814 | 815 | if (con_type == WIFI_STA) { 816 | SERIAL_DEBUG.print(F("WIFI_STA")); 817 | lcolor=COLOR_GREEN; 818 | } else if (con_type == WIFI_AP_STA) { 819 | SERIAL_DEBUG.print(F("WIFI_AP_STA")); 820 | lcolor=COLOR_CYAN; 821 | } else if (con_type == WIFI_AP) { 822 | SERIAL_DEBUG.print(F("WIFI_AP")); 823 | lcolor=COLOR_MAGENTA; 824 | } else { 825 | SERIAL_DEBUG.print(F("????")); 826 | lcolor = COLOR_RED; 827 | } 828 | 829 | SERIAL_DEBUG.print(F("on HTTP://")); 830 | SERIAL_DEBUG.print(WiFi.localIP()); 831 | SERIAL_DEBUG.print(F(" and WS://")); 832 | SERIAL_DEBUG.print(WiFi.localIP()); 833 | SERIAL_DEBUG.println(F("/ws")); 834 | 835 | // Set Breathing color during startup script 836 | LedRGBON(lcolor, true); 837 | 838 | // Run startup script if any 839 | execCommand(NULL, PSTR("read /startup.ini") ); 840 | 841 | // We're alive, CYAN LED 842 | // move animation from fade to blink 843 | #ifdef RGB_LED_PIN 844 | rgb_anim_state=RGB_ANIM_BLINK_ON; 845 | LedRGBON(COLOR_CYAN, true); 846 | #endif 847 | 848 | #ifdef RN2483 849 | app_state = APP_IDLE; 850 | #endif 851 | } 852 | 853 | /* ====================================================================== 854 | Function: loop 855 | Purpose : infinite loop main code 856 | Input : - 857 | Output : - 858 | Comments: - 859 | ====================================================================== */ 860 | void loop() { 861 | static bool led_state ; 862 | bool new_led_state ; 863 | uint16_t anim_speed; 864 | uint8_t button_port; 865 | 866 | #ifdef SERIAL_DEVICE 867 | // Got one serial char ? 868 | if (SERIAL_DEVICE.available()) { 869 | // Read it and store it in buffer 870 | char inChar = (char)SERIAL_DEVICE.read(); 871 | 872 | // CR line char, discard ? 873 | if (inChar == '\r') { 874 | // Do nothing 875 | 876 | // LF ok let's do our job 877 | } else if (inChar == '\n') { 878 | // Send to all client without cr/lf 879 | ws.textAll(inputString); 880 | // Display on debug 881 | SERIAL_DEBUG.printf(" <- \"%s\"\r\n", inputString.c_str()); 882 | 883 | #ifdef MOD_RN2483 884 | if (radioResponse(inputString)) { 885 | delay(200); 886 | } 887 | #endif 888 | 889 | inputString = ""; 890 | } else { 891 | // Add char to input string 892 | if (inChar>=' ' && inChar<='}') 893 | inputString += inChar; 894 | else 895 | inputString += '.'; 896 | } 897 | } 898 | #endif 899 | 900 | // Led blink management 901 | if (WiFi.status()==WL_CONNECTED) { 902 | new_led_state = ((millis() % 1000) < 200) ? LOW:HIGH; // Connected long blink 200ms on each second 903 | anim_speed = 1000; 904 | } else { 905 | new_led_state = ((millis() % 333) < 111) ? LOW:HIGH;// AP Mode or client failed quick blink 111ms on each 1/3sec 906 | anim_speed = 333; 907 | } 908 | // Led management 909 | if (led_state != new_led_state) { 910 | led_state = new_led_state; 911 | 912 | #if (SERIAL_DEBUG != Serial1) && (BUILTIN_LED != 2) 913 | digitalWrite(BUILTIN_LED, led_state); 914 | #endif 915 | } 916 | 917 | #if defined (BTN_GPIO) && defined (MOD_RN2483) 918 | 919 | // Get switch port state 920 | button_port = digitalRead(BTN_GPIO); 921 | 922 | // Button pressed 923 | if (button_port==BTN_PRESSED){ 924 | btn_state_e btn_state; 925 | 926 | // we enter into the loop to manage 927 | // the function that will be done 928 | // depending on button press duration 929 | do { 930 | // keep watching the push button: 931 | btn_state = buttonManageState(button_port); 932 | 933 | // read new state button 934 | button_port = digitalRead(BTN_GPIO); 935 | 936 | // this loop can be as long as button is pressed 937 | yield(); 938 | ArduinoOTA.handle(); 939 | } 940 | // we loop until button state machine finished 941 | while (btn_state != BTN_WAIT_PUSH); 942 | 943 | // Do what we need to do depending on action 944 | radioManageState(_btn_Action); 945 | 946 | //SERIAL_DEBUG.printf("Button %d\r\n", _btn_Action); 947 | 948 | // button not pressed 949 | } else { 950 | radioManageState(BTN_NONE); 951 | } 952 | 953 | // move next animation to blink 954 | if (radioState()==RADIO_IDLE && app_state==APP_IDLE) { 955 | rgb_anim_state=RGB_ANIM_BLINK_ON; 956 | LedRGBON(COLOR_CYAN); 957 | } 958 | // move next animation to default fade 959 | if (app_state==APP_CONTINUOUS_LISTEN) { 960 | rgb_anim_state=RGB_ANIM_NONE; 961 | LedRGBON(COLOR_CYAN); 962 | } 963 | if (radioState()==RADIO_IDLE && app_state==APP_CONTINUOUS_SEND) { 964 | rgb_anim_state=RGB_ANIM_NONE; 965 | LedRGBON(COLOR_MAGENTA); 966 | } 967 | #endif 968 | 969 | // Handle remote Wifi Updates 970 | ArduinoOTA.handle(); 971 | 972 | // RGB LED Animation 973 | LedRGBAnimate(anim_speed); 974 | } 975 | -------------------------------------------------------------------------------- /webdev/js/terminal.js: -------------------------------------------------------------------------------- 1 | /**@license 2 | * __ _____ ________ __ 3 | * / // _ /__ __ _____ ___ __ _/__ ___/__ ___ ______ __ __ __ ___ / / 4 | * __ / // // // // // _ // _// // / / // _ // _// // // \/ // _ \/ / 5 | * / / // // // // // ___// / / // / / // ___// / / / / // // /\ // // / /__ 6 | * \___//____ \\___//____//_/ _\_ / /_//____//_/ /_/ /_//_//_/ /_/ \__\_\___/ 7 | * \/ /____/ version {{VER} 8 | * 9 | * This file is part of jQuery Terminal. http://terminal.jcubic.pl 10 | * 11 | * Copyright (c) 2010-2016 Jakub Jankiewicz 12 | * Released under the MIT license 13 | * 14 | * Contains: 15 | * 16 | * Storage plugin Distributed under the MIT License 17 | * Copyright (c) 2010 Dave Schindler 18 | * 19 | * jQuery Timers licenced with the WTFPL 20 | * 21 | * 22 | * Cross-Browser Split 1.1.1 23 | * Copyright 2007-2012 Steven Levithan 24 | * Available under the MIT License 25 | * 26 | * jQuery Caret 27 | * Copyright (c) 2009, Gideon Sireling 28 | * 3 clause BSD License 29 | * 30 | * sprintf.js 31 | * Copyright (c) 2007-2013 Alexandru Marasteanu 32 | * licensed under 3 clause BSD license 33 | * 34 | * Date: Mon, 02 May 2016 15:26:54 +0000 35 | */ 36 | (function(ctx){var sprintf=function(){if(!sprintf.cache.hasOwnProperty(arguments[0])){sprintf.cache[arguments[0]]=sprintf.parse(arguments[0])}return sprintf.format.call(null,sprintf.cache[arguments[0]],arguments)};sprintf.format=function(parse_tree,argv){var cursor=1,tree_length=parse_tree.length,node_type="",arg,output=[],i,k,match,pad,pad_character,pad_length;for(i=0;i>>0;break;case"x":arg=arg.toString(16);break;case"X":arg=arg.toString(16).toUpperCase();break}arg=/[def]/.test(match[8])&&match[3]&&arg>=0?"+"+arg:arg;pad_character=match[4]?match[4]=="0"?"0":match[4].charAt(1):" ";pad_length=match[6]-String(arg).length;pad=match[6]?str_repeat(pad_character,pad_length):"";output.push(match[5]?arg+pad:pad+arg)}}return output.join("")};sprintf.cache={};sprintf.parse=function(fmt){var _fmt=fmt,match=[],parse_tree=[],arg_names=0;while(_fmt){if((match=/^[^\x25]+/.exec(_fmt))!==null){parse_tree.push(match[0])}else if((match=/^\x25{2}/.exec(_fmt))!==null){parse_tree.push("%")}else if((match=/^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt))!==null){if(match[2]){arg_names|=1;var field_list=[],replacement_field=match[2],field_match=[];if((field_match=/^([a-z_][a-z_\d]*)/i.exec(replacement_field))!==null){field_list.push(field_match[1]);while((replacement_field=replacement_field.substring(field_match[0].length))!==""){if((field_match=/^\.([a-z_][a-z_\d]*)/i.exec(replacement_field))!==null){field_list.push(field_match[1])}else if((field_match=/^\[(\d+)\]/.exec(replacement_field))!==null){field_list.push(field_match[1])}else{throw"[sprintf] huh?"}}}else{throw"[sprintf] huh?"}match[2]=field_list}else{arg_names|=2}if(arg_names===3){throw"[sprintf] mixing positional and named placeholders is not (yet) supported"}parse_tree.push(match)}else{throw"[sprintf] huh?"}_fmt=_fmt.substring(match[0].length)}return parse_tree};var vsprintf=function(fmt,argv,_argv){_argv=argv.slice(0);_argv.splice(0,0,fmt);return sprintf.apply(null,_argv)};function get_type(variable){return Object.prototype.toString.call(variable).slice(8,-1).toLowerCase()}function str_repeat(input,multiplier){for(var output=[];multiplier>0;output[--multiplier]=input){}return output.join("")}ctx.sprintf=sprintf;ctx.vsprintf=vsprintf})(typeof global!="undefined"?global:window);(function($,undefined){"use strict";$.omap=function(o,fn){var result={};$.each(o,function(k,v){result[k]=fn.call(o,k,v)});return result};var Clone={clone_object:function(object){var tmp={};if(typeof object=="object"){if($.isArray(object)){return this.clone_array(object)}else if(object===null){return object}else{for(var key in object){if($.isArray(object[key])){tmp[key]=this.clone_array(object[key])}else if(typeof object[key]=="object"){tmp[key]=this.clone_object(object[key])}else{tmp[key]=object[key]}}}}return tmp},clone_array:function(array){if(!$.isFunction(Array.prototype.map)){throw new Error("You'r browser don't support ES5 array map "+"use es5-shim")}return array.slice(0).map(function(item){if(typeof item=="object"){return this.clone_object(item)}else{return item}}.bind(this))}};var clone=function(object){return Clone.clone_object(object)};var hasLS=function(){var testKey="test",storage=window.localStorage;try{storage.setItem(testKey,"1");storage.removeItem(testKey);return true}catch(error){return false}};var isLS=hasLS();function wls(n,v){var c;if(typeof n==="string"&&typeof v==="string"){localStorage[n]=v;return true}else if(typeof n==="object"&&typeof v==="undefined"){for(c in n){if(n.hasOwnProperty(c)){localStorage[c]=n[c]}}return true}return false}function wc(n,v){var dt,e,c;dt=new Date;dt.setTime(dt.getTime()+31536e6);e="; expires="+dt.toGMTString();if(typeof n==="string"&&typeof v==="string"){document.cookie=n+"="+v+e+"; path=/";return true}else if(typeof n==="object"&&typeof v==="undefined"){for(c in n){if(n.hasOwnProperty(c)){document.cookie=c+"="+n[c]+e+"; path=/"}}return true}return false}function rls(n){return localStorage[n]}function rc(n){var nn,ca,i,c;nn=n+"=";ca=document.cookie.split(";");for(i=0;itimes&×!==0||fn.call(element,counter)===false){jQuery.timer.remove(element,label,fn)}handler.inProgress=false};handler.$timerID=fn.$timerID;if(!element.$timers[label][fn.$timerID]){element.$timers[label][fn.$timerID]=window.setInterval(handler,interval)}if(!this.global[label]){this.global[label]=[]}this.global[label].push(element)},remove:function(element,label,fn){var timers=element.$timers,ret;if(timers){if(!label){for(var lab in timers){if(timers.hasOwnProperty(lab)){this.remove(element,lab,fn)}}}else if(timers[label]){if(fn){if(fn.$timerID){window.clearInterval(timers[label][fn.$timerID]);delete timers[label][fn.$timerID]}}else{for(var _fn in timers[label]){if(timers[label].hasOwnProperty(_fn)){window.clearInterval(timers[label][_fn]);delete timers[label][_fn]}}}for(ret in timers[label]){if(timers[label].hasOwnProperty(ret)){break}}if(!ret){ret=null;delete timers[label]}}for(ret in timers){if(timers.hasOwnProperty(ret)){break}}if(!ret){element.$timers=null}}}}});if(/(msie) ([\w.]+)/.exec(navigator.userAgent.toLowerCase())){jQuery(window).one("unload",function(){var global=jQuery.timer.global;for(var label in global){if(global.hasOwnProperty(label)){var els=global[label],i=els.length;while(--i){jQuery.timer.remove(els[i],label)}}}})}(function(undef){if(!String.prototype.split.toString().match(/\[native/)){return}var nativeSplit=String.prototype.split,compliantExecNpcg=/()??/.exec("")[1]===undef,self;self=function(str,separator,limit){if(Object.prototype.toString.call(separator)!=="[object RegExp]"){return nativeSplit.call(str,separator,limit)}var output=[],flags=(separator.ignoreCase?"i":"")+(separator.multiline?"m":"")+(separator.extended?"x":"")+(separator.sticky?"y":""),lastLastIndex=0,separator2,match,lastIndex,lastLength;separator=new RegExp(separator.source,flags+"g");str+="";if(!compliantExecNpcg){separator2=new RegExp("^"+separator.source+"$(?!\\s)",flags)}limit=limit===undef?-1>>>0:limit>>>0;while(match=separator.exec(str)){lastIndex=match.index+match[0].length;if(lastIndex>lastLastIndex){output.push(str.slice(lastLastIndex,match.index));if(!compliantExecNpcg&&match.length>1){match[0].replace(separator2,function(){for(var i=1;i1&&match.index=limit){break}}if(separator.lastIndex===match.index){separator.lastIndex++}}if(lastLastIndex===str.length){if(lastLength||!separator.test("")){output.push("")}}else{output.push(str.slice(lastLastIndex))}return output.length>limit?output.slice(0,limit):output};String.prototype.split=function(separator,limit){return self(this,separator,limit)};return self})();$.fn.caret=function(pos){var target=this[0];var isContentEditable=target.contentEditable==="true";if(arguments.length==0){if(window.getSelection){if(isContentEditable){target.focus();var range1=window.getSelection().getRangeAt(0),range2=range1.cloneRange();range2.selectNodeContents(target);range2.setEnd(range1.endContainer,range1.endOffset);return range2.toString().length}return target.selectionStart}if(document.selection){target.focus();if(isContentEditable){var range1=document.selection.createRange(),range2=document.body.createTextRange();range2.moveToElementText(target);range2.setEndPoint("EndToEnd",range1);return range2.text.length}var pos=0,range=target.createTextRange(),range2=document.selection.createRange().duplicate(),bookmark=range2.getBookmark();range.moveToBookmark(bookmark);while(range.moveStart("character",-1)!==0)pos++;return pos}return 0}if(pos==-1)pos=this[isContentEditable?"text":"val"]().length;if(window.getSelection){if(isContentEditable){target.focus();window.getSelection().collapse(target.firstChild,pos)}else target.setSelectionRange(pos,pos)}else if(document.body.createTextRange){var range=document.body.createTextRange();range.moveToElementText(target);range.moveStart("character",pos);range.collapse(true);range.select()}if(!isContentEditable)target.focus();return pos};function str_parts(str,length){var result=[];var len=str.length;if(len0?data[data.length-1]:null},clone:function(){return new Stack(data.slice(0))}})}$.json_stringify=function(object,level){var result="",i;level=level===undefined?1:level;var type=typeof object;switch(type){case"function":result+=object;break;case"boolean":result+=object?"true":"false";break;case"object":if(object===null){result+="null"}else if(object instanceof Array){result+="[";var len=object.length;for(i=0;i1?",":"";if(level===1){result=result.replace(/,([\]}])/g,"$1")}return result.replace(/([\[{]),/g,"$1")};function History(name,size){var enabled=true;var storage_key="";if(typeof name==="string"&&name!==""){storage_key=name+"_"}storage_key+="commands";var data=$.Storage.get(storage_key);data=data?$.parseJSON(data):[];var pos=data.length-1;$.extend(this,{append:function(item){if(enabled){if(data[data.length-1]!==item){data.push(item);if(size&&data.length>size){data=data.slice(-size)}pos=data.length-1;$.Storage.set(storage_key,$.json_stringify(data))}}},data:function(){return data},reset:function(){pos=data.length-1},last:function(){return data[length-1]},end:function(){return pos===data.length-1},position:function(){return pos},current:function(){return data[pos]},next:function(){if(pos0){--pos}if(old!==-1){return data[pos]}},clear:function(){data=[];this.purge()},enabled:function(){return enabled},enable:function(){enabled=true},purge:function(){$.Storage.remove(storage_key)},disable:function(){enabled=false}})}var is_paste_supported=function(){var el=document.createElement("div");el.setAttribute("onpaste","return;");return typeof el.onpaste=="function"}();var first_cmd=true;$.fn.cmd=function(options){var self=this;var maybe_data=self.data("cmd");if(maybe_data){return maybe_data}self.addClass("cmd");self.append(''+' ');var clip=$("