add registerRequestHandler to the API with examples

This commit is contained in:
andreas 2024-11-14 18:15:12 +01:00
parent 506dd7ea9f
commit 538f643fbf
9 changed files with 133 additions and 4 deletions

View File

@ -6,7 +6,9 @@
#include "GWConfig.h" #include "GWConfig.h"
#include "GwBoatData.h" #include "GwBoatData.h"
#include "GwXDRMappings.h" #include "GwXDRMappings.h"
#include "GwSynchronized.h"
#include <map> #include <map>
#include <ESPAsyncWebServer.h>
class GwApi; class GwApi;
typedef void (*GwUserTaskFunction)(GwApi *); typedef void (*GwUserTaskFunction)(GwApi *);
//API to be used for additional tasks //API to be used for additional tasks
@ -171,6 +173,20 @@ class GwApi{
virtual void remove(int idx){} virtual void remove(int idx){}
virtual TaskInterfaces * taskInterfaces()=0; 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<void(AsyncWebServerRequest *)>;
/**
* @param url: the url of that will trigger the handler.
* it will be prefixed with /api/user/<taskname>
* 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 * only allowed during init methods
*/ */

View File

@ -7,6 +7,7 @@
#include <vector> #include <vector>
#include "N2kMessages.h" #include "N2kMessages.h"
#include "GwXdrTypeMappings.h" #include "GwXdrTypeMappings.h"
/** /**
* INVALID!!! - the next interface declaration will not work * INVALID!!! - the next interface declaration will not work
* as it is not in the correct header file * as it is not in the correct header file
@ -144,6 +145,26 @@ String formatValue(GwApi::BoatValue *value){
return String(buffer); 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){ void exampleTask(GwApi *api){
GwLog *logger=api->getLogger(); GwLog *logger=api->getLogger();
//get some configuration data //get some configuration data
@ -172,8 +193,24 @@ void exampleTask(GwApi *api){
LOG_DEBUG(GwLog::LOG,"exampleNotWorking update returned %d",(int)nwrs); LOG_DEBUG(GwLog::LOG,"exampleNotWorking update returned %d",(int)nwrs);
String voltageTransducer=api->getConfig()->getString(GwConfigDefinitions::exTransducer); String voltageTransducer=api->getConfig()->getString(GwConfigDefinitions::exTransducer);
int voltageInstance=api->getConfig()->getInt(GwConfigDefinitions::exInstanceId); 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){ while(true){
delay(1000); delay(1000);
loopcounter++;
webData.set(loopcounter);
/* /*
* getting values from the internal data store (boatData) requires some special handling * 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 * our tasks runs (potentially) at some time on a different core then the main code

View File

@ -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. 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)). The defined config items can later be accessed in the code (see the example in [GwExampleTask.cpp](GwExampleTask.cpp)).
* [index.js](index.js)<br>
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.<br>
Please be aware that your js code is always combined with the code from the core into one js file.<br>
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.<br>
After a change just start the compilation and reload the page.
* [index.css](index.css)<br>
You can add own css to influence the styling of the display.
Interfaces Interfaces
---------- ----------
The task init function and the task function interact with the core using an [API](../api/GwApi.h) that they get when started. The task init function and the task function interact with the core using an [API](../api/GwApi.h) that they get when started.

View File

@ -28,6 +28,21 @@
//you can use the helper addEl to create elements //you can use the helper addEl to create elements
let page=api.addTabPage(tabName,"Example"); let page=api.addTabPage(tabName,"Example");
api.addEl('div','hdg',page,"this is a test tab"); 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){ api.addEl('button','',page,'Info').addEventListener('click',function(ev){
window.open(infoUrl,'info'); window.open(infoUrl,'info');
}) })

View File

@ -4,6 +4,7 @@
#include <functional> #include <functional>
#include "GwMessage.h" #include "GwMessage.h"
#include "GwLog.h" #include "GwLog.h"
#include "GwApi.h"
class GwWebServer{ class GwWebServer{
private: private:
AsyncWebServer *server; AsyncWebServer *server;
@ -11,7 +12,7 @@ class GwWebServer{
GwLog *logger; GwLog *logger;
public: public:
typedef GwRequestMessage *(RequestCreator)(AsyncWebServerRequest *request); typedef GwRequestMessage *(RequestCreator)(AsyncWebServerRequest *request);
using HandlerFunction=std::function<void(AsyncWebServerRequest *)>; using HandlerFunction=GwApi::HandlerFunction;
GwWebServer(GwLog *logger, GwRequestQueue *queue,int port); GwWebServer(GwLog *logger, GwRequestQueue *queue,int port);
~GwWebServer(); ~GwWebServer();
void begin(); void begin();

View File

@ -7,10 +7,10 @@ class GwSynchronized{
public: public:
GwSynchronized(SemaphoreHandle_t *locker){ GwSynchronized(SemaphoreHandle_t *locker){
this->locker=locker; this->locker=locker;
xSemaphoreTake(*locker, portMAX_DELAY); if (locker != nullptr) xSemaphoreTake(*locker, portMAX_DELAY);
} }
~GwSynchronized(){ ~GwSynchronized(){
xSemaphoreGive(*locker); if (locker != nullptr) xSemaphoreGive(*locker);
} }
}; };

View File

@ -191,6 +191,7 @@ class TaskApi : public GwApiInternal
SemaphoreHandle_t *mainLock; SemaphoreHandle_t *mainLock;
SemaphoreHandle_t localLock; SemaphoreHandle_t localLock;
std::map<int,GwCounter<String>> counter; std::map<int,GwCounter<String>> counter;
std::map<String,GwApi::HandlerFunction> webHandlers;
String name; String name;
bool counterUsed=false; bool counterUsed=false;
int counterIdx=0; int counterIdx=0;
@ -315,6 +316,10 @@ public:
virtual bool addXdrMapping(const GwXDRMappingDef &def){ virtual bool addXdrMapping(const GwXDRMappingDef &def){
return api->addXdrMapping(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){ virtual void addCapability(const String &name, const String &value){
if (! isInit) return; if (! isInit) return;
userCapabilities[name]=value; userCapabilities[name]=value;
@ -335,6 +340,16 @@ public:
virtual void setCalibrationValue(const String &name, double value){ virtual void setCalibrationValue(const String &name, double value){
api->setCalibrationValue(name,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;
}
}; };
@ -405,3 +420,18 @@ int GwUserCode::getJsonSize(){
} }
return rt; 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");
}

View File

@ -11,6 +11,7 @@ class GwApiInternal : public GwApi{
~GwApiInternal(){} ~GwApiInternal(){}
virtual void fillStatus(GwJsonDocument &status){}; virtual void fillStatus(GwJsonDocument &status){};
virtual int getJsonSize(){return 0;}; virtual int getJsonSize(){return 0;};
virtual bool handleWebRequest(const String &url,AsyncWebServerRequest *req){return false;}
}; };
class GwUserTask{ class GwUserTask{
public: public:
@ -50,5 +51,6 @@ class GwUserCode{
Capabilities *getCapabilities(); Capabilities *getCapabilities();
void fillStatus(GwJsonDocument &status); void fillStatus(GwJsonDocument &status);
int getJsonSize(); int getJsonSize();
void handleWebRequest(const String &url,AsyncWebServerRequest *);
}; };
#endif #endif

View File

@ -348,6 +348,8 @@ public:
} }
return xdrMappings.addFixedMapping(mapping); return xdrMappings.addFixedMapping(mapping);
} }
virtual void registerRequestHandler(const String &url,HandlerFunction handler){
}
virtual void addCapability(const String &name, const String &value){} virtual void addCapability(const String &name, const String &value){}
virtual bool addUserTask(GwUserTaskFunction task,const String Name, int stackSize=2000){ virtual bool addUserTask(GwUserTaskFunction task,const String Name, int stackSize=2000){
return false; return false;
@ -768,6 +770,7 @@ void loopFunction(void *){
//delay(1); //delay(1);
} }
} }
const String USERPREFIX="/api/user/";
void setup() { void setup() {
mainLock=xSemaphoreCreateMutex(); mainLock=xSemaphoreCreateMutex();
uint8_t chipid[6]; uint8_t chipid[6];
@ -846,6 +849,11 @@ void setup() {
buffer[29]=0; buffer[29]=0;
request->send(200,"text/plain",buffer); 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(); webserver.begin();
xdrMappings.begin(); xdrMappings.begin();