#!/usr/bin/wish

# The One-Bit Data Scope
#
# This program provides an oscilloscope-like display of the
# data stream from the "1 bit logic analyzer" circuit.  The
# display can zoom about three orders of magnitude in steps
# of 1, 2, and 5.  It can save displayed data and reload it
# at a later time.  It can trigger on a specified duration
# either 1 or 0.
#
# This is the author's first Tcl/Tk program and constructive
# comments are very welcome.  (bsmith@linuxtoys.org)
#
# (C) Copyright 2003, Bob Smith
#
# This code is release under terms of the GNU Pubic License.
#
# Initial release: Nov 1, 2003


# Begin ....
wm title . "1 Bit Data Scope"
wm iconname . "1BitDS"
wm resizable . 1 1
wm minsize . 300 285


# Build the panel to enter config info
destroy .configFR .dataFR .mainM
frame .configFR -bd 4

# Put a label at the top
label .configFR.label -text "Configuration"
pack .configFR.label -side top

# A place to enter the serial port
frame .configFR.inputFR -bd 4
label .configFR.inputFR.inputLB -text "Serial Port:   "
entry .configFR.inputFR.inputEntry -textvariable "inport" -bg white
set inport "/dev/ttyS1"
pack .configFR.inputFR.inputLB .configFR.inputFR.inputEntry -side left
pack .configFR.inputFR -side top -anchor w

# A place to enter the serial port baud rate
frame .configFR.baudFR -bd 4
label .configFR.baudFR.baudLB -text "K Baud Rate: "
tk_optionMenu .configFR.baudFR.baudOM baudrate " 19.2" " 38.4" " 57.6" "115.2"
set baudrate " 57.6"
pack .configFR.baudFR.baudLB .configFR.baudFR.baudOM -side left
pack .configFR.baudFR -side top -anchor w

# A place to enter the maximum capture time
frame .configFR.captimeFR -bd 4
label .configFR.captimeFR.captimeLB -text "Capture at most "
entry .configFR.captimeFR.captimeEntry -textvariable "captime" \
       -bg white -width 9 
set captime "1000"
bind .configFR.captimeFR.captimeEntry <KeyRelease> { 
       drawGrid
}
label .configFR.captimeFR.captimeLB2 -text "ms. of data"
pack .configFR.captimeFR.captimeLB \
     .configFR.captimeFR.captimeEntry \
     .configFR.captimeFR.captimeLB2 -side left
pack .configFR.captimeFR -side top -anchor w

# A place to enter the maximum number of transitions to capture
frame .configFR.captransFR -bd 4
label .configFR.captransFR.captransLB -text "Capture at most "
entry .configFR.captransFR.captransEntry -textvariable "captrans" \
       -bg white -width 8
set captrans "10000"
label .configFR.captransFR.captransLB2 -text "transitions"
pack .configFR.captransFR.captransLB \
     .configFR.captransFR.captransEntry \
     .configFR.captransFR.captransLB2 -side left
pack .configFR.captransFR -side top -anchor w

# A place to enter the trigger mode
frame .configFR.modeFR -bd 4
label .configFR.modeFR.modeLB -text "Mode:"
radiobutton .configFR.modeFR.rb1 -variable "mode" -value "continuous" \
      -text "Continuous Display" -command {
      pack forget .configFR.trigtimeFR }
.configFR.modeFR.rb1 select
radiobutton .configFR.modeFR.rb2 -variable "mode" -value "oneshot" \
      -text "One-shot" -command {
      pack forget .configFR.trigtimeFR }
radiobutton .configFR.modeFR.rb3 -variable "mode" -value "triggerc" \
      -text "Triggered, Auto-retrigger" -command {
      pack .configFR.trigtimeFR -side top -anchor w }
radiobutton .configFR.modeFR.rb4 -variable "mode" -value "trigOneshot" \
      -text "Triggered, One-shot" -command {
      pack .configFR.trigtimeFR -side top -anchor w }
pack .configFR.modeFR.modeLB -side left -anchor nw
pack .configFR.modeFR.rb1 .configFR.modeFR.rb2 \
     .configFR.modeFR.rb3 .configFR.modeFR.rb4 -side top -anchor w
pack .configFR.modeFR -side top -anchor w

# A place to enter the trigger time and value
frame .configFR.trigtimeFR -bd 4
label .configFR.trigtimeFR.trigtimeLB1 -text "Trigger on first transition after"
entry .configFR.trigtimeFR.trigtimeEntry -textvariable "trigtime1" \
       -width 8 -bg white
global trigtime1
set trigtime1 "50"
label .configFR.trigtimeFR.trigtimeLB3 -text "ms. of"
radiobutton .configFR.trigtimeFR.rb1 -variable "trigval1" -value "0" \
             -text "0"
.configFR.trigtimeFR.rb1 select
radiobutton .configFR.trigtimeFR.rb2 -variable "trigval1" -value "1" \
             -text "1"
pack .configFR.trigtimeFR.trigtimeLB1 -side top -anchor w
pack .configFR.trigtimeFR.trigtimeEntry \
     .configFR.trigtimeFR.trigtimeLB3 \
     .configFR.trigtimeFR.rb1 .configFR.trigtimeFR.rb2 -side left



# Build the panel with the data display
frame .dataFR -bd 4

# Put a label at the top
label .dataFR.label -text "Data Display"
pack .dataFR.label -side top -pady 10

# Initialize data set
set dataCount 0
set newScWidth 530
set screenScale 1

# Add the canvas for the actual data display
canvas .dataFR.canvas -relief ridge -height 100 -bg white -bd 2 \
       -width $newScWidth -xscrollcommand [ list .dataFR.scrollbar set ]
pack .dataFR.canvas -side top -expand 1 -fill both

# Capture the new canvas size if the user resizes it
bind .dataFR.canvas <Configure> {
        set canvasWidth %w
        set canvasHeight %h
        setScale $screenScale
}

# Add the horizontal scroll bar for the canvas
scrollbar .dataFR.scrollbar -orient horizontal -width 15 \
       -command [ list .dataFR.canvas xview ]
pack .dataFR.scrollbar -side top -expand 0 -fill x -anchor n

# Add the "Run" and "Stop" buttons
button .dataFR.runBN -text "RUN " -command {
    setTrace on
}
button .dataFR.stopBN -text "STOP" -command {
    setTrace off
}

# Add the scale slider
scale .dataFR.scaleSC -label "Scale" -orient horizontal -length 150 \
        -from 1 -to 10 -variable screenScale  -command  setScale
pack .dataFR.runBN -side right -pady 20
pack .dataFR.scaleSC -side left -pady 10


# Add the menu bar to the top level panel
menu .mainM
menu .mainM.fileM -tearoff 0
menu .mainM.viewM -tearoff 0
menu .mainM.help -tearoff 0
.mainM add cascade -menu ".mainM.fileM" -label "File"
.mainM add cascade -menu ".mainM.viewM" -label "View"
# No help yet.  Maybe one day
# .mainM add cascade -menu ".mainM.help" -label "Help"
.mainM.fileM add command -command {loadFileData } -label {Load Data}
.mainM.fileM add command -command {saveFileData } -label {Save Data}
.mainM.fileM add command -command { exit 0 } -label {Exit}
.mainM.viewM add command -label {Configuration} -command {
    # User wants to see the config panel
    pack forget .dataFR
    pack .configFR -fill both -expand 1
}
.mainM.viewM add command -label {Data Display} -command {
    # User wants to see the data panel
    pack forget .configFR
    pack .dataFR -fill both -expand 1 -padx 30
}
.mainM.help add command -command {# run command here } -label {About}
. config -menu .mainM

pack forget .configFR
pack .dataFR -fill both -expand 1 -padx 30


# Procedure setTrace:  starts or stops the capture and display of 
# data from the serial port.  The input parameter can be either
# "on" or "off".  To give slightly better performance, we compute
# several constants on the transition to the on state.  
proc setTrace { state } {
    global fid
    global dataCount
    global totalTicks
    global maxTicks
    global xBarOld
    global dataList
    global inport
    global baudrate
    global captime
    global mode
    global triggerState
    global triggerTicks
    global triggerCount
    global trigtime1

    if { $state == "on" } {
        # Remove the "Run" button; show the "Stop button.
        pack forget .dataFR.runBN
        pack .dataFR.stopBN -side right -pady 20
        # Clear the data display if it is not already empty
        if {$dataCount != 0} {
            set dataList {}
            .dataFR.canvas delete "canvasData"
            set dataCount 0
        }
        # Set trigger'ed state based partly on mode
        if { $mode == "continuous" || $mode == "oneshot" } {
            set triggerState "triggered"
        } else {
            set triggerState "waiting"
            # convert trigger time to tick count
            # (BTW: a "tick" is the time of one data bit)
            set triggerTicks [expr $trigtime1 * $baudrate * 0.8] 
            set triggerCount 0
        }
        # Initialize variables and compute display time in ticks
        set totalTicks 0
        set maxTicks [expr $captime * $baudrate * 0.8]
        set xBarOld 0
        # Start the C program to sample the data
        set fid [open "|1bitda $inport $baudrate" r]
        # Call getSample for each sample returned from 1bitda
        fileevent $fid readable getSample
    }
    # Go to off state
    if { $state == "off" } {
        # Remove the "Stop" button; show the "Run" button.
        pack forget .dataFR.stopBN
        pack .dataFR.runBN -side right -pady 20
        # Stop the C program which monitors the serial port
        if [catch {close $fid}] { }
    }
}


# Procedure getSample:  Reads a sample from the C program to get
# data from the serial port.  Based on the sample, we may display
# the new data on the screen or change the trigger state.
proc getSample {} {
    global fid
    global mode
    global dataCount
    global dataList
    global dataFirstValue
    global captime
    global captrans
    global totalTicks
    global xBarOld
    global maxTicks
    global gridSpacing
    global canvasHeight
    global lastValue
    global triggerCount
    global triggerTicks
    global triggerState
    global trigtime1
    global trigval1
    global baudrate
    global pixelsPerTick

    # Read the sample
    set line [gets $fid]
    if {[eof $fid]} {
        # On error, turn off the trace.  This might be a good
        # place to add more sophisticated error processing.
        setTrace off
    }
    # Get data from sample.  We set "dataValue" to a 1 or 0 and
    # "dataDuration" to the number of ticks at dataValue.  See
    # the www.linuxtoys.org article on the "1 bit logic analyzer"
    # for a description of the protocol.
    if { 2 == [scan $line {%d, %d} dataValue dataDuration] } {
        # See if we can get out of the "waiting for trigger" state
        if { $triggerState == "waiting" } {
            if { $trigval1 != $dataValue } {
                set triggerCount 0
                return
            }
            set triggerCount [expr $triggerCount + $dataDuration]
            if {$triggerCount >= $triggerTicks} {
                set dataList {}
                .dataFR.canvas delete "canvasData"
                set dataCount 0
                set totalTicks 0
                set triggerState "triggered"
            }
            return
        }
        # We keep track of the number of transitions in "dataCount".
        incr dataCount
        if { $dataCount == 1 } {
            # Do some initialization if this is our first sample
            set dataFirstValue $dataValue
            set lastValue $dataValue
            lappend dataList $dataDuration
            set pixelsPerTick [expr (50 /($baudrate * 0.8) /$gridSpacing)]
            set xBarOld [expr round($totalTicks * $pixelsPerTick)]
        } elseif {$dataValue == $lastValue} {
            # Value did not change, so we modify the last sample by
            # adding in the additional duration.
            # (We get samples from 1bitda even if the data did not
            # change.  This helps to give a continuous display like an
            # oscilloscope.)
            set dataList [ lreplace $dataList end end [expr \
                [lindex $dataList end] + $dataDuration ] ]
            set dataCount [expr $dataCount - 1]
        } else {
            # Add the sample to our list of samples
            # We record the first data value (0 or 1) in the variable
            # "dataFirstValue".  Doing this means we need only store
            # the dataDuration in our list of sample since every entry
            # is a transition.
            lappend dataList $dataDuration
        }

        # draw vertical and horizontal bars on graph if new X location
        # Compute where we are on the display
        set totalTicks [expr $totalTicks + $dataDuration ]
        set xBarNew [expr round($totalTicks * $pixelsPerTick)]
        # Update the screen only if we moved onto an new screen location
        if { $xBarOld != $xBarNew } {
            if {$dataValue != $lastValue } {
                # Display vertical bar only if data changed
                .dataFR.canvas create line $xBarOld 40 $xBarOld \
                    [expr $canvasHeight - 40] -fill black -width 2 \
                    -tags "canvasData"
            }
            # Update horizontal line even if no data transition.  This
            # gives the smooth oscilloscope like display
            set y 40
            if { $dataValue == 0 } {
                 set y [expr $canvasHeight - 40]
            }
            .dataFR.canvas create line $xBarOld $y $xBarNew $y \
                 -fill black -width 2 -tags "canvasData"
        }
        set xBarOld $xBarNew
        # Look for the terminal count of samples or time
        if { $dataCount == $captrans || $totalTicks >= $maxTicks } {
            if { $mode == "continuous" } {
                if {$dataCount != 0} {
                    set dataList {}
                    .dataFR.canvas delete "canvasData"
                    set dataCount 0
                    set totalTicks 0
                    set xBarOld 0
                }
            } elseif { $mode == "triggerc" } {
                set triggerState "waiting"
                set triggerCount 0
            } else {
                # must be one-shot or triggered one-shot
                setTrace off
            }
        }
        set lastValue $dataValue
    }
}


# Procedure setScale:  Computes and redraws the data display in
# reaction to the user changing the "Scale" slider.  The scale
# slider allows the user to zoom in to view more detail.  The
# range of zoom is about three orders of magnitude and is set
# by the maximum value associated with the slider itself.  We
# try to get the zoom increments to multiples of 1, 2, 5, 10.
# The scale slider is certainly one part of the UI which could
# use some improvement.
proc setScale { s } {
    global canvasWidth
    global canvasHeight
    global newScWidth

    set s [expr $s - 1]
    set scalefactor [ expr pow(10, ($s / 3)) ]
    if  { [ expr ( $s % 3 ) ] == 1 } {
        set scalefactor [ expr $scalefactor * 2 ]
    } elseif  { [ expr ( $s % 3 ) ] == 2 } {
        set scalefactor [ expr $scalefactor * 5 ]
    }
    set newScWidth [ expr $canvasWidth * $scalefactor ]
    set oldStartP [ lindex [ .dataFR.scrollbar get ] 0 ]
    set oldStopP  [ lindex [ .dataFR.scrollbar get ] 1 ]
    set CentrP    [ expr ($oldStartP + $oldStopP) / 2 ]
    set newStart  [ expr ($newScWidth * $CentrP) - ($canvasWidth / 2) ]
    set newStartP [ expr $newStart / $newScWidth ]
    .dataFR.canvas configure -scrollregion\
            [ list 0 0 $newScWidth $canvasHeight ]
    .dataFR.canvas xview moveto $newStartP
    drawGrid
}


# Procedure drawGrid:  draws the grid on the data display and redraws
# any data in the area actually displayed.
proc drawGrid { } {
    global dataCount
    global dataList
    global dataFirstValue
    global captrans
    global totalTicks
    global gridSpacing
    global newScWidth
    global canvasHeight
    global captime
    global baudrate

    set nLines [ expr $newScWidth  / 50 ]
    if {$nLines == 0 } {set nLines 1}
    if {$captime == ""} {
        set gridLog 1
    } else {
        set gridLog [ expr log10 ($captime / $nLines) ]
    }
    set fract [ expr $gridLog - floor($gridLog) ]
    if { $fract < .301 } {
        set gridSpacing [ expr 2 * pow(10, floor($gridLog)) ]
    } elseif { $fract < .699 } {
        set gridSpacing [ expr 5 * pow(10, floor($gridLog)) ]
    } else {
        set gridSpacing [ expr 10 * pow(10, floor($gridLog)) ]
    }
    # Clear the display area.  Remove grid and display data
    .dataFR.canvas delete "canvasText"
    .dataFR.canvas delete "canvasData"

    # Redraw the grid lines and labels
    for {set x 50} {$x < $newScWidth} {set x [ expr $x + 50 ]} {
        .dataFR.canvas create line $x 0 $x $canvasHeight -fill gray
        if { [ expr $x % 250 ] == 0 } {
            .dataFR.canvas create text $x 20 -tags "canvasText" \
                 -text [expr ($x/50)* $gridSpacing ]
        }
    }

    # Redraw any data that was displayed
    if {$dataCount == 0} return
    .dataFR.canvas delete "canvasData"
    set totalTicks 0
    set xBarOld 0
    set pixelsPerTick [expr (50 / ($baudrate * 0.8) / $gridSpacing) ]
    set dataValue $dataFirstValue
    set lastValue $dataFirstValue
    for {set i 0} {$i < $dataCount} {incr i} {
        set dataDuration [lindex $dataList $i]
        # draw vertical and horizontal bars on graph
        set totalTicks [expr $totalTicks + $dataDuration ]
        set xBarNew [expr round($totalTicks * $pixelsPerTick)]
        if {$dataValue != $lastValue } {
            # Display vertical bar only if data changed
            .dataFR.canvas create line $xBarOld 40 $xBarOld \
                     [expr $canvasHeight - 40] -fill black -width 2 \
                     -tags "canvasData"
        }
        set lastValue $dataValue
        if { $dataValue == 0 } {
            set y [expr $canvasHeight - 40]
            set dataValue 1
        } else {
            set y 40
            set dataValue 0
        }
        .dataFR.canvas create line $xBarOld $y $xBarNew $y -fill black \
               -width 2 -tags "canvasData"
        set xBarOld $xBarNew
    }
}


# Procedure loadFileData:  reads samples from a data file.  The file
# can be from a previous "save" or can be the captured output from the
# 1bitla program.
proc loadFileData {} {
    global totalTicks
    global dataCount
    global triggerState 
    global maxTicks
    global mode
    global captime
    global baudrate
    global dataList
    global fid

    set fid [open [ tk_getOpenFile ] r]
    if {$dataCount != 0} {
        set dataList {}
        .dataFR.canvas delete "canvasData"
    }
    set dataCount 0
    set totalTicks 0
    set triggerState "triggered"
    set mode "oneshot"
    set maxTicks [expr $captime * $baudrate * 0.8]
    fileevent $fid readable getSample
}


# Procedure saveFileData: stores the current trace buffer into a file.
# The data is stored as value, duration pairs with one pair per line.
proc saveFileData {} {
    global dataCount
    global dataFirstValue
    global dataList

    set fid [open [ tk_getSaveFile ] w]

    if {$dataCount == 0} return
    set dataValue $dataFirstValue
    for {set i 0} {$i < $dataCount} {incr i} {
        set dataDuration [lindex $dataList $i]
        puts $fid "$dataValue, $dataDuration"
        if { $dataValue == 1 } {
             set dataValue 0
        } else {
             set dataValue 1
        }
    }
    close $fid
}

