STEREO-A is a NASA solar observation spacecraft that was launched in 2006 to a heliocentric orbit with a radius of about 1 au. One of the instruments, called SECCHI, takes images of the Sun using different telescopes and coronagraphs to study coronal mass ejections. Near real-time images of this instrument can be seen in this webpage. These images are certainly the most eye-catching data transmitted by STEREO-A in its X-band space weather beacon.
In September 2022, Wei Mingchuan BG2BHC made some recordings of the space weather beacon using a 13 meter antenna in Harbin Institute of Technology. I wrote a blog post showing a GNU Radio decoder and a Jupyter notebook analysing the decoded frames. I managed to figure out that some of the data corresponded to the S/WAVES instrument, which is an electric field sensor with a spectrometer covering 125 kHz to 16 MHz.
During this summer, STEREO-A is very close to Earth for the first time since it was launched. This has enabled amateur observers with small stations to receive and decode the space weather beacon. As part of these activities, Alan Antoine F4LAU and Scott Tilley VE7TIL have figured out how to decode the SECCHI images. Scott has published a summary of this in his blog, and Alan has added a decoding pipeline to SatDump. Using this information, I have now extended my Jupyter notebook to decode the SECCHI images. Eventually I want to turn this into a GNU Radio demo, and the Jupyter notebook serves as reference code.
The main documentation explaining how the SECCHI images are transmitted in the space weather beacon is the STEREO SECCHI/L0 to L0.5 Calibration and Measurement Algorithm Document (this seems to date from 2021, which is relatively recently, given the age of the mission). This document describes pretty much all that is needed to obtain the compressed image files from the telemetry frames, though some comments can serve as a complement and make this document easier to read.
SECCHI images are transmitted in Space Packets in the following APIDs, as Scott has figured out:
- APID 1136. COR1 coronagraph (though it appears to be unused)
- APID 1137. COR2 coronagraph.
- APID 1138. HI1 heliospheric imager.
- APID 1139. HI2 heliospheric imager.
- APID 1140. EUVI extreme UV imager.
In addition to this, the other instruments use APID 624 (IMPACT), APID 880 (PLASTIC), and APID 1393 (S/WAVES), while spacecraft telemetry goes in APID 0.
Each image is divided in three blocks: a header, which contains some metadata from which a FITS header can be produced; a block with the compressed image data; and a trailer, which is a copy of the header. The metadata is not essential to obtain and display bitmap images, so I haven’t taken a look at the headers.
These blocks are carried in Space Packets in the corresponding APIDs, and fragmented and aggregated using a system based on a first header pointer, very much like how TM frames carry Space Packets. The structure of the SECCHI Space Packets is shown below. The Primary Header is the Space Packet Primary Header. The first header pointer (first block pointer) field points to the first byte of the first block header (field of 0’s and image count) occurring in the packet payload, if any.
The block header format seems to support transmitting images in multiple blocks, since it has a 14-bit block number. However, it seems that this isn’t done. The header is transmitted in block number 0 (with block type 1), then all the data is transmitted in block number 1 (with block type 0), and finally the trailer is transmitted in block number 2 (with block type 2).
The documentation describes different compression algorithms for the image data, but it seems that the one that is used normally is ICER. This is a wavelet-based image compression algorithm with some slight similarities to JPEG-2000. As far as I know, an open source implementation of this algorithm is not available, and some people say that there are patents surrounding the algorithm, though perhaps they have already expired. There is this relatively new work-in-progress open source implementation of ICER. I haven’t tried to use it, since the documentation says that it only supports 5-6 bits per pixel, while SECCHI uses 13-14 (and besides this, I’m not sure if this implementation is intended to be interoperable with other ICER implementations).
One of the key findings of Alan was that there is a closed-source ICER implementation in the STEREO website. This is available as an i386 Linux binary only. The assembly code of the decompressor looks relatively simple. Not in the sense that reading it makes obvious how the algorithm works, but in the sense that decompiling it to compile it for other platforms is not a too far fetched idea. These are the only library functions that the binary calls:
$ objdump -T idecomp.linux idecomp.linux: file format elf32-i386 DYNAMIC SYMBOL TABLE: 0804855c DF *UND* 0000006d (GLIBC_2.0) feof 0804856c DF *UND* 00000023 (GLIBC_2.0) fprintf 0804857c DF *UND* 000001ab (GLIBC_2.0) malloc 0804858c DF *UND* 0000003f (GLIBC_2.0) scanf 0804859c DF *UND* 0000010b (GLIBC_2.0) fread 08051160 g DO .bss 00000004 (GLIBC_2.0) stderr 080485ac DF *UND* 000000f8 (GLIBC_2.0) fseek 080485bc DF *UND* 00000046 (GLIBC_2.0) getopt 080485cc DF *UND* 000000f3 (GLIBC_2.0) __libc_start_main 080485dc DF *UND* 00000039 (GLIBC_2.0) printf 080485ec DF *UND* 00000180 (GLIBC_2.1) fclose 080485fc DF *UND* 000000cc (GLIBC_2.0) exit 0804860c DF *UND* 000002e0 (GLIBC_2.0) calloc 0804861c DF *UND* 00000034 (GLIBC_2.0) sscanf 0804862c DF *UND* 000000b1 (GLIBC_2.0) free 0804863c DF *UND* 00000036 (GLIBC_2.1) fopen 08051164 g DO .bss 00000004 (GLIBC_2.0) optarg 0804c5c4 g DO .rodata 00000004 Base _IO_stdin_used 0804864c DF *UND* 00000165 (GLIBC_2.0) fwrite 00000000 w D *UND* 00000000 _Jv_RegisterClasses 00000000 w D *UND* 00000000 __gmon_start__ 0804865c DF *UND* 00000030 (GLIBC_2.0) strcpy
In any case, using this ICER decompressor binary gives a way to obtain the SECCHI images from the STEREO-A telemetry.
The output of the ICER decompressor is a 16-bit grayscale image of square size (the resolution depends on the sensor). The images need to be rotated 90 degrees, and perhaps flipped (depending on the sensor) to obtain the usual north up orientation (though I haven’t seen this documented anywhere).
I have updated the Jupyter notebook to extract the image blocks from the SECCHI APIDs, call the ICER decompressor, and plot the bitmap data. Since the recordings that Wei made are not very long, and the space weather beacon data rate is rather low, there are only a few images.
APID 1137 (COR2) contains the following blocks:
Container(padding=None, image_count=806, block_type=2, block_number=2, block_length=215), Container(padding=None, image_count=2047, block_type=0, block_number=0, block_length=111), Container(padding=None, image_count=879, block_type=1, block_number=0, block_length=215), Container(padding=None, image_count=879, block_type=0, block_number=1, block_length=12139), Container(padding=None, image_count=879, block_type=2, block_number=2, block_length=215), Container(padding=None, image_count=2047, block_type=0, block_number=0, block_length=45)
This gives the following image.
APID 1140 (EUVI) contains the following blocks (the data block for imagee 810 was lost because some Space Packets were lost):
Container(padding=None, image_count=810, block_type=1, block_number=0, block_length=363), Container(padding=None, image_count=810, block_type=2, block_number=2, block_length=363), Container(padding=None, image_count=2047, block_type=0, block_number=0, block_length=167), Container(padding=None, image_count=858, block_type=1, block_number=0, block_length=363), Container(padding=None, image_count=858, block_type=0, block_number=1, block_length=10443), Container(padding=None, image_count=858, block_type=2, block_number=2, block_length=363), Container(padding=None, image_count=2047, block_type=0, block_number=0, block_length=155), Container(padding=None, image_count=901, block_type=1, block_number=0, block_length=363), Container(padding=None, image_count=901, block_type=0, block_number=1, block_length=10383), Container(padding=None, image_count=901, block_type=2, block_number=2, block_length=363), Container(padding=None, image_count=2047, block_type=0, block_number=0, block_length=215)
These give the following two images: