diff --git a/FORMAT.md b/FORMAT.md index 5aea15d..c2c84c0 100644 --- a/FORMAT.md +++ b/FORMAT.md @@ -178,7 +178,53 @@ Notes: ### INP: "new packet" -Work in progress. +Work in progress. Simplex-DL burst format distinct from Block1 channels, seen in captures on 1626.40-1626.52 MHz. Each burst carries a 96-bit header (three 32-bit hdr words), up to 18 data blocks of 40 bits, and a 24-bit trailer. + +Header variants (selected by `hdr[1][4:8]`): + +- `H:1` (`hdr_id in {1000, 0111}`): `[hdr0:32][hdr1:32][data:16][CRC8][tail:8]`. CRC scope = 80 bits. +- `H:2` (`hdr_id == 0110`): `[hdr0:32][hdr1:32][CRC8][tail:24]`. CRC scope = 72 bits. Source IDs observed so far are disjoint from H:1, suggesting a distinct beam/sat role. +- `H:0`: any other `hdr_id`, layout not yet resolved. + +Types v1/v2/v3 depend on which CRC variant validates; see `IridiumNPMessage` for the exact logic. + +#### source_id split heuristic + +The `:NNNNN` value after the frame header is a 14-bit `source_id` extracted from `s2s[0][10:24]`. In our captures it partitions cleanly as `[sv_id:8][beam_id:6]`, i.e. `sv_id = source_id >> 6` and `beam_id = source_id & 0x3f`. This split is heuristic and not universally validated - some `hdr[0]` values span many sv_ids - so the fields are printed with a `?` marker: + + INP: ... :04294 sv?:067 bm?:06 H:2 T:0 h<...> + +Consumers should treat `sv?` / `bm?` as best-effort interpretation, not authoritative decode. + +### IC1 / IC2 / IC8: Iridium Certus (NEXT / EBBS) traffic bursts + +Recognizer for Iridium NEXT Certus traffic channel bursts carrying user voice or IP data. Same 0x789 unique word as Block1 channels, but the rest of the waveform uses coherent QPSK (or 16APSK) with Turbo FEC instead of DEQPSK with BCH. The Block1-oriented demod pipeline emits differentially-decoded bits that are not directly meaningful for these bursts, so we classify them by symbol count alone and expose the raw (wrongly-decoded) bits for diagnostic purposes. + +Symbol counts and modulations consistent with publicly documented Certus bearers: + +| Tag | Symbol count | Modulation | Code | Payload bits | +|---|---|---|---|---| +| `IC1` | 200 | QPSK 4/5 | Turbo rate 4/5 | 320 | +| `IC2` | 432 | QPSK 2/3 | Turbo rate 2/3 | 576 | +| `IC8` | 1824 | QPSK 2/3 or 16APSK 2/3 | Turbo rate 2/3 | 2432 or 4848 | + +Example: + + IC2: [...] 432 DL raw:0011001111110011 0011001101100011 [...] + +Column|Content|Example|Comment +--:|-|-|- +8|Length in symbols|432|200 for C1, 432 for C2, 1824 for C8 +9|Direction|DL|Certus L-band simplex downlink (1626.1-1626.5 MHz) +10|raw:|raw:\|Raw demod output - NOT directly decodable in this form + +Notes and caveats: + +1. These bits are not the payload. The gr-iridium demodulator applies differential decoding unconditionally. That is correct for Block1 DEQPSK but wrong for coherent-QPSK Certus. Proper decoding requires a gr-iridium patch to expose the coherent-QPSK symbols before differential decode, followed by the Iridium-specific Turbo decoder and interleaver. +2. Interleaver not public. The Iridium-NEXT-specific interleaver and puncturing patterns are not publicly documented; without them, Turbo decoding will not recover user data even with the correct bits. +3. The recognizer only covers NEXT traffic (C1/C2/C8). DBCCH and other new broadcast channels are not currently detected. +4. The current recognizer does not distinguish 16APSK-modulated C8 bursts from QPSK-modulated variants - both produce the same per-symbol count in the demod output. +5. The paper "Systematic Security Analysis of the Iridium Satellite Radio Link" (Jedermann et al., USENIX Security 2026, arXiv:2603.12062) covers the Iridium radio link in depth and is a recommended follow-up reference. ### IBC: Broadcast diff --git a/bitsparser.py b/bitsparser.py index a5c241e..99d89fb 100755 --- a/bitsparser.py +++ b/bitsparser.py @@ -7,6 +7,7 @@ import struct import fileinput import datetime +import itertools from math import sqrt,atan2,pi,log import crcmod @@ -27,6 +28,18 @@ NXT_UW_UPLINK = [0,2,0,0,0,0,0,0,2,0,2,0] header_messaging="00110011111100110011001111110011" # 0x9669 in BPSK header_time_location="11"+"0"*94 +# Iridium Certus (NEXT / EBBS) traffic bursts. Same 0x789 unique word as all other +# Iridium channels, but coherent QPSK + Turbo coding (not DEQPSK + BCH). +# Symbol counts per publicly documented NEXT traffic bearers: +# C1 30ksps QPSK 4/5: 320 payload bits -> 400 coded -> 200 symbols +# C2 60ksps QPSK 2/3: 576 payload bits -> 864 coded -> 432 symbols +# C8 240ksps QPSK 2/3: 2432 payload bits -> 3648 coded -> 1824 symbols +# The raw bits we see here are the *differentially-decoded* form of the coherent +# QPSK symbols (diff-decode is applied unconditionally in gr-iridium). That's +# wrong for Certus - the underlying demodulation is coherent, not differential - +# so these bits are not directly payload-recoverable without a gr-iridium patch. +# We recognize them by symbol count and simplex-DL band alone; the payload is left +# as-is for future research (Turbo decoding + correct interleaver required). messaging_bch_poly=1897 ringalert_bch_poly=1207 acch_bch_poly=3545 # 1207 also works? @@ -285,6 +298,23 @@ def __init__(self,msg): self._new_error("filtered message") return + # Certus (EBBS / NEXT) traffic burst recognition. Classify by symbol count on the + # simplex DL band. Guarded on self.next so we only claim frames that used + # next_access_dl sync - stops us stealing IRA/VOC bursts that happen to have + # matching symbol counts on iridium_access. + if "msgtype" not in self.__dict__ and self.next and (not args.freqclass or self.frequency > f_simplex) and not (args.freqclass and self.uplink): + payload_syms = self.symbols - (len(iridium_access) // 2) + if payload_syms == 200: + self.msgtype="C1" # NEXT C1 30ksps QPSK 4/5, 320 payload bits + elif payload_syms == 432: + self.msgtype="C2" # NEXT C2 60ksps QPSK 2/3, 576 payload bits + elif payload_syms == 1824: + self.msgtype="C8" # NEXT C8 240ksps QPSK 2/3 (or 16APSK 2/3), 2432 / 4848 payload bits + + if "msgtype" not in self.__dict__ and args.linefilter['type'] == "IridiumCertusMessage": + self._new_error("filtered message") + return + if "msgtype" not in self.__dict__ and (not args.freqclass or self.frequency < f_duplex) and not (args.freqclass and self.uplink): hdrlen=6 blocklen=64 @@ -328,8 +358,39 @@ def __init__(self,msg): return if "msgtype" not in self.__dict__ and (not args.freqclass or self.frequency < f_duplex) and self.uplink: - if len(data)>=2*26 and len(data)<2*50: + plen = 26 + if len(data) >= 2*plen and len(data) <= 2*(plen+4): # Decoder adds at least 3 symbols self.msgtype="AQ" + elif len(data) >= 2*plen and len(data) <= 2*(54): # Longer only if valid BPSK + sym = [['0', 'e', 'e', '1'][int(data[2*x])*2 + int(data[2*x+1])] for x in range(26)] + if 'e' not in sym: + self.msgtype = "AQ" + + if "msgtype" not in self.__dict__: # and (not args.freqclass or self.frequency > f_simplex) and not (args.freqclass and self.uplink): + if len(data)>=170:#==432*2: + symbols=de_dqpsk(data) + bits=sym2bits(symbols) + + bits=bits[:160] + h1=bits[0::4] + h2=bits[1::4] + # 1 2 3 1 2 3 + # 01234567890123456789012345678901 01234567890123456789012345678901 + #h<01000010001101001100011001101110 00011000000000001111011000000000 + #h<11000010011101000100011001111110 00010110000000110110110101000000 + # ++++++ ++++ ++++++ ++++ +++ ++ ++++++ + h1_id=h1[2:8]+","+h1[12:16]+","+h1[19:25]+","+h1[28:32] + h2_id=h2[0:3]+","+h2[8:10]+","+h2[26:32] + if h1_id=="000010,0100,001100,1110" and h2_id=="000,00,000000": + self.msgtype="NP" + elif args.harder: + h3=bits[2::4] + ok1,_ = magic_checksum(h1) + ok2,_ = magic_checksum(h2) + ok3,_ = magic_checksum(h3) + if ok1 and ok2 and ok3: + self.ec_lcw=1 + self.msgtype="NP" if "msgtype" not in self.__dict__: if args.harder: @@ -390,6 +451,20 @@ def __init__(self,msg): self.ec_lcw=1 self.msgtype="MS" + # try Certus traffic bursts by length alone under --harder + # guarded on self.next so we don't steal IRA/VOC bursts on iridium_access sync + if "msgtype" not in self.__dict__ and self.next and not (args.freqclass and self.uplink): + payload_syms = self.symbols - (len(iridium_access) // 2) + if payload_syms == 200: + self.ec_lcw=1 + self.msgtype="C1" + elif payload_syms == 432: + self.ec_lcw=1 + self.msgtype="C2" + elif payload_syms == 1824: + self.ec_lcw=1 + self.msgtype="C8" + if "msgtype" not in self.__dict__: if len(data)<64: raise ParserError("Iridium message too short") @@ -409,6 +484,14 @@ def __init__(self,msg): (blocks,self.descramble_extra)=slice_extra(data[hdrlen:],64) for x in blocks: self.descrambled+=de_interleave(x) + elif self.msgtype in ("C1", "C2", "C8"): + # Certus (NEXT / EBBS) traffic burst - carry raw bits through unchanged. + # The differential decoding that gr-iridium applies is incorrect for + # coherent-QPSK Certus waveforms, so these bits are not directly decodable. + # We still expose them so downstream tooling can look at them. + self.header="" + self.descrambled=data + self.descramble_extra="" elif self.msgtype=="AQ": datalen=2*26 self.header="" @@ -419,6 +502,10 @@ def __init__(self,msg): self.header=data[:hdrlen] self.descrambled=data[hdrlen:hdrlen+(256*3)] self.descramble_extra=data[hdrlen+(256*3):] + elif self.msgtype=="NP": +# self.header="" + self.descrambled=data[:432*2] + self.descramble_extra=data[(432*2):] elif self.msgtype=="RA": firstlen=3*32 if len(data)>2 @@ -756,6 +852,8 @@ def pretty(self): st+= " " + "Rid:%03d"%self.rid if self.ridcrc==self.crcval: st+= " " + "CRC:OK" + elif type(self.ridcrc) is str: + st += " " + "CRC:no[%s/%s]"%(self.ridcrc, '{0:08b}'.format(self.crcval)) else: st+= " " + "CRC:no[%04x]"%self.ridcrc @@ -764,6 +862,260 @@ def pretty(self): return st +np_crc16=crcmod.mkCrcFun(poly=0x1755b,initCrc=0,rev=False,xorOut=0) +np_crc8=crcmod.mkCrcFun(poly=0x12f,initCrc=0,rev=False,xorOut=0) + +class IridiumNPMessage(IridiumMessage): + def __init__(self,imsg): + self.__dict__=imsg.__dict__ + + symbols=de_dqpsk(self.descrambled) + bits=sym2bits(symbols) + + if "header" not in self.__dict__: + self.header="" + + # Re-sort bits + if len(bits)<840: + bcnt=len(bits)//160 + trailer=bits[160*bcnt::] + bits=bits[:160*bcnt] + + s1s=slice(bits[0::4],40) + s2s=slice(bits[1::4],40) + s3s=slice(bits[2::4],40) + s4s=slice(bits[3::4],40) + + blocks=list(itertools.chain.from_iterable(zip(s1s,s2s,s3s,s4s))) + +# btrail=blocks[-4:] +# blocks=blocks[:-4] +# trailer="".join(btrail)+trailer + +# blocks+=[blocks[-2][-8:]+btrail[0][:32]] +# blocks+=[btrail[1]] + else: + trailer=bits[800::2]+bits[801::2] + bits=bits[:800] + + s1s=slice(bits[0::4],40) + s2s=slice(bits[1::4],40) + s3s=slice(bits[2::4],40) + s4s=slice(bits[3::4],40) + + blocks=list(itertools.chain.from_iterable(zip(s1s,s2s,s3s,s4s))) + + blocks+=[trailer[:40]] + trailer=trailer[40:] + + checks=[magic_checksum(b)[0] for b in blocks] + + ### magic debug + ok="" + for b in blocks: + good, r = magic_checksum(b) +# print(b[32:],"%02x"%(r)) + if good and b[32:]=="00000000": + ok+="o" + elif good: + ok+="O" + else: + ok+="n" + self.magic=ok + ### end magic + + if not all(checks[:3]): # "INP" + raise ParserError("INP header not valid") + + hdr=[b[:32] for b in blocks[:3]] + blocks=blocks[3:] + checks=checks[3:] + + hdr_id=hdr[1][4:8] + + if hdr_id in ('1000','0111'): + self.hdr_type=1 + elif hdr_id in ('0110',): + self.hdr_type=2 + else: + self.hdr_type=0 + + # H1 + self.hdr_crc1=np_crc8(bytes( [int(x,2) for x in slice( + hdr[0]+hdr[1]+hdr[2][:-8] + ,8)])) + + # H2 + self.hdr_crc2=np_crc8(bytes( [int(x,2) for x in slice( + hdr[0]+hdr[1]+hdr[2][:8] + ,8)])) + + # Begin pkts + self.type=0 + self.hdr=hdr # 3 blocks a 32 bits + self.trailer=trailer # 24 bits + self.blocks=blocks # 18 blocks a 40 bits + self.checks=checks + + # Pkt v1 + self.v1trail=trailer[:8] + self.cs_v1=trailer[8:] + if self.cs_v1 == '': + self.cs_v1='0' + + self.the_crc_v1=np_crc16(bytes( [int(x,2) for x in slice( + hdr[-1][-8:]+ + "".join(blocks)+ + trailer + ,8)])) + + if self.the_crc_v1==0: + self.type=1 + + # Pkt v2 + csblocks=[b[:32] for b in blocks] + self.csblocks=csblocks + + self.cs_v2=self.trailer[:16] + self.v2trail=self.trailer[16:] + if self.cs_v2 == '': + self.cs_v2='0' + + self.the_crc_v2=np_crc16(bytes([int(x,2) for x in slice( + hdr[-1][-8:]+ + "".join(csblocks)+ + trailer[:16] + ,8)])) + + if self.the_crc_v2==0: + self.type=2 + + # Pkt v3 + if all(checks[:-3]) and not any(checks[-3:]): + self.type=3 + + # source_id: 14-bit field at s2s[0][10:24]. In captures the value + # partitions as [sv_id:8][beam_id:6]; the split is heuristic (partial + # empirical support only), so pretty() marks both fields with "?" + # to discourage treating them as authoritative. + self.source_id=int(s2s[0][10:24],2) + self.sv_id_guess=(self.source_id>>6) & 0xff + self.beam_id_guess=self.source_id & 0x3f + self.header+=":%05d"%self.source_id + return + + def upgrade(self): + return self + + def pretty(self): + st= "INP: "+self._pretty_header() + + st+= " sv?:%03d bm?:%02d"%(self.sv_id_guess, self.beam_id_guess) + st+= " H:%d"%self.hdr_type + st+= " T:%d"%self.type + + st+= " h<" + if self.hdr_type == 1: + st+= self.hdr[0] + " " + self.hdr[1] + " " + self.hdr[2][:-16] + st+=" CRC=%02x"%int(self.hdr[2][-16:-8],2) + if self.hdr_crc1==0: + st+="[OK]" + else: + st+="[no]" + st+= " " + st+= self.hdr[2][-8:] + elif self.hdr_type == 2: + st+= self.hdr[0] + " " + self.hdr[1] + st+=" CRC=%02x"%int(self.hdr[2][:8],2) + if self.hdr_crc2==0: + st+="[OK]" + else: + st+="[no]" + st+= " " + st+= " " # alignment + st+= self.hdr[2][8:] # 24 bit + else: + st+= self.hdr[0] + " " + self.hdr[1] + " " + self.hdr[2] + st+=">" + + if self.type==1: + st+= " d<" + for x in self.blocks: + st+= "".join(slice(x,8)) + st+= " " + st=st[:-1] + st+=">" + + st+= " t<"+self.v1trail+">" + st+= " CRC=%04x"%int(self.cs_v1,2) + if self.the_crc_v1==0: + st+="[OK]" + else: + st+="[no]" + st+= " " # alignment + elif self.type==2: + st+= " d<" + for x in self.csblocks: + st+= "".join(slice(x,8)) + st+= " " + st+= " " # alignment + st=st[:-1] + st+=">" + + st+= " CRC=%04x"%int(self.cs_v2,2) + if self.the_crc_v2==0: + st+="[OK]" + else: + st+="[no]" + + st+= " t<"+self.v2trail +">" + # Trailer checksum 'steals' two bytes from the previous block + ok, _=magic_checksum(self.blocks[-1][-16:]+self.trailer) + if ok: + st+="OK" + else: + st+="no" + + if all(self.checks): + st+=" MAGC" + else: + st+=" nomg" + elif self.type==3: + st+= " d<" + for x in self.csblocks[:-3]: + st+= "".join(slice(x,8)) + st+= " " + st+= " " # alignment + for x in self.blocks[-3:]: + st+= "".join(slice(x,8)) + st+= " " + st=st[:-1] + st+=">" + + st+= " t<"+" ".join(slice(self.trailer,8))+">" + else: + st+= " d<" + for i,x in enumerate(self.blocks): + if self.checks[i]: + st+= "".join(slice(x[:32],8)) + st+= " " # alignment + else: + st+= "".join(slice(x,8)) + st+= " " + st=st[:-1] + st+=">" + + st+= " t<"+" ".join(slice(self.trailer,8))+">" + + st+= " v1=%04x"%(self.the_crc_v1) + st+= " v2=%04x"%(self.the_crc_v2) + st+= " h1=%02x"%(self.hdr_crc1) + st+= " h2=%02x"%(self.hdr_crc2) + st+= " magic=%s"%(self.magic) + + st+=self._pretty_trailer() + return st + class IridiumSTLMessage(IridiumMessage): def __init__(self,imsg): self.__dict__=imsg.__dict__ @@ -834,11 +1186,11 @@ def __init__(self,imsg): if x in itl.MAP_PRS: self.msg.append(itl.MAP_PRS[x]) else: - if i==0 or self.msg[0] != 108: # special message does not contain normal PRS + if i == 0 or self.msg[0] not in (108, 109): # special message does not contain normal PRS raise ParserError("ITL V2 PRS Q#%d unknown"%i) self.msg.append(x) - if self.msg[0] != 108: + if self.msg[0] not in (108, 109): #sanity check the PRS sequence order sanity = "".join([str(itl.MAP_PRS_TYPE[x]) for x in self.q]) if self.plane%2 == 0: @@ -866,7 +1218,7 @@ def __init__(self,imsg): cat=None for qidx in range(len(self.q)): mindist=999 - if qidx > 0 and self.msg[0] == 108: + if qidx > 0 and self.msg[0] in (108, 109): self.msg[qidx]=self.q[qidx] next if self.q[qidx] in itl.MAP_PRS: @@ -1974,6 +2326,68 @@ def pretty(self): st += self._pretty_trailer() return st +# Inverse of gr-iridium's decode_deqpsk(), so callers can recover the original +# coherent QPSK symbols from a .bits capture. gr-iridium applies diff-decode to +# every burst unconditionally, which is correct for Block1 DEQPSK but wrong for +# coherent-QPSK Certus. Since check_sync_word() in gr-iridium has already +# validated the unique word against the coherent symbols (phase locked via PLL), +# the diff-decode is a deterministic bijection and invertible from the bits alone. +# +# Forward (gr-iridium decode_deqpsk): out[i] = _DEQ_FWD[ (s[i] - s[i-1]) % 4 ] +# Inverse: s[i] = (s[i-1] + _DEQ_INV[out[i]]) % 4 +_DEQ_FWD = (0, 2, 3, 1) +_DEQ_INV = tuple(_DEQ_FWD.index(x) for x in range(4)) # (0, 3, 1, 2) + +def un_deqpsk(bits, seed_symbol=0): + """Invert decode_deqpsk on a bit string (2 bits per symbol, MSB-first). + seed_symbol is the coherent symbol that preceded the first bit pair - for + payload bits stripped of the UW, this is the last UW coherent symbol (=2 + for UW_DL and UW_UL, both of which end with symbol 2). + Returns a bit string of the same length representing coherent QPSK symbols. + """ + old = seed_symbol + out = [] + for i in range(0, len(bits) - 1, 2): + sym_out = (int(bits[i]) << 1) | int(bits[i+1]) + diff = _DEQ_INV[sym_out] + s = (old + diff) & 3 + out.append('1' if s & 2 else '0') + out.append('1' if s & 1 else '0') + old = s + if len(bits) & 1: + out.append(bits[-1]) # stray bit pass-through (shouldn't happen) + return ''.join(out) + +class IridiumCertusMessage(IridiumMessage): + # Iridium Certus (NEXT / EBBS) traffic burst - coherent QPSK + Turbo-coded. + # Identified by symbol count on the simplex DL band: + # C1 = 200 symbols, NEXT C1 30ksps QPSK 4/5 (320 payload bits) + # C2 = 432 symbols, NEXT C2 60ksps QPSK 2/3 (576 payload bits) + # C8 = 1824 symbols, NEXT C8 240ksps QPSK 2/3 or 16APSK 2/3 (2432 or 4848 payload bits) + # + # gr-iridium emits diff-decoded bits that are correct for Block1 DEQPSK but + # wrong for these. We recover the coherent QPSK symbols in un_deqpsk() and + # expose them as the payload. Turbo decoding of the resulting stream is left + # as future work (requires the Iridium-specific interleaver, puncturing + # pattern, and Turbo polynomial, none of which are publicly documented). + def __init__(self, imsg): + self.__dict__ = imsg.__dict__ + # Seed from the last coherent UW symbol (UW_DL[11] = UW_UL[11] = 2). + self.coherent = un_deqpsk(self.descrambled, seed_symbol=2) + + def upgrade(self): + if self.error: return self + return self + + def pretty(self): + st = "I" + self.msgtype + ": " + self._pretty_header() # "IC1:", "IC2:", "IC8:" + # Emit coherent QPSK bits (recovered), grouped per symbol-pair for + # readability. These are the actual transmitted QPSK symbols, + # pre-Turbo-decoded, in the 2-bits-per-symbol form Iridium uses. + st += " coh:" + group(self.coherent, 16) + st += self._pretty_trailer() + return st + def symbol_reverse(bits): r = bytearray(bits.encode("us-ascii")) for x in range(0,len(r)-1,2): @@ -2026,6 +2440,41 @@ def de_dqpsk(bits): return symbols +def sym2bits(symbols): + bits="" + omap=["00","10","11","01"] + omap=["11","01","00","10"] # XXX: Negate everything. + bits="".join([omap[sym] for sym in symbols]) + return bits + +# calculate "Magic" checksum +# +# Input is 40 bits. 32 bits "data" and 8 bits "checksum" +# +# No idea how it is inteded to work, but it is +# completely linear from input bits, so we can check it that +# way, even if it is a bit convoluted. +# +# Checksum uses only lower 7 bits. +# +# Initial/Zero value is bits 4&5. +# +def magic_checksum(bits): + cv=int(bits[32:],2) + bits=bits[:32] + + magic=[87, 7, 91, 107, 11, 115, 19, 35, 28, 44, 76, 52, 84, 100, 104, 56, 22, 70, 26, 42, 74, 50, 82, 98, 93, 109, 13, 117, 21, 37, 41, 121] + + check=0 + for i, bit in enumerate(bits): + if bit=="1": + check^=magic[i] + + # bit 4 & 5 are the "check value" + if (check^cv) == 0b11000: + return True, check^cv + return False, check^cv + def split_qpsk(symbols): i_list="" q_list="" diff --git a/iridiumtk/reassembler/burst.py b/iridiumtk/reassembler/burst.py index 1e3dd23..f16c655 100755 --- a/iridiumtk/reassembler/burst.py +++ b/iridiumtk/reassembler/burst.py @@ -54,7 +54,7 @@ def consume(self, msg): multi = {} def process_l2(self, msg): - if not msg.correct: + if not msg.correct or not msg.fmt == 5: return ct = msg.content diff --git a/iridiumtk/reassembler/ida.py b/iridiumtk/reassembler/ida.py index ab8eee3..b06d0e8 100755 --- a/iridiumtk/reassembler/ida.py +++ b/iridiumtk/reassembler/ida.py @@ -807,12 +807,10 @@ def consume(self,q): # 5: number of messages waiting to be delivered / backlog # 6+7: "ack request" (answer: <50:xx:xx>) -- only for <26> - if data[0]==0x26: -# prehdr=data[:7] + if data[0] == 0x26 or (data[0] == 0x20 and data[7] == 0x10): prehdr="<%02x MTMSN=(%02x%02x) msgct:%d backlog=%d mid=(%02x:%02x)>"%(data[0],data[1],data[2],data[3],data[4],data[5],data[6]) data=data[7:] elif data[0]==0x20: -# prehdr=data[:5] prehdr="<%02x MTMSN=(%02x%02x) msgct:%d backlog=%d>"%(data[0],data[1],data[2],data[3],data[4]) data=data[5:] else: diff --git a/iridiumtk/reassembler/msg.py b/iridiumtk/reassembler/msg.py index 6b1099a..fe47d2c 100755 --- a/iridiumtk/reassembler/msg.py +++ b/iridiumtk/reassembler/msg.py @@ -4,6 +4,8 @@ import sys import datetime import re +import base64 +import crcmod from util import to_ascii, slice_extra, dt from .base import * @@ -67,12 +69,8 @@ def __init__(self): self.err=re.compile(r' ERR:') self.msg=re.compile(r'.* ric:(\d+) fmt:(\d+) seq:(\d+) (?:C:(..)\S*|[01 ]+) (\d)/(\d) csum:([0-9a-f][0-9a-f]) msg:([0-9a-f]*)\.([01]*) ') self.ms3=re.compile(r'.* ric:(\d+) fmt:(\d+) seq:(\d+) [01]+ \d BCD: ([0-9a-f]+)') - if 'noburst' in config.args: - global base64 - import base64 - import crcmod - self.crc16 = crcmod.predefined.mkPredefinedCrcFun("xmodem") - self.BURST_TRANS = bytes.maketrans(b'*-',b'/=') + self.crc16 = crcmod.predefined.mkPredefinedCrcFun("xmodem") + self.BURST_TRANS = bytes.maketrans(b'*-', b'/=') def filter(self,line): q=super().filter(line) @@ -182,15 +180,18 @@ def consume(self, msg): date = dt.epoch_local(msg.time).isoformat(timespec='seconds') str="Message %07d %02d @%s (len:%d)"%(msg.ric, msg.seq, date, msg.pcnt) txt= msg.content - if 'noburst' in config.args: - try: - ct = txt.translate(self.BURST_TRANS) - dec = base64.b64decode(ct) - crcv = self.crc16(dec) - if crcv == 0: - return - except Exception: - pass + try: + msg.burst = False + ct = txt.translate(self.BURST_TRANS) + dec = base64.b64decode(ct) + crcv = self.crc16(dec) + if crcv == 0: + msg.burst = True + except Exception: + pass + + if 'noburst' in config.args and msg.burst: + return if msg.fmt==5: out=to_ascii(txt, escape=True) @@ -198,7 +199,12 @@ def consume(self, msg): elif msg.fmt==3: out=txt str+= " BCD" - str+= (" fail:"," OK:")[msg.correct] + if msg.burst and msg.correct: + str += " GDB:" + elif msg.correct: + str += " OK:" + else: + str += " fail:" str+= " %s"%(out) print(str, file=outfile) diff --git a/iridiumtk/reassembler/pktstats.py b/iridiumtk/reassembler/pktstats.py index 8f23032..c888754 100755 --- a/iridiumtk/reassembler/pktstats.py +++ b/iridiumtk/reassembler/pktstats.py @@ -25,7 +25,7 @@ def __init__(self): self.default={} for k in ['UL', 'DL']: self.default[k]={} - for x in ['IBC', 'IDA', 'IIP', 'IIQ', 'IIR', 'IIU', 'IMS', 'IRA', 'IRI', 'ISY', 'ITL', 'IU3', 'I36', 'I38', 'MSG', 'VDA', 'VO6', 'VOC', 'VOD', 'MS3', 'VOZ', 'IAQ', 'NXT']: + for x in ['IBC', 'IDA', 'IIP', 'IIQ', 'IIR', 'IIU', 'IMS', 'IRA', 'IRI', 'ISY', 'ITL', 'IU3', 'I36', 'I38', 'MSG', 'VDA', 'VO6', 'VOC', 'VOD', 'MS3', 'VOZ', 'IAQ', 'INP', 'NXT']: self.default[k][x]=0 pass diff --git a/iridiumtk/reassembler/sbd.py b/iridiumtk/reassembler/sbd.py index 9ec6f8f..9640cc1 100755 --- a/iridiumtk/reassembler/sbd.py +++ b/iridiumtk/reassembler/sbd.py @@ -59,11 +59,11 @@ def process_l2(self,q): if data[0]==0x76: if ul: if data[1]<0x0c or data[1]>0x0e: - print("WARN: SBD: ul pkt with unclear type",data.hex(":"), file=sys.stderr) + #print("WARN: SBD: ul pkt with unclear type",data.hex(":"), file=sys.stderr) return else: if data[1]<0x08 or data[1]>0x0b: - print("WARN: SBD: dl pkt with unclear type",data.hex(":"), file=sys.stderr) + #print("WARN: SBD: dl pkt with unclear type",data.hex(":"), file=sys.stderr) return if data[0]==0x06: @@ -97,6 +97,12 @@ def process_l2(self,q): elif data[0]==0x20: prehdr=data[:5] data=data[5:] + # sometimes it seems to be 7 bytes long + if len(data) >= 2 and data[2] == 0x10: + # first DL Message will have 0x01 in data[2], so + # there should be no danger of misclassification + prehdr += data[:2] + data = data[2:] else: print("WARN: SBD: DL pkt with unclear header",data.hex(":"), file=sys.stderr) prehdr=data[:7] diff --git a/iridiumtk/reassembler/stats.py b/iridiumtk/reassembler/stats.py index ceb63f2..2236ac7 100755 --- a/iridiumtk/reassembler/stats.py +++ b/iridiumtk/reassembler/stats.py @@ -7,7 +7,7 @@ from .base import * from ..config import config, outfile, state -ft=['IBC', 'IDA', 'IIP', 'IIQ', 'IIR', 'IIU', 'IMS', 'IRA', 'IRI', 'ISY', 'ITL', 'IU3', 'I36', 'I38', 'MSG', 'VDA', 'VO6', 'VOC', 'VOD', 'MS3', 'VOZ', 'IAQ', 'NXT'] +ft=['IBC', 'IDA', 'IIP', 'IIQ', 'IIR', 'IIU', 'IMS', 'IRA', 'IRI', 'ISY', 'ITL', 'IU3', 'I36', 'I38', 'MSG', 'VDA', 'VO6', 'VOC', 'VOD', 'MS3', 'VOZ', 'IAQ', 'INP', 'NXT'] class StatsPKT(Reassemble): stats={} diff --git a/iridiumtk/reassembler/time.py b/iridiumtk/reassembler/time.py index 4c92a39..6c3fd1d 100755 --- a/iridiumtk/reassembler/time.py +++ b/iridiumtk/reassembler/time.py @@ -4,11 +4,53 @@ import sys import re from util import dt +import numpy as np from .base import * from ..config import config, outfile +early_frame = 3 +conv_start = None + + +def lbfc_str(ts, start=0): + """Convert milliseconds into L-Band frame counter""" + global conv_start + + if start != 0: + conv_start = start + + if conv_start is None: + return " " + + ts = ts - conv_start + lbfc_c = int((ts)//90) + lbfc_o = ts-lbfc_c*90 + + # guard + simplex + guard + 4* uplink + 4* downlink + # 1 + 20.32 + 1.24 + 4 * (8.28 + 0.22) + 0.02 + 4 * (8.28 + 0.1) - 0.1 + + # moved the first guard from the begining to the end + slots = (20.32+1.24, + 8.28+0.22, 8.28+0.22, 8.28+0.22, 8.28+0.22+0.02, + 8.28+0.1, 8.28+0.1, 8.28+0.1, 8.28+1 + early_frame, ) + + sname = ("S", "U1", "U2", "U3", "U4", "D1", "D2", "D3", "D4", ) + + st = 0 + for i, t in enumerate(slots): + if lbfc_o < st + t - early_frame: # allow slots to start slightly early + if len(sname[i]) == 1: + slot = f"{sname[i]}{round(lbfc_o - st):+03d}" + else: + slot = f"{sname[i]}{round(lbfc_o - st):+2d}" + break + st += t + return f"{lbfc_c:03d}∆{lbfc_o:+03.0f}#{slot}" + + +simplex = ("IRA:", "ITL:", "INP:", "IMS:", "MSG:") class ReassembleTIME(Reassemble): toff = None @@ -17,17 +59,29 @@ def __init__(self): def filter(self, line): q = super().filter(line) - if q is None: return None + if q is None: + print("-", line, end="") + return None return q def process(self, q): q.enrich(channelize=True) - if self.toff is None: + q.uxtime = np.datetime64(int(q.starttime), 's') + q.uxtime += np.timedelta64(q.nstime, 'ns') + if q.typ in ("IBC:", ) + simplex: # has a longer preamble + q.uxtime -= np.timedelta64((64-16)*(1000000//25000), 'us') + q.mstime -= (64-16)/25000 * 1000 + + if self.toff is None and q.typ in simplex: self.toff = q.mstime - strtime = dt.epoch(q.time).isoformat(timespec='centiseconds') - lbfc_c = (q.mstime-self.toff+45)//90 - lbfc_o = q.mstime-self.toff-lbfc_c*90 - return [f"{strtime} {lbfc_c%48:02.0f}#{lbfc_o:+07.3f} {q.typ} {q.freq_print} {q.confidence:3d}% {q.level:6.2f} {q.symbols} {q.uldl} {q.data}"] + + strtime = str(q.uxtime)[:-2] + if False: + lbfc_c = (q.mstime-self.toff+45)//90 + lbfc_o = q.mstime-self.toff-lbfc_c*90 + return [f"{strtime} {lbfc_c%48:02.0f}#{lbfc_o:+07.3f} {q.typ} {q.freq_print} {q.confidence:3d}% {q.level:6.2f} {q.symbols} {q.uldl} {q.data}"] + lbs = lbfc_str(q.mstime, self.toff) + return [f"{strtime} {lbs} {q.typ} {q.freq_print} {q.confidence:3d}% {q.level:6.2f} {q.symbols} {q.uldl} {q.data}"] def consume(self, q): print(q, end="", file=outfile) diff --git a/itl.py b/itl.py index 2eb0e88..e9be293 100644 --- a/itl.py +++ b/itl.py @@ -707,8 +707,8 @@ def map_sat(num, version): return ("S%02d"%(num-84), "N%02d"%(2)) elif num >=96 and num <=107: return ("R%02d"%(((num-96)%3)+1), "N%02d"%(((num-96)//3)+3)) - elif num == 108: - return ("---", "SSS") + elif num >= 108 and num <= 109: + return ("---", "SS%d"%(num-108)) elif num == 111: return ("---", "N%02d"%(8)) else: diff --git a/stats.py b/stats.py index 80c3070..f8bbb7b 100755 --- a/stats.py +++ b/stats.py @@ -45,6 +45,8 @@ frames['VDA'] = [colors[ 2], 'o', 1] frames['VO6'] = [colors[12], 'o', 1] +frames['INP'] = [colors[ 5], 'x', 1] + frames['IRI'] = ['purple', 'x', 0] frames['RAW'] = ['grey', 'x', 0] frames['NC1'] = ['grey', 'x', 0]