<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Projects Archives | Microcontroller Tutorials</title>
	<atom:link href="https://www.teachmemicro.com/category/projects/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.teachmemicro.com/category/projects/</link>
	<description>Microcontroller Tutorials and Resources</description>
	<lastBuildDate>Sun, 28 Jun 2026 00:42:33 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.2</generator>

<image>
	<url>https://www.teachmemicro.com/wp-content/uploads/2019/04/blue-icon-65x65.png</url>
	<title>Projects Archives | Microcontroller Tutorials</title>
	<link>https://www.teachmemicro.com/category/projects/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Raspberry Pi Pico Access Control System Using the Elecrow Pico Advanced Kit</title>
		<link>https://www.teachmemicro.com/raspberry-pi-pico-access-control-system-using-the-elecrow-pico-advanced-kit/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=raspberry-pi-pico-access-control-system-using-the-elecrow-pico-advanced-kit</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Sun, 17 May 2026 11:02:02 +0000</pubDate>
				<category><![CDATA[Raspberry Pi Projects]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=11485</guid>

					<description><![CDATA[<p>Access control systems are commonly used in offices, schools, laboratories, apartments, and industrial facilities. At the center of these systems is a controller that decides whether a person is allowed to enter, controls the door lock, and monitors alarm inputs. In this tutorial, I will build a small access control system using a Raspberry Pi &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/raspberry-pi-pico-access-control-system-using-the-elecrow-pico-advanced-kit/">Raspberry Pi Pico Access Control System Using the Elecrow Pico Advanced Kit</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Access control systems are commonly used in offices, schools, laboratories, apartments, and industrial facilities. At the center of these systems is a controller that decides whether a person is allowed to enter, controls the door lock, and monitors alarm inputs.</p>
<p>In this tutorial, I will build a small access control system using a Raspberry Pi Pico and several modules from the <a href="https://www.elecrow.com/raspberry-pi-pico-advanced-kit-with-pico-board-32-modules-and-32-detailed-projects-lessons.html?idd=5"><strong>Elecrow Raspberry Pi Pico Advanced Kit</strong></a>. The system can read an RFID card, accept a PIN from a keypad, show messages on an LCD, control a solenoid lock through a relay, and respond to fire and tamper alarm inputs.</p>
<p>This is not meant to replace a real commercial access control panel, but it is a very good learning project if you want to understand how access control logic works at the microcontroller level.</p>
<p><span id="more-11485"></span></p>
<hr />
<h2>Project Overview</h2>
<p>The Raspberry Pi Pico will act as the main controller. It will accept access requests from two sources:</p>
<ol>
<li>An RFID card scanned through the RC522 RFID module</li>
<li>A PIN entered through a membrane keypad</li>
</ol>
<p>If the card UID or PIN is valid, the Pico activates a relay driver. The relay then powers a solenoid lock for a few seconds to simulate unlocking a door.</p>
<p>The system also monitors two alarm inputs:</p>
<ol>
<li><strong>Fire alarm input</strong></li>
<li><strong>Tamper alarm input</strong></li>
</ol>
<p>When the fire alarm is triggered, the door is released and the buzzer sounds. This simulates a real access control system where doors may be released during emergency conditions.</p>
<p>When the tamper alarm is triggered, the system keeps the door locked and sounds the buzzer. This simulates someone trying to force open or tamper with the door or panel.</p>
<h3>About the Elecrow Raspberry Pi Pico Advanced Kit</h3>
<p><img decoding="async" class="aligncenter" src="https://media-cdn.elecrow.com/media/webp_image/catalog/product/cache/b6b9577937e6a96f50e53ddc42983628/r/a/raspberry_pico_advanced_kit_package_11.21.webp" alt="raspberry pi pico" /></p>
<p>For this project, I used modules from the <a href="https://www.elecrow.com/raspberry-pi-pico-advanced-kit-with-pico-board-32-modules-and-32-detailed-projects-lessons.html?idd=5">Elecrow Raspberry Pi Pico Advanced Kit</a>. The kit includes a Raspberry Pi Pico, breadboards, LEDs, an RGB module, sound sensor, PIR motion sensor, photoresistor module, passive buzzer, vibration sensor, magnetic spring module, soil moisture sensor, potentiometer, servo, joystick, RC522 RFID module, 4-digit display, traffic light module, rotary encoder, 1602 LCD, temperature and humidity sensor, raindrops module, flame sensor, OLED module, membrane keypad, smart car parts, ultrasonic sensor, infrared remote, and several other accessories.</p>
<p><img decoding="async" class="aligncenter" src="https://media-cdn.elecrow.com/media/webp_image/catalog/product/cache/b6b9577937e6a96f50e53ddc42983628/r/a/raspberry_pico_advanced_kit_1_n.webp" /></p>
<p>For this access control project, I will use the parts that make sense for a small door controller:</p>
<ul>
<li>Raspberry Pi Pico</li>
<li>RC522 RFID module</li>
<li>Membrane keypad</li>
<li>1602 LCD module</li>
<li> Passive buzzer</li>
<li>LEDs (traffic light module)</li>
<li>Flame sensor or button for fire alarm simulation</li>
<li>Vibration sensor for tamper detection</li>
</ul>
<p>I will also use an external solenoid lock and relay driver. These are not driven directly by the Pico because a solenoid needs more current than a GPIO pin can provide.</p>
<hr />
<h2>How the System Works</h2>
<p>Here is the basic block diagram of the project:</p>
<pre><a href="https://www.teachmemicro.com/wp-content/uploads/2026/05/raspberry-pi-pico-access-control.avif"><img data-dominant-color="e7e9e7" data-has-transparency="false" style="--dominant-color: #e7e9e7;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-11486 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/05/raspberry-pi-pico-access-control.avif" alt="" width="724" height="543" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/05/raspberry-pi-pico-access-control.avif 724w, https://www.teachmemicro.com/wp-content/uploads/2026/05/raspberry-pi-pico-access-control-300x225.avif 300w" sizes="auto, (max-width: 724px) 100vw, 724px" /></a></pre>
<p>The Pico continuously checks the system inputs. In normal standby mode, the LCD asks the user to scan a card or enter a PIN.</p>
<p>If access is granted, the relay turns on and the solenoid unlocks for a short time. If access is denied, the buzzer gives an error sound and the red LED turns on.</p>
<p>The fire alarm and tamper alarm have higher priority than normal card or PIN access.</p>
<hr />
<h2>Features of This Project</h2>
<p>This project includes the following features:</p>
<ul>
<li>RFID card access using the RC522 module</li>
<li>PIN access using a membrane keypad</li>
<li>LCD status messages</li>
<li>Solenoid lock control using a relay driver</li>
<li>Buzzer feedback</li>
<li>LED status indicators</li>
<li>Fire alarm input</li>
<li>Tamper alarm input</li>
<li>Optional door contact monitoring</li>
<li>MicroPython code for beginners</li>
</ul>
<hr />
<h2>Required Components</h2>
<p>Most of the components used in this project are included in the <strong>Elecrow Raspberry Pi Pico Advanced Kit</strong>. I also added a solenoid lock and relay driver to make the project closer to a real access control setup.</p>
<table class="w-fit min-w-(--thread-content-width)">
<thead>
<tr>
<th class="last:pe-10">Component</th>
<th class="last:pe-10">Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td>Raspberry Pi Pico</td>
<td>Main controller</td>
</tr>
<tr>
<td>RC522 RFID module</td>
<td>Reads RFID cards</td>
</tr>
<tr>
<td>Membrane keypad</td>
<td>PIN entry</td>
</tr>
<tr>
<td>1602 LCD module</td>
<td>Displays system status</td>
</tr>
<tr>
<td>Passive buzzer</td>
<td>Alarm and feedback sound</td>
</tr>
<tr>
<td>Red LED</td>
<td>Access denied / alarm indicator</td>
</tr>
<tr>
<td>Green LED</td>
<td>Access granted indicator</td>
</tr>
<tr>
<td>Yellow LED</td>
<td>Standby / warning indicator</td>
</tr>
<tr>
<td>Flame sensor or button</td>
<td>Fire alarm input</td>
</tr>
<tr>
<td>Vibration sensor</td>
<td>Tamper alarm input</td>
</tr>
<tr>
<td>Relay driver module</td>
<td>Controls the solenoid lock</td>
</tr>
<tr>
<td>Solenoid lock</td>
<td>Door lock output</td>
</tr>
<tr>
<td>External power supply</td>
<td>Powers the solenoid lock</td>
</tr>
</tbody>
</table>
<p>You can also use a servo motor instead of a solenoid lock if you want a safer beginner version of the project. However, using a relay and solenoid makes the project feel closer to a real access control system.</p>
<hr />
<h2>Important Safety Note About Solenoid Locks</h2>
<p>The Raspberry Pi Pico <strong>cannot directly drive a solenoid lock</strong>. A solenoid usually requires more voltage and current than a Pico GPIO pin can provide.</p>
<p>The correct setup is:</p>
<pre><pre><code class="language-cpp">Pico GPIO → Relay Driver Input → Relay Contact → Solenoid Power</code></pre></pre>
<p>The Pico only controls the relay input. The solenoid must have its own power supply.</p>
<p>Also, make sure the Pico ground and relay module ground are connected together, unless you are using a fully isolated relay module.</p>
<p>If your relay board or solenoid driver does not already include flyback protection, you should add a diode across the solenoid coil to protect the circuit from voltage spikes.</p>
<p>For a real door access system, use proper certified hardware. This tutorial is only for learning and prototyping.</p>
<hr />
<h2>Pin Connections</h2>
<p>The exact pins can be changed in the code, but I will use this pin assignment for the tutorial.</p>
<h3>RC522 RFID Module to Raspberry Pi Pico</h3>
<p>The RC522 uses SPI communication.</p>
<table>
<thead>
<tr>
<th>RC522 Pin</th>
<th>Raspberry Pi Pico Pin</th>
</tr>
</thead>
<tbody>
<tr>
<td>SDA / SS</td>
<td>GP17</td>
</tr>
<tr>
<td>SCK</td>
<td>GP18</td>
</tr>
<tr>
<td>MOSI</td>
<td>GP19</td>
</tr>
<tr>
<td>MISO</td>
<td>GP16</td>
</tr>
<tr>
<td>RST</td>
<td>GP20</td>
</tr>
<tr>
<td>3.3V</td>
<td>3V3</td>
</tr>
<tr>
<td>GND</td>
<td>GND</td>
</tr>
</tbody>
</table>
<p>Do not connect the RC522 to 5V. The RC522 module works with 3.3V logic.</p>
<hr />
<h3>I2C LCD to Raspberry Pi Pico</h3>
<p>This tutorial assumes that the 1602 LCD has an I2C backpack.</p>
<table>
<thead>
<tr>
<th>LCD Pin</th>
<th>Raspberry Pi Pico Pin</th>
</tr>
</thead>
<tbody>
<tr>
<td>VCC</td>
<td>VBUS or 5V</td>
</tr>
<tr>
<td>GND</td>
<td>GND</td>
</tr>
<tr>
<td>SDA</td>
<td>GP4</td>
</tr>
<tr>
<td>SCL</td>
<td>GP5</td>
</tr>
</tbody>
</table>
<p>Most I2C LCD backpacks work at 5V, but the I2C pull-ups may also pull SDA and SCL to 5V. Many hobby modules work in practice, but the safer method is to use a level shifter or power the backpack in a way that keeps the I2C lines at 3.3V.</p>
<p>If your LCD has 16 pins instead of 4 pins, then it is probably a parallel LCD and will need different wiring and a different library.</p>
<hr />
<h3>Keypad to Raspberry Pi Pico</h3>
<p>For a 4x4 membrane keypad:</p>
<table>
<thead>
<tr>
<th>Keypad Line</th>
<th>Pico Pin</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1</td>
<td>GP6</td>
</tr>
<tr>
<td>Row 2</td>
<td>GP7</td>
</tr>
<tr>
<td>Row 3</td>
<td>GP8</td>
</tr>
<tr>
<td>Row 4</td>
<td>GP9</td>
</tr>
<tr>
<td>Column 1</td>
<td>GP10</td>
</tr>
<tr>
<td>Column 2</td>
<td>GP11</td>
</tr>
<tr>
<td>Column 3</td>
<td>GP12</td>
</tr>
<tr>
<td>Column 4</td>
<td>GP13</td>
</tr>
</tbody>
</table>
<p>If you are using a 4x3 keypad, you can modify the keypad map and remove one column.</p>
<hr />
<h3>Relay Driver and Solenoid Lock</h3>
<table>
<thead>
<tr>
<th>Relay Module Pin</th>
<th>Pico / Supply Connection</th>
</tr>
</thead>
<tbody>
<tr>
<td>IN</td>
<td>GP14</td>
</tr>
<tr>
<td>VCC</td>
<td>5V or relay module required voltage</td>
</tr>
<tr>
<td>GND</td>
<td>GND</td>
</tr>
</tbody>
</table>
<p>The solenoid is connected through the relay contacts, not directly to the Pico.</p>
<p>A simple wiring idea is:</p>
<pre><pre><code class="language-cpp">Solenoid Power +  → Relay COM
Relay NO          → Solenoid +
Solenoid -        → Solenoid Power -</code></pre></pre>
<p>When the relay turns on, the solenoid receives power and unlocks.</p>
<p>Some relay modules are active-low. This means the relay turns on when the Pico output is LOW instead of HIGH. I will make this configurable in the code.</p>
<hr />
<h3>Buzzer and LEDs</h3>
<table>
<thead>
<tr>
<th>Part</th>
<th>Pico Pin</th>
</tr>
</thead>
<tbody>
<tr>
<td>Buzzer</td>
<td>GP15</td>
</tr>
<tr>
<td>Green LED</td>
<td>GP21</td>
</tr>
<tr>
<td>Red LED</td>
<td>GP22</td>
</tr>
<tr>
<td>Yellow LED</td>
<td>GP26</td>
</tr>
</tbody>
</table>
<p>Use current-limiting resistors for the LEDs.</p>
<hr />
<h3>Alarm Inputs</h3>
<table>
<thead>
<tr>
<th>Alarm Input</th>
<th>Suggested Component</th>
<th>Pico Pin</th>
</tr>
</thead>
<tbody>
<tr>
<td>Fire alarm input</td>
<td>Flame sensor DO or push button</td>
<td>GP27</td>
</tr>
<tr>
<td>Tamper alarm input</td>
<td>Vibration sensor or magnetic sensor</td>
<td>GP28</td>
</tr>
<tr>
<td>Door contact, optional</td>
<td>Magnetic spring module</td>
<td>GP2</td>
</tr>
</tbody>
</table>
<p>For beginner testing, I recommend using push buttons first. Once the logic works, you can replace the buttons with the flame sensor and tamper sensor.</p>
<p>This is my setup on a breadboard. I didn't use the relay + solenoid lock yet; the red LED on the middle of the breadboard will serve as the indicator that the lock released or not.</p>
<p><a href="https://www.teachmemicro.com/wp-content/uploads/2026/05/raspberry-pi-pico-access-control-setup.avif"><img data-dominant-color="d3d4d7" data-has-transparency="false" style="--dominant-color: #d3d4d7;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-11510 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/05/raspberry-pi-pico-access-control-setup.avif" alt="Raspberry Pi Pico Access Control Setup" width="869" height="652" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/05/raspberry-pi-pico-access-control-setup.avif 869w, https://www.teachmemicro.com/wp-content/uploads/2026/05/raspberry-pi-pico-access-control-setup-300x225.avif 300w, https://www.teachmemicro.com/wp-content/uploads/2026/05/raspberry-pi-pico-access-control-setup-768x576.avif 768w" sizes="auto, (max-width: 869px) 100vw, 869px" /></a></p>
<hr />
<h2>Installing MicroPython on the Raspberry Pi Pico</h2>
<p>Before uploading the code, install MicroPython on the Raspberry Pi Pico. You can download the latest UF2 file in the <a href="https://micropython.org/download/RPI_PICO/">official MicroPython website</a>.</p>
<ol>
<li>Hold the <strong>BOOTSEL</strong> button on the Pico.</li>
<li>Connect the Pico to your computer using a USB cable.</li>
<li>Release the BOOTSEL button.</li>
<li>The Pico should appear as a USB drive.</li>
<li>Copy the MicroPython UF2 file to the Pico.</li>
<li>Open Thonny.</li>
<li>Select the MicroPython interpreter for Raspberry Pi Pico.</li>
</ol>
<p>Once this is done, you can run MicroPython code on the Pico using Thonny.</p>
<hr />
<h2>Required MicroPython Libraries</h2>
<p>This project needs libraries for:</p>
<ul>
<li>RC522 RFID module</li>
<li>I2C LCD</li>
</ul>
<p>For the RC522, you need a file named:</p>
<pre><pre><code class="language-cpp">mfrc522.py</code></pre></pre>
<p>For the I2C LCD, you need files such as:</p>
<pre><pre><code class="language-cpp">lcd_api.py
pico_i2c_lcd.py</code></pre></pre>
<p>You can find the mfrc522.py in <a href="https://github.com/kevinmcaleer/pico-rfid">this library</a>, while the I2C LCD files are in <a href="https://github.com/T-622/RPI-PICO-I2C-LCD">this library</a>.</p>
<p>Upload these files to the Raspberry Pi Pico using Thonny. You can place them directly on the Pico or inside a <em>/lib</em> folder.</p>
<p>&nbsp;</p>
<hr />
<h2>Testing the RFID Reader First</h2>
<p>Before combining all parts, it is better to test the RFID reader alone. This helps you get the UID of your RFID card.</p>
<p>A card UID may look something like this:</p>
<pre><pre><code class="language-cpp">12 AB 34 CD</code></pre></pre>
<p>You will later place this UID inside the list of authorized cards.</p>
<p>In the final code, the authorized cards are stored here:</p>
<pre><pre><code class="language-cpp">AUTHORIZED_CARDS = [
    z&quot;&gt;&quot;12 AB 34 CD&quot;,
    z&quot;&gt;&quot;A1 B2 C3 D4&quot;
]</code></pre></pre>
<p>Replace these values with your actual card UID.</p>
<hr />
<h2>System Behavior</h2>
<p>The system has several operating conditions.</p>
<h3>Idle Mode</h3>
<p>In idle mode, the LCD shows:</p>
<pre><pre><code class="language-cpp">PICO ACCESS
SCAN CARD/PIN</code></pre></pre>
<p>The system waits for an RFID card or keypad input.</p>
<hr />
<h3>Access Granted</h3>
<p>If the card or PIN is valid:</p>
<pre><pre><code class="language-cpp">ACCESS GRANTED
DOOR UNLOCKED</code></pre></pre>
<p>The green LED turns on, the buzzer gives a short beep, and the relay unlocks the solenoid for a few seconds.</p>
<hr />
<h3>Access Denied</h3>
<p>If the card or PIN is invalid:</p>
<pre><pre><code class="language-cpp">ACCESS DENIED
TRY AGAIN</code></pre></pre>
<p>The red LED turns on and the buzzer gives an error sound.</p>
<hr />
<h3>Fire Alarm</h3>
<p>If the fire alarm input is triggered:</p>
<pre><pre><code class="language-cpp">FIRE ALARM!
DOOR RELEASED</code></pre></pre>
<p>The door unlocks and the buzzer sounds repeatedly.</p>
<p>This is done because many access control systems release doors during emergency conditions. Again, this project is only a simulation.</p>
<hr />
<h3>Tamper Alarm</h3>
<p>If the tamper input is triggered:</p>
<pre><pre><code class="language-cpp">TAMPER ALARM!
SYSTEM LOCKED</code></pre></pre>
<p>The system sounds the buzzer and flashes the red LED. The door remains locked.</p>
<hr />
<h2>Event Priority</h2>
<p>The Pico should not treat all events equally. Alarm inputs should have higher priority than normal access requests.</p>
<p>For this project, I use this priority:</p>
<table>
<thead>
<tr>
<th>Priority</th>
<th>Event</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Fire alarm</td>
<td>Unlock door and sound alarm</td>
</tr>
<tr>
<td>2</td>
<td>Tamper alarm</td>
<td>Keep locked and sound alarm</td>
</tr>
<tr>
<td>3</td>
<td>Valid RFID or PIN</td>
<td>Unlock door temporarily</td>
</tr>
<tr>
<td>4</td>
<td>Invalid RFID or PIN</td>
<td>Deny access</td>
</tr>
<tr>
<td>5</td>
<td>Idle</td>
<td>Wait for input</td>
</tr>
</tbody>
</table>
<p>Fire alarm has the highest priority because it is a safety-related condition.</p>
<hr />
<h2>Final MicroPython Code</h2>
<p>Upload the required libraries first, then save the following code as</p>
<pre><pre><code class="language-cpp">main.py</code></pre></pre>
<p>on the Raspberry Pi Pico.</p>
<p>This code assumes:</p>
<ul>
<li>RC522 uses SPI0</li>
<li>LCD uses I2C0</li>
<li>Keypad is 4x4</li>
<li>Relay output is active-high by default</li>
<li>Fire and tamper inputs are active-low using pull-up resistors</li>
</ul>
<p>If your relay module is active-low, change:</p>
<pre><pre><code class="language-cpp">RELAY_ACTIVE_HIGH = True</code></pre></pre>
<p>to:</p>
<pre><pre><code class="language-cpp">RELAY_ACTIVE_HIGH = False</code></pre></pre>
<p>Here is the complete code:</p>
<pre><pre><code class="language-cpp">from machine import Pin, PWM, I2C, SPI
from time import sleep, ticks_ms, ticks_diff

# ------------------------------------------------------------
# Optional libraries
# ------------------------------------------------------------
# These imports depend on the library files you uploaded.
# If your library uses different class names, adjust these lines.

try:
    from mfrc522 import MFRC522
except ImportError:
    MFRC522 = None

try:
    from pico_i2c_lcd import I2cLcd
except ImportError:
    I2cLcd = None


# ------------------------------------------------------------
# User settings
# ------------------------------------------------------------

AUTHORIZED_CARDS = [
    &quot;C3 A4 13 29&quot;,
    &quot;12 AB 34 CD&quot;,
    &quot;A1 B2 C3 D4&quot;
]

AUTHORIZED_PIN = &quot;1234&quot;

ALARM_CHECK_INTERVAL_SECONDS = 5
UNLOCK_TIME_SECONDS = 3

RELAY_ACTIVE_HIGH = True

LCD_I2C_ADDR = 0x27
LCD_ROWS = 2
LCD_COLS = 16


# ------------------------------------------------------------
# Pin assignments
# ------------------------------------------------------------

# RFID RC522 SPI pins
RFID_SCK_PIN = 18
RFID_MOSI_PIN = 19
RFID_MISO_PIN = 16
RFID_CS_PIN = 17
RFID_RST_PIN = 20

# LCD I2C pins
LCD_SDA_PIN = 4
LCD_SCL_PIN = 5

# Keypad pins
ROW_PINS = [6, 7, 8, 9]
COL_PINS = [10, 11, 12, 13]

# Outputs
RELAY_PIN = 14
BUZZER_PIN = 15
GREEN_LED_PIN = 21
RED_LED_PIN = 22
YELLOW_LED_PIN = 26

# Inputs
FIRE_INPUT_PIN = 27
TAMPER_INPUT_PIN = 28
DOOR_CONTACT_PIN = 2


# ------------------------------------------------------------
# Hardware setup
# ------------------------------------------------------------

green_led = Pin(GREEN_LED_PIN, Pin.OUT)
red_led = Pin(RED_LED_PIN, Pin.OUT)
yellow_led = Pin(YELLOW_LED_PIN, Pin.OUT)

relay = Pin(RELAY_PIN, Pin.OUT)

buzzer = PWM(Pin(BUZZER_PIN))
buzzer.duty_u16(0)

fire_input = Pin(FIRE_INPUT_PIN, Pin.IN, Pin.PULL_UP)
tamper_input = Pin(TAMPER_INPUT_PIN, Pin.IN, Pin.PULL_UP)
door_contact = Pin(DOOR_CONTACT_PIN, Pin.IN, Pin.PULL_UP)

row_pins = [Pin(pin, Pin.OUT) for pin in ROW_PINS]
col_pins = [Pin(pin, Pin.IN, Pin.PULL_UP) for pin in COL_PINS]

key_map = [
    [&quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;A&quot;],
    [&quot;4&quot;, &quot;5&quot;, &quot;6&quot;, &quot;B&quot;],
    [&quot;7&quot;, &quot;8&quot;, &quot;9&quot;, &quot;C&quot;],
    [&quot;*&quot;, &quot;0&quot;, &quot;#&quot;, &quot;D&quot;]
]

# LCD setup
lcd = None

if I2cLcd is not None:
    try:
        i2c = I2C(0, sda=Pin(LCD_SDA_PIN), scl=Pin(LCD_SCL_PIN), freq=400000)
        lcd = I2cLcd(i2c, LCD_I2C_ADDR, LCD_ROWS, LCD_COLS)
    except Exception as e:
        print(&quot;LCD init failed:&quot;, e)
        lcd = None
else:
    print(&quot;LCD library not found. LCD output disabled.&quot;)

# RFID setup
rfid = None

if MFRC522 is not None:
    try:
        # This constructor may be different depending on your mfrc522.py library.
        # Some libraries use:
        # rfid = MFRC522(spi_id=0, sck=18, miso=16, mosi=19, cs=17, rst=20)
        #
        # If this line fails, check the documentation of your RC522 library.
        rfid = MFRC522(
            spi_id=0,
            sck=RFID_SCK_PIN,
            miso=RFID_MISO_PIN,
            mosi=RFID_MOSI_PIN,
            cs=RFID_CS_PIN,
            rst=RFID_RST_PIN
        )
    except Exception as e:
        print(&quot;RFID init failed:&quot;, e)
        rfid = None
else:
    print(&quot;MFRC522 library not found. RFID disabled.&quot;)


# ------------------------------------------------------------
# Helper functions
# ------------------------------------------------------------

def set_relay(active):
    if RELAY_ACTIVE_HIGH:
        relay.value(1 if active else 0)
    else:
        relay.value(0 if active else 1)


def lock_door():
    set_relay(False)


def unlock_door():
    set_relay(True)


def all_leds_off():
    green_led.off()
    red_led.off()
    yellow_led.off()

def pad_lcd_text(text, width=16):
    text = str(text)[:width]

    while len(text) &lt; width:
        text += &quot; &quot;

    return text

last_lcd_line1 = None
last_lcd_line2 = None

def lcd_message(line1, line2=&quot;&quot;):
    global last_lcd_line1, last_lcd_line2

    line1 = str(line1)[:16]
    line2 = str(line2)[:16]

    # Do not refresh the LCD if the message did not change
    if line1 == last_lcd_line1 and line2 == last_lcd_line2:
        return

    print(line1, line2)

    last_lcd_line1 = line1
    last_lcd_line2 = line2

    if lcd is None:
        return

    lcd.move_to(0, 0)
    lcd.putstr(pad_lcd_text(line1, 16))

    lcd.move_to(0, 1)
    lcd.putstr(pad_lcd_text(line2, 16))


def beep(freq=2000, duration=0.1):
    buzzer.freq(freq)
    buzzer.duty_u16(30000)
    sleep(duration)
    buzzer.duty_u16(0)


def beep_success():
    beep(1800, 0.08)
    sleep(0.05)
    beep(2200, 0.08)


def beep_error():
    for _ in range(3):
        beep(600, 0.12)
        sleep(0.08)


def beep_alarm():
    beep(1200, 0.15)
    sleep(0.08)


def is_fire_alarm_active():
    # Active-low input
    return fire_input.value() == 0


def is_tamper_active():
    # Active-low input
    return tamper_input.value() == 0


def is_door_open():
    # This depends on your magnetic switch module.
    # For this example, LOW means door open.
    return door_contact.value() == 0


# ------------------------------------------------------------
# Keypad functions
# ------------------------------------------------------------

def scan_keypad():
    for row_index, row in enumerate(row_pins):
        for r in row_pins:
            r.value(1)

        row.value(0)

        for col_index, col in enumerate(col_pins):
            if col.value() == 0:
                sleep(0.05)  # debounce

                if col.value() == 0:
                    key = key_map[row_index][col_index]

                    while col.value() == 0:
                        sleep(0.01)

                    return key

    return None


# ------------------------------------------------------------
# RFID functions
# ------------------------------------------------------------

def uid_to_string(uid):
    return &quot; &quot;.join(&quot;{:02X}&quot;.format(byte) for byte in uid)


def read_rfid_card():
    if rfid is None:
        return None

    try:
        (stat, tag_type) = rfid.request(rfid.REQIDL)

        if stat == rfid.OK:
            (stat, uid) = rfid.SelectTagSN()

            if stat == rfid.OK:
                return uid_to_string(uid)

    except Exception as e:
        print(&quot;RFID read error:&quot;, e)

    return None


# ------------------------------------------------------------
# Access control actions
# ------------------------------------------------------------

def grant_access(source):
    all_leds_off()
    green_led.on()

    lcd_message(&quot;ACCESS GRANTED&quot;, source)
    beep_success()

    unlock_door()
    sleep(UNLOCK_TIME_SECONDS)
    lock_door()

    green_led.off()
    lcd_message(&quot;DOOR LOCKED&quot;, &quot;SCAN CARD/PIN&quot;)
    sleep(1)


def deny_access(reason):
    all_leds_off()
    red_led.on()

    lcd_message(&quot;ACCESS DENIED&quot;, reason)
    beep_error()

    red_led.off()
    sleep(1)

def sound_alarm_cycle(duration_seconds, fire_mode=False):
    start_time = ticks_ms()

    while ticks_diff(ticks_ms(), start_time) &lt; duration_seconds * 1000:
        if fire_mode:
            red_led.toggle()
            yellow_led.toggle()
        else:
            red_led.toggle()
            yellow_led.off()

        beep_alarm()
        sleep(0.2)

    buzzer.duty_u16(0)

def handle_fire_alarm():
    all_leds_off()

    lcd_message(&quot;FIRE ALARM!&quot;, &quot;DOOR RELEASED&quot;)
    unlock_door()

    while True:
        sound_alarm_cycle(ALARM_CHECK_INTERVAL_SECONDS, fire_mode=True)

        if not is_fire_alarm_active():
            break

        lcd_message(&quot;FIRE ALARM!&quot;, &quot;STILL ACTIVE&quot;)

    all_leds_off()
    lock_door()
    lcd_message(&quot;FIRE CLEARED&quot;, &quot;DOOR LOCKED&quot;)
    sleep(1)


def handle_tamper_alarm():
    all_leds_off()

    lcd_message(&quot;TAMPER ALARM!&quot;, &quot;SYSTEM LOCKED&quot;)
    lock_door()

    while True:
        sound_alarm_cycle(ALARM_CHECK_INTERVAL_SECONDS, fire_mode=False)

        if not is_tamper_active():
            break

        lcd_message(&quot;TAMPER ALARM!&quot;, &quot;STILL ACTIVE&quot;)

    all_leds_off()
    lcd_message(&quot;TAMPER CLEARED&quot;, &quot;SCAN CARD/PIN&quot;)
    sleep(1)


def handle_forced_door():
    all_leds_off()
    red_led.on()

    lcd_message(&quot;DOOR FORCED!&quot;, &quot;ALARM ACTIVE&quot;)
    lock_door()

    # Sound alarm for a few seconds
    start_time = ticks_ms()

    while ticks_diff(ticks_ms(), start_time) &lt; 5000:
        red_led.toggle()
        beep_alarm()
        sleep(0.2)

    all_leds_off()
    lcd_message(&quot;SCAN CARD/PIN&quot;, &quot;&quot;)
    sleep(1)


# ------------------------------------------------------------
# Main program
# ------------------------------------------------------------

def main():
    entered_pin = &quot;&quot;
    last_idle_update = 0

    lock_door()
    all_leds_off()

    lcd_message(&quot;PICO ACCESS&quot;, &quot;SCAN CARD/PIN&quot;)
    yellow_led.on()

    door_was_unlocked_by_system = False

    while True:
        # Highest priority: fire alarm
        if is_fire_alarm_active():
            handle_fire_alarm()
            entered_pin = &quot;&quot;
            lcd_message(&quot;PICO ACCESS&quot;, &quot;SCAN CARD/PIN&quot;)
            yellow_led.on()
            continue

        # Second priority: tamper alarm
        if is_tamper_active():
            handle_tamper_alarm()
            entered_pin = &quot;&quot;
            lcd_message(&quot;PICO ACCESS&quot;, &quot;SCAN CARD/PIN&quot;)
            yellow_led.on()
            continue

        # Optional forced door detection
        # This is basic logic only. A real system needs better door timing rules.
        if is_door_open() and not door_was_unlocked_by_system:
            handle_forced_door()
            entered_pin = &quot;&quot;
            lcd_message(&quot;PICO ACCESS&quot;, &quot;SCAN CARD/PIN&quot;)
            yellow_led.on()
            continue

        # RFID access
        uid = read_rfid_card()

        if uid is not None:
            print(&quot;Card UID:&quot;, uid)

            if uid in AUTHORIZED_CARDS:
                yellow_led.off()
                door_was_unlocked_by_system = True
                grant_access(&quot;RFID CARD&quot;)
                door_was_unlocked_by_system = False
            else:
                yellow_led.off()
                deny_access(&quot;INVALID CARD&quot;)

            lcd_message(&quot;PICO ACCESS&quot;, &quot;SCAN CARD/PIN&quot;)
            yellow_led.on()
            entered_pin = &quot;&quot;
            sleep(0.5)
            continue

        # Keypad access
        key = scan_keypad()

        if key is not None:
            print(&quot;Key:&quot;, key)

            if key == &quot;*&quot;:
                entered_pin = &quot;&quot;
                lcd_message(&quot;ENTER PIN:&quot;, &quot;&quot;)
                beep(1200, 0.05)

            elif key == &quot;#&quot;:
                if entered_pin == AUTHORIZED_PIN:
                    yellow_led.off()
                    door_was_unlocked_by_system = True
                    grant_access(&quot;PIN ENTRY&quot;)
                    door_was_unlocked_by_system = False
                else:
                    yellow_led.off()
                    deny_access(&quot;WRONG PIN&quot;)

                entered_pin = &quot;&quot;
                lcd_message(&quot;PICO ACCESS&quot;, &quot;SCAN CARD/PIN&quot;)
                yellow_led.on()

            elif key in &quot;0123456789&quot;:
                if len(entered_pin) &lt; 8: entered_pin += key lcd_message(&quot;ENTER PIN:&quot;, &quot;*&quot; * len(entered_pin)) beep(1500, 0.04) # Refresh idle display occasionally if ticks_diff(ticks_ms(), last_idle_update) &gt; 3000 and entered_pin == &quot;&quot;:
            lcd_message(&quot;PICO ACCESS&quot;, &quot;SCAN CARD/PIN&quot;)
            yellow_led.on()
            last_idle_update = ticks_ms()

        sleep(0.05)


main()</code></pre></pre>
<hr />
<h2>How the Code Works</h2>
<p>The code starts by defining the authorized cards and PIN:</p>
<pre><pre><code class="language-cpp">AUTHORIZED_CARDS = [
    &quot;12 AB 34 CD&quot;,
    &quot;A1 B2 C3 D4&quot;
]

AUTHORIZED_PIN = &quot;1234&quot;</code></pre></pre>
<p>You need to replace the sample RFID UIDs with the actual UIDs of your cards.</p>
<p>The system also defines how long the door will unlock:</p>
<pre><pre><code class="language-cpp">UNLOCK_TIME_SECONDS = 3</code></pre></pre>
<p>This means that when access is granted, the solenoid is released for 3 seconds.</p>
<p>There is also another delay for when the fire alarm or tamper alarm is triggered:</p>
<pre><pre><code class="language-cpp">ALARM_CHECK_INTERVAL_SECONDS = 5</code></pre></pre>
<p>This means that the buzzer will stay in alarm mode for 5 seconds before checking the states of the sensor again.</p>
<hr />
<h2>Relay Control</h2>
<p>The relay is controlled by this function:</p>
<pre><pre><code class="language-cpp">def set_relay(active):
    if RELAY_ACTIVE_HIGH:
        relay.value(1 if active else 0)
    else:
        relay.value(0 if active else 1)</code></pre></pre>
<p>This is useful because some relay modules turn on when the input pin is HIGH, while others turn on when the input pin is LOW.</p>
<p>If your relay works backward, simply change this line:</p>
<pre><pre><code class="language-cpp">RELAY_ACTIVE_HIGH = True</code></pre></pre>
<p>to:</p>
<pre><pre><code class="language-cpp">RELAY_ACTIVE_HIGH = False</code></pre></pre>
<hr />
<h2>LCD Messages</h2>
<p>The function below prints messages to both the LCD and the Thonny shell:</p>
<pre><pre><code class="language-cpp">def lcd_message(line1, line2=z&quot;&gt;&quot;&quot;):
    print(line1, line2)

    if lcd is None:
        return

    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr(line1[:16])

    lcd.move_to(0, 1)
    lcd.putstr(line2[:16])</code></pre></pre>
<p>The</p>
<pre><pre><code class="language-cpp">[:16]</code></pre></pre>
<p>part makes sure that the message fits on a 16-column LCD.</p>
<hr />
<h2>Reading the Keypad</h2>
<p>The keypad is scanned row by row. Each row is pulled LOW one at a time, and the Pico checks which column also becomes LOW.</p>
<pre><pre><code class="language-cpp">def scan_keypad():
    for row_index, row in enumerate(row_pins):
        for r in row_pins:
            r.value(1)

        row.value(0)

        for col_index, col in enumerate(col_pins):
            if col.value() == 0:
                sleep(0.05)

                if col.value() == 0:
                    key = key_map[row_index][col_index]

                    while col.value() == 0:
                        sleep(0.01)

                    return key

    return None</code></pre></pre>
<p>The small delay is used for basic button debounce.</p>
<hr />
<h2>RFID Access Logic</h2>
<p>The RFID function returns the UID of a scanned card:</p>
<pre><pre><code class="language-cpp">uid = read_rfid_card()</code></pre></pre>
<p>Then the main loop checks whether the UID is in the list of authorized cards:</p>
<pre><pre><code class="language-cpp">if uid in AUTHORIZED_CARDS:
    grant_access(&quot;RFID CARD&quot;)
else:
    deny_access(&quot;INVALID CARD&quot;)</code></pre></pre>
<p>This is the simplest way to handle RFID authorization in a beginner project.</p>
<hr />
<h2>PIN Access Logic</h2>
<p>The keypad allows the user to enter a PIN. The</p>
<pre><pre><code class="language-cpp">*</code></pre></pre>
<p>key clears the input, while the</p>
<pre><pre><code class="language-cpp">#</code></pre></pre>
<p>key submits it.</p>
<pre><pre><code class="language-cpp">if key == &quot;*&quot;:
    entered_pin = &quot;&quot;

elif key == &quot;#&quot;:
    if entered_pin == AUTHORIZED_PIN:
        grant_access(&quot;PIN ENTRY&quot;)
    else:
        deny_access(&quot;WRONG PIN&quot;)</code></pre></pre>
<p>This is similar to how many keypad-based access systems work.</p>
<hr />
<h2>Fire Alarm Logic</h2>
<p>The fire alarm input has the highest priority. If it is active, the system immediately runs this function:</p>
<pre><pre><code class="language-cpp">handle_fire_alarm()</code></pre></pre>
<p>Inside that function, the door is unlocked:</p>
<pre><pre><code class="language-cpp">unlock_door()</code></pre></pre>
<p>Then the buzzer and LEDs continue until the fire alarm input clears.</p>
<p>This behavior simulates an emergency door release.</p>
<hr />
<h2>Tamper Alarm Logic</h2>
<p>The tamper alarm works differently from the fire alarm. When tamper is detected, the system keeps the door locked:</p>
<pre><pre><code class="language-cpp">lock_door()</code></pre></pre>
<p>Then it sounds the buzzer and flashes the red LED.</p>
<p>This simulates a forced access or panel tamper condition.</p>
<hr />
<h2>Door Contact Logic</h2>
<p>The optional door contact can detect if the door was opened without a valid unlock command.</p>
<p>In this tutorial, the logic is simplified:</p>
<pre><pre><code class="language-cpp">if is_door_open() and not door_was_unlocked_by_system:
    handle_forced_door()</code></pre></pre>
<p>In a real access control system, this part would usually include a delay timer. For example, after access is granted, the door is allowed to open for a few seconds. If it stays open too long, the controller may trigger a “door held open” alarm.</p>
<p>You can add this as an advanced improvement.</p>
<hr />
<h2>Video</h2>
<p>Here's the project in action:<br />
<iframe loading="lazy" src="https://drive.google.com/file/d/1t0qkxlgHkuYVN1Nwjg1UlPogPIhFxE2y/preview" width="640" height="480"></iframe></p>
<p>Again, I didn't include the relay and solenoid lock for this video. The red LED lighting up means the door is unlocked. Replacing the red LED with the relay module and solenoid lock combination opens the lock when the LED is on.</p>
<h2></h2>
<h2>Testing the Project</h2>
<p>Test the project in stages. Do not connect everything at once.</p>
<h3>Step 1: Test the LCD</h3>
<p>Upload a simple LCD test first and confirm that the LCD can display text.</p>
<h3>Step 2: Test the Keypad</h3>
<p>Print each key to the Thonny shell. Make sure every key is detected correctly.</p>
<h3>Step 3: Test the RFID Reader</h3>
<p>Scan your card and copy the UID from the shell.</p>
<p>Then place the UID inside:</p>
<pre><pre><code class="language-cpp">AUTHORIZED_CARDS = [
    &quot;YOUR CARD UID HERE&quot;
]</code></pre></pre>
<p>Step 4: Test the Relay Without the Solenoid</p>
<p>Before connecting the solenoid, test the relay with only the relay module connected. You should hear the relay click when access is granted.</p>
<h3>Step 5: Test the Solenoid Lock</h3>
<p>Once the relay works, connect the solenoid to its external power supply through the relay contacts.</p>
<p>Do not power the solenoid from the Pico.</p>
<h3>Step 6: Test Fire and Tamper Inputs</h3>
<p>Use push buttons first. Once the logic works, replace the buttons with the flame sensor, magnetic sensor, or vibration sensor.</p>
<hr />
<h3>RFID Reader Not Detected</h3>
<p>Check the SPI wiring carefully. The most common mistake is swapping MOSI and MISO.</p>
<p>Also make sure the RC522 is powered from 3.3V, not 5V.</p>
<h3>LCD Shows Nothing</h3>
<p>Try adjusting the contrast potentiometer on the I2C backpack.</p>
<p>If the LCD still does not work, scan for the I2C address. Some LCD backpacks use 0x27, while others use 0x3F.</p>
<p>Change this line if needed:</p>
<pre><pre><code class="language-cpp">LCD_I2C_ADDR = 0x27</code></pre></pre>
<h3>Relay Works Backward</h3>
<p>Change:</p>
<pre><pre><code class="language-cpp">RELAY_ACTIVE_HIGH = True</code></pre></pre>
<p>to:</p>
<pre><pre><code class="language-cpp">RELAY_ACTIVE_HIGH = False</code></pre></pre>
<h3>Buzzer Is Too Weak</h3>
<p>The passive buzzer may need a transistor driver if you want it to be louder. For simple breadboard testing, direct GPIO control is usually enough.</p>
<h3>Solenoid Resets the Pico</h3>
<p>This usually means the solenoid is causing voltage drops or electrical noise.</p>
<p>Use a separate power supply for the solenoid, connect grounds properly, and add flyback protection if needed.</p>
<hr />
<h2>Improving the Project</h2>
<p>There are many ways to improve this project.</p>
<p>You can add an enrollment mode where an admin card allows new cards to be registered. You can also save authorized cards in the Pico’s flash memory so they are not hardcoded in the program.</p>
<p>Another useful improvement is event logging. The Pico can record events such as:</p>
<pre><pre><code class="language-cpp">ACCESS GRANTED - RFID
ACCESS DENIED - INVALID CARD
ACCESS GRANTED - PIN
FIRE ALARM
TAMPER ALARM
DOOR FORCED</code></pre></pre>
<p>If you use a Raspberry Pi Pico W instead of a regular Pico, you can add a simple web dashboard. This dashboard can show the latest access events and alarm status.</p>
<p>You can also add a real-time clock module to timestamp each event.</p>
<hr />
<h2>Final Thoughts</h2>
<p>This Raspberry Pi Pico access control project combines several important embedded system concepts into one practical build. It uses SPI for RFID, GPIO scanning for the keypad, I2C for the LCD, PWM for the buzzer, and digital outputs for the relay and LEDs.</p>
<p>The best part is that the project behaves like a small version of a real access control controller. It accepts credentials, controls a lock, displays status messages, and responds to alarm inputs.</p>
<p data-is-last-node="" data-is-only-node="">For beginners, MicroPython makes the project easier to understand because the code is readable and the system logic is clear. For a production access control device, I would normally use C or C++ for better structure, timing, and reliability. But for learning how the system works, MicroPython is a very good starting point.</p>
<p>The nice thing about using a kit like the Elecrow Raspberry Pi Pico Advanced Kit is that the project can be expanded easily. After getting the RFID and keypad access working, you can add more modules from the same kit, such as the OLED display, PIR motion sensor, vibration sensor, magnetic spring module, infrared receiver, or ultrasonic sensor.</p>
<p>This makes the project more than just a one-time RFID door lock demo. It becomes a small platform for learning how real embedded control systems handle inputs, outputs, alarms, and user feedback.</p>
<p>The post <a href="https://www.teachmemicro.com/raspberry-pi-pico-access-control-system-using-the-elecrow-pico-advanced-kit/">Raspberry Pi Pico Access Control System Using the Elecrow Pico Advanced Kit</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>ESP32 Time-of-Flight Sensor Tutorial (VL53L0X / VL53L1X Distance Measurement)</title>
		<link>https://www.teachmemicro.com/esp32-time-of-flight-sensor-vl53l0x/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=esp32-time-of-flight-sensor-vl53l0x</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Thu, 30 Apr 2026 21:45:36 +0000</pubDate>
				<category><![CDATA[ESP32 Projects]]></category>
		<category><![CDATA[Sensor Tutorial]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=11246</guid>

					<description><![CDATA[<p>When I first moved from ultrasonic sensors to Time-of-Flight (ToF) sensors, the difference was night and day. Measurements became faster, more stable, and far more precise. In this tutorial, I’ll show you exactly how I use a ToF sensor like the VL53L0X with the ESP32. What is a Time-of-Flight Sensor? A Time-of-Flight sensor measures distance &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/esp32-time-of-flight-sensor-vl53l0x/">ESP32 Time-of-Flight Sensor Tutorial (VL53L0X / VL53L1X Distance Measurement)</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>When I first moved from ultrasonic sensors to Time-of-Flight (ToF) sensors, the difference was night and day. Measurements became faster, more stable, and far more precise. In this tutorial, I’ll show you exactly how I use a ToF sensor like the VL53L0X with the ESP32.</p>
<h3><b>What is a Time-of-Flight Sensor?</b></h3>
<p>A Time-of-Flight sensor measures distance using light instead of sound. It emits an infrared laser and calculates how long it takes for the reflection to return.</p>
<p><a href="https://www.teachmemicro.com/wp-content/uploads/2026/05/tof-sensor-concept.avif"><img data-dominant-color="29282c" data-has-transparency="false" style="--dominant-color: #29282c;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-11248 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/05/tof-sensor-concept.avif" alt="VL53L0X ToF concept" width="768" height="512" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/05/tof-sensor-concept.avif 768w, https://www.teachmemicro.com/wp-content/uploads/2026/05/tof-sensor-concept-300x200.avif 300w" sizes="auto, (max-width: 768px) 100vw, 768px" /></a></p>
<p>Because it uses light, it avoids common ultrasonic issues like:</p>
<ul>
<li>Poor readings on soft surfaces</li>
<li>Wide beam angles</li>
<li>Slow response time</li>
</ul>
<p>With ToF sensors, I consistently get millimeter-level precision, especially indoors.</p>
<h3><b>Why Use ESP32 with ToF Sensors?</b></h3>
<p>The ESP32 is perfect for this because:</p>
<ul>
<li>Built-in I2C makes wiring simple</li>
<li>Plenty of processing power for filtering</li>
<li>Easy to expand into WiFi dashboards later</li>
</ul>
<p>This combo is something I often use for smart sensing projects.</p>
<h3><b>Materials</b></h3>
<p>For this setup, I used:</p>
<ul>
<li>ESP32 development board</li>
<li>VL53L0X (or VL53L1X) ToF sensor</li>
<li>Breadboard</li>
<li>Jumper wires</li>
</ul>
<h3><b>Wiring the ToF Sensor to ESP32</b></h3>
<p>The ToF sensor uses I2C, so wiring is straightforward.</p>
<table>
<tbody>
<tr>
<td><b>ToF Sensor</b></td>
<td><b>ESP32</b></td>
</tr>
<tr>
<td>VCC</td>
<td>3.3V</td>
</tr>
<tr>
<td>GND</td>
<td>GND</td>
</tr>
<tr>
<td>SDA</td>
<td>GPIO 21</td>
</tr>
<tr>
<td>SCL</td>
<td>GPIO 22</td>
</tr>
</tbody>
</table>
<p>I always stick to 3.3V with ESP32 to avoid voltage compatibility issues.</p>
<h3><b>Installing the Library</b></h3>
<p>In Arduino IDE, install:</p>
<ul>
<li><b>Adafruit VL53L0X</b></li>
</ul>
<p>This library is reliable and easy to use, especially if you’re just starting.</p>
<h3><b>Basic ESP32 ToF Example Code</b></h3>
<p>Here’s the exact code I use for quick testing:</p>
<pre><pre><code class="language-cpp">#include &lt;Wire.h&gt;
#include &quot;Adafruit_VL53L0X.h&quot;

Adafruit_VL53L0X lox = Adafruit_VL53L0X();

void setup() {
 Serial.begin(115200);
 Wire.begin();

 if (!lox.begin()) {
   Serial.println(&quot;Failed to initialize VL53L0X&quot;);
   while (1);
 }
 Serial.println(&quot;ToF Sensor Ready!&quot;);
}

void loop() {

 VL53L0X_RangingMeasurementData_t measure;
 lox.rangingTest(&amp;measure, false);

 if (measure.RangeStatus != 4) {
   Serial.print(&quot;Distance: &quot;);
   Serial.print(measure.RangeMilliMeter);
   Serial.println(&quot; mm&quot;);
 } else {
   Serial.println(&quot;Out of range&quot;);
 }

 delay(500);
}</code></pre></pre>
<h3><b>What the Output Means</b></h3>
<p>The sensor returns distance in millimeters.</p>
<p>Typical ranges:</p>
<ul>
<li>VL53L0X → up to ~2 meters</li>
<li>VL53L1X → up to ~4 meters</li>
</ul>
<p>If you see <i>“Out of range”</i>, it usually means:</p>
<ul>
<li>Object is too far</li>
<li>Surface is not reflective enough</li>
</ul>
<h3><b>Improving Accuracy </b></h3>
<p>Raw readings can still fluctuate slightly. In real projects, I always apply simple filtering.</p>
<p>Here’s a quick averaging function:</p>
<pre><pre><code class="language-cpp">int get_average_distance(int samples) {
 int sum = 0;
 int valid = 0;

 for (int i = 0; i &lt; samples; i++) {
   VL53L0X_RangingMeasurementData_t measure;
   lox.rangingTest(&amp;measure, false);

   if (measure.RangeStatus != 4) {
     sum += measure.RangeMilliMeter;
     valid++;
   }

   delay(50);
 }

 if (valid == 0) return -1;
 return sum / valid;
}</code></pre></pre>
<p>This alone dramatically improves stability.</p>
<h3><b>Common Problems and Fixes</b></h3>
<h3><b>Sensor Not Detected</b></h3>
<p>I usually check:</p>
<ul>
<li>SDA/SCL swapped</li>
<li>Loose wiring</li>
<li>I2C address (normally 0x29)</li>
</ul>
<h3><b>Constant Invalid Readings</b></h3>
<p>This often comes from:</p>
<ul>
<li>Weak power supply</li>
<li>Too fast reading loop</li>
</ul>
<p>Adding delay usually fixes it.</p>
<h3><b>Noisy Measurements</b></h3>
<p>If readings jump around:</p>
<ul>
<li>Use averaging</li>
<li>Avoid direct sunlight</li>
<li>Keep sensor steady</li>
</ul>
<h3><b>Real-World Applications</b></h3>
<p>Once this is working, you can easily expand it into:</p>
<ul>
<li>Touchless switches</li>
<li>Distance-triggered automation</li>
<li>Liquid level sensing</li>
<li>Robot obstacle detection</li>
<li>Smart parking systems</li>
</ul>
<p>This sensor is one of those components that scales really well from beginner to advanced projects.</p>
<p>&nbsp;</p>
<h3><b>Example Project: Touchless Switch (ToF + Relay)</b></h3>
<p>One of my favorite practical uses of a ToF sensor is creating a <b>touchless switch</b>. Instead of physically pressing a button, you simply place your hand in front of the sensor to toggle a device—like a light or fan.</p>
<p>This is especially useful for:</p>
<ul>
<li>Hygiene (no physical contact)</li>
<li>Smart home projects</li>
<li>Modern UI interactions</li>
</ul>
<h3><b>How It Works</b></h3>
<p>The idea is simple:</p>
<ul>
<li>The ToF sensor continuously measures distance</li>
<li>When your hand comes within a threshold (e.g., &lt; 100 mm), it triggers an action</li>
<li>The ESP32 toggles a relay module</li>
<li>The relay acts like a physical switch (ON/OFF)</li>
</ul>
<p>I usually add a small delay or state logic to prevent repeated triggering.</p>
<h3><b>Additional Components</b></h3>
<p>For this project, I added:</p>
<ul>
<li>1-channel relay module (3.3V or 5V compatible)</li>
<li>External load (e.g., lamp, fan, or test LED)</li>
</ul>
<h3><b>Relay Wiring</b></h3>
<table>
<tbody>
<tr>
<td><b>Relay Module</b></td>
<td><b>ESP32</b></td>
</tr>
<tr>
<td>VCC</td>
<td>5V or 3.3V</td>
</tr>
<tr>
<td>GND</td>
<td>GND</td>
</tr>
<tr>
<td>IN</td>
<td>GPIO 26</td>
</tr>
</tbody>
</table>
<p>Note: Some relay modules are <b>active LOW</b>, meaning:</p>
<ul>
<li>LOW → ON</li>
<li>HIGH → OFF</li>
</ul>
<h3><b>Concept Logic</b></h3>
<p>Instead of constantly switching while your hand is present, I use a <b>toggle mechanism</b>:</p>
<ol>
<li>Detect hand within threshold</li>
<li>Toggle relay state</li>
<li>Wait until hand is removed before detecting again</li>
</ol>
<p>This prevents flickering or rapid switching.</p>
<h3><b>Example Code: Touchless Toggle Switch</b></h3>
<pre><pre><code class="language-cpp">#include &lt;Wire.h&gt;
#include &quot;Adafruit_VL53L0X.h&quot;
#define RELAY_PIN 26
#define DIST_THRESHOLD 100  // mm

Adafruit_VL53L0X lox = Adafruit_VL53L0X();
bool relay_state = false;
bool hand_detected = false;

void setup() {
 Serial.begin(115200);
 Wire.begin();

 pinMode(RELAY_PIN, OUTPUT);
 digitalWrite(RELAY_PIN, HIGH); // OFF (for active LOW relay)

 if (!lox.begin()) {
   Serial.println(&quot;Failed to initialize VL53L0X&quot;);
   while (1);
 }

 Serial.println(&quot;Touchless Switch Ready&quot;);

}


void loop() {

 VL53L0X_RangingMeasurementData_t measure;
 lox.rangingTest(&amp;measure, false);

 if (measure.RangeStatus != 4) {
   int distance = measure.RangeMilliMeter;
   Serial.print(&quot;Distance: &quot;);
   Serial.println(distance);

   // Hand detected

   if (distance &lt; DIST_THRESHOLD &amp;&amp; !hand_detected) {
     relay_state = !relay_state;
     digitalWrite(RELAY_PIN, relay_state ? LOW : HIGH);
     Serial.println(relay_state ? &quot;Relay ON&quot; : &quot;Relay OFF&quot;);
     hand_detected = true;
     delay(300); // debounce
   }

   // Hand removed
   if (distance &gt;= DIST_THRESHOLD) {
     hand_detected = false;
   }
 }
 delay(50);
}</code></pre></pre>
<h3><b>Behavior Explanation</b></h3>
<p>Here’s what actually happens when I use it:</p>
<ul>
<li>I move my hand close to the sensor</li>
<li>The ESP32 detects distance &lt; 100 mm</li>
<li>The relay toggles ON</li>
<li>I remove my hand → system resets</li>
<li>Next gesture toggles it OFF</li>
</ul>
<p>It feels very natural—almost like a “gesture button”.</p>
<h3><b>Safety Notes (Important)</b></h3>
<p>If you’re controlling AC devices:</p>
<ul>
<li>Use proper relay modules with isolation</li>
<li>Never touch exposed wiring</li>
<li>Consider using a relay module with optocoupler</li>
</ul>
<p>For testing, I recommend starting with:</p>
<ul>
<li>LED + resistor instead of mains devices</li>
</ul>
<h3><b>Improvements I Usually Add</b></h3>
<p>In real builds, I often extend this by:</p>
<ul>
<li>Adding <b>LED indicator</b> for ON/OFF state</li>
<li>Using <b>moving average filtering</b> for stable detection</li>
<li>Adding <b>sound or buzzer feedback</b></li>
<li>Integrating with <b>WiFi control (web dashboard or MQTT)</b></li>
</ul>
<h3><b>Conclusion</b></h3>
<p>Using a Time-of-Flight sensor with ESP32 is one of the easiest ways to build accurate distance-based projects. Once I started using these sensors, I stopped relying on ultrasonic modules for most indoor applications.</p>
<p>They’re precise, fast, and incredibly easy to integrate.</p>
<p>The post <a href="https://www.teachmemicro.com/esp32-time-of-flight-sensor-vl53l0x/">ESP32 Time-of-Flight Sensor Tutorial (VL53L0X / VL53L1X Distance Measurement)</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Use an NPK Soil Sensor with ESP32</title>
		<link>https://www.teachmemicro.com/how-to-use-an-npk-soil-sensor-with-esp32/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=how-to-use-an-npk-soil-sensor-with-esp32</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Tue, 21 Apr 2026 00:35:55 +0000</pubDate>
				<category><![CDATA[ESP32 Projects]]></category>
		<category><![CDATA[Sensor Tutorial]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=11170</guid>

					<description><![CDATA[<p>If you are building a smart agriculture project, one of the most useful things you can measure is the nutrient condition of the soil. Moisture sensors can tell you if the soil is wet or dry, but they cannot tell you whether the soil still has enough nutrients for healthy plant growth. This is where &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/how-to-use-an-npk-soil-sensor-with-esp32/">How to Use an NPK Soil Sensor with ESP32</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>If you are building a smart agriculture project, one of the most useful things you can measure is the nutrient condition of the soil. Moisture sensors can tell you if the soil is wet or dry, but they cannot tell you whether the soil still has enough nutrients for healthy plant growth. This is where an NPK sensor becomes useful.</p>
<p><span id="more-11170"></span></p>
<p>An NPK sensor measures the levels of Nitrogen (N), Phosphorus (P), and Potassium (K) in soil. These three nutrients are the primary macronutrients needed by plants. In this tutorial, I will show you how to connect an RS485 NPK sensor to an ESP32, read the values using Modbus RTU, and then create a simple web dashboard so you can monitor the readings from your browser without needing to keep the Serial Monitor open.</p>
<h3><strong>What is an NPK sensor?</strong></h3>
<p>An NPK sensor is a soil probe designed to estimate the levels of nitrogen, phosphorus, and potassium present in the soil. These values are commonly reported in mg/kg or in similar concentration-based units depending on the sensor model.</p>
<p><img decoding="async" class="aligncenter" src="https://www.teachmemicro.com/wp-content/uploads/2023/05/npk-sensor.png" /></p>
<p>Most low-cost NPK sensors used in hobby and prototype projects do not output simple analog voltages. Instead, they usually communicate using RS485 and Modbus RTU. Because of this, you cannot connect the sensor directly to a normal ESP32 GPIO pin and expect useful readings. The ESP32 needs help from an RS485-to-TTL converter, usually a MAX485 module or something similar.</p>
<p>This sounds more complicated at first, but once the wiring and Modbus request frames are set up correctly, reading the sensor becomes straightforward.</p>
<p>&nbsp;</p>
<h4><strong>How the sensor communicates</strong></h4>
<p>The type of NPK sensor used in this tutorial communicates over RS485, which is commonly used in industrial devices because it works well over longer cable distances and in electrically noisy environments.</p>
<p>The protocol on top of RS485 is usually Modbus RTU. In simple terms, the ESP32 sends a request asking for a specific register, and the sensor replies with the data stored in that register.</p>
<p><img decoding="async" class="aligncenter" src="https://www.teachmemicro.com/wp-content/uploads/2023/05/DataByte.gif" alt="logic signals - RS232 vs TTL vs RS485" /></p>
<p>For many common NPK sensors, the nutrient registers are as follows:</p>
<blockquote><p>Nitrogen at register 0x001E</p>
<p>Phosphorus at register 0x001F</p>
<p>Potassium at register 0x0020</p></blockquote>
<p>The ESP32 must send a proper Modbus frame including the CRC bytes at the end. The sensor then replies with the value.</p>
<p>&nbsp;</p>
<h3><strong>Materials needed</strong></h3>
<p>You will need the following parts for this project:</p>
<ul>
<li>ESP32 development board</li>
<li>RS485 NPK soil sensor</li>
<li>MAX485 or equivalent RS485-to-TTL converter</li>
<li>Jumper wires</li>
<li>External power supply for the NPK sensor</li>
<li>Breadboard or terminal connections as needed</li>
</ul>
<p>The external power supply is important because many NPK sensors require 9V to 24V, and they will not work correctly from the ESP32’s 3.3V pin.</p>
<p><img decoding="async" class="aligncenter" src="https://www.teachmemicro.com/wp-content/uploads/2023/05/rs485-transceiver-module-1024x683.png" /></p>
<h3><strong>Wiring the NPK sensor to the ESP32</strong></h3>
<p>The RS485 lines from the sensor connect to the A and B terminals of the MAX485 module. The MAX485 then converts the differential RS485 signals into TTL serial that the ESP32 can understand.</p>
<p>The typical connections are as follows.</p>
<h4><strong>NPK sensor to MAX485</strong></h4>
<ul>
<li>Sensor A to MAX485 A</li>
<li>Sensor B to MAX485 B</li>
<li>Sensor VCC to external sensor supply positive</li>
<li>Sensor GND to external sensor supply ground</li>
</ul>
<h4><strong>MAX485 to ESP32</strong></h4>
<ul>
<li>RO to ESP32 RX pin</li>
<li>DI to ESP32 TX pin</li>
<li>RE and DE tied together, then connected to one ESP32 GPIO</li>
<li>VCC to ESP32 3.3V or module-rated logic supply</li>
<li>GND to ESP32 GND</li>
</ul>
<p>For this tutorial, I will use these ESP32 pins:</p>
<ul>
<li>MAX485 RO to GPIO16</li>
<li>MAX485 DI to GPIO17</li>
<li>MAX485 RE+DE to GPIO4</li>
</ul>
<p>The ESP32 and the sensor power supply must share a common ground. Without a common ground, communication may fail or become unstable.</p>
<h4><strong>Why the RE/DE pin matters</strong></h4>
<p>RS485 communication is usually half-duplex. This means the same pair of wires is used for both sending and receiving data, but not at the same time.</p>
<p>The MAX485 module uses the RE and DE pins to control whether it is transmitting or receiving. In many hobby circuits, these two pins are tied together and driven by one ESP32 GPIO.</p>
<p>When the control pin is HIGH, the module transmits. When it is LOW, the module listens for incoming data. So every time the ESP32 sends a Modbus request, it must briefly switch the MAX485 into transmit mode, then immediately switch back to receive mode so it can catch the sensor’s reply.</p>
<p>&nbsp;</p>
<h3><strong>Basic ESP32 code to read NPK values</strong></h3>
<p>Before jumping into a web dashboard, I always prefer starting with the simplest working version. That means reading the values first and printing them to the Serial Monitor. Once that works, it becomes much easier to build additional features on top.</p>
<p>Here is the basic code:</p>
<pre><pre><code class="language-cpp">#include &lt;HardwareSerial.h&gt;

#define RXD2 16
#define TXD2 17
#define RS485_DIR 4

HardwareSerial npk_serial(2);

byte nitrogen_cmd[]   = {0x01, 0x03, 0x00, 0x1E, 0x00, 0x01, 0xE4, 0x0C};
byte phosphorus_cmd[] = {0x01, 0x03, 0x00, 0x1F, 0x00, 0x01, 0xB5, 0xCC};
byte potassium_cmd[]  = {0x01, 0x03, 0x00, 0x20, 0x00, 0x01, 0x85, 0xC0};


void setup() {
  Serial.begin(115200);
  npk_serial.begin(9600, SERIAL_8N1, RXD2, TXD2);
  pinMode(RS485_DIR, OUTPUT);
  digitalWrite(RS485_DIR, LOW);
}

int read_npk_value(byte *command) {
  while (npk_serial.available()) {
    npk_serial.read();
  }

  digitalWrite(RS485_DIR, HIGH);
  delay(10);

  npk_serial.write(command, 8);
  npk_serial.flush();

  digitalWrite(RS485_DIR, LOW);
  delay(20);

 

  byte response[7];
  int index = 0;
  unsigned long start_time = millis();

 

  while ((millis() - start_time) &lt; 1000 &amp;&amp; index &lt; 7) {
    if (npk_serial.available()) {
      response[index++] = npk_serial.read();
    }
  }

 
  if (index == 7) {
    return response[4];
  }

   return -1;

}

 

void loop() {
  int nitrogen = read_npk_value(nitrogen_cmd);
  int phosphorus = read_npk_value(phosphorus_cmd);
  int potassium = read_npk_value(potassium_cmd);

 

  Serial.print(&quot;Nitrogen: &quot;);
  Serial.print(nitrogen);
  Serial.print(&quot; mg/kg, Phosphorus: &quot;);
  Serial.print(phosphorus);
  Serial.print(&quot; mg/kg, Potassium: &quot;);
  Serial.print(potassium);
  Serial.println(&quot; mg/kg&quot;);

  delay(2000);
}</code></pre></pre>
<h4><strong>Understanding the code</strong></h4>
<p>The ESP32 is using HardwareSerial(2) so that we can use a second UART for the sensor. That keeps the main Serial Monitor free for debugging.</p>
<p>Each nutrient has its own Modbus command frame. These command frames already include the sensor address, function code, register address, number of registers to read, and CRC bytes.</p>
<p>The read_npk_value() function performs four important steps:</p>
<p>First, it clears any old bytes from the UART buffer. This prevents leftover data from earlier reads from corrupting the next reading.</p>
<p>Second, it sets the RS485 direction pin HIGH so the MAX485 goes into transmit mode.</p>
<p>Third, it writes the Modbus command to the sensor.</p>
<p>Fourth, it switches the RS485 module back to receive mode and waits for the reply.</p>
<p>If the sensor responds correctly, the function returns the value byte. If not, it returns -1, which is useful for indicating a failed read.</p>
<h4><strong>Expected Serial Monitor output</strong></h4>
<p>If everything is wired correctly, you should see values similar to this:</p>
<blockquote><p>Nitrogen: 32 mg/kg, Phosphorus: 14 mg/kg, Potassium: 56 mg/kg</p>
<p>Nitrogen: 31 mg/kg, Phosphorus: 14 mg/kg, Potassium: 57 mg/kg</p>
<p>Nitrogen: 32 mg/kg, Phosphorus: 15 mg/kg, Potassium: 56 mg/kg</p></blockquote>
<p>The numbers will vary depending on your soil and sensor model. Some cheaper sensors may not be highly accurate, but they are still useful for monitoring trends and comparing one soil condition against another.</p>
<h4><strong>Looking at the Modbus response</strong></h4>
<p>A typical sensor reply might look something like this:</p>
<blockquote><p>01 03 02 00 20 B8 44</p></blockquote>
<p>This can be interpreted as follows:</p>
<ul>
<li>01 is the sensor address</li>
<li>03 is the Modbus function code for reading holding registers</li>
<li>02 means two data bytes follow</li>
<li>00 20 is the returned value in hexadecimal</li>
<li>B8 44 is the CRC</li>
</ul>
<p>Hex 0x0020 is decimal 32, so that would mean the nutrient value is 32.</p>
<p>In the basic example above, I kept things simple and returned response[4], since many sensors return small values that fit in one byte. However, if your sensor returns larger values, you may need to combine both data bytes like this:</p>
<pre>int value = (response[3] &lt;&lt; 8) | response[4];</pre>
<p>&nbsp;</p>
<p>That is the safer approach if you expect readings greater than 255.</p>
<p>&nbsp;</p>
<h3><strong>Adding a web dashboard</strong></h3>
<p>Once I can see valid readings in the Serial Monitor, the next step is making the ESP32 serve those values on a web page. This way, I can open the ESP32 IP address in a browser on my phone or laptop and watch the nutrient values update automatically.</p>
<p>To keep things simple, I will use the built-in WiFi.h and WebServer.h libraries. The browser will periodically request the latest readings from a /data endpoint, and JavaScript on the page will update the displayed values automatically.</p>
<p>This approach is simple, reliable, and easy to understand.</p>
<h4><strong>How the web dashboard works</strong></h4>
<p>The idea is straightforward:</p>
<p>The ESP32 connects to Wi-Fi and starts a web server. The main page contains some HTML and JavaScript. Every two seconds, the JavaScript sends a request to /data. The ESP32 responds with the latest NPK values in JSON format. The browser then updates the page without needing a manual refresh.</p>
<p>So while the sensor continues to be read by the ESP32, the browser becomes a live dashboard.</p>
<h4><strong>ESP32 NPK web dashboard code</strong></h4>
<pre><pre><code class="language-cpp">#include &lt;WiFi.h&gt;
#include &lt;WebServer.h&gt;
#include &lt;HardwareSerial.h&gt;

#define RXD2 16
#define TXD2 17
#define RS485_DIR 4

const char* ssid = &quot;YOUR_WIFI_NAME&quot;;
const char* password = &quot;YOUR_WIFI_PASSWORD&quot;;

HardwareSerial npk_serial(2);
WebServer server(80);

byte nitrogen_cmd[]   = {0x01, 0x03, 0x00, 0x1E, 0x00, 0x01, 0xE4, 0x0C};
byte phosphorus_cmd[] = {0x01, 0x03, 0x00, 0x1F, 0x00, 0x01, 0xB5, 0xCC};
byte potassium_cmd[]  = {0x01, 0x03, 0x00, 0x20, 0x00, 0x01, 0x85, 0xC0};

int nitrogen_value = -1;
int phosphorus_value = -1;
int potassium_value = -1;

unsigned long last_read_time = 0;
const unsigned long read_interval = 2000;

const char dashboard_html[] PROGMEM = R&quot;rawliteral(
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;
  &lt;title&gt;NPK Sensor Dashboard&lt;/title&gt;
  &lt;style&gt;
    body {
      font-family: Arial, sans-serif;
      text-align: center;
      margin: 0;
      padding: 20px;
      background: #f4f4f4;
    }
    .container {
      max-width: 500px;
      margin: auto;
    }
    .card {
      background: white;
      margin: 15px 0;
      padding: 20px;
      border-radius: 12px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }
    .label {
      font-size: 18px;
      color: #555;
    }
    .value {
      font-size: 36px;
      font-weight: bold;
      margin-top: 10px;
    }
    .unit {
      font-size: 16px;
      color: #777;
    }
    .status {
      margin-top: 20px;
      color: #666;
      font-size: 14px;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;h2&gt;NPK Soil Sensor Dashboard&lt;/h2&gt;
    &lt;div class=&quot;card&quot;&gt;
      &lt;div class=&quot;label&quot;&gt;Nitrogen&lt;/div&gt;
      &lt;div class=&quot;value&quot; id=&quot;nitrogen&quot;&gt;--&lt;/div&gt;
      &lt;div class=&quot;unit&quot;&gt;mg/kg&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;card&quot;&gt;
      &lt;div class=&quot;label&quot;&gt;Phosphorus&lt;/div&gt;
      &lt;div class=&quot;value&quot; id=&quot;phosphorus&quot;&gt;--&lt;/div&gt;
      &lt;div class=&quot;unit&quot;&gt;mg/kg&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;card&quot;&gt;
      &lt;div class=&quot;label&quot;&gt;Potassium&lt;/div&gt;
      &lt;div class=&quot;value&quot; id=&quot;potassium&quot;&gt;--&lt;/div&gt;
      &lt;div class=&quot;unit&quot;&gt;mg/kg&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;status&quot; id=&quot;status&quot;&gt;Waiting for data...&lt;/div&gt;
  &lt;/div&gt;
  &lt;script&gt;
    function updateData() {
      fetch(&#039;/data&#039;)
        .then(response =&gt; response.json())
        .then(data =&gt; {
          document.getElementById(&#039;nitrogen&#039;).innerText = data.nitrogen;
          document.getElementById(&#039;phosphorus&#039;).innerText = data.phosphorus;
          document.getElementById(&#039;potassium&#039;).innerText = data.potassium;
          document.getElementById(&#039;status&#039;).innerText =
            &#039;Last updated: &#039; + new Date().toLocaleTimeString();
        })
        .catch(error =&gt; {
          document.getElementById(&#039;status&#039;).innerText = &#039;Connection error&#039;;
        });
    }
    updateData();
    setInterval(updateData, 2000);
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
)rawliteral&quot;;
 

int read_npk_value(byte *command) {
  while (npk_serial.available()) {
    npk_serial.read();
  }

  digitalWrite(RS485_DIR, HIGH);
  delay(10);

  npk_serial.write(command, 8);
  npk_serial.flush();

  digitalWrite(RS485_DIR, LOW);
  delay(20);

  byte response[7];
  int index = 0;
  unsigned long start_time = millis();

  while ((millis() - start_time) &lt; 1000 &amp;&amp; index &lt; 7) {
    if (npk_serial.available()) {
      response[index++] = npk_serial.read();
    }
  }
 
  if (index == 7) {
    return (response[3] &lt;&lt; 8) | response[4];
  }
 
  return -1;
}

void update_sensor_values() {
  nitrogen_value = read_npk_value(nitrogen_cmd);
  delay(100);

  phosphorus_value = read_npk_value(phosphorus_cmd);
  delay(100);

  potassium_value = read_npk_value(potassium_cmd);
}

void handle_root() {
  server.send(200, &quot;text/html&quot;, dashboard_html);
}

void handle_data() {
  String json = &quot;{&quot;;
  json += &quot;\&quot;nitrogen\&quot;:&quot; + String(nitrogen_value) + &quot;,&quot;;
  json += &quot;\&quot;phosphorus\&quot;:&quot; + String(phosphorus_value) + &quot;,&quot;;
  json += &quot;\&quot;potassium\&quot;:&quot; + String(potassium_value);
  json += &quot;}&quot;;
  server.send(200, &quot;application/json&quot;, json);
}

void setup() {
  Serial.begin(115200);
  npk_serial.begin(9600, SERIAL_8N1, RXD2, TXD2);
  pinMode(RS485_DIR, OUTPUT);
  digitalWrite(RS485_DIR, LOW);
  WiFi.begin(ssid, password);
  Serial.print(&quot;Connecting to WiFi&quot;);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(&quot;.&quot;);
  }

  Serial.println();
  Serial.print(&quot;Connected. ESP32 IP address: &quot;);
  Serial.println(WiFi.localIP());

  update_sensor_values();

  server.on(&quot;/&quot;, handle_root);
  server.on(&quot;/data&quot;, handle_data);
  server.begin();

  Serial.println(&quot;Web server started&quot;);
}

void loop() {
  server.handleClient();

  if (millis() - last_read_time &gt;= read_interval) {
    last_read_time = millis();
    update_sensor_values();
 
    Serial.print(&quot;N: &quot;);
    Serial.print(nitrogen_value);
    Serial.print(&quot;  P: &quot;);
    Serial.print(phosphorus_value);
    Serial.print(&quot;  K: &quot;);
    Serial.println(potassium_value);
  }
}</code></pre></pre>
<h4><strong>What this code adds</strong></h4>
<p>This version builds on the earlier example. It still reads the sensor the same way, but now it also does three additional things.</p>
<p>First, it connects the ESP32 to your Wi-Fi network.</p>
<p>Second, it starts a web server and serves a dashboard page when you visit the ESP32 IP address.</p>
<p>Third, it creates a /data endpoint that sends the current nitrogen, phosphorus, and potassium readings in JSON format.</p>
<p>The JavaScript in the browser requests this JSON every two seconds, so the dashboard updates automatically.</p>
<h4><strong>Viewing the dashboard</strong></h4>
<p>After uploading the code, open the Serial Monitor. Once the ESP32 connects to Wi-Fi, it will print something like this:</p>
<blockquote><p>Connected. ESP32 IP address: 192.168.1.25</p>
<p>Web server started</p></blockquote>
<p>Open that IP address in your browser. You should see a simple dashboard showing the current nitrogen, phosphorus, and potassium values.</p>
<p>As the ESP32 reads new values, the page updates automatically every two seconds. There is no need to refresh the browser manually.</p>
<h4><strong>Why this approach is useful</strong></h4>
<p>A web dashboard makes the project much more practical. Instead of connecting the ESP32 to a computer and opening the Serial Monitor every time, you can simply use your phone or laptop on the same Wi-Fi network.</p>
<p>This is useful for greenhouse monitoring, classroom demonstrations, farm prototypes, and remote sensor nodes where you want a quick way to view live data.</p>
<p>Once this basic dashboard is working, you can later expand it into something more advanced such as charts, historical logging, cloud upload, or mobile notifications.</p>
<p>&nbsp;</p>
<h3><strong>Common problems and fixes</strong></h3>
<p>One of the most common issues is getting -1 values from the sensor. This usually means the ESP32 did not receive a valid response. If this happens, I would first check the A and B lines on the RS485 connection. Some sensors label these differently, so swapping them is often worth trying.</p>
<p>Another common issue is power. Many NPK sensors draw more power than expected and require a stable external supply. If the supply is weak or noisy, the readings may fail or jump around.</p>
<p>If the web page loads but the values do not update, check whether the ESP32 is still reading the sensor properly in the Serial Monitor. If the Serial Monitor values are wrong, the browser will simply display those same wrong values.</p>
<p>If the browser cannot load the dashboard at all, verify that your phone or laptop is on the same Wi-Fi network as the ESP32.</p>
<h4><strong>Possible improvements</strong></h4>
<p>This version is intentionally simple so it is easy to follow. But once you understand the flow, there are several ways to improve it.</p>
<p>You could add color indicators so the dashboard visually shows low, medium, or high nutrient levels. You could also add a chart so you can see how values change over time. Another useful upgrade would be to save the readings into EEPROM, SPIFFS, an SD card, or a cloud database.</p>
<p>If you want smoother real-time updates, you can also replace periodic fetch() requests with WebSockets, but for many projects the current method is already enough.</p>
<p>&nbsp;</p>
<h3><strong>Final thoughts</strong></h3>
<p>Using an NPK sensor with an ESP32 is a very good beginner-to-intermediate project because it introduces several useful topics at once. You learn how to work with RS485, how Modbus RTU communication works, how to read industrial-style sensors, and how to turn the ESP32 into a live monitoring device with a browser-based dashboard.</p>
<p>I like this kind of project because it moves beyond simple LEDs and basic analog sensors. It feels much closer to how real agricultural and industrial monitoring systems are built.</p>
<p>The most important thing is to get the basic serial-reading version working first. Once you can reliably read nitrogen, phosphorus, and potassium values, adding the web dashboard becomes much easier.</p>
<p>The post <a href="https://www.teachmemicro.com/how-to-use-an-npk-soil-sensor-with-esp32/">How to Use an NPK Soil Sensor with ESP32</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>ESP32 + INMP441 I2S Microphone with Live Web Graph</title>
		<link>https://www.teachmemicro.com/esp32-inmp441-i2s-microphone-with-live-web-graph/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=esp32-inmp441-i2s-microphone-with-live-web-graph</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Thu, 16 Apr 2026 22:57:19 +0000</pubDate>
				<category><![CDATA[ESP32 Projects]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=11133</guid>

					<description><![CDATA[<p>In this project, I’ll show you how I interface the ESP32 with the INMP441 I2S microphone module, starting from basic audio capture and then moving toward a more interactive application where the sound is visualized in real time on a web browser. When I first worked with microphones on microcontrollers, I used analog modules. They &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/esp32-inmp441-i2s-microphone-with-live-web-graph/">ESP32 + INMP441 I2S Microphone with Live Web Graph</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>In this project, I’ll show you how I interface the <b>ESP32</b> with the <b>INMP441 I2S microphone module</b>, starting from basic audio capture and then moving toward a more interactive application where the sound is visualized in real time on a web browser.</p>
<p>When I first worked with microphones on microcontrollers, I used analog modules. They worked, but noise and signal stability were always a concern. The INMP441 solves that by outputting digital audio through I2S. Once I got it working with the ESP32, the readings became much more consistent and reliable.</p>
<p>To make this project more practical, I won’t stop at just reading audio values. I’ll also show how to turn the ESP32 into a web server that displays a live graph of the captured sound—no page refresh needed.</p>
<h3><b>Materials</b></h3>
<ul>
<li style="font-weight: 400;" aria-level="1">ESP32 development board</li>
<li style="font-weight: 400;" aria-level="1">INMP441 I2S microphone module</li>
<li style="font-weight: 400;" aria-level="1">Jumper wires</li>
<li style="font-weight: 400;" aria-level="1">Breadboard</li>
<li style="font-weight: 400;" aria-level="1">Wi-Fi network</li>
</ul>
<h3><b>Understanding the INMP441 and I2S</b></h3>
<p>The INMP441 is a MEMS microphone with a built-in ADC, which means it outputs digital audio instead of an analog voltage. Because of this, the ESP32 uses its I2S peripheral instead of the ADC.</p>
<p>In this setup, the ESP32 acts as the master and generates the clock signals, while the microphone sends audio data back in sync. The key signals involved are the serial clock, word select, and serial data lines.</p>
<p>Although I2S supports stereo, the INMP441 outputs only one channel. The channel depends on the L/R pin connection.</p>
<h3><b>Wiring the ESP32 to INMP441</b></h3>
<p>The connections are simple once you understand the signals. I typically use GPIO26 for the clock, GPIO25 for the word select line, and GPIO33 for the data input.</p>
<p>The L/R pin is connected to ground so the microphone outputs on the left channel. The ESP32 is then configured to read only that channel.</p>
<p>Make sure to power the module using 3.3V. Using 5V can damage it.</p>
<h3><b>Basic Example: Reading Audio Data</b></h3>
<p>Before building anything more advanced, I always start by verifying that the microphone is working correctly. The simplest way to do that is to read the audio samples and print them to the Serial Monitor.</p>
<p>At first glance, the values may look random, but they actually represent the sound waveform.</p>
<h3><b>Example Code</b></h3>
<pre><pre><code class="language-cpp">#include &quot;driver/i2s.h&quot;

#define I2S_WS 25
#define I2S_SD 33
#define I2S_SCK 26

#define I2S_PORT I2S_NUM_0
#define BUFFER_LEN 64

int32_t sBuffer[BUFFER_LEN];

void setup() {

 Serial.begin(115200);

 i2s_config_t i2s_config = {
   .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
   .sample_rate = 16000,
   .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
   .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
   .communication_format = I2S_COMM_FORMAT_I2S,
   .intr_alloc_flags = 0,
   .dma_buf_count = 4,
   .dma_buf_len = BUFFER_LEN
 };

 i2s_pin_config_t pin_config = {
   .bck_io_num = I2S_SCK,
   .ws_io_num = I2S_WS,
   .data_out_num = -1,
   .data_in_num = I2S_SD
 };

 i2s_driver_install(I2S_PORT, &amp;i2s_config, 0, NULL);
 i2s_set_pin(I2S_PORT, &amp;pin_config);

}

void loop() {

 size_t bytes_read;

 i2s_read(I2S_PORT, &amp;sBuffer, sizeof(sBuffer), &amp;bytes_read, portMAX_DELAY);

 for (int i = 0; i &lt; BUFFER_LEN; i++) {
   Serial.println(sBuffer[i]);
 }

}</code></pre></pre>
<h3><b>Visualizing the Data (Serial Plotter)</b></h3>
<p>After uploading the code, I usually switch to the Serial Plotter instead of the Serial Monitor. This makes it much easier to understand what’s going on.</p>
<p>When there is silence, the signal appears relatively flat. As soon as I speak or make noise, the waveform starts to move. Louder sounds produce larger peaks.</p>
<p>This step is important because it confirms that both the wiring and I2S configuration are correct before moving on.</p>
<h3><b>From Raw Audio to Usable Data</b></h3>
<p>At this point, we are reading raw audio samples, but sending all of these directly to a web page would not be practical. The ESP32 would have to transmit thousands of samples per second, which is unnecessary for simple visualization.</p>
<p>Instead, I process a block of samples and compute a single value that represents the sound level. This makes the system more efficient while still capturing how loud the environment is.</p>
<h3><b>Turning the ESP32 into a Web Server</b></h3>
<p>Once I confirmed that the microphone works, I moved on to building a web interface.</p>
<p>The ESP32 connects to Wi-Fi and hosts a web page. When I open that page in a browser, it displays a graph that updates continuously based on the microphone input.</p>
<p>To achieve real-time updates, I use <b>Server-Sent Events (SSE)</b>. This allows the browser to stay connected to the ESP32 and receive new data automatically, without refreshing the page.</p>
<h3><b>How the Live Graph Works</b></h3>
<p>The ESP32 reads a buffer of samples and computes an average magnitude value. This value represents the sound intensity at that moment.</p>
<p>That value is then streamed to the browser. JavaScript on the page receives the data and draws it on a canvas. As new values arrive, the graph shifts and updates continuously.</p>
<p>The result is a smooth, real-time visualization of sound activity.</p>
<h3><b>Full Example: ESP32 Web Server with Live Sound Graph</b></h3>
<pre><pre><code class="language-cpp">#include &lt;WiFi.h&gt;
#include &lt;WebServer.h&gt;
#include &quot;driver/i2s.h&quot;

#define I2S_WS   25
#define I2S_SD   33
#define I2S_SCK  26

#define I2S_PORT       I2S_NUM_0
#define SAMPLE_BUFFER  128

const char* ssid = &quot;YOUR_WIFI_NAME&quot;;
const char* password = &quot;YOUR_WIFI_PASSWORD&quot;;

WebServer server(80);

int32_t sample_buffer[SAMPLE_BUFFER];

const char webpage[] PROGMEM = R&quot;rawliteral(
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;
  &lt;title&gt;ESP32 Sound Graph&lt;/title&gt;
  &lt;style&gt;
    body { font-family: Arial; text-align: center; }
    canvas { border: 1px solid black; }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

  &lt;h3&gt;Live Sound Graph&lt;/h3&gt;
  &lt;canvas id=&quot;graph&quot; width=&quot;600&quot; height=&quot;250&quot;&gt;&lt;/canvas&gt;
  &lt;script&gt;

    const canvas = document.getElementById(&quot;graph&quot;);
    const ctx = canvas.getContext(&quot;2d&quot;);

    const maxPoints = 120;
    let dataPoints = new Array(maxPoints).fill(0);

    function drawGraph() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.beginPath();

      for (let i = 0; i &lt; dataPoints.length; i++) {
        let x = i * (canvas.width / maxPoints);
        let y = canvas.height - dataPoints[i];
        if (i === 0) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
      }

      ctx.stroke();
    }

    const source = new EventSource(&#039;/events&#039;);

    source.onmessage = function(event) {
      let val = parseInt(event.data);
      if (isNaN(val)) return;

      if (val &gt; 250) val = 250;
      if (val &lt; 0) val = 0;

      dataPoints.push(val);
      dataPoints.shift();
      drawGraph();
    };

  &lt;/script&gt;

&lt;/body&gt;

&lt;/html&gt;

)rawliteral&quot;;

void setup_i2s() {

  i2s_config_t config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = 16000,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags = 0,
    .dma_buf_count = 4,
    .dma_buf_len = SAMPLE_BUFFER

  };

  i2s_pin_config_t pins = {
    .bck_io_num = I2S_SCK,
    .ws_io_num = I2S_WS,
    .data_out_num = -1,
    .data_in_num = I2S_SD
  };

  i2s_driver_install(I2S_PORT, &amp;config, 0, NULL);
  i2s_set_pin(I2S_PORT, &amp;pins);
}

int get_sound_level() {
  size_t bytes_read;
  i2s_read(I2S_PORT, sample_buffer, sizeof(sample_buffer), &amp;bytes_read, portMAX_DELAY);

  int samples = bytes_read / sizeof(int32_t);
  uint64_t total = 0;

  for (int i = 0; i &lt; samples; i++) {
    int32_t s = sample_buffer[i] &gt;&gt; 14;
    if (s &lt; 0) s = -s;
    total += s;
  }

  int avg = total / samples;
  return map(avg, 0, 3000, 0, 250);

}

void handle_root() {
  server.send_P(200, &quot;text/html&quot;, webpage);
}


void handle_events() {

  WiFiClient client = server.client();

  client.println(&quot;HTTP/1.1 200 OK&quot;);
  client.println(&quot;Content-Type: text/event-stream&quot;);
  client.println(&quot;Cache-Control: no-cache&quot;);
  client.println(&quot;Connection: keep-alive&quot;);
  client.println();


  while (client.connected()) {

    int level = get_sound_level();

    client.print(&quot;data: &quot;);

    client.print(level);

    client.print(&quot;\n\n&quot;);

    delay(50);

  }

}

void setup() {

  Serial.begin(115200);

  setup_i2s();

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }

  Serial.println(WiFi.localIP());

  server.on(&quot;/&quot;, handle_root);
  server.on(&quot;/events&quot;, handle_events);
  server.begin();
}

void loop() {
  server.handleClient();
}</code></pre></pre>
<h3><b>Testing the Web Graph</b></h3>
<p>After uploading the code, I check the Serial Monitor for the ESP32’s IP address. Then I open that address in a browser on the same network.</p>
<p>The graph starts updating immediately. When I speak or clap, the graph responds in real time. If needed, I adjust the scaling in the map() function to better match the environment.</p>
<h3><b>Final Thoughts</b></h3>
<p>I always like starting simple—just reading raw data—before moving into more advanced features. It makes debugging much easier and helps build confidence that each part of the system is working correctly.</p>
<p>Once everything is combined, the ESP32 and INMP441 become a powerful platform for audio-based projects. Adding a live web interface makes it even more practical and interactive.</p>
<p>The post <a href="https://www.teachmemicro.com/esp32-inmp441-i2s-microphone-with-live-web-graph/">ESP32 + INMP441 I2S Microphone with Live Web Graph</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>ESPectre Tutorial: Motion Detection Using Wi-Fi Signals (ESP32 + Home Assistant)</title>
		<link>https://www.teachmemicro.com/espectre-tutorial-motion-detection-using-wi-fi-signals-esp32-home-assistant/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=espectre-tutorial-motion-detection-using-wi-fi-signals-esp32-home-assistant</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Mon, 05 Jan 2026 12:28:31 +0000</pubDate>
				<category><![CDATA[ESP32 Projects]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=10344</guid>

					<description><![CDATA[<p>Motion detection usually means PIR sensors, cameras, or microphones. ESPectre takes a very different approach: it detects movement by analyzing how the human body disturbs Wi-Fi radio waves. Interestingly, many modern signal-processing systems — from Wi-Fi sensing to broadcast monitoring infrastructures like this Thor Broadcast deployment — rely on analyzing how signals propagate through networks. &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/espectre-tutorial-motion-detection-using-wi-fi-signals-esp32-home-assistant/">ESPectre Tutorial: Motion Detection Using Wi-Fi Signals (ESP32 + Home Assistant)</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Motion detection usually means PIR sensors, cameras, or microphones. <strong>ESPectre</strong> takes a very different approach: it detects movement by analyzing how the human body disturbs Wi-Fi radio waves.</p>
<p>Interestingly, many modern signal-processing systems — from Wi-Fi sensing to broadcast monitoring infrastructures like this <a href="https://thorbroadcast.com/article/nationwide-media-monitoring-network-using-multi-channel-hdmi-iptv-encoders-hdmi-8230.html">Thor Broadcast</a> deployment — rely on analyzing how signals propagate through networks.</p>
<p>In this tutorial, I’ll walk through what ESPectre is, how it works, and how to set it up using an ESP32 and Home Assistant. No cameras, no audio recording, and no machine learning — just physics and signal processing.</p>
<p><span id="more-10344"></span></p>
<hr />
<h3><strong>What Is ESPectre?</strong></h3>
<p><span class="hover:entity-accent entity-underline inline cursor-pointer align-baseline"><span class="whitespace-normal">ESPectre is an open-source motion detection system that runs on an ESP32 and uses <strong>Wi-Fi Channel State Information (CSI)</strong> to detect movement.</span></span></p>
<p>When a person moves through a room, their body changes the way Wi-Fi signals bounce and propagate between a router and the ESP32. ESPectre measures these changes and converts them into a motion signal that can be used for automation or security.</p>
<p>Key characteristics:</p>
<ul>
<li>No cameras</li>
<li>No microphones</li>
<li>Works in total darkness</li>
<li>Preserves privacy</li>
<li>Integrates with Home Assistant via MQTT</li>
</ul>
<hr />
<h3><strong>How Wi-Fi Motion Detection Works</strong></h3>
<p><a href="https://www.teachmemicro.com/wp-content/uploads/2026/01/how-espectre-works.avif"><img data-dominant-color="efeff0" data-has-transparency="false" style="--dominant-color: #efeff0;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-10345 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/01/how-espectre-works.avif" alt="" width="768" height="512" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/01/how-espectre-works.avif 768w, https://www.teachmemicro.com/wp-content/uploads/2026/01/how-espectre-works-300x200.avif 300w" sizes="auto, (max-width: 768px) 100vw, 768px" /></a></p>
<p>Wi-Fi signals do not travel in a straight line. They reflect off walls, furniture, and people. Modern Wi-Fi chips expose detailed radio measurements known as <strong>Channel State Information (CSI)</strong>.</p>
<p>ESPectre:</p>
<ol>
<li>Captures CSI data from Wi-Fi packets</li>
<li>Stabilizes and filters the signal</li>
<li>Measures variance caused by movement</li>
<li>Produces a real-time motion score</li>
<li>Publishes motion events via MQTT</li>
</ol>
<p>Instead of learning patterns through AI, ESPectre relies on signal statistics such as variance and segmentation. This makes it lightweight and fast enough to run entirely on the ESP32.</p>
<hr />
<h3><strong>Hardware Requirements</strong></h3>
<p>You will need the following:</p>
<ul>
<li>ESP32 board with CSI support
<ul>
<li>ESP32-S3 is recommended</li>
<li>ESP32-C6 also works</li>
</ul>
</li>
<li>A 2.4 GHz Wi-Fi network</li>
<li>USB cable for flashing</li>
<li>Optional external antenna for better range</li>
</ul>
<p>No special router configuration is required.</p>
<hr />
<h3><strong>Software Requirements</strong></h3>
<ul>
<li>ESP-IDF version 6.1</li>
<li>Git</li>
<li>MQTT broker
<ul>
<li>Home Assistant’s built-in broker, or</li>
<li>Mosquitto on a PC, Raspberry Pi, or NAS</li>
</ul>
</li>
<li>Home Assistant (optional but recommended)</li>
</ul>
<hr />
<h3><strong>Cloning the ESPectre Repository</strong></h3>
<p>Open a terminal and clone the project:</p>
<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<pre><code class="whitespace-pre!">git clone https://github.com/francescopace/espectre.git
cd espectre
</code></pre>
</div>
</div>
<p>This repository contains the firmware, configuration options, and tuning documentation.</p>
<hr />
<h3><strong>Setting Up ESP-IDF</strong></h3>
<p>ESPectre is built using Espressif’s official framework.</p>
<p>Make sure:</p>
<ul>
<li>ESP-IDF v6.1 is installed</li>
<li>idf.py is available in your terminal</li>
<li>Your ESP32 drivers are correctly installed</li>
</ul>
<p>Once ESP-IDF is ready, connect your ESP32 via USB.</p>
<hr />
<h3><strong>Configuring the Firmware</strong></h3>
<p>Set the target chip (example for ESP32-S3):</p>
<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<pre><code class="whitespace-pre!">idf.py set-target esp32s3
</code></pre>
</div>
</div>
<p>Open the configuration menu:</p>
<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<pre><code class="whitespace-pre!">idf.py menuconfig
</code></pre>
</div>
</div>
<p>Inside menuconfig, configure:</p>
<ul>
<li>Wi-Fi SSID and password</li>
<li>MQTT broker address</li>
<li>MQTT credentials (if required)</li>
<li>CSI capture settings (leave defaults initially)</li>
</ul>
<p>Save and exit.</p>
<hr />
<h3><strong>Building and Flashing</strong></h3>
<p>Compile the firmware:</p>
<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<pre><code class="whitespace-pre!">idf.py build
</code></pre>
</div>
</div>
<p>Flash it to the ESP32:</p>
<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<pre><code class="whitespace-pre!">idf.py flash
</code></pre>
</div>
</div>
<p>After flashing, monitor the output:</p>
<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<pre><code class="whitespace-pre!">idf.py monitor
</code></pre>
</div>
</div>
<p>If everything is correct, the ESP32 will connect to Wi-Fi and start publishing motion data.</p>
<hr />
<h3><strong>MQTT Integration</strong></h3>
<p>ESPectre communicates using MQTT.</p>
<h4><strong>Using Home Assistant</strong></h4>
<p>If you are running Home Assistant with MQTT enabled:</p>
<ul>
<li>ESPectre uses Home Assistant MQTT auto-discovery</li>
<li>Motion sensors will appear automatically</li>
</ul>
<p>You will typically see:</p>
<ul>
<li>A binary motion sensor</li>
<li>A numeric movement or activity score</li>
</ul>
<p>No YAML configuration is required for basic usage.</p>
<h4><strong>Using a Standalone MQTT Broker</strong></h4>
<p>If you are not using Home Assistant, you can subscribe manually:</p>
<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<pre><code class="whitespace-pre!">mosquitto_sub -t "espectre/#"
</code></pre>
</div>
</div>
<p>This allows you to inspect raw motion data and debug signal behavior.</p>
<hr />
<h3><strong>Sensor Placement Guidelines</strong></h3>
<p>Placement has a major effect on detection quality.</p>
<p>Recommended setup:</p>
<ul>
<li>ESP32 placed 3 to 8 meters from the Wi-Fi router</li>
<li>Height of approximately 1 to 1.5 meters</li>
<li>Avoid metal surfaces and dense electronics</li>
<li>Clear line-of-sight is helpful but not required</li>
</ul>
<p>The router and ESP32 create a radio “field.” Motion is detected when someone crosses or disturbs that field.</p>
<hr />
<h3><strong>Tuning Motion Sensitivity</strong></h3>
<p>ESPectre includes tuning parameters that affect sensitivity and noise rejection.</p>
<p>Common parameters include:</p>
<ul>
<li>Segmentation threshold</li>
<li>Filtering strength</li>
<li>Subcarrier selection</li>
</ul>
<p>Start with default values. Once motion detection is working, adjust thresholds to:</p>
<ul>
<li>Reduce false positives</li>
<li>Ignore pets or fans</li>
<li>Improve responsiveness</li>
</ul>
<p>Tuning is environment-specific and may take some experimentation.</p>
<hr />
<h3><strong>Example Use Cases</strong></h3>
<p>ESPectre can be used for:</p>
<ul>
<li>Turning lights on when someone enters a room</li>
<li>Detecting occupancy without cameras</li>
<li>Privacy-friendly home security</li>
<li>Energy saving automation</li>
<li>Presence detection in offices or bedrooms</li>
</ul>
<p>Because it does not rely on infrared or optics, it works through furniture and in complete darkness.</p>
<hr />
<h3><strong>Troubleshooting Tips</strong></h3>
<p>If no motion is detected:</p>
<ul>
<li>Confirm Wi-Fi connection</li>
<li>Check MQTT broker address</li>
<li>Verify CSI is enabled in firmware</li>
</ul>
<p>If motion is always detected:</p>
<ul>
<li>Increase segmentation threshold</li>
<li>Move ESP32 farther from the router</li>
<li>Reduce environmental noise sources</li>
</ul>
<p>If Home Assistant sensors do not appear:</p>
<ul>
<li>Ensure MQTT discovery is enabled</li>
<li>Restart Home Assistant</li>
<li>Check MQTT logs</li>
</ul>
<hr />
<h3><strong>Final Thoughts</strong></h3>
<p>ESPectre demonstrates that motion detection does not require cameras or microphones. By using Wi-Fi CSI and an ESP32, it offers a low-cost, privacy-respecting alternative that integrates cleanly with Home Assistant.</p>
<p>If you are interested in experimental sensing, smart homes, or signal processing, ESPectre is a project worth exploring.</p>
<p>The post <a href="https://www.teachmemicro.com/espectre-tutorial-motion-detection-using-wi-fi-signals-esp32-home-assistant/">ESPectre Tutorial: Motion Detection Using Wi-Fi Signals (ESP32 + Home Assistant)</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Use LM35 Temperature Sensor with Arduino and ESP32: Complete Guide with Example Projects</title>
		<link>https://www.teachmemicro.com/how-to-use-lm35-temperature-sensor-with-arduino-and-esp32-complete-guide-with-example-projects/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=how-to-use-lm35-temperature-sensor-with-arduino-and-esp32-complete-guide-with-example-projects</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Mon, 03 Nov 2025 01:00:27 +0000</pubDate>
				<category><![CDATA[Arduino Projects]]></category>
		<category><![CDATA[ESP32 Projects]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=9613</guid>

					<description><![CDATA[<p>Introduction to LM35 Temperature Sensor The LM35 is a precision temperature sensor that provides an analog voltage output proportional to the measured temperature. It's popular among electronics enthusiasts for its simplicity, accuracy, and ease of use with microcontrollers like Arduino and ESP32. In this tutorial, you’ll learn how LM35 works, how to interface it with &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/how-to-use-lm35-temperature-sensor-with-arduino-and-esp32-complete-guide-with-example-projects/">How to Use LM35 Temperature Sensor with Arduino and ESP32: Complete Guide with Example Projects</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h3><strong>Introduction to LM35 Temperature Sensor</strong></h3>
<p>The <strong>LM35</strong> is a precision temperature sensor that provides an analog voltage output proportional to the measured temperature. It's popular among electronics enthusiasts for its <strong>simplicity, accuracy, and ease of use</strong> with microcontrollers like <strong>Arduino and ESP32</strong>.</p>
<p>In this tutorial, you’ll learn how LM35 works, how to interface it with Arduino and ESP32, and how to build <strong>two practical projects</strong> — a basic temperature reader and an IoT-based web server display.</p>
<p><span id="more-9613"></span></p>
<hr />
<h3><strong>What Is an LM35 Sensor?</strong></h3>
<p><a href="https://www.teachmemicro.com/wp-content/uploads/2025/11/lm35-1.avif"><img data-dominant-color="beb4ab" data-has-transparency="true" style="--dominant-color: #beb4ab;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-9786 has-transparency" src="https://www.teachmemicro.com/wp-content/uploads/2025/11/lm35-1.avif" alt="lm35" width="512" height="512" srcset="https://www.teachmemicro.com/wp-content/uploads/2025/11/lm35-1.avif 512w, https://www.teachmemicro.com/wp-content/uploads/2025/11/lm35-1-300x300.avif 300w, https://www.teachmemicro.com/wp-content/uploads/2025/11/lm35-1-150x150.avif 150w" sizes="auto, (max-width: 512px) 100vw, 512px" /></a></p>
<p>The <strong>LM35</strong> is an analog temperature sensor that outputs voltage linearly related to the Celsius temperature. It doesn’t require calibration and provides a direct temperature-to-voltage conversion, making it extremely beginner-friendly.</p>
<p><strong>Manufacturer:</strong> Texas Instruments<br />
<strong>Output Type:</strong> Analog (10mV/°C)<br />
<strong>Operating Range:</strong> -55°C to +150°C<br />
<strong>Accuracy:</strong> ±0.5°C (at room temperature)</p>
<hr />
<h3><strong>Key Features and Specifications of LM35</strong></h3>
<div class="_tableContainer_1rjym_1">
<div class="group _tableWrapper_1rjym_13 flex w-fit flex-col-reverse" tabindex="-1">
<table class="w-fit min-w-(--thread-content-width)">
<thead>
<tr>
<th data-col-size="sm">Feature</th>
<th data-col-size="sm">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td data-col-size="sm">Operating Voltage</td>
<td data-col-size="sm">4V to 30V</td>
</tr>
<tr>
<td data-col-size="sm">Output Voltage</td>
<td data-col-size="sm">10 mV per °C</td>
</tr>
<tr>
<td data-col-size="sm">Temperature Range</td>
<td data-col-size="sm">-55°C to +150°C</td>
</tr>
<tr>
<td data-col-size="sm">Accuracy</td>
<td data-col-size="sm">±0.5°C (at 25°C)</td>
</tr>
<tr>
<td data-col-size="sm">Output Type</td>
<td data-col-size="sm">Analog</td>
</tr>
<tr>
<td data-col-size="sm">Power Consumption</td>
<td data-col-size="sm">Low</td>
</tr>
</tbody>
</table>
</div>
</div>
<hr />
<h3><strong>How the LM35 Sensor Works: Principle of Operation</strong></h3>
<p>The LM35 works on the principle that the output voltage changes linearly with temperature.<br />
For every <strong>1°C rise</strong>, the output increases by <strong>10 mV</strong>.</p>
<p>So if the LM35 outputs <strong>250 mV</strong>, the temperature is <strong>25°C</strong>.<br />
Formula:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_b5484f5138f1d97c16e6b62d873cbfc4.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="T(^{0}C) = \frac{V_{out} (in\; mV)}{10}" /></span><script type='math/tex'>T(^{0}C) = \frac{V_{out} (in\; mV)}{10}</script></p>
</div>
</div>
<p>This linear relationship makes the LM35 simple to use — no complex calibration or signal conditioning is required.</p>
<p>That means:</p>
<ul>
<li>0°C → 0V</li>
<li>25°C → 250mV</li>
<li>100°C → 1.0V</li>
</ul>
<p>Since the ESP32 ADC reads voltage in a range (0–3.3V), we can compute the temperature as:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_2f34b41f3800ba68b54f2a4506d11883.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="T(^{0}C)=(\frac{ADC_{value}}{4095})\cdot3.3\cdot100" /></span><script type='math/tex'>T(^{0}C)=(\frac{ADC_{value}}{4095})\cdot3.3\cdot100</script></p>
<hr />
<h3><strong>Understanding the LM35 Pin Configuration and Circuit Diagram</strong></h3>
<p><a href="https://www.teachmemicro.com/wp-content/uploads/2025/11/lm35-pinout.avif"><img data-dominant-color="e6e6e6" data-has-transparency="false" style="--dominant-color: #e6e6e6;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-9789 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2025/11/lm35-pinout.avif" alt="LM35 pinout" width="984" height="756" srcset="https://www.teachmemicro.com/wp-content/uploads/2025/11/lm35-pinout.avif 984w, https://www.teachmemicro.com/wp-content/uploads/2025/11/lm35-pinout-300x230.avif 300w, https://www.teachmemicro.com/wp-content/uploads/2025/11/lm35-pinout-768x590.avif 768w" sizes="auto, (max-width: 984px) 100vw, 984px" /></a></p>
<h4><strong>LM35 Pinout Explained</strong></h4>
<div class="_tableContainer_1rjym_1">
<div class="group _tableWrapper_1rjym_13 flex w-fit flex-col-reverse" tabindex="-1">
<table class="w-fit min-w-(--thread-content-width)">
<thead>
<tr>
<th data-col-size="sm">Pin</th>
<th data-col-size="sm">Function</th>
<th data-col-size="sm">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td data-col-size="sm">1</td>
<td data-col-size="sm">VCC</td>
<td data-col-size="sm">Connects to 5V (Arduino) or 3.3V (ESP32)</td>
</tr>
<tr>
<td data-col-size="sm">2</td>
<td data-col-size="sm">VOUT</td>
<td data-col-size="sm">Analog output voltage</td>
</tr>
<tr>
<td data-col-size="sm">3</td>
<td data-col-size="sm">GND</td>
<td data-col-size="sm">Ground connection</td>
</tr>
</tbody>
</table>
</div>
</div>
<hr />
<h4><strong>LM35 Wiring and Power Requirements</strong></h4>
<ul>
<li>For <strong>Arduino</strong>, power it with <strong>5V</strong>.</li>
<li>For <strong>ESP32</strong>, use <strong>3.3V</strong>.</li>
<li>Always use <strong>a common ground</strong> between LM35 and the microcontroller.</li>
<li>Optionally, add a <strong>0.1 µF capacitor</strong> between VOUT and GND to stabilize readings.</li>
</ul>
<hr />
<h3><strong>Using LM35 with Arduino: Step-by-Step Guide</strong></h3>
<h4><strong>Components Required for Arduino + LM35 Project</strong></h4>
<ul>
<li>Arduino Uno (or Nano)</li>
<li>LM35 temperature sensor</li>
<li>Jumper wires</li>
<li>Breadboard</li>
<li>(Optional) 16x2 LCD display</li>
</ul>
<hr />
<h4><strong>Circuit Diagram for LM35 with Arduino</strong></h4>
<p><a href="https://www.teachmemicro.com/wp-content/uploads/2025/11/arduino-lm35.avif"><img data-dominant-color="97acba" data-has-transparency="false" style="--dominant-color: #97acba;" loading="lazy" decoding="async" class="aligncenter wp-image-9787 size-full not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2025/11/arduino-lm35.avif" alt="Arduino and LM35 wiring diagram" width="613" height="377" srcset="https://www.teachmemicro.com/wp-content/uploads/2025/11/arduino-lm35.avif 613w, https://www.teachmemicro.com/wp-content/uploads/2025/11/arduino-lm35-300x185.avif 300w" sizes="auto, (max-width: 613px) 100vw, 613px" /></a></p>
<p><strong>Connections:</strong></p>
<ul>
<li>LM35 VCC → 5V</li>
<li>LM35 GND → GND</li>
<li>LM35 VOUT → A0 (analog pin)</li>
</ul>
<hr />
<h4><strong>Arduino Code to Read Temperature from LM35</strong></h4>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs"></div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr">
<pre><pre><code class="language-cpp">const int sensorPin = A0;
float temperature;

void setup() {
  Serial.begin(9600);
}

void loop() {
  int sensorValue = analogRead(sensorPin);
  float voltage = (sensorValue / 1023.0) * 5.0;
  temperature = voltage * 100; // 10mV per degree Celsius
  Serial.print(&quot;Temperature: &quot;);
  Serial.print(temperature);
  Serial.println(&quot; °C&quot;);
  delay(1000);
}</code></pre></pre>
</div>
</div>
<hr />
<h4><strong>Displaying Temperature on Serial Monitor or LCD</strong></h4>
<ul>
<li>Open <strong>Serial Monitor</strong> (Ctrl + Shift + M).</li>
<li>You’ll see continuous readings like:
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs"></div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr">Temperature: 27.35 °C<br />
Temperature: 27.42 °C</div>
<div dir="ltr"></div>
</div>
</li>
<li>To display it on an <strong>LCD</strong>, use the <a href="https://www.teachmemicro.com/arduino-lcd-tutorial/"><em>LiquidCrystal</em> library</a> and connect pins RS, E, D4–D7 accordingly.</li>
</ul>
<hr />
<h3><strong>Using LM35 with ESP32: IoT-Based Temperature Monitoring</strong></h3>
<h4><strong>Components Required for ESP32 + LM35 Project</strong></h4>
<ul>
<li>ESP32 board</li>
<li>LM35 temperature sensor</li>
<li>Breadboard and jumper wires</li>
<li>(Optional) Wi-Fi network for IoT display</li>
</ul>
<hr />
<h4><strong>Circuit Diagram for LM35 with ESP32</strong></h4>
<p><strong>Connections:</strong></p>
<ul>
<li>LM35 VCC → 3.3V</li>
<li>LM35 GND → GND</li>
<li>LM35 VOUT → GPIO34 (analog input)</li>
</ul>
<hr />
<h4><strong>ESP32 Code to Read Temperature from LM35</strong></h4>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<pre><pre><code class="language-cpp">const int sensorPin = 34;
float temperature;

void setup() {
  Serial.begin(115200);
}

void loop() {
  int sensorValue = analogRead(sensorPin);
  float voltage = (sensorValue / 4095.0) * 3.3;
  temperature = voltage * 100;
  Serial.print(&quot;Temperature: &quot;);
  Serial.print(temperature);
  Serial.println(&quot; °C&quot;);
  delay(2000);
}</code></pre></pre>
</div>
</div>
<hr />
<h4><strong>Displaying Temperature on a Web Server (IoT Project)</strong></h4>
<p>Below is a full example code that:</p>
<ol>
<li>Reads temperature from LM35.</li>
<li>Connects to Wi-Fi.</li>
<li>Hosts a simple webpage showing live temperature readings.</li>
</ol>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<pre><pre><code class="language-cpp">#include &lt;WiFi.h&gt;
#include &lt;WebServer.h&gt;

// Replace with your Wi-Fi credentials
const char* ssid = &quot;YOUR_WIFI_SSID&quot;;
const char* password = &quot;YOUR_WIFI_PASSWORD&quot;;

const int lm35_pin = 34; // Analog pin for LM35
WebServer server(80);    // Web server on port 80

float read_temperature() {
  int adc_value = analogRead(lm35_pin);
  float voltage = (adc_value / 4095.0) * 3.3;  // Convert ADC reading to voltage
  float temperature_c = voltage * 100;         // 10mV per °C
  return temperature_c;
}


String html_page(float temp_c) {
  String html = &quot;&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta name=&#039;viewport&#039; content=&#039;width=device-width, initial-scale=1.0&#039;&gt;&quot;;
  html += &quot;&lt;title&gt;ESP32 LM35 Temperature&lt;/title&gt;&quot;;
  html += &quot;&lt;style&gt;body{font-family:Arial;text-align:center;background:#111;color:#0ff;}h1{color:#00ffff;}&lt;/style&gt;&quot;;
  html += &quot;&lt;/head&gt;&lt;body&gt;&quot;;
  html += &quot;&lt;h1&gt;ESP32 LM35 Temperature Monitor&lt;/h1&gt;&quot;;
  html += &quot;&lt;h2&gt;Current Temperature: &quot; + String(temp_c, 2) + &quot; &deg;C&lt;/h2&gt;&quot;;
  html += &quot;&lt;meta http-equiv=&#039;refresh&#039; content=&#039;2&#039;&gt;&quot;; // auto-refresh every 2 seconds
  html += &quot;&lt;/body&gt;&lt;/html&gt;&quot;;
  return html;
}


void handle_root() {
  float temp = read_temperature();
  server.send(200, &quot;text/html&quot;, html_page(temp));
}

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  Serial.print(&quot;Connecting to Wi-Fi&quot;);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(&quot;.&quot;);
  }
  Serial.println(&quot;\nConnected!&quot;);
  Serial.print(&quot;ESP32 IP address: &quot;);
  Serial.println(WiFi.localIP());

  server.on(&quot;/&quot;, handle_root);
  server.begin();
  Serial.println(&quot;Web server started!&quot;);
}

void loop() {
  server.handleClient();
}</code></pre></pre>
</div>
<hr />
<h4>Accessing the Web Page</h4>
<ol>
<li>Upload the sketch to your ESP32.</li>
<li>Open the Serial Monitor at 115200 baud.</li>
<li>Wait until you see:
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9"></div>
<div class="overflow-y-auto p-4" dir="ltr">Connected!<br />
ESP32 IP <span class="hljs-selector-tag">address</span>: <span class="hljs-number">192.168</span>.<span class="hljs-number">1</span>.xxx</div>
</div>
</li>
<li>Open that IP in your browser (e.g., <em>http://192.168.1.45</em>)</li>
<li>You’ll see a <strong>temperature dashboard</strong> refreshing every 2 seconds!</li>
</ol>
<hr />
<h3><strong>Troubleshooting Common Issues</strong></h3>
<h4><strong>Incorrect Temperature Readings</strong></h4>
<ul>
<li>Check voltage reference (5V or 3.3V).</li>
<li>Ensure correct analog pin.</li>
<li>Add a capacitor between VOUT and GND to reduce noise.</li>
</ul>
<h4><strong>Sensor Noise and Calibration Tips</strong></h4>
<ul>
<li>Use shielded cables for long distances.</li>
<li>Calibrate by comparing with a digital thermometer.</li>
</ul>
<hr />
<h3><strong>Applications of LM35 in Real-Life Projects</strong></h3>
<h4><strong>Home Automation Systems</strong></h4>
<ul>
<li>Temperature-controlled fans or air conditioners.</li>
</ul>
<h4><strong>Environmental Monitoring and IoT Systems</strong></h4>
<ul>
<li>Smart agriculture</li>
<li>Weather monitoring stations</li>
<li>IoT dashboards (via Blynk, ThingSpeak, etc.)</li>
</ul>
<hr />
<h3><strong>Advantages and Limitations of LM35 Sensor</strong></h3>
<h4><strong>Benefits of Using LM35</strong></h4>
<ul>
<li>Linear and accurate output</li>
<li>Low cost and easy to interface</li>
<li>No external calibration needed</li>
</ul>
<h4><strong>Limitations and Alternatives</strong></h4>
<ul>
<li>Analog-only output (needs ADC)</li>
<li>Not waterproof</li>
<li>Alternatives: DHT11, DS18B20, TMP36</li>
</ul>
<hr />
<h3><strong>FAQs: How to Use LM35 Temperature Sensor with Arduino and ESP32</strong></h3>
<p><strong>1. What voltage does LM35 require?</strong><br />
It operates between <strong>4V and 30V</strong>, commonly powered by 5V (Arduino) or 3.3V (ESP32).</p>
<p><strong>2. Can I use LM35 with ESP8266 or Raspberry Pi?</strong><br />
Yes, but ensure you use analog-to-digital conversion, as some boards (like ESP8266) have limited ADC support.</p>
<p><strong>3. Why are my readings unstable?</strong><br />
Add a <strong>capacitor (0.1 µF)</strong> across output and ground to filter noise.</p>
<p><strong>4. How accurate is the LM35 sensor?</strong><br />
±0.5°C at room temperature and ±1°C across a wide range.</p>
<p><strong>5. Can LM35 measure below 0°C?</strong><br />
Yes, but output voltage may go negative, requiring offset circuitry.</p>
<p><strong>6. Which IoT platforms support LM35 + ESP32 projects?</strong><br />
Popular ones include <strong>Blynk</strong>, <strong>ThingSpeak</strong>, and <strong>Google Firebase</strong>.</p>
<hr />
<h3><strong>Conclusion</strong></h3>
<p>The <strong>LM35 temperature sensor</strong> is a simple, reliable, and precise device for temperature measurement and monitoring. Whether you’re using <strong>Arduino for basic sensor readings</strong> or <strong>ESP32 for IoT web dashboards</strong>, the LM35 offers versatility and accuracy.</p>
<p>Experimenting with LM35 is an excellent way to start learning about <strong>analog sensors, ADC conversion, and IoT data visualization</strong>.</p>
<p>For more information, visit the <a class="decorated-link cursor-pointer" target="_new" rel="noopener">Texas Instruments LM35 Datasheet</a>.</p>
<p>The post <a href="https://www.teachmemicro.com/how-to-use-lm35-temperature-sensor-with-arduino-and-esp32-complete-guide-with-example-projects/">How to Use LM35 Temperature Sensor with Arduino and ESP32: Complete Guide with Example Projects</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>ESP32 Earthquake Monitor with USGS API, GPS, LCD, and Web Dashboard</title>
		<link>https://www.teachmemicro.com/esp32-earthquake-monitor-with-usgs-api-gps-lcd-and-web-dashboard/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=esp32-earthquake-monitor-with-usgs-api-gps-lcd-and-web-dashboard</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Mon, 20 Oct 2025 01:00:00 +0000</pubDate>
				<category><![CDATA[ESP32 Projects]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=9630</guid>

					<description><![CDATA[<p>Was that an earthquake, or did I just stand up too fast? If you’ve ever asked that question — then this project’s for you. We’re building an ESP32 Earthquake Monitor that fetches live data from the USGS Earthquake API, locates your position via NEO-6M GPS, and alerts you through an LCD, buzzer, and RGB LED. &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/esp32-earthquake-monitor-with-usgs-api-gps-lcd-and-web-dashboard/">ESP32 Earthquake Monitor with USGS API, GPS, LCD, and Web Dashboard</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>Was that an earthquake, or did I just stand up too fast?</em> If you’ve ever asked that question — then this project’s for you.</p>
<p>We’re building an <strong>ESP32 Earthquake Monitor</strong> that fetches live data from the <strong>USGS Earthquake API</strong>, locates your position via <strong>NEO-6M GPS</strong>, and alerts you through an <strong>LCD</strong>, <strong>buzzer</strong>, and <strong>RGB LED</strong>. It even logs quake data to an <strong>SD card</strong> and hosts a <strong>mini web dashboard</strong> for quake history — because if you’re going to feel the earth move, you might as well have the data to prove it.</p>
<p><span id="more-9630"></span></p>
<hr />
<h3>Project Overview</h3>
<p>This project combines <strong>IoT</strong>, <strong>geolocation</strong>, and <strong>real-time data processing</strong> in one neat ESP32 package.</p>
<p>Here’s what happens behind the scenes:</p>
<ol>
<li>The <strong>ESP32</strong> connects to Wi-Fi and syncs its clock with an <strong>NTP (Network Time Protocol)</strong> server.</li>
<li>Using the current date, it requests the day’s earthquakes in a given location via the <a href="https://earthquake.usgs.gov/fdsnws/event/1/"><strong>USGS Earthquake API</strong></a>.</li>
<li>The <a href="https://www.teachmemicro.com/arduino-gps-tutorial/"><strong>NEO-6M GPS module</strong></a> provides your current coordinates.</li>
<li>The ESP32 computes which earthquakes are <strong>closest</strong> to you.</li>
<li>If a quake is detected within ~200 km:
<ul>
<li>The <a href="https://www.teachmemicro.com/arduino-i2c-lcd/"><strong>LCD</strong></a> shows the event.</li>
<li>The <strong>buzzer</strong> sounds an alarm.</li>
<li>The <a href="https://www.teachmemicro.com/arduino-rgb-led-tutorial/"><strong>RGB LED</strong></a> turns <strong>red</strong> (otherwise it stays <strong>green</strong>).</li>
</ul>
</li>
<li>All quake data is logged to the <a href="https://www.teachmemicro.com/using-microsd-breakout-board-arduino/"><strong>SD card</strong></a>, and you can view it later through the <strong>ESP32’s <a href="https://www.teachmemicro.com/sensor-display-on-esp32-web-server/">built-in web page</a></strong>.</li>
</ol>
<hr />
<h3>Components Needed</h3>
<div class="_tableContainer_1rjym_1">
<div class="group _tableWrapper_1rjym_13 flex w-fit flex-col-reverse" tabindex="-1">
<table class="w-fit min-w-(--thread-content-width)">
<thead>
<tr>
<th data-col-size="sm">Component</th>
<th data-col-size="md">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td data-col-size="sm"><strong>ESP32 Dev Board</strong></td>
<td data-col-size="md">Main microcontroller with Wi-Fi</td>
</tr>
<tr>
<td data-col-size="sm"><strong>NEO-6M GPS Module</strong></td>
<td data-col-size="md">Provides latitude &amp; longitude</td>
</tr>
<tr>
<td data-col-size="sm"><strong>16x2 I²C LCD</strong></td>
<td data-col-size="md">Displays quake status</td>
</tr>
<tr>
<td data-col-size="sm"><strong>Active Buzzer</strong></td>
<td data-col-size="md">Alarm sound for nearby quakes</td>
</tr>
<tr>
<td data-col-size="sm"><strong>RGB LED</strong></td>
<td data-col-size="md">Visual indicator (green = calm, red = alert)</td>
</tr>
<tr>
<td data-col-size="sm"><strong>MicroSD Module</strong></td>
<td data-col-size="md">Stores quake history</td>
</tr>
<tr>
<td data-col-size="sm"><strong>Breadboard + wires</strong></td>
<td data-col-size="md">For connections</td>
</tr>
</tbody>
</table>
</div>
</div>
<hr />
<h3>Fetching Data from the USGS Earthquake API</h3>
<p>We’ll use this endpoint to get the latest quakes in the Philippines (my country):</p>
<pre><pre><code class="language-cpp">https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson
&amp;starttime=2025-10-17
&amp;endtime=2025-10-18
&amp;minmagnitude=4.5
&amp;minlatitude=5.0
&amp;maxlatitude=20.0
&amp;minlongitude=115.0
&amp;maxlongitude=130.0
&amp;orderby=time</code></pre></pre>
<p>But instead of fixed dates, we’ll dynamically replace starttime and endtime each day using the <strong>ESP32’s NTP-synced clock</strong>.</p>
<p>That means the device automatically fetches <em>today’s earthquakes</em> every 24 hours — no manual editing required.</p>
<hr />
<h3>GPS and Connections</h3>
<div class="_tableContainer_1rjym_1">
<div class="group _tableWrapper_1rjym_13 flex w-fit flex-col-reverse" tabindex="-1">
<table class="w-fit min-w-(--thread-content-width)">
<thead>
<tr>
<th data-col-size="sm">Module</th>
<th data-col-size="sm">ESP32 Pin</th>
<th data-col-size="sm">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td data-col-size="sm">NEO-6M TX</td>
<td data-col-size="sm">GPIO16 (RX2)</td>
<td data-col-size="sm">GPS data to ESP32</td>
</tr>
<tr>
<td data-col-size="sm">NEO-6M RX</td>
<td data-col-size="sm">GPIO17 (TX2)</td>
<td data-col-size="sm">Optional</td>
</tr>
<tr>
<td data-col-size="sm">LCD SDA</td>
<td data-col-size="sm">GPIO21</td>
<td data-col-size="sm">I²C</td>
</tr>
<tr>
<td data-col-size="sm">LCD SCL</td>
<td data-col-size="sm">GPIO22</td>
<td data-col-size="sm">I²C</td>
</tr>
<tr>
<td data-col-size="sm">Buzzer +</td>
<td data-col-size="sm">GPIO25</td>
<td data-col-size="sm">Output</td>
</tr>
<tr>
<td data-col-size="sm">RGB LED R</td>
<td data-col-size="sm">GPIO26</td>
<td data-col-size="sm">Red pin</td>
</tr>
<tr>
<td data-col-size="sm">RGB LED G</td>
<td data-col-size="sm">GPIO27</td>
<td data-col-size="sm">Green pin</td>
</tr>
<tr>
<td data-col-size="sm">SD CS</td>
<td data-col-size="sm">GPIO5</td>
<td data-col-size="sm">SD card chip select</td>
</tr>
<tr>
<td data-col-size="sm">SD MOSI</td>
<td data-col-size="sm">GPIO23</td>
<td data-col-size="sm">SPI data (ESP32 → SD)</td>
</tr>
<tr>
<td data-col-size="sm">SD MISO</td>
<td data-col-size="sm">GPIO19</td>
<td data-col-size="sm">SPI data (SD → ESP32)</td>
</tr>
<tr>
<td data-col-size="sm">SD SCK</td>
<td data-col-size="sm">GPIO18</td>
<td data-col-size="sm">SPI clock</td>
</tr>
</tbody>
</table>
</div>
</div>
<hr />
<h3>Code Overview</h3>
<p>Here’s the project flow summarized:</p>
<ol>
<li>Connect to Wi-Fi and get time from NTP.</li>
<li>Format today’s date into <em>YYYY-MM-DD</em>.</li>
<li>Construct the API URL dynamically.</li>
<li>Fetch and parse the GeoJSON data.</li>
<li>Get your GPS location (latitude &amp; longitude).</li>
<li>Compute the distance between your location and each quake’s epicenter using the <a href="https://en.wikipedia.org/wiki/Haversine_formula"><strong>Haversine formula</strong></a>.</li>
<li>Update the LCD, RGB LED, and buzzer accordingly.</li>
<li>Log quake data to the SD card.</li>
<li>Host a simple web page displaying recent logs.</li>
</ol>
<hr />
<h3>Example Code Snippet (Simplified)</h3>
<pre><pre><code class="language-cpp">#include &lt;WiFi.h&gt;
#include &lt;HTTPClient.h&gt;
#include &lt;ArduinoJson.h&gt;
#include &lt;Wire.h&gt;
#include &lt;LiquidCrystal_I2C.h&gt;
#include &lt;TinyGPSPlus.h&gt;
#include &lt;HardwareSerial.h&gt;
#include &lt;SD.h&gt;
#include &lt;NTPClient.h&gt;
#include &lt;WiFiUdp.h&gt;

#define BUZZER_PIN 25
#define LED_R 26
#define LED_G 27
#define SD_CS 5

LiquidCrystal_I2C lcd(0x27, 16, 2);
HardwareSerial gpsSerial(2);
TinyGPSPlus gps;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, &quot;pool.ntp.org&quot;, 28800); // +8 GMT (Philippines)

const char* ssid = &quot;YOUR_WIFI_SSID&quot;;
const char* password = &quot;YOUR_WIFI_PASSWORD&quot;;

void setup() {
  Serial.begin(115200);
  lcd.init(); lcd.backlight();
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(LED_R, OUTPUT);
  pinMode(LED_G, OUTPUT);

  gpsSerial.begin(9600, SERIAL_8N1, 16, 17);

  WiFi.begin(ssid, password);
  lcd.print(&quot;Connecting WiFi...&quot;);
  while (WiFi.status() != WL_CONNECTED) delay(500);

  timeClient.begin();
  while(!timeClient.update()) timeClient.forceUpdate();

  if(!SD.begin(SD_CS)) lcd.print(&quot;SD Fail!&quot;); else lcd.print(&quot;SD Ready&quot;);
  delay(2000);
  lcd.clear();

  fetchEarthquakes();
}

void fetchEarthquakes() {
  // Get today&#039;s date
  time_t epoch = timeClient.getEpochTime();
  struct tm *tm_info = localtime(&amp;epoch);
  char date_str[11];
  strftime(date_str, 11, &quot;%Y-%m-%d&quot;, tm_info);

  String url = &quot;https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&quot;;
  url += &quot;&amp;starttime=&quot; + String(date_str);
  url += &quot;&amp;endtime=&quot; + String(date_str);
  url += &quot;&amp;minmagnitude=4.5&amp;minlatitude=5.0&amp;maxlatitude=20.0&amp;minlongitude=115.0&amp;maxlongitude=130.0&amp;orderby=time&quot;;

  HTTPClient http;
  http.begin(url);
  int code = http.GET();
  if(code == 200) {
    DynamicJsonDocument doc(16384);
    deserializeJson(doc, http.getString());

    for(JsonObject f : doc[&quot;features&quot;].as&lt;JsonArray&gt;()) {
      double lon = f[&quot;geometry&quot;][&quot;coordinates&quot;][0];
      double lat = f[&quot;geometry&quot;][&quot;coordinates&quot;][1];
      double mag = f[&quot;properties&quot;][&quot;mag&quot;];
      const char* place = f[&quot;properties&quot;][&quot;place&quot;];

      double myLat = gps.location.lat();
      double myLon = gps.location.lng();

      double dist = TinyGPSPlus::distanceBetween(myLat, myLon, lat, lon) / 1000.0;
      logToSD(place, mag, dist);

      if(dist &lt; 200) alertUser(place, mag);
    }
  }
  http.end();
}

void alertUser(const char* place, double mag) {
  lcd.clear();
  lcd.print(&quot;ALERT!&quot;);
  lcd.setCursor(0,1);
  lcd.print(place);
  digitalWrite(LED_R, HIGH);
  digitalWrite(LED_G, LOW);
  tone(BUZZER_PIN, 1000, 2000);
  delay(5000);
  noTone(BUZZER_PIN);
  digitalWrite(LED_R, LOW);
  digitalWrite(LED_G, HIGH);
}

void logToSD(const char* place, double mag, double dist) {
  File f = SD.open(&quot;/quakes.csv&quot;, FILE_APPEND);
  if(f) {
    f.printf(&quot;%s,%.1f,%.1f km\n&quot;, place, mag, dist);
    f.close();
  }
}</code></pre></pre>
<hr />
<h3>Adding a Web Dashboard</h3>
<p>With the ESP32’s Wi-Fi capability, it’s easy to serve quake logs over a local webpage.</p>
<p>The idea:</p>
<ul>
<li>The ESP32 starts a web server.</li>
<li>When a user visits the ESP32’s IP address (e.g., <em>http://192.168.1.25</em>), the page displays the contents of <em>quakes.csv</em> from the SD card.</li>
<li>The page auto-refreshes every 60 seconds, so you always see the latest events.</li>
</ul>
<pre><pre><code class="language-cpp">#include &lt;WebServer.h&gt;

WebServer server(80);

void handleRoot() {
  if (!SD.exists(&quot;/quakes.csv&quot;)) {
    server.send(200, &quot;text/html&quot;, &quot;&lt;h3&gt;No quake data logged yet.&lt;/h3&gt;&quot;);
    return;
  }

  File file = SD.open(&quot;/quakes.csv&quot;);
  if (!file) {
    server.send(500, &quot;text/html&quot;, &quot;Error opening quake log.&quot;);
    return;
  }

  String html = &quot;&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=&#039;UTF-8&#039;&gt;&quot;;
	html += &quot;&lt;meta http-equiv=&#039;refresh&#039; content=&#039;60&#039;&gt;&quot;;
	html += &quot;&lt;title&gt;ESP32 Earthquake Monitor&lt;/title&gt;&quot;;
	html += &quot;&lt;style&gt;body{font-family:Arial;background:#f8f9fa;color:#333;text-align:center;}&quot;;
	html += &quot;table{margin:auto;border-collapse:collapse;width:80%;}&quot;;
	html += &quot;th,td{border:1px solid #aaa;padding:8px;}th{background:#004b87;color:white;}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&quot;;
	html += &quot;&lt;h1&gt;ESP32 Earthquake Log&lt;/h1&gt;&lt;table&gt;&lt;tr&gt;&lt;th&gt;Location&lt;/th&gt;&lt;th&gt;Magnitude&lt;/th&gt;&lt;th&gt;Distance (km)&lt;/th&gt;&lt;/tr&gt;&quot;;


  while (file.available()) {
    String line = file.readStringUntil(&#039;\n&#039;);
    int firstComma = line.indexOf(&#039;,&#039;);
    int secondComma = line.indexOf(&#039;,&#039;, firstComma + 1);
    if (firstComma &gt; 0 &amp;&amp; secondComma &gt; 0) {
      String place = line.substring(0, firstComma);
      String mag = line.substring(firstComma + 1, secondComma);
      String dist = line.substring(secondComma + 1);
      html += &quot;&lt;tr&gt;&lt;td&gt;&quot; + place + &quot;&lt;td&gt;&lt;td&gt;&quot; + mag + &quot;&lt;/td&gt;&lt;td&gt;&quot; + dist + &quot;&lt;/td&gt;&lt;/tr&gt;&quot;;
    }
  }
  file.close();

  html += &quot;&lt;/table&gt;&lt;p&gt;&lt;em&gt;Auto-refreshes every 60 seconds&lt;/em&gt;&lt;/p&gt;&lt;body&gt;&lt;/html&gt;&quot;;
  server.send(200, &quot;text/html&quot;, html);
}

void startWebServer() {
  server.on(&quot;/&quot;, handleRoot);
  server.begin();
  Serial.println(&quot;Web server started. Access via http://&quot; + WiFi.localIP().toString());
}

void loop() {
  gpsSerial.listen();
  while (gpsSerial.available()) gps.encode(gpsSerial.read());

  server.handleClient();
}</code></pre></pre>
<h3>How it works</h3>
<ol>
<li><strong><em>WebServer server(80);</em></strong> creates a basic web server on port 80.</li>
<li>The <strong><em>handleRoot()</em></strong> function reads <em>quakes.csv</em> and formats it into an HTML table.</li>
<li>The <strong><em>meta refresh</em></strong> tag reloads the page every minute automatically.</li>
<li>The <strong><em>startWebServer()</em></strong> function launches the web interface after Wi-Fi connects and SD initializes.</li>
</ol>
<p>To start the dashboard, just call this inside <em>setup()</em> after SD initialization:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<pre><pre><code class="language-cpp">startWebServer();</code></pre></pre>
</div>
</div>
<p>When you connect to your ESP32’s IP (check Serial Monitor), you’ll see a simple HTML table listing quake records stored on the SD card:</p>
<div class="_tableContainer_1rjym_1">
<div class="group _tableWrapper_1rjym_13 flex w-fit flex-col-reverse" tabindex="-1">
<table class="w-fit min-w-(--thread-content-width)">
<thead>
<tr>
<th data-col-size="sm">Date</th>
<th data-col-size="sm">Location</th>
<th data-col-size="sm">Magnitude</th>
<th data-col-size="sm">Distance</th>
</tr>
</thead>
<tbody>
<tr>
<td data-col-size="sm">2025-10-18</td>
<td data-col-size="sm">Davao Region</td>
<td data-col-size="sm">5.7</td>
<td data-col-size="sm">120 km</td>
</tr>
<tr>
<td data-col-size="sm">2025-10-18</td>
<td data-col-size="sm">Mindoro Strait</td>
<td data-col-size="sm">4.9</td>
<td data-col-size="sm">450 km</td>
</tr>
</tbody>
</table>
</div>
</div>
<p>The full code can be downloaded here: <a href="https://github.com/rppelayo/esp32-earthquake">github repo</a></p>
<h3>Notes</h3>
<ul>
<li>You can open the page from any device on the same Wi-Fi network (phone, laptop, tablet).</li>
<li>To clear old logs, simply remove or format the SD card.</li>
<li>You can expand this with JavaScript charting (like Chart.js) for visual magnitude trends later.</li>
</ul>
<hr />
<h3>Visual Indicators</h3>
<ul>
<li><strong>RGB LED Green</strong> — No nearby quakes.</li>
<li><strong>RGB LED Red + Buzzer</strong> — Earthquake within ~200 km radius.</li>
</ul>
<p>You can tweak the threshold or add <strong>yellow</strong> for “moderate distance.”</p>
<hr />
<h3>Bonus Improvements</h3>
<ul>
<li>Add a <strong>real-time clock (RTC)</strong> backup if NTP isn’t reachable.</li>
<li>Include <strong>magnitude filtering</strong> (e.g., only warn for M≥5.0).</li>
<li>Replace the buzzer with a <strong><a href="https://www.teachmemicro.com/arduino-relay-module-tutorial/">relay</a> and a siren</strong> for stronger sound alerts</li>
<li>Create a <strong>map visualization</strong> using <a href="https://www.teachmemicro.com/esp32-gps-google-maps/">Google Maps API</a> for stored quakes.</li>
</ul>
<hr />
<h3>Final Thoughts</h3>
<p>With the NTP-based auto date update, GPS-based distance calculation, SD card logging, RGB alert LED, and now this web dashboard, your ESP32 Earthquake Monitor is a full-fledged, connected device worthy of a spot on your desk.</p>
<p>It not only helps answer “Was that an earthquake?” —it lets you prove it with data, logs, and a neat little web interface.</p>
<p>The post <a href="https://www.teachmemicro.com/esp32-earthquake-monitor-with-usgs-api-gps-lcd-and-web-dashboard/">ESP32 Earthquake Monitor with USGS API, GPS, LCD, and Web Dashboard</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Using a 16x2 Monochrome LCD with the PIC16F84A (Assembly Tutorial)</title>
		<link>https://www.teachmemicro.com/using-a-16x2-monochrome-lcd-with-the-pic16f84a-assembly-tutorial/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=using-a-16x2-monochrome-lcd-with-the-pic16f84a-assembly-tutorial</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Wed, 24 Sep 2025 00:48:52 +0000</pubDate>
				<category><![CDATA[PIC Projects]]></category>
		<category><![CDATA[PIC Tutorial]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=9316</guid>

					<description><![CDATA[<p>Introduction A 16x2 monochrome LCD module is a widely used display in microcontroller projects. It can show two lines of text, each up to 16 characters long, making it ideal for simple user interfaces. These LCDs are based on the HD44780 controller (or compatible), which accepts commands and data via an 8-bit or 4-bit parallel &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/using-a-16x2-monochrome-lcd-with-the-pic16f84a-assembly-tutorial/">Using a 16x2 Monochrome LCD with the PIC16F84A (Assembly Tutorial)</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h3><strong>Introduction</strong></h3>
<p>A 16x2 monochrome LCD module is a widely used display in microcontroller projects. It can show <strong>two lines of text, each up to 16 characters long</strong>, making it ideal for simple user interfaces. These LCDs are based on the <strong>HD44780 controller (or compatible)</strong>, which accepts commands and data via an 8-bit or 4-bit parallel interface.</p>
<p>The LCD works by receiving control signals (RS, R/W, E) and data signals (D0–D7 or D4–D7 for 4-bit mode). In this tutorial, we’ll use <strong>8-bit mode</strong> for simplicity. The PIC16F84A will send initialization commands to the LCD, followed by ASCII characters to be displayed.</p>
<p>Understanding the control lines:</p>
<ul>
<li><strong>RS (Register Select):</strong>
<ul>
<li>0 = Instruction/command (e.g., clear display).</li>
<li>1 = Data (character to display).</li>
</ul>
</li>
<li><strong>R/W (Read/Write):</strong>
<ul>
<li>0 = Write to LCD.</li>
<li>1 = Read from LCD (rarely needed, we’ll keep it  0.</li>
</ul>
</li>
<li><strong>E (Enable):</strong>
<ul>
<li>A high-to-low pulse latches the data/command into the LCD.</li>
</ul>
</li>
</ul>
<p>Once initialized, the LCD can display text by sending ASCII codes as data.</p>
<hr />
<h3><strong>Materials</strong></h3>
<ul>
<li>PIC16F84A microcontroller</li>
<li>16x2 HD44780-based monochrome LCD module</li>
<li>4 MHz crystal oscillator + 2 × 22pF capacitors</li>
<li>10kΩ potentiometer (for LCD contrast adjustment)</li>
<li>Breadboard and jumper wires</li>
<li>5V regulated power supply</li>
</ul>
<hr />
<h3><strong>Code (Assembly for PIC16F84A)</strong></h3>
<p>We’ll walk through the full code, then explain each part.</p>
<h4><strong>Full Program</strong></h4>
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-plain" data-lang="Plain Text"><pre><code class="language-cpp">;===========================================================
; Program: LCD 16x2 Display with PIC16F84A
;===========================================================

    list p=16F84A
    include &lt;p16f84a.inc&gt;

    __CONFIG _FOSC_HS &amp; _WDTE_OFF &amp; _PWRTE_ON &amp; _CP_OFF

;-------------------------------
; Pin connections
; LCD Data -&gt; PORTB
; RS -&gt; RA0
; RW -&gt; RA1 (tied low, write only)
; EN -&gt; RA2
;-------------------------------

    cblock 0x20
        temp
    endc

;-------------------------------
; Reset vector
;-------------------------------
    org 0x00
    goto main

;-------------------------------
; Subroutines
;-------------------------------
delay_ms
    movlw   d&#039;250&#039;
    movwf   temp
d1  nop
    nop
    decfsz  temp, f
    goto d1
    return

lcd_pulse
    bsf     PORTA,2      ; E=1
    call    delay_ms
    bcf     PORTA,2      ; E=0
    call    delay_ms
    return

lcd_cmd
    bcf     PORTA,0      ; RS=0
    movwf   PORTB        ; Send command
    call    lcd_pulse
    return

lcd_data
    bsf     PORTA,0      ; RS=1
    movwf   PORTB        ; Send data
    call    lcd_pulse
    return

lcd_init
    ; Function Set: 8-bit, 2 line, 5x8 font
    movlw   0x38
    call    lcd_cmd

    ; Display ON, Cursor OFF
    movlw   0x0C
    call    lcd_cmd

    ; Clear display
    movlw   0x01
    call    lcd_cmd

    ; Entry mode: auto-increment
    movlw   0x06
    call    lcd_cmd
    return

;-------------------------------
; Main Program
;-------------------------------
main
    bsf     STATUS, RP0
    movlw   0x00
    movwf   TRISB       ; PORTB = output
    movlw   0xF8
    movwf   TRISA       ; RA0-RA2 output, RA3-RA4 input
    bcf     STATUS, RP0

    call    lcd_init

    ; Write &quot;HELLO&quot;
    movlw   &#039;H&#039;
    call    lcd_data
    movlw   &#039;E&#039;
    call    lcd_data
    movlw   &#039;L&#039;
    call    lcd_data
    movlw   &#039;L&#039;
    call    lcd_data
    movlw   &#039;O&#039;
    call    lcd_data

endless
    goto endless

    end</code></pre></pre>
</div>
<p><strong><a class="sim-btn" href="https://picsimulator.com/?example=lcd-hello" target="_blank" rel="noopener" aria-label="Open LCD Hello example in PICSimulator"><img loading="lazy" decoding="async" class="sim-icon" src="https://picsimulator.com/favicon.png" width="20" height="20" style="width: 20px; height: 20px; vertical-align: middle; margin-right: 5px;" data-wp-editing="1" />Simulate this code in PICSimulator</a></strong></p>
<h3>Code Explanation</h3>
<h4><strong>1. Configuration and Setup</strong></h4>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-plain" data-lang="Plain Text"><pre><code class="language-cpp">list p=16F84A
    include &lt;P16F84A.inc&gt;
    __CONFIG _FOSC_HS &amp; _WDTE_OFF &amp; _PWRTE_ON &amp; _CP_OFF</code></pre></pre>
</div>
</div>
<ul>
<li>Sets processor type and includes the register definitions.</li>
<li>Configures the microcontroller for <strong>High-Speed crystal oscillator</strong>, <strong>watchdog timer off</strong>, <strong>power-up timer on</strong>, and <strong>code protection off</strong>.</li>
</ul>
<h4><strong>2. LCD Pin Mapping</strong></h4>
<ul>
<li><strong>PORTB</strong> is used for LCD data (D0–D7).</li>
<li><strong>RA0 = RS</strong>, <strong>RA1 = R/W (grounded)</strong>, <strong>RA2 = E</strong>.</li>
<li>Only writing to the LCD is needed, so R/W is tied to ground.</li>
</ul>
<h4><strong>3. Delay Routine</strong></h4>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-plain" data-lang="Plain Text"><pre><code class="language-cpp">delay_ms
    movlw d&#039;250&#039;
    movwf temp
d1: decfsz temp, f
    goto d1
    return</code></pre></pre>
</div>
</div>
<p>A simple software delay loop to let the LCD process commands.</p>
<h4><strong>4. LCD Enable Pulse</strong></h4>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-plain" data-lang="Plain Text"><pre><code class="language-cpp">lcd_pulse
    bsf PORTA,2   ; E=1
    call delay_ms
    bcf PORTA,2   ; E=0
    call delay_ms
    return</code></pre></pre>
</div>
</div>
<p>The LCD latches commands/data on the <strong>falling edge of E</strong>.</p>
<h4><strong>5. Sending Commands and Data</strong></h4>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-plain" data-lang="Plain Text"><pre><code class="language-cpp">lcd_cmd
    bcf PORTA,0   ; RS=0 (command)
    movwf PORTB
    call lcd_pulse
    return</code></pre></pre>
</div>
</div>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-plain" data-lang="Plain Text"><pre><code class="language-cpp">lcd_data
    bsf PORTA,0   ; RS=1 (data)
    movwf PORTB
    call lcd_pulse
    return</code></pre></pre>
</div>
</div>
<p>These subroutines separate <strong>instructions (clear, cursor)</strong> from <strong>characters (ASCII)</strong>.</p>
<h4><strong>6. LCD Initialization</strong></h4>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-plain" data-lang="Plain Text"><pre><code class="language-cpp">movlw 0x38   ; Function set: 8-bit, 2-line, 5x8 font
movlw 0x0C   ; Display ON, cursor OFF
movlw 0x01   ; Clear display
movlw 0x06   ; Entry mode: auto-increment</code></pre></pre>
</div>
</div>
<p>These commands are required at startup to prepare the LCD.</p>
<h4><strong>7. Writing Text</strong></h4>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-plain" data-lang="Plain Text"><pre><code class="language-cpp">movlw &#039;H&#039;
call lcd_data</code></pre></pre>
</div>
</div>
<p>Each ASCII character is written sequentially to the LCD.</p>
<hr />
<h3><strong>Conclusion</strong></h3>
<p>In this tutorial, we connected a <strong>16x2 monochrome LCD</strong> to a <strong>PIC16F84A microcontroller</strong> using an <strong>8-bit parallel interface</strong>. We wrote initialization and display routines in <strong>assembly</strong>, giving full control of the LCD at a low level.</p>
<p>This project demonstrates how to:</p>
<ul>
<li>Initialize the LCD in 8-bit mode.</li>
<li>Send commands and data separately.</li>
<li>Display custom strings of text.</li>
</ul>
<p>With this foundation, you can extend the program to display sensor data, menu options, or real-time feedback for embedded projects.</p>
<p>The post <a href="https://www.teachmemicro.com/using-a-16x2-monochrome-lcd-with-the-pic16f84a-assembly-tutorial/">Using a 16x2 Monochrome LCD with the PIC16F84A (Assembly Tutorial)</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>ESP32 Meets AI: Talking to Large Language Models via OpenRouter</title>
		<link>https://www.teachmemicro.com/esp32-meets-ai-talking-to-large-language-models-via-openrouter/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=esp32-meets-ai-talking-to-large-language-models-via-openrouter</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Mon, 15 Sep 2025 02:33:34 +0000</pubDate>
				<category><![CDATA[ESP32 Projects]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=9213</guid>

					<description><![CDATA[<p>Large Language Models (LLMs) like ChatGPT are usually something you access from a laptop or phone. But what if your humble ESP32 could send a question over Wi-Fi and get an answer back? That’s what we’ll build in this tutorial. We’ll make the ESP32 query an LLM through the OpenRouter API, and print the response on &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/esp32-meets-ai-talking-to-large-language-models-via-openrouter/">ESP32 Meets AI: Talking to Large Language Models via OpenRouter</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Large Language Models (LLMs) like ChatGPT are usually something you access from a laptop or phone. But what if your humble <strong>ESP32</strong> could send a question over Wi-Fi and get an answer back? That’s what we’ll build in this tutorial. We’ll make the ESP32 query an LLM through the <strong>OpenRouter API</strong>, and print the response on Serial. We’ll cover <strong>two approaches</strong>:</p>
<ol>
<li><strong>Direct:</strong> ESP32 → OpenRouter API</li>
<li><strong>With Proxy:</strong> ESP32 → Python Flask Server → OpenRouter API</li>
</ol>
<p>By the end, you’ll have an ESP32 that can “talk” to AI — and you’ll understand which setup is best for demos vs real projects.</p>
<hr />
<h3><strong>What is OpenRouter?</strong></h3>
<p><a class="decorated-link cursor-pointer" href="https://openrouter.ai/" target="_new" rel="noopener">OpenRouter</a> is a service that lets you use many different AI models (OpenAI, Anthropic, Mistral, LLaMA, etc.) with just <strong>one API</strong>. Instead of juggling keys and endpoints, you use a single <strong>chat completions</strong> endpoint:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">POST https://openrouter.ai/api/v1/chat/completions</code></pre></pre>
</div>
</div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr">You send:</div>
</div>
<ul>
<li><em>Authorization: Bearer &lt;API_KEY&gt;</em></li>
<li>Optional: <em>HTTP-Referer</em> and <em>X-Title</em> headers (to identify your app)</li>
<li>JSON body with <em>model</em>, <em>messages</em>, etc.</li>
</ul>
<p>The response looks like OpenAI’s API: a JSON with <em>choices[0].message.content</em>.</p>
<hr />
<h3><strong>Approach A: ESP32 Directly to OpenRouter</strong></h3>
<p>This is the “bare minimum” setup: the ESP32 connects to Wi-Fi and makes an HTTPS POST request.</p>
<h4><strong>Hardware Needed</strong></h4>
<ul>
<li>Any ESP32 development board</li>
<li>Wi-Fi connection</li>
<li>Arduino IDE</li>
</ul>
<h4><strong>Sketch</strong></h4>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-cpp" data-lang="C++"><pre><code class="language-cpp">#include &lt;WiFi.h&gt;
#include &lt;WiFiClientSecure.h&gt;
#include &lt;HTTPClient.h&gt;

// ====== EDIT THESE ======
const char* WIFI_SSID = &quot;YOUR_WIFI&quot;;
const char* WIFI_PASS = &quot;YOUR_PASS&quot;;
const char* OPENROUTER_API_KEY = &quot;YOUR_API_KEY&quot;;
const char* OPENROUTER_URL = &quot;https://openrouter.ai/api/v1/chat/completions&quot;;
const char* MODEL = &quot;openai/gpt-4o-mini&quot;;
// ========================

void setup() {
  Serial.begin(115200);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print(&quot;.&quot;); }
  Serial.println(&quot;\nWiFi connected.&quot;);

  WiFiClientSecure client;
  client.setInsecure(); // For demo only! Use setCACert() in production.

  HTTPClient https;
  if (!https.begin(client, OPENROUTER_URL)) {
    Serial.println(&quot;HTTPS begin failed&quot;);
    return;
  }

  https.addHeader(&quot;Content-Type&quot;, &quot;application/json&quot;);
  https.addHeader(&quot;Authorization&quot;, String(&quot;Bearer &quot;) + OPENROUTER_API_KEY);
  https.addHeader(&quot;HTTP-Referer&quot;, &quot;https://teachmemicro.com&quot;);
  https.addHeader(&quot;X-Title&quot;, &quot;ESP32 LLM Demo&quot;);

  String body = String(&quot;{\&quot;model\&quot;:\&quot;&quot;) + MODEL + &quot;\&quot;,&quot;
                &quot;\&quot;messages\&quot;:[&quot;
                  &quot;{\&quot;role\&quot;:\&quot;system\&quot;,\&quot;content\&quot;:\&quot;Answer in &lt;=25 words.\&quot;},&quot; &quot;{\&quot;role\&quot;:\&quot;user\&quot;,\&quot;content\&quot;:\&quot;Say hi from an ESP32.\&quot;}&quot; &quot;],&quot; &quot;\&quot;max_tokens\&quot;: 50,&quot; &quot;\&quot;temperature\&quot;: 0.7&quot; &quot;}&quot;; int code = https.POST(body); if (code &gt; 0) {
    Serial.printf(&quot;HTTP %d\n&quot;, code);
    String resp = https.getString();
    Serial.println(resp);
  } else {
    Serial.printf(&quot;HTTP error: %d\n&quot;, code);
  }
  https.end();
}

void loop() {}</code></pre></pre>
</div>
</div>
</div>
</div>
</div>
<h4><strong>Things to Note</strong></h4>
<ul>
<li><strong>API key is on the device.</strong> Anyone who gets your ESP32 could extract it.</li>
<li><strong>TLS certificates.</strong> I used <em>setInsecure()</em> for demo. Properly, you should embed OpenRouter’s root CA.</li>
<li><strong>Memory.</strong> ESP32 RAM is small. Don’t ask for long essays; keep <em>max_tokens</em> low.</li>
<li><strong>Parsing JSON.</strong> The full response is big. Use <em>ArduinoJson</em> filters if you only need <em>choices[0].message.content</em>.</li>
</ul>
<p>This works fine for <strong>demos and lab projects</strong>. But for real-world apps, we need more control.</p>
<hr />
<h3><strong>Approach B: ESP32 → Python Proxy → OpenRouter</strong></h3>
<p>Here, the ESP32 doesn’t talk to OpenRouter directly. Instead, it calls a <strong>tiny Python Flask server</strong> you run on your PC, Raspberry Pi, or VPS. The proxy calls OpenRouter, trims the answer, and returns a small JSON.</p>
<p>Why bother?</p>
<ul>
<li>No API key on the ESP32.</li>
<li>Responses are small and easy to parse.</li>
<li>You can switch models or preprocess answers without touching the ESP32.</li>
</ul>
<h3><strong>Setting Up the Python Flask Proxy on Windows</strong></h3>
<p>We’ll create a tiny Flask server that your ESP32 can call on your home network. These steps use <strong>Windows 10/11</strong>, <strong>PowerShell</strong>, and <strong>Python 3.10+</strong>.</p>
<h4><strong>1) Install Python (and add to PATH)</strong></h4>
<ol>
<li>Download Python from <strong>python.org → Downloads → Windows</strong>.</li>
<li>Run the installer and <strong>tick “Add python.exe to PATH.”</strong></li>
<li>Verify in PowerShell:
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">python --version
pip --version</code></pre></pre>
</div>
</div>
</div>
<p>If either command is not found, sign out/in or reboot.</li>
</ol>
<hr />
<h4><strong>2) Make a project folder</strong></h4>
<p>Open <strong>PowerShell</strong> and create a folder anywhere (e.g., Documents):</p>
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">cd $HOME\Documents
mkdir esp32-llm-proxy
cd esp32-llm-proxy</code></pre></pre>
</div>
<hr />
<h4><strong>3) Create and activate a virtual environment</strong></h4>
<p>A <em>venv</em> keeps your dependencies clean and local to the project.</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">python -m venv .venv
.\.venv\Scripts\Activate.ps1   # You should see (.venv) prefix in the prompt</code></pre></pre>
</div>
</div>
</div>
<p>If you get an execution policy error, run PowerShell <strong>as Administrator</strong> once:</p>
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">Set-ExecutionPolicy RemoteSigned</code></pre></pre>
</div>
<p>Then retry <em>Activate.ps1</em>.</p>
<h4><strong>4) Add the three files</strong></h4>
<p>Create <strong>requirements.txt</strong>, <strong>.env</strong>, and <strong>server.py</strong> in the folder.</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">esp32-llm-proxy/
├─ server.py
├─ requirements.txt
├─ .env</code></pre></pre>
</div>
</div>
</div>
<p><strong>requirements.txt</strong></p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">flask
requests
python-dotenv</code></pre></pre>
</div>
<p><strong>.env</strong></p>
</div>
</div>
</div>
</div>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">OPENROUTER_API_KEY=your_key_here
MODEL=openai/gpt-4o-mini
APP_URL=https://teachmemicro.com
APP_TITLE=ESP32 LLM Proxy</code></pre></pre>
</div>
</div>
</div>
<p><strong>server.py</strong></p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-python" data-lang="Python"><pre><code class="language-cpp">import os, json, requests
from flask import Flask, request, jsonify
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv(&quot;OPENROUTER_API_KEY&quot;)
MODEL = os.getenv(&quot;MODEL&quot;, &quot;openai/gpt-4o-mini&quot;)
APP_URL = os.getenv(&quot;APP_URL&quot;)
APP_TITLE = os.getenv(&quot;APP_TITLE&quot;)

OPENROUTER_URL = &quot;https://openrouter.ai/api/v1/chat/completions&quot;
app = Flask(__name__)

@app.post(&quot;/ask&quot;)
def ask():
    q = str((request.json or {}).get(&quot;q&quot;, &quot;&quot;))[:200]
    payload = {
        &quot;model&quot;: MODEL,
        &quot;messages&quot;: [
            {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;Answer concisely (&lt;=25 words).&quot;},
            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: q}
        ],
        &quot;max_tokens&quot;: 60
    }
    headers = {
        &quot;Authorization&quot;: f&quot;Bearer {API_KEY}&quot;,
        &quot;Content-Type&quot;: &quot;application/json&quot;,
        &quot;HTTP-Referer&quot;: APP_URL,
        &quot;X-Title&quot;: APP_TITLE,
    }
    r = requests.post(OPENROUTER_URL, headers=headers, data=json.dumps(payload))
    data = r.json()
    text = data.get(&quot;choices&quot;, [{}])[0].get(&quot;message&quot;, {}).get(&quot;content&quot;, &quot;&quot;)
    return jsonify({&quot;text&quot;: text[:512]})</code></pre></pre>
</div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr">
<p>Run it:</p>
</div>
</div>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">pip install -r requirements.txt
python server.py</code></pre></pre>
</div>
</div>
</div>
</div>
</div>
<p>Test from your PC:</p>
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">curl -X POST http://localhost:3000/ask -H &quot;Content-Type: application/json&quot; -d &#039;{&quot;q&quot;:&quot;Say hi!&quot;}&#039;</code></pre></pre>
</div>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<p>Or if you don't have curl:</p>
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">Invoke-RestMethod -Method Post -Uri http://localhost:3000/ask -Body (@{q=&quot;Say hi!&quot;} | ConvertTo-Json) -ContentType &quot;application/json&quot;</code></pre></pre>
</div>
<p>You should see a JSON response:</p>
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">{&quot;text&quot;:&quot;Hi from the LLM!&quot;}</code></pre></pre>
</div>
<p><strong>ESP32 Sketch (Proxy Version)</strong></p>
<p>Now that your python proxy server is running on PC, upload this sketch to your ESP32:</p>
</div>
</div>
</div>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-cpp" data-lang="C++"><pre><code class="language-cpp">#include &lt;WiFi.h&gt;
#include &lt;HTTPClient.h&gt; 

const char* WIFI_SSID = &quot;YOUR_WIFI&quot;;
const char* WIFI_PASS = &quot;YOUR_PASS&quot;;
const char* PROXY_URL = &quot;http://192.168.1.100:3000/ask&quot;;

void setup() {
  Serial.begin(115200);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print(&quot;.&quot;); }
  Serial.println(&quot;\nWiFi connected.&quot;);

  HTTPClient http;
  http.begin(PROXY_URL);
  http.addHeader(&quot;Content-Type&quot;, &quot;application/json&quot;);

  String body = R&quot;({&quot;q&quot;:&quot;ESP32 says hello! Keep it short.&quot;})&quot;;
  int code = http.POST(body);

  if (code &gt; 0) {
    String resp = http.getString(); // Example: {&quot;text&quot;:&quot;Hi from the LLM!&quot;}
    Serial.println(resp);
  } else {
    Serial.printf(&quot;HTTP error: %d\n&quot;, code);
  }
  http.end();
}

void loop() {}</code></pre></pre>
</div>
</div>
</div>
<p>Open <strong>Serial Monitor @ 115200</strong>. You should see the tiny JSON:</p>
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">{&quot;text&quot;:&quot;Hi from the LLM!&quot;}</code></pre></pre>
</div>
<hr />
<h3><strong>Pros and Cons</strong></h3>
<p><strong>Direct (ESP32 → OpenRouter)</strong><br />
<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Simple, no server<br />
<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> One less hop<br />
✘ API key in firmware<br />
✘ Large JSON responses<br />
✘ Hard to change models without reflashing</p>
<p><strong>Proxy (ESP32 → Flask → OpenRouter)</strong><br />
<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> API key stays safe<br />
<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Tiny, clean responses<br />
<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Easy to swap models or add features (caching, logging, RAG)<br />
✘ Requires hosting a server<br />
✘ One extra hop</p>
<hr />
<h3><strong>When to Use Which</strong></h3>
<ul>
<li><strong>For a classroom demo or quick experiment:</strong> Direct is fine.</li>
<li><strong>For anything serious (multiple devices, production, or publishing a project):</strong> Use a proxy.</li>
</ul>
<hr />
<h3><strong>Final Thoughts</strong></h3>
<p>This project shows how the ESP32 — a microcontroller with just a few hundred KB of RAM — can still talk to cutting-edge AI models. Thanks to <strong>OpenRouter</strong>, you can choose models freely without rewriting code.</p>
<p>Start with the direct approach if you just want to see “Hello from ESP32” echoed back by an AI. But if you plan to make an AI gadget (like a smart display or voice assistant), invest the extra step: build a proxy. It makes your setup more secure, scalable, and flexible.</p>
<p>The post <a href="https://www.teachmemicro.com/esp32-meets-ai-talking-to-large-language-models-via-openrouter/">ESP32 Meets AI: Talking to Large Language Models via OpenRouter</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Getting Started with TensorFlow Lite on ESP32 (With Voice Activity Detection Project)</title>
		<link>https://www.teachmemicro.com/getting-started-with-tensorflow-lite-on-esp32-with-voice-activity-detection-project/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=getting-started-with-tensorflow-lite-on-esp32-with-voice-activity-detection-project</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Mon, 16 Jun 2025 23:00:16 +0000</pubDate>
				<category><![CDATA[ESP32 Projects]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=8683</guid>

					<description><![CDATA[<p>Machine learning (ML) isn’t just for powerful computers anymore. With TensorFlow Lite for Microcontrollers (TFLM), we can run tiny ML models on low-power devices like the ESP32, enabling real-time intelligence at the edge—no cloud needed! What is TensorFlow Lite for Microcontrollers? TensorFlow Lite for Microcontrollers is a lightweight version of TensorFlow designed specifically to run &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/getting-started-with-tensorflow-lite-on-esp32-with-voice-activity-detection-project/">Getting Started with TensorFlow Lite on ESP32 (With Voice Activity Detection Project)</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><a href="https://www.teachmemicro.com/tinyml-with-esp32-tutorial/">Machine learning (ML)</a> isn’t just for powerful computers anymore. With <strong>TensorFlow Lite for Microcontrollers (TFLM)</strong>, we can run tiny ML models on low-power devices like the ESP32, enabling real-time intelligence at the edge—no cloud needed!</p>
<p><span id="more-8683"></span></p>
<h2>What is TensorFlow Lite for Microcontrollers?</h2>
<p>TensorFlow Lite for Microcontrollers is a lightweight version of TensorFlow designed specifically to run machine learning models on devices with limited memory and processing power, such as the ESP32.</p>
<p><strong>Key features include:</strong></p>
<ul>
<li style="list-style-type: none;">
<ul>
<li>Lightweight and optimized for microcontrollers</li>
<li>Can run with less than 100 KB RAM</li>
<li>Supports common ML operations like fully connected layers and convolution</li>
</ul>
</li>
</ul>
<h2>Requirements</h2>
<h3>Hardware</h3>
<ul>
<li style="list-style-type: none;">
<ul>
<li>ESP32 Development Board</li>
<li>Microphone Module (e.g., INMP441, MAX9814)</li>
<li>Micro USB Cable</li>
<li>(Optional) LED</li>
</ul>
</li>
</ul>
<h3>Software</h3>
<ul>
<li style="list-style-type: none;">
<ul>
<li>Arduino IDE</li>
<li>ESP32 Board Package</li>
<li>TensorFlow Lite Micro Arduino Library</li>
<li>Python (optional, for custom model preparation)</li>
</ul>
</li>
</ul>
<h2>Setting Up the Environment</h2>
<h3>1. Install Arduino IDE</h3>
<p style="padding-left: 40px;"><a class="cursor-pointer" href="https://www.arduino.cc/en/software" target="_new" rel="noopener">Download from Arduino.cc</a></p>
<h3>2. Install ESP32 Board Support</h3>
<ul>
<li style="list-style-type: none;">
<ul>
<li>Go to <strong>File → Preferences</strong>, and add the URL: <em>https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json</em></li>
<li>Install via <strong>Tools → Board → Board Manager → ESP32</strong>.</li>
</ul>
</li>
</ul>
<h3>3. Install TensorFlow Lite Library</h3>
<ul>
<li style="list-style-type: none;">
<ul>
<li>Navigate to <strong>Sketch → Include Library → Manage Libraries</strong>.</li>
<li>Search and install the <em>Arduino_TensorFlowLite</em> library.</li>
</ul>
</li>
</ul>
<h2>Practical Example: Voice Activity Detection (VAD)</h2>
<p>This project demonstrates detecting voice activity using TensorFlow Lite on ESP32.</p>
<h3>Step 1: Download and Prepare the Model</h3>
<p>Download the pre-trained model: <a class="" href="https://github.com/tensorflow/tflite-micro/blob/main/tensorflow/lite/micro/examples/micro_speech/models/audio_preprocessor_int8.tflite" target="_new" rel="noopener">Download Model (audio_preprocessor.tflite)</a></p>
<p>Convert the downloaded model into a C source file (<em>speech_model_data.cc</em>) using the following command:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">xxd -i audio_prepocessor.tflite &gt; speech_model_data.cc</code></pre></pre>
</div>
</div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr">
<p>You can install Windows Subsystem for Linux using Administrator Command Prompt to run <em>xxd</em>:</p>
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-bash" data-lang="Bash"><pre><code class="language-cpp">wsl --install</code></pre></pre>
</div>
<p>Or you can use this python script:</p>
</div>
</div>
<div dir="ltr">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-python" data-lang="Python"><pre><code class="language-cpp">with open(&quot;audio_preprocessor.tflite&quot;, &quot;rb&quot;) as f:
    data = f.read()

with open(&quot;speech_model_data.cc&quot;, &quot;w&quot;) as f:
    f.write(&quot;const unsigned char micro_speech_model_data[] = {&quot;)
    for i, byte in enumerate(data):
        if i % 12 == 0:
            f.write(&quot;\n  &quot;)
        f.write(f&quot;0x{byte:02x}, &quot;)
    f.write(&quot;\n};\nconst int micro_speech_model_data_len = &quot;)
    f.write(f&quot;{len(data)};\n&quot;)</code></pre></pre>
</div>
</div>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">Include this generated file in your Arduino project folder.</div>
</div>
<h3>Step 2: Arduino Sketch (Main Code)</h3>
<p>Open Arduino IDE and create a new sketch. Copy this code into your Arduino sketch file.</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<div class="hcb_wrap">
<pre class="prism undefined-numbers lang-cpp" data-lang="C++"><pre><code class="language-cpp">#include &quot;TensorFlowLite.h&quot;
#include &quot;speech_model_data.cc&quot;
#include &quot;tensorflow/lite/micro/micro_interpreter.h&quot;
#include &quot;tensorflow/lite/schema/schema_generated.h&quot;
#include &quot;tensorflow/lite/version.h&quot;

const tflite::Model* model = nullptr;
tflite::MicroInterpreter* interpreter = nullptr;
constexpr int kTensorArenaSize = 10 * 1024;
uint8_t tensor_arena[kTensorArenaSize];

void setup() {  
    Serial.begin(115200);  
    model = tflite::GetModel(speech_model_data);  
    if (model-&gt;version() != TFLITE_SCHEMA_VERSION) {    
        Serial.println(&quot;Model schema mismatch!&quot;);    
        return;  
    }  

    static tflite::MicroMutableOpResolver&lt;5&gt; resolver;  
    resolver.AddFullyConnected();  
    resolver.AddSoftmax();  
    resolver.AddReshape();  
    resolver.AddConv2D();  
    resolver.AddMaxPool2D();  
    static tflite::MicroInterpreter static_interpreter(    
        model, resolver, tensor_arena, kTensorArenaSize);  
    interpreter = &amp;static_interpreter;  

    interpreter-&gt;AllocateTensors();  
    Serial.println(&quot;Model ready.&quot;);
}

void loop() {  
    float dummy_input[49 * 10] = {0};  
    // Simulated data (replace with real audio input)  
    memcpy(interpreter-&gt;input(0)-&gt;data.f, dummy_input, sizeof(dummy_input));  
    TfLiteStatus invoke_status = interpreter-&gt;Invoke();  
    if (invoke_status != kTfLiteOk) {    
        Serial.println(&quot;Invoke failed!&quot;);    
        return;  
    }  

    float* output = interpreter-&gt;output(0)-&gt;data.f;  
    Serial.print(&quot;Speech confidence: &quot;);  
    Serial.println(output[1]);  

    delay(500);
}</code></pre></pre>
</div>
</div>
</div>
<h3>Step 3: Real Audio Input (Optional)</h3>
<p>To incorporate a microphone:</p>
<ul>
<li style="list-style-type: none;">
<ul>
<li>Use I2S to read microphone data (e.g., INMP441).</li>
<li>Convert audio data into a log Mel spectrogram.</li>
<li>Feed real audio input instead of the dummy data.</li>
</ul>
</li>
</ul>
<p>You can use libraries such as <a class="cursor-pointer" href="https://www.arduino.cc/reference/en/libraries/arduinofft/" target="_new" rel="noopener">ArduinoFFT</a> for FFT computation.</p>
<h2>Application Possibilities</h2>
<ul>
<li style="list-style-type: none;">
<ul>
<li>Smart Speakers</li>
<li>Voice-controlled Devices</li>
<li>Noise-level Monitoring Systems</li>
</ul>
</li>
</ul>
<h2>Next Steps and Further Exploration</h2>
<ul>
<li style="list-style-type: none;">
<ul>
<li>Train custom models easily with <a class="" href="https://edgeimpulse.com/" target="_new" rel="noopener">Edge Impulse</a>.</li>
<li>Implement network capabilities with <a href="https://www.teachmemicro.com/esp32-wifi-manager-dynamic-ssid-password/">ESP32 Wi-Fi</a>.</li>
</ul>
</li>
</ul>
<h2>Helpful Resources</h2>
<ul>
<li style="list-style-type: none;">
<ul>
<li><a class="cursor-pointer" href="https://www.tensorflow.org/lite/microcontrollers" target="_new" rel="noopener">TensorFlow Lite for Microcontrollers</a></li>
<li><a class="" href="https://github.com/espressif/arduino-esp32" target="_new" rel="noopener">ESP32 Arduino Library Documentation</a></li>
<li><a class="cursor-pointer" href="https://studio.edgeimpulse.com/" target="_new" rel="noopener">Edge Impulse (Free ML training platform)</a></li>
</ul>
</li>
</ul>
<p>The post <a href="https://www.teachmemicro.com/getting-started-with-tensorflow-lite-on-esp32-with-voice-activity-detection-project/">Getting Started with TensorFlow Lite on ESP32 (With Voice Activity Detection Project)</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: www.teachmemicro.com @ 2026-07-21 16:26:22 by W3 Total Cache
-->