A MaixPy spectrometer application for Sipeed MaixCAM2 and the AMS/OSRAM AS7341 11-channel multispectral sensor. The project provides a complete measurement pipeline: GPIO software I2C, AS7341 dual-SMUX acquisition, touchscreen interaction, local GUI rendering, lightweight WebUI control, relative spectral power distribution reconstruction, peak detection, estimated illuminance, and long-format CSV export.
This README is written as a technical report so that users can understand not only how to run the application, but also the physical and numerical assumptions behind the spectrum reconstruction.
The AS7341 does not measure a continuous spectrum directly. It measures a small set of filtered broadband channels. Each visible channel integrates incoming optical power over a wavelength-dependent spectral response curve. Recovering a continuous spectrum from those channel values is therefore an inverse problem: many different spectra can produce similar channel readings.
This project reconstructs a useful relative visible SPD over 380-780 nm by combining an AS7341 response model, exposure normalization, non-negative multiplicative inversion, smoothing, Clear-channel global constraint, and sensor-support priors near poorly constrained spectral boundaries. The result is suitable for visualization, comparative spectral analysis, peak tracking, and educational work. It is not a replacement for a calibrated laboratory spectrometer unless the device is calibrated against reference optical equipment.
The physical system is a compact spectral measurement platform:
- AS7341 sensor module mounted to MaixCAM2 over GPIO software I2C
- MaixCAM2 local display and touch input for standalone operation
- Optional HTTP WebUI for remote control and visualization
- CSV export for offline analysis and calibration
flowchart LR
A[Incident light field] --> B[Optics, diffuser, aperture]
B --> C[AS7341 spectral filters]
C --> D[Photodiodes and ADC]
D --> E[Dual SMUX channel readout]
E --> F[GPIO software I2C on B20/B19]
F --> G[MaixPy acquisition loop]
G --> H[Dark subtraction and exposure normalization]
H --> I[Visible SPD inverse reconstruction]
H --> J[NIR estimate]
I --> K[Peak detection and Lux estimate]
J --> K
K --> L[MaixCAM2 GUI]
K --> M[WebUI on port 2932]
K --> N[Long-format CSV export]
- Real-time MaixCAM2 touchscreen interface
- AS7341 dual-SMUX sampling for F1-F8, Clear, and NIR channels
- GPIO open-drain software I2C on B20/B19 for the current wiring
- Dark baseline zeroing, gain control, integration-time control, pause/run, CSV save, and exit controls
- 380-780 nm visible relative SPD reconstruction with a 0.1 nm internal grid and filled area plot
- Default photon-count SPD display with a Photon/Power toggle on both GUI and WebUI
- Automatic visible-spectrum peak annotation, up to 5 peak wavelengths
- Separate 760-1000 nm IR estimate window, hidden by default and toggled by the top-right
IRtab - Lightweight WebUI on port
2932for remote control, spectrum viewing, and CSV download
Target hardware:
- Sipeed MaixCAM2
- AS7341 spectral sensor module
- 3.3 V I/O wiring
Default wiring:
| AS7341 | MaixCAM2 |
|---|---|
| VDD | 3V3 |
| GND | GND |
| SCL | B20 |
| SDA | B19 |
| INT | B18 |
| GPIO | B21 |
B20/B19 are not assumed to be hardware I2C pins on the MaixCAM2 connector. This project uses GPIO open-drain software I2C by default, so the wiring above can be used directly. Do not drive MaixCAM2 GPIO pins with 5 V logic.
main.py: Self-contained MaixPy application entry point, recommended for direct upload and execution.app.yaml: MaixPy application descriptor.soft_i2c_gpio.py: Modular software I2C implementation.as7341_driver.py: Modular AS7341 driver and spectrum reconstruction code.spectrometer_ui.py: Modular GUI drawing code.as7341_spectrometer_maixcam2.py: Modular application entry point.AS7341_DS000504_3-00.pdf: Local copy of the official AS7341 datasheet.maixcam2_pins.jpg: MaixCAM2 pin reference image.Spectrum_Platform.png: WebUI screenshot.GUI.png: MaixCAM2 GUI screenshot.
For MaixVision or the MaixPy runner, running main.py is recommended to avoid missing module upload errors.
- Wire the AS7341 to MaixCAM2 using the default wiring table.
- Upload and run
main.pyon the MaixCAM2. - The app probes the AS7341 automatically at I2C address
0x39. - Use the touchscreen buttons:
Run/Pause: Start or pause samplingZero: Capture the current dark baselineGain -/+: Adjust AS7341 analog gainInt -/+: Adjust integration timePhoton/Power: Switch the main SPD plot between integrated photon-count and radiant-power viewsSave: Append the current sample toas7341_spectrum_long.csvExit: Exit the application
- Tap the top-right
IRtab to show or hide the IR estimate window.
The application starts a lightweight HTTP service on the MaixCAM2 when possible. The default port is 2932.
Open this URL from a browser on the same network:
http://<MaixCAM2-IP>:2932/
The WebUI supports:
- Live 380-780 nm visible SPD filled curve and peak labels
- Raw and corrected values for each AS7341 channel
- Remote
Run/Pause,Zero,Photon/Power,Gain -/+,Int -/+,Save, andExit - Direct
Download CSVexport ofas7341_spectrum_long.csv
If the web service fails to start, the local touchscreen application still works.
The implementation follows this processing chain for every valid sample:
flowchart TD
A[Raw AS7341 channel counts] --> B[Dark baseline subtraction]
B --> C[Corrected counts]
C --> D[Exposure normalization by gain and integration time]
D --> E[Visible vector y for F1-F8]
D --> F[Clear normalized signal]
D --> G[NIR normalized signal]
E --> H[Build or reuse response matrix A]
H --> I[Non-negative multiplicative inversion]
I --> J[Smoothing and sensor-support prior]
J --> K[Clear-channel global amplitude constraint]
K --> L[Relative visible SPD on 0.1 nm grid]
G --> M[Separate NIR curve estimate]
L --> N[Peak detection]
L --> O[Photopic Lux estimate]
L --> P[GUI and WebUI plot]
L --> Q[CSV long table]
M --> P
M --> Q
N --> P
O --> P
O --> Q
Let
For channel
Where:
-
$m_i$ is the raw ADC count for channel$i$ -
$g$ is sensor gain -
$t$ is integration time -
$d_i$ is the dark offset or dark baseline -
$n_i$ includes shot noise, read noise, quantization noise, I2C timing effects, and environment variation -
$R_i(\lambda)$ is the effective spectral response of the channel -
$\Phi(\lambda)$ is the incident spectral power distribution at the sensor
After dark subtraction and exposure normalization:
The application also applies a per-channel sensitivity normalization from typical AS7341 response data, producing a corrected channel vector:
Here
The continuous spectrum is discretized onto a wavelength grid. In this project:
Let
In matrix form:
Where:
-
$\mathbf{y}$ is the 8-element visible channel vector from F1-F8 -
$A$ is the response matrix built from channel center wavelength and FWHM -
$\mathbf{x}$ is the reconstructed visible SPD on the 0.1 nm grid
This system is underdetermined: there are only 8 visible measurements and 4001 wavelength grid points. Therefore, a unique high-resolution true spectrum cannot be recovered from the AS7341 alone. The 0.1 nm grid should be understood as a smooth interpolation and numerical representation of a low-dimensional inverse estimate, not as real 0.1 nm optical resolving power.
The AS7341 datasheet provides nominal center wavelengths and bandwidths for visible channels. The implementation approximates each channel response as a Gaussian-like band:
Each row is normalized:
This produces a numerical response matrix whose rows represent how much each wavelength contributes to each channel. The model is approximate, because real sensor responses are not perfect Gaussians and vary with module optics, cover glass, diffuser, angle, temperature, and manufacturing tolerance.
For speed, the code stores sparse response rows. Very small Gaussian tails are truncated, and the response matrix is cached after first use.
The reconstruction requires non-negativity because optical radiant power cannot be negative:
The implementation uses a multiplicative update similar in spirit to Richardson-Lucy style positive inverse methods:
In expanded form, the update is:
This update has useful properties for embedded use:
- It keeps
$x_j$ non-negative. - It avoids matrix inversion.
- It is stable with a small number of sensor channels.
- It can be implemented with simple loops and cached response rows.
The application starts from an initial weighted estimate:
Then it performs a limited number of iterations. More iterations do not create true higher optical resolution; they mainly sharpen the numerical estimate and can amplify noise. The current code uses a conservative iteration count and smoothing to keep the displayed SPD stable.
Because the inverse problem is underdetermined, regularization is essential. The project uses three practical forms of regularization.
First, smoothing suppresses narrow numerical spikes that cannot be justified by AS7341 channel bandwidths:
Here
Second, a sensor-support prior reduces confidence where the sum of channel responses is weak:
Here
This is especially important at the low-wavelength boundary. F1 is centered near 415 nm, so the 380-405 nm region has weak independent support. Without a support prior, the inverse update can push energy toward the boundary and create false 380 nm peaks.
Third, peak detection ignores candidate peaks in regions with insufficient support. This prevents the GUI from labeling poorly constrained edge artifacts as physical spectral peaks.
The AS7341 Clear channel has a broad response. It is not a spectral channel, but it is useful as a global energy constraint. The implementation compares the reconstructed SPD's predicted Clear response with the measured Clear-normalized signal:
The scale factor is clamped to a bounded range before being applied to the visible SPD. This prevents Clear from overwhelming the spectral shape while still making the reconstructed amplitude more consistent with the total broadband light level.
The NIR channel is intentionally not merged into the visible SPD. The visible reconstruction covers 380-780 nm, while the AS7341 NIR channel is centered around the near-infrared region. Merging NIR into the visible SPD would distort the visible plot and confuse peak interpretation.
Instead, the application reconstructs a separate NIR estimate over 760-1000 nm using an NIR-shaped response curve. The local GUI shows this in a separately toggleable IR window, and the CSV exports it with kind=ir_spd.
Illuminance is a photometric quantity. It weights optical power by the human photopic luminous efficiency curve
The code first converts corrected channel counts into datasheet-referenced equivalent irradiance in uW/cm^2, using the AS7341 optical responsivity tables, optical gain ratios, and integration time. A visible-channel calibration vector is then applied before reconstruction. This keeps the original datasheet calculation available while preventing the low-responsivity blue channels, especially F2, from dominating the inverse result on this hardware setup. The calibrated SPD is reduced to a photopic equivalent irradiance:
The Lux estimate then uses the physical conversion:
where 0.01 converts uW/cm^2 into W/m^2. This is still an estimate because the AS7341 datasheet does not provide a device-specific absolute CIE photopic calibration matrix. For certified absolute Lux, calibrate with a reference lux meter.
The CSV also records clear_irradiance_uW_cm2, allowing users to compare SPD-derived Lux with a Clear-channel proxy and build a device-specific calibration curve if their optical setup makes Clear more reliable.
Photon energy is:
This relation is required when converting radiant-power spectra into photon-flux or photon-count spectra, such as PPFD for plant lighting. The AS7341 response model used here first reconstructs a radiant-power-like SPD because the channel responses are modeled as optical-power responses. A separate photon-count view is then computed from that reconstruction:
The application keeps both meanings separate:
- GUI and WebUI default to photon-count SPD and can toggle back to radiant-power SPD.
- CSV
relative_poweris retained for compatibility;reconstructed_irradiance_uW_cm2stores the calibrated reconstructed irradiance estimate. - CSV channel fields include both raw datasheet irradiance and calibrated irradiance:
datasheet_irradiance_uw_cm2_*andirradiance_uw_cm2_*. - CSV
relative_photon_countstores the wavelength-weighted photon-count proxy, andphoton_relative_intensitystores its normalized plot value. -
lux_estremains a photopic radiant-power estimate using$V(\lambda)$ ; it is not computed from photon-count SPD.
This avoids applying a wavelength-energy correction directly to the physical meaning of the radiant-power plot while still making photon-count analysis immediately available.
Peak detection is performed on the normalized visible SPD. A candidate peak must:
- Be a local maximum
- Be above a minimum relative intensity threshold
- Be sufficiently separated from previously selected peaks
- Lie in a wavelength region with enough sensor support
The GUI and WebUI label up to 5 peaks. Because AS7341 channels are broad, peak positions should be interpreted as reconstructed peak estimates, not monochromator-grade line positions.
The 0.1 nm grid contains 4001 visible points. Direct dense inversion would be unnecessarily expensive on embedded Python. The implementation reduces cost by:
- Caching the visible response matrix
- Truncating negligible Gaussian tails into sparse rows
- Caching Clear and NIR response curves
- Caching photopic
$V(\lambda)$ weights - Using multiplicative updates instead of matrix decompositions
- Downsampling WebUI plot data while preserving full-resolution CSV export
The CSV still exports every 0.1 nm point. The GUI and WebUI draw a downsampled representation to keep interaction responsive.
Pressing Save appends data to as7341_spectrum_long.csv. The export uses a long vertical table where each wavelength occupies one row. Fields include:
- Timestamp, gain, integration time, and saturation state
- Raw, dark, corrected, normalized, basic-count, datasheet
uW/cm^2, and calibrateduW/cm^2irradiance data for all 10 exported channels - Visible SPD summary: dominant wavelength, centroid, estimated CCT, fit confidence, and peak list
kind,wavelength_nm,relative_intensity,relative_power,reconstructed_irradiance_uW_cm2,photon_relative_intensity,relative_photon_countlux_est,lux_source,clear_lux_signal,clear_irradiance_uW_cm2- Photon summary fields:
photon_integral,photon_dominant_nm,photon_centroid_nm,photon_peaks_nm - Full 0.1 nm visible SPD data over 380-780 nm
- Full 0.1 nm IR estimate data over 760-1000 nm
For rigorous use, perform at least two levels of calibration.
First, dark calibration:
- Cover the sensor or use a dark enclosure.
- Press
Zero. - Repeat if integration time or gain changes substantially.
Second, optical calibration:
- Use a stable reference light source or calibrated lux meter.
- Record the displayed
lux_est,clear_irradiance_uW_cm2, and reference Lux. - Fit a device-specific calibration curve if absolute photometry is required.
- For spectral accuracy, use a monochromator or calibrated spectral source to derive a real device-specific response matrix.
A proper calibration matrix can be written conceptually as:
Where
- AS7341 has broad, overlapping channels; it cannot resolve narrow spectral lines like a grating spectrometer.
- The 0.1 nm grid is a reconstruction grid, not the physical optical resolution.
- Low-light measurements are affected by dark noise and quantization.
- Saturation invalidates the linear measurement model.
- Module optics, diffuser, angle of incidence, and temperature affect the response.
- Lux and CCT are estimates unless calibrated.
- NIR is exported separately and should not be interpreted as visible SPD.
New source code in this project is released under the MIT License, copyright (c) 2026 Fona. See LICENSE.
This project depends on or references the following third-party projects, hardware documents, and vendor materials. Their copyright and license notices remain in effect and are not relicensed by this project's MIT License.
-
MaixPy and MaixCDK
Copyright (c) 2023- Sipeed Ltd.
Licensed under the Apache License, Version 2.0.
Project: https://github.com/sipeed/MaixPy -
Adafruit CircuitPython AS7341 driver
Copyright (c) 2020 Bryan Siepert for Adafruit Industries.
Licensed under the MIT License.
Project: https://github.com/adafruit/Adafruit_CircuitPython_AS7341 -
AS7341 datasheet and sensor documentation
AS7341 is a product and trademarked device documentation of AMS/OSRAM or its respective rights holders. The included datasheet is retained as vendor reference documentation and remains under its original copyright. -
MaixCAM2 hardware, pinout image, board names, and product documentation
MaixCAM2 and related board documentation belong to Sipeed Ltd. or their respective rights holders. Hardware names and diagrams are used only for compatibility and wiring reference.
This software is provided for research, education, and prototyping. It is not certified for safety-critical, medical, industrial metrology, or regulatory measurement use. Validate hardware, calibration, optical path, and exported spectral data before relying on results.

