{"id":20989804,"url":"https://github.com/robtillaart/ags02ma","last_synced_at":"2025-05-14T18:32:11.062Z","repository":{"id":40563581,"uuid":"395400368","full_name":"RobTillaart/AGS02MA","owner":"RobTillaart","description":"Arduino library for AGS02MA TVOC sensor","archived":false,"fork":false,"pushed_at":"2024-04-13T08:50:15.000Z","size":66,"stargazers_count":14,"open_issues_count":0,"forks_count":3,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-08-07T18:27:21.445Z","etag":null,"topics":["arduino","arduino-library","esp32","esp8266"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RobTillaart.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null},"funding":{"github":"RobTillaart","custom":"https://www.paypal.me/robtillaart"}},"created_at":"2021-08-12T17:46:21.000Z","updated_at":"2023-12-10T22:57:19.000Z","dependencies_parsed_at":"2023-02-12T10:10:21.008Z","dependency_job_id":"22cfdfe0-ef20-4e85-9d70-a3719da2abfe","html_url":"https://github.com/RobTillaart/AGS02MA","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobTillaart%2FAGS02MA","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobTillaart%2FAGS02MA/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobTillaart%2FAGS02MA/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobTillaart%2FAGS02MA/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RobTillaart","download_url":"https://codeload.github.com/RobTillaart/AGS02MA/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225305673,"owners_count":17453424,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["arduino","arduino-library","esp32","esp8266"],"created_at":"2024-11-19T06:26:12.522Z","updated_at":"2024-11-19T06:26:13.354Z","avatar_url":"https://github.com/RobTillaart.png","language":"C++","funding_links":["https://github.com/sponsors/RobTillaart","https://www.paypal.me/robtillaart"],"categories":[],"sub_categories":[],"readme":"\n[![Arduino CI](https://github.com/RobTillaart/AGS02MA/workflows/Arduino%20CI/badge.svg)](https://github.com/marketplace/actions/arduino_ci)\n[![Arduino-lint](https://github.com/RobTillaart/AGS02MA/actions/workflows/arduino-lint.yml/badge.svg)](https://github.com/RobTillaart/AGS02MA/actions/workflows/arduino-lint.yml)\n[![JSON check](https://github.com/RobTillaart/AGS02MA/actions/workflows/jsoncheck.yml/badge.svg)](https://github.com/RobTillaart/AGS02MA/actions/workflows/jsoncheck.yml)\n[![GitHub issues](https://img.shields.io/github/issues/RobTillaart/AGS02MA.svg)](https://github.com/RobTillaart/AGS02MA/issues)\n\n[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/RobTillaart/AGS02MA/blob/master/LICENSE)\n[![GitHub release](https://img.shields.io/github/release/RobTillaart/AGS02MA.svg?maxAge=3600)](https://github.com/RobTillaart/AGS02MA/releases)\n[![PlatformIO Registry](https://badges.registry.platformio.org/packages/robtillaart/library/AGS02MA.svg)](https://registry.platformio.org/libraries/robtillaart/AGS02MA)\n\n\n# AGS02MA\n\nArduino library for AGS02MA TVOC sensor.\n\n\n#### Description\n\nThis library is still experimental, so please use with care.\n\nThe AGS02MA is a sensor that measures the TVOC = Total Volatile Organic Compounds\nin the air. It does not measure a specific gas, but several.\n\n\nNote the warning about the I2C low speed, the device works at max 30 KHz.\nSince 0.3.1 this library uses 25 KHz.\n\nNote this library is not meant to replace professional monitoring systems.\n\n\n#### 0.4.0 Breaking change\n\nVersion 0.4.0 introduced a breaking change.\nYou cannot set the pins in **begin()** any more.\nThis reduces the dependency of processor dependent Wire implementations.\nThe user has to call **Wire.begin()** and can optionally set the Wire pins \nbefore calling **begin()**.\n\n\n#### Related\n\n- https://www.renesas.com/us/en/document/whp/overview-tvoc-and-indoor-air-quality\n\n\n## I2C\n\n### PIN layout from left to right\n\n|  Front L-\u003eR  |  Description  |\n|:------------:|:--------------|\n|   pin 1      |   VDD +       |\n|   pin 2      |   SDA data    |\n|   pin 3      |   GND         |\n|   pin 4      |   SCL clock   |\n\n\n#### WARNING - LOW SPEED\n\nThe sensor uses I2C at very low speed \u003c= 30 KHz.\nFor an Arduino UNO the lowest speed supported is about 30.4KHz (TWBR = 255) which works.\nFirst runs with Arduino UNO indicate 2 failed reads in \u003e 500 Reads, so less than 1%\n\nTests with ESP32 / ESP8266 at 30 KHz look good,\ntests with ESP32 at lower clock speeds are to be done but expected to work.\n\nThe library sets the clock speed to 30 KHz (for non AVR) during operation\nand resets it default to 100 KHz after operation.\nThis is done to minimize interference with the communication of other devices.\nThe reset clock speed can be changed with **setI2CResetSpeed(speed)** e.g. to 200 or 400 KHz.\n\n\n#### 0.3.1 fix.\n\nVersion 0.3.1 sets the **I2C prescaler TWSR** register of the Arduino UNO to 4 so the lowest\nspeed possible is reduced to about 8 KHz.\nA test run 4 hours with 6000++ reads on an UNO at 25 KHz gave 0 errors.\nSo the communication speed will be set to 25 KHz, also for other boards, for stability.\nAfter communication the clock (+ prescaler) is reset again as before.\n\n\n#### I2C multiplexing\n\nSometimes you need to control more devices than possible with the default\naddress range the device provides.\nThis is possible with an I2C multiplexer e.g. TCA9548 which creates up \nto eight channels (think of it as I2C subnets) which can use the complete \naddress range of the device. \n\nDrawback of using a multiplexer is that it takes more administration in \nyour code e.g. which device is on which channel. \nThis will slow down the access, which must be taken into account when\ndeciding which devices are on which channel.\nAlso note that switching between channels will slow down other devices \ntoo if they are behind the multiplexer.\n\n- https://github.com/RobTillaart/TCA9548\n\n\n## Version 118 problems\n\nThe library can request the version with **getSensorVersion()**.\nMy devices all report version 117 and this version is used to develop / test this library.\nThere are devices reported with version 118 which behave differently.\n\n\n#### ugM3 not supported\n\nSee  - https://github.com/RobTillaart/AGS02MA/issues/11\n\nThe version 118 seems only to support the **PPB** and not the **ugM3** mode.\nIt is unclear if this is an incident, bug or a structural change in the firmware.\n\nIf you encounter similar problems with setting the mode (any version), please let me know.\nThat will help indicating if this is a \"structural change\" or incident.\n\n\n#### Calibrate problem!\n\nSee - https://github.com/RobTillaart/AGS02MA/issues/13\n\nIn this issue a problem is reported with a version 118 sensor.\nThe problem exposed itself after running the calibration sketch (command).\nThe problem has been confirmed by a 2nd version 118 sensor.\nAdditional calibration runs did not fix the problem.\nVersion 117 seem to have no problems with calibration.\n\n**Advice**: do **NOT** calibrate a version 118.\n\nNote: the version 0.2.0 determines the version in the calibration function so\nit won't calibrate any non 117 version.\n\n\n#### Please report your experiences.\n\nIf you have a AGS20MA device, version 117 or 118 or other,\nplease let me know your experiences\nwith the sensor and this (or other) library.\n\n\n## Interface\n\n```cpp\n#include \"AGS02MA.h\"\n```\n\n#### Constructor\n\n- **AGS02MA(uint8_t deviceAddress = 26, TwoWire \\*wire = \u0026Wire)** constructor, \nwith default address and default I2C interface.\n- **bool begin()** initialize the library.\n- **bool isConnected()** returns true if device address can be seen on I2C.\n- **void reset()** reset internal variables.\n\n\n#### Timing\n\n- **bool isHeated()** returns true if 2 minutes have passed after startup (call of **begin()** ).\nOtherwise the device is not optimal ready.\nAccording to the datasheet the preheating will improve the quality of the measurements.\n- **uint32_t lastRead()** last time the device is read, timestamp is in milliseconds since start.\nReturns 0 if **readPPB()** or **readUGM3()** is not called yet.\nThis function allows to implement sort of asynchronous wait.\nOne must keep reads at least 1.5 seconds but preferred 3 seconds apart according to the datasheet.\n\n\n#### Administration\n\n- **bool setAddress(const uint8_t deviceAddress)** sets a new address for the sensor.\nIf function succeeds the address changes immediately and will be persistent over a reboot.\n- **uint8_t getAddress()** returns the set address. Default the function will return 26 or 0x1A.\n- **uint8_t getSensorVersion()** reads sensor version from device.\nIf the version cannot be read the function will return 255.\n(My test sensors all return version 117, version 118 is reported)\n- **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.\n\n```cpp\nuint32_t dd = sensor.getSensorDate();\nSerial.println(dd, HEX);   //  prints YYYYMMDD e.g. 20210203\n```\n\n\n#### I2C clock speed\n\nThe library sets the clock speed to 25 KHz during operation\nand resets it to 100 KHz after operation.\nThis is done to minimize interference with the communication of other devices.\nThe following function can change the I2C reset speed to e.g. 200 or 400 KHz.\n\n- **void setI2CResetSpeed(uint32_t speed)** sets the I2C speed the library need to reset the I2C speed to.\n- **uint32_t getI2CResetSpeed()** returns the value set. Default is 100 KHz.\n\n\n#### setMode\n\nThe default mode at startup of the sensor is PPB = parts per billion.\n\n- **bool setPPBMode()** sets device in PartPerBillion mode. Returns true on success.\n- **bool setUGM3Mode()** sets device in micro gram per cubic meter mode. Returns true on success.\n- **uint8_t getMode()** returns mode set. 0 = PPB, 1 = UGm3, 255 = not set.\n\n\n#### Air quality classification\n\nIndicative\n\n| TVOC(ppb) |  Scale  |  Description          |  Colour      |\n|:---------:|:-------:|:---------------------:|:-------------|\n|  \u003c= 220   |    1    |  Good                 |  Green       |\n|  \u003c= 660   |    3    |  Moderate             |  Yellow      |\n|  \u003c= 1430  |    7    |  Bad                  |  Orange      |\n|  \u003c= 2200  |   10    |  Unhealthy            |  Red         |\n|  \u003c= 3300  |   15    |  Very unhealthy       |  Purple      |\n|  \u003c= 5500  |   25    |  Hazardous            |  Deep Purple |\n|  \u003e 5500   |   50    |  Extremely Hazardous  |  Deep Purple |\n\n[Source](https://learn.kaiterra.com/en/resources/understanding-tvoc-volatile-organic-compounds)\n\n- Scale is a relative scale where 220 ~~ 1\n- Colour is an indicative colour mapping.\n  - https://github.com/RobTillaart/map2colour for continuous scale.\n\n\n#### PPB versus UGM3\n\nThere is no 1 to 1 relation between the PPB and the uG/m3 readings as this relation depends\non the weight of the individual molecules.\nPPB is therefore an more an absolute indicator where uG/m3 is sort of relative indicator.\nIf the gas is unknown, PPB is in my opinion the preferred measurement.\n\n\nFrom an unverified source the following formula:\nM = molecular weight of the gas.\n\n**μg/m3 = ppb \\* M \\* 12.187 / (273.15 + °C)**\n\nSimplified formula for 1 atm @ 25°C:\n\n**μg/m3 = ppb \\* M \\* 0.04087539829**\n\nSome known gasses\n\n|  gas   |  Common name        |  ratio  ppb-μg/m3     |  molecular weight M  |\n|:-------|:--------------------|:----------------------|:--------------------:|\n|  SO2   |  Sulphur dioxide    |  1 ppb = 2.62 μg/m3   |    64 gr/mol         |\n|  NO2   |  Nitrogen dioxide   |  1 ppb = 1.88 μg/m3   |    46 gr/mol         |\n|  NO    |  Nitrogen monoxide  |  1 ppb = 1.25 μg/m3   |    30 gr/mol         |\n|  O3    |  Ozone              |  1 ppb = 2.00 μg/m3   |    48 gr/mol         |\n|  CO    |  Carbon Monoxide    |  1 ppb = 1.145 μg/m3  |    28 gr/mol         |\n|  C6H6  |  Benzene            |  1 ppb = 3.19 μg/m3   |    78 gr/mol         |\n\n\n- https://github.com/RobTillaart/AtomicWeight  (determine M from chemical formula)\n\n\n#### Read the sensor\n\nWARNING: The datasheet advises to take 3 seconds between reads.\nTests gave stable results at 1.5 second intervals.\nUse this faster rate at your own risk.\n\n- **uint32_t readPPB()** reads PPB (parts per billion) from device.\nTypical value should be between 1 .. 999999.\nReturns **lastPPB()** value if failed so one does not get sudden jumps in graphs.\nCheck **lastStatus()** and **lastError()** to get more info about success.\nTime needed is ~35 milliseconds.\n- **uint32_t readUGM3()** reads UGM3 (microgram per cubic meter) current value from device.\nTypical values depend on the molecular weight of the TVOC.\nReturns **lastUGM3()** if failed so one does not get sudden jumps in graphs.\n- **float readPPM()** returns parts per million (PPM).\nThis function is a wrapper around readPPB().\nTypical value should be between 0.01 .. 999.99\n- **float readMGM3()** returns milligram per cubic meter.\n- **float readUGF3()** returns microgram per cubic feet.\n\n\n#### Error Codes\n\n|  ERROR_CODES                |  value  |\n|:----------------------------|:-------:|\n|  AGS02MA_OK                 |     0   |\n|  AGS02MA_ERROR              |   -10   |\n|  AGS02MA_ERROR_CRC          |   -11   |\n|  AGS02MA_ERROR_READ         |   -12   |\n|  AGS02MA_ERROR_NOT_READY    |   -13   |\n\n\n#### Cached values\n\n- **float lastPPM()** returns last readPPM (parts per million) value (cached).\n- **uint32_t lastPPB()** returns last read PPB (parts per billion) value (cached). Should be between 1..999999.\n- **uint32_t lastUGM3()** returns last read UGM3 (microgram per cubic meter) value (cached).\n\n\n#### Calibration\n\n- **bool zeroCalibration()** to be called after at least 5 minutes in fresh air.\nSee example sketch.\n- **bool manualZeroCalibration(uint16_t value = 0)** Set the zero calibration value manually.\nTo be called after at least 5 minutes in fresh air.\n  - For v117: 0-65535 = automatic calibration.\n  - For v118: 0 = automatic calibration, 1-65535 manual calibration.\n- **bool getZeroCalibrationData(ZeroCalibrationData \u0026data)** fills a data struct with the \ncurrent zero calibration status and value. \nReturns true on success.\n\n\n#### Other\n\n- **bool readRegister(uint8_t address, RegisterData \u0026reg)** fills a data struct with the chip's register data at that address.\nPrimarily intended for troubleshooting and analysis of the sensor. Not recommended to build applications on top of this method's raw data.\nReturns true when the **RegisterData** is filled, false when the data could not be read.\nNote: unlike other public methods, CRC errors don't return false or show up in `lastError()`, \ninstead the CRC result is stored in `RegisterData.crcValid`.\n- **int lastError()** returns last error.\n- **uint8_t lastStatus()** returns status byte from last read.\nRead datasheet or table below for details. A new read is needed to update this.\n- **uint8_t dataReady()** returns RDY bit from last read.\n\n\n#### Status bits.\n\n|  bit  |  description                        |  notes  |\n|:-----:|:------------------------------------|:--------|\n|  7-4  |  internal use                       |\n|  3-1  |  000 = PPB  001 = uG/M3             |\n|   0   |  RDY bit  0 = ready  1 = not ready  |  1 == busy\n\n\n## Future\n\n#### Must\n\n- improve documentation\n  - references?\n\n#### Should\n\n- check the mode bits of the status byte with internal \\_mode.\n  - maximize robustness of state\n- test with hardware\n  - different gasses ? indoor / outdoor?\n- test with different processors\n\n#### Could\n\n- elaborate error handling.\n- create an async interface for **readPPB()** if possible\n  - delay(30) blocks performance ==\u003e async version of **readRegister()**\n  - could introduce complex I2C speed handling...\n  - separate state - request pending or so?\n- move code to .cpp?\n\n#### Wont\n\n\n## Support\n\nIf you appreciate my libraries, you can support the development and maintenance.\nImprove the quality of the libraries by providing issues and Pull Requests, or\ndonate through PayPal or GitHub sponsors.\n\nThank you,\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobtillaart%2Fags02ma","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frobtillaart%2Fags02ma","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobtillaart%2Fags02ma/lists"}