Add support for AGS02MA TVOC Sensor (#24109)

* Add ags02ma library

* Initial commit

* fix object init

* fix bugs

* Update I2C Driver number
Device disabled by default

* refactoring for consistency
This commit is contained in:
Akshaylal S 2025-12-08 16:10:42 +05:30 committed by GitHub
parent 66dc5c926d
commit 96e174bcc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 2944 additions and 0 deletions

View File

@ -133,5 +133,6 @@ Index | Define | Driver | Device | Address(es) | Bus2 | Descrip
92 | USE_PCF85063 | xdrv_56 | PCF85063 | 0x51 | | PCF85063 Real time clock 92 | USE_PCF85063 | xdrv_56 | PCF85063 | 0x51 | | PCF85063 Real time clock
93 | USE_AS33772S | xdrv_119 | AS33772S | 0x52 | Yes | AS33772S USB PD Sink Controller 93 | USE_AS33772S | xdrv_119 | AS33772S | 0x52 | Yes | AS33772S USB PD Sink Controller
94 | USE_RV3028 | xdrv_56 | RV3028 | 0x52 | Yes | RV-3028-C7 RTC Controller 94 | USE_RV3028 | xdrv_56 | RV3028 | 0x52 | Yes | RV-3028-C7 RTC Controller
95 | USE_AGS02MA | xsns_118 | AGS02MA | 0x1A | | TVOC Gas sensor
NOTE: Bus2 supported on ESP32 only. NOTE: Bus2 supported on ESP32 only.

View File

@ -0,0 +1,28 @@
platforms:
rpipico:
board: rp2040:rp2040:rpipico
package: rp2040:rp2040
gcc:
features:
defines:
- ARDUINO_ARCH_RP2040
warnings:
flags:
packages:
rp2040:rp2040:
url: https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
compile:
# Choosing to run compilation tests on 2 different Arduino platforms
platforms:
- uno
# - due
# - zero
# - leonardo
- m4
- esp32
- esp8266
# - mega2560
- rpipico

View File

@ -0,0 +1,386 @@
//
// FILE: AGS02MA.cpp
// AUTHOR: Rob Tillaart, Viktor Balint, Beanow
// DATE: 2021-08-12
// VERSION: 0.4.3
// PURPOSE: Arduino library for AGS02MA TVOC sensor
// URL: https://github.com/RobTillaart/AGS02MA
#include "AGS02MA.h"
// REGISTERS
#define AGS02MA_DATA 0x00
#define AGS02MA_CALIBRATION 0x01
#define AGS02MA_VERSION 0x11
#define AGS02MA_SLAVE_ADDRESS 0x21
AGS02MA::AGS02MA(const uint8_t deviceAddress, TwoWire *wire)
{
_address = deviceAddress;
_wire = wire;
reset();
}
bool AGS02MA::begin()
{
_startTime = millis(); // PREHEAT TIMING
return isConnected();
}
bool AGS02MA::isConnected()
{
_setI2CLowSpeed();
_wire->beginTransmission(_address);
bool rv = ( _wire->endTransmission(true) == 0);
_setI2CHighSpeed();
return rv;
}
void AGS02MA::reset()
{
_I2CResetSpeed = 100000;
_startTime = millis();
_lastRead = 0;
_lastPPB = 0;
_mode = 255;
_status = AGS02MA_OK;
_error = AGS02MA_OK;
}
bool AGS02MA::setAddress(const uint8_t deviceAddress)
{
if ((deviceAddress < 10) or (deviceAddress > 119)) return false;
_buffer[2] = _buffer[0] = deviceAddress;
_buffer[3] = _buffer[1] = 0xFF ^ deviceAddress;
_buffer[4] = _CRC8(_buffer, 4);
if (_writeRegister(AGS02MA_SLAVE_ADDRESS))
{
_address = deviceAddress;
}
return isConnected();
}
uint8_t AGS02MA::getSensorVersion()
{
uint8_t version = 0xFF;
if (_readRegister(AGS02MA_VERSION))
{
// for (int i = 0; i < 5; i++)
// {
// Serial.print(_buffer[i]);
// Serial.print('\t');
// }
// Serial.println();
// unclear what the other bytes have for information.
// datasheet names these 3 bytes as KEEP.
// BUFFER VALUE MEANING
// buffer [0] == 20 year ?
// buffer [1] == 07 month ?
// buffer [2] == 28 day ?
// buffer [3] == 117 VERSION
// buffer [4] == CRC
version = _buffer[3];
if (_CRC8(_buffer, 5) != 0)
{
_error = AGS02MA_ERROR_CRC;
}
}
return version;
}
uint32_t AGS02MA::getSensorDate()
{
uint32_t date = 0xFFFFFFFF;
if (_readRegister(AGS02MA_VERSION))
{
date = 0x20;
date <<= 8;
date += _bin2bcd(_buffer[0]);
date <<= 8;
date += _bin2bcd(_buffer[1]);
date <<= 8;
date += _bin2bcd(_buffer[2]);
// version = _buffer[3];
if (_CRC8(_buffer, 5) != 0)
{
_error = AGS02MA_ERROR_CRC;
}
}
return date;
}
bool AGS02MA::setPPBMode()
{
_buffer[0] = 0x00;
_buffer[1] = 0xFF;
_buffer[2] = 0x00;
_buffer[3] = 0xFF;
_buffer[4] = 0x30;
if (_writeRegister(AGS02MA_DATA))
{
_mode = 0;
return true;
}
return false;
}
bool AGS02MA::setUGM3Mode()
{
_buffer[0] = 0x02;
_buffer[1] = 0xFD;
_buffer[2] = 0x02;
_buffer[3] = 0xFD;
_buffer[4] = 0x00;
if (_writeRegister(AGS02MA_DATA))
{
_mode = 1;
return true;
}
return false;
}
uint32_t AGS02MA::readPPB()
{
uint32_t value = _readSensor();
if (_error == AGS02MA_OK)
{
_lastRead = millis();
_lastPPB = value;
}
else
{
value = _lastPPB;
}
return value;
}
uint32_t AGS02MA::readUGM3()
{
uint32_t value = _readSensor();
if (_error == AGS02MA_OK)
{
_lastRead = millis();
_lastUGM3 = value;
}
else
{
value = _lastUGM3;
}
return value;
}
bool AGS02MA::manualZeroCalibration(uint16_t value)
{
_buffer[0] = 0x00;
_buffer[1] = 0x0C;
_buffer[2] = (uint8_t) (value >> 8);
_buffer[3] = (uint8_t) (value & 0x00FF);
_buffer[4] = _CRC8(_buffer, 4);
return _writeRegister(AGS02MA_CALIBRATION);
}
bool AGS02MA::getZeroCalibrationData(AGS02MA::ZeroCalibrationData &data) {
if (!_readRegister(AGS02MA_CALIBRATION))
{
return false;
}
if (_CRC8(_buffer, 5) != 0)
{
_error = AGS02MA_ERROR_CRC;
return false;
}
_error = AGS02MA_OK;
// Don't pollute the struct given to us, until we've handled all error cases.
data.status = _getDataMSB();
data.value = _getDataLSB();
return true;
}
int AGS02MA::lastError()
{
int e = _error;
_error = AGS02MA_OK; // reset error after read
return e;
}
bool AGS02MA::readRegister(uint8_t address, AGS02MA::RegisterData &reg) {
if (!_readRegister(address))
{
return false;
}
if (_CRC8(_buffer, 5) != 0)
{
_error = AGS02MA_ERROR_CRC;
return false;
}
_error = AGS02MA_OK;
// Don't pollute the struct given to us, until we've handled all error cases.
reg.data[0] = _buffer[0];
reg.data[1] = _buffer[1];
reg.data[2] = _buffer[2];
reg.data[3] = _buffer[3];
reg.crc = _buffer[4];
reg.crcValid = true; // checked above.
return true;
}
/////////////////////////////////////////////////////////
//
// PRIVATE
//
uint32_t AGS02MA::_readSensor()
{
uint32_t value = 0;
if (_readRegister(AGS02MA_DATA))
{
_error = AGS02MA_OK;
_status = _buffer[0];
if (_status & 0x01)
{
_error = AGS02MA_ERROR_NOT_READY;
}
value = _buffer[1] * 65536UL;
value += _buffer[2] * 256;
value += _buffer[3];
if (_CRC8(_buffer, 5) != 0)
{
_error = AGS02MA_ERROR_CRC;
}
}
return value;
}
bool AGS02MA::_readRegister(uint8_t reg)
{
while (millis() - _lastRegTime < 30) yield();
_setI2CLowSpeed();
_wire->beginTransmission(_address);
_wire->write(reg);
_error = _wire->endTransmission(true);
if (_error != 0)
{
// _error will be I2C error code
_setI2CHighSpeed();
return false;
}
// TODO investigate async interface
delay(30);
if (_wire->requestFrom(_address, (uint8_t)5) != 5)
{
_error = AGS02MA_ERROR_READ;
_setI2CHighSpeed();
return false;
}
for (uint8_t i = 0; i < 5; i++)
{
_buffer[i] = _wire->read();
}
_error = AGS02MA_OK;
_setI2CHighSpeed();
return true;
}
bool AGS02MA::_writeRegister(uint8_t reg)
{
while (millis() - _lastRegTime < 30) yield();
_lastRegTime = millis();
_setI2CLowSpeed();
_wire->beginTransmission(_address);
_wire->write(reg);
for (uint8_t i = 0; i < 5; i++)
{
_wire->write(_buffer[i]);
}
_error = _wire->endTransmission(true);
_setI2CHighSpeed();
return (_error == 0);
}
void AGS02MA::_setI2CLowSpeed()
{
#if defined (__AVR__)
// TWBR = 255; // == 30.4 KHz with TWSR = 0x00
TWBR = 78; // == 25.0 KHZ
TWSR = 0x01; // pre-scaler = 4
#else
_wire->setClock(AGS02MA_I2C_CLOCK);
#endif
}
void AGS02MA::_setI2CHighSpeed()
{
#if defined (__AVR__)
TWSR = 0x00;
#endif
_wire->setClock(_I2CResetSpeed);
}
uint16_t AGS02MA::_getDataMSB()
{
return (_buffer[0] << 8) + _buffer[1];
}
uint16_t AGS02MA::_getDataLSB()
{
return (_buffer[2] << 8) + _buffer[3];
}
uint8_t AGS02MA::_CRC8(uint8_t * buf, uint8_t size)
{
uint8_t crc = 0xFF; // start value
for (uint8_t b = 0; b < size; b++)
{
crc ^= buf[b];
for (uint8_t i = 0; i < 8; i++)
{
if (crc & 0x80) crc = (crc << 1) ^ 0x31;
else crc = (crc << 1);
}
}
return crc;
}
uint8_t AGS02MA::_bin2bcd (uint8_t value)
{
return value + 6 * (value / 10);
}
// -- END OF FILE --

View File

@ -0,0 +1,145 @@
#pragma once
//
// FILE: AGS02MA.h
// AUTHOR: Rob Tillaart, Viktor Balint, Beanow
// DATE: 2021-08-12
// VERSION: 0.4.3
// PURPOSE: Arduino library for AGS02MA TVOC sensor
// URL: https://github.com/RobTillaart/AGS02MA
#include "Arduino.h"
#include "Wire.h"
#define AGS02MA_LIB_VERSION (F("0.4.3"))
#define AGS02MA_OK 0
#define AGS02MA_ERROR -10
#define AGS02MA_ERROR_CRC -11
#define AGS02MA_ERROR_READ -12
#define AGS02MA_ERROR_NOT_READY -13
#define AGS02MA_ERROR_REQUEST -14
#define AGS02MA_I2C_CLOCK 25000 // max 30000
class AGS02MA
{
public:
struct RegisterData
{
uint8_t data[4];
uint8_t crc;
bool crcValid;
};
struct ZeroCalibrationData
{
/**
* Warning, the exact meaning of this status is not fully documented.
* It seems like it's a bit mask:
* 0000 1100 | 0x0C | 12 | Typical value
* 0000 1101 | 0x0D | 13 | Sometimes seen on v117
* 0111 1101 | 0x7D | 125 | Seen on v118, after power-off (gives different data than 12!)
*/
uint16_t status;
uint16_t value;
};
// address 26 = 0x1A
explicit AGS02MA(const uint8_t deviceAddress = 26, TwoWire *wire = &Wire);
bool begin();
bool isConnected();
void reset();
bool isHeated() { return (millis() - _startTime) > 120000UL; };
// CONFIGURATION
bool setAddress(const uint8_t deviceAddress);
uint8_t getAddress() { return _address; };
uint8_t getSensorVersion();
uint32_t getSensorDate();
// to set the speed the I2C bus should return to
// as the device operates at very low bus speed of 30 kHz.
void setI2CResetSpeed(uint32_t speed) { _I2CResetSpeed = speed; };
uint32_t getI2CResetSpeed() { return _I2CResetSpeed; };
// to be called after at least 5 minutes in fresh air.
bool zeroCalibration() { return manualZeroCalibration(0); };
/**
* Set the zero calibration value manually.
* To be called after at least 5 minutes in fresh air.
* For v117: 0-65535 = automatic calibration.
* For v118: 0 = automatic calibration, 1-65535 manual calibration.
*/
bool manualZeroCalibration(uint16_t value = 0);
bool getZeroCalibrationData(ZeroCalibrationData &data);
// MODE
bool setPPBMode();
bool setUGM3Mode();
uint8_t getMode() { return _mode; };
// READ functions
uint32_t readPPB(); // parts per billion 10^9
uint32_t readUGM3(); // microgram per cubic meter
// derived read functions
float readPPM() { return readPPB() * 0.001; }; // parts per million
float readMGM3() { return readUGM3() * 0.001; }; // milligram per cubic meter
float readUGF3() { return readUGM3() * 0.0283168466; }; // microgram per cubic feet
float lastPPM() { return _lastPPB * 0.001; };
uint32_t lastPPB() { return _lastPPB; }; // fetch last PPB measurement
uint32_t lastUGM3() { return _lastUGM3; }; // fetch last UGM3 measurement
// STATUS
uint32_t lastRead() { return _lastRead; }; // timestamp last measurement
int lastError();
uint8_t lastStatus() { return _status; };
uint8_t dataReady() { return _status & 0x01; };
// Reading registers
bool readRegister(uint8_t address, RegisterData &reg);
private:
uint32_t _readSensor();
bool _readRegister(uint8_t reg);
bool _writeRegister(uint8_t reg);
uint32_t _I2CResetSpeed = 100000;
uint32_t _startTime = 0;
uint32_t _lastRead = 0;
uint32_t _lastRegTime = 0;
uint32_t _lastPPB = 0;
uint32_t _lastUGM3 = 0;
uint8_t _address = 0;
uint8_t _mode = 255;
uint8_t _status = 0;
uint8_t _buffer[5];
void _setI2CLowSpeed();
void _setI2CHighSpeed();
uint16_t _getDataMSB();
uint16_t _getDataLSB();
uint8_t _CRC8(uint8_t * buf, uint8_t size);
uint8_t _bin2bcd(uint8_t val);
int _error = AGS02MA_OK;
TwoWire* _wire;
};
// -- END OF FILE --

View File

@ -0,0 +1,65 @@
# Change Log AGS02MA
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.4.3] - 2025-08-15
- update readme.md
- update license
- minor edits
## [0.4.2] - 2024-02-03
- update readme.md
- added multiplexer section
- extended PPB "health" table
- clean up examples
- refactor conditional code
- added **setI2CLowSpeed()** and **setI2CHighSpeed()**
- improved error handling a bit.
- added **AGS02MA_ERROR_REQUEST**
- redo **readRegister()**
- minor edits
## [0.4.1] - 2023-12-10
- fix #26, error in readme.
## [0.4.0] - 2023-12-06
- refactor API, begin()
- update readme.md
----
## [0.3.4] - 2023-09-25
- add Wire1 support for ESP32
- update readme.md
- minor edits
## [0.3.3] - 2023-01-21
- update GitHub actions
- update license 2023
- update keywords
- minor edit readme.md
- minor edit code
- add CHANGELOG.md (for real)
## [0.3.2] - 2022-10-26
- add CHANGELOG.md
- add RP2040 in build
----
## no info
- 0.3.1
- 0.3.0
- 0.2.0
- 0.1.4
- 0.1.3
- 0.1.2
- 0.1.1
- 0.1.0

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-2025 Rob Tillaart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,400 @@
[![Arduino CI](https://github.com/RobTillaart/AGS02MA/workflows/Arduino%20CI/badge.svg)](https://github.com/marketplace/actions/arduino_ci)
[![Arduino-lint](https://github.com/RobTillaart/AGS02MA/actions/workflows/arduino-lint.yml/badge.svg)](https://github.com/RobTillaart/AGS02MA/actions/workflows/arduino-lint.yml)
[![JSON check](https://github.com/RobTillaart/AGS02MA/actions/workflows/jsoncheck.yml/badge.svg)](https://github.com/RobTillaart/AGS02MA/actions/workflows/jsoncheck.yml)
[![GitHub issues](https://img.shields.io/github/issues/RobTillaart/AGS02MA.svg)](https://github.com/RobTillaart/AGS02MA/issues)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/RobTillaart/AGS02MA/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/release/RobTillaart/AGS02MA.svg?maxAge=3600)](https://github.com/RobTillaart/AGS02MA/releases)
[![PlatformIO Registry](https://badges.registry.platformio.org/packages/robtillaart/library/AGS02MA.svg)](https://registry.platformio.org/libraries/robtillaart/AGS02MA)
# AGS02MA
Arduino library for AGS02MA TVOC sensor.
### Description
**Experimental**
This library is experimental, so please use with care.
The AGS02MA is a sensor that measures the TVOC = Total Volatile Organic Compounds
in the air. It does not measure a specific gas, but several.
Note the warning about the I2C low speed, the device works at max 30 KHz.
Since 0.3.1 this library uses 25 KHz.
Feedback as always, is welcome. Please open an issue.
Note this library is **not** meant to replace professional monitoring systems.
### 0.4.0 Breaking change
Version 0.4.0 introduced a breaking change.
You cannot set the pins in **begin()** any more.
This reduces the dependency of processor dependent Wire implementations.
The user has to call **Wire.begin()** and can optionally set the Wire pins
before calling **begin()**.
### Related
- https://github.com/RobTillaart/AGS02MA TVOC sensor
- https://github.com/RobTillaart/AGS2616 H2 sensor
- https://github.com/RobTillaart/AGS3870 CH4 sensor
- https://github.com/RobTillaart/AGS3871 CO sensor
- https://www.renesas.com/us/en/document/whp/overview-tvoc-and-indoor-air-quality
- https://github.com/RobTillaart/SGP30 (experimental)
- https://github.com/RobTillaart/AtomicWeight (determine Mass from chemical formula)
- https://github.com/RobTillaart/map2colour
## I2C
### Pin layout from left to right
Always check datasheet!
| Front L->R | Description |
|:------------:|:--------------|
| pin 1 | VDD +5V |
| pin 2 | SDA data |
| pin 3 | GND |
| pin 4 | SCL clock |
### Address
The device has a fixed address of 26 or 0x1A.
The device works at 100 kHz I2C bus speed (datasheet).
Note: several AGS devices use the same I2C address 0x1A.
Known are the AGS2616 (H2), AGS3870 (CH4), AGS3871 (CO), AGS02MA (TVOC).
If you want to use them on one I2C bus, you need multiplexing.
See section below.
### WARNING - LOW SPEED
The sensor uses I2C at very low speed <= 30 KHz.
For an Arduino UNO the lowest speed supported is about 30.4KHz (TWBR = 255) which works
in my testruns so far.
First runs with Arduino UNO indicate 2 failed reads in > 500 Reads, so less than 1%
failure rate.
Tests with ESP32 / ESP8266 at 30 KHz look good,
tests with ESP32 at lower clock speeds are to be done but expected to work.
The library sets the clock speed to 30 KHz (for non AVR) during operation
and resets the I2C clock speed default to 100 KHz after operation.
This is done to minimize interference with the communication of other devices.
The "reset clock speed" can be changed with **setI2CResetSpeed(speed)** e.g. to 200 or 400 KHz.
### 0.3.1 fix.
Version 0.3.1 sets the **I2C prescaler TWSR** register of the Arduino UNO to 4 so the lowest
speed possible is reduced to about 8 KHz.
A test run 4 hours with 6000++ reads on an UNO at 25 KHz gave 0 errors.
So the communication speed will be set to 25 KHz, also for other boards, for stability.
After communication the I2C clock (+ prescaler) is reset again as before.
### I2C multiplexing
Sometimes you need to control more devices than possible with the default
address range the device provides.
This is possible with an I2C multiplexer e.g. TCA9548 which creates up
to eight channels (think of it as I2C subnets) which can use the complete
address range of the device.
Drawback of using a multiplexer is that it takes more administration in
your code e.g. which device is on which channel.
This will slow down the access, which must be taken into account when
deciding which devices are on which channel.
Also note that switching between channels will slow down other devices
too if they are behind the multiplexer.
- https://github.com/RobTillaart/TCA9548
## Version 118 problems
The library can request the version with **getSensorVersion()**.
My devices all report version 117 and this version is used to develop / test this library.
There are devices reported with version 118 which behave differently.
### ugM3 not supported
See - https://github.com/RobTillaart/AGS02MA/issues/11
The version 118 seems only to support the **PPB** and not the **ugM3** mode.
It is unclear if this is an incident, bug or a structural change in the firmware.
If you encounter similar problems with setting the mode (any version), please let me know.
That will help indicating if this is a "structural change" or incident.
### Calibrate problem!
See - https://github.com/RobTillaart/AGS02MA/issues/13
In this issue a problem is reported with a version 118 sensor.
The problem exposed itself after running the calibration sketch (command).
The problem has been confirmed by a 2nd version 118 sensor.
Additional calibration runs did not fix the problem.
Version 117 seem to have no problems with calibration.
**Advice**: do **NOT** calibrate a version 118.
Note: the version 0.2.0 determines the version in the calibration function so
it won't calibrate any non 117 version.
### Please report your experiences.
If you have a AGS20MA device, version 117 or 118 or other,
please let me know your experiences
with the sensor and this (or other) library.
## Interface
```cpp
#include "AGS02MA.h"
```
### Constructor
- **AGS02MA(uint8_t deviceAddress = 26, TwoWire \*wire = &Wire)** constructor,
with default address and default I2C interface.
- **bool begin()** initialize the library.
Returns false if deviceAddress cannot be seen on the I2C bus.
- **bool isConnected()** returns true if deviceAddress can be seen on I2C, false otherwise.
- **void reset()** resets the internal variables.
### Heating
- **bool isHeated()** returns true if 2 minutes have passed after call of **begin()**.
Otherwise the device is not optimal ready.
According to the datasheet, preheating will improve the quality of the measurements.
Note: if begin() is not called, isHeated() might be incorrect.
- **uint32_t lastRead()** returns the last time the device is read,
timestamp is in milliseconds since start.
Returns 0 if **readPPB()** or **readUGM3()** is not called yet.
This function allows to implement sort of asynchronous wait.
One must keep reads / measurements at least 1.5 seconds but preferred 3 seconds
apart according to the datasheet.
### Administration
- **bool setAddress(const uint8_t deviceAddress)** sets a new address for the sensor.
If function succeeds the address changes immediately and will be persistent over a reboot.
- **uint8_t getAddress()** returns the set address. Default the function will return 26 or 0x1A.
- **uint8_t getSensorVersion()** reads sensor version from the device.
If the version cannot be read the function will return 255.
My test sensors all return version 117, version 118 is reported to exist too.
- **uint32_t getSensorDate()** (experimental) reads bytes from the sensor that seem
to indicate the production date(?).
This date is encoded in an uint32_t to minimize footprint as it is a debug function.
```cpp
uint32_t dd = sensor.getSensorDate();
Serial.println(dd, HEX); // prints YYYYMMDD e.g. 20210203
```
### I2C clock speed
The library sets the clock speed to 25 KHz during operation
and resets it to 100 KHz after operation.
This is done to minimize interference with the communication of other devices.
The following function can change the I2C reset speed to e.g. 200 or 400 KHz.
- **void setI2CResetSpeed(uint32_t speed)** sets the I2C speed the library need to reset the I2C speed to.
- **uint32_t getI2CResetSpeed()** returns the value set. Default is 100 KHz.
### setMode
The default mode at startup of the sensor is PPB = parts per billion.
- **bool setPPBMode()** sets device in PartPerBillion mode. Returns true on success.
- **bool setUGM3Mode()** sets device in micro gram per cubic meter mode. Returns true on success.
- **uint8_t getMode()** returns mode set. 0 = PPB, 1 = UGm3, 255 = not set.
### Air quality classification
Indicative description and colour representation.
| TVOC(ppb) | Scale | Description | Colour | Notes |
|:---------:|:-------:|:---------------------:|:-------------|:--------|
| <= 220 | 1 | Good | Green |
| <= 660 | 3 | Moderate | Yellow |
| <= 1430 | 7 | Bad | Orange |
| <= 2200 | 10 | Unhealthy | Red |
| <= 3300 | 15 | Very unhealthy | Purple | add pulsating effect
| <= 5500 | 25 | Hazardous | Deep Purple | add pulsating effect
| > 5500 | 50 | Extremely Hazardous | Deep Purple | add pulsating effect
[Source](https://learn.kaiterra.com/en/resources/understanding-tvoc-volatile-organic-compounds)
- Scale is a relative (linear) scale where 220 ~~ 1
- Colour is an indicative colour mapping.
- https://github.com/RobTillaart/map2colour for continuous colour scale mapping.
### PPB versus UGM3
There is no 1 to 1 relation between the PPB and the uG/m3 readings as this relation depends
on the weight of the individual molecules.
PPB is therefore an more an absolute indicator where uG/m3 is sort of relative indicator.
If the gas is unknown, PPB is in my opinion the preferred measurement.
From an unverified source the following formula:
M = molecular weight of the gas.
**μg/m3 = ppb \* M \* 12.187 / (273.15 + °C)**
Simplified formula for 1 atm @ 25°C:
**μg/m3 = ppb \* M \* 0.04087539829**
Some known gasses
| gas | Common name | ratio ppb-μg/m3 | molecular weight M |
|:-------|:--------------------|:----------------------|:--------------------:|
| SO2 | Sulphur dioxide | 1 ppb = 2.62 μg/m3 | 64 gr/mol |
| NO2 | Nitrogen dioxide | 1 ppb = 1.88 μg/m3 | 46 gr/mol |
| NO | Nitrogen monoxide | 1 ppb = 1.25 μg/m3 | 30 gr/mol |
| O3 | Ozone | 1 ppb = 2.00 μg/m3 | 48 gr/mol |
| CO | Carbon Monoxide | 1 ppb = 1.145 μg/m3 | 28 gr/mol |
| C6H6 | Benzene | 1 ppb = 3.19 μg/m3 | 78 gr/mol |
- https://github.com/RobTillaart/AtomicWeight (determine Mass from chemical formula)
### Read the sensor
WARNING: The datasheet advises to take 3 seconds between reads.
Tests gave stable results at 1.5 second intervals.
Use this faster rate at your own risk.
- **uint32_t readPPB()** reads the PPB (parts per billion) from the device.
Typical value should be between 1 .. 999999.
Returns **lastPPB()** value if failed so one does not get sudden jumps in graphs.
Check **lastStatus()** and **lastError()** to get more info about the success of the read().
Time needed is ~35 milliseconds (which might cause problems).
- **uint32_t readUGM3()** reads UGM3 (microgram per cubic meter) current value from device.
Typical values depend on the molecular weight of the TVOC.
Returns **lastUGM3()** if failed so one does not get sudden jumps in graphs.
Wrappers
- **float readPPM()** returns parts per million (PPM).
This function is a wrapper around readPPB().
Typical value should be between 0.01 .. 999.99
- **float readMGM3()** returns milligram per cubic meter.
- **float readUGF3()** returns microgram per cubic feet.
### Error Codes
| ERROR_CODES | value |
|:----------------------------|:-------:|
| AGS02MA_OK | 0 |
| AGS02MA_ERROR | -10 |
| AGS02MA_ERROR_CRC | -11 |
| AGS02MA_ERROR_READ | -12 |
| AGS02MA_ERROR_NOT_READY | -13 |
### Cached values
- **float lastPPM()** returns last readPPM (parts per million) value (cached).
- **uint32_t lastPPB()** returns last read PPB (parts per billion) value (cached). Should be between 1..999999.
- **uint32_t lastUGM3()** returns last read UGM3 (microgram per cubic meter) value (cached).
### Calibration
- **bool zeroCalibration()** to be called after at least 5 minutes in fresh air.
See example sketch.
- **bool manualZeroCalibration(uint16_t value = 0)** Set the zero calibration value manually.
To be called after at least 5 minutes in fresh air.
- For v117: 0-65535 = automatic calibration.
- For v118: 0 = automatic calibration, 1-65535 manual calibration.
- **bool getZeroCalibrationData(ZeroCalibrationData &data)** fills a data struct with the
current zero calibration status and value.
Returns true on success.
### Other
- **bool readRegister(uint8_t address, RegisterData &reg)** fills a data struct with the chip's register data at that address.
Primarily intended for troubleshooting and analysis of the sensor. Not recommended to build applications on top of this method's raw data.
Returns true when the **RegisterData** is filled, false when the data could not be read.
Note: unlike other public methods, CRC errors don't return false or show up in `lastError()`,
instead the CRC result is stored in `RegisterData.crcValid`.
- **int lastError()** returns last error.
- **uint8_t lastStatus()** returns status byte from last read.
Read datasheet or table below for details. A new read is needed to update this.
- **uint8_t dataReady()** returns RDY bit from last read.
### Status bits.
| bit | description | notes |
|:-----:|:------------------------------------|:--------|
| 7-4 | internal use |
| 3-1 | 000 = PPB 001 = uG/M3 |
| 0 | RDY bit 0 = ready 1 = not ready | 1 == busy
## Future
#### Must
- improve documentation
- references?
#### Should
- check the mode bits of the status byte with internal \_mode.
- maximize robustness of state
- test with hardware
- different gasses ? indoor / outdoor?
- test with different processors
- isHeated() bugs if begin() is not called before...
#### Could
- elaborate error handling.
- create an async interface for **readPPB()** if possible
- delay(30) blocks performance ==> async version of **readRegister()**
- could introduce complex I2C speed handling...
- separate state - request pending or so?
- move code to .cpp?
#### Wont
## Support
If you appreciate my libraries, you can support the development and maintenance.
Improve the quality of the libraries by providing issues and Pull Requests, or
donate through PayPal or GitHub sponsors.
Thank you,

View File

@ -0,0 +1,74 @@
//
// FILE: AGS02MA_PPB.ino
// AUTHOR: Rob Tillaart
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
#include "AGS02MA.h"
AGS02MA AGS(26);
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t");
Serial.println(b);
Serial.print("VERSION:\t");
Serial.println(AGS.getSensorVersion());
// pre-heating improves measurement quality
// can be skipped
Serial.println("\nWarming up (120 seconds = 24 dots)");
while (AGS.isHeated() == false)
{
delay(5000);
Serial.print(".");
}
Serial.println();
b = AGS.setPPBMode();
uint8_t m = AGS.getMode();
Serial.print("MODE:\t");
Serial.print(b);
Serial.print("\t");
Serial.println(m);
uint8_t version = AGS.getSensorVersion();
Serial.print("VERS:\t");
Serial.println(version);
}
void loop()
{
delay(3000);
uint32_t value = AGS.readPPB();
Serial.print("PPB:\t");
Serial.print(value);
Serial.print("\t");
Serial.print(AGS.lastStatus(), HEX);
Serial.print("\t");
Serial.print(AGS.lastError(), HEX);
Serial.println();
}
// -- END OF FILE --

View File

@ -0,0 +1,75 @@
//
// FILE: AGS02MA_PPB_TIMING.ino
// AUTHOR: Rob Tillaart
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
//
#include "AGS02MA.h"
AGS02MA AGS(26);
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t");
Serial.println(b);
Serial.print("VERS:\t");
Serial.println(AGS.getSensorVersion());
// pre-heating improves measurement quality
// can be skipped
Serial.println("\nWarming up (120 seconds = 24 dots)");
while (AGS.isHeated() == false)
{
delay(5000);
Serial.print(".");
}
Serial.println();
b = AGS.setPPBMode();
uint8_t m = AGS.getMode();
Serial.print("MODE:\t");
Serial.print(b);
Serial.print("\t");
Serial.println(m);
}
void loop()
{
delay(1500);
uint32_t start = millis();
uint32_t value = AGS.readPPB();
uint32_t stop = millis();
uint32_t duration = stop - start;
Serial.print(duration);
Serial.print("\t");
Serial.print(value);
Serial.print("\t");
Serial.print(AGS.lastStatus(), HEX);
Serial.print("\t");
Serial.print(AGS.lastError(), HEX);
Serial.println();
}
// -- END OF FILE --

View File

@ -0,0 +1,29 @@
30 KHz
16:14:33.046 -> ...\AGS02MA_PPB_TIMING.ino
16:14:33.046 -> AGS02MA_LIB_VERSION: 0.3.0
16:14:33.046 ->
16:14:33.046 -> BEGIN: 1
16:14:33.046 -> VERS: 117
16:14:33.093 ->
16:14:33.093 -> Warming up (120 seconds = 24 dots)
16:14:38.107 -> ........................
16:16:33.243 -> MODE: 1 0
16:16:34.790 -> 32 549 10 0
16:16:36.325 -> 32 547 10 0
16:16:37.872 -> 31 545 10 0
16:16:39.418 -> 32 545 10 0
16:16:40.922 -> 33 544 10 0
16:16:42.469 -> 33 543 10 0
16:16:44.015 -> 33 542 10 0
16:16:45.562 -> 33 543 10 0
16:16:47.062 -> 33 536 10 0
16:16:48.608 -> 33 541 10 0
16:16:50.155 -> 33 537 10 0
16:16:51.701 -> 32 537 10 0
16:16:53.201 -> 33 536 10 0
16:16:54.747 -> 33 535 10 0
16:16:56.294 -> 33 534 10 0
16:16:57.810 -> 32 533 10 0
16:16:59.357 -> 32 531 10 0

View File

@ -0,0 +1,27 @@
10 KHz (TWBR 198, TWSR 1) just a test
16:08:08.836 -> ...\AGS02MA_PPB_TIMING.ino
16:20:01.824 -> AGS02MA_LIB_VERSION: 0.3.1
16:20:01.824 ->
16:20:01.824 -> BEGIN: 1
16:20:01.824 -> VERS: 117
16:20:01.871 ->
16:20:01.871 -> Warming up (120 seconds = 24 dots)
16:20:06.885 -> ........................
16:22:02.077 -> MODE: 1 0
16:22:03.624 -> 38 476 10 0
16:22:05.165 -> 38 474 10 0
16:22:06.712 -> 38 471 10 0
16:22:08.247 -> 38 474 10 0
16:22:09.794 -> 38 471 10 0
16:22:11.325 -> 38 472 10 0
16:22:12.871 -> 38 468 10 0
16:22:14.418 -> 37 465 10 0
16:22:15.917 -> 38 463 10 0
16:22:17.464 -> 38 463 10 0
16:22:19.016 -> 38 459 10 0
16:22:20.547 -> 37 460 10 0
16:22:22.094 -> 36 459 10 0
16:22:23.640 -> 38 461 10 0

View File

@ -0,0 +1,32 @@
25 KHz
16:08:08.836 -> ...\AGS02MA_PPB_TIMING.ino
16:08:08.836 -> AGS02MA_LIB_VERSION: 0.3.1
16:08:08.836 ->
16:08:08.836 -> BEGIN: 1
16:08:08.836 -> VERS: 117
16:08:08.929 ->
16:08:08.929 -> Warming up (120 seconds = 24 dots)
16:08:13.944 -> ........................
16:10:09.086 -> MODE: 1 0
16:10:10.633 -> 33 845 10 0
16:10:12.179 -> 32 846 10 0
16:10:13.726 -> 32 847 10 0
16:10:15.225 -> 33 847 10 0
16:10:16.772 -> 32 847 10 0
16:10:18.318 -> 33 841 10 0
16:10:19.849 -> 33 835 10 0
16:10:21.396 -> 34 829 10 0
16:10:22.895 -> 33 825 10 0
16:10:24.442 -> 33 828 10 0
16:10:25.988 -> 33 823 10 0
16:10:27.535 -> 34 824 10 0
16:10:29.081 -> 33 821 10 0
16:10:30.581 -> 33 821 10 0
16:10:32.127 -> 33 816 10 0
16:10:33.674 -> 34 826 10 0
16:10:35.221 -> 33 839 10 0
16:10:36.720 -> 33 855 10 0
16:10:38.267 -> 33 857 10 0
16:10:39.813 -> 33 857 10 0

View File

@ -0,0 +1,71 @@
//
// FILE: AGS02MA_PPM.ino
// AUTHOR: Rob Tillaart
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
//
#include "AGS02MA.h"
AGS02MA AGS(26);
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t");
Serial.println(b);
Serial.print("VERSION:\t");
Serial.println(AGS.getSensorVersion());
// pre-heating improves measurement quality
// can be skipped
Serial.println("\nWarming up (120 seconds = 24 dots)");
while (AGS.isHeated() == false)
{
delay(5000);
Serial.print(".");
}
Serial.println();
b = AGS.setPPBMode();
uint8_t m = AGS.getMode();
Serial.print("MODE:\t");
Serial.print(b);
Serial.print("\t");
Serial.println(m);
}
void loop()
{
delay(3000);
Serial.print("PPM:\t");
Serial.print(AGS.readPPM(), 3);
Serial.print("\t");
Serial.print(AGS.dataReady(), HEX);
Serial.print("\t");
Serial.print(AGS.lastStatus(), HEX);
Serial.print("\t");
Serial.print(AGS.lastError(), HEX);
Serial.println();
}
// -- END OF FILE --

View File

@ -0,0 +1,71 @@
//
// FILE: AGS02MA_UGM3.ino
// AUTHOR: Rob Tillaart
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
#include "AGS02MA.h"
AGS02MA AGS(26);
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t");
Serial.println(b);
// pre-heating improves measurement quality
// can be skipped
Serial.println("\nWarming up (120 seconds = 24 dots)");
while (AGS.isHeated() == false)
{
delay(5000);
Serial.print(".");
}
Serial.println();
b = AGS.setUGM3Mode();
uint8_t m = AGS.getMode();
Serial.print("MODE:\t");
Serial.print(b);
Serial.print("\t");
Serial.println(m);
uint8_t version = AGS.getSensorVersion();
Serial.print("VERS:\t");
Serial.println(version);
}
void loop()
{
delay(3000);
uint32_t value = AGS.readUGM3();
Serial.print("UGM3:\t");
Serial.print(value);
Serial.print("\t");
Serial.print(AGS.lastStatus(), HEX);
Serial.print("\t");
Serial.print(AGS.lastError(), HEX);
Serial.println();
}
// -- END OF FILE --

View File

@ -0,0 +1,164 @@
//
// FILE: AGS02MA_calibrate.ino
// AUTHOR: Rob Tillaart, Beanow
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
#include "AGS02MA.h"
// You can decrease/disable warmup when you're certain the chip already warmed up.
#define WARMUP_MINUTES 6
#define READ_INTERVAL 3000
uint32_t start, stop;
uint8_t version;
AGS02MA AGS(26);
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Serial.print("WARMUP:\t\t");
Serial.println(WARMUP_MINUTES);
Serial.print("INTERVAL:\t");
Serial.println(READ_INTERVAL);
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t\t");
Serial.println(b);
Serial.print("VERSION:\t");
version = AGS.getSensorVersion();
Serial.println(version);
int err = AGS.lastError();
// Reading version correctly matters, as we display additional comments based on it.
if(err != AGS02MA_OK)
{
Serial.print("Error reading version:\t");
Serial.println(err);
Serial.println("Won't attempt to calibrate. Reset when connection with the sensor is stable.");
Serial.println();
return;
}
b = AGS.setPPBMode();
uint8_t m = AGS.getMode();
Serial.print("MODE:\t\t");
Serial.print(b);
Serial.print("\t");
Serial.println(m);
Serial.println();
Serial.print("Place the device outside in open air for ");
Serial.print(WARMUP_MINUTES);
Serial.println(" minute(s).");
Serial.println("Make sure your device has warmed up sufficiently for the best results.");
Serial.println("The PPB values should be stable (may include noise) not constantly decreasing.");
Serial.println();
start = millis();
stop = WARMUP_MINUTES * 60000UL;
while(millis() - start < stop)
{
Serial.print("[PRE ]\t");
printPPB();
delay(READ_INTERVAL);
}
Serial.println();
Serial.println("About to perform calibration now.");
AGS02MA::ZeroCalibrationData initialValue;
if (!AGS.getZeroCalibrationData(initialValue))
{
Serial.print("Error reading zero calibration data:\t");
Serial.println(AGS.lastError());
Serial.println("Won't attempt to calibrate. Reset when connection with the sensor is stable.");
Serial.println();
return;
}
Serial.println("Your previous calibration data was:");
printZeroCalibrationData(initialValue);
delay(1000);
// returns 1 if successful written
b = AGS.zeroCalibration();
Serial.println();
Serial.print("CALIB:\t");
Serial.println(b);
Serial.println();
Serial.println("Calibration done.");
AGS02MA::ZeroCalibrationData zc;
while (!AGS.getZeroCalibrationData(zc))
{
Serial.print("Error:\t");
Serial.print(AGS.lastError());
Serial.println("\tretrying...");
delay(READ_INTERVAL);
}
Serial.println("Your new calibration data is:");
printZeroCalibrationData(zc);
Serial.println();
Serial.println("Showing what PPB values look like post calibration.");
// A 125 status is typically shown on v118's after they've been powered off.
// Either having this version at all, or seeing this status, we'll display a notice.
if (version == 118 || initialValue.status == 125)
{
Serial.println("NOTICE: v118 sensors are known to give different results after powering off!");
Serial.println("You may need to manually set your calibration value every time power was lost.");
}
Serial.println();
}
void loop()
{
Serial.print("[POST]\t");
printPPB();
delay(READ_INTERVAL);
}
void printZeroCalibrationData(AGS02MA::ZeroCalibrationData &zc) {
Serial.print("Status:\t");
Serial.println(zc.status);
Serial.print("Value:\t");
Serial.println(zc.value);
}
void printPPB()
{
uint32_t value = AGS.readPPB();
Serial.print("PPB:\t");
Serial.print(value);
Serial.print("\t");
Serial.print(AGS.lastStatus(), HEX);
Serial.print("\t");
Serial.print(AGS.lastError(), HEX);
Serial.println();
}
// -- END OF FILE --

View File

@ -0,0 +1,172 @@
//
// FILE: AGS02MA_calibrate_manual.ino
// AUTHOR: Rob Tillaart, Beanow
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
#include "AGS02MA.h"
// The zero calibration value we'll (temporarily) set in the example.
#define ZC_VALUE 700
#define READS 10
#define INTERVAL 3000
AGS02MA AGS(26);
AGS02MA::ZeroCalibrationData initialValue;
uint8_t version;
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Serial.print("READS:\t\t");
Serial.println(READS);
Serial.print("INTERVAL:\t");
Serial.println(INTERVAL);
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t\t");
Serial.println(b);
Serial.print("VERSION:\t");
version = AGS.getSensorVersion();
Serial.println(version);
int err = AGS.lastError();
// Reading version correctly matters, as we display additional comments based on it.
if(err != AGS02MA_OK)
{
Serial.print("Error reading version:\t");
Serial.println(err);
Serial.println("Won't attempt to calibrate. Reset when connection with the sensor is stable.");
Serial.println();
return;
}
if (version != 118)
{
Serial.println();
Serial.println("Only v118 sensors support manual zero calibration. For other versions, you can use the 'AGS02MA_calibrate' example instead.");
}
else
{
b = AGS.setPPBMode();
uint8_t m = AGS.getMode();
Serial.print("MODE:\t\t");
Serial.print(b);
Serial.print("\t");
Serial.println(m);
while (!AGS.getZeroCalibrationData(initialValue))
{
onError(AGS.lastError());
}
Serial.println();
Serial.println("Your initial zero calibration is:");
printZeroCalibrationData(initialValue);
Serial.println();
Serial.println("Showing sample data before changing.");
for (size_t i = 0; i < READS; i++)
{
delay(INTERVAL);
printPPB();
}
Serial.println();
Serial.println("Manually setting zero calibration:");
b = AGS.manualZeroCalibration(ZC_VALUE);
Serial.print("CALIB:\t");
Serial.println(b);
AGS02MA::ZeroCalibrationData newValue;
while (!AGS.getZeroCalibrationData(newValue))
{
onError(AGS.lastError());
}
printZeroCalibrationData(newValue);
Serial.println();
Serial.println("Showing sample data.");
Serial.println("NOTICE: v118 sensors are known to give different results after powering off!");
Serial.println("You may need to manually set your calibration value every time power was lost.");
for (size_t i = 0; i < READS; i++)
{
delay(INTERVAL);
printPPB();
}
Serial.println();
Serial.println("Restoring initial zero calibration:");
b = AGS.manualZeroCalibration(initialValue.value);
Serial.print("CALIB:\t");
Serial.println(b);
AGS02MA::ZeroCalibrationData restoredValue;
while (!AGS.getZeroCalibrationData(restoredValue))
{
onError(AGS.lastError());
}
printZeroCalibrationData(restoredValue);
Serial.println();
}
}
void loop()
{
delay(INTERVAL);
printPPB();
}
void onError(int err) {
Serial.print("Error:\t");
Serial.print(err);
Serial.println("\tretrying...");
delay(INTERVAL);
}
void printZeroCalibrationData(AGS02MA::ZeroCalibrationData &zc) {
Serial.print("Status:\t");
Serial.println(zc.status);
Serial.print("Value:\t");
Serial.println(zc.value);
}
void printPPB()
{
uint32_t value = AGS.readPPB();
Serial.print("PPB:\t");
Serial.print(value);
Serial.print("\t");
Serial.print(AGS.lastStatus(), HEX);
Serial.print("\t");
Serial.print(AGS.lastError(), HEX);
Serial.println();
}
// -- END OF FILE --

View File

@ -0,0 +1,28 @@
platforms:
rpipico:
board: rp2040:rp2040:rpipico
package: rp2040:rp2040
gcc:
features:
defines:
- ARDUINO_ARCH_RP2040
warnings:
flags:
packages:
rp2040:rp2040:
url: https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
compile:
# Choosing to run compilation tests on 2 different Arduino platforms
platforms:
# - uno
# - due
# - zero
# - leonardo
# - m4
# - esp32
# - esp8266
# - mega2560
# - rpipico

View File

@ -0,0 +1,108 @@
//
// FILE: AGS02MA_get_registers.ino
// AUTHOR: Rob Tillaart
// PURPOSE: low level develop application
// URL: https://github.com/RobTillaart/AGS02MA
//
// PURPOSE: this is a debugging tool for developing / investigating.
// Do not use it unless you are willing to crash your sensor.
//
// usage: make _readRegister(), _writeRegister() and _buffer public
//
// USE AT OWN RISK
#include "AGS02MA.h"
AGS02MA AGS(26);
void setup()
{
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t");
Serial.println(b);
uint8_t version = AGS.getSensorVersion();
Serial.print("VERS:\t");
Serial.println(version);
// AGS._buffer[0] = 0;
// AGS._buffer[1] = 13;
// AGS._buffer[2] = 14;
// AGS._buffer[3] = 90;
// AGS._buffer[5] = 248;
// int x = AGS._writeRegister(0x01); // does not work.
// Serial.println(x);
for (uint8_t reg = 0; reg < 9; reg++)
{
dumpRegister(reg);
}
dumpRegister(0x11);
dumpRegister(0x20);
dumpRegister(0x21);
AGS.setPPBMode();
}
void loop()
{
// dumpRegister(0);
// uint32_t zero = dumpRegister(1);
// uint32_t x = dumpRegister(0x20); // seems to be the raw value.
// delay(100);
// uint32_t y = AGS.readPPB();
// Serial.print(zero);
// Serial.print("\t");
// Serial.print(x);
// Serial.print("\t");
// Serial.print(y);
// Serial.print("\t");
// Serial.print((1.0 * x) / y, 2);
// Serial.print("\t");
// Serial.print((zero - x) / 350);
// Serial.println();
// Serial.println();
// delay(2000);
}
uint32_t dumpRegister(uint8_t reg)
{
Serial.print("REG[");
Serial.print(reg);
Serial.print("]");
bool b = AGS._readRegister(reg);
uint32_t value = 0;
for (int i = 0; i < 4; i++)
{
Serial.print("\t");
Serial.print(AGS._buffer[i]);
value *= 256;
value += AGS._buffer[i];
}
Serial.print("\t");
Serial.print(value);
Serial.println();
delay(100);
return value;
}
// -- END OF FILE --

View File

@ -0,0 +1,59 @@
//
// FILE: AGS02MA_minimal.ino
// AUTHOR: Rob Tillaart
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
//
// default register is 0x00 at start of the sensor
// datasheet states one can get the value with minimal interaction.
// note this sketch does not use the library!
#include "Wire.h"
uint8_t buffer[5];
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
// Serial.print("AGS02MA_LIB_VERSION: ");
// Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
Wire.setClock(30400); // lowest speed an UNO supports that works with sensor.
}
void loop()
{
delay(3000);
Wire.requestFrom(26, 5);
for ( int i = 0; i < 5; i++)
{
buffer[i] = Wire.read();
// Serial.print(buffer[i], HEX); // for debugging.
// Serial.print('\t');
}
Serial.println();
// CONVERT RAW DATA
Serial.print("STAT:\t");
Serial.println(buffer[0]);
Serial.print("PPB:\t");
Serial.println(buffer[1] * 65536UL + buffer[2] * 256 + buffer[3]);
Serial.print("CRC:\t");
Serial.println(buffer[4]);
Serial.println();
}
// -- END OF FILE --

View File

@ -0,0 +1,69 @@
//
// FILE: AGS02MA_minimal_plotter.ino
// AUTHOR: Rob Tillaart
// PURPOSE: test application not using the library
// URL: https://github.com/RobTillaart/AGS02MA
//
// default register is 0x00 at start of the sensor
// datasheet states one can get the value with minimal interaction.
// note this sketch does not use the library!
#include "Wire.h"
uint8_t buffer[5];
uint8_t cnt = 0;
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
// Serial.println();
// Serial.println(__FILE__);
// Serial.print("AGS02MA_LIB_VERSION: ");
// Serial.println(AGS02MA_LIB_VERSION);
// Serial.println();
Wire.begin();
Wire.setClock(30400); // lowest speed an UNO supports that works with sensor.
}
void loop()
{
Wire.requestFrom(26, 5);
for ( int i = 0; i < 5; i++)
{
buffer[i] = Wire.read();
// Serial.print(buffer[i], HEX); // for debugging.
// Serial.print('\t');
}
// Serial.println();
if (cnt == 0)
{
// CONVERT RAW DATA
Serial.println("\nSTAT\tPPB\tCRC");
cnt = 20;
}
cnt--;
if (buffer[0] == 0x10)
{
Serial.print(buffer[0]);
Serial.print("\t");
Serial.print(buffer[1] * 65536UL + buffer[2] * 256 + buffer[3]);
Serial.print("\t");
Serial.print(buffer[4]);
Serial.println();
delay(2000);
}
}
// -- END OF FILE --

View File

@ -0,0 +1,123 @@
//
// FILE: AGS02MA_readRegister.ino
// AUTHOR: Rob Tillaart, Beanow
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
#include "AGS02MA.h"
const uint8_t addresses[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 0x11, 0x20, 0x21};
AGS02MA::RegisterData reg;
AGS02MA AGS(26);
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t");
Serial.println(b);
}
void loop()
{
delay(3000);
for (auto address : addresses)
{
bool b = AGS.readRegister(address, reg);
Serial.print("REG[0x");
Serial.print(address, HEX);
Serial.print("]");
if(b)
{
printRegister(address, reg);
}
else
{
Serial.print("\tError:\t");
Serial.println(AGS.lastError());
}
delay(50);
}
Serial.println();
}
void printRegister(uint8_t address, AGS02MA::RegisterData &reg) {
// Raw bytes first for any register.
for (auto b : reg.data)
{
Serial.print("\t");
Serial.print(b);
}
Serial.print("\tCRC: ");
Serial.print(reg.crcValid ? "OK " : "ERR ");
Serial.print(reg.crc);
// Specific interpretations
switch (address)
{
case 0x00:
Serial.print("\tSensor data:\t");
Serial.print(reg.data[0]);
Serial.print("\t");
Serial.print(
(reg.data[1] << 16) +
(reg.data[2] << 8) +
reg.data[3]
);
break;
case 0x01:
case 0x02:
case 0x03:
case 0x04:
Serial.print("\tCalibration:\t");
Serial.print(
(reg.data[0] << 8) +
reg.data[1]
);
Serial.print("\t");
Serial.print(
(reg.data[2] << 8) +
reg.data[3]
);
break;
case 0x11:
Serial.print("\tVersion:\t");
Serial.print(reg.data[3]);
break;
case 0x21:
Serial.print("\tI2C address:\t0x");
Serial.print(reg.data[0], HEX);
break;
default:
break;
}
Serial.println();
}
// -- END OF FILE --

View File

@ -0,0 +1,60 @@
//
// FILE: AGS02MA_setAddress.ino
// AUTHOR: Rob Tillaart
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
#include "AGS02MA.h"
AGS02MA AGS(26);
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t");
Serial.println(b);
b = AGS.setAddress(42);
Serial.print("SET_ADDR:\t");
Serial.print(b, HEX);
Serial.print("\t");
Serial.print(AGS.lastStatus(), HEX);
Serial.print("\t");
Serial.print(AGS.lastError(), HEX);
Serial.println();
uint8_t addr = AGS.getAddress();
Serial.print("GET_ADDR:\t");
Serial.print(addr, HEX);
Serial.print("\t");
Serial.print(AGS.lastStatus(), HEX);
Serial.print("\t");
Serial.print(AGS.lastError(), HEX);
Serial.println();
Serial.println("\ndone...");
}
void loop()
{
}
// -- END OF FILE --

View File

@ -0,0 +1,55 @@
//
// FILE: AGS02MA_test.ino
// AUTHOR: Rob Tillaart
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
#include "AGS02MA.h"
AGS02MA AGS(26);
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t");
Serial.println(b);
b = AGS.setPPBMode();
uint8_t m = AGS.getMode();
Serial.print("MODE:\t");
Serial.print(b);
Serial.print("\t");
Serial.println(m);
uint8_t version = AGS.getSensorVersion();
Serial.print("VERS:\t");
Serial.println(version);
}
void loop()
{
delay(2000);
uint32_t value = AGS.readPPB();
Serial.print("PPB:\t");
Serial.println(value);
}
// -- END OF FILE --

View File

@ -0,0 +1,7 @@
compile:
# Choosing to run compilation tests on 2 different Arduino platforms
platforms:
# - uno
# - leonardo
# - due
# - zero

View File

@ -0,0 +1,76 @@
//
// FILE: AGS02MA_test_CRC8.ino
// AUTHOR: Rob Tillaart
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
//
// NOTE: this is a low level test for the communication / CRC
// to have this example to work,
// one need to make the _CRC8() and _buffer[]
// public in the AGS02MA.h file.
#include "AGS02MA.h"
AGS02MA AGS(26);
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t");
Serial.println(b);
uint8_t version = AGS.getSensorVersion();
Serial.print("VERS:\t");
Serial.println(version);
dump("getSensorVersion");
}
void loop()
{
delay(3000);
AGS.setPPBMode();
dump("MODE0");
AGS.readPPB();
dump("PPB");
delay(3000);
AGS.setUGM3Mode();
dump("MODE1");
AGS.readUGM3();
dump("UGM3");
}
void dump(char * str)
{
Serial.print(str);
for (int i = 0; i < 5; i++)
{
Serial.print("\t");
Serial.print(AGS._buffer[i], HEX);
}
Serial.print('\t');
Serial.print(AGS._CRC8(AGS._buffer, 4), HEX);
Serial.println();
}
// -- END OF FILE --

View File

@ -0,0 +1,105 @@
//
// FILE: AGS02MA_PPB.ino
// AUTHOR: Rob Tillaart
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
#include "AGS02MA.h"
AGS02MA AGS(26);
uint32_t rounds = 0;
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t");
Serial.println(b);
Serial.print("VERSION:\t");
Serial.println(AGS.getSensorVersion());
Serial.print("DATE:\t");
Serial.println(AGS.getSensorDate(), HEX);
// pre-heating improves measurement quality
// can be skipped
// Serial.println("\nWarming up (120 seconds = 24 dots)");
// while (AGS.isHeated() == false)
// {
// delay(5000);
// Serial.print(".");
// }
// Serial.println();
uint8_t version = AGS.getSensorVersion();
Serial.print("VERS:\t");
Serial.println(version);
}
void loop()
{
delay(3000);
uint8_t kind = rounds % 20;
// Switch mode every 10 and 20 rounds.
bool b;
if (kind == 0) {
b = AGS.setPPBMode();
uint8_t m = AGS.getMode();
Serial.print("MODE:\t");
Serial.print(b);
Serial.print("\t");
Serial.println(m);
} else if (kind == 10) {
b = AGS.setUGM3Mode();
uint8_t m = AGS.getMode();
Serial.print("MODE:\t");
Serial.print(b);
Serial.print("\t");
Serial.println(m);
}
// Read PPB in first half of a 20-round cycle.
if (kind < 10) {
uint32_t value = AGS.readPPB();
Serial.print("PPB:\t");
Serial.print(value);
Serial.print("\t");
Serial.print(AGS.lastStatus(), HEX);
Serial.print("\t");
Serial.print(AGS.lastError(), HEX);
Serial.println();
} else {
uint32_t value = AGS.readUGM3();
Serial.print("UGM3:\t");
Serial.print(value);
Serial.print("\t");
Serial.print(AGS.lastStatus(), HEX);
Serial.print("\t");
Serial.print(AGS.lastError(), HEX);
Serial.println();
}
rounds++;
}
// -- END OF FILE --

View File

@ -0,0 +1,7 @@
compile:
# Choosing to run compilation tests on 2 different Arduino platforms
platforms:
# - uno
# - leonardo
# - due
# - zero

View File

@ -0,0 +1,83 @@
//
// FILE: test_CRC8.ino
// AUTHOR: Rob Tillaart
// PURPOSE: test application
// URL: https://github.com/RobTillaart/AGS02MA
//
// just for develop scratch pad
// need to make the CRC function public in the library
#include "AGS02MA.h"
AGS02MA AGS(26);
void setup()
{
// ESP devices typically miss the first serial log lines after flashing.
// Delay somewhat to include all output.
delay(1000);
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("AGS02MA_LIB_VERSION: ");
Serial.println(AGS02MA_LIB_VERSION);
Serial.println();
Wire.begin();
bool b = AGS.begin();
Serial.print("BEGIN:\t");
Serial.println(b);
uint8_t _buffer[8];
_buffer[0] = 0x00;
_buffer[1] = 0x0C;
_buffer[2] = 0xFF;
_buffer[3] = 0xF3;
_buffer[4] = 0xFC;
Serial.println(AGS._CRC8(_buffer, 5), HEX);
_buffer[0] = 0x02;
_buffer[1] = 0xFD;
_buffer[2] = 0x02;
_buffer[3] = 0xFD;
_buffer[4] = 0x00;
Serial.println(AGS._CRC8(_buffer, 5), HEX);
_buffer[0] = 0x00;
_buffer[1] = 0xFF;
_buffer[2] = 0x00;
_buffer[3] = 0xFF;
_buffer[4] = 0x30;
Serial.println(AGS._CRC8(_buffer, 5), HEX);
_buffer[0] = 0x14;
_buffer[1] = 0x14;
_buffer[2] = 0x14;
_buffer[3] = 0x1C;
_buffer[4] = 0x75;
Serial.println(AGS._CRC8(_buffer, 5), HEX);
_buffer[0] = 0x0;
_buffer[1] = 0x0;
_buffer[2] = 0xBB;
Serial.println( _buffer[0] * 65536UL + _buffer[1] * 256 + _buffer[2]);
_buffer[0] = 0x0;
_buffer[1] = 0x01;
_buffer[2] = 0xAE;
Serial.println( _buffer[0] * 65536UL + _buffer[1] * 256 + _buffer[2]);
}
void loop()
{
}
// -- END OF FILE --

View File

@ -0,0 +1,60 @@
# Syntax Colouring Map for AGS02MA
# Data types (KEYWORD1)
AGS02MA KEYWORD1
# Methods and Functions (KEYWORD2)
begin KEYWORD2
isConnected KEYWORD2
reset KEYWORD2
isHeated KEYWORD2
setAddress KEYWORD2
getAddress KEYWORD2
getSensorVersion KEYWORD2
getSensorDate KEYWORD2
setI2CResetSpeed KEYWORD2
getI2CResetSpeed KEYWORD2
zeroCalibration KEYWORD2
manualZeroCalibration KEYWORD2
getZeroCalibrationData KEYWORD2
setPPBMode KEYWORD2
setUGM3Mode KEYWORD2
getMode KEYWORD2
readPPB KEYWORD2
readUGM3 KEYWORD2
readPPM KEYWORD2
readMGM3 KEYWORD2
readUGF3 KEYWORD2
lastPPM KEYWORD2
lastPPB KEYWORD2
lastUGM3 KEYWORD2
lastRead KEYWORD2
lastError KEYWORD2
lastStatus KEYWORD2
dataReady KEYWORD2
readRegister KEYWORD2
# Constants ( LITERAL1)
AGS02MA_LIB_VERSION LITERAL1
AGS02MA_OK LITERAL1
AGS02MA_ERROR LITERAL1
AGS02MA_ERROR_CRC LITERAL1
AGS02MA_ERROR_READ LITERAL1
AGS02MA_I2C_CLOCK LITERAL1
AGS02MA_ERROR_REQUEST LITERAL1

View File

@ -0,0 +1,29 @@
{
"name": "AGS02MA",
"keywords": "I2C, AGS02MA, tvoc",
"description": "Arduino library for AGS02MA - TVOC sensor.",
"authors":
[
{
"name": "Viktor Balint"
},
{
"name": "Rob Tillaart",
"email": "Rob.Tillaart@gmail.com",
"maintainer": true
},
{
"name": "Beanow"
}
],
"repository":
{
"type": "git",
"url": "https://github.com/RobTillaart/AGS02MA.git"
},
"version": "0.4.3",
"license": "MIT",
"frameworks": "*",
"platforms": "*",
"headers": "AGS02MA.h"
}

View File

@ -0,0 +1,11 @@
name=AGS02MA
version=0.4.3
author=Rob Tillaart <rob.tillaart@gmail.com>
maintainer=Rob Tillaart <rob.tillaart@gmail.com>
sentence=Arduino library for AGS02MA - TVOC sensor
paragraph=Note it uses slow I2C < 30KHz. See readme.md
category=Sensors
url=https://github.com/RobTillaart/AGS02MA.git
architectures=*
includes=AGS02MA.h
depends=

View File

@ -0,0 +1,102 @@
//
// FILE: unit_test_001.cpp
// AUTHOR: Rob Tillaart
// DATE: 2021-08-12
// PURPOSE: unit tests for the AGS02MA TVOC sensor
// https://github.com/RobTillaart/AGS02MA
// https://github.com/Arduino-CI/arduino_ci/blob/master/REFERENCE.md
//
// supported assertions
// ----------------------------
// assertEqual(expected, actual)
// assertNotEqual(expected, actual)
// assertLess(expected, actual)
// assertMore(expected, actual)
// assertLessOrEqual(expected, actual)
// assertMoreOrEqual(expected, actual)
// assertTrue(actual)
// assertFalse(actual)
// assertNull(actual)
#include <ArduinoUnitTests.h>
#include "Arduino.h"
#include "AGS02MA.h"
// writing to a virtual device does not work
// as millis() function is not implemented in
// the Arduino-CI environment
unittest_setup()
{
fprintf(stderr, "AGS02MA_LIB_VERSION: %s\n", (char *) AGS02MA_LIB_VERSION);
}
unittest_teardown()
{
}
unittest(test_constants)
{
assertEqual( 0, AGS02MA_OK);
assertEqual(-10, AGS02MA_ERROR);
assertEqual(-11, AGS02MA_ERROR_CRC);
assertEqual(-12, AGS02MA_ERROR_READ);
assertEqual(-13, AGS02MA_ERROR_NOT_READY);
assertEqual(-14, AGS02MA_ERROR_REQUEST);
assertEqual(25000, AGS02MA_I2C_CLOCK);
}
unittest(test_base)
{
AGS02MA AGS(26);
Wire.begin();
assertTrue(AGS.begin());
assertTrue(AGS.isConnected()); // TODO - GODMODE
assertFalse(AGS.isHeated());
assertEqual(0, AGS.lastRead());
assertEqual(26, AGS.getAddress());
//assertTrue(AGS.setAddress(42));
//assertEqual(42, AGS.getAddress());
assertEqual(100000, AGS.getI2CResetSpeed());
AGS.setI2CResetSpeed(400000);
assertEqual(400000, AGS.getI2CResetSpeed());
assertEqual(0, AGS.lastError());
assertEqual(0, AGS.lastStatus());
}
unittest(test_mode)
{
AGS02MA AGS(26);
Wire.begin();
assertTrue(AGS.begin());
assertTrue(AGS.isConnected()); // TODO - GODMODE
assertEqual(255, AGS.getMode());
// assertTrue(AGS.setPPBMode());
// assertEqual(0, AGS.getMode());
// assertTrue(AGS.setUGM3Mode());
// assertEqual(1, AGS.getMode());
}
unittest_main()
// -- END OF FILE --

View File

@ -807,6 +807,8 @@
// #define USE_RX8030 // [I2cDriver90] Enable RX8030 RTC - used by #23855 - support both I2C buses on ESP32 (I2C address 0x32) (+0k7 code) // #define USE_RX8030 // [I2cDriver90] Enable RX8030 RTC - used by #23855 - support both I2C buses on ESP32 (I2C address 0x32) (+0k7 code)
// #define USE_PCF85063 // [I2cDriver92] Enable PCF85063 RTC support (I2C address 0x51) // #define USE_PCF85063 // [I2cDriver92] Enable PCF85063 RTC support (I2C address 0x51)
// #define USE_AGS02MA // [I2cDriver95] Enable AGS02MA Air Quality Sensor (I2C address 0x1A)
// #define USE_DISPLAY // Add I2C/TM1637/MAX7219 Display Support (+2k code) // #define USE_DISPLAY // Add I2C/TM1637/MAX7219 Display Support (+2k code)
#define USE_DISPLAY_MODES1TO5 // Enable display mode 1 to 5 in addition to mode 0 #define USE_DISPLAY_MODES1TO5 // Enable display mode 1 to 5 in addition to mode 0
#define USE_DISPLAY_LCD // [DisplayModel 1] [I2cDriver3] Enable Lcd display (I2C addresses 0x27 and 0x3F) (+6k code) #define USE_DISPLAY_LCD // [DisplayModel 1] [I2cDriver3] Enable Lcd display (I2C addresses 0x27 and 0x3F) (+6k code)

View File

@ -0,0 +1,199 @@
/*
xsns_118_ags02ma.ino - AGS02MA TVOC sensor support for Tasmota
Copyright (C) 2025 Akshaylal S
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef USE_I2C
#ifdef USE_AGS02MA
/*********************************************************************************************\
* AGS02MA - TVOC (Total Volatile Organic Compounds) Sensor
*
* Source: RobTillaart/AGS02MA library
* Adaption for TASMOTA: Akshaylal S
*
* I2C Address: 0x1A
\*********************************************************************************************/
#define XSNS_118 118
#define XI2C_95 95 // See I2CDEVICES.md
#define AGS02MA_ADDRESS 0x1A
#include "AGS02MA.h"
enum AGS02MA_State {
STATE_AGS02MA_START,
STATE_AGS02MA_INIT,
STATE_AGS02MA_HEATING,
STATE_AGS02MA_NORMAL,
STATE_AGS02MA_FAIL,
};
AGS02MA *ags02ma = nullptr;
AGS02MA_State ags02ma_state = STATE_AGS02MA_START;
bool ags02ma_init = false;
bool ags02ma_read_pend = false;
uint32_t ags02ma_ppb_value = 0;
uint16_t heating_counter = 0; // Counter for heating period (120 seconds / 24 checks = 5 seconds per check)
/********************************************************************************************/
void Ags02maInit(void)
{
if (!I2cSetDevice(AGS02MA_ADDRESS)) { return; }
ags02ma = new AGS02MA(AGS02MA_ADDRESS, &I2cGetWire());
bool b = ags02ma->begin();
if (!b) {
AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Sensor not found or initialization failed"));
ags02ma_state = STATE_AGS02MA_FAIL;
return;
}
uint8_t version = ags02ma->getSensorVersion();
AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Sensor version 0x%02X"), version);
// Set PPB mode
b = ags02ma->setPPBMode();
if (!b) {
AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Failed to set PPB mode"));
ags02ma_state = STATE_AGS02MA_FAIL;
return;
}
uint8_t mode = ags02ma->getMode();
AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Mode set to %d"), mode);
I2cSetActiveFound(AGS02MA_ADDRESS, "AGS02MA", XI2C_95);
ags02ma_init = true;
ags02ma_state = STATE_AGS02MA_HEATING;
heating_counter = 0;
AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Starting warm-up period (120 seconds)"));
}
void Ags02maUpdate(void)
{
if (ags02ma_state == STATE_AGS02MA_FAIL) {
AddLog(LOG_LEVEL_DEBUG, PSTR("AGS02MA: In FAIL state"));
return;
}
// Handle heating period
if (ags02ma_state == STATE_AGS02MA_HEATING) {
// Check every 5 seconds (called from FUNC_EVERY_SECOND, so count to 5)
if (heating_counter % 5 == 0) {
if (ags02ma->isHeated()) {
AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Warm-up complete, sensor ready"));
ags02ma_state = STATE_AGS02MA_NORMAL;
heating_counter = 0;
return;
}
}
heating_counter++;
// Log progress every 24 seconds (approximately)
if (heating_counter % 24 == 0) {
AddLog(LOG_LEVEL_DEBUG, PSTR("AGS02MA: Warming up... %d seconds elapsed"), heating_counter);
}
return;
}
// Normal operation - read sensor value
if (ags02ma_state == STATE_AGS02MA_NORMAL) {
ags02ma_ppb_value = ags02ma->readPPB();
uint8_t status = ags02ma->lastStatus();
uint8_t error = ags02ma->lastError();
if (error != 0) {
AddLog(LOG_LEVEL_DEBUG, PSTR("AGS02MA: Read error - Status: 0x%02X, Error: 0x%02X"), status, error);
} else {
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("AGS02MA: PPB value: %d"), ags02ma_ppb_value);
}
}
}
#ifdef USE_WEBSERVER
const char HTTP_SNS_AGS02MA[] PROGMEM =
"{s}AGS02MA " D_TVOC "{m}%d " D_UNIT_PARTS_PER_BILLION "{e}"; // {s} = <tr><th>, {m} = </th><td>, {e} = </td></tr>
#endif
void Ags02maShow(bool json)
{
if (ags02ma_state == STATE_AGS02MA_NORMAL) {
if (json) {
ResponseAppend_P(PSTR(",\"AGS02MA\":{\"" D_JSON_TVOC "\":%d}"),
ags02ma_ppb_value);
#ifdef USE_DOMOTICZ
if (0 == TasmotaGlobal.tele_period) {
DomoticzSensor(DZ_AIRQUALITY, ags02ma_ppb_value);
}
#endif // USE_DOMOTICZ
#ifdef USE_WEBSERVER
} else {
WSContentSend_PD(HTTP_SNS_AGS02MA, ags02ma_ppb_value);
#endif
}
} else if (ags02ma_state == STATE_AGS02MA_HEATING) {
if (json) {
ResponseAppend_P(PSTR(",\"AGS02MA\":{\"Status\":\"Heating\"}"));
#ifdef USE_WEBSERVER
} else {
WSContentSend_PD(PSTR("{s}AGS02MA Status{m}Warming up...{e}"));
#endif
}
}
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
bool Xsns118(uint32_t function)
{
if (!I2cEnabled(XI2C_95)) { return false; }
bool result = false;
if (FUNC_INIT == function) {
Ags02maInit();
}
else if (ags02ma_init) {
switch (function) {
case FUNC_EVERY_SECOND:
Ags02maUpdate();
break;
case FUNC_JSON_APPEND:
Ags02maShow(1);
break;
#ifdef USE_WEBSERVER
case FUNC_WEB_SENSOR:
Ags02maShow(0);
break;
#endif // USE_WEBSERVER
}
}
return result;
}
#endif // USE_AGS02MA
#endif // USE_I2C