-- Eclipse Magic 
-- Programmed exposure sequence for a solar eclipse
-- Copyright 2017 by Brian Greenberg, grnbrg@grnbrg.org.
-- Modified by G Abramson 2019-2020
-- Distributed under the GNU General Public License.
-- 1. Set TEST or SHOOT
-- 2. Set Contact times for test and shoot
-- 3. Set Exposure settings for all phases
-- 4. Check (set) camera time
-- 5. Run script from ML menu
require ("logger")
-- Variable definitons that have to go here.  Ignore them.
c1 = {}
c2 = {}
c3 = {}
c4 = {}
-- *** TEST: SET 1. SHOOT: SET 0. ***********************************************
-- Test: beeps and logs, no shoot, time starts counting with script run. 
-- Shoot: actually shoot, log, timed with camera clock
TestBeepNoShutter = 0
-- *** CONTACTS *****************************************************************
-- Set the 4 contact times here.  Time zone is irrelevant, as long as your camera and these
-- times are the same.  Make sure the times are correct for your location, and that your camera
-- is accurately set to GPS or NTP time.
--
if ( TestBeepNoShutter == 0 )
then
-- Piedra del Įguila (playas) 70 00 31.91 W 40 03 16.16 S 
	c1.hr = 11; c1.min =  45; c1.sec = 40;
	c2.hr = 13; c2.min =  08; c2.sec = 05;
--  MAX     13	          09           05 
	c3.hr = 13; c3.min =  10; c3.sec = 02;
	c4.hr = 14; c4.min =  35; c4.sec = 50; 
else
-- Testing:
	c1.hr = 00; c1.min =  00; c1.sec = 30;
	c2.hr = 00; c2.min =  08; c2.sec = 07;
	c3.hr = 00; c3.min =  10; c3.sec = 01;
	c4.hr = 00; c4.min =  15; c4.sec = 30;
end
--
-- Send log information to file ECLIPSE.LOG at the
-- top directory of the camera card.  Useful for testing.
--
LogToFile = 1
LoggingFile = nil
-- APERTURE
-- Set an aperture value.  The script assumes that the aperture stays constant throughout the
-- eclipse.  The camera will (try to) set this aperture (f-number) at the beginning of the script.
-- Useful, as it is easy to forget this, if you are shooting with a regular camera lens.
-- If the script and camera are being used with a fixed aperture lens (or telescope), then
-- set the "SetAperture" to 0.
SetAperture = 1
Aperture    = 8    -- Aperture for totality
C23Aperture = 16   -- Smaller aperture for nicer ring and beads
PAperture   = 11   -- Aperture for partial (filtered)
--
-- If you shoot with LiveView active, you will reduce the amount of mirror slap, giving
-- less vibration.  But, some cameras (Such as the 5DmkII.) have a limited range of shutter 
-- speed when LV and Movie mode are enabled.  This reduces the available speeds from 30 seconds 
-- through 1/8000th of a second to 1/30th to 1/4000th of a second.  Exceeding that range while 
-- LV and Movie mode are enabled will crash the script.
--
-- Setting this variable to 1 will, before taking any images, check if LV is running, and if it
-- is, will tell you to turn it off.  If you have disabled movie mode (a menu option on some cameras,
-- like the 5DmkII, and a hardware switch on others) you should set this to 0.
--
-- Test your camera.  If the exposure speeds you want crash the script in LV, check how to disable
-- movie mode.
--
WarnLiveView = 0
--
-- If you're shooting in Live View (to reduce mirror slap) you'll probably still want to check
-- your focus occasionally.  However, the status display of the script pretty much fills the screen,
-- making this difficult.  Enabling HideConsole will display the console for the indicated number
-- of seconds both before and after the next shutter event, and turn the console off between.  This
-- also gives you an idea of how much time you have before the next shutter event -- if the console
-- is hidden, you have at least ConsoleShowDelay seconds to mess around.  If you turn this off, the
-- script will start with the console displayed, and you will have to manage it through the Magic
-- Lantern menu.  If you turn it off, it stays off until you turn it on again.
--
HideConsole = 1
ConsoleShowDelay = 30
--
-- Even if you shoot with Live View with Silent Mode 1 (which forces the mirror to be 
-- locked up, and eliminates the vibration from the shutter opening) there will be slight 
-- vibrations introduced as the shutter closes.  According to Jerry Lodriguss at
-- http://www.astropix.com/wp/2017/07/17/mirror-slap-and-shutter-shock/ these vibrations
-- are most prominent between 0.125s and 2s, and can be somewhat mitigated by a slight 
-- delay before exposure, to allow the vibration to dampen a bit.
--
-- Enabling this option lets you set a range of shutter speeds to delay before, and 
-- how long (in milliseconds) to pause.
--
DoShutterShockDelay   = 1
SlowestDelayedShutter = 2
FastestDelayedShutter = (1/8)
ShutterShockDelayMS   = 300		-- Value is in milliseconds!
-- ** EXPOSURE SETTINGS *****************************************************************
-- Partial phase settings. 
--
PartialISO            = 100
PartialShutterSpeed   = (1/80)  -- Filter Saracco OK
PartialMarginTime     = 30			-- Number of seconds after C1 or C3 and before C2 or C4 to start exposures
PartialExposureCount  = 72   		-- Number of partial phase exposures before and after totality
PartialDoBkt          = 1		  	-- Do you want to do exposure bracketing?  1 - yes, 0 - no
PartialBktStep        = 0.666667		-- Number of f-stops in each step.  Can 0.333333, 1, 2, etc
PartialBktCount       = 1				-- How many brackets on each side of the neutral exposure?
-- ** BAILY's BEADS ****************************************************************
-- Do a fast burst of exposures at C2 and C3, to try to get Baily's beads and chromosphere.
-- You will need to know how many exposures your camera will buffer, and how long it takes the
-- buffer to fill.  At "StartOffset" seconds before C2 or C3, the camera will take "BurstCount"
-- exposures, as fast as it can.  You should adjust C23StartOffset so that burst of image straddles 
-- the contact time.  Note that, between the setting of the camera clock and the jitter in this
-- script, there will be some error in the timing.  +/- half a second or more is possible.
--
C23BurstCount        = 7 	-- Note that most Canon DSLRs can't take more than 13-14 RAW images
								          -- in a burst before the buffer is full, and they slow to ~1 image/second.
C2BurstStartOffset   = 4
C2BurstTime          = 8
C3BurstStartOffset   = 4
C3BurstTime          = 8
C23BurstISO          = 100
C23BurstShutterSpeed = (1/800)
-- ** DIAMOND RING ********************************************************************
-- Do a fast burst of exposures before C2 and after C3, to try to get the diamond ring.
--
-- Be careful setting the RingStartOffset, Count and Time.  If the burst of images for the
-- pre-C2 rings runs longer than expected, it can cause the pre-C2 Baily's Beads exposures to
-- be skipped.
--
-- The post-C3 Rings exposures (if enabled) will run immediately after the post-C3 Baily's Beads
-- exposures.
--
DoRing          = 1		-- Are we going to try for a burst for the diamond ring?
RingStartOffset = C2BurstStartOffset + 5  -- How long before C2/after C3 to start?  Be careful that
										 -- this does not interfere with the Baily's burst!
RingBurstCount  = 3  -- How many images?
RingBurstTime   = 4  -- If we're doing a manual burst, how long should it last?
RingBurstISO    = 400
RingBurstShutterSpeed = (1/60)
-- ** TOTALITY ***************************************************************************
-- During the time between C2 and C3, the script will run back and forth between the 
-- "MinShutterSpeed" and "MaxShutterSpeed" as quickly as possible, with an extra 2 long exposures
-- at midpoint.  "ExpStep" is the size of the f-stop variation, and can be set to 0.333333, 1, 2, etc.
-- Min, Max and PrefISO:  Totality exposures will run (where possible) at PrefISO.  However, there
-- will also be exposures at MinShutterSpeed from MinISO to PrefISO, and MaxShutterSpeed from
-- PrefISO to MaxISO.
--
TotalityMinISO          = 100 
TotalityMaxISO          = 800
TotalityPrefISO         = 200
TotalityMinShutterSpeed = (1/250) -- (MinShutterSpeed is the *fastest* speed to use.)
TotalityMaxShutterSpeed = 1       -- 1 sec OK (MaxShutterSpeed is the *slowest*, longest speed used.)
TotalityExpStep         = 1  
-- ** ASHEN LIGHT **********************************************************************
-- One of the more difficult exposures to capture is earthshine -- the surface of the moon,
-- illuminated by light reflected from the earth.
--
-- The best time to do this is at the point of maximum eclipse, where the sun is centred behind
-- the moon, as much as possible.  Exposures here are kind of guesswork, and I have actually turned this
-- off by default.
--
DoMaxExposures  = 1		-- Number of (possibly bracketed) exposures to take at max-eclipse.
MaxOffset       = 3   -- How long before max eclipse to start these exposures.  You'll have to test
						          -- or use math to determine this value.  (Value is in seconds.)
NumMaxExposures = 1
DoMaxBrackets   = 1		-- Brackets?
NumMaxBrackets  = 1 
MaxBracketStep  = 1
MaxISO = 800
MaxShutterSpeed = 1
--
-- Optionally enable high speed burst mode
--
-- As originally written, the script used the camera.burst() function, to take a burst of
-- images as quickly as possible, as though the shutter button was held down.  This had two issues:
-- The first being that the buffer in the camera fills after around 2-3 seconds when shooting
-- RAW, and then slows drastically, and the second being that once called, the camera will take
-- exposures until the requested number of images are captured, and if the buffer fills, this may
-- take considerably longer than expected.
--
-- I have (by default) replaced this function with a burst function that tries to take a given number 
-- of images over a set period of time, each with a single shutter release call.  This is much slower 
-- (two or three frames per second, tops) but therefor fills the buffer more slowly, and allows the script
-- to abort the burst (and take fewer than the requested number of images) if the capture runs past the 
-- specified time limit.
--
-- If you prefer the old high speed burst call, set this variable to 1.
--
UseBurst = 0
-- ** END SETTINGS **************************************************************************************
-- ** START CALCULATIONS ***  BE CAREFUL BELOW THIS LINE ************************************************
--
-- Times are easiest to deal with in seconds.  This would be painful if they crossed over midnight,
-- but late-night solar eclipses are rare.
--
c1_sec = c1.hr * 3600 + c1.min * 60 + c1.sec 
c2_sec = c2.hr * 3600 + c2.min * 60 + c2.sec 
c3_sec = c3.hr * 3600 + c3.min * 60 + c3.sec 
c4_sec = c4.hr * 3600 + c4.min * 60 + c4.sec 
max_sec = math.floor(c2_sec + ((c3_sec - c2_sec) / 2))
MaxOffset = MaxOffset * DoMaxExposures -- This is ugly, and shouldn't be here.
tick_offset   = 0
TestStartTime = 0
--
-- Log to stdout and optionally to a file
--
function log (s, ...)
	local str = string.format (s, ...)
	str = str .. "\n"
	if (LogToFile == 0 or LoggingFile == nil)
	then
		io.write (str)
	else
		LoggingFile:write (str)
	end
	return
end
--
-- Open log file
--
function log_start ()
	if (LogToFile ~= 0)
	then
		local cur_time = dryos.date
		-- Opening logger with long filename fails. Works with short name.
		-- local filename = string.format("eclipse_%04d%02d%02d_%02d%02d%02d.log", cur_time.year, cur_time.month, cur_time.day, cur_time.hour, cur_time.min, cur_time.sec)
		-- local filename = string.format("EM%02d%02d.log", cur_time.hour, cur_time.min)
		local filename = string.format("eclipse.log")
		print (string.format ("Open log file %s", filename))
		LoggingFile = logger (filename)
	else
		print (string.format ("Logging not configured"))
	end
end
--
-- Close log file
--
function log_stop ()
	if (LogToFile ~= 0)
	then
		print (string.format ("Close log file"))
		LoggingFile:close ()
	end
end
--
-- Get the current time (in seconds) from the camera's clock.
--
function get_cur_secs ()
	local cur_time = dryos.date
	local cur_secs = (cur_time.hour * 3600 + cur_time.min * 60 + cur_time.sec)
	
	if ( TestBeepNoShutter == 1 )
	then
		cur_secs = (cur_secs - TestStartTime)	 -- If we're testing, start the clock at 
 													                 -- now, not actual time.
	end	
	return cur_secs
end
--
-- Take a time variable expressed in seconds (which is what all times are 
-- stored as) and convert it back to HH:MM:SS
--
function pretty_time (time_secs)
	local text_time = ""
	local hrs  = 0
	local mins = 0
	local secs = 0
	
	hrs  =  math.floor(time_secs / 3600)
  mins = math.floor((time_secs - (hrs * 3600)) / 60)
	secs = (time_secs - (hrs*3600) - (mins * 60))
	
	text_time = string.format("%02d:%02d:%02d", hrs, mins, secs)
	
	return text_time
end
--
-- Take a shutter speed expressed in (fractional) seconds and convert it to 1/x.
--
function pretty_shutter (shutter_speed)
	local text_time = ""
	if (shutter_speed >= 1.0)
	then
		text_time = tostring (shutter_speed)
	else
		text_time = string.format ("1/%s", tostring (1/shutter_speed))
	end
	return text_time
end
--
-- Hurry up and wait for the next important time to arrive.
--
-- Leave the console displayed for 60 seconds at the start and end of 
-- a wait.  Turn it off between, so that tracking can be done via live view, etc.
--
function wait_until (done_waiting)
	local counter = get_cur_secs()
	local next_sec = 0
	local show_console = ConsoleShowDelay
	local console_visible = 1
	
	console.show()
	
	log ("Waiting for %s in %d seconds.", pretty_time(done_waiting), done_waiting - counter)
	
	repeat
		task.yield (1000) -- Let the camera do other tasks for a second.
		
		if ((HideConsole == 1) and (show_console > 0))
		then
			show_console = show_console -1
		elseif ((HideConsole == 1) and (show_console == 0))
		then
			console.hide()
			show_console    = -1
			console_visible = 0
		end
		
		if ((HideConsole == 1) and ((done_waiting - counter) < 30 ) and (console_visible == 0))
		then
			console.show()
			console_visible = 1
		end
		
		counter = get_cur_secs()
					
	until (counter >= (done_waiting - 1))
			
	if ( counter < done_waiting)	
	then
									-- Loop /should/ exit the second before we are done. But
									--  It's possible that it could exit early in our target
									--  second. If so, we don't want to wait around to 
									--  (done_waiting + 1) to exit.
		next_sec = (1000 - ((dryos.ms_clock - tick_offset) % 1000))
		msleep (next_sec) -- Hard sleep, don't let anything else have priority.
	end
				
end
--
-- Set up the camera, and take a picture.  Also deals with any requested bracketing.
--
function take_shot(iso, shutter_speed, dobkt, bktstep, bktcount)
	local bktspeed = 0.0
	
	if ((lv.enabled == true) and (WarnLiveView == 1))
	then
		print ("TURN LIVEVIEW OFF (OR DISABLE MOVIE MODE) AND PRESS A BUTTON!!!")
		do_beep()
		key.wait()
	end
	
	camera.iso.value = iso
	
	if (dobkt == 0)
	then	-- Single exposure
		camera.shutter.value = shutter_speed
		log ("Click! Time: %s  ISO: %s  shutter: %s", 
			pretty_time(get_cur_secs()), tostring(camera.iso.value), pretty_shutter(camera.shutter.value))
		if (DoShutterShockDelay == 1)
		then
			if ((shutter_speed >= FastestDelayedShutter) and (shutter_speed 
				<= SlowestDelayedShutter))
			then
				task.yield(ShutterShockDelayMS)
			end
		end
	
		if (TestBeepNoShutter == 0) 
		then
			camera.shoot(false)
			task.yield(10) -- Exposures can take time.  Give other stuff a chance to run.
		else
			beep(1,50)
			task.yield (600 + camera.shutter.ms)
		end
		
	else	-- Bracketing exposure
		    -- Loop through the requested number of exposure brackets.
		for bktnum = bktcount,(-1 * bktcount),-1 do
			bktspeed = shutter_speed * (2.0^(bktnum * bktstep))
			camera.shutter.value = bktspeed
			log ("Click! Time: %s  ISO: %s  shutter: %s", 
				pretty_time(get_cur_secs()), tostring(camera.iso.value), pretty_shutter(camera.shutter.value))
			if (DoShutterShockDelay == 1)
			then
				if ((shutter_speed >= FastestDelayedShutter) and (shutter_speed 
					<= SlowestDelayedShutter))
				then
					task.yield(ShutterShockDelayMS)
				end
			end
			
			if (TestBeepNoShutter == 0) then
				camera.shoot(false)
				task.yield(10) -- Give other stuff a chance to run
			else
				beep(1,50)
				task.yield ((600 + camera.shutter.ms))
			end
		end
	end
end
--
-- Burst is simpler than single shot, because no brackets.  Set the camera and shoot.
--
-- I have tried replacing this with multiple calls to "camera.shoot()", but it is still considerably
-- slower than "camera.burst()", and can also crash the camera -- nothing permanent, but still
-- not something I want to put out.
--
function take_burst (count, iso, speed)
	camera.shutter.value = speed
	camera.iso.value = iso
	
	if ((lv.enabled == true) and (WarnLiveView == 1))
	then
		print ("TURN LIVEVIEW OFF (OR DISABLE MOVIE MODE) AND PRESS A BUTTON!!!")
		do_beep()
		key.wait()
	end
	
	log ("Burst! Time: %s  ISO: %s  shutter: %s  count: %d", 
		pretty_time(get_cur_secs()), tostring(camera.iso.value), pretty_shutter(camera.shutter.value), count)
	
	if (TestBeepNoShutter == 0)
	then
		camera.burst(count)
		task.yield(10)
	else
		beep(3,50)
		task.yield (4000 + (count * camera.shutter.ms))		
	end
end
--
-- Take X pictures over Y seconds
--
-- The camera.burst() function is useful for taking exposures as fast as possible, but is
-- limited by the camera's buffer space.  Depending on the body, burst mode will fill the buffer in 
-- around 2 seconds.  Even the slower cameras will fill the buffer with RAW images in 3-4 seconds, 
-- which is too fast to reliably capture Baily's Beads.  Switching to JPG would help, but must be 
-- done manually.  Not a good option for several reasons.
--
-- This function implements a manually controlled burst mode, which stretches out the exposure speed.
-- This spreads the time where the ~14 exposures that generally fit into the buffer out over a longer
-- time, and also gives the camera time to write to the card.  Instead of 14 frames over 4 seconds,
-- (~4fps) then slowing to maybe 4 frames every 3 seconds (1.5fps), we might be able to sustain 
-- 2fps for 10 seconds.  Actual best framerate and duration will need testing for each camera and
-- memory card.
--
-- Timing is more important than number of exposures, so this function will exit at the end of the
-- specificed timespan, even if the required number of images have not been taken yet.
function take_timed_burst(count, timespan, iso, speed)
	local start_time     = dryos.ms_clock -- Millisecond clock time that we're starting.
	local end_time       = (dryos.ms_clock + (timespan * 1000)) -- Clock time (in ms) where that we're done.
	local burst_interval = ((timespan * 1000) / count) -- Time between shutters, in milliseconds.
	local time_now       = start_time
	local pause_time     = 0
	local exposure_num   = 0
	local last_time      = 0
	
	for exposure_num = 1, count, 1
	do
		last_time =time_now
		time_now  = dryos.ms_clock
		take_shot (iso, speed, 0, 0, 0)
		if (time_now > end_time)
		then
			return -- No time for another image.
		else
			pause_time = ((start_time + burst_interval * exposure_num) - (dryos.ms_clock + 75))
			if (pause_time > 0) -- Pause for the next interval to pass, if we're not running late.
			then
				task.yield(pause_time)
			end
		end
		
	end
end
-- 
-- Simple camera beep.
--
function do_beep()  
		beep (5,100)
end
--
-- Take the spaced exposures for the C1-C2 and C3-C4 periods.  Take the margin times off either
-- end, split the time into the right intervals, and fire off take_picture()
--
function do_partial (start_phase, stop_phase, which_partial)
	local image_time = 0
	local image_interval = math.floor((stop_phase - start_phase) / (PartialExposureCount))
	local exposure_count = 0
	
	if (SetAperture == 1)
	then
		camera.aperture.value = PAperture -- Set aperture for partial phase shots
		log ("Aperture %s for partial phase shots", PAperture)	
	end	
	
	-- In a series of (PartialCount + 1) images, totality is either
	--	the first or last image.  This arranges the timing so that there
	--	will be a equidistance set of ((2 x PartialCount) + 1) exposures,
	--	with totality properly centered.
	
	if (which_partial == "Pre")
	then
		image_time = start_phase
	else
		image_time = start_phase + image_interval
	end
	
	if ( get_cur_secs() >= stop_phase ) -- Are we past this phase already?
	then
		log ("Skip %s Partial. Finished %d seconds ago.", which_partial, (get_cur_secs() - stop_phase))
		return
	end
	
	repeat
	
		log ("%s Partial: %d/%d  Interval: %d s  Remaining: %d",
			which_partial, exposure_count + 1, PartialExposureCount, image_interval, stop_phase - get_cur_secs())
		if (get_cur_secs() <= image_time)
		then
		
-- Reminder to center sun
			wait_until (image_time - 30)
			print()
			print("********************************************************")
			log  ("30 seconds to partial shot! Center!")
			print("********************************************************")
			print()
			do_beep()
-- Shoot		
			wait_until(image_time)
			take_shot (PartialISO, PartialShutterSpeed, PartialDoBkt, PartialBktStep, PartialBktCount)
			
		end
		
		image_time = image_time + image_interval
		exposure_count = exposure_count + 1
	    
-- Reminder at mid partial
		if ( exposure_count == math.floor(PartialExposureCount/2)+1 )
		then
  		  print()
		  print("********************************************************")
		  log  ("Mid partial phase. Check focus w. lunar limb!")
		  print("********************************************************")
		  print()		
		  do_beep()
		end
		
	until (exposure_count >= PartialExposureCount)
	
end
--
-- Start the burst shot a little before C2, then start running through exposure settings, going from
-- short, fast exposures to slow, long exposures and then back to short, until just before the midpoint
-- of the eclipse.  Take two long exposures at that point, for good measure.
--
function do_c2max()
	local cur_shutter_speed = 0
	local CurISO = 0
	
	if ( get_cur_secs() >= max_sec ) -- Are we past this phase already?
	then
		log ("Skip C2->Max. Finished %d seconds ago.", (get_cur_secs() - max_sec))
		return
	end
	
	if (get_cur_secs() <= (c2_sec - (math.max(RingStartOffset, C2BurstStartOffset) + 30)))
	then
		log ("Main C2->Max loop for %d seconds.", (c2_sec - (math.max(RingStartOffset, C2BurstStartOffset) + 30)))
		wait_until (c2_sec - (math.max(RingStartOffset, C2BurstStartOffset) + 30))
		print()
		print("********************************************************")
		log  ("30 seconds to C2! Remove Filter! Center! ")
		print("********************************************************")
		print()
		do_beep()
	end
	
	if (SetAperture == 1)
	then
		camera.aperture.value = C23Aperture -- Set aperture for C2 shots
		log ("Aperture %s for C2 shots", C23Aperture)
	end	
	
	if ((get_cur_secs() <= (c2_sec - RingStartOffset) ) and ( DoRing == 1)) -- Are we taking Diamond Ring shots?
	then
		wait_until (c2_sec - RingStartOffset)
		if (UseBurst == 1)
		then
			take_burst (RingBurstCount, RingBurstISO, RingBurstShutterSpeed)
		else
			take_timed_burst (RingBurstCount, RingBurstTime, RingBurstISO, RingBurstShutterSpeed)
		end
	end
	
	if ( get_cur_secs() < c2_sec ) -- Have we passed the burst for Baily's beads?
	then
		wait_until (c2_sec - C2BurstStartOffset)
		if (UseBurst == 1)
		then
			take_burst (C23BurstCount, C23BurstISO, C23BurstShutterSpeed)
		else
			take_timed_burst (C23BurstCount, C2BurstTime, C23BurstISO, C23BurstShutterSpeed)
		end
		print()
		print("********************************************************")
		log  ("Post C2 warning!")
		print("********************************************************")
		print()		
		do_beep()
		
	end
	
	cur_shutter_speed = TotalityMinShutterSpeed
	CurISO = TotalityMinISO
	if (SetAperture == 1)
	then
		camera.aperture.value = Aperture -- Reset aparture for totality shots
		log ("Aperture %s for totality", Aperture)	
	end	
	
	repeat
	
		take_shot(CurISO, cur_shutter_speed, 0, 0, 0)
		if (CurISO < (TotalityPrefISO * 0.95))
		then
			CurISO = CurISO * 2.0^TotalityExpStep
		elseif ((CurISO < (TotalityPrefISO * 1.1)) and (cur_shutter_speed < (TotalityMaxShutterSpeed * 0.95)))
		then
			cur_shutter_speed = cur_shutter_speed * 2.0^TotalityExpStep
		elseif (CurISO < (TotalityMaxISO * 0.95))
		then
			CurISO = CurISO * 2.0^TotalityExpStep
		else
			cur_shutter_speed = TotalityMinShutterSpeed
			CurISO = TotalityMinISO
		end
			
	until (get_cur_secs() >= (max_sec - MaxOffset)) -- Stop, and leave time to do the mid-eclipse earthshine
													-- exposures.
end
--
-- do_max -- Take a number of long exposures at the time of maximum eclipse, to try to capture
-- an earthshine image.  These can be bracketed.
--
function do_max()
	if (DoMaxExposures == 0)  -- Are we doing this?
	then
		log ("Skip Max Eclipse long exposures. Not configured.")
		return 		-- Nope.
	end
	if ( get_cur_secs() >= max_sec) -- Have we passed max already?
	then
		log ("Skip Max. Finished %d seconds ago.", (get_cur_secs() - max_sec))
		return
	end
	for count_max_exp = NumMaxExposures , 1 , -1 do
		log ("do_max: MaxISO=%d, MaxShutterSpeed=%s, DoMaxBrackets=%d, NumMaxBrackets=%d, MaxBracketStep=%d",
			MaxISO, tostring(MaxShutterSpeed), DoMaxBrackets, NumMaxBrackets, MaxBracketStep)
		take_shot(MaxISO, MaxShutterSpeed, DoMaxBrackets, MaxBracketStep, NumMaxBrackets)
	end
	
end
 
--
-- Similar to do_c2max, but reversed.  Exposures run from longest to shortest (and then repeat), 
-- the burst starts just before C3, and there are no bonus exposures.
--
function do_maxc3()
	local cur_shutter_speed = 0
	local CurISO = 0
	if ( get_cur_secs() >= (c3_sec + RingStartOffset) ) -- Are we past this phase already?
	then
		log ("Skip Max->C3. Finished %d seconds ago.", (get_cur_secs() - (c3_sec + C3BurstStartOffset)))
		return
	elseif ( get_cur_secs() < (c3_sec - C3BurstStartOffset) ) -- Do we have time for some totality exposures?
	then
		cur_shutter_speed = TotalityMaxShutterSpeed
		CurISO = TotalityMaxISO
		log ("Main Max->C3 loop for %d seconds.", (c3_sec - C3BurstStartOffset - get_cur_secs()))
		repeat
			take_shot(CurISO, cur_shutter_speed, 0, 0, 0)
			if (CurISO > (TotalityPrefISO * 1.05))
			then
				CurISO = CurISO / 2.0^TotalityExpStep
			elseif ((CurISO > (TotalityPrefISO * 0.95)) and (cur_shutter_speed > (TotalityMinShutterSpeed * 1.05)))
			then
				cur_shutter_speed = cur_shutter_speed / 2.0^TotalityExpStep
			elseif (CurISO > (TotalityMinISO * 1.01))
			then
				CurISO = CurISO / 2.0^TotalityExpStep
			else
				cur_shutter_speed = TotalityMaxShutterSpeed
				CurISO = TotalityMaxISO
			end
						
		until (get_cur_secs() >= (c3_sec - (C3BurstStartOffset + 3)))
		print()
		print("********************************************************")
		log  ("3 seconds to C3!  Filter warning!")
		print("********************************************************")
		print()
		do_beep()
			
	end
	
	if (SetAperture == 1)
	then
		camera.aperture.value = C23Aperture -- Set aperture for C3
		log ("Aperture %s for C3 shots", C23Aperture)
	end	
	
	wait_until (c3_sec - C3BurstStartOffset)
	
	if (UseBurst == 1)
	then
		take_burst (C23BurstCount, C23BurstISO, C23BurstShutterSpeed)
	else
		take_timed_burst(C23BurstCount, C3BurstTime, C23BurstISO, C23BurstShutterSpeed)
	end
	
	if (DoRing == 1)
	then
		if (UseBurst == 1)
		then
			take_burst (RingBurstCount, RingBurstISO, RingBurstShutterSpeed)
		else
			take_timed_burst(RingBurstCount, RingBurstTime, RingBurstISO, RingBurstShutterSpeed)
		end
	end
	print()
	print("*************************************************************")
	log  ("End of totality! Replace filter! Display Off!")
	print("*************************************************************")
	print()
	do_beep()
end
--
-- The ringleader.
--
function main()
	local starttime
	local offset = 0
	local offset_count = 0
	starttime = get_cur_secs()
	TestStartTime = starttime
	
    menu.close()
    console.show()
	log_start ()
	--
	-- The camera maintains a millisecond timer since power-on.  We can use this to
	-- get close to the beginning of a given second.  I think.
	--
	event.seconds_clock = function (ignore)
	
	offset = offset + (dryos.ms_clock - (1000 * offset_count))
	offset_count = offset_count + 1
			
	return true
	
end
	print ()
	print ()
	print ("-------------------------------------")
	print ("  Eclipse Magic")
	print ("  Copyright 2017, grnbrg@grnbrg.org")
	print ("  Mod 2019-2020 by G Abramson")
	print ("  Released under the GNU GPL")
	print ("-------------------------------------")
	print ()
	print ("Starting 10 second timing calibration....")
	--
	-- There is a fair amount of jitter in the event timer.  Averaging over 10 seconds will
	-- give us a reasonable offset.
	--
	task.yield(10500)
	-- Turn off the second_clock event timer.
	event.seconds_clock = nil
	
	tick_offset = (math.floor(offset / offset_count) % 1000)
	
	print ("Done!")
	print ()
	log ("TestBeepNoShutter: %d", TestBeepNoShutter)
	log ("C1: %s", pretty_time(c1_sec))
	log ("C2: %s", pretty_time(c2_sec))
	log ("C3: %s", pretty_time(c3_sec))
	log ("C4: %s", pretty_time(c4_sec))
	-- If the camera is not in manual mode, trying to set the shutter speed throws errors.
	-- Check to make sure we are in manual mode, and refuse to run if we're not.
	if (camera.mode == MODE.M)
	then
--		if (SetAperture == 1)
--		then
--			camera.aperture.value = Aperture
--           log ("Aperture %s", Aperture)			
--		end
	
		do_partial ((c1_sec + PartialMarginTime), c2_sec, "Pre")
	
		do_c2max()
		
		do_max()
	
		do_maxc3()
	
		do_partial (c3_sec, (c4_sec - PartialMarginTime), "Post")
		
	else
		beep (5, 100)
		log  ("Camera must be in manual (M) mode!!")
		print()
		print("Press any button to exit the script.  Change the mode and re-run.")
		key.wait()
	end
		
	log ("All done. Normal exit.")
	log_stop ()
	print("Press any button to exit the script.")
	key.wait()
    console.hide()
	
end -- Done.  Hope there were no clouds.
main() -- Run the program.
-- CHANGES
-- 1.0.1
	-- Stopped running the seconds_clock event at all times, and moved the tick_offset
		 -- calculation to the setup at the start of execution
	-- Fixed the pretty-printing of the timer
	-- Fixed the sign of the C23BurstStartOffset in do_maxc3
	-- Fixed the calculation of the difference between current time and the next second
		 -- in wait_until()
	-- Added code to turn off the console during long waits
	-- Massaged the end-of-exposure-bracketing conditions in do_c2max() and do_maxc3()
	
-- 1.1
	-- Changed the partial phase exposure logic, so that instead of there being an exposure
		 -- at C1 and C2, there is an exposure at C1 and an exposure at (C2 - exposure_interval)
		 -- so that the totality images are properly centered between the requested partial phase
		 -- exposures.
	-- Added an alarm at 30 seconds before C2 and 3 seconds before C3 to alert for any filter changes.  
		 -- There is also a beep after the C2 and C3 bursts to flag any needed changes.
	-- Split the C23BurstStartOffset into separate variables, to allow an asymmetric burst over
		 -- each period.  (ie: 10 seconds before C2 to 3 seconds after C2)
		
-- 1.2
	-- Added ISO brackets to totality exposure sequence.  Totality now has a preferred ISO, 
		 -- and will shift to that ISO at the fastest or slowest shutter speeds, then use that
		 -- preferred ISO for the requested range of shutter speeds, then shift ISO to the end of
		 -- the requested range.
	-- Option to stop LiveView before touching the shutter controls.
	-- Removed the two long exposures at max eclipse -- probably not needed.
	
-- 1.2.1
	-- Changed call to lv.stop() to a beep, and instruction to the user to turn off LV.
		 -- lv.stop() doesn't seem to work.
	-- Added print statements to explain filter warning beeps.
	
-- 1.3.0
	-- Changed the startup timing loop to be more accurate if the script is started close to
	   -- a second boundary.  (The average offset of 999ms and 1ms is 1ms, not 500ms)
	-- Corrected an error in wait_until() -- Used div, where modulus was correct, and had
	   -- the tick_offset correction wrong.  Don't code while tired.  Thanks to
	   -- matman730 for pointing out this goof.
	-- Improved the configuration comments around TestBeepNoShutter.  They apparently weren't
	   -- clear as I thought.
		
-- 1.4.0  (Not released)
	-- Attempt to implment the changing of the file prefix for the saved images.  It didn't
	   -- work well, and I scrapped it.
		
-- 1.5.0
	-- Make it optional to hide the console during script running, and allow the delay before
	   -- and after the next image to be configured.
	-- Shutter shock reduction:  Add a configurable (in milliseconds) delay before an exposure
	   -- where the shutter speed is within a (also configurable) range.
	-- Add a mid-eclipse section.  This is a short section around the max eclipse point to optionally
	   -- try for some earthshine exposures.
	-- Set the aperture on program start.
	
-- 1.6.0 -- Contributions from Eric Krohn,  (Many thanks!)
	-- Added logging to permanent file, "ECLIPSE.LOG" at top level of the memory card
	-- More extensive logging added throughout the script
	-- Bugfix:  In do_max() the last two arguments to take_shot() were reversed. 
	-- Bugfix:  do_max() is unguarded as far as current time
	-- Added pretty_shutter() to make the shutter speed numbers more sensible
-- 1.7.0
	-- Added optional function to take some diamond ring images before and after the Baily's Beads
	   -- exposures.  Be careful not to overlap the Ring and Beads exposures before C2!
	-- Added a new burst function that takes a burst of images, one at a time, rather than using the
	   -- camera's burst function.  Slower, but slower is better for the buffer, and gives us the
	   -- opportunity to stop shooting at a specific time, where a burst will run until the requested
	   -- number of images have been captured.
-- 1.7.1: 
    -- Added reminder at mid partial to check focus.
    -- Added reminder 30 sec before each partial shot to recenter.
	  -- Function beep: just one instead of 3 5-beeps.
	  -- Added apertures for partial, ring and beads, and totality.