I am writing my first HE driver and need some help on variable scope.
I want to control my Carrier thermostat through its "System Access Model" (SAM), this uses a simple ASCII protocol through an RS232 port. I have that port connected to a telnet server so I can access it from the hub. I send the thermostat a request through a "sendHubCommand" call and receive the response through parse(), all of that works OK.
The SAM works strictly on a send request/receive response polling basis. You send the request then you must wait for the response or a 5 second timeout, whichever comes first.
I now need to understand how I can pass the data received in the parse method back to the calling method for error checking. I (maybe wrongly) assumed that the hub is running multiple processes at once so the request code will be run if a different process to the parse code. If so simple script variables can't be used pass data from one process to another, correct?
I usually choose to specify a named callback in the parameters Map for the HubAction.
That way, you can have a pair of methods for each operation - one that issues the command and names the other as its callback for the HubAction, and the other which is a specialized handler for that command.
That helps avoid having the parse() method get too unwieldy.
I agree specifying a separate callback for each command could help the error checking but let me dig a little deeper. Having sent the command I need to know if the response was successful.
I think I need a timeout routine in the sender that is cancelled when the callback receives a valid response. In other words if no valid response is received, resend the command. That means I need to send something from the callback to the sender?
You could try using a state variable as a global for this.
For example:
Set state.cmdSuccess = false, then send your command in the caller.
Implement your wait function to check periodically whether state.cmdSuccess is true, or eventually time out. Use it in the caller to support your retry logic.
In parse or your custom callback, set state.cmdSuccess to true if the response indicates that the command succeeded.
I was doing exactly that but using parse as the callback. But I thought the state variables were not "carrying over" from one process to another so I presumed I was doing it wrong. I am travelling now but I think my test (state,variable == "success") was incorrect. Will check it out when I get back.
However I guessed at what state variables are used for by looking at others code. I could not find any Hubitat documentation on how I'm supposed to use them. At the moment I "register" them all in my initialize routine and then treat them just like a script variable, is that correct?
I have had hiccups with state that I couldn't explain, and I just assumed they were related to its implementation and cacheing behaviors or other potential issues that were opaque to me because it is a closed source function that I couldn't review.
When I want to be absolutely sure that data is available and updated across multiple threads, I implement my own thing like this:
and I make setters and getters for members within that. It always works for me, but I have also been advised by Hubitat staff that I shouldn't be so paranoid about this and that they'd like to hear about issues where state doesn't behave as intended. So, YMMV.
@tomw : With those documentation links I think I have found a clue to my issue. In my logs I see the parse() method set the state variable at 03:23:41.963 then the sending methods checks it at 1 second intervals up to 03:23:46.312 but does not see a change and so times out. In other words the sending method has still not seen a change in the state variable 4 seconds after parse() has updated it.
I use pauseExecution(1000) to create my timeout loop and I am wondering if this maybe the cause of the problem? The Hubitat documentation says " state writes data just before the app goes to sleep again". The ST documentation says sate is "A map of name/value pairs that a Device Handler can use to save and retrieve data across executions". Maybe the sending thread only reads the state variable when it starts up after sleep() and pause execution does allow the thread to update the state variable?
Anything is possible, since unfortunately neither of us can read the internal implementation of state to understand how it might behave in various situations.
It was my impression in the past that state is only sampled once at the "top" of a method call and isn't updated continuously throughout, so maybe you're seeing something of that sort. It would be interesting to see if the singleThreaded setting makes any difference. If not, you may have to refactor your method to make individual method calls to do the checking while you're waiting so that you get an updated sample of state. This may be as simple as creating a sub-function to check state instead of doing it directly in the calling method.
Thanks for all the help so far, it looks like state variables will not work here. I can get round most of the issues but I think I still need the calling function to wait for a "Success" signal from the callback and if it doesn't get it within 5 seconds resend the command. Sounds like I should use the following for that:
That may be overkill. You could still do something like this which causes state to be resampled each time it is checked. Here's a demonstration. Note that I had to select the singleThreaded option for it to be reliable.
def trace()
{
// always clear the variable first
setDoneVar(false)
//
// TODO: send your command to the device here
//
// the actual wait for completion happens here
checkDone(4)
// this completion would actually happen later in parse() for a real response
runIn(2, simulateComplete)
}
def setDoneVar(val)
{
state.done = val
}
def checkDoneVar()
{
return state.done
}
def simulateComplete()
{
setDoneVar(true)
}
def checkDone(timeoutSecs)
{
log.debug "${timeoutSecs} time remaining"
if(!timeoutSecs || checkDoneVar())
{
log.debug "state.done = ${state.done}"
return
}
runIn(1, checkDone, [data: --timeoutSecs])
}
@tomw - Many thanks - I have been playing with this code snippet to make sure I understand it!
One of your comments in trace() says "// the actual wait for completion happens here" but I don't think trace does actually wait there. It runs through to completion in ~100msec, leaving checkDone() running and presumably the hub OS to fire off simulateComplete() after two seconds.
Doesn't that mean this code is running multi-threaded and so we still have the same problem of how we communicate success back to the method that sends the command, trace() in this example?
Yes, good point. This synchronizes the calling thread with the success in a separate thread, but it doesn't tell you whether it was a success or a timeout. so it isn't the complete solution to your problem.
I'll have to give it some more thought, but maybe others have ideas on how to pass data back to the caller from a callback like parse().
I swapped out the direct references to state.variable and replaced them with the method calls in your last reply and set the singleThreaded flag to true. I think the single threading forces parse() to run in the same thread as the command sending method. Consequently I see the command sent, this method then waits for the state variable to change (which it doesn't) before the method times out. Then parse() fires and changes the state variable. Bottom line is I don't think you can run this driver as single threaded unless you can force parse() to run in its own thread.
Without the single threaded flag set it behaves as before, parse() fires at the correct time, sets the state variable but the sending method does not see the change.
Could I use another "high level" variable (attribute, hub variable) or a semaphore?
I think we could use some advice from @bravenel or someone else in known from the Hubitat team.
What is the recommended structure and method for sending data from an async callback like parse() back to a synchronous device caller that wants to wait for a response from the device?
Sorry haven’t fully digested this but I think I had similar difficulty in the MQTT app.
I think previous responses mentioned this and it avoids state variables.
If you have a parent app and also a device driver that contains the parse method then you can recover the returned value within parse (and test it if needed) and then return your value instantly to the parent app’s recoveredData method which you include in the app. You can additional parameters too if wanted
parent.recoveredData (value)
If the expected value doesn’t arrive in the expected timeout you can re-call the driver method from the app to try again
This will avoid the use of a state variable which I also found to not be updated quite as I expected and atomicState can’t be used in drivers.
My data was arriving totally asynchronously from the MQTT broker and wasn’t usually related to the last data I sent so this allowed me to test from where the payloads were coming from and include additional parameters for the topic and also choose which app method handled the particular data returned.
@kevin Many thanks for joining the conversation. Let me briefly give you a few more details.
I am writing a driver to communicate with a Carrier thermostat. I use a Carrier System Access Module that works on a very simple RS232 ASCII send command/receive response protocol. The hub driver talks to the RS232 port through a telnet server. The commands are sent through a hubAction method and, if successful, the parse() method fires about 2 seconds later.
The problem is how to get the "success" state back from parse() to the sending method - no response or a bad response after say, 5 seconds, means you should resend the command.
It looks like the recoveredData method does this for an parent app/child driver. Could it be adapted for communication between two methods (ie two threads?) in the same driver?
You could just drop parent. from the method call. recoveredData was just an example method name I used, it is nothing special. You could maybe even call your same sending routine with the returned value or with a ‘retry’ or ‘timeout’ value. Test this value and decide how to progress. Trying to keep state variables in synch (or in scope) between methods just didn’t work for me but the method call does pass the correct value.
I think I also experimented with setting the device drivers attributes using sendEvent and that was more successful (consistent) than state.
There’s bound to be a more elegant solution, my approach as a Groovy newbie was just to find a solution that worked consistently for me and then go with that.