diff --git a/lib/api/GwApi.h b/lib/api/GwApi.h index 5a24e92..9d79ef3 100644 --- a/lib/api/GwApi.h +++ b/lib/api/GwApi.h @@ -6,7 +6,9 @@ #include "GWConfig.h" #include "GwBoatData.h" #include "GwXDRMappings.h" +#include "GwSynchronized.h" #include +#include class GwApi; typedef void (*GwUserTaskFunction)(GwApi *); //API to be used for additional tasks @@ -171,6 +173,20 @@ class GwApi{ virtual void remove(int idx){} virtual TaskInterfaces * taskInterfaces()=0; + /** + * register handler for web URLs + * Please be aware that this handler function will always be called from a separate + * task. So you must ensure proper synchronization! + */ + using HandlerFunction=std::function; + /** + * @param url: the url of that will trigger the handler. + * it will be prefixed with /api/user/ + * taskname is the name that you used in addUserTask + * @param handler: the handler function (see remark above about thread synchronization) + */ + virtual void registerRequestHandler(const String &url,HandlerFunction handler)=0; + /** * only allowed during init methods */ diff --git a/lib/exampletask/GwExampleTask.cpp b/lib/exampletask/GwExampleTask.cpp index 340b785..9b4cbcd 100644 --- a/lib/exampletask/GwExampleTask.cpp +++ b/lib/exampletask/GwExampleTask.cpp @@ -7,6 +7,7 @@ #include #include "N2kMessages.h" #include "GwXdrTypeMappings.h" + /** * INVALID!!! - the next interface declaration will not work * as it is not in the correct header file @@ -144,6 +145,26 @@ String formatValue(GwApi::BoatValue *value){ return String(buffer); } +class ExampleWebData{ + SemaphoreHandle_t lock; + int data=0; + public: + ExampleWebData(){ + lock=xSemaphoreCreateMutex(); + } + ~ExampleWebData(){ + vSemaphoreDelete(lock); + } + void set(int v){ + GWSYNCHRONIZED(&lock); + data=v; + } + int get(){ + GWSYNCHRONIZED(&lock); + return data; + } +}; + void exampleTask(GwApi *api){ GwLog *logger=api->getLogger(); //get some configuration data @@ -172,8 +193,24 @@ void exampleTask(GwApi *api){ LOG_DEBUG(GwLog::LOG,"exampleNotWorking update returned %d",(int)nwrs); String voltageTransducer=api->getConfig()->getString(GwConfigDefinitions::exTransducer); int voltageInstance=api->getConfig()->getInt(GwConfigDefinitions::exInstanceId); + ExampleWebData webData; + /** + * an example web request handler + * it uses a synchronized data structure as it gets called from a different thread + * be aware that you must not block for longer times here! + */ + api->registerRequestHandler("data",[&webData](AsyncWebServerRequest *request){ + int data=webData.get(); + char buffer[30]; + snprintf(buffer,29,"%d",data); + buffer[29]=0; + request->send(200,"text/plain",buffer); + }); + int loopcounter=0; while(true){ delay(1000); + loopcounter++; + webData.set(loopcounter); /* * getting values from the internal data store (boatData) requires some special handling * our tasks runs (potentially) at some time on a different core then the main code diff --git a/lib/exampletask/Readme.md b/lib/exampletask/Readme.md index 965e177..2a7e42e 100644 --- a/lib/exampletask/Readme.md +++ b/lib/exampletask/Readme.md @@ -32,6 +32,26 @@ Files This file allows to add some config definitions that are needed for our task. For the possible options have a look at the global [config.json](../../web/config.json). Be careful not to overwrite config defitions from the global file. A good practice wood be to prefix the names of definitions with parts of the library name. Always put them in a separate category so that they do not interfere with the system ones. The defined config items can later be accessed in the code (see the example in [GwExampleTask.cpp](GwExampleTask.cpp)). + * [index.js](index.js)
+ You can add javascript code that will contribute to the UI of the system. The WebUI provides a small API that allows you to "hook" into some functions to include your own parts of the UI. This includes adding new tabs, modifying/replacing the data display items, modifying the status display or accessing the config items. + For the API refer to [../../web/index.js](../../web/index.js#L2001). + To start interacting just register for some events like api.EVENTS.init. You can check the capabilities you have defined to see if your task is active. + By registering an own formatter [api.addUserFormatter](../../web/index.js#L2054) you can influence the way boat data items are shown. + You can even go for an own display by registering for the event *dataItemCreated* and replace the dom element content with your own html. By additionally having added a user formatter you can now fill your own html with the current value. + By using [api.addTabPage](../../web/index.js#L2046) you can add new tabs that you can populate with your own code. Or you can link to an external URL.
+ Please be aware that your js code is always combined with the code from the core into one js file.
+ For fast testing there is a small python script that allow you to test the UI without always flushing each change. + Just run it with + ``` + tools/testServer.py nnn http://x.x.x.x/api + ``` + with nnn being the local port and x.x.x.x the address of a running system. Open `http://localhost:nnn` in your browser.
+ After a change just start the compilation and reload the page. + + * [index.css](index.css)
+ You can add own css to influence the styling of the display. + + Interfaces ---------- The task init function and the task function interact with the core using an [API](../api/GwApi.h) that they get when started. diff --git a/lib/exampletask/index.js b/lib/exampletask/index.js index da098e0..155991c 100644 --- a/lib/exampletask/index.js +++ b/lib/exampletask/index.js @@ -28,6 +28,21 @@ //you can use the helper addEl to create elements let page=api.addTabPage(tabName,"Example"); api.addEl('div','hdg',page,"this is a test tab"); + let vrow=api.addEl('div','row',page); + api.addEl('span','label',vrow,'loops: '); + let lcount=api.addEl('span','value',vrow,'0'); + //query the loop count + window.setInterval(()=>{ + fetch('/api/user/exampleTask/data') + .then((res)=>{ + if (! res.ok) throw Error("server error: "+res.status); + return res.text(); + }) + .then((txt)=>{ + lcount.textContent=txt; + }) + .catch((e)=>console.log("rq:",e)); + },1000); api.addEl('button','',page,'Info').addEventListener('click',function(ev){ window.open(infoUrl,'info'); }) diff --git a/lib/gwwebserver/GwWebServer.h b/lib/gwwebserver/GwWebServer.h index 84c265a..c2b48d8 100644 --- a/lib/gwwebserver/GwWebServer.h +++ b/lib/gwwebserver/GwWebServer.h @@ -4,6 +4,7 @@ #include #include "GwMessage.h" #include "GwLog.h" +#include "GwApi.h" class GwWebServer{ private: AsyncWebServer *server; @@ -11,7 +12,7 @@ class GwWebServer{ GwLog *logger; public: typedef GwRequestMessage *(RequestCreator)(AsyncWebServerRequest *request); - using HandlerFunction=std::function; + using HandlerFunction=GwApi::HandlerFunction; GwWebServer(GwLog *logger, GwRequestQueue *queue,int port); ~GwWebServer(); void begin(); diff --git a/lib/queue/GwSynchronized.h b/lib/queue/GwSynchronized.h index 53241db..786b5f0 100644 --- a/lib/queue/GwSynchronized.h +++ b/lib/queue/GwSynchronized.h @@ -7,10 +7,10 @@ class GwSynchronized{ public: GwSynchronized(SemaphoreHandle_t *locker){ this->locker=locker; - xSemaphoreTake(*locker, portMAX_DELAY); + if (locker != nullptr) xSemaphoreTake(*locker, portMAX_DELAY); } ~GwSynchronized(){ - xSemaphoreGive(*locker); + if (locker != nullptr) xSemaphoreGive(*locker); } }; diff --git a/lib/usercode/GwUserCode.cpp b/lib/usercode/GwUserCode.cpp index 4522c92..211eda9 100644 --- a/lib/usercode/GwUserCode.cpp +++ b/lib/usercode/GwUserCode.cpp @@ -191,6 +191,7 @@ class TaskApi : public GwApiInternal SemaphoreHandle_t *mainLock; SemaphoreHandle_t localLock; std::map> counter; + std::map webHandlers; String name; bool counterUsed=false; int counterIdx=0; @@ -315,6 +316,10 @@ public: virtual bool addXdrMapping(const GwXDRMappingDef &def){ return api->addXdrMapping(def); } + virtual void registerRequestHandler(const String &url,HandlerFunction handler){ + GWSYNCHRONIZED(&localLock); + webHandlers[url]=handler; + } virtual void addCapability(const String &name, const String &value){ if (! isInit) return; userCapabilities[name]=value; @@ -335,6 +340,16 @@ public: virtual void setCalibrationValue(const String &name, double value){ api->setCalibrationValue(name,value); } + virtual bool handleWebRequest(const String &url,AsyncWebServerRequest *req){ + GWSYNCHRONIZED(&localLock); + auto it=webHandlers.find(url); + if (it == webHandlers.end()){ + api->getLogger()->logDebug(GwLog::LOG,"no web handler task=%s url=%s",name.c_str(),url.c_str()); + return false; + } + it->second(req); + return true; + } }; @@ -404,4 +419,19 @@ int GwUserCode::getJsonSize(){ } } return rt; +} +void GwUserCode::handleWebRequest(const String &url,AsyncWebServerRequest *req){ + int sep1=url.indexOf('/'); + String tname; + if (sep1 > 0){ + tname=url.substring(0,sep1); + for (auto &&it:userTasks){ + if (it.api && it.name == tname){ + if (it.api->handleWebRequest(url.substring(sep1+1),req)) return; + break; + } + } + } + LOG_DEBUG(GwLog::DEBUG,"no task found for web request %s[%s]",url.c_str(),tname.c_str()); + req->send(404, "text/plain", "not found"); } \ No newline at end of file diff --git a/lib/usercode/GwUserCode.h b/lib/usercode/GwUserCode.h index a218bc9..94e745d 100644 --- a/lib/usercode/GwUserCode.h +++ b/lib/usercode/GwUserCode.h @@ -11,6 +11,7 @@ class GwApiInternal : public GwApi{ ~GwApiInternal(){} virtual void fillStatus(GwJsonDocument &status){}; virtual int getJsonSize(){return 0;}; + virtual bool handleWebRequest(const String &url,AsyncWebServerRequest *req){return false;} }; class GwUserTask{ public: @@ -50,5 +51,6 @@ class GwUserCode{ Capabilities *getCapabilities(); void fillStatus(GwJsonDocument &status); int getJsonSize(); + void handleWebRequest(const String &url,AsyncWebServerRequest *); }; #endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 90e5691..ae8f3d5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -348,6 +348,8 @@ public: } return xdrMappings.addFixedMapping(mapping); } + virtual void registerRequestHandler(const String &url,HandlerFunction handler){ + } virtual void addCapability(const String &name, const String &value){} virtual bool addUserTask(GwUserTaskFunction task,const String Name, int stackSize=2000){ return false; @@ -768,6 +770,7 @@ void loopFunction(void *){ //delay(1); } } +const String USERPREFIX="/api/user/"; void setup() { mainLock=xSemaphoreCreateMutex(); uint8_t chipid[6]; @@ -845,7 +848,12 @@ void setup() { snprintf(buffer,29,"%g",value); buffer[29]=0; request->send(200,"text/plain",buffer); - }); + }); + webserver.registerHandler((USERPREFIX+"*").c_str(),[&USERPREFIX](AsyncWebServerRequest *req){ + String turl=req->url().substring(USERPREFIX.length()); + logger.logDebug(GwLog::DEBUG,"user web request for %s",turl.c_str()); + userCodeHandler.handleWebRequest(turl,req); + }); webserver.begin(); xdrMappings.begin();