I have set up a permanent antenna and processing computer to receive imagery from the GOES-16 satellite that is in geostationary orbit at 75.2° West, 22,200 miles away. This satellite continuously streams data to Earth at the 1694.100MHz frequency. It sends a full disk image in each channel every thirty minutes, and each image is 5424 pixels square. It also sends what are called “mesoscales,” which are native resolution images of 2000 pixels square of regions of the eastern US that are of interest at the current time. These mesoscale images are sent about every seven minutes. For each image type, I display the most current image as well as a timelapse video of the last 24 hours of images. The boundaries on the images are rendered locally.
Hardware
The antenna is the Nooelec GOES Weather Satellite Mesh Antenna, combined with the Nooelec SAWbird+ GOES LNA and the NESDR SMArTee XTR Software Defined Radio. The signal is collected and processed by a cheap refurbished desktop mini-PC running FreeBSD 13.
Software
The software is run on a FreeBSD 13 box using goestools
. goestools
compiles and runs fine on FreeBSD with a few caveats:
You need to install the rtl-sdr
package,
pkg install rtl-sdr
You need to compile and install airspyone_host
, https://github.com/airspy/airspyone_host. This compiles and installs easily.
git clone https://github.com/airspy/airspyone_host.git host
cd host
mkdir build
cd build
cmake .. -DLIBUSB_LIBRARIES=/usr/lib/libusb.so
make
su
make install
Clone the goestools
repository,
git clone --recursive https://github.com/pietern/goestools
As of this writing, goestools
as it is in the repository requires a compatibility layer that was removed in Proj 8. There is a patch available to fix this,
diff --git a/src/goesproc/CMakeLists.txt b/src/goesproc/CMakeLists.txt
index 8778c54..e013267 100644
--- a/src/goesproc/CMakeLists.txt
+++ b/src/goesproc/CMakeLists.txt
@@ -30,7 +30,11 @@ endif()
pkg_check_modules(PROJ proj)
if(PROJ_FOUND)
- list(APPEND GOESPROC_SRCS proj.cc map_drawer.cc)
+ if(${PROJ_VERSION} VERSION_GREATER_EQUAL 8.0)
+ list(APPEND GOESPROC_SRCS proj_v8.cc map_drawer.cc)
+ else()
+ list(APPEND GOESPROC_SRCS proj.cc map_drawer.cc)
+ endif()
endif()
add_executable(goesproc ${GOESPROC_SRCS})
diff --git a/src/goesproc/map_drawer.cc b/src/goesproc/map_drawer.cc
index 2ab1bcb..30890fd 100644
--- a/src/goesproc/map_drawer.cc
+++ b/src/goesproc/map_drawer.cc
@@ -36,8 +36,8 @@ void MapDrawer::generatePoints(
double lat, lon;
double x, y;
for (const auto& coord : coords) {
- lon = coord.at(0).get<double>() * DEG_TO_RAD;
- lat = coord.at(1).get<double>() * DEG_TO_RAD;
+ lon = coord.at(0).get<double>() * PROJ_COORD_FACTOR;
+ lat = coord.at(1).get<double>() * PROJ_COORD_FACTOR;
std::tie(x, y) = proj_.fwd(lon, lat);
// If out of range, ignore
diff --git a/src/goesproc/proj.h b/src/goesproc/proj.h
index 9f76ef9..20f77ce 100644
--- a/src/goesproc/proj.h
+++ b/src/goesproc/proj.h
@@ -1,11 +1,14 @@
#pragma once
-#if PROJ_VERSION_MAJOR < 4
-#error "proj version >= 4 required"
-#else
-// Assume proj continues to ship with a backwards compatibility layer.
+#if PROJ_VERSION_MAJOR >= 8
+#define PROJ_COORD_FACTOR .017453292519943296
+#elif PROJ_VERSION_MAJOR > 4
+// proj shipped with a backwards compatibility layer.
// See for a migration guide https://proj.org/development/migration.html.
+#define PROJ_COORD_FACTOR DEG_TO_RAD
#define ACCEPT_USE_OF_DEPRECATED_PROJ_API_H 1
+#else
+#error "proj version >= 4 required"
#endif
#include <map>
@@ -13,11 +16,15 @@
#include <tuple>
#include <vector>
-#include <proj_api.h>
+#include <proj.h>
class Proj {
public:
+#if PROJ_VERSION_MAJOR >= 8
+ explicit Proj(const std::string args);
+#else
explicit Proj(const std::vector<std::string>& args);
+#endif
explicit Proj(const std::map<std::string, std::string>& args);
@@ -28,5 +35,9 @@ public:
std::tuple<double, double> inv(double x, double y);
protected:
+#if PROJ_VERSION_MAJOR >= 8
+ PJ* proj_;
+#else
projPJ proj_;
+#endif
};
diff --git a/src/goesproc/proj_v8.cc b/src/goesproc/proj_v8.cc
new file mode 100644
index 0000000..cf9d2ea
--- /dev/null
+++ b/src/goesproc/proj_v8.cc
@@ -0,0 +1,51 @@
+#include "proj.h"
+
+#include <cstring>
+#include <sstream>
+
+namespace {
+
+std::string toProjStr(
+ const std::map<std::string, std::string>& args) {
+ std::stringstream ss;
+ for (const auto& arg : args) {
+ ss << " +" << arg.first << "=" << arg.second;
+ }
+ return ss.str();
+}
+
+std::string pj_error(PJ* P, std::string prefix = "proj: ") {
+ std::stringstream ss;
+ ss << prefix << proj_errno(P);
+ return ss.str();
+}
+
+} // namespace
+
+Proj::Proj(const std::string args) {
+ proj_ = proj_create(0, args.c_str());
+ if (!proj_) {
+ throw std::runtime_error(pj_error(0, "proj initialization error: "));
+ }
+}
+
+Proj::Proj(const std::map<std::string, std::string>& args)
+ : Proj(toProjStr(args)) {
+}
+
+Proj::~Proj() {
+ proj_destroy(proj_);
+}
+
+std::tuple<double, double> Proj::fwd(double lon, double lat) {
+ PJ_COORD in = {{ lon, lat, 0, 0 }};
+ PJ_COORD out = proj_trans(proj_, PJ_DIRECTION::PJ_FWD, in);
+ return std::make_tuple<double, double>(std::move(out.uv.u), std::move(out.uv.v));
+}
+
+std::tuple<double, double> Proj::inv(double x, double y) {
+ PJ_COORD in = {{ x, y, 0, 0 }};
+ PJ_COORD out = proj_trans(proj_, PJ_DIRECTION::PJ_INV, in);
+ return std::make_tuple<double, double>(std::move(out.uv.u), std::move(out.uv.v));
+}
+
Apply this patch in the goestools
source directory,
patch < ~/proj8.patch
Add the following near the top of CMakeLists.txt
to make sure it finds the dependency headers and libraries,
include_directories(/usr/local/include)
link_directories(/usr/local/lib)
Finally, build and install,
mkdir -p build
cd build
cmake ../ -DCMAKE_INSTALL_PREFIX=/usr/local
su
make install
Configuration
goestools
is configured with text files. For goesrecv
, the program that reads from the SDR and demodulates and decodes the data, the following config should work for GOES-16,
## By default, goesrecv will use the lowest sample rate available.
## This is 2.5 MSPS for the R2 and 3.0 MSPS for the Mini.
## Because different Airspy models support different sample rates,
## it is recommended to leave the "sample_rate" field commented,
## so that it works for either model.
##
# sample_rate = 3000000
# gain = 18
# bias_tee = false
[rtlsdr]
frequency = 1694100000
sample_rate = 2400000
gain = 30
bias_tee = false
device_index = 0
# [nanomsg]
# sample_rate = 2400000
# connect = "tcp://1.2.3.4:5005"
# receive_buffer = 2097152
[costas]
max_deviation = 200e3
[clock_recovery.sample_publisher]
bind = "tcp://0.0.0.0:5002"
send_buffer = 2097152
[quantization.soft_bit_publisher]
bind = "tcp://0.0.0.0:5001"
send_buffer = 1048576
[decoder.packet_publisher]
bind = "tcp://0.0.0.0:5004"
send_buffer = 1048576
# The demodulator stats publisher sends a JSON object that describes
# the state of the demodulator (gain, frequency correction, samples
# per symbol), for every block of samples.
[demodulator.stats_publisher]
bind = "tcp://0.0.0.0:6001"
# The decoder stats publisher sends a JSON object for every packet it
# decodes (Viterbi corrections, Reed-Solomon corrections, etc.).
[decoder.stats_publisher]
bind = "tcp://0.0.0.0:6002"
# The monitor can log aggregated stats (counters, gauges, and
# histograms) to a statsd daemon. Because this uses UDP, you can keep
# this enabled even if you haven't setup a statsd daemon yet.
[monitor]
statsd_address = "udp4://localhost:8125"
Run the program as follows,
goesrecv -v -c ./goesrecv.conf &
and it will start decoding at streaming the data using Nanomsg over tcp://localhost:5004
. You can then run the goesproc
command either locally or elsewhere and have it process the packets,
goesproc -m packet --subscribe tcp://localhost:5004 -c ./goesproc.conf
A full config can be found along with the source code in goestools/etc
, but here is what I am using,
# GOES-16 mesoscale region 1 imagery is stored at ./goes16/m1/YYYY-MM-DD
# The pattern specified in {time:XXX} is extrapolated using strftime(3).
# It can be used more than once if needed.
[[handler]]
type = "image"
origin = "goes16"
region = "m1"
dir = "./goes16/m1/{time:%Y-%m-%d}"
# GOES-16 full disk originals.
[[handler]]
type = "image"
origin = "goes16"
region = "fd"
dir = "./goes16/fd/{time:%Y-%m-%d}"
# GOES-16 full disk, channel 2, with contrast curve applied.
# The section [handler.remap] below applies to this handler.
[[handler]]
type = "image"
origin = "goes16"
region = "fd"
channels = [ "ch02" ]
directory = "./goes16/fd/{time:%Y-%m-%d}"
filename = "{filename}_contrast"
Here is a FreeBSD startup script for goesrecv
,
#!/bin/sh
# PROVIDE: goesrecv
# REQUIRE: NETWORK
#
# Add the following line in /etc/rc.conf to enable motion at startup
#
# goesrecv_enable="YES"
. /etc/rc.subr
name=goesrecv
rcvar=goesrecv_enable
goesrecv_user="goes"
goesrecv_group="goes"
goesrecv_chdir="/usr/home/goes/goes"
load_rc_config $name
: ${goesrecv_enable:=NO}
command=/usr/local/bin/${name}
command_args="-v -c ./goesrecv.conf > /dev/null &"
run_rc_command "$1"
And here is a startup script for goesproc
,
#!/bin/sh
# PROVIDE: goesproc
# REQUIRE: NETWORK
#
# Add the following line in /etc/rc.conf to enable motion at startup
#
# goesproc_enable="YES"
. /etc/rc.subr
name=goesproc
rcvar=goesproc_enable
goesproc_user="goes"
goesproc_group="goes"
goesproc_chdir="/usr/home/goes/goes/"
load_rc_config $name
: ${goesproc_enable:=NO}
command=/usr/local/bin/${name}
command_args="-m packet --subscribe tcp://localhost:5004 -c /usr/local/share/goestools/goesproc-goesr.conf &"
run_rc_command "$1"