Lilka: DIY ESP32-S3 Learning Console with NES Emulation & Lua Support
To make the experience fit your profile, pick a username and tell us what interests you.
We found and based on your interests.
Lilka looks simple at first glance: a screen, some buttons, an SD card. But dig a little deeper and you'll find an entire ecosystem living inside — a microcontroller, an operating system, scripting engines, a dynamic app loader, and a web interface. In this article, we'll break down what it's all made of and how it works together.
The project used to live in a single monorepo. Today, the lilka-dev organization on GitHub hosts 16 repositories. The core ones are:
This question comes up all the time, and Lilka's developer has a short answer: these are fundamentally different things, alike only in that both of them can be programmed.
ESP32 is a microcontroller made by the Chinese company Espressif. A small chip, cheap, low-power, boots in milliseconds. There's no operating system inside in the usual sense, no file system, no monitor, no keyboard. It just runs a single piece of code written into its flash memory — continuously. Lilka uses a specific version: the ESP32-S3-WROOM-1-N16R8, with two Xtensa LX7 cores at 240 MHz, 16 MB of flash, and 8 MB of PSRAM.
Raspberry Pi is a microcomputer. It runs a full Linux-like operating system, has an HDMI output, USB ports, and hundreds of megabytes of RAM. It's a good fit for tasks that need serious computing power: multimedia, web servers, computer vision. But it's significantly more expensive, physically larger, and draws far more power. It simply doesn't fit into a battery-powered device like a handheld console.
In short: ESP32 is for embedded systems and IoT, Raspberry Pi is for tasks that actually need a tiny computer.
The chip on its own is just silicon. To actually work with it, you need a toolset. Espressif packaged one under the name ESP-IDF (Espressif IoT Development Framework) — the official SDK for ESP32 development. It bundles forks of several existing projects adapted to this chip: notably FreeRTOS for multitasking and lwIP for networking.
FreeRTOS is a small real-time operating system (RTOS). Its main job is to give developers the abstraction of threads (in FreeRTOS terminology they're called "tasks," but it's the same idea). Two pieces of code can run "in parallel," and the system handles switching between them.
The ESP32-S3 has two physical cores. This matters. One core handles screen rendering, the other runs user code. That's why even when your Lua script isn't doing anything special, the display still updates — a separate core takes care of that.
But even with two cores, each one runs multiple tasks in turn, not simultaneously. Switching is handled by the task scheduler — FreeRTOS's planner. Here's how it works: your code runs continuously until it hits a call to sleep(), delay(), or something similar. That's the exact moment the scheduler gets a chance to hand control to another task. So having delays in your program isn't just a convenience — it's a hard requirement. Without them, other tasks will never get CPU time.
One practical consequence: sleep(100) does not guarantee...
This is the second article dedicated to Lua programming for Lilka. The first article can be found here. When you start working with sensors, you almost immediately run into strange terms like High and Low. It might sound complicated, but in reality, it’s much simpler Just imagine a regular light switch: it’s either on or off. In electronics, that’s exactly what High and Low mean.
High means there is voltage on the pin — in other words, the signal is “on” ⚡ For a microcontroller, this is usually a logical one. Low, on the other hand, means there is no voltage (or it’s very low), which corresponds to a logical zero. So any simple sensor communicates with your device using this same language: either “yes” or “no.”
On Lilka, this feels especially intuitive. Press a button — you get High Release it — Low. A motion sensor detects movement — High again. Nothing happens — Low Even though these terms are rooted in electrical concepts, it’s enough at the beginning to think of them as just two states your device constantly switches between.
This simple idea is the foundation for most of the examples that follow. Once you get it, working with sensors stops feeling complicated and starts to feel more like building with a constructor set where everything is logical and predictable.
This program turns a CW-020 relay on and off using button A on the Lilka console. A relay is an electrical switch that can control external devices: a light bulb, a fan, and so on ⚡. The module has a low level trigger — it activates when the INpin receives a low signal (~0V). That's why the program immediately sets HIGH on startup, so the relay doesn't accidentally switch on during boot. Button A toggles the relay, button B turns the relay off and exits the program. The screen shows the current state: green "State: ON" or red "State: OFF" 🟢🔴.
Wiring:
| Lilka | CW-020 relay |
|---|---|
| 3.3V | VCC |
| GND | GND |
| 12 | IN |
local relay_pin = 12
local relay_on = false
function lilka.init()
gpio.set_mode(relay_pin, gpio.OUTPUT)
gpio.write(relay_pin, gpio.HIGH)
end
function lilka.update(delta)
local state = controller.get_state()
if state.a.just_pressed then
if relay_on then
relay_on = false
gpio.write(relay_pin, gpio.HIGH)
else
relay_on = true
gpio.write(relay_pin, gpio.LOW)
end
end
if state.b.just_pressed then
gpio.write(relay_pin, gpio.HIGH)
util.exit()
end
end
function lilka.draw()
display.fill_screen(display.color565(0, 0, 0))
display.set_text_color(display.color565(255, 255, 255))
display.set_cursor(10, 32)
display.print("Relay control")
if relay_on then
display.set_text_color(display.color565(0, 200, 0))
else
display.set_text_color(display.color565(200, 0, 0))
end
display.set_cursor(10, 64)
display.print(relay_on and "State: ON" or "State: OFF")
display.set_text_color(display.color565(255, 255, 255))
display.set_cursor(10, 100)
display.print("A - toggle")
display.set_cursor(10, 120)
display.print("B - exit")
end
This program measures the distance to an object using the HC-SR04P ultrasonic sensor and displays the result in centimeters on the Lilka screen. The sensor works like a bat 🦇 — it sends an ultrasonic pulse and waits for the echo. The farther the object, the longer the sound travels. The program measures this time and converts it into centimeters.
The key technical challenge — Lua has no built-in microsecond delay, but the sensor requires one. The solution is simple: util.sleep(0.001) gives a 1ms pulse — 100× more than the required 10µs minimum, and perfectly stable ⚙️. Readings are smooth thanks to averaging of eight measurements and an outlier filter — no sudden jumps to 800 cm 🚫. If the sensor is not connected, the screen shows a wiring guide. Button B to exit.
| Lilka... |
|---|
Lilka is an ESP32 console that makes it easy to create your own apps. Once your app is ready, you can publish it to the catalog at catalog.lilka.dev.
You will need a GitHub account. Fun fact: GitHub is built on Git, a protocol created by Linus Torvalds, the author of the Linux kernel. I will show you how to add an app directly through the browser, without installing any additional software.
First, you need a GitHub account. Go to github.com and sign up — it's free.
Once registered, create a repository for your app's code. Click New repository, choose a name, check Add a README file and click Create repository.
Once the repository is created, you can edit the README.md file by clicking the pencil icon in the upper right corner — add a short description of your app. When done, click the green Commit changes button in the upper right corner to save. The README file uses Markdown format — a simple and easy-to-understand text markup language.
You can write the description yourself, use an online editor like markdownlivepreview.com or stackedit.io, or ask an AI to generate a Markdown description for GitHub based on your code.
Add your code file by clicking Add file → Create new file in the upper right corner. At the top of the screen, enter the file name, for example timer.lua, and paste your code. Then click the green Commit changes button in the upper right corner.
To make changes to any existing repository, you need to copy it to your own space — create a fork — make your changes, and then send them for review to the repository owner.
To fork the App Catalog, go to github.com/lilka-dev/catalog and click the Fork button with the fork icon in the upper left corner of the screen. This creates a copy of the catalog in your GitHub account.
Once forked, the repository will appear in your repository list under the same name lilka-dev/catalog. Open it, navigate to the apps folder and click Add file → Create new file in the upper right corner.
Enter the file name as timer/DESCRIPTION.md — this automatically creates a folder named timer with the file inside. This file is the app description for the Lilka Catalog and can be a full copy of the README.md from your own repository created earlier.
The last required file is manifest.yml. It is essentially the app's passport: the catalog uses it to find out the name, author, where the code is located and which files need to be downloaded to the console. Without this file the app will not appear in the catalog.
Create the file timer/manifest.yml the same way as the previous files — via Add file → Create new file. Below is an example for the timer and an explanation of each field:
name — the app name as it appears in the catalogsources — link to the repository with the source code. This is the regular GitHub repository addressentryfile — a direct link to the main file that Lilka launches. To get the link, open the file in your repository and click the Raw button in the upper right corner — then copy the address from the browsershort_description — a short description visible in the catalog listdescription: "@DESCRIPTION.md" — means the full description is taken from the DESCRIPTION.md file in the same folderauthor — the author's GitHub username with the @ signlicense — allows you to specify a license. You can use an open license so that other users can use and modify your code — for example MIT, GNU or any other. NONE means the author has not specified onekeira_version — the minimum Keira OS firmware version required to run the appicon...Lua is one of those programming languages that just makes sense. The code reads almost like plain text — you don't need to wrestle with complex rules to understand what's happening. But what makes Lua especially exciting on Lilka is the total absence of compilation ⚡ You write your code, drop the file onto a microSD card, and run it. That's it. Want to try a new game, a small utility, or hook up a sensor? Just copy the file and go 🚀 No extra tools, no flashing, no waiting — the result is instantly in your hands.
Lilka provides a full set of modules for working with all the hardware. You draw on the screen with display, read button states with controller, and play sounds and melodies with buzzer. If you need resources from the card — there's resources and sdcard. For math and geometry there are the corresponding math and geometry modules. Want to connect an external sensor — gpio gives access to pins, serial to UART. And there's also wifi for networking, http for requests, crypto for encryption, and state to save program state between runs. All these modules load automatically — no manual imports needed.
Any text editor works for Lua, but the most convenient is VS Code — it's free and has Lua extensions with syntax highlighting and autocompletion. Sublime Text and Notepad++ are also popular if you want something lighter. For Lilka specifically there's Live Lua support — you can send code directly to the device over USB without taking out the card, which is very handy during development.
How long a sequence can you remember? Four shapes sit on screen — each tied to a direction button, a color, and a musical note. Press A and the game lights up one shape while playing its note. Your job: repeat it with the matching arrow. Get it right and the sequence grows by one. Miss — and the screen shows your score and best result before resetting after two seconds.
All the logic lives in lilka.update(delta), organized as a five-state machine: IDLE → SHOW → INPUT → WIN/LOSE. The SHOWstate uses a countdown timer and a lit variable to highlight shapes one by one with buzzer.play() in between. INPUT checks each button press against the stored sequence array step by step.
lilka.draw() knows nothing about the game logic — it just reads the current state and lit, and draws accordingly. Every frame the screen is cleared and redrawn from scratch. 🖥️
--[[
Simon Says for Lilka
A - start / again | B - exit
]]
local BLACK = display.color565(0,0,0)
local WHITE = display.color565(255,255,255)
local GRAY = display.color565(100,100,100)
local GREEN = display.color565(60,255,100)
local W, H = display.width, display.height
local cx, cy = W/2, H/2 - 10
local buttons = {
up = { x=cx, y=cy-55, note=notes.E5,
dim=display.color565(0,0,100), bright=display.color565(80,160,255),
draw=function(x,y,c) display.fill_circle(x,y,28,c) end },
down = { x=cx, y=cy+55, note=notes.G5,
dim=display.color565(0,80,0), bright=display.color565(60,255,100),
draw=function(x,y,c) display.fill_rect(x-25,y-25,50,50,c) end },
left = { x=cx-55, y=cy, note=notes.C5,
dim=display.color565(100,80,0), bright=display.color565(255,220,50),
draw=function(x,y,c) display.fill_triangle(x-28,y+22,x+28,y+22,x,y-28,c) end },
right = { x=cx+55, y=cy, note=notes.B4,
dim=display.color565(100,0,60), bright=display.color565(255,80,180),
draw=function(x,y,c)
display.fill_triangle(x,y-28,x+28,y,x,y+28,c)
display.fill_triangle(x,y-28,x-28,y,x,y+28,c)
end },
}
local order = {"up","down","left","right"}
local IDLE, SHOW, INPUT, LOSE, WIN = 1,2,3,4,5
local SHOW_ON, SHOW_OFF = 0.5, 0.25
local state, sequence, step, score, best, lit, timer
local function reset()
state=IDLE; sequence={}; step=1; score=0; lit=nil; timer=0
end
...
Read more »
Sometimes you need to update or replace the firmware on Lilka. Firmware can come in two forms: a ready-made compiled file (.bin or .img) that you simply upload to the device, or source code that you first need to build on your computer to get a file ready for flashing.
The easiest way is to use the official web flasher at. It runs directly in the browser, nothing to install, and the firmware is transferred to the device over a USB cable.
The flasher lets you install the official KeiraOS — with a choice of language, Ukrainian or English. Just connect Lilka with a cable, select the file, click "Connect and Flash", and choose the COM port. One important thing: the site uses the Web Serial API, so you'll need Chrome, Edge, or Opera. Firefox won't work.
Keira Web Manager is a built-in web service inside KeiraOS itself. It runs directly on Lilka, and to use it you just open a browser on any device connected to the same Wi-Fi network and navigate to Lilka's IP address. No cable needed at all.
Through this interface you can browse files on the SD card, upload and download files, and copy them between folders. You can also update the firmware over-the-air (OTA) 📡 — just drag a .bin file into the browser and that's it. One important note though: only KeiraOS is supported, and you'll need a non-merged .bin file from the GitHub releases, not the merged one.
There's also a neat feature: you can launch firmware directly from the browser by clicking the rocket icon 🚀 next to any .bin file. A confirmation prompt will appear — "Boot this file via Multiboot?" — and after confirming, a progress bar shows up at the top while Lilka loads the new image. Very convenient. From experience though, some firmware files may simply refuse to launch — and if that's the case, they won't run either from Web Manager or from the File Manager on the device itself.
Esptool is the official utility from Espressif, the company behind the ESP32 chip that powers Lilka. It works through the command line: you open a terminal, type a command, and the firmware is flashed directly to the device over a USB cable.
What's interesting is that esptool actually runs under the hood of almost every other tool — PlatformIO, and even some web flashers. It's just hidden behind a graphical interface there. Here you interact with it directly, which gives you more flexibility: you can flash any .bin file, wipe the device's memory, or make a backup of the current firmware. But you'll need to get a little comfortable with the command line.
If you want to flash third-party firmware, there's a web-based tool from Espressif themselves: It's essentially esptool running in the browser — no installation needed, works over USB cable, and supports any .bin file. Like Lilka Flasher, it requires Chrome, Edge, or Opera.
If you're writing your own program or want to build KeiraOS from source — use PlatformIO in Visual Studio Code. You clone the repository, open the project, connect Lilka with a cable (important: only Type-C → Type-A, USB 3.0 and Type-C → Type-C cables are not supported), put it into bootloader mode by holding the SELECT button while turning it on — and PlatformIO builds and uploads the firmware. Once done, press RESET and Lilka boots with the new firmware.
One neat detail: the first time you flash, a bootloader gets installed alongside the firmware. After that you no longer need to manually hold SELECT — PlatformIO will put Lilka into bootloader mode automatically on every subsequent Upload.
KeiraOS can run .bin files directly from the SD card — no cable, no reflashing. Just drop the file onto the card, open File Manager in KeiraOS and launch it from there.
After turning Lilka off and back on, it returns to KeiraOS — as if nothing happened....
Read more »Lilka is a Ukrainian open-source DIY educational gaming console based on the ESP32-S3 microcontroller. It is designed so that even a beginner can assemble it using widely available modules.
By itself, the console is just hardware: a screen, buttons, and a chip. To make everything work, it needs firmware — a software “control center”. The main firmware is KeiraOS (named after the developer’s cat). It is an open-source operating system based on FreeRTOS, written in C++, that controls the device and allows you to run games and applications.
You can develop apps for KeiraOS in several ways:
You can find example apps in the Lilka Apps & Mods repository.
Lilka is not just for games. You can connect sensors and modules via GPIO and work with common interfaces:
KeiraOS can also run retro games (.ROM and .NES) directly from an SD card using the Nofrendo emulator.
In addition to the main system, Lilka supports running third-party firmware in .bin format. This means you can:
So Lilka is not just a single firmware, but a whole platform for experimentation, learning, and building your own projects.
Pixeler (formerly Meow UI) is an alternative firmware for Lilka, ported from the “Ideal Console” project.
Its main feature is the ability to develop graphical user interfaces for both the microcontroller and a PC using the same code. This means you can design and test your UI directly on a computer 💻 without reflashing the device every time, which significantly speeds up development.
The firmware is built on the Pixeler framework and written in C++. It includes ready-to-use GUI examples such as menus, settings screens, a file manager, Wi-Fi interface, MP3 player 🎵, and simple 2D games 🎮.
An interesting feature is support for server-based multiplayer games 🌐 — meaning you can play with friends on different devices.
Pixeler uses the Arduino_GFX graphics driver, which provides better display support, more built-in fonts (including Cyrillic), and overall greater flexibility compared to older solutions.
Since this is a port for Lilka, some UI elements may not render perfectly, and certain features may be limited.
The firmware can be launched directly from KeiraOS as a .bin file — just copy it to the SD card and run it on the device.
Important note: Pixeler is built using Pioarduino and is not compatible with standard PlatformIO.
Rustilka (Gitlab) is a small project that brings support for the Rust programming language to Lilka.
In simple terms, it’s an alternative way to write firmware for Lilka without using the standard SDK. To get started, you need to install a special firmware from rust.lilka.dev, after which you can develop applications in Rust 🦀.
Rustilka helps simplify project setup and provides a library for working with the hardware — the display, buttons, and other features of Lilka. However, it’s important to understand that this approach is better suited for those who already have some experience with Rust and basic knowledge of microcontrollers.
Rustilka and the standard SDK represent two different approaches:
In Rustilka, everything is written from scratch in Rust without relying on existing C/C++ libraries....
Read more »Lilka (Keira OS) offers two wireless ways to work with files — and neither requires a cable or a card reader.
🌐 Keira Web Manager
Keira Web Manager is a built-in web service in Keira OS, aptly named File & Firmware Management for Lilka. Just open a browser on any device connected to the same Wi-Fi network, and you instantly get a clean, fully functional file management interface.
What it can do:
.bin file into the browser, and that's itThe design deserves a special mention — it's clean, intuitive, and genuinely pleasant to use. For a web interface running on a microcontroller, that's a rather delightful surprise.
📡 FTP
The FTP server built into Keira OS lets you connect to Lilka just like any regular file server. Any FTP client works — on a computer or a mobile phone.
On desktop, FileZilla is a great choice — free, straightforward, and battle-tested. On mobile, there are plenty of options: FTP Client Pro for iOS or AndFTP for Android, to name a few.
Once connected, you get full access to the SD card's file system: uploading, downloading, creating folders — just like working with any regular server.
📋 Bonus: Pastebin
Inside the Applications section of Keira OS, there's one more handy tool — the Pastebin app. It lets you fetch code directly from pastebin.com: just enter a link or a paste ID, and the script lands on your device instantly. Perfect for quickly trying out a Lua snippet you found online or received from a friend — no computer, no cables, right from the console.
One of the most convenient features of Lilka is a full-featured app store, accessible both from a browser and directly on the console itself — no computer needed. The catalog is hosted at catalog.lilka.dev and contains two sections: Apps and Mods.
The Mods section covers physical modifications for Lilka: cases, covers, and buttons. Shields to extend the console's capabilities are also planned to be added in the future. Each mod includes manufacturing files along with a description and photos.
All apps in the Apps tab are divided into three types:
Lua scripts are the most convenient way to run programs on Lilka. They require no compilation and run instantly directly from the file system. Just download the .lua file to the device — and it's ready to go. Learn more about Lua on Lilka →
Custom firmware also requires no compilation — it is loaded as a ready-made .bin file and runs directly on the device. The key advantage: after restarting Lilka, you automatically return to the standard Keira OS operating system — so an alternative firmware doesn't replace the system permanently. This opens up interesting possibilities: firmware can be used as fully standalone programs or as part of applications within Keira OS. Learn more about custom firmware →
C++ applications are also present in the catalog — for reference and as examples for developers. However, they require compilation and cannot be run directly on the console without a prior build. Learn more about creating C++ apps →
The process is straightforward even for beginners. Each app's catalog entry consists of just a few elements:
DESCRIPTION.md — a text description in Markdown formaticon.png and screenshotsmanifest.yml — the main file with app metadataTo publish your app:
apps/yourapp.app/ inside the apps/ folder — this can be done online directly in GitHub or via GitHub Desktopmanifest.yml, icon.png, and DESCRIPTION.mdHere is an example manifest.yml from a real app — the "ChuVaChi" rock-paper-scissors game:
name: Game "ChuVaChi" rock-paper-scissors (LUA)
sources:
type: git
location:
origin: https://github.com/andrijpv/LIlkaChuVaChi
entryfile:
type: lua
location:
origin: https://raw.githubusercontent.com/andrijpv/LIlkaChuVaChi/refs/heads/main/filegame/lilkachuvachi.lua
files:
- type: image
location:
origin: https://raw.githubusercontent.com/andrijpv/LIlkaChuVaChi/refs/heads/main/filegame/paper.bmp
description: Paper image
- type: image
location:
origin: https://raw.githubusercontent.com/andrijpv/LIlkaChuVaChi/refs/heads/main/filegame/scissors.bmp
description: Scissors image
- type: image
location:
origin: https://raw.githubusercontent.com/andrijpv/LIlkaChuVaChi/refs/heads/main/filegame/stone.bmp
description: Stone image
short_description: Classic "ChuVaChi" rock-paper-scissors game for Keira OS
description: "@DESCRIPTION.md"
author: "@andrijpv"
license: NONE
keira_version: 1.0.0
icon: image.png
screenshots:
- image.png
The manifest structure is intuitive: entryfile points to the executable file (.lua, .bin, etc.), files lists additional app resources (images, sounds), and description can reference a separate DESCRIPTION.md file with the full description.
Lilka is an open-source portable console built around the ESP32-S3-WROOM-1 microcontroller. What makes it special? You can build it entirely from off-the-shelf modules available at most electronics stores — no custom PCBs required. Total cost in Ukraine is around $15-20.
Key Features:
But here's the important part: Lilka isn't marketed as a gaming console. It's a DIY learning platform that happens to play games. The goal is to provide affordable hardware for tinkering, plus a complete library for working with display, SD card, buttons, sound, battery, and other components.
Getting Started: Complete kits available at https://autkin.net/lilka/, or you can order the PCB from JLCPCB/PCBWay and source components yourself. Full assembly documentation available at docs.lilka.dev.
What Can You Build?
Software: Lilka runs Keira OS (based on FreeRTOS) with support for C++, Lua, and mJS scripting. The system recently received full English localization. You can write embedded programs or run scripts directly from SD card. The lilka library provides simple APIs for all hardware features.
Community: Active Discord community at discord.gg/ycmaz4vnbs Full source on GitHub: github.com/lilka-dev
Create an account to leave a comment. Already have an account? Log In.
Become a member to follow this project and never miss any updates
About Us Contact Hackaday.io Give Feedback Terms of Use Privacy Policy Hackaday API Do not sell or share my personal information
Voja Antonic
TM
torehc