#
#  gpsman --- GPS Manager: a manager for GPS receiver data
#
#  Copyright (c) 2004 Miguel Filgueiras (mig@ncc.up.pt) / Universidade do Porto
#
#    This program is free software; you can redistribute it and/or modify
#      it under the terms of the GNU General Public License as published by
#      the Free Software Foundation; either version 2 of the License, or
#      (at your option) any later version.
#
#      This program is distributed in the hope that it will be useful,
#      but WITHOUT ANY WARRANTY; without even the implied warranty of
#      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#      GNU General Public License for more details.
#
#      You should have received a copy of the GNU General Public License
#      along with this program.
#
#  File: serial.tcl
#  Last change:  11 July 2004
#
# Includes contributions by Matt Martin (matt.martin@ieee.org)
#  marked "MGM contribution"
#

# utilities for using the serial port with Garmin and Magellan receivers

# logging the communication with the serial port (for debugging purposes)
set SERIALLOG $USERDIR/logfile
set NotLogging 1

#### no configurable values below this line!

set LogFile ""
set ErrorCount 0
set MAXTRIALS 5
set ProgrWindow 0

##### low-level read procedures

## the following two procs do not work under MS-Windows...

proc ReadChar {} {
    # read chars when using file events or polling
    # call $ProcProcChar to process it
    global SRLFILE Eof InBuff NoInProc Polling GPSState

    if { $Eof } { return }
    set buff [read $SRLFILE]
    if { "$buff" != "" } {
	append InBuff $buff
	if { $NoInProc } { after 0 UseReadChars }
    }
    if { [set Eof [eof $SRLFILE]] } {
	Log "RP> at eof"
	set GPSState offline
	close $SRLFILE
    } elseif { $Polling } { update ; after 2 ReadChar }
    return
}

proc UseReadChars {} {
    # use chars read from serial channel in string $InBuff
    # call $ProcProcChar for each character read in
    global ProcProcChar InBuff NoInProc SInPacketState

    set NoInProc 0 ; set buff $InBuff ; set InBuff ""
    while { [set n [string length $buff]] } {
        for { set i 0 } { $i < $n } { incr i } {
	    while { "$SInPacketState" == "block" } { update ; after 1 }
	    $ProcProcChar [string index $buff $i]
	}
	set buff $InBuff ; set InBuff ""
    }
    set NoInProc 1
    return
}

## special procs for use in MS-Windows...
# adapted from GPSMan 5.2
# implement fileevent readable with polling
#      as Tcl/Tk 8.0 and 8.1 do not support it for MS-Windows

proc ReadPollChar {} {
    # poll and read from serial channel
    global Eof InBuff SRLFILE

    if { $Eof } { return }
    set InBuff [read $SRLFILE]
    if { [set Eof [eof $SRLFILE]] } {
	Log "RPoll> at EOF"
	close $SRLFILE
    }
    UsePollChars 0 [string length $InBuff]
    return
}

proc UsePollChars {i n} {
    # use chars read from serial channel by polling routine
    #  $i is index of next char in buffer
    #  $n is length of buffer
    # call $ProcProcChar for each character read in
    global InBuff Eof SInPacketState ProcProcChar

    while { $i != $n } {
      if { "$SInPacketState" == "block" } {
	  after 5 "UsePollChars $i $n"
	  return
      }
      $ProcProcChar [string index $InBuff $i]
      incr i
    }
    if { ! $Eof } {
	after 1 ReadPollChar
    }
    return
}

##### logging

proc OpenSerialLog {} {
    # open (if requested) logfile for the communication with the serial port
    #  (also used when importing NMEA files)
    global NotLogging LogFile SERIALLOG DateFormat VERSION VERSIONDATE \
	    tcl_version tcl_platform
    
    if { ! $NotLogging && $LogFile == "" } {
	set LogFile [open $SERIALLOG w]
	set d [FormatDay $DateFormat $VERSIONDATE(year) $VERSIONDATE(month) \
		         $VERSIONDATE(day)]
	Log "GPSMan $VERSION / $d"
	Log "tcl_version = $tcl_version"
	foreach f "platform os osVersion machine byteOrder" {
	    if { ! [catch {set v $tcl_platform($f)}] } {
		Log "$f = $v"
	    }
	}
    }
    return
}

proc Log {m} {
    global NotLogging LogFile

    if { $NotLogging } { return }
    puts $LogFile $m
    flush $LogFile
    return
}

##### resetting

proc ResetSerial {} {
    global SOutBusy SInState SInPacketState SInBuffer LInBuffer \
	    PkInState PkLastPID ErrorCount

    set SOutBusy 0
    set SInPacketState start ; set SInState idle
    set SInBuffer "" ; set LInBuffer ""
    set PkInState idle ; set PkLastPID -1
    set ErrorCount 0
    return
}

##### error handling

proc BadPacket {mess} {
    # count errors in receiving packets and abort operation if too many
    global ErrorCount MAXTRIALS MESS

    Log "BP> $mess"
    incr ErrorCount
    if { $ErrorCount == $MAXTRIALS } {
	GMMessage $MESS(toomanyerr)
	AbortComm
    }
    return
}

##### open serial port

proc OpenSerialFailed {baud} {
    # open serial port at given baud rate, and log file if needs be
    # return 1 on failure
    global MESS SRLFILE SERIALPORT InBuff Polling Eof tcl_platform

    if { [catch {set SRLFILE [open $SERIALPORT r+]}] } {
	GMMessage [format $MESS(badserial) $SERIALPORT]
	return 1
    }
    switch $tcl_platform(platform) {
	unix {
	    set Polling 0 ; set InBuff ""
	    fconfigure $SRLFILE -blocking 0 -mode $baud,n,8,1 \
		    -translation binary
	    fileevent $SRLFILE readable ReadChar
	}
	windows {
	    # Tcl/Tk 8.0p2 does not support I/O from/to serial ports
	    set Polling 1 ; set InBuff ""
	    fconfigure $SRLFILE -blocking 0 -mode $baud,n,8,1 \
		    -translation binary
	    # after 0 ReadPollChar
	    after 0 ReadChar
	}
	default {
	    GMMessage $MESS(badplatform)
	    return 1
	}
    }
    set Eof 0
    OpenSerialLog
    return 0
}

##### upper level

proc EndOutProt {pid} {
    # deal with end of output protocol
    #  $pid in {XfrWP, XfrRT, XfrTR}; not being used
    # set GPSOpResult to 0
    global Jobs GPSOpResult

    CloseInProgrWindow
    set Jobs ""
    ResetSerial
    set GPSOpResult 0
    return
}

##### floating point representation

proc BadFloats {} {
    # check whether conversions of bytes to float are working correctly
    #  in this machine
    global MESS

    if { abs([UnPackData "A B C D" float]-781.035217285) > 1e-3 } {
	return [expr ! [GMConfirm $MESS(badfloats)]]
    }
    return 0
}

##### control

proc GPSConnection {args} {
    # check connection with receiver if protocol is garmin or magellan
    #  open serial port if needs be
    #  if connected the 1st argument will be called, else the 2nd one;
    #  continuation is either to SentPacket and EndConnCheck/AbortComm, or to
    #  AbortComm
    # return 0 on immediate failure, otherwise 1
    global Jobs Request MESS NoGarmin GPSProtocol ProcProcChar SERIALBAUD

    # includes MGM contribution
    if { "$GPSProtocol" != "garmin" && "$GPSProtocol" != "magellan"} {
	GMMessage $MESS(cantchkprot)
	return 0
    }
    if { $NoGarmin } {
	if { [BadFloats] } { return 0 }
	if { [OpenSerialFailed $SERIALBAUD] } {
	    eval [lindex $args 1]
	    return 0
	}
	set ProcProcChar ProcChar
	set NoGarmin 0
    }
    if { [FailsInProgrWindow $MESS(check)] } {
	eval [lindex $args 1]
	return 0
    }
    set Request "check=$args"
    set Jobs [list [after 10000 AbortComm] [after 0 "SendData product"]]
    return 1
}

proc EndConnCheck {messid} {
    # end connection check successfully
    # set GPSOpResult to 0
    global MYGPS Request MESS CurrPSPID NotLogging GPSOpResult

    CloseInProgrWindow
    if { "$MYGPS" == "Garmin" } {
	SymbolsDOForProtocol $CurrPSPID(WPData)
    }
    ResetSerial
    Log "ECC> $MESS($messid)"
    regsub check= $Request "" as
    eval [lindex $as 0]
    set GPSOpResult 0
    return
}

proc GPSOff {} {
    global Request

    set Request turnOff
    SendData turnOff
    return
}

proc GPSBye {} {

    return
}

proc InitGPS {} {

    return
}

proc StartGPS {} {
    global NoGarmin

    set NoGarmin 1
    return
}

proc GetGPS {wh} {
    global GetSet

    set GetSet($wh) ""
    DoGetGPS $wh
    return
}

proc GetGPSIn {wh ixs} {
    # get data of type $wh (in {WP, RT}), but only to replace items with
    #  given indices $ixs (that may contain -1, meaning that new items
    #  can be created)
    global GetSet

    set GetSet($wh) $ixs
    DoGetGPS $wh
    return
}

proc DoGetGPS {wh} {
    # there must be a call to proc EndWPRenaming where this operation
    #  ends if data was stored (see, e.g., proc EndInProt, garmin.tcl)
    global Request MESS PkLastPID

    set PkLastPID -1
    if { [FailsInProgrWindow $MESS(get$wh)] } { return }
    InitWPRenaming
    set Request get$wh
    SendData get $wh
    return
}

proc PutGPS {wh ixs} {
    global Request MESS PkLastPID

    set PkLastPID -1
    if { [FailsInProgrWindow $MESS(put$wh)] } { return }
    set Request put$wh
    SendData put $wh $ixs
    return
}

proc AbortComm {args} {
    # abort communication in progress
    # if $MYGPS==Garmin this proc is replaced in garmin.tcl
    #  $args either void or a message id to be shown
    # global $Request==get$wh  where $wh in {WP RT TR PosnData DtTMData}
    #                          (cf. SendData)
    #                         GPSOpResult is set to 1 (error)
    #                ==check=$args  where 2nd arg should be executed as
    #                          connection is apparently down
    global Request Jobs MESS GPSOpResult

    CloseInProgrWindow
    # should it be more cautious concerning these cancelations?
    foreach j $Jobs {
	catch { after cancel $j }
    }
    ResetSerial
    if { "$args" != "" } { GMMessage $MESS($args) }
    set Jobs ""
    switch -glob $Request {
	get* {
	    set Request abort
	    SendData abort
	    # after which SentPacket will call AbortComm again
	    set GPSOpResult 1
	}
	check=* {
	    # assume connection is down
	    regsub check= $Request "" as
	    eval [lindex $as 1]
	    set GPSOpResult 1
	}
	abort { }
    }
    return
}

##### input/output progress window

proc CloseInProgrWindow {} {
    global CMDLINE ProgrWindow ProgrWGrabs

    if { $CMDLINE } {
	set ProgrWindow 0
    } else {
	DestroyRGrabs .inprogr $ProgrWGrabs
    }
    return
}

proc FailsInProgrWindow {mess} {
    # create dialog for signaling operation in progress
    # fail if window already exists
    #  single button: Abort; no bindings
    global COLOUR EPOSX EPOSY TXT SRecACKs SRecNAKs SPckts CMDLINE \
	    ProgrWindow ProgrWGrabs

    if { $CMDLINE } {
	if { $ProgrWindow } { return 1 }
	incr ProgrWindow
	set SRecACKs 0 ; set SRecNAKs 0 ; set SPckts 0
	return 0
    }
    if { [winfo exists .inprogr] } { Raise .inprogr ; bell ; return 1 }

    set ProgrWGrabs [grab current]
    set SRecACKs 0 ; set SRecNAKs 0 ; set SPckts 0
    toplevel .inprogr
    wm protocol .inprogr WM_DELETE_WINDOW { AbortComm }
    wm title .inprogr "$TXT(commrec)/GPS Manager"
    wm transient .inprogr
    wm geometry .inprogr +$EPOSX+$EPOSY

    frame .inprogr.fr -borderwidth 5 -bg $COLOUR(messbg)
    label .inprogr.fr.title -text "..." -relief sunken
    label .inprogr.fr.text -text "$mess"
    foreach f "A N P" v "SRecACKs SRecNAKs SPckts" t "ACKs NAKs packets" {
	set fw .inprogr.fr.frc$f
	frame $fw
	label $fw.val -textvariable $v -width 3
	label $fw.tit -text $TXT($t)
	pack $fw.val $fw.tit -side left -padx 0
    }
    button .inprogr.fr.ok -text $TXT(abort) -command AbortComm
    pack .inprogr.fr -side top
    pack .inprogr.fr.title .inprogr.fr.text .inprogr.fr.frcA .inprogr.fr.frcN \
	    .inprogr.fr.frcP .inprogr.fr.ok -side top -pady 5
    update idletasks
    grab .inprogr
    RaiseWindow .inprogr
    return 0
}

