I have made a timer that is similar to AddWindowTimer() but is more accurate.
It also gives some extra features; pause, holdoff, and random intervals.
Multiple timers are allowed.
AccuTimer runs as a separate thread so you should enable 'create threadsafe executable' under compiler options.
AccuTimers generate '#PB_Event_Timer' events just like AddWindowTimer().
This is my first attempt at using threads that is not borrowed code so I will ask for some advice.
Do I need to use a mutex here and if so how do you code it?
The thread runs using each of the structured map elements in AccuTimer().
The support procedures can change the elements in the AccuTimer() map while the thread is running.
This is why I think I might need to use a mutex but it seems to work ok as is without it.
I have tested on Linux and Windows 7, it should work on Mac as well.
BP
7/21/2013
Edit: added mutex protection.
7/22/2013
Edit: bug fixes
Code: Select all
; AccuTimer.pbi
; by BasicallyPure
; version 1.2
; 7.22.2013
; license: Free
; platform = All
;
; <><><><><><><><><><><><><><><> AccuTimer instructions <><><><><><><><><><><><><><><>
; Description:
; provides a timer similar To AddWindowTimer() but With more accuracy, and extra features
; including 'pause', 'holdoff', and 'random intervals'. AccuTimer timers are accurate
; over time with system clock accuracy. A built-in error correction removes accumulated
; errors from timer events.
;
; Syntax: AddAccuTimer(nWindow, nTimer, Timeout, [Holdoff], [RandomSpan])
;
; Parameters:
; nWindow | this is the number that can be retreived with EventWindow() after a #PB_Event_Timer event.
;
; nTimer | The Timer parameter is a user defined number that identifies this timer.
; This value will later be returned from EventTimer() when a #PB_Event_Timer is received.
; It can also be used To remove the timer again with the RemoveAccuTimer() function.
;
; Timeout | This specifies the amount of time in milliseconds between #PB_Event_Timer events.
; The value of timeout is required to be >= 20 milliseconds.
;
; Holdoff | This optional parameter produces an extra delay in milliseconds before the first timer event
; from this timer is produced. This can be used to produce a phase offset between two Accutimers that
; have the same timeout value.
;
; RandomSpan | This optional parameter will cause the timer to produce #PB_Event_Timer events that have
; random time intervals where timeout is the minimum interval and timeout + randomSpan is the maximum
; interval.
;
; <><><> Support procedures: <><><>
;
; PauseAccuTimer(nTimer, state)
; This procedure will pause or resume a timer's operation.
; nTimer | the timer number to pause or resume.
; state | 1 = pause, 0 = resume
;
; ReviseAccuTimer(nTimer.i, timeout.i, holdOff.i = 0, RandomSpan.i = 0)
; This procedure allows changing a timer's parameters after it has been created with AddAccuTimer().
; The parameters are the same as for AddAccuTimer().
;
; RemoveAccuTimer(nTimer)
; Removes the specified AccuTimer.
; nTimer is the same value that was used in AddAccuTimer() To create the timer.
; There will be no further events received from this timer.
; If all of the AccuTimers have been removed the AccuTimer thread will end.
;
; RemoveAllAccuTimers()
; Removes all AccuTimers created with AddAccuTimer()
; The AccuTimer thread will end.
;
; <><><><><><><><><><><><><><><><> End instructions <><><><><><><><><><><><><><><><>
CompilerIf Not #PB_Compiler_Thread
MessageRequester("WARNING!", "Set compiler options: 'Create threadsafe executable'")
End
CompilerEndIf
; this code is compatable with EnableExplicit
; EnableExplicit
Structure timerType
winNum.i ; window number returned by EventWindow()
timNum.i ; unique number to identify each AccuTimer
rtMax.i ; upper time limit for random timer, 0 for fixed timer
rtMin.i ; lower time limit for random timer, 0 for fixed timer
timeout.i ; time between #PB_Event_Timer events in milliseconds
paused.i ; flag to pause timer, 1 = paused, 0 = run
start.i ; set equal to ElapsedMilliseconds() when initalized
EndStructure
Global NewMap AccuTimer.timerType()
Global Mutex1 = CreateMutex()
Global.i AccuTimerThreadID
Global.i StopAccuTimerThread
Procedure AccuTimer_Thread(unused.i)
; Purpose generate #PB_Event_Timer events for each structured map element of AccuTimer().
; Multiple timers may run simultaneously.
Protected.i now, elapsed, err
StopAccuTimerThread = #False
Repeat
now = ElapsedMilliseconds()
LockMutex(Mutex1)
ForEach AccuTimer()
With AccuTimer()
If Not \paused
elapsed = now - \start
If elapsed >= \timeout
err = elapsed - \timeout
\start = now - err
PostEvent(#PB_Event_Timer, \winNum, \timNum)
If \rtMax : \timeout = Random(\rtMax, \rtMin) : EndIf
EndIf
EndIf
EndWith
Next
UnlockMutex(Mutex1)
Delay(10) ; arbitrary delay sets lower limit of timeout
Until StopAccuTimerThread
EndProcedure
Procedure AddAccuTimer(nWindow.i, nTimer.i, timeout.i, holdOff.i = 0, RandomSpan.i = 0)
; Purpose: create a new AccuTimer with the specified parameters. #PB_Event_Timer events
; will be received by WindowEvent() or WaitWindowEvent().
;
; #Window | this is the number that is returned by EventWindow() after a #PB_Event_Timer event.
;
; nTimer | The user defined number used to identify this timer.
;
; timeout | This specifies the amount of time in milliseconds between #PB_Event_Timer events.
; The value of timeout is required to be >= 20 milliseconds.
;
; holdoff | This optional parameter produces an extra delay in milliseconds before the first timer event
;
; RandomSpan | This optional parameter will cause the timer To produce #PB_Event_Timer events that have
; random time intervals where 'timeout' sets the minimum interval and 'timeout + RandomSpan'
; sets the maximum interval.
Protected result.i = #False
If IsWindow(nWindow) = 0 Or timeout < 20 Or RandomSpan < 0
ProcedureReturn result ; error, invalid parameters
EndIf
LockMutex(Mutex1)
If FindMapElement(AccuTimer(), Str(nTimer)) = 0
If IsThread(AccuTimerThreadID) = #False
AccuTimerThreadID = CreateThread(@AccuTimer_Thread(), #NUL)
EndIf
With AccuTimer(Str(nTimer)) ; this creates a new map element
\winNum = nWindow
\timNum = nTimer
If RandomSpan
\rtMax = timeout + RandomSpan
\rtMin = timeout
\timeout = Random(\rtMax, \rtMin)
Else
\timeout = timeout
EndIf
\paused = #False
\start = ElapsedMilliseconds() + holdOff
EndWith
result = #True
EndIf
UnlockMutex(Mutex1)
ProcedureReturn result
EndProcedure
Procedure PauseAccuTimer(nTimer.i, state.i)
; suspend/enable timer events of the specified timer
; state = 0 (enable), state = 1 (suspend)
Protected nt$ = Str(nTimer), Result = #False
If state <> 0 : state = 1 : EndIf
LockMutex(Mutex1)
If FindMapElement(AccuTimer(), nt$) ; check for valid timer number
With AccuTimer(nt$)
\paused = state
If state = 0 : \start = ElapsedMilliseconds() : EndIf
EndWith
Result = #True
EndIf
UnlockMutex(Mutex1)
ProcedureReturn Result
EndProcedure
Procedure ReviseAccuTimer(nTimer.i, timeout.i, holdOff.i = 0, RandomSpan.i = 0)
; purpose: change the settings of a timer that has already been created with AddAccuTimer().
Protected nt$ = Str(nTimer), result.i = #False
LockMutex(Mutex1)
; check for valid timer number
If FindMapElement(AccuTimer(), nt$) And timeout >= 20
With AccuTimer(nt$)
If RandomSpan
\rtMax = timeout + RandomSpan
\rtMin = timeout
\timeout = Random(\rtMax, \rtMin)
Else
\rtMax = 0
\rtMin = 0
\timeout = timeout
EndIf
\start = ElapsedMilliseconds() + holdOff
EndWith
result = #True
EndIf
UnlockMutex(Mutex1)
ProcedureReturn result
EndProcedure
Procedure RemoveAccuTimer(nTimer.i)
; Purpose: Removes the specified AccuTimer.
; nTimer has To be the same value that was used in AddAccuTimer() To create the timer.
; There will be no further events received from this timer.
Protected nt$ = Str(nTimer), result.i = #False
LockMutex(Mutex1)
If FindMapElement(AccuTimer(), nt$) ; check for valid timer number
DeleteMapElement(AccuTimer(),nt$)
If MapSize(AccuTimer()) = 0
UnlockMutex(Mutex1)
StopAccuTimerThread = #True ; no AccuTimers exist so stop the thread
WaitThread(AccuTimerThreadID, 2000)
If IsThread(AccuTimerThreadID)
KillThread(AccuTimerThreadID)
EndIf
Else
UnlockMutex(Mutex1)
EndIf
result = #True
Else
UnlockMutex(Mutex1)
EndIf
ProcedureReturn result
EndProcedure
Procedure RemoveAllAccuTimers()
; Purpose: removes all AccuTimers created with AddAccuTimer()
Protected result.i = #False
LockMutex(Mutex1)
If MapSize(AccuTimer())
ForEach AccuTimer()
DeleteMapElement(AccuTimer())
Next
UnlockMutex(Mutex1)
StopAccuTimerThread = #True
WaitThread(AccuTimerThreadID, 2000)
If IsThread(AccuTimerThreadID)
KillThread(AccuTimerThreadID)
EndIf
result = #True
Else
UnlockMutex(Mutex1)
EndIf
ProcedureReturn result
EndProcedure
; ******************************************************************* ;
;- DEMONSTRATION -
; you can leave this in the include file
CompilerIf #PB_Compiler_IsMainFile
#WinMain = 0
#WinAux1 = 1
Enumeration
#strGad_01
#strGad_02
#strGad_03
#btnRevise
#btnPause
#btnRemove
#btnRemoveAll
#ProgBar
EndEnumeration
Enumeration
#AccuTimer_01
#AccuTimer_02
#AccuTimer_03
#WindowTimer_01
EndEnumeration
Define text.s = "Comparison AccuTimer vs WindowTimer"
If OpenWindow(#WinMain,020,300,280,250,text) = 0 : End : EndIf
text = "bar movement is triggered by AccuTimer's random interval event option"
If OpenWindow(#WinAux1,330,300,500,100,text,#PB_Window_TitleBar) = 0 : End : EndIf
SetActiveWindow(#WinMain)
Define Courier_ID = LoadFont(0,"Courier New",28,#PB_Font_Bold)
UseGadgetList(WindowID(#WinMain))
TextGadget(#PB_Any,10,3,250,20,"PB Date() function as reference")
StringGadget(#strGad_01,10,20,250,50,"00:00:00")
SetGadgetFont(#strGad_01,Courier_ID)
TextGadget(#PB_Any,10,83,250,20,"variable incremented with AccuTimer")
StringGadget(#strGad_02,10,100,250,50,"00:00:00")
SetGadgetFont(#strGad_02,Courier_ID)
TextGadget(#PB_Any,10,163,250,20,"variable incremented with PB WindowTimer")
StringGadget(#strGad_03,10,180,250,50,"00:00:00")
SetGadgetFont(#strGad_03,Courier_ID)
UseGadgetList(WindowID(#WinAux1))
ButtonGadget(#btnPause, 5, 70, 80, 25, "Pause bar", #PB_Button_Toggle)
ProgressBarGadget(#ProgBar, 0, 0, 500, 60, 0, 500)
ButtonGadget(#btnRevise, 90, 70, 130, 25, "Revise bar timer", #PB_Button_Toggle)
ButtonGadget(#btnRemove, 225, 70, 130, 25, "Remove AccuTimer_02")
ButtonGadget(#btnRemoveAll, 360, 70, 130, 25, "Remove all AccuTimers")
; ******* Initalize the timers ***************
;
AddAccuTimer(#WinMain, #AccuTimer_01, 1000) ; <-- event every second
AddAccuTimer(#WinMain, #AccuTimer_02, 100) ; <-- event every 100 mS
AddAccuTimer(#WinAux1, #AccuTimer_03, 200, 0, 2000) ;<-- random event intervals 200 to 200 + 2000 mS
;
AddWindowTimer(#WinMain, #WindowTimer_01, 100) ; <-- event approximately every 100 mS
;
; ********************************************
Define.i AccuCount ; <-- variable to be incremented by AccuTimer event
Define.i WindowTimerCount ; <-- variable to be incremented by WindowTimer event
; initalize both reference variables to the current time
Define ACref.i = Date() ; for AccuTimer
Define WTref.i = Date() ; for WindowTimer
Define progress.i = 10
Dim caption.s(1) : caption(0) = "Pause bar" : caption(1) = "Resume"
Repeat
Select WaitWindowEvent()
Case #PB_Event_CloseWindow : End
Case #PB_Event_Timer; AccuTimer produces events just like the WindowTimer
Select EventTimer()
Case #AccuTimer_01 ; update the display of the Date() function
SetGadgetText(#strGad_01, FormatDate("%hh:%ii:%ss", Date()))
Case #AccuTimer_02 ; update the display of the AccuTimer counter
AccuCount + 1
SetGadgetText(#strGad_02, FormatDate("%hh:%ii:%ss", ACref + AccuCount/10)+"."+Str(AccuCount%10))
Case #WindowTimer_01 ; update the display of the WindowTimer counter
WindowTimerCount + 1
SetGadgetText(#strGad_03, FormatDate("%hh:%ii:%ss", WTref + WindowTimerCount/10)+"."+Str(WindowTimerCount%10))
Case #AccuTimer_03 ; move the bar at the randomly constrained time interval
SetGadgetState(#ProgBar, progress) : progress + 10
If progress > 500 : progress = 0 : EndIf
EndSelect
Case #PB_Event_Gadget
Select EventGadget()
Case #btnPause ; demonstrate PauseAccuTimer()
PauseAccuTimer(#AccuTimer_03, GetGadgetState(#btnPause))
SetGadgetText(#btnPause, caption(GetGadgetState(#btnPause)))
Case #btnRemove ; demonstrate RemoveAccuTimer()
RemoveAccuTimer(#AccuTimer_02)
DisableGadget(#btnRemove, #True)
Case #btnRemoveAll ; demonstrate RemoveAllAccuTimers()
RemoveAllAccuTimers()
DisableGadget(#btnRemove, #True)
DisableGadget(#btnPause, #True)
DisableGadget(#btnRemoveAll, #True)
Case #btnRevise ; demonstrate ReviseAccuTimer()
If GetGadgetState(#btnRevise)
ReviseAccuTimer(#AccuTimer_03, 50, 0, 250)
Else
ReviseAccuTimer(#AccuTimer_03, 200, 0, 2000)
EndIf
EndSelect
EndSelect
ForEver
CompilerEndIf