Building a tiny earth

A DIY IoT satellite imagery display

Gain
6 min readJul 8, 2023
Example of satellite images on AtomS3 (24mm × 24mm × 13mm)

Inspired by @mohitbhoite, I want to build something that would remind me of space and time. Something that would remind me that I’m living in a tiny earth among billions of stars in a galaxy. It’s easy to forget that when you’re busy with work, family, and life. So what’s better than looking at the earth from the outside in, to see yourself from the space through the lens of the near real-time satellite imagery. Earth’s Weather changes every day, and all living things on earth are affected by it. The shade and sunshine also reflect the time of day better than any digital clock. So here’s how I create my own tiny earth.

Hardware:

I’ve been wanting to try M5stack for a while as I see a lots of IoT community use M5stack to create so many fun projects. However, I haven’t found any device that I would match my need. After a while, AtomS3 came out last year and it’s perfectly for its simplicity and price.

  • M5Stack AtomS3 ($15.50 + $5 global shipping): WiFi 128x128 pixel display with a push screen button

Software:

You can use both Arduino and UIFlow to program AtomS3. I chose UIFlow 2.0 because it supported MicroPython and has a no code dev interface which I have no idea how it work. But I know that kids these day learn how to code using these kind of tools and it would be fun for me to experience how that work as well.

Before connecting your AtomS3 to UIFlow, you will need to burn the UIFlow 2.0 firmware to your AtomS3 using M5Burner. To do that, just download M5Burner form here, install it to your pc, and run the program. When M5Burner is open, select ATOMS3 on the left menu, and look for UIFlow2.0 on the main screen. After that, you will need to create your login account. Then, select the latest version of firmware on the right dropdown, click configurate, enter your WiFi name and password, and click Burn.

Now when you push a reset button your AtomS3, you can see whether your AtomS3 can connect to WiFi or not. Once it’s connected to WiFi, you should be able to go to https://uiflow2.m5stack.com/ and start coding. Note that you have to login to the same account you use in M5Burner. You will also need to select your active AtomS3 using the bottom right device selection menu before running/flashing the code.

I did try the drag-drop UI tool a bit and was able to do a simple thing like displaying a message on the screen. But other than that, I found it a bit difficult to implement the logic I want. So I switched to the coding tab instead (top center menu, far right icon). The UI tool is still useful for exploring what features/libraries the device has though.

Now, what I want to do is quite straightforward. I want to display a satellite image and automatically update the image with a fixed interval timer. For the source of the images I use:

Later on after I finished my first prototype, I feel that the image was too small and I kind of want to zoom in or change the image. So I added more images including west-coast specific image, sandwich, and different region image like Southeast Asia. I use the on-screen push button to switch between images.

Code:

This is also one of the first project I use ChatGPT to write about 90% of the total code. It’s amazing and save me so much time. I thought I know python but I was wrong because MicroPython functions are very limited and not everything in python works in MicroPython.

PHP code on a web server:

Since the AtomS3 can only display a 128x128 pixels image, the satellite image will need to be resized somewhere either on AtomS3 itself(not sure if it’s possible with limited memory) or somewhere else like a webserver. In this case, I just wrote a one file PHP script that take in an image URL and display a resized version of that image so that AtomS3 can download it and show it on the screen.

<?php

function downloadResizeAndDisplayImage($imageUrl) {
// Download the image
$imageData = file_get_contents($imageUrl);

// Create the image resource from the downloaded image data
$image = imagecreatefromstring($imageData);

// Resize the image to 128x128 pixels
$resizedImage = imagescale($image, 128, 128);

// Set the appropriate header for image output
header('Content-Type: image/jpeg');

// Output the resized image
imagejpeg($resizedImage);

// Free up memory by destroying the images
imagedestroy($image);
imagedestroy($resizedImage);
}

$imageurl = $_GET['url'];
downloadResizeAndDisplayImage($imageurl);

?>

MicroPython code for AtomS3:

Below is the code for AtomS3. I put it in UIFlow code tab and download it to AtomS3.

import os, sys, io
import M5
from M5 import *
import socket
import time
import utime
from hardware import *

image1 = None
show_img_num = 0 # start with first image on boot
latest_update = 0 # 0 for first run on boot
image_dict = [
{ # GO-East full disk - GeoColor
"filepath": "res/img/geoeast_128x128.jpg",
"url": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/FD/GEOCOLOR/339x339.jpg"
},
{ # GO-East full disk - sandwich
"filepath": "res/img/sandwitch.jpg",
"url": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/FD/Sandwich/339x339.jpg"
},
{ # GO-West Pacific coast - geocolor
"filepath": "res/img/geowest-us_pacific_coast-geocolor.jpg",
"url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/wus/GEOCOLOR/250x250.jpg"
},
{ # GO-West Pacific coast - sandwich
"filepath": "res/img/geowest-us_pacific_coast-sandwich.jpg",
"url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/wus/Sandwich/250x250.jpg"
},
{ # Himawari SE true color
"filepath": "res/img/himawari-se1-visibleb03.jpg",
"url": "https://www.data.jma.go.jp/mscweb/data/himawari/img/se1/se1_trm_{}.jpg"
},
{ # Himawari SE sandwich
"filepath": "res/img/himawari-se1-sandwichsnd.jpg",
"url": "https://www.data.jma.go.jp/mscweb/data/himawari/img/se1/se1_snd_{}.jpg"
}
]

def download_image(url, save_path):
# Parse the URL to extract the host and path
host = url.split('/')[2]
path = '/' + '/'.join(url.split('/')[3:])

# Establish a socket connection
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((host, 80))

# Send HTTP GET request
request = f"GET {path} HTTP/1.1\r\nHost: {host}\r\n\r\n"
client_socket.sendall(request.encode())

# Receive the response
response = b""
while True:
data = client_socket.recv(512)
if not data:
break
response += data

# Find the start of the image content
start_index = response.find(b'\r\n\r\n') + 4

# Save the image content to a file
with open(save_path, 'wb') as file:
file.write(response[start_index:])

# Close the socket connection
client_socket.close()

def get_current_utc_hour_and_minute():
# Subtract 20 minutes (20 minutes * 60 seconds = 1200 seconds) because himawari delay
current_time = utime.gmtime(utime.time() - 1200)
current_hour = current_time[3]
current_minute = current_time[4] // 10 * 10 # Round down to the nearest 10-minute interval
formatted_time = "{:02d}{:02d}".format(current_hour, current_minute)
return formatted_time

def update_earth_image():
global image1
global show_img_num
global image_dict
global latest_update

downloaded_image_fp = image_dict[show_img_num]["filepath"] # local filepath to save to
source_url = image_dict[show_img_num]["url"]
# if Himari images, need to add UTC hour:min -20 min
if show_img_num == 4 or show_img_num == 5:
source_url = source_url.format(get_current_utc_hour_and_minute())
image_url = "https://YOUR_OWN_URL/resize_image.php?url=" + source_url
download_image(image_url, downloaded_image_fp)
image1.setImage(downloaded_image_fp)
latest_update = time.time()

def btnA_wasClicked_event(state):
global image1
global show_img_num
global image_dict

if show_img_num < len(image_dict)-1: # array start from 0
show_img_num += 1
else:
show_img_num = 0
# set image from local filepath
image_local_fp = image_dict[show_img_num]["filepath"]
image_filename = image_local_fp.split("/")[-1]
if image_filename in os.listdir("res/img/"): # micro python doesn't havve os.path.isfile()
image1.setImage(image_local_fp)
else: # if image is not exist (hasn't been downloaded)
image1.setImage("res/img/uiflow.jpg")
update_earth_image()

def setup():
global image1
global show_img_num
global image_dict

M5.begin()
image1 = Widgets.Image(image_dict[show_img_num]["filepath"], 0, 0)

def loop():
global latest_update
M5.update()
BtnA.setCallback(type=BtnA.CB_TYPE.WAS_CLICKED, cb=btnA_wasClicked_event)
# Update every 8 min (480 s), the rest of time is waiting if button is press to toggle image
# 8 min is from NOAA image update interval 10 min minus update_earth_image() that takes 2 min to finish
if time.time() - latest_update >= 480 or latest_update == 0:
# zero when first run from global var initialization at the top
try: # just in case there's no internet an get error
update_earth_image() # download latest image and set to display
except Exception:
time.sleep(10)

if __name__ == '__main__':
try:
setup()
while True:
loop()
except (Exception, KeyboardInterrupt) as e:
try:
from utility import print_error_msg
print_error_msg(e)
except ImportError:
print("please update to latest firmware")

I hope you enjoy coding and making your own tiny earth! I would love to hear from you if you’re building something fun.

--

--