One firmware architecture and one web interface, reused across eleven
low-cost spectral/light sensors: AS7341, AS7343, TCS3448, AS7262,
TCS34725, APDS9960, BH1750, LTR303, LTR329, LTR390, TSL2591. Pick a
sensor, build that one PlatformIO environment, open the same
web/index.html — the UI builds itself from whatever the firmware says it
can do.
New: TCS3448 (a.k.a TCS34488). Released by Adafruit in September 2026 as product #6525, this is ams-OSRAM's newer "refresh" of the AS7343 — same 14-channel architecture, same register map, but retuned optical filters and a changed I2C address (0x39 → 0x59), so it needs its own firmware build. See the "Sensor particularities" table and the TCS3448 note below for the details, and "What's verified" for exactly how each spec was sourced (this chip is recent enough that none of it could come from training data).
TCS34725 is discontinued by Adafruit.
APDS9960(Proximity, Light, RGB, and Gesture Sensor — STEMMA QT/Qwiic) is the recommended replacement for new builds: same 4-channel RGBC + derived Color Temp/Lux shape, same "color" sensor type, same wiring. The one functional difference is that APDS9960's breakout has no controllable illumination LED usable for this project (its onboard IR LED only feeds the separate proximity/gesture engine), so APDS9960 only ever offers Absorbance and Fluorescence mode — Reflectance never appears for it, the same way it never appears for BH1750/LTR3xx/TSL2591. TCS34725's own firmware/library entry is kept below for anyone maintaining existing TCS34725 hardware.
No Wi-Fi anywhere. The browser talks to the board over USB using the Web Serial API, so the exact same firmware/webpage pair works on an ESP32-S3 Feather or a plain Arduino Uno.
This section assumes zero prior experience with VS Code, PlatformIO, or Arduino-style boards. It's written as a literal click-by-click walkthrough. If you're already comfortable with PlatformIO, skip ahead to the "How to run it" quick-reference section below instead — this one's deliberately slow and detailed.
- Go to https://code.visualstudio.com in your browser.
- Click the big blue Download button. The site auto-detects your operating system (Windows / Mac / Linux), so you usually don't need to pick anything.
- Open the downloaded installer and click through it with the default options (Next → Next → Install → Finish). On Windows, it's fine to leave every checkbox at its default.
- Launch VS Code once it finishes installing, just to confirm it opens. You'll see a mostly-empty window with a row of icons down the left edge — that's the sidebar, and you'll use it a lot below.
PlatformIO is what teaches VS Code how to build and upload firmware for these boards — VS Code can't do it on its own.
- In the left sidebar, click the Extensions icon (it looks like four small squares, with one slightly detached — usually the 5th icon down).
- A search box appears at the top. Type PlatformIO IDE.
- The correct result is called "PlatformIO IDE", published by PlatformIO, with a little alien-head logo. Click the blue Install button on it.
- Wait — this takes a minute or two and downloads quite a bit in the background. You'll see a progress notification in the bottom-right corner. Don't close VS Code while it's working.
- When it finishes, VS Code will ask you to reload the window (or it will just do it automatically). Let it reload.
- After the reload, look at the sidebar again — you should now see a new icon that looks like an alien head 👽. That's the PlatformIO icon. If you see it, the extension installed correctly.
If someone sent you this project as a .zip file:
- Find the
.zipfile in your Downloads folder (or wherever you saved it). - Right-click it and choose Extract All... (Windows) or double-click it (Mac) to unzip it. Remember which folder you extracted it to — you'll need to find it again in Step 4.
If you're getting it from a Git repository instead: clone it however
you normally would (or just download it as a .zip from the repository's
"Code" button and follow the steps above).
- In VS Code, go to the top menu: File → Open Folder... (on Mac: File → Open...).
- Browse to the folder you extracted in Step 3 — the one that directly
contains
platformio.ini— and select it, then click Select Folder (Windows) or Open (Mac). - VS Code will reload with the project's file list now visible in the sidebar (icon that looks like two overlapping pages, usually the very top one).
- Because this folder contains a
platformio.inifile, PlatformIO recognizes it automatically and starts initializing in the background — you may see a notification like "PlatformIO: Project Initialization" or a spinning icon in the bottom status bar. Just wait for this to finish before doing anything else; it can take a little while the very first time.
- Plug your sensor board (ESP32-S3 Feather or Arduino Uno) into your computer with a USB cable. Use a cable you know carries data, not a charge-only cable — if the board doesn't show up anywhere later, this is the first thing to double check.
- You don't need to manually find or select which USB port it's on — PlatformIO auto-detects it at upload time in almost all cases.
This project supports 11 different sensor chips, each on 2 possible
boards, and every combination is its own "environment" (e.g.
as7341_esp32s3, tcs3448_uno). You need to tell PlatformIO which one you
want.
- Look at the very bottom of the VS Code window — there's a thin blue
status bar that PlatformIO adds. It has several small icons:
a house 🏠, a checkmark ✓, a right-pointing arrow →, a trash can 🗑,
a plug 🔌, and — importantly — some text showing an environment
name (e.g.
Default (as7341_esp32s3)) usually toward the left/middle of that bar. - Click on that environment name text. A list pops up at the top of
the window showing every environment defined in
platformio.ini(as7341_esp32s3,as7341_uno,as7343_esp32s3,tcs3448_esp32s3,tcs3448_uno, etc.). - Click the one matching your sensor chip (printed on the small
breakout board you have) and your microcontroller board (ESP32-S3
Feather, or plain Arduino Uno). For example, if you have a TCS3448
breakout plugged into an ESP32-S3 Feather, choose
tcs3448_esp32s3. - That name now shows in the bottom status bar, confirming it's selected for every action below.
- Still in that same bottom blue status bar, click the checkmark (✓) icon. This is "Build" — it compiles the firmware without uploading it yet.
- A terminal panel opens at the bottom of the window and starts printing a lot of text. The very first build for a given sensor can take a few minutes, because PlatformIO is also downloading that sensor's vendor library and the board toolchain in the background (you don't need to have run any install script separately for this to work — building triggers it automatically).
- Wait for it to finish. You're looking for a green line near the bottom
that says
[SUCCESS], along withTook XX seconds. If it instead ends in red with[FAILED], scroll up in that same terminal to read the error — it's usually a missing/renamed library, which is worth reporting back.
- With the board still plugged in, click the right-pointing arrow (→) icon in that same bottom status bar. This is "Upload" — it builds (again, quickly, since it's already built) and then flashes the board.
- Watch the terminal again. You'll see it connect to the board, write the
firmware, and finish with another
[SUCCESS]line.- Arduino Uno users: if upload fails immediately, it's often because something else (like the Arduino IDE's own Serial Monitor) is holding the USB port open. Close any other program that might be talking to the board and try again.
- ESP32-S3 users: most boards upload automatically. If it hangs waiting to connect, check your board's documentation for whether it needs a BOOT button held during upload — this varies by board vendor.
- Once you see
[SUCCESS]for the upload, the firmware is live on the board. You can unplug and replug the USB cable at this point if you want to restart it fresh — it'll keep running the firmware you just flashed even after VS Code is closed.
- In VS Code's file sidebar, find the
webfolder, then findindex.htmlinside it. - Right-click
index.htmland look for an option like "Reveal in File Explorer" (Windows) or "Reveal in Finder" (Mac) — this opens your normal file browser pointed right at that file. - Double-click
index.htmlthere. It should open in your default browser. This step needs Chrome or Edge — Firefox and Safari don't support the Web Serial feature this project relies on, and it won't work on a phone/tablet browser either.- If it opens in the wrong browser, right-click the file instead and choose Open with → Google Chrome (or Edge).
- On the page, click the Connect via USB button.
- Your browser shows a small popup listing available serial/USB devices — click your board's entry, then click Connect.
- The page should now fill itself in automatically: sensor name, channel list, gain options, integration time controls, and an LED control if your sensor has one. If you see this, everything worked — you're ready to take a measurement.
- Nothing shows up in the "Connect via USB" popup at all: try a
different USB cable/port, and make sure Step 8's upload actually ended
in
[SUCCESS]. - The build (Step 7) fails on the very first try for a sensor: this usually means the download of that sensor's library is still in progress or hit a network hiccup — try Build again once.
- The page connects but shows blank/wrong controls: double-check you picked the environment (Step 6) that actually matches the physical chip on your breakout board, not a different sensor's environment.
- Still stuck? The "How to run it" section right below covers the exact same steps as command-line instructions, which sometimes surfaces a more specific error message than the VS Code UI does.
- First time only — install every sensor's libraries in one go:
Requires PlatformIO Core (
./install_libraries.sh # macOS / Linux install_libraries.bat # Windows
pip install -U platformio) on your PATH — or just open this folder in VS Code with the "PlatformIO IDE" extension and run the script from its integrated terminal. The script reads every[env:...]section straight out ofplatformio.iniand runspio pkg installfor each one, so it downloads all 11 sensors' vendor libraries and both board toolchains (ESP32-S3 + AVR/Uno) up front — you don't need to know yet which sensor you'll end up using, and you never have to manually search/install a library by hand. It's safe to re-run any time (e.g. after adding a 12th sensor toplatformio.ini); already-installed packages are skipped. - Pick your sensor + board and build/upload that PlatformIO environment, e.g.:
(Full list of the 22 environments is in
pio run -e as7341_esp32s3 -t upload pio run -e tcs3448_esp32s3 -t upload pio run -e apds9960_uno -t upload
platformio.ini.) Since step 1 already downloaded everything, this step works offline. - Open
web/index.htmldirectly in Chrome or Edge (desktop only — Web Serial isn't available on Firefox/Safari or mobile). - Click Connect via USB, pick the board's port. The page requests
get_info, the board replies with a full self-description, and the UI builds itself — modes, gain control, LED control, integration time control, and any sensor-specific extra parameters.
install_libraries.sh / .bat run once to fetch all 11 sensors' libraries + both board toolchains (see "How to run it")
platformio.ini the 22 build environments (11 sensors × 2 boards)
src/
├── main.cpp setup()/loop(), reads Serial lines, hands them to Protocol
├── core/
│ ├── Pins.h the two FIXED external LEDs (pin 6 = Absorbance/180°, pin 5 = Fluorescence/90°)
│ ├── Utils.h PROGMEM string helpers, JSON key builder — no String class anywhere
│ ├── SensorManager.h picks the ONE compiled-in sensor via a SENSOR_xxx build flag
│ ├── Measurement.h/.cpp the generic 10-second sampling loop + log/raw math — sensor-agnostic
│ ├── Protocol.h/.cpp the ONLY file that knows JSON; builds "info", parses commands
│ └── Export.h placeholder for future on-device export (CSV/Excel already happen in the browser)
└── sensors/
├── SensorBase.h the interface every sensor implements — READ THIS FIRST if adding sensor #12
├── AS7341.h/.cpp
├── AS7343.h/.cpp
├── TCS3448.h/.cpp 2026 register-compatible "refresh" of AS7343 — see note above
├── AS7262.h/.cpp
├── TCS34725.h/.cpp discontinued sensor — kept for existing hardware, see note above
├── APDS9960.h/.cpp recommended TCS34725 replacement
├── BH1750.h/.cpp
├── LTR303.h/.cpp
├── LTR329.h/.cpp near-duplicate of LTR303 on purpose — see note at the top of LTR303.h
├── LTR390.h/.cpp
└── TSL2591.h/.cpp
web/
├── index.html / style.css generic shell, zero sensor-specific markup
├── script.js builds the ENTIRE UI at runtime from the "info" event — zero sensor-specific logic
└── xlsx.core.min.js vendored SheetJS build (Excel export works fully offline)
core/ never imports a vendor library and never mentions a sensor by
name. Everything sensor-specific lives behind SensorBase. If you're
adding a 12th sensor, sensors/SensorBase.h and any existing sensors/*.cpp
is the only reading you need to do — core/ and web/ don't change. (This
is exactly how TCS3448 — the 11th sensor — was just added: no changes to
core/ or web/ at all, only SensorManager.h's one-line dispatch,
Protocol.cpp's sizing constants, platformio.ini's two environments, and
a new sensors/TCS3448.h/.cpp pair.)
Fixed: the board used to boot with
Measurement::modedefaulted toREFLECTANCE(or, for LED-less sensors, a mode the frontend never agreed with) but never actually drove the LEDs to match — so the sensor's own internal LED sat in the vendor library's power-on-default (off) state, invisibly out of sync with the "Reflectance" tab the web UI shows active the moment it connects, until you manually re-clicked a mode tab. This first became visible on AS7262 (the onlyLED_DISCRETEsensor, so the least-exercised LED code path) but affected every LED-capable sensor equally.main.cppnow picks the same default modesendInfo()will report and calls the newly-publicProtocol::syncOutputsForMode()right after the sensor initializes, so the physical LED state always matches what the UI shows as active from the very first frame — seesrc/main.cppandsrc/core/Protocol.h.
One JSON object per line (\n-terminated), both directions.
Browser → Board
{"cmd":"get_info"}
{"cmd":"set_mode","mode":"reflectance|absorbance|fluorescence"}
{"cmd":"set_submode","sub":"log|raw"}
{"cmd":"set_led", ...} // shape depends on led.internal.type — see below
{"cmd":"set_gain", ...} // shape depends on gain.type — see below
{"cmd":"set_time", ...} // shape depends on time.type — see below
{"cmd":"set_channels","channels":[0,2,5]}
{"cmd":"set_extra","idx":0,"value":3} // sensor-specific extra parameter
{"cmd":"measure_ref"}
{"cmd":"measure"}
Board → Browser
{"evt":"info", ...} // see schema below
{"evt":"ack","cmd":"...", "tint_ms":...} // tint_ms only present after set_time
{"evt":"progress","t":1.2,"n":3,"data":{"0":123.4}}
{"evt":"ref_saved","data":{"0":512.0}}
{"evt":"result","mode":"...","sub":"...","data":{...},"gain":"...","tint_ms":...}
{"evt":"error","msg":"..."}
set_led / set_gain / set_time payload shape mirrors whichever variant
info reported:
type |
Command payload |
|---|---|
led.internal: binary |
{"cmd":"set_led","on":true} |
led.internal: discrete |
{"cmd":"set_led","idx":2} |
led.internal: continuous |
{"cmd":"set_led","current":50} |
gain: discrete |
{"cmd":"set_gain","idx":4} |
gain: continuous |
{"cmd":"set_gain","value":150} |
time: formula |
{"cmd":"set_time","params":{"atime":100,"astep":999}} |
time: presets |
{"cmd":"set_time","preset":2} |
| Signal | Pin | Notes |
|---|---|---|
| SDA / SCL | board's default I2C | every sensor |
| Absorbance LED (180°) | 6 | fixed, same on every sensor/board |
| Fluorescence LED (90°) | 5 | fixed, same on every sensor/board |
| TCS34725's onboard LED | 7 | only this sensor — see "Particularities" below |
| Every other sensor's internal LED | — | driven through I2C (AS7341/AS7343/TCS3448/AS7262) or doesn't exist (photodiode sensors, including APDS9960) |
| Sensor | Channels | Gain | Integration time | Internal LED |
|---|---|---|---|---|
| AS7341 | 8 (415–680nm) | 11 discrete (0.5X–512X) | ATIME×ASTEP formula | Continuous, 4–258mA (I2C) |
| AS7343 | 13 (405–855nm+Clear) | 13 discrete (0.5X–2048X) | ATIME×ASTEP formula | Continuous, 4–258mA (I2C) |
| TCS3448 | 13 (407–855nm+Clear) | 13 discrete (0.5X–2048X) | ATIME×ASTEP formula | Continuous, 4–258mA (I2C) |
| AS7262 | 6 (450–650nm) | 4 discrete (1X–64X) | value×2.8ms formula | Discrete, 4 levels (I2C) |
| TCS34725 | 4 raw + 2 derived (Temp, Lux) | 4 discrete (1X–60X) | 6 fixed presets | Binary, needs GPIO pin 7 (not I2C — see below) — discontinued, see note at top |
| APDS9960 | 4 raw + 2 derived (Temp, Lux) | 4 discrete (1X–64X) | Direct-ms formula (~3–700ms) | None — onboard IR LED only serves proximity/gesture, unusable here |
| BH1750 | 1 (Lux) | Continuous (MTreg 31–254) | 3 resolution presets | None |
| LTR303 / LTR329 | 2 raw + 1 derived (Visible) | 6 discrete | 8 presets + Measurement Rate (extra param) | None |
| LTR390 | 1 (UV or ALS — mode extra param picks which) | 5 discrete, non-linear | 6 resolution presets | None |
| TSL2591 | 2 raw + 2 derived (Visible, Lux) | 4 discrete, huge non-uniform range | 6 presets | None |
Sensors with no internal LED never offer Reflectance mode at all — the
modes array in info simply won't include it, and the frontend's mode
tabs reflect that automatically.
TCS34725's LED is a special case. The Adafruit library's
setInterrupt() only toggles the onboard LED if you've soldered the
breakout's LED pin to its INT pin — a fragile, optional hardware mod most
builds won't have done. Instead, TCS34725Sensor drives the LED from a
dedicated GPIO (pin 7, same on every board) using plain digitalWrite,
following the exact same "on entering Reflectance, off leaving it" logic
core/Protocol.cpp already applies generically to every LED type — no core
changes were needed to support this.
APDS9960 has no equivalent workaround. Its onboard IR LED (and driver)
exist purely to support the proximity/gesture engine — the Adafruit library
gives no way to fire it during a color/ALS reading, and IR isn't a useful
wavelength for reflectance anyway. APDS9960Sensor::ledType() simply
returns LedType::LED_NONE, the same as every other photodiode-only sensor
in this project, so sendInfo()'s existing "only list Reflectance if
ledType() != LED_NONE" logic hides Reflectance for it automatically —
again, no core changes needed. Absorbance and Fluorescence both still work
normally, driven entirely by the two fixed external LEDs on pins 6/5.
Adafruit product #6525, released September 2026 — new enough that this whole section was written from live sources (Adafruit's product page, the ams-OSRAM TCS3448 datasheet DS001121, and the Adafruit_TCS3448/CircuitPython library docs), never from training data. See "What's verified" below for exactly what was checked where.
- It's ams-OSRAM's "refresh" of the AS7343, not an unrelated chip. Per
Adafruit's own product description: "The TCS3448 register map and
automatic channel multiplexer are compatible with the AS7343." That's why
TCS3448Sensor(sensors/TCS3448.h/.cpp) is structured identically toAS7343Sensor— same 13 exposed channels, same 13-step discrete gain table (0.5X–2048X), same ATIME/ASTEP integration-time formula ((ATIME+1) × (ASTEP+1) × 2.78µs— confirmed directly in the ams-OSRAM datasheet's own footnote), same 4–258mA continuous internal LED driver (also confirmed in the datasheet's LED Driving Strength table). - It still needs its own firmware build, not AS7343's, for two reasons
Adafruit calls out explicitly:
- I2C address changed from 0x39 to 0x59. The
Adafruit_TCS3448Arduino library (which subclassesAdafruit_AS7343) selects the new address internally, so nothing inTCS3448Sensor::begin()has to handle this — but it does mean AS7343 firmware simply won't find a TCS3448 on the bus, and vice versa. - Retuned optical filters — channel center wavelengths shift by a
few nanometers versus AS7343.
sensors/TCS3448.cpp's channel labels use the TCS3448-specific typical wavelengths from datasheet Table 7 (e.g. F1 = 407nm, FY = 560nm, FXL = 596nm), not copy-pasted from AS7343's slightly different values (405nm/555nm/600nm respectively).
- I2C address changed from 0x39 to 0x59. The
- Flicker detection is excluded from
channelCount(), on purpose, the same way it's excluded for AS7343. Adafruit's own spec sheet describes the chip as "14 readable individual sensor elements (13 light channels plus flicker detection)" — flicker is a separate digital 50/60Hz-style detection feature, not a spectral intensity value comparable to the other 13 channels' -log₁₀(I/I₀) math, so it isn't a good fit for this project's genericreadChannels()abstraction. A future contributor wanting flicker data would add it as anextraParamrather than a channel. - One thing flagged, not silently assumed: the exact spelling of the
TCS3448_CHANNEL_*/TCS3448_GAIN_*constants and theAdafruit_TCS3448class's method names (begin,readAllChannels,setGain,setATIME,setASTEP,enableLED,setLEDCurrent) couldn't be confirmed by reading the actual header file directly (this sandbox can search and read web pages, but not fetch arbitrary raw GitHub file contents outside a prior search result, and no such raw link turned up). What is confirmed directly is Adafruit's own description of the library: it "subclasses the Adafruit AS7343 driver, selects the TCS3448 I2C address, and adds TCS3448-named data types" — i.e., the same method names asAdafruit_AS7343(inherited), plus aTCS3448_-prefixed mirror ofAS7343_CHANNEL_*/as7343_gain_tfor the numerically-identical register-compatible values.sensors/TCS3448.cppis written on that basis. If the real header spells any of these differently, the compiler will point at the exact line (unlike a silent wrong-value bug, a wrong symbol name fails loudly at compile time) — the fix is contained entirely tosensors/TCS3448.cpp, nothing else in this project would need to change.
- Read
sensors/SensorBase.htop to bottom (it's short, and documents the 4 LED types / 2 gain types / 2 time types). - Copy the sensor whose particularities are closest to yours as a
starting point (e.g. a photodiode sensor with no LED → start from
BH1750.h/.cpporTSL2591.h/.cpp; a 4-channel RGBC color sensor with no LED → start fromAPDS9960.h/.cppinstead ofTCS34725.h/.cpp, since the latter's binary-LED-on-GPIO-7 logic is extra complexity you probably don't need; a 13-channel spectral sensor with a continuous internal LED driver → start fromAS7343.h/.cpporTCS3448.h/.cpp, which are near-identical to each other). - Add a
SENSOR_yourchipbranch tocore/SensorManager.h. - Add the per-sensor
INFO_MAX_*capacity constants tocore/Protocol.cpp(sized to your sensor's actual channel/gain/preset/ extra-param counts — see the existing#ifblock; oversizing just wastes RAM on AVR, it won't break anything). - Add two environments to
platformio.ini(ESP32-S3 + Uno), copying an existing pair and swappingSENSOR_yourchip,build_src_filter, andlib_deps. web/needs zero changes.
Every library in platformio.ini — name, version pin, and every vendor
method/enum/constant referenced from sensors/*.cpp — has been checked
directly against the real library.properties (or library.json) and
source files committed in each vendor's own GitHub repo, fetched live
rather than recalled from memory. This caught two real issues:
- AS7341 was pinned to a version that was never published
(
^1.4.2, when the real latest release is1.4.1). PlatformIO's^requirement means "this version or higher, same major line" — with no1.4.2or later 1.x release to satisfy that,pio pkg installwould have hard-failed with "not found". Fixed to^1.4.1. - AS726x's real registry name is
Adafruit AS726X(capital X) —platformio.inihad lowercasex. PlatformIO's registry match is case-insensitive, so the old spelling would likely have resolved fine regardless, but it's been corrected for certainty.
Everything else checked out with no changes needed, including some things
that looked suspicious enough to double-check and turned out fine — e.g.
Adafruit_AS726X's own library.properties lists Adafruit ST7735 and ST7789 Library, Adafruit GFX Library as dependencies (alongside BusIO),
which looks like a copy-paste leftover from an unrelated Adafruit template;
confirmed Adafruit_AS726x.h doesn't actually #include any GFX/ST77xx
header, and PlatformIO's Library Dependency Finder resolves by scanning
real #include statements in source, not by trusting that field — so it's
harmless. Similarly, Adafruit_TSL2591::begin() leaves the sensor powered
down and this project's TSL2591Sensor::readChannels() never calls
enable() — but getFullLuminosity() calls enable()/disable()
internally around every read, so that's already handled by the library
itself, not a gap in this driver.
Per-sensor specifics:
- AS7341: this refactor's
core/+ AS7341 combination was compiled and linked for real against the actual ArduinoJson v6.21.5 library, targetingatmega328p(Arduino Uno), using hand-written stand-ins forArduino.h/Wire.h/Adafruit_AS7341.h(this sandbox can reachraw.githubusercontent.comto read source, but notregistry.platformio.orgto actually install packages and run a realpio run). Result: links cleanly, 685 bytes of permanent RAM,"info"packet peaks at 840 bytes on the stack alone for AS7341 specifically (tuned per-sensor incore/Protocol.cpp). - AS7341 / AS7343: every method used (
begin,setATIME,setASTEP,setGain,readAllChannels,getChannel,enableLED,setLEDCurrent) matchesAdafruit_AS7341.h/Adafruit_AS7343.hexactly, including theas7341_gain_t/as7343_gain_tenum pattern. - TCS3448: released September 2026 (Adafruit product #6525), so this is
the newest sensor in the project and gets the most explicit sourcing
breakdown. Confirmed directly against live sources: the chip is
ams-OSRAM's register-compatible "refresh" of AS7343 (Adafruit's product
page); the I2C address change 0x39→0x59 (same page); the 13-step gain
table (0.5X–2048X) and the
(ATIME+1)×(ASTEP+1)×2.78µsintegration-time formula (ams-OSRAM datasheet DS001121, footnote 6 and the gain-code table); the 4–258mA continuous LED driver range (same datasheet, "LED Driving Strength" register table,000 0000: 4 mA…111 1111: 258 mA); the per-channel typical wavelengths used inTCS3448.cpp(same datasheet, Table 7 "Optical characteristics of TCS3448" — F1 407nm, F2 424nm, FZ 450nm, F3 473nm, F4 516nm, F5 546nm, FY 560nm, FXL 596nm, F6 636nm, F7 687nm, F8 748nm, NIR 855nm); the Arduino library's real registry name ("Adafruit TCS3448", confirmed via itsarduino/library-registryPR, including the linter's note that the name legitimately contains a space). Inferred, not independently confirmed (flagged clearly since this matters): the exact spelling of theTCS3448_CHANNEL_*/TCS3448_GAIN_*enum constants used inTCS3448.cpp'sCHANNEL_INDEX/GAIN_VALUEStables — the library's own README confirms it "adds TCS3448-named data types" mirroring AS7343's, but this sandbox couldn't fetch the actual header file to read the literal symbol spelling. See the dedicated "TCS3448 — why it's a separate driver" section above for the full reasoning and what happens if a name turns out slightly different (a loud compile error, confined to this one file — not a silent bad reading). - AS7262: every method and constant (
begin,setGain/GAIN_1XthroughGAIN_64X,setConversionType/MODE_2,setIntegrationTime,dataReady,readCalibratedValues,drvOn/drvOff,setDrvCurrentand itsLIMIT_*constants) matchesAdafruit_AS726x.hexactly. - TCS34725 / APDS9960:
Adafruit_APDS9960.h's real surface (begin(iTimeMS, gain, addr, wire),enableColor(),colorDataReady(),getColorData(),setADCGain()/apds9960AGain_t,setADCIntegrationTime(),calculateColorTemperature(),calculateLux()) matches whatAPDS9960.cppcalls. The integration-time millisecond range (~3–700ms) is derived from the library'sATIME = 256 - iTimeMS/2.78conversion formula rather than stated as a single number anywhere in the header — confirm the exact clamping behavior against the installed library version if precise timing matters for your measurements. - BH1750:
begin,readLightLevel,setMTreg, and theBH1750::CONTINUOUS_HIGH_RES_MODE/_MODE_2/CONTINUOUS_LOW_RES_MODEenum values all matchsrc/BH1750.hinclaws/BH1750exactly. - LTR303 / LTR329: both classes really do come from one shared
Adafruit_LTR329_LTR303.hheader as assumed (Adafruit_LTR303 : public Adafruit_LTR329), and every method used (begin,setGain,setIntegrationTime,setMeasurementRate,newDataAvailable,readBothChannels(uint16_t&, uint16_t&)— note it takes references, not pointers, which this driver already gets right) matches exactly.LTR329Sensorremains a deliberate near-duplicate ofLTR303Sensorrather than a template — see the note at the top ofLTR303.h. - LTR390 resolution→ms table (400/200/100/50/25/12.5ms for
20/19/18/17/16/13-bit): the method surface (
begin,setMode,setGain,setResolution,newDataAvailable,readUVS,readALS) matchesAdafruit_LTR390.hexactly; the specific ms-per-resolution numbers come from the sensor's measurement-rate register behavior as documented in other open-source drivers (e.g. ESPHome), not stated directly in Adafruit's own header/examples — worth a spot-check against your specific library version if exact timing matters. - TSL2591:
begin,setGain/tsl2591Gain_t,setTiming/tsl2591IntegrationTime_t,getFullLuminosity,calculateLuxall matchAdafruit_TSL2591.hexactly.
None of the 11 environments have actually been run through a real
pio run in this sandbox (no route to registry.platformio.org/the
Arduino package index to install and compile against). Build each one
yourself before flashing real hardware — but every name, version, and API
call it depends on has now been checked against the real source, not
assumed. This applies especially to tcs3448_esp32s3/tcs3448_uno,
since it's the only environment here whose library isn't even a year old
yet — build it first and confirm it compiles before relying on it.
If any of the above turns out to not match a given library version, the
fix is contained entirely to that one sensors/*.cpp file.
Every library name below was checked against the real PlatformIO/Arduino
library registries (via web search, since this environment can't reach
api.registry.platformio.org directly) — including catching two mistakes
along the way: the LTR303/LTR329 library's real name has no "Library" suffix
(Adafruit LTR329 and LTR303, not ...Library), and two version pins I'd
guessed (Adafruit AS726x @ ^1.5.2, Adafruit LTR390 Library @ ^1.2.1) were
both higher than the real latest release (1.2.3 and 1.1.2 respectively) and
would have hard-failed the build with "version not found". APDS9960's real
registry name does keep the "Library" suffix (Adafruit APDS9960 Library), confirmed the same way.
Only AS7341's version pin (^1.4.2) and the two universally-common
dependencies (ArduinoJson ^6.21.5, Adafruit BusIO ^1.16.1) are
pinned to a specific version with reasonable confidence. Every other
sensor's library is listed without a version pin on purpose — a wrong
guessed pin actively breaks the build (PlatformIO refuses to "downgrade" to
satisfy a caret range), while no pin just resolves to whatever's current.
If you want fully reproducible builds, confirm each real version at
https://registry.platformio.org and add @ ^x.y.z yourself.
Adafruit TCS3448 (used by tcs3448_esp32s3/tcs3448_uno) is left
unpinned for the same reason, with one extra note: its
arduino/library-registry submission PR was only merged/tagged in
September 2026 (release 1.0.0), and PlatformIO's own registry mirrors
the Arduino Library Manager index with some lag — if pio pkg install
can't find it yet for your PlatformIO installation, that's most likely a
registry-sync delay rather than a wrong name; check
https://registry.platformio.org/search?q=TCS3448 directly, or install it
by Git URL (https://github.com/adafruit/Adafruit_TCS3448.git) as a
fallback in lib_deps until the mirror catches up.
{ "evt": "info", "sensor": "AS7262", "sensorType": "color", // or "photodiode" "channels": [{"id":0,"name":"450nm","color":"#7F00FF"}, ...], "modes": ["absorbance","fluorescence"], // "reflectance" only appears if led.internal.type != "none" "gain": { "type": "discrete", // or "continuous" "options": ["1X","3.7X","16X","64X"] // discrete // "min": 31, "max": 254 // continuous (e.g. BH1750's MTreg) }, "led": { "internal": { "type": "none|binary|discrete|continuous", "options": [...] // discrete only // "minMA": 4, "maxMA": 258 // continuous only }, "absorbance": {"type":"external","pin":6,"angle":180}, "fluorescence": {"type":"external","pin":5,"angle":90} }, "time": { "type": "formula", // or "presets" "formula": "value x 2.8ms", "currentMs": 140.0, "params": [{"key":"itime","label":"Integration Time","min":0,"max":255}] // "presets": ["2.4 ms", "24 ms", ...] // presets mode instead }, "extraParams": [ // omitted entirely if the sensor has none {"key":"measRate","label":"Measurement Rate","type":"select","options":["50 ms", ...]} // {"key":"...", "type":"number", "min":0, "max":255} ] }