{"id":9248,"date":"2026-04-01T16:40:21","date_gmt":"2026-04-01T23:40:21","guid":{"rendered":"https:\/\/visualgdb.com\/w\/?p=9248"},"modified":"2026-04-01T16:40:21","modified_gmt":"2026-04-01T23:40:21","slug":"connecting-a-rotary-encoder-to-esp32-using-ai","status":"publish","type":"post","link":"https:\/\/visualgdb.com\/tutorials\/ai\/esp32\/encoder\/","title":{"rendered":"Connecting a Rotary Encoder to ESP32 using AI"},"content":{"rendered":"<p>In this tutorial we will use AI edits to modify the ESP32 HTTP server example to handle a rotary encoder and show its real-time position on the main page.<\/p>\n<p>We will use AI edits to quickly do trivial work (e.g. translate encoder signals to rotation value), while still keeping full control over the code quality. Before you begin, install VisualGDB 6.1 or later.<\/p>\n<ol>\n<li>Start Visual Studio and launch the ESP-IDF project wizard:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/01-idf.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9249\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/01-idf.png\" alt=\"\" width=\"890\" height=\"575\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/01-idf.png 890w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/01-idf-300x194.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/01-idf-768x496.png 768w\" sizes=\"(max-width: 890px) 100vw, 890px\" \/><\/a><\/li>\n<li>Enter the name and location where you would like to put the project:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/02-path.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9250\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/02-path.png\" alt=\"\" width=\"890\" height=\"575\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/02-path.png 890w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/02-path-300x194.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/02-path-768x496.png 768w\" sizes=\"(max-width: 890px) 100vw, 890px\" \/><\/a><\/li>\n<li>Proceed with the default build system settings:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/03-type.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9254\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/03-type.png\" alt=\"\" width=\"856\" height=\"693\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/03-type.png 856w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/03-type-300x243.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/03-type-768x622.png 768w\" sizes=\"(max-width: 856px) 100vw, 856px\" \/><\/a><\/li>\n<li>Select the consolidated ESP-IDF toolchain and ESP-IDF version you would like to use:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/04-dev.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9255\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/04-dev.png\" alt=\"\" width=\"856\" height=\"693\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/04-dev.png 856w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/04-dev-300x243.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/04-dev-768x622.png 768w\" sizes=\"(max-width: 856px) 100vw, 856px\" \/><\/a><\/li>\n<li>On the Project Sample selection page select <strong>http_server -&gt; simple<\/strong>:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/05-server.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9256\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/05-server.png\" alt=\"\" width=\"856\" height=\"693\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/05-server.png 856w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/05-server-300x243.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/05-server-768x622.png 768w\" sizes=\"(max-width: 856px) 100vw, 856px\" \/><\/a><\/li>\n<li>On the last page of the wizard, pick the debug settings that work with your device:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/06-debug.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9257\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/06-debug.png\" alt=\"\" width=\"856\" height=\"693\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/06-debug.png 856w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/06-debug-300x243.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/06-debug-768x622.png 768w\" sizes=\"(max-width: 856px) 100vw, 856px\" \/><\/a><\/li>\n<li>Press &#8220;Finish&#8221; to create the project. Make sure it gets configured correctly:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/07-main.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9258\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/07-main.png\" alt=\"\" width=\"1465\" height=\"907\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/07-main.png 1465w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/07-main-300x186.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/07-main-1024x634.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/07-main-768x475.png 768w\" sizes=\"(max-width: 1465px) 100vw, 1465px\" \/><\/a><\/li>\n<li>Before adding any changes, we will verify that the original project can connect to the Wi-Fi network. Open VisualGDB Project Properties and configure the Wi-Fi SSID\/Password parameters:\u00a0<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/08-password.png\"><br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9259\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/08-password.png\" alt=\"\" width=\"1129\" height=\"770\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/08-password.png 1129w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/08-password-300x205.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/08-password-1024x698.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/08-password-768x524.png 768w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/08-password-130x90.png 130w\" sizes=\"(max-width: 1129px) 100vw, 1129px\" \/><\/a><\/li>\n<li>Press F5 to build the project and start debugging it. Once it loads and connects to the Wi-Fi network, try opening <strong>http:\/\/&lt;IP ADDRESS&gt;\/<\/strong> (not https) in the browser and make sure it shows the default message:\u00a0<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/09-mainpage.png\"><br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9260\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/09-mainpage.png\" alt=\"\" width=\"754\" height=\"401\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/09-mainpage.png 754w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/09-mainpage-300x160.png 300w\" sizes=\"(max-width: 754px) 100vw, 754px\" \/><\/a><\/li>\n<li>We will use the GPIO driver to interface with the rotary encoder, so make sure it is referenced from the main component&#8217;s <strong>CMakeLists.txt<\/strong> file:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/10-gpio.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9261\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/10-gpio.png\" alt=\"\" width=\"1465\" height=\"907\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/10-gpio.png 1465w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/10-gpio-300x186.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/10-gpio-1024x634.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/10-gpio-768x475.png 768w\" sizes=\"(max-width: 1465px) 100vw, 1465px\" \/><\/a><\/li>\n<li>In this tutorial we will use the <a href=\"https:\/\/docs.espressif.com\/projects\/esp-idf\/en\/release-v4.2\/esp32\/hw-reference\/esp32\/get-started-wrover-kit.html\">ESP32-WROVER kit board<\/a>, that uses most GPIO pins for existing peripherals. As we are not using on-board LCD display in this tutorial, we will borrow the GPIO19 and GPIO22 pins for use with the encoder:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/04\/pins.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9284\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/04\/pins.png\" alt=\"\" width=\"721\" height=\"579\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/04\/pins.png 721w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/04\/pins-300x241.png 300w\" sizes=\"(max-width: 721px) 100vw, 721px\" \/><\/a><\/li>\n<li>To start using them, we would need to first configure them as inputs using the ESP-IDF GPIO functions in <strong>main()<\/strong>. Instead of doing it by hand, we will use VisualGDB&#8217;s symbol-level edits to quickly update the code based on a minimal prompt. Click the edit button in the CodeJumps link above <strong>main() <\/strong>to begin the edit:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/11-start.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9262\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/11-start.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/11-start.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/11-start-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/11-start-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/11-start-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>Use the following prompt:\n<pre>setup gpio#19+22 as pullup inputs using @gpio_config + read to tmp vars using @gpio_get_level<\/pre>\n<p><a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/12-edit.png\"><br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9263\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/12-edit.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/12-edit.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/12-edit-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/12-edit-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/12-edit-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>VisualGDB will automatically pick <strong>main()<\/strong> and all declarations used by it, will pass it to the model together with the symbols mentioned in the prompt. This works much more efficiently than passing entire files at a time, so it only takes a few seconds to get the edits done:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/13-snippet.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9264\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/13-snippet.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/13-snippet.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/13-snippet-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/13-snippet-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/13-snippet-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a>Note that VisualGDB automatically passes the definitions of <strong>gpio_config_t<\/strong>, <strong>gpio_get_level()<\/strong>, etc. to the model, so it will work equally well with frameworks that are less known than ESP-IDF.<\/li>\n<li>Now we will ask the AI to read and print the GPIO values in a loop. Use this prompt on <strong>main()<\/strong>:\n<pre>in loop, print IO19=..,..<\/pre>\n<p>This will update the while() loop in the end to print the GPIO values:<br \/>\n<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/14-print.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9265\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/14-print.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/14-print.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/14-print-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/14-print-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/14-print-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>Build the code and start another debugging session. Once the firmware starts, connect to the board&#8217;s COM port via a context menu in Solution Explorer and make sure it prints the values:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/15-output.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9266\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/15-output.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/15-output.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/15-output-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/15-output-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/15-output-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>You can now connect the encoder to the pins on the board and try turning it. The IO19 and IO22 values should change as your rotate the knob:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/encoder.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9282\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/encoder.jpg\" alt=\"\" width=\"1280\" height=\"862\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/encoder.jpg 1280w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/encoder-300x202.jpg 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/encoder-1024x690.jpg 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/encoder-768x517.jpg 768w\" sizes=\"(max-width: 1280px) 100vw, 1280px\" \/><\/a><\/li>\n<li>Now we will restructure the code to have all encoder-related logic in a single source file. It will use the <strong>RotaryEncoder<\/strong> struct to store the encoder configuration and state, and will have <strong>RotaryEncoder_XXX()<\/strong> functions for various functionality.\u00a0 Add a new file called <strong>RotaryEncoder.c<\/strong> the project:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/16-newfile.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9267\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/16-newfile.png\" alt=\"\" width=\"702\" height=\"311\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/16-newfile.png 702w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/16-newfile-300x133.png 300w\" sizes=\"(max-width: 702px) 100vw, 702px\" \/><\/a><\/li>\n<li>Similarly, add <strong>RotaryEncoder.h<\/strong> containing an empty RotaryEncoder struct:\n<pre>#pragma once\r\n\r\nstruct RotaryEncoder\r\n{\r\n};<\/pre>\n<p>Make sure <strong>RotaryEncoder.h<\/strong> is included from <strong>main.c<\/strong> and <strong>RotaryEncoder.c<\/strong>.<\/li>\n<li>Now we can use the AI to move initialization code to our new encoder driver. Add the &#8220;<strong>RotaryEncoderInit(&amp;s_Encoder)<\/strong>&#8221; call to main and use the following prompt:\n<pre>s_Encoder above main, init in @RotaryEncoder.c; move gpio init there<\/pre>\n<p><a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/17-move.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9268\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/17-move.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/17-move.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/17-move-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/17-move-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/17-move-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>This is sufficient for the AI to create the missing declarations, definitions, and move the current initialization code where it belongs:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/18-moved.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9269\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/18-moved.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/18-moved.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/18-moved-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/18-moved-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/18-moved-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>Note that the init function still has hardcoded GPIO19 and GPIO22 pins. To make the driver more flexible, we should instead pass them as arguments and store in RotaryEncoder&#8217;s fields. Use this prompt:\n<pre>Take int pinA=19, pinB as args, store in PinA\/B fields<\/pre>\n<p><a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/19-changesig.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9270\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/19-changesig.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/19-changesig.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/19-changesig-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/19-changesig-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/19-changesig-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>This will quickly add the fields to the struct, add initialization code and update the call in<strong> main()<\/strong>:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/20-args.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9271\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/20-args.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/20-args.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/20-args-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/20-args-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/20-args-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>Now we will add an interrupt handler that will get invoked whenever the signal on either of the pins changes. Use this prompt on the init function:\n<pre>Add edge-triggered interrupt handler (static RotaryEncoder_HandleGPIOInterrupt) on both pins.\r\nUse @gpio_install_isr_service , @gpio_isr_handler_add<\/pre>\n<p><a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/21-isr.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9272\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/21-isr.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/21-isr.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/21-isr-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/21-isr-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/21-isr-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a>In this example we used a very small, but fast model, that generated the correct function, but did not make a forward declaration. You can fix it by moving the function manually, or by using a larger model to generate code.<\/li>\n<li>Note that the AI used the <strong>ESP_ERROR_CHECK<\/strong> macro for error handling, because it was used in the original main() function. In our encoder driver, we want to return an error code instead. Use the &#8220;<strong>if\/return instead of ESP_ERROR_CHECK<\/strong>&#8221; prompt to quickly change the relevant parts:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/22-return.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9273\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/22-return.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/22-return.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/22-return-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/22-return-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/22-return-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>Now we will use AI to actually analyze the rising\/falling edges on the encoder signals, and update the count. The logic for it is very straight-forward if you look at the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Rotary_encoder#\/media\/File:Quadrature_Diagram.svg\">state diagram<\/a> from Wikipedia, however it requires several similar checks. AI can very easily write them based on a fairly concise summary:\n<pre>Maintain EncoderPosition field &amp; LastKnownA\/B\r\nUpdate like this:\r\n\r\nPositive edge on A with B=1 or negative with B=0 =&gt; increment\r\nPositive B with !A or negative B with A =&gt; also\r\nOthers =&gt; decrement<\/pre>\n<p>As the edits are concentrated in a single relatively small function, even large models will manage them quite fast:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/23-logic.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9274\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/23-logic.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/23-logic.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/23-logic-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/23-logic-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/23-logic-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>Now let&#8217;s manually update the main loop to print the encoder position and try it out:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/24-values.png\"><br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9275\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/24-values.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/24-values.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/24-values-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/24-values-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/24-values-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a>If you try rotating the encoder now, you should see the position update in real time.<\/li>\n<li>The last step would be to update the HTTP server to show the encoder position. We will do it by adding a root page with a simple field, and an <strong>\/encoderval<\/strong> API returning the value. The example project registers handlers for various pages in the <strong>start_webserver()<\/strong> function. AI can very easily add more similar pages there, along with the necessary structures. Begin editing <strong>start_webserver()<\/strong>:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/25-start.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9276\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/25-start.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/25-start.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/25-start-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/25-start-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/25-start-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>Note how VisualGDB highlighted the symbols that were used by <strong>start_webserver(). <\/strong>They will be automatically included in the context window, so AI be able to use them for reference:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/26-scope.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9277\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/26-scope.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/26-scope.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/26-scope-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/26-scope-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/26-scope-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>Use this prompt:\n<pre>register handlers for \/ (root) and \/encoderval<\/pre>\n<p><a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/27-addhandlers.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9278\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/27-addhandlers.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/27-addhandlers.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/27-addhandlers-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/27-addhandlers-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/27-addhandlers-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a>This will quickly copy the layout of the existing handlers, adding the two additional ones.<\/li>\n<li>Select both handlers for editing and use this prompt to create basic implementations:\n<pre>Implement handlers: encoderval return value from @s_Encoder ; root - page with a div and a script loading \/encoderval in an infinite loop with await<\/pre>\n<p><a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/28-update.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9279\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/28-update.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/28-update.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/28-update-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/28-update-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/28-update-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>In this example, the model decided to call the update function instead of having an infinite loop with awaits. Using &#8220;fetch\/set in an infinite loop, no delays&#8221; as a refinement prompt quickly changes the layout to the fastest possible:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/29-loop.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9280\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/29-loop.png\" alt=\"\" width=\"1171\" height=\"730\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/29-loop.png 1171w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/29-loop-300x187.png 300w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/29-loop-1024x638.png 1024w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/29-loop-768x479.png 768w\" sizes=\"(max-width: 1171px) 100vw, 1171px\" \/><\/a><\/li>\n<li>If you run the application now and open the device IP address in the browser, you will see the encoder value being updated in real time:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/30-page.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9281\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/30-page.png\" alt=\"\" width=\"754\" height=\"401\" srcset=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/30-page.png 754w, https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2026\/03\/30-page-300x160.png 300w\" sizes=\"(max-width: 754px) 100vw, 754px\" \/><\/a><\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>In this tutorial we will use AI edits to modify the ESP32 HTTP server example to handle a rotary encoder<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[253],"tags":[],"_links":{"self":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts\/9248"}],"collection":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/comments?post=9248"}],"version-history":[{"count":6,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts\/9248\/revisions"}],"predecessor-version":[{"id":9288,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts\/9248\/revisions\/9288"}],"wp:attachment":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/media?parent=9248"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/categories?post=9248"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/tags?post=9248"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}