sgslib 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/sgs/alarm.rb +103 -0
- data/lib/sgs/command.rb +167 -0
- data/lib/sgs/gps.rb +53 -0
- data/lib/sgs/location.rb +328 -0
- data/lib/sgs/logger.rb +96 -0
- data/lib/sgs/mission.rb +362 -0
- data/lib/sgs/navigate.rb +141 -0
- data/lib/sgs/nmea.rb +138 -0
- data/lib/sgs/otto.rb +163 -0
- data/lib/sgs/polar.rb +86 -0
- data/lib/sgs/redis_base.rb +251 -0
- data/lib/sgs/setup.rb +40 -0
- data/lib/sgs/timing.rb +45 -0
- data/lib/sgs/version.rb +3 -0
- data/lib/sgs/waypoint.rb +115 -0
- data/lib/sgslib.rb +46 -0
- data/sgslib.gemspec +35 -0
- metadata +129 -0
data/lib/sgs/location.rb
ADDED
@@ -0,0 +1,328 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013, Kalopa Research. All rights reserved. This is free
|
3
|
+
# software; you can redistribute it and/or modify it under the terms of the
|
4
|
+
# GNU General Public License as published by the Free Software Foundation;
|
5
|
+
# either version 2, or (at your option) any later version.
|
6
|
+
#
|
7
|
+
# It is distributed in the hope that it will be useful, but WITHOUT
|
8
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
9
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
10
|
+
# for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License along
|
13
|
+
# with this product; see the file COPYING. If not, write to the Free
|
14
|
+
# Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
|
15
|
+
#
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY KALOPA RESEARCH "AS IS" AND ANY EXPRESS OR
|
17
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
18
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
19
|
+
# IN NO EVENT SHALL KALOPA RESEARCH BE LIABLE FOR ANY DIRECT, INDIRECT,
|
20
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
21
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
22
|
+
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
23
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
24
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
25
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
|
+
#
|
27
|
+
|
28
|
+
##
|
29
|
+
# Routines for handling sailboat location and bearings.
|
30
|
+
#
|
31
|
+
require 'date'
|
32
|
+
require 'json'
|
33
|
+
|
34
|
+
module SGS
|
35
|
+
##
|
36
|
+
#
|
37
|
+
# Class for dealing with latitude/longitude. Includes methods for parsing,
|
38
|
+
# converting to a printable string, and so forth.
|
39
|
+
#
|
40
|
+
# Note that for convenience, we retain latitude and longitude in Radians
|
41
|
+
# rather than degrees.
|
42
|
+
#
|
43
|
+
class Location
|
44
|
+
attr_accessor :latitude, :longitude
|
45
|
+
|
46
|
+
#
|
47
|
+
# Nominal radius of the planet, in nautical miles.
|
48
|
+
# http://en.wikipedia.org/wiki/Earth_radius#Mean_radii
|
49
|
+
EARTH_RADIUS = 3440.069528437724
|
50
|
+
|
51
|
+
#
|
52
|
+
# Create the Location instance.
|
53
|
+
def initialize(lat = nil, long = nil)
|
54
|
+
@latitude = lat.to_f if lat
|
55
|
+
@longitude = long.to_f if long
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Calculate a new position from the current position
|
60
|
+
# given a bearing (angle and distance)
|
61
|
+
#
|
62
|
+
# This code was derived from formulae on the Movable Type site:
|
63
|
+
# http://www.movable-type.co.uk/scripts/latlong.html
|
64
|
+
#
|
65
|
+
# var lat2 = Math.asin( Math.sin(lat1)*Math.cos(d/R) +
|
66
|
+
# Math.cos(lat1)*Math.sin(d/R)*Math.cos(angle) );
|
67
|
+
# var lon2 = lon1 + Math.atan2(Math.sin(angle)*Math.sin(d/R)*Math.cos(lat1),
|
68
|
+
# Math.cos(d/R)-Math.sin(lat1)*Math.sin(lat2));
|
69
|
+
def calculate(bearing)
|
70
|
+
loc = Location.new
|
71
|
+
sin_angle = Math.sin(bearing.angle)
|
72
|
+
cos_angle = Math.cos(bearing.angle)
|
73
|
+
sin_dstr = Math.sin(bearing.distance / EARTH_RADIUS)
|
74
|
+
cos_dstr = Math.cos(bearing.distance / EARTH_RADIUS)
|
75
|
+
sin_lat1 = Math.sin(@latitude)
|
76
|
+
cos_lat1 = Math.cos(@latitude)
|
77
|
+
loc.latitude = Math.asin(sin_lat1*cos_dstr + cos_lat1*sin_dstr*cos_angle)
|
78
|
+
sin_lat2 = Math.sin(loc.latitude)
|
79
|
+
loc.longitude = @longitude + Math.atan2(sin_angle*sin_dstr*cos_lat1,
|
80
|
+
cos_dstr - sin_lat1*sin_lat2)
|
81
|
+
loc
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# Move to the new location
|
86
|
+
def move!(bearing)
|
87
|
+
loc = calculate(bearing)
|
88
|
+
self.latitude = loc.latitude
|
89
|
+
self.longitude = loc.longitude
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Create a new location from a string.
|
94
|
+
# Uses the instance method to parse.
|
95
|
+
def self.parse_str(str)
|
96
|
+
loc = new
|
97
|
+
loc.parse_str(str)
|
98
|
+
loc
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Create a new location from a lat/long string pair
|
103
|
+
# Uses the instance method to parse.
|
104
|
+
def self.parse(latstr, longstr)
|
105
|
+
loc = new
|
106
|
+
loc.parse(latstr, longstr)
|
107
|
+
loc
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Parse a lat/long value (in degrees)
|
112
|
+
def parse_str(str)
|
113
|
+
latstr, longstr = str.split(',')
|
114
|
+
parse(latstr, longstr)
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Parse a lat/long value pair (in degrees)
|
119
|
+
def parse(latstr, longstr)
|
120
|
+
@latitude = ll_parse(latstr.split, "NS")
|
121
|
+
@longitude = ll_parse(longstr.split, "EW")
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Is this location valid?
|
126
|
+
def valid?
|
127
|
+
@latitude and @longitude
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Display the lat/long as a useful string (in degrees).
|
132
|
+
def to_s
|
133
|
+
if valid?
|
134
|
+
"%s, %s" % [ll_to_s(@latitude, "NS"), ll_to_s(@longitude, "EW")]
|
135
|
+
else
|
136
|
+
"unknown"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Display the lat/long as it would appear in a KML file.
|
142
|
+
def to_kml(sep = ' ')
|
143
|
+
vals = [@longitude, @latitude, 0.0]
|
144
|
+
str_vals = vals.map {|val| "%.6f" % Bearing.radians_to_degrees(val)}
|
145
|
+
str_vals.join(sep)
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# Helper functions for working in degrees.
|
150
|
+
def latitude_degrees
|
151
|
+
Bearing.radians_to_degrees @latitude
|
152
|
+
end
|
153
|
+
|
154
|
+
def latitude_degrees=(val)
|
155
|
+
@latitude = Bearing.degrees_to_radians val
|
156
|
+
end
|
157
|
+
|
158
|
+
def latitude_array(fmt = nil)
|
159
|
+
make_ll_array latitude_degrees, "NS", fmt
|
160
|
+
end
|
161
|
+
|
162
|
+
def longitude_degrees
|
163
|
+
Bearing.radians_to_degrees @longitude
|
164
|
+
end
|
165
|
+
|
166
|
+
def longitude_degrees=(val)
|
167
|
+
@longitude = Bearing.degrees_to_radians val
|
168
|
+
end
|
169
|
+
|
170
|
+
def longitude_array(fmt = nil)
|
171
|
+
make_ll_array longitude_degrees, "EW", fmt
|
172
|
+
end
|
173
|
+
|
174
|
+
#
|
175
|
+
# Subtract one location from another, returning a bearing
|
176
|
+
def -(loc)
|
177
|
+
Bearing.calculate(self, loc)
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
#
|
182
|
+
# Parse a string into a lat or long.
|
183
|
+
def ll_parse(args, nsew)
|
184
|
+
dir = args[-1].gsub(/[\d\. ]+/, '').upcase
|
185
|
+
args.map! {|val| val.to_f}
|
186
|
+
val = args.shift
|
187
|
+
val = val + args.shift / 60.0 if args.length > 0
|
188
|
+
val = val + args.shift / 3600.0 if args.length > 0
|
189
|
+
Bearing.degrees_to_radians val * ((nsew.index(dir) == 1) ? -1 : 1)
|
190
|
+
end
|
191
|
+
|
192
|
+
#
|
193
|
+
#
|
194
|
+
def ll_to_s(val, str)
|
195
|
+
if val < 0.0
|
196
|
+
chr = str[1]
|
197
|
+
val = -val
|
198
|
+
else
|
199
|
+
chr = str[0]
|
200
|
+
end
|
201
|
+
"%8.6f%c" % [Bearing.radians_to_degrees(val), chr]
|
202
|
+
end
|
203
|
+
|
204
|
+
#
|
205
|
+
# Create a Lat/Long array suitable for an NMEA output
|
206
|
+
def make_ll_array(val, nsew, fmt = nil)
|
207
|
+
fmt ||= "%02d%07.4f"
|
208
|
+
if (val < 0)
|
209
|
+
val = -val
|
210
|
+
ne = nsew[1]
|
211
|
+
else
|
212
|
+
ne = nsew[0]
|
213
|
+
end
|
214
|
+
deg = val.to_i
|
215
|
+
val = (val - deg) * 60
|
216
|
+
[fmt % [deg, val], ne.chr]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Class for dealing with the angle/distance vector.
|
222
|
+
#
|
223
|
+
# Note that for convenience, we retain the angle in Radians. The
|
224
|
+
# distance is in nautical miles.
|
225
|
+
class Bearing
|
226
|
+
attr_accessor :distance
|
227
|
+
|
228
|
+
#
|
229
|
+
# Create the Bearing instance.
|
230
|
+
def initialize(angle = 0.0, distance = 0.0)
|
231
|
+
self.angle = angle.to_f
|
232
|
+
self.distance = distance.to_f
|
233
|
+
end
|
234
|
+
|
235
|
+
#
|
236
|
+
# Create a bearing from an angle in degrees.
|
237
|
+
def self.degrees(angle, distance)
|
238
|
+
new(Bearing.degrees_to_radians(angle), distance)
|
239
|
+
end
|
240
|
+
|
241
|
+
#
|
242
|
+
# Handy function to translate degrees to radians
|
243
|
+
def self.degrees_to_radians(deg)
|
244
|
+
deg.to_f * Math::PI / 180.0
|
245
|
+
end
|
246
|
+
|
247
|
+
#
|
248
|
+
# Handy function to translate radians to degrees
|
249
|
+
def self.radians_to_degrees(rad)
|
250
|
+
rad.to_f * 180.0 / Math::PI
|
251
|
+
end
|
252
|
+
|
253
|
+
#
|
254
|
+
# Handy function to re-adjust an angle away from negative
|
255
|
+
def self.absolute(angle)
|
256
|
+
(angle + 2.0 * Math::PI) % (2.0 * Math::PI)
|
257
|
+
end
|
258
|
+
|
259
|
+
#
|
260
|
+
# Another handy function to re-adjust an angle (in degrees) away from
|
261
|
+
# negative.
|
262
|
+
def self.absolute_degrees(angle)
|
263
|
+
(angle + 360) % 360
|
264
|
+
end
|
265
|
+
|
266
|
+
#
|
267
|
+
# Haversine formula for calculating distance and angle, given two
|
268
|
+
# locations.
|
269
|
+
#
|
270
|
+
# To calculate an angle and distance from two positions:
|
271
|
+
#
|
272
|
+
# This code was derived from formulae on the Movable Type site:
|
273
|
+
# http://www.movable-type.co.uk/scripts/latlong.html
|
274
|
+
#
|
275
|
+
# var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) +
|
276
|
+
# Math.cos(lat1)*Math.cos(lat2) *
|
277
|
+
# Math.cos(lon2-lon1)) * R;
|
278
|
+
# var y = Math.sin(dLon) * Math.cos(lat2);
|
279
|
+
# var x = Math.cos(lat1)*Math.sin(lat2) -
|
280
|
+
# Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
|
281
|
+
# var angle = Math.atan2(y, x).toDeg();
|
282
|
+
def self.calculate(loc1, loc2)
|
283
|
+
bearing = new
|
284
|
+
sin_lat1 = Math.sin(loc1.latitude)
|
285
|
+
sin_lat2 = Math.sin(loc2.latitude)
|
286
|
+
cos_lat1 = Math.cos(loc1.latitude)
|
287
|
+
cos_lat2 = Math.cos(loc2.latitude)
|
288
|
+
sin_dlon = Math.sin(loc2.longitude - loc1.longitude)
|
289
|
+
cos_dlon = Math.cos(loc2.longitude - loc1.longitude)
|
290
|
+
bearing.distance = Math.acos(sin_lat1*sin_lat2 + cos_lat1*cos_lat2*cos_dlon) *
|
291
|
+
Location::EARTH_RADIUS
|
292
|
+
y = sin_dlon * cos_lat2
|
293
|
+
x = cos_lat1 * sin_lat2 - sin_lat1 * cos_lat2 * cos_dlon
|
294
|
+
bearing.angle = Math.atan2(y, x)
|
295
|
+
bearing
|
296
|
+
end
|
297
|
+
|
298
|
+
#
|
299
|
+
# Set the angle
|
300
|
+
def angle=(angle)
|
301
|
+
@angle = Bearing.absolute(angle)
|
302
|
+
end
|
303
|
+
|
304
|
+
#
|
305
|
+
# Get the angle
|
306
|
+
def angle
|
307
|
+
@angle
|
308
|
+
end
|
309
|
+
|
310
|
+
#
|
311
|
+
# Return the angle (in degrees)
|
312
|
+
def angle_degrees
|
313
|
+
Bearing.radians_to_degrees(@angle).to_i
|
314
|
+
end
|
315
|
+
|
316
|
+
#
|
317
|
+
# Get the back-angle (the angle viewed from the opposite end of the line)
|
318
|
+
def back_angle
|
319
|
+
Bearing.absolute(@angle - Math::PI)
|
320
|
+
end
|
321
|
+
|
322
|
+
#
|
323
|
+
# Convert to a string
|
324
|
+
def to_s
|
325
|
+
"BRNG %03dd,%.3fnm" % [angle_degrees, @distance]
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
data/lib/sgs/logger.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013, Kalopa Research. All rights reserved. This is free
|
3
|
+
# software; you can redistribute it and/or modify it under the terms of the
|
4
|
+
# GNU General Public License as published by the Free Software Foundation;
|
5
|
+
# either version 2, or (at your option) any later version.
|
6
|
+
#
|
7
|
+
# It is distributed in the hope that it will be useful, but WITHOUT
|
8
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
9
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
10
|
+
# for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License along
|
13
|
+
# with this product; see the file COPYING. If not, write to the Free
|
14
|
+
# Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
|
15
|
+
#
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY KALOPA RESEARCH "AS IS" AND ANY EXPRESS OR
|
17
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
18
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
19
|
+
# IN NO EVENT SHALL KALOPA RESEARCH BE LIABLE FOR ANY DIRECT, INDIRECT,
|
20
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
21
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
22
|
+
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
23
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
24
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
25
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
|
+
#
|
27
|
+
|
28
|
+
##
|
29
|
+
# Routines for handling sailboat logging.
|
30
|
+
#
|
31
|
+
require 'date'
|
32
|
+
|
33
|
+
module SGS
|
34
|
+
#
|
35
|
+
# Waypoint, Attractor, and Repellor definitions
|
36
|
+
class Logger < RedisBase
|
37
|
+
attr_accessor :watch
|
38
|
+
|
39
|
+
#
|
40
|
+
# Watch names and definitions. The first watch is from 8PM until
|
41
|
+
# midnight (local time), the middle watch is from midnight until
|
42
|
+
# 4AM. The morning watch is from 4AM until 8AM. The forenoon watch
|
43
|
+
# runs from 8AM until noon and the afternoon watch runs from noon
|
44
|
+
# until 4PM. The dog watches run from 4PM until 8PM and around we
|
45
|
+
# go again.
|
46
|
+
#
|
47
|
+
# Logs are sent back to base every four hours (6 per day) starting
|
48
|
+
# at midnight UTC. However, some of the reporting is based on
|
49
|
+
# the watch system rather than UTC. For example, reporting battery
|
50
|
+
# voltage is most useful at the start of the forenoon watch, because
|
51
|
+
# this represents the lowest voltage point after driving the boat all
|
52
|
+
# night. As a result, the watch ID is computed based on the longitude,
|
53
|
+
# which is a rough approximation of the timezone.
|
54
|
+
FIRST_WATCH = 0
|
55
|
+
MIDDLE_WATCH = 1
|
56
|
+
MORNING_WATCH = 2
|
57
|
+
FORENOON_WATCH = 3
|
58
|
+
AFTERNOON_WATCH = 4
|
59
|
+
DOG_WATCH = 5
|
60
|
+
ALARM_REPORT = 7
|
61
|
+
|
62
|
+
WATCH_NAMES = [
|
63
|
+
"First Watch", "Middle Watch", "Morning Watch",
|
64
|
+
"Forenoon Watch", "Afternoon Watch", "Dog Watch",
|
65
|
+
"", "** ALARM REPORT **"
|
66
|
+
].freeze
|
67
|
+
|
68
|
+
def initialize()
|
69
|
+
@watch = FIRST_WATCH
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Convert the watch ID to a name
|
74
|
+
def watch_name
|
75
|
+
WATCH_NAMES[@watch]
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Determine the watch report. This takes account our actual latitude
|
80
|
+
# and the current time. It does a rudimentary timezone conversion and
|
81
|
+
# calculates the watch. Note that what we're calculating here is what
|
82
|
+
# was the previous watch. So any time after 23:45 (local), we'll report
|
83
|
+
# from the First Watch. Right up until 03:45 (local) when we'll start
|
84
|
+
# reporting from the Middle Watch.
|
85
|
+
def determine_watch(longitude)
|
86
|
+
utc = Time.now
|
87
|
+
local_hour = utc.hour + longitude * 12.0 / Math::PI
|
88
|
+
p utc
|
89
|
+
p local_hour
|
90
|
+
local_hour += 24.0 if local_hour < 0.0
|
91
|
+
local_hour -= 24.0 if local_hour >= 24.0
|
92
|
+
@watch = ((local_hour / 4.0) + 0.25).to_i
|
93
|
+
@watch = FIRST_WATCH if @watch == 6
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|