Pentax-http.ps1

<#PSScriptInfo
 
.VERSION 0.0.1
 
.GUID fd054f67-0a09-4db8-8b5c-a1f2150043b9
 
.AUTHOR James O'Neill
 
.COMPANYNAME Mobula Consulting Ltd
 
.COPYRIGHT (c) James O'Neill 2016
 
.TAGS Pentax DSLR Remote WIFI
 
.LICENSEURI https://opensource.org/licenses/MS-PL
 
.DESCRIPTION
   Script to to manage Wifi equipped Pentax Cameras
#>
 

<#
    No warranty of any kind is offered for this script; it is provided "as-is" and without support.
    You may use it ONLY at your own risk.
    The information to get me started was at http://www.pentaxforums.com/forums/184-pentax-k-s1-k-s2/295501-k-s2-wifi-laptop-3.html
    and I learned lots by looking at Claus Zielke's work at https://sourceforge.net/projects/pentaxks2wifiremote/
    I recommend Claus's program if you want to shoot over WiFi with control over settings and/or with Live view.
    This script is designed to run while you are shooting and download preview images as they are shot - which his is not.
#>



<#
.Synopsis
    Script to to manage Wifi equipped Pentax Cameras
.Example
    Pentax-http
    Starts the script, outputs some information about the camera and its settings,
    downloads todays photos to the default folder (my pictures\temp) and
    monitors the camera for new photos - building a html file to preview them -
    until either the script is stopped or the connection to the cameras is lost.
.Example
    Pentax-http -NewOnly
    As with the previous example, except that it doesn't copy existing files from today.
.Example
    Pentax-http.ps1 -Path C:\Users\James\Pictures\Dec-26 -CutOff "26 December 2016 00:01"
    As with previous examples, copies files shot after 00:01 on 26th December and places them in a named folder
.Example
    Pentax-http.ps1 -ShowBrowser -ClearFiles
    Removes any pre-existing files, from the default folder and loads the preview in the default web browser (this
    works better with a tablet in portrait mode). The preview will be updated with each download.
    Uses the default folder and todays date as the cut off.
#>

[cmdletbinding()]
Param (
    #Specifies an IP address for the camera, if is not using the default.
    $CameraIP         = "192.168.0.1", 
    #Specifies a cut off date/time - older pictures will not be downloaded.
    [datetime]$CutOff = ([datetime]::Today ) ,
    #Location for files to be download to.
    $Path             = (Join-Path -Path ([environment]::GetFolderPath([System.Environment+specialfolder]::MyPictures)) -ChildPath "Temp"),
    #If Specified, files won't be downloaded (and -path will be ignored).
    [switch]$NoDownload,
    #If specified any JPEG files in the download folder will be deleted.
    [Switch]$ClearFiles,
    #Time to wait after polling the camera for new pictures.
    $PauseSeconds     = 10 ,
    #The script builds a preview HTML file, if ShowBrowser is specified it is opened and you can just refresh it.
    [switch]$ShowBrowser,
    #If specified only new files will be downloaded (and any value for CutOff is ignored). Old files will still be built into the preview HTML .
    [switch]$NewOnly   
)

#Ensure System.Drawing.Bitmap is loaded - it is used to rotate images.
[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") | Out-Null
if ([System.Drawing.Imaging.ImageFormat]::Jpeg) {Write-Verbose -Message "System.Drawing assembly is availbable for image processing"}
else                                            {Write-Warning -Message "Images may not be rotated or saved because System.Drawing assembly did not load."}

#The script has download turned on unless -NoDownload is specified. Everything else requires -Download.
$Download  = -not $NoDownload 
#If the directory specified as -PATH doesn't exist, create it, or if -Clearfiles was specfied delete JPGs from it.
if (-not (Test-path -Path $Path -PathType Container)) {
                                  New-Item    -Path  $Path -ItemType Directory | Out-Null
}
if ($ClearFiles)                 {Remove-Item -Path "$path\*.JPG" -ErrorAction SilentlyContinue}

#After script has run details are in $cameraFileList, so you can do (for example) $cameraFileList | out-gridview;
#If it doesn't exist already, define it as an array.
if (-not $global:cameraFileList) { $global:cameraFileList = @() }
if ($NewOnly)                    { 
    # If there are files already in the list use them to calculate the cutoff for new files. Otherwise use the local time,
    # but if the computer and camera don't agree on the time there can be problems. Camera settings don't include its time.
    if ($global:cameraFileList.where({$_.datetime}))  {
           $CutOff      = ($global:cameraFileList.where({$_.datetime}) | Measure-Object -Maximum -Property datetime).Maximum
    }
    else { $CutOff      = [datetime]::Now }
}

Function Format-TimeValue        {
<#
.Synopsis
    Converts Pentax's time value (two integers with a "." between them) into a printable shutter speed.
.Example Format-TimeValue 1.250
Returns "1/250th"
.Example Format-TimeValue 25.10
Returns "2.5Sec"
#>

  Param (
        [parameter(ValueFromPipeline=$true)]
        [Alias("TV")]
        [string]$TimeValue
  ) 
  process {
    foreach ($tv in $TimeValue) {
        #if it is already formatted return it as is , otherwise it should be two integers seperated with a "."
        #if it is "1.xxx" 1.500, for example, that's 1/500th. If it written 25.10 that's 25/10 i.e. 2.5 secs.
        #So replace "1." with "1/" or do the division operation, and add "th" or "Sec" as needed. Don't forget to escape "." in regex!
        
        if     ($tv    -match "^1/\d+th$|^[.\d]+sec$") {$tv}
        elseif ($tv -notmatch "^\d+\.\d+$")            {Write-Warning "$tv isn't correctly formatted."; return}
        elseif ($tv    -match "1\.")                   {($tv -replace '1\.','1/') + 'th'}
        else  {
            [int[]]$v=$tv -split '\.'
            ($v[0]/$v[1]).ToString() + "Sec"
        }
    }
  }
} 

Function Write-Picinfo           {
<#
.Synopsis
    Writes a one line summary of a picture to the console.
.Example
    Get-CameraFiles -Download | Write-Picinfo
    Gets, downloads and rotates files writing a one line summary of each to the console.
#>

    Param   (
        #Picture(s) to write details to console.
        [Parameter(ValueFromPipeline=$true)]
        $Picture
    )
    Process {
        Foreach ($p in $Picture) { 
            $Listing = 'File: {0}/{1} {2:f} ISO:{3,7}, Exp. Comp:{4,5}, {6,9} @ {5,7}' -f $p.dir , $p.file, $p.dateTime , 
                                            $p.ISO , $p.ExposureCompensation, $p.Apperture, $p.Shutter 
            if ($p.SavePath -ne ""){$Listing += " SAVED "}
            if ($p.rotated  -gt 0) {$Listing += " Rotated " + $p.rotated}

            #Use a different colour so we can see if we are shooting JPG or RAW.
            if ($p.file -match '\.JPG') {Write-Host -ForegroundColor Yellow $Listing } 
            else                        {Write-Host -ForegroundColor Green  $Listing }
        }
    }
}

<# Requests to the camera generally take the form
     * Send an HTTP GET to a URL in the form http://<<ip Address>>/v1/<function>
     * Process the JSON data which is returned.
   Two different ones are POST to /v1/camera/shoot with body of af=on / af=off or af=auto takes a picture
   and PUT to /v1/params/camera sets params: put "" is equiv of the green button
          
   The following functions all return FALSE if there is an error (e.g. the camera was turned off!)
#>


Function Request-CameraSettings  {
<#
.Synopsis
    Asks the camera for its properties. By default returns a summary of the information.
.Example
    (Request-CameraSettings -Raw).avlist
    Gives the possible apperture settings
#>

param(
    #If Specified returns the data unprocessed
    [switch]$Raw
) 
    try   {
      $settings = Invoke-WebRequest -Method Get -Uri "http://$CameraIP/v1/props" | ConvertFrom-Json 
      if ($raw) {$settings} 
      else      {
        Select-Object -InputObject $settings -Property Model,SerialNo,FirmwareVersion, Battery , 
            @{n="Hot"                 ;e={[convert]::ToBoolean($_.hot)}} , 
            @{n="Space"               ;e={($_.storages | ForEach-Object {'{0}: {1,5} {2,6} shots remaining' -f $_.NAME,$_.REMAIN , $_.format } )-join '; ' }},
            @{n="Shots"               ;e={($_.storages | Measure-Object -Sum -Property remain).sum}},
            FocusMode, shootMode, effect, filter
            @{n="ExposureMode"        ;e={$_.exposuremode + ' ' + $_.exposureModeOption}} ,
            @{n="ExposureCompensation";e={$_.xv}}  ,
            @{n="Apperture"           ;e={"f/"+$_.av }},
            @{n="Shutter"             ;e={Format-TimeValue $_.tv } },
            @{n="ISO"                 ;e={$_.sv}}
        }
    }
    catch {$false}   #Use a return value of FALSE to indicate failure - we don't send any parameters to the camera, so probably means we couldn't see it.
}

Function Request-CameraFileList  {
[cmdletbinding()]
<#
.Synopsis
    Asks the camera for a full list of its files (which will include duplicates if it is set for RAW + JPG).
#>

   
    # TODO Haven't tested filling first card when in the 2 card sequential mode of K1
    # does camera continue to return card 1, or return card 1 + 2 or return only card the active card (which has become 2)
    # K1 (v1.40) appears to only show what is on the primary card (slot 2 if slot 1 is empty) so if RAWs are written to card 1
    # and JPEGS to card 2 only RAW files are returned
    Param()     
    try   {
     (Invoke-WebRequest -Method Get -Uri "http://$CameraIP/v1/photos" | ConvertFrom-Json).DIRS | ForEach-Object {
        $n = $_.name 
        Write-Verbose -Message ($_.files.count.ToString() + " files in $n")
        foreach ($f in $_.files) {
            $fSplit = $f -split '\.'  #Will want the file name and file extension as distinct parts
            New-Object -TypeName psobject -Property @{Dir=$n; File=$f; Name=$fsplit[0]; ext=$fSplit[1]; Apperture=''; Shutter=''; ISO=''; ExposureCompensation=''; DateTime=$null; Orientation=0 ; Rotated=0; SavePath=""}
        }  
     }
    }
    Catch {$false}     #Use a return value of FALSE to indicate failure - we don't send any parameters to the camera, so probably means we couldn't see it.
} 

Function Request-CameraPicInfo   {
<#
.Synopsis
    Asks the camera for information about a specific picture.
.Example
   Request-CameraPicInfo IMGP0123.JPG
   Gets details of the named picture, if it is found in 100PENTX, otherwise returns FALSE.
.Example
   Request-CameraPicInfo IMGP12345.JPG
   Will return FALSE because this file name is not valid (too long).
.Example
   Request-CameraPicInfo -dir 101PENTX -file IMGP1234.JPG
   Gets details of the named picture from the 101PENTX directory.
.Example
   Request-CameraFileList | select -Last 1 | Request-CameraPicInfo
   Gets the list of files from the camera and requests info about the most recent.
#>

    param (
        #The filename (e.g. IMGP0001.JPG). The K1 (V1.40) is case insentive for names.
        [Parameter(ValueFromPipelineByPropertyName=$true)] 
        [string[]]$File, 
        #The folder where the picture lives (e.g. 101PENTX) if nothing is specified, 100PENTX will be assumed.
        [parameter(ValueFromPipelineByPropertyName=$true)]
        [string]$Dir = "100PENTX"

    )
    process { 
        foreach ($f in $File) {
            try {
                $uri    = 'http://{0}/v1/photos/{1}/{2}/info' -f $CameraIP, $Dir, $f 
                $result = Invoke-WebRequest -Method Get -Uri $uri |  ConvertFrom-Json 
                if ($result.errMsg -eq "OK") {$result} 
                else                    {$false } 
            } #FALSE indicates failure - either bad request (wrong file or dir) or camera didn't answer.
            catch                       {$false }    
        }
    }
}

Function Request-CameraLatest    {
<#
.Synopsis
    Asks the camera for information the most recent picture.
#>

    try {
        Invoke-WebRequest "http://$CameraIP/v1/photos/latest/info" | ConvertFrom-Json
    }
    Catch {$false}   #Use a return value of FALSE to indicate failure - we don't send any parameters to the camera, so probably means we couldn't see it.
}

Function Copy-CameraPic          {
[cmdletbinding()]
<#
.SYNOPSIS
    Copies images from the camera to the computer.
.EXAMPLE
    Copy-CameraPic imgp0123.dng
    Copies the 720x480 JPG view of the specified picture, if it is found in the 100PENTX directory; returns false if it is not found.
.EXAMPLE
    Copy-CameraPic -dir 100PENTX -File imgp1234.jpg
    Copies the 720x480 JPG view of the specified picture from 101PENTX directory; returns false if it is not found.
.EXAMPLE
   Get-CameraFiles | select -first 3 | Copy-CameraPic -Destination $pwd | ft name,length,rotation
   Gets the 3 most recent picture and copies them to the current directory showing their name, file size and rotation applied.
.EXAMPLE
   start ( Request-CameraLatest | Copy-CameraPic )
   Downloads the most recent file and opens it in the default image app.
#>

    param(
       #The filename (e.g. IMGP0001.JPG). The K-1 (V1.40) is case insentive for names.
       [parameter(ValueFromPipelineByPropertyName=$true)]
       $File, 
       #The folder where the picture lives (e.g. 101PENTX) if nothing is specified, 100PENTX will be assumed.
       [parameter(ValueFromPipelineByPropertyName=$true)]
       $Dir = "100PENTX",
       #The orientation of the picture : Pentax use 8 for camera rolled left. 6 For rolled camera right , and 3 for camera inverted.
       #If the camera is rolled left the image is turned to the right and vice vesa, so 8 needs to be turned 270 degrees, and 6 turned 90 degrees clockwise.
       [parameter(ValueFromPipelineByPropertyName=$true)]
       $Orientation , 
       #Folder or full name where the file should be saved.
       $Destination = $script:Path,
       #Image size: WiFi throughput makes full size images slow (even timing out in some cases) but view files are ~50KB JPEGS. DNG files don't provide thumbs on K-1 with V1.40.
       [ValidateSet("view","thumb", "full")]
       $Size = "view"
    )
    Process {
        try {
            #Build the URI, GET the file, and convert to a bitmap object; rotate if needed, save as JPG.
            $uri    = 'http://{0}/v1/photos/{1}/{2}?size={3}' -f $CameraIP, $Dir, $File, $Size
            $webReq = Invoke-WebRequest -Uri $uri -Method Get
            $bitmap = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $webReq.RawContentStream 
            if     ($Orientation -eq 6) {
                    Write-Verbose -Message "Picture loaded, rotating 90" 
                    $bitmap.RotateFlip([System.Drawing.RotateFlipType]::Rotate90FlipNone)   
                    $rotation =  90 
            }
            elseif ($Orientation -eq 3) {
                    Write-Verbose -Message "Picture loaded, rotating 180" 
                    $bitmap.RotateFlip([System.Drawing.RotateFlipType]::Rotate180FlipNone)   
                    $rotation =  180 
            }
            elseif ($Orientation -eq 8) {
                    Write-Verbose -Message "Picture loaded, rotating 270"
                    $bitmap.RotateFlip([System.Drawing.RotateFlipType]::Rotate270FlipNone)
                    $rotation =  270  
            }
            else   {
                    Write-Verbose -Message "Picture loaded, no rotation"
                    $rotation =  0
            } 
            #Need to specify the save format explictly otherwise rotated pictures don't save as JPG format (some apps DO read non-JPEG format with JPG extension).
            if (Test-Path -Path $Destination -PathType Container) {$SavePath = Join-Path -Path $Destination -ChildPath ($File -replace 'PEF$|DNG$',"JPG")}
            else                                             {$SavePath = $Destination }
            Write-Verbose -Message "Saving Picture to $Destination"
            $bitmap.Save($SavePath, [System.Drawing.Imaging.ImageFormat]::Jpeg) 
            Get-item -Path $SavePath | Add-Member -PassThru -NotePropertyName Rotation -NotePropertyValue $rotation
        }
        catch {$false}   #Use a return value of FALSE to indicate failure - bad file name/path, problem processing, or no connection to camera
    }          
}

Function Set-Camera              {
<#
.Synopsis
    Sets the camera options **without checking validity**
.Description
    Request-CameraSettings -Raw will show the camera settings, and various lists for values they can take.
    For example AV might be 8.0 and Avlist will show the possible apperture values
    so you could send "Av=11" to set f/11. If the value isn't on the list the camera will return "bad request"
    "AV=11.0" or "Av=8" will cause errors because they need to be written as in the list.
.Example
    Set-Camera
    Used with no paramters this is eqivalent to pressing the green button
.Example
    Set-Camera -raw | Format-Table av,@{n="tv";e={Format-TimeValue $_.tv}},sv,xv
    Set camera returns the values from the camera which format to show the selected settings
.Example
    Set-Camera "sv=auto","xv=0.0","","effect=cim_natural"
    Sets multiple options, ISO (sensitivity value) to auto -this will return the current value, not "auto" ;
    exposure compensation (xv) to 0, exposure to program line, and processing to natural
    (After setting "effect", the K-1 needs a moment to be ready for then next change, so set this last)
#>

    param   ( 
        #The key/value pair to set. If empty, is equivalent to pressing green to return to default settings
        [parameter(ValueFromPipeline=$true)]
        [string[]]$Setting = "",
        #if specified returns whatever came back from the camera
        [switch]$Raw
    ) 
    process {
        foreach   ($s in $setting) {
            try   {
                $result =  Invoke-WebRequest -Method Put -Uri "http://$CameraIP/v1/params/camera" -body $s | ConvertFrom-Json
                if     ($Raw)                    {$result}
                elseif ($result.errMsg -eq "OK") {
                    if (-not $s) {"Defaults set"}    
                    else         {
                        #return what the value IS - some things like exposure mode may not be set to what we asked for
                        $param = ($s -split "=")[0]
                        "{0}={1}" -f $param, $result.$param  
                    }
                } 
                else                             {Write-Warning "Camera responded to '$s' with '$($result.errMsg)'."}
            }
            catch {$false}
        }
    }
}
 
Function Request-CameraShoot     {
[cmdletbinding(SupportsShouldProcess=$true)]
<#
.Synopsis
    Requests the camera to take a picture.
.Description
    On K-1 with f/w 1.40 the return data suggests no shot was taken. But the shutter fires and picture is there.
#>
    
    Param (
        #Auto focus mode.
        [ValidateSet("auto","on","off")]
        [Alias("AF")]
        $AutoFocus   = "Auto", 
        #If specified downloads the file
        [Switch]$Download,
        #Size of the file to download (ignored if -Download not specified)
        [ValidateSet("view","thumb", "full")]
        $Size = "view", 
        #Where to download the file (ignored if -Download not specified)
        $Path = $Script:Path 
    )
    try   {
      if ($PSCmdlet.ShouldProcess("Camera", "Take photo")) {
        $result = Invoke-WebRequest -Method Post -Uri "http://$CameraIP/v1/camera/shoot" -body "af=$AutoFocus" |  ConvertFrom-Json 
        if     ($result.errMsg -ne "OK") {
                    Write-Warning -Message "Camera didn't take picture with Autofocus set to AF. Message was $($result.errmsg)."
                    return
        } 
        elseif ($Download) { 
                    Start-sleep -Seconds 5 # need to wait for the camera to save and update what is in latest .
                    Request-CameraLatest | Copy-CameraPic -size $size -Destination $Path 
        }
        else {$true }
      }
    }
    catch {   $false}
}

Function Request-CameraFocus     {
[cmdletbinding(SupportsShouldProcess=$true)]
<#
.Synopsis
    Requests the camera to take a focus. Returns "Focused" or "Did not focus" (if the camera responds)
#>
    
    Param ()
    try   {
      if ($PSCmdlet.ShouldProcess("Camera", "Focus")) {
        #on my K-1 with 1.40 firmware focus centers is always an empty list and focus effective area is always 70,64
        $result = Invoke-WebRequest -Method Post -Uri "http://$CameraIP/v1/lens/focus" |  ConvertFrom-Json 
        if     ($result.errMsg -ne "OK") {
                    Write-Warning -Message "Camera returned an error. Message was $($result.errmsg)."
                    return
        } 
        elseif ($result.focused) {return "Focused"}
        else                     {return "Did not focus" }
      }
    }
    catch {   $false}   #Use a return value of FALSE to indicate failure - we don't send any parameters to the camera, so probably means we couldn't see it.
}

Function Get-CameraFiles         {
<#
.Synopsis
    Reads the list of files from the camera, gets details of new ones and (optionally) downloads them.
.Example
    Get-CameraFiles -CutOff ([datetime]::Today) -download
    Gets images which have been shot today, and downloads their previews to the default folder.
.Example
    Get-CameraFiles -CutOff ([datetime]::Today) -download -path "C:\Users\james\OneDrive\Pictures\MyShoot"
    Gets images which have been shot today, and downloads their previews to the a specified directory (in this case one which will be sync'd later).
.Example
    Get-CameraFiles
    Gets details of all images on the camera but does not download any. SavePath will be filled in for any already downloaded.
#>

    param (       
        #If Specified pictures shot before this time will be ignored
        [datetime]$CutOff, 
        #Files will only be downloaded if specified
        [switch]$Download,
        #Location to save the downloaded files, if applicable
        $Path = $script:Path,
        #Download size, if applicable: WiFi throughput makes full size images too slow for most uses but view files are <50KB JPEGS
        [ValidateSet("view","thumb", "full")]
        $Size = "view"
    ) 
    #region get / extend the list of camera files
    Write-Progress -Activity "Getting list of files from the camera" 
    #Get the cameraFileList from the camera - and catch lack of connection.
    #Files which aren't already in the Filenames list and aren't JPGs must be new RAW files - add them to the file list
    #Files which aren't already in the Filenames list and *are* JPGs get added to the file list if they don't have a matching RAW
    #The file list is a global variable so we can access it after the script has run. (It needs at least script: scope)
    $fileNames              = $cameraFileList.name    
    $allFiles               = Request-cameraFileList 
    if (-not $allFiles)     { Write-Warning -Message "Can't find the camera at $CameraIP. Check you are connected to its private WiFi name and try again." ; return } 
    $newRaws                = $allFiles.Where({($_.ext -ne "JPG") -and ($fileNames -notcontains $_.name)} )
    $newRawnames            = $newRaws.name
    $global:cameraFileList += $newRaws + $allFiles.where({($_.ext -eq "JPG") -and ($newRawnames -notcontains $_.name) -and $fileNames -notcontains $_.name} ) 
    Write-Progress -Activity "Getting file list" -Completed
    #endregion
    
    #Now we have a list of files, process it as newest name first [don't handle wrap round of names yet], get properties and fill them in and download if specified.
    foreach ($file in ($cameraFileList | Sort-Object -Property name -Descending)) {  
        Write-Progress -Activity "Getting file info" -CurrentOperation $file.file 
        $properties = Request-CameraPicInfo -dir $file.dir -file $file.file 
        if ($properties.captured -eq $true) {
            $file.DateTime  =  [convert]::ToDateTime($properties.datetime)
            $file.Shutter   =  (Format-TimeValue -TV $properties.tv) 
            $file.Apperture            =      "f/" + $properties.av
            $file.ISO                  =             $properties.sv
            $file.ExposureCompensation =             $properties.xv
            $file.Orientation          =             $properties.orientation
            if ($Download -and ($file.DateTime  -gt  $CutOff)) {
                $destination           = $file.Name + ".JPG"
                $destination           = Join-Path  -Path $Path -ChildPath $destination
                $cp = $file | Copy-CameraPic -Destination $destination -size $Size
                if ($cp.FullName) {
                      $file.Rotated    = $cp.Rotation
                      $file.Savepath   = $cp.FullName    
                }
                else {Write-Warning -Message "Problem downloading $($file.file)" } 
            }
        }
        else {Write-Warning -Message "Couldn't get info for $($file.file)" } #Expect to see this if recent files have been deleted and script is re-run
        #We're working back from oldest to newest; bail out if we've passed the CutOff point, otherwise return the file object.
        #Note that the most recent file in the list will ALWAYs have the date set. Other things rely on that.
        if ($file.dateTime -le $CutOff) {break}
        else                            {$file}
    }

    #Add paths for any previously downloaded files.
    $SavedFiles = (dir $path\*.jpg).name -replace '\.jpg',''
    foreach ($file in $cameraFileList.Where({-not $_.SavePath -and $savedFiles -contains $_.name})) {
        $file.SavePath = Join-Path  -Path $Path -ChildPath ($file.Name + ".JPG")
    }  
    Write-Progress -Activity "Getting file info" -Completed
}    

Function Watch-Camera            {
<#
.Synopsis
    Polls the camera and gets new files when there are any.
.Example
    Watch-Camera -Download -CutOff [DateTime]::now() -PauseSeconds 10 -HtmlPath "$path\images.htm"
    Dowloads new images from the camera to the default drectory, pausing 10 seconds between each check. Rebuilds a preview html file after each download.
    Note that if the times are not in Sync between computer the first new file(s) may be ignored, or one or more old files may be downloaded when a new file arrives.
.Example
    Watch-Camera -Download -CutOff [DateTime]::now() -PauseSeconds 10 -HtmlPath "$path\images.htm" -passthrough
    Performs the same actions as before but outputs objects describing the image.
}
#>

    param (
        #Files will only be downloaded if -Download is specified.
        [switch]$Download,
        #Location to save the downloaded files, if applicable .
        $Path         = $script:Path,
        #Time to wait after checking for new files.
        $PauseSeconds = $Script:PauseSeconds,
        #Pictures shot before the time cut off time are assumed to have been processed already.
        $CutOff       = $Script:CutOff,
        #HTML File to update
        $HtmlPath     = $script:HtmlPath, 
        #If Specified, watch-camera will return objects for each picture rather than writing output.
        [switch]$PassThru
    )
 
    while ($true) {
        Write-Progress -Activity "Watching camera. Press [Ctrl]+[c] or break wifi conection to exit." -Status  (
                                 "Next check at "+ [datetime]::Now.AddSeconds($PauseSeconds).ToLongTimeString()  )
        Start-Sleep -Seconds $PauseSeconds
        $latest = Request-CameraLatest  #This returns false if the camera connection drops, if this happens exit the loop.
        if     (-not $latest)  {break} 
        elseif ($latest.captured -and ([convert]::ToDateTime($latest.datetime) -gt $CutOff)) {
            #There is at least one new image to download. Get all new images (though probably there is only one).
            #if -passthru is specified return the objects, otherwise put them through Write-PicInfo for CONSOLE output.
            if ($passthru) { Get-CameraFiles -CutOff $CutOff -Download:$Download -Path $Path }
            else           { Get-CameraFiles -CutOff $CutOff -Download:$Download -Path $Path | Write-Picinfo} 
            
            #Set CutOff to be the latest picture. (Note need to use Greater/Less than OR EQUAL TO when comparing with this date.
            $CutOff = ($global:cameraFileList.Where({$_.datetime}) | Measure-Object -Maximum -Property datetime).Maximum
            
            #Output quick HTML preview - most recent first. If there are portrait orientation pictures, it will work much better on a portrait format screen.
            $cameraFileList | Where-Object {$_.savepath} | Sort-Object -Property DateTime -Descending | 
                ForEach-Object -Begin    {'<html><body><table width="100%" align="Center">'}`
                               -Process  {'<tr><td align="Center"><img width="85%" src="{0}" /><br/><b>{1}</b></td></tr>' -f $_.savepath, $_.file} `
                               -End {'</table></body></html>'} | 
                      Out-File -FilePath $HtmlPath 
        } 
    }
}

$cameraSettings = Request-CameraSettings
if ($cameraSettings) {                    ##### DEFAULT START-UP change at will
    #region Display Camera status
    #Clear-Host
    Write-Host ("`r`n" * 6)
    Write-Host -ForegroundColor Yellow -BackgroundColor DarkCyan (" Monitoring {0,-12}, Firmware:{1,-8}, S/N:{2,-29}"     -f $CameraSettings.model, 
                                                                 $cameraSettings.firmwareVersion, $cameraSettings.serialNo)
    if     ($cameraSettings.battery -gt 66 ) {Write-host -ForegroundColor Black -BackgroundColor Green  -NoNewline ("Battery:{0,4}% "          -f $cameraSettings.battery)}
    elseif ($cameraSettings.battery -gt 33 ) {Write-host -ForegroundColor Black -BackgroundColor Yellow -NoNewline ("Battery:{0,4}% "          -f $cameraSettings.battery)}
    else                                     {Write-host -ForegroundColor Black -BackgroundColor Red    -NoNewline ("Battery:{0,4}% "          -f $cameraSettings.battery)}
    if     ($cameraSettings.Shots -gt 500 )  {Write-Host -ForegroundColor Black -BackgroundColor Green  -NoNewline (" Space available {0,-50}" -f $cameraSettings.Space)}
    elseif ($cameraSettings.Shots -gt 100 )  {Write-Host -ForegroundColor Black -BackgroundColor Yellow -NoNewline (" Space available {0,-50}" -f $cameraSettings.Space)}
    else                                     {Write-Host -ForegroundColor Black -BackgroundColor Red    -NoNewline (" Space available {0,-50}" -f $cameraSettings.Space)}
    if     ($cameraSettings.Hot)             {Write-host -ForegroundColor Black -BackgroundColor Red   "Temperature warning" }
    Else                                     {Write-host "" }
    Write-Host ("Focus Mode:{1}; Shot Mode:{0}; Exposure mode:{2}; Exposure Compensation:{3}" -f  $cameraSettings.shootMode,
                         $cameraSettings.focusMode.toupper(),       $cameraSettings.ExposureMode, $cameraSettings.ExposureCompensation )
    #endregion
    
    #Get files on the camera before the CutOff
    Get-CameraFiles -Download:$Download -Path $Path  -CutOff $CutOff | Write-Picinfo
    
    #Output HTML to display these files
    $HtmlPath = (Join-Path -Path $Path -ChildPath "IMAGES.htm")
    $cameraFileList.Where({$_.savepath})  | Sort-Object -Property DateTime -Descending | 
        ForEach-Object -Begin    {'<html><body><table width="100%" align="Center">'} `
                       -Process  {'<tr><td align="Center"><img width="85%" src="{0}" /><br/><b>{1}</b></td></tr>' -f $_.savepath, $_.file} `
                       -End {'</table></body></html>'} |
              Out-File -FilePath $HtmlPath 
    
    if ($ShowBrowser) {Start-Process -FilePath $HtmlPath}
    #Watch for new files
    $CutOff = ($cameraFileList.where({$_.datetime}) | Measure-Object -Maximum -Property datetime).Maximum
    Watch-camera -Download:$Download -Path $Path -CutOff $CutOff -PauseSeconds $PauseSeconds -HtmlPath $HtmlPath 
}
else                 { write-warning "Can't find the camera at $CameraIP. Check you are connected to its private WiFi name and try again." }