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  
11:    put the seconds into varSecondsEnd    
12:    put (varSecondsEnd - varSecondsBegin) + varTimeElapsedPrevious into varTimeElapsed //Modified December 6, 2020  
13:    put varSecondsEnd - varSecondsBeginInterval into varTimeElapsedInterval  
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"  
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  
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  
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  
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     
61:     put true into varIntervalCheck  
62:    end if  
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:

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"  
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  
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"  
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"  
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"  
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"  
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"  
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  
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  
61:     end repeat  
62:    end if  
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:

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:

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. 

Wednesday, November 25, 2020

My Toothbrush Timer App: Early Struggles in Going Mobile

LiveCode is an amazing computer programming language and software development environment. The apps you make with it can be run on Macintosh computers, Windows computers, Apple mobile (iOS via iPhone or iPad), and Android. The only thing it hasn't yet figured out completely is online web apps, though the promise of LiveCode's ability to export to HTML5 remains very exciting (though mostly yet unfulfilled).

With that opening lovefest of a paragraph behind me, I now turn to some frustrations. To be fair, the frustrations aren't really with LiveCode, but mainly with Apple and how the Apple Computer Company makes compliance with its software application standards a moving target. 

The first step in taking my toothbrush interval timer to a mobile platform, specifically the iPhone, is to test the app in Apple's iPhone simulator, which is a part of the Xcode development software. To get that to work, one has to have all of the "stars" aligned, with the stars in this case being a metaphor for using the right version of LiveCode with the right version of Xcode and with the right version of MacOS on my laptop. In the end, I did get it all to work, but only after many hours spent downloading different versions of LiveCode, different versions of Xcode, and finally biting the bullet and updating my MacOS. And even then, I had to resort to searching the LiveCode user forums for a last bit of advice to make it all work. I detail my journey here with the hope that it might help others (or most likely just me in a few months time because I will have forgotten all of this by then).

The Challenging in Getting "Everyone to Play Nice" Together

In the end, here are the system and software specifications that are "playing together" well:

  • MacOS version: 10.15.2
  • Xcode version: 11.4
  • LiveCode version: 9.6.0

But, they didn't at first. 

Let's start at the beginning. I had not made a mobile app since I made my little game Lunar Hotel Shuttle, the inaugural project for this blog. This app first appeared in the app store on November 28, 2012. Yes, that's almost exactly eight years ago, so yes, it's been a long time since I made a mobile app. Due to a very faded memory of that experience I knew that all of my software had to be aligned to work, so I crossed my fingers as I clicked the "test" button in LiveCode after choosing iOS as my standalone application setting. If it all worked, then just clicking the "test" button would automatically launch Xcode's simulator followed by LiveCode packaging and bundling the app for mobile, then exporting the app to the simulator for testing. But, did I mention this all only happens if everything is aligned properly? Right, sorry to repeat myself. Not surprisingly, it didn't work.

No worries, I thought. I quickly found this handy dandy reference page on the LiveCode web site showing all of the different configurations one could use:

My Mac's OS was Catalina 10.15.1 and my version of Xcode was 11.1.1. So, I downloaded LiveCode 9.6.0 DP-3 since that was the version of LiveCode that seemed to align with the MacOS and Xcode. That's when the frustration really began. I began exploring one rabbit hole after another, though I did learn a lot along the way. LiveCode 9.6.0 DP-3 could not be "verified" by Apple. After some Googling, I learned a way to bypass Mac's verification process:

LiveCode opened, but I still had no luck getting the iPhone simulator to work. So, I went ahead and downloaded the LiveCode 9.6.0 (this is the "stable" version, not the "developer preview" version). Alas, I was greeted with a message from LiveCode (that kept popping up dozens of times) that I had to upgrade my MacOS to 10.15.2 for my version of Xcode to work. I am always loathe to update my operating system given some very bad past experiences. But, I decided to do it anyway. Well, the only option my Macintosh's update button was giving me was a major upgrade to MacOS Big Sur and no way was I going to do that. So, after a little more Googling, I found Apple's MacOS download page and did the upgrade. Remember, all of these software downloads and installations take time, particularly given my unusually slow Internet speed here in rural Georgia. Plus, I do have a day job, so I can only work on this other "hobby" periodically throughout the day. So, the hours are ticking by. But, as I finished, my confidence now was very high for my next test.

And Again It Didn't Work

At this point, if I had rum in the house I would have started drinking. But, I need to back up a little bit because it almost worked, and more importantly, this was not the first time it had almost worked. That is, the iPhone simulator did launch and LiveCode was giving me messages that it was dutifully creating all of the needed "bundles." But the toothpaste timer app just never appeared on the simulated iPhone. Within about 10 seconds or so, a message popped up from LiveCode asking me if I wanted to keep trying given how long it was taking for the app to load. I pressed "yes" and the message popped up again about 10 seconds later. I eventually said no. When I got this far in some of my previous testing, I just assumed that the stars were still not aligned. That, it turns out, was a faulty assumption. It was at this point I turned to the LiveCode forums and the very helpful community of LiveCode programmers.

LiveCode Forums to the Rescue

I quick found this forum thread in the "iOS Deployment" topic area:

A LiveCode developer named Panos offered the advice of opening the simulator first in Xcode and to wait until the simulated iPhone shows the home screen. Then, go back to LiveCode and try to test the app. Well, that worked! Here are the step to do just that:

  • Right-click on the Xcode app icon in the Macintosh's dock.
  • Choose: "Open Developer tool > Simulator"
  • Make sure the simulated iPhone boots all the way to the home screen.
  • Then, and only then, choose to "test" your app on the simulator from within LiveCode.

Here's a screen shot:

Yeah, pretty ugly, but the most important thing is that the app export and open successfully in the simulator.

A key point to be made and emphasized here is all of the right software was aligned the first time I saw the simulator launch. It was just that the simulated phone wasn't "turned on" yet. Maybe if I kept clicking "yes" to all of those message it would eventually have worked, but who knows. I'm just happy I now know what to do in order to move on.

Time, Finally, to Begin Revising the App for Mobile

Yes. I am finally ready to start the work to revise my app for the mobile platform.

I'll end by including some links to two other useful web pages:

A final good "lesson learned" worth sharing is to search the LiveCode Forums earlier in the troubleshooting process.

Monday, November 23, 2020

My Toothbrush Timer: On the Cusp of Going Mobile

A couple of days ago I had a few extra minutes in the morning to work on my app. I thought I would quickly add the option to pause the timer. This sounded simple enough and I figured I could add this feature in 15 or 20 minutes. Well, about three hours later I had it more or less figured out. You just never know where app development will take you.

The concept sounds simple enough - just stop everything with one click and restart it all with another click. But, this proved to be a more thornier challenge than it would seem at first glance. It stems from the fact that I'm using LiveCode's function the seconds as the way to keep the timer and the intervals accurate. (I explained how the seconds works in LiveCode in my previous blog posting. I recommend reading that first before continuing here.)

Here's a short explanation:

When the timer begins it saves the current time (in seconds) in a variable. For simplicity sake, let's just say that the seconds is equal to 0. The app then continually checks the current time and puts those seconds into the display. When the Stop button is pressed, the current time at that moment is saved into another variable and the timer is stopped. Of course, the seconds - time, in other words - just keeps going. When the Start button is pressed to start the timer up again I can't just restart the timer based the seconds at that moment because the time elapsed includes the time that the timer was paused. So, I have to subtract the paused time from the total time and only show the "running" time in the display. Here's the solution I programmed into the app:

0 seconds: Start the timer

50 seconds: Pause the timer. At this point, 0:50 remains on the display.

150 seconds: Start the timer back up. 150-50 is 100 seconds. So the display needs to show the following:

Current Time (150) - Original Starting Time (0) - Total Time Paused (100) = Total timer seconds to be displayed

And, every time the user pressed the Stop button, the total time paused must be increased accordingly.

All I can say is that I couldn't get it to work for about three hours. Somewhere my logic was failing me. In the end, I think the problem was that I had this algorithm sitting in the wrong place, namely inside the LloydStopWatch procedure I explained in my previous post. After I moved it and a few more tweaks, things seemed to work, more or less. I say "more or less" because there is still a small bug. Somehow one second is sometimes added to the total time elapsed. The problem is minor, but annoying. Here's what can happen. Imagine the timer with two intervals, each two seconds apart. The first interval is signaled with a bell and the second with chimes. So, the pattern is a timer going off every two seconds, first with a bell and then with chimes. So, an alert should be given only when an even number appears on the display. However, sometimes when pause is pressed, the alert sounds moves to odd numbers instead, but at two second intervals. It seems to resemble a kind of "rounding" error, but I can't find the source. So, after those three hours, I decided to just "let it go."

Displaying the Time Correctly

I made one other improvement to the app, namely showing the timer not just in seconds, but in minutes and seconds using the conventional display of Minutes:Seconds. This was a fun challenge to program. So, consider that a total of 147 seconds have gone by. What should the display read? A quick mental calculation results in 2 minutes and 27 seconds, or 02:27. That same mental calculation is shown in this code:

1:    put trunc(varTimeElapsed/60) into varDisplayMinutes  
2:    put (varTimeElapsed)-(varDisplayMinutes*60) into varDisplaySeconds  
3:    if the length of varDisplaySeconds < 2 then put "0"&varDisplaySeconds into varDisplaySeconds  
4:    put varDisplayMinutes&":"&varDisplaySeconds into line 1 of field "my time"  

In line 1, we divide the total seconds elapsed (varTimeElapsed) by 60. Dividing 147 by 60 gives you 2.45. Well, we just care first about the number of minutes. The function trunc truncates, or chops off, any decimal that remains. So, we are left with just two. That goes into the variable varDisplayMinutes.

Now we have to figure out the remaining number of seconds. All we need to do is take the total seconds elapsed (again, varTimeElapsed) and subtract from it the number minutes we just calculated.  Of course, we have to convert those minutes back into seconds first (varDisplayMinutes*60). The result is line 2: 147-120 = 27. We put the 27 into the variable varDisplaySeconds.

Lines 4 pretties everything up before displaying the time elapsed in the field "my time." Using the concatenation symbol "&" we stitch together minutes, a colon in the middle, followed by the seconds.

Adding a Fourth Interval Option and Alert Sound Options

In my last blog post, I alluded to the fact that having at least four intervals is preferred. So, this version of the app added that as well. I also had some fun finding and adding a menu of alert sounds. I downloaded several sound effects using the .wav sound format from this web site:

Here's a screen shot of the current version of the app paused after 97 seconds:

One More Important Step to Go

The app works well enough for me to brush my teeth, except for one thing. I don't want to put my laptop on the bathroom sink. So, the obvious next step is to "go mobile." This will require quite a few changes to the app's interface, such as replacing all of the text input fields with selector wheels. 

I also will need to move the app to my iPhone for testing. I am not looking forward to this. It will first require me to relearn how to create a "provisioning profile" in my Apple Developer Account using my iPhone's serial number and then relearning how to get the app from my computer to the iPhone. I've done it before and I'm sure I can do it again. But, it's been a few years and my memory of doing it isn't very pleasant.

Thursday, November 19, 2020

My Toothbrush Timer: Adding Initial Functionality

I've made progress on this app over the past few weeks, despite only being able to work on it now and then. As usual, my progress in building the app is outpacing my blogging about its construction. So, this blog post is at least the first of two in an effort to catch up. Here I give a report of the design of the app as of about two weeks ago.

Here is a screenshot of the app while running:

One obvious thing I need to work on is showing the time elapsed in the format of minutes:seconds. Using the example of brushing my teeth for 2 minutes, these intervals alert me half-way through (to switch from lower to upper teeth), then a warning 50 seconds later that I only have 10 seconds left, followed by the 2 minute bell. Really, this last alert is not necessary because the toothbrush I use just turns off automatically at 2 minutes. If I wanted to switch between lower and upper every 30 seconds, then just one interval setting of 30 would be needed. In the example of giving myself alerts while making a 12-minute conference presentation - a typical time limit - I would likely add alerts at intervals of 6 minutes (half-way point), 4 minutes (the 10-minute mark), 1 minute (just one minute remaining), and 45 seconds (to give myself 15 seconds to thank everyone and get off the stage). Of course, that would require four intervals, not three, so I would need to add one more interval to the app. (Hint hint: That's a clue about what's coming up in next blog post.)

Telling the Time in LiveCode with the Seconds

Having an accurate timer is fundamental to many applications. The best way to develop a timer in LiveCode is with the function the Seconds. My timer app starts by checking to see what the time is now in seconds. As I write this, it is 7:48:11 am on Wednesday, November 18, 2020. The idea is to make that the starting time when the Start button is pressed. Then, all timers are checking to see how much time has passed since then. So, if the first timer is set to go off 30 seconds later, the time will need to be 7:48:41. But if you think about it, this standard format for telling time is based on a past starting point using an arbitrary system we call the Julian calendar. Today's date and time uses as its starting point the likewise artibrary big "changover" from Before the Common eEa (BCE) to the Common Era (CE), which is just the secular version of BC and AD. The key word there is "arbitrary." Really, any starting point would do as long as we all recognize and accept it as the starting point. In LiveCode, the seconds is based on a similar starting point called the eon, defined in the LiveCode dictionary as follows: 
LiveCode uses midnight, January 1, 1970 as the start of the eon. Date and time functions are computed from that date.
So, the function the seconds gives you the current time in seconds since since very arbitrary starting point. As I write this sentence, the current time, in seconds, according to LiveCode is 1605704460. But now, as I write this sentence the time is 1605704492. So, if you subtract the first from the second you get 32 seconds. That interval is the basis for how my app works.

Doing Two (or More) Things at Once

The important thing to note is that while the timer is running, the app is constantly checking to see if the user presses any buttons. In contrast, many apps work by waiting until the user presses a button to perform a task. The app does the task, then waits for the user to press another button. If the task takes a long time to complete, the user has to wait until the task is completed before any other buttons can be pressed. Doing one thing at a time versus doing multiple things at the same time may sound like a subtle difference, but it requires a special programming technique to pull this off.

Here is the code for the app's engine called MainEventLoop, a procedure I wrote to run everything in the background while the user chooses timing intervals and clicks buttons to stop and stop the app's timer:

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

A procedure called MainEventLoop just runs and runs by looping forever while the app is running. This works by having it "call itself" in line 12 in a very short amount of time (i.e. 50 milliseconds). I know, that's a very weird idea. It's usually very hard for new programmers to grasp. I know it was for me. In that short amount of time, it completes any assigned task while "watching" to see if the user does something, such as clicking a button. There is nothing special about 50 ms. It could be called in one second, but 50 ms is plenty of time for the app to do its work.

Another procedure, LloydStopWatch, has all of the code that actually computes how many seconds have transpired since starting the timer. This code also keeps track of the intervals and knows when play a chime at the end of each interval. Note that line 5 is where the LloydStopWatch procedure is triggered, but only if the variable varRunMainLoop is true. This variable controls whether the timer is active. If the user clicks the Start button it becomes true. If the user clicks the Stop button, it switches to false.

So, how does one get MainEventLoop to start running. The answer is in the code for the button "Start:"

1:  on mouseup  
2:    put 0 into varTimeCurrentLoop  
3:    put 0 into varIntervalTotal  
4:    put 1 into varIntervalTurn  
5:    put false into varRestartInterval  
6:    put empty into field "notes"  
7:    put empty into field "Interval Totals"  
8:    put empty into varTimeLog  
9:    put field "Iminutes1" into varIminutes1  
10:    put field "Iseconds1" into varIseconds1  
11:    put field "Iminutes2" into varIminutes2  
12:    put field "Iseconds2" into varIseconds2  
13:    put field "Iminutes3" into varIminutes3  
14:    put field "Iseconds3" into varIseconds3  
15:    put true into varRunMainLoop  
16:    put the seconds into varSecondsBegin  
17:    put varSecondsBegin into varSecondsBeginInterval  
18:    MainEventLoop  
19:  end mouseup  

Lines 2-18 initiate all of the main fields and variables, essentially getting them ready for the start of a new timing session. Line 15 sets the variable varRunMainLoop to true and line 18 initiates the MainEventLoop procedure.

Lines 9-14 takes whatever interval values the user had entered and passes them on to unique variables for each for use within the LloydStopWatch procedure.

Line 16 essentially checks the current time and becomes the moment in time from which the time elapsed is figured. This value is held by the variable varSecondsBegin. Line 4 in the MainEventLoop procedure continually checks the current time, puts it in a variable named varSecondsEnd, then calls the LloydStopWatch procedure. The time elapsed in seconds is simply the difference between these two values. This simple subtraction problem is happening every 50 ms.

Interestingly, I first put the line "put the seconds into varSecondsEnd" into the LloydStopWatch procedure. But, I experienced some weirdness in the operation of the app. After the app had been running for about 10 minutes, the app started skipping a bunch of seconds which, of course, ruined the reliability of the app. I really couldn't figure out why. I tried all sorts of programming hacks, but none worked. So, in a bit of desperation, I moved this line to the MainEventLoop procedure and it solved the problem. All I can say I had a hunch it would work. I think the problem had something to do with the recursive nature of this MainEventLoop procedure. I've experienced weirdness in this coding technique in previous apps. I actually asked for help in one of the LiveCode forums a couple of years ago about this. Lines 8-11 and the lock/unlock screen code were suggested to me by an expert LiveCode user which solved the problem for me back then. So, I've continued to use this code anytime I use this looping technique. In short, you start building up a backlog of calls for the MainEventLoop procedure and eventually all hell breaks loose. Lines 8-11 clear out all previous calls for the procedure.

Finally, the timer stops when the user presses the "Stop" button - here is the code for the Stop button:

1:  on mouseup  
2:    put false into varRunMainLoop  
3:  end mouseup  

By changing the value of varRunMainLoop to false, it stops the loop within MainEventLoop that had been triggering the LloydStopWatch procedure.

Short List of Things to Do

Actually, the list is quite long, but here are some of the obvious revisions and updates that are needed:

  • Add a fourth timer interval
  • Change the elapsed time format
  • Allow the user to pause the timer
  • Add a choice of alert sounds
  • Add a mobile interface design, including spinner wheels to select time intervals and a vibrate option for alerts

Friday, October 30, 2020

My Toothbrush Timer App

It's been a long time since I started a new LiveCode project. Well, the truth is that I've been very preoccupied with my Q sort project for the past year. Fortunately, my "Lloyd's Q Sort Tool" app has been quite stable for some time now. I've only been making small tweaks to fix small bugs and to add various improvements. So, I've been wanting to do something completely different and have some fun doing it. 

Fortunately, I'm teaching a doctoral course this semester on the topic of design thinking. One of the books we've been reading is Creative Confidence by the brothers Tom and David Kelley. David is one of the co-founders of both the IDEO design company and the d.School at Stanford University. Here's the full reference: 

Kelley, T., & Kelley, D. (2013). Creative Confidence: Unleashing the Creative Potential Within Us All: New York: Crown Business.

I've been using this book for a few years in my teaching and it get great reviews from students. The book's main theme is to inspire everyone to believe that the ability to be creative is in all of us. Rereading the book for the umpteenth time always puts me back into a very creative design mood. The book encourages you to look around for design problems, challenges, and opportunities - big and small. You can't help but to look around your personal world to see things you otherwise take for granted.

Last Sunday, I was reading the book in preparation for class on Monday. So, I was thinking about design a little later on as I was brushing my teeth. Suddenly, an idea hit me. I have an older Oral-B toothbrush that I bought about 10 years ago at the recommendation of my dentist. This battery operated toothbrush has a built-in timer that runs the motor for two minutes - the recommended amount of time to spend brushing one's teeth. In order to make sure I brush evenly - north and south - I've developed a habit where I carefully monitor my Casio wristwatch's stopwatch as I brush to make sure I switch from my lower to upper teeth precisely from time to time in order to brush evenly between both. It suddenly dawned on me that it would great to have a timer app with audio prompts that allow me to punch in various time intervals for a bunch of reminder chimes. The timer option on my iPhone's clock app doesn't meet the need because I need a reminder at each intermediate interval without having to touch the phone (given that my hand is covered in toothpaste "runoff"). My first thoughts of how to design a timer app with the option to add flexible intervals began that moment. 

I also thought of other uses for such a timer app. For example, a favorite exercise of mine is to walk briskly for two or three minutes followed by jogging for two or three minutes. It would be great to have a timer that would sound a chime at two minutes, then three minutes later, and then repeat the sequence. Another use would be when I'm making a presentation in class or at a conference. If my presentation lasts 15 minutes, it would be nice to get a reminder at the 10 minute mark, then three minutes later, then one minute later. And, if I'm presenting with multiple people, the app can signal when the "torch needs to be handed" over to the next presenter.

I quickly created a mock-up using LiveCode. My goal was to show my design thinking students a visual example of my design idea - the first prototype. This first version had no functionality, but it provided a nice visual representation of what I was doing:

This took about 30 minutes. Although the screen looks a little busy, those smaller buttons below the "Start" button would actually come and go as needed for a clean design. As I created this mock-up, the need for those button functions became clear, so adding them acts as notes for my next iteration. For example, the "Stop" button would replace the "Start" button when the app was running. A "Pause" button would also need to appear as soon as the "Start" button was pressed and would be replaced with a "Resume" button when the "Pause" button was pressed. (Honestly, I don't remember why I thought I needed the "Restart" button.) So, all of these are "placeholder" buttons that I'll integrate into the design at some point.

Beginning to Code

On Monday, as I was finalizing my class lesson plans, I couldn't help but start to code some initial functionality into the app. I started by coding how to start and stop a timer using "the seconds" function in LiveCode. This also required me to relearn how to set up a repeating loop in the background while the user interacts with the app, something I learned years ago in one of my earlier gaming/simulation projects. But, I'll save any details about coding for my next post. The main point here to celebrate finding a problem to be solved and beginning to design a solution that offers ways to improve many other problems as well. That's the great thing about design thinking -- even small design ideas are valued. You never know where they might lead. 

Final Thoughts

This is actually a mobile app I could use. I don't plan on keeping the "toothbrush" title, but it's a fun title to keep for now. (I've always struggled with choosing good app titles.) It has also been a long time since I created a mobile app, so I thought this project would also be a good way to reacquaint myself with the process, particularly on the all-to-complicated Apple iOS side.

Oh, one more thing ... I've since learned that the newer Oral-B toothbrushes models now have a 30 second chime reminder. Maybe it's time I buy a new toothbrush.

Monday, December 31, 2018

Are You With Me? A Role-Playing Simulation for First Year Agricultural Education Teachers

New Year's Eve is always a good time for reflection. As the year ends I find myself reflecting a great deal on my research and design work from the past year. I think this is because I've spent this holiday break trying to catch up on my writing for each. In doing so, I've noticed I've not focused nearly as much attention on this blog as I have in the years since I began it. I think the reason for this can be explained by the title of the blog - Learning LiveCode. It turns out I've devoted very little time in 2018 to learning new programming skills with LiveCode and instead I've spent most of the year using my LiveCode knowledge and skill to work on two fairly large software projects. The first is my Q sort tool project, something I've written about extensively here since I started the Q sort project in 2015. The second is a project I've been working on in collaboration with colleagues in UGA's College of Agricultural and Environmental Sciences, specifically the Department of Agricultural Leadership, Education & Communication.

I've led the development of a game for the project titled "Are You With Me?" This game is about helping new agricultural education teachers be successful in both their teaching and their interactions with the community while maintaining a balance in personal and work life in order to avoid teacher burnout.  This game is probably best thought of as a role-playing simulation of being a first year agricultural education teacher in a community that is also new to the Ag Ed teacher.

The game is designed to help new Ag Ed teachers to understand the importance of first coming to know their community and community leaders and then how to think strategically to make decisions that will lead them to earning credibility, respect, and trust of everyone in the community. The goal of the game is to build community engagement while maintaining enthusiasm for teaching. If successful, the teacher will begin securing important structured agricultural experiences for students.

This project has been fun and challenging. First, the project consists of a wonderful group of people. I won't name them here because I don't have permission from them to do so yet (I'll update this posting when I do). Everyone respects each other and everyone brings creative ideas to the table, while also being willing to do a reality check when necessary. The project has been fun for me because I've been able to practice my preferred way of doing design, namely to build an initial prototype of a design idea and let the group react to it. Using this approach, we've slowly arrived at a design that is very solid. I built the first prototype almost exactly two years ago and this is the first time I've written about it.

How the Game Works

Here is a screen shot of the game's main screen:
The game consists mainly of making decisions and dealing with the consequences. Five possible decisions choices are presented to the new teacher at each round, along with information about how much engagement will be created in the community if that decision is a good one. However, the best decisions must match the agricultural profile of the community in which the school resides. If the community's agricultural is largely based on crops, then choosing to form a dairy or cattle club aren't advised. Each decision also has a "credibility threshold." This means that even a decision that is well-suited to the community will not work if the teacher hasn't at least attained as much credibility as shown. It takes time and effort to gain credibility in a community, especially if you are new to it. A new teacher's enthusiasm and high hopes must be tempered with reflective, strategic action. The player can track their credibility in the "Teacher Dashboard" in the bottom portion of the screen.

As engagement in the community increases, as shown in the "Community Involvement Dashboard" in the top portion of the screen, opportunities to create a structured agricultural experience, or SAE, are presented. These SAE opportunities can best be thought of as internship experiences for the teacher's high school students. An example would be a dairy farmer willing to mentor a high school student in the dairy business by having them come to the dairy farm on a regular basis and help out in some way.

If poor decisions are made, or if the credibility threshold of good decisions is ignored, the teacher will slowly lose their enthusiasm. Wouldn't we all if our enthusiastic attempts at making a difference in the community are met with resistance or, worse, indifference? If this happens the player will slowly see their "Teacher Quality of Life" diminish, going from enthusiastic to optimistic to wary to frustrated, and finally to burned out. At that point, they may very likely leave the teaching profession. This is the worst possible outcome of the game.

But, if they are able to maintain their teacher quality of life and begin to gain credibility, followed by increasing community engagement, they will end the round successfully. The player can gauge their success by seeing how long it took them to achieve 100% community engagement - as denoted in "teacher time" - and also how many SAE opportunities they were able to secure. These results become the "score to beat" during the next round. Most important, the teacher/player can reflect on their game performance by consulting the decision log (on the left). This shows each of the decisions they made along with the consequences of each. You might notice some other features, such as the opportunity to consult an expert from time to time to offer guidance and counsel at critical times. The player can also "Seek Community Support" from time to time. This is akin to reaching out to someone in the community for help. Reaching out like this in a humble way automatically garners a little bit of community support. However, getting this kind of "just in time" expert or community assistance is offered only on a limited basis. One cannot - and should not - dip from these wells too often.

We also have created a total of five different community profiles, with the potential to create an unlimited number, in order to keep the game fresh and appealing no matter how many times one has played it.

I should note that this game is but one of several elements to the project, so there are many other resources available to support these new teachers.

That's Great, But...

Yes, I'm sure you can quickly see a serious flaw in the project, namely the poor graphic design. Well, true to my design style, I always focus on the functionality of the project first - in this case, the elements of the game play - and then come back to work on the graphic design. Fortunately, this is a funded project and we have just hired a graphic designer to help us. I've be revising the project with the graphic designer's help in the next few weeks.

I hope to write more about this project in the months ahead.

On that note, I look forward to many rewarding design experiences in 2019 - most of which I'm sure will include LiveCode.