Introduction: A Raspberry Pi Colorimeter With E-Paper Display
I had been starting to work on this idea in 2018, being an extension of a previous project, a colorimeter. My intension was to use a e-paper display, so the colorimeter could be used as a stand-alone solution without the requirements for an external monitor, e.g. for class room or field applications.
I had some time to play on the project over Christmas vacations 2018/2019, but, while even a draft of the instructable had already been written, a few things I intended to do were still missing. Then I had to concentrate again on the job, had to finish my projects there and started in a new position in April. So I had not much time for silly projects for a while, and finally the project below became one of several ideas and concepts hibernating in my small "Bastelecke" ("tinker corner"?), being untouched since January 2019.
If it would not be for the "Finish it already" contest, this instructable might be still unpublished for years.
So as Pentecost 2020 is nearing now, I decided to make a just few changes to the draft instructable's text and layout, and publish it.
And maybe I will find the time to build a housing for the device and perform these enzyme kinetics measurements I wanted to present someday. Or you will do that before me.
Happy Tinkering
H
----------------------------------------------------------------------------------------
In this instructable I would like to describe a small, inexpensive and mobile six channel photometer composed of a Raspberry Pi Zero with an Inky pHAT e-ink display, an AS7262 six color sensor breakout, a cuvette holder and some push buttons, LEDs and cables.
To assemble the device does not require much specialized skills or tools above the soldering of header strips. The device might be of interest for educational, hobby or citizen science applications and could be a nice STEM project.
In the configuration described here, instructions and measurement results are displayed on the e-ink display and on an optional computer display. The results of the measurement are also stored in CSV-files on the SD card of the RasPi, allowing a subsequent data analysis.
Instead of the Inky pHAT you could use other displays as well. But the e-ink display has a number of benefits, including very low power consumption and very good legibility even in bright daylight, allowing to build devices for in-field applications that can run for hours being powered by a power pack or batteries.
I am using the AS7262 six channel color sensor. This sensor measures the intensity of light at relatively narrow ranges (~40 nm) throughout the visible spectrum, covering violet (450 nm), blue (500 nm), green (550 nm), yellow (570 nm), orange (600 nm) and red (650 nm). This allows much more precise measurements compared to RGB-sensors as the TCS34725. A minor limitation is that a few areas of the visible spectrum, e.g. cyan, are not well covered. But as most dyes will have a wide absorption spectrum, this issue should not be too relevant for most applications.
The program is written in Python3 and uses the Adafruit Blinka and AS7262 libraries as well as the Pimoroni Inky pHAT and the GPIOzero libraries. It therefore should be easy to modify and optimize the script for your special application.
As several parts and concepts already have been described in previous instructables, I like to refer to these for some details or layout options.
Supplies
Please see "Materials" step, as the original draft of this instructable had been written a while ago.
Step 1: Theory and Background
A photometer basically measures the amount of light that reaches the sensor. If a beam of light passes through a colored solution or a color filter, a certain fraction of the light gets absorbed or scattered, resulting in a loss of signal compared to a blank reference. Dyes do absorb light of a given wavelength, whereas particles that make a solution turbid will scatter the light out of the light path, both resulting in a loss of signal. But while the absorption of light by a dye is wavelength-dependent, scatter usually is not, or at least to a much lesser degree.
By using a multi-channel photometer you can measure the color composition of a solution, filter or surface. In analytical chemistry color reactions often are used to detect specific substances, e.g. metal ions, or the acidity/pH of a solution. In some cases the detection reactions will result in colored substances, while many indicator dyes are changing their color from one to another, e.g.l depending on the pH. In the first case the absolute color intensity must be measured, in the second the measurement of the ratio of intensities of two or more colors will allow a very precise pH determination.
While absorption defines the fraction of light that gets "catched" by the dye, we are actually measuring the amount of light detected by the sensor. The amount of light absorbed depends on the absorbance of the dye at a given wavelength, which is a molecular constant, the concentration of the dye, and the path length. The Beer-Lambert law allows to calculate the concentration of a dye, or particles, in a solution.
It is a non-linear function: -log(I/Io) = e*c*d.
I and Io are the measured and the reference (blank) intensities. Given the molar extinction coefficients e and the path length d are constant, the concentration c can be calculated from I/Io. Therefore a logarithmic serial dilution of a dye (e.g. 1:2, 1:4, 1:8, …) shall give a linear I/Io curve, which usually is true in the range of I/Io between 20 and 80%. Have a look on the experimental part a later step.
More on the theory behind spectrophotometry and its applications can be found at the Wikipedia articles on this subject, and one of my previous instructables.
For chemical analyses, in most cases a set of defined standard solutions of the component will be used to calibrate a photometric device. This allows to perform quantification by direct comparison and improves precision, as photometric curves usually follow a sigmoid shape, i.e. are flat at very low and high concentrations and steep in the middle.
Standard photometric cuvettes have a defined thickness/path length of 10 mm, allowing very reproducible measurements. For most measurements in the visible spectrum, cheap plastic single use cuvettes should be sufficient.
For demonstration and evaluation purposes I am using color filter strips, as these allow to perform measurements very easily and reproducibly. I would recommend to use such as reference materials and for introduction lectures. Aquarium shops are a good source for reagents for wet experiments, as they are offering a wide spectrum of indicator kits.
Step 2: Materials
- Raspberry Pi Zero.
Or any other Raspberry version, with Raspian (Jessie or later) installed.
- Pimoroni Inky pHAT, black version.
The red or yellow versions should work as well, but they need much (!) longer to update the display.
- A Pimoroni Pico HAT Hacker Shim,
with female headers attached (normal and long versions)
- AS7262 breakout.
I here used the SparkFun version. The breakout from Adafruit should work as well, but will require changes in the layout of the cuvette holder.
- Cuvette and breakout holder.
I here used a version printed in polyamide I described in an earlier instructable. There you may also find a simpler layout made of Forex plates. Attached you find the layout file.
- Three 20 mm M3 nylon screws and two 40 mm M3 brass screws, M3 nuts.
- Two push buttons, two naturally white LEDs (5 mm, 3 V, narrow emission angle, e.g. Nichia NSPW500DS), a breadboard and some jumper cables.
- An USB power pack or power adapter to power the RasPi.
- Standard plastic photometric cuvettes, available e.g. via Amazon.
- Disposable 1 ml or 2 ml pipettes.
- Food colorants, inks, pH indicator dyes, detection reagents, … as required.
Estimation of costs:
Pi Zero W, incl. SD card: about €25
AS7262 breakout: about €25
Cuvette/Sensor holder: about €20 (incl. shipping)
Inky pHAT: €23 Euro
LEDs, buttons, cables, screws: about 10 Euro
Pico Hacker Shim: about €3
Headers: about €5
Estimated total cost: about €120
Attachments
Step 3: Assembly
Build the cuvette and breakout holder. I had my version printed in polyamide by a service provider for 16 Euro plus shipping. Alternatively, you could build a cheaper one using Forex, balsa or plywood, as described in a previous instructable.
Solder a six-pin header (or cables) to the back (!) of AS7262 breakout. Cut the heads of the Nylon screws and glue them to the LED side of the housing. Fix the breakout to the cuvette holder using M3 brass screws, nuts and plastic washers and check that the hole on the sensor chip is placed correctly in the light path. Attach the 5 mm LED and fix its position with backplate and nuts, looking for its alignment to the light path.
Solder the headers to the Pico Hacker shim, as described in another instructable. There are two versions, that have different space requirements. Take care to use sufficient but not too much solder and not to contaminate the long pins with solder.
Get your RasPi up and running. Install the Adafruit Blinka and AS7262 CircuitPython libraries and the Pimoroni Inky pHAT and the GPIOzero libraries, as described on the corresponding websites. Copy the scripts attached to this instructable to the RasPi.
Combine RasPi, the header holder and the Inky pHAT and check that everything is working well.
Place the LED and push buttons on the breadboard and connect them with the header shim on the Pi using male jumper cables. The LED is connected to GPIO 17, the push buttons to GPIO 23 and 24.
Connect the Vin, GND, SCL and SDA port of the AS7262 breakout with the 3V (!), GND, SCL and SDA ports of the Pi. Take care, as 5V may damage the SparkFun AS7262 breakout. Preferentially connect them via the breadboard, as you may need a second 3V port to power the LED at the cuvette holder. Connect it and keep in mind that the longer foot is Vin. Check all connections.
Turn on the RasPi, check that Inky pHAT and AS7262 are working.
Open the provided program in Python 3 and run it.
Step 4: Using the Device
- Check all connections, power the RasPi. Start the script.
- As requested, place a reference cuvette into the cuvette holder, press button B. Wait for the Inky pHAT to confirm.
- Place a cuvette with the sample to measure into the cuvette holder, press the M button. Wait for the results to be displayed on the Inky pHAT. Perform further measurements.
- In case, reset the blanks values with a reference cuvette by pressing button B.
- To end the script, press both buttons at the same time.
- Read and analyze the values from the CSV-file(s). They are named according to the timestamp at the time of creation. Values contain the timestamp at measurement, the [v/b/g/y/o/r]-values, absolute values for blanks and % transmittance for measured values, and an indicator (“B”/”M”) for blanks or measurements.
- To optimize measurement conditions, you may change the gain and integration time parameters.
Step 5: Example: Color Filters
Above you find an image of a set of color filters I measured using the device, and a bar graph with the corresponding results. The results are fitting quite well to the expectations.
The color filters are about 8 mm wide strips cut from a set of Rosco filters I had bought as a sample pack. The filters were placed into the cuvette.
Step 6: Example: Three Food Colorants and a Dilution Curve
For class room experiments you may use food colorants or inks. These are easy to get in a wide variety if colors, cheap and non-toxic.
Here I used three food colorants I bought at the supermarket and measured dilutions of them.
- "Green" is cooper chlorophyllin, E141, a modification of the green color of plants.
- "Blue" is indigo carmine, E132. It is also a pH and redox indicator.
- "Red" is a plant extract containing anthocyanins, E162x.
Their color is also pH dependent, red in acid, violet-blue in basic solutions. It is the type of color found in red cabbage and red wine.
I also prepared a dilution curve of the green colorant.
Here at first I identified the dilution where about 95% of light gets absorbed, in this case a 1:5. Then, using this as starting point, subsequent 1:3 dilutions (2+1 volumes) were prepared and analysed. As you can see, the resulting curves are sigmoid, meaning S-shaped, and nearly linear for I/Io values between 10 and 85%. As we have a logarithmic dilution, this can be expected from the Beer-Lambert law. Values outside of this range are much more sensitive to noise or measurement errors.
The results were astonishingly good for such a simple device. They also shows you a mayor limitation of colorimetric assays, as here just dilutions between 15- and 450-fold are in the linear range, allowing a precise quantification. Fluorometric or luminometric assay have a much wider linear range and are therefore preferred in many clinical analysis applications.
Step 7: The Script
Here your find the script. It is written in Python 3 and requires a number of external libraries.
Some remarks on the script:
In the presented version the script uses calibrated, i.e. corrected, values, not the raw data coming from the sensor. To my understanding the differences are due to a color compensation process that corrects the reported values. Especially for the yellow channel, the detected range of the spectrum overlaps with the ones of the green and orange channels. The correction omits that a very strong green or orange signal will result in an artificially enhanced yellow signal. This color compensation will be helpful in most cases, but may lead to artefacts under very specific circumstances. You may also read the raw data from the breakout.
The example code
'''A script for the AS7262 color sensor using Adafruit Blinka and the Pimoroni Inky pHAT Based on the AS726X/Circuit Python and Inky pHAT example codes by Adafruit ans Pimoroni Version: Dec 31 2018 HF Requires Adafuit Blinka, Circuit Python AS726X, and the Inky pHAT libraries, and GPIOzero. ''' # libraries # General import time import datetime # Blinka import board import digitalio import busio # AS7262 from adafruit_as726x import Adafruit_AS726x #GPIOzero from gpiozero import Button, LED from signal import pause # for Inky pHAT import sys from PIL import ImageFont import inkyphat # CSV function import csv # definitions b_button = Button (24) # Momentary button at GPIO 24 m_button = Button (23) # Momentary button at GPIO 23 led = LED(16) #LED at GPIO 16. Do not use GPIO 4 or 17. inkyphat.set_colour('black') # for b/w inky phat #inkyphat.set_colour('red') # for red inky phat inkyphat.set_rotation(180) # turn display 180° font1 = ImageFont.truetype(inkyphat.fonts.FredokaOne, 16) # Select standard font header font2 = ImageFont.truetype(inkyphat.fonts.FredokaOne, 14) # Select standard font data # define language "EN", "DE", ... lang = "EN" #Initialize # Try digital") input pin= digitalio.DigitalInOut(board.D4) print ("Digital IO OK") #Try I2C i2c = busio.I2C(board.SCL, board. SDA) print ("I2C ok") sensor = Adafruit_AS726x(i2c) print () #AS7262 settings sensor.conversion_mode = sensor.MODE_2 # mode_2: contious reading of all 6 channels sensor.gain = 1 # 1, 3.7, 16, 64; default 64 adjust if required sensor.integration_time = 360 # between 2.8 and 714 ms; default: 140 ''' # blank values (manual setting, required in basic version) v0 = 18650 b0 = 20490 g0 = 28980 y0 = 47420 o0 = 35000 r0 = 28760 ''' CSV_name = 'Meas_AS7262-2.csv' #functions def get_timestamp(): #get timestamp ts0_EN ='{:%Y-%m-%d}'.format(datetime.datetime.now()) # timestamp - date EN ts0_DE ='{:%d.%m.%Y}'.format(datetime.datetime.now()) # timestamp - date DE/german ts1='{:%H:%M:%S}'.format(datetime.datetime.now()) # timestamp - time if (lang =="DE"): ts0 = ts0_DE else: ts0 = ts0_EN ts = ts0 + " " + ts1 return ts def init_csv(CSV_Name): with open (CSV_Name,'w', newline='') as csvfile: fieldnames = ['timestamp', 'v','b','g','y','o','r', 'type'] csv_writer = csv.DictWriter(csvfile,fieldnames=fieldnames) csv_writer.writeheader() #csv_writer.close() def write_to_csv(CSV_Name, t_stamp, values, typ): #timestamp, vales (as tuple) and type (B or M) with open (CSV_Name,'a', newline='') as csvfile: fieldnames = ['timestamp', 'v','b','g','y','o','r', 'type'] csv_writer = csv.DictWriter(csvfile,fieldnames=fieldnames) # extract value tupels v_c = ("{0:0.1f}".format(values[0]) ) b_c = ("{0:0.1f}".format(values[1]) ) g_c = ("{0:0.1f}".format(values[2]) ) y_c = ("{0:0.1f}".format(values[3]) ) o_c = ("{0:0.1f}".format(values[4]) ) r_c = ("{0:0.1f}".format(values[5]) ) csv_writer.writerow({'timestamp': t_stamp,'v':v_c, 'b':b_c, 'g':g_c, 'y':y_c, 'o':o_c, 'r':r_c,'type':typ})</p><p>def blank(): # read blank values while not sensor.data_ready: time.sleep(.1) #read calibrated values from sensor violet = sensor.violet blue = sensor.blue green = sensor.green yellow = sensor.yellow orange = sensor.orange red = sensor.red b_val =(violet, blue, green, yellow, orange, red) # blanks as tuple led.on() #reading sensor indicator time.sleep (1) led.off() # get timestamp ts = get_timestamp() # print timestamp to display print (ts) print () # write to csv write_to_csv(CSV_name, ts, b_val, 'B') return b_val def measure(): #perform measurement while not sensor.data_ready: time.sleep(.1) #read calibrated values from sensor violet = sensor.violet blue = sensor.blue green = sensor.green yellow = sensor.yellow orange = sensor.orange red = sensor.red led.on() # sensor read indicator time.sleep (1) led.off() # pause() # reads channel-compensated values, not the raw values print ("calibrated values") print ("V: " + str(int(violet))) print ("B: " + str(int(blue))) print ("G: " + str(int(green))) print ("Y: " + str(int(yellow))) print ("O: " + str(int(orange))) print ("R: " + str(int(red))) print (" \n") # calculate transluminescence in % t_v = 100*violet/v0 t_b = 100*blue/b0 t_g = 100*green/g0 t_y = 100*yellow/y0 t_o = 100*orange/o0 t_r = 100*red/r0 t_val = (t_v, t_b, t_g, t_y, t_o, t_r) # print transluminescence values to screen print ("T_v: " + str(round(t_v)) + " %") print ("T_b: " + str(round(t_b)) + " %") print ("T_g: " + str(round(t_g)) + " %") print ("T_y: " + str(round(t_y)) + " %") print ("T_o: " + str(round(t_o)) + " %") print ("T_r: " + str(round(t_r)) + " %") print (" \n") # bargraph transluminescence values, 50x "=" equals 100% print ("T_V: " + int(t_v/2)*'=') print ("T_B: " + int(t_b/2)*'=') print ("T_G: " + int(t_g/2)*'=') print ("T_Y: " + int(t_y/2)*'=') print ("T_O: " + int(t_o/2)*'=') print ("T_R: " + int(t_r/2)*'=') print (" |" + 10*'____|') print ("\n") # get timestamp ts = get_timestamp() # print timestamp to display print (ts) print () # write to csv write_to_csv(CSV_name, ts, t_val, 'M') # print values to Inky pHAT # set tabs (simplifies optimization of layout) t1 = 5 # tab 1, frist column t2 = 30 # tab 2, second column t3 = 100 # tab 3, 3rd column t4 = 160 # tab 4, not used here # write timestamp inkyphat.clear() inkyphat.text((t1, 0), ts, inkyphat.BLACK, font2) # write timestamp #inkyphat.text((t3, 0), ts1, inkyphat.BLACK, font2) # write timestamp time inkyphat.line((t1, 16, 207,16), 1,3) # draw a horizontal line inkyphat.line((t3-15,16, t3-15,111), 1,3) # draw a vertical line # write transluminescence values inkyphat.text((t1, 18), "V: ", inkyphat.BLACK, font2) inkyphat.text((t2, 18), ("{0:0.1f}".format(t_v) + "%"), inkyphat.BLACK, font2) inkyphat.text((t1, 32), "B: ", inkyphat.BLACK, font2) inkyphat.text((t2, 32), ("{0:0.1f}".format(t_b) + "%"), inkyphat.BLACK, font2) inkyphat.text((t1, 46), "G: ", inkyphat.BLACK, font2) inkyphat.text((t2, 46), ("{0:0.1f}".format(t_g) + "%"), inkyphat.BLACK, font2) inkyphat.text((t1, 60), "Y: ", inkyphat.BLACK, font2) inkyphat.text((t2, 60), ("{0:0.1f}".format(t_y) + "%"), inkyphat.BLACK, font2) inkyphat.text((t1, 74), "O: ", inkyphat.BLACK, font2) inkyphat.text((t2, 74), ("{0:0.1f}".format(t_o) + "%"), inkyphat.BLACK, font2) inkyphat.text((t1, 88), "R: ", inkyphat.BLACK, font2) inkyphat.text((t2, 88), ("{0:0.1f}".format(t_r) + "%"), inkyphat.BLACK, font2) # draw bargraph inkyphat.rectangle((t3, 21,(t3 + 100), 21+9),fill=inkyphat.WHITE, outline=inkyphat.BLACK) inkyphat.rectangle((t3, 35,(t3 + 100), 35+9),fill=inkyphat.WHITE, outline=inkyphat.BLACK) inkyphat.rectangle((t3, 49,(t3 + 100), 49+9),fill=inkyphat.WHITE, outline=inkyphat.BLACK) inkyphat.rectangle((t3, 63,(t3 + 100), 63+9),fill=inkyphat.WHITE, outline=inkyphat.BLACK) inkyphat.rectangle((t3, 77,(t3 + 100), 77+9),fill=inkyphat.WHITE, outline=inkyphat.BLACK) inkyphat.rectangle((t3, 91,(t3 + 100), 91+9),fill=inkyphat.WHITE, outline=inkyphat.BLACK) inkyphat.rectangle((t3, 21,(t3 + int(t_v)), 21+9),fill=inkyphat.BLACK, outline=inkyphat.BLACK) inkyphat.rectangle((t3, 35,(t3 + int(t_b)), 35+9),fill=inkyphat.BLACK, outline=inkyphat.BLACK) inkyphat.rectangle((t3, 49,(t3 + int(t_g)), 49+9),fill=inkyphat.BLACK, outline=inkyphat.BLACK) inkyphat.rectangle((t3, 63,(t3 + int(t_y)), 63+9),fill=inkyphat.BLACK, outline=inkyphat.BLACK) inkyphat.rectangle((t3, 77,(t3 + int(t_o)), 77+9),fill=inkyphat.BLACK, outline=inkyphat.BLACK) inkyphat.rectangle((t3, 91,(t3 + int(t_r)), 91+9),fill=inkyphat.BLACK, outline=inkyphat.BLACK) # write to Inky pHAT inkyphat.show() #main part: ts_csv = get_timestamp() CSV_name = ("AS7262-"+ ts_csv +".csv") init_csv(CSV_name) # Ouput to display print ("Hello, this is a script for the AS7262 color sensor") print ("using Adafruit Blinka and the Pimoroni Inky pHAT") print () print ("Press button 'B' to perform a blank measurement") print ("Press button 'M' to perform a measurement") print () print ("Please start with a blank measurement ") print () print ("Please now place the reference (blank) cuvette into photometer,") print ("then press (white) B-button") # b_button.wait_for_press() # output on Inky pHAT inkyphat.clear() inkyphat.text((5, 5), "AS7262/Inky pHAT", inkyphat.BLACK, font1) inkyphat.text((5, 20), "6 channel photometer", inkyphat.BLACK, font1) inkyphat.text((5, 40), "First perform a blanks", inkyphat.BLACK, font1) inkyphat.text((5, 55), "measurement. Insert", inkyphat.BLACK, font1) inkyphat.text((5, 70), "reference cuvette ", inkyphat.BLACK, font1) inkyphat.text((5, 85), "and press button B", inkyphat.BLACK, font1) inkyphat.show() b_button.wait_for_press() #read blank values at start b_val = blank() v0 = int(b_val [0]) b0 = int(b_val [1]) g0 = int(b_val [2]) y0 = int(b_val [3]) o0 = int(b_val [4]) r0 = int(b_val [5]) # write_to_csv('Start', b_val, 'B') #output to display print ("Blanks measured") print print ("You may now perform your measurements") print () print (" Press button 'M' for measurement") print (" or button 'B' to reset blanks,") print (" or both to stop the script") print () print (" You may have to press buttons for up to two seconds!") #output instructions to Inky pHAT inkyphat.clear() inkyphat.text((5, 5), "Starting blanks taken.", inkyphat.BLACK, font1) inkyphat.text((5, 25), "For sample measurements", inkyphat.BLACK, font1) inkyphat.text((5, 45), "press button M, to reset", inkyphat.BLACK, font1) inkyphat.text((5, 65), "blanks press button B or", inkyphat.BLACK, font1) inkyphat.text((5, 85), "both buttons to end.", inkyphat.BLACK, font1) inkyphat.show() # measurement loop while True: if (m_button.is_pressed and b_button.is_pressed): # exit loop and end script led.on() time.sleep (.5) led.off() break elif m_button.is_pressed: # evoke measurement sample measure () print ("Measurement performed") print ("Press M or B or both") print () elif b_button.is_pressed: # measure and reset blank values blank () #read blank values to reset b_val = blank() v0 = int(b_val [0]) b0 = int(b_val [1]) g0 = int(b_val [2]) y0 = int(b_val [3]) o0 = int(b_val [4]) r0 = int(b_val [5]) ts=get_timestamp() print ("Blanks have been redefined at " + ts) print ("Press buttons M or B or both") inkyphat.clear() inkyphat.text((5, 5), "Blanks redefined at", inkyphat.BLACK, font1) inkyphat.text((5, 25), ts, inkyphat.BLACK, font1) inkyphat.text((5, 70), "To measure samples,", inkyphat.BLACK, font1) inkyphat.text((5, 85), "press button M. ", inkyphat.BLACK, font1) inkyphat.show() else: time.sleep(2) inkyphat.clear() # empty Inky pHAT display procedure, inkyphat.show() print ("bye!")
Attachments
Step 8: Limitations and Outlook
The current version is more a working prototype then a complete solution. One of the mayor limitations is that all parts are not located in a compact housing. A project for the future, I won’t have the time now.
The LEDs seem to deteriorate over time, so it is recommended to perform blank corrections from time to time and to replace the LED if required. One option to improve LED lifetime would be to place a switch in the LEDs power line or to toggle its state by an additional push button. for a more elaborate version, a secondary light sensor, as a TSL2561, may be used to control the amount of light emitted by the LED and to allow internal correction.
White LEDs come with a basic limitation. Their spectrum is not homogenous over the whole spectrum but have a strong violet/blue component and a broad peak from blue to red, with a weak signal in the cyan and red areas. As the 6-channel sensor just is measuring thin slices of the spectrum, and has a gap at cyan, this is not a critical issue in our case. Nevertheless, even with a natural white LED, the emission of blue and red is less then 30% compared to violet and yellow.
I have been experimenting with small 3.6 V Xenon lamps, as these should a more continuous emission throughout the visible spectrum. But these require much power and get very hot. They might just be an option if just powered for a short time ( -> Xenon flash) and placed in a heat dissipation frame. In addition, the brightness of the Xenon lamp was much lower then that of the LED.
For a housed version some additional status LEDs could be handy, as does an on/off-switch.
The program does have much room for optimization, any help and suggestions are welcome.
To use the device as a stand alone solution you may run the script on startup. There a several ways to do this, you may find detailed information on this elsewhere.