Structure ESP8266
This article is also available as PDF document available.
Can the lamp that we controlled in the previous episodes with an ESP32 and touchpads also run with an ESP8266 or even a Raspberry Pi Pico? If you put in extra effort for the touch service - yes! In addition, the module needs to be adapted in each case touch.py. The operating program can essentially be taken over from the ESP32. But small changes have to be made there too. Why is that?
The ESP8266 has an analog-to-digital converter (ADC) but only one with one channel and we need three inputs for the three touch sensors. The controller does not have touch inputs. We therefore have to switch to pressure sensors. These supply an analog signal, i.e. a voltage of up to 3.3 volts.

Figure 1: Pressure sensors as a touchpad replacement
On the ESP8266 we use an AD converter of type ADS1115 With its four channels, it can easily fulfill our requirements.
Another problem with the ESP8266 is the lack of the module asyncio module in the kernel. Parallel operation of function units as with the ESP32 is therefore not possible. In addition, the NVS module is missing, so we have to fall back on the file system to save the favorite color.
With the Raspberry Pi Pico, we can do without the ADS1115 converter because it has three ADC inputs itself. And the MicroPython core of the Raspberry Pi Pico also speaks asyncio. But here too, storage is via the file system.
After this preliminary information, it's time to get down to business. Welcome to a new episode in the series
MicroPython on the (ESP32) ESP8266 and Raspberry Pi Pico
today
Sensor lamp 3.0
The only difference between the hardware for the ESP8266 and the Raspberry Pi Pico, apart from the controller used, is that the ESP8266 setup requires an external ADC. An RGB LED, a neopixel ring and the three pressure sensors together with six resistors are required for both solutions.
The hardware
ESP8266
1 |
NodeMCU Lua Amica Module V2 ESP8266 ESP-12F WIFI Wifi Development Board with CP2102 or D1 Mini NodeMcu with ESP8266-12F WLAN module compatible with Arduino or |
1 |
|
1 |
|
1 |
KY-016 FZ0455 3-color RGB LED module 3 Color or LED diode assortment kit, 350 pieces, 3mm & 5mm, 5 colors - 1x set |
1 |
|
1 |
|
1 |
|
1 |
Resistor 560 Ω |
2 |
Resistor 10kΩ |
3 |
Resistance 100kΩ |
optional |
A small breadboard is sufficient for the specified controller boards. The rows of pins on the boards are close enough (nine grid units) that one row of contacts remains available on each side of the board.

Figure 2: Setup with ESP8266
I use the RGB LED again to display the operating status. The colors of the LEDs on the ring can be individually adjusted up and down. The RGB LED shows which color is currently on. There will be a total of six states: red, green, blue, light-dark, on-off and automatic change.
The series resistors are dimensioned so that the signal LED produces approximately white light. The blue and especially the green LED are much brighter than the red one. This is the reason for the large differences in the resistance values. If you want a brighter display, use smaller ohm values.
The circuit diagram in Figure 3 shows how everything fits together.

Figure 3: Circuit - ESP8266 with ADS1115 and pressure sensors
Raspberry Pi Pico
1 |
|
1 |
|
1 |
KY-016 FZ0455 3-Color RGB LED Module 3 Color or LED diode assortment kit, 350 pieces, 3mm & 5mm, 5 colors - 1x set |
1 |
|
1 |
|
1 |
|
1 |
Resistor 560 Ω |
2 |
Resistor 10kΩ |
3 |
Resistance 100kΩ |
optional |
We also use the pressure sensors on the Raspberry Pi Pico, but save the ADS1115 AD converter. The circuit also fits on a small breadboard.
Figure 4: Setup with Raspberry Pi Pico

Figure 5: Circuit - Raspberry Pi Pico
In addition to the sensor, the pressure sensor circuit boards also contain a resistor. The sensor itself also represents a resistor whose value decreases under load. The sensor and fixed resistor form a series circuit in which the sensor is connected to Vcc. The circuit therefore forms a voltage divider. The voltage at the output increases in value when pressure is applied to the sensor.

Figure 6: Pressure sensor circuit
Even with small loads on the sensor surface, the resistance drops to values in the double-digit kiloohm range. With the 510kΩ resistor alone, the output voltage increases by leaps and bounds. If you want a softer response, you can place an external resistor in parallel with the built-in object, as shown in Figure 5. The total resistance of this parallel circuit is 83kΩ. This makes it easier to detect loads of different strengths.
The test circuit is supplied with power via the USB cable. I chose a 18650 Li-ion cell as the power supply for the production system and a battery holder with charging unit and 5V output. I soldered the supply lines for my circuit to the pins of the USB-A socket. This way, I don't need a USB plug and can still use the mechanical switch on the circuit board. It will point towards the bottom of the housing, as will the charging socket at the top of the board in Figure 5.
I used an RGB LED module during the development phase. For the lamp itself, I found one of the LEDs from the assortment more optimal because of the easier installation.

Figure 7: RGB LED with common cathode
The lamp
I have already explained how the lamp is constructed in the first part. In this episode, the focus is on adapting the program to the new controllers. The construction of the lamp itself is described in the first episode described in the first episode. There you will also find a dimensioned pattern plan.

Figure 8: Sensor Lampy 2.0
The software
For flashing and programming the ESP32:
Thonny or
Signal tracking:
Firmware used for an ESP32:
The MicroPython programs for the project:
timeout.py Non-blocking software timer
touch8266.py Touchpad module
touchrpp.py Touchpad module
touchlampy8266.py Operating program
touchlampyrpp.py Operating program expansion stage 1
MicroPython - Language - Modules and programs
For the installation of Thonny you will find here a detailed instructions (english version). There is also a description of how the Micropython firmware (as of 18.06.2022) to the ESP chip burned is burned onto the ESP chip. How to use the Raspberry Pi Pico ready for use, you will find here.
MicroPython is an interpreter language. The main difference to the Arduino IDE, where you always and only flash entire programs, is that you only have to flash the MicroPython firmware once at the beginning on the ESP32 so that the controller understands MicroPython instructions. You can use Thonny, µPyCraft or esptool.py for this. For Thonny I have described the process here described here.
As soon as the firmware is flashed, you can talk to your controller in a casual conversation, test individual commands and see the response immediately without having to compile and transfer an entire program first. This is exactly what bothers me about the Arduino IDE. You simply save an enormous amount of time if you can test simple syntax and hardware tests and even try out and refine functions and entire program parts via the command line before you knit a program from it. I also like to create small test programs for this purpose. As a kind of macro, they summarize recurring commands. Entire applications are then sometimes developed from such program fragments.
Autostart
If you want the program to start automatically when the controller is switched on, copy the program text into a newly created blank file. Save this file under main.py in the workspace and upload it to the ESP chip. The program will start automatically the next time the controller is reset or switched on.
Testing programs
Programs are started manually from the current editor window in the Thonny IDE using the F5 key. This is quicker than clicking on the start button, or via the menu Run. Only the modules used in the program must be in the flash of the ESP32.
In between times Arduino IDE again?
If you want to use the controller together with the Arduino IDE again later, simply flash the program in the usual way. However, the ESP32/ESP8266 will then have forgotten that it ever spoke MicroPython. Conversely, any Espressif chip that contains a compiled program from the Arduino IDE or the AT firmware or LUA or ... can easily be provided with the MicroPython firmware. The process is always the same as here described here.
The neopixel ring
I have also written about the Neopixelring, how it works and the NeoPixel module in the first article the essentials. If you would like to find out more about this topic, I recommend reading the chapter of the same name from this episode.
The program and its changes
I use two of the previous versions of the ESP32 as a template, touchlampy_2.py and touchlampy_4.py. Of course, the module touch.py module must also be adapted to the use of the pressure sensors and thus an AD converter.
ESP8266
As the ESP8266 does not have the module asyncio module, we have to use an endless loop to query the sensors. Due to the lack of concurrency, we also run out of the randomly controlled color sequence touchlampy_4.py which we can replace with the program-controlled version from touchlampy_2.py can replace.
from machine import Pin, SoftI2C
from neopixel import NeoPixel as NP
from time import sleep, sleep_ms
from ads1115 import ADS1115
from touch8266 import TP
i2c=SoftI2C(scl=Pin(5),sda=Pin(4))
ads=ADS1115(i2c)
tp1=TP(ads, 0, 2000)
tp2=TP(ads, 1, 2000)
tp3=TP(ads, 2, 2000)
The import deletes a few lines and adds two new ones. For the conversation with the ADS1115 we need the class SoftI2C. We pass an instance of this to the constructor of the class ADS1115 class. The object ads object requires the TP-objects to retrieve the values from the pressure sensors. From the class TP class, we need the two methods getDuration() and touched(). Only the latter must be adapted to the new situation. After setting the channel, we retrieve the converter result and compare it with the limit value. A 1 is returned if val True is true, otherwise 0 is returned.
def touched(self):
self.adc.setChannel(self.kanal)
tpin=self.adc.getConvResult()
val=(tpin >= self.threshold)
return 1 if val else 0
We create the NeoPixel object, set the channel to 0 and define the start color. The list col list is needed when converting the color values to color (0...100) into the values that are sent to the ring (0...255).
In the file color.txt file stores the color values and the brightness value. To do this, the integers must be converted into strings. I like to use the with-statement to open files because it saves me having to close the file. This happens automatically when leaving the with block. So that each value is on a separate line, I append a New Line = \n =0x0A.
neoPin=Pin(15,Pin.OUT) # D8
neo=NP(neoPin,8)
kanal=0 # sw=0, rt=1, gn=2, bl=3, ws=4, ge=5
color=[16,32,8]
col=[0,0,0]
lumi=100
def storeColor():
with open("color.txt","w") as f:
for i in range(3):
f.write(str(color[i])+"\n")
f.write(str(lumi)+"\n")
print("Fardcode gesichert",color)
def loadColor():
global color, lumi
with open("color.txt","r") as f:
for i in range(3):
color[i]=int(f.readline())
lumi=int(f.readline())
rt,gn,bl = color
print("Fardcode geladen:",[rt,gn,bl])
try:
loadColor()
except:
storeColor()
When reading in the strings line by line, I make an integer out of them again and store the values in the global variable color and lumi off. The saved color info is loaded when the program is started. An exception is thrown if no file exists at the very first start. The file is then created and the specified color value is saved.
The GPIO outputs for the signal LED still need to be defined, as do some global variables. In shapes contains the tuples for controlling the signal LED.
ledR=Pin(14,Pin.OUT,value=0)
ledG=Pin(12,Pin.OUT,value=0)
ledB=Pin(13,Pin.OUT,value=0)
leds=(ledR,ledG,ledB)
# aus rot gruen blau weiss gelb
shapes=((0,0,0),(1,0,0),(0,1,0),(0,0,1),(1,1,1),(1,1,0),)
mode=len(shapes)
channels=("red","green","blue","lumi","auto")
step=1
m=0
program=0
merken=color
merkenL=lumi
gemerkt=False
The function lum() calculates from the color code and the luminosity specified in lumi the numerical values that are to be sent to the ring. Color component/100 times brightness /100 times 255.
def lum(lumi):
global col
for i in range(3):
col[i]=int(color[i]/100*255*lumi/100)
for i in range(8):
neo[i]=col
neo.write()
def lumAuto():
for i in range(8):
neo[i]=color
neo.write()
In automatic mode, values in the range from 0 to 255 are used a priori. lumAuto() therefore sends the values directly.
switchChannel() and showChannel() are taken from the program version touchlampy_4.py which can be found in episode 2 is described. However, this becomes a normal function because the ESP8266 is not a asyncio is recognized.
def switchChannel():
global kanal, color
if tp1.touched():
tp1.getDuration()
hold=tp1.dauer()
print("Dauer",hold)
if hold < 500:
kanal = (kanal + 1) % mode
showChannel(kanal)
elif 500 <= hold <2000:
storeColor()
else:
kanal=0
color=[0,0,0]
showChannel(kanal)
lum()
def showChannel(num):
assert num in range(mode)
for i in range(3):
leds[i](shapes[num][i])
The same fate befalls the functions increase() and decrease(). They are called cyclically in the main loop when the corresponding sensors are tapped. With increase() increases the color components in channels 1 to 3 from 0 to 100. This sets a mixed color in the desired brightness. Channel 4 controls the overall brightness. This allows you to decrease the previously set color in 100 steps (decrease()) and raise it again (increase()) without changing the color tone. However, there may be deviations with very small values, because ultimately the values must be rounded to integers.
def increase():
global color,merken,merkenL,gemerkt,program,m,lumi
if tp2.touched():
n=0
print("increase",kanal)
while tp2.touched():
if 1 <= kanal <= 3:
ptr=kanal-1
col=color[ptr]
if n < 10:
col = col + 1
else:
col = col + 5
col = min(col,100)
color[ptr]= col
print(channels[ptr], color)
lum(100)
elif kanal == 4:
if n < 10:
lumi = lumi + 1
else:
lumi = lumi + 5
lumi = min(lumi,100)
print("Lumineszenz", color, lumi)
lum(lumi)
elif kanal == 0:
if gemerkt:
color=merken
lumi=merkenL
print(color)
lum(lumi)
gemerkt=False
elif kanal == 5:
if not gemerkt:
merken=color
merkenL=lumi
gemerkt=True
m=0
color=[0,0,0]
lum(0)
print("auto gestartet",m,merken)
program=1
auto()
n+=1
The ring can be switched on via channel 0 (signal LED off) after it has been switched on via channel 0 and decrease() to switch it off. When switching off, we temporarily remember the last set values in remember and memorizeL and set memorized to True. When switching on with increase(), the values are restored. Channel 5 behaves similarly, via which the values specified in auto() is played and stopped.
def decrease():
global color,merken,merkenL,m,gemerkt,program,lumi
print("dec entered")
if tp3.touched():
n=0
print("decrease",kanal)
while tp3.touched():
if 1 <= kanal <= 3:
ptr=kanal-1
col=color[ptr]
if n < 10:
col = col - 1
else:
col = col - 5
col = max(col,0)
color[ptr]= col
print(channels[ptr], color)
lum(100)
elif kanal == 4:
if n < 10:
lumi = lumi - 1
else:
lumi = lumi - 5
lumi = max(lumi,0)
print("Lumineszenz", color, lumi)
lum(lumi)
elif kanal == 0:
if not gemerkt:
merken=color
merkenL=lumi
print(color)
color=[0,0,0]
lum(0)
gemerkt=True
elif kanal == 5:
if gemerkt:
program=0
gemerkt=False
color=merken
lumi=merkenL
print("auto gestoppt",merken)
lum(lumi)
n+=1
The function auto() function also loses the status of a coroutine. In touchlampy_4.py we did not have to worry about querying the touchpads because of the concurrency of the tasks. Here, we have to query the tp3 into the function because otherwise the automatically running color sequence cannot be exited.
def auto():
global program, m, color
print("Automatik gestartet")
while 1:
if tp3.touched()==1:
print("stopped in 1")
break
color=[0,0,255]
m=0
while program == 1:
if tp3.touched():
break
if 0 < m < 256//step:
color[0]=(color[0]+step)
color[2]=(color[2]-step)
print("\nRunde1")
elif 256//step < m < 512 // step:
color[0]=(color[0]-step)
color[1]=(color[1]+step)
print("\nRunde2")
elif 512 // step < m < 768 // step:
color[0]=(color[0]+ step)
color[1]=(color[1]- step)
color[2]=(color[2]+ step//2)
print("\nRunde3")
elif 768//step < m < 1024//step:
color[0]=(color[0]- step)
color[2]=(color[2]+ step//2)
print("\nRunde4")
elif 1024//step < m < 1280//step:
color[1]=(color[1]+ step)
color[2]=(color[2]- step//2)
print("\nRunde5")
elif 1280//step < m < 1536//step:
color[1]=(color[1]- step)
print("\nRunde6")
elif 1536//step < m < 1792//step:
color[2]=(color[2]+ step// step)
print("\nRunde7")
print(m, color)
lumAuto()
m=(m+1) % (1792//step)
Im Übrigen muss darauf geachtet werden, dass die Komponentenwerte nicht negativ werden, weil sonst seltsame Effekte auftreten. [-1,0,0] wird dann nämlich zum Beispiel zu hellstem Rot.
>>> -1 & 0xff
255
The main loop takes over the sensor query and thus controls the program sequence.
lum(100)
while 1:
if tp1.touched():
switchChannel()
elif tp2.touched():
print("inc")
increase()
elif tp3.touched():
print("dec")
decrease()
Raspberry Pi Pico
Because the Raspberry Pi Pico asyncio we can use the program touchlampy_4.py program from the ESP32 almost 1:1. But only almost, because the Raspberry Pi Pico cannot do NVS. But it does have its own ADC inputs - three of them - which is fine! We take over the storage and loading of the color components from the ESP8266.
Let's start by discussing the relevant objects of the module touchrpp.py. We get the class ADC on board.
from machine import Pin, ADC
import timeout
We pass the channel number and the limit value, which is between 0 and 65535 for the Raspberry Pi Pico, to the constructor. I have determined 10000 as the default value through test measurements. The channel number is transferred to the instance attribute channel instance attribute and, when the ADC object is instantiated, is transferred as an index to the ADCs list of ADC connection numbers.
def __init__(self, channelNbr,
grenze=Grenze):
self.threshold=0 # Grenzwert
self.duration=None # Touch-Dauer
self.grenzwert(grenze)
if not channelNbr in range(3):
raise ValueError
sys.exit()
self.kanal=channelNbr # Eingang festlegen
self.adc=ADC(TP.ADCs[channelNbr])
print("Konstruktor TP",channelNbr)
The method touched() method now looks like this.
def touched(self):
tpin=self.adc.read_u16()
val=(tpin >= self.threshold)
return 1 if val else 0
Finally, in touchrpp.py the function threshold() function.
def grenzwert(self,grenzwert=None):
if grenzwert is None:
return self.threshold
else:
gw = int(grenzwert)
gw = (gw if gw > 0 and gw < 65536 else Grenze)
print("Grenzwert ist jetzt {}.".format(gw))
self.threshold = gw
return gw
With the exception of a few lines, the operating program of the lamp corresponds to the content of touchlampy_4.py. The deviations are formatted in bold in the listing.
from touchrpp import TP
from machine import Pin
from neopixel import NeoPixel as NP
from time import sleep, sleep_ms
import uasyncio as asyncio
import random
from sys import exit
tp1=TP(0, 10000)
tp2=TP(1, 10000)
tp3=TP(2, 10000)
neoPin=Pin(15,Pin.OUT)
neo=NP(neoPin,8)
kanal=0 # sw=0, rt=1, gn=2, bl=3, ws=4, ge=5
color=[0,0,0] # rt, gn, bl
lumi=100
col=[0,0,0]
def storeColor():
with open("color.txt","w") as f:
for i in range(3):
f.write(str(color[i])+"\n")
f.write(str(lumi)+"\n")
print("Fardcode gesichert",color)
def loadColor():
global color, lumi
with open("color.txt","r") as f:
for i in range(3):
color[i]=int(f.readline())
lumi=int(f.readline())
rt,gn,bl = color
print("Fardcode geladen:",[rt,gn,bl])
try:
loadColor()
except:
storeColor()
ledR=Pin(7,Pin.OUT,value=0)
ledG=Pin(8,Pin.OUT,value=0)
ledB=Pin(9,Pin.OUT,value=0)
leds=(ledR,ledG,ledB)
# Signal: aus rot gruen blau weiss gelb
shapes=((0,0,0),(1,0,0),(0,1,0),(0,0,1),(1,1,1),(1,1,0),)
channels=("red","green","blue","lumi","auto")
mode=len(shapes)
program=0
merken=color
merkenL=lumi
gemerkt=False
step=2
m=0
def lum(lumi):
global col
for i in range(3):
col[i]=int(color[i]/100*255*lumi/100)
for i in range(8):
neo[i]=col
neo.write()
async def switchChannel():
global kanal, color
while 1:
await asyncio.sleep_ms(50)
if tp1.touched():
tp1.getDuration()
hold=tp1.dauer()
print("Dauer",hold)
if hold < 500:
kanal = (kanal + 1) % mode
showChannel(kanal)
elif 500 <= hold <2000:
storeColor()
else:
kanal=0
color=[0,0,0]
showChannel(kanal)
lum()
def showChannel(num):
assert num in range(mode)
for i in range(3):
leds[i](shapes[num][i])
async def increase():
global color,merken,merkenL,gemerkt,program,m,lumi
while 1:
await asyncio.sleep_ms(10)
if tp2.touched():
n=0
print("increase",kanal)
while tp2.touched():
await asyncio.sleep_ms(70)
if 1 <= kanal <= 3:
ptr=kanal-1
col=color[ptr]
if n < 10:
col = col + 1
else:
col = col + 3
col = min(col,100)
color[ptr]= col
print(channels[ptr], color)
lum(100)
elif kanal == 4:
if n < 10:
lumi = lumi + 1
else:
lumi = lumi + 3
lumi = min(lumi,100)
print("Lumineszenz", color, lumi)
lum(lumi)
elif kanal == 0:
if gemerkt:
color=merken
lumi=merkenL
print(color)
lum(lumi)
gemerkt=False
elif kanal == 5:
if not gemerkt:
merken=color
merkenL=lumi
gemerkt=True
m=0
color=[0,0,0]
lum(0)
program=1
print("auto gestartet",m,freeze)
n+=1
async def decrease():
global color,merken,merkenL,m,gemerkt,program,lumi
while 1:
await asyncio.sleep_ms(10)
if tp3.touched():
n=0
print("decrease",kanal)
while tp3.touched():
await asyncio.sleep_ms(70)
if 1 <= kanal <= 3:
ptr=kanal-1
col=color[ptr]
if n < 10:
col = col - 1
else:
col = col - 3
col = max(col,0)
color[ptr]= col
print(channels[ptr], color)
lum(100)
elif kanal == 4:
if n < 10:
lumi = lumi - 1
else:
lumi = lumi - 3
lumi = max(lumi,0)
print("Lumineszenz", color, lumi)
lum(lumi)
elif kanal == 0:
if not gemerkt:
merken=color
merkenL=lumi
print(color)
color=[0,0,0]
lum(0)
gemerkt=True
elif kanal == 5:
if gemerkt:
program=0
gemerkt=False
await asyncio.sleep_ms(50)
color=merken
lumi=merkenL
print("auto gestoppt",merken)
lum(lumi)
n+=1
async def auto(index):
global program, color
print("Random gestartet")
while 1:
await asyncio.sleep_ms(10)
if program==1:
await asyncio.sleep_ms(10)
lumMax=random.randint(0,100)
step=random.randint(1,3)
duration=random.randint(50,300)
direction=random.getrandbits(1)
col=0 if direction==1 else lumMax
steps=lumMax // step
m=0
color[index]=col
lum(100)
while program == 1:
await asyncio.sleep_ms(duration)
if direction==1:
col=col+step
col=min(col,lumMax)
else:
col=col-step
col=max(col,2)
color[index]=col
print(index, m, color)
lum(100)
m += 1
if m >= steps:
break
async def main():
loop = asyncio.get_event_loop()
loop.create_task(switchChannel())
loop.create_task(increase())
loop.create_task(decrease())
loop.create_task(auto(0))
loop.create_task(auto(1))
loop.create_task(auto(2))
loop.run_forever()
lum(lumi)
asyncio.run(main())
You are now able to program the lamp with a controller of your choice in MicroPython. With asyncio, we have a module at hand with which we can run several processes in parallel. We now know how data can be permanently stored on the systems and how the missing touchpad functionality can be replaced by pressure sensors. Of course, these features can also be easily applied to our own projects.
Have fun with it!