Skip to content

cleishm/electro-cpp

Repository files navigation

electro

A type-safe, header-only C++20 library for electrical units, modeled after std::chrono::duration.

📚 Full API Documentation

Features

  • Type-safe electrical quantities: voltage, current, resistance, power, charge, energy, capacitance, inductance
  • Standard SI multiples: microvolts through kilovolts, milliohms through gigaohms, picofarads through farads, and more
  • Cross-quantity arithmetic following circuit theory: Ohm's law (I * R = V), power (V * I = P), and friends
  • std::chrono interop: mA x hours = mAh, W x hours = Wh, Ω x F = RC time constant (as a std::chrono::duration)
  • Decibels: dB gains and dBm/dBW/dBµV levels, related as duration is to time_point, so that dBm + dBm is a compile error
  • Integer and floating-point representations for exact or fractional precision
  • Lossless implicit conversions (lossy conversions require explicit casts)
  • User-defined literals for concise notation (3300_mV, 10_kΩ, 2000_mAh, -73_dBm)
  • std::format support (when available)
  • Fully constexpr - all operations can be evaluated at compile time (except the dB ↔ linear conversions, which need std::log10/std::pow)

Requirements

  • C++20 compiler (GCC 12+, Clang 15+, MSVC 19.30+)
  • MSVC users: The /utf-8 flag is required (the source contains unit symbols such as Ω)

Installation

CMake FetchContent

include(FetchContent)
FetchContent_Declare(
    electro
    GIT_REPOSITORY https://github.com/cleishm/electro-cpp.git
    GIT_TAG v0.2.0
)
FetchContent_MakeAvailable(electro)

target_link_libraries(your_target PRIVATE electro::electro)

vcpkg

vcpkg install cleishm-electro-cpp
find_package(electro CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE electro::electro)

Manual

Copy the include/electro/ directory to your project.

Usage

#include <electro/electro>  // or <electro/electro.hpp>

using namespace electro;
using namespace electro_literals;

// Basic quantities
millivolts vbat{3300};
milliamperes load{150};
kiloohms pullup{10};

// Using literals
auto usb = 5_V;
auto shunt = 50_mOhm;   // or 50_mΩ
auto cap = 100_nF;

// Precision control
millivolts precise{3312};
volts coarse = volts(precise);   // explicit lossy conversion (truncates to 3 V)
millivolts back = coarse;        // implicit lossless conversion (3000 mV)

// Same-unit arithmetic
auto total = 10_kOhm + 20_kOhm;  // 30 kΩ
auto scaled = 100_mA * 3;        // 300 mA
auto ratio = 10_kOhm / 5_kOhm;   // 2 (scalar)

// Comparisons work across precisions
if (kilovolts{1} == volts{1000}) {
    // true - same voltage, different precision
}

// String conversion
std::string s = to_string(kiloohms(10));  // "10kΩ"

// Formatting: a floating-point format spec renders in display units
// (base unit, or the conventional unit for Ah/Wh precisions), while the
// default renders the exact stored value.
millivolts reading{3300};
std::string disp = std::format("{:.1f}", reading);  // "3.3V"
std::string raw  = std::format("{}", reading);      // "3300mV" (exact stored value)
std::format("{:.1f}", milliampere_hours(200));      // "0.2Ah"

Cross-Quantity Arithmetic

Quantities of different kinds combine according to circuit theory, producing results in the correct unit with a precision derived from the operands:

// Ohm's law: V = I x R
auto v = 100_mA * 2_kOhm;          // 200 V   (milli x kilo = 1)
auto i = 5000_mV / 100_Ohm;        // 50 mA
auto r = 12_V / 3_A;               // 4 Ω

// Power: P = V x I
auto p = 12_V * 2_A;               // 24 W
auto i2 = 60_W / 12_V;             // 5 A

// Charge and energy via std::chrono
auto q = 100_mA * std::chrono::hours(2);   // 200 mAh
auto e = 100_W * std::chrono::hours(5);    // 500 Wh
auto runtime = 2000_mAh / 100_mA;          // 20 h (a std::chrono::duration)
auto capacity = 100_Wh / 5_V;              // 20 Ah

// Time constants
auto tau = 47_kOhm * 10_uF;        // 470 ms (RC)
auto lr = 10_mH / 2_Ohm;           // 5 ms (L/R)

Products are exact for integer representations. Quotients truncate like integer division, so choose operand precisions accordingly: 5_V / 3_Ohm is 1 A, while 5000_mV / 3_Ohm is 1666 mA. Floating-point representations (e.g. voltage<double>) avoid truncation entirely.

Decibels and Levels

Decibels live in a separate, opt-in header:

#include <electro/decibel>  // or <electro/decibel.hpp>

They come in two kinds, and conflating them is the classic source of bugs. This library keeps them apart using the same distinction std::chrono draws between a duration and a time_point:

Closed under
gain (dB) a dimensionless ratio — the duration analog +, -, * scalar
level (dBm, dBW, dBµV) an absolute point on a log scale — the time_point analog level ± gain, level - level → gain
auto tx     = 20_dBm;
auto eirp   = tx + 6_dB - 2_dB;    // level ± gain -> level (24 dBm)
auto rx     = eirp - 90_dB;        // path loss             (-66 dBm)
auto margin = rx - -100_dBm;       // level - level -> gain (34 dB)

// tx + tx;        // error: adding two absolute levels is meaningless
// tx * 2;         // error: scaling an absolute level is meaningless
// tx + 10_dBW;    // error: references do not mix

That last group matters: a plain dBm unit tag would accept all three and silently compute nonsense (10 dBm + 10 dBm is 13.01 dBm, not 20 dBm). Combining the powers of two uncorrelated signals is a named function, never operator+:

add_powers(1000_cdBm, 1000_cdBm);  // 13.01 dBm

Crossing into the linear domain

Conversions are explicit, named functions rather than constructors: they are non-constexpr, nonlinear, and have domain errors a constructor cannot report. A level converts at its own reference precision, so dBm yields milliwatts:

to_linear(30_dBm);                       // 1000.0 mW, as power<double, std::milli>
quantity_cast<watts>(to_linear(30_dBm)); // 1 W
to_level<dbm>(milliwatts(1));            // 0 dBm
to_level<dbm>(watts(1));                 // 30 dBm

A zero quantity has no finite logarithm and maps to dbm::min(); a negative one asserts. Values are rounded to nearest, not truncated.

Power and field references

A reference records whether its linear quantity is a power (10·log10) or a field such as voltage (20·log10), so callers never have to remember which factor applies:

to_level<dbuv>(volts(1));  // 120 dBµV, not 60

For the same reason, a bare dB value cannot be turned into a linear ratio unambiguously — 3 dB is a factor of 2 in power but ~1.41 in amplitude — so the two conversions are separately named:

power_ratio(3_dB);      // ~1.995
amplitude_ratio(6_dB);  // ~1.995

add_powers is available only for power references; for a field reference, summing linear amplitudes would model coherent addition instead, which is a different operation with a different answer.

Precision

Integer decibels at 1 dB resolution are coarse. Use a finer precision or a floating-point representation:

centi_dbm fine{-7350};        // -73.50 dBm
dbm_level<double> exact{-73.5};
to_string(fine);              // "-73.50dBm"

Quantity Types

All standard aliases use int64_t representation. Each quantity also has an alias template for custom representations and precisions, e.g. voltage<double> or current<float, std::milli>.

Quantity Alias template Standard aliases
Voltage (V) voltage<Rep, Precision> microvolts, millivolts, volts, kilovolts
Current (A) current<Rep, Precision> microamperes/microamps, milliamperes/milliamps, amperes/amps
Resistance (Ω) resistance<Rep, Precision> milliohms, ohms, kiloohms, megaohms, gigaohms
Power (W) power<Rep, Precision> microwatts, milliwatts, watts, kilowatts, megawatts, gigawatts
Charge (C) charge<Rep, Precision> microcoulombs, millicoulombs, coulombs, milliampere_hours, ampere_hours
Energy (J) energy<Rep, Precision> millijoules, joules, kilojoules, megajoules, watt_hours, kilowatt_hours
Capacitance (F) capacitance<Rep, Precision> picofarads, nanofarads, microfarads, millifarads, farads
Inductance (H) inductance<Rep, Precision> nanohenries, microhenries, millihenries, henries

Decibel Types

From <electro/decibel>. Levels are level<Reference, Rep, Precision>; gains are ordinary quantities of decibel_unit.

Kind Alias template Standard aliases
Gain (dB) gain<Rep, Precision> decibels, centidecibels, millidecibels
Level vs 1 mW dbm_level<Rep, Precision> dbm, centi_dbm
Level vs 1 W dbw_level<Rep, Precision> dbw
Level vs 1 V dbv_level<Rep, Precision> dbv
Level vs 1 mV dbmv_level<Rep, Precision> dbmv
Level vs 1 µV dbuv_level<Rep, Precision> dbuv, centi_dbuv

Literals

Quantity Literals
Voltage _uV, _mV, _V, _kV
Current _uA, _mA, _A
Resistance _mOhm, _Ohm, _kOhm, _MOhm (also _mΩ, , _kΩ, _MΩ)
Power _uW, _mW, _W, _kW, _MW
Charge _C, _mAh, _Ah
Energy _mJ, _J, _kJ, _Wh, _kWh
Capacitance _pF, _nF, _uF, _F
Inductance _nH, _uH, _mH, _H
Gain _dB, _cdB (0.01 dB steps)
Level _dBm, _cdBm, _dBW, _dBV, _dBmV, _dBuV (also _dBµV)

Literals are integral, so fractional values use the centi variants (1301_cdBm is 13.01 dBm) or an explicit floating-point type. Negative levels such as -73_dBm work because level provides a unary operator-, which reflects the level about its reference.

Conversions

Following std::chrono semantics:

  • Implicit conversions are allowed when lossless (finer target precision, or floating-point target representation)
  • Explicit conversions (constructor or quantity_cast) are required when the conversion may lose precision
  • floor, ceil and round provide explicit rounding control
millivolts mv = volts(5);              // implicit: 5000 mV
volts v = volts(millivolts(5500));     // explicit: truncates to 5 V
auto r = round<volts>(millivolts(5500)); // 6 V (round half to even)
auto c = quantity_cast<ampere_hours>(coulombs(7200)); // 2 Ah

Levels follow the same rules, via level_cast and the same floor, ceil and round overloads:

centi_dbm fine = 20_dBm;                  // implicit: 2000 (20.00 dBm)
dbm coarse = dbm(centi_dbm(1301));        // explicit: truncates to 13 dBm
auto n = round<dbm>(centi_dbm(1350));     // 14 dBm

ESP-IDF

The library can be used as an ESP-IDF component. Add it to your project's idf_component.yml:

dependencies:
  cleishm/electro: "^0.2.0"

std::format support is controlled via CONFIG_ELECTRO_STD_FORMAT in menuconfig (under "Component config" → "Electro Library").

Building and Testing

cmake -B build -DELECTRO_BUILD_TESTS=ON
cmake --build build
ctest --test-dir build

License

MIT License - see LICENSE for details.

About

A type-safe, header-only C++20 library for electrical units, modeled after `std::chrono::duration`.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors