-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathnmea.py
265 lines (224 loc) · 8.15 KB
/
nmea.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
'''
nmea.py
Converts Sailaway API data to NMEA sentences for communication
with nautical charting software.
'''
import socket
import threading
import sys
from datetime import datetime, timedelta
from rich.console import Console
from rich.markdown import Markdown
from sailaway import sailaway, saillog
from utils import geo, units
SERVER_ADDR = "127.0.0.1"
SERVER_PORT = 10110
BUFFER_SIZE = 1024
NMEA_TIME_FORMAT = "%H%M%S"
NMEA_DATE_FORMAT = "%d%m%y"
class nmea:
def formatSentence(msgStr,checksum=True):
msgBytes = bytes(msgStr,'utf-8')
csum = ""
if checksum:
checkSumByte = 0
for byte in msgBytes:
checkSumByte ^= byte
csum = "*" + hex(checkSumByte)[2:]
sentence = "$" + msgStr + csum + "\n"
#print(sentence)
return bytes(sentence, 'utf-8')
class NMEAServer:
def __init__(self, port):
self.port = port
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error as msg:
sys.exit("Cannot initialize network socket : " + msg)
try:
self.sock.bind((SERVER_ADDR, port))
except socket.error as msg:
sys.exit("Cannot bind socket to port " + str(port))
self.clients = []
self.listener = None
self.sender = None
self.sentence = None
def listen(self):
while True:
try:
self.sock.listen(1)
client, addr = self.sock.accept()
except socket.error as msg:
break
self.clients.append(client)
if len(self.clients) == 1:
self.startUpdates()
def startUpdates(self):
self.refresh()
def stopUpdates(self):
if self.sender != None:
self.sender.cancel()
self.sender = None
def start(self):
self.listener = threading.Thread(target=NMEAServer.listen, args=(self,))
self.listener.start()
def stop(self):
self.stopUpdates()
for client in self.clients:
client.close()
self.sock.close()
# Send updates to all clients every 2 seconds
def refresh(self):
if self.sentence != None:
self.sendAll(self.sentence)
self.sender = threading.Timer(2, NMEAServer.refresh, args=(self,))
self.sender.start()
def sendAll(self, msg):
badClients = []
for client in self.clients:
try:
client.send(msg)
except BrokenPipeError:
client.close()
badClients.append(client)
except socket.error:
#print("Cannot reach client: " + errmsg)
client.close()
badClients.append(client)
# remove disconnected clients from list
for client in badClients:
self.clients.remove(client)
if len(self.clients) == 0:
self.stopUpdates()
def update(self, lat, lon, hdg, sog, cog, twd, tws, curTime):
timeStr = curTime.strftime(NMEA_TIME_FORMAT)
dateStr = curTime.strftime(NMEA_DATE_FORMAT)
posStr = geo.latlon_to_nmea(lat, lon)
hdgStr = str(round(hdg,1)) + ",T"
sogStr = geo.format_sog(str(round(sog,1)))
cogStr = geo.format_sog(str(round(cog,1)))
# NMEA TWA is bearing, not heading
twaStr = str(round(geo.wrap_angle(twd-hdg),1)) + ",T"
twsStr = str(round(tws,1)) + ",N"
# Construct NMEA sentences
# indicates what follows is from a virtual boat
sOrigin = nmea.formatSentence("SOL", False)
# Position
sGPGLL = nmea.formatSentence("GPGLL," + posStr + "," + timeStr + ",A")
# Position (GPS)
sGPGAA = nmea.formatSentence("GPGAA," + timeStr + "," + posStr + ",1,08,0,0,M,,,,")
# true heading
sIIHDT = nmea.formatSentence("IIHDT," + hdgStr)
# true wind speed & angle
sWIMWV = nmea.formatSentence("WIMWV,"+ twaStr + "," + twsStr + ",A")
# recommended minimum sentence
sGPRMC = nmea.formatSentence("GPRMC," + timeStr + ",A," + posStr + "," + sogStr + "," + cogStr + "," + dateStr + ",,,")
self.sentence = sOrigin + sGPGLL + sGPGAA + sIIHDT + sWIMWV + sGPRMC
class NMEAUpdater:
def __init__(self, port=SERVER_PORT):
self.api = sailaway()
self.logbook = saillog()
self.isRunning = False
self.updateThread = None
self.boatNum = -1
self.boats = []
self.serverport = port
def version():
return "(v0.1.4a)"
def start(self):
# start the TCP server
self.server = NMEAServer(self.serverport)
self.server.start()
# start the update loop
self.isRunning = True
self.queryAndUpdate()
def getBoats(self):
return self.boats
def getPort(self):
return self.server.port
def getLogbook(self):
return self.logbook
def getBoat(self):
return self.boatNum
def setBoat(self, num):
if num != self.boatNum:
self.boatNum = num
self.updateBoat()
def stop(self):
if self.updateThread != None:
self.updateThread.cancel()
self.updateThread = None
self.isRunning = False
self.server.stop()
def updateBoat(self):
if len(self.boats) > 0 and self.boatNum != -1:
# update NMEA server with boat information
boat = self.boats[self.boatNum]
boatHdg = geo.wrap_angle(boat['hdg'])
boatSpeed = units.mps_to_kts(boat['sog'])
boatCourse = geo.wrap_angle(boat['cog'])
windDirection = geo.wrap_angle(boat['twd'])
windSpeed = units.mps_to_kts(boat['tws'])
# Update our NMEA sentence clients
self.server.update(boat['latitude'], boat['longitude'], boatHdg, boatSpeed, boatCourse, windDirection, windSpeed, self.api.lastUpdate)
def refresh(self):
# schedule next update
curTime = datetime.utcnow()
nextUpdateTime = sailaway.updateInterval() - (curTime - self.api.lastUpdate).total_seconds()
if nextUpdateTime > 0:
self.updateThread = threading.Timer(nextUpdateTime, NMEAUpdater.queryAndUpdate, args=(self,))
self.updateThread.start()
else:
self.queryAndUpdate()
def queryAndUpdate(self):
# retrieve data from cache or server
self.boats = self.api.query()
for b in self.boats:
self.logbook.write(self.api.lastUpdate, b)
# Send updated boat positon to NMEA server
self.updateBoat()
# set up next update
self.refresh()
def printArgs():
sys.exit("\nusage: nmea [port number] [boat number]\n\nPort number is " + str(SERVER_PORT) + " by default.\n")
if __name__ == '__main__':
port = SERVER_PORT
boatNum = -1
if len(sys.argv) > 1:
port = sys.argv[1]
try:
port = int(port)
except ValueError:
printArgs()
if len(sys.argv) > 2:
boatNum = sys.argv[2]
try:
boatNum = int(boatNum)
except ValueError:
printArgs()
console = Console()
updater = NMEAUpdater(port)
updater.start()
console.print(Markdown("### **NMEA** " + NMEAUpdater.version()))
print("")
boats = updater.getBoats()
if len(boats) == 0:
updater.stop()
sys.exit("You don't have any boats to track.")
if boatNum == -1:
for i in range(len(boats)):
boat = boats[i]
console.print(Markdown("# (" + str(i) + ") *" + boat['boatname'] + "* - " + boat['boattype']))
boatNum = input("Enter boat # for NMEA tracking (or press return to quit): ")
try:
boatNum = int(boatNum)
except ValueError:
updater.stop()
sys.exit()
else:
boat = boats[boatNum]
console.print(Markdown("# (" + str(boatNum) + ") *" + boat['boatname'] + "* - " + boat['boattype']))
updater.setBoat(boatNum)
print("NMEA server now listening on TCP port " + str(updater.getPort()) + " - press return to quit.")
input("")
updater.stop()