diff --git a/README.md b/README.md index f3c7798..76db786 100644 --- a/README.md +++ b/README.md @@ -20,21 +20,21 @@ Arduino Library for the SparkFun Qwiic MEMS gas sensor breakouts, covering all n ## Library Description -The SparkFun Qwiic MEMS gas sensor boards put Winsen's GM-x02B metal-oxide semiconductor (MOS) gas sensing elements on Qwiic-enabled breakouts. Each board pairs one sensing element with a Texas Instruments ADC101C021 10-bit I2C ADC, so gas readings arrive straight over Qwiic - no external ADC required. +The SparkFun Qwiic MEMS gas sensor boards put Winsen's GM-x02B metal-oxide semiconductor (MOS) gas sensing elements on Qwiic-enabled breakouts. Each board pairs one sensing element with a Texas Instruments ADC101C021 10-bit I2C ADC, so gas readings arrive over Qwiic with no external ADC required. All nine variants share one board design and one library. Pick your gas: | Board | Class | Target gas | Range | Default address | | -- | -- | -- | -- | -- | -| **GM-102B** | `SfeGM102BArdI2C` | Nitrogen dioxide (NO2) | 0.1 - 10 ppm | `0x50` | -| **GM-202B** | `SfeGM202BArdI2C` | Alcohol, smoke | 10 - 1000 ppm | `0x51` | -| **GM-302B** | `SfeGM302BArdI2C` | Ethanol | 1 - 500 ppm | `0x52` | -| **GM-402B** | `SfeGM402BArdI2C` | Methane, propane | 1 - 10000 ppm | `0x54` | -| **GM-502B** | `SfeGM502BArdI2C` | VOCs | 1 - 500 ppm | `0x55` | -| **GM-512B** | `SfeGM512BArdI2C` | Breath odor (H2S) | 0.5 - 50 ppm | `0x56` | -| **GM-602B** | `SfeGM602BArdI2C` | Hydrogen sulfide (H2S) | 0.5 - 50 ppm | `0x58` | -| **GM-702B** | `SfeGM702BArdI2C` | Carbon monoxide (CO) | 5 - 5000 ppm | `0x59` | -| **GM-802B** | `SfeGM802BArdI2C` | Ammonia (NH3) | 1 - 300 ppm | `0x5A` | +| **GM-102B** | `SfeGM102BArd` | Nitrogen dioxide (NO2) | 0.1 - 10 ppm | `0x50` | +| **GM-202B** | `SfeGM202BArd` | Alcohol, smoke | 10 - 1000 ppm | `0x51` | +| **GM-302B** | `SfeGM302BArd` | Ethanol | 1 - 500 ppm | `0x52` | +| **GM-402B** | `SfeGM402BArd` | Methane, propane | 1 - 10000 ppm | `0x54` | +| **GM-502B** | `SfeGM502BArd` | VOCs | 1 - 500 ppm | `0x55` | +| **GM-512B** | `SfeGM512BArd` | Breath odor (H2S) | 0.5 - 50 ppm | `0x56` | +| **GM-602B** | `SfeGM602BArd` | Hydrogen sulfide (H2S) | 0.5 - 50 ppm | `0x58` | +| **GM-702B** | `SfeGM702BArd` | Carbon monoxide (CO) | 5 - 5000 ppm | `0x59` | +| **GM-802B** | `SfeGM802BArd` | Ammonia (NH3) | 1 - 300 ppm | `0x5A` | Built on the [SparkFun Toolkit](https://github.com/sparkfun/SparkFun_Toolkit). @@ -42,35 +42,35 @@ Built on the [SparkFun Toolkit](https://github.com/sparkfun/SparkFun_Toolkit). - Raw 10-bit ADC readings, output voltage, and sensing element resistance (Rs) - Clean-air calibration (measure and store R0, or restore a saved value) -- **Direction-normalized gas response** — one number that means the same thing on all nine parts +- **Direction-normalized gas response** that reads the same way on all nine parts - Optional user-fitted ppm conversion, plus per-part detection ranges - Hardware gas threshold alerts with programmable limits and hysteresis - ALERT pin support (open-drain, configurable polarity and hold behavior) -- Automatic conversion mode — the ADC monitors the sensor entirely on its own -- Hardware peak gas tracking, mapped to the correct extremum register per part -- Multiple boards on one bus, mixing gases freely +- Automatic conversion mode, where the ADC monitors the sensor on its own +- Hardware peak gas tracking, mapped to the correct extremum register per part, in counts or volts +- Multiple boards on one bus, in any mix of gases -## The one thing to know: the GM-102B runs backwards +## GM-102B sensor direction Eight of the nine sensing elements detect **reducing** gases. Their resistance **falls** as gas concentration rises, so ADC counts rise. -The GM-102B detects NO₂, an **oxidizing** gas. Its resistance **rises** with concentration, so its ADC counts *fall*. Its datasheet says so directly — `R0(air)/Rs(2ppm NO2) ≤ 0.5`, where every other part specifies `≥ 2` or `≥ 3`. +The GM-102B detects NO₂, an **oxidizing** gas. Its resistance **rises** with concentration, so its ADC counts *fall*. Its datasheet specifies `R0(air)/Rs(2ppm NO2) ≤ 0.5`, where every other part specifies `≥ 2` or `≥ 3`. -Because Rs sits on the high side of the load-resistor divider, that inversion propagates through everything: which direction the counts move, which alert limit means "gas detected", and which hardware min/max register holds the peak gas reading. +Because Rs sits on the high side of the load-resistor divider, that inversion affects which direction the counts move, which alert limit means "gas detected", and which hardware min/max register holds the peak gas reading. -**This library handles it for you.** Use `getGasResponse()` instead of `getRatio()`, `setGasAlertResponse()` instead of the raw limit registers, `getGasAlert()` instead of `getAlertStatus()`, and `getPeakGasCounts()` instead of `getHighestConversion()`. Those four are direction-aware, so one sketch works on any board in the family. The raw register accessors are all still there — they're direction-*neutral* by definition, and `rsRisesWithGas()` tells you which way your part goes if you want to do the mapping yourself. +The library handles this. Use `getGasResponse()` instead of `getRatio()`, `setGasAlertResponse()` instead of the raw limit registers, `getGasAlert()` instead of `getAlertStatus()`, and `getPeakGasCounts()` / `getPeakGasVoltage_V()` instead of `getHighestConversion()`. Those four are direction-aware, so one sketch works on any board in the family. The raw register accessors are also available. They're direction-neutral, and `rsRisesWithGas()` tells you which way your part goes if you want to do the mapping yourself. ## Hardware Connections -The boards connect over I2C using the Qwiic connector — no soldering required. Each variant ships at a unique default address (set by the tri-level ADR1 / ADR0 jumpers), so multiple boards coexist on one bus. The PTH header additionally breaks out the raw analog VOUT and the ADC's ALERT output. +The boards connect over I2C using the Qwiic connector, so no soldering is required. Each variant ships at a unique default address (set by the tri-level ADR1 / ADR0 jumpers), so multiple boards coexist on one bus. The PTH header also breaks out the raw analog VOUT and the ADC's ALERT output. > [!WARNING] -> **No pull-up is fitted on ALERT.** The ADC101C021's ALERT is an open-drain output in *both* polarity modes, so the pin floats unless something pulls it high. For a single board the MCU's internal pull-up is enough — `pinMode(alertPin, INPUT_PULLUP)` — because ALERT leaks at most 1 µA when high-impedance, so even a weak 50 kΩ internal pull-up droops only 50 mV. Fit a discrete **10 kΩ to 3.3 V** when your MCU pin has no internal pull-up, when several boards share one alert line, or when the wire is long: that's 330 µA of sink current (well inside the 3 mA VOL spec), 10 mV of droop, and a ~0.2 µs edge. Keep it between 1 kΩ and 100 kΩ. You don't need the pin at all to use the alert function — `getGasAlert()` reads the same condition over I2C. Example03 shows both. +> **No pull-up is fitted on ALERT.** The ADC101C021's ALERT is an open-drain output in *both* polarity modes, so the pin floats unless something pulls it high. For a single board the MCU's internal pull-up is enough (`pinMode(alertPin, INPUT_PULLUP)`) because ALERT leaks at most 1 µA when high-impedance, so even a weak 50 kΩ internal pull-up droops only 50 mV. Fit a discrete **10 kΩ to 3.3 V** when your MCU pin has no internal pull-up, when several boards share one alert line, or when the wire is long: that's 330 µA of sink current (well inside the 3 mA VOL spec), 10 mV of droop, and a ~0.2 µs edge. Keep it between 1 kΩ and 100 kΩ. The pin is not required to use the alert function; `getGasAlert()` reads the same condition over I2C. Example03 shows both. -One board-level difference worth knowing: **the GM-102B carries a 68.1 kΩ load resistor** where the rest of the family uses 4.7 kΩ. Its resistance decade sits two orders higher than the other parts', and with 4.7 kΩ a unit at the high end of the manufacturing spread produces under five ADC counts of usable swing. The library reads the correct value from its per-part table, so you never set it by hand. +**The GM-102B carries a 68.1 kΩ load resistor** where the rest of the family uses 4.7 kΩ. Its datasheet Rs spread (10 kΩ to 1 MΩ) sits 10 to 30 times higher than most of the family's (1 kΩ to 30 kΩ), and with 4.7 kΩ a unit at the high end of the manufacturing spread produces under five ADC counts of usable swing. The library reads the correct value from its per-part table, so you don't need to set it. > [!IMPORTANT] -> These are heated MOS sensors and must be preheated before use: allow a few minutes for the output to stabilize after power-up, and after long unpowered storage Winsen recommends 24–168 hours of powered aging depending on the part (see its datasheet). Out of the box these boards are good for **relative** measurement and threshold alerts; **absolute ppm requires calibration** against known gas concentrations. Never expose the sensing element to organic silicon vapors, and see the datasheet cautions for other handling notes. +> These are heated MOS sensors and must be preheated before use: allow a few minutes for the output to stabilize after power-up, and after long unpowered storage Winsen recommends 24 to 168 hours of powered aging depending on the part (see its datasheet). Out of the box these boards are suited to **relative** measurement and threshold alerts; **absolute ppm requires calibration** against known gas concentrations. Never expose the sensing element to organic silicon vapors, and see the datasheet cautions for other handling notes. ## Using the Library @@ -80,29 +80,29 @@ Install through the Arduino Library Manager by searching for **SparkFun Qwiic Ga ### Getting Started -Declare which board you have by picking its class. There is one per variant, so the board identity lives in the type — nothing to pass, nothing to get wrong: +Declare which board you have by picking its class. There is one per variant, so the part is set by the class type: ```c++ #include // Uncomment the board you're using -// SfeGM102BArdI2C mySensor; // GM-102B - nitrogen dioxide (NO2) -// SfeGM202BArdI2C mySensor; // GM-202B - alcohol, smoke -// SfeGM302BArdI2C mySensor; // GM-302B - ethanol -// SfeGM402BArdI2C mySensor; // GM-402B - methane, propane -// SfeGM502BArdI2C mySensor; // GM-502B - VOCs -// SfeGM512BArdI2C mySensor; // GM-512B - breath odor -SfeGM602BArdI2C mySensor; // GM-602B - hydrogen sulfide (H2S) -// SfeGM702BArdI2C mySensor; // GM-702B - carbon monoxide (CO) -// SfeGM802BArdI2C mySensor; // GM-802B - ammonia (NH3) +// SfeGM102BArd mySensor; // GM-102B - nitrogen dioxide (NO2) +// SfeGM202BArd mySensor; // GM-202B - alcohol, smoke +// SfeGM302BArd mySensor; // GM-302B - ethanol +// SfeGM402BArd mySensor; // GM-402B - methane, propane +// SfeGM502BArd mySensor; // GM-502B - VOCs +// SfeGM512BArd mySensor; // GM-512B - breath odor +SfeGM602BArd mySensor; // GM-602B - hydrogen sulfide (H2S) +// SfeGM702BArd mySensor; // GM-702B - carbon monoxide (CO) +// SfeGM802BArd mySensor; // GM-802B - ammonia (NH3) ``` -The library can't read the sensing element back from the hardware — the ADC101C021 has no ID register — and getting it wrong silently inverts the GM-102B's readings and picks the wrong load resistor. So it has to be declared somewhere, and the type is the safest place for it. +The library can't read the sensing element back from the hardware (the ADC101C021 has no ID register), and the wrong part inverts the GM-102B's readings and selects the wrong load resistor, so the part must be declared. -If you need to pick the part at runtime, or hold several boards in one array, use the generic class with a **required** part argument (there is deliberately no default): +If you need to pick the part at runtime, or hold several boards in one array, use the generic class with a **required** part argument (there is no default): ```c++ -SfeGMX02BArdI2C mySensor(GM_602B); +SfeGMX02BArd mySensor(GM_602B); ``` In `setup()`, start I2C and call `begin()`: @@ -117,83 +117,45 @@ while (mySensor.begin() == false) // this part's factory address, 0x58 } ``` -### Declaring the part vs. guessing it +### Part and address -Sensor identity and I2C address are two different facts, and this library keeps them separate on purpose. The **part** is declared; the **address** defaults from it but can be overridden: +The part and the I2C address are set separately. The **part** is declared; the **address** defaults from it but can be overridden: ```c++ mySensor.begin(); // factory address for the declared part -mySensor.begin(0x51); // jumpers moved — same sensor, new address, correct math +mySensor.begin(0x51); // jumpers moved: same sensor, new address, correct math ``` -That's what makes a re-addressed board safe. If identity rode on the address, moving an ADR jumper to dodge a bus conflict could silently turn a GM-102B into a "GM-602B" and invert every reading, with nothing to signal it. - -`beginAutoDetect()` is still available for a single-board demo, and it reports honestly: - -```c++ -if (mySensor.beginAutoDetect()) -{ - Serial.print("Found something at 0x"); - Serial.println(mySensor.getAddress(), HEX); - - if (mySensor.isIdentityAssumed()) - Serial.println("...sensor type ASSUMED from the address, not measured."); -} -``` - -> [!WARNING] -> Address-based detection is a guess, and the guess picks the curve direction. It also can't distinguish a gas sensor from anything else on those addresses — `0x50`–`0x57` is the classic 24-series EEPROM block, and SparkFun's own Qwiic EEPROM defaults to `0x50`. `probeADC()` adds a read-only check that the responding registers look like an ADC101C021 (it never writes, precisely because the device might *be* an EEPROM), but a zeroed EEPROM would still pass. Declare the part for anything real. - -To list what's actually on the bus without claiming to identify it: - -```c++ -uint8_t found[9]; -uint8_t count = SfeGMX02BArdI2C::scanFamily(found, sizeof(found)); -``` - -And if `begin()` fails, `findMisplacedBoard()` diagnoses the likeliest cause — it returns the address of the single other family board responding, if there's exactly one. +This keeps a re-addressed board reading correctly. If the part were inferred from the address, moving an ADR jumper could cause a GM-102B to be treated as a GM-602B, inverting its readings. ### Using several of the same sensor -Nothing in this library ties a part number to an address, so several boards of the *same* type are no different from several boards of different types. Give each one its own object and its own address: +The library doesn't tie a part number to an address, so several boards of the same type work the same way as several boards of different types. Give each one its own object and its own address: ```c++ // Three H2S boards. Two have had their ADR jumpers moved off the factory 0x58. -SfeGM602BArdI2C intake; -SfeGM602BArdI2C exhaust; -SfeGM602BArdI2C ambient; +SfeGM602BArd intake; +SfeGM602BArd exhaust; +SfeGM602BArd ambient; intake.begin(); // 0x58, the GM-602B factory address exhaust.begin(0x50); // re-jumpered ambient.begin(0x55); // re-jumpered ``` -All three keep the GM-602B's curve direction, its 4.7 kΩ load resistor, and its detection range, because those come from the **declared part**, not the address. Each object holds its own bus handle, its own cached reading, and its own R0 — the const part table is shared but read-only, so instances never interfere. +All three keep the GM-602B's curve direction, its 4.7 kΩ load resistor, and its detection range, because those come from the **declared part**, not the address. Each object holds its own bus handle, its own cached reading, and its own R0. The const part table is shared but read-only, so instances don't interfere. -**Any board can take any of the nine addresses.** The two tri-state jumpers give 3 × 3 = 9 combinations, and each variant just *ships* on a different one so that a one-of-each bus works out of the box. Re-jumper freely; `sfe_gm_familyAddresses()` returns the nine valid values, and the decode table is in the comment block at the top of `sfDevGMX02B.cpp`. +**Any board can take any of the nine addresses.** The two tri-state jumpers give 3 × 3 = 9 combinations, and each variant ships on a different one so that a one-of-each bus works without changes. The decode table is in the comment block at the top of `sfDevGMX02B.cpp`. -Three consequences worth knowing: +Things to keep in mind: -> [!WARNING] -> **Never use `beginAutoDetect()` with re-addressed boards.** It infers the part from the address, so three GM-602Bs at 0x58 / 0x50 / 0x55 would be reported as a GM-602B, a GM-102B and a GM-502B — and the "GM-102B" would get an inverted curve and a 68.1 kΩ load resistor. This isn't a corner case here, it's guaranteed. Auto-detect is for a single board on its factory address. - -- **Nine boards per bus is the ceiling**, whatever the mix of types, because there are only nine addresses. For more, use a second I2C port — `begin()` takes a `TwoWire&`, so `fourth.begin(0x58, Wire1)` puts another nine on `Wire1`. -- **Each unit needs its own clean-air calibration.** R0 belongs to an individual sensing element, not to a part number, and Winsen's part-to-part spread is on the order of 10:1 — so two GM-602Bs sitting side by side in the same clean air will have genuinely different R0 values. Call `measureCleanAirResistance()` per object. -- **The library can't detect an address collision.** If you forget to re-jumper one board and two sit at 0x58, both ACK and you get contention, not an error. Sanity-check the bus before you trust it: - -```c++ -uint8_t found[9]; -uint8_t count = SfeGMX02BArdI2C::scanFamily(found, sizeof(found)); - -if (count != 3) - Serial.println("Expected 3 boards - check the ADR jumpers for duplicates."); -``` +- **Nine boards per bus is the ceiling**, whatever the mix of types, because there are only nine addresses. For more, use a second I2C port: `begin()` takes a `TwoWire&`, so `fourth.begin(0x58, Wire1)` puts another nine on `Wire1`. +- **Each unit needs its own clean-air calibration.** R0 belongs to an individual sensing element, not to a part number, and Winsen's part-to-part spread is on the order of 10:1, so two GM-602Bs in the same clean air will have different R0 values. Call `measureCleanAirResistance()` per object. +- **The library can't detect an address collision.** If you forget to re-jumper one board and two sit at 0x58, both ACK and you get contention, not an error. Double-check the ADR jumpers for duplicates before you trust the readings. ### Reading the Sensor -One `readMeasurement()` call fetches a conversion; the getters then return it in whichever form you need: - -Following the SparkFun Toolkit convention, getters that can fail return an `sfTkError_t` and hand back the value through an output reference: +`readMeasurement()` fetches a conversion, and the getters return it in different forms. Following the SparkFun Toolkit convention, getters that can fail return an `sfTkError_t` and hand back the value through an output reference: ```c++ float ohms; @@ -209,11 +171,13 @@ Serial.print(" Rs: "); Serial.println(ohms, 0); // ohms ``` -`getRaw()` and `getVoltage_V()` return directly because they read the cached conversion and cannot fail. `getSensorResistance_Ohm()`, `getRatio()`, `getGasResponse()` and `getConcentration_ppm()` return an `sfTkError_t`, because each has a real failure mode — no clean-air reference, an off-scale reading, no fitted curve. Example03 shows the checked form; the other examples skip it, since once `begin()` succeeds the rest is reliable. +`getRaw()` and `getVoltage_V()` return directly because they read the cached conversion and cannot fail. `getSensorResistance_Ohm()`, `getRatio()`, `getGasResponse()` and `getConcentration_ppm()` return an `sfTkError_t`, because each can fail: no clean-air reference, an off-scale reading, no fitted curve. Example03 shows the checked form; the other examples skip it for brevity. + +To express a raw register value in volts, `countsToVoltage_V()` converts ADC counts to sensor output voltage. It is the inverse of `voltageToCounts()`. ### Clean-Air Response -Calibrate once in clean air with the sensor warmed up, then track the gas response — near **1.0 in clean air and rising with gas on every part in the family**: +Calibrate once in clean air with the sensor warmed up, then read the gas response. It is near **1.0 in clean air and rising with gas on every part in the family**: ```c++ float r0; @@ -235,7 +199,7 @@ Because the response is normalized in the same direction the datasheets quote se ### Hardware Alerts -The ADC can watch the sensor by itself. Set a threshold in response units, enable automatic conversion mode, and poll the flag (or wire the ALERT pin to an interrupt): +The ADC can monitor the sensor on its own. Set a threshold in response units, enable automatic conversion mode, and poll the flag (or wire the ALERT pin to an interrupt): ```c++ mySensor.setGasAlertResponse(2.0); // twice the clean-air response @@ -250,11 +214,25 @@ if (gasDetected) `setGasAlertResponse()` writes the high limit on a falling-Rs part and the low limit on the GM-102B; `getGasAlert()` maps the status flags back the same way. +### Peak Tracking + +In automatic conversion mode the ADC also records its lowest and highest conversions in hardware, so a gas spike between polls is still captured. `getPeakGasVoltage_V()` reads whichever register holds the peak gas reading for your part: + +```c++ +mySensor.setCycleTime(GM_CYCLE_TIME_2048); // min/max registers only update in automatic mode +mySensor.clearPeakGasCounts(); + +float peak; +mySensor.getPeakGasVoltage_V(peak); // peak sensor output voltage since the last clear +``` + +`getPeakGasCounts()` returns the same reading in raw counts. + ### Concentration in ppm -This library ships **no** conversion curves, and that's deliberate. Winsen publishes no numeric curve data for any GM-x02B part — the datasheet sensitivity plots are raster images with no axis values — and the GM-602B manual describes the response as "non linear, hysteretic and non-repetitive." Any coefficient table here would be invented numbers wearing a datasheet's authority. +This library does not include conversion curves. Winsen publishes no numeric curve data for any GM-x02B part (the datasheet sensitivity plots are raster images with no axis values), and the GM-602B manual describes the response as "non linear, hysteretic and non-repetitive." -If you characterize a sensor against known gas concentrations, fit a power law and hand it over: +If you characterize a sensor against known gas concentrations, fit a power law and pass it to the library: ```c++ mySensor.setResponseCurve(a, b); // ppm = a * pow(gas response, b) @@ -268,12 +246,11 @@ else if (!mySensor.isInRange(ppm)) ## Examples -- **Example01_BasicReadings** — raw counts, voltage, and sensor resistance once per second -- **Example02_CleanAirResponse** — clean-air calibration and direction-normalized response -- **Example03_GasAlertThreshold** — hardware gas threshold alert using automatic conversion mode -- **Example04_PeakTracking** — hardware peak gas tracking over a rolling window -- **Example05_FindBoards** — scan the bus, and the honest limits of address-based detection -- **Example06_MultipleSensors** — three gases on one bus, including the inverted GM-102B (see [Using several of the same sensor](#using-several-of-the-same-sensor) for identical boards at custom addresses) +- **Example01_BasicReadings**: raw counts, voltage, and sensor resistance once per second +- **Example02_CleanAirResponse**: clean-air calibration and direction-normalized response +- **Example03_GasAlertThreshold**: hardware gas threshold alert using automatic conversion mode +- **Example04_PeakTracking**: hardware peak gas tracking over a rolling window +- **Example05_MultipleSensors**: three gases on one bus, including the inverted GM-102B (see [Using several of the same sensor](#using-several-of-the-same-sensor) for identical boards at custom addresses) ## Repository Contents diff --git a/examples/Example01_BasicReadings/Example01_BasicReadings.ino b/examples/Example01_BasicReadings/Example01_BasicReadings.ino index 794ee7e..0f9fa1d 100644 --- a/examples/Example01_BasicReadings/Example01_BasicReadings.ino +++ b/examples/Example01_BasicReadings/Example01_BasicReadings.ino @@ -34,15 +34,15 @@ #include // Uncomment the board you're using -// SfeGM102BArdI2C mySensor; // GM-102B - nitrogen dioxide (NO2) -// SfeGM202BArdI2C mySensor; // GM-202B - alcohol, smoke -// SfeGM302BArdI2C mySensor; // GM-302B - ethanol -// SfeGM402BArdI2C mySensor; // GM-402B - methane, propane -// SfeGM502BArdI2C mySensor; // GM-502B - VOCs -// SfeGM512BArdI2C mySensor; // GM-512B - breath odor -SfeGM602BArdI2C mySensor; // GM-602B - hydrogen sulfide (H2S) -// SfeGM702BArdI2C mySensor; // GM-702B - carbon monoxide (CO) -// SfeGM802BArdI2C mySensor; // GM-802B - ammonia (NH3) +// SfeGM102BArd mySensor; // GM-102B - nitrogen dioxide (NO2) +// SfeGM202BArd mySensor; // GM-202B - alcohol, smoke +// SfeGM302BArd mySensor; // GM-302B - ethanol +// SfeGM402BArd mySensor; // GM-402B - methane, propane +// SfeGM502BArd mySensor; // GM-502B - VOCs +// SfeGM512BArd mySensor; // GM-512B - breath odor +SfeGM602BArd mySensor; // GM-602B - hydrogen sulfide (H2S) +// SfeGM702BArd mySensor; // GM-702B - carbon monoxide (CO) +// SfeGM802BArd mySensor; // GM-802B - ammonia (NH3) void setup() { @@ -55,7 +55,7 @@ void setup() { Serial.print("No "); Serial.print(mySensor.getSensorName()); - Serial.println(" found - check your wiring!"); + Serial.println(" found, check your wiring!"); delay(1000); } diff --git a/examples/Example02_CleanAirResponse/Example02_CleanAirResponse.ino b/examples/Example02_CleanAirResponse/Example02_CleanAirResponse.ino index f5c1cd8..ac52f0f 100644 --- a/examples/Example02_CleanAirResponse/Example02_CleanAirResponse.ino +++ b/examples/Example02_CleanAirResponse/Example02_CleanAirResponse.ino @@ -8,10 +8,10 @@ Eight of the nine sensing elements detect reducing gases: their resistance FALLS as gas rises, so the datasheet's Rs/R0 ratio drops below 1.0. The GM-102B detects NO2, an oxidizing gas, and - its resistance RISES - so its Rs/R0 climbs ABOVE 1.0 instead. A sketch that tests - "ratio < 0.5 means gas" is exactly backwards on that one part. + its resistance RISES, so its Rs/R0 climbs ABOVE 1.0 instead. A sketch that tests + "ratio < 0.5 means gas" is backwards on that part. - getGasResponse() removes the trap: it is R0/Rs on falling-Rs parts and Rs/R0 on rising-Rs parts, + getGasResponse() handles this: it is R0/Rs on falling-Rs parts and Rs/R0 on rising-Rs parts, so it sits near 1.0 in clean air and RISES with target gas on all nine. It is also the direction the datasheets quote sensitivity in, so it compares directly against getRatedSensitivity(): @@ -43,15 +43,15 @@ #include // Uncomment the board you're using -// SfeGM102BArdI2C mySensor; // GM-102B - nitrogen dioxide (NO2) -// SfeGM202BArdI2C mySensor; // GM-202B - alcohol, smoke -// SfeGM302BArdI2C mySensor; // GM-302B - ethanol -// SfeGM402BArdI2C mySensor; // GM-402B - methane, propane -// SfeGM502BArdI2C mySensor; // GM-502B - VOCs -// SfeGM512BArdI2C mySensor; // GM-512B - breath odor -SfeGM602BArdI2C mySensor; // GM-602B - hydrogen sulfide (H2S) -// SfeGM702BArdI2C mySensor; // GM-702B - carbon monoxide (CO) -// SfeGM802BArdI2C mySensor; // GM-802B - ammonia (NH3) +// SfeGM102BArd mySensor; // GM-102B - nitrogen dioxide (NO2) +// SfeGM202BArd mySensor; // GM-202B - alcohol, smoke +// SfeGM302BArd mySensor; // GM-302B - ethanol +// SfeGM402BArd mySensor; // GM-402B - methane, propane +// SfeGM502BArd mySensor; // GM-502B - VOCs +// SfeGM512BArd mySensor; // GM-512B - breath odor +SfeGM602BArd mySensor; // GM-602B - hydrogen sulfide (H2S) +// SfeGM702BArd mySensor; // GM-702B - carbon monoxide (CO) +// SfeGM802BArd mySensor; // GM-802B - ammonia (NH3) // Flag for an elevated reading. Tune this for your environment. getRatedSensitivity() is a // reasonable upper anchor (the response this part is specified to reach at its rated ppm). diff --git a/examples/Example03_GasAlertThreshold/Example03_GasAlertThreshold.ino b/examples/Example03_GasAlertThreshold/Example03_GasAlertThreshold.ino index d86a347..1dbc180 100644 --- a/examples/Example03_GasAlertThreshold/Example03_GasAlertThreshold.ino +++ b/examples/Example03_GasAlertThreshold/Example03_GasAlertThreshold.ino @@ -1,25 +1,23 @@ /* Example 03 - Gas Alert Threshold - Hardware gas threshold detection with no polling. The onboard ADC101C021 can watch the sensor - entirely by itself: in automatic conversion mode it converts on its own schedule, compares every + Hardware gas threshold detection with no polling. The onboard ADC101C021 can monitor the sensor + on its own: in automatic conversion mode it converts on its own schedule, compares every result against a programmable limit, and raises a flag (and optionally its ALERT pin) when the limit is crossed. - setGasAlertResponse() takes a threshold in the same units as getGasResponse() - 1.0 is clean air, - larger means more gas - and writes it to whichever limit register means "more gas" for your part. - That matters: on the GM-102B, resistance RISES with NO2, so ADC counts FALL and the gas-side trip + setGasAlertResponse() takes a threshold in the same units as getGasResponse() (1.0 is clean air, + larger means more gas) and writes it to whichever limit register means "more gas" for your part. + On the GM-102B, resistance RISES with NO2, so ADC counts FALL and the gas-side trip point is the LOW limit, not the high one. getGasAlert() maps the status flags back the same way. Neither call requires your sketch to know which direction its sensor moves. If you want to work in raw ADC counts instead, setAlertLowLimit() / setAlertHighLimit() / - getAlertStatus() are still there and are direction-neutral - but then the direction is yours to - handle. rsRisesWithGas() tells you which way this part goes. + getAlertStatus() are also available. They are direction-neutral, so your sketch has to handle the + direction; rsRisesWithGas() tells you which way this part goes. - This is also the one example that checks return codes on every call. The others skip it: once - begin() has succeeded, the rest is reliable enough that error handling is just clutter. Here it - earns its place, because a threshold set against a missing clean-air reference would silently - never fire. + This example checks return codes on every call. The other examples skip this for brevity, but it + matters here: a threshold set without a clean-air reference would never fire. SparkFun Electronics Date: 2026 @@ -30,10 +28,10 @@ IoT RedBoard --> GM-x02B Breakout QWIIC --> QWIIC - By default this sketch polls the alert over I2C with getGasAlert(), which needs no extra wiring - at all. Uncomment USE_ALERT_PIN below to read the ADC's hardware ALERT pin instead. + By default this sketch polls the alert over I2C with getGasAlert(), which needs no extra wiring. + Uncomment USE_ALERT_PIN below to read the ADC's hardware ALERT pin instead. - IMPORTANT - THE BOARD DOES NOT POPULATE A PULL-UP ON ALERT. The ADC101C021's ALERT is an open + IMPORTANT: THE BOARD DOES NOT POPULATE A PULL-UP ON ALERT. The ADC101C021's ALERT is an open drain output in BOTH polarity modes (setAlertPinActiveHigh() changes only the logic sense, not the output structure), so nothing holds it high on its own. Without a pull-up the pin floats and reads as garbage. The sketch enables the MCU's internal pull-up for you; see the notes at @@ -48,23 +46,23 @@ #include // Uncomment the board you're using -// SfeGM102BArdI2C mySensor; // GM-102B - nitrogen dioxide (NO2) -// SfeGM202BArdI2C mySensor; // GM-202B - alcohol, smoke -// SfeGM302BArdI2C mySensor; // GM-302B - ethanol -// SfeGM402BArdI2C mySensor; // GM-402B - methane, propane -// SfeGM502BArdI2C mySensor; // GM-502B - VOCs -// SfeGM512BArdI2C mySensor; // GM-512B - breath odor -SfeGM602BArdI2C mySensor; // GM-602B - hydrogen sulfide (H2S) -// SfeGM702BArdI2C mySensor; // GM-702B - carbon monoxide (CO) -// SfeGM802BArdI2C mySensor; // GM-802B - ammonia (NH3) - -// Trip when the response reaches twice its clean-air value. Tune this for your application - +// SfeGM102BArd mySensor; // GM-102B - nitrogen dioxide (NO2) +// SfeGM202BArd mySensor; // GM-202B - alcohol, smoke +// SfeGM302BArd mySensor; // GM-302B - ethanol +// SfeGM402BArd mySensor; // GM-402B - methane, propane +// SfeGM502BArd mySensor; // GM-502B - VOCs +// SfeGM512BArd mySensor; // GM-512B - breath odor +SfeGM602BArd mySensor; // GM-602B - hydrogen sulfide (H2S) +// SfeGM702BArd mySensor; // GM-702B - carbon monoxide (CO) +// SfeGM802BArd mySensor; // GM-802B - ammonia (NH3) + +// Trip when the response reaches twice its clean-air value. Tune this for your application; // getRatedSensitivity() is a reasonable upper anchor (the response the part is specified to // reach at getRatedConcentration_ppm()). const float kAlertResponse = 2.0; // Hysteresis, so the alert does not chatter right at the threshold. Set in millivolts of sensor -// output rather than raw ADC counts - setAlertHysteresis_mV() does the conversion. +// output rather than raw ADC counts; setAlertHysteresis_mV() does the conversion. const float kHysteresis_mV = 32.0; // --------------------------------------------------------------------------------------------- @@ -78,12 +76,12 @@ const float kHysteresis_mV = 32.0; // GPIO wired to the board's ALERT pad. Change to a pin your board actually has. const uint8_t kAlertPin = 2; -// ALERT is open drain and the board has NO pull-up fitted, so one has to come from somewhere. +// ALERT is open drain and the board has NO pull-up fitted, so one must be provided. // The MCU's internal pull-up is enough for a single board on a short wire: // // ALERT leaks at most 1 uA when high-impedance (IOZ, datasheet), so even a weak 50 kOhm -// internal pull-up droops only 50 kOhm x 1 uA = 50 mV below the rail - still a solid logic high. -// Pulling low is never the problem: VOL is 0.4 V max at 3 mA, and the internal pull-up sources +// internal pull-up droops only 50 kOhm x 1 uA = 50 mV below the rail, still a solid logic high. +// Pulling low is not an issue: VOL is 0.4 V max at 3 mA, and the internal pull-up sources // only 3.3 V / 50 kOhm = 66 uA. // // Set this false and fit a discrete resistor from ALERT to 3.3V instead when your MCU pin has no @@ -92,8 +90,8 @@ const uint8_t kAlertPin = 2; // // USE 10 kOhm. Sink current 3.3 V / 10 kOhm = 330 uA (well inside the 3 mA VOL spec), droop // 10 kOhm x 1 uA = 10 mV, and an edge of 10 kOhm x ~20 pF = 0.2 us. -// Floor 1 kOhm - 3.3 mA sink, already past the 0.4 V VOL test point. -// Ceiling 100 kOhm - 100 mV of leakage droop and a ~2 us edge. +// Floor: 1 kOhm, 3.3 mA sink, already past the 0.4 V VOL test point. +// Ceiling: 100 kOhm, 100 mV of leakage droop and a ~2 us edge. // const bool kUseInternalPullup = true; @@ -137,7 +135,7 @@ void setup() Serial.print("R0 (ohm): "); Serial.println(r0, 0); - // Set the trip point, then report where it landed in raw counts so you can see the mapping. + // Set the trip point, then report it as a sensor output voltage. if (mySensor.setGasAlertResponse(kAlertResponse) != ksfTkErrOk) { Serial.println("Failed to set the alert threshold!"); @@ -150,8 +148,8 @@ void setup() Serial.print("Alerting above a response of "); Serial.print(kAlertResponse, 1); Serial.print(" ("); - Serial.print(counts); - Serial.print(" ADC counts), hysteresis "); + Serial.print(mySensor.countsToVoltage_V(counts), 3); + Serial.print(" V), hysteresis "); Serial.print(kHysteresis_mV, 0); Serial.println(" mV"); @@ -175,8 +173,8 @@ void setup() : " (expecting an external 10 kOhm pull-up to 3.3V)"); #endif - // Hand the sensor over to the ADC. The slowest cycle time is plenty for a MOS gas sensor and - // uses the least power. + // Put the ADC in automatic conversion mode. The slowest cycle time is sufficient for a MOS gas + // sensor and uses the least power. mySensor.setCycleTime(GM_CYCLE_TIME_2048); Serial.println(); @@ -189,8 +187,8 @@ void loop() bool gasDetected = false; #ifdef USE_ALERT_PIN - // Configured active low, so a LOW pin means the ADC has flagged an alert. No I2C traffic at - // all until we want to know why. + // Configured active low, so a LOW pin means the ADC has flagged an alert. No I2C traffic is + // needed to detect it. gasDetected = (digitalRead(kAlertPin) == LOW); #else if (mySensor.getGasAlert(gasDetected) != ksfTkErrOk) @@ -203,7 +201,7 @@ void loop() if (gasDetected) { - // Read the current value for context - the alert itself came from the hardware. + // Read the current value for context; the alert itself came from the hardware. float response = 0.0; mySensor.readMeasurement(); diff --git a/examples/Example04_PeakTracking/Example04_PeakTracking.ino b/examples/Example04_PeakTracking/Example04_PeakTracking.ino index c94048a..b6e6cfe 100644 --- a/examples/Example04_PeakTracking/Example04_PeakTracking.ino +++ b/examples/Example04_PeakTracking/Example04_PeakTracking.ino @@ -3,12 +3,12 @@ The ADC101C021 keeps two extra registers in hardware: the lowest and the highest conversion it has seen since each was last cleared. In automatic conversion mode they update on every - conversion with no I2C traffic at all, so a gas spike between your polls still gets recorded. + conversion with no I2C traffic, so a gas spike between your polls still gets recorded. Which register holds "most gas" depends on the part. On the eight reducing-gas sensors, more gas - means lower Rs, which means HIGHER ADC counts - so peak gas is the highest conversion. On the - GM-102B (NO2) it is the opposite, and peak gas is the LOWEST conversion. getPeakGasCounts() and - clearPeakGasCounts() pick the right register for you; getLowestConversion() and + means lower Rs and a HIGHER output voltage, so peak gas is the highest conversion. On the + GM-102B (NO2) it is the opposite, and peak gas is the LOWEST conversion. getPeakGasVoltage_V() + and clearPeakGasCounts() pick the right register for you; getLowestConversion() and getHighestConversion() remain available if you want the raw registers. SparkFun Electronics @@ -29,15 +29,15 @@ #include // Uncomment the board you're using -// SfeGM102BArdI2C mySensor; // GM-102B - nitrogen dioxide (NO2) -// SfeGM202BArdI2C mySensor; // GM-202B - alcohol, smoke -// SfeGM302BArdI2C mySensor; // GM-302B - ethanol -// SfeGM402BArdI2C mySensor; // GM-402B - methane, propane -// SfeGM502BArdI2C mySensor; // GM-502B - VOCs -// SfeGM512BArdI2C mySensor; // GM-512B - breath odor -SfeGM602BArdI2C mySensor; // GM-602B - hydrogen sulfide (H2S) -// SfeGM702BArdI2C mySensor; // GM-702B - carbon monoxide (CO) -// SfeGM802BArdI2C mySensor; // GM-802B - ammonia (NH3) +// SfeGM102BArd mySensor; // GM-102B - nitrogen dioxide (NO2) +// SfeGM202BArd mySensor; // GM-202B - alcohol, smoke +// SfeGM302BArd mySensor; // GM-302B - ethanol +// SfeGM402BArd mySensor; // GM-402B - methane, propane +// SfeGM502BArd mySensor; // GM-502B - VOCs +// SfeGM512BArd mySensor; // GM-512B - breath odor +SfeGM602BArd mySensor; // GM-602B - hydrogen sulfide (H2S) +// SfeGM702BArd mySensor; // GM-702B - carbon monoxide (CO) +// SfeGM802BArd mySensor; // GM-802B - ammonia (NH3) // Report and reset the peak every 10 seconds. const unsigned long kWindowMillis = 10000; @@ -73,19 +73,19 @@ void setup() mySensor.clearPeakGasCounts(); windowStart = millis(); - Serial.println("Current\t\tPeak (10 s window)"); + Serial.println("Current (V)\tPeak (V, 10 s window)"); } void loop() { - uint16_t peak; + float peak; mySensor.readMeasurement(); - mySensor.getPeakGasCounts(peak); + mySensor.getPeakGasVoltage_V(peak); - Serial.print(mySensor.getRaw()); + Serial.print(mySensor.getVoltage_V(), 3); Serial.print("\t\t"); - Serial.print(peak); + Serial.print(peak, 3); if (millis() - windowStart >= kWindowMillis) { diff --git a/examples/Example05_FindBoards/Example05_FindBoards.ino b/examples/Example05_FindBoards/Example05_FindBoards.ino deleted file mode 100644 index 8717fe0..0000000 --- a/examples/Example05_FindBoards/Example05_FindBoards.ino +++ /dev/null @@ -1,121 +0,0 @@ -/* - Example 05 - Find Boards - - Bus discovery for the GM-x02B family, and an honest look at its limits. - - The nine board variants ship at nine different factory addresses (0x50 to 0x5A), so scanning - finds them. But the onboard ADC101C021 has NO ID REGISTER, which means: - - * scanFamily() can tell you which ADDRESSES are responding. That is a measurement. - * Guessing the SENSOR from the address is an ASSUMPTION. It holds only while the board's ADR - jumpers are at their factory setting and nothing else occupies a family address - and note - that 0x50-0x57 is also the classic 24-series EEPROM block, so SparkFun's own Qwiic EEPROM - at 0x50 will look exactly like a GM-102B here. - - That assumption matters more than it looks, because the assumed part selects the sensitivity - curve DIRECTION. Mistake a GM-102B for a GM-602B and every gas reading comes out inverted, with - no error anywhere to tell you. - - So: beginAutoDetect() is handy for a single-board demo, and isIdentityAssumed() reports that the - identity was a guess. For anything real, declare the part: - - SfeGM602BArdI2C mySensor; // or SfeGM102BArdI2C, SfeGM802BArdI2C, ... - mySensor.begin(); // factory address for that part - mySensor.begin(0x51); // or wherever you moved the jumpers to - - probeADC() adds a read-only sanity check that the responding device's registers actually look - like an ADC101C021. It rejects a blank EEPROM outright and a populated one with high probability, - but a fully zeroed EEPROM would still pass - so it raises confidence, it does not prove identity. - It never writes, precisely because the device might BE an EEPROM holding customer data. - - SparkFun Electronics - Date: 2026 - SparkFun code, firmware, and software is released under the MIT License. - Please see LICENSE.md for further details. - - Hardware Connections: - IoT RedBoard --> GM-x02B Breakout - QWIIC --> QWIIC - - Open the Serial Monitor at 115200 baud. - - Feel like supporting our work? Buy a board from SparkFun! - https://www.sparkfun.com/ -*/ - -#include - -// Every other example uses a per-part class (SfeGM602BArdI2C and friends), which bakes the board -// identity into the type. This sketch is the one exception, and the awkwardness is the point: -// auto-detection genuinely does not know the part yet, so it uses the generic class with a -// placeholder that beginAutoDetect() then overwrites. -SfeGMX02BArdI2C mySensor(GM_602B); - -void setup() -{ - Serial.begin(115200); - Serial.println("SparkFun GM-x02B Example 5 - Find Boards"); - - Wire.begin(); - - // Step 1: what addresses are actually responding? Addresses only - no identity claimed. - uint8_t found[9]; - uint8_t count = SfeGMX02BArdI2C::scanFamily(found, sizeof(found)); - - Serial.print("Family addresses responding: "); - Serial.println(count); - - for (uint8_t i = 0; i < count; i++) - { - Serial.print(" 0x"); - Serial.print(found[i], HEX); - Serial.print(" - would be a "); - Serial.print(sfe_gmx02b_partName(sfe_gmx02b_sensorFromAddress(found[i]))); - Serial.print(" ("); - Serial.print(sfe_gmx02b_targetGas(sfe_gmx02b_sensorFromAddress(found[i]))); - Serial.println(") at factory jumper settings"); - } - - // Step 2: connect to the first one, requiring the ADC register signature to match. - while (mySensor.beginAutoDetect() == false) - { - Serial.println("Nothing on the bus looked like an ADC101C021 - check your wiring!"); - delay(2000); - } - - Serial.println(); - Serial.print("Using the board at 0x"); - Serial.print(mySensor.getAddress(), HEX); - Serial.print(", treating it as a "); - Serial.println(mySensor.getSensorName()); - - if (mySensor.isIdentityAssumed()) - { - Serial.println(); - Serial.println("*** Sensor type ASSUMED from the I2C address, not measured. ***"); - Serial.print("*** If this is not really a "); - Serial.print(mySensor.getSensorName()); - Serial.println(", gas readings may be inverted. ***"); - Serial.println("*** Declare the part in the constructor to be certain. ***"); - } - - Serial.println(); - Serial.println("Raw (0-1023)\tVoltage (V)\tRs (ohm)"); -} - -void loop() -{ - // Reading is identical for every family member - only the target gas differs. - float ohms; - - mySensor.readMeasurement(); - mySensor.getSensorResistance_Ohm(ohms); - - Serial.print(mySensor.getRaw()); - Serial.print("\t\t"); - Serial.print(mySensor.getVoltage_V(), 3); - Serial.print("\t\t"); - Serial.println(ohms, 0); - - delay(1000); -} diff --git a/examples/Example06_MultipleSensors/Example06_MultipleSensors.ino b/examples/Example05_MultipleSensors/Example05_MultipleSensors.ino similarity index 66% rename from examples/Example06_MultipleSensors/Example06_MultipleSensors.ino rename to examples/Example05_MultipleSensors/Example05_MultipleSensors.ino index 4a4f4fa..fe186fb 100644 --- a/examples/Example06_MultipleSensors/Example06_MultipleSensors.ino +++ b/examples/Example05_MultipleSensors/Example05_MultipleSensors.ino @@ -1,36 +1,32 @@ /* - Example 06 - Multiple Sensors + Example 05 - Multiple Sensors - Three different gases on one Qwiic bus. This is the whole reason the nine board variants ship at - nine different addresses - they were designed to coexist. + Three different gases on one Qwiic bus. The nine board variants ship at nine different addresses + so they can share a bus. - The interesting part is the loop: it is identical for all three sensors even though the GM-102B + The loop is identical for all three sensors even though the GM-102B runs BACKWARDS from the other two. NO2 is an oxidizing gas, so the GM-102B's resistance rises with concentration while the H2S and CO elements fall. getGasResponse() and getGasAlert() - normalize that away, so nothing in your application code needs to special-case a part. + handle the difference, so application code doesn't need to special-case a part. Add or remove sensors by editing the array. Each object declares its own part, so each one gets the right address, the right load resistance (the GM-102B board carries 68.1k rather than the family's 4.7k), and the right curve direction. - SEVERAL OF THE SAME SENSOR works exactly the same way - nothing here ties a part number to an + Several of the same sensor work the same way, since the library doesn't tie a part number to an address. Declare the same part three times and give each object the address you jumpered it to: - SfeGM602BArdI2C intake; intake.begin(); // 0x58, factory - SfeGM602BArdI2C exhaust; exhaust.begin(0x50); // re-jumpered - SfeGM602BArdI2C ambient; ambient.begin(0x55); // re-jumpered + SfeGM602BArd intake; intake.begin(); // 0x58, factory + SfeGM602BArd exhaust; exhaust.begin(0x50); // re-jumpered + SfeGM602BArd ambient; ambient.begin(0x55); // re-jumpered - The two tri-state ADR jumpers give 3 x 3 = 9 addresses, and any board can use any of them - the - nine variants just SHIP on different ones so a one-of-each bus works untouched. Nine boards per + The two tri-state ADR jumpers give 3 x 3 = 9 addresses, and any board can use any of them. The + nine variants ship on different ones so a one-of-each bus works without changes. Nine boards per I2C port is therefore the ceiling; pass a different TwoWire to begin() for more. - Do NOT use beginAutoDetect() once you have re-addressed anything: it infers the part from the - address, so three GM-602Bs at 0x58/0x50/0x55 would come back as a GM-602B, a GM-102B and a - GM-502B - and the "GM-102B" would get an inverted curve and the wrong load resistor. - - Note that each sensor needs its OWN clean-air reference - R0 is a property of an individual + Each sensor needs its own clean-air reference, because R0 is a property of an individual sensing element, not of a part number. Winsen's part-to-part spread is around 10:1, so two - GM-602Bs in the same clean air will have genuinely different R0. + GM-602Bs in the same clean air will have different R0 values. SparkFun Electronics Date: 2026 @@ -51,13 +47,13 @@ // >>> Edit this list to match the boards on your bus. <<< // -// This uses the GENERIC class rather than the per-part ones (SfeGM602BArdI2C and friends), because +// This uses the GENERIC class rather than the per-part ones (SfeGM602BArd and friends), because // one array has to hold one type. Per-part classes are nicer when a sketch names each board -// individually - see the comment above. -SfeGMX02BArdI2C sensors[] = { - SfeGMX02BArdI2C(GM_602B), // H2S at 0x58 - SfeGMX02BArdI2C(GM_702B), // CO at 0x59 - SfeGMX02BArdI2C(GM_102B), // NO2 at 0x50 - resistance RISES with gas +// individually (see the comment above). +SfeGMX02BArd sensors[] = { + SfeGMX02BArd(GM_602B), // H2S at 0x58 + SfeGMX02BArd(GM_702B), // CO at 0x59 + SfeGMX02BArd(GM_102B), // NO2 at 0x50, resistance RISES with gas }; const uint8_t kSensorCount = sizeof(sensors) / sizeof(sensors[0]); @@ -69,7 +65,7 @@ const float kAlertResponse = 1.5; void setup() { Serial.begin(115200); - Serial.println("SparkFun GM-x02B Example 6 - Multiple Sensors"); + Serial.println("SparkFun GM-x02B Example 5 - Multiple Sensors"); Wire.begin(); @@ -98,7 +94,7 @@ void setup() // Each element needs its own R0. Do this once, in clean air, warmed up. Serial.println(); - Serial.println("Calibrating in clean air - make sure the air is clean..."); + Serial.println("Calibrating, make sure the sensors are in clean air..."); for (uint8_t i = 0; i < kSensorCount; i++) { @@ -120,7 +116,7 @@ void setup() void loop() { - // One loop body, three parts, two curve directions. No special cases. + // The same code handles every part, regardless of curve direction. for (uint8_t i = 0; i < kSensorCount; i++) { if (!online[i]) diff --git a/keywords.txt b/keywords.txt index faca362..e4228ea 100644 --- a/keywords.txt +++ b/keywords.txt @@ -1,16 +1,16 @@ # Syntax Coloring Map for SparkFun GM-X02B Arduino Library # Datatypes / Classes (KEYWORD1) -SfeGMX02BArdI2C KEYWORD1 -SfeGM102BArdI2C KEYWORD1 -SfeGM202BArdI2C KEYWORD1 -SfeGM302BArdI2C KEYWORD1 -SfeGM402BArdI2C KEYWORD1 -SfeGM502BArdI2C KEYWORD1 -SfeGM512BArdI2C KEYWORD1 -SfeGM602BArdI2C KEYWORD1 -SfeGM702BArdI2C KEYWORD1 -SfeGM802BArdI2C KEYWORD1 +SfeGMX02BArd KEYWORD1 +SfeGM102BArd KEYWORD1 +SfeGM202BArd KEYWORD1 +SfeGM302BArd KEYWORD1 +SfeGM402BArd KEYWORD1 +SfeGM502BArd KEYWORD1 +SfeGM512BArd KEYWORD1 +SfeGM602BArd KEYWORD1 +SfeGM702BArd KEYWORD1 +SfeGM802BArd KEYWORD1 sfDevGMX02B KEYWORD1 sfe_gmx02b_sensor_t KEYWORD1 sfe_gmx02b_cycle_time_t KEYWORD1 @@ -18,9 +18,6 @@ sfe_gmx02b_part_t KEYWORD1 # Methods and Functions (KEYWORD2) begin KEYWORD2 -beginAutoDetect KEYWORD2 -scanFamily KEYWORD2 -findMisplacedBoard KEYWORD2 isConnected KEYWORD2 probeADC KEYWORD2 getAddress KEYWORD2 @@ -31,7 +28,6 @@ getSensorName KEYWORD2 getTargetGas KEYWORD2 getDefaultAddress KEYWORD2 rsRisesWithGas KEYWORD2 -isIdentityAssumed KEYWORD2 getRangeMin_ppm KEYWORD2 getRangeMax_ppm KEYWORD2 getRatedSensitivity KEYWORD2 @@ -48,6 +44,7 @@ getSupplyVoltage_V KEYWORD2 setLoadResistance_Ohm KEYWORD2 getLoadResistance_Ohm KEYWORD2 voltageToCounts KEYWORD2 +countsToVoltage_V KEYWORD2 resistanceToCounts KEYWORD2 responseToCounts KEYWORD2 measureCleanAirResistance KEYWORD2 @@ -75,6 +72,7 @@ clearAlertStatus KEYWORD2 setCycleTime KEYWORD2 getCycleTime KEYWORD2 getPeakGasCounts KEYWORD2 +getPeakGasVoltage_V KEYWORD2 clearPeakGasCounts KEYWORD2 getLowestConversion KEYWORD2 getHighestConversion KEYWORD2 @@ -83,8 +81,6 @@ clearHighestConversion KEYWORD2 sfe_gmx02b_partInfo KEYWORD2 sfe_gmx02b_partName KEYWORD2 sfe_gmx02b_targetGas KEYWORD2 -sfe_gmx02b_sensorFromAddress KEYWORD2 -sfe_gmx02b_familyAddresses KEYWORD2 # Constants (LITERAL1) GM_102B LITERAL1 diff --git a/library.properties b/library.properties index 0c6c55d..5ed682f 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=SparkFun Qwiic Gas Sensor GM-X02B -version=2.0.0 +version=1.0.0 author=SparkFun Electronics maintainer=SparkFun Electronics sentence=An Arduino library for the SparkFun Qwiic MEMS gas sensor breakouts built on the Winsen GM-x02B sensor family diff --git a/src/SparkFun_GMX02B.h b/src/SparkFun_GMX02B.h index 2217001..03c04fc 100644 --- a/src/SparkFun_GMX02B.h +++ b/src/SparkFun_GMX02B.h @@ -4,25 +4,21 @@ * * @details * This file provides the Arduino-specific wrapper for the SparkFun Qwiic gas sensor boards built - * around the Winsen GM-x02B MEMS sensing elements. SfeGMX02BArdI2C inherits from the + * around the Winsen GM-x02B MEMS sensing elements. SfeGMX02BArd inherits from the * platform-independent sfDevGMX02B driver and implements I2C communication using Arduino's Wire - * library via the SparkFun Toolkit. It is the only class a sketch needs. + * library via the SparkFun Toolkit. * - * IDENTITY AND ADDRESS ARE SEPARATE THINGS. Which sensing element is installed cannot be read back - * from the hardware - the onboard ADC101C021 has no ID register - so it is DECLARED, in the - * constructor. Which I2C address the board answers at is a jumper setting, so it is DISCOVERABLE - * and can be overridden independently: + * The sensor type and the I2C address are set separately. The installed sensing element cannot be + * read back from the hardware (the onboard ADC101C021 has no ID register), so it is declared in the + * constructor. The I2C address is set by jumpers and can be overridden in begin(): * - * SfeGMX02BArdI2C mySensor(GM_602B); + * SfeGMX02BArd mySensor(GM_602B); * mySensor.begin(); // this part's factory address, 0x58 * mySensor.begin(0x51); // jumpers moved: same sensor, new address, correct math * - * Keeping them separate matters because the declared part selects the sensitivity curve DIRECTION - * (see sfDevGMX02B). If identity were inferred from the address, a customer moving an ADR jumper to - * dodge a bus conflict could silently turn a GM-102B into a "GM-602B" and get inverted readings - * with no error anywhere. beginAutoDetect() is still available for the single-board case, and - * isIdentityAssumed() reports when the identity was a guess rather than a declaration. - * + * This matters because the declared part selects the sensitivity curve direction (see + * sfDevGMX02B). If the part were inferred from the address, moving an ADR jumper could cause a + * GM-102B to be treated as a GM-602B, inverting its readings. * * @author SparkFun Electronics * @date September 2026 @@ -42,28 +38,28 @@ // clang-format on /** - * @class SfeGMX02BArdI2C + * @class SfeGMX02BArd * @brief Arduino I2C implementation for the SparkFun Qwiic GM-x02B gas sensor breakouts. * * @details * Provides Arduino-specific I2C communication for the board's ADC101C021. Inherits all * measurement, calibration, alert, and per-part behavior from sfDevGMX02B and adds the begin() - * methods and bus discovery helpers required for Arduino. The class owns an sfTkArdI2C bus object - * which wraps the Arduino Wire library. + * methods required for Arduino. The class owns an sfTkArdI2C bus object which wraps the Arduino + * Wire library. * * @see sfDevGMX02B * @see TwoWire */ -class SfeGMX02BArdI2C : public sfDevGMX02B +class SfeGMX02BArd : public sfDevGMX02B { public: /** * @brief Construct a driver for a specific board variant. * - * @param sensor The GM-x02B variant installed on the board. Required - see sfDevGMX02B for + * @param sensor The GM-x02B variant installed on the board. Required; see sfDevGMX02B for * why there is no default. */ - SfeGMX02BArdI2C(sfe_gmx02b_sensor_t sensor) : sfDevGMX02B(sensor) + SfeGMX02BArd(sfe_gmx02b_sensor_t sensor) : sfDevGMX02B(sensor) { } @@ -72,8 +68,8 @@ class SfeGMX02BArdI2C : public sfDevGMX02B * * @note The sensing element needs preheating before its readings are meaningful: minutes for * the output to settle after power-up, and 24 to 168 hours of powered aging after long - * unpowered storage (see the datasheet for this part). begin() succeeding means the board is - * communicating - not that the sensor is warmed up. + * unpowered storage (see the datasheet for this part). A successful begin() means the board is + * communicating, not that the sensor is warmed up. * * @param wirePort TwoWire instance to use for I2C communication (default: Wire). * @return true if initialization is successful. @@ -87,8 +83,8 @@ class SfeGMX02BArdI2C : public sfDevGMX02B * @brief Initialize the board at an explicit I2C address. * * @details Use this when a board's ADR1 / ADR0 jumpers have been moved off the factory - * setting. The declared sensor variant - and therefore the curve direction and load resistance - * - is unaffected, which is the point: a re-addressed board still reads correctly. + * setting. The declared sensor variant, and therefore the curve direction and load resistance, + * is unaffected, so a re-addressed board still reads correctly. * * @param address 7-bit I2C address of the board. * @param wirePort TwoWire instance to use for I2C communication (default: Wire). @@ -96,11 +92,6 @@ class SfeGMX02BArdI2C : public sfDevGMX02B */ bool begin(uint8_t address, TwoWire &wirePort = Wire) { - // Remember what was asked for, even if the bus init fails - findMisplacedBoard() needs the - // address this object was pointed at, which is not the part's factory address once a board - // has been deliberately re-jumpered. - _requestedAddress = address; - // Initialize the Toolkit I2C bus with the given Wire port and address. if (_theI2CBus.init(wirePort, address) != ksfTkErrOk) return false; @@ -131,127 +122,6 @@ class SfeGMX02BArdI2C : public sfDevGMX02B return begin(getDefaultAddress(), wirePort); } - /** - * @brief Scan the family addresses and connect to the first board found, ASSUMING its identity. - * - * @details Tries each of the nine factory-default addresses in ascending order (0x50 to 0x5A), - * connects to the first that responds, and infers the sensor variant from that address. After - * success, getSensorName() reports the assumed part, getAddress() reports the address, and - * isIdentityAssumed() returns true. - * - * @warning The inferred identity selects the sensitivity curve direction, and it is only as - * good as the board's jumper settings. Addresses 0x50 to 0x57 are also the classic 24-series - * EEPROM block - SparkFun's own Qwiic EEPROM defaults to 0x50 - so on a mixed bus this can - * latch onto the wrong device entirely. Prefer declaring the part in the constructor for - * anything beyond a single-board demo. - * - * @param wirePort TwoWire instance to use for I2C communication (default: Wire). - * @param requireAdcProbe If true (the default), only accept an address whose registers look - * like an ADC101C021. See sfDevGMX02B::probeADC() for what that does and does not prove. - * @return true if a board was found and initialized. - */ - bool beginAutoDetect(TwoWire &wirePort = Wire, bool requireAdcProbe = true) - { - const uint8_t *addresses = nullptr; - uint8_t count = sfe_gmx02b_familyAddresses(&addresses); - - for (uint8_t i = 0; i < count; i++) - { - if (!begin(addresses[i], wirePort)) - continue; - - if (requireAdcProbe && !probeADC()) - continue; - - sfe_gmx02b_sensor_t sensor = sfe_gmx02b_sensorFromAddress(addresses[i]); - if (!setSensorType(sensor)) - continue; - - // setSensorType() adopted this part's load resistance and cleared the stale R0, but it - // also cleared the assumed-identity flag - the identity here really is a guess. - _identityAssumed = true; - return true; - } - - return false; - } - - /** - * @brief List which family addresses currently have something responding on the bus. - * - * @details A discovery utility: it returns ADDRESSES, not identities, and makes no claim about - * what is answering. Pair it with begin() plus probeADC() to check a candidate, or use it to - * report a bus layout to the user. - * - * @param found Output array that receives the responding addresses, in ascending order. - * @param maxFound Capacity of @p found. - * @param wirePort TwoWire instance to use for I2C communication (default: Wire). - * @return The number of addresses written to @p found. - */ - static uint8_t scanFamily(uint8_t *found, uint8_t maxFound, TwoWire &wirePort = Wire) - { - if (found == nullptr || maxFound == 0) - return 0; - - const uint8_t *addresses = nullptr; - uint8_t count = sfe_gmx02b_familyAddresses(&addresses); - uint8_t n = 0; - - for (uint8_t i = 0; i < count && n < maxFound; i++) - { - sfTkArdI2C bus; - - if (bus.init(wirePort, addresses[i]) != ksfTkErrOk) - continue; - - if (bus.ping() != ksfTkErrOk) - continue; - - found[n++] = addresses[i]; - } - - return n; - } - - /** - * @brief Diagnose the most likely wiring failure: a board whose address jumpers were moved. - * - * @details Call this after begin() has failed. If the address this object was pointed at is - * silent but exactly ONE other family address is responding, that address is almost certainly - * the board, re-jumpered - and passing it to begin() will read it correctly. If several - * addresses answer, the situation is ambiguous and this reports nothing rather than guessing. - * - * @note This is a SINGLE-BOARD diagnostic. On a bus carrying several family boards there is no - * way to tell which one moved, so it returns 0 - a safe no-answer rather than a wrong one. - * - * @param wirePort TwoWire instance to use for I2C communication (default: Wire). - * @return The single responding family address, or 0 if the expected address answers, nothing - * answers, or more than one other address answers. - */ - uint8_t findMisplacedBoard(TwoWire &wirePort = Wire) - { - // Compare against the address we were actually told to use, falling back to the part's - // factory address if begin() has not been called yet. - uint8_t expected = (_requestedAddress != 0) ? _requestedAddress : getDefaultAddress(); - - uint8_t found[GM_PART_COUNT] = {0}; - uint8_t n = scanFamily(found, sizeof(found), wirePort); - - uint8_t candidate = 0; - uint8_t others = 0; - - for (uint8_t i = 0; i < n; i++) - { - if (found[i] == expected) - return 0; // The expected address is answering - this is a different problem. - - candidate = found[i]; - others++; - } - - return (others == 1) ? candidate : 0; - } - /** * @brief Get the 7-bit I2C address this object is communicating on. * @return The configured I2C address. @@ -262,9 +132,6 @@ class SfeGMX02BArdI2C : public sfDevGMX02B } private: - /** @brief The address most recently passed to begin(); 0 until then. */ - uint8_t _requestedAddress = 0; - /** @brief Arduino I2C bus interface instance used for all communication with the board. */ sfTkArdI2C _theI2CBus; }; @@ -273,180 +140,181 @@ class SfeGMX02BArdI2C : public sfDevGMX02B // Per-Part Arduino Classes /////////////////////////////////////////////////////////////////////////////// // -// One thin class per board, following the same pattern as the INA2XX library's SfeINA228ArdI2C / -// SfeINA237ArdI2C. Each simply hands its part to the generic constructor, which means the board -// identity lives in the TYPE rather than in an argument: +// One thin class per board, following the same per-part pattern as the INA2XX library. Each passes +// its part to the generic constructor, so the part is set by the class type rather than by an +// argument: // -// SfeGM602BArdI2C gasSensor; // no enum to pass, no part to get wrong -// gasSensor.begin(); // 0x58 -// gasSensor.begin(0x51); // re-jumpered board, same correct math +// SfeGM602BArd gasSensor; // no enum to pass +// gasSensor.begin(); // 0x58 +// gasSensor.begin(0x51); // re-jumpered board // -// Prefer these for a sketch that knows its board at compile time. Use SfeGMX02BArdI2C directly -// when the part is chosen at runtime, or when you want one array holding several boards - see -// Example06. +// Prefer these for a sketch that knows its board at compile time. Use SfeGMX02BArd directly +// when the part is chosen at runtime, or when you want one array holding several boards (see +// Example05). /** - * @class SfeGM102BArdI2C + * @class SfeGM102BArd * @brief Arduino I2C implementation for the SparkFun Qwiic GM-102B gas sensor breakout (nitrogen dioxide (NO2)). * - * @details Convenience class - identical to SfeGMX02BArdI2C constructed with ::GM_102B, with - * the part baked into the type. Factory I2C address 0x50. + * @details Convenience class, equivalent to SfeGMX02BArd constructed with ::GM_102B. + * Factory I2C address 0x50. * - * @note Rs RISES with gas on this part - the only one in the family that does. The library handles it. + * @note Rs RISES with gas on this part, the only one in the family that does. + * The library handles this. * - * @see SfeGMX02BArdI2C + * @see SfeGMX02BArd * @see sfDevGMX02B */ -class SfeGM102BArdI2C : public SfeGMX02BArdI2C +class SfeGM102BArd : public SfeGMX02BArd { public: - SfeGM102BArdI2C() : SfeGMX02BArdI2C(GM_102B) + SfeGM102BArd() : SfeGMX02BArd(GM_102B) { } }; /** - * @class SfeGM202BArdI2C + * @class SfeGM202BArd * @brief Arduino I2C implementation for the SparkFun Qwiic GM-202B gas sensor breakout (alcohol and smoke). * - * @details Convenience class - identical to SfeGMX02BArdI2C constructed with ::GM_202B, with - * the part baked into the type. Factory I2C address 0x51. + * @details Convenience class, equivalent to SfeGMX02BArd constructed with ::GM_202B. + * Factory I2C address 0x51. * - * @see SfeGMX02BArdI2C + * @see SfeGMX02BArd * @see sfDevGMX02B */ -class SfeGM202BArdI2C : public SfeGMX02BArdI2C +class SfeGM202BArd : public SfeGMX02BArd { public: - SfeGM202BArdI2C() : SfeGMX02BArdI2C(GM_202B) + SfeGM202BArd() : SfeGMX02BArd(GM_202B) { } }; /** - * @class SfeGM302BArdI2C + * @class SfeGM302BArd * @brief Arduino I2C implementation for the SparkFun Qwiic GM-302B gas sensor breakout (ethanol vapor). * - * @details Convenience class - identical to SfeGMX02BArdI2C constructed with ::GM_302B, with - * the part baked into the type. Factory I2C address 0x52. + * @details Convenience class, equivalent to SfeGMX02BArd constructed with ::GM_302B. + * Factory I2C address 0x52. * - * @see SfeGMX02BArdI2C + * @see SfeGMX02BArd * @see sfDevGMX02B */ -class SfeGM302BArdI2C : public SfeGMX02BArdI2C +class SfeGM302BArd : public SfeGMX02BArd { public: - SfeGM302BArdI2C() : SfeGMX02BArdI2C(GM_302B) + SfeGM302BArd() : SfeGMX02BArd(GM_302B) { } }; /** - * @class SfeGM402BArdI2C + * @class SfeGM402BArd * @brief Arduino I2C implementation for the SparkFun Qwiic GM-402B gas sensor breakout (methane and propane). * - * @details Convenience class - identical to SfeGMX02BArdI2C constructed with ::GM_402B, with - * the part baked into the type. Factory I2C address 0x54. + * @details Convenience class, equivalent to SfeGMX02BArd constructed with ::GM_402B. + * Factory I2C address 0x54. * - * @see SfeGMX02BArdI2C + * @see SfeGMX02BArd * @see sfDevGMX02B */ -class SfeGM402BArdI2C : public SfeGMX02BArdI2C +class SfeGM402BArd : public SfeGMX02BArd { public: - SfeGM402BArdI2C() : SfeGMX02BArdI2C(GM_402B) + SfeGM402BArd() : SfeGMX02BArd(GM_402B) { } }; /** - * @class SfeGM502BArdI2C + * @class SfeGM502BArd * @brief Arduino I2C implementation for the SparkFun Qwiic GM-502B gas sensor breakout (VOCs). * - * @details Convenience class - identical to SfeGMX02BArdI2C constructed with ::GM_502B, with - * the part baked into the type. Factory I2C address 0x55. + * @details Convenience class, equivalent to SfeGMX02BArd constructed with ::GM_502B. + * Factory I2C address 0x55. * - * @see SfeGMX02BArdI2C + * @see SfeGMX02BArd * @see sfDevGMX02B */ -class SfeGM502BArdI2C : public SfeGMX02BArdI2C +class SfeGM502BArd : public SfeGMX02BArd { public: - SfeGM502BArdI2C() : SfeGMX02BArdI2C(GM_502B) + SfeGM502BArd() : SfeGMX02BArd(GM_502B) { } }; /** - * @class SfeGM512BArdI2C + * @class SfeGM512BArd * @brief Arduino I2C implementation for the SparkFun Qwiic GM-512B gas sensor breakout (breath odor (H2S, ethanol, acetone)). * - * @details Convenience class - identical to SfeGMX02BArdI2C constructed with ::GM_512B, with - * the part baked into the type. Factory I2C address 0x56. + * @details Convenience class, equivalent to SfeGMX02BArd constructed with ::GM_512B. + * Factory I2C address 0x56. * - * @see SfeGMX02BArdI2C + * @see SfeGMX02BArd * @see sfDevGMX02B */ -class SfeGM512BArdI2C : public SfeGMX02BArdI2C +class SfeGM512BArd : public SfeGMX02BArd { public: - SfeGM512BArdI2C() : SfeGMX02BArdI2C(GM_512B) + SfeGM512BArd() : SfeGMX02BArd(GM_512B) { } }; /** - * @class SfeGM602BArdI2C + * @class SfeGM602BArd * @brief Arduino I2C implementation for the SparkFun Qwiic GM-602B gas sensor breakout (hydrogen sulfide (H2S)). * - * @details Convenience class - identical to SfeGMX02BArdI2C constructed with ::GM_602B, with - * the part baked into the type. Factory I2C address 0x58. + * @details Convenience class, equivalent to SfeGMX02BArd constructed with ::GM_602B. + * Factory I2C address 0x58. * - * @see SfeGMX02BArdI2C + * @see SfeGMX02BArd * @see sfDevGMX02B */ -class SfeGM602BArdI2C : public SfeGMX02BArdI2C +class SfeGM602BArd : public SfeGMX02BArd { public: - SfeGM602BArdI2C() : SfeGMX02BArdI2C(GM_602B) + SfeGM602BArd() : SfeGMX02BArd(GM_602B) { } }; /** - * @class SfeGM702BArdI2C + * @class SfeGM702BArd * @brief Arduino I2C implementation for the SparkFun Qwiic GM-702B gas sensor breakout (carbon monoxide (CO)). * - * @details Convenience class - identical to SfeGMX02BArdI2C constructed with ::GM_702B, with - * the part baked into the type. Factory I2C address 0x59. + * @details Convenience class, equivalent to SfeGMX02BArd constructed with ::GM_702B. + * Factory I2C address 0x59. * * @note The datasheet specifies a cycled heater for this element, not a steady voltage. * - * @see SfeGMX02BArdI2C + * @see SfeGMX02BArd * @see sfDevGMX02B */ -class SfeGM702BArdI2C : public SfeGMX02BArdI2C +class SfeGM702BArd : public SfeGMX02BArd { public: - SfeGM702BArdI2C() : SfeGMX02BArdI2C(GM_702B) + SfeGM702BArd() : SfeGMX02BArd(GM_702B) { } }; /** - * @class SfeGM802BArdI2C + * @class SfeGM802BArd * @brief Arduino I2C implementation for the SparkFun Qwiic GM-802B gas sensor breakout (ammonia (NH3)). * - * @details Convenience class - identical to SfeGMX02BArdI2C constructed with ::GM_802B, with - * the part baked into the type. Factory I2C address 0x5A. + * @details Convenience class, equivalent to SfeGMX02BArd constructed with ::GM_802B. + * Factory I2C address 0x5A. * - * @see SfeGMX02BArdI2C + * @see SfeGMX02BArd * @see sfDevGMX02B */ -class SfeGM802BArdI2C : public SfeGMX02BArdI2C +class SfeGM802BArd : public SfeGMX02BArd { public: - SfeGM802BArdI2C() : SfeGMX02BArdI2C(GM_802B) + SfeGM802BArd() : SfeGMX02BArd(GM_802B) { } }; diff --git a/src/sfTk/sfDevGMX02B.cpp b/src/sfTk/sfDevGMX02B.cpp index 3ed9d0a..e624c05 100644 --- a/src/sfTk/sfDevGMX02B.cpp +++ b/src/sfTk/sfDevGMX02B.cpp @@ -30,7 +30,7 @@ // Sources: Winsen GM-x02B manuals (various revisions) for the electrical and range figures; the // SparkFun Qwiic gas sensor board design for the load resistance. // -// Two things to know about the datasheet numbers: +// Notes on the datasheet numbers: // // 1. rsMin / rsMax are PART-TO-PART SPREAD at a single reference gas concentration, not the // swing one unit produces. The GM-102B's 10 k to 1 M is a 100:1 manufacturing tolerance. @@ -55,7 +55,7 @@ static const sfe_gmx02b_part_t kGMParts[GM_PART_COUNT] = { // GM-102B - NO2. The family's only oxidizing-gas part: Rs RISES with concentration. The // datasheet states R0(air)/Rs(2 ppm NO2) <= 0.5, i.e. Rs >= 2 x R0, hence a rated response of // 2.0 in getGasResponse() units. Its Rs decade sits far above the other parts', so this board - // carries a 68.1 k load resistor rather than the family's 4.7 k - with 4.7 k, a unit at the + // carries a 68.1 k load resistor rather than the family's 4.7 k. With 4.7 k, a unit at the // high end of the spread produces under five ADC counts of usable swing. // NOTE: Winsen's product page gives the reference concentration as 5 ppm while manual V2.1 // gives 2 ppm; the manual value is used here. @@ -78,7 +78,7 @@ static const sfe_gmx02b_part_t kGMParts[GM_PART_COUNT] = { // GM-512B - breath / mouth odor: H2S, ethanol, acetone. Baseline resistance decade is an order // higher than the other reducing-gas parts (10 k to 100 k), which puts its worst-case - // quantization at ~2.3 % of reading on the shared 4.7 k load - well below the sensor's own + // quantization at ~2.3 % of reading on the shared 4.7 k load, well below the sensor's own // repeatability, so the board keeps the family value. {0x56, false, 4700.0f, 10000.0f, 100000.0f, 2.0f, 5.0f, 0.5f, 50.0f}, @@ -86,8 +86,8 @@ static const sfe_gmx02b_part_t kGMParts[GM_PART_COUNT] = { {0x58, false, 4700.0f, 1000.0f, 30000.0f, 3.0f, 50.0f, 0.5f, 50.0f}, // GM-702B - carbon monoxide (CO), also sensitive to H2. - // NOTE: this is the only part whose datasheet specifies a CYCLED heater - 2.5 V for 60 s then - // 0.5 V for 90 s - rather than a steady voltage. If the board drives the heater at a fixed + // NOTE: this is the only part whose datasheet specifies a CYCLED heater (2.5 V for 60 s then + // 0.5 V for 90 s) rather than a steady voltage. If the board drives the heater at a fixed // voltage, readings will not follow the datasheet's characterization method. {0x59, false, 4700.0f, 1000.0f, 30000.0f, 3.0f, 150.0f, 5.0f, 5000.0f}, @@ -107,10 +107,10 @@ const sfe_gmx02b_part_t *sfe_gmx02b_partInfo(sfe_gmx02b_sensor_t sensor) return &kGMParts[sensor]; } -// Names live behind a switch rather than in the descriptor struct on purpose: a table of const -// char* references every literal unconditionally, so the linker must keep all nine strings even in -// a sketch that never prints one. With a switch, unused arms are discarded - which matters on an -// AVR, where string literals are copied into RAM at startup. +// Names are returned from a switch rather than stored in the descriptor struct: a table of const +// char* references every literal, so the linker must keep all nine strings even in a sketch that +// never prints one. With a switch, unused arms are discarded. This matters on AVR, where string +// literals are copied into RAM at startup. const char *sfe_gmx02b_partName(sfe_gmx02b_sensor_t sensor) { switch (sensor) @@ -165,29 +165,6 @@ const char *sfe_gmx02b_targetGas(sfe_gmx02b_sensor_t sensor) } } -sfe_gmx02b_sensor_t sfe_gmx02b_sensorFromAddress(uint8_t address) -{ - for (uint8_t i = 0; i < GM_PART_COUNT; i++) - { - if (kGMParts[i].defaultAddress == address) - return (sfe_gmx02b_sensor_t)i; - } - - return GM_UNKNOWN; -} - -uint8_t sfe_gmx02b_familyAddresses(const uint8_t **addresses) -{ - // Ascending order, matching the descriptor table. - static const uint8_t kAddresses[GM_PART_COUNT] = {0x50, 0x51, 0x52, 0x54, 0x55, - 0x56, 0x58, 0x59, 0x5A}; - - if (addresses != nullptr) - *addresses = kAddresses; - - return GM_PART_COUNT; -} - // ========================= Setup & Identity =============================== sfTkError_t sfDevGMX02B::begin(sfTkIBus *theBus) @@ -206,8 +183,8 @@ sfTkError_t sfDevGMX02B::begin(sfTkIBus *theBus) bool sfDevGMX02B::isConnected(void) { - // There is no ID register on either the ADC or the sensing element, so the register-layout - // signature is the strongest identity check available. See probeADC(). + // Neither the ADC nor the sensing element has an ID register, so check the register-layout + // signature instead. See probeADC(). return probeADC(); } @@ -215,11 +192,11 @@ bool sfDevGMX02B::probeADC(bool assumeUnconfigured) { // Every 16-bit value register carries its 10 bits in D11:D2, so D15:D12 and D1:D0 must read // back as zero. Five registers x 6 bits, plus one reserved config bit, is 31 required-zero - // bits - enough to reject a 24-series EEPROM holding anything but zeros. + // bits, enough to reject a 24-series EEPROM holding anything but zeros. // - // Deliberately read-only. A write-and-read-back probe would be a stronger test, but at these - // addresses the device might be an EEPROM, and the "register pointer" would be a word address: - // the probe itself would destroy customer data. + // Read-only. A write-and-read-back probe would be a stronger test, but at these addresses the + // device might be an EEPROM, where the "register pointer" is a word address and a write would + // overwrite stored data. static const uint8_t kValueRegs[] = {kRegLowLimit, kRegHighLimit, kRegHysteresis, kRegLowestConversion, kRegHighestConversion}; @@ -260,16 +237,15 @@ bool sfDevGMX02B::setSensorType(sfe_gmx02b_sensor_t sensor) { const sfe_gmx02b_part_t *part = sfe_gmx02b_partInfo(sensor); if (part == nullptr) - return false; // Includes GM_UNKNOWN - leaves the previous part in place. + return false; // Includes GM_UNKNOWN; leaves the previous part in place. _sensorType = sensor; _part = part; // Adopt this board variant's load resistor, discarding any setLoadResistance_Ohm() override. A - // stale R0 from a different part would be meaningless, so drop it too. + // stale R0 from a different part is not valid, so clear it too. _loadResistance = part->loadResistance; _cleanAirResistance = 0.0f; - _identityAssumed = false; return true; } @@ -304,11 +280,6 @@ bool sfDevGMX02B::rsRisesWithGas(void) return _part->rsRisesWithGas; } -bool sfDevGMX02B::isIdentityAssumed(void) -{ - return _identityAssumed; -} - float sfDevGMX02B::getRangeMin_ppm(void) { return _part->rangeMin; @@ -356,8 +327,7 @@ uint16_t sfDevGMX02B::getRaw(void) float sfDevGMX02B::getVoltage_V(void) { - // The ADC uses its supply as the reference: 1 LSB = VA / 1024. - return (float)_counts * kAdcFullScaleInv * _supplyVoltage; + return countsToVoltage_V(_counts); } sfTkError_t sfDevGMX02B::getSensorResistance_Ohm(float &ohms) @@ -365,10 +335,10 @@ sfTkError_t sfDevGMX02B::getSensorResistance_Ohm(float &ohms) ohms = 0.0f; // The sensor and load resistor form a divider: Vout = VA x RL / (Rs + RL), which rearranges - // to Rs = RL x (VA - Vout) / Vout. In ADC counts the supply cancels out entirely: + // to Rs = RL x (VA - Vout) / Vout. In ADC counts the supply cancels out: // Rs = RL x (1024 - counts) / counts. if (_counts == 0) - return ksfTkErrFail; // No measurable current through the divider - resistance is off scale. + return ksfTkErrFail; // No measurable current through the divider; resistance is off scale. ohms = _loadResistance * (float)(kAdcFullScale - _counts) / (float)_counts; return ksfTkErrOk; @@ -403,7 +373,7 @@ sfTkError_t sfDevGMX02B::getGasResponse(float &response) return rc; if (rs <= 0.0f) - return ksfTkErrFail; // Off scale - no usable response. + return ksfTkErrFail; // Off scale, no usable response. // Normalize so 1.0 is clean air and larger always means more target gas, whichever way this // part's sensing element moves. This is also the direction the datasheets quote sensitivity in. @@ -451,6 +421,12 @@ uint16_t sfDevGMX02B::voltageToCounts(float volts) return (uint16_t)counts; } +float sfDevGMX02B::countsToVoltage_V(uint16_t counts) +{ + // The ADC uses its supply as the reference: 1 LSB = VA / 1024. + return (float)counts * kAdcFullScaleInv * _supplyVoltage; +} + uint16_t sfDevGMX02B::resistanceToCounts(float ohms) { // Inverse of getSensorResistance_Ohm(): counts = 1024 x RL / (Rs + RL). @@ -506,7 +482,7 @@ sfTkError_t sfDevGMX02B::measureCleanAirResistance(float &r0, uint8_t samples, u uint16_t average = (uint16_t)(sum / samples); if (average == 0) - return ksfTkErrFail; // Output too low to compute a resistance - is the sensor preheated? + return ksfTkErrFail; // Output too low to compute a resistance. Is the sensor preheated? r0 = _loadResistance * (float)(kAdcFullScale - average) / (float)average; _cleanAirResistance = r0; @@ -544,8 +520,8 @@ sfTkError_t sfDevGMX02B::getConcentration_ppm(float &ppm) if (rc != ksfTkErrOk) return rc; - // Kept out of the measurement path deliberately: pow() drags a sizeable chunk of libm into an - // AVR build, and a sketch that never asks for ppm should not pay for it. + // Kept out of the measurement path because pow() pulls a large part of libm into an AVR build, + // and a sketch that never asks for ppm should not pay for it. ppm = _curveA * powf(response, _curveB); return ksfTkErrOk; } @@ -627,7 +603,7 @@ sfTkError_t sfDevGMX02B::getAlertHysteresis_mV(float &millivolts) if (rc != ksfTkErrOk) return rc; - millivolts = (float)counts * kAdcFullScaleInv * _supplyVoltage * 1000.0f; + millivolts = countsToVoltage_V(counts) * 1000.0f; return ksfTkErrOk; } @@ -698,6 +674,19 @@ sfTkError_t sfDevGMX02B::getPeakGasCounts(uint16_t &counts) return _part->rsRisesWithGas ? getLowestConversion(counts) : getHighestConversion(counts); } +sfTkError_t sfDevGMX02B::getPeakGasVoltage_V(float &volts) +{ + volts = 0.0f; + + uint16_t counts = 0; + sfTkError_t rc = getPeakGasCounts(counts); + if (rc != ksfTkErrOk) + return rc; + + volts = countsToVoltage_V(counts); + return ksfTkErrOk; +} + sfTkError_t sfDevGMX02B::clearPeakGasCounts(void) { return _part->rsRisesWithGas ? clearLowestConversion() : clearHighestConversion(); diff --git a/src/sfTk/sfDevGMX02B.h b/src/sfTk/sfDevGMX02B.h index 059f700..6b03021 100644 --- a/src/sfTk/sfDevGMX02B.h +++ b/src/sfTk/sfDevGMX02B.h @@ -11,7 +11,7 @@ * * The ADC101C021 also provides a programmable out-of-range alert function: with automatic * conversion mode enabled, the ADC monitors the sensor continuously in hardware and flags (or - * signals on its ALERT pin) whenever the reading crosses a programmable high or low limit - no + * signals on its ALERT pin) whenever the reading crosses a programmable high or low limit, with no * polling required. The lowest and highest conversion results are tracked in hardware as well. * * @note SENSOR DIRECTION. Eight of the nine elements sense reducing gases: their resistance (Rs) @@ -26,7 +26,7 @@ * for the output to settle after power-up, and 24 to 168 hours of powered aging after long unpowered * storage (the exact schedule is per-part; see the datasheet). Out of the box these sensors are good * for relative measurement and threshold alerts. Absolute ppm requires calibration against a known - * gas concentration - see setResponseCurve(). + * gas concentration (see setResponseCurve()). * * @author SparkFun Electronics * @date September 2026 @@ -53,12 +53,11 @@ /** * @brief The Winsen GM-x02B sensing elements carried by the SparkFun Qwiic gas sensor boards. * - * @details These are ordinals, not I2C addresses - they index the per-part descriptor table. Use + * @details These are ordinals, not I2C addresses; they index the per-part descriptor table. Use * sfe_gmx02b_partInfo() or getDefaultAddress() to obtain a part's factory address. * * @note The onboard ADC101C021 has no product ID register, so the sensing element cannot be read - * back from the hardware. Which variant is installed is a fact about the board that was purchased; - * it must be declared by the sketch, or assumed from the I2C address (see isIdentityAssumed()). + * back from the hardware and must be declared by the sketch. */ typedef enum sfe_gmx02b_sensor_t : uint8_t { @@ -105,7 +104,7 @@ typedef enum sfe_gmx02b_cycle_time_t : uint8_t /////////////////////////////////////////////////////////////////////////////// /** - * @brief Everything the driver needs to know about one GM-x02B variant and its board. + * @brief Per-part parameters for one GM-x02B variant and its board. * * @details One const instance per family member, indexed by ::sfe_gmx02b_sensor_t. This is the only * place in the library that carries part-specific knowledge; adding a variant means adding a row. @@ -151,24 +150,6 @@ const char *sfe_gmx02b_partName(sfe_gmx02b_sensor_t sensor); */ const char *sfe_gmx02b_targetGas(sfe_gmx02b_sensor_t sensor); -/** - * @brief Map a factory-default I2C address back to its family member. - * - * @note This is an ASSUMPTION, not a measurement - it holds only while a board's ADR jumpers are - * at their factory setting and no other device occupies a family address. - * - * @param address The 7-bit I2C address. - * @return The matching variant, or ::GM_UNKNOWN. - */ -sfe_gmx02b_sensor_t sfe_gmx02b_sensorFromAddress(uint8_t address); - -/** - * @brief Get the family's nine factory-default addresses, in ascending order. - * @param addresses Output pointer that receives the address table. - * @return The number of addresses in the table. - */ -uint8_t sfe_gmx02b_familyAddresses(const uint8_t **addresses); - /////////////////////////////////////////////////////////////////////////////// // Class Declaration /////////////////////////////////////////////////////////////////////////////// @@ -195,8 +176,8 @@ class sfDevGMX02B * * @details The sensing element cannot be identified from the hardware, so declaring it here is * how the driver learns the curve direction and the board's load resistance. There is - * deliberately no default: a wrong part silently inverts the GM-102B's readings and picks the - * wrong load resistor, so the library makes you say which board you have. + * no default, because the wrong part inverts the GM-102B's readings and selects the wrong load + * resistor. * * @param sensor The sensor variant installed on the board. ::GM_UNKNOWN and * out-of-range values are a programming error; they fall back to the GM-602B's circuit @@ -224,12 +205,10 @@ class sfDevGMX02B * * @details Verifies that the device at the configured address has the register signature of an * ADC101C021 (see probeADC()). begin() calls this, so a successful begin() means the board - * both answered and looked right. + * responded with a valid register signature. * - * @note Unlike most SparkFun boards, this one has no ID register to verify identity against - - * the ADC101C021 does not have one, and neither does the Winsen sensing element. The register - * signature is the closest available substitute, and it confirms the ADC, never which gas - * sensor is wired to it. + * @note Neither the ADC101C021 nor the Winsen sensing element has an ID register. The register + * signature confirms the ADC is present, but not which gas sensor is connected to it. * * @return true if the device responds and its register signature matches. */ @@ -242,16 +221,15 @@ class sfDevGMX02B * D1:D0 and D15:D12 read back as zero on every value register, and D1 of the 8-bit * configuration register is reserved-zero. That is 31 required-zero bits across six registers. * - * This check is deliberately READ-ONLY. Addresses 0x50 to 0x57 are also the classic 24-series + * This check is read-only. Addresses 0x50 to 0x57 are also the classic 24-series * EEPROM block (SparkFun's own Qwiic EEPROM defaults to 0x50), and writing a probe pattern to * a register that turned out to be an EEPROM word address would corrupt a customer's data. * - * @note This confirms the ADC, never the gas sensor. A fully zeroed EEPROM would also pass, so - * treat a true result as raised confidence rather than proof of identity. + * @note This confirms the ADC, not the gas sensor. A fully zeroed EEPROM would also pass. * * @param assumeUnconfigured If true, additionally require the alert limit registers to hold - * their power-on values (low = 0, high = 1023). Much stronger, but only valid before the - * sketch has configured the alert function. + * their power-on values (low = 0, high = 1023). This is a stronger check, but only valid + * before the sketch has configured the alert function. * @return true if the device's registers are consistent with an ADC101C021. */ bool probeADC(bool assumeUnconfigured = false); @@ -263,7 +241,7 @@ class sfDevGMX02B * * @details Also resets the load resistance to the board default for that variant, discarding * any prior setLoadResistance_Ohm() override. Does not touch the stored clean-air resistance, which - * is no longer meaningful if the part changed - re-run measureCleanAirResistance(). + * is no longer valid if the part changed; re-run measureCleanAirResistance(). * * @param sensor The sensor variant. ::GM_UNKNOWN and out-of-range values are rejected. * @return true if the variant was accepted. @@ -271,7 +249,7 @@ class sfDevGMX02B bool setSensorType(sfe_gmx02b_sensor_t sensor); /** - * @brief Get the declared (or assumed) sensor variant. + * @brief Get the declared sensor variant. * @return The sensor variant. */ sfe_gmx02b_sensor_t getSensorType(void); @@ -311,17 +289,6 @@ class sfDevGMX02B */ bool rsRisesWithGas(void); - /** - * @brief Report whether the sensor identity was assumed rather than declared. - * - * @details Set when the identity came from an I2C address scan instead of the constructor or - * setSensorType(). An assumed identity is only as good as the board's jumper settings, and it - * selects the curve direction - so sketches and diagnostics should surface it. - * - * @return true if the identity was inferred from the I2C address. - */ - bool isIdentityAssumed(void); - /** * @brief Get the low end of the part's detection range. * @return Concentration in ppm. @@ -442,7 +409,8 @@ class sfDevGMX02B // ====================== Board Circuit Parameters ====================== /** - * @brief Set the sensing circuit supply voltage used by getVoltage_V() and voltageToCounts(). + * @brief Set the sensing circuit supply voltage used by getVoltage_V(), voltageToCounts() and + * countsToVoltage_V(). * * @details The ADC101C021 uses its supply as the conversion reference. On the SparkFun boards * both the ADC and the sensing loop run from the 3.3 V Qwiic rail (the default). @@ -484,6 +452,18 @@ class sfDevGMX02B */ uint16_t voltageToCounts(float volts); + /** + * @brief Convert 10-bit ADC counts into the equivalent sensor output voltage. + * + * @details The inverse of voltageToCounts(): volts = counts / 1024 x supply voltage. Useful for + * expressing raw register values, such as the alert limits or the lowest / highest conversion + * registers, in volts. + * + * @param counts Raw ADC counts (0 to 1023). + * @return Equivalent sensor output voltage in volts. + */ + float countsToVoltage_V(uint16_t counts); + /** * @brief Convert a sensor resistance into the equivalent 10-bit ADC counts. * @@ -548,7 +528,7 @@ class sfDevGMX02B * * @details Defines ppm = a x pow(getGasResponse(), b). No coefficients are shipped with this * library, and none should be inferred from the datasheets: Winsen publishes no numeric curve - * data for any GM-x02B part, and the GM-602B manual explicitly describes the sensitivity as + * data for any GM-x02B part, and the GM-602B manual describes the sensitivity as * "non linear, hysteretic and non-repetitive". Fit @p a and @p b yourself against known gas * concentrations for your part, in your enclosure, at your operating temperature and humidity. * @@ -581,7 +561,7 @@ class sfDevGMX02B * @brief Set a hardware gas threshold in gas response units. * * @details Converts @p response into ADC counts and writes it to whichever limit register - * corresponds to "more gas" for this part - the high limit on falling-Rs parts, the low limit + * corresponds to "more gas" for this part: the high limit on falling-Rs parts, the low limit * on the GM-102B. Pair with getGasAlert(), which maps the status flags back the same way. * * @note Writes one limit register and leaves the other alone. If you previously set the other @@ -664,9 +644,8 @@ class sfDevGMX02B /** * @brief Set the alert hysteresis as a sensor-output voltage band. * - * @details Convenience form of setAlertHysteresis() - the ADC's hysteresis field is in raw - * counts, which is not a quantity anyone thinks in. Hysteresis is linear in the sensor's - * output voltage, so millivolts convert exactly. + * @details Convenience form of setAlertHysteresis(), which takes raw counts. Hysteresis is + * linear in the sensor's output voltage, so millivolts convert exactly. * * @param millivolts Hysteresis in millivolts of sensor output. * @return ::ksfTkErrOk on success, or an error code on failure. @@ -693,20 +672,20 @@ class sfDevGMX02B * * @details The ALERT pin is broken out on the board's PTH header. Per the ADC101C021 * datasheet it "is an open drain data line that must be pulled to the supply (VA) with an - * external pull-up resistor" - open-drain in BOTH polarity modes, since setAlertPinActiveHigh() - * changes only the logic sense, not the output structure. + * external pull-up resistor". It is open-drain in both polarity modes, since + * setAlertPinActiveHigh() changes only the logic sense, not the output structure. * - * @warning THE SPARKFUN BOARD DOES NOT POPULATE A PULL-UP ON ALERT. Nothing holds the pin - * high on its own, so it floats and reads as garbage until you provide one. + * @warning THE SPARKFUN BOARD DOES NOT POPULATE A PULL-UP ON ALERT. The pin floats until you + * provide one. * * @note An external resistor is usually unnecessary: the pin's high-impedance leakage is 1 uA * max, so an MCU's internal pull-up (pinMode(pin, INPUT_PULLUP), typically 20-50 kOhm) holds it * within ~50 mV of the rail. Fit a discrete 10 kOhm to 3.3 V when the MCU pin has no internal - * pull-up, when several boards share one alert line, or when a long trace needs faster edges - - * that gives 330 uA of sink current, 10 mV of droop and a ~0.2 us edge. Stay between 1 kOhm - * and 100 kOhm. Pulling low is never the problem - VOL is 0.4 V max at 3 mA. + * pull-up, when several boards share one alert line, or when a long trace needs faster edges. + * That gives 330 uA of sink current, 10 mV of droop and a ~0.2 us edge. Stay between 1 kOhm + * and 100 kOhm. Pulling low is not an issue: VOL is 0.4 V max at 3 mA. * - * @note You do not need the pin at all to use the alert function. getGasAlert() reads the same + * @note The pin is not required to use the alert function. getGasAlert() reads the same * condition over I2C. When the pin is disabled (the default), it stays high-impedance. * * @param enable true to enable the ALERT pin, false to disable it. @@ -725,7 +704,7 @@ class sfDevGMX02B * @brief Enable or disable alert hold. * * @details When enabled, alerts do not self-clear when the reading moves back within the - * limits - they persist until cleared with clearAlertStatus(). + * limits; they persist until cleared with clearAlertStatus(). * * @param enable true to hold alerts, false to allow self-clearing. * @return ::ksfTkErrOk on success, or an error code on failure. @@ -761,8 +740,8 @@ class sfDevGMX02B * @details With a non-zero cycle time, the ADC converts continuously on its own: the alert * limits are evaluated and the lowest / highest conversion registers are updated after every * conversion, with no I2C traffic required. Pass ::GM_CYCLE_DISABLED to return to normal mode. - * Slower cycle times use less power; ::GM_CYCLE_TIME_2048 is plenty for a slow-responding MOS - * gas sensor. + * Slower cycle times use less power; ::GM_CYCLE_TIME_2048 is sufficient for a slow-responding + * MOS gas sensor. * * @param cycleTime The conversion interval (see ::sfe_gmx02b_cycle_time_t). * @return ::ksfTkErrOk on success, or an error code on failure. @@ -779,7 +758,7 @@ class sfDevGMX02B /** * @brief Get the peak GAS conversion recorded in automatic conversion mode. * - * @details Reads whichever hardware extremum register corresponds to "most gas" for this part - + * @details Reads whichever hardware extremum register corresponds to "most gas" for this part: * the highest conversion on falling-Rs parts, the lowest on the GM-102B. Only updated while * automatic conversion mode is active. * @@ -788,6 +767,16 @@ class sfDevGMX02B */ sfTkError_t getPeakGasCounts(uint16_t &counts); + /** + * @brief Get the peak gas conversion as a sensor output voltage. + * + * @details Same as getPeakGasCounts(), converted with countsToVoltage_V(). + * + * @param volts Output reference that receives the peak sensor output voltage in volts. + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t getPeakGasVoltage_V(float &volts); + /** * @brief Reset the peak gas tracking register for this part. * @return ::ksfTkErrOk on success, or an error code on failure. @@ -888,9 +877,8 @@ class sfDevGMX02B sfTkIBus *_theBus; ///< Pointer to the communication bus device. // --- Sensor identity --- - sfe_gmx02b_sensor_t _sensorType = GM_602B; ///< Declared or assumed sensor variant. + sfe_gmx02b_sensor_t _sensorType = GM_602B; ///< Declared sensor variant. const sfe_gmx02b_part_t *_part = nullptr; ///< Descriptor for _sensorType. Never null after construction. - bool _identityAssumed = false; ///< True if _sensorType came from an address scan. // --- Cached values from the most recent readMeasurement() --- uint16_t _counts = 0; ///< Raw 10-bit ADC value.