Keep vanetza-idf in obu-firmware, so a plain clone builds the firmware

obu-firmware builds against the vanetza-idf C-ITS library, which until now
came from the colleague's microbu-esp32c5 tree beside the repository and was
not tracked here, so a clone of this repository could not build the firmware
it ships. The library alone is now part of obu-firmware, as
obu-firmware/external/vanetza-idf: their external/vanetza-idf at commit
cf4b99f, unchanged (9775 files; see its PROVENANCE.md). CMake takes it from
there by default; -DVANETZA_IDF_DIR still points the build elsewhere.

The rest of the colleague's tree (their own VAM firmware, PKI tooling,
station-link Python tools, the V2X2MAP bridge) stays out of this repository
and gitignored; nothing is pushed to their repository. NOTES.md, docs/06,
TODO.md and the pcap verifier's usage line point at the new location.
This commit is contained in:
Ashin Walpola
2026-09-24 10:56:05 +02:00
parent 2f60623e18
commit d107534eb2
9781 changed files with 1560475 additions and 17 deletions
@@ -0,0 +1,7 @@
include(UseGTest)
configure_gtest_directory(LINK_LIBRARIES geodesy)
add_gtest(Geodesy geodesy.cpp)
add_gtest(M49Code m49_code.cpp)
add_gtest(CountryDataReader country_data_reader.cpp)
add_gtest(CountryDatabase country_database.cpp)
@@ -0,0 +1,276 @@
#include <vanetza/common/byte_order.hpp>
#include <vanetza/geodesy/country_data_reader.hpp>
#include <gtest/gtest.h>
#include <cstring>
#include <map>
#include <vector>
using namespace vanetza;
using namespace vanetza::geodesy;
namespace
{
template<ByteOrder Order, typename T>
void append(std::vector<uint8_t>& buf, T v)
{
EndianType<T, Order> e = host_cast(v);
auto raw = e.get();
const auto* p = reinterpret_cast<const uint8_t*>(&raw);
buf.insert(buf.end(), p, p + sizeof(raw));
}
void append_u16le(std::vector<uint8_t>& buf, uint16_t v)
{
append<ByteOrder::LittleEndian>(buf, v);
}
void append_u32le(std::vector<uint8_t>& buf, uint32_t v)
{
append<ByteOrder::LittleEndian>(buf, v);
}
void append_u32be(std::vector<uint8_t>& buf, uint32_t v)
{
append<ByteOrder::BigEndian>(buf, v);
}
void append_f64le(std::vector<uint8_t>& buf, double v)
{
uint64_t bits;
std::memcpy(&bits, &v, sizeof(bits));
append<ByteOrder::LittleEndian>(buf, bits);
}
void append_f64be(std::vector<uint8_t>& buf, double v)
{
uint64_t bits;
std::memcpy(&bits, &v, sizeof(bits));
append<ByteOrder::BigEndian>(buf, bits);
}
std::vector<uint8_t> make_wkb_polygon_le(const std::vector<std::pair<double, double>>& ring)
{
std::vector<uint8_t> wkb;
wkb.push_back(0x01); // little-endian
append_u32le(wkb, 3); // type = Polygon
append_u32le(wkb, 1); // num_rings = 1
uint32_t num_points = static_cast<uint32_t>(ring.size()) + 1; // +1 for closing point
append_u32le(wkb, num_points);
for (const auto& p : ring) {
append_f64le(wkb, p.first); // lon
append_f64le(wkb, p.second); // lat
}
append_f64le(wkb, ring.front().first);
append_f64le(wkb, ring.front().second);
return wkb;
}
std::vector<uint8_t> make_wkb_polygon_be(const std::vector<std::pair<double, double>>& ring)
{
std::vector<uint8_t> wkb;
wkb.push_back(0x00); // big-endian
append_u32be(wkb, 3); // type = Polygon
append_u32be(wkb, 1); // num_rings = 1
uint32_t num_points = static_cast<uint32_t>(ring.size()) + 1;
append_u32be(wkb, num_points);
for (const auto& p : ring) {
append_f64be(wkb, p.first);
append_f64be(wkb, p.second);
}
append_f64be(wkb, ring.front().first);
append_f64be(wkb, ring.front().second);
return wkb;
}
std::vector<uint8_t> make_wkb_multipolygon_le(const std::vector<std::pair<double, double>>& ring)
{
auto poly_wkb = make_wkb_polygon_le(ring);
std::vector<uint8_t> wkb;
wkb.push_back(0x01); // little-endian
append_u32le(wkb, 6); // type = MultiPolygon
append_u32le(wkb, 1); // num_polygons = 1
wkb.insert(wkb.end(), poly_wkb.begin(), poly_wkb.end());
return wkb;
}
std::vector<uint8_t> make_entry(uint16_t m49, const std::vector<uint8_t>& wkb)
{
std::vector<uint8_t> entry;
append_u16le(entry, m49);
append_u32le(entry, static_cast<uint32_t>(wkb.size()));
entry.insert(entry.end(), wkb.begin(), wkb.end());
return entry;
}
std::vector<uint8_t> make_file(std::initializer_list<std::vector<uint8_t>> entries)
{
std::vector<uint8_t> buf;
append_u16le(buf, vanetza::geodesy::detail::country_data_format_version);
for (const auto& e : entries) {
buf.insert(buf.end(), e.begin(), e.end());
}
return buf;
}
// A simple square polygon approximating Europe: (5,45) to (15,55)
const std::vector<std::pair<double, double>> square_europe = {
{5.0, 45.0}, {15.0, 45.0}, {15.0, 55.0}, {5.0, 55.0}
};
// A simple square polygon approximating France: (-5,42) to (10,51)
const std::vector<std::pair<double, double>> square_france = {
{-5.0, 42.0}, {10.0, 42.0}, {10.0, 51.0}, {-5.0, 51.0}
};
} // anonymous namespace
TEST(CountryDataReader, empty_input)
{
auto result = read_country_data(nullptr, 0, [](M49Code, CountryPolygon&&) {});
EXPECT_TRUE(result.failed());
}
TEST(CountryDataReader, header_only)
{
auto data = make_file({});
std::map<uint16_t, CountryPolygon> result;
auto r = read_country_data(data.data(), data.size(), [&](M49Code code, CountryPolygon&& poly) {
result[code.value()] = std::move(poly);
});
EXPECT_TRUE(r.ok());
}
TEST(CountryDataReader, single_polygon_entry)
{
auto wkb = make_wkb_polygon_le(square_europe);
auto data = make_file({make_entry(276, wkb)});
std::map<uint16_t, CountryPolygon> result;
auto r = read_country_data(data.data(), data.size(), [&](M49Code code, CountryPolygon&& poly) {
result[code.value()] = std::move(poly);
});
EXPECT_TRUE(r.ok());
ASSERT_EQ(1u, result.size());
EXPECT_EQ(1u, result.count(276));
}
TEST(CountryDataReader, multipolygon_entry)
{
auto wkb = make_wkb_multipolygon_le(square_europe);
auto data = make_file({make_entry(276, wkb)});
std::map<uint16_t, CountryPolygon> result;
auto r = read_country_data(data.data(), data.size(), [&](M49Code code, CountryPolygon&& poly) {
result[code.value()] = std::move(poly);
});
EXPECT_TRUE(r.ok());
ASSERT_EQ(1u, result.size());
EXPECT_EQ(1u, result.count(276));
}
TEST(CountryDataReader, multiple_entries)
{
auto wkb1 = make_wkb_polygon_le(square_europe);
auto wkb2 = make_wkb_polygon_le(square_france);
auto data = make_file({make_entry(276, wkb1), make_entry(250, wkb2)});
std::map<uint16_t, CountryPolygon> result;
auto r = read_country_data(data.data(), data.size(), [&](M49Code code, CountryPolygon&& poly) {
result[code.value()] = std::move(poly);
});
EXPECT_TRUE(r.ok());
EXPECT_EQ(2u, result.size());
EXPECT_EQ(1u, result.count(276));
EXPECT_EQ(1u, result.count(250));
}
TEST(CountryDataReader, big_endian_wkb_payload)
{
auto wkb = make_wkb_polygon_be(square_europe);
auto data = make_file({make_entry(276, wkb)});
std::map<uint16_t, CountryPolygon> result;
auto r = read_country_data(data.data(), data.size(), [&](M49Code code, CountryPolygon&& poly) {
result[code.value()] = std::move(poly);
});
EXPECT_TRUE(r.ok());
ASSERT_EQ(1u, result.size());
EXPECT_EQ(1u, result.count(276));
}
TEST(CountryDataReader, truncated_version_header)
{
std::vector<uint8_t> data = {0x14}; // only 1 byte, not enough for version field
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
EXPECT_TRUE(result.failed());
}
TEST(CountryDataReader, unsupported_version)
{
std::vector<uint8_t> data;
append_u16le(data, vanetza::geodesy::detail::country_data_format_version + 1);
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
EXPECT_TRUE(result.failed());
}
TEST(CountryDataReader, truncated_entry_header)
{
std::vector<uint8_t> data;
append_u16le(data, vanetza::geodesy::detail::country_data_format_version);
data.push_back(0x14); // one byte of an entry header, not enough
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
EXPECT_TRUE(result.failed());
}
TEST(CountryDataReader, truncated_wkb_payload)
{
auto wkb = make_wkb_polygon_le(square_europe);
auto data = make_file({make_entry(276, wkb)});
// keep the version header + entry header intact but truncate halfway through the WKB
data.resize(data.size() - wkb.size() / 2);
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
EXPECT_TRUE(result.failed());
EXPECT_FALSE(result.message().empty());
}
TEST(CountryDataReader, wkb_size_exceeds_remaining)
{
std::vector<uint8_t> data;
append_u16le(data, vanetza::geodesy::detail::country_data_format_version);
append_u16le(data, 276);
append_u32le(data, 9999); // claims 9999 bytes of WKB
data.push_back(0x01); // just 1 byte
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
EXPECT_TRUE(result.failed());
}
TEST(CountryDataReader, unsupported_geometry_type)
{
std::vector<uint8_t> wkb;
wkb.push_back(0x01); // little-endian
append_u32le(wkb, 1); // type = Point (unsupported)
append_f64le(wkb, 10.0);
append_f64le(wkb, 50.0);
auto data = make_file({make_entry(276, wkb)});
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
EXPECT_TRUE(result.failed());
}
TEST(CountryDataReader, failure_reports_position)
{
// "truncated entry header" — position is the offset of the partial entry (right after version).
std::vector<uint8_t> data;
append_u16le(data, vanetza::geodesy::detail::country_data_format_version);
data.push_back(0x14); // partial entry header at offset 2
auto result = read_country_data(data.data(), data.size(), [](M49Code, CountryPolygon&&) {});
ASSERT_TRUE(result.failed());
EXPECT_EQ(sizeof(uint16_t), result.position());
}
@@ -0,0 +1,186 @@
#include <vanetza/common/byte_order.hpp>
#include <vanetza/geodesy/country_data_reader.hpp>
#include <vanetza/geodesy/country_database.hpp>
#include <gtest/gtest.h>
#include <cstring>
#include <vector>
using namespace vanetza;
using namespace vanetza::geodesy;
using vanetza::units::degree;
namespace
{
template<ByteOrder Order, typename T>
void append(std::vector<uint8_t>& buf, T v)
{
EndianType<T, Order> e;
e = host_cast(v);
auto raw = e.get();
const auto* p = reinterpret_cast<const uint8_t*>(&raw);
buf.insert(buf.end(), p, p + sizeof(raw));
}
void append_u16le(std::vector<uint8_t>& buf, uint16_t v)
{
append<ByteOrder::LittleEndian>(buf, v);
}
void append_u32le(std::vector<uint8_t>& buf, uint32_t v)
{
append<ByteOrder::LittleEndian>(buf, v);
}
void append_f64le(std::vector<uint8_t>& buf, double v)
{
uint64_t bits;
std::memcpy(&bits, &v, sizeof(bits));
append<ByteOrder::LittleEndian>(buf, bits);
}
// Build WKB Polygon (LE) for a rectangle
std::vector<uint8_t> make_rect_wkb(double lon_min, double lat_min, double lon_max, double lat_max)
{
std::vector<uint8_t> wkb;
wkb.push_back(0x01); // LE
append_u32le(wkb, 3); // Polygon
append_u32le(wkb, 1); // 1 ring
append_u32le(wkb, 5); // 5 points (closed)
append_f64le(wkb, lon_min); append_f64le(wkb, lat_min);
append_f64le(wkb, lon_max); append_f64le(wkb, lat_min);
append_f64le(wkb, lon_max); append_f64le(wkb, lat_max);
append_f64le(wkb, lon_min); append_f64le(wkb, lat_max);
append_f64le(wkb, lon_min); append_f64le(wkb, lat_min);
return wkb;
}
std::vector<uint8_t> make_entry(uint16_t m49, const std::vector<uint8_t>& wkb)
{
std::vector<uint8_t> entry;
append_u16le(entry, m49);
append_u32le(entry, static_cast<uint32_t>(wkb.size()));
entry.insert(entry.end(), wkb.begin(), wkb.end());
return entry;
}
const uint16_t germany_m49 = 276;
const uint16_t france_m49 = 250;
std::vector<uint8_t> make_test_data()
{
// Germany bounding box (rough): lon 5.9-15.0, lat 47.3-55.1
auto de_wkb = make_rect_wkb(5.9, 47.3, 15.0, 55.1);
// France bounding box (rough): lon -5.1-9.6, lat 42.3-51.1
auto fr_wkb = make_rect_wkb(-5.1, 42.3, 9.6, 51.1);
auto de_entry = make_entry(germany_m49, de_wkb);
auto fr_entry = make_entry(france_m49, fr_wkb);
std::vector<uint8_t> data;
append_u16le(data, vanetza::geodesy::detail::country_data_format_version);
data.insert(data.end(), de_entry.begin(), de_entry.end());
data.insert(data.end(), fr_entry.begin(), fr_entry.end());
return data;
}
} // anonymous namespace
TEST(CountryDatabase, initially_empty)
{
CountryDatabase db;
EXPECT_TRUE(db.empty());
}
TEST(CountryDatabase, load_from_buffer)
{
auto data = make_test_data();
CountryDatabase db;
EXPECT_TRUE(db.load(data.data(), data.size()));
EXPECT_FALSE(db.empty());
}
TEST(CountryDatabase, berlin_inside_germany)
{
auto data = make_test_data();
CountryDatabase db;
ASSERT_TRUE(db.load(data.data(), data.size()));
GeodeticPosition berlin(52.52 * degree, 13.405 * degree);
EXPECT_TRUE(db.is_inside(M49Code(germany_m49), berlin));
}
TEST(CountryDatabase, paris_inside_france)
{
auto data = make_test_data();
CountryDatabase db;
ASSERT_TRUE(db.load(data.data(), data.size()));
GeodeticPosition paris(48.8566 * degree, 2.3522 * degree);
EXPECT_TRUE(db.is_inside(M49Code(france_m49), paris));
}
TEST(CountryDatabase, berlin_not_in_france)
{
auto data = make_test_data();
CountryDatabase db;
ASSERT_TRUE(db.load(data.data(), data.size()));
GeodeticPosition berlin(52.52 * degree, 13.405 * degree);
EXPECT_FALSE(db.is_inside(M49Code(france_m49), berlin));
}
TEST(CountryDatabase, mid_atlantic_in_no_country)
{
auto data = make_test_data();
CountryDatabase db;
ASSERT_TRUE(db.load(data.data(), data.size()));
GeodeticPosition ocean(40.0 * degree, -30.0 * degree);
EXPECT_FALSE(db.is_inside(M49Code(germany_m49), ocean));
EXPECT_FALSE(db.is_inside(M49Code(france_m49), ocean));
}
TEST(CountryDatabase, unknown_country_code)
{
auto data = make_test_data();
CountryDatabase db;
ASSERT_TRUE(db.load(data.data(), data.size()));
GeodeticPosition berlin(52.52 * degree, 13.405 * degree);
EXPECT_FALSE(db.is_inside(M49Code(999), berlin));
}
TEST(CountryDatabase, load_invalid_data)
{
std::vector<uint8_t> bad_data = {0x14};
CountryDatabase db;
std::string error;
EXPECT_FALSE(db.load(bad_data.data(), bad_data.size(), &error));
EXPECT_FALSE(error.empty());
EXPECT_TRUE(db.empty());
}
#ifdef VANETZA_WITH_EMBEDDED_COUNTRY_DATA
TEST(CountryDatabase, embedded_not_empty)
{
auto db = CountryDatabase::embedded();
EXPECT_FALSE(db.empty());
}
TEST(CountryDatabase, embedded_ingolstadt_in_germany)
{
auto db = CountryDatabase::embedded();
// Ingolstadt, Germany
GeodeticPosition ingolstadt(48.7665 * degree, 11.4258 * degree);
EXPECT_TRUE(db.is_inside(M49Code(germany_m49), ingolstadt));
}
TEST(CountryDatabase, embedded_etsi_hq_in_france)
{
auto db = CountryDatabase::embedded();
// ETSI headquarters, Sophia Antipolis, France
GeodeticPosition etsi_hq(43.6244 * degree, 7.0494 * degree);
EXPECT_TRUE(db.is_inside(M49Code(france_m49), etsi_hq));
}
#endif // VANETZA_WITH_EMBEDDED_COUNTRY_DATA
@@ -0,0 +1,110 @@
#include <gtest/gtest.h>
#include <vanetza/geodesy/geodesy.hpp>
#include <vanetza/geodesy/haversine.hpp>
#ifdef VANETZA_WITH_GEOGRAPHICLIB
#include <vanetza/geodesy/geographiclib.hpp>
#endif
using namespace vanetza::geodesy;
namespace units = vanetza::units;
using units::si::meter;
using units::degree;
// Technische Hochschule Ingolstadt
static const GeodeticPosition thi(48.76714 * degree, 11.43263 * degree);
// Zentrum fuer Angewandte Forschung
static const GeodeticPosition zaf(48.7656 * degree, 11.4296 * degree);
// Munich (short range ~70 km)
static const GeodeticPosition munich(48.1351 * degree, 11.5820 * degree);
// Frankfurt (medium range ~304 km)
static const GeodeticPosition frankfurt(50.1109 * degree, 8.6821 * degree);
// Sao Paulo (long range ~10000 km)
static const GeodeticPosition sao_paulo(-25.41272 * degree, -49.24815 * degree);
TEST(GeodesyHaversine, distance_zero)
{
auto d = haversine::distance(thi, thi);
EXPECT_DOUBLE_EQ(0.0, d / meter);
}
TEST(GeodesyHaversine, distance_short_range)
{
auto d = haversine::distance(thi, zaf);
EXPECT_NEAR(d / meter, 280.0, 10.0);
}
TEST(GeodesyHaversine, distance_medium_range)
{
auto d = haversine::distance(frankfurt, munich);
EXPECT_NEAR(d / meter, 304000.0, 3000.0);
}
TEST(GeodesyHaversine, local_cartesian_short_range)
{
auto cart = haversine::local_cartesian(thi, zaf);
// ZAF is roughly south-west of THI
EXPECT_NEAR(cart.x / meter, -222.0, 5.0);
EXPECT_NEAR(cart.y / meter, -171.0, 5.0);
}
TEST(GeodesyHaversine, local_cartesian_zero)
{
auto cart = haversine::local_cartesian(thi, thi);
EXPECT_DOUBLE_EQ(cart.x / meter, 0.0);
EXPECT_DOUBLE_EQ(cart.y / meter, 0.0);
}
#ifdef VANETZA_WITH_GEOGRAPHICLIB
TEST(GeodesyGeographicLib, distance_matches_known_value)
{
auto d = geographiclib::distance(thi, sao_paulo);
EXPECT_NEAR(d / meter, 10185367.442, 0.5);
}
TEST(GeodesyGeographicLib, local_cartesian_known_value)
{
auto cart = geographiclib::local_cartesian(thi, zaf);
EXPECT_NEAR(cart.x / meter, -222.74, 0.01);
EXPECT_NEAR(cart.y / meter, -171.25, 0.01);
}
TEST(GeodesyComparison, distance_short_range)
{
auto h = haversine::distance(thi, zaf);
auto g = geographiclib::distance(thi, zaf);
EXPECT_NEAR(h / meter, g / meter, std::abs(g / meter * 0.005)); // <0.5%
}
TEST(GeodesyComparison, distance_medium_range)
{
auto h = haversine::distance(frankfurt, munich);
auto g = geographiclib::distance(frankfurt, munich);
EXPECT_NEAR(h / meter, g / meter, std::abs(g / meter * 0.005)); // <0.5%
}
TEST(GeodesyComparison, distance_long_range)
{
auto h = haversine::distance(thi, sao_paulo);
auto g = geographiclib::distance(thi, sao_paulo);
EXPECT_NEAR(h / meter, g / meter, std::abs(g / meter * 0.005)); // <0.5%
}
TEST(GeodesyComparison, local_cartesian_short_range)
{
auto h = haversine::local_cartesian(thi, zaf);
auto g = geographiclib::local_cartesian(thi, zaf);
EXPECT_NEAR(h.x / meter, g.x / meter, 1.0); // <1m
EXPECT_NEAR(h.y / meter, g.y / meter, 1.0);
}
TEST(GeodesyComparison, local_cartesian_medium_range)
{
auto h = haversine::local_cartesian(thi, munich);
auto g = geographiclib::local_cartesian(thi, munich);
EXPECT_NEAR(h.x / meter, g.x / meter, std::abs(g.x / meter * 0.02)); // <2%
EXPECT_NEAR(h.y / meter, g.y / meter, std::abs(g.y / meter * 0.02));
}
#endif // VANETZA_WITH_GEOGRAPHICLIB
@@ -0,0 +1,39 @@
#include <vanetza/geodesy/m49_code.hpp>
#include <gtest/gtest.h>
#include <map>
#include <string>
#include <unordered_set>
using vanetza::geodesy::M49Code;
TEST(M49Code, value)
{
M49Code code(276);
EXPECT_EQ(276, code.value());
}
TEST(M49Code, equality)
{
EXPECT_EQ(M49Code(276), M49Code(276));
EXPECT_NE(M49Code(276), M49Code(250));
}
TEST(M49Code, std_hash)
{
std::unordered_set<M49Code> codes;
codes.insert(M49Code(276));
codes.insert(M49Code(250));
codes.insert(M49Code(276)); // duplicate
EXPECT_EQ(2u, codes.size());
EXPECT_EQ(1u, codes.count(M49Code(276)));
EXPECT_EQ(1u, codes.count(M49Code(250)));
}
TEST(M49Code, std_less)
{
std::map<M49Code, std::string> names;
names[M49Code(276)] = "Germany";
names[M49Code(250)] = "France";
EXPECT_EQ("Germany", names.at(M49Code(276)));
EXPECT_EQ("France", names.at(M49Code(250)));
}