Web Dev Alphabet Soup: CORS and CSRF

After a helpful comment helped me find documentation on the no-longer-mysterious AS7341 SMUX (sensor multiplexor) I went to learn more about another mystery I stumbled across as a beginner web developer: CORS (cross-origin resource sharing.) Why does CORS policy exist? After a bit of poking around, I believe the answer is to mitigate a type of attack under the umbrella of CSRF (cross-site request forgery.)

When developing my AS7341 web app, I had the AS7341 accessible via a HTTP GET on my ESP32 and thought I could develop the HTML interface on my desktop machine. But when my desktop-served JavaScript tried to query my ESP32, I was blocked by browser CORS policy. By default, JavaScript served from one server (my desktop) is not allowed to query resources on another (my ESP32.)

Reading various resources online, I learned I could set my ESP32’s HTTP response header “Access-Control-Allow-Origin” to a wildcard “*” to opt out of CORS protection. But that’s merely a “make the error go away” kind of recommendation. I know CORS is security related, but I don’t understand the motivation. What security problem does CORS prevent? Without knowing the motivation, I don’t know what I am opening up by setting “Access-Control-Allow-Origin : *” In my web app, I started out cautiously by only setting that header when I’m developing the HTML UI, serving from my desktop to query my ESP32. In “production”, my ESP32 will serve the HTML and would not need “Access-Control-Allow-Origin : *” in the header to query itself, so that header is absent.

Is that the right thing to do, or is that being overly cautious? I set out to learn more. Curiously, reading MDN and other resources give me information about HOW CORS works, but not a lot about WHY CORS exists. I guess CORS documentation assume the reader already knows! Based on that fact, I know I am looking for a relatively common website security issue that is now considered basic knowledge by network professionals.

Another data point is the fact that CORS is only applicable to HTTP queries from JavaScript running in the browser. From a command line on my desktop, I can use the “curl” tool to query my ESP32 and CORS does nothing to block that. My browser on my desktop can query the endpoint directly and that is not blocked by CORS policy, either.

Things didn’t make much sense until I found a key piece of information: HTTP request sent from a browser’s JavaScript runtime not only sends the URL and its parameters, but the browser would also attach all cookies set by that host. These cookies may contain user authentication (the “Keep me logged in” checkbox) and it makes sense such capability shouldn’t be available to just any piece of JavaScript served by random hosts. Knowing this fact and knowing the kind of abuse such code can cause eventually led me to a category of security attacks known as CSRF (cross-site request forgery.)

Once I understood CORS is here to mitigate a subset of CSRF attacks, I could look at my ESP32 AS7341 access endpoint and decide CSRF is not a problem here. Setting “Access-Control-Allow-Origin : *” does not open me up to security nastiness, so my ESP32 sketch sets that header all the time now not just during development. This is a handy bit of knowledge, but it merely scratched the surface of web security. Another item I found to be big and intimidating is OAuth.

Code for this project is publicly available on GitHub

AS7341 Project Postscript: SMUX Mystery Solved

I’ve wrapped up version 1.0 of my AS7341 interaction web app project with some ideas for future improvements, but I learned of a big one after I wrote up my project. When an earlier post in my AS7341 series “Sample Code Gave Incomplete Picture of AS7341 SMUX Configuration” was published, there was a comment by [Sebastian] telling me that I’ve overlooked the “Tools & Resources” tab of AMS AS7341 product page.

[Sebastian] is correct! There were several large ZIP file downloads under “Resources” of type “Evaluation Software”. Their descriptions line up with several AMS demos for this sensor. I probably dismissed them as irrelevant as I don’t have the corresponding AMS concept demonstration hardware. But [Sebastian] didn’t make the same mistake. Thanks to his investigation, I’ve been prompted to look inside and found that, in additional to demo-specific resources, there are subdirectories with reference resources including everything I complained was missing:

  • Windows application installer, likely for AMS AS7341 GUI software mentioned in calibration Application Note. (I didn’t install on my own computer.)
  • Excel spreadsheet also mentioned in calibration Application Note.
  • Calibration Application Note along with a few other Application Notes.
  • Most importantly: an Application Note on SMUX configuration details!

The gold nugget found within the ZIP file is AMS Application Note AN000666. “SMUX Configuration: How to Configure SMUX for Reading Out Results.” The precise location probably varies from file to file, but for the file I examined (AS7341_EvalSW_ALS_v1-26-3) it was under subdirectory “Documents”/”application notes”/”SMUX”

The key piece of information I had been missing earlier is the concept of mapping AS7341 sensor array to pixel IDs. These pixel IDs are not sequential or regular in any pattern I can decipher, and many pixel IDs are unused. I suspect these ID assignments made sense for reasons important to the engineering team that laid out this implementation on silicon wafers. Between their seemingly random order and the fact roughly half of the IDs were just unused, it was no wonder I failed to reverse-engineer this information from sample code.

But with this Application Note as reference, we now have information in hand to create SMUX configurations to best suit future projects. This is wonderful. Thanks, [Sebastian]! It’s a weight off my shoulders as I proceeded to learn about other mysteries.

AS7341 Project Future Enhancements

With my AS7341+ESP32 assembly all tidied up, alongside my web app project for interacting with them, I think this is a good point to declare version 1.0 complete and move on to something else. Naturally I have more ideas, but today I’m just going to write them down as ideas for later.

Color Accuracy

The most obvious point of improvement is a better translation from detected wavelengths to human-perceived color. I think this would require at least a few days of study (possibly more) before I can be conversant in the topic and maybe understand that Python code sample I found.

Beyond the theoretical math, there are hardware component to better color: the AS7341 has many additional capabilities that I have not used in my little exploratory app. While the eight sensors for specific wavelengths get the attention, the other sensors weren’t there just for fun. They also have a role in color accuracy as per AS7341 application node on color calibration. Those channels provide information on various distortions that may be affecting those eight wavelength sensors.

The clear channel shows the sensor response without a color filter, and the NIR channel shows raw silicon sensor response without even an infrared filter. When any of these sensors return a strong reading, that means enough of their respective types of light are likely “bleeding” into the other color sensors. Flicker detection is likewise also involved because flickering light patterns would impact sensor readings. All of these factors should need to be compensated before feeding into color space conversion.

Temperature Compensation

I haven’t used AS7341’s temperature compensation feature beyond its default behavior of running once upon powerup. Ambient temperature changes would affect sensor behavior, which is true of all sensors. Or to paraphrase what I’ve heard from veteran embedded engineer Elicia White: “Every sensor is a temperature sensor. Some even sense other things.”

Auto Gain Control

A little tangential to the topic of color accuracy, this sensor seems to have some sort of auto gain control to ensure sensors get a good range of values without going too far into saturation. Ideally, I can add an “Auto Gain” checkbox to my app and let the sensor take care of gain control automatically, but that isn’t as easy as it looked at first glance. This feature was not exposed in the Adafruit library and my effort to explore it with twi_nonblock API produced behavior I didn’t understand.

Web App Evolution

Orthogonal to anything I might do to improve AS7341 performance, I might choose to evolve just the web app itself. This first version was written directly in HTML/CSS/JavaScript, the only library I used was Chart.js to plot the bar graph. This process is fine for a simple app but will get more cumbersome for larger projects. So even though this app is fine for its scale, I might use it as “Hello World” exploration of tools that help manage larger projects. Like learning how to use NPM to manage dependencies like Chart.js. Or using TypeScript to tame some of JavaScript’s wild and annoying sides. Or convert it to use an application framework like Angular. That would be sheer overkill for such a small app, but I have big web app project ideas and I need someplace small to start learning.

ESP32 Evolution

Or I might focus on the ESP32 side of things. Top of the list here is using it as a learning project for ESPAsyncWebServer, which has more of the features I might want over the current basic WebServer implementation. Before that, though, I’ll probably switch over to using PlatformIO so I can upload HTML/CSS/JavaScript files to SPIFFS and serve from there, instead of the current unnecessarily cumbersome process of converting them over to hex values in a header file.

Platform Migration

Or a future path would not involve the web app at all. It’s totally possible for a future project idea to be done entirely onboard the ESP32, porting my browser-side JavaScript code to C on the ESP32. It all depends on what motivates me to create enhancements in the future.

Which may be triggered by something like discovering information I had mistakenly overlooked.

Compact Assembly of AS7341 and ESP32 Boards

Once I implemented a visual warning for AS7341 sensor saturation, I’ve completed a decent baseline set of features. Enough for me to declare version 1.0 is complete. I tidied up the source code with comments and license headers, now I will tidy up the hardware side as well. During development I used what I built at the start of this project, with my ESP32 mini dev board and AS7341 breakout board connected by a pre-crimped JST-SH cable(*) mechanically compatible with Adafruit’s STEMMA QT form factor. I thought the cable would give me flexibility in moving the AS7341 around, but it has just been a huge hassle with it dangling and flopping about.

For experiments in portability, I taped both components to a USB power bank. This worked well enough and taught me I prefer having everything as a single unit. I will rebuild my sensor package into a single compact form.

I unsolder the STEMMA QT-compatible JST-PH cable because I’m going to skip the cable and put these two modules back-to-back. I cut up an expired credit card to place between them as insulation. Since I would only need short sections of wire, I dug up a short cutoff piece of wire that was probably from a resistor.

SCL and SDA pins are connected straight through. Ground is almost as straightforward and handled with a short S-shaped length. Power has to come from the opposite side, though, so I used actual wire with insulation to connect to 3.3V power.

Once in place, I can power the ESP32 with a small adapter sold as an USB OTG adapter(*) but it also works as a zero-length USB cable.

Plugging everything in together, I have an AS7341 sensor mounted to the back of my ESP32 dev board, which is plugged in to a USB power bank serving as a handle. A nice small compact and portable setup, with nothing flopping about.

After a quick test run to verify everything still works correctly, I protected the circuit board assembly with clear heat-shrink tubing. (*) Or at least, it is clear to my eyes. It seems to affect AS7341 sensors readings somewhat, so it is not completely transparent across all wavelengths. In order to remove this interference, I cut a small window to ensure sensor has unobstructed view. This completes version 1.0 of my AS7341 project, but before I move on I wanted to write down ideas of what I might do later.

(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

Sensor Saturation Warning as Final V1 Feature of My AS7341 App

It’s been fun playing with my AS7341 interactive UI web app, seeing what the sensor sees and what the app guesses my eyes would see. They’re similar but rarely agree, as should be expected of a quick hack job. Occasionally I would see something wildly inaccurate, and it would take a few seconds for me to realize the problem: one or more AS7341 channels had hit their saturation (ADCfullscale) values. This “peg the scale” value (a.k.a. “off-scale high”) is no longer accurate. In photography this causes overexposure.

Since the raw server response JSON is printed at the bottom of my app, I could see sensor values that are at their ADCfullscale value. But this means mentally calculating ADCfullscale (which isn’t always 65535) and comparing them against those printed numbers. I could do it, but I wouldn’t know to do it unless I suspect something is wrong with the output. I want notification of sensor saturation whenever it happens, even (or especially!) when the result looks reasonable but are actually wrong.

The JavaScript code to detect saturation on a sensor channel was straightforward and can be performed in a loop over values for sensors F1-F8. Though it’s more likely to happen on Clear or NIR first, so I checked those as well. But then I had to show the result in a way that I would notice.

My first modification was to change the header text for server response JSON. Normally displaying “Sensor data OK” but switch to “Sensor saturation (overexposure) detected” as notification. This is an improvement, but I found I don’t always notice when my eyes are glued to the spectrum chart. To make sure I can’t miss it I changed the chart. Every bar was given a white border and, if saturation is detected, that border turns red. This was an improvement, but I might be focused on one channel and miss the fact another channel had saturated. As I understand it, when one channel is saturated, every reading is unreliable. So if this is a global problem, I’ll make the indicator global. The third modification changes every border to red if any channel had saturated. Now I can’t miss it!

With this sensor saturation notification feature, I think my AS7341 interactive web app is at a good place to wrap up for version 1.0. Before I put my sensor away, though, I wanted to make the hardware more compact and portable.

Code for this project is publicly available on GitHub

Approximate Color from AS7341 Spectral Data

It’s not going to win any design contests, but I’ve updated my AS7341 web app’s stylesheet so at least its layout is no longer confusing and no longer an embarrassing eyesore. It’s the first of several things I wanted to do for polish. My next challenge is to interpret AS7341 spectrum information into to a color hue a human eye would perceive from looking at the same thing. This is not a trivial conversion, as human color perception has been a long-running area of research. After a few minutes of trying to get my bearings on Wikipedia, I’ve reached the conclusion that doing a good job with my own implementation would take more time than I’m willing to spend on it.

What about somebody else’s implementation? A search for spectrum math library in JavaScript led me to a color picker control named Spectrum, which is not helpful to my current project. Looking on NPM, I found a CIE color conversion library, but I don’t see how to make it perform the type of conversion I seek. Casting my net wider than JavaScript, I found this article titled “Converting a spectrum to a colour” that opens with “This article presents a Python script to map a spectrum of wavelengths to a representation of a colour.” This is exactly what I want! Unfortunately, I struggled to understand the Python code, certainly not enough to convert it to JavaScript for my use. Maybe I can come back later to try again, but in the short term I will try a hack.

AS7341 datasheet tells us which wavelengths each sensors F1-F8 are designed to be sensitive to. Looking online, I found Academo’s “Wavelength to Colour Relationship” page that lets me input a wavelength and translate that to RGB value. Taking a table of RGB values for wavelengths corresponding to AS7341 sensors F1-F8, I added up each column: all the red in one value, all the green, then all the blue.


We see the highest sum for red, followed by green a bit behind, and blue is significantly lower. This is consistent with datasheet telling us the silicon underneath each wavelength filter is naturally more sensitive to red. Since white (and shades of gray) is represented by equal portions of red, green, and blue, getting there required boosting the blue-focused colors a bit to even things out. I didn’t put in rigorous mathematics to make them balanced since I don’t even know if this action makes sense in color science. As a quick hack, I used a spreadsheet and fiddled with numbers via trial and error. I found that if I multiplied 415nm by 1.72, 445nm by 1.6, and 480nm by 1.4, I would get red/green/blue within 1% of each other. From here I can multiply them by F1-F8 readings and calculate each of their contribution to red/green/blue channels and generate a color value.

This is an empirically derived formula with no basis in color science, but it does generate a color value that is vaguely in the right general ballpark. I piped that color into Chart.js to be used as my chart background color, following instructions on their documentation. This is most of what I wanted, and maybe the best I can do without investing the work to understand human color perception. Not great, but good enough for a quick hack project so I can move on to the next feature.

Code for this project is publicly available on GitHub

Rudimentary Stylesheet for AS7341 Web App

Writing a simple web app to interact with an AS7341 sensor, I initially focused on functionality. It didn’t much matter how it looked until I had basic parameter input and sensor value graphing output running. But now that I’ve got basic functionality in place, my attention turned to the CSS stylesheet. Up until this point my only item was to make input sliders full width of the window, as that helped me fine tune parameters and considered part of functionality. Now I’ll fill in the rest purely for aesthetics. I don’t need it to look gorgeous, but I did want to make sure it didn’t look embarrassing.

This was my first opportunity to apply what I learned from Codecademy outside of their CSS course exercises, though I did make this project easier for myself with a few design decisions. I didn’t need to drastically change the layout, HTML’s default top-to-bottom arrangement would suit me just fine. I’m only dealing with a single page, so there’s no concern of site navigation controls. And finally, I decided not to worry overly much about creating separate mobile vs. desktop layouts: everybody gets the same thing. No media queries in my stylesheet. I intended to use this web app on my phone, so I want it to look good in portrait mode. On my desktop, I can easily resize my browser window to match the aspect ratio of phone in portrait mode. The minimalist nature of this app meant there were no additional data I could add to a desktop view anyway.

Hit target size was a concern. Parameter sliders were fine, but I was worried about the buttons selecting normalization curve. Fortunately there were no problems in practice. However, I had some trouble with my “repeat read” checkbox being too close to the “Start read” button, and I think I will eventually need to space them further apart.

This was a good start. A few lines of CSS made the page look much more pleasant to my eye. Enough that I can go back and add a few more bits of nice-to-have functionality.

Code for this project is publicly available on GitHub

AS7341 Sees Sunlight Very Differently From LED

Thanks to Chart.js documentation, I was quickly up and running with a simple bar chart to visualize AS7341 data. My first draft was done late at night, so this chart corresponded to the spectral distribution of my room’s ceiling light.

With an advertised color temperature of 2700K, its “warm white” showed a strong response in the yellow and orange areas of the spectrum. The next morning, I had a ray of sunshine coming in a window and I set my AS7341 sensor within it.

The first lesson was that sunlight — even just a tiny beam at an oblique angle — is significantly stronger than my ceiling light. Direct exposure will always reach sensor saturation (ADCfullscale value) no matter what I do. I ended up placing a sheet of printer paper at my sunlight spot and aiming the sensor at that reflected light.

This spectrum has whatever distortion added by a sheet of paper, but it is still very interestingly different from my ceiling light. There is a huge response on NIR sensor, and there isn’t as strong of a peak on orange. My brain sees both of these light sources as white, but the sensor sees very different spectrum between them. Raw sensor data (with clear channel hitting saturation) are as follows:

  "415nm": 5749,
  "445nm": 6342,
  "480nm": 9533,
  "515nm": 10746,
  "555nm": 11245,
  "590nm": 12577,
  "630nm": 12633,
  "680nm": 15217,
  "clear": 65535,
  "nir": 34114,
  "settings": {
    "atime": 30,
    "astep": 3596,
    "gain": 64,
    "led_ma": 0,
    "read_time": 674

According to AMS AS7341 calibration application note, knowing NIR level is important for properly compensating values of spectral sensors F1-F8. They are sensitive to whatever NIR leaked past their filters, so knowing NIR level is important for precise color accuracy. The clear channel and flicker channels likewise have their own impact on color accuracy. But since I’m just goofing around and not concerned with utmost accuracy, I’m choosing to ignore them and dropping NIR from my visualization.

I will, however, make use of this sunlight spectrum to compensate for the differing sensitivities across spectral sensors F1-F8. Using sunlight as my reference for a light source emitting all wavelengths of light, we confirm AMS AS7341 datasheet information that 415nm is the least sensitive and 680nm is the most sensitive. I can selectively boost sensor values so that F1-F8 would all return the same value under direct sunlight. This is crudely analogous to a camera’s color balance (or “white balance”) features, and I implemented the following normalization options in my app:

  • Default normalization curve based on these sunlight values.
  • A direct data option skipping the selective boost.
  • An option to use the next sensor reading as reference. I can point the sensor at something and activate this option to tell my app: “treat this color as white”.

Each of these options had a corresponding button onscreen. Functional, but the jumble of controls on screen is starting to cause usability problems. I built this app and if I get disoriented, how bad would it be for everyone else? It’s time to put some effort into layout with CSS.

Code for this project is publicly available on GitHub

AS7341 ADC Fullscale and LED Illumination Control

Getting interactive control over AS7341 sensor parameters helped me better understand their effect on resulting data. Interactive control over sensor integration time (photography analogy: exposure time) made it easy for me to see how the data reacted mostly linearly until they reach their limit. I had known the AS7341 ADCs were 16-bit, so I thought the limit is always 65535. This is wrong: it is actually 65535 OR ADCfullscale, whichever is lower.

I came across ADCfullscale in the datasheet but I didn’t understand what that information meant at the time. I had mistakenly thought it placed a limit on integration time, as it is calculated from integration time parameters “atime” and “astep” with the formula (atime+1)*(astep+1). Now I know it does not limit integration time but is actually a cap on ADC values if that formula results in less than 65535. For example, right now I’m running with a fixed “astep” of 3596 which corresponded roughly to 10 milliseconds per “atime”. If I configure “atime” to 9, (atime+1)*(astep+1) = (10)*(3597) = 35970 is the sensor saturation limit ADCfullscale. Not 65535.

Another thing I learned was that my original plan for “LED stay on” parameter wouldn’t work. I had designed it to be a parameter sent alongside atime, astep, and gain at the beginning of sensor read. It seemed reasonable enough until I tried to design a control to toggle whether we are going to read the sensor continuously. When will the user toggle that to be “OFF”? Odds are, such toggle would happen while we are in the middle of sensor integration. By that time, it was too late to communicate LED should be turned off.

Oh well, mistakes like this happen. That useless “led_stay_on” parameter was removed, and I added code so “led_ma” could be a valid operation by itself without triggering a sensor read. This lets me adjust the LED illumination (usually turning it off) without performing a sensor read. Just another instance where iterative development is useful, updating my design as I go.

Code for this project is publicly available on GitHub

Notes on AS7341 Integration Time

After pleasantly surprised at the fact my web app project is unlikely to leave my old Android phones behind, I got back to my “learn by doing” process. As originally planned, I’m going to leave my first draft “Basic” UI as-is for fallback/debug purposes. I made a copy to serve as the starting point of my “Standard” UI and initial focus is on sensor integration time.

Full Width Control

First change was minor and cosmetic: “Basic” didn’t use any CSS styles, and “Standard” started with just one style to make input sliders 100% of available width. The default width was annoyingly narrow. Making it full width helps me make finer adjustments.

ASTEP Value Fixed at ~10ms

AS7341 sensor integration time is controlled by two parameters: ATIME and ASTEP. As per datasheet, the resulting integration time follows the formula: (ATIME+1)*(ASTEP+1)*2.78 microseconds. My “Basic” UI exposed both parameters directly, but I want to use something more human-friendly for the “Standard” UI. I decided to keep ASTEP fixed at 3596. Per the formula (3596+1)2.78 microseconds = 9999.66 microseconds, or just a tiny bit less than 10 milliseconds. Keeping ASTEP fixed at 3596 means the ATIME range of 0-255 can dictate integration time anywhere from 10ms to 2560ms a.k.a. 2.56 seconds. This covers the entire time range I want for initial experiments. I may adjust this range in the future after playing with the sensor some more.

Read Time is Double Integration Time

Once I had set up the UI to adjust integration in 10ms increments, I took a few readings at various settings and noticed the actual time spent in readAllChannels() is a lot longer than integration time. If I configure for 1 second (1000ms), I end up waiting two seconds. I added a bit of tracking code to my ESP32 Arduino sketch and verified it wasn’t just my imagination: actual read time is over double integration time.

This was puzzling until I remembered readAllChannels() implementation: it configures AS7341 sensor multiplexor (SMUX) to read sensors F1-F4 plus clear and NIR, performs a read, then configures SMUX for sensors F5-F8 (keeping clear and NIR) and perform a second read. So, a one-second sensor integration time meant one second spent for F1-F4 and another second spent on F4-F8. Add in a few tens of milliseconds for processing and communication overhead, and we’ve explained the observation of a little bit over twice integration time.

This is something to keep in mind depending on sensor application. For example, if I want my browser UI to update once a second, I need to set integration time to be less than half a second.

UX Design Decision

Which led to the next question: Should my browser UI show the integration time, because that’s how I’m configuring the sensor? Or should it show double that time, because that is a better estimate of time I should expect to wait for a reading? I decided to go with double for my “Standard” UI: this is a user experience issue, so I should be faithful to what the user will experience.

Code for this project is publicly available on GitHub

HTML Location Matters for CORS and Mixed Content

I have written a basic browser-based UI to interact with an AS7341 spectral color sensor. I wanted an educational learning project and that’s exactly what I got. My first lesson happened before I could even get anything onscreen! I had my ESP32 translating a formatted HTTP GET request into a call into Arduino AS7341 library readAllChannels() and return the results as JSON. This basic browser-based UI was supposed to query that ESP32 and display results onscreen. In the interest of rapid development, I hosted the browser HTML and JavaScript files on my desktop, and that was my mistake. The HTTP GET action failed with this error message in the browser developer console:

Access to fetch at 'http://esp32-as7341.local/as7341?atime=29&astep=599&gain=8&led_ma=0' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

As a beginner web developer, I had no idea what this error message meant. My journey of enlightenment started by searching for this error message, which led me to this page that unfortunately assumed more knowledge than I had. But this error message did suggest a solution of setting ‘no-cors’ which I tried without understanding what it meant. I no longer got the error message, but I didn’t get any data results, either. I read up on “What is an opaque response?” and confirmed data results were intended to be inaccessible. Well, that’s not going to work for me! So I went to MDN page on CORS. “Cross-Origin Resource Sharing” is something a web server has to explicitly choose to participate in, something my ESP32 Arduino sketch has not done. I decided it’s OK for arbitrary web pages to query my ESP32 and I could declare that with a single additional line added to my Arduino sketch (wildcard for Access-Control-Allow-Origin).

server.sendHeader("Access-Control-Allow-Origin", "*");

Once my ESP32 started sending that in its server headers, I could host browser HTML and JS from my desktop development server and enjoy the rapid code/test/fix/repeat loop of browser JavaScript code. But what about later? It would be a big hassle to require everyone to set up their own web server to play with this code. I thought I had a solution for that: host the browser UI on GitHub pages, but that idea was also a failure. When I tested the idea, I got this error:

Mixed Content: The page at 'https://roger-random.github.io/as7341_webui/' was loaded over HTTPS, but requested an insecure resource 'http://esp32-as7341.local/as7341?atime=29&astep=599&gain=8&led_ma=0'. This request has been blocked; the content must be served over HTTPS.

So much for that! I remember early days of the web when it was common to mix HTTP and HTTPS. My banking site was in HTTPS but used icons and images served over HTTP. This approach meant the server didn’t have to encrypt those images, saving CPU time and its follow-on effects. (Lower electricity consumption, less datacenter cooling, etc.) But it didn’t take long before nefarious geniuses figured out how to cause problems, so mixing HTTP and HTTPS went from commonplace, to triggering a notification, to triggering obnoxious dialogs, and now to complete ban. GitHub Pages would not serve over HTTP, and I’m not going to make everyone jump though the hoops of adding HTTPS to an ESP32 Arduino sketch.

To avoid problems with CORS and with Mixed Content, the best solution is to have my ESP32 Arduino sketch serve the HTML and JavaScript in addition to the AS7341 HTTP API endpoint. This turned out to be more complicated than I thought it would be.

Code for this project is publicly available on GitHub

Basic Browser UI for AS7341

I wrote an ESP32 Arduino sketch that exposes Adafruit AS7341 library readAllChannels() to be accessible via HTTP GET. Now I need to write browser-side code to use it. My first version will be bare bones: plain HTML and as little JavaScript as I can get away with. No consideration will be given for page layout aesthetics, so no CSS will be involved.

Input controls are direct translation of AS7341 parameters: atime, astep, and gain. Output will be JSON returned by my ESP32 sketch, direct with no processing. Making the AS7341 parameters more user-friendly is out of scope for this first version, as is any of the processing and visualization of AS7341 data. I want to do both in the future, but not at first. I wanted to start with this level of direct input/output because I intend to keep this first version around for debugging purposes: when my future fancy version goes awry, I want to be able to bring up this basic version to verify the sensor itself and the network API is still working correctly.

But I deviated from that bare-bones intent pretty quickly, because as soon as I started moving those setting sliders around, I wanted more. I added some JavaScript code to calculate the integration time (in milliseconds) described by atime and astep parameters, and I also added a slider to control milliamps of current to illuminate onboard LED during measurement. (Zero milliamps to turn LED off.) After a few measurements of LED flashing in my face, I added another parameter: whether or not to leave LED illuminated after taking a measurement. A steady-on LED is less annoying than a rapidly blinking one.

Another reason for keeping the user interface bare-bones is to verify all behind-the-scenes infrastructure are working as I expected. They did not! Debugging those failures led me to realize my ignorance with some security-related web development concepts. This is great: I wanted a learning project, and now I’m learning about “CORS” and “Mixed Content”.

Code for this project is publicly available on GitHub

ESP32 WebServer Made AS7341 Accessible via HTTP GET

I’ve decided to build an interactive AS7341 explorer application using web-based technologies, shifting most of the interactive input and visual output to a web browser. But web-based technologies are not able to communicate directly to an AS7341 via I2C, so I still need something to bridge the hardware to the browser. The answer is a small ESP32 Arduino Core sketch using Adafruit’s AS7341 library on one side and a web server library on the other.

In the interest of starting simple, I used the WebServer library included as part of ESP32 Arduino Core. This is a simple implementation of HTTP server that can only handle a single connection at a time, but its limited features also meant simple code. I started with the HelloServer example which does everything I need: parse arguments and send a response. The most informative section is the handler for “HTTP 404 Not Found”, as this is where it prints out all the arguments parsed out of the URI and serves as a handy reference to do the same in my implementation. I wanted to be able to pass in AS7341 parameters “atime” and “astep” to control sensor exposure time, “gain” for sensitivity, and “led_ma” to control brightness of illumination LED. These parameters are passed directly into Adafruit AS7341 API.

My first iteration would turn on LED just for the duration of sensor integration then turn it off. But when I read the sensor continuously in a loop, this would result in an annoying flash between reads. To address this problem, I added a “led_stay_on” parameter to control whether the illumination LED would stay on between reads.

Once sensor integration is complete, I packaged readings for sensors F1-F8, Clear, and NIR into a JSON formatted string and returned it to caller as mime type application/json.

  "415nm" : 7,
  "445nm" : 22,
  "480nm" : 31,
  "515nm" : 65,
  "555nm" : 113,
  "590nm" : 175,
  "630nm" : 243,
  "680nm" : 125,
  "clear" : 408,
  "nir" : 28,
  "settings" : {
    "atime" : 10,
    "astep" : 599,
    "gain" : 64,
    "led_ma" : 0

In hindsight, using an ESP32 was overkill: an ESP8266 would have been perfectly capable of serving as a HTTP to I2C bridge. But I already had this ESP32 ready to go, so I stayed with it.

If I want capabilities beyond what that simple WebServer library could do, in the future I could swap it out for something more powerful like the ESPAsyncWebServer library. It includes a templating feature so I wouldn’t have to do as much direct string manipulation. It also includes the ArduinoJson library for simpler and more robust JSON formatting instead of the string operations I used. And finally, it includes WebSocket capability which would be very useful if I want to migrate the messy ESP-IDF code I wrote for my ESP32 Sawppy controller.

But for today, simple WebServer should be enough to let get me started on browser side code.

Code for this project is publicly available on GitHub

New Project: AS7341 Interactive Web UI

I’ve modified my ESP32 development board to help me better understand the AS7341 spectral color sensor. I’ve removed provision for Mozzi audio output, added heat-shrink tubing to reduce damage from handling, and covered a worryingly bright power LED on Adafruit’s AS7341 board. That takes care of the hardware, but what about the software?

Here are my goals:

  • Allow interactive adjustments to AS7341 parameters. Right now, I have to edit parameters in code, compile the Arduino sketch, and upload to my ESP32 before I can see how changes in parameters affect output. I want to streamline this process.
  • Better visualize AS7341 sensor data. Right now, I just receive a list of numbers. While sufficient for some fun experiments like Emily’s color organ, they are not the most intuitive presentation of vision-based data.
  • Rapid experimentation for sensor normalization. Every light source has a different spectrum, and every individual filter on the AS7341 has a different response curve. How do I compensate for those variations in a “good enough” way? AMS has an Application Note on precisely calibrating AS7341 results, but that requires domain specific expertise such as CIE color spaces. I want to be able to play with ideas and hope to find something that’s 80% as good for 10% of the effort.

For an ESP32, adding interactive adjustments should be easy. I can solder in a few potentiometers, and an ESP32 has plenty of ADC channels to let me pipe that through. I also have a lot of options for ESP32 display. My most recent Adafruit order (which included my AS7341 breakout board) included a small 1.8″ color LCD which would work well. Where this idea might fall apart is my wish for rapid experimentation. It only takes about thirty seconds for me to compile an ESP32 Arduino Core sketch and upload it to my ESP32 board, but that time adds up if I’m trying a lot of small changes in rapid succession. Wouldn’t it be nice if I could iterate as quickly as web development? In that world, I can make a small change and hit F5 to refresh my browser and see immediate results.

Then I realized: hey, I could totally do that! In fact, it would line up with my desire to practice working with web related technologies. Using HTML controls, I could quickly add points of interactivity. There would be no shortage of display options to visualize AS7341 data on screen, and I get that rapid edit/F5/result loop I wish for. Would this be the best way to interactively visualize AS7341 data? Probably not! But it’s a way for me to build my hardware and software skills simultaneously, making it a great learning project for my purposes. I will start by writing a thin stub running on my ESP32 to interact with AS7341, then I can get my feet wet with browser-side development.

Code for this project is publicly available on GitHub

Modifying ESP32 Mini to Focus on AS7341

In order to eliminate Mozzi audio glitches while reading AS7341 spectral color sensor, I was prepared to dive down to learn ESP-IDF I2C API. Fortunately, it turned out Adafruit AS7341 library’s provision for asynchronous read (sensor integration occurs while Arduino code continues running) was good enough to eliminate those popping noises. This brought one project to a rapid conclusion so I can contemplate my next one.

I’ve barely scratched the surface of this AS7341’s capabilities, and I still haven’t quite grasped the meaning of all those numbers that it generated. I don’t have an intuitive grasp of how the set of numbers generated by this sensor relates to how human eyes perceive color. I want to further explore the AS7341 and take it around the house to measure different things, but the contraption I have on hand is quite cumbersome with its STEMMA QT compatible wiring for the AS7341 and a salvaged 3.5mm jack for connecting an audio cable.

To focus on AS7341, I will leave the audio subsystem behind for my next set of experiments. After I unsoldered the 3.5mm jack for audio output, I am much less likely to catch an inconvenient wire and risk damaging my test circuit. I then wrapped the ESP32 mini and wires to the AS7341 sensor inside a bit of clear heat-shrink tube (*) so I am no longer handling bare circuit board. The micro-USB connector would serve as the best metal contact point for grounding purposes, so it was left outside of the shrink-wrapped area as was the reset button that should remain accessible.

(If this is all I wanted to do, and I knew this to begin with, I would have used something like an Adafruit QT Py ESP32 Pico. But I’m sure I will want to do more down the line, and I’m making it up as I go along.)

In addition to a large white LED intended to provide illumination for the color sensor, Adafruit’s AS7341 board also has a bright green LED to indicate power. I’m worried this LED’s green light might distort color sensor results. I like the idea of a power LED indicator, but I would have much rather preferred a dim green glow over this bright beacon. [In contrast, DFRobot’s AS7341 board does not have a bright LED close to the sensor.]

I thought about unsoldering either the LED itself or its adjacent current-limiting resistor, but they are right next to a STEMMA QT connector and I think my hot-air gun would melt and damage its plastic. Then I had a better idea: I put on a small piece of double-sided foam tape. (“Servo tape” from the world of remote-control hobby.) It is an easily reversible modification that blocked majority of green light, good enough to let me contemplate the software side of my next project.

(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

Performing AS7341 Sensor Integration in Parallel Resolved Mozzi Glitches

I’ve set up an ESP32 development board for interfacing with both an AS7341 spectral color sensor as well as Mozzi audio generation libraries. This is a follow-up to my project doing the same with an AVR ATmega328-based Arduino Nano. On those AVR boards, trying to compile Mozzi with Adafruit’s AS7341 library would fail due to conflict between Mozzi’s twi_nonblock I2C library and Arduino’s Wire I2C library.

[...]\libraries\Mozzi-master\twi_nonblock.cpp.o (symbol from plugin): In function `initialize_twi_nonblock()':
(.text+0x0): multiple definition of `__vector_24'
[...]\libraries\Wire\utility\twi.c.o (symbol from plugin):(.text+0x0): first defined here
collect2.exe: error: ld returned 1 exit status

exit status 1

Compilation error: exit status 1

My earlier project solved that problem by avoiding Arduino Wire library, writing AS7341 interface code with twi_nonblock while using Adafruit’s library as a guide. (But unable to copy code straight from it.) However, twi_nonblock is excluded from non-AVR platforms like ESP32. The good news is that we avoid above compiler error, the bad news is that we’re on our own to find a non-blocking I2C API. For an ESP32, that meant dropping down to Espressif’s ESP-IDF I2C API. I believe that’s within my coding capabilities, but there was an easier way.

Why does as7341.readAllChannels() consume so much time that it causes Mozzi audio glitches? Arduino Wire’s blocking I2C operations are certainly a part of that process, but most of that time is spent in AS7341 sensor integration. as7341.readAllChannels() starts an integration and waits for it to complete before returning results, blocking our Arduino sketch the entire time. But Adafruit foresaw this problem and included a provision in their AS7341 library: we can start an integration without blocking on it. Our sketch resumes code execution, allowing Mozzi to update audio while integration occurs in parallel. Once integration is complete, we can retrieve the values and do all the same things we would do for results of as7341.readAllChannels().

This concept was illustrated in the Adafruit example sketch reading_while_looping, which I thought was promising when I reviewed all example sketches earlier. I couldn’t try it on an AVR due to Wire/twi_nonblock compiler conflict, but I could give it a shot on this ESP32. I started with the reading_while_looping example sketch and converted it over to a Mozzi sketch. First by moving existing code in loop() into updateControl(), leaving just a call to audioHook() inside loop(). For my first test I didn’t need anything fancy, so I just had Mozzi play a steady 440Hz sine wave in updateAudio(). (A440 is the note used by western classical music orchestra to verify all instruments are in tune with each other.)

The first run was a disaster! Audio glitches all over the place, but I knew there was room for improvement. There was an entirely unnecessary delay(500), which I deleted. Interleaved with the parallel integration is a blocking integration to our old friend as7341.readAllChannels(). I don’t understand why the blocking code is in the middle of a non-blocking example, but I deleted that, too. This removed most of the problems and left a little recurring audible click. Looking over what’s left, I noticed this sketch made quite a number of calls to Serial.println(). After their removal I no longer heard glitches in Mozzi audio.

I2C communication is still performed with Arduino Wire library. But this experiment empirically showed the communication is fast enough on an ESP32 that Mozzi does not audibly glitch despite Wire’s blocking nature. This is much easier than dropping down to ESP-IDF I2C API. Also, this approach portable to other non-AVR Mozzi platforms like Teensy.

After this successful experiment, I modified one of Emily’s color organ sketches and the resulting pull request shows the changes I had to make. They were quite minimal compared to rewriting everything with twi_nonblock.

Playing with Mozzi was a fun challenge catering to its timing requirements. But as I proceed to play with AS7341, I’d prefer to shed Mozzi timing constraint and focus on other capabilities of this sensor.

Code for this exploration is publicly available on GitHub

JST-SH (STEMMA QT) and 3.5mm (Headphone Audio) Jack for ESP32 Mini

It was an interesting challenge to write code which talked to an AS7341 spectral color sensor using Mozzi’s twi_nonblock API for I2C communication. I referenced Adafruit’s AS7341 library heavily, but I couldn’t copy much (any?) code directly on account of the differences between Arduino Wire I2C and twi_nonblock. But twi_nonblock is only supported for AVR chips, and Mozzi runs on additional architectures such as ESP32. Can I get AS7341 to play nice with Mozzi on those platforms?

For my earlier AVR adventures, I laid out my hardware components on a breadboard. This time, with a bit more confidence, I’m going to wire the components point-to-point without a breadboard. Which means I am free to use my breadboard unfriendly ESP32 Mini board and equip it for integrating Mozzi with AS7341.

For Mozzi audio output on AVR ATmega328 Arduino Nano, I wired an earbud headphone directly to pin D9. I was comfortable doing this as the ATmega328 is a fairly robust chip tolerant of simple direct designs like this. However, the ESP32 is not designed for similar scenarios, and I should take a bit of effort to make sure I don’t kill my chip. Thankfully Mozzi has a guide on how to connect audio with an RC (resistor+capacitor) filter which should be better than nothing to protect the ESP32 pin used for audio output. According to Mozzi documentation, both GPIO25 and GPIO26 are used. I soldered my resistor to GPIO26.

For audio hardware interface, I used a 3.5mm jack salvaged from a cheap digital photo frame I tore down long ago. (Before I started documenting my teardowns on this blog.) This was technically the video output port with four conductors inside the 3.5mm TRRS jack for composite video, audio left, audio right, and ground. But I only need two of the wires: ground plus one audio signal. The other two wires were left unused here.

For AS7341 interface, I dug up my pack of JST-SH connectors (*) originally bought for a BeagleBone Blue but went unused. This is mechanically compatible with Adafruit’s STEMMA QT connectors on their AS7341 breakout board #4698. However, the wire colors in my pack of pre-crimped connectors do not match convention for how they are used in a STEMMA QT. Testing for continuity, I found the following:

  • White = Ground (GND) should be black
  • Yellow = Power (VIN) should be red
  • Black = Data (SDA)
  • Red = Clock (SCL)

I briefly contemplated popping individual pre-crimped wires out of the connector and rearranging them, then I decided this was a quick hack prototype and I didn’t care enough to spend time fiddling with tiny fussy connectors. (This is why I bought them pre-crimped!) Hopefully this decision wouldn’t come back to bite me later. I soldered I2C data wire (black) to GPIO21 and I2C clock wire (red) to GPIO22. Power and ground went to their respective pins on the ESP32 Mini.

This should be enough hardware for me to start investigating the software side.

(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

AS7341 Spectral Color Sensor with Mozzi on AVR Arduino

Looks like I will never learn how to build custom configurations for AS7341 SMUX, but at least I have presets I could copy/paste and that’s good enough for my hobbyist level project. The goal is to convert I2C communication from Arduino’s standard Wire library to the twi_nonblock library bundled as part of Mozzi library. Arduino Wire library would block execution while I2C communication is ongoing, which causes audible glitches with Mozzi sketches. Converting to twi_nonblock avoids that issue.

Using Adafruit’s AS7341 Arduino library and sample sketches as my guide, it was relatively straightforward to converting initialization routines. Partly because most of them were single-register writes, but mostly because non-blocking isn’t a big concern for setup. I could get away with some blocking operations which also verified I2C hardware was working before adding the variable of nonblocking behavior into the mix.

To replicate Adafruit’s readAllChannels() behavior in a nonblocking manner, I split the task into three levels of state machines to track asynchronous actions:

  • Lowest level state machine tracks a single I2C read operation. Write the address, start a read, and copy the results.
  • Mid level state machine pulls data from all 6 AS7341 ADC. This means setting up SMUX to one of two presets then waiting for that to complete. Start sensor integration, then wait for that to complete. And finally copy sensor results. Each read operation is delegated to the lower level state machine.
  • High level state machine runs the mid-level state machine twice, once for each SMUX preset configuration.

Once that high level state machine runs through both SMUX preset configurations, we have data from 10 spectral sensors. The flicker detector is the 11th sensor and not used here. This replicates the results of Adafruit’s readAllChannels() without its blocking behavior. Getting there required an implementation which is significantly different. (I don’t think I copied any Adafruit code.)

While putting this project together, my test just printed all ten sensor values out to serial console. In order to make things a little more interesting, I brought this back full circle and emulated the behavior of Emily Velasco’s color organ experiment. Or at least, an earlier version of it that played a single note depending on which sensor had returned highest count. (Emily has since experimented with vibrato and chords.) I added a lot of comments my version of the experiment hopefully making it suitable as a learning example. As a “Thank You” to Emily for introducing me to the AS7341, I packaged this project into a pull request on her color organ repository, which she accepted and merged.

This is not the end of the story. twi_nonblock supports only AVR-based Arduino boards like the ATmega328P-based Arduino Nano I used for this project. What about processors at the heart of other Arduino-compatible boards like the ESP32? They can’t use twi_nonblock and would have to do something else for glitch-free Mozzi audio. That’ll be a separate challenge to tackle.

Code for this project is publicly available on GitHub.

Sample Code Gave Incomplete Picture of AS7341 SMUX Configuration

Reading through Adafruit AS7341 Arduino library implementation of readAllChannels(), I was happy to see it mostly confirmed information I understood from datasheet reading. It also had something else: how to configure AS7341 sensor multiplexor (SMUX). Details of which were critical information completely missing from the datasheet, mere a note that AMS provides sample code. I say sample code is a poor substitute for proper documentation and my position remains unchanged.

I thought I might have a good shot at figuring out SMUX configuration registers, given that I have found three sample points. DFRobot has an Arduino AS7341 library. Adafruit has their own implementation of an Arduino AS7341 library. And hidden in the examples folder of Adafruit’s library is an Arduino sketch that doesn’t use Adafruit’s library at all, written by an AMS application support engineer. Comparing and contrasting between them should tell me a lot!

Unfortunately, as soon as I took a closer look my expectation went down in flames. These three examples are writing the exact same bytes to the exact same registers. There’s nothing to compare and contrast at all. This feels like someone at AMS wrote a SMUX configuration at one point and it’s just been copied and pasted ever since. All we really have to work with are the skimpy comments in these configuration routines.

Based on these comments, each register corresponds to a single sensor, except when it controls two sensors. Bits in the register control which ADC is connected. It was easy to infer that zero representing no connection to any ADC at all, but other than that the bits are inconsistent. Setting register 0x0F to 0x30 supposedly connected to ADC2, but for register 0x0A it was 0x03 that connected to ADC2. Some of the sensors have a “left” and “right” but it’s not clear what that means. My best guess is from the datasheet figure showing sensor array layout:

Maybe “F3 left set to ADC2” means connecting only the left side F3 sensor, leaving the right side F3 sensor unused? That would mean we’re using half the available sensor area to gather light, which seems sub-optimal.

And most critically, the comments weren’t enough for me to figure out how I’d create a different SMUX configuration. There’s nothing about any of these in the datasheet, which remains my biggest complaint to AMS. The only description I found in the datasheet is that flicker detection sensor must be connected to ADC5.

One odd thing I noticed about this copied-and-pasted code snippet is the fact it sends twenty bytes of SMUX configuration as twenty write operations of single bytes to sequential registers 0x00 through 0x13 inclusive. Why do it in such an efficient way? For my project I converted that to a faster and more efficient single write operation of twenty bytes and it seems to work.

The good news is that these sample code snippets help us configure SMUX to do something useful in line with functionality demonstrated by these samples. The bad news is that we don’t have enough information to design and build our own SMUX configurations to suit specific needs. I guess if someone wants to use AS7341 professionally they can ask AMS application support team to build a custom SMUX configuration for them. For an electronics hobbyist like myself, these fixed configurations are sufficient for me to complete my project, but I am still grumpy with AMS about it.

Code for this project is publicly available on GitHub.

Unrolling Adafruit AS7341 readAllChannels()

After successfully reading an AS7341’s product ID via a non-blocking I2C API, I have gained confidence I can replicate any existing AS7341 library’s actions with the non-blocking I2C API. This way it will play nice with Mozzi library. I decided to focus on Adafruit_AS7341::readAllChanels() because it seemed like the most useful one to convert to non-blocking operation. It was used in multiple examples as well as Emily’s color organ project.

bool Adafruit_AS7341::readAllChannels(uint16_t *readings_buffer) {

  setSMUXLowChannels(true);        // Configure SMUX to read low channels
  enableSpectralMeasurement(true); // Start integration
  delayForData(0);                 // I'll wait for you for all time

  Adafruit_BusIO_Register channel_data_reg =
      Adafruit_BusIO_Register(i2c_dev, AS7341_CH0_DATA_L, 2);

  bool low_success = channel_data_reg.read((uint8_t *)readings_buffer, 12);

  setSMUXLowChannels(false);       // Configure SMUX to read high channels
  enableSpectralMeasurement(true); // Start integration
  delayForData(0);                 // I'll wait for you for all time

  return low_success &&
         channel_data_reg.read((uint8_t *)&readings_buffer[6], 12);

Examining this code, we can see it does the same thing twice, differing only in a single boolean parameter to setSMUXLowChannels. This reflects AS7341 architecture where we have more sensors than ADCs so we have to configure SMUX to read a subset then reconfigure SMUX to read remaining sensors.

void Adafruit_AS7341::setSMUXLowChannels(bool f1_f4) {
  if (f1_f4) {
  } else {

Digging into setSMUXLowChannels we see the following actions:

  1. enableSpectralMeasurement disables a register bit to turn off spectral measurement, necessary preparation for SMUX reconfiguration.
  2. setSMUXCommand flips a different register bit to notify AS7341 that a new SMUX configuration is about to be uploaded.
  3. Upload one of two SMUX configurations, depending on boolean parameter.
  4. enableSMUX repeatedly reads a register bit until it flips to 0, which is how AS7341 signifies that SMUX reconfiguration is complete.

Steps 1-3 above are I2C writes and can be done quickly. Step 4 will add complication: not only is it an I2C read, but we might also need to read it several times before the bit flips to zero.

Backing out to readAllChannels, we see spectral measurement bit is flipped back on after SMUX reconfiguration. According to the datasheet, flipping this bit back on is a signal to start a new reading. How do we know when sensor integration is complete? That’s indicated by yet another register bit. delayForData repeatedly reads that until it flips to 1, clearing the way for us to read 12 bytes of sensor data. Representing data from all six ADCs channels, each of which gives us 16 bits (2 bytes) of data.

Unrolling all of the above code in terms of I2C operations, readAllChannels breaks down to:

  1. I2C register write to turn OFF spectral measurement.
  2. I2C register write to notify incoming SMUX configuration data.
  3. I2C write to upload SMUX configuration for ADC to read sensors F1 through F4, plus Clear and NIR channels.
  4. Repeated I2C read for “SMUX reconfiguration” flag until that bit flips to 0.
  5. I2C register write to turn ON spectral measurement. (Starts new sensor integration.)
  6. Repeated I2C read for “Sensor integration complete” flag until that bit flips to 1.
  7. I2C read to download sensor data
  8. Repeat 1-7, except step #3 uploads SMUX configuration for sensors F5 through F8 instead of F1 through F4.

I like this plan but before I roll up my sleeves, I wanted to take a closer look at SMUX configuration.

Code for this project is publicly available on GitHub.