diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/build-deploy-ghpages.yml b/.github/workflows/build-deploy-ghpages.yml new file mode 100644 index 0000000..95df86b --- /dev/null +++ b/.github/workflows/build-deploy-ghpages.yml @@ -0,0 +1,64 @@ +name: Build Documentation and Deploy + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + id-token: write + pages: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + # Checkout the repository + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: "true" + + - name: Set Version + run: echo "PROJECT_NUMBER = `git describe --tags`" >> ./docs/doxygen/doxygen-config + + - name: Build Documentation + uses: mattnotmitt/doxygen-action@v1.9.5 + with: + doxyfile-path: "./docs/doxygen/doxygen-config" + + # Upload the documentation as an artifact + - name: Upload documentation + uses: actions/upload-pages-artifact@v3.0.1 + with: + path: ./docs/html + + # Deploy job + deploy: + # Add a dependency to the build job + needs: build + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + # Specify runner + deployment step + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action diff --git a/.github/workflows/test-compile-sketch.yml b/.github/workflows/test-compile-sketch.yml new file mode 100644 index 0000000..abf68a9 --- /dev/null +++ b/.github/workflows/test-compile-sketch.yml @@ -0,0 +1,91 @@ +name: Cross-compilation Library Test + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + compile-sketch: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + matrix: + board: + # Uno / AVR (Mega has the flash headroom for the examples) + - fqbn: arduino:avr:mega + name: arduino:avr + source-url: https://downloads.arduino.cc/packages/package_index.json + + # ESP32 + - fqbn: esp32:esp32:esp32 + name: esp32:esp32 + source-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + + # ESP32-C3 + - fqbn: esp32:esp32:esp32c3 + name: esp32:esp32 + source-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + + # ESP8266 + - fqbn: esp8266:esp8266:thingdev + name: esp8266:esp8266 + source-url: https://arduino.esp8266.com/stable/package_esp8266com_index.json + + # SAMD21 + - fqbn: arduino:samd:mkr1000 + name: arduino:samd + source-url: https://downloads.arduino.cc/packages/package_index.json + + # Nano BLE 33 / nRF52840 + - fqbn: arduino:mbed:nano33ble + name: arduino:mbed + source-url: https://downloads.arduino.cc/packages/package_index.json + + # RP2040 + - fqbn: rp2040:rp2040:sparkfun_promicrorp2040 + name: rp2040:rp2040 + source-url: https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json + + # RP2350 + - fqbn: rp2040:rp2040:sparkfun_promicrorp2350 + name: rp2040:rp2040 + source-url: https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json + + # STM32 + - fqbn: STMicroelectronics:stm32:GenF4 + name: STMicroelectronics:stm32 + source-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Branch name + run: echo running on branch ${GITHUB_REF##*/} + + - name: Arduino - Install and setup the Arduino CLI + uses: arduino/setup-arduino-cli@v2 + + - name: Arduino - Start config file + run: arduino-cli config init --additional-urls ${{ matrix.board.source-url}} + + - name: Arduino - Update index + run: arduino-cli core update-index + + - name: Arduino - Install platform + run: arduino-cli core install ${{ matrix.board.name}} + + - name: Arduino - Install library dependencies + run: arduino-cli lib install "SparkFun Toolkit" + + - name: Compile Sketches + run: | + for example in examples/*/; do + echo "Compiling $example" + arduino-cli compile --fqbn ${{ matrix.board.fqbn }} "$example" --library . + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4353847 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Local tool settings +.claude/ + +# Doxygen output (built and deployed by CI) +docs/html/ + +# OS cruft +.DS_Store +Thumbs.db diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a8d0052 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "docs/doxygen/doxygen-awesome-css"] + path = docs/doxygen/doxygen-awesome-css + url = https://github.com/jothepro/doxygen-awesome-css.git diff --git a/README.md b/README.md index af3e78c..eac6be5 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,10 @@ int16_t correction = 0; mySensor.performForcedRecalibration(420, correction); // outdoor air ~420 ppm ``` -`performFactoryReset()` clears all recalibration and self-calibration history. After more than 3 hours without power, `performConditioning()` (a one-off, blocking 22 s routine) is recommended to speed the sensor back to full accuracy. +`performFactoryReset()` clears all recalibration and self-calibration history. After more than 3 hours without power, `performConditioning()` is recommended to speed the sensor back to full accuracy. + +> [!IMPORTANT] +> `performConditioning()` **blocks for about 22 seconds** while the sensor runs its conditioning profile — the call does not return until it finishes. This is by far the longest-blocking call in the library, so run it once at startup (not inside `loop()`), and don't mistake the pause for a hang. Start a measurement afterwards. ## Examples diff --git a/docs/doxygen/doxygen-awesome-css b/docs/doxygen/doxygen-awesome-css new file mode 160000 index 0000000..d52eafe --- /dev/null +++ b/docs/doxygen/doxygen-awesome-css @@ -0,0 +1 @@ +Subproject commit d52eafe3e9303399fda15661f3d7bb8fe3d7eabc diff --git a/docs/doxygen/doxygen-config b/docs/doxygen/doxygen-config new file mode 100644 index 0000000..d8fddf0 --- /dev/null +++ b/docs/doxygen/doxygen-config @@ -0,0 +1,61 @@ +# Doxygen configuration for the SparkFun STCC4 Arduino Library +# The CI workflow appends a PROJECT_NUMBER line (from git describe) before running. + +DOXYFILE_ENCODING = UTF-8 +PROJECT_NAME = "SparkFun Qwiic CO2 Sensor - STCC4" +PROJECT_BRIEF = "Arduino Library for the SparkFun Qwiic CO2 Sensor (STCC4)" +OUTPUT_DIRECTORY = ./docs +CREATE_SUBDIRS = NO +ALLOW_UNICODE_NAMES = NO +OPTIMIZE_OUTPUT_FOR_C = NO +MARKDOWN_SUPPORT = YES +AUTOLINK_SUPPORT = YES + +# Extraction +EXTRACT_ALL = YES +EXTRACT_PRIVATE = NO +EXTRACT_STATIC = YES +HIDE_UNDOC_MEMBERS = NO +JAVADOC_AUTOBRIEF = YES + +# Input +INPUT = src \ + README.md \ + docs +INPUT_ENCODING = UTF-8 +FILE_PATTERNS = *.h *.cpp *.md +RECURSIVE = YES +IMAGE_PATH = docs/images +USE_MDFILE_AS_MAINPAGE = README.md + +# HTML output +GENERATE_HTML = YES +HTML_OUTPUT = html +GENERATE_TREEVIEW = YES +DISABLE_INDEX = NO +FULL_SIDEBAR = NO + +# SparkFun branding (doxygen-awesome-css theme + custom header/logo) +PROJECT_LOGO = ./docs/images/sfe_flame.png +HTML_HEADER = ./docs/doxygen/doxygen-custom/header.html +HTML_EXTRA_STYLESHEET = docs/doxygen/doxygen-awesome-css/doxygen-awesome.css \ + docs/doxygen/doxygen-awesome-css/doxygen-awesome-sidebar-only.css \ + docs/doxygen/doxygen-custom/custom.css +HTML_EXTRA_FILES = docs/images/sfe_flame.png +HTML_COLORSTYLE = AUTO_LIGHT +HTML_COLORSTYLE_HUE = 209 +HTML_COLORSTYLE_SAT = 255 +HTML_COLORSTYLE_GAMMA = 113 + +# Disable other generators +GENERATE_LATEX = NO +GENERATE_XML = NO +GENERATE_RTF = NO +GENERATE_MAN = NO + +# Diagrams +HAVE_DOT = NO + +# Warnings +QUIET = YES +WARN_IF_UNDOCUMENTED = NO diff --git a/docs/doxygen/doxygen-custom/custom.css b/docs/doxygen/doxygen-custom/custom.css new file mode 100644 index 0000000..92e001c --- /dev/null +++ b/docs/doxygen/doxygen-custom/custom.css @@ -0,0 +1,21 @@ +:root { + --side-nav-fixed-width: 300px; +} + +.github-corner svg { + fill: var(--primary-light-color); + color: var(--page-background-color); + width: 72px; + height: 72px; +} + +#projectnumber { + margin-right: 22px; +} + +@media screen and (max-width: 767px) { + .github-corner svg { + width: 55px; + height: 55px; + } +} diff --git a/docs/doxygen/doxygen-custom/header.html b/docs/doxygen/doxygen-custom/header.html new file mode 100644 index 0000000..bff281d --- /dev/null +++ b/docs/doxygen/doxygen-custom/header.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + $projectname: $title + + $title + + + + + $treeview + $search + $mathjax + + $extrastylesheet + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
+
$projectname +  $projectnumber +
+ +
$projectbrief
+
+
$projectbrief
+
$searchbox
+
+ + diff --git a/docs/images/sfe_flame.png b/docs/images/sfe_flame.png new file mode 100644 index 0000000..4141287 Binary files /dev/null and b/docs/images/sfe_flame.png differ diff --git a/examples/Example01_BasicReadings/Example01_BasicReadings.ino b/examples/Example01_BasicReadings/Example01_BasicReadings.ino new file mode 100644 index 0000000..e48c75d --- /dev/null +++ b/examples/Example01_BasicReadings/Example01_BasicReadings.ino @@ -0,0 +1,80 @@ +/* + Example 01 - Basic Readings + + The simplest way to read the SparkFun Qwiic CO2 Sensor (STCC4). This sketch starts + continuous measurement and reads the CO2 concentration, temperature, and relative + humidity about once per second. + + The board pairs the STCC4 with an SHT40 humidity and temperature sensor, wired to the + STCC4's dedicated sensor interface pins. The STCC4 reads the SHT40 by itself, uses the + values to compensate its CO2 output, and returns them with every measurement - so one + readMeasurement() call gets you all three values, already compensated. + + Note: After a long time without power, the STCC4 can take up to an hour of operation to + reach full accuracy, and it self-calibrates by assuming it sees fresh air (~400 ppm) at + least once per week. The very first 20 seconds after the first ever power-up output a + fixed bypass value of 390 ppm. + + 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 --> STCC4 + QWIIC --> QWIIC + + Open the Serial Monitor at 115200 baud. + + Feel like supporting our work? Buy a board from SparkFun! + https://www.sparkfun.com/ +*/ + +#include + +SfeSTCC4ArdI2C mySensor; + +void setup() +{ + // Start serial right away so we can report what is happening. + Serial.begin(115200); + Serial.println("SparkFun STCC4 Example 1 - Basic Readings"); + + // Start I2C communication. + Wire.begin(); + + // Attempt to connect to the sensor. Keep trying so the message is not missed if the + // Serial Monitor is opened late. + while (mySensor.begin() == false) + { + Serial.println("STCC4 not connected, check your wiring!"); + delay(1000); + } + + Serial.println("STCC4 connected!"); + + // Bring the sensor out of idle and into continuous measurement mode (1 s interval). + mySensor.startContinuousMeasurement(); + + // Wait for the first data point to become available. + delay(1000); + + Serial.println("CO2 (ppm)\tTemperature (C)\tHumidity (%RH)"); +} + +void loop() +{ + // Read a fresh data point: CO2, plus the temperature and humidity the STCC4 gathered + // from the onboard SHT40. This validates the CRC of every value before storing it, and + // automatically retries briefly if the next data point is not quite ready yet. + mySensor.readMeasurement(); + + Serial.print(mySensor.getCO2()); + Serial.print("\t\t"); + Serial.print(mySensor.getTemperature(), 1); + Serial.print("\t\t"); + Serial.println(mySensor.getHumidity(), 1); + + // The STCC4 produces a new data point every second. + delay(1000); +} diff --git a/examples/Example02_LowPowerSingleShot/Example02_LowPowerSingleShot.ino b/examples/Example02_LowPowerSingleShot/Example02_LowPowerSingleShot.ino new file mode 100644 index 0000000..925a9db --- /dev/null +++ b/examples/Example02_LowPowerSingleShot/Example02_LowPowerSingleShot.ino @@ -0,0 +1,83 @@ +/* + Example 02 - Low Power Single Shot + + The STCC4 supports a low-power, on-demand "single shot" measurement mode: the sensor + sleeps (about 1 uA) between measurements and is woken only when a reading is needed. + With a 10 second sampling interval the sensor averages under 100 uA, compared to about + 950 uA in continuous mode - ideal for battery-powered projects. + + Each cycle this sketch: + 1. Wakes the STCC4 from sleep mode. + 2. Triggers a single shot measurement (takes 500 ms) and reads the result. The STCC4 + reads the onboard SHT40 itself, so the measurement arrives already compensated and + includes temperature and humidity. + 3. Puts the STCC4 back to sleep and waits out the rest of the interval. + + Note: Keep the sampling interval between 5 and 600 seconds so the sensor's automatic + self-calibration algorithm works correctly. The first 2 single shot measurements after + the first ever power-up output a fixed bypass value of 390 ppm. + + 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 --> STCC4 + QWIIC --> QWIIC + + Open the Serial Monitor at 115200 baud. + + Feel like supporting our work? Buy a board from SparkFun! + https://www.sparkfun.com/ +*/ + +#include + +SfeSTCC4ArdI2C mySensor; + +// Time between measurements. The sensor sleeps for most of this interval. +const unsigned long kSamplingIntervalMs = 10000; + +void setup() +{ + Serial.begin(115200); + Serial.println("SparkFun STCC4 Example 2 - Low Power Single Shot"); + + Wire.begin(); + + while (mySensor.begin() == false) + { + Serial.println("STCC4 not connected, check your wiring!"); + delay(1000); + } + + Serial.println("STCC4 connected!"); + Serial.println("CO2 (ppm)\tTemperature (C)\tHumidity (%RH)"); + + // Start with the sensor asleep; the loop wakes it for each measurement. + mySensor.enterSleepMode(); +} + +void loop() +{ + // The sensor sleeps through this interval. Keep it between 5 and 600 seconds. (If your + // application needs measurements on a precise schedule, account for the ~500 ms measurement + // time yourself - for example with millis() timing instead of a plain delay.) + delay(kSamplingIntervalMs); + + // Wake the STCC4, trigger one measurement (blocks for the 500 ms measurement time), read it, + // then put the sensor back to sleep. Compensation values and the self-calibration state are + // retained while asleep. + mySensor.exitSleepMode(); + mySensor.measureSingleShot(); + mySensor.readMeasurement(); + + Serial.print(mySensor.getCO2()); + Serial.print("\t\t"); + Serial.print(mySensor.getTemperature(), 1); + Serial.print("\t\t"); + Serial.println(mySensor.getHumidity(), 1); + + mySensor.enterSleepMode(); +} diff --git a/examples/Example03_ProductInfo/Example03_ProductInfo.ino b/examples/Example03_ProductInfo/Example03_ProductInfo.ino new file mode 100644 index 0000000..001b3c2 --- /dev/null +++ b/examples/Example03_ProductInfo/Example03_ProductInfo.ino @@ -0,0 +1,77 @@ +/* + Example 03 - Product Info + + Every STCC4 reports a 32-bit product ID (0x0901018A) and a unique 64-bit serial number + assigned by Sensirion during production. This sketch reads and prints both - handy for + verifying communication and for telling boards apart. + + Note: The product ID can only be read while the STCC4 is in idle state (no measurement + running). begin() leaves the sensor idle, so this sketch reads it before starting any + measurement. + + 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 --> STCC4 + QWIIC --> QWIIC + + Open the Serial Monitor at 115200 baud. + + Feel like supporting our work? Buy a board from SparkFun! + https://www.sparkfun.com/ +*/ + +#include + +SfeSTCC4ArdI2C mySensor; + +// Print an unsigned value as a fixed number of hexadecimal digits, with leading zeros. +void printHexPadded(uint32_t value, uint8_t digits) +{ + for (int8_t shift = (digits - 1) * 4; shift >= 0; shift -= 4) + { + uint8_t nibble = (value >> shift) & 0x0F; + Serial.print(nibble, HEX); + } +} + +void setup() +{ + Serial.begin(115200); + Serial.println("SparkFun STCC4 Example 3 - Product Info"); + + Wire.begin(); + + while (mySensor.begin() == false) + { + Serial.println("STCC4 not connected, check your wiring!"); + delay(1000); + } + + Serial.println("STCC4 connected!"); + Serial.println(); + + // Read the product ID and unique serial number. + uint32_t productId = 0; + uint64_t serialNumber = 0; + mySensor.getProductId(productId, serialNumber); + + Serial.print("Product ID: 0x"); + printHexPadded(productId, 8); + Serial.println(" (expected 0x0901018A)"); + + // A uint64_t cannot be printed directly on all platforms, so split it into the upper + // and lower 32 bits and print each as zero-padded hex (8 + 8 = 16 digits). + Serial.print("Serial number: 0x"); + printHexPadded((uint32_t)(serialNumber >> 32), 8); + printHexPadded((uint32_t)(serialNumber & 0xFFFFFFFF), 8); + Serial.println(); +} + +void loop() +{ + // Nothing to do here. +} diff --git a/examples/Example04_SelfTest/Example04_SelfTest.ino b/examples/Example04_SelfTest/Example04_SelfTest.ino new file mode 100644 index 0000000..c5ee48a --- /dev/null +++ b/examples/Example04_SelfTest/Example04_SelfTest.ino @@ -0,0 +1,86 @@ +/* + Example 04 - Self-test + + The STCC4 has a built-in self-test that checks the supply voltage, internal memory, and + whether it can see the SHT4x on its dedicated sensor interface pins. It is useful for + verifying a board after assembly and for debugging. The test takes 360 ms; run it under + stable conditions (steady supply, temperature, and CO2 concentration). + + Result interpretation (bit masks, see sfe_stcc4_self_test_t): + 0x0000 : everything passed + bit 0 : supply voltage out of range + bits 3:1 : debug flags - contact Sensirion if nonzero + bit 4 : no SHT4x found on the STCC4's sensor interface pins + bits 6:5 : memory error (try a soft reset, then a power cycle) + + Note: On the SparkFun Qwiic CO2 Sensor - STCC4, the onboard SHT40 is wired to the + STCC4's sensor interface pins, so a fully healthy board reports 0x0000. If bit 4 is set, + the STCC4 cannot talk to the SHT40 - its readings will fall back to defaults (25 C, + 50 %RH) and the CO2 output will not be properly compensated. + + 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 --> STCC4 + QWIIC --> QWIIC + + Open the Serial Monitor at 115200 baud. + + Feel like supporting our work? Buy a board from SparkFun! + https://www.sparkfun.com/ +*/ + +#include + +SfeSTCC4ArdI2C mySensor; + +void setup() +{ + Serial.begin(115200); + Serial.println("SparkFun STCC4 Example 4 - Self-test"); + + Wire.begin(); + + while (mySensor.begin() == false) + { + Serial.println("STCC4 not connected, check your wiring!"); + delay(1000); + } + + Serial.println("STCC4 connected!"); + + // Run the self-test. This blocks for the 360 ms execution time. + Serial.println("Running self-test..."); + + uint16_t result = 0; + mySensor.performSelfTest(result); + + Serial.print("Self-test result: 0x"); + Serial.println(result, HEX); + + if (result == STCC4_SELF_TEST_OK) + { + Serial.println("Self-test PASSED."); + } + else + { + Serial.println("Self-test FAILED:"); + + if (result & STCC4_SELF_TEST_VDD_OUT_OF_RANGE) + Serial.println(" - Supply voltage is out of the specified range"); + if (result & STCC4_SELF_TEST_DEBUG_MASK) + Serial.println(" - Debug flags set; contact Sensirion for support"); + if (result & STCC4_SELF_TEST_SHT_NOT_CONNECTED) + Serial.println(" - The STCC4 cannot see the SHT40 on its sensor interface pins"); + if (result & STCC4_SELF_TEST_MEMORY_ERROR_MASK) + Serial.println(" - Memory error: soft reset the sensor, then power cycle if it persists"); + } +} + +void loop() +{ + // Nothing to do here. +} diff --git a/examples/Example05_ForcedRecalibration/Example05_ForcedRecalibration.ino b/examples/Example05_ForcedRecalibration/Example05_ForcedRecalibration.ino new file mode 100644 index 0000000..52c954d --- /dev/null +++ b/examples/Example05_ForcedRecalibration/Example05_ForcedRecalibration.ino @@ -0,0 +1,115 @@ +/* + Example 05 - Forced Recalibration + + The STCC4 continuously self-calibrates by assuming it sees fresh air (~400 ppm CO2) at + least once per week. If you need to correct the sensor immediately - for example after + installing it, or if it cannot regularly see fresh air - you can force a recalibration + against a known reference concentration. + + To use this sketch, place the board in air with a known CO2 concentration. Outdoor air + works well: it is roughly 420 ppm. Set kReferenceCO2 below to your reference value. The + sketch then: + 1. Runs continuous measurement for 60 seconds so the reading stabilizes. + 2. Stops the measurement (the sensor must be idle for recalibration). + 3. Performs the forced recalibration and prints the correction that was applied. + 4. Restarts continuous measurement so you can watch the corrected output. + + Keep the environment (and the CO2 concentration!) stable for the whole procedure. + To undo all forced recalibrations and self-calibration history, use performFactoryReset(). + + 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 --> STCC4 + QWIIC --> QWIIC + + Open the Serial Monitor at 115200 baud. + + Feel like supporting our work? Buy a board from SparkFun! + https://www.sparkfun.com/ +*/ + +#include + +SfeSTCC4ArdI2C mySensor; + +// The known CO2 concentration of the reference air around the sensor, in ppm. Outdoor air is ~420. +// ("Reference" rather than "target": this is the concentration the sensor is calibrated against, +// not a value the sketch drives the air toward.) +const uint16_t kReferenceCO2 = 420; + +// How long to measure before recalibrating. The datasheet requires at least 30 s. +const uint8_t kStabilizeSeconds = 60; + +// Take one reading and print the CO2 value. +void readAndPrint() +{ + mySensor.readMeasurement(); + + Serial.print("CO2: "); + Serial.print(mySensor.getCO2()); + Serial.println(" ppm"); +} + +void setup() +{ + Serial.begin(115200); + Serial.println("SparkFun STCC4 Example 5 - Forced Recalibration"); + + Wire.begin(); + + while (mySensor.begin() == false) + { + Serial.println("STCC4 not connected, check your wiring!"); + delay(1000); + } + + Serial.println("STCC4 connected!"); + Serial.print("Recalibrating to "); + Serial.print(kReferenceCO2); + Serial.println(" ppm. Keep the sensor in your reference air for the whole procedure."); + Serial.println(); + + // Step 1: measure for a while so the sensor output stabilizes in the reference air. + mySensor.startContinuousMeasurement(); + + Serial.println("Stabilizing..."); + delay(1000); + + for (uint8_t i = 0; i < kStabilizeSeconds; i++) + { + // Show how long is left so it is clear the sketch is still working. + Serial.print("Seconds remaining for stabilization: "); + Serial.println(kStabilizeSeconds - i); + + readAndPrint(); + delay(1000); + } + + // Step 2: the sensor must be idle for the recalibration command. + Serial.println("Stopping measurement (takes 1.2 s)..."); + mySensor.stopContinuousMeasurement(); + + // Step 3: recalibrate. The sensor reports the correction it applied to its output. + int16_t frcCorrection = 0; + mySensor.performForcedRecalibration(kReferenceCO2, frcCorrection); + + Serial.print("Forced recalibration applied. Correction: "); + Serial.print(frcCorrection); + Serial.println(" ppm"); + Serial.println(); + + // Step 4: back to normal operation - watch the corrected output. + mySensor.startContinuousMeasurement(); + + delay(1000); +} + +void loop() +{ + readAndPrint(); + delay(1000); +} diff --git a/examples/Example06_PressureCompensation/Example06_PressureCompensation.ino b/examples/Example06_PressureCompensation/Example06_PressureCompensation.ino new file mode 100644 index 0000000..0a53ef8 --- /dev/null +++ b/examples/Example06_PressureCompensation/Example06_PressureCompensation.ino @@ -0,0 +1,90 @@ +/* + Example 06 - Pressure Compensation + + The STCC4 assumes sea-level air pressure (101,300 Pa) by default. If you are at altitude, + or you have a barometric pressure sensor handy, writing the true ambient pressure to the + sensor improves CO2 accuracy. The value persists until overwritten or power-cycled, and + the sensor accepts 40,000 to 110,000 Pa. + + (Humidity and temperature compensation need no help from you - the STCC4 reads the + onboard SHT40 by itself. Pressure is the one input it cannot sense.) + + Set kAmbientPressurePa below for your location. Typical values by altitude: + + Altitude (m) | Pressure (Pa) + -------------|-------------- + Sea level | 101,300 + 500 | 95,500 + 1,000 | 89,900 + 1,564 (SFE!) | 83,900 + 2,000 | 79,500 + 3,000 | 70,100 + + If the pressure changes while your sketch runs (weather, or a moving platform), simply + call setPressureCompensation() again - it is accepted during measurement. + + 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 --> STCC4 + QWIIC --> QWIIC + + Open the Serial Monitor at 115200 baud. + + Feel like supporting our work? Buy a board from SparkFun! + https://www.sparkfun.com/ +*/ + +#include + +SfeSTCC4ArdI2C mySensor; + +// The ambient pressure at your location, in Pascals (1 hPa / mbar = 100 Pa). Defaults to sea +// level. For reference, SparkFun HQ in Boulder, Colorado sits at 1,564 m, where the pressure is +// about 83,900 Pa - set kAmbientPressurePa accordingly if you are at altitude. +const uint32_t kAmbientPressurePa = 101325; + +void setup() +{ + Serial.begin(115200); + Serial.println("SparkFun STCC4 Example 6 - Pressure Compensation"); + + Wire.begin(); + + while (mySensor.begin() == false) + { + Serial.println("STCC4 not connected, check your wiring!"); + delay(1000); + } + + Serial.println("STCC4 connected!"); + + // Tell the sensor the true ambient pressure before measuring. + mySensor.setPressureCompensation(kAmbientPressurePa); + + Serial.print("Pressure compensation set to "); + Serial.print(kAmbientPressurePa); + Serial.println(" Pa"); + + mySensor.startContinuousMeasurement(); + + delay(1000); + + Serial.println("CO2 (ppm)\tTemperature (C)\tHumidity (%RH)"); +} + +void loop() +{ + mySensor.readMeasurement(); + + Serial.print(mySensor.getCO2()); + Serial.print("\t\t"); + Serial.print(mySensor.getTemperature(), 1); + Serial.print("\t\t"); + Serial.println(mySensor.getHumidity(), 1); + + delay(1000); +} diff --git a/keywords.txt b/keywords.txt new file mode 100644 index 0000000..f85c5f5 --- /dev/null +++ b/keywords.txt @@ -0,0 +1,43 @@ +# Syntax Coloring Map for SparkFun STCC4 Arduino Library + +# Datatypes / Classes (KEYWORD1) +SfeSTCC4ArdI2C KEYWORD1 +sfDevSTCC4 KEYWORD1 + +# Methods and Functions (KEYWORD2) +begin KEYWORD2 +isConnected KEYWORD2 +reset KEYWORD2 +startContinuousMeasurement KEYWORD2 +stopContinuousMeasurement KEYWORD2 +measureSingleShot KEYWORD2 +readMeasurement KEYWORD2 +getCO2 KEYWORD2 +getHumidity KEYWORD2 +getTemperature KEYWORD2 +getTemperatureF KEYWORD2 +getStatus KEYWORD2 +isTestingModeEnabled KEYWORD2 +getHumidityRaw KEYWORD2 +getTemperatureRaw KEYWORD2 +setRHTCompensation KEYWORD2 +setPressureCompensation KEYWORD2 +enterSleepMode KEYWORD2 +exitSleepMode KEYWORD2 +performConditioning KEYWORD2 +performForcedRecalibration KEYWORD2 +performFactoryReset KEYWORD2 +performSelfTest KEYWORD2 +enableTestingMode KEYWORD2 +disableTestingMode KEYWORD2 +getProductId KEYWORD2 + +# Structures / Types (KEYWORD3) +sfe_stcc4_self_test_t KEYWORD3 + +# Constants (LITERAL1) +STCC4_SELF_TEST_OK LITERAL1 +STCC4_SELF_TEST_VDD_OUT_OF_RANGE LITERAL1 +STCC4_SELF_TEST_DEBUG_MASK LITERAL1 +STCC4_SELF_TEST_SHT_NOT_CONNECTED LITERAL1 +STCC4_SELF_TEST_MEMORY_ERROR_MASK LITERAL1 diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..e05b1c1 --- /dev/null +++ b/library.properties @@ -0,0 +1,10 @@ +name=SparkFun STCC4 Arduino Library +version=1.0.0 +author=SparkFun Electronics +maintainer=SparkFun Electronics +sentence=An Arduino library for the SparkFun Qwiic CO2 Sensor (STCC4) +paragraph=This library allows you to interface with the Sensirion STCC4 CO2 sensor and the onboard SHT40, providing CO2 concentration, temperature, and relative humidity (with automatic CO2 compensation) for your Arduino projects. +category=Sensors +url=https://github.com/sparkfun/SparkFun_STCC4_Arduino_Library +architectures=* +depends=SparkFun Toolkit (>=1.2.0) diff --git a/src/SparkFun_STCC4.h b/src/SparkFun_STCC4.h new file mode 100644 index 0000000..074d3e2 --- /dev/null +++ b/src/SparkFun_STCC4.h @@ -0,0 +1,89 @@ +/** + * @file SparkFun_STCC4.h + * @brief Arduino-specific implementation for the SparkFun Qwiic CO2 Sensor - STCC4. + * + * @details + * This file provides the Arduino-specific wrapper for the SparkFun Qwiic CO2 Sensor - STCC4. The + * SfeSTCC4ArdI2C class inherits from the platform-independent sfDevSTCC4 driver and implements I2C + * communication using Arduino's Wire library via the SparkFun Toolkit. + * + * On this board the companion SHT40 humidity and temperature sensor is wired to the STCC4's + * dedicated sensor interface pins, so the STCC4 reads it by itself: a single readMeasurement() + * returns compensated CO2 plus ambient temperature and humidity. SfeSTCC4ArdI2C is the only class + * a sketch needs. See the examples folder. + * + * @author SparkFun Electronics + * @date 2026 + * @copyright Copyright (c) 2026, SparkFun Electronics Inc. This project is released under the MIT License. + * + * SPDX-License-Identifier: MIT + * + * @see https://github.com/sparkfun/SparkFun_STCC4_Arduino_Library + */ + +#pragma once + +// clang-format off +#include +#include "sfTk/sfDevSTCC4.h" +#include +// clang-format on + +/** + * @class SfeSTCC4ArdI2C + * @brief Arduino I2C implementation for the STCC4 CO2 sensor. + * + * @details + * This class provides Arduino-specific I2C communication for the STCC4. It inherits all command + * access methods from sfDevSTCC4 (including reset()) and adds the begin() method required for + * Arduino initialization. The class owns an sfTkArdI2C bus object which wraps the Arduino Wire + * library. + * + * @see sfDevSTCC4 + * @see TwoWire + */ +class SfeSTCC4ArdI2C : public sfDevSTCC4 +{ + public: + SfeSTCC4ArdI2C() + { + } + + /** + * @brief Initializes the STCC4 with I2C communication. + * + * @details + * Initializes the Toolkit I2C bus, confirms the device is present on the bus, then calls the + * base class begin() to verify the device identity by reading and validating its product ID. + * The identity check requires the sensor to be idle; if the sensor was left asleep or left + * measuring (for example, the controller was reset or re-flashed mid-sketch while the sensor + * kept power), the base class begin() recovers automatically by waking it or stopping the + * running measurement. The sensor is always idle when begin() returns true. Worst case, the + * measurement-stop recovery blocks for its 1.2 s execution time. + * + * @param address 7-bit I2C address of the device: 0x64 with the ADDR pin low (the default on + * the SparkFun board), or 0x65 with the ADDR pin high. + * @param wirePort TwoWire instance to use for I2C communication (default: Wire). + * + * @return true If initialization is successful. + * @return false If any initialization step fails. + */ + bool begin(uint8_t address = kI2CAddressDefault, TwoWire &wirePort = Wire) + { + // Initialize the Toolkit I2C bus with the given Wire port and address. + if (_theI2CBus.init(wirePort, address) != ksfTkErrOk) + return false; + + // Confirm a device is actually responding at this address before we read from it. + if (_theI2CBus.ping() != ksfTkErrOk) + return false; + + // The base class begin() verifies the device identity (and recovers it to idle if it was + // left asleep or measuring). + return sfDevSTCC4::begin(&_theI2CBus) == ksfTkErrOk; + } + + private: + /** @brief Arduino I2C bus interface instance used for all communication with the STCC4. */ + sfTkArdI2C _theI2CBus; +}; diff --git a/src/sfTk/sfDevSTCC4.cpp b/src/sfTk/sfDevSTCC4.cpp new file mode 100644 index 0000000..cb79634 --- /dev/null +++ b/src/sfTk/sfDevSTCC4.cpp @@ -0,0 +1,467 @@ +/** + * @file sfDevSTCC4.cpp + * @brief Implementation file for the SparkFun STCC4 CO2 Sensor Driver. + * + * @details + * This file implements the sfDevSTCC4 class methods for controlling and reading data from the + * Sensirion STCC4 CO2 sensor. The driver provides a comms-agnostic interface using the SparkFun + * Toolkit and implements Sensirion's command-based I2C protocol, including CRC-8 validation of + * every data word sent and received. + * + * @author SparkFun Electronics + * @date 2026 + * @copyright Copyright (c) 2026, SparkFun Electronics Inc. This project is released under the MIT License. + * + * SPDX-License-Identifier: MIT + * + * @see https://github.com/sparkfun/SparkFun_STCC4_Arduino_Library + */ + +#include "sfDevSTCC4.h" + +// ========================= Setup & Identity =============================== + +sfTkError_t sfDevSTCC4::begin(sfTkIBus *theBus) +{ + // Adopt the supplied bus if one was provided; otherwise keep any bus set by a prior begin(). + if (theBus != nullptr) + _theBus = theBus; + + // We need a bus to talk to. + if (_theBus == nullptr) + return ksfTkErrBusNotInit; + + // Confirm an STCC4 is actually present and responding correctly before continuing. The product + // ID read also validates the CRC, so a successful match is strong evidence of a real STCC4. + if (isConnected()) + return ksfTkErrOk; + + // The identity check requires the sensor to be idle, so it fails if the sensor was left in + // sleep mode or left measuring. Both states survive a controller reset or re-upload, because + // the sensor keeps power from the bus the whole time. Recover from each in turn. + + // Sleep mode: send the wake-up byte and check again. exitSleepMode() re-verifies the product ID + // once the sensor is awake. + if (exitSleepMode() == ksfTkErrOk) + return ksfTkErrOk; + + // Continuous measurement still running: stop it (this blocks for the 1.2 s execution time) and + // check one last time. + if (stopContinuousMeasurement() != ksfTkErrOk) + return ksfTkErrBusNoResponse; + + return isConnected() ? ksfTkErrOk : ksfTkErrBusNoResponse; +} + +sfTkError_t sfDevSTCC4::reset(void) +{ + // The soft reset uses I2C-specific addressing (the general call address), so we need the I2C + // view of the bus. The STCC4 is an I2C-only device, so the bus is always an sfTkII2C. + if (_theBus == nullptr) + return ksfTkErrBusNotInit; + + sfTkII2C *i2cBus = (sfTkII2C *)_theBus; + + // Remember the configured sensor address - begin() may have selected the alternate. + uint8_t sensorAddress = i2cBus->address(); + + // Send the single-byte reset command to the I2C general call address. The command is not + // acknowledged by the sensor, so ignore the write result. + i2cBus->setAddress(kGeneralCallAddress); + + uint8_t command = kCommandSoftReset; + (void)i2cBus->writeData(&command, sizeof(command)); + + // Restore the sensor's own address for all subsequent communication. + i2cBus->setAddress(sensorAddress); + + // Give the sensor time to complete the reset before it is addressed again. + sftk_delay_ms(kSoftResetDelayMs); + return ksfTkErrOk; +} + +bool sfDevSTCC4::isConnected(void) +{ + uint32_t productId = 0; + uint64_t serialNumber = 0; + + if (getProductId(productId, serialNumber) != ksfTkErrOk) + return false; + + return productId == kProductId; +} + +// ========================= Measurement Control ============================ + +sfTkError_t sfDevSTCC4::startContinuousMeasurement(void) +{ + return sendCommand(kCommandStartContinuousMeasurement); +} + +sfTkError_t sfDevSTCC4::stopContinuousMeasurement(void) +{ + sfTkError_t rc = sendCommand(kCommandStopContinuousMeasurement); + if (rc != ksfTkErrOk) + return rc; + + // The sensor does not respond on the bus until the stop command finishes executing. + sftk_delay_ms(kStopMeasurementDelayMs); + return ksfTkErrOk; +} + +sfTkError_t sfDevSTCC4::measureSingleShot(void) +{ + sfTkError_t rc = sendCommand(kCommandMeasureSingleShot); + if (rc != ksfTkErrOk) + return rc; + + // Wait out the measurement execution time so the data is ready for readMeasurement(). + sftk_delay_ms(kSingleShotDelayMs); + return ksfTkErrOk; +} + +sfTkError_t sfDevSTCC4::readMeasurement(uint8_t maxAttempts) +{ + if (maxAttempts == 0) + maxAttempts = 1; + + // Read four CRC-protected words: CO2, temperature, humidity, and status. The sensor NACKs the + // transfer when no new data point is available yet, so retry on the datasheet-recommended + // 150 ms interval. + uint16_t words[4] = {0}; + sfTkError_t rc = ksfTkErrFail; + + for (uint8_t attempt = 0; attempt < maxAttempts; attempt++) + { + if (attempt > 0) + sftk_delay_ms(kDataNotReadyDelayMs); + + rc = readWords(kCommandReadMeasurement, words, 4, kReadMeasurementDelayMs); + if (rc == ksfTkErrOk) + break; + } + + if (rc != ksfTkErrOk) + return rc; + + _co2Ticks = words[0]; + _temperatureTicks = words[1]; + _humidityTicks = words[2]; + _status = words[3]; + + return ksfTkErrOk; +} + +// ====================== Cached Measurement Values ========================= + +int16_t sfDevSTCC4::getCO2(void) +{ + // The CO2 word is a signed 16-bit value in ppm. + return (int16_t)_co2Ticks; +} + +float sfDevSTCC4::getHumidity(void) +{ + float humidity = kHumidityOffset + kHumiditySlope * (float)_humidityTicks * kTicksFullScaleInv; + + // The conversion can produce values slightly outside the physical range; clamp per datasheet. + if (humidity < 0.0f) + humidity = 0.0f; + else if (humidity > 100.0f) + humidity = 100.0f; + + return humidity; +} + +float sfDevSTCC4::getTemperature(void) +{ + return kTemperatureOffsetC + kTemperatureSlopeC * (float)_temperatureTicks * kTicksFullScaleInv; +} + +float sfDevSTCC4::getTemperatureF(void) +{ + return kTemperatureOffsetF + kTemperatureSlopeF * (float)_temperatureTicks * kTicksFullScaleInv; +} + +uint16_t sfDevSTCC4::getStatus(void) +{ + return _status; +} + +bool sfDevSTCC4::isTestingModeEnabled(void) +{ + return (_status & kStatusTestingModeMask) != 0; +} + +// ===================== Raw Cached Measurement Ticks ======================= + +uint16_t sfDevSTCC4::getHumidityRaw(void) +{ + return _humidityTicks; +} + +uint16_t sfDevSTCC4::getTemperatureRaw(void) +{ + return _temperatureTicks; +} + +// ========================== Compensation ================================== + +sfTkError_t sfDevSTCC4::setRHTCompensation(float temperature, float humidity) +{ + // Clamp to the convertible input ranges before applying the datasheet input formulas. + if (temperature < kTemperatureOffsetC) + temperature = kTemperatureOffsetC; + else if (temperature > kTemperatureOffsetC + kTemperatureSlopeC) + temperature = kTemperatureOffsetC + kTemperatureSlopeC; + + if (humidity < 0.0f) + humidity = 0.0f; + else if (humidity > 100.0f) + humidity = 100.0f; + + // Input ticks = (T + 45) * 65535 / 175 and (RH + 6) * 65535 / 125, rounded to nearest. The + // divisions are folded into the precomputed ticks-per-unit constants so this stays multiply-only. + uint16_t args[2]; + args[0] = (uint16_t)((temperature - kTemperatureOffsetC) * kTempTicksPerDegreeC + 0.5f); + args[1] = (uint16_t)((humidity - kHumidityOffset) * kHumidityTicksPerPercent + 0.5f); + + sfTkError_t rc = sendCommand(kCommandSetRHTCompensation, args, 2); + if (rc != ksfTkErrOk) + return rc; + + sftk_delay_ms(kSetCompensationDelayMs); + return ksfTkErrOk; +} + +sfTkError_t sfDevSTCC4::setPressureCompensation(uint32_t pascals) +{ + // The sensor clips inputs to this range; clamp here so the 16-bit conversion below is valid. + if (pascals < kPressureMinPa) + pascals = kPressureMinPa; + else if (pascals > kPressureMaxPa) + pascals = kPressureMaxPa; + + // Input ticks = Pascals / 2. + uint16_t arg = (uint16_t)(pascals / kPressureDivisor); + + sfTkError_t rc = sendCommand(kCommandSetPressureCompensation, &arg, 1); + if (rc != ksfTkErrOk) + return rc; + + sftk_delay_ms(kSetCompensationDelayMs); + return ksfTkErrOk; +} + +// ========================= Power Management =============================== + +sfTkError_t sfDevSTCC4::enterSleepMode(void) +{ + sfTkError_t rc = sendCommand(kCommandEnterSleepMode); + if (rc != ksfTkErrOk) + return rc; + + sftk_delay_ms(kEnterSleepDelayMs); + return ksfTkErrOk; +} + +sfTkError_t sfDevSTCC4::exitSleepMode(void) +{ + if (_theBus == nullptr) + return ksfTkErrBusNotInit; + + // The wake-up is a single payload byte that the sensor intentionally does not acknowledge, so + // the write is expected to report a NACK error - ignore it. + uint8_t payload = kCommandExitSleepMode; + (void)_theBus->writeData(&payload, sizeof(payload)); + + sftk_delay_ms(kExitSleepDelayMs); + + // Confirm the sensor actually woke up by reading its product ID (per the datasheet). + return isConnected() ? ksfTkErrOk : ksfTkErrBusNoResponse; +} + +// ====================== Calibration & Maintenance ========================= + +sfTkError_t sfDevSTCC4::performConditioning(void) +{ + sfTkError_t rc = sendCommand(kCommandPerformConditioning); + if (rc != ksfTkErrOk) + return rc; + + // Conditioning runs a fixed 22 s operation profile; the sensor is busy for the duration. + sftk_delay_ms(kConditioningDelayMs); + return ksfTkErrOk; +} + +sfTkError_t sfDevSTCC4::performForcedRecalibration(uint16_t targetCO2, int16_t &frcCorrection) +{ + frcCorrection = 0; + + uint16_t rawCorrection = 0; + sfTkError_t rc = readWords(kCommandForcedRecalibration, &targetCO2, 1, &rawCorrection, 1, kForcedRecalDelayMs); + if (rc != ksfTkErrOk) + return rc; + + // The sensor reports 0xFFFF when the recalibration could not be performed. + if (rawCorrection == kCommandFailed) + return ksfTkErrFail; + + // The applied correction is offset-encoded: ppm = raw - 0x8000. + frcCorrection = (int16_t)((int32_t)rawCorrection - (int32_t)kFrcCorrectionOffset); + return ksfTkErrOk; +} + +sfTkError_t sfDevSTCC4::performFactoryReset(void) +{ + uint16_t result = 0; + sfTkError_t rc = readWords(kCommandPerformFactoryReset, &result, 1, kFactoryResetDelayMs); + if (rc != ksfTkErrOk) + return rc; + + // The sensor reports 0 on success, 0xFFFF on failure. + return (result == 0) ? ksfTkErrOk : ksfTkErrFail; +} + +sfTkError_t sfDevSTCC4::performSelfTest(uint16_t &result) +{ + return readWords(kCommandPerformSelfTest, &result, 1, kSelfTestDelayMs); +} + +// =========================== Testing Mode ================================= + +sfTkError_t sfDevSTCC4::enableTestingMode(void) +{ + return sendCommand(kCommandEnableTestingMode); +} + +sfTkError_t sfDevSTCC4::disableTestingMode(void) +{ + return sendCommand(kCommandDisableTestingMode); +} + +// ============================= Identity =================================== + +sfTkError_t sfDevSTCC4::getProductId(uint32_t &productId, uint64_t &serialNumber) +{ + // The response is six 16-bit words: two for the product ID (MSW first), four for the serial. + uint16_t words[kMaxWords] = {0}; + sfTkError_t rc = readWords(kCommandGetProductId, words, kMaxWords, kProductIdDelayMs); + if (rc != ksfTkErrOk) + return rc; + + productId = ((uint32_t)words[0] << 16) | (uint32_t)words[1]; + serialNumber = + ((uint64_t)words[2] << 48) | ((uint64_t)words[3] << 32) | ((uint64_t)words[4] << 16) | (uint64_t)words[5]; + + return ksfTkErrOk; +} + +// ============================ Protected Helpers =========================== + +sfTkError_t sfDevSTCC4::sendCommand(uint16_t command) +{ + return sendCommand(command, nullptr, 0); +} + +sfTkError_t sfDevSTCC4::sendCommand(uint16_t command, const uint16_t *args, uint8_t numArgs) +{ + if (_theBus == nullptr) + return ksfTkErrBusNotInit; + + if (numArgs > kMaxArgWords || (numArgs > 0 && args == nullptr)) + return ksfTkErrInvalidParam; + + // The command (MSB first) is followed by each argument word as two data bytes plus a CRC-8. + // Build the buffer explicitly so the wire order does not depend on the bus byte-order setting. + uint8_t buffer[2 + kMaxArgWords * kBytesPerWord]; + buffer[0] = (uint8_t)(command >> 8); + buffer[1] = (uint8_t)(command & 0xFF); + + size_t length = 2; + for (uint8_t i = 0; i < numArgs; i++) + { + buffer[length] = (uint8_t)(args[i] >> 8); + buffer[length + 1] = (uint8_t)(args[i] & 0xFF); + buffer[length + 2] = computeCRC8(&buffer[length], 2); + length += kBytesPerWord; + } + + return _theBus->writeData(buffer, length); +} + +sfTkError_t sfDevSTCC4::readWords(uint16_t command, uint16_t *words, uint8_t numWords, uint32_t readDelayMs) +{ + return readWords(command, nullptr, 0, words, numWords, readDelayMs); +} + +sfTkError_t sfDevSTCC4::readWords(uint16_t command, const uint16_t *args, uint8_t numArgs, uint16_t *words, + uint8_t numWords, uint32_t readDelayMs) +{ + if (_theBus == nullptr) + return ksfTkErrBusNotInit; + + if (words == nullptr || numWords == 0 || numWords > kMaxWords) + return ksfTkErrInvalidParam; + + if (numArgs > kMaxArgWords || (numArgs > 0 && args == nullptr)) + return ksfTkErrInvalidParam; + + // The command word (plus any CRC-protected argument words) is sent as the "register address", + // after which the response is read back following the command's execution time. + uint8_t commandBytes[2 + kMaxArgWords * kBytesPerWord]; + commandBytes[0] = (uint8_t)(command >> 8); + commandBytes[1] = (uint8_t)(command & 0xFF); + + size_t commandLength = 2; + for (uint8_t i = 0; i < numArgs; i++) + { + commandBytes[commandLength] = (uint8_t)(args[i] >> 8); + commandBytes[commandLength + 1] = (uint8_t)(args[i] & 0xFF); + commandBytes[commandLength + 2] = computeCRC8(&commandBytes[commandLength], 2); + commandLength += kBytesPerWord; + } + + uint8_t buffer[kMaxWords * kBytesPerWord] = {0}; + size_t numBytes = (size_t)numWords * kBytesPerWord; + size_t readBytes = 0; + + sfTkError_t rc = _theBus->readRegister(commandBytes, commandLength, buffer, numBytes, readBytes, readDelayMs); + if (rc != ksfTkErrOk) + return rc; + + if (readBytes != numBytes) + return ksfTkErrBusUnderRead; + + // Each word is two data bytes followed by a CRC-8 of those two bytes. + for (uint8_t i = 0; i < numWords; i++) + { + const uint8_t *group = &buffer[i * kBytesPerWord]; + + if (computeCRC8(group, 2) != group[2]) + return ksfTkErrFail; + + words[i] = ((uint16_t)group[0] << 8) | (uint16_t)group[1]; + } + + return ksfTkErrOk; +} + +uint8_t sfDevSTCC4::computeCRC8(const uint8_t *data, size_t length) +{ + uint8_t crc = kCrcInitialValue; + + for (size_t i = 0; i < length; i++) + { + crc ^= data[i]; + for (uint8_t bit = 0; bit < 8; bit++) + { + if (crc & 0x80) + crc = (uint8_t)((crc << 1) ^ kCrcPolynomial); + else + crc = (uint8_t)(crc << 1); + } + } + + return crc; +} diff --git a/src/sfTk/sfDevSTCC4.h b/src/sfTk/sfDevSTCC4.h new file mode 100644 index 0000000..0990af5 --- /dev/null +++ b/src/sfTk/sfDevSTCC4.h @@ -0,0 +1,565 @@ +/** + * @file sfDevSTCC4.h + * @brief Header file for the SparkFun STCC4 CO2 Sensor Driver. + * + * @details + * sfDevSTCC4 is a comms-agnostic driver for the Sensirion STCC4 miniature CO2 sensor, built on + * the SparkFun Toolkit. The STCC4 measures CO2 concentration (ppm) using the thermal conductivity + * sensing principle and supports both a continuous measurement mode (1 s sampling interval) and a + * low-power single shot measurement mode. + * + * The STCC4 requires external relative humidity and temperature values for accurate compensation + * of the CO2 output. On the SparkFun Qwiic CO2 Sensor - STCC4, the companion SHT40 sensor is wired + * to the STCC4's dedicated I2C controller interface pins, so the STCC4 reads it autonomously: every + * measurement returns CO2 already compensated, along with the ambient temperature and humidity. In + * custom designs without an SHT4x on those pins, the host must supply ambient conditions with + * setRHTCompensation() instead. + * + * The STCC4 uses Sensirion's command-based I2C protocol: 16-bit commands are sent most significant + * byte first, and data words (sent and received) are 16 bits followed by a CRC-8 byte. + * + * @author SparkFun Electronics + * @date 2026 + * @copyright Copyright (c) 2026, SparkFun Electronics Inc. This project is released under the MIT License. + * + * SPDX-License-Identifier: MIT + * + * @see https://github.com/sparkfun/SparkFun_STCC4_Arduino_Library + */ + +#pragma once + +#include +#include + +// SparkFun Toolkit core headers +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +// Self-test Result Bits +/////////////////////////////////////////////////////////////////////////////// + +/** + * @brief Bit masks for decoding the result word returned by performSelfTest(). + * + * @details A successful self-test returns 0x0000. On the SparkFun Qwiic CO2 Sensor - STCC4 the + * onboard SHT40 is wired to the STCC4's dedicated I2C controller interface pins, so a healthy + * board reports 0x0000; a result of 0x0010 means the STCC4 cannot see the SHT40. + */ +typedef enum sfe_stcc4_self_test_t : uint16_t +{ + STCC4_SELF_TEST_OK = 0x0000, ///< All checks passed. + STCC4_SELF_TEST_VDD_OUT_OF_RANGE = 0x0001, ///< Bit 0: supply voltage out of the specified range. + STCC4_SELF_TEST_DEBUG_MASK = 0x000E, ///< Bits 3:1: for debugging; contact Sensirion if nonzero. + STCC4_SELF_TEST_SHT_NOT_CONNECTED = 0x0010, ///< Bit 4: no SHT4x on the STCC4 controller interface pins. + STCC4_SELF_TEST_MEMORY_ERROR_MASK = 0x0060 ///< Bits 6:5: memory error (soft reset / power cycle to clear). +} sfe_stcc4_self_test_t; + +/////////////////////////////////////////////////////////////////////////////// +// Class Declaration +/////////////////////////////////////////////////////////////////////////////// + +/** + * @brief Platform-independent driver for the Sensirion STCC4 CO2 sensor. + * + * @details This class implements command-level access to the STCC4 via the SparkFun Toolkit bus + * interface. Most methods return a SparkFun Toolkit error code (::ksfTkErrOk on success, a negative + * value on error). The CRC-8 checksum of every received data word is validated automatically; + * ::ksfTkErrFail is returned if any checksum does not match. + * + * The typical continuous-mode flow is: begin(), startContinuousMeasurement(), then once per second + * call readMeasurement() and read the converted values with getCO2() / getTemperature() / + * getHumidity(). On the SparkFun board the STCC4 reads the onboard SHT40 by itself, so the CO2 + * value arrives already compensated and the temperature / humidity values are real ambient + * readings. Only designs without an SHT4x on the STCC4's controller interface pins need to supply + * ambient conditions with setRHTCompensation(). + */ +class sfDevSTCC4 +{ + public: + sfDevSTCC4() : _theBus{nullptr} + { + } + + /** + * @brief Initialize the device driver with the given bus. + * + * @details Adopts the supplied bus and confirms an STCC4 is responding by reading and + * validating its product ID. The identity check requires the sensor to be in idle state, so if + * the sensor was left asleep or left measuring (for example, the controller was reset or + * re-flashed mid-sketch while the sensor kept bus power), this method recovers automatically by + * waking it or stopping the running measurement. The sensor is always idle when this returns + * ::ksfTkErrOk. Worst case, the measurement-stop recovery blocks for its 1.2 s execution time. + * + * @param theBus Pointer to the initialized bus object. If null, a bus set by a prior call is used. + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t begin(sfTkIBus *theBus = nullptr); + + /** + * @brief Perform a soft reset of the sensor via the I2C general call. + * + * @details The STCC4 soft reset is issued as an I2C general call: the single-byte reset command + * is sent to address 0x00 rather than the sensor's own address, and is not acknowledged. This + * method temporarily re-points the bus at the general call address to send the command, restores + * the sensor address, and waits for the reset to complete. The sensor returns to the same state + * as after a power cycle (idle mode, default compensation values). + * + * @note All devices on the bus that respond to an I2C general call reset will also reset. + * + * @return ::ksfTkErrOk on success, ::ksfTkErrBusNotInit if the bus is not initialized. + */ + sfTkError_t reset(void); + + /** + * @brief Check whether the STCC4 is connected and responding. + * + * @details Reads the product ID, validates its CRC, and compares it against the STCC4 product + * ID (0x0901018A). Requires the sensor to be in idle state (no measurement running, not asleep). + * + * @return true if the device responds with a valid STCC4 product ID, false otherwise. + */ + bool isConnected(void); + + // ========================= Measurement Control ======================== + + /** + * @brief Start continuous measurement mode. + * + * @details Brings the sensor from idle into continuous measurement mode with a 1 s sampling + * interval. Wait 1 s after starting before calling readMeasurement(). The sensor must be in + * idle state (this command is not accepted while a measurement is running). + * + * @note During the first 20 s of continuous mode after the very first power-up, the sensor + * outputs a fixed bypass value of 390 ppm (see the datasheet, "Initial Operation"). + * + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t startContinuousMeasurement(void); + + /** + * @brief Stop continuous measurement mode and return the sensor to idle. + * + * @details Waits the command's 1.2 s execution time before returning. During that time the + * sensor does not acknowledge its I2C address or accept commands. + * + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t stopContinuousMeasurement(void); + + /** + * @brief Perform a single shot (on-demand) CO2 measurement. + * + * @details Triggers one measurement and waits the 500 ms execution time before returning. Call + * readMeasurement() afterwards to fetch the result. The sensor must be in idle state. For + * low-power operation, combine with enterSleepMode() / exitSleepMode() and keep the sampling + * interval between 5 s and 600 s so the automatic self-calibration algorithm works correctly. + * + * @note The first 2 single shot measurements after the very first power-up output a fixed + * bypass value of 390 ppm (see the datasheet, "Initial Operation"). + * + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t measureSingleShot(void); + + /** + * @brief Read one measurement data point and cache the result. + * + * @details Reads the CO2, temperature, humidity and status words, validates the CRC of each, + * and stores the raw values for retrieval via getCO2(), getTemperature(), getHumidity(), and + * getStatus(). The sensor NACKs the read when no new data point is available (its internal 1 s + * interval has a ±150 ms tolerance), in which case this method waits 150 ms and retries, up to + * @p maxAttempts total attempts. + * + * @note The temperature and humidity words are the values the sensor is using for + * compensation. On the SparkFun board these are live readings from the onboard SHT40 (wired + * to the STCC4's controller interface pins); in designs without an SHT4x there, they echo the + * values last written with setRHTCompensation() (defaults: 25 °C / 50 %RH after power-up). + * + * @param maxAttempts Total number of read attempts before giving up (default 4). A value of 0 + * is treated as 1 (at least one attempt is always made). + * @return ::ksfTkErrOk on success, ::ksfTkErrFail on a CRC mismatch, or an error code on a + * communication failure / no data available. + */ + sfTkError_t readMeasurement(uint8_t maxAttempts = kReadMeasurementAttempts); + + // ====================== Cached Measurement Values ===================== + + /** + * @brief Get the CO2 concentration from the most recent readMeasurement(). + * + * @details The output range is 380 to 32'000 ppm; the value is signed because readings can + * undershoot slightly at the low end of the range. + * + * @return CO2 concentration in parts per million (ppm). + */ + int16_t getCO2(void); + + /** + * @brief Get the relative humidity from the most recent readMeasurement(). + * + * @details This echoes the humidity the sensor is using for compensation (see + * readMeasurement()). The value is clamped to the 0–100 %RH range. + * + * @return Relative humidity in percent (%RH). + */ + float getHumidity(void); + + /** + * @brief Get the temperature from the most recent readMeasurement(), in degrees Celsius. + * + * @details This echoes the temperature the sensor is using for compensation (see + * readMeasurement()). + * + * @return Temperature in degrees Celsius (°C). + */ + float getTemperature(void); + + /** + * @brief Get the temperature from the most recent readMeasurement(), in degrees Fahrenheit. + * + * @return Temperature in degrees Fahrenheit (°F). + */ + float getTemperatureF(void); + + /** + * @brief Get the raw 16-bit sensor status word from the most recent readMeasurement(). + * + * @return The raw status word (use isTestingModeEnabled() to decode the testing mode bit). + */ + uint16_t getStatus(void); + + /** + * @brief Check whether the sensor is in testing mode (automatic self-calibration paused). + * + * @details Reflects the status word from the most recent readMeasurement(). See + * enableTestingMode() / disableTestingMode(). + * + * @return true if testing mode is enabled, false otherwise. + */ + bool isTestingModeEnabled(void); + + // ===================== Raw Cached Measurement Ticks =================== + + /** + * @brief Get the raw humidity ticks from the most recent readMeasurement(). + * + * @return Raw 16-bit humidity value (%RH = 125 * ticks / 65535 - 6). + */ + uint16_t getHumidityRaw(void); + + /** + * @brief Get the raw temperature ticks from the most recent readMeasurement(). + * + * @return Raw 16-bit temperature value (°C = 175 * ticks / 65535 - 45). + */ + uint16_t getTemperatureRaw(void); + + // ========================== Compensation ============================== + + /** + * @brief Write external relative humidity and temperature compensation values. + * + * @details For custom designs without an SHT4x on the STCC4's dedicated I2C controller + * interface pins. The STCC4 uses these values to compensate the CO2 output; they take effect + * within one measurement interval and persist until overwritten or power-cycled (defaults: + * 25 °C / 50 %RH). Both values must come from the same RHT sensor, and for best accuracy + * should be refreshed regularly (for example once per measurement cycle). May be called while + * a measurement is running. + * + * @warning Do NOT use this method when an SHT4x is wired to the STCC4's controller interface + * pins — as it is on the SparkFun Qwiic CO2 Sensor - STCC4, where the STCC4 manages the + * onboard SHT40 (and compensation) entirely by itself. + * + * @param temperature Ambient temperature in degrees Celsius (clamped to -45 to 130 °C). + * @param humidity Ambient relative humidity in percent (clamped to 0 to 100 %RH). + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t setRHTCompensation(float temperature, float humidity); + + /** + * @brief Write an external ambient pressure compensation value. + * + * @details Improves CO2 accuracy when operating away from the default 101'300 Pa (sea level), + * for example at altitude. The value takes effect within one measurement interval and persists + * until overwritten or power-cycled. The sensor accepts 40'000 to 110'000 Pa (clamped here). + * May be called while a measurement is running. + * + * @param pascals Ambient pressure in Pascals (1 hPa / mbar = 100 Pa). + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t setPressureCompensation(uint32_t pascals); + + // ========================= Power Management =========================== + + /** + * @brief Put the sensor into its lowest-power sleep mode. + * + * @details The sensor must be in idle state. Compensation values and the automatic + * self-calibration state are retained during sleep. Wake the sensor with exitSleepMode(). + * + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t enterSleepMode(void); + + /** + * @brief Wake the sensor from sleep mode into idle mode. + * + * @details Sends the wake-up byte (which the sensor intentionally does not acknowledge), waits + * the 5 ms execution time, then confirms the sensor is awake by reading its product ID. + * + * @return ::ksfTkErrOk on success, or ::ksfTkErrBusNoResponse if the sensor did not wake. + */ + sfTkError_t exitSleepMode(void); + + // ====================== Calibration & Maintenance ===================== + + /** + * @brief Condition the sensor to improve initial accuracy after long idle periods. + * + * @details Recommended when the sensor has not measured for more than 3 hours. Runs a fixed + * operation profile on the sensor and BLOCKS for its full 22 s execution time. The sensor must + * be in idle state; start a measurement afterwards. + * + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t performConditioning(void); + + /** + * @brief Perform a forced recalibration (FRC) of the CO2 output. + * + * @details Adjusts the sensor output to match an externally known CO2 concentration (for + * example outdoor air at ~420 ppm). Before calling: operate the sensor for at least 30 s of + * continuous measurement (or 30 single shots), then stop continuous measurement — the sensor + * must be in idle state, with environmental conditions held stable. Blocks for the 90 ms + * execution time. + * + * @param targetCO2 The known reference CO2 concentration, in ppm (0 to 32'000). + * @param frcCorrection Output reference that receives the applied correction in ppm. + * @return ::ksfTkErrOk on success, ::ksfTkErrFail if the sensor reports the recalibration + * failed, or an error code on a communication failure. + */ + sfTkError_t performForcedRecalibration(uint16_t targetCO2, int16_t &frcCorrection); + + /** + * @brief Reset the forced recalibration and automatic self-calibration history. + * + * @details Returns the calibration state to factory defaults and re-enables the initial + * bypass phase. The sensor must be in idle state. Blocks for the 90 ms execution time. + * + * @return ::ksfTkErrOk on success, ::ksfTkErrFail if the sensor reports the reset failed, or + * an error code on a communication failure. + */ + sfTkError_t performFactoryReset(void); + + /** + * @brief Run the on-chip self-test. + * + * @details Checks sensor functionality; useful for end-of-line testing and debugging. Run it + * under stable conditions. The sensor must be in idle state. Blocks for the 360 ms execution + * time. A healthy SparkFun Qwiic CO2 Sensor - STCC4 reports ::STCC4_SELF_TEST_OK; + * ::STCC4_SELF_TEST_SHT_NOT_CONNECTED means the STCC4 cannot see the onboard SHT40 on its + * controller interface pins. + * + * @param result Output reference that receives the raw 16-bit self-test result (see + * sfe_stcc4_self_test_t for bit definitions). + * @return ::ksfTkErrOk on success, ::ksfTkErrFail on a CRC mismatch, or an error code on failure. + */ + sfTkError_t performSelfTest(uint16_t &result); + + // =========================== Testing Mode ============================= + + /** + * @brief Pause the automatic self-calibration (ASC) algorithm. + * + * @details Temporarily disables updates to the ASC state, for example while characterizing the + * sensor with reference gases. May be called while a measurement is running. Check the mode + * with isTestingModeEnabled() after the next readMeasurement(). + * + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t enableTestingMode(void); + + /** + * @brief Resume the automatic self-calibration (ASC) algorithm. + * + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t disableTestingMode(void); + + // ============================= Identity ================================ + + /** + * @brief Read the product ID and unique serial number. + * + * @details The STCC4 product ID is 0x0901018A. The 64-bit serial number is unique to each + * sensor. The sensor must be in idle state. + * + * @param productId Output reference that receives the 32-bit product ID. + * @param serialNumber Output reference that receives the 64-bit serial number. + * @return ::ksfTkErrOk on success, ::ksfTkErrFail on a CRC mismatch, or an error code on failure. + */ + sfTkError_t getProductId(uint32_t &productId, uint64_t &serialNumber); + + protected: + /** + * @brief Send a 16-bit command to the sensor (most significant byte first). + * + * @param command The 16-bit command code. + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t sendCommand(uint16_t command); + + /** + * @brief Send a 16-bit command followed by CRC-protected 16-bit argument words. + * + * @details Each argument is transmitted as two data bytes (MSB first) followed by its CRC-8. + * + * @param command The 16-bit command code. + * @param args Pointer to the argument words. + * @param numArgs Number of argument words (no more than kMaxArgWords). + * @return ::ksfTkErrOk on success, or an error code on failure. + */ + sfTkError_t sendCommand(uint16_t command, const uint16_t *args, uint8_t numArgs); + + /** + * @brief Send a command and read back a sequence of CRC-protected 16-bit words. + * + * @details Each word arrives as two data bytes (MSB first) followed by a CRC-8 byte. The CRC of + * every word is validated before the word is stored. + * + * @param command The 16-bit command code that requests the data. + * @param words Output buffer that receives @p numWords 16-bit words. + * @param numWords Number of 16-bit words to read (no more than kMaxWords). + * @param readDelayMs Delay, in milliseconds, between sending the command and reading. + * @return ::ksfTkErrOk on success, ::ksfTkErrFail on a CRC mismatch, or an error code on failure. + */ + sfTkError_t readWords(uint16_t command, uint16_t *words, uint8_t numWords, uint32_t readDelayMs = 0); + + /** + * @brief Send a command with CRC-protected argument words, then read back CRC-protected words. + * + * @param command The 16-bit command code. + * @param args Pointer to the argument words (may be null when @p numArgs is 0). + * @param numArgs Number of argument words (no more than kMaxArgWords). + * @param words Output buffer that receives @p numWords 16-bit words. + * @param numWords Number of 16-bit words to read (no more than kMaxWords). + * @param readDelayMs Delay, in milliseconds, between sending the command and reading. + * @return ::ksfTkErrOk on success, ::ksfTkErrFail on a CRC mismatch, or an error code on failure. + */ + sfTkError_t readWords(uint16_t command, const uint16_t *args, uint8_t numArgs, uint16_t *words, uint8_t numWords, + uint32_t readDelayMs); + + /** + * @brief Compute the Sensirion CRC-8 over a buffer of data bytes. + * + * @details Polynomial 0x31, initial value 0xFF, no final XOR. + * + * @param data Pointer to the data bytes. + * @param length Number of bytes to include in the checksum. + * @return The 8-bit checksum. + */ + static uint8_t computeCRC8(const uint8_t *data, size_t length); + + sfTkIBus *_theBus; ///< Pointer to the communication bus device. + + // --- Cached raw values from the most recent readMeasurement() --- + uint16_t _co2Ticks = 0; ///< Raw CO2 value (signed ppm). + uint16_t _temperatureTicks = 0; ///< Raw temperature value. + uint16_t _humidityTicks = 0; ///< Raw relative humidity value. + uint16_t _status = 0; ///< Raw 16-bit sensor status word. + + /////////////////////////////////////////////////////////////////////////// + // I2C Addressing + /////////////////////////////////////////////////////////////////////////// + static const uint8_t kI2CAddressDefault = 0x64; ///< 7-bit I2C address with the ADDR pin low (default). + static const uint8_t kI2CAddressAlt = 0x65; ///< 7-bit I2C address with the ADDR pin high. + static const uint8_t kGeneralCallAddress = 0x00; ///< I2C general call address used for soft reset. + + /////////////////////////////////////////////////////////////////////////// + // Command Codes + /////////////////////////////////////////////////////////////////////////// + static const uint16_t kCommandStartContinuousMeasurement = 0x218B; ///< Start continuous measurement (1 s interval). + static const uint16_t kCommandStopContinuousMeasurement = 0x3F86; ///< Stop continuous measurement. + static const uint16_t kCommandReadMeasurement = 0xEC05; ///< Read measurement data (12 bytes). + static const uint16_t kCommandSetRHTCompensation = 0xE000; ///< Write RH/T compensation values. + static const uint16_t kCommandSetPressureCompensation = 0xE016; ///< Write the pressure compensation value. + static const uint16_t kCommandMeasureSingleShot = 0x219D; ///< Perform a single shot measurement. + static const uint16_t kCommandEnterSleepMode = 0x3650; ///< Enter sleep mode (from idle). + static const uint16_t kCommandPerformConditioning = 0x29BC; ///< Condition the sensor (22 s). + static const uint16_t kCommandPerformFactoryReset = 0x3632; ///< Reset FRC / ASC history. + static const uint16_t kCommandPerformSelfTest = 0x278C; ///< Run the on-chip self-test. + static const uint16_t kCommandEnableTestingMode = 0x3FBC; ///< Pause the ASC algorithm. + static const uint16_t kCommandDisableTestingMode = 0x3F3D; ///< Resume the ASC algorithm. + static const uint16_t kCommandForcedRecalibration = 0x362F; ///< Forced recalibration (FRC). + static const uint16_t kCommandGetProductId = 0x365B; ///< Read product ID + serial number. + static const uint8_t kCommandExitSleepMode = 0x00; ///< Single-byte wake-up payload (not acknowledged). + static const uint8_t kCommandSoftReset = 0x06; ///< Soft reset (sent to the general call address). + + /////////////////////////////////////////////////////////////////////////// + // Product ID + /////////////////////////////////////////////////////////////////////////// + static const uint32_t kProductId = 0x0901018A; ///< Product ID reported by every STCC4. + + /////////////////////////////////////////////////////////////////////////// + // CRC Parameters + /////////////////////////////////////////////////////////////////////////// + static const uint8_t kCrcPolynomial = 0x31; ///< CRC-8 polynomial. + static const uint8_t kCrcInitialValue = 0xFF; ///< CRC-8 initial value. + + /////////////////////////////////////////////////////////////////////////// + // Conversion Constants + /////////////////////////////////////////////////////////////////////////// + static constexpr float kTicksFullScale = 65535.0f; ///< Full-scale tick count for RH / temperature. + static constexpr float kHumiditySlope = 125.0f; ///< Humidity conversion slope. + static constexpr float kHumidityOffset = -6.0f; ///< Humidity conversion offset (%RH). + static constexpr float kTemperatureSlopeC = 175.0f; ///< Temperature conversion slope (°C). + static constexpr float kTemperatureOffsetC = -45.0f; ///< Temperature conversion offset (°C). + static constexpr float kTemperatureSlopeF = 315.0f; ///< Temperature conversion slope (°F). + static constexpr float kTemperatureOffsetF = -49.0f; ///< Temperature conversion offset (°F). + + // Precomputed reciprocals / scale factors so the conversion math uses multiplication instead of + // (much slower) floating-point division on low-end MCUs. These fold at compile time. + static constexpr float kTicksFullScaleInv = 1.0f / kTicksFullScale; ///< 1 / full-scale tick count. + static constexpr float kTempTicksPerDegreeC = + kTicksFullScale / kTemperatureSlopeC; ///< Input ticks per °C (= 65535 / 175). + static constexpr float kHumidityTicksPerPercent = + kTicksFullScale / kHumiditySlope; ///< Input ticks per %RH (= 65535 / 125). + + static const uint32_t kPressureDivisor = 2; ///< Pressure input ticks = Pascals / 2. + static const uint32_t kPressureMinPa = 40000; ///< Minimum accepted pressure input (Pa). + static const uint32_t kPressureMaxPa = 110000; ///< Maximum accepted pressure input (Pa). + + /////////////////////////////////////////////////////////////////////////// + // Status / Result Decoding + /////////////////////////////////////////////////////////////////////////// + static const uint16_t kStatusTestingModeMask = 0x0040; ///< Testing mode flag in the status word. + static const uint16_t kCommandFailed = 0xFFFF; ///< FRC / factory reset failure value. + static const uint16_t kFrcCorrectionOffset = 0x8000; ///< FRC correction ppm = raw - 0x8000. + + /////////////////////////////////////////////////////////////////////////// + // Timing (execution times, in milliseconds) + /////////////////////////////////////////////////////////////////////////// + static const uint32_t kStopMeasurementDelayMs = 1200; ///< Execution time for stop continuous measurement. + static const uint32_t kReadMeasurementDelayMs = 1; ///< Execution time for read measurement. + static const uint32_t kSetCompensationDelayMs = 1; ///< Execution time for the compensation commands. + static const uint32_t kSingleShotDelayMs = 500; ///< Execution time for a single shot measurement. + static const uint32_t kEnterSleepDelayMs = 1; ///< Execution time for enter sleep mode. + static const uint32_t kExitSleepDelayMs = 5; ///< Execution time for exit sleep mode. + static const uint32_t kConditioningDelayMs = 22000; ///< Execution time for conditioning. + static const uint32_t kSoftResetDelayMs = 10; ///< Execution time for soft reset. + static const uint32_t kFactoryResetDelayMs = 90; ///< Execution time for factory reset. + static const uint32_t kSelfTestDelayMs = 360; ///< Execution time for the self-test. + static const uint32_t kForcedRecalDelayMs = 90; ///< Execution time for forced recalibration. + static const uint32_t kProductIdDelayMs = 1; ///< Execution time for read product ID. + static const uint32_t kDataNotReadyDelayMs = 150; ///< Retry delay when measurement data is not ready. + static const uint8_t kReadMeasurementAttempts = 4; ///< Default total attempts for readMeasurement(). + + /////////////////////////////////////////////////////////////////////////// + // Buffer Sizing + /////////////////////////////////////////////////////////////////////////// + static const uint8_t kBytesPerWord = 3; ///< Two data bytes plus one CRC byte. + static const uint8_t kMaxWords = 6; ///< Largest response (product ID + serial) is six words. + static const uint8_t kMaxArgWords = 2; ///< Largest argument list (RHT compensation) is two words. +};