As I wrote I am planning to add a push service, that is to collect responses in defined intervals and send those out via UDP. I am considering some design details I would like to get your opinion.
All your comments are welcome indeed!
Structure
The push service will be attached to a ModbusServer - regardless which type it is. It will use the server's localRequest()
interface to gather data from the local server, or, if the server in fact is a ModbusBridge, from all servers attached to the bridge as well.
It will use one single UDP client (WiFi or Ethernet) to push, as no connections have to be maintained.
Pushes will be organized in "jobs", that have a target host/port, a run interval and a number of repetitions as attributes, plus a unique token to identify the job. Each job has a list of requests whose responses are collected to be pushed as one packet.
A push packet has the job token, the repetition count and the current time, a list of response lengths and the responses proper as data. Thus a recipient can tell the order, reference time etc. of the push packet received and f.i. cope with packet losses with this information.
The size of a push packet is determined when defining the job - but must not be larger than the MTU size minus UDP header data. A response not fitting any more in the push packet will have a response length of 0 in the table.
We will not know in advance the size of all the responses in a job, if we do not restrict the function codes. I do not know yet if that is sensible, but I tend to think so - on one hand I cannot see what sense a repeated WRITE_HOLD_REGISTER with necessarily constant values may make, but on the other hand we do not know what a user-defined FC will do at all, that makes pretty sense when called again and again.
Two tasks will be run when the push service is started, one maintaining the jobs list and doing the UDP pushes (the "pusher" task), another gathering the responses for requests that are due (the "requester" task).
Job request
The smallest unit of a push "job" is a single request. It has a serverID, functionCode, data and len data set, that exactly fits the signature of the localRequest()
call. The response is a part of the job request, as it may be used multiply (described below).
A request has a "due" flag, protected by mutex, that is used to signal a new response is required. The response is protected by the mutex as well to prevent a job reading the response data while it is refreshed.
A request may be used in more than one job; if the identical request is required a second time, it will have its "user count" incremented. If a job is deleted, the requests used by it are either deleted as well (if only used by the one job) or the count decremented.
To avoid unnecessary duplicate request execution, a request will have to be called with the shortest interval of the jobs using it. In addition, if a job is requiring the response, no new request will be started, if the last response is not older than some definable latency time, but the last response will be used.
To give you an example: assume three jobs J3, J5 and J10, running with intervals of 3, 5 and 10 seconds, respectively, that all will need some request R. The latency shall be 2 seconds.
When starting the job, R is run once for J3. J5 and J10 will use the response, as time has not advanced yet.
At second 3, R is run again for J3, since the last call is 3 seconds ago - longer than the 2 seconds latency.
At second 5 the request is not run for J5, as the last response is 2 seconds old only.
Second 9 sees another run for J3, but in second 10 J10 will get the same response again.
Second 12 R is run for J3, second 15 again, but not for J5 etc.
In effect the job with the shortest interval will have the most actual data, whereas those with longer intervals will have data outdated up to the latency time as a maximum. In return you will save quite a number of requests.
Pusher task
The pusher task will loop over the jobs list to see if one is due. If so, it will take the currently held responses of the requests for the job and build up the push packet. This way the jobs will have a push almost at the time their interval mandates, for the cost of slightly outdated data (as described above)
The pusher task will also set requests to 'due' if the responses need to be refreshed.
Requester task
The requester task will loop over the requests to see if one is marked "due", then call ``localRequest()```to get a fresh response. The response will be swapped with the old one upon completion. This swap and the reset of the "due" flag is protected by the mutex.
Main task
The push service instance will create new jobs, add requests to it, start and stop jobs and delete finished jobs with their further unused requests. The whole service can be started and stopped.
Requests will only be accepted if they are addressing one of the serverIDs served by the ModbusServer/ModbusBridge the push service is associated to.
Jobs options
- A common application will be a device that, once configured, will have just a few fixed push jobs throughout its lifetime. It would be stupid to have to recompile the firmware every time a job may have to be modified, so it could be handy to have a job definition as data. This job definition could be saved in a SPIFFS file to be loaded at startup, or be received over air.
- My bridge has a special "local" job that is used to store preset requests. These requests are used to continuously display current values of attached servers on the tiny OLED screen. One can modify the requests on the device and store the changes.
- A dedicated user function code is used to start new jobs, transporting the job definitions in the request data. For security reasons I restricted the jobs to a defined repeat count (no unlimited repeats), the interval to at least 2 seconds and to only have the sender of the request as the target for the push packets.