Polling vs. Callback

Polling vs. Callback

The Yoctopuce API provides several ways to communicate with Yoctopuce modules: polling, value change callbacks, and periodic callbacks. We mentioned them all from time to time in this blog, but we never took time, until now, to present in details these different techniques. So let's have look :-)




Polling

Polling is the easiest and most intuitive way. It consists in explicitly interrogating the module and in waiting for the answer. Here is a basic example written in Python using the YSensor class to query a Yocto-Temperature with the "TMPSENS1-00BDA" serial number.


import sys
from yocto_api import *

# Sets up the API to use local USB devices
errmsg=YRefParam()
if YAPI.RegisterHub("usb", errmsg)!= YAPI.SUCCESS: sys.exit(errmsg.value)

# Finds the temperature sensor named TMPSENS1-00BDA.temperature
sensor=YSensor.FindSensor("TMPSENS1-00BDA.temperature")
if not sensor.isOnline() : sys.exit("device not found")

print(sensor.get_currentValue()+" "+sensor.get_unit())
 



You need only 4 simple function calls to read a sensor:

  1. One call to initialize the API and to specify if we are going to work with USB or network sensors
  2. One call to find the sensor named TMPSENS1-00BDA.temperature
  3. One call, optional but recommended, to check that the sensor is connected
  4. One call to retrieve the sensor value


However, this ease hides an efficiency issue: Let's imagine that you have a job to do and that your manager calls you regularly on the phone to check where you are at. If there is one call a day, it's annoying, but you can manage it. But if the manager starts calling every three minutes (I barely exaggerate), you are going to spend more time on the phone than actually working. Moreover, this won't improve the manager's own productivity. In short, it's certainly more sensible to you to take the initiative to call your manager once in a while to inform him or her of the significant advances of your work. The same reasoning applies to Yoctopuce modules.

Value change callbacks

You can ask the Yoctopuce API to call one of your functions each time a sensor value changes significantly. The code is slightly longer, but much more efficient:


import sys
from yocto_api import *

def MyValueCallback(sensor,value):
    print(value+" "+sensor.get_unit())

# Sets up the API to use local USB devices
errmsg=YRefParam()
if YAPI.RegisterHub("usb", errmsg)!= YAPI.SUCCESS: sys.exit(errmsg.value)

# Finds the temperature sensor named TMPSENS1-00BDA.temperature
sensor=YSensor.FindSensor("TMPSENS1-00BDA.temperature")

# Registers a callback which  is called each time the sensor value changes
sensor.registerValueCallback(MyValueCallback)

while True:
    YAPI.Sleep(1000)
 



We defined a MyValueCallback function and we asked the API to automatically call it each time the sensor value changes significantly. To avoid unpleasant surprises, callback functions are not called anytime, but only when executing YAPI.Sleep() and YAPI.HandleEvents() functions. You must therefore call one of these functions regularly.

If you don't want to clutter your code with calls to YAPI.Sleep() or to YAPI.HandleEvents(), you can very well make this call from a parallel thread. But be careful of race conditions and other deadlocks.



import sys
from threading import *
from yocto_api import *

def YoctoMonitor():
  while True:
      YAPI.Sleep(1000)

def MyValueCallback(fct,value):
    print(value+" "+fct.get_unit())

# Sets up the API to use local USB devices
errmsg=YRefParam()
if YAPI.RegisterHub("usb", errmsg)!= YAPI.SUCCESS: sys.exit(errmsg.value)

# Finds the temperature sensor named TMPSENS1-00BDA.temperature
sensor=YSensor.FindSensor("TMPSENS1-00BDA.temperature")

# Registers a callback which  be called each the sensor value changes
sensor.registerValueCallback(MyValueCallback)

# Starts a thread to automatically handle Yoctopuce devices
t = Thread(target=YoctoMonitor)
t.start()

# Does nothing at all
while True: pass
 



Note that callbacks are not limited to sensors. You can very well put a callback on an actuator. For example, you can be warned each time a relay changes state thanks to a similar callback.

Managing Plug and play

Callback programming is also very interesting to manage the list of connected modules. As Yoctopuce modules are USB devices, it's very likely that they'll be connected and disconnected several times while your application is running. You naturally could perform an exhaustive enumeration at regular intervals. But it's much easier to keep an up-to-date module list with callbacks.


import sys
from yocto_api import *

devicelist = []

def deviceArrival(m):
  devicelist.append(m)
  print("new device: "+m.get_serialNumber()+" / "+str(len(devicelist))+' device(s) connected')

def deviceRemoval(m):
  devicelist.remove(m)
  print("device left: "+m.get_serialNumber()+" / "+str(len(devicelist))+' device(s) connected')

# Sets up the API to use local USB devices
errmsg=YRefParam()
if YAPI.RegisterHub("usb", errmsg)!= YAPI.SUCCESS: sys.exit(errmsg.value)

# deviceArrival and deviceRemoval are called each time a device
# is plugged in or removed.
YAPI.RegisterDeviceArrivalCallback(deviceArrival)
YAPI.RegisterDeviceRemovalCallback(deviceRemoval)

while True:
    YAPI.UpdateDeviceList()
    YAPI.Sleep(500)
 



arrival and removal callbacks are called only during the call to UpdateDeviceList . If this function is easy to call, it internally triggers a relatively heavy enumeration process. So, it's not recommended to call it in a loop hundreds of time per second.

Periodic callbacks

Using periodic callbacks is the third technique available with the Yoctopuce API. Functionally, it is half-way between polling and normal callbacks: it allows you to call the callback at a regular interval even if the sensor value hasn't changed. Setting up a periodic callback is done almost in the same way as for normal callbacks, with two differences:

  • You must not forget to indicate the rhythm at which the callback must be called. Use the set_reportFrequency function to do so. The parameter is a character string in the form of number of calls / time unit, where the time unit can be the second (s), the minute (m), or the hour (h).
  • Instead of directly receiving a value, the callbacks receive a YMeasure object enabling you to know not only the average value but also the maximum value, minimum value, and the start and end times of the interval.



import sys,datetime
from yocto_api import *

def MyTimedCallback(fct,measure):
    start = measure.get_startTimeUTC()
    now = datetime.datetime.fromtimestamp(start).strftime('%Y-%m-%d %H:%M:%S')
    print( now+' : ' + str(measure.get_averageValue())+" "+fct.get_unit())

# Sets up the API to use local USB devices
errmsg=YRefParam()
if YAPI.RegisterHub("usb", errmsg)!= YAPI.SUCCESS: sys.exit(errmsg.value)

# Finds the temperature sensor named TMPSENS1-00BDA.temperature
sensor=YSensor.FindSensor("TMPSENS1-00BDA.temperature")

# Registers a callback which  is called once per second
sensor.set_reportFrequency("1/s")
sensor.registerTimedReportCallback(MyTimedCallback)

while True:
    YAPI.Sleep(1000)
 



If the time unit given when calling set_reportFrequency is the second (from one to a hundred measures per second), the returned value is not an averaged value but an instant value. Measures are averaged when frequency is specified in measures per minute or per hour. You can obtain up to 60 averaged measures per minute by defining the callback frequency as "60/m" instead of "1/s".

Note that it is useless to set up periodic callbacks with a frequency higher than the refresh frequency of the corresponding sensor. For example, in the case of our Yocto-Temperature, this frequency is of 10Hz. Setting up a periodic callback running faster than 10/s serves no purpose except wasting bandwidth.

To conclude

One of the interesting side effects of callback programming is that you don't necessarily need to explicitly manage the presence of modules. If you set up a callback but that the corresponding module is not there yet/anymore, your callback is simply not called. If the module comes back, the callback starts again... easy as pie.

A second side effect is higher robustness to the loss of USB packets happening sometimes on an overloaded USB bus, or with headless computers which have a somewhat borderline USB support. Indeed, in polling communication (query/answer), the unplanned loss of a packet necessarily triggers a timeout. In the opposite, when transmitting values by callback (asynchronous notification), the loss of a packet is limited to the loss of a measure but doesn't trigger a significant timeout.

Here you are, we talked about everything, you now know everything you need to know on the several ways to query a Yoctopuce module. All the examples of this post are in Python, but obviously these techniques are available in all the programming languages. Have fun!

Add a comment 2 comments
Back to blog



1 - schling Saturday,november 22,2014 12H31

Hi,

A comment/question regarding value callback. This was my first route to detect significant changes without polling (a YoctoVolt checking the voltage of a backup battery).

In order not to get too frequent callbacks I set the resolution to 0.1 Volts (set_resolution). Unfortunately, the battery sits just around 13.55 Volts, which means that through rounding, it oscillates between 13.5 and 13.6, triggering constant callbacks (i.e. the voltage is 13.548, then it's 13.553, etc).

Don't know whether there is a solution for this. If not, maybe -for value callback- one could set an "averaging period" to avoid callbacks from values oscillating around the resolution "rounding trigger".

Currently, I use timed callback instead of polling the module from my code.

Thanks,

Robert

2 - mvuilleu (Yocto-Team)Saturday,november 22,2014 12H38

@schling: timed reports are indeed the best solution, and they can do the averaging. We cannot change the behaviour of the value change callbacks by adding averaging, as it would have an high risk of impacting customers who need an immediate callback when the value changes even a bit.

Yoctopuce, get your stuff connected.