Functions

  Name
uint16_t word16(const uint8_t * d)
int32_t word32(const uint8_t * d)
float volts(float v)

Attributes

  Name
constexpr uint8_t ALS_ADR
constexpr uint8_t WHITE_ADR
constexpr uint8_t LUXMUL_ADR
constexpr uint8_t UVA_ADR
constexpr uint8_t UVB_ADR
constexpr uint8_t IR_SHORT_ADR
constexpr uint8_t IR_MID_ADR
constexpr uint8_t THERM_ADR
constexpr uint8_t XAXIS
constexpr uint8_t YAXIS
constexpr uint8_t ZAXIS
const char *const chips

Functions Documentation

function word16

static uint16_t word16(
    const uint8_t * d
)

function word32

static int32_t word32(
    const uint8_t * d
)

function volts

static float volts(
    float v
)

Attributes Documentation

variable ALS_ADR

static constexpr uint8_t ALS_ADR = 0x48;

variable WHITE_ADR

static constexpr uint8_t WHITE_ADR = 0x4A;

variable LUXMUL_ADR

static constexpr uint8_t LUXMUL_ADR = 0x4C;

variable UVA_ADR

static constexpr uint8_t UVA_ADR = 0x50;

variable UVB_ADR

static constexpr uint8_t UVB_ADR = 0x54;

variable IR_SHORT_ADR

static constexpr uint8_t IR_SHORT_ADR = 0x58;

variable IR_MID_ADR

static constexpr uint8_t IR_MID_ADR = 0x5A;

variable THERM_ADR

static constexpr uint8_t THERM_ADR = 0x5C;

variable XAXIS

static constexpr uint8_t XAXIS = 0x32;

variable YAXIS

static constexpr uint8_t YAXIS = 0x34;

variable ZAXIS

static constexpr uint8_t ZAXIS = 0x36;

variable chips

static const char *const chips = {"VEML6075", "VEML6030", "ADS1115", "ADXL343"};

Source code

/******************************************************************************
Libelle.cpp
Library for interfacing with the Libelle shortwave pyranometer
Bobby Schulz @ Northern Widget LLC
7/5/2018
https://github.com/NorthernWidget-Skunkworks/Libelle_Library

Measures solar radiation across six spectral bands from UV-B through
short-wave infrared (~280-1700 nm), with tilt correction via onboard
accelerometer.

"The laws of nature are constructed in such a way as to make the universe
as interesting as possible."
-Freeman Dyson

Distributed as-is; no warranty is given.
******************************************************************************/

#include "Wire.h"
#include "math.h"
#include "Libelle.h"

// Schema 1 Page 2 register addresses (NW-Device-Specification, Libelle appendix)
static constexpr uint8_t ALS_ADR      = 0x48;  // Block 1, VEML6030: ALS, white, lux multiplier (uint16 each)
static constexpr uint8_t WHITE_ADR    = 0x4A;
static constexpr uint8_t LUXMUL_ADR   = 0x4C;
static constexpr uint8_t UVA_ADR      = 0x50;  // Block 2, VEML6075: UVA, UVB (int32 each)
static constexpr uint8_t UVB_ADR      = 0x54;
static constexpr uint8_t IR_SHORT_ADR = 0x58;  // Block 3, ADS1115: IR short, IR mid, thermistor (uint16 each)
static constexpr uint8_t IR_MID_ADR   = 0x5A;
static constexpr uint8_t THERM_ADR    = 0x5C;

// ADXL343 accelerometer data register addresses
static constexpr uint8_t XAXIS = 0x32;
static constexpr uint8_t YAXIS = 0x34;
static constexpr uint8_t ZAXIS = 0x36;

Libelle::Libelle(Orientation orientation)
{
  _orientation = orientation;
  if(_orientation == DOWN) {
    Accel_ADR = 0x53;
  }
  else {
    Accel_ADR = 0x1D;
  }
}

bool Libelle::begin(uint8_t ADR_)
{
  if(ADR_ == 0) ADR_ = (_orientation == DOWN) ? DEFAULT_ADDRESS_DOWN : DEFAULT_ADDRESS_UP;
  // Page 0 gates: Schema 1, the name "Libelle", firmware patch >= LIBELLE_FW_MIN_PATCH.
  _bridgeOk = _dev.begin(ADR_, "Libelle", LIBELLE_FW_MIN_PATCH);
  _accelOk = initAccel();
  delay(2); // ADXL343 standby→measurement startup time: 1.4 ms max
  return _bridgeOk && _accelOk;
}

bool Libelle::initAccel()
{
  bool ok = true;
  ok &= WriteByte(Accel_ADR, 0x2D, 0x08);
  ok &= WriteByte(Accel_ADR, 0x31, 0x08);
  ok &= WriteByte(Accel_ADR, 0x38, 0x00);
  ok &= WriteByte(Accel_ADR, 0x2C, 0x0A);
  return ok;
}

float Libelle::getG(uint8_t Axis)
{
  WriteByte(Accel_ADR, 0x2D, 0x08);
  Wire.beginTransmission(Accel_ADR);
  Wire.write(XAXIS + 2*Axis);
  Wire.endTransmission();
  Wire.requestFrom(Accel_ADR, 2);
  int LSB = Wire.read();
  int MSB = Wire.read();
  float g = ((MSB << 8) | LSB)*(0.0039);
  return g;
}

float Libelle::getRoll(bool update)
{
  if(update) updateMeasurements(ADXL343);
  return _roll;
}

float Libelle::getPitch(bool update)
{
  if(update) updateMeasurements(ADXL343);
  return _pitch;
}

long Libelle::getUVA(bool update)
{
  if(update) updateMeasurements(VEML6075);
  return _uva;
}

long Libelle::getUVB(bool update)
{
  if(update) updateMeasurements(VEML6075);
  return _uvb;
}

long Libelle::getALS(bool update)
{
  if(update) updateMeasurements(VEML6030);
  return _als;
}

long Libelle::getWhite(bool update)
{
  if(update) updateMeasurements(VEML6030);
  return _white;
}

float Libelle::getLux(bool update)
{
  if(update) updateMeasurements(VEML6030);
  return _lux;
}

float Libelle::getIR_Short(bool update)
{
  if(update) updateMeasurements(ADS1115);
  return _irShort;
}

float Libelle::getIR_Mid(bool update)
{
  if(update) updateMeasurements(ADS1115);
  return _irMid;
}

float Libelle::getTemp(bool update)
{
  if(update) updateMeasurements(ADS1115);
  return _temp;
}

float Libelle::TempConvert(float V, float Vcc, float R, float A, float B, float C, float D, float R25)
{
  float Rt = (Vcc/V)*R - R;
  float LogRt = log(Rt/R25);
  float T = 1.0/(A + B*LogRt + C*pow(LogRt, 2.0) + D*pow(LogRt, 3.0));
  return T;
}

bool Libelle::updateMeasurements(uint8_t component)
{
  resetReadings(component);
  if((component & BRIDGE) == BRIDGE && _uvCfg.n <= 1 && _lightCfg.n <= 1 && _irCfg.n <= 1) {
    // One reading of everything on the bridge: one trigger, one 22-byte read.
    _dev.resetBatch();
    if(_dev.takeReading(BRIDGE)) readData();
  }
  else {
    // Per chip group: N readings each, appended to the arrays; a chip that
    // reports absent (not answering / self-test failed) stops its batch.
    if(component & VEML6075) _dev.takeReadings(VEML6075, _uvCfg.n, [this] { return updateUV(); });
    if(component & VEML6030) _dev.takeReadings(VEML6030, _lightCfg.n, [this] { return updateLight(); });
    if(component & ADS1115) _dev.takeReadings(ADS1115, _irCfg.n, [this] { return updateIR(); });
  }
  if(component & ADXL343) {
    // Hardware v1: the accelerometer answers the controller directly, so no
    // batch word; an unresponsive accelerometer costs one reading, not N.
    for(uint16_t i = 0; i < _tiltCfg.n; i++) if(!updateTilt()) break;
  }
  summarise(component);
  bool ok = true;
  if(component & VEML6075) ok = ok && _uvaReadings.count() > 0;
  if(component & VEML6030) ok = ok && _alsReadings.count() > 0;
  if(component & ADS1115) ok = ok && _irShortReadings.count() > 0;
  if(component & ADXL343) ok = ok && _rollReadings.count() > 0;
  return ok;
}

bool Libelle::updateUV()
{
  uint8_t d[8];
  if(!_dev.takeReading(VEML6075) || !_dev.readData(UVA_ADR, d, 8)) return false;
  return readUV(d);
}

bool Libelle::updateLight()
{
  uint8_t d[6];
  if(!_dev.takeReading(VEML6030) || !_dev.readData(ALS_ADR, d, 6)) return false;
  return readLight(d);
}

bool Libelle::updateIR()
{
  uint8_t d[6];
  if(!_dev.takeReading(ADS1115) || !_dev.readData(IR_SHORT_ADR, d, 6)) return false;
  return readIR(d);
}

bool Libelle::updateTilt()
{
  float ValX = getG(0);
  float ValY = getG(1);
  float ValZ = getG(2);
  _accelFault = (ValX == ValY && ValX == ValZ); // unresponsive: every axis reads the same
  if(_accelFault) return false;
  _rollReadings.append(atan(ValX / sqrt(pow(ValY, 2) + pow(ValZ, 2))) * (180.0 / M_PI));
  _pitchReadings.append(atan(ValY / sqrt(pow(ValX, 2) + pow(ValZ, 2))) * (180.0 / M_PI));
  return true;
}

static uint16_t word16(const uint8_t* d) { return d[0] | (d[1] << 8); }
static int32_t word32(const uint8_t* d) { return (int32_t)((uint32_t)d[0] | ((uint32_t)d[1] << 8) | ((uint32_t)d[2] << 16) | ((uint32_t)d[3] << 24)); }

bool Libelle::readUV(uint8_t* d)
{
  if(_dev.faulted(0)) return false; // VEML6075: UVA, UVB int32 compensated counts
  _uvaReadings.append(word32(d));
  _uvbReadings.append(word32(d + 4));
  return true;
}

bool Libelle::readLight(uint8_t* d)
{
  if(_dev.faulted(1)) return false; // VEML6030: ALS, white, auto-range multiplier, uint16 each
  uint16_t als = word16(d), white = word16(d + 2), luxMul = word16(d + 4);
  _alsReadings.append(als);
  _whiteReadings.append(white);
  _luxReadings.append(float(als)*float(luxMul)*LuxRes);
  return true;
}

bool Libelle::readIR(uint8_t* d)
{
  if(_dev.faulted(2)) return false; // ADS1115: IR short, IR mid, thermistor, uint16 raw counts
  _irShortReadings.append(word16(d));
  _irMidReadings.append(word16(d + 2));
  float Val = float(word16(d + 4))*(1.25e-4);
  _tempReadings.append(TempConvert(Val, 3.3, 10000.0, A, B, C, D, 10000.0) - 273.15);
  return true;
}

bool Libelle::readData()
{
  // Blocks 1-3 are consecutive (0x48-0x5D): one read.
  uint8_t d[22];
  if(!_dev.readData(NW_REG_DATA, d, 22)) return false;
  readLight(d);
  readUV(d + 8);
  readIR(d + 16);
  return true;
}

void Libelle::resetReadings(uint8_t component)
{
  if(component & VEML6075) { _uvaReadings.reset(); _uvbReadings.reset(); }
  if(component & VEML6030) { _alsReadings.reset(); _whiteReadings.reset(); _luxReadings.reset(); }
  if(component & ADS1115) { _irShortReadings.reset(); _irMidReadings.reset(); _tempReadings.reset(); }
  if(component & ADXL343) { _rollReadings.reset(); _pitchReadings.reset(); }
}

void Libelle::summarise(uint8_t component)
{
  // Means over the readings taken; LIBELLE_ERROR when none (its rounded mean
  // is itself). Counts are rounded; IR is scaled from counts to volts.
  if(component & VEML6075) {
    _uva = lround(_uvaReadings.mean());
    _uvb = lround(_uvbReadings.mean());
  }
  if(component & VEML6030) {
    _als   = lround(_alsReadings.mean());
    _white = lround(_whiteReadings.mean());
    _lux   = _luxReadings.mean();
  }
  if(component & ADS1115) {
    _irShort = nwScaled(_irShortReadings.mean(), 8000.0); // 1.25e-4 V per count
    _irMid   = nwScaled(_irMidReadings.mean(), 8000.0);
    _temp    = _tempReadings.mean();
  }
  if(component & ADXL343) {
    _roll  = _rollReadings.mean();
    _pitch = _pitchReadings.mean();
  }
}

uint16_t Libelle::setUVReadings(uint16_t n)    { return _uvCfg.set(n, LIBELLE_UV_CAPACITY); }
uint16_t Libelle::setLightReadings(uint16_t n) { return _lightCfg.set(n, LIBELLE_LIGHT_CAPACITY); }
uint16_t Libelle::setIRReadings(uint16_t n)    { return _irCfg.set(n, LIBELLE_IR_CAPACITY); }
uint16_t Libelle::setTiltReadings(uint16_t n)  { return _tiltCfg.set(n, LIBELLE_TILT_CAPACITY); }
void     Libelle::setUVStats(bool enable)      { _uvCfg.stats = enable; }
void     Libelle::setLightStats(bool enable)   { _lightCfg.stats = enable; }
void     Libelle::setIRStats(bool enable)      { _irCfg.stats = enable; }
void     Libelle::setTiltStats(bool enable)    { _tiltCfg.stats = enable; }
uint16_t Libelle::getUVCount()    { return _uvaReadings.count(); }
uint16_t Libelle::getLightCount() { return _alsReadings.count(); }
uint16_t Libelle::getIRCount()    { return _irShortReadings.count(); }
uint16_t Libelle::getTiltCount()  { return _rollReadings.count(); }

// Statistics are computed from the arrays each call (NW_Readings), in the
// units stored: counts for UVA, UVB, ALS and white; lux, degrees C and degrees
// for the rest; IR counts are scaled to volts. LIBELLE_ERROR when empty.
static float volts(float v) { return nwScaled(v, 8000.0); }
float Libelle::getUVAMean()        { return _uvaReadings.mean(); }
float Libelle::getUVAStd()         { return _uvaReadings.std(); }
float Libelle::getUVASterr()       { return _uvaReadings.sterr(); }
float Libelle::getUVAMedian()      { return _uvaReadings.median(); }
float Libelle::getUVBMean()        { return _uvbReadings.mean(); }
float Libelle::getUVBStd()         { return _uvbReadings.std(); }
float Libelle::getUVBSterr()       { return _uvbReadings.sterr(); }
float Libelle::getUVBMedian()      { return _uvbReadings.median(); }
float Libelle::getALSMean()        { return _alsReadings.mean(); }
float Libelle::getALSStd()         { return _alsReadings.std(); }
float Libelle::getALSSterr()       { return _alsReadings.sterr(); }
float Libelle::getALSMedian()      { return _alsReadings.median(); }
float Libelle::getWhiteMean()      { return _whiteReadings.mean(); }
float Libelle::getWhiteStd()       { return _whiteReadings.std(); }
float Libelle::getWhiteSterr()     { return _whiteReadings.sterr(); }
float Libelle::getWhiteMedian()    { return _whiteReadings.median(); }
float Libelle::getLuxMean()        { return _luxReadings.mean(); }
float Libelle::getLuxStd()         { return _luxReadings.std(); }
float Libelle::getLuxSterr()       { return _luxReadings.sterr(); }
float Libelle::getLuxMedian()      { return _luxReadings.median(); }
float Libelle::getIR_ShortMean()   { return volts(_irShortReadings.mean()); }
float Libelle::getIR_ShortStd()    { return volts(_irShortReadings.std()); }
float Libelle::getIR_ShortSterr()  { return volts(_irShortReadings.sterr()); }
float Libelle::getIR_ShortMedian() { return volts(_irShortReadings.median()); }
float Libelle::getIR_MidMean()     { return volts(_irMidReadings.mean()); }
float Libelle::getIR_MidStd()      { return volts(_irMidReadings.std()); }
float Libelle::getIR_MidSterr()    { return volts(_irMidReadings.sterr()); }
float Libelle::getIR_MidMedian()   { return volts(_irMidReadings.median()); }
float Libelle::getTempMean()       { return _tempReadings.mean(); }
float Libelle::getTempStd()        { return _tempReadings.std(); }
float Libelle::getTempSterr()      { return _tempReadings.sterr(); }
float Libelle::getTempMedian()     { return _tempReadings.median(); }
float Libelle::getRollMean()       { return _rollReadings.mean(); }
float Libelle::getRollStd()        { return _rollReadings.std(); }
float Libelle::getRollSterr()      { return _rollReadings.sterr(); }
float Libelle::getRollMedian()     { return _rollReadings.median(); }
float Libelle::getPitchMean()      { return _pitchReadings.mean(); }
float Libelle::getPitchStd()       { return _pitchReadings.std(); }
float Libelle::getPitchSterr()     { return _pitchReadings.sterr(); }
float Libelle::getPitchMedian()    { return _pitchReadings.median(); }

String Libelle::column(const char* name, const char* unit, bool stats)
{
  // "R_u [deg]," for the UP unit, "R_d [deg]," for DOWN; with stats the std
  // and sterr columns follow: "R_u std [deg],R_u sterr [deg],".
  String Name = String(name) + (_orientation == DOWN ? "_d" : "_u");
  String Header = Name + unit + ",";
  if(stats) Header += Name + " std" + unit + "," + Name + " sterr" + unit + ",";
  return Header;
}

String Libelle::getHeader()
{
  bool st = _tiltCfg.columns(), su = _uvCfg.columns(), sl = _lightCfg.columns(), si = _irCfg.columns();
  return column("R", " [deg]", st) + column("P", " [deg]", st) + column("UVA", "", su) + column("UVB", "", su)
       + column("White", "", sl) + column("Vis", " [lx]", sl)
       + column("IR_S", "", si) + column("IR_M", "", si) + column("PyroT", " [C]", si);
}

String Libelle::getString()
{
  updateMeasurements(ALL);
  bool st = _tiltCfg.columns(), su = _uvCfg.columns(), sl = _lightCfg.columns(), si = _irCfg.columns();
  String s = String(getRoll()) + ",";
  if(st) s += String(getRollStd()) + "," + String(getRollSterr()) + ",";
  s += String(getPitch()) + ",";
  if(st) s += String(getPitchStd()) + "," + String(getPitchSterr()) + ",";
  s += String(getUVA()) + ",";
  if(su) s += String(getUVAStd()) + "," + String(getUVASterr()) + ",";
  s += String(getUVB()) + ",";
  if(su) s += String(getUVBStd()) + "," + String(getUVBSterr()) + ",";
  s += String(getWhite()) + ",";
  if(sl) s += String(getWhiteStd()) + "," + String(getWhiteSterr()) + ",";
  s += String(getLux()) + ",";
  if(sl) s += String(getLuxStd()) + "," + String(getLuxSterr()) + ",";
  s += String(getIR_Short()) + ",";
  if(si) s += String(getIR_ShortStd()) + "," + String(getIR_ShortSterr()) + ",";
  s += String(getIR_Mid()) + ",";
  if(si) s += String(getIR_MidStd()) + "," + String(getIR_MidSterr()) + ",";
  s += String(getTemp()) + ",";
  if(si) s += String(getTempStd()) + "," + String(getTempSterr()) + ",";
  return s;
}

// The reading interface: one reading per logReading(), printed as it is taken.
void Libelle::beginReadings(uint8_t component, uint16_t n)
{
  _component = component;
  resetReadings(component);
  if(component & BRIDGE) _dev.beginBatch(n);
  else _dev.resetBatch(); // the accelerometer alone: nothing to tell the bridge
}

void Libelle::endReadings()
{
  // No cleanup required currently
}

size_t Libelle::printHeader(Print& out)
{
  size_t n = 0;
  if(_component & ADXL343)  n += out.print(column("R", " [deg]", false) + column("P", " [deg]", false));
  if(_component & VEML6075) n += out.print(column("UVA", "", false) + column("UVB", "", false));
  if(_component & VEML6030) n += out.print(column("White", "", false) + column("Vis", " [lx]", false));
  if(_component & ADS1115)  n += out.print(column("IR_S", "", false) + column("IR_M", "", false) + column("PyroT", " [C]", false));
  return n;
}

size_t Libelle::printReading(Print& out)
{
  size_t n = 0;
  if(_component & ADXL343)  { n += out.print(_roll); n += out.print(','); n += out.print(_pitch); n += out.print(','); }
  if(_component & VEML6075) { n += out.print(_uva); n += out.print(','); n += out.print(_uvb); n += out.print(','); }
  if(_component & VEML6030) { n += out.print(_white); n += out.print(','); n += out.print(_lux); n += out.print(','); }
  if(_component & ADS1115)  { n += out.print(_irShort); n += out.print(','); n += out.print(_irMid); n += out.print(','); n += out.print(_temp); n += out.print(','); }
  return n;
}

size_t Libelle::logReading(Print& out)
{
  // One acquisition per chip group selected, then the values just taken.
  if(_component & ADXL343) {
    _roll = _pitch = LIBELLE_ERROR;
    if(updateTilt()) { _roll = _rollReadings.last(); _pitch = _pitchReadings.last(); }
  }
  if(_component & VEML6075) {
    _uva = _uvb = (long)LIBELLE_ERROR;
    if(updateUV()) { _uva = _uvaReadings.last(); _uvb = _uvbReadings.last(); }
  }
  if(_component & VEML6030) {
    _als = _white = (long)LIBELLE_ERROR; _lux = LIBELLE_ERROR;
    if(updateLight()) { _als = _alsReadings.last(); _white = _whiteReadings.last(); _lux = _luxReadings.last(); }
  }
  if(_component & ADS1115) {
    _irShort = _irMid = _temp = LIBELLE_ERROR;
    if(updateIR()) { _irShort = volts(_irShortReadings.last()); _irMid = volts(_irMidReadings.last()); _temp = _tempReadings.last(); }
  }
  return printReading(out);
}

bool    Libelle::faulted(uint8_t chip) { return (chip == 3) ? _accelFault : _dev.faulted(chip); } // chip 3, the ADXL343, is the library's own on hardware v1
bool    Libelle::anyFault()            { return _dev.anyFault() || _accelFault; }
uint8_t Libelle::reportChip()           { return report().chip(); }
uint8_t Libelle::reportKind()           { return report().kind(); }
uint8_t Libelle::getHardwareMajor()    { return _dev.hardwareMajor(); }
uint8_t Libelle::getHardwareMinor()    { return _dev.hardwareMinor(); }
uint8_t Libelle::getFirmwareVersion()  { return _dev.firmwareVersion(); }

NW_Report Libelle::report()
{
  // The bridge's report first. Hardware v1: the accelerometer is the
  // library's to judge; not answering at begin() is kind 1, three identical
  // axes afterwards kind 4 (out of range).
  if(_dev.reportKind() == 0 && _accelFault) {
    NW_Report f;
    f.code = (3 << 5) | (_accelOk ? 4 : 1);
    return f;
  }
  return _dev.report();
}

String Libelle::beginFailure()
{
  if(_bridgeOk && !_accelOk) return "NoAccel"; // the bridge passed; the accelerometer did not acknowledge
  return _dev.beginFailure();
}

// The chip names are Libelle's own (the spec's chip table); NW_Report prints the rest.
static const char* const chips[] = {"VEML6075", "VEML6030", "ADS1115", "ADXL343"};

size_t Libelle::printReport(Print& out)
{
  return report().print(out, chips, 4);
}

size_t Libelle::printStatus(Print& out, bool boot)
{
  return _dev.printSnapshot(out, chips, 4, boot, LIBELLE_LIBRARY_VERSION, LIBELLE_LIBRARY_COMMIT);
}

bool    Libelle::reportIsFault()   { return report().isFault(); } // the accelerometer's own fault counts too
uint8_t Libelle::bootReportKind()  { return _dev.bootReport().kind(); }
void    Libelle::clearBootReport() { _dev.clearBootReport(); }

String Libelle::reportNote()
{
  // One word for a data-table note: the chip, then the kind ("VEML6075NotAnswering").
  return report().note(chips, 4);
}

void Libelle::PrintAllRegs()
{
  uint8_t Regs[96]; // Page 0 (identity), Page 1 (calibration) and Page 2 (status, control, data)
  _dev.readBytes(0x00, Regs, 96);
  for(int i = 0; i < 96; i++) {
    Serial.print("Reg"); Serial.print(i, HEX); Serial.print(":\t");
    Serial.println(Regs[i]);
  }
  Serial.print("\n\n");
}

bool Libelle::WriteByte(uint8_t Adr, uint8_t Pos, uint8_t Val)
{
  Wire.beginTransmission(Adr);
  Wire.write(Pos);
  Wire.write(Val);
  return Wire.endTransmission() == 0;
}

Updated on 2026-09-23 at 22:43:23 +0000