Skip to content

Commit

Permalink
add registerRequestHandler to the API with examples
Browse files Browse the repository at this point in the history
  • Loading branch information
andreas committed Nov 14, 2024
1 parent 506dd7e commit 538f643
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 4 deletions.
16 changes: 16 additions & 0 deletions lib/api/GwApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
#include "GWConfig.h"
#include "GwBoatData.h"
#include "GwXDRMappings.h"
#include "GwSynchronized.h"
#include <map>
#include <ESPAsyncWebServer.h>
class GwApi;
typedef void (*GwUserTaskFunction)(GwApi *);
//API to be used for additional tasks
Expand Down Expand Up @@ -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<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
*/
Expand Down
37 changes: 37 additions & 0 deletions lib/exampletask/GwExampleTask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <vector>
#include "N2kMessages.h"
#include "GwXdrTypeMappings.h"

/**
* INVALID!!! - the next interface declaration will not work
* as it is not in the correct header file
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions lib/exampletask/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)<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
----------
The task init function and the task function interact with the core using an [API](../api/GwApi.h) that they get when started.
Expand Down
15 changes: 15 additions & 0 deletions lib/exampletask/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
})
Expand Down
3 changes: 2 additions & 1 deletion lib/gwwebserver/GwWebServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
#include <functional>
#include "GwMessage.h"
#include "GwLog.h"
#include "GwApi.h"
class GwWebServer{
private:
AsyncWebServer *server;
GwRequestQueue *queue;
GwLog *logger;
public:
typedef GwRequestMessage *(RequestCreator)(AsyncWebServerRequest *request);
using HandlerFunction=std::function<void(AsyncWebServerRequest *)>;
using HandlerFunction=GwApi::HandlerFunction;
GwWebServer(GwLog *logger, GwRequestQueue *queue,int port);
~GwWebServer();
void begin();
Expand Down
4 changes: 2 additions & 2 deletions lib/queue/GwSynchronized.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};

Expand Down
30 changes: 30 additions & 0 deletions lib/usercode/GwUserCode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ class TaskApi : public GwApiInternal
SemaphoreHandle_t *mainLock;
SemaphoreHandle_t localLock;
std::map<int,GwCounter<String>> counter;
std::map<String,GwApi::HandlerFunction> webHandlers;
String name;
bool counterUsed=false;
int counterIdx=0;
Expand Down Expand Up @@ -315,6 +316,10 @@ class TaskApi : public GwApiInternal
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;
Expand All @@ -335,6 +340,16 @@ class TaskApi : public GwApiInternal
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;
}

};

Expand Down Expand Up @@ -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");
}
2 changes: 2 additions & 0 deletions lib/usercode/GwUserCode.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -50,5 +51,6 @@ class GwUserCode{
Capabilities *getCapabilities();
void fillStatus(GwJsonDocument &status);
int getJsonSize();
void handleWebRequest(const String &url,AsyncWebServerRequest *);
};
#endif
10 changes: 9 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ class ApiImpl : public GwApiInternal
}
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;
Expand Down Expand Up @@ -768,6 +770,7 @@ void loopFunction(void *){
//delay(1);
}
}
const String USERPREFIX="/api/user/";
void setup() {
mainLock=xSemaphoreCreateMutex();
uint8_t chipid[6];
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 538f643

Please sign in to comment.