Sunday, December 20, 2020

My Toothbrush Time App: Nearing Completion

I solved the problem I mentioned in my last post about the interval timer not working properly when the app is paused in the middle of a timing session. I completely overhauled the programming for this and the app's been working fine since. I barely remember how the original approach worked. I remember I used some (in hindsight) whacky system where I was subtracting interval seconds from the total seconds elapsed plus the amount of time paused, or something like that. It was yet another example of me programming by the seat of my pants without thinking it through ahead of time. But honestly, that's the way I like to program. I have a firm goal in mind and I just "have at it" until I reach the goal. I find this an exhilarating quest. Usually things work out fine, but occasionally - as in this case - they don't. A reasonable trade-off as far as I'm concerned.

The new approach is a simple one that occurred to me on a long walk as I was pondering the problem. It uses the the time on the clock as the starting point for figuring out when each new interval should end. So, if the next next interval is 20 seconds long and it begins when the clock reads 0:30, obviously the alert should sound at the 0:50 mark. If the person pauses the timer, the display is frozen until the Restart button is pressed. I still had to do some transposing of time based on the theSeconds function, but the logic was sound.

I had also mentioned in my previous post that the vibrate alert option seemed to really throw things off. It turned out that an extra second was added to the current interval every time the vibrate option was used. Given that the pattern of adding one and only one extra second never changed, I decided to just add the hack of subtracting a second when the vibrate option was used. I accomplished this with a variable I created named varIntervalFudge (in my mind fudge = hack).

Yeah, programming a hack rather than tracing down the actual problem is the lazy way to do things. But hey, I'm a busy guy.

Here is a review of the programming engine for this app. MainEventLoop, as its name suggests, is the main programming loop that allows the timer to run while the user is making choices, such as starting, stopping, and resetting the timer:

1:  on MainEventLoop  
2:    if varRunMainLoop is true then  
3:     lock screen  
4:     LloydStopWatch  
5:     unlock screen  
6:    end if  
7:    put the pendingMessages into tMessages   
8:    repeat for each line aLine in tMessages   
9:     if item 3 of aLine is "mainEventLoop" then cancel item 1 of aLine   
10:    end repeat    
11:    send "mainEventLoop" to me in 50 milliseconds  
12:  end MainEventLoop  

Line 4 triggers the procedure LloydStopWatch to run every 50 ms. The LloydStopWatch procedure contains the programming that operates the timer and checks on the intervals:

1:  on LloydStopWatch    
2:    put ((varIminutes1*60) + varIseconds1 - varIntervalFudge) into varInterval1  
3:    put ((varIminutes2*60) + varIseconds2 - varIntervalFudge) into varInterval2  
4:    put ((varIminutes3*60) + varIseconds3 - varIntervalFudge) into varInterval3  
5:    put ((varIminutes4*60) + varIseconds4 - varIntervalFudge) into varInterval4  
6:    if varIntervalTurn =1 then put varInterval1 into varIntervalCurrent  
7:    if varIntervalTurn =2 then put varInterval2 into varIntervalCurrent  
8:    if varIntervalTurn =3 then put varInterval3 into varIntervalCurrent  
9:    if varIntervalTurn =4 then put varInterval4 into varIntervalCurrent  
10:      
11:    put the seconds into varSecondsEnd    
12:    put (varSecondsEnd - varSecondsBegin) + varTimeElapsedPrevious into varTimeElapsed //Modified December 6, 2020  
13:    put varSecondsEnd - varSecondsBeginInterval into varTimeElapsedInterval  
14:      
15:    //Display the time elapsed  
16:    put trunc(varTimeElapsed/60) into varDisplayMinutes  
17:    put (varTimeElapsed)-(varDisplayMinutes*60) into varDisplaySeconds  
18:    if the length of varDisplaySeconds < 2 then put "0"&varDisplaySeconds into varDisplaySeconds  
19:    put varDisplayMinutes&":"&varDisplaySeconds into line 1 of field "my time"  
20:      
21:    if varIntervalCurrent = 0 then exit LloydStopWatch //Only when happens when all internal fields are 0  
22:    if varIntervalTurn = 1 and varIntervalCheck = true then //December 6, 2020  
23:     put varInterval1 + varTimeElapsed into varTimeOfNextAlert  
24:    end if  
25:    if varIntervalTurn = 2 and varIntervalCheck = true then //December 6, 2020  
26:     put varInterval2 + varTimeElapsed into varTimeOfNextAlert  
27:    end if  
28:    if varIntervalTurn = 3 and varIntervalCheck = true then //December 6, 2020  
29:     put varInterval3 + varTimeElapsed into varTimeOfNextAlert  
30:    end if  
31:    if varIntervalTurn = 4 and varIntervalCheck = true then //December 6, 2020  
32:     put varInterval4 + varTimeElapsed into varTimeOfNextAlert  
33:    end if  
34:    put false into varIntervalCheck  
35:      
36:    if varTimeElapsed = varTimeOfNextAlert then  
37:     if varIntervalTurn = 1 and varChime1 <> "vibrate" then play specialFolderPath("engine")&varChime1  
38:     if varIntervalTurn = 1 and varChime1 = "vibrate" then mobileVibrate 2  
39:     if varIntervalTurn = 2 and varChime2 <> "vibrate" then play specialFolderPath("engine")&varChime2  
40:     if varIntervalTurn = 2 and varChime2 = "vibrate" then mobileVibrate 2  
41:     if varIntervalTurn = 3 and varChime3 <> "vibrate" then play specialFolderPath("engine")&varChime3  
42:     if varIntervalTurn = 3 and varChime3 = "vibrate" then mobileVibrate 2  
43:     if varIntervalTurn = 4 and varChime4 <> "vibrate" then play specialFolderPath("engine")&varChime4  
44:     if varIntervalTurn = 4 and varChime4 = "vibrate" then mobileVibrate 2  
45:       
46:     //Hack when vibrate alert is used - December 6, 2020  
47:     //Reduce the next interval time by 1 second  
48:     put 0 into varIntervalFudge  
49:     if varIntervalTurn = 1 and varChime1 = "vibrate" then put 1 into varIntervalFudge  
50:     if varIntervalTurn = 2 and varChime2 = "vibrate" then put 1 into varIntervalFudge  
51:     if varIntervalTurn = 3 and varChime3 = "vibrate" then put 1 into varIntervalFudge  
52:     if varIntervalTurn = 4 and varChime4 = "vibrate" then put 1 into varIntervalFudge  
53:       
54:     add 1 to varIntervalTurn  
55:     //Next 3 lines account for when either interval 2 or 3 are empty; if interval 2 is empty, then code assumes interval 3 is empty (even if isn't)  
56:     if varInterval2 = 0 then put 1 into varIntervalTurn  
57:     if varIntervalTurn = 3 and varInterval3 = 0 then put 1 into varIntervalTurn  
58:     if varIntervalTurn = 4 and varInterval4 = 0 then put 1 into varIntervalTurn  
59:     if varIntervalTurn > 4 then put 1 into varIntervalTurn     
60:       
61:     put true into varIntervalCheck  
62:    end if  
63:      
64:  end LloydStopWatch   

Note the use of the variable varIntervalFudge when accounting for the vibrate alert.

Option to Save the Current Settings

One nice little feature I added was giving the user the option to save the current settings. This is really helpful if you have a main use for the app, such as teeth brushing, and want to revert to those settings after using the app to do other things. The solution to this is similar to the idea of a "cookie." The idea is to save a small data file on the iPhone that can be retrieved later. Here's a link to a good tutorial on how to set up something like this:

https://lessons.livecode.com/m/4069/l/14301-how-do-i-read-write-to-files-on-mobile

Here is the code for saving the current settings located in the button "Save current settings":

1:  on mousedown  
2:    put empty into field "appdata" on card "data"  
3:    Put "Interval Data" into line 1 of field "appdata" on card "data"  
4:    put "Interval 1,"&field "IMinutes1"&comma&field "ISeconds1"&comma&field "chime1" into line 2 of field "appdata" on card "data"  
5:    put "Interval 2,"&field "IMinutes2"&comma&field "ISeconds2"&comma&field "chime2" into line 3 of field "appdata" on card "data"  
6:    put "Interval 3,"&field "IMinutes3"&comma&field "ISeconds3"&comma&field "chime3" into line 4 of field "appdata" on card "data"  
7:    put "Interval 4,"&field "IMinutes4"&comma&field "ISeconds4"&comma&field "chime4" into line 5 of field "appdata" on card "data"  
8:      
9:    //Activate the following line to actually save the data  
10:    put field "appdata" on card "data" into URL ("file:data.txt")  
11:    wait 1 second  
12:    show image "checkmark"  
13:    wait 1 second  
14:    hide image "checkmark"  
15:  end mousedown  

Lines 4-7 put the data for each of the four intervals into the text field "appdata" on card "data."

Line 10 saves everything in field "appdata" into a text file named "data.txt"

I decided to give the user feedback that the file was saved by displaying a very small graphic checkmark. It looks better if there is a pause for one second before it is displayed. I then hide the checkmark after another second.

Here is the code in the button "Revert to saved settings" that retrieves the settings data:

1:  on mousedown  
2:      
3:    put empty into field "appdata" on card "data"  
4:    set the defaultFolder to specialFolderPath("Documents")  
5:    put URL ("file:data.txt") into field "appdata" on card "data"  
6:      
7:    if field "appdata" on card "data" is empty then  
8:     put "No saved settings" into field "notes"  
9:     put "0" into field ISeconds1  
10:     put "0" into field ISeconds2  
11:     put "0" into field ISeconds3  
12:     put "0" into field ISeconds4  
13:     put "0" into field IMinutes1  
14:     put "0" into field IMinutes2  
15:     put "0" into field IMinutes3  
16:     put "0" into field IMinutes4  
17:     put "Chimes" into field "chime1"  
18:     put "Chimes" into field "chime2"  
19:     put "Chimes" into field "chime3"  
20:     put "Chimes" into field "chime4"  
21:     put "/sounds for toothbrush app/chimes.wav" into varChime1  
22:     put "/sounds for toothbrush app/chimes.wav" into varChime2  
23:     put "/sounds for toothbrush app/chimes.wav" into varChime3  
24:     put "/sounds for toothbrush app/chimes.wav" into varChime4  
25:    else  
26:     put item 2 of line 2 of field "appdata" on card "data" into field IMinutes1  
27:     put item 3 of line 2 of field "appdata" on card "data" into field ISeconds1  
28:     put item 4 of line 2 of field "appdata" on card "data" into field "Chime1"  
29:       
30:     put item 2 of line 3 of field "appdata" on card "data" into field IMinutes2  
31:     put item 3 of line 3 of field "appdata" on card "data" into field ISeconds2  
32:     put item 4 of line 3 of field "appdata" on card "data" into field "Chime2"  
33:       
34:     put item 2 of line 4 of field "appdata" on card "data" into field IMinutes3  
35:     put item 3 of line 4 of field "appdata" on card "data" into field ISeconds3  
36:     put item 4 of line 4 of field "appdata" on card "data" into field "Chime3"  
37:       
38:     put item 2 of line 5 of field "appdata" on card "data" into field IMinutes4  
39:     put item 3 of line 5 of field "appdata" on card "data" into field ISeconds4  
40:     put item 4 of line 5 of field "appdata" on card "data" into field "Chime4"  
41:       
42:     repeat with i = 1 to 4  
43:       put "chime"&i into varI  
44:       if field varI is "Vibrate" then put "vibrate" into varChimeLocal  
45:       if field varI is "Applause" then put "/sounds for toothbrush app/applause.wav" into varChimeLocal  
46:       if field varI is "Bell" then put "/sounds for toothbrush app/bell.wav" into varChimeLocal  
47:       if field varI is "Bicycle Bell" then put "/sounds for toothbrush app/bicycle_bell.wav" into varChimeLocal  
48:       if field varI is "Chimes" then put "/sounds for toothbrush app/chimes.wav" into varChimeLocal  
49:       if field varI is "Ding" then put "/sounds for toothbrush app/ding.wav" into varChimeLocal  
50:       if field varI is "Laser" then put "/sounds for toothbrush app/laser_x.wav" into varChimeLocal  
51:       if field varI is "Oops" then put "/sounds for toothbrush app/oops.wav" into varChimeLocal  
52:       if field varI is "Slide Whistle" then put "/sounds for toothbrush app/slide_whistle_up.wav" into varChimeLocal  
53:       if field varI is "Tada" then put "/sounds for toothbrush app/tada2.wav" into varChimeLocal  
54:       if field varI is "Trumpet" then put "/sounds for toothbrush app/trumpet_x.wav" into varChimeLocal  
55:         
56:       if i = 1 then put varChimeLocal into varChime1  
57:       if i = 2 then put varChimeLocal into varChime2  
58:       if i = 3 then put varChimeLocal into varChime3  
59:       if i = 4 then put varChimeLocal into varChime4  
60:         
61:     end repeat  
62:    end if  
63:      
64:  end mousedown  

Yeah, there is quite a bit to this script, but that's because I had to take into account the chance that no settings had yet been saved. Line 5 is the key line. It takes whatever data are saved and puts it back into the text field "appdata" on card "data." From there, I can set all of the intervals to the saved settings properly.

Playing Alerts When the iPhone is Locked

Unlike tooth brushing, many other tasks may take quite a long time. The example that comes to mind is giving a 20 or 30 minute presentation where I'd like to have (most likely vibrate) alerts with about three or five minutes to go, then with a minute left, and then a final alert with 15 seconds to go so I can get off the stage for the next person. It is easy to see that the iPhone will likely automatically lock during task like this. I have my iPhone auto-lock after five minutes if not used. I want to be assured that the alerts will come whether the phone is locked or not. Unfortunately, right now although the timer keeps working, the alerts never sound.

I did a search on the LiveCode forums about this and found what should be the solution, the command iphoneSetAudioCategory. Here is a good web page with information about this command:

https://livecode.fandom.com/wiki/Iphonesetaudiocategory

The problem should be solved just by adding the command iphoneSetAudioCategory "playback" to my code. Unfortunately, it isn't working.  I'll continue playing around with it. If I don't have any luck soon, I'll reach out to the LiveCode community in the forums.

Creating an Home Icon for the App

Despite my lack of graphic design skills - something I've written about frequently in this blog - I still enjoy the challenge of graphic design. Here is the design I've come up with for the app's icon on the iPhone home screen:


Though trite, a clock face is obviously synonymous with time. The four different colored squares give the clock face a distinctive look while also symbolizing the four opportunities to set four different times with unique alert sounds. I created this icon - and I'm almost embarrassed to admit it - in PowerPoint. Of all of the graphic tools out there, I find those in PowerPoint to be the most intuitive and simple to use. It's then easy to copy and paste the graphical object into Photoshop for exporting as a PNG file. My motto is always to use the tools that work for me and get the job done.

I also started toying around with some better names for this app other than "Lloyd's Toothbrush Timer App." Brushing my teeth was just the inspiration for the need for this app after all. I have a very bad track record of naming my apps, hence the names "Lloyd's Video Analysis Tool" and "Lloyd's Q Sort Tool." So, yes, I may just wind up calling this something like "Lloyd's Interval Timer App."

But, in an effort to force some extra drops of creativity, I thought I might name this the "This, Then That Timer." I played around with the alliteration of the four Ts in an earlier version of the app's icon, but none of those looked very good. Still, I think this name has potential. Of course, the boring - but descriptive - name of "Multiple Interval Timer" might win the day.

Just a Few Remaining Tasks

Very little is left on my punch list for this app other than solving the auto-lock issue just described. Here is the short list of the remaining tasks:

  • Continue to improve the graphical interface.

  • Improve the timer display to also include the number of seconds until when the next alert will be sounded. A "countdown" displays like this seems like a good idea to me.

So, look for at least one more blog post about this project.


Saturday, December 12, 2020

My Toothbrush Timer App: On My iPhone and Working (Well Enough for Now)

I've made much progress on my app in the past week. In fact, for the past few days I've been brushing my teeth with the help of my iPhone sitting on the edge of the bathroom sink running my app. It works great, just like I had hoped. I can "set it, start it, then forget it" as I start brushing. 

I like to set it to provide some gentle chimes after 50 seconds to let me know the first minute is just about up. Ten seconds later, at the one minute mark, I set it to ring a soft bell to let me it is time to switch "levels" of brushing from bottom to top. Chimes again alert me 50 seconds later to let me know that I have just 10 more to brush. It's actually quite satisfying to hear the final "tada!" alert just as the toothbrush automatically turns itself off. I also find it great for my one minute of mouthwash swishing immediately after brushing. 

Of course, with progress comes many lessons learned. This post contains a few highlights of what I learned over the past week.

Trouble Getting Any Sounds to Play on the iPhone Simulator or the Real iPhone

For a few days, I was baffled by the fact that although the app would play the sound alerts on my Macintosh during development, they would not play on either the iPhone simulator or later when I figured out how to transfer the app to my iPhone. At first, I just figured it was a problem with the simulator, so I ignored the problem. But, when the sounds failed to play on the iPhone itself, I knew something was wrong. I had been consulting this LiveCode tutorial:

https://lessons.livecode.com/m/4069/l/12353-how-do-i-play-sounds-on-an-ios-device

The problem turned out to be that I had failed to provide the full path to the sounds. I had put the sounds in a folder on my Macintosh called "Sounds for Toothbrush App." This folder is at the same level as the app. I used the "copy files" option in the standalone settings, just as the tutorial described. I didn't realize that the path shown in that window had to match the code exactly. Here's a screenshot of the copy files window:


Here was the code I was using:

play specialFolderPath("engine") & "/bell.wav"

The sounds only started to play once I changed the code to the following:

play specialFolderPath("engine") & "/sounds for toothbrush app/bell.wav"

Note: The tutorial uses "resource" instead of "engine" for the code, but I had used "engine" in a previous app I made back in 2013, so I switched to it instead. Not sure if it makes a difference.

Testing the App on an Actual iPhone

I had been dreading this part because I knew it would require me to crawl back into the very frustrating world of the Apple Development Center. I vaguely recall needing all sorts of certificates and something called a provisioning profile. I also recall having to tell Apple the identifying information for the iPhone I wanted to test the app out on. I figured this was going to be a dark day.

Fortunately, I kept good notes the last time I created an iPhone app back in 2013. I found these notes on my Macintosh, dusted them off (metaphorically speaking), and began to follow them blindly. Lo and behold, they pretty much guided me through the entire process. There was one trouble spot. In my old notes (which, of course, were new at the time I wrote them), I got to a point where I had written "Not sure what to do next! So, I check my old notes..." Remember, these "old notes" were old back in 2013. So, I had to figure all that out. But, I updated all of these notes as I went. Here is a link to those updated notes - maybe someone out there in the blogosphere will find them useful. (Beware: This are written only well enough for me to fully understand. All bets are off if you will find them useful.) But, really, I figure I'll be the one needing these notes again when I likely make my next iPhone app seven or eight years from now.

Interestingly, I first tried getting my app to work on an old iPhone SE running an up-to-date iOS, but I had no luck. I then tried to get the app on my iPhone 6S and it worked fine. I think the problem has to do with the age of the phone and the version of Xcode I'm using. 

Updating the App's Interface

I also took some time to update the graphical design of the app. My goal was just to reduce some of its ugliness. Here's a screenshot of the current app:

Current Problems to Solve

After much testing, it's clear that the interval timer is not working properly when the app is paused in the middle of a timing session. I had mentioned this problem in an earlier blog post. At that time, I thought the problem was minor - I called is likely a "rounding error" problem. But, it's much more serious than that. Also, when I use the vibrate alert instead of an audible sound, the timer really gets screwed up. So, I realize I have to completely overhaul the programming of the intervals. I have some ideas of how to go about this, but I'll save that for a future post.

For now, I at least have an app on my iPhone that helps me brush my teeth very evenly.