|
| 1 | +/******************** |
| 2 | +Arduino generic menu system |
| 3 | +
|
| 4 | +Feb. 2019 Rui Azevedo - ruihfazevedo(@rrob@)gmail.com |
| 5 | +
|
| 6 | +IMPORTANT!: |
| 7 | +this requires the data folder to be stored on esp8266 spiff |
| 8 | +Extra libraries should be present |
| 9 | +
|
| 10 | +arduinoWebSockets - https://github.com/Links2004/arduinoWebSockets |
| 11 | +ESP8266WiFi - https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi |
| 12 | +ESP8266WebServer - https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer |
| 13 | +
|
| 14 | +*/ |
| 15 | +#include <Servo.h> |
| 16 | + |
| 17 | +#include <menu.h> |
| 18 | +#include <menuIO/esp8266Out.h> |
| 19 | +#include <menuIO/xmlFmt.h>//to write a menu has html page |
| 20 | +#include <menuIO/serialIn.h> |
| 21 | +#include <menuIO/xmlFmt.h>//to write a menu has xml page |
| 22 | +#include <menuIO/jsonFmt.h>//to write a menu has xml page |
| 23 | +#ifndef ARDUINO_STREAMING |
| 24 | + #include <streamFlow.h>//https://github.com/neu-rah/streamFlow |
| 25 | +#else |
| 26 | + #include <Streaming.h>//https://github.com/scottdky/Streaming |
| 27 | +#endif |
| 28 | +//#include <menuIO/jsFmt.h>//to send javascript thru web socket (live update) |
| 29 | +#include <FS.h> |
| 30 | +#include <Hash.h> |
| 31 | +extern "C" { |
| 32 | + #include "user_interface.h" |
| 33 | +} |
| 34 | + |
| 35 | +using namespace Menu; |
| 36 | + |
| 37 | +#ifdef WEB_DEBUG |
| 38 | + // on debug mode I put aux files on external server to allow changes without SPIFF update |
| 39 | + // on this mode the browser MUST be instructed to accept cross domain files |
| 40 | + String xslt("http://neurux:8080/"); |
| 41 | +#else |
| 42 | + String xslt(""); |
| 43 | +#endif |
| 44 | + |
| 45 | +menuOut& operator<<(menuOut& o,unsigned long int i) { |
| 46 | + o.print(i); |
| 47 | + return o; |
| 48 | +} |
| 49 | +menuOut& operator<<(menuOut& o,endlObj) { |
| 50 | + o.println(); |
| 51 | + return o; |
| 52 | +} |
| 53 | + |
| 54 | +//this version numbers MUST be the same as data/1.2 |
| 55 | +#define CUR_VERSION "1.4" |
| 56 | +#define APName "EscControl" |
| 57 | + |
| 58 | +#define ESCPIN 2 |
| 59 | +Servo myservo; |
| 60 | + |
| 61 | +#ifndef MENU_SSID |
| 62 | + #error "need to define WiFi SSID here" |
| 63 | + #define MENU_SSID "r-site.net" |
| 64 | +#endif |
| 65 | +#ifndef MENU_PASS |
| 66 | + #error "need to define WiFi password here" |
| 67 | + #define MENU_PASS "" |
| 68 | +#endif |
| 69 | + |
| 70 | +// char wifiSSID[wifiSSIDLen+1];//=" "; |
| 71 | +// char wifiPwd [wifiPwdLen+1];//=" "; |
| 72 | + |
| 73 | +const char* ssid = MENU_SSID; |
| 74 | +const char* password = MENU_PASS; |
| 75 | +const char* serverName="192.168.1.79"; |
| 76 | + |
| 77 | +#define HTTP_PORT 80 |
| 78 | +#define WS_PORT 81 |
| 79 | +#define USE_SERIAL Serial |
| 80 | +ESP8266WebServer server(80); |
| 81 | +WebSocketsServer webSocket(81); |
| 82 | + |
| 83 | +#define MAX_DEPTH 2 |
| 84 | +idx_t web_tops[MAX_DEPTH]; |
| 85 | +PANELS(webPanels,{0,0,80,100}); |
| 86 | +xmlFmt<esp8266_WebServerStreamOut> serverOut(server,web_tops,webPanels); |
| 87 | +jsonFmt<esp8266_WebServerStreamOut> jsonOut(server,web_tops,webPanels); |
| 88 | +jsonFmt<esp8266BufferedOut> wsOut(web_tops,webPanels); |
| 89 | + |
| 90 | +int speed=0;//0 to 180 |
| 91 | +result setEsc() { |
| 92 | + myservo.write(speed); |
| 93 | +} |
| 94 | +result armEsc() { |
| 95 | + Serial.println("arming ESC"); |
| 96 | + serverOut<<"arming ESC<br/>"; |
| 97 | + // arm the speed controller, modify as necessary for your ESC |
| 98 | + speed=0; |
| 99 | + setEsc(); |
| 100 | + delay(1000); //delay 1 second, some speed controllers may need longer |
| 101 | + return proceed; |
| 102 | +} |
| 103 | +result stopEsc() { |
| 104 | + Serial.println("ESC stop"); |
| 105 | + serverOut<<"ESC stop<br/>"; |
| 106 | + speed=0; |
| 107 | + setEsc(); |
| 108 | + return proceed; |
| 109 | +} |
| 110 | + |
| 111 | +//the menu |
| 112 | +MENU(mainMenu,"Main menu",doNothing,noEvent,wrapStyle |
| 113 | + ,OP("Stop",stopEsc,enterEvent) |
| 114 | + ,OP("Arm",armEsc,enterEvent) |
| 115 | + ,FIELD(speed,"Speed","%",0,100,10,1, setEsc, enterEvent, noStyle) |
| 116 | + ,EXIT("Exit!") |
| 117 | +); |
| 118 | + |
| 119 | +result idle(menuOut& o,idleEvent e) { |
| 120 | + //if (e==idling) |
| 121 | + Serial.println("suspended"); |
| 122 | + o<<"suspended..."<<endl<<"press [select]"<<endl<<"to continue"<<endl<<(millis()%1000); |
| 123 | + return quit; |
| 124 | +} |
| 125 | + |
| 126 | +template<typename T>//some utill to help us calculate array sizes (known at compile time) |
| 127 | +constexpr inline size_t len(T& o) {return sizeof(o)/sizeof(decltype(o[0]));} |
| 128 | + |
| 129 | +//serial menu navigation |
| 130 | +MENU_OUTLIST(out,&serverOut); |
| 131 | +serialIn serial(Serial); |
| 132 | +NAVROOT(nav,mainMenu,MAX_DEPTH,serial,out); |
| 133 | + |
| 134 | +//xml+http navigation control |
| 135 | +noInput none;//web uses its own API |
| 136 | +menuOut* web_outputs[]={&serverOut}; |
| 137 | +outputsList web_out(web_outputs,len(web_outputs)); |
| 138 | +navNode web_cursors[MAX_DEPTH]; |
| 139 | +navRoot webNav(mainMenu, web_cursors, MAX_DEPTH, none, web_out); |
| 140 | + |
| 141 | +//json+http navigation control |
| 142 | +menuOut* json_outputs[]={&jsonOut}; |
| 143 | +outputsList json_out(json_outputs,len(json_outputs)); |
| 144 | +navNode json_cursors[MAX_DEPTH]; |
| 145 | +navRoot jsonNav(mainMenu, json_cursors, MAX_DEPTH, none, json_out); |
| 146 | + |
| 147 | +//websockets navigation control |
| 148 | +menuOut* ws_outputs[]={&wsOut}; |
| 149 | +outputsList ws_out(ws_outputs,len(ws_outputs)); |
| 150 | +navNode ws_cursors[MAX_DEPTH]; |
| 151 | +navRoot wsNav(mainMenu, ws_cursors, MAX_DEPTH, none, ws_out); |
| 152 | + |
| 153 | +//config myOptions('*','-',defaultNavCodes,false); |
| 154 | + |
| 155 | +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { |
| 156 | + switch(type) { |
| 157 | + case WStype_DISCONNECTED: |
| 158 | + //USE_SERIAL.printf("[%u] Disconnected!\n", num); |
| 159 | + break; |
| 160 | + case WStype_CONNECTED: { |
| 161 | + IPAddress ip = webSocket.remoteIP(num); |
| 162 | + //USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); |
| 163 | + webSocket.sendTXT(num, "console.log('ArduinoMenu Connected')"); |
| 164 | + } |
| 165 | + break; |
| 166 | + case WStype_TEXT: { |
| 167 | + //USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); |
| 168 | + // nav.async((const char*)payload);//this is slow!!!!!!!! |
| 169 | + __trace(Serial.printf("[%u] get Text: %s\n", num, payload)); |
| 170 | + char*s=(char*)payload; |
| 171 | + _trace(Serial<<"serve websocket menu"<<endl); |
| 172 | + wsOut.response.remove(0); |
| 173 | + wsOut<<"{\"output\":\""; |
| 174 | + wsNav.async((const char*)payload); |
| 175 | + wsOut<<"\",\n\"menu\":"; |
| 176 | + wsNav.doOutput(); |
| 177 | + wsOut<<"\n}"; |
| 178 | + webSocket.sendTXT(num,wsOut.response); |
| 179 | + // wsOut.response.remove(0); |
| 180 | + // jsonEnd(); |
| 181 | + } break; |
| 182 | + case WStype_BIN: { |
| 183 | + USE_SERIAL<<"[WSc] get binary length:"<<length<<"["; |
| 184 | + for(int c=0;c<length;c++) { |
| 185 | + USE_SERIAL.print(*(char*)(payload+c),HEX); |
| 186 | + USE_SERIAL.write(','); |
| 187 | + } |
| 188 | + USE_SERIAL<<"]"<<endl; |
| 189 | + uint16_t id=*(uint16_t*) payload++; |
| 190 | + idx_t len=*((idx_t*)++payload); |
| 191 | + idx_t* pathBin=(idx_t*)++payload; |
| 192 | + const char* inp=(const char*)(payload+len); |
| 193 | + //Serial<<"id:"<<id<<endl; |
| 194 | + if (id==nav.active().hash()) { |
| 195 | + //Serial<<"id ok."<<endl;Serial.flush(); |
| 196 | + //Serial<<"input:"<<inp<<endl; |
| 197 | + //StringStream inStr(inp); |
| 198 | + //while(inStr.available()) |
| 199 | + nav.doInput(inp); |
| 200 | + webSocket.sendTXT(num, "binBusy=false;");//send javascript to unlock the state |
| 201 | + } //else Serial<<"id not ok!"<<endl; |
| 202 | + //Serial<<endl; |
| 203 | + } |
| 204 | + break; |
| 205 | + default:break; |
| 206 | + } |
| 207 | +} |
| 208 | + |
| 209 | +void pageStart() { |
| 210 | + _trace(Serial<<"pasgeStart!"<<endl); |
| 211 | + serverOut<<"HTTP/1.1 200 OK\r\n" |
| 212 | + <<"Content-Type: text/xml\r\n" |
| 213 | + <<"Connection: close\r\n" |
| 214 | + <<"Expires: 0\r\n" |
| 215 | + <<"\r\n"; |
| 216 | + serverOut<<"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\r\n" |
| 217 | + "<?xml-stylesheet type=\"text/xsl\" href=\""; |
| 218 | + serverOut<<xslt; |
| 219 | + serverOut<<CUR_VERSION"/device.xslt"; |
| 220 | + serverOut<<"\"?>\r\n<menuLib" |
| 221 | + #ifdef WEB_DEBUG |
| 222 | + <<" debug=\"yes\"" |
| 223 | + #endif |
| 224 | + <<" host=\""; |
| 225 | + serverOut.print(APName); |
| 226 | + serverOut<<"\">\r\n<sourceURL ver=\"" CUR_VERSION "/\">"; |
| 227 | + if (server.hasHeader("host")) |
| 228 | + serverOut.print(server.header("host")); |
| 229 | + else |
| 230 | + serverOut.print(APName); |
| 231 | + serverOut<<"</sourceURL>"; |
| 232 | +} |
| 233 | + |
| 234 | +void pageEnd() { |
| 235 | + serverOut<<"</menuLib>"; |
| 236 | + server.client().stop(); |
| 237 | +} |
| 238 | + |
| 239 | +void jsonStart() { |
| 240 | + _trace(Serial<<"jsonStart!"<<endl); |
| 241 | + serverOut<<"HTTP/1.1 200 OK\r\n" |
| 242 | + <<"Content-Type: application/json; charset=utf-8\r\n" |
| 243 | + <<"Connection: close\r\n" |
| 244 | + <<"Expires: 0\r\n" |
| 245 | + <<"\r\n"; |
| 246 | +} |
| 247 | + |
| 248 | +void jsonEnd() { |
| 249 | + server.client().stop(); |
| 250 | +} |
| 251 | + |
| 252 | +bool handleMenu(navRoot& nav){ |
| 253 | + _trace( |
| 254 | + uint32_t free = system_get_free_heap_size(); |
| 255 | + Serial.print(F("free memory:")); |
| 256 | + Serial.print(free); |
| 257 | + Serial.print(F(" handleMenu ")); |
| 258 | + Serial.println(server.arg("at").c_str()); |
| 259 | + ); |
| 260 | + String at=server.arg("at"); |
| 261 | + bool r; |
| 262 | + r=nav.async(server.hasArg("at")?at.c_str():"/"); |
| 263 | + return r; |
| 264 | +} |
| 265 | + |
| 266 | +//redirect to version folder, |
| 267 | +//this allows agressive caching with no need to cache reset on version change |
| 268 | +auto mainPage= []() { |
| 269 | + _trace(Serial<<"serving main page from root!"<<endl); |
| 270 | + server.sendHeader("Location", CUR_VERSION "/index.html", true); |
| 271 | + server.send ( 302, "text/plain", ""); |
| 272 | + if (server.hasArg("at")) |
| 273 | + nav.async(server.arg("at").c_str()); |
| 274 | +}; |
| 275 | + |
| 276 | +void setup(){ |
| 277 | + Serial.begin(115200); |
| 278 | + while(!Serial); |
| 279 | + |
| 280 | + myservo.attach(ESCPIN); |
| 281 | + |
| 282 | + webNav.canExit=false; |
| 283 | + jsonNav.canExit=false; |
| 284 | + wsNav.canExit=false; |
| 285 | + |
| 286 | + for(uint8_t t = 4; t > 0; t--) { |
| 287 | + Serial.printf("[SETUP] BOOT WAIT %d...\n", t); |
| 288 | + Serial.flush(); |
| 289 | + delay(1000); |
| 290 | + } |
| 291 | + |
| 292 | + Serial.println(); |
| 293 | + Serial.println("Arduino menu webserver example"); |
| 294 | + |
| 295 | + SPIFFS.begin(); |
| 296 | + |
| 297 | + Serial.print("Connecting to "); |
| 298 | + Serial.println(ssid); |
| 299 | + |
| 300 | + WiFi.begin(ssid, password); |
| 301 | + // Wait for connection |
| 302 | + while (WiFi.status() != WL_CONNECTED) { |
| 303 | + delay(500); |
| 304 | + Serial.print("."); |
| 305 | + } |
| 306 | + webSocket.begin(); |
| 307 | + Serial.println(""); |
| 308 | + webSocket.onEvent(webSocketEvent); |
| 309 | + Serial.println("Connected."); |
| 310 | + Serial.print("IP address: "); |
| 311 | + Serial.println(WiFi.localIP()); |
| 312 | + |
| 313 | + webSocket.begin(); |
| 314 | + |
| 315 | + nav.idleTask=idle;//point a function to be used when menu is suspended |
| 316 | + |
| 317 | + server.on("/",HTTP_GET,mainPage); |
| 318 | + |
| 319 | + //menu xml server over http |
| 320 | + server.on("/menu", HTTP_GET, []() { |
| 321 | + pageStart(); |
| 322 | + serverOut<<"<output state=\""<<((int)&webNav.idleTask)<<"\"><![CDATA["; |
| 323 | + _trace(Serial<<"output count"<<webNav.out.cnt<<endl); |
| 324 | + handleMenu(webNav);//do navigation (read input) and produce output messages or reports |
| 325 | + serverOut<<"]]></output>"; |
| 326 | + webNav.doOutput(); |
| 327 | + pageEnd(); |
| 328 | + }); |
| 329 | + |
| 330 | + //menu json server over http |
| 331 | + server.on("/json", HTTP_GET, []() { |
| 332 | + _trace(Serial<<"json request!"<<endl); |
| 333 | + jsonStart(); |
| 334 | + serverOut<<"{\"output\":\""; |
| 335 | + handleMenu(jsonNav); |
| 336 | + serverOut<<"\",\n\"menu\":"; |
| 337 | + jsonNav.doOutput(); |
| 338 | + serverOut<<"\n}"; |
| 339 | + jsonEnd(); |
| 340 | + }); |
| 341 | + |
| 342 | + server.begin(); |
| 343 | + Serial.println("HTTP server started"); |
| 344 | + Serial.println("Serving ArduinoMenu example."); |
| 345 | + #ifdef MENU_DEBUG |
| 346 | + server.serveStatic("/", SPIFFS, "/","max-age=30"); |
| 347 | + #else |
| 348 | + server.serveStatic("/", SPIFFS, "/","max-age=31536000"); |
| 349 | + #endif |
| 350 | +} |
| 351 | + |
| 352 | +void loop(void){ |
| 353 | + wsOut.response.remove(0);//clear websocket json buffer |
| 354 | + webSocket.loop(); |
| 355 | + server.handleClient(); |
| 356 | + delay(1); |
| 357 | +} |
0 commit comments