Add SparkFun STCC4 Arduino Library - #1
Conversation
Full library: STCC4 driver and Arduino I2C wrapper, examples, keywords, library metadata, and Doxygen documentation setup (doxygen-awesome theme). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| if (stopContinuousMeasurement() != ksfTkErrOk) | ||
| return false; | ||
|
|
||
| return isConnected(); |
There was a problem hiding this comment.
A couple things:
Should the ping() call be in isConnectec() ?
Should isConnected() be called before begin(), exitSleepMode() and stopContinuousMeasurement()
- Don't you want to make sure the device is connected before sending commands to it?
- OR does it need to be setup before getting the productId?
- If you start with another device connected at that address, the above commands risk cause weird side-effects...
BUT if that's they way it has to be done - nothing you can do about it.
There was a problem hiding this comment.
This is based on Sensirion's recommendations in the datasheet, but I think the I2C conflict issue is fairly low-risk.
| /////////////////////////////////////////////////////////////////////////////// | ||
| // Self-test Result Bits | ||
| /////////////////////////////////////////////////////////////////////////////// | ||
| /// @brief Bit masks for decoding the result word returned by performSelfTest(). |
There was a problem hiding this comment.
Style Nit - These dense header blocks make it difficult to digest the comment content.
Suggestions:
- place a spaces between some of the elements in the block
- Convert to C style comment style for the doxygen headers -- reduces density
From this:
/// @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.to
/**
* @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.
*/There was a problem hiding this comment.
I'll make sure all the comments in both files match.
| steps: | ||
| # Checkout the repository | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v2 |
There was a problem hiding this comment.
actions/checkout@v2 in build-deploy-ghpages.yml (line 39) should be @v4. The companion test-compile-sketch.yml already uses @v4 correctly — inconsistency worth fixing.
There was a problem hiding this comment.
Should fix this in the template. @gigapod?
| on: | ||
| push: | ||
| branches: | ||
| - main |
There was a problem hiding this comment.
Compile CI doesn't run on develop branch pushes — test-compile-sketch.yml triggers on push to main and on PRs, but not on develop. Commits pushed directly to develop without opening a PR get no compile check. Consider adding develop to the push trigger.
There was a problem hiding this comment.
Do we care to test on the develop branch? I think main only is fine
| sfTkError_t reset(void) | ||
| { | ||
| // Remember the configured sensor address - begin() may have selected the alternate. | ||
| uint8_t sensorAddress = _theI2CBus.address(); |
There was a problem hiding this comment.
reset() in SfeSTCC4ArdI2C always returns ksfTkErrOk regardless of bus state. The intentional NACK discard is explained in comments, but the method could still fail if the bus is not initialized. Adding a null-check on _theI2CBus (matching the pattern in sendCommand) would make it consistent.
There was a problem hiding this comment.
I'll fix this to match sendCommand.
| /// @param maxAttempts Total number of read attempts before giving up (default 4). | ||
| /// @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); |
There was a problem hiding this comment.
readMeasurement: if (maxAttempts == 0) maxAttempts = 1 silently corrects a caller mistake. Since this is a library API, document this behavior in the header comment — the current doc just says "default 4" — or consider returning ksfTkErrInvalidParam on 0 to make the contract explicit.
There was a problem hiding this comment.
Just going to add a basic comment to document.
| 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; |
There was a problem hiding this comment.
The enum sfe_stcc4_self_test_t uses C-style typedef enum ... { } name; — in C++11 (Arduino) this is redundant. Not wrong, but the constants are global-scoped, which can cause collisions in user code. A scoped enum (enum class) would be cleaner, though it would require updating the example comparisons to use the scope operator. Your call given the target audience.
There was a problem hiding this comment.
I recall there was question about adding typedef from a previous review. IMO we could just say we only support C++11 or greater. Or just keep it, I'm indifferent :)
There was a problem hiding this comment.
I'll keep this as is.
| /// @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); |
There was a problem hiding this comment.
Missing example: performConditioning() blocks for 22 s — unusual enough to warrant a dedicated example or at minimum a note in the README's "Getting Started" section. Users won't expect a method to block for that long.
There was a problem hiding this comment.
I'm adding a README note and a short serial message
There was a problem hiding this comment.
Missing banner image: Acknowledged in the PR — docs/images/gh-banner-2026-arduino-stcc4.png is referenced by the README but not included. Should be added before merging.
| width: 55px; | ||
| height: 55px; | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
Missing newline at EOF: custom.css (line 301) and header.html (line 431) both end without a trailing newline, which the diff flags. Minor, but worth fixing.
There was a problem hiding this comment.
Should fix this in the template. @gigapod?
| </table> | ||
| </div> | ||
| <!--END TITLEAREA--> | ||
| <!-- end header part --> No newline at end of file |
There was a problem hiding this comment.
Missing newline at EOF: custom.css (line 301) and header.html (line 431) both end without a trailing newline, which the diff flags. Minor, but worth fixing.
There was a problem hiding this comment.
Should fix this in the template. @gigapod?
There was a problem hiding this comment.
Arduino only uses library.properties, not library.json
Looks like PlatformIO can use either, but library.json has more capabilities. Doesn't look like this is any different from library.properties though, and duplicate content always has maintenance concerns. Do we need both files, or can this file be removed?
| Serial.println("STCC4 connected!"); | ||
|
|
||
| // Bring the sensor out of idle and into continuous measurement mode (1 s interval). | ||
| if (mySensor.startContinuousMeasurement() != ksfTkErrOk) |
There was a problem hiding this comment.
Should startContinuousMeasurement() be called automatically by begin()?
There was a problem hiding this comment.
I want them separate because combining would break the low-power option in example 2. I want to keep both modes available.
| // 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 by reading and validating its product ID. | ||
| if (sfDevSTCC4::begin(&_theI2CBus) == ksfTkErrOk) | ||
| return true; | ||
|
|
||
| // 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 true; | ||
|
|
||
| // Continuous measurement still running: stop it (this blocks for the 1.2 s execution | ||
| // time) and check one last time. | ||
| if (stopContinuousMeasurement() != ksfTkErrOk) | ||
| return false; | ||
|
|
||
| return isConnected(); |
There was a problem hiding this comment.
I feel this logic belongs in sfDevSTCC4::begin()
| /** | ||
| * @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, or an error code on failure. | ||
| */ | ||
| sfTkError_t reset(void) | ||
| { | ||
| // Remember the configured sensor address - begin() may have selected the alternate. | ||
| uint8_t sensorAddress = _theI2CBus.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. | ||
| _theI2CBus.setAddress(kGeneralCallAddress); | ||
|
|
||
| uint8_t command = kCommandSoftReset; | ||
| (void)_theI2CBus.writeData(&command, sizeof(command)); | ||
|
|
||
| // Restore the sensor's own address for all subsequent communication. | ||
| _theI2CBus.setAddress(sensorAddress); | ||
|
|
||
| // Give the sensor time to complete the reset before it is addressed again. | ||
| sftk_delay_ms(kSoftResetDelayMs); | ||
| return ksfTkErrOk; | ||
| } |
There was a problem hiding this comment.
This should move to the sfDevSTCC4 class.
|
|
||
| float sfDevSTCC4::getHumidity(void) | ||
| { | ||
| float humidity = kHumidityOffset + kHumiditySlope * (float)_humidityTicks / kTicksFullScale; |
There was a problem hiding this comment.
If you can, it's best to avoid division where possible, because it can be quite slow on some lower end hardware. Because kTicksFullScale is a known constant, you can define another constant kTicksFullScaleInv = 1 / kTicksFullScale, then change from / kTicksFullScale to * kTicksFullScaleInv (multiplication is generally much faster).
This logic applies to anywhere division occurs.
There was a problem hiding this comment.
Easy change, I'll add this.
Initial library for the SparkFun Qwiic CO2 Sensor (STCC4), opened for review/comment before merging to
main.What's here
sfDevSTCC4plus the Arduino I2C wrapperSfeSTCC4ArdI2C(built on the SparkFun Toolkit)library.properties,library.json,keywords.txtdoxygen-awesome-csstheme (submodule pinned to v2.4.2), custom header/CSS, and GitHub Pages deploy workflowNotes for reviewers
readMeasurement()returns compensated CO2 plus temperature and humidity. There is no standalone SHT40 driver — it isn't needed for this board.docs/images/gh-banner-2026-arduino-stcc4.pngreferenced by the README.🤖 Generated with Claude Code