diff --git a/src/libais/ais.h b/src/libais/ais.h index 4619db19..a2e72fcb 100644 --- a/src/libais/ais.h +++ b/src/libais/ais.h @@ -1979,6 +1979,22 @@ class Ais8_367_22 : public Ais8 { }; ostream& operator<< (ostream& o, Ais8_367_22 const& msg); +// +// SSW Satellite Ship Weather 1-Slot Version +class Ais8_367_24 : public Ais8 { + public: + int version; + + int utc_hour; + int utc_min; + + AisPoint position; // 25, 24 bits + int pressure; // hPa + + Ais8_367_24(const char *nmea_payload, const size_t pad); +}; +ostream& operator<< (ostream &o, const Ais8_367_24 &msg); + class Ais9 : public AisMsg { public: int alt; // m above sea level diff --git a/src/libais/ais8.cpp b/src/libais/ais8.cpp index 2e960deb..b28727a2 100644 --- a/src/libais/ais8.cpp +++ b/src/libais/ais8.cpp @@ -626,8 +626,6 @@ Ais8_1_31::Ais8_1_31(const char *nmea_payload, const size_t pad) spare2 = bits.ToUnsignedInt(350, 10); assert(bits.GetRemaining() == 0); - status = AIS_OK; } - } // namespace libais diff --git a/src/libais/ais8_367.cpp b/src/libais/ais8_367.cpp index 647c100e..0e70a18f 100644 --- a/src/libais/ais8_367.cpp +++ b/src/libais/ais8_367.cpp @@ -4,6 +4,9 @@ // // http://www.e-navigation.nl/content/geographic-notice // http://www.e-navigation.nl/sites/default/files/asm_files/GN%20Release%20Version%201b.pdf +// +// 8:367:24 +// https://www.e-navigation.nl/content/satellite-ship-weather-small #include @@ -169,4 +172,37 @@ Ais8_367_22::~Ais8_367_22() { } } +// SSW FI24 Satellite Ship Weather Small - Less than 1-Slot Version +Ais8_367_24::Ais8_367_24(const char *nmea_payload, const size_t pad) + : Ais8(nmea_payload, pad), version(0), + utc_hour(), utc_min(), position(), pressure(0.0) { + assert(dac == 367); + assert(fi == 24); + + if (!CheckStatus()) { + return; + } + + if ((num_bits != 128) && (num_bits != 144)) { + status = AIS_ERR_BAD_BIT_COUNT; + return; + } + + bits.SeekTo(56); + + version = bits.ToUnsignedInt(56, 3); + utc_hour = bits.ToUnsignedInt(59, 5); + utc_min = bits.ToUnsignedInt(64, 6); + position = bits.ToAisPoint(70, 49); + pressure = bits.ToUnsignedInt(119, 9) + 799; // hPa + + // NOTE: If message comes in at 144, then there will be 16 bits remaining. + // assert(bits.GetRemaining() == 0); + status = AIS_OK; +} + +ostream& operator<< (ostream &o, const Ais8_367_24 &msg) { + return o << msg.mmsi << ": " << msg.version << ": " << msg.utc_hour << ": " << msg.utc_min << ": " << msg.pressure; +} + } // namespace libais diff --git a/src/libais/ais8_367_33.cpp b/src/libais/ais8_367_33.cpp new file mode 100644 index 00000000..17e20221 --- /dev/null +++ b/src/libais/ais8_367_33.cpp @@ -0,0 +1,288 @@ +// 8:367:33 Defined by EM Version 3-23MAR15.pdf. +// The USCG Environmental Message + +#include "ais.h" + +namespace libais { + +Ais8_367_33_Location::Ais8_367_33_Location(const AisBitset &bits, + const size_t offset) { + version = bits.ToUnsignedInt(offset, 6); + position = bits.ToAisPoint(offset + 6, 55); + + precision = bits.ToUnsignedInt(offset + 61, 3); + altitude = bits.ToUnsignedInt(offset + 64, 12) / 10.; // meters + owner = bits.ToUnsignedInt(offset + 76, 4); + timeout = bits.ToUnsignedInt(offset + 80, 3); + spare2 = bits.ToUnsignedInt(offset + 83, 2); +} + +Ais8_367_33_Station::Ais8_367_33_Station(const AisBitset &bits, + const size_t offset) { + name = bits.ToString(offset, 84); + spare2 = bits.ToUnsignedInt(offset + 84, 1); +} + +Ais8_367_33_Wind::Ais8_367_33_Wind(const AisBitset &bits, + const size_t offset) { + wind_speed = bits.ToUnsignedInt(offset, 7); + wind_gust = bits.ToUnsignedInt(offset + 7, 7); // knots + wind_dir = bits.ToUnsignedInt(offset + 14, 9); + wind_gust_dir = bits.ToUnsignedInt(offset + 23, 9); + sensor_type = bits.ToUnsignedInt(offset + 32, 3); + wind_forecast = bits.ToUnsignedInt(offset + 35, 7); + wind_gust_forecast = bits.ToUnsignedInt(offset + 42, 7); // knots + wind_dir_forecast = bits.ToUnsignedInt(offset + 49, 9); + utc_day_forecast = bits.ToUnsignedInt(offset + 58, 5); + utc_hour_forecast = bits.ToUnsignedInt(offset + 63, 5); + utc_min_forecast = bits.ToUnsignedInt(offset + 68, 6); + duration = bits.ToUnsignedInt(offset + 74, 8); + spare2 = bits.ToUnsignedInt(offset + 82, 3); +} + +Ais8_367_33_WaterLevel::Ais8_367_33_WaterLevel(const AisBitset &bits, + const size_t offset) { + type = bits[offset]; + level = bits.ToInt(offset + 1, 16); + trend = bits.ToUnsignedInt(offset + 17, 2); + vdatum = bits.ToUnsignedInt(offset + 19, 5); + sensor_type = bits.ToUnsignedInt(offset + 24, 3); + forecast_type = bits[offset + 27]; + level_forecast = bits.ToInt(offset + 28, 16); + utc_day_forecast = bits.ToUnsignedInt(offset + 44, 5); + utc_hour_forecast = bits.ToUnsignedInt(offset + 49, 5); + utc_min_forecast = bits.ToUnsignedInt(offset + 54, 6); + duration = bits.ToUnsignedInt(offset + 60, 8); + spare2 = bits.ToUnsignedInt(offset + 68, 17); +} + +Ais8_367_33_Curr2D::Ais8_367_33_Curr2D(const AisBitset &bits, + const size_t offset) { + for (size_t idx = 0; idx < 3; idx++) { + size_t start = offset + idx * 26; + currents[idx].speed = bits.ToUnsignedInt(start, 8) / 10.; + currents[idx].dir = bits.ToUnsignedInt(start + 8, 9); + currents[idx].depth = bits.ToUnsignedInt(start + 17, 9); + } + type = bits.ToUnsignedInt(offset + 78, 3); + spare2 = bits.ToUnsignedInt(offset + 81, 4); +} + +Ais8_367_33_Curr3D::Ais8_367_33_Curr3D(const AisBitset &bits, + const size_t offset) { + for (size_t idx = 0; idx < 2; idx++) { + size_t start = offset + idx * 36; + currents[idx].north = bits.ToUnsignedInt(start, 9) / 10.; + currents[idx].east = bits.ToUnsignedInt(start + 9, 9) / 10.; + currents[idx].up = bits.ToUnsignedInt(start + 18, 9) / 10.; + currents[idx].depth = bits.ToUnsignedInt(start + 27, 9); + } + type = bits.ToUnsignedInt(offset + 72, 3); + spare2 = bits.ToUnsignedInt(offset + 75, 10); +} + +Ais8_367_33_HorzFlow::Ais8_367_33_HorzFlow(const AisBitset &bits, + const size_t offset) { + bearing = bits.ToUnsignedInt(offset, 9); + for (size_t idx = 0; idx < 2; idx++) { + size_t start = offset + 9 + (idx * 35); + currents[idx].dist = bits.ToUnsignedInt(start, 9); + currents[idx].speed = bits.ToUnsignedInt(start + 9, 8) / 10.; + currents[idx].dir = bits.ToUnsignedInt(start + 17, 9); + currents[idx].level = bits.ToUnsignedInt(start + 26, 9); + } + type = bits.ToUnsignedInt(offset + 79, 3); + spare2 = bits.ToUnsignedInt(offset + 82, 3); +} + +Ais8_367_33_SeaState::Ais8_367_33_SeaState(const AisBitset &bits, + const size_t offset) { + swell_height = bits.ToUnsignedInt(offset, 8) / 10.; + swell_period = bits.ToUnsignedInt(offset + 8, 6); + swell_dir = bits.ToUnsignedInt(offset + 14, 9); + sea_state = bits.ToUnsignedInt(offset + 23, 4); + swell_sensor_type = bits.ToUnsignedInt(offset + 27, 3); + water_temp = bits.ToInt(offset + 30, 10) / 10.; + water_temp_depth = bits.ToUnsignedInt(offset + 40, 7) / 10.; + water_sensor_type = bits.ToUnsignedInt(offset + 47, 3); + wave_height = bits.ToUnsignedInt(offset + 50, 8) / 10.; + wave_period = bits.ToUnsignedInt(offset + 58, 6); + wave_dir = bits.ToUnsignedInt(offset + 64, 9); + wave_sensor_type = bits.ToUnsignedInt(offset + 73, 3); + salinity = bits.ToUnsignedInt(offset + 76, 9) / 10.; +} + + +Ais8_367_33_Salinity::Ais8_367_33_Salinity(const AisBitset &bits, + const size_t offset) { + water_temp = bits.ToUnsignedInt(offset, 10) / 10. - 10; + conductivity = bits.ToUnsignedInt(offset + 10, 10) / 100.; + pressure = bits.ToUnsignedInt(offset + 20, 16) / 10.; + salinity = bits.ToUnsignedInt(offset + 36, 9) / 10.; + salinity_type = bits.ToUnsignedInt(offset + 45, 2); + sensor_type = bits.ToUnsignedInt(offset + 47, 3); + spare2[0] = bits.ToUnsignedInt(offset + 50, 32); + spare2[1] = bits.ToUnsignedInt(offset + 82, 3); +} + +Ais8_367_33_Wx::Ais8_367_33_Wx(const AisBitset &bits, + const size_t offset) { + air_temp = bits.ToInt(offset, 11) / 10.; + air_temp_sensor_type = bits.ToUnsignedInt(offset + 11, 3); + precip = bits.ToUnsignedInt(offset + 14, 2); + horz_vis = bits.ToUnsignedInt(offset + 16, 8) / 10.; + dew_point = (bits.ToUnsignedInt(offset + 24, 10) / 10.) - 20.0; + dew_point_type = bits.ToUnsignedInt(offset + 34, 3); + air_pressure = bits.ToUnsignedInt(offset + 37, 9) + 800; // hPa. + air_pressure_trend = bits.ToUnsignedInt(offset + 46, 2); + air_pressor_type = bits.ToUnsignedInt(offset + 48, 3); + salinity = bits.ToUnsignedInt(offset + 51, 9) / 10.; + spare2 = bits.ToUnsignedInt(offset + 60, 25); +} + +Ais8_367_33_AirGap::Ais8_367_33_AirGap(const AisBitset &bits, + const size_t offset) { + draught = bits.ToUnsignedInt(offset, 13) / 100.; + gap = bits.ToUnsignedInt(offset + 13, 13) / 10.; + trend = bits.ToUnsignedInt(offset + 26, 2); + forecast_gap = bits.ToUnsignedInt(offset + 28, 13) / 10.; + utc_day_forecast = bits.ToUnsignedInt(offset + 41, 5); + utc_hour_forecast = bits.ToUnsignedInt(offset + 46, 5); + utc_min_forecast = bits.ToUnsignedInt(offset + 51, 6); + type = bits.ToUnsignedInt(offset + 57, 3); + spare2 = bits.ToUnsignedInt(offset + 60, 25); +} + +Ais8_367_33_Wind_V2::Ais8_367_33_Wind_V2(const AisBitset &bits, + const size_t offset) { + wind_speed = bits.ToUnsignedInt(offset, 7); + wind_gust = bits.ToUnsignedInt(offset + 7, 7); + wind_dir = bits.ToUnsignedInt(offset + 14, 7); + averaging_time = bits.ToUnsignedInt(offset + 23, 6); + sensor_type = bits.ToUnsignedInt(offset + 29, 3); + wind_speed_forecast = bits.ToUnsignedInt(offset + 32, 7); + wind_gust_forecast = bits.ToUnsignedInt(offset + 39, 7); + wind_dir_forecast = bits.ToUnsignedInt(offset + 46, 9); + utc_hour_forecast = bits.ToUnsignedInt(offset + 55, 5); + utc_min_forecast = bits.ToUnsignedInt(offset + 60, 6); + duration = bits.ToUnsignedInt(offset + 66, 8); + spare2 = bits.ToUnsignedInt(offset + 74, 11); +} + + +Ais8_367_33_SensorReport* +ais8_367_33_sensor_report_factory(const AisBitset &bits, + const size_t offset) { + const Ais8_367_33_SensorEnum rpt_type = + (Ais8_367_33_SensorEnum)bits.ToUnsignedInt(offset, 4); + + // WARNING: out of order decoding + // Only get the report header if we can decode the type + const size_t rpt_start = offset + 27; // skip tp after site_id + bits.SeekTo(rpt_start); + Ais8_367_33_SensorReport *rpt = nullptr; + switch (rpt_type) { + case AIS8_367_33_SENSOR_LOCATION: + rpt = new Ais8_367_33_Location(bits, rpt_start); + break; + case AIS8_367_33_SENSOR_STATION: + rpt = new Ais8_367_33_Station(bits, rpt_start); + break; + case AIS8_367_33_SENSOR_WIND: + rpt = new Ais8_367_33_Wind(bits, rpt_start); + break; + case AIS8_367_33_SENSOR_WATER_LEVEL: + rpt = new Ais8_367_33_WaterLevel(bits, rpt_start); + break; + case AIS8_367_33_SENSOR_CURR_2D: + rpt = new Ais8_367_33_Curr2D(bits, rpt_start); + break; + case AIS8_367_33_SENSOR_CURR_3D: + rpt = new Ais8_367_33_Curr3D(bits, rpt_start); + break; + case AIS8_367_33_SENSOR_HORZ_FLOW: + rpt = new Ais8_367_33_HorzFlow(bits, rpt_start); + break; + case AIS8_367_33_SENSOR_SEA_STATE: + rpt = new Ais8_367_33_SeaState(bits, rpt_start); + break; + case AIS8_367_33_SENSOR_SALINITY: + rpt = new Ais8_367_33_Salinity(bits, rpt_start); + break; + case AIS8_367_33_SENSOR_WX: + rpt = new Ais8_367_33_Wx(bits, rpt_start); + break; + case AIS8_367_33_SENSOR_AIR_GAP: + rpt = new Ais8_367_33_AirGap(bits, rpt_start); + break; + case AIS8_367_33_SENSOR_WIND_REPORT_2: + rpt = new Ais8_367_33_Wind_V2(bits, rpt_start); + break; + // Leave rpt == 0 to indicate error + case AIS8_367_33_SENSOR_RESERVED_12: break; + case AIS8_367_33_SENSOR_RESERVED_13: break; + case AIS8_367_33_SENSOR_RESERVED_14: break; + case AIS8_367_33_SENSOR_RESERVED_15: break; + default: + {} // Leave rpt == 0 to indicate error + } + + if (!rpt) { + return rpt; + } + + rpt->report_type = rpt_type; + bits.SeekTo(offset + 4); + rpt->utc_day = bits.ToUnsignedInt(offset + 4, 5); + rpt->utc_hr = bits.ToUnsignedInt(offset + 9, 5); + rpt->utc_min = bits.ToUnsignedInt(offset + 14, 6); + rpt->site_id = bits.ToUnsignedInt(offset + 20, 7); + return rpt; +} + +Ais8_367_33::Ais8_367_33(const char *nmea_payload, const size_t pad) + : Ais8(nmea_payload, pad) { + assert(dac == 33); + assert(fi == 33); + + if (!CheckStatus()) { + return; + } + if (168 > num_bits || num_bits > 952) { + status = AIS_ERR_BAD_BIT_COUNT; + return; + } + + const size_t num_sensor_reports = (num_bits - 56) / AIS8_367_33_REPORT_SIZE; + + if (num_sensor_reports > 8) { + return; + } + + for (size_t report_idx = 0; report_idx < num_sensor_reports; report_idx++) { + const size_t start = 56 + report_idx * AIS8_367_33_REPORT_SIZE; + bits.SeekTo(start); + Ais8_367_33_SensorReport *sensor = + ais8_367_33_sensor_report_factory(bits, start); + if (sensor) { + reports.push_back(sensor); + } else { + status = AIS_ERR_BAD_SUB_SUB_MSG; + return; + } + } + + // NOTE: Enable this assert after fixing the message. + // assert(bits.GetRemaining() == 0); + status = AIS_OK; +} + +// TODO(schwehr): Use unique_ptr to manage memory. +Ais8_367_33::~Ais8_367_33() { + for (size_t i = 0; i < reports.size(); i++) { + delete reports[i]; + reports[i] = nullptr; + } +} + +} // namespace libais diff --git a/src/libais/ais_py.cpp b/src/libais/ais_py.cpp index a0fcce67..f6f08cd0 100644 --- a/src/libais/ais_py.cpp +++ b/src/libais/ais_py.cpp @@ -61,6 +61,7 @@ enum AIS_FI { AIS_FI_8_200_55_RIS_PERSONS_ON_BOARD = 50, AIS_FI_8_366_22_AREA_NOTICE = 22, // USCG. AIS_FI_8_367_22_AREA_NOTICE = 22, // USCG. + AIS_FI_8_367_24_SSW_SMALL = 24, // USCG Satellite Ship Weather Small }; void @@ -1988,6 +1989,31 @@ ais8_367_22_append_pydict(const char *nmea_payload, PyObject *dict, DictSafeSetItem(dict, "sub_areas", sub_area_list); } +AIS_STATUS +ais8_367_24_append_pydict(const char *nmea_payload, PyObject *dict, + const size_t pad) { + assert(nmea_payload); + assert(dict); + assert(pad < 6); + Ais8_367_24 msg(nmea_payload, pad); + if (msg.had_error()) { + return msg.get_error(); + } + + DictSafeSetItem(dict, "version", msg.version); + DictSafeSetItem(dict, "utc_hour", msg.utc_hour); + DictSafeSetItem(dict, "utc_min", msg.utc_min); + DictSafeSetItem(dict, "x", "y", msg.position); + if (msg.pressure <= 1201) { + DictSafeSetItem(dict, "pressure", msg.pressure); + } else { + // Raw values was 403 (N/A), or reserved value. + DictSafeSetItem(dict, "pressure", Py_None); + } + + return AIS_OK; +} + // AIS Binary broadcast messages. There will be a huge number of subtypes // If we don't know how to decode it, just return the dac, fi PyObject* @@ -2106,6 +2132,9 @@ ais8_to_pydict(const char *nmea_payload, const size_t pad) { case 22: // USCG Area Notice 2012 (v5?). ais8_367_22_append_pydict(nmea_payload, dict, pad); break; + case AIS_FI_8_367_24_SSW_SMALL: + status = ais8_367_24_append_pydict(nmea_payload, dict, pad); + break; default: DictSafeSetItem(dict, "parsed", false); break; diff --git a/src/libais/decode_body.cpp b/src/libais/decode_body.cpp index 0e6a1c9d..d7727f79 100644 --- a/src/libais/decode_body.cpp +++ b/src/libais/decode_body.cpp @@ -139,6 +139,8 @@ unique_ptr CreateAisMsg8(const string &body, const int fill_bits) { switch (msg.fi) { case 22: return MakeUnique(body.c_str(), fill_bits); + case 24: + return MakeUnique(body.c_str(), fill_bits); } // FI not handled. break; diff --git a/src/test/ais8_367_test.cc b/src/test/ais8_367_test.cc index 4b91df3c..6252aaa5 100644 --- a/src/test/ais8_367_test.cc +++ b/src/test/ais8_367_test.cc @@ -178,5 +178,36 @@ TEST(Ais8_367_22Test, DecodeUscgWhaleBouyTest2) { } #endif +std::unique_ptr Init_24(const string &nmea_string) { + const string body(GetBody(nmea_string)); + const int pad = GetPad(nmea_string); + + // TODO(schwehr): Switch to c++14 make_unique. + std::unique_ptr msg(new Ais8_367_24(body.c_str(), pad)); + if (!msg || msg->had_error()) { + return nullptr; + } + + return msg; +} + +TEST(Ais8_367_24Test, DecodeSingleTest_24) { + std::unique_ptr msg = + Init_24("!SAVDM,1,1,3,A,85Mr6<1Kn15CGvG@C4rmessage_id); + ASSERT_EQ(0, msg->repeat_indicator); + ASSERT_EQ(366904880, msg->mmsi); + ASSERT_EQ(0, msg->spare); + ASSERT_EQ(367, msg->dac); + ASSERT_EQ(24, msg->fi); + + ASSERT_EQ(0, msg->version); + ASSERT_EQ(17, msg->utc_hour); + ASSERT_EQ(20, msg->utc_min); + + ASSERT_EQ(1019, msg->pressure); +} + } // namespace } // namespace libais diff --git a/test/test_data.py b/test/test_data.py index 30f06a7a..47d7e1d0 100644 --- a/test/test_data.py +++ b/test/test_data.py @@ -673,4 +673,20 @@ 'y': -2.5533333333333332} }, + { + 'nmea': ['!SAVDM,1,1,3,A,85Mr6<1Kn15CGvG@C4r