1
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
2
# See LICENSE for details.
5
"""Rockwell Semiconductor Zodiac Serial Protocol
6
Coded from official protocol specs (Order No. GPS-25, 09/24/1996, Revision 11)
8
Maintainer: Bob Ippolito
10
The following Rockwell Zodiac messages are currently understood::
11
EARTHA\\r\\n (a hack to "turn on" a DeLorme Earthmate)
12
1000 (Geodesic Position Status Output)
13
1002 (Channel Summary)
14
1003 (Visible Satellites)
17
The following Rockwell Zodiac messages require implementation::
18
None really, the others aren't quite so useful and require bidirectional communication w/ the device
20
Other desired features::
21
- Compatability with the DeLorme Tripmate and other devices with this chipset (?)
24
import struct, operator, math
25
from twisted.internet import protocol
26
from twisted.python import log
30
class ZodiacParseError(ValueError):
33
class Zodiac(protocol.Protocol):
35
# Output Messages (* means they get sent by the receiver by default periodically)
36
1000: 'fix', # *Geodesic Position Status Output
37
1001: 'ecef', # ECEF Position Status Output
38
1002: 'channels', # *Channel Summary
39
1003: 'satellites', # *Visible Satellites
40
1005: 'dgps', # Differential GPS Status
41
1007: 'channelmeas', # Channel Measurement
42
1011: 'id', # *Receiver ID
43
1012: 'usersettings', # User-Settings Output
44
1100: 'testresults', # Built-In Test Results
45
1102: 'meastimemark', # Measurement Time Mark
46
1108: 'utctimemark', # UTC Time Mark Pulse Output
47
1130: 'serial', # Serial Port Communication Parameters In Use
48
1135: 'eepromupdate', # EEPROM Update
49
1136: 'eepromstatus', # EEPROM Status
51
# these aren't used for anything yet, just sitting here for reference
54
'fix': 1200, # Geodesic Position and Velocity Initialization
55
'udatum': 1210, # User-Defined Datum Definition
56
'mdatum': 1211, # Map Datum Select
57
'smask': 1212, # Satellite Elevation Mask Control
58
'sselect': 1213, # Satellite Candidate Select
59
'dgpsc': 1214, # Differential GPS Control
60
'startc': 1216, # Cold Start Control
61
'svalid': 1217, # Solution Validity Control
62
'antenna': 1218, # Antenna Type Select
63
'altinput': 1219, # User-Entered Altitude Input
64
'appctl': 1220, # Application Platform Control
65
'navcfg': 1221, # Nav Configuration
66
'test': 1300, # Perform Built-In Test Command
67
'restart': 1303, # Restart Command
68
'serial': 1330, # Serial Port Communications Parameters
69
'msgctl': 1331, # Message Protocol Control
70
'dgpsd': 1351, # Raw DGPS RTCM SC-104 Data
73
allow_earthmate_hack = 1
76
def dataReceived(self, recd):
77
self.recvd = self.recvd + recd
78
while len(self.recvd) >= 10:
80
# hack for DeLorme EarthMate
81
if self.recvd[:8] == 'EARTHA\r\n':
82
if self.allow_earthmate_hack:
83
self.allow_earthmate_hack = 0
84
self.transport.write('EARTHA\r\n')
85
self.recvd = self.recvd[8:]
88
if self.recvd[0:2] != '\xFF\x81':
90
raise ZodiacParseError('Invalid Sync %r' % self.recvd)
92
raise ZodiacParseError
93
sync, msg_id, length, acknak, checksum = struct.unpack('<HHHHh', self.recvd[:10])
96
cksum = -(reduce(operator.add, (sync, msg_id, length, acknak)) & 0xFFFF)
97
cksum, = struct.unpack('<h', struct.pack('<h', cksum))
100
raise ZodiacParseError('Invalid Header Checksum %r != %r %r' % (checksum, cksum, self.recvd[:8]))
102
raise ZodiacParseError
104
# length was in words, now it's bytes
107
# do we need more data ?
110
neededBytes += length + 2
111
if len(self.recvd) < neededBytes:
114
if neededBytes > self.MAX_LENGTH:
115
raise ZodiacParseError("Invalid Header??")
117
# empty messages pass empty strings
120
# does this message have data ?
122
message, checksum = self.recvd[10:10+length], struct.unpack('<h', self.recvd[10+length:neededBytes])[0]
123
cksum = 0x10000 - (reduce(operator.add, struct.unpack('<%dH' % (length/2), message)) & 0xFFFF)
124
cksum, = struct.unpack('<h', struct.pack('<h', cksum))
125
if cksum != checksum:
127
log.dmsg('msg_id = %r length = %r' % (msg_id, length), debug=True)
128
raise ZodiacParseError('Invalid Data Checksum %r != %r %r' % (checksum, cksum, message))
130
raise ZodiacParseError
132
# discard used buffer, dispatch message
133
self.recvd = self.recvd[neededBytes:]
134
self.receivedMessage(msg_id, message, acknak)
136
def receivedMessage(self, msg_id, message, acknak):
137
dispatch = self.dispatch.get(msg_id, None)
139
raise ZodiacParseError('Unknown msg_id = %r' % msg_id)
140
handler = getattr(self, 'handle_%s' % dispatch, None)
141
decoder = getattr(self, 'decode_%s' % dispatch, None)
142
if not (handler and decoder):
143
# missing handler or decoder
145
# log.msg('MISSING HANDLER/DECODER PAIR FOR: %r' % (dispatch,), debug=True)
147
decoded = decoder(message)
148
return handler(*decoded)
150
def decode_fix(self, message):
151
assert len(message) == 98, "Geodesic Position Status Output should be 55 words total (98 byte message)"
152
(ticks, msgseq, satseq, navstatus, navtype, nmeasure, polar, gpswk, gpses, gpsns, utcdy, utcmo, utcyr, utchr, utcmn, utcsc, utcns, latitude, longitude, height, geoidalsep, speed, course, magvar, climb, mapdatum, exhposerr, exvposerr, extimeerr, exphvelerr, clkbias, clkbiasdev, clkdrift, clkdriftdev) = struct.unpack('<LhhHHHHHLLHHHHHHLlllhLHhhHLLLHllll', message)
154
# there's a lot of shit in here..
155
# I'll just snag the important stuff and spit it out like my NMEA decoder
156
utc = (utchr * 3600.0) + (utcmn * 60.0) + utcsc + (float(utcns) * 0.000000001)
158
log.msg('utchr, utcmn, utcsc, utcns = ' + repr((utchr, utcmn, utcsc, utcns)), debug=True)
160
latitude = float(latitude) * 0.00000180 / math.pi
161
longitude = float(longitude) * 0.00000180 / math.pi
162
posfix = not (navstatus & 0x001c)
163
satellites = nmeasure
164
hdop = float(exhposerr) * 0.01
165
altitude = float(height) * 0.01, 'M'
166
geoid = float(geoidalsep) * 0.01, 'M'
169
# seconds since 00:00 UTC
173
# longitude (degrees)
175
# position fix status (invalid = False, valid = True)
177
# number of satellites [measurements] used for fix 0 <= satellites <= 12
179
# horizontal dilution of precision
181
# (altitude according to WGS-84 ellipsoid, units (always 'M' for meters))
183
# (geoid separation according to WGS-84 ellipsoid, units (always 'M' for meters))
185
# None, for compatability w/ NMEA code
189
def decode_id(self, message):
190
assert len(message) == 106, "Receiver ID Message should be 59 words total (106 byte message)"
191
ticks, msgseq, channels, software_version, software_date, options_list, reserved = struct.unpack('<Lh20s20s20s20s20s', message)
192
channels, software_version, software_date, options_list = map(lambda s: s.split('\0')[0], (channels, software_version, software_date, options_list))
193
software_version = float(software_version)
194
channels = int(channels) # 0-12 .. but ALWAYS 12, so we ignore.
195
options_list = int(options_list[:4], 16) # only two bitflags, others are reserved
196
minimize_rom = (options_list & 0x01) > 0
197
minimize_ram = (options_list & 0x02) > 0
198
# (version info), (options info)
199
return ((software_version, software_date), (minimize_rom, minimize_ram))
201
def decode_channels(self, message):
202
assert len(message) == 90, "Channel Summary Message should be 51 words total (90 byte message)"
203
ticks, msgseq, satseq, gpswk, gpsws, gpsns = struct.unpack('<LhhHLL', message[:18])
205
message = message[18:]
207
flags, prn, cno = struct.unpack('<HHH', message[6 * i:6 * (i + 1)])
208
# measurement used, ephemeris available, measurement valid, dgps corrections available
209
flags = (flags & 0x01, flags & 0x02, flags & 0x04, flags & 0x08)
210
channels.append((flags, prn, cno))
211
# ((flags, satellite PRN, C/No in dbHz)) for 12 channels
212
# satellite message sequence number
213
# gps week number, gps seconds in week (??), gps nanoseconds from Epoch
214
return (tuple(channels),) #, satseq, (gpswk, gpsws, gpsns))
216
def decode_satellites(self, message):
217
assert len(message) == 90, "Visible Satellites Message should be 51 words total (90 byte message)"
218
ticks, msgseq, gdop, pdop, hdop, vdop, tdop, numsatellites = struct.unpack('<LhhhhhhH', message[:18])
219
gdop, pdop, hdop, vdop, tdop = map(lambda n: float(n) * 0.01, (gdop, pdop, hdop, vdop, tdop))
221
message = message[18:]
222
for i in range(numsatellites):
223
prn, azi, elev = struct.unpack('<Hhh', message[6 * i:6 * (i + 1)])
224
azi, elev = map(lambda n: (float(n) * 0.0180 / math.pi), (azi, elev))
225
satellites.push((prn, azi, elev))
226
# ((PRN [0, 32], azimuth +=[0.0, 180.0] deg, elevation +-[0.0, 90.0] deg)) satellite info (0-12)
227
# (geometric, position, horizontal, vertical, time) dilution of precision
228
return (tuple(satellites), (gdop, pdop, hdop, vdop, tdop))
230
def decode_dgps(self, message):
231
assert len(message) == 38, "Differential GPS Status Message should be 25 words total (38 byte message)"
232
raise NotImplementedError
234
def decode_ecef(self, message):
235
assert len(message) == 96, "ECEF Position Status Output Message should be 54 words total (96 byte message)"
236
raise NotImplementedError
238
def decode_channelmeas(self, message):
239
assert len(message) == 296, "Channel Measurement Message should be 154 words total (296 byte message)"
240
raise NotImplementedError
242
def decode_usersettings(self, message):
243
assert len(message) == 32, "User-Settings Output Message should be 22 words total (32 byte message)"
244
raise NotImplementedError
246
def decode_testresults(self, message):
247
assert len(message) == 28, "Built-In Test Results Message should be 20 words total (28 byte message)"
248
raise NotImplementedError
250
def decode_meastimemark(self, message):
251
assert len(message) == 494, "Measurement Time Mark Message should be 253 words total (494 byte message)"
252
raise NotImplementedError
254
def decode_utctimemark(self, message):
255
assert len(message) == 28, "UTC Time Mark Pulse Output Message should be 20 words total (28 byte message)"
256
raise NotImplementedError
258
def decode_serial(self, message):
259
assert len(message) == 30, "Serial Port Communication Paramaters In Use Message should be 21 words total (30 byte message)"
260
raise NotImplementedError
262
def decode_eepromupdate(self, message):
263
assert len(message) == 8, "EEPROM Update Message should be 10 words total (8 byte message)"
264
raise NotImplementedError
266
def decode_eepromstatus(self, message):
267
assert len(message) == 24, "EEPROM Status Message should be 18 words total (24 byte message)"
268
raise NotImplementedError