How to distinguish timers when running multiple modal operators?

Hello,

In my framework I run two modal operators at the same time.

The first is, let’s say, a background cycle performing some checks and printing info. It runs at relatively low frequency of 5 frames per second (delay 0.2 secs)

The second is a real modal operator, dealing with user interaction. It is also instantiatin an operator, with delay 0.04 secs (25 fps)

The problem is that both of them receive the ‘TIMER’ event from both the own timer and the other one. Resulting in a inconstant and higher frequency of calls of the modal(self, context, event) method.
if(event==‘TIMER’):
# do my stuff… but it happens too frequently!

THE QUESTION:
Is there any way to recognise, within the modal() method, which timer instance triggered the ‘TIMER’ event?

Thanks a lot,
Fabrizio Nunnari

It’s impossible, there’s no information given in the event-object about the timer. It should really provide a reference to the timer, so we could compare to self._timer. I’m not sure if it’s easy to do in the C code, will check on it.

For the time being, you could use a single timer and keep track of number of timer events, like

class ModalTimerOperator(bpy.types.Operator):
    # ...

    _timer = None
    _timer_count = 0

# ...

if event.type == 'TIMER':
    self._timer_count += 1

    if self._timer_count % 5 == 0:
        # do background cycle

    # do user interaction

Dugg in the code and this is where the event is set:

                    event.type = wt->event_type;
                    event.val = 0;
                    event.keymodifier = 0;
                    event.custom = EVT_DATA_TIMER;
                    event.customdata = wt;
                    wm_event_add(win, &event);

The timer object (wt) is customdata, which isn’t ever exposed to python AFAIK.

Thanx codemanx,

Yep. I can confirm that I don’t see the .customdata attribute from python.

If I can suggest, taking inspiration from the Java AWT framework, I would add to each event a .source attribute, which holds a reference to the “Object” that triggered the event.
So in Blender I would do something like:

class ModalTimerOperator(bpy.types.Operator):
    # ...

    _timer = None

    # ...

def modal(....):
    if event.type == 'TIMER':

        if event.source == self._timer:
            # do my stuff

best,
Fabrizio

I have two modal operators running on TIMER events, too, but in my case they run with the same delay (.008 seconds, or about 120Hz), and I let the user choose whether to run one or the other or both. Since the operators can run independently, both need to have the power to start a timer, but if one’s already running then a second shouldn’t be created since that just clogs up the pipeline and brings the Blender UI from buttery-smooth to extremely-glitchy. Is there some way to recognize if an event timer is already running within one of the initialization routines of a modal operator?

store the timer object as a static class variable, you can then test for it even from outside that class. Make sure to set it to None when you remove the timer.

How do you refer to a class from outside of it? I tried defining a stripped-down class of the sample modal operator class like this:

class ModalTimerOperator(bpy.types.Operator):
    """Useful for multiple modal operators all utilizing Timers"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"


    _timer = None


    def modal(self, context, event):
        return {'PASS_THROUGH'}


    def execute(self, context):
        if self._timer == None:
            self._timer = context.window_manager.event_timer_add(0.008, context.window)
            context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}


    def cancel(self, context):
        context.window_manager.event_timer_remove(self._timer)
        return {'CANCELLED'}

However, I seem to be unable to refer to it with bpy.ops.wm.modal_timer_operator._timer or anything like that. I’m probably missing something :\ Do I need to define all my classes within the same script file, and then refer to the class directly, avoiding the blender interface?

bpy.ops is the interface for calling operators only, you can’t access the operator instance, it’s class variables or attributes or anything this way. You need to use the class name (standard python). Inside class methods, you can either use the explicit name, or self.class to refer to the class instead of the instance (=self).

In the below example, I use both the explicit and the self.class way inside the class methods.

Whether something is a class variable or an attribute (=instance variable) is automatically determined by python and fully depends on how you use it:

class ClassName:
    var = 123

var could be a class variable, as well as an attribute. If you access it via the class name (ClassName.var), then it will be a class variable, if you refer to it inside a class method via self.var, then it’s an attribute.

It’s different with class / static methods in classes, they require explicit declaration:

@classmethod
def func(cls):
    ...

@staticmethod
def func():
    ...
import bpy


class ModalTimerOperator(bpy.types.Operator):
    """Useful for multiple modal operators all utilizing Timers"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Timer 1"

    _timer = None
    _count = 0

    def modal(self, context, event):
        if self.__class__._timer is None:
            return {'CANCELLED'}

        if event.type == 'TIMER':
            ModalTimerOperator._count += 1
            redraw_region(context, 'PROPERTIES')
        return {'PASS_THROUGH'}


    def invoke(self, context, event):
        if self.__class__._timer is None:
            if ModalTimerOperator2._timer is not None:
                bpy.ops.wm.modal_timer_operator_2('INVOKE_DEFAULT')
            self.__class__._timer = context.window_manager.event_timer_add(0.008, context.window)
            context.window_manager.modal_handler_add(self)
        else:
            return self.cancel(context)
        return {'RUNNING_MODAL'}


    def cancel(self, context):
        context.window_manager.event_timer_remove(self.__class__._timer)
        self.__class__._timer = None
        ModalTimerOperator._count = 0
        return {'CANCELLED'}
    

class ModalTimerOperator2(bpy.types.Operator):
    """Useful for multiple modal operators all utilizing Timers"""
    bl_idname = "wm.modal_timer_operator_2"
    bl_label = "Timer 2"

    _timer = None
    _count = 0

    def modal(self, context, event):
        if self.__class__._timer is None:
            return {'CANCELLED'}

        if event.type == 'TIMER':
            ModalTimerOperator2._count += 1
            redraw_region(context, 'PROPERTIES')
        return {'PASS_THROUGH'}


    def invoke(self, context, event):
        if self.__class__._timer is None:
            if ModalTimerOperator._timer is not None:
                bpy.ops.wm.modal_timer_operator('INVOKE_DEFAULT')
            self.__class__._timer = context.window_manager.event_timer_add(0.5, context.window)
            context.window_manager.modal_handler_add(self)
        else:
            return self.cancel(context)
        return {'RUNNING_MODAL'}


    def cancel(self, context):
        context.window_manager.event_timer_remove(self.__class__._timer)
        self.__class__._timer = None
        ModalTimerOperator2._count = 0
        return {'CANCELLED'}
    
def redraw_region(context, area_type, region_type='WINDOW'):
    for area in context.screen.areas:
        if area.type == area_type:
            for region in area.regions:
                if region.type == region_type:
                    region.tag_redraw()
    
def draw_func(self, context):
    layout = self.layout
    top1 = ModalTimerOperator
    top2 = ModalTimerOperator2
    layout.operator(ModalTimerOperator.bl_idname, text=top1.bl_label if top1._timer is None else str(top1._count))
    layout.operator(ModalTimerOperator2.bl_idname, text=top2.bl_label if top2._timer is None else str(top2._count))
    
    layout.label("Timer 1: %s" % ModalTimerOperator._timer)
    layout.label("Timer 2: %s" % ModalTimerOperator2._timer)

def register():
    bpy.utils.register_module(__name__)
    bpy.types.RENDER_PT_render.prepend(draw_func)
    
def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.RENDER_PT_render.remove(draw_func)
    
if __name__ == '__main__':
    register()

Thanks for the suggestions.
But this solution don’t “scale” well.
Cross-invocation between operators doesn’t work if you have many different classes and operators using the same “modal” technique.
And our framework grows day-by-day.

I might implement a TimerManager class, holding a static global timer operating at the higher requested frequency.
Then each modal() function can test if the invocation is too close to the previous one.

DELAY = 0.04
last_time = 0
def modal(...):
    now = time.time()
    dt = now - self.last_time
    if(dt < self.DELAY):
        return {''PASS_THROUGH'}

    self.last_time = now

    # Do the rest ...

But this would generate irregular frequency calls for lower frequencies requests.

Of course, testing (event.source == self._timer), as initially proposed, would be much easier to write and read :wink:

What can I do to propose such implementation?
Insert an entry in the Coding section of blenderartists?
Or is there a more appropriate section for feature request?

Thanks.
Best,
FN

Hi,

Depending on your desired usage you may find handlers another way to deal with multiple “timers”


import bpy


class Timer():
    ticker = 0
    def __init__(self, prop, ticks, kill):
        self.prop = prop
        self.ticks = ticks
        # a negative kill will never die.
        self.kill  = kill
        bpy.app.handlers.scene_update_pre.append(self.scene_update)
        
    def scene_update(self, scene):
        self.ticker += 1
        if not self.ticker % self.ticks:
            #self.ticker = 0
            print("update", self.prop)
        if self.ticker > self.kill:
            print("cancel", self.prop)
            self.cancel()
    
    def cancel(self):
        bpy.app.handlers.scene_update_pre.remove(self.scene_update)


# for testing and coding to prevent remnant handlers.
bpy.app.handlers.scene_update_pre.clear()        


timer1 = Timer("foo", 10, 1000)
timer2 = Timer("bar", 300, 10000)