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,23 @@
set(CXX_SOURCES
burst_budget.cpp
bursty_transmit_rate_control.cpp
channel_load.cpp
flow_control.cpp
fully_meshed_state_machine.cpp
gradual_state_machine.cpp
hooked_channel_probe_processor.cpp
limeric.cpp
limeric_budget.cpp
limeric_transmit_rate_control.cpp
mapping.cpp
single_reactive_transmit_rate_control.cpp
state_machine_budget.cpp
smoothing_channel_probe_processor.cpp
transmission.cpp
)
add_vanetza_component(dcc ${CXX_SOURCES})
target_link_libraries(dcc PUBLIC access net)
add_test_subdirectory(tests)
@@ -0,0 +1,71 @@
#include <vanetza/common/runtime.hpp>
#include <vanetza/dcc/burst_budget.hpp>
#include <cassert>
namespace vanetza
{
namespace dcc
{
namespace
{
// these constants are given in C2C-CC Basic System Profile (last v1.3.0)
constexpr Clock::duration T_Burst = std::chrono::seconds(1);
constexpr Clock::duration T_BurstPeriod = std::chrono::seconds(10);
constexpr std::size_t N_Burst = 20;
} // namespace
BurstBudget::BurstBudget(const Runtime& rt) :
m_runtime(rt), m_messages(N_Burst), m_burst_duration(T_Burst), m_burst_period(T_BurstPeriod)
{
}
BurstBudget::~BurstBudget()
{
}
Clock::duration BurstBudget::delay()
{
assert(m_burst_duration < m_burst_period);
Clock::duration delay = Clock::duration::max();
if (m_messages.empty()) {
delay = Clock::duration::zero();
} else if (m_messages.front() + m_burst_duration > m_runtime.now() && !m_messages.full()) {
delay = Clock::duration::zero();
} else if (m_messages.front() + m_burst_period < m_runtime.now()) {
m_messages.clear();
delay = Clock::duration::zero();
} else {
delay = m_messages.front() + m_burst_period - m_runtime.now();
}
return delay;
}
void BurstBudget::notify()
{
m_messages.push_back(m_runtime.now());
}
void BurstBudget::burst_messages(std::size_t n)
{
// circular_buffer::resize removes last elements if necessary
m_messages.resize(n);
}
void BurstBudget::burst_duration(Clock::duration d)
{
assert(d > Clock::duration::zero());
m_burst_duration = d;
}
void BurstBudget::burst_period(Clock::duration p)
{
assert(p > Clock::duration::zero());
m_burst_period = p;
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,73 @@
#ifndef BURST_BUDGET_HPP_U5GXDCZN
#define BURST_BUDGET_HPP_U5GXDCZN
#include <vanetza/common/clock.hpp>
#include <boost/circular_buffer.hpp>
#include <chrono>
#include <cstddef>
namespace vanetza
{
// forward declaration
class Runtime;
namespace dcc
{
/**
* BurstBudget: TRC restrictions for DP0 message bursts as per C2C-CC BSP.
*/
class BurstBudget
{
public:
BurstBudget(const Runtime&);
~BurstBudget();
/**
* Get current delay to remain in budget
* \return shortest delay not exceeding budget
*/
Clock::duration delay();
/**
* Notify budget of consumption
*/
void notify();
/**
* Set upper limit of messages per burst
* \param n burst limit
*/
void burst_messages(std::size_t n);
/**
* Set maximum duration per burst
* \param d burst duration
*/
void burst_duration(Clock::duration d);
/**
* Set minimum duration between bursts
* \param p burst period
*/
void burst_period(Clock::duration p);
/**
* Get burst interval
* \return bust interval
*/
Clock::duration burst_period() const { return m_burst_period; }
private:
const Runtime& m_runtime;
boost::circular_buffer<Clock::time_point> m_messages;
Clock::duration m_burst_duration;
Clock::duration m_burst_period;
};
} // namespace dcc
} // namespace vanetza
#endif /* BURST_BUDGET_HPP_U5GXDCZN */
@@ -0,0 +1,76 @@
#include <vanetza/common/runtime.hpp>
#include <vanetza/dcc/bursty_transmit_rate_control.hpp>
#include <vanetza/dcc/state_machine.hpp>
#include <stdexcept>
namespace vanetza
{
namespace dcc
{
BurstyTransmitRateControl::BurstyTransmitRateControl(const StateMachine& fsm, const Runtime& rt) :
m_burst_budget(rt), m_fsm_budget(fsm, rt), m_fsm(fsm)
{
}
Clock::duration BurstyTransmitRateControl::delay(const Transmission& tx)
{
Clock::duration delay = Clock::duration::max();
switch (tx.profile()) {
case Profile::DP0:
delay = m_burst_budget.delay();
break;
case Profile::DP1:
case Profile::DP2:
case Profile::DP3:
delay = m_fsm_budget.delay();
break;
default:
throw std::invalid_argument("Invalid DCC Profile");
break;
};
return delay;
}
Clock::duration BurstyTransmitRateControl::interval(const Transmission& tx)
{
Clock::duration interval = Clock::duration::max();
switch (tx.profile()) {
case Profile::DP0:
interval = m_burst_budget.burst_period();
break;
case Profile::DP1:
case Profile::DP2:
case Profile::DP3:
interval = m_fsm.transmission_interval();
break;
default:
throw std::invalid_argument("Invalid DCC Profile");
break;
}
return interval;
}
void BurstyTransmitRateControl::notify(const Transmission& tx)
{
switch (tx.profile()) {
case Profile::DP0:
m_burst_budget.notify();
break;
case Profile::DP1:
case Profile::DP2:
case Profile::DP3:
m_fsm_budget.notify();
break;
default:
throw std::invalid_argument("Invalid DCC Profile");
break;
};
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,43 @@
#ifndef BURSTY_TRANSMIT_RATE_CONTROL_HPP_AM7LROYD
#define BURSTY_TRANSMIT_RATE_CONTROL_HPP_AM7LROYD
#include <vanetza/common/clock.hpp>
#include <vanetza/dcc/burst_budget.hpp>
#include <vanetza/dcc/profile.hpp>
#include <vanetza/dcc/state_machine_budget.hpp>
#include <vanetza/dcc/transmit_rate_control.hpp>
namespace vanetza
{
// forward declarations
class Runtime;
namespace dcc { class StateMachine; }
namespace dcc
{
/**
* Transmit Rate Control with occasional DP0 message bursts.
* DP1, DP2 and DP3 messages are controlled by a state machine only.
*/
class BurstyTransmitRateControl : public TransmitRateControl
{
public:
BurstyTransmitRateControl(const StateMachine&, const Runtime& rt);
Clock::duration delay(const Transmission&) override;
Clock::duration interval(const Transmission&) override;
void notify(const Transmission&) override;
private:
BurstBudget m_burst_budget;
StateMachineBudget m_fsm_budget;
const StateMachine& m_fsm;
};
} // namespace dcc
} // namespace vanetza
#endif /* BURSTY_TRANSMIT_RATE_CONTROL_HPP_AM7LROYD */
@@ -0,0 +1,31 @@
#include "channel_load.hpp"
namespace vanetza
{
namespace dcc
{
ChannelLoad::ChannelLoad(const UnitInterval& interval) :
UnitInterval(interval)
{
}
ChannelLoad::ChannelLoad(unsigned probes_busy, unsigned probes_total) :
UnitInterval(create_from_probes(probes_busy, probes_total))
{
}
UnitInterval ChannelLoad::create_from_probes(unsigned probes_busy, unsigned probes_total)
{
double fraction = 0.0;
if (probes_total != 0) {
fraction = probes_busy;
fraction /= probes_total;
}
return UnitInterval(fraction);
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,40 @@
#ifndef CHANNEL_LOAD_HPP_D1JOCNLP
#define CHANNEL_LOAD_HPP_D1JOCNLP
#include <vanetza/common/unit_interval.hpp>
namespace vanetza
{
namespace dcc
{
class ChannelLoad : public UnitInterval
{
public:
using UnitInterval::UnitInterval;
ChannelLoad() = default;
ChannelLoad(const UnitInterval&);
/**
* Create ChannelLoad from rational probes
* \see ChannelLoad::create_from_probes
*
* \param probes_busy number of probes above busy threshold
* \param probes_total total number of probes
*/
ChannelLoad(unsigned probes_busy, unsigned probes_total);
/**
* Calculate UnitInterval representing ChannelLoad from rational probes
* \param probes_busy number of probes above busy threshold
* \param probes_total total number of probes
* \return interval representing channel load (capped if probes_total < probes_busy)
*/
static UnitInterval create_from_probes(unsigned probes_busy, unsigned probes_total);
};
} // namespace dcc
} // namespace vanetza
#endif /* CHANNEL_LOAD_HPP_D1JOCNLP */
@@ -0,0 +1,32 @@
#ifndef CHANNEL_PROBE_PROCESSOR_HPP_QBFTHSVC
#define CHANNEL_PROBE_PROCESSOR_HPP_QBFTHSVC
#include <vanetza/dcc/channel_load.hpp>
namespace vanetza
{
namespace dcc
{
/**
* Access point for radio layers to propagate their local channel load measurements
*/
class ChannelProbeProcessor
{
public:
/**
* Indicate a new channel load measurement
* \see TS 102 686 V1.1.1 Annex A.1.2 for definition of "channel load"
*
* \param cl locally measured channel load
*/
virtual void indicate(ChannelLoad cl) = 0;
virtual ~ChannelProbeProcessor() = default;
};
} // namespace dcc
} // namespace vanetza
#endif /* CHANNEL_PROBE_PROCESSOR_HPP_QBFTHSVC */
@@ -0,0 +1,29 @@
#ifndef DATA_REQUEST_HPP_A5DWTTJN
#define DATA_REQUEST_HPP_A5DWTTJN
#include <vanetza/common/byte_order.hpp>
#include <vanetza/common/clock.hpp>
#include <vanetza/dcc/profile.hpp>
#include <vanetza/net/mac_address.hpp>
namespace vanetza
{
namespace dcc
{
struct DataRequest
{
DataRequest() : dcc_profile(Profile::DP0) {}
uint16be_t ether_type;
MacAddress source;
MacAddress destination;
Profile dcc_profile;
Clock::duration lifetime;
};
} // namespace dcc
} // namespace vanetza
#endif /* DATA_REQUEST_HPP_A5DWTTJN */
@@ -0,0 +1,30 @@
#ifndef DUTY_CYCLE_PERMIT_HPP_9QUTPOPH
#define DUTY_CYCLE_PERMIT_HPP_9QUTPOPH
#include <vanetza/common/unit_interval.hpp>
namespace vanetza
{
namespace dcc
{
/**
* Interface for controlling channel usage by duty cycle limits
*/
class DutyCyclePermit
{
public:
/**
* Get allowed channel occupancy for local station in current time window
* \return permitted duty cycle
*/
virtual UnitInterval permitted_duty_cycle() const = 0;
virtual ~DutyCyclePermit() = default;
};
} // namespace dcc
} // namespace vanetza
#endif /* DUTY_CYCLE_PERMIT_HPP_9QUTPOPH */
@@ -0,0 +1,37 @@
#ifndef ENTITY_HPP_KUAWS3PK
#define ENTITY_HPP_KUAWS3PK
#include <vanetza/dcc/channel_probe_processor.hpp>
#include <vanetza/dcc/transmit_rate_control.hpp>
namespace vanetza
{
namespace dcc
{
class Entity
{
public:
/**
* Provide TRC interface for Facilities
*
* Cooperative Awareness adapts its message rate according to TRC (T_GenCam_Dcc).
* \see EN 302 637-2 V1.3.2 (section 6.1.3)
*/
virtual TransmitRateControl& transmit_rate_control() = 0;
/**
* Provide interface for reporting channel probes.
*
* Usually, radio hardware will generate these reports periodically.
*/
virtual ChannelProbeProcessor& channel_probe_processor() = 0;
virtual ~Entity() = default;
};
} // namespace dcc
} // namespace vanetza
#endif /* ENTITY_HPP_KUAWS3PK */
@@ -0,0 +1,187 @@
#include "data_request.hpp"
#include "flow_control.hpp"
#include "mapping.hpp"
#include "transmit_rate_control.hpp"
#include <vanetza/access/data_request.hpp>
#include <vanetza/access/interface.hpp>
#include <vanetza/common/runtime.hpp>
#include <algorithm>
namespace vanetza
{
namespace dcc
{
FlowControl::FlowControl(Runtime& runtime, TransmitRateControl& trc, access::Interface& ifc) :
m_runtime(runtime), m_trc(trc), m_access(ifc), m_queue_length(0)
{
}
FlowControl::~FlowControl()
{
m_runtime.cancel(this);
}
void FlowControl::request(const DataRequest& request, std::unique_ptr<ChunkPacket> packet)
{
drop_expired();
const TransmissionLite transmission { request.dcc_profile, packet->size() };
if (transmit_immediately(transmission)) {
m_trc.notify(transmission);
transmit(request, std::move(packet));
} else {
enqueue(request, std::move(packet));
}
}
void FlowControl::trigger()
{
drop_expired();
auto transmission = dequeue();
if (transmission) {
m_trc.notify(*transmission);
transmit(transmission->request, std::move(transmission->packet));
}
PendingTransmission* next = next_transmission();
if (next) {
schedule_trigger(*next);
}
}
void FlowControl::schedule_trigger(const Transmission& tx)
{
auto callback_delay = m_trc.delay(tx);
m_runtime.schedule(callback_delay, std::bind(&FlowControl::trigger, this), this);
}
void FlowControl::enqueue(const DataRequest& request, std::unique_ptr<ChunkPacket> packet)
{
const bool first_packet = empty();
const auto ac = map_profile_onto_ac(request.dcc_profile);
auto expiry = m_runtime.now() + request.lifetime;
while (m_queue_length > 0 && m_queues[ac].size() >= m_queue_length) {
m_queues[ac].pop_front();
m_packet_drop_hook(ac, packet.get());
}
m_queues[ac].emplace_back(expiry, request, std::move(packet));
if (first_packet) {
schedule_trigger(m_queues[ac].back());
}
}
boost::optional<FlowControl::PendingTransmission> FlowControl::dequeue()
{
boost::optional<PendingTransmission> transmission;
Queue* queue = next_queue();
if (queue) {
transmission = std::move(queue->front());
queue->pop_front();
}
return transmission;
}
bool FlowControl::transmit_immediately(const Transmission& transmission) const
{
const auto ac = map_profile_onto_ac(transmission.profile());
// is there any packet enqueued with equal or higher priority?
bool contention = false;
for (auto it = m_queues.cbegin(); it != m_queues.end(); ++it) {
if (it->first >= ac && !it->second.empty()) {
contention = true;
break;
}
}
return !contention && m_trc.delay(transmission) == Clock::duration::zero();
}
bool FlowControl::empty() const
{
return std::all_of(m_queues.cbegin(), m_queues.cend(),
[](const std::pair<access::AccessCategory, const Queue&>& kv) {
return kv.second.empty();
});
}
FlowControl::Queue* FlowControl::next_queue()
{
Queue* next = nullptr;
Clock::duration min_delay = Clock::duration::max();
for (auto& kv : m_queues) {
Queue& queue = kv.second;
if (!queue.empty()) {
const auto delay = m_trc.delay(queue.front());
if (delay < min_delay) {
min_delay = delay;
next = &queue;
}
}
}
return next;
}
FlowControl::PendingTransmission* FlowControl::next_transmission()
{
Queue* queue = next_queue();
return queue ? &queue->front() : nullptr;
}
void FlowControl::drop_expired()
{
for (auto& kv : m_queues) {
access::AccessCategory ac = kv.first;
Queue& queue = kv.second;
queue.remove_if([this, ac](const PendingTransmission& transmission) {
bool drop = transmission.expiry < m_runtime.now();
if (drop) {
m_packet_drop_hook(ac, transmission.packet.get());
}
return drop;
});
}
}
void FlowControl::transmit(const DataRequest& request, std::unique_ptr<ChunkPacket> packet)
{
access::DataRequest access_request;
access_request.source_addr = request.source;
access_request.destination_addr = request.destination;
access_request.ether_type = request.ether_type;
access_request.access_category = map_profile_onto_ac(request.dcc_profile);
m_packet_transmit_hook(access_request.access_category, packet.get());
m_access.request(access_request, std::move(packet));
}
void FlowControl::set_packet_drop_hook(PacketDropHook::callback_type&& cb)
{
m_packet_drop_hook = std::move(cb);
}
void FlowControl::set_packet_transmit_hook(PacketTransmitHook::callback_type&& cb)
{
m_packet_transmit_hook = std::move(cb);
}
void FlowControl::queue_length(std::size_t length)
{
m_queue_length = length;
}
void FlowControl::reschedule()
{
PendingTransmission* next = next_transmission();
if (next) {
m_runtime.cancel(this);
schedule_trigger(*next);
}
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,126 @@
#ifndef FLOW_CONTROL_HPP_PG7RKD8V
#define FLOW_CONTROL_HPP_PG7RKD8V
#include <vanetza/common/clock.hpp>
#include <vanetza/common/hook.hpp>
#include <vanetza/dcc/data_request.hpp>
#include <vanetza/dcc/interface.hpp>
#include <vanetza/dcc/profile.hpp>
#include <vanetza/dcc/transmission.hpp>
#include <vanetza/net/chunk_packet.hpp>
#include <boost/optional/optional.hpp>
#include <vanetza/access/access_category.hpp>
#include <list>
#include <memory>
#include <map>
namespace vanetza
{
// forward declarations
namespace access { class Interface; }
class Runtime;
namespace dcc
{
// forward declarations
class TransmitRateControl;
/**
* FlowControl is a gatekeeper above access layer.
*
* There is a queue for each access category. Packets might be enqueued
* because of exceeded transmission intervals determined by Scheduler.
* If a packet's lifetime expires before transmission it will be dropped.
*/
class FlowControl : public RequestInterface
{
public:
using PacketDropHook = Hook<access::AccessCategory, const ChunkPacket*>;
using PacketTransmitHook = Hook<access::AccessCategory, const ChunkPacket*>;
/**
* Create FlowControl instance
* \param rt Runtime used for timed actions, e.g. packet expiry
* \param scheduler Scheduler providing transmission intervals
* \param access Interface to access layer
*/
FlowControl(Runtime&, TransmitRateControl&, access::Interface&);
~FlowControl();
/**
* Request packet transmission
* \param request DCC request parameters
* \param packet Packet data
*/
void request(const DataRequest&, std::unique_ptr<ChunkPacket>) override;
/**
* Set callback to be invoked at packet drop. Replaces any previous callback.
* \param cb Callback
*/
void set_packet_drop_hook(PacketDropHook::callback_type&&);
/**
* Set callback to be invoked at packet transmission. Replaces any previous callback.
* \param cb Callback
*/
void set_packet_transmit_hook(PacketTransmitHook::callback_type&&);
/**
* Set length of each queue
*
* The first queue element is dropped when the length limit is hit.
* \param length Maximum number of queue elements, 0 for unlimited length
*/
void queue_length(std::size_t length);
/**
* Reschedule queued transmissions
* This reevaluates TRC restrictions as well, packets may get transmitted earlier.
*/
void reschedule();
private:
struct PendingTransmission : public Transmission
{
PendingTransmission(Clock::time_point expiry, const DataRequest& request, std::unique_ptr<ChunkPacket> packet) :
expiry(expiry), request(request), packet(std::move(packet)) {}
Clock::time_point expiry;
DataRequest request;
std::unique_ptr<ChunkPacket> packet;
Profile profile() const override { return request.dcc_profile; }
const access::DataRateG5* data_rate() const override { return &access::G5_6Mbps; }
std::size_t body_length() const override { return packet ? packet->size() : 0; }
};
using Queue = std::list<PendingTransmission>;
void enqueue(const DataRequest&, std::unique_ptr<ChunkPacket>);
boost::optional<PendingTransmission> dequeue();
void transmit(const DataRequest&, std::unique_ptr<ChunkPacket>);
bool transmit_immediately(const Transmission&) const;
void drop_expired();
bool empty() const;
void trigger();
void schedule_trigger(const Transmission&);
PendingTransmission* next_transmission();
Queue* next_queue();
Runtime& m_runtime;
TransmitRateControl& m_trc;
access::Interface& m_access;
std::map<access::AccessCategory, Queue, std::greater<access::AccessCategory>> m_queues;
std::size_t m_queue_length;
PacketDropHook m_packet_drop_hook;
PacketTransmitHook m_packet_transmit_hook;
};
} // namespace dcc
} // namespace vanetza
#endif /* FLOW_CONTROL_HPP_PG7RKD8V */
@@ -0,0 +1,181 @@
#include "channel_load.hpp"
#include "fully_meshed_state_machine.hpp"
#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <limits>
namespace vanetza
{
namespace dcc
{
static constexpr std::size_t N_samples_up = std::chrono::seconds(1) / NDL_minDccSampling;
static constexpr std::size_t N_samples_down = std::chrono::seconds(5) / NDL_minDccSampling;
static constexpr double NDL_minChannelLoad = 0.19;
static constexpr double NDL_maxChannelLoad = 0.59;
Clock::duration Relaxed::transmission_interval() const
{
return std::chrono::milliseconds(60);
}
const char* Relaxed::name() const
{
return "Relaxed";
}
Clock::duration Restrictive::transmission_interval() const
{
return std::chrono::milliseconds(460);
}
const char* Restrictive::name() const
{
return "Restrictive";
}
const std::size_t Active::sc_substates = 5;
Active::Active() : m_substate(0)
{
}
void Active::update(double min_cl, double max_cl)
{
assert(min_cl <= max_cl);
static const std::array<double, sc_substates> channel_loads {{
0.27, 0.35, 0.43, 0.51, 0.59
}};
auto state_up_it = std::upper_bound(channel_loads.begin(), channel_loads.end(), min_cl);
auto state_up = std::distance(channel_loads.begin(), state_up_it);
auto state_down_it = std::upper_bound(channel_loads.begin(), channel_loads.end(), max_cl);
auto state_down = std::distance(channel_loads.begin(), state_down_it);
m_substate = std::max(state_up, state_down);
m_substate = std::min(sc_substates - 1, m_substate);
assert(m_substate < sc_substates);
}
Clock::duration Active::transmission_interval() const
{
static const std::array<Clock::duration, sc_substates> tx_intervals {{
std::chrono::milliseconds(100),
std::chrono::milliseconds(180),
std::chrono::milliseconds(260),
std::chrono::milliseconds(340),
std::chrono::milliseconds(420),
}};
const std::size_t index = std::min(tx_intervals.size() - 1, m_substate);
return tx_intervals[index];
}
const char* Active::name() const
{
static const std::array<const char*, sc_substates> names {{
"Active 1",
"Active 2",
"Active 3",
"Active 4",
"Active 5"
}};
assert(m_substate < sc_substates);
return names[m_substate];
}
FullyMeshedStateMachine::FullyMeshedStateMachine() :
m_state(&m_relaxed),
m_channel_loads(std::max(N_samples_up, N_samples_down))
{
}
FullyMeshedStateMachine::~FullyMeshedStateMachine()
{
}
void FullyMeshedStateMachine::update(ChannelLoad cl)
{
m_channel_loads.push_front(cl);
if (m_state == &m_relaxed) {
if (min_channel_load() >= NDL_minChannelLoad) {
m_state = &m_active;
m_active.update(min_channel_load(), max_channel_load());
}
} else if (m_state == &m_restrictive) {
if (max_channel_load() < NDL_maxChannelLoad) {
m_state = &m_active;
m_active.update(min_channel_load(), max_channel_load());
}
} else {
if (max_channel_load() < NDL_minChannelLoad) {
m_state = &m_relaxed;
} else if (min_channel_load() >= NDL_maxChannelLoad) {
m_state = &m_restrictive;
} else {
m_state = &m_active;
m_active.update(min_channel_load(), max_channel_load());
}
}
}
double FullyMeshedStateMachine::message_rate() const
{
std::chrono::duration<double> one_sec = std::chrono::seconds(1);
return one_sec / transmission_interval();
}
Clock::duration FullyMeshedStateMachine::transmission_interval() const
{
return m_state->transmission_interval();
}
const State& FullyMeshedStateMachine::state() const
{
assert(m_state != nullptr);
return *m_state;
}
double FullyMeshedStateMachine::min_channel_load() const
{
assert(N_samples_up > 0);
double min_cl = std::numeric_limits<double>::infinity();
std::size_t sample_cnt = 0;
for (auto sample : m_channel_loads) {
if (sample_cnt >= N_samples_up) {
break;
} else if (sample.value() < min_cl) {
min_cl = sample.value();
}
++sample_cnt;
}
return std::isinf(min_cl) ? 0.0 : min_cl;
}
double FullyMeshedStateMachine::max_channel_load() const
{
assert(N_samples_down > 0);
double max_cl = 0.0;
std::size_t sample_cnt = 0;
for (auto sample : m_channel_loads) {
if (sample_cnt >= N_samples_down) {
break;
} else if (sample.value() > max_cl) {
max_cl = sample.value();
}
++sample_cnt;
}
return max_cl;
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,105 @@
#ifndef FULLY_MESHED_STATE_MACHINE_HPP_YPE958OH
#define FULLY_MESHED_STATE_MACHINE_HPP_YPE958OH
#include <vanetza/common/clock.hpp>
#include <vanetza/dcc/channel_load.hpp>
#include <vanetza/dcc/state_machine.hpp>
#include <boost/circular_buffer.hpp>
#include <boost/optional.hpp>
namespace vanetza
{
namespace dcc
{
// constants
static constexpr Clock::duration NDL_minDccSampling = std::chrono::milliseconds(100);
class State
{
public:
virtual Clock::duration transmission_interval() const = 0;
virtual const char* name() const = 0;
virtual ~State() {}
};
class Relaxed : public State
{
public:
Clock::duration transmission_interval() const override;
const char* name() const override;
};
class Active : public State
{
public:
Active();
void update(double min, double max);
Clock::duration transmission_interval() const override;
const char* name() const override;
private:
static const std::size_t sc_substates;
std::size_t m_substate;
};
class Restrictive : public State
{
public:
Clock::duration transmission_interval() const override;
const char* name() const override;
};
/**
* Fully meshed TRC state machine as per TS 102 687 v1.1.1
*
* States are modelled according to C2C-CC DCC Whitepaper / BSP v1.2
* State transitions are deferred internally for ramping up and cooling down over time.
*/
class FullyMeshedStateMachine : public StateMachine
{
public:
FullyMeshedStateMachine();
~FullyMeshedStateMachine();
/**
* Notify state machine about current channel load.
* This method expects to be called in regular intervals
* of NDL_minDccSampling length.
*/
void update(ChannelLoad channel_load);
/**
* Get currently allowed maximum message rate depending on state
* \return messages per second
*/
double message_rate() const;
/**
* Get advised transmission interval between consecutive messages
* \return message transmission interval
*/
Clock::duration transmission_interval() const;
/**
* Get state machine's active state
*/
const State& state() const;
private:
double max_channel_load() const;
double min_channel_load() const;
Relaxed m_relaxed;
Active m_active;
Restrictive m_restrictive;
State* m_state;
boost::circular_buffer<ChannelLoad> m_channel_loads;
};
} // namespace dcc
} // namespace vanetza
#endif /* FULLY_MESHED_STATE_MACHINE_HPP_YPE958OH */
@@ -0,0 +1,66 @@
#include "gradual_state_machine.hpp"
#include <boost/format.hpp>
#include <iterator>
namespace vanetza
{
namespace dcc
{
GradualStateMachine::GradualStateMachine(const std::set<State>& states) :
m_states(states), m_current(m_states.begin())
{
repair();
}
GradualStateMachine::GradualStateMachine(std::set<State>&& states) :
m_states(std::move(states)), m_current(m_states.begin())
{
repair();
}
void GradualStateMachine::update(ChannelLoad cbr)
{
static_assert(std::is_base_of<std::bidirectional_iterator_tag,
std::iterator_traits<StateContainer::const_iterator>::iterator_category>::value,
"State transitions require bidirectional iterators");
if (cbr < m_current->lower_limit) {
if (m_current != m_states.begin()) {
std::advance(m_current, -1);
}
} else {
StateContainer::const_iterator up = std::next(m_current);
if (up != m_states.end() && cbr >= up->lower_limit) {
m_current = up;
}
}
}
Clock::duration GradualStateMachine::transmission_interval() const
{
return m_current->off_time;
}
std::string GradualStateMachine::state() const
{
if (m_current == m_states.begin()) {
return "Relaxed";
} else if (m_current == std::prev(m_states.end())) {
return "Restrictive";
} else {
static const boost::format fmt("Active %1%");
return (boost::format(fmt) % std::distance(m_states.begin(), m_current)).str();
}
}
void GradualStateMachine::repair()
{
if (m_states.empty()) {
m_states.emplace(ChannelLoad(0.0), Clock::duration::zero());
m_current = m_states.begin();
}
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,77 @@
#ifndef GRADUAL_STATE_MACHINE_HPP_CGPVG4CS
#define GRADUAL_STATE_MACHINE_HPP_CGPVG4CS
#include <vanetza/common/clock.hpp>
#include <vanetza/dcc/channel_load.hpp>
#include <vanetza/dcc/state_machine.hpp>
#include <set>
#include <string>
namespace vanetza
{
namespace dcc
{
/**
* Reactive Transmit Rate Control (TRC) based on a state machine.
*
* This implementation complies with ETSI TS 102 687 v1.2.1,
* i.e. transitions can only happen gradually between neighbouring states.
* No ramping up or cooling down timing behaviour exists (not specified anymore).
*/
class GradualStateMachine : public StateMachine
{
public:
struct State
{
constexpr State(ChannelLoad limit, Clock::duration off_time) :
lower_limit(limit), off_time(off_time) {}
ChannelLoad lower_limit;
Clock::duration off_time;
bool operator<(const State& other) const { return lower_limit < other.lower_limit; }
};
using StateContainer = std::set<State>;
GradualStateMachine(const StateContainer&);
GradualStateMachine(StateContainer&&);
void update(ChannelLoad) override;
Clock::duration transmission_interval() const override;
std::string state() const;
private:
void repair();
StateContainer m_states;
StateContainer::const_iterator m_current;
};
/**
* CBR mapping as per TS 102 687 V1.2.1 Table A.1 (max T_on = 1ms)
*/
static const GradualStateMachine::StateContainer etsiStates1ms = {
{ ChannelLoad(0.00), std::chrono::milliseconds(100) },
{ ChannelLoad(0.30), std::chrono::milliseconds(200) },
{ ChannelLoad(0.40), std::chrono::milliseconds(400) },
{ ChannelLoad(0.50), std::chrono::milliseconds(500) },
{ ChannelLoad(0.60), std::chrono::milliseconds(1000) }
};
/**
* CBR mapping as per TS 102 686 V1.2.1 Table A.2 (max T_on = 500 us)
*/
static const GradualStateMachine::StateContainer etsiStates500us = {
{ ChannelLoad(0.00), std::chrono::milliseconds(50) },
{ ChannelLoad(0.30), std::chrono::milliseconds(100) },
{ ChannelLoad(0.40), std::chrono::milliseconds(200) },
{ ChannelLoad(0.50), std::chrono::milliseconds(250) },
{ ChannelLoad(0.65), std::chrono::milliseconds(1000) }
};
} // namespace dcc
} // namespace vanetza
#endif /* GRADUAL_STATE_MACHINE_HPP_CGPVG4CS */
@@ -0,0 +1,19 @@
#include "hooked_channel_probe_processor.hpp"
namespace vanetza
{
namespace dcc
{
HookedChannelProbeProcessor::HookedChannelProbeProcessor() :
on_indication(m_indication_hook)
{
}
void HookedChannelProbeProcessor::indicate(ChannelLoad cl)
{
m_indication_hook(cl);
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,31 @@
#ifndef HOOKED_CHANNEL_PROBE_PROCESSOR_HPP_M1O7VHKS
#define HOOKED_CHANNEL_PROBE_PROCESSOR_HPP_M1O7VHKS
#include <vanetza/common/hook.hpp>
#include <vanetza/dcc/channel_probe_processor.hpp>
namespace vanetza
{
namespace dcc
{
/**
* Implementation of ChannelProbeProcessor invoking hook on indication
*/
class HookedChannelProbeProcessor : public ChannelProbeProcessor
{
public:
HookedChannelProbeProcessor();
void indicate(ChannelLoad) override;
HookRegistry<ChannelLoad> on_indication;
private:
Hook<ChannelLoad> m_indication_hook;
};
} // namespace dcc
} // namespace vanetza
#endif /* HOOKED_CHANNEL_PROBE_PROCESSOR_HPP_M1O7VHKS */
@@ -0,0 +1,41 @@
#ifndef INTERFACE_HPP_4SUUTA6X
#define INTERFACE_HPP_4SUUTA6X
#include <memory>
namespace vanetza
{
// forward declarations
class ChunkPacket;
namespace dcc
{
// forward declarations
struct DataRequest;
/**
* DCC_access interface for data request from upper layers
*/
class RequestInterface
{
public:
virtual void request(const DataRequest&, std::unique_ptr<ChunkPacket>) = 0;
virtual ~RequestInterface() = default;
};
/**
* Null implemenation of DCC data request interface
*/
class NullRequestInterface : public RequestInterface
{
public:
void request(const DataRequest&, std::unique_ptr<ChunkPacket>) override {}
};
} // namespace dcc
} // namespace vanetza
#endif /* INTERFACE_HPP_4SUUTA6X */
@@ -0,0 +1,100 @@
#include "limeric.hpp"
#include <vanetza/common/runtime.hpp>
#include <cassert>
#include <cmath>
#include <numeric>
namespace vanetza
{
namespace dcc
{
static const Limeric::Parameters limericDefaultParams;
Limeric::Limeric(Runtime& rt) : Limeric(rt, limericDefaultParams)
{
}
Limeric::Limeric(Runtime& rt, const Parameters& params) :
on_duty_cycle_change(m_duty_cycle_change), m_runtime(rt), m_params(params),
m_duty_cycle(mean(params.delta_max, params.delta_min)), m_cbr(2)
{
assert(m_cbr.empty());
schedule();
}
Limeric::~Limeric()
{
m_runtime.cancel(this);
}
ChannelLoad Limeric::average_cbr() const
{
if (m_cbr.full()) {
return 0.5 * mean(m_cbr.begin(), m_cbr.end()) + 0.5 * m_channel_load;
} else {
return m_channel_load;
}
}
void Limeric::update_cbr(ChannelLoad cbr)
{
const bool full = m_cbr.full();
m_cbr.push_back(cbr);
if (!full) {
m_channel_load = mean(m_cbr.begin(), m_cbr.end());
}
}
UnitInterval Limeric::calculate_duty_cycle() const
{
const double cbr_delta = m_params.cbr_target.value() - m_channel_load.value();
double delta_offset = 0.0;
if (cbr_delta > 0.0) {
delta_offset = std::min(m_params.beta.value() * cbr_delta, m_params.g_plus_max);
} else {
delta_offset = std::max(m_params.beta.value() * cbr_delta, m_params.g_minus_max);
}
UnitInterval delta = m_params.alpha.complement() * m_duty_cycle + delta_offset;
delta = std::min(std::max(delta, m_params.delta_min), m_params.delta_max);
if (m_dual_alpha) {
if (m_duty_cycle - delta > m_dual_alpha->threshold) {
delta = m_dual_alpha->alternate_alpha.complement() * m_duty_cycle + delta_offset;
delta = std::min(std::max(delta, m_params.delta_min), m_params.delta_max);
}
}
return delta;
}
void Limeric::calculate(Clock::time_point tp)
{
m_channel_load = average_cbr();
m_duty_cycle = calculate_duty_cycle(); // uses m_channel_load
m_duty_cycle_change(this, tp);
schedule();
}
void Limeric::schedule()
{
// schedule for next possible modulo 2 * cbr_interval (usually 200ms) time point
const Clock::duration scheduling_interval = 2 * m_params.cbr_interval;
Clock::time_point tp = m_runtime.now() + scheduling_interval;
const Clock::duration scheduling_bias = tp.time_since_epoch() % scheduling_interval;
if (scheduling_bias > m_params.cbr_interval) {
tp += scheduling_interval - scheduling_bias;
} else if (scheduling_bias > Clock::duration::zero()) {
tp -= scheduling_bias;
}
m_runtime.schedule(tp, [this](Clock::time_point tp) {
this->calculate(tp);
});
}
void Limeric::configure_dual_alpha(const boost::optional<DualAlphaParameters>& params)
{
m_dual_alpha = params;
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,110 @@
#ifndef LIMERIC_HPP_OPCJEHBN
#define LIMERIC_HPP_OPCJEHBN
#include <vanetza/common/clock.hpp>
#include <vanetza/common/hook.hpp>
#include <vanetza/common/unit_interval.hpp>
#include <vanetza/dcc/channel_load.hpp>
#include <vanetza/dcc/duty_cycle_permit.hpp>
#include <boost/circular_buffer.hpp>
#include <boost/optional/optional.hpp>
#include <chrono>
namespace vanetza
{
// forward declaration
class Runtime;
namespace dcc
{
/**
* LIMERIC adapted to ETSI ITS
*
* This implementation follows TS 102 687 v1.2.1 section 5.4
* Optionally, the dual-alpha convergence proposed in [1] can be enabled.
*
* [1] Ignacio Soto, Oscar Amador, Manuel Uruena, Maria Calderon
* "Strengths and Weaknesses of the ETSI Adaptive DCC Algorithm: A Proposal for Improvement"
* DOI: 10.1109/LCOMM.2019.2906178
*/
class Limeric : public DutyCyclePermit
{
public:
/**
* Limeric paremeters as given by TS 102 687 v1.2.1, Table 3
*/
struct Parameters
{
UnitInterval alpha { 0.016 }; /*< also named alpha_low if dual-alpha is enabled */
UnitInterval beta { 0.0012 };
UnitInterval delta_max { 0.03 }; /*< upper bound permitted duty cycle */
UnitInterval delta_min { 0.0006 }; /*< lower bound permitted duty cycle */
double g_plus_max = 0.0005;
double g_minus_max = -0.00025;
ChannelLoad cbr_target { 0.68 };
Clock::duration cbr_interval = std::chrono::milliseconds(100); /*< algorithm is scheduled every second interval */
};
struct DualAlphaParameters
{
UnitInterval alternate_alpha { 0.1 }; /*< called alpha_high in [1] */
UnitInterval threshold { 0.00001 }; /* heuristically determined threshold for switching between alphas */
};
Limeric(Runtime&);
Limeric(Runtime&, const Parameters&);
~Limeric();
/**
* Report new channel load measurement
* \param cl channel load measurement
*/
void update_cbr(ChannelLoad);
/**
* Get current averaged CBR.
* \note The result incorporates previous measurements as well as the averaged CBR during last periodic update.
* \return averaged CBR
*/
ChannelLoad average_cbr() const;
/**
* Get permitted duty cycle as calculated by the last periodic update
* \return permitted duty cycle
*/
UnitInterval permitted_duty_cycle() const override { return m_duty_cycle; }
/**
* Called every time the permitted duty cycle is updated
* \param this instance itself
* \param time point for which algorithm update has been scheduled
*/
HookRegistry<const Limeric*, Clock::time_point> on_duty_cycle_change;
/**
* Configure dual-alpha convergence
* \param params dual-alpha parameters, boost::none disables dual-alpha convergence
*/
void configure_dual_alpha(const boost::optional<DualAlphaParameters>& params);
private:
void calculate(Clock::time_point);
void schedule();
UnitInterval calculate_duty_cycle() const;
Runtime& m_runtime;
Parameters m_params;
boost::optional<DualAlphaParameters> m_dual_alpha;
ChannelLoad m_channel_load; /*< moving average channel load */
UnitInterval m_duty_cycle;
boost::circular_buffer<ChannelLoad> m_cbr;
Hook<const Limeric*, Clock::time_point> m_duty_cycle_change;
};
} // namespace dcc
} // namespace vanetza
#endif /* LIMERIC_HPP_OPCJEHBN */
@@ -0,0 +1,82 @@
#include "duty_cycle_permit.hpp"
#include "limeric_budget.hpp"
#include <vanetza/common/runtime.hpp>
#include <chrono>
#include <cmath>
namespace vanetza
{
namespace dcc
{
namespace
{
constexpr Clock::duration min_interval = std::chrono::milliseconds(25);
constexpr Clock::duration max_interval = std::chrono::seconds(1);
} // namespace
LimericBudget::LimericBudget(const DutyCyclePermit& dcp, const Runtime& rt) :
m_duty_cycle_permit(dcp), m_runtime(rt),
m_interval(min_interval), m_tx_start(Clock::time_point::min()),
m_tx_on(Clock::duration::zero())
{
update();
}
Clock::duration LimericBudget::delay()
{
Clock::duration delay = Clock::duration::max();
if (m_runtime.now() >= m_tx_start + m_interval) {
delay = Clock::duration::zero();
} else {
delay = m_tx_start + m_interval - m_runtime.now();
}
return delay;
}
Clock::duration LimericBudget::interval()
{
return m_interval;
}
void LimericBudget::notify(Clock::duration tx_on)
{
m_tx_start = m_runtime.now();
m_tx_on = tx_on;
using std::chrono::duration_cast;
const auto duty_cycle = m_duty_cycle_permit.permitted_duty_cycle();
const auto interval = duration_cast<Clock::duration>(tx_on / duty_cycle.value());
m_interval = clamp_interval(interval);
}
void LimericBudget::update()
{
using std::chrono::duration_cast;
using FloatingPointDuration = std::chrono::duration<double, Clock::period>;
const FloatingPointDuration delay = m_tx_start + m_interval - m_runtime.now();
const double duty_cycle = m_duty_cycle_permit.permitted_duty_cycle().value();
if (duty_cycle > 0.0) {
if (delay.count() > 0.0) {
// Apply equation B.2 of TS 102 687 v1.2.1 if gate is closed at the moment
const FloatingPointDuration interval = (m_tx_on / duty_cycle) * (delay / m_interval);
m_interval = clamp_interval(duration_cast<Clock::duration>(interval) + m_runtime.now() - m_tx_start);
} else {
// use equation B.1 otherwise
const FloatingPointDuration interval = m_tx_on / duty_cycle;
m_interval = clamp_interval(duration_cast<Clock::duration>(interval));
}
} else {
// bail out with maximum interval if duty cycle is not positive
m_interval = max_interval;
}
}
Clock::duration LimericBudget::clamp_interval(Clock::duration interval) const
{
return std::min(std::max(interval, min_interval), max_interval);
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,67 @@
#ifndef LIMERIC_BUDGET_HPP_RQYL16AG
#define LIMERIC_BUDGET_HPP_RQYL16AG
#include <vanetza/common/clock.hpp>
namespace vanetza
{
// forward declaration
class Runtime;
namespace dcc
{
// forward declaration
class DutyCyclePermit;
/**
* LimericBudget models Annex B of TS 102 687 v1.2.1, i.e.
* packet handling to meet the channel occupancy limit
*/
class LimericBudget
{
public:
LimericBudget(const DutyCyclePermit&, const Runtime&);
/**
* Get delay until next transmission is allowed
* \return remaining transmission delay
*/
Clock::duration delay();
/**
* Get current interval between transmissions
* \return transmission interval
*/
Clock::duration interval();
/**
* Notify budget about transmission activity
* \param tx_on over-the-air duration of transmission
*/
void notify(Clock::duration tx_on);
/**
* Recalculate current transmission interval.
*
* Transmission interval is derived from Limeric's current permitted duty cycle.
* Hence, this method should be called whenever Limeric changes its duty cycle.
*/
void update();
private:
Clock::duration clamp_interval(Clock::duration) const;
const DutyCyclePermit& m_duty_cycle_permit;
const Runtime& m_runtime;
Clock::duration m_interval;
Clock::time_point m_tx_start;
Clock::duration m_tx_on;
};
} // namespace dcc
} // namespace vanetza
#endif /* LIMERIC_BUDGET_HPP_RQYL16AG */
@@ -0,0 +1,35 @@
#include "limeric_transmit_rate_control.hpp"
#include <vanetza/common/runtime.hpp>
namespace vanetza
{
namespace dcc
{
LimericTransmitRateControl::LimericTransmitRateControl(const Runtime& rt, const Limeric& limeric) :
m_budget(limeric, rt)
{
}
Clock::duration LimericTransmitRateControl::delay(const Transmission&)
{
return m_budget.delay();
}
Clock::duration LimericTransmitRateControl::interval(const Transmission&)
{
return m_budget.interval();
}
void LimericTransmitRateControl::notify(const Transmission& transmission)
{
m_budget.notify(transmission.channel_occupancy());
}
void LimericTransmitRateControl::update()
{
m_budget.update();
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,43 @@
#ifndef LIMERIC_TRANSMIT_RATE_CONTROL_HPP_RY1TIBMJ
#define LIMERIC_TRANSMIT_RATE_CONTROL_HPP_RY1TIBMJ
#include <vanetza/dcc/limeric.hpp>
#include <vanetza/dcc/limeric_budget.hpp>
#include <vanetza/dcc/transmit_rate_control.hpp>
namespace vanetza
{
// forward declaration
class Runtime;
namespace dcc
{
/**
* Transmit Rate Control implementation based on Limeric algorithm
*/
class LimericTransmitRateControl : public TransmitRateControl
{
public:
LimericTransmitRateControl(const Runtime&, const Limeric&);
Clock::duration delay(const Transmission&) override;
Clock::duration interval(const Transmission&) override;
void notify(const Transmission&) override;
/**
* Update TRC limits.
* Call this method whenever Limeric updates its duty cycle.
*/
void update();
private:
LimericBudget m_budget;
};
} // namespace dcc
} // namespace vanetza
#endif /* LIMERIC_TRANSMIT_RATE_CONTROL_HPP_RY1TIBMJ */
@@ -0,0 +1,36 @@
#include "mapping.hpp"
#include <stdexcept>
namespace vanetza
{
namespace dcc
{
access::AccessCategory map_profile_onto_ac(Profile dp_id)
{
access::AccessCategory ac = access::AccessCategory::BE;
switch (dp_id)
{
case Profile::DP0:
ac = access::AccessCategory::VO;
break;
case Profile::DP1:
ac = access::AccessCategory::VI;
break;
case Profile::DP2:
ac = access::AccessCategory::BE;
break;
case Profile::DP3:
ac = access::AccessCategory::BK;
break;
default:
throw std::invalid_argument("Invalid DCC Profile ID");
break;
}
return ac;
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,23 @@
#ifndef MAPPING_HPP_MZ2RU7VX
#define MAPPING_HPP_MZ2RU7VX
#include <vanetza/access/access_category.hpp>
#include <vanetza/dcc/profile.hpp>
namespace vanetza
{
namespace dcc
{
/**
* Map DCC Profile to EDCA access category
* \param profile DCC Profile ID
* \return mapped access category
*/
access::AccessCategory map_profile_onto_ac(Profile);
} // namespace dcc
} // namespace vanetza
#endif /* MAPPING_HPP_MZ2RU7VX */
@@ -0,0 +1,21 @@
#ifndef PROFILE_HPP_V9RMCGL2
#define PROFILE_HPP_V9RMCGL2
namespace vanetza
{
namespace dcc
{
enum class Profile : unsigned
{
DP0,
DP1,
DP2,
DP3
};
} // namespace dcc
} // namespace vanetza
#endif /* PROFILE_HPP_V9RMCGL2 */
@@ -0,0 +1,30 @@
#include "single_reactive_transmit_rate_control.hpp"
#include "state_machine.hpp"
namespace vanetza
{
namespace dcc
{
SingleReactiveTransmitRateControl::SingleReactiveTransmitRateControl(const StateMachine& fsm, const Runtime& rt) :
m_fsm(fsm), m_fsm_budget(fsm, rt)
{
}
Clock::duration SingleReactiveTransmitRateControl::interval(const Transmission&)
{
return m_fsm.transmission_interval();
}
Clock::duration SingleReactiveTransmitRateControl::delay(const Transmission&)
{
return m_fsm_budget.delay();
}
void SingleReactiveTransmitRateControl::notify(const Transmission&)
{
m_fsm_budget.notify();
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,38 @@
#ifndef SINGLE_REACTIVE_TRANSMIT_RATE_CONTROL_HPP_2GPPO79B
#define SINGLE_REACTIVE_TRANSMIT_RATE_CONTROL_HPP_2GPPO79B
#include <vanetza/dcc/state_machine_budget.hpp>
#include <vanetza/dcc/transmit_rate_control.hpp>
namespace vanetza
{
// forward declarations
class Runtime;
namespace dcc { class StateMachine; }
namespace dcc
{
/**
* Transmit Rate Control using a single reactive state machine for all messages
*/
class SingleReactiveTransmitRateControl : public TransmitRateControl
{
public:
SingleReactiveTransmitRateControl(const StateMachine&, const Runtime&);
Clock::duration delay(const Transmission&) override;
Clock::duration interval(const Transmission&) override;
void notify(const Transmission&) override;
private:
const StateMachine& m_fsm;
StateMachineBudget m_fsm_budget;
};
} // namespace dcc
} // namespace vanetza
#endif /* SINGLE_REACTIVE_TRANSMIT_RATE_CONTROL_HPP_2GPPO79B */
@@ -0,0 +1,25 @@
#include "smoothing_channel_probe_processor.hpp"
namespace vanetza
{
namespace dcc
{
SmoothingChannelProbeProcessor::SmoothingChannelProbeProcessor(UnitInterval alpha) :
m_alpha(alpha)
{
}
void SmoothingChannelProbeProcessor::indicate(ChannelLoad cl)
{
m_channel_load = m_alpha * cl + m_alpha.complement() * m_channel_load;
HookedChannelProbeProcessor::indicate(m_channel_load);
}
ChannelLoad SmoothingChannelProbeProcessor::channel_load() const
{
return m_channel_load;
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,49 @@
#ifndef SMOOTHING_CHANNEL_PROBE_PROCESSOR_HPP_EIP1WUDK
#define SMOOTHING_CHANNEL_PROBE_PROCESSOR_HPP_EIP1WUDK
#include <vanetza/common/unit_interval.hpp>
#include <vanetza/dcc/channel_load.hpp>
#include <vanetza/dcc/hooked_channel_probe_processor.hpp>
#include <functional>
namespace vanetza
{
namespace dcc
{
/**
* Smooth local channel load measurements as per
* C2C-CC Whitepaper on DCC for Day One (Version 1.0 from 2013)
* and Basic System Profile (RS_BSP_240 in Version 1.3)
*/
class SmoothingChannelProbeProcessor : public HookedChannelProbeProcessor
{
public:
/**
* Initialize ChannelProbeProcessor with smoothing behaviour.
* \param alpha smoothing factor (influence of new raw measurement)
*/
SmoothingChannelProbeProcessor(UnitInterval alpha = UnitInterval(0.5));
/**
* Feed new local channel load measurement into smoothing algorithm.
* Side effect: Update function is called afterwards with smoothed channel load.
* \param cl raw local channel load
*/
void indicate(ChannelLoad cl) override;
/**
* Get smoothed channel load value
*/
ChannelLoad channel_load() const;
private:
UnitInterval m_alpha;
ChannelLoad m_channel_load;
};
} // namespace dcc
} // namespace vanetza
#endif /* SMOOTH_CHANNEL_PROBE_PROCESSOR_HPP_EIP1WUDK */
@@ -0,0 +1,37 @@
#ifndef STATE_MACHINE_HPP_0MHYOQU7
#define STATE_MACHINE_HPP_0MHYOQU7
#include <vanetza/dcc/channel_load.hpp>
#include <vanetza/common/clock.hpp>
namespace vanetza
{
namespace dcc
{
/**
* State machine interface used for Transmit Rate Control
*/
class StateMachine
{
public:
/**
* Trigger state transition by updated channel load
* \param cl new channel load measurement
*/
virtual void update(ChannelLoad cl) = 0;
/**
* Get current transmission interval
* \return duration between two transmissions
*/
virtual Clock::duration transmission_interval() const = 0;
virtual ~StateMachine() = default;
};
} // namespace dcc
} // namespace vanetza
#endif /* STATE_MACHINE_HPP_0MHYOQU7 */
@@ -0,0 +1,40 @@
#include <vanetza/common/runtime.hpp>
#include <vanetza/dcc/state_machine.hpp>
#include <vanetza/dcc/state_machine_budget.hpp>
namespace vanetza
{
namespace dcc
{
StateMachineBudget::StateMachineBudget(const StateMachine& fsm, const Runtime& rt) :
m_fsm(fsm), m_runtime(rt)
{
}
Clock::duration StateMachineBudget::delay()
{
Clock::duration delay = Clock::duration::max();
if (m_last_tx) {
const auto last_tx = m_last_tx.get();
const auto tx_interval = m_fsm.transmission_interval();
if (last_tx + tx_interval < m_runtime.now()) {
delay = Clock::duration::zero();
} else {
delay = last_tx + tx_interval - m_runtime.now();
}
} else {
delay = Clock::duration::zero();
}
return delay;
}
void StateMachineBudget::notify()
{
m_last_tx = m_runtime.now();
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,37 @@
#ifndef STATE_MACHINE_BUDGET_HPP_9AL8A2JG
#define STATE_MACHINE_BUDGET_HPP_9AL8A2JG
#include <vanetza/common/clock.hpp>
#include <boost/optional.hpp>
namespace vanetza
{
// forward declarations
class Runtime;
namespace dcc { class StateMachine; }
namespace dcc
{
/**
* StateMachineBudget: TRC restrictions as determined by a state machine
*/
class StateMachineBudget
{
public:
StateMachineBudget(const StateMachine&, const Runtime&);
Clock::duration delay();
void notify();
private:
const StateMachine& m_fsm;
const Runtime& m_runtime;
boost::optional<Clock::time_point> m_last_tx;
};
} // namespace dcc
} // namespace vanetza
#endif /* STATE_MACHINE_BUDGET_HPP_9AL8A2JG */
@@ -0,0 +1,15 @@
include(UseGTest)
configure_gtest_directory(LINK_LIBRARIES dcc)
add_gtest(BurstBudget burst_budget.cpp)
add_gtest(BurstyTransmitRateControl bursty_transmit_rate_control.cpp)
add_gtest(ChannelLoad channel_load.cpp)
add_gtest(FlowControl flow_control.cpp)
add_gtest(FullyMeshedStateMachine fully_meshed_state_machine.cpp)
add_gtest(GradualStateMachine gradual_state_machine.cpp)
add_gtest(Limeric limeric.cpp)
add_gtest(LimericBudget limeric_budget.cpp)
add_gtest(Mapping mapping.cpp)
add_gtest(SmoothingChannelProbeProcessor smoothing_channel_probe_processor.cpp)
add_gtest(StateMachineBudget state_machine_budget.cpp)
@@ -0,0 +1,63 @@
#include <gtest/gtest.h>
#include <vanetza/common/manual_runtime.hpp>
#include <vanetza/dcc/burst_budget.hpp>
using Runtime = vanetza::ManualRuntime;
using namespace vanetza::dcc;
static const vanetza::Clock::duration immediately = std::chrono::milliseconds(0);
TEST(BurstBudget, normal)
{
Runtime rt;
BurstBudget budget(rt);
// consume whole budget
for (unsigned i = 0; i < 20; ++i) {
rt.trigger(std::chrono::milliseconds(49));
EXPECT_EQ(immediately, budget.delay());
budget.notify();
}
// nothing left now
rt.trigger(std::chrono::milliseconds(20));
EXPECT_LT(std::chrono::seconds(9), budget.delay());
EXPECT_GT(std::chrono::seconds(10), budget.delay());
}
TEST(BurstBudget, too_many_messages)
{
Runtime rt;
BurstBudget budget(rt);
// consume whole budget immediately
for (unsigned i = 0; i < 20; ++i) {
EXPECT_EQ(immediately, budget.delay());
budget.notify();
}
// check if budget delay recovers gradually
EXPECT_EQ(std::chrono::seconds(10), budget.delay());
rt.trigger(std::chrono::seconds(5));
EXPECT_EQ(std::chrono::seconds(5), budget.delay());
rt.trigger(std::chrono::seconds(5));
EXPECT_EQ(immediately, budget.delay());
}
TEST(BurstBudget, too_long)
{
Runtime rt;
BurstBudget budget(rt);
// start burst with one consumption
EXPECT_EQ(immediately, budget.delay());
budget.notify();
// ensure we are still able to participate in burst
rt.trigger(std::chrono::milliseconds(990));
EXPECT_EQ(immediately, budget.delay());
// burst is over, we will have to wait for next one
rt.trigger(std::chrono::milliseconds(10));
EXPECT_EQ(std::chrono::seconds(9), budget.delay());
}
@@ -0,0 +1,86 @@
#include <gtest/gtest.h>
#include <vanetza/common/manual_runtime.hpp>
#include <vanetza/dcc/bursty_transmit_rate_control.hpp>
#include <vanetza/dcc/fully_meshed_state_machine.hpp>
using namespace std::chrono;
using namespace vanetza::dcc;
using vanetza::ManualRuntime;
static const vanetza::Clock::duration immediately = milliseconds(0);
static const TransmissionLite dp0 { Profile::DP0, 0 };
static const TransmissionLite dp1 { Profile::DP1, 0 };
static const TransmissionLite dp2 { Profile::DP2, 0 };
static const TransmissionLite dp3 { Profile::DP3, 0 };
class BurstyTransmitRateControlTest : public ::testing::Test
{
protected:
BurstyTransmitRateControlTest() :
runtime(vanetza::Clock::time_point { seconds(4711) }),
trc(fsm, runtime) {}
ManualRuntime runtime;
FullyMeshedStateMachine fsm;
BurstyTransmitRateControl trc;
};
TEST_F(BurstyTransmitRateControlTest, burst)
{
for (unsigned i = 0; i < 20; ++i) {
runtime.trigger(milliseconds(49));
EXPECT_EQ(immediately, trc.delay(dp0));
trc.notify(dp0);
}
runtime.trigger(milliseconds(20));
EXPECT_GT(seconds(10), trc.delay(dp0));
EXPECT_LT(seconds(9), trc.delay(dp0));
}
TEST_F(BurstyTransmitRateControlTest, regular)
{
const auto tx_int = milliseconds(60);
ASSERT_EQ(tx_int, fsm.transmission_interval());
EXPECT_EQ(immediately, trc.delay(dp1));
trc.notify(dp1);
EXPECT_EQ(tx_int, trc.delay(dp1));
runtime.trigger(milliseconds(50));
EXPECT_EQ(milliseconds(10), trc.delay(dp1));
EXPECT_EQ(milliseconds(10), trc.delay(dp2));
EXPECT_EQ(milliseconds(10), trc.delay(dp3));
runtime.trigger(milliseconds(20));
EXPECT_EQ(immediately, trc.delay(dp1));
EXPECT_EQ(immediately, trc.delay(dp2));
EXPECT_EQ(immediately, trc.delay(dp3));
}
TEST_F(BurstyTransmitRateControlTest, burst_regular_independence)
{
ASSERT_EQ(immediately, trc.delay(dp1));
// consume whole burst budget
for (unsigned i = 0; i < 20; ++i) {
trc.notify(dp0);
}
ASSERT_LT(immediately, trc.delay(dp0));
// can send regular budget messages nonetheless
EXPECT_EQ(immediately, trc.delay(dp3));
// recover burst budget
runtime.trigger(std::chrono::seconds(20));
ASSERT_EQ(immediately, trc.delay(dp0));
// use regular budget
EXPECT_EQ(immediately, trc.delay(dp2));
trc.notify(dp2);
EXPECT_LT(immediately, trc.delay(dp2));
// burst budget is not influenced
EXPECT_EQ(immediately, trc.delay(dp0));
}
@@ -0,0 +1,23 @@
#include <gtest/gtest.h>
#include <vanetza/dcc/channel_load.hpp>
using namespace vanetza::dcc;
TEST(ChannelLoad, ctor)
{
ChannelLoad cl1;
EXPECT_DOUBLE_EQ(0.0, cl1.value());
ChannelLoad cl2(30, 250);
EXPECT_DOUBLE_EQ(0.12, cl2.value());
ChannelLoad cl3(0, 0);
EXPECT_DOUBLE_EQ(0.0, cl3.value());
}
TEST(ChannelLoadRational, less)
{
EXPECT_LT(ChannelLoad(30, 100), ChannelLoad(31, 100));
EXPECT_LT(ChannelLoad(30, 100), ChannelLoad(8, 25));
EXPECT_LT(ChannelLoad(0,10), ChannelLoad(1, 2));
}
@@ -0,0 +1,235 @@
#include <gtest/gtest.h>
#include <vanetza/access/data_request.hpp>
#include <vanetza/access/interface.hpp>
#include <vanetza/common/manual_runtime.hpp>
#include <vanetza/dcc/flow_control.hpp>
#include <vanetza/dcc/transmit_rate_control.hpp>
#include <chrono>
using namespace vanetza;
using namespace vanetza::dcc;
using namespace std::chrono;
static const TransmissionLite dp0 { Profile::DP0, 0 };
static const TransmissionLite dp1 { Profile::DP1, 0 };
static const TransmissionLite dp2 { Profile::DP2, 0 };
static const TransmissionLite dp3 { Profile::DP3, 0 };
class FakeAccessInterface : public access::Interface
{
public:
void request(const access::DataRequest& req, std::unique_ptr<ChunkPacket> packet) override
{
last_request = req;
last_packet = std::move(packet);
++transmissions;
}
boost::optional<access::DataRequest> last_request;
std::unique_ptr<ChunkPacket> last_packet;
unsigned transmissions = 0;
};
class FakeTransmitRateControl : public TransmitRateControl
{
public:
FakeTransmitRateControl(const Runtime& rt) :
runtime(rt), trc_off(milliseconds(200)), last_notify(Clock::time_point::min()) {}
Clock::duration delay(const Transmission&) override
{
auto delay = runtime.now() - last_notify + trc_off;
return delay < Clock::duration::zero() ? Clock::duration::zero() : delay;
}
Clock::duration interval(const Transmission&) override { return trc_off; }
void notify(const Transmission&) override { last_notify = runtime.now(); }
const Runtime& runtime;
Clock::duration trc_off;
Clock::time_point last_notify;
};
class FlowControlTest : public testing::Test
{
protected:
FlowControlTest() :
runtime(), trc(runtime),
flow_control(runtime, trc, access)
{}
std::unique_ptr<ChunkPacket> create_packet(std::size_t length = 0)
{
std::unique_ptr<ChunkPacket> packet { new ChunkPacket() };
packet->layer(OsiLayer::Application) = ByteBuffer(length);
return packet;
}
MacAddress mac(char x)
{
return MacAddress { 0, 0, 0, 0, 0, static_cast<uint8_t>(x) };
}
ManualRuntime runtime;
FakeTransmitRateControl trc;
FakeAccessInterface access;
FlowControl flow_control;
};
TEST_F(FlowControlTest, immediate_transmission)
{
ASSERT_EQ(milliseconds(0), trc.delay(dp1));
ASSERT_FALSE(access.last_request);
DataRequest request;
request.dcc_profile = Profile::DP1;
flow_control.request(request, create_packet());
ASSERT_TRUE(!!access.last_request);
EXPECT_EQ(access::AccessCategory::VI, access.last_request->access_category);
EXPECT_EQ(trc.interval(dp2), trc.delay(dp2));
request.dcc_profile = Profile::DP2;
access.last_request = boost::none;
flow_control.request(request, create_packet());
EXPECT_FALSE(access.last_request);
// DP0 bursts are implemented by TRC not by FlowControl
EXPECT_EQ(trc.interval(dp0), trc.delay(dp0));
request.dcc_profile = Profile::DP0;
flow_control.request(request, create_packet());
EXPECT_FALSE(access.last_request);
}
TEST_F(FlowControlTest, queuing)
{
DataRequest request;
request.lifetime = hours(1); // expired lifetime shall be no concern here
trc.notify(dp1);
EXPECT_LT(Clock::duration::zero(), trc.delay(dp1));
EXPECT_LT(Clock::duration::zero(), trc.delay(dp2));
EXPECT_LT(Clock::duration::zero(), trc.delay(dp3));
request.destination = mac(1);
request.dcc_profile = Profile::DP1;
flow_control.request(request, create_packet());
request.destination = mac(2);
request.dcc_profile = Profile::DP3;
flow_control.request(request, create_packet());
request.destination = mac(3);
request.dcc_profile = Profile::DP2;
flow_control.request(request, create_packet());
runtime.trigger(trc.delay(dp1));
ASSERT_TRUE(!!access.last_request);
EXPECT_EQ(mac(1), access.last_request->destination_addr);
EXPECT_EQ(1, access.transmissions);
runtime.trigger(trc.delay(dp2) / 2);
EXPECT_EQ(1, access.transmissions);
runtime.trigger(trc.delay(dp2));
EXPECT_EQ(2, access.transmissions);
EXPECT_EQ(mac(3), access.last_request->destination_addr);
request.destination = mac(4);
request.dcc_profile = Profile::DP2;
flow_control.request(request, create_packet());
request.destination = mac(5);
request.dcc_profile = Profile::DP3;
flow_control.request(request, create_packet());
runtime.trigger(trc.delay(dp2));
EXPECT_EQ(3, access.transmissions);
EXPECT_EQ(mac(4), access.last_request->destination_addr);
runtime.trigger(trc.delay(dp3));
EXPECT_EQ(4, access.transmissions);
EXPECT_EQ(mac(2), access.last_request->destination_addr);
runtime.trigger(trc.delay(dp3));
EXPECT_EQ(5, access.transmissions);
EXPECT_EQ(mac(5), access.last_request->destination_addr);
// no future transmissions queued anymore
runtime.trigger(Clock::time_point::max());
EXPECT_EQ(5, access.transmissions);
}
TEST_F(FlowControlTest, drop_expired)
{
std::list<access::AccessCategory> drops;
flow_control.set_packet_drop_hook([&drops](access::AccessCategory ac, const ChunkPacket*) {
drops.push_back(ac);
});
trc.notify(dp3);
DataRequest request;
request.dcc_profile = Profile::DP3;
request.lifetime = trc.delay(dp3) - milliseconds(10);
flow_control.request(request, create_packet());
runtime.trigger(trc.delay(dp3) + milliseconds(10));
EXPECT_FALSE(access.last_request);
ASSERT_FALSE(drops.empty());
EXPECT_EQ(access::AccessCategory::BK, drops.back());
EXPECT_EQ(0, access.transmissions);
trc.notify(dp3);
auto delay = trc.delay(dp3);
EXPECT_NE(Clock::duration::zero(), delay);
request.lifetime = delay;
flow_control.request(request, create_packet());
request.lifetime = delay / 2;
flow_control.request(request, create_packet());
request.lifetime = 3 * delay / 2;
flow_control.request(request, create_packet());
request.lifetime = 2 * delay;
flow_control.request(request, create_packet());
request.lifetime = delay / 4;
flow_control.request(request, create_packet());
runtime.trigger(delay);
EXPECT_EQ(3, drops.size());
EXPECT_EQ(1, access.transmissions);
runtime.trigger(delay);
EXPECT_EQ(4, drops.size());
EXPECT_EQ(2, access.transmissions);
// all queues should be empty now, no future transmissions
runtime.trigger(Clock::time_point::max());
EXPECT_EQ(2, access.transmissions);
}
TEST_F(FlowControlTest, queue_length)
{
// set queue length limit (default is unlimited)
flow_control.queue_length(2);
// count drops
std::size_t drops = 0;
flow_control.set_packet_drop_hook([&drops](access::AccessCategory, const ChunkPacket*) { ++drops; });
DataRequest request;
request.dcc_profile = Profile::DP1;
request.lifetime = std::chrono::seconds(5);
// cause enqueuing of arriving DP1 packets
trc.notify(dp1);
ASSERT_LT(Clock::duration::zero(), trc.delay(dp1));
flow_control.request(request, create_packet(1));
flow_control.request(request, create_packet(2));
EXPECT_EQ(0, access.transmissions);
EXPECT_EQ(0, drops);
flow_control.request(request, create_packet(3));
EXPECT_EQ(0, access.transmissions);
EXPECT_EQ(1, drops);
runtime.trigger(trc.delay(dp1));
EXPECT_EQ(1, access.transmissions);
EXPECT_EQ(2, access.last_packet->size());
runtime.trigger(trc.delay(dp1));
EXPECT_EQ(2, access.transmissions);
EXPECT_EQ(3, access.last_packet->size());
EXPECT_EQ(1, drops);
}
@@ -0,0 +1,112 @@
#include <gtest/gtest.h>
#include <vanetza/dcc/fully_meshed_state_machine.hpp>
using std::chrono::milliseconds;
using namespace vanetza::dcc;
TEST(FullyMeshedStateMachine, ctor)
{
FullyMeshedStateMachine sm;
EXPECT_STREQ("Relaxed", sm.state().name());
EXPECT_EQ(milliseconds(60), sm.transmission_interval());
EXPECT_NEAR(16.66, sm.message_rate(), 0.01);
}
TEST(FullyMeshedStateMachine, ramp_up)
{
FullyMeshedStateMachine sm;
// keep below minChannelLoad at first: relaxed
sm.update(ChannelLoad(0.16));
EXPECT_STREQ("Relaxed", sm.state().name());
// now exceed minChannelLoad for 10 samples: active 1
for (unsigned i = 0; i < 9; ++i) {
sm.update(ChannelLoad(0.2));
EXPECT_STREQ("Relaxed", sm.state().name());
}
sm.update(ChannelLoad(0.2));
EXPECT_STREQ("Active 1", sm.state().name());
// now let's jump to active 3 directly
sm.update(ChannelLoad(0.4));
EXPECT_STREQ("Active 3", sm.state().name());
// jump to active 5
sm.update(ChannelLoad(0.55));
EXPECT_STREQ("Active 5", sm.state().name());
// ramp up to restrictive
for (unsigned i = 0; i < 9; ++i) {
sm.update(ChannelLoad(0.6));
EXPECT_STREQ("Active 5", sm.state().name());
}
sm.update(ChannelLoad(0.6));
EXPECT_STREQ("Restrictive", sm.state().name());
}
TEST(FullyMeshedStateMachine, ramp_down)
{
FullyMeshedStateMachine sm;
// fill up CL ring buffer for restrictive
for (unsigned i = 0; i < 10; ++i) {
sm.update(ChannelLoad(0.7));
}
ASSERT_STREQ("Restrictive", sm.state().name());
// insert 55 % CL for active 5 state (later on)
sm.update(ChannelLoad(0.55));
// cool down 48 of 50 samples to CL = 50% (active 4)
for (unsigned i = 0; i < 48; ++i) {
sm.update(ChannelLoad(0.5));
}
EXPECT_STREQ("Restrictive", sm.state().name());
// -> active 5 (one last 55 % CL sample)
sm.update(ChannelLoad(0.5));
EXPECT_STREQ("Active 5", sm.state().name());
// -> active 4
sm.update(ChannelLoad(0.5));
EXPECT_STREQ("Active 4", sm.state().name());
}
TEST(State, relaxed)
{
Relaxed relaxed;
EXPECT_STREQ("Relaxed", relaxed.name());
EXPECT_EQ(milliseconds(60), relaxed.transmission_interval());
}
TEST(State, active)
{
Active active;
EXPECT_STREQ("Active 1", active.name());
EXPECT_EQ(milliseconds(100), active.transmission_interval());
active.update(0.20, 0.36);
EXPECT_STREQ("Active 3", active.name());
EXPECT_EQ(milliseconds(260), active.transmission_interval());
active.update(0.51, 0.52);
EXPECT_STREQ("Active 5", active.name());
EXPECT_EQ(milliseconds(420), active.transmission_interval());
active.update(0.30, 0.44);
EXPECT_STREQ("Active 4", active.name());
EXPECT_EQ(milliseconds(340), active.transmission_interval());
active.update(0.20, 0.30);
EXPECT_STREQ("Active 2", active.name());
EXPECT_EQ(milliseconds(180), active.transmission_interval());
}
TEST(State, restrictive)
{
Restrictive restrictive;
EXPECT_STREQ("Restrictive", restrictive.name());
EXPECT_EQ(milliseconds(460), restrictive.transmission_interval());
}
@@ -0,0 +1,60 @@
#include <gtest/gtest.h>
#include <vanetza/dcc/gradual_state_machine.hpp>
#include <chrono>
using namespace vanetza::dcc;
using namespace std::chrono;
TEST(GradualStateMachine, initial_state)
{
GradualStateMachine fsm(etsiStates1ms);
EXPECT_EQ("Relaxed", fsm.state());
EXPECT_EQ(milliseconds(100), fsm.transmission_interval());
}
TEST(GradualStateMachine, transitions)
{
GradualStateMachine fsm(etsiStates1ms);
EXPECT_EQ("Relaxed", fsm.state());
// now ramp up to Active 3
fsm.update(ChannelLoad { 0.5 });
EXPECT_EQ("Active 1", fsm.state());
fsm.update(ChannelLoad { 0.5 });
EXPECT_EQ("Active 2", fsm.state());
fsm.update(ChannelLoad { 0.5 });
EXPECT_EQ("Active 3", fsm.state());
fsm.update(ChannelLoad { 0.5 });
EXPECT_EQ("Active 3", fsm.state());
// step down one
fsm.update(ChannelLoad { 0.495 });
EXPECT_EQ("Active 2", fsm.state());
// go up to Restrictive gradually
fsm.update(ChannelLoad { 0.55 });
EXPECT_EQ("Active 3", fsm.state());
fsm.update(ChannelLoad { 0.65 });
EXPECT_EQ("Restrictive", fsm.state());
EXPECT_EQ(milliseconds(1000), fsm.transmission_interval());
}
TEST(GradualStateMachine, empty_states)
{
GradualStateMachine fsm(GradualStateMachine::StateContainer {});
EXPECT_EQ("Relaxed", fsm.state());
EXPECT_EQ(seconds(0), fsm.transmission_interval());
}
TEST(GradualStateMachine, one_state)
{
GradualStateMachine fsm(GradualStateMachine::StateContainer {{ ChannelLoad(0.5), milliseconds(30) }});
EXPECT_EQ("Relaxed", fsm.state());
EXPECT_EQ(milliseconds(30), fsm.transmission_interval());
fsm.update(ChannelLoad { 0.0 });
EXPECT_EQ(milliseconds(30), fsm.transmission_interval());
fsm.update(ChannelLoad { 1.0 });
EXPECT_EQ(milliseconds(30), fsm.transmission_interval());
EXPECT_EQ("Relaxed", fsm.state());
}
@@ -0,0 +1,120 @@
#include <gtest/gtest.h>
#include <vanetza/common/manual_runtime.hpp>
#include <vanetza/dcc/limeric.hpp>
using namespace vanetza;
using namespace vanetza::dcc;
using std::chrono::milliseconds;
namespace vanetza {
void PrintTo(const UnitInterval& cl, std::ostream* os) { *os << cl.value(); }
}
class LimericTest : public ::testing::Test
{
public:
LimericTest() : runtime(Clock::time_point { milliseconds(567) }), limeric(runtime) {}
ManualRuntime runtime;
Limeric limeric;
};
TEST_F(LimericTest, init)
{
EXPECT_EQ(ChannelLoad { 0.0 }, limeric.average_cbr());
EXPECT_EQ(UnitInterval { 0.0153 }, limeric.permitted_duty_cycle());
}
TEST_F(LimericTest, average_cbr_only_measured)
{
limeric.update_cbr(ChannelLoad { 0.2 });
EXPECT_EQ(ChannelLoad { 0.2 }, limeric.average_cbr());
limeric.update_cbr(ChannelLoad { 0.4 });
EXPECT_EQ(ChannelLoad { 0.3 }, limeric.average_cbr());
// now internal buffer filled up, 0.3 is assumed to be "previous" average
limeric.update_cbr(ChannelLoad { 0.6 });
EXPECT_EQ(ChannelLoad { 0.4 }, limeric.average_cbr());
// previous average changes only at update cycle if buffer is full
limeric.update_cbr(ChannelLoad { 0.6 });
EXPECT_EQ(ChannelLoad { 0.45 }, limeric.average_cbr());
}
TEST_F(LimericTest, average_cbr_with_cycle)
{
limeric.update_cbr(ChannelLoad { 0.3 });
limeric.update_cbr(ChannelLoad { 0.4 });
EXPECT_EQ(ChannelLoad { 0.35 }, limeric.average_cbr());
runtime.trigger(milliseconds(200));
// internal average is set to 0.35 now
EXPECT_EQ(ChannelLoad { 0.35 }, limeric.average_cbr());
limeric.update_cbr(ChannelLoad { 0.2 });
EXPECT_EQ(ChannelLoad { 0.325}, limeric.average_cbr());
limeric.update_cbr(ChannelLoad { 0.1 });
EXPECT_EQ(ChannelLoad { 0.25 }, limeric.average_cbr());
limeric.update_cbr(ChannelLoad { 0.1 });
EXPECT_EQ(ChannelLoad { 0.225 }, limeric.average_cbr());
runtime.trigger(milliseconds(200));
// internal average is set to 0.225 now
EXPECT_EQ(ChannelLoad { 0.1625 }, limeric.average_cbr());
limeric.update_cbr(ChannelLoad { 0.3 });
limeric.update_cbr(ChannelLoad { 0.5 });
EXPECT_EQ(ChannelLoad { 0.3125 }, limeric.average_cbr());
}
TEST_F(LimericTest, scheduling)
{
unsigned invocation_count = 0;
limeric.on_duty_cycle_change = [&](const Limeric* limeric_on_change, Clock::time_point tp) {
EXPECT_EQ(&limeric, limeric_on_change);
// expectation: on_duty_cycle_change invocactions exactly at 200ms boundaries
EXPECT_EQ(milliseconds(0), tp.time_since_epoch() % milliseconds(200));
++invocation_count;
};
// start at 567 ms, expected first invocation at 800 ms
runtime.trigger(milliseconds(200)); // 767 ms
EXPECT_EQ(0, invocation_count);
runtime.trigger(milliseconds(50)); // 817 ms
EXPECT_EQ(1, invocation_count);
runtime.trigger(milliseconds(100)); // 917 ms
EXPECT_EQ(1, invocation_count);
runtime.trigger(milliseconds(50)); // 967 ms
EXPECT_EQ(1, invocation_count);
runtime.trigger(milliseconds(33)); // 1000 ms
EXPECT_EQ(2, invocation_count);
}
TEST_F(LimericTest, dual_alpha)
{
Limeric::DualAlphaParameters dual_params;
Limeric limeric_dual(runtime);
limeric_dual.configure_dual_alpha(dual_params);
auto update_cbr = [&](double cbr) {
limeric.update_cbr(ChannelLoad { cbr });
limeric_dual.update_cbr(ChannelLoad { cbr });
};
// set average CBR to 0.8
update_cbr(0.8);
update_cbr(0.8);
EXPECT_EQ(limeric.permitted_duty_cycle(), limeric_dual.permitted_duty_cycle());
runtime.trigger(milliseconds(200));
EXPECT_EQ(limeric.permitted_duty_cycle(), limeric_dual.permitted_duty_cycle());
// Limeric with dual-alpha is expected to converge earlier towards target CBR
for (int i = 0; i < 30; ++i) {
runtime.trigger(milliseconds(200));
}
EXPECT_GT(limeric.permitted_duty_cycle(), limeric_dual.permitted_duty_cycle());
}
@@ -0,0 +1,92 @@
#include <gtest/gtest.h>
#include <vanetza/common/manual_runtime.hpp>
#include <vanetza/dcc/duty_cycle_permit.hpp>
#include <vanetza/dcc/limeric_budget.hpp>
#include <chrono>
using namespace vanetza;
using namespace vanetza::dcc;
using std::chrono::milliseconds;
using std::chrono::microseconds;
namespace std { namespace chrono {
template<typename Rep, typename Period>
void PrintTo(const duration<Rep, Period> d, std::ostream* os)
{
duration<double, std::milli> ms = d;
*os << ms.count() << " ms";
}
}}
class LimericBudgetTest : public ::testing::Test
{
public:
LimericBudgetTest() : budget(dcp, runtime) {}
class MockDutyCyclePermit : public vanetza::dcc::DutyCyclePermit
{
public:
MockDutyCyclePermit() : m_duty_cycle(0.02) {}
UnitInterval permitted_duty_cycle() const { return m_duty_cycle; }
void permitted_duty_cycle(double dc) { m_duty_cycle = UnitInterval { dc }; }
private:
UnitInterval m_duty_cycle;
};
ManualRuntime runtime;
MockDutyCyclePermit dcp;
LimericBudget budget;
};
TEST_F(LimericBudgetTest, init)
{
EXPECT_EQ(milliseconds(25), budget.interval());
EXPECT_EQ(milliseconds(0), budget.delay());
}
TEST_F(LimericBudgetTest, notify)
{
budget.notify(milliseconds(2));
EXPECT_EQ(milliseconds(100), budget.interval());
EXPECT_EQ(budget.interval(), budget.delay());
runtime.trigger(milliseconds(60));
EXPECT_EQ(milliseconds(100), budget.interval());
EXPECT_EQ(milliseconds(40), budget.delay());
runtime.trigger(milliseconds(60));
EXPECT_EQ(milliseconds(0), budget.delay());
budget.notify(microseconds(100));
EXPECT_EQ(milliseconds(25), budget.interval()); // lower limit
budget.notify(milliseconds(30));
EXPECT_EQ(milliseconds(1000), budget.interval()); // upper limit
}
TEST_F(LimericBudgetTest, update)
{
budget.update(); // usually this should be called by Limeric's hook directly
EXPECT_EQ(milliseconds(25), budget.interval()); // no previous transmission duration known yet
budget.notify(milliseconds(1));
EXPECT_EQ(milliseconds(50), budget.interval());
runtime.trigger(milliseconds(10));
EXPECT_EQ(milliseconds(40), budget.delay());
dcp.permitted_duty_cycle(0.01); // half of previous duty cycle
budget.update();
EXPECT_EQ(milliseconds(90), budget.interval());
EXPECT_EQ(milliseconds(80), budget.delay());
runtime.trigger(milliseconds(62));
dcp.permitted_duty_cycle(0.04);
budget.update();
EXPECT_EQ(milliseconds(77), budget.interval());
EXPECT_EQ(milliseconds(5), budget.delay());
}
@@ -0,0 +1,16 @@
#include <gtest/gtest.h>
#include <vanetza/dcc/mapping.hpp>
using namespace vanetza;
using namespace vanetza::dcc;
TEST(Mapping, map_profile_onto_ac)
{
EXPECT_EQ(access::AccessCategory::VO, map_profile_onto_ac(Profile::DP0));
EXPECT_EQ(access::AccessCategory::VI, map_profile_onto_ac(Profile::DP1));
EXPECT_EQ(access::AccessCategory::BE, map_profile_onto_ac(Profile::DP2));
EXPECT_EQ(access::AccessCategory::BK, map_profile_onto_ac(Profile::DP3));
auto malicious_profile = static_cast<Profile>(4);
EXPECT_THROW(map_profile_onto_ac(malicious_profile), std::invalid_argument);
}
@@ -0,0 +1,30 @@
#include <gtest/gtest.h>
#include <vanetza/dcc/smoothing_channel_probe_processor.hpp>
using namespace vanetza::dcc;
TEST(SmoothingChannelProbeProcessor, smoothing) {
SmoothingChannelProbeProcessor cpp;
EXPECT_EQ(ChannelLoad(0.0), cpp.channel_load());
cpp.indicate(ChannelLoad(0.5));
EXPECT_EQ(ChannelLoad(0.25), cpp.channel_load());
cpp.indicate(ChannelLoad(1.0));
EXPECT_EQ(ChannelLoad(0.625), cpp.channel_load());
cpp.indicate(ChannelLoad(0.0));
EXPECT_EQ(ChannelLoad(0.3125), cpp.channel_load());
cpp.indicate(ChannelLoad(0.0));
EXPECT_EQ(ChannelLoad(0.15625), cpp.channel_load());
}
TEST(SmoothingChannelProbeProcessor, update_call) {
ChannelLoad tmp;
SmoothingChannelProbeProcessor cpp;
cpp.on_indication = [&tmp](ChannelLoad cl) { tmp = cl; };
cpp.indicate(ChannelLoad(0.5));
EXPECT_EQ(ChannelLoad(0.25), tmp);
}
@@ -0,0 +1,61 @@
#include <gtest/gtest.h>
#include <vanetza/common/clock.hpp>
#include <vanetza/common/manual_runtime.hpp>
#include <vanetza/dcc/fully_meshed_state_machine.hpp>
#include <vanetza/dcc/state_machine_budget.hpp>
using namespace vanetza::dcc;
using vanetza::ManualRuntime;
using std::chrono::milliseconds;
static const vanetza::Clock::duration immediately = milliseconds(0);
class StateMachineBudgetTest : public ::testing::Test
{
protected:
StateMachineBudgetTest() :
runtime(vanetza::Clock::time_point { std::chrono::seconds(4711) }),
budget(fsm, runtime) {}
ManualRuntime runtime;
FullyMeshedStateMachine fsm;
StateMachineBudget budget;
};
TEST_F(StateMachineBudgetTest, relaxed)
{
Relaxed relaxed;
const auto relaxed_tx_interval = relaxed.transmission_interval();
ASSERT_EQ(relaxed_tx_interval, fsm.transmission_interval());
EXPECT_EQ(immediately, budget.delay());
budget.notify();
EXPECT_EQ(relaxed_tx_interval, budget.delay());
runtime.trigger(relaxed_tx_interval - milliseconds(10));
EXPECT_EQ(milliseconds(10), budget.delay());
runtime.trigger(milliseconds(20));
EXPECT_EQ(immediately, budget.delay());
}
TEST_F(StateMachineBudgetTest, restrictive)
{
Restrictive restrictive;
const auto restrictive_tx_interval = restrictive.transmission_interval();
// put FSM into restrictive state
for (unsigned i = 0; i < 10; ++i) {
fsm.update(ChannelLoad(0.6));
}
ASSERT_STREQ("Restrictive", fsm.state().name());
EXPECT_EQ(immediately, budget.delay());
budget.notify();
EXPECT_EQ(restrictive_tx_interval, budget.delay());
runtime.trigger(restrictive_tx_interval / 2);
EXPECT_EQ(restrictive_tx_interval / 2, budget.delay());
runtime.trigger(restrictive_tx_interval / 2);
EXPECT_EQ(immediately, budget.delay());
}
@@ -0,0 +1,33 @@
#include "transmission.hpp"
#include <chrono>
namespace vanetza
{
namespace dcc
{
Clock::duration Transmission::channel_occupancy() const
{
using namespace std::chrono;
// assume 6 Mbps as default data rate
const access::DataRateG5* rate = data_rate() ? data_rate() : &access::G5_6Mbps;
// PHY
static const auto phy_preamble = microseconds(32);
static const auto phy_signal = microseconds(8);
// MAC
static const std::size_t bytes_epd = 2; // EtherType Protocol Discrimination (no LLC!)
static const std::size_t bytes_mac = 34; // 802.11 MAC header
const std::size_t bytes = rate->data_length(body_length() + bytes_epd + bytes_mac);
const double seconds_per_byte = 1.0 / (rate->bytes_per_second());
const duration<double> data_duration { bytes * seconds_per_byte };
return phy_preamble + phy_signal + duration_cast<microseconds>(data_duration);
}
} // namespace dcc
} // namespace vanetza
@@ -0,0 +1,41 @@
#ifndef TRANSMISSION_HPP_SDC4RMQE
#define TRANSMISSION_HPP_SDC4RMQE
#include <vanetza/access/data_rates.hpp>
#include <vanetza/common/clock.hpp>
#include <vanetza/dcc/profile.hpp>
#include <cstddef>
namespace vanetza
{
namespace dcc
{
class Transmission
{
public:
virtual Profile profile() const = 0;
virtual const access::DataRateG5* data_rate() const = 0;
virtual std::size_t body_length() const = 0;
virtual Clock::duration channel_occupancy() const;
virtual ~Transmission() = default;
};
struct TransmissionLite : public Transmission
{
constexpr TransmissionLite(Profile dp, std::size_t len) : m_profile(dp), m_length(len) {}
Profile m_profile;
std::size_t m_length = 0; /*< length in bytes of MAC frame body */
const access::DataRateG5* m_data_rate = nullptr;
Profile profile() const override { return m_profile; }
const access::DataRateG5* data_rate() const override { return m_data_rate; }
std::size_t body_length() const override { return m_length; }
};
} // namespace dcc
} // namespace vanetza
#endif /* TRANSMISSION_HPP_SDC4RMQE */
@@ -0,0 +1,52 @@
#ifndef TRANSMIT_RATE_CONTROL_HPP_NOPDFSY6
#define TRANSMIT_RATE_CONTROL_HPP_NOPDFSY6
#include <vanetza/common/clock.hpp>
#include <vanetza/dcc/transmission.hpp>
namespace vanetza
{
namespace dcc
{
class TransmitRateThrottle
{
public:
/**
* Duration until next transmission has to be delayed
* \param tx transmission
* \return waiting time until next transmission is allowed
*/
virtual Clock::duration delay(const Transmission& tx) = 0;
/**
* Current interval between packets
* \param tx transmission
* \return interval enforced by DCC_access
*/
virtual Clock::duration interval(const Transmission& tx) = 0;
virtual ~TransmitRateThrottle() = default;
};
class TransmitRateFeedback
{
public:
/**
* Notify about an actual transmission at link layer
* \param tx transmission
*/
virtual void notify(const Transmission& tx) = 0;
virtual ~TransmitRateFeedback() = default;
};
class TransmitRateControl : public TransmitRateThrottle, public TransmitRateFeedback
{
};
} // namespace dcc
} // namespace vanetza
#endif /* TRANSMIT_RATE_CONTROL_HPP_NOPDFSY6 */