initial commit
This commit is contained in:
commit
368f8b23fc
|
@ -0,0 +1,8 @@
|
|||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
nexrad-browser/bin/
|
||||
nexrad-browser/pkg/
|
||||
nexrad-browser/wasm-pack.log
|
||||
nexrad-browser/www/wasm
|
||||
nexrad-browser/www/
|
|
@ -0,0 +1,8 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettingsMigration">
|
||||
<option name="stateVersion" value="1" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/nexrad-browser/www" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,12 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"nexrad2",
|
||||
"rtwx",
|
||||
"nexrad-browser",
|
||||
"nxar2"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release.package.nexrad-browser]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
opt-level = "s"
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,32 @@
|
|||
[package]
|
||||
name = "nexrad-browser"
|
||||
version = "0.1.0"
|
||||
authors = ["core <core@coredoes.dev>"]
|
||||
edition = "2021"
|
||||
description = "A web-based browser for NEXRAD Archive II datafiles"
|
||||
repository = "https://git.e3t.cc/core/rtwx"
|
||||
license = "GPL-3"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.84"
|
||||
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||
# code size when deploying.
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
nexrad2 = { version = "0.1.0", path = "../nexrad2" }
|
||||
log = "0.4"
|
||||
web-sys = "0.3"
|
||||
js-sys = "0.3"
|
||||
wasm-logger = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.34"
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
pub mod utils;
|
||||
|
||||
use std::io::Cursor;
|
||||
use js_sys::Uint8Array;
|
||||
use log::info;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
fn alert(s: &str);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn __nxrd_browser_init() {
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
utils::set_panic_hook();
|
||||
info!("nexrad-browser initialized successfully");
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn load_ar2(buf: &JsValue) {
|
||||
|
||||
info!("loading archive 2 file");
|
||||
info!("load 01: convert to uint8array");
|
||||
let array = Uint8Array::new(buf);
|
||||
info!("load 02: convert to rust vec");
|
||||
let rvec = array.to_vec();
|
||||
info!("load 03: create cursor");
|
||||
let mut cursor = Cursor::new(rvec);
|
||||
info!("load 04: load");
|
||||
let loaded = nexrad2::parse_nx2_chunk(&mut cursor).unwrap();
|
||||
info!("load 05: dump");
|
||||
info!("Loaded: {:#?}", loaded);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
pub fn set_panic_hook() {
|
||||
// When the `console_error_panic_hook` feature is enabled, we can call the
|
||||
// `set_panic_hook` function at least once during initialization, and then
|
||||
// we will get better error messages if our code ever panics.
|
||||
//
|
||||
// For more details see
|
||||
// https://github.com/rustwasm/console_error_panic_hook#readme
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "nexrad2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
#bzip2-rs = "0.1"
|
||||
bzip2 = "0.4"
|
||||
log = "0.4"
|
|
@ -0,0 +1,183 @@
|
|||
//! # nexrad2
|
||||
//! A parser for the NEXRAD II raw datafile format.
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
use std::str::Utf8Error;
|
||||
use bzip2::read::BzDecoder;
|
||||
//use bzip2_rs::DecoderReader;
|
||||
use log::{debug, trace};
|
||||
|
||||
pub mod message;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Nexrad2Chunk {
|
||||
pub volume_header_record: VolumeHeaderRecord,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// The Volume Header Record as defined by 2620010H (Build 19.0) section 7.3.3 Volume Header Record
|
||||
pub struct VolumeHeaderRecord {
|
||||
/// A character constant of which the last 2 characters identify the version.
|
||||
/// Versions defined by 262010H Build 19.0:
|
||||
/// - `AR2V0002.`: Super Resolution disabled at the RDA (pre RDA Build 12.0)
|
||||
/// - `AR2V0003.`: Super Resolution (pre RDA Build 12.0)
|
||||
/// - `AR2V0004.`: Recombined Super Resolution
|
||||
/// - `AR2V0005.`: Super Resolution disabled at the RDA (RDA Build 12.0 and later)
|
||||
/// - `AR2V0006.`: Super Resolution (RDA Build 12.0 and later)
|
||||
/// - `AR2V0007.`: Recombined Super Resolution (RDA Build 12.0 and later)
|
||||
pub tape_filename: String,
|
||||
/// Increases by 1 for each volume of radar data in the queue to a maximum of 999, whereupon it rolls back to 001
|
||||
pub extension_number: String,
|
||||
/// Date - NEXRAD-modified Julian - days since 1/1/1970 where 1/1/1970 is 1.
|
||||
pub date: u32,
|
||||
/// Time - milliseconds past midnight
|
||||
pub time: u32,
|
||||
/// ICAO of the radar
|
||||
pub icao: String
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NexradParseError {
|
||||
InvalidHeaderTapeFilename(Utf8Error),
|
||||
HeaderReadError(io::Error),
|
||||
InvalidHeaderLength(usize),
|
||||
InvalidHeaderExtensionNumber(Utf8Error),
|
||||
InvalidICAO(Utf8Error),
|
||||
FileTooShort,
|
||||
DecompressError(io::Error),
|
||||
InvalidMetaChunkLength(usize),
|
||||
TcmHeaderReadFailed(io::Error),
|
||||
TcmChunkReadFailed(io::Error),
|
||||
FailedToReadFile(io::Error),
|
||||
LdmReadFailed(io::Error)
|
||||
}
|
||||
impl Display for NexradParseError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidHeaderTapeFilename(e) => write!(f, "invalid header tape filename: {}", e),
|
||||
Self::HeaderReadError(e) => write!(f, "error reading header from cursor: {}", e),
|
||||
Self::InvalidHeaderLength(read) => write!(f, "invalid header length, expected 24 bytes but only read {}", read),
|
||||
Self::InvalidHeaderExtensionNumber(e) => write!(f, "invalid header extension number: {}", e),
|
||||
Self::InvalidICAO(e) => write!(f, "invalid station icao: {}", e),
|
||||
Self::FileTooShort => write!(f, "file too short (failed to fill whole buffer)"),
|
||||
Self::DecompressError(e) => write!(f, "decompression error: {}", e),
|
||||
Self::InvalidMetaChunkLength(real) => write!(f, "invalid meta chunk length, spec defined as {NEXRAD2_META_CHUNK_FIXED_LENGTH} but got {real}"),
|
||||
Self::TcmHeaderReadFailed(e) => write!(f, "tcm message header read failed: {}", e),
|
||||
Self::TcmChunkReadFailed(e) => write!(f, "tcm chunk read failed: {}", e),
|
||||
Self::FailedToReadFile(e) => write!(f, "failed to read file: {}", e),
|
||||
Self::LdmReadFailed(e) => write!(f, "ldm read failed: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Error for NexradParseError {}
|
||||
|
||||
pub const NEXRAD2_META_CHUNK_FIXED_LENGTH: usize = 325888;
|
||||
|
||||
pub const MESSAGE_HEADER_SIZE: usize = 2 + 1 + 1 + 2 + 2 + 4 + 2 + 2;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MessageHeader {
|
||||
pub message_size: u16,
|
||||
pub rda_redundant_channel: u8,
|
||||
pub message_type: u8,
|
||||
pub message_sequence_number: u16,
|
||||
pub julian_date: u16,
|
||||
pub millis_after_midnight: u32,
|
||||
pub num_of_message_segments: u16,
|
||||
pub message_segment_num: u16
|
||||
|
||||
}
|
||||
|
||||
pub enum Message {
|
||||
|
||||
}
|
||||
|
||||
pub const LEGACY_TCM_HEADER_LENGTH: i64 = 12;
|
||||
|
||||
pub fn parse_nx2_chunk(cursor: &mut (impl Read + Seek)) -> Result<Nexrad2Chunk, NexradParseError> {
|
||||
let mut volume_header = [0u8; 24];
|
||||
let read = cursor.read(&mut volume_header).map_err(|e| NexradParseError::HeaderReadError(e))?;
|
||||
if read != 24 { return Err(NexradParseError::InvalidHeaderLength(read)); }
|
||||
|
||||
let tape_filename_bytes = &volume_header[0..9];
|
||||
let tape_filename = std::str::from_utf8(tape_filename_bytes).map_err(|e| NexradParseError::InvalidHeaderTapeFilename(e))?;
|
||||
|
||||
let extension_number_bytes = &volume_header[9..12];
|
||||
let extension_number = std::str::from_utf8(extension_number_bytes).map_err(|e| NexradParseError::InvalidHeaderTapeFilename(e))?;
|
||||
|
||||
let days_since_epoch = u32::from_be_bytes(volume_header[12..16].try_into().unwrap());
|
||||
let millis_after_midnight = u32::from_be_bytes(volume_header[16..20].try_into().unwrap());
|
||||
|
||||
let icao_bytes = &volume_header[20..24];
|
||||
let icao = std::str::from_utf8(icao_bytes).map_err(|e| NexradParseError::InvalidHeaderTapeFilename(e))?;
|
||||
|
||||
let header = VolumeHeaderRecord {
|
||||
tape_filename: tape_filename.to_string(),
|
||||
extension_number: extension_number.to_string(),
|
||||
date: days_since_epoch,
|
||||
time: millis_after_midnight,
|
||||
icao: icao.to_string(),
|
||||
};
|
||||
|
||||
trace!("Loaded - {:#?}", header);
|
||||
|
||||
loop {
|
||||
// LDM records
|
||||
let mut ldm_size_bytes = [0u8; 4];
|
||||
cursor.read_exact(&mut ldm_size_bytes).map_err(|e| NexradParseError::LdmReadFailed(e))?;
|
||||
let ldm_size = i32::from_be_bytes(ldm_size_bytes).abs() as usize;
|
||||
|
||||
trace!("Reading LDM record - {} bytes compressed", ldm_size);
|
||||
|
||||
if ldm_size == 0 {
|
||||
trace!("Missing LDM record, unseeking size");
|
||||
cursor.seek(SeekFrom::Current(-4)).map_err(|e| NexradParseError::LdmReadFailed(e))?;
|
||||
}
|
||||
|
||||
let mut compressed_buf = vec![0u8; ldm_size];
|
||||
cursor.read_exact(&mut compressed_buf).map_err(|e| NexradParseError::LdmReadFailed(e))?;
|
||||
|
||||
let mut bz_decoder = BzDecoder::new(Cursor::new(compressed_buf));
|
||||
|
||||
let mut decompressed_buf = vec![];
|
||||
|
||||
io::copy(&mut bz_decoder, &mut decompressed_buf).map_err(|e| NexradParseError::DecompressError(e))?;
|
||||
|
||||
trace!("LDM record decompressed to {} bytes", decompressed_buf.len());
|
||||
|
||||
let mut decompressed = Cursor::new(decompressed_buf);
|
||||
|
||||
loop {
|
||||
decompressed.seek(SeekFrom::Current(LEGACY_TCM_HEADER_LENGTH)).map_err(|e| NexradParseError::TcmChunkReadFailed(e))?;
|
||||
|
||||
let mut message_header = [0u8; MESSAGE_HEADER_SIZE];
|
||||
decompressed.read_exact(&mut message_header).map_err(|e| NexradParseError::TcmChunkReadFailed(e))?;
|
||||
|
||||
let message_header = MessageHeader {
|
||||
message_size: u16::from_be_bytes(message_header[0..2].try_into().unwrap()),
|
||||
rda_redundant_channel: message_header[2],
|
||||
message_type: message_header[3],
|
||||
message_sequence_number: u16::from_be_bytes(message_header[4..6].try_into().unwrap()),
|
||||
julian_date: u16::from_be_bytes(message_header[6..8].try_into().unwrap()),
|
||||
millis_after_midnight: u32::from_be_bytes(message_header[8..12].try_into().unwrap()),
|
||||
num_of_message_segments: u16::from_be_bytes(message_header[12..14].try_into().unwrap()),
|
||||
message_segment_num: u16::from_be_bytes(message_header[14..].try_into().unwrap()),
|
||||
};
|
||||
|
||||
debug!("Message: {:#?}", message_header);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
Ok(Nexrad2Chunk {
|
||||
volume_header_record: header
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
pub enum Message {
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "nxar2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
simple_logger = "4"
|
||||
nexrad2 = { version = "0.1", path = "../nexrad2" }
|
|
@ -0,0 +1,12 @@
|
|||
use std::fs;
|
||||
use std::fs::File;
|
||||
use nexrad2::parse_nx2_chunk;
|
||||
|
||||
fn main() {
|
||||
simple_logger::init().unwrap();
|
||||
let test = std::env::args().nth(1).unwrap();
|
||||
|
||||
let mut data = File::open(test).unwrap();
|
||||
|
||||
parse_nx2_chunk(&mut data).unwrap();
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "nexrad"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
simple_logger = "4.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
aws-sdk-sqs = "0.34"
|
||||
aws-config = "0.56"
|
||||
prometheus = "0.13"
|
||||
lazy_static = "1.4"
|
||||
serde_json = "1"
|
|
@ -0,0 +1 @@
|
|||
pub const ARCHIVE_SQS_URL: &str = "https://sqs.us-east-1.amazonaws.com/232232181562/NEXRAD_m1_q2";
|
|
@ -0,0 +1,29 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const FIREHOSE_SQS_URL: &str = "https://sqs.us-east-1.amazonaws.com/232232181562/NEXRAD_m1_q1";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct S3NEXRADData {
|
||||
#[serde(rename = "Key")]
|
||||
pub key: String,
|
||||
#[serde(rename = "SiteID")]
|
||||
pub site_id: String,
|
||||
#[serde(rename = "DateTime")]
|
||||
pub date_time: String,
|
||||
#[serde(rename = "VolumeID")]
|
||||
pub volume_id: i32,
|
||||
#[serde(rename = "ChunkID")]
|
||||
pub chunk_id: i32,
|
||||
#[serde(rename = "ChunkType")]
|
||||
pub chunk_type: String,
|
||||
#[serde(rename = "L2Version")]
|
||||
pub l2version: String,
|
||||
#[serde(rename = "S3Bucket")]
|
||||
pub s3bucket: String
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct S3Notification {
|
||||
#[serde(rename = "Message")]
|
||||
pub message: String,
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
use std::error::Error;
|
||||
use std::time::SystemTime;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{debug, error, info, warn};
|
||||
use prometheus::{IntGauge, IntCounter, register_int_counter, register_int_gauge};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
lazy_static! {
|
||||
static ref NEXRAD_LAST_CHUNK_TIME: IntGauge = register_int_gauge!("rtwx_nexrad_last_chunk_time", "Unix timestamp of when the last chunk was received from NEXRAD").unwrap();
|
||||
static ref NEXRAD_CHUNKS_RECEIVED: IntCounter = register_int_counter!("rtwx_nexrad_chunks_total", "Total number of chunks received from NEXRAD").unwrap();
|
||||
}
|
||||
|
||||
pub mod firehose;
|
||||
pub mod archive;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum NexradMode {
|
||||
Firehose,
|
||||
Archive
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
simple_logger::init_with_env()?;
|
||||
|
||||
info!("core's realtime weather API {} starting up", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
info!("initializing primary radar (NOAA NEXRAD) via AWS API");
|
||||
|
||||
let nexrad_mode = std::env::var("RTWX_NEXRAD_MODE").expect("env var RTWX_NEXRAD_MODE is required and must be set to 'archive' (recommended) or 'firehose' (expensive)");
|
||||
let nexrad_mode = match nexrad_mode.as_str() {
|
||||
"firehose" => NexradMode::Firehose,
|
||||
"archive" => NexradMode::Archive,
|
||||
_ => {
|
||||
error!("env var RTWX_NEXRAD_MODE must be set to 'archive' or 'firehose'");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
if nexrad_mode == NexradMode::Firehose {
|
||||
warn!("warning: firehose intake mode is *very* expensive ($2-5/day!) to operate");
|
||||
warn!("warning: please be careful!");
|
||||
}
|
||||
|
||||
let config = aws_config::load_from_env().await;
|
||||
let sqs_client = aws_sdk_sqs::Client::new(&config);
|
||||
|
||||
info!("NEXRAD in alignment. May take up to 10 minutes for data to be available worldwide");
|
||||
|
||||
let mut total_frames = 0;
|
||||
let start_time = SystemTime::now();
|
||||
|
||||
let queue_url = match nexrad_mode {
|
||||
NexradMode::Firehose => firehose::FIREHOSE_SQS_URL,
|
||||
NexradMode::Archive => archive::ARCHIVE_SQS_URL
|
||||
};
|
||||
|
||||
loop {
|
||||
let rcv_message_output = sqs_client.receive_message().max_number_of_messages(10).queue_url(queue_url).send().await?;
|
||||
for message in rcv_message_output.messages.unwrap_or_default() {
|
||||
// decode frame
|
||||
if nexrad_mode == NexradMode::Firehose {
|
||||
let notification: firehose::S3Notification = serde_json::from_str(&message.body.unwrap())?;
|
||||
let frame: firehose::S3NEXRADData = serde_json::from_str(¬ification.message)?;
|
||||
total_frames += 1;
|
||||
let total_seconds: f64 = (SystemTime::now().duration_since(start_time).unwrap().as_millis() as f64) / 1000f64;
|
||||
info!("New NEXRAD frame: {} {} vol {} chunk {} type {} version {} @ {}/{} rate {} frames/second", frame.site_id, frame.date_time, frame.volume_id, frame.chunk_id, frame.chunk_type, frame.l2version, frame.s3bucket, frame.key, (total_frames as f64) / total_seconds);
|
||||
} else {
|
||||
info!("Archive mode not available yet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
info!("lightning data not yet available (Blitzortung)");
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue