Skip to content

Add SparkFun STCC4 Arduino Library - #1

Merged
FacioErgoSum merged 2 commits into
mainfrom
develop
Jul 7, 2026
Merged

FacioErgoSum merged 2 commits into
mainfrom
develop

Conversation

@FacioErgoSum

Copy link
Copy Markdown
Collaborator

Initial library for the SparkFun Qwiic CO2 Sensor (STCC4), opened for review/comment before merging to main.

What's here

  • STCC4 driver — comms-agnostic sfDevSTCC4 plus the Arduino I2C wrapper SfeSTCC4ArdI2C (built on the SparkFun Toolkit)
  • Six examples — basic readings, low-power single shot, product info, self-test, forced recalibration, pressure compensation
  • Library metadatalibrary.properties, library.json, keywords.txt
  • Docs — Doxygen config wired to the SparkFun doxygen-awesome-css theme (submodule pinned to v2.4.2), custom header/CSS, and GitHub Pages deploy workflow

Notes for reviewers

  • The onboard SHT40 is read autonomously by the STCC4 over its dedicated interface pins, so a single readMeasurement() returns compensated CO2 plus temperature and humidity. There is no standalone SHT40 driver — it isn't needed for this board.
  • Conforms to the SparkFun Arduino Library HowTo template (structure, headers/SPDX, naming, docs tooling).
  • Still to add: the project banner image docs/images/gh-banner-2026-arduino-stcc4.png referenced by the README.

🤖 Generated with Claude Code

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>
Comment thread src/SparkFun_STCC4.h Outdated
if (stopContinuousMeasurement() != ksfTkErrOk)
return false;

return isConnected();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is based on Sensirion's recommendations in the datasheet, but I think the I2C conflict issue is fairly low-risk.

Comment thread src/sfTk/sfDevSTCC4.h Outdated
///////////////////////////////////////////////////////////////////////////////
// Self-test Result Bits
///////////////////////////////////////////////////////////////////////////////
/// @brief Bit masks for decoding the result word returned by performSelfTest().

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
 */

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make sure all the comments in both files match.

@sfe-SparkFro sfe-SparkFro left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review comments from Claude

steps:
# Checkout the repository
- name: Checkout repository
uses: actions/checkout@v2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix this in the template. @gigapod?

Comment on lines +3 to +6
on:
push:
branches:
- main

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care to test on the develop branch? I think main only is fine

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

Comment thread src/SparkFun_STCC4.h Outdated
sfTkError_t reset(void)
{
// Remember the configured sensor address - begin() may have selected the alternate.
uint8_t sensorAddress = _theI2CBus.address();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix this to match sendCommand.

Comment thread src/sfTk/sfDevSTCC4.h Outdated
Comment on lines +130 to +133
/// @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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just going to add a basic comment to document.

Comment thread src/sfTk/sfDevSTCC4.h
Comment on lines +46 to +53
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep this as is.

Comment thread src/sfTk/sfDevSTCC4.h Outdated
Comment on lines +221 to +226
/// @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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding a README note and a short serial message

Comment thread docs/images/sfe_flame.png

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/doxygen/doxygen-custom/custom.css Outdated
width: 55px;
height: 55px;
}
} No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix this in the template. @gigapod?

Comment thread docs/doxygen/doxygen-custom/header.html Outdated
</table>
</div>
<!--END TITLEAREA-->
<!-- end header part --> No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix this in the template. @gigapod?

Comment thread library.json Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

Comment thread examples/Example01_BasicReadings/Example01_BasicReadings.ino Outdated
Comment thread examples/Example01_BasicReadings/Example01_BasicReadings.ino Outdated
Comment thread examples/Example02_LowPowerSingleShot/Example02_LowPowerSingleShot.ino Outdated
Comment thread examples/Example02_LowPowerSingleShot/Example02_LowPowerSingleShot.ino Outdated
Comment thread examples/Example06_PressureCompensation/Example06_PressureCompensation.ino Outdated
Serial.println("STCC4 connected!");

// Bring the sensor out of idle and into continuous measurement mode (1 s interval).
if (mySensor.startContinuousMeasurement() != ksfTkErrOk)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should startContinuousMeasurement() be called automatically by begin()?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want them separate because combining would break the low-power option in example 2. I want to keep both modes available.

Comment thread src/SparkFun_STCC4.h Outdated
Comment on lines +77 to +99
// 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this logic belongs in sfDevSTCC4::begin()

Comment thread src/SparkFun_STCC4.h Outdated
Comment on lines +102 to +134
/**
* @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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should move to the sfDevSTCC4 class.

Comment thread src/sfTk/sfDevSTCC4.cpp Outdated

float sfDevSTCC4::getHumidity(void)
{
float humidity = kHumidityOffset + kHumiditySlope * (float)_humidityTicks / kTicksFullScale;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Easy change, I'll add this.

@FacioErgoSum
FacioErgoSum merged commit 4911351 into main Jul 7, 2026
9 checks passed
@FacioErgoSum
FacioErgoSum deleted the develop branch September 4, 2026 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants