Friday, December 19, 2025

My ESP8266 Blog: An Experiment in Minimalist Web Hosting inside a Mushroom House!




A few weeks ago, I did something unconventional: I moved a blog of mine to an ESP8266 microcontroller. Yes, you read that right: a €10 chip with 80KB of RAM now hosts my thoughts on the internet. (plus the SD card shield of course)





To make it even funnier, I decided to place it inside a wooden mushroom house I was recently working on!





As a minimalist who believes in independence and focusing on what matters without unnecessary complexity, this project was more than a technical experiment. It was a return to first principles: control over your own infrastructure, elimination of unnecessary complexity, and the freedom to build things the simplest way possible. What started as a nostalgic journey into hardware became a fascinating lesson about how little you actually need.

The Hardware Reality: Back to Basics


As a Senior Fullstack Developer, I've spent recent years living in high-level languages and cloud infrastructure. Docker containers with gigabytes of RAM, Kubernetes clusters, serverless architectures – all abstractions that make us forget what's really happening under the hood.

The ESP8266 brutally brought me back to reality:

  • 80KB RAM total – less than a single high-resolution photo
  • 4MB Flash storage – wouldn't fit a single MP3 file
  • 80MHz CPU – slower than my first digital camera
  • ~20mA power consumption – could theoretically run for weeks on a AA battery (or even better: solar!)

The 16KB Lesson


The best example was a blog post that hit 16KB in markdown. A tiny text from my perspective as a web developer – maybe worth 2-3 screenshots. But for the ESP8266? A disaster. The chip tried to load the text, parse the HTML template, escape special characters... and simply gave up. Instead of the rendered post, it just showed `{{CONTENT}}` – the unreplaced template variable.

The solution? Split the post into two parts, add cross-links, configure redirects. Suddenly I was thinking about memory management again, about buffer overflows, about the physical limits of hardware. Skills I hadn't used since my university days when I experimented with Arduino and embedded C.

It was simultaneously frustrating and refreshing.

The Software Philosophy: Less is More


Writing code for the ESP8266 reminded me how bloated modern web development has become. My current work stack? React, TypeScript, Webpack, Babel, 300MB of `node_modules` for "Hello World". The ESP8266? Vanilla C++, direct hardware access, no dependencies except the ESP8266 library.

The Entire Blog System


```cpp
ESP8266WebServer server(80);
File file = SD.open(filePath);
server.send(200, "text/html", htmlContent);
```

That's it. No ORMs, no middleware chains, no dependency injection containers. Just direct, transparent code. Every line has a purpose.

Traffic Logging with NTP


A particular highlight: I wanted visitor statistics with real timestamps. But how does a chip without an RTC module get the current time? NTP – Network Time Protocol. A few UDP packets to `pool.ntp.org`, offset calculation, done. The same protocol that's been keeping the internet synchronized since 1985.

```cpp
WiFi.hostByName(ntpServerName, timeServerIP);
sendNTPpacket(timeServerIP);
// Wait for response...
unsigned long epoch = secsSince1900 - 2208988800UL;
```

Suddenly I understood NTP not just theoretically – I had implemented it. That's the kind of learning you lose in the cloud era.

The Practical Possibilities: More Than Just a Blog


What surprised me most: How versatile such a mini-server is. The ESP8266 isn't just a blog host – it's a platform for dozens of use cases:

Pilot Projects and MVPs

Got an app idea that needs a backend? Forget Firebase, forget AWS. Flash an ESP8266, write a few REST endpoints, done. Ideal for:
  • API prototypes for mobile apps
  • IoT dashboards for smart home experiments
  • Webhook receivers for automations
  • Local development APIs without Docker

Temporary Landing Pages

  • Event website for a party next week? ESP8266.
  • Wedding website with RSVP form? ESP8266.
  • "Coming Soon" page for a side project? ESP8266.

No hosting account, no credit card, no monthly fees. Just boot up, use, shut down.

Internal Tools

  • Note taker
  • Stock and groceries manager
  • Password manager for your home network (offline!)
  • File sharing without cloud services
  • Monitoring dashboard for other home servers

Educational Projects

  • Teaching kids HTML with instant feedback
  • Demonstrating networking fundamentals
  • Debugging in a transparent environment
  • An alternative for web hosting courses

Creative Applications

  • Digital picture frame with web interface
  • Recipe database in the kitchen
  • Personal journal with physical backup (SD card)

The Security Aspects: Reclaiming Control


In an era of cloud leaks and data scandals, there's something reassuring about physically holding your own server in your hand.

What I Control

  • The hardware – I can touch it, disconnect it, destroy it
  • The software – every line of code is mine, no black boxes
  • The data – on my SD card, in my house, under my jurisdiction
  • The logs – I decide what gets logged, nothing goes to third parties

What I Don't Control

  • My ISP – can see traffic (HTTP, no HTTPS on the ESP8266)
  • My router – port forwarding means exposure
  • DDoS protection – an attacker could take down my home internet (but my router can partially manage that + an external tunnel can be added for more protection)

But for a personal blog? The threat is minimal. Who wants to DDoS a personal hobby project?

The Freedom Aspect


The most important learning: Platform independence. As a user of big tech, I am trapped:
  • Terms of Service
  • Content policies
  • Monetization requirements
  • Data collection

Now? I could write a post about any topic tomorrow, and nobody can censor it. No platform bans, no shadow banning, no "This content violates community guidelines".

For me as a minimalist, this is more than technical independence – it's philosophical consistency. Fewer dependencies mean more freedom. Less complexity means more control. A €10 chip in my hand is more powerful than a million-dollar data center that belongs to someone else.

This is real digital freedom.

The Challenges: Being Honest


Not everything is sunshine. The ESP8266 blog has real limitations:

Performance

  • Slow – up to 2-3 second load time per page for pages with many images (caching helps later)
  • Single-threaded – only one request at a time
  • No compression – GZip would overload the chip

Availability

  • Home internet dependent – if my router goes down, the site is gone
  • No CDN – visitors from Japan have 300ms latency
  • Dynamic IP – DuckDNS must update every 5 minutes

Development Workflow

  • No hot reload – every change: SD card out, edit file, SD card in, ESP8266 restart
  • Limited debugging – serial port or nothing
  • No admin panel – for large files I must physically remove the SD card

Scaling

  • ~10 concurrent users maximum – after that it becomes unstable
  • File size limits – posts >12KB must be split
  • No HTTPS – impossible without additional hardware

But honestly? For a personal blog, these aren't real problems. I don't have 1000 simultaneous visitors. And the 2-second load time? In a world of JavaScript frameworks that download 5MB, that's acceptable.

The Meta-Lesson: Constraints Foster Creativity


The best part of this project wasn't the technology – it was the mindset.

With unlimited resources (cloud, modern hardware) you don't think. You need user tracking? Add Google Analytics. You want images? Throw 4K PNGs on the page. You want a feature? Install an npm library.

With 80KB RAM you must think:
  • Do I really need this variable?
  • Can I reuse this string?
  • Does this function need to be so complex?

This is minimalism in action. Not the absence of possibilities, but the conscious choice for what's essential. Every line of code justifies its existence. Every feature solves a real problem. No bloatware, no "nice-to-haves", no technical debt castle.

It's like Haiku poetry – the constraint of 17 syllables makes the words more powerful. The ESP8266 makes my code better.

What I Learned as a Senior Developer


  • Abstraction has a price – Modern frameworks hide complexity, but also understanding
  • Performance is relative – A slow ESP8266 blog is faster than many React SPAs
  • Ownership matters – Controlling your own infrastructure is liberating
  • Constraints are teachers – Limitations force better solutions
  • Hardware understanding is valuable – Even in the cloud era, understanding physical reality helps

Who Is This For?


Try it if you:
  • Are a developer who wants to rediscover hardware
  • Want to run a personal project without cloud costs
  • Want to develop understanding of low-level web technologies
  • Seek digital sovereignty over your content
  • Enjoy tinkering with projects

Skip it if you:
  • Need a production service with high availability
  • Expect heavy traffic (>1000 visitors/day)
  • Absolutely need HTTPS (unless you build a reverse proxy)
  • Don't have time for tinkering
  • Just want a blog (use WordPress.com)

Conclusion: A Successful Experiment


Would I do it again? Absolutely.

Would I recommend it for a professional client? Absolutely not.

The ESP8266 blog is the perfect hobby project: technically challenging, practically usable, financially cheap (~€15 hardware: the chip + an SD card shield), and philosophically satisfying. It reminded me of forgotten skills, showed new possibilities, and gave me back control over my online presence.

In a world of cloud services, platform monopolies, and monthly subscriptions, there's something wonderfully anarchistic about hosting your content on a €10 chip sitting in your living room.

This is my server. This is my data. This is my platform.

And it just keeps running, consuming less power than an LED bulb, serving content – one request at a time.

You can checkout my recent and most polished version of the code here https://github.com/MahmoudAdly/esp8266-blog-server

Monday, April 6, 2020

Creating a prayer times action on Google Home

Saving a lot of time during the COVID-19 shutdown gave me the chance to do something I wanted to try for a while, getting my Google Home Mini to tell me the prayer time instead of looking at a screen. So this weekend I got to play with a dummy piece of code I made during the week and finalize my test version of my own prayer times assistant.

So far, I can only ask about the next prayer or name a specific prayer to know its time.

Important note: you cannot replicate the following project without a premium account on Firebase to enable calls to external APIs.



Here is what I had to do in simple words:

1- Create a new Dialogflow agent


Dialogflow is where I started my project with creating a blank agent with nothing but a Welcome intent.



2 - Modify the default welcome intent


For the sake of having fun, I changed the way of addressing the user to say the magic word: "Salam! How can I help you?" plus accepting similar magic words like "Assalm Alaykom".



3 - Create a new intent for the next prayer time


This intent is meant to looks for the next prayer time in the array of today's prayers. But first, in which city or location?

Circumvent the location access step


To get the user's location, I need to ask for permissions. I didn't want one extra step to ask for permission. I actually don't know if I will have to do this every time or I can add checks once and then I have the location for always. But to cut the working time, I decided to let the user simply mention the city. This can make it faster to develop my agent for now. So I added some training phrases with and without the city parameter. But notice that I added the city parameter as required and added some prompts to ask the user if the parameter is not found in the phrase. I also helped the training by double-clicking the word "Berlin" and chose my city parameter from that popup list which appeared.



Use fulfillment for fetching prayer times


To get to write some code to get the answer I need, I should enable fulfillments (enable webhooks checkbox). This is a special section where you can programmatically manipulate your agent. I also marked this intent as an end of conversation. This means that once I respond, the agent stops and does not listen to any more input from the user.



I will get to the fulfillments section a bit later.

4 - Create a new intent for a specific prayer time


If I already can fetch and scan through some prayer times, creating the next intent was pretty easy in comparison to the previous one. That's because I includes no comparisons or calculations, but as simple as looking for a specific key in the prayers key-value object.

Create a Prayer entity


To detect the prayer in the user's phrase, I had to create a special entity which does not exist by default, which is the prayer entity.

This entity can be created from the left side panel. In the entity, I define the possible entries, along with any synonyms which can mean the same thing. So I added the names of the five prayers and added some synonyms for any different pronunciation.



Create the intent with the phrases and responses like before


Just like the previous intent, I created a new one and added some training phrases and responses. And this time I added one more parameter: prayer. and I set its entity as Prayer, which I just created. And then I marked the prayer in each training phrase by marking the text and choosing the correct parameter from the list appearing.




Enable fulfillment and end of conversation


This step is similar to the previous entity. I enabled fulfillments and marked the intent as end of conversation.




5 - Fulfillment section


Now comes the programmatic part. In this section, we should enable the inline editor. This is where we write handlers to do what we want. There is another option to have webhooks written with some standards. But lets stick to the quick win of scripting something. Remember that you need a premium Firebase account to do this step to be able to call external APIs.



The code here is as simple as writing a handler and registering this handler to a specific intent by matching its name. So the skeleton of my code looks like this:

function nextPrayerTimeHandler(agent) {
    // some code here
    agent.add(`This is something the agent is going to say to the user!`);
}

function specificPrayerTimeHandler(agent) {
    // some code here
    agent.add(`This is something the agent is going to say to the user!`);
}

intentMap.set('NextPrayerIntent', nextPrayerTimeHandler);
intentMap.set('SpecificPrayerIntent', specificPrayerTimeHandler);


This skeleton blends with the existing boilerplate in the inline editor. The content of my functions is where magic happens. I wrote some quick and dirty code based on the first API I found in my search for prayer times APIs. And spent a good amount of time fiddling with the rabbit hole of JS date objects and local vs UTC mess.

Here is my code if you are curious or want to make your own cleaner code :)

const https = require("https");
const apiBaseTodayPrayers = 'https://api.pray.zone/v2/times/today.json?higher-latitudes=3&school=3&city=';
const prayerKeys = ['Fajr', 'Dhuhr', 'Asr', 'Maghrib', 'Isha'];
var city = '';
var prayer = '';

function constructTimestampFromData(date, time, offset) {
    var offsetSign = (offset > 0) ? '+' : '-';

    var fullTimeString = `${date} ${time} GMT${offsetSign}${offset}`;

    return (new Date(fullTimeString).getTime());
}

function findNextPrayerTimeFromJsonResponse(jsonData) {
    let allPrayerTimes = jsonData.results.datetime[0].times;
    let date = jsonData.results.datetime[0].date.gregorian;
    let offset = jsonData.results.location.local_offset;

    // Filter prayer times and convert from string to timestamp
    var filteredPrayerTimes = {};

    for (var timeKey in allPrayerTimes) {
      if (prayerKeys.includes(timeKey)) {
        filteredPrayerTimes[timeKey] = constructTimestampFromData(date, allPrayerTimes[timeKey], offset);
      }
    }

    // Get local time
    let now = new Date().getTime();

    // Find the next prayer
    var nextPrayerName = '';
    var nextPrayerTime = '';

    for (var key in filteredPrayerTimes) {
      if (filteredPrayerTimes[key] > now) {
        return [key, allPrayerTimes[key]];
      }
    }
}
 
function nextPrayerTimeHandler(agent) {   
    // Get the current city
    city = agent.parameters.city;
   
    // Make API call to fetch prayer times
    const todayPrayersUrl = apiBaseTodayPrayers + city;
    return new Promise((resolve, reject) => {
      https.get(todayPrayersUrl, function(resp) {
        var json = "";
        resp.on("data", function(chunk) {
          json += chunk;
        });

        resp.on("end", function() {
          let jsonData = JSON.parse(json);
          let nextPrayerNameAndTime = findNextPrayerTimeFromJsonResponse(jsonData);

          // Respond
          if (nextPrayerNameAndTime[0] === '') {
            agent.add(`There are no more prayers in ${city} today.`);
          } else {
            agent.add(`The next prayer in ${city} is ${nextPrayerNameAndTime[0]} at ${nextPrayerNameAndTime[1]}.`);
          }

          resolve();
        });
      });
    });
}
 
function specificPrayerTimeHandler(agent) {   
    // Get the current city
    city = agent.parameters.city;
    
    // Get the prayer name
    prayer = agent.parameters.prayer;
    
    const todayPrayersUrl = apiBaseTodayPrayers + city;
    return new Promise((resolve, reject) => {
      https.get(todayPrayersUrl, function(resp) {
        var json = "";
        resp.on("data", function(chunk) {
          json += chunk;
        });

        resp.on("end", function() {
          let jsonData = JSON.parse(json);
          let prayerTime = jsonData.results.datetime[0].times[prayer];

          // Respond
          if (prayerTime === undefined) {
            agent.add(`Sorry, I cound not find ${prayer} time for ${city}.`);
          } else {
            agent.add(`${prayer} in ${city} is at ${prayerTime}.`);
          }

          resolve();
        });
      });
    });
}
 

intentMap.set('NextPrayerIntent', nextPrayerTimeHandler);
intentMap.set('SpecificPrayerIntent', specificPrayerTimeHandler);


6 - Testing with Google Assistant


On the right side of the screen, there is a message saying;
See how it works in Google Assistant.
Click it and you will be directed to Actions Console where you can test against a simulator for Google Assistant as well as other Google devices. You can also test on your own Google Home if you are connected to the same account from Google Home.



And from the Actions Console you can modify some settings and deploy your app to alpha and beta versions, or even submit it to be reviewed and publicly published.

Live demo


That's it! I created a simple app to help me with my daily need. And I learned something new along the way. I have more plans to improve my project to my needs and maybe publish it later to others.

I hope you learned something from my post. :)




Resources


Wednesday, April 1, 2020

Retry ruby code with exceptions multiple times

This problem I faced is pretty simple. A method calling an external API or resource which might throw a random error or fail intermittently. So I want to simple retry because this error is ignorable if it happens one or two times.

Now the standard ruby's retry and redo keywords are not helping here. Because the retry will keep retrying endlessly and I want to raise the error after a few retries, and the redo requires a flag to make it stop. So my solution to retry my ruby code for a few times if a specific exception happens was as simple as a method with the retry logic and accepting a block to keep my code cleaner. And whenever I need to retry a not-so-reliable API, I wrap my code in this method as a block and choose the exception to retry when it happens and how many times to retry.

# whatever method I am using
def do_something_with_tolerance_to_errors
  response = with_error_retry(WhateverException, 2) do
    # my code goes here
  end
end

# my retry handler
def with_error_retry(error_class, retries_count)
  yield
rescue error_class => e
  @retries ||= 0
  raise e if @retries >= retries_count

  @retries += 1
  retry
end