Reply
kgober
Posts: 5,487
Kudos: 744
Solutions: 453
Registered: ‎05-28-2009

"Polling" in Lua

before I explain what polling is and how it works, let me first explain the problem that makes it necessary:

 

Lua is single-threaded.  what this means is, Lua does only one thing at a time.  when LGS calls your OnEvent function to inform you that something has happened, it waits for that function to complete before it lets you know about other things.  so, if you press 3 buttons in quick succession (say, 250ms apart), LGS will call your OnEvent function 3 times (once for each button press).  for the first button press this happens right away, but for the second and third, if your first OnEvent call is still in progress those events are queued and OnEvent doesn't actually get called until the first call is done.

 

what this means, is, if your OnEvent functions take too long to finish, it makes things seem laggy or unresponsive.

 

there are good technical reasons for Lua to be single-threaded, so the easy answer isn't for Logitech to simply make it multi-threaded.  that would actually create a lot more problems than it solves.

 

so, the situation is this: in order to remain responsive, you always want your OnEvent function to return quickly.  but sometimes (for example because you have a macro that needs to run for a long time) you don't want to return, because you have no idea when (or if) you will get called again.  things like no-recoil scripts are an excellent example of this: when you press a button, you are going to want to do something essentially continuously until the button is released.  but as long as your script continues to do something, you won't receive a notification that the button has been released (and all other button notifications will be paused as well).  in order to receive notifications about other buttons, or even the fact that the first button has been released, you have to stop -- but that stops your macro from running.

 

so you can't find out if it's time to stop yet unless you stop first, but if you stop first it might turn out to be too early.  how do you know?

 

sometimes we can use a function like IsMouseButtonPressed() or IsModifierPressed() to figure out when something has happened (these functions bypass Lua's single-threadedness by going directly to Windows, but are limited in what they can do), but this isn't always practical.

 

so, what we really need is to be able to return from OnEvent immediately (so that queued events can be processed) but set things up so that we know that it will be called in a short amount of time (so we can continue our macros where they left off).  essentially, we need some kind of guarantee that a future event is going to arrive soon before we can safely allow our current OnEvent call to return.

 

that guarantee turns out to be SetMKeyState().  when you call that function, it causes LGS to send M_PRESSED and M_RELEASED events to your script, just as if you had pressed an actual M-key (mice don't have M-keys, but the G-series keyboards do).  essentially, calling SetMKeyState() simulates the pressing of an M-key, and that causes OnEvent to be called.

 

so, what "polling" is, is a technique of repeatedly calling SetMKeyState() to ensure that OnEvent can exit, but also that it will be called again very soon.  this lets you receive events for button presses and releases, and also allows you to perform long-running tasks (as long as you break them up into short segments).  if you have a system programming background, you will recognize that this isn't actually polling, but that's the name we use for lack of a good one-word term that describes the process of sending yourself a stream of virtual interrupts.  it's "polling" in this sense: your OnEvent function effectively loops, and in each loop you can check to see if an event you're waiting for has occurred yet (like a key being released, indicating that a long-running macro should be stopped).

 

polling turns out to be a bit tricky to do, because you have to be very careful how often you do this.  if you do it wrong you can so overload the event queue with SetMKeyState calls that LGS becomes laggy or crashes.  it's a delicate tightrope between too much and too little, and this is why I don't recommend the technique to beginners.

 

my next post in this thread will be a sample script that uses polling to do color-cycling.

 

-ken

________________________________
I do not work for Logitech. I'm just a user.
kgober
Posts: 5,487
Kudos: 744
Solutions: 453
Registered: ‎05-28-2009

Re: "Polling" in Lua

[ Edited ]

here is a color cycling script that rotates the backlight color on devices with Lua-programmable backlighting (such as the G13, the G510 and the G19).

 

function t_ColorCycle()
	H, S, L = 0,240,120
	local f = true
	while f do
		local r, g, b = HSLtoRGB(H, S, L)
		SetBacklightColor(r, g, b, "kb")
		SetBacklightColor(r, g, b, "lhc")
		H = H + 1
		if H >= 240 then H = 0 end
		f = TaskSleep(50)
	end
	return -1
end

-- HSL to RGB color space conversion
-- you don't need to understand this
function HSLtoRGB(H, S, L)
	H, S, L = math.min(240, H), math.min(240, S), math.min(240, L)
	H, S, L = math.max(0, H), math.max(0, S), math.max(0, L)
	local R, G, B = 0, 0, 0
	if H < 80 then
		R = math.min(255, 255 * (80 - H) / 40)
	elseif H > 160 then
		R = math.min(255, 255 * (H - 160) / 40)
	end
	if H < 160 then
		G = math.min(255, 255 * (80 - math.abs(H - 80)) / 40)
	end
	if H > 80 then
		B = math.min(255, 255 * (80 - math.abs(H - 160)) / 40)
	end
	if S < 240 then
		k = S / 240
		R, G, B = R*k, G*k, B*k
		k = 128 * (240 - S) / 240
		R, G, B = R+k, G+k, B+k
	end
	k = (120 - math.abs(L - 120)) / 120
	R, G, B = R*k, G*k, B*k
	if L > 120 then
		k = 256 * (L - 120) / 120
		R, G, B = R+k, G+k, B+k
	end
	return R, G, B
end

function OnEvent(event, arg, family)
	local st = StateTimer
	if event == "PROFILE_ACTIVATED" then
		InitPolling()
		RunTask("ColorCycle", t_ColorCycle)
	elseif event == "PROFILE_DEACTIVATED" then
		AbortTask("ColorCycle")
	end
	DoTasks()
	Poll(event, arg, family, st)
end

POLL_FAMILY = "mouse"	-- current mice don't have M-states, so this is a good choice
POLL_INTERVAL = 10	-- delay (in milliseconds) before next loop, used to throttle polling rate
POLL_DEADTIME = 100	-- settling time (in milliseconds) during which old poll events are drained

function InitPolling()
	ActiveState = GetMKeyState_Hook(POLL_FAMILY)
	SetMKeyState_Hook(ActiveState, POLL_FAMILY)
end

function Poll(event, arg, family, st)
	if st == nil and StateTimer ~= nil then return end
	local t = GetRunningTime()
	if family == POLL_FAMILY then
		if event == "M_PRESSED" and arg ~= ActiveState then
			if StateTimer ~= nil and t >= StateTimer then StateTimer = nil end
			if StateTimer == nil then ActiveState = arg end
			StateTimer = t + POLL_DEADTIME
		elseif event == "M_RELEASED" and arg == ActiveState then
			Sleep(POLL_INTERVAL)
			SetMKeyState_Hook(ActiveState, POLL_FAMILY)
		end
	end
end

GetMKeyState_Hook = GetMKeyState
GetMKeyState = function(family)
	family = family or "kb"
	if family == POLL_FAMILY then
		return ActiveState
	else
		return GetMKeyState_Hook(family)
	end
end

SetMKeyState_Hook = SetMKeyState
SetMKeyState = function(mkey, family)
	family = family or "kb"
	if family == POLL_FAMILY then
		if mkey == ActiveState then return end
		ActiveState = mkey
		StateTimer = GetRunningTime() + POLL_DEADTIME
	end
	return SetMKeyState_Hook(mkey, family)
end


-- Task Management functions

TaskList = {}

function DoTasks()
	local t = GetRunningTime()
	for key, task in pairs(TaskList) do
		if t >= task.time then
			local s, d = coroutine.resume(task.task, task.run)
			if (not s) or ((d or -1) < 0) then
				TaskList[key] = nil
			else
				task.time = task.time + d
			end
		end
	end
end

function RunTask(key, func, ...)
	AbortTask(key)
	local task = {}
	task.time = GetRunningTime()
	task.task = coroutine.create(func)
	task.run = true
	local s, d = coroutine.resume(task.task, ...)
	if (s) and ((d or -1) >= 0) then
		task.time = task.time + d
		TaskList[key] = task
	end
end

function StopTask(key)
	local task = TaskList[key]
	if task ~= nil then task.run = false end
end

function AbortTask(key)
	local task = TaskList[key]
	if task == nil then return end
	while true do
		local s, d = coroutine.resume(task.task, false)
		if (not s) or ((d or -1) < 0) then
			TaskList[key] = nil
			return
		end
	end
end

function TaskRunning(key)
	local task = TaskList[key]
	if task == nil then return false end
	return task.run
end

function TaskSleep(delay)
	return coroutine.yield(delay)
end

 

in this example, the "t_ColorCycle" task function is started as soon as the profile is activated, and it remains active until the profile is deactivated.  but you could use a similar technique to start or stop a task based on a G-key or a mouse button event.  using a G-key or mouse button to start or stop a task is where the "polling" technique becomes truly valuable.

 

note that this example also incldues a simple task framework.  within a task function, you should never use Sleep().  instead, use the TaskSleep() function.  TaskSleep will return a value of false if your task needs to be stopped, so you should check for this value and end the task if you see it.  when a task is done it should return a negative number.

 

this is a good script to use in the Default Profile, but be warned: if you want to tinker with the script at all make a new profile to test changes in (like a profile for Notepad).  this way, if you mess something up it will be easier to fix.  problems with the Default Profile (especially script problems) can sometimes be hard to fix, especially if they crash LGS.

 

EDIT: try to avoid having the LGS window open if you use this script, unless you are ok with LGS pegging one of your CPU cores at 100%.  running the script with the LGS window closed is pretty harmless in terms of CPU utilization, though, as long as you have a reasonably modern system.

 

-ken

________________________________
I do not work for Logitech. I'm just a user.
kgober
Posts: 5,487
Kudos: 744
Solutions: 453
Registered: ‎05-28-2009

Re: "Polling" in Lua

[ Edited ]

so, how does the script work?  in terms of polling?

 

to make things easy I use Lua coroutines.  coroutines are a Lua feature that make it easy to pause and resume a function, which makes them perfect for this application.

 

you resume a coroutine by calling coroutine.resume().  when the coroutine wants to pause, it calls coroutine.yield().  if the coroutine passes a value to coroutine.yield(), then those values are returned from the coroutine.resume() call, like so:

 

1. your 'main' function calls coroutine.resume() to restart task A

2. task A picks up where it left off and runs for a while

3. when task A wants to pause, it calls coroutine.yield()

4. control now goes back to your 'main' function, and any arguments provided to yield() in step 3 are returned as the 'return value' of the coroutine.resume() call from step 1

 

to make things easier, all of this is wrapped up in some helper functions, notably RunTask() and TaskSleep().

 

in this case, the return value from yield() is used as a time delay, so we know how long to pause for.  we take that time value and store it away, and every time we go through the polling loop we check to see if it is time for any tasks to wake up yet (this is what the DoTasks function does).

 

as for the polling itself, that is done by the Poll() function.  it's complicated and I won't try to explain it completely, except to say most of the complexity is related to making sure that we don't call SetMKeyState() more often than absolutely necessary, and to making sure that a user can still use the M1, M2 or M3 buttons to change modes manually without fouling up the polling.

 

-ken

________________________________
I do not work for Logitech. I'm just a user.
Logi Nu
Heartzbane
Posts: 3
Registered: ‎03-18-2014

Re: "Polling" in Lua

Thank you for this! I have some LUA scripts that use timers and cooldowns to manage the firing of different macros/keys but I don't always know how long it will take a single button press to process an event (depending on various timers/cooldowns it could take anywhere from 10 ms to several seconds). I have been looking for a way to handle or ignore subsequent events that fire before one event finishes, and it looks like this will do the trick nicely. Good stuff! :smileyhappy:

Logi Nu
Heartzbane
Posts: 3
Registered: ‎03-18-2014

Re: "Polling" in Lua

Well, I'm not quite getting the results I was expecting. I made a test that I mapped to G19 of my LHC. Essentially, I wanted it to simulate pressing the "X" key every 1 second until I pressed the same button again, and then I wanted it to stop. Seemed simple enough:

 

MeleeCombatIsActive = false

function OnEvent(event, arg, family)
	local st = StateTimer
	if event == "PROFILE_ACTIVATED" then
		InitPolling()
	elseif event == "PROFILE_DEACTIVATED" then
		AbortTask("MeleeCombat")
	elseif event == "G_PRESSED" and arg == 19 then
		if MeleeCombatIsActive then
			AbortTask("MeleeCombat")
			MeleeCombatIsActive = false
		else
			MeleeCombatIsActive = true
			RunTask("MeleeCombat",RunMeleeCombat);
		end
	end
	DoTasks()
	Poll(event, arg, family, st)
end

function RunMeleeCombat()
	while 0==0 do
		PressAndReleaseKey("X");
		TaskSleep(1000);
	end
	return
end

 The first time I press the G19 key, it starts pressing "X" every second, as expected. However, when I press the G19 key again, it suddenly creates a whole bunch of X's and crashes LGS. It seems to do the same thing whether I use StopTask() or AbortTask() -- I tried both as I wasn't sure what the difference is. 

 

What am I missing here? I would expect StopTask() or AbortTask() to kill the running function and stop pressing the "X" key, and the G19 button to act as a toggle switch turning the "X" per second on/off with each key press.

 

Logi Nu
Heartzbane
Posts: 3
Registered: ‎03-18-2014

Re: "Polling" in Lua

I got it to work by adding the check for a false return value on TaskSleep():

 

function RunMeleeCombat()
	local f = true
	while 0==0 do
		PressAndReleaseKey("X")
		f = TaskSleep(1000)
		if f == false then break end
	end
	return
end

 It seems to be working great now!

Logi Nu
nauli
Posts: 3
Registered: ‎02-24-2014

Re: "Polling" in Lua

why this script not work... can somebody fix it..?

MeleeCombatIsActive = false

function OnEvent(event, arg, family)
	local st = StateTimer
	if event == "PROFILE_ACTIVATED" then
		InitPolling()
	elseif event == "PROFILE_DEACTIVATED" then
		AbortTask("MeleeCombat")
	elseif event == "MOUSE_BUTTON_PRESSED" and arg == 1 then
		if MeleeCombatIsActive then
			AbortTask("MeleeCombat")
			MeleeCombatIsActive = false
		else
			MeleeCombatIsActive = true
			RunTask("MeleeCombat",RunMeleeCombat);
		end
	end
	DoTasks()
	Poll(event, arg, family, st)
end

function RunMeleeCombat()
	local f = true
	while 0==0 do
		PressAndReleaseMouseButton("1")
		f = TaskSleep(1000)
		if f == false then break end
	end
	return
end

 

[string "LuaVM"]:18: attempt to call global 'DoTasks' (a nill value)

 

 

kgober
Posts: 5,487
Kudos: 744
Solutions: 453
Registered: ‎05-28-2009

Re: "Polling" in Lua

you need all the stuff that came after the OnEvents function in the original post.

 

also, for something simple like this (click a mouse button once per second), you might as well just use a multi key macro with a 'toggle' repeat option,  much easier.

 

-ken

________________________________
I do not work for Logitech. I'm just a user.
Logi Apprentice
G_sus
Posts: 121
Registered: ‎01-29-2013

Re: "Polling" in Lua

[ Edited ]

hi ken,

i created a function Press(),that runs a task t_Press... its working fine by itself.

but when i call this function within another task i must add a TaskSleep() after for the duration of the keypress.

is there an easier way to do so?

Logi Nu
nauli
Posts: 3
Registered: ‎02-24-2014

Re: "Polling" in Lua

i  newbie to programing in lua can some one explaiin with some easy function of template "polling" kgober mine... "after the OnEvents" ??