Академический Документы
Профессиональный Документы
Культура Документы
And then I found the IRC Channel called #hearsat in server StarChat (
irc.starchat.net ). So a lot of people actually helped me by giving me the
information I need to take a decision over which dish antenna should I
buy. They were suggesting a 1.2m offset dish (same time of the Sky,
Claro, and some other TV dishes, but bigger) for receiving GOES. Sadly I
couldn’t find a offset dish bigger than 90 cm here. So someone suggested
that I need at least 1.5m prime focus (the “old” type of TV dishes) that
should have equivalent surface area than a 1.2m offset dish. So I went to
Santa Efigenia Street (people from São Paulo will recognize) and went
to Sat Imagem Store. Since my father and I was already friends with a
seller, I asked what dishes they had to sell and said that I want something
around 1.5m. He said that he has a 1.9m dish to sell.
So I bought the dish, costed about R$200 (that’s about US$60) and also
some cables and adapters. So since I was very excited to get things
working, I asked my father’s help to assemble the antenna. I arrived
home about 5 PM so it was very late to assemble the antenna. So I let it
for the day after.
Assemble Process
So the assemble process was sort of straightforward. It was not easy to
do, but even without a manual (that did not came) you should be able to
figure out what piece goes where. We were in 4 persons, we started just
after lunch (about 1PM) and took us about 6 – 7 hours to get it
assembled. LOL
I also decided to inject Polyurethane Foam into the dish tube, so when it
rains it doesn’t get filled with water. The only tip I have is: Be careful, your
dish can grow balls.
So the dish assemble is done. Now we need to worry about the feed.
So I did several tries for like one month and half. The guys in #hearsat
and me was struggling to find what was the reason that I couldn’t get the
signal. I have all pictures of my failed feeds
here: https://www.flickr.com/photos/energylabs/albums/72157675387388
535. I will not detail each one of the assembles of these feeds because it
didn’t work. So for historical reasons as well the reason why they didn’t
work is because of the illumination angle of the Helical Coils. Usually the
Helix Feeds have a very narrow beam width (and high gain), this causes
to only some portion of the dish to be actually used by the feed. I only
discovered that when I drawed my dish in FreeCad and did some
auxiliary lines to show the angles. Then I noticed that something was
REALLY wrong: Only like 60cm of my dish was actually illuminated, and
from this 60cm only 40cm was actually visible by the dish (we need to
discount the dead center). This is the drawing I did. So mybit from hearsat
suggested me a Wave Guide Feed. For those that used to hack wireless
systems, this is known as CanAntenna or Cantenna. Its very simple to do
and I found this calculator to get things better:
http://www.changpuak.ch/electronics/cantenna.php
I also bought some aluminum boxes (in Santa Efigenia as well, but now
in Multcomercial ) for putting these LNA and filters. I made some tests
with the topology Feed -> LNA -> Filter, but it looked like the GSM
Signals was overloading the LNA. So I decided for a non-optimal setup
that is Feed -> Filter -> LNA that gave me what I want. So them I just
tried to fit everything inside a box and here is the result:
(Part 2 – Demodulator)
This is the LRIT Specification (theoretically):
According to the spec the LRIT signal is a BPSK (Binary Phase Shift
Keying) modulated that has a symbol rate of 293883 symbols /
second. The first thing is: How a BPSK Modulation works?
First we have two signals: The Carrier Wave and the Binary Stream that
we want to send. The Carrier Wave is a Sine signal that will carry the
modulated data along the path. It’s frequency is usually way higher than
the
e bitrate or symbol rate. In BPSK Modulation what we do is
basically change the phase of the signal. Since its a binary phase shift,
we basically only invert the phase of the signal, and that is basically invert
the local polarity of the wave. You can see in the picture below that when
we change the polarity we generate “notches” over the carrier
wave. That’s our bits being modulated into the carrier wave using a binary
phase shift. The advantage about changing phase is that we only need to
have a consistentnt phase in the receptor, not necessarily a very huge
amplitude of the signal.
BPSK Modulation
Just for reference, in QPSK (b) we would have a gray coded binary
values:
First we need to make sure that we have our carrier in the base band
(a.k.a. center of our spectrum). You might think that just tunning at
1691MHz will make sure of that, but there are some factor
factorss that we need
to account for:
1. The SDR Tuner Cristal is not 100% precise, and there will be some
deviation from the actual signal.
2. The temperature change over the SDR will also lead to deviation
on the signal.
3. Wind, atmosphere and other factors can doppler shift the signal.
How to do it then? We need to find where our signal really is, and
translate to the base band (center of the spectrum). But we have a
problem: The LRIT signal doesn’t actually contains the carrier wave, just
the modulated signal (so its a signal that is a consequence of the
modulated carrier wave).
If we had a Carrier Wave in the signal we could use a simple PLL (Phase
Locked Loop) to track the carrier wave and translate the signal. But
Carrier Waves also consume transmission power so the total
transmission power (from the satellite) would be “wasted” in a signal just
to track. For carrier-less
less signals like LRIT we need to use a more complex
PLL system. There are several that can do that, but I will use the famous
Costas Loop as suggested by Trango in #hearsat.
Costas Loop is basically a double PLL system. The main idea is: If our
supposed carrier is not centered, there will be a phase error that will
accumulate over the time in the Quadrature Signal. So what we do? Use
this error to translate our signal to the base band. The actual details of
how the costas loop works will not be discussed here, but there are plenty
of documents explaining how it works in the internet. For a BPSK we will
use a 2nd Order Costas Lo Loop.
op. There is also the 3rd order costas loop for
QPSK and 4th order costas loop for 8 8-PSK.
Ok, after that we need to recovery a second information: The Symbol
Clock. The thing is: we modulated a binary signal over the carrier wave,
but if we have like a string of 10 bits one, how we would know that the
string has 10 bits, and not 5 or 7? We will need to recover the original
clock using a M&M (Mueller and Müller) algorithm. But before that, how
can we even recovery such thing?
After that we should have our symbol in the I vector of our IQ Sample.
Let’s set the Sample rate to 3e6, the Center Frequency to 1691e6 and all
gains to 15.. For the gains setting you can experiment your own values,
but I found that I get the best SNR with everything maxed out (thats not
very common). Also Osmocom Source has a “bug” for airspy, that is it
doesn’t get the Mixer Gain Available (just because its not BB gain. That’s
stupid.). I made a patch (that was actively rrejected
ejected because of the Gain
name) to map the Mixer gain to BB gain (as it is for RTLSDR). In the
future I will probably do a new GRC Block to use with airspy with the
correct names and stuff, but for now you can compile gr gr-osmosdr
osmosdr from
source code using my fork: https://github.com/racerxdl/gr-osmosdr
osmosdr.
Another step is the RRC Filter (Root Raised Cosine Filter). This is a filter
optimized for nPSK modulations and uses as a parameter our symbol
rate. The Filter is not very hard to generate (its a FIR with some specific
taps), but luckily GNU Radio provide a block for us.
RRC Filter
As I talked before, we will use 2nd Order Costas Loop as Carrier Wave
Recovery (synchronization) and M&M Clock Recovery to recover the
Symbol Clock. GNU Radio provides blocks for both algorithms. Let’s start
with Costas Loop.
Costas Loop
For the M&M Parameters we will use Omega as 4.25339 that is basically
our symbols per sample rate, or sample_rate / symbol_rate. That is the
first symbolrate guess for M&M. For Gain Omega we use (alpha ^ 2) / 4,
that is alpha = 3.7e-3,
3, so our Gain Omega be 3.4225e
3.4225e-6, Mu as
0.5, Gain Mu as alpha (or 3.7e
3.7e-3), Omega Relative Limit as 5e-3.
5e
So you can notice that I called a new parameter alpha in M&M that is not
a direct parameter of the block. That alpha is a parameter to adjust how
much the M&M clock recovery can deviate from the initial guess. You can
experiment with your own values, but 3.7e
3.7e-3
3 was the best option to me.
Now at the output of M&M we will have our Complex Symbols pumped
out with the correct rate. Now we only need to extract our values.
For now what we need to do is to get the Complex output from the M&M,
get only the Real Part ( Component I ) transform to byte and output (to file
or TCP Pipe).
Thats a simple two block operation. First we use Complex to Real block
that will output a Float with the Real component of the complex number,
and then convert to char multiplying by 127 (since the Complex is
normalized). After that we can use File Sink to output to a file or create a
vector stream to output to a TCP Socket. In my case I will use TCP
Socket.
The Stream to Vector just aggregates every 16 bytes before sending
through TCP. This will reduce the TCP Packet Overhead. The TCP Sink
parameters are:
With this parameters, when we run the GRC Flow, it will try to connect to
localhost at port 5000 and send every group of 16 bytes (or if you prefer,
symbols) through TCP. The next part of this article will deal with the
software part to decode this and generate the packets for creating the
output files. Below you can check my final flow. It also has some other
blocks to have e a better flexibility changing the parameters and also to
show the waterfall / fft of the input signal as well the constelation map.
(Part 3 – Frame Decoder)
In the last chapter of GOES Satellite Hunt, I explained how I did the
BPSK Demodulator for the LRIT Signal. Now I will explain how to decode
the output of the data we got in the last chapter.
One thing that is worth mentioning is that most (if not all) weather
satellites that transmit digital signals use the CCSDS standard packet
format, or at least something based on it. For example this frame decoder
can be used (with some modifications due QPSK instead BPSK) for
LRPT Signals from Meteor Satellites (I plan to do a LRPT decoder as well
in the future, and I will post about it). I will not descri
describe
be my entire code
here, just the pieces for decoding the data. I will also not write the entire
code here, since it can be checked in github. So before start see the
picture below (again). We will some info from it as well.
Convolution Encoding
Convolution Encoding basically is a process that generates parity of
original data as the output. This parity has a special feature that is
constructing a Trellis Diagram Sequence.
By Qef – Own work, Public Domain. Click on the image for details
A trellis diagram of two bits (as shown by the image), for every pair of
bits, there is only two possible next two bits. For example if my pair is 00,
the only possible next pair is 00 or 10. By convolution encoding a data,
we generate parity that follows the Trellis diagram, in this way if we have
errors in our bit stream, we can use a statistical way to find the most
possible path in the trellis diagram that represents that data, then
correcting the errors.
So how actually
tually the data is encoded? The image below will show a
example of convolution encoder:
By Teridon at English Wikipedia - Own work (Teridon), Public Domain, Link
So let me give an example. Suppose we’re already with the k buffer filled
with 101. Our next bit is 1. So we shift our buffer k right, and put our input
bit on the left-most position. So our buffer now will have 110. Now our 3
output bits will be:
n0 = 1 + 1 + 0 = 0
n1 = 0 + 1 + 0 = 1
n2 = 1 + 0 + 0 = 1
Keep in mind, this is a mod 2 operation (we only have one bit output). So
basically we just get every buffer k bits, xor with the corresponding
position of the corresponding poly, and them sum to the output.
In this chapter we will use a lot of libfec routines to process our data. The
first one is to encode the sync words.
1 #include <stdio.h>
2 #include <stdlib.h>
#include <stdint.h>
3 #include <fec.h>
4
5 int main(int argc,char *argv[]) {
6 int polys[2] = {0x4f, 0x6d};
7 char syncword[] = {0x1A, 0xCF, 0xFC, 0x1D};
char outputword[64];
8
9 uint64_t encodedWord = 0;
10
11 char buff[2];
12
13 unsigned int encstate;
14 int wordSize = 4;
15 int processedBytes = 0;
int encodedWordPosition = 63;
16 int outputwordPosition = 0;
17
18 char c;
19
20 encstate = 0;
21 while(processedBytes < wordSize){
c = syncword[processedBytes];
22
for(int i=7;i>=0;i--){
23 encstate = (encstate << 1) | ((c >> 7) & 1);
24 c <<= 1;
25 // Soft Encoded Word
26 outputword[outputwordPosition] = 0 - parity(encstate & polys[0]);
outputword[outputwordPosition + 1] = 0 - parity(encstate & polys[1]);
27 outputwordPosition += 2;
28
29 // Encoded Word
30 encodedWord |= (uint64_t)(1-parity(encstate & polys[0])) << (encodedWordP
31 encodedWord |= (uint64_t)(1-parity(encstate & polys[1])) << (encodedWordP
32 encodedWordPosition-=2;
}
33 processedBytes++;
34 }
35
36 printf("Encoded Word: 0x%016lx\n", encodedWord);
37 printf("Soft Encoded Word: \n");
for (int i=0; i<64; i++) {
38 printf("0x%x ", outputword[i] & 0xFF);
39 }
40
41 printf("\n");
42
43 return 0;
44 }
45
46
47
48
49
50
51
Also the program will output the Soft Symbols for that Encoded Sync
Word, that is basically for every bit 0 it will have a byte 0, for every bit 1 it
will have a byte 0xFF (256). That’s because we are sure that these are
the correct bits (so we get the maximum values). In our decoder we’ll
convert the uint64_t encoded syncwords to a soft symbol array to use in
the correlation filter.
Frame Synchronization
Now we have the sync word, we can do the frame synchronization. I will
use a correlation system to find the most probably location for the sync
word. For that I will basically sum the difference of each XOR (UW[n] ^
data[i+n]) to a variable. The position that has the highest value (a.k.a.
highest correlation) is where our word is most probably.
1
2
3 typedef struct {
4 uint32_t uw0mc;
uint32_t uw0p;
5 uint32_t uw2mc;
6 uint32_t uw2p;
7 } correlation_t ;
8
9
10 const uint64_t UW0 = 0xfca2b63db00d9794; // 0 degrees inverted phase shift
const uint64_t UW2 = 0x035d49c24ff2686b; // 180 degrees inverted phase shift
11
12 uint8_t UW0b[64]; // The Encoded UW0
13 uint8_t UW2b[64]; // The Encoded UW2
14
15 void initUW() {
16 printf("Converting Sync Words to Soft Data\n");
17 for (int i = 0; i < 64; i++) {
UW0b[i] = (UW0 >> (64-i-1)) & 1 ? 0xFF : 0x00;
18 UW2b[i] = (UW2 >> (64-i-1)) & 1 ? 0xFF : 0x00;
19 }
20 }
21
22 uint32_t hardCorrelate(uint8_t dataByte, uint8_t wordByte) {
//1 if (a > 127 and b == 255) or (a < 127 and b ==
23 return (dataByte >= 127 & wordByte == 0) | (dataByte < 127 & wordByte == 255);
24 }
25
26
27 void resetCorrelation(correlation_t * corr) {
28 memset(corr, 0x00, sizeof(correlation_t));
29 }
30
void checkCorrelation(uint8_t *buffer, int buffLength, correlation_t *corr) {
31 resetCorrelation(corr);
32 for (int i = 0; i < buffLength - 64; i++) {
33 uint32_t uw0c = 0;
34 uint32_t uw2c = 0;
35
for (int k = 0; k < 64; k++) {
36
uw0c += hardCorrelate(buffer[i+k], UW0b[k]);
37 uw2c += hardCorrelate(buffer[i+k], UW2b[k]);
38 }
39
40 corr->uw0p = uw0c > corr->uw0mc ? i : corr->uw0p;
41 corr->uw2p = uw2c > corr->uw2mc ? i : corr->uw2p;
42
corr->uw0mc = uw0c > corr->uw0mc ? uw0c : corr->uw0mc;
43 corr->uw2mc = uw2c > corr->uw2mc ? uw2c : corr->uw2mc;
44 }
45 }
46
47
48
49
50
Synced Frames
See that the syncword is pretty clear in the bit display? You can use that
to visually identify sync words or static data inside the frames. You can
also notice that there are even more static data in the frames. We will talk
about it later. Now our data is ready to decode.
So as a side note now: Do you remember in the last chapter that I said
about saving only the soft symbols? We synced using the soft symbols
and viterbi will also use the soft symbols. What is the advantage? Since
both Correlation Search and Viterbi are statistical algorithms, this
increase the odds that a value that is on the middle of the decision tree
(for example something close to 0 on a signed byte, that can be either bit
1 or bit 0) will not count much for the statistics. So a number that is on the
middle can be either 0 or 1, viterbi will use that as a *I can be any value*
in the trellis diagram and see which one is the most probably using other
values in the sequence. This increases the quality of decoding the same
as if you signal as about 2dB Higher SNR, and believe, that’s a HUGE
improvement.
Usually after decoding a frame, you might also want to check the quality
of the decoding. So a good way to measure is by re-encoding the signal
and comparing the two streams of the original encoded signal and the
expected encoded signal (the one you encoded).
1 /**/
2
void convEncode(uint8_t *data, int dataLength, uint8_t *output, int outputLen) {
3 unsigned int encstate = 0;
4 uint8_t c;
5 uint32_t pos = 0;
6 uint32_t opos = 0;
7
8 memset(output, 0x00, outputLen);
while (pos < dataLength && (pos * 16) < outputLen) {
9 c = data[pos];
10 for(int i=7;i>=0;i--){
11 encstate = (encstate << 1) | ((c >> 7) & 1);
12 c <<= 1;
output[opos] = ~(0 - parity(encstate & viterbiPolynomial[0]));
13 output[opos+1] = ~(0 - parity(encstate & viterbiPolynomial[1]));
14
15 opos += 2;
16 }
17 pos++;
}
18 }
19
20 uint32_t calculateError(uint8_t *original, uint8_t *corrected, int length) {
21 uint32_t errors = 0;
22 for (int i=0; i<length; i++) {
23 errors += hardCorrelate(original[i], ~corrected[i]);
}
24
25 return errors;
26 }
27 /**/
28
29 convEncode(decodedData, 1024, correctedData, 16384);
uint32_t errors = calculateError(codedData, correctedData, 16384) / 2;
30 float signalErrors = (100.f * errors) / 8192;
31 /**/
32
33
34
35
36
37
Then in the variable signalErrors you will have the percent of wrong bits
that had been corrected in your frame output. After that if you output to a
file and check with BitDisplay you will see something like this:
Decoded Frame View (cropped to the first bits only)
The bitdisplay actually shows the 0 bits as white and 1 bits as black. So
the overall perception is inverted. You can see the sync word pattern and
just after that a pattern that looks like a counter. This counter is actually
the frame counter. After that you can see a wide black bar and some
small patterns on the right side of this bar. This is actually the 11 bit
Virtual Channel ID. Since all channel IDs are usually lower than 64, most
of the bits are 0.
Now if you strip the first 4 bytes of each 1024 byte frame, you will have a
virtual channel frame from the satellite! In the next part I will show how to
parse this frame data and extract the packets that will in the last chapter
form our files. You can check my frame decoder code here:
https://github.com/racerxdl/open-satellite-
project/blob/225a36d4144c0fe0704eb50a8fbc428914f654c0/GOES/netw
ork/decoder_tcp.c
(Part 4 – Packet Demuxer)
In the last chapter I showed how to get the frames from the demodulated
bit stream. In this chapter I will show you how to parse these frames and
get the packets that will on next chapter generate the files that GOES
send. I will first add C code to the code I did in the last chapter to
separated all the virtual channels by ID. But mainly this chapter will be
done in python (just because its easier, I will eventually make a C code as
well to do the stuff).
De-randomization
randomization of the data
One of the missing things on the last chapter was the frame data de
de-
randomization. The data inside the frame (excluding the sync word) is
randomized by a generator polynomial. This is done because of few
things:
CCSDS has a standard Polynomial as well, and the image below shows
how to generate the pseudo
pseudo-random bistream:
CCSDS Pseudo-random Bitstream Generator
The PN Generator polynomial (as shown in the LRIT spec) is x^8 + x^7 +
x^5 + x^3 + 1. You can check several PN Sequence Generators on the
internet, but since the repeating period of this PN is 255 bytes and we’re
xor’ing with our bytestream I prefer to make a lookup table with all the 255
byte sequence and then just xor (instead generating and xor). Here is the
255 byte PN:
1 char pn[255] = {
};
31
32
33
34
And for de-randomization just xor’it with the frame (excluding the 4 byte
sync word):
1 for (int i=0; i<1020; i++) {
2 decodedData[i] ^= pn[i%255];
3 }
I will assume you have a uint8_t buffer with a frame data of 1020 bytes
(that is, the data we got in the last chapter with the sync word excluded).
The CCSDS standard RS uses 255,223 as the parameters. That means
that each RS Frame has 255 bytes which 223 bytes are data and 32
bytes are parity. With this specs, we can correct any 16 bytes in our 223
byte of data. In our LRIT Frame we have 4 RS Frames, but the structure
are not linear. Since the Viterbi uses a Trellis diagram, the error in Trellis
path can generate a sequence of bad bytes in the stream. So if we had a
linear sequence of RS Frames, we could corrupt a lot of bytes fro from one
frame and lose one of the RS Frames (that means that we lose the entire
LRIT frame). So the data is interleaved by byte. The image below shows
how the data is spread over the lrit frame.
ReedSolomon Interleaving
// Copy data
4
for (int i=0; i<223; i++) {
5
rsbuff[i] = data[i*I + pos];
6
}
7
// Copy parity
8
for (int i=0; i<32; i++) {
9 rsbuff[i+223] = data[PARITY_OFFSET + i*I + pos];
10 }
11 }
12
// Copy data
14
for (int i=0; i<223; i++) {
15
outbuff[i*I + pos] = idata[i];
16
}
17
// Copy parity - Not needed here, but I do.
18
for (int i=0; i<32; i++) {
19 outbuff[PARITY_OFFSET + i*I + pos] = idata[i+223];
20 }
21 }
22
1 #define RSBLOCKS 4
2 int derrors[4] = { 0, 0, 0, 0 };
3 uint8_t rsWorkBuffer[255];
4 uint8_t rsCorrectedData[1020];
5
for (int i=0; i<RSBLOCKS; i++) {
6
deinterleaveRS(decodedData, rsWorkBuffer, i, RSBLOCKS);
7
derrors[i] = decode_rs_ccsds(rsWorkBuffer, NULL, 0, 0);
8
interleaveRS(rsWorkBuffer, rsCorrectedData, i, RSBLOCKS);
9
}
10
In the variable derrors we will have how many bytes it was corrected for
each RS Frames. In rsCorrectedData we will have the error corrected
output. The value -1 in derrors it means the data is corrupted beyond
correction (or the parity is corrupted beyond correction). I usually drop the
entire frame if all derrors are -1,
1, but keep in mind that the corruption can
happen in the parity only (we can have corrupted bytes in parity that will
lead to -1
1 in error correction) so it would be wise to not do like I did. After
that we will have the corrected LRIT Frame that is 892 bytes wide.
VCDU Header
Fields:
Basically we will only use 2 values from the header: VCID and Counter.
1
uint32_t swapEndianess(uint32_t num) {
2
return ((num>>24)&0xff) | ((num<<8)&0xff0000) | ((num>>8)&0xff00) | ((num<<24
3
}
4
5 (...)
counter = swapEndianess(counter);
10
counter &= 0xFFFFFF00;
11
counter = counter >> 8;
12
I usually save the last counter value and compare with the current one to
see if I lost any frame. Just be carefull that the counter value is per
channel ID (VCID). I actually never got any VCID higher than 63, so I
store the counter in a 256 int32_t array.
One last thing I do in the C code is to discard any frame that has 63 as
VCID. The VCID 63 only contains Fill Packets, that is used for keeping
the satellite signal continuous, even when not sending anything. The
payload of the frame will always contain the same sequence (that can be
sequence of 0, 1 or 01).
Packet Demuxer
Having our virtual channels demuxed for files channel_ID.bin, we can do
the packet demuxer. I did the packet demuxer in python because of the
easy of use. I plan to rewrite in C as well, but I will explain using python
code.
Channel Data
Each channel Data can contain one or more packets. If the Channel
contains and packet end and another start, the First Header Pointer (the
11 bits from the header) will contain the address for the first header inside
the packet zone.
So let’s talk first about handling a new packet. Here is the structure of a
packet:
We have a 6 byte header containing some useful info, and a user data
that can vary from 1 byte to 8192 bytes. So this packet can span across
several frames and we need to handle it. Also there is another tricky thing
here:
e: Even the packet header can be split across two frames (the 6 first
bytes can be at two frames) so we need to handle that we might not have
enough data to even check the packet header. I created a function
called CreatePacket that receives a buffer param
parameter
eter that can or not
have enough data for creating a packet. It will return a tuple that contains
the APID for the packet (or -1
1 if buffer doesn’t have at least 6 bytes) and
a buffer that contains any unused data for the packet (for example if there
was moreore than one packet in the buffer). We also have a function
called ParseMSDU that will receive a buffer that contains at least 6 bytes
and return a tuple with the MSDU (packet) header decomposed. There is
also a SavePacket function that will receive the ch channelId
annelId (VCID) and a
object to save the data to a packet file. I will talk about the SavePacket
later.
1 import struct
3 SEQUENCE_FLAG_MAP = {
4 0: "Continued Segment",
5 1: "First Segment",
6 2: "Last Segment",
7 3: "Single Data"
}
8
9
pendingpackets = {}
10
11
def ParseMSDU(data):
12
o = struct.unpack(">H", data[:2])[0]
13
version = (o & 0xE000) >> 13
14
type = (o & 0x1000) >> 12
15 shf = (o & 0x800) >> 11
17
18 o = struct.unpack(">H", data[2:4])[0]
24
def CreatePacket(data):
25
while True:
26 if len(data) < 6:
27 return -1, data
29 pdata = data[:packetlength+2]
30 if apid != 2047:
pendingpackets[apid] = {
31
"data": pdata,
32
33 "version": version,
34 "type": type,
"apid": apid,
35
"sequenceflag": SEQUENCE_FLAG_MAP[sequenceflag],
36
"sequenceflag_int": sequenceflag,
37
"packetnumber": packetnumber,
38
"framesdropped": False,
39
"size": packetlength
40 }
41
43 else:
44 apid = -1
45
if not packetlength+2 == len(data) and packetlength+2 < len(data): # Multiple p
46
SavePacket(sys.argv[1], pendingpackets[apid])
47
del pendingpackets[apid]
48
data = data[packetlength+2:]
49
apid = -1
50
print " Multiple packets in same buffer. Repeating."
51 else:
52 break
54
55
56
57
6 print " Data was incomplete from last FHP. Parsing packet now"
7 if fhp > 0:
18 pendingpackets[lastAPID]["data"] += data[:fhp]
SavePacket(sys.argv[1], pendingpackets[lastAPID])
20
del pendingpackets[lastAPID] # Erase the last packet data
21
lastAPID = -1
22
23
# Try to create a new packet
24
buff += data[fhp:]
25
lastAPID, data = CreatePacket(buff)
26 if lastAPID == -1:
27 buff = data
else:
28
buff = ""
29
30
31
32
This should handle all frames that has a new header. But maybe the
packet is so big that we got frames without any header (continuation
packets). In this case the FHP will be 2047, and basically we have three
things that can lead to that:
1. The header was split between last frame end, and the current
frame. FHP will be 2047 and after we append to our buffer we will
have a full header to start a packet
2. We just need to append the data to last packet.
3. We lost some frame (or we just started) and we got a continuation
packet. So we drop it.
...
1
else:
2
if len(buff) > 0 and lastAPID == -1: # Split Header
3
print " Data was incomplete from last FHP. Parsing packet now"
4 buff += data
6 if lastAPID == -1:
7 buff = data
8 else:
buff = ""
9
elif len(buff)> 0:
10
# This shouldn't happen, but I put a warn here if it does
11
print " PROBLEM!"
12
elif lastAPID == -1:
13 # We don't have any pending packets, and we received
pass
15
else:
16
# We have a last packet, so we append the data.
17
print " Appending %s bytes to %s" % (lastAPID, len(data))
18
pendingpackets[lastAPID]["data"] += data
19
20
21
Now let’s talk about the SavePacket function. I will describe some of the
stuff here, but there will be also something described on the next chapter.
Since the packet data can be compressed, we will need to check if the
data is compressed, and if it is, we need to decompress. In this part we
will not handle the decompression or the file assembler (that will need
decompression).
1 {
2 "data": pdata,
3 "version": version,
4 "type": type,
"apid": apid,
5
6 "sequenceflag": SEQUENCE_FLAG_MAP[sequenceflag],
7 "sequenceflag_int": sequenceflag,
"packetnumber": packetnumber,
8
"framesdropped": False,
9
"size": packetlength
10
}
11
The data field have the data we need to save, the type says the type of
packet (and also if its compressed), the sequenceflag says if the packet
is:
The size parameter is the length of data field – 2 bytes. The two last
bytes is the CRC of the packet. The CCSDS only specify the polynomial
for the CRC, CRC-CCITT standard. I made a very small function based
on a few C functions I found over the internet:
1 def CalcCRC(data):
2 lsb = 0xFF
msb = 0xFF
3
for c in data:
4
x = ord(c) ^ msb
5
x ^= (x >> 4)
6
msb = (lsb ^ (x >> 3) ^ (x << 4)) & 255
7
lsb = (x ^ (x << 5)) & 255
8 return (msb << 8) + lsb
12 if not c == crc:
15
On SavePacket function we should check the CRC to see if any data was
corrupted or if we did any mistake. So we just check the CRC and then
save the packet to a file (at least for now):
EXPORTCORRUPT = False
1
def SavePacket(channelid, packet):
2
global totalCRCErrors
3
global totalSavedPackets
4
global tsize
5
global isCompressed
6 global pixels
7 global startnum
8 global endnum
10 try:
os.mkdir("channels/%s" %channelid)
11
except:
12
pass
13
14
if packet["apid"] == 2047:
15
print " Fill Packet. Skipping"
16
return
17
18 datasize = len(packet["data"])
19
20 if not datasize - 2 == packet["size"]: # CRC is the latest 2 bytes of the payloa
21 print " WARNING: Packet Size does not match! Expected %s Found: %s" %(pack
25
data = packet["data"][:datasize-2]
26
27
if packet["sequenceflag_int"] == 1:
28
print "Starting packet %s_%s_%s.lrit" % (packet["apid"], packet["version"], p
29
startnum = packet["packetnumber"]
30 elif packet["sequenceflag_int"] == 2:
32 endnum = packet["packetnumber"]
33 if startnum == -1:
return
35
elif packet["sequenceflag_int"] != 3 and startnum == -1:
36
print "Orphan Packet. Dropping."
37
return
38
39
if packet["framesdropped"]:
40
print " WARNING: Some frames has been droped for this packet."
41 filename = "channels/%s/%s_%s_%s.lrit" % (channelid, packet["apid"], packet["ve
42 print "- Saving packet to %s" %filename
43
44
45 crc = packet["data"][datasize-2:datasize]
46 if len(crc) == 2:
47 crc = struct.unpack(">H", crc)[0]
else:
49
crc = False
50
if not crc:
51
print " WARNING: CRC does not match!"
52
totalCRCErrors += 1
53
54
if crc or (EXPORTCORRUPT and not crc):
55 f = open(filename, "wb")
56 f.write(data)
57 f.close()
58
59 totalSavedPackets += 1
else:
60
print " Corrupted frame, skipping..."
61
62
63
64
65
66
With that you should be able to see a lot of files being out of your
channel, each one being a packet. If you get the first packet (with the
sequenceflag = 1), you will also have the Transport Layer header that
contains the decompressed file size, and file number. We will handle the
decompression and lrit file composition in next chapter. You can check
the final code here: https://github.com/racerxdl/open-satellite-
project/blob/master/GOES/standalone/channeldecoder.py
GOES Satellite Hunt (Part 5 – File
Assembler)
0 Comments
In the packets before we have a flag called continuation flag that will
specify if the packet is a start, continuation or end packet. It will also
say if the packet itself is a single file. Usually the file header is entire
contained on the first packet. One of the things we need to do is
aggregate the start, continuation and end packets into a single file. This is
easy since the when a start packet appears inside a channel with the
same APID, the entire file will be transmitted at once, so the packets that
will follow will be either continuation or end before a new start comes. So
basically we just need to find a Packet Start with a certain APID and then
grab and aggregate all of the following packets that has the same APID.
But the packet content may also be compressed
using LritRice.lib (provided by NOAA). To check if we need to
decompress the packet or not, we need to check the header of the start
packet (the header will always be decompressed).
Discarding the first 10 bytes (2 bytes for file counter and 8 bytes for the
length), you will have the Primary Header. The primary header has
basically just the size of the LRIT File header. We will need to parse all
the headers (including the secondary) in order to find if the continuation
packets will be compressed and if they are, what are the parameters to
decompress (yes, they can change). I created
a packetmanager.py script to export few helper functions to parse the
header inside channeldecode.py
channeldecode.py.. There are several header types with
different lengths, but they do have two parameters in common:
So what I usually do, is to grab the first 3 bytes of a header, check the
size (the size includes these 3 bytes) and then fetch more size – 3 bytes
to the buffer. With this buffer I pass to another function that will parse the
header data and return a obje
object with their parameters. This is
my parseHeader function:
1 def getHeaderData(data):
headers = []
2 while len(data) > 0:
3 type = ord(data[0])
4 size = struct.unpack(">H", data[1:3])[0]
5 o = data[3:size]
6 data = data[size:]
td = parseHeader(type, o)
7 headers.append(td)
8 if td["type"] == 0:
9 data = data[:td["headerlength"]-size]
10 return headers
11
12
With that, we have enough stuff for using in our channeldecoder.py and
know if the file is compressed. Basically we can do a simple import
packetmanager and use the packetmanager.py functions.
LritRice Compression
Usually for images, the data is compressed using LritRice.lib. Although
RICE compression is a open standard (NASA’s fitsio library has
compression and decompression of RICE), the LritRice use a modified
version. With time I will reverse engineer and create a open version that
will be able to decompress LRIT data, but for now I had to do a
workarround. Since the LritRice from linux is “broken”, I made a very
nasty workarround:
The decompressor is made to receive some arguments and has two ways
of operation:
We’re gonna use the Multi File decompression. It does basically the same
as single file, but iterates over several files and decompress all of them
into a single file. So the output file will be a single file with all of the
original files together (so our final LRIT file). The StartNumber should be
the number of the first continuation packet (so not the header packet).
The Multi File Decompression will look into StartNumber-1 to EndNumber
files, being the StartNumber-1 just rewrited to the output file that will have
a _decomp suffix. So in our channeldecoder.py we need to do few
steps.
First let’s check if either the packet stream will need to be decompressed
or just appended. If they just need to be appended, we only do that. Text
files and DCS files are usually not compressed.
1
if packet["sequenceflag_int"] == 1:
2
print "Starting packet %s_%s_%s.lrit" % (packet["apid"], packet["version"], pack
3 startnum = packet["packetnumber"]
4 p = packetmanager.getHeaderData(data[10:])
5 for i in p:
6 if i["type"] == 1 or i["type"] == 129:
7 isCompressed = not i["compression"] == 0
if i["type"] == 1:
8 pixels = i["columns"]
9
1
elif packet["sequenceflag_int"] == 2:
2
print "Ending packet %s_%s_%s.lrit" % (packet["apid"], packet["version"], packe
3 endnum = packet["packetnumber"]
4 if startnum == -1:
5 print "Orphan Packet. Dropping"
6 return
7 elif packet["sequenceflag_int"] != 3 and startnum == -1:
print "Orphan Packet. Dropping."
8 return
9
1 if isCompressed:
2 filename = "channels/%s/%s_%s_%s.lrit" % (channelid, packet["apid"], packet["ver
3 else:
4 filename = "channels/%s/%s_%s.lrit" % (channelid, packet["apid"], packet["versio
Now, in aspect of saving the file. If its not compressed we need to open
for appending, if it is we just save by skipping the 10 first bytes.
1
2 from subprocess import call
3
def Decompressor(prefix, pixels, startnum, endnum):
4 startnum += 1
5 call(["wine", "Decompress.exe", prefix, str(pixels), str(startnum), str(endnu
6 for i in range(startnum-1, endnum+1):
7 k = "%s%s.lrit" % (prefix, i)
if os.path.exists(k):
8 os.unlink(k)
9 return "%s_decomp%s.lrit" % (prefix, startnum-1)
10
The decompressed var will have the final filename of the decompressed
file.
1
2
3 def manageFile(filename):
f = open(filename, "r")
4
5 try:
6 k = readHeader(f)
7 type, filetypecode, headerlength, datalength = k
8 except:
9 print " Header 0 is corrupted for file %s" %filename
return
10
11 newfilename = filename
12 while f.tell() < headerlength:
13 data = readHeader(f)
14 if data[0] == 4:
#print " Filename is %s" % data[1]
15 newfilename = data[1]
16 break
17 f.close()
18 if filename != newfilename:
19 print " Renaming %s to %s/%s" %(filename, os.path.dirname(filename), newfi
os.rename(filename, "%s/%s" %(os.path.dirname(filename), newfilename))
20 else:
21 print " Couldn't find name in %s" %filename
22
23
This code will search for a filename in header, if it finds, it will rename the
input filename to whatever is in the header. If not, it will just keep the
same name. So in the channeldecoder.py I can just do this to have
everything processed:
1
if (packet["sequenceflag_int"] == 2 or packet["sequenceflag_int"] == 3):
2 if isCompressed:
3 if USEDECOMPRESSOR and startnum != -1:
4 decompressed = Decompressor("channels/%s/%s_%s_" % (channelid, packet["apid"
5 packetmanager.manageFile(decompressed)
6 else:
print "File is not compressed. Checking headers."
7 packetmanager.manageFile(filename)
8
After that, you should have all files with the correct naming (if they have in
the header) and decompressed! The filenames are usually
like gos13chnIR04rgnNHseg001res04dat308034918927.lrit.
Viewing the files content
I still need to do some program to parse, but at least for now there is
the xrit2pic that can parse some of the GOES LRIT (and other satellites
LRIT) files. If you want to make your own parser, most of the files are
easy to process. The Text files are just raw text data (so just skip the
headers), the images are in raw format (check the headers to see how
they’re composed). Some aditional details about the headers that I
mapped are here: https://github.com/racerxdl/open-satellite-
project/blob/master/GOES/standalone/packetmanager.py#L172-L260
In a future article I will make a User Guide to my LRIT Decoder. For now I
want to make it better and with a more user friendly interface, so this
articles are intended to someone who wants to understand how the
protocol works. These are some data I got from GOES 13:
Text Messages:
————————————–
LRIT Admin Message #011
Start:14-April-2010
End:20-December-2018
Distribution: East and West
Subject: LRIT contact information
————————————–
The LRIT Systems team, in an effort to be more responsive
to the user community, would like for users to have
contact information. In the event that a user notices any
long term trends or anomalies in the LRIT data stream, or
has suggestions or comments. We ask that contact be made
via email to LRIT@noaa.gov.
Ending
This is the last chapter of my GOES Satellite Hunt. For sure there will be
more things that I will right about it but at least with this Article you should
be able to create your own Demodulator / Decoder for LRIT signals. I still
have some stuff todo, but I will post my progress here in this blog.