1# nmea.tcl --
2#
3# NMEA protocol implementation
4#
5# Copyright (c) 2006-2009 Aaron Faupell
6#
7# RCS: @(#) $Id: nmea.tcl,v 1.5 2009/01/09 06:49:25 afaupell Exp $
8
9package require Tcl 8.4
10package provide nmea 1.0.0
11
12namespace eval ::nmea {
13    array set ::nmea::nmea [list checksum 1 log {} rate 0]
14    array set ::nmea::dispatch ""
15}
16
17proc ::nmea::open_port {port {speed 4800}} {
18    variable nmea
19    if {[info exists nmea(fh)]} { ::nmea::close }
20    set nmea(fh) [open $port]
21    fconfigure $nmea(fh) -mode $speed,n,8,1 -handshake xonxoff -buffering line -translation crlf
22    fileevent $nmea(fh) readable [list ::nmea::read_port $nmea(fh)]
23    return $port
24}
25
26proc ::nmea::open_file {file {rate {}}} {
27    variable nmea
28    if {[info exists nmea(fh)]} { ::nmea::close }
29    set nmea(fh) [open $file]
30    if {[string is integer -strict $rate]} {
31        if {$rate < 0} { set rate 0 }
32        set nmea(rate) $rate
33    }
34    fconfigure $nmea(fh) -buffering line -blocking 0 -translation auto
35    if {$nmea(rate) > 0} {
36        after $nmea(rate) [list ::nmea::read_file $nmea(fh)]
37    }
38    return $file
39}
40
41proc ::nmea::configure_port {settings} {
42    variable nmea
43    fconfigure $nmea(fh) -mode $settings
44}
45
46proc ::nmea::close {} {
47    variable nmea
48    catch {::close $nmea(fh)}
49    unset -nocomplain nmea(fh)
50    foreach x [after info] {
51        if {[lindex [after info $x] 0 0] == "::nmea::read_file"} {
52            after cancel $x
53        }
54    }
55}
56
57proc ::nmea::read_port {f} {
58    if {[catch {gets $f} line] || [eof $f]} {
59        if {[info exists ::nmea::dispatch(EOF)]} {
60            $::nmea::dispatch(EOF)
61        }
62        nmea::close
63    }
64    if {$::nmea::nmea(log) != ""} {
65        puts $::nmea::nmea(log) $line
66    }
67    ::nmea::parse_nmea $line
68}
69
70proc ::nmea::read_file {f {auto 1}} {
71    variable nmea
72    set line [gets $f]
73    if {[eof $f]} {
74        if {[info exists ::nmea::dispatch(EOF)]} {
75            $::nmea::dispatch(EOF)
76        }
77        nmea::close
78        return 0
79    }
80    if {[string match {$*} $line]} {
81        ::nmea::parse_nmea $line
82    } else {
83        ::nmea::parse_nmea \$$line
84    }
85    if {$auto} {
86        after $nmea(rate) [list ::nmea::read_file $f]
87    }
88    return 1
89}
90
91proc ::nmea::do_line {} {
92    variable nmea
93    if {![info exists nmea(fh)]} { return -code error "there is no currently open file" }
94    return [::nmea::read_file $nmea(fh) 0]
95}
96
97proc ::nmea::configure {opt {val {}}} {
98    variable nmea
99    switch -exact -- $opt {
100        rate {
101            if {$val == ""} { return $nmea(rate) }
102            if {![string is integer $val]} { return -code error "rate must be an integer value" }
103            if {$val <= 0} {
104                foreach x [after info] {
105                    if {[lindex [after info $x] 0 0] == "::nmea::read_file"} {
106                        after cancel $x
107                    }
108                }
109                set val 0
110            }
111            if {$nmea(rate) == 0 && $val > 0} {
112                after $val [list ::nmea::read_file $nmea(fh)]
113            }
114            set nmea(rate) $val
115            return $val
116        }
117        checksum {
118            if {$val == ""} { return $nmea(checksum) }
119            if {![string is bool $val]} { return -code error "checksum must be a boolean value" }
120            set nmea(checksum) $val
121            return $val
122        }
123        default {
124            return -code error "unknown option $opt"
125        }
126    }
127}
128
129proc ::nmea::input {sentence} {
130    if {![string match "*,*" $sentence]} { set sentence [join $sentence ,] }
131    if {[string match {$*} $sentence]} {
132        ::nmea::parse_nmea $sentence
133    } else {
134        ::nmea::parse_nmea \$$sentence
135    }
136}
137
138proc ::nmea::log {{file _X}} {
139    variable nmea
140    if {$file == "_X"} { return [expr {$nmea(log) != ""}] }
141    if {$file != ""} {
142        if {$nmea(log) != ""} { ::nmea::log {} }
143        set nmea(log) [open $file a]
144    } else {
145        catch {::close $nmea(log)}
146        set nmea(log) ""
147    }
148    return $file
149}
150
151proc ::nmea::parse_nmea {line} {
152    set line [split $line \$*]
153    set cksum [lindex $line 2]
154    set line [lindex $line 1]
155    if {$cksum == "" || !$::nmea::nmea(checksum) || [checksum $line] == $cksum} {
156        set line [split $line ,]
157        set sentence [lindex $line 0]
158        set line [lrange $line 1 end]
159        if {[info exists ::nmea::dispatch($sentence)]} {
160            $::nmea::dispatch($sentence) $line
161        } elseif {[info exists ::nmea::dispatch(DEFAULT)]} {
162            $::nmea::dispatch(DEFAULT) $sentence $line
163        }
164    }
165}
166
167proc ::nmea::checksum {line} {
168    set sum 0
169    binary scan $line c* line
170    foreach char $line {
171        set sum [expr {$sum ^ ($char % 128)}]
172    }
173    return [format %02X [expr {$sum % 256}]]
174}
175
176proc ::nmea::write {type args} {
177    variable nmea
178    set data $type,[join $args ,]
179    puts $nmea(fh) \$$data*[checksum $data]
180}
181
182proc ::nmea::event {sentence {command _X}} {
183    variable dispatch
184    set sentence [string toupper $sentence]
185    if {$command == "_X"} {
186        if {[info exists dispatch($sentence)]} {
187            return $dispatch($sentence)
188        }
189        return {}
190    }
191    if {$command == ""} {
192        unset -nocomplain dispatch($sentence)
193        return {}
194    }
195    set dispatch($sentence) $command
196    return $command
197}
198