tech explorers, welcome!

Tag: electronics

Case + OLED display for Raspberry Pi with status panel

I was tempted to just switch cases from my Raspberry Pi 4 to the Raspberry Pi 5, but look at it…

  • 16×2 LCD display from an Arduino Starter Kit
  • C++ program with the WiringPi library
  • DIY case

I was really proud of it at the time and it was good learning back in 2020, but surely I could do better. Surely I could do a cool status panel!!

Components

  • Raspberry Pi (probably compatible with any of them)
  • OLED display (RGB 1.5 inch size is ideal)
  • PCB prototype board
  • Jumper cables
  • Soldering iron
(courtesy of epiCRealism model in ComfyUI)
  • Casing (3D printed or handcrafted)

Software config

I'll configure the Raspberry to interact with this display using python, with the Adafruit Circuitpython library for the SSD1351 display controller:
https://learn.adafruit.com/adafruit-1-5-color-oled-breakout-board/python-wiring-and-setup#setup-3042417

It's as simple as installing Circuitpython with the following command:

sudo pip3 install --upgrade click setuptools adafruit-python-shell build adafruit-circuitpython-rgb-display

If you find issues with your python version not finding a compatible circuit python version, include --break-system-packages at the end. (It wont break anything today, but don't get used to it...)

sudo pip3 install --upgrade click setuptools adafruit-python-shell build adafruit-circuitpython-rgb-display --break-system-packages

Wiring

Now wire your display according to the manufacturer guidance. Mine is this one from BuyDisplay:

https://www.buydisplay.com/full-color-1-5-inch-arduino-raspberry-pi-oled-display-module-128x128

OLED DisplayRaspberry Pi (pin #)
GNDGND (20)
VCC3V3 (17)
SCLSPI0 SCLK (23)
SDASPI0 MOSI (19)
RESGPIO25 (22)
DCGPIO 24 (18)
CSSPI0 CE0 (24)

Use a site like pinout.xyz to find a suitable wiring configuration.

You're ready to do some tests before making your final move to the PCB.

Script config

You can try Adafruit's demo script. Just make sure that you choose the right display and update any changes to the ping assignment (use IO numbers/names rather than physical pin numbers):

# Configuration for CS and DC pins (adjust to your wiring):
cs_pin = digitalio.DigitalInOut(board.CE0)
dc_pin = digitalio.DigitalInOut(board.D24)
reset_pin = digitalio.DigitalInOut(board.D25)

# Setup SPI bus using hardware SPI:
spi = board.SPI()

disp = ssd1351.SSD1351(spi, rotation=270,                         # 1.5" SSD1351
    cs=cs_pin,
    dc=dc_pin,
    rst=reset_pin,
    baudrate=BAUDRATE
)

Assembly

Here are the STL 3D files for this case design:

Now let's put it all together:

  1. Screw the frame to the display
  2. Solder the 7 pins of the display to 7 jumper cables across the PCB
  3. Wire all connections to the Raspberry Pi
  4. Screw the top and bottom pieces together
  5. Place the display on the support

Final result

I've shared the script you see on the images via github:

https://github.com/TheRoam/RaspberryPi-SSD1351-OLED

It currently displays:

  • Time and date
  • System stats (OS, disk usage and CPU temperature)
  • Local weather from World Meteorological Organization (updated hourly)

And this is how it ends up looking. Much better right?

As always, enjoy your tinkering, and let me know any comments or issues on Twitter!

🐦 @RoamingWorkshop

UNIHIKER-PAL: open-source python home assistant simplified

PAL is a simplified version of my python home assistant that I’m running in the DFRobot UNIHIKER which I’m releasing as free open-source.

This is just a demonstration for voice-recognition command-triggering simplicity using python and hopefully will serve as a guide for your own assistant.

Current version: v0.1.0

Features

Current version includes the following:

  • Voice recognition: using open-source SpeechRecognition python library, returns an array of all the recognised audio strings.
  • Weather forecast: using World Meteorological Organization API data, provides today's weather and the forecast for the 3 coming days. Includes WMO weather icons.
  • Local temperature: reads local BMP-280 temperature sensor to provide a room temperature indicator.
  • IoT HTTP commands: basic workflow to control IoT smart home devices using HTTP commands. Currently turns ON and OFF a Shelly2.5 smart switch.
  • Power-save mode: controls brightness to lower power consumption.
  • Connection manager: regularly checks wifi and pings to the internet to restore connection when it's lost.
  • PAL voice samples: cloned voice of PAL from "The Mitchells vs. The Machines" using the AI voice model CoquiAI-TTS v2.
  • UNIHIKER buttons: button A enables a simple menu (this is thought to enable a more complex menu in the future).
  • Touchscreen controls: restore brightness (center), switch program (left) and close program (right), when touching different areas of the screen.

Installation

  1. Install dependencies for voice recognition:
    pip install SpeechRecognition
  2. Download the github repo:
    https://github.com/TheRoam/UNIHIKER-PAL
  3. Upload the files and folders to the UNIHIKER in /root/upload/PAL/
  4. Configure in the script the WIFI credentials, IoT devices, theme, etc.
  5. Run the python script PAL_v010.py from the Mind+ terminal or from the UNIHIKER touch interface.

If you enable Auto boot from the Service Toggle menu , the script will run every time the UNIHIKER is restarted.

https://www.unihiker.com/wiki/faq#Error:%20python3:%20can't%20open%20file…

Configuration

The code includes different configurable aspects:

Theme

Some theme configuration has been enabled by allowing to choose between different eyes as a background image.

Use the variables "eyesA" and "eyesB" specify one of the following values to change the background image expression of PAL:

  • "happy"
  • "angry"
  • "surprised"
  • "sad"

"eyesA" is used as the default background and "eyesB" will be used as a transition when voice recognition is activated and PAL is talking.

The default value for "eyesA" is "surprised" and it will change to "happy" when a command is recognized.

PAL voice

Use the sample audio file "PAL_full" below (also in the github repo in /mp3) as a reference audio for CoquiAI-TTS v2 voice cloning and produce your personalized voices:

https://huggingface.co/spaces/coqui/xtts

Customizable commands

Adding your own commands to PAL is simple using the "comandos" function.

Every audio recognized by SpeechRecognition is sent as a string to the "comandos" function, which then filters the content and triggers one or another matching command.

Just define all the possible strings that could be recognized to trigger your command (note that sometimes SpeechRecognition provides wrong or inaccurate transcriptions).

Then define the command that is triggered if the string is matched.

def comandos(msg):
    # LAMP ON
    if any(keyword in msg for keyword in ["turn on the lamp", "turn the lights on","turn the light on", "turn on the light", "turn on the lights"]):
        turnLAMP("on")
        os.system("aplay '/root/upload/PAL/mp3/Turn_ON_lights.wav'")

Activation keyword

You can customize the keywords or strings that will activate command functions. If any of the keywords in the list is recognized, the whole sentence is sent to the "comandos" function to find any specific command to be triggered.

For the case of PAL v0.1, these are the keywords that activate it (90% it's Paypal):

activate=[
    "hey pal",
    "hey PAL",
    "pal",
    "pall",
    "Pall",
    "hey Pall",
    "Paul",
    "hey Paul",
    "pol",
    "Pol",
    "hey Pol",
    "poll",
    "pause",
    "paypal",
    "PayPal",
    "hey paypal",
    "hey PayPal"
]

You can change this to any other sentence or name, so PAL is activated when you call it by these strings.

Location

The variable "CityID" is used by the WMO API to provide more accurate weather forecast for your location.

Choose one of the available locations from the official list:

https://worldweather.wmo.int/en/json/full_city_list.txt

IoT devices

At the moment, PAL v0.1.0 only includes compatibility with Shelly2.5 for demonstration purposes.

Use variables lampBrand, lampChannel and lampIP to suit your Shelly2.5 configuration.

This is just as an example to show how different devices could be configured. These variables should be used to change the particularities of the HTTP command that is sent to different IoT devices.

More devices will be added in future releases, like Shelly1, ShellyDimmer, Sonoff D1, etc.

Power save mode

Power saving reduces the brightness of the device in order to reduce the power consumption of the UNIHIKER. This is done using the system command "brightness".

Change "ps_mode" variable to enable ("1") or disable ("0") the power-save mode.

Room temperature

Change "room_temp" variable to enable ("1") or disable ("0") the local temperature reading module. This requires a BMP-280 sensor to be installed using the I2C connector.

Check this other post for details on sensor installation:

https://theroamingworkshop.cloud/b/en/2490/

Demo

Below are a few examples of queries and replies from PAL:

"Hey PAL, turn on the lights!"
"Hey PAL, turn the lights off"

Future releases (To-Do list)

I will be developing these features in my personal assistant, and will be updating the open-source release every now and then. Get in touch via github if you have special interest in any of them:

  • Advanced menu: allow configuration and manually triggering commands.
  • IoT devices: include all Shelly and Sonoff HTTP API commands.
  • Time query: requires cloning all number combinations...
  • Wikipedia/browser query: requires real-time voice generation...
  • Improved animations / themes.

Any thoughts, issues or improvements, I'll be happy to read them via github or Twitter!

🐦 @RoamingWorkshop

🌡UNIHIKER real-time temperature sensor set up in 2 minutes

I keep experimenting with the UNIHIKER board by DFRobot and it’s incredibly fast to make things work in it. Today I’ll show you how to set up on-screen real-time temperature display in two minutes using a BMP-280 module and zero programming.

Prerrequisites

Here's the trick. I was expecting you already had a few things working before starting the countdown:

  • Download and install Mind+, DFRobot's IDE for UNIHIKER.
    On Linux, it is a .deb file which does take a while to install:
    https://mindplus.cc/download-en.html
  • Solder a BMP-280 temperature and pressure module and connect it to the I2C cable. You might need to bend your pins slightly as the connector seems to be 1mm nano JST.

You're ready to go!

Set-up

  1. In Mind+, go to the Blocks editor and open the Extensions menu.
  2. Go to the pinpong tab and select the pinpong module (which enables interaction with the UNIHIKER pinout) and the BMP-280 module extension, for interaction with the temperature module.
  1. Go back to the Blocks editor and start building your code block. Just navigate through the different sections on the left hand side and drag all you need below the Python program start block:
    • pinpong - initialize board.
    • bmp280 - initialize module at standard address 0x76.
    • control - forever block (to introduce a while True loop).
    • unihiker - add objects to the display. I firstly add a filled rectangle object to clear previous text, then add a text object. Specify X,Y coordinates where every object will be displayed on the screen and its color.
    • bmp280 - read temperature property. Drag this inside the text field of the text object.
    • python - (optional) add a print to show the data on the terminal. I included all other sensor values.
    • control - add a wait object and wait for 1 second before next loop.
      All of it should look something like this (click to enlarge)

Launch

And that's all your program done, without any programming! Press RUN above and see how it loads and displays in your UNIHIKER screen. Touch the sensor with your finger to see how values change with the increase in temperature.

Wasn't that only 2 minutes? Let me know via Twitter ; )

🐦 @RoamingWorkshop

Shelly 1: wifi switch/scheduler, local HTTP API config and plug box assembly

Today I’m bringing you the second chance that I’ll give Shelly. My first Shelly Dimmer blew up for excess temperature inside a connection box, but another Shelly 2.5 controlling two lights is holding fine, also fitted in the wall.

Maybe the difference is the extra 5ºC that they withstand, so I’m going to fit a wall plug with switch and scheduler using a tiny Shelly 1, and just hope it survives.

Apart from the tiny size, Shelly are easy to configure, so we’ll also see how to control them locally via the HTTP API.

Requirements

My goal is to enable a wall plug that I can control and schedule via WIFI, in my case, to manage the electrical water heater. This is what I'll use:

  • Shelly 1.
  • Two wire electric cable (line and neutral).
  • Male plug.
  • Female plug socket.
  • Assembling material for a case (3D printer, plywood, etc).

Electric connection

Let's look at Shelly's user manual and see how we need to make the connections:

https://www.shelly.com/documents/user_guide/shelly_1_multi_language.pdf

The idea for this standard schematic is to connect Shelly 1 to a light bulb and its switch, where every symbol means the following:

  • L: line
  • N: neutral
  • SW: switch
  • I: input
  • O: output

As I want to enable a plug socket, the schematic will vary slightly, as I will not be using any switch and I can connect the input directly to the line. On the other hand, and for I reason I ignore, there is no cabling inside the connection box, so I bring the electric line from another plug using the cable... In the end, it all ends like this:

TIP! I'd say that I confused the cable color norm, but it is not important in this case as its a closed circuit and it will work anyways.

Assembly

You might see that I made a small 3D support to guide the cabling, as well as a lid to cover the void around the socket. Modelling every part in 3D, with real measures, helps to distribute the space properly and ensure that your solution fits:

I'll leave here the 3D .stl models ready to send to your slider software.

Shelly-Plug_support_v1.stl

https://theroamingworkshop.cloud/demos/Shelly-Plug_support_v1.stl

Shelly-Plug_tapa_v1.stl

https://theroamingworkshop.cloud/demos/Shelly-Plug_tapa_v1.stl

Finally, this is how it all looks crafted in place. It's not the perfect fit, but it does the job I needed.

Internet connection

Let's now see how to bring the Shelly 1 to life and control it locally.

Opposite to Sonoff, Shelly makes it much easier and you just need to follow the user manual.

  1. Power Shelly 1 using the male plug.
  2. This will activate an AP (Access Point) or Wi-Fi network with an SSID looking like "shelly1-01A3B4". Connect to this Wi-Fi network using a smartphone or PC.
  3. Once connected, use a web browser to access the IP at 192.168.33.1 and it will take you to Shelly's web interface for device configuration.
  1. Once in, you must config the device (inside Internet & Security menu) so that it automatically connects to your local Wi-Fi network, as well as it is recommended to restrict access with username and password.

We're all set to communicate with Shelly 1 locally.

Shelly HTTP API usage

To use the command of the HTTP API you must know the device IP in your local network.

Find IP in the router

You can access the network map in your router, usually from the address http://192.168.1.1

The address and the password should be in some sticker in your router. Then you'll see your device with a name like shelly1-XXXXXXXXXXXX:

Find IP using nmap

In a terminal you can use the tool nmap to scan your local network.

  • Download it if not done yet:
    sudo apt-get update
    sudo apt-get install nmap
  • Scan your network (using sudo you'll get the MAC address, which is useful as the IP could change when restarting the router)
    sudo nmap -v -sn 192.168.1.0/241.0/24

Send HTTP requests to the device

Shelly's HTTP API is well documented in their website:

https://shelly-api-docs.shelly.cloud/gen1/#common-http-api

In order to communicate with the device, you need to send HTTP requests using some software like Postman or using curl or wget in a terminal.

The request will be sent to the device IP with:

$ curl -X GET http://192.168.1.XX/command

If you defined user and password, you need to include them in the URL like below, or you'll receive a "401 unauthorized" response:

$ curl -X GET http://user:[email protected]/command

Now let's see some specific cases:

Device information

http://[user]:[pass]@[ip]/status

  • curl

curl -X GET 'http://user:[email protected]/status

  • Response
{"wifi_sta":{"connected":true,"ssid":"MYWIFINETWORK","ip":"192.168.1.XX","rssi":-70},"cloud":{"enabled":false,"connected":false},"mqtt":{"connected":false},"time":"19:30","unixtime":1699295403,"serial":1,"has_update":false,"mac":"A4CF12F407B1","cfg_changed_cnt":0,"actions_stats":{"skipped":0},"relays":[{"ison":false,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"source":"input"}],"meters":[{"power":0.00,"is_valid":true}],"inputs":[{"input":0,"event":"","event_cnt":0}],"ext_sensors":{},"ext_temperature":{},"ext_humidity":{},"update":{"status":"idle","has_update":false,"new_version":"20230913-112003/v1.14.0-gcb84623","old_version":"20230913-112003/v1.14.0-gcb84623"},"ram_total":51688,"ram_free":39164,"fs_size":233681,"fs_free":146333,"uptime":2679}

Turn (on/off)

http://[usr]:[pass]@[ip]/relay/0?turn=[on/off]

  • curl

curl -X GET http://user:[email protected]/relay/0?turn=on

  • Response
{"ison":true,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"source":"http"}

The value 0 in the URL matches te number of the relay or internal switch in the Shelly. In this case there is only one, but in the case of Shelly 2.5 you have two relays, so you can call them individually changing this value.

Scheduler

http://[usr]:[pass]@[ip]/settings/relay/0?schedule_rules=[HHMM]-[0123456]-[on/off]

  • curl

curl -X GET http://user:[email protected]/settings/relay/0?schedule_rules=1945-0123456-on

  • Response
{"name":"CALENTADOR","appliance_type":"General","ison":false,"has_timer":false,"default_state":"off","btn_type":"toggle","btn_reverse":0,"auto_on":0.00,"auto_off":0.00,"power":0.00,"schedule":true,"schedule_rules":["1945-0123456-on"]}

In this case, the URL defines the following schedule rule parameters:

  • HHMM: hour and minute that activate the rule
  • 0123456: days of the week when the rule is active
  • on/off: status that the rule triggers

This way, to schedule the on and off of the device (except during weekends), you could send a request like this one:

curl -X GET http://192.168.1.XX/settings/relay/0?schedule_rules=2300-01234-on,0700-01234-off

Obviously you can also configure the schedule rules from the web interface, or just check the commands worked:

And that would cover all of it. Jump off and fill your house with tiny Shellys completely customizable. Any questions or comments on Twitter 🐦 please! (though given what's going on with the X thing, who knows how long I'll last...)

Sonoff D1 Dimmer: configuring local HTTP API (DIY mode) and assembling for external connection.

I had a Shelly Dimmer inside a plug box in the wall, but one good day it stopped working (probably because of high temperatures, as it stands up to 35ºC). Looking for an alternative, I found Sonoff had released their equivalent for 1/3 the price of Shelly.

But in the end, cheap turns expensive, as it is much more complicated to configure than Shelly Dimmer and it has a bigger size.

After many tests, and given the poor documentation, here I explain how to configure Sonoff D1 Dimmer to use the local API without depending on the e-weLink app.

Additionally, given its size, you wont find much space for it in your connection boxes, so I’ll give you the idea to craft an external connection expansor.

Requirements

  • Sonoff D1 Dimmer.
  • Two wire electrical cable (line and neutral).
  • Female plug socket.
  • Male plug.
  • Sonoff RM-433 remote controller (very recommended).
  • Assembling material for a case (3D printer, plywood, etc).

Electrical connection

The first thing you need to achieve is to connect the D1 to the 220V domestic network, following the schematic given in the user manual:

https://sonoff.tech/wp-content/uploads/2021/03/%E8%AF%B4%E6%98%8E%E4%B9%A6-D1-V-1.1-20210305.pdf

The previous schematic more or less complies with European norm:

  • Line (positive): black, brown or grey (red in this case...)
  • Neutral (negative): blue.

Shelly Dimmer is much more compact and fits easily in a connection box. But not in this case, so I will connect it externally using an extension lead, and I will later detail a simple case for its assembly.

TIP! If you're not experienced in electricity, you should review quite a bit and move forward with caution. It's not nice to have a shock with the domestic network. If you do the connection externally this way you won't be in much danger.

For the moment we can now make it work.

Internet connection

This is the complicated bit, as with so much casing, apparently there was no place for the usual pushbutton to power on/off and restore the device.

If you're lucky, your Sonoff wont be preconfigured and you might be able to connect to it on the first attempt. If it's preconfigured, probably to check its operation in another network, the device is no longer accessible even with the e-weLink app, unless you are in the network where it was configured.

To detect it, you must restore to default settings and for this you have two options:

  • Restore using e-weLink app from the network where it was configured (very unlikely you have access to it).
  • Restore using Sonoff RM-433 remote controller (you'll end up buying this extra accessory).

Pairing Sonoff RM-433 remote controller

In the end, the cheap D1 price has doubled with the need to buy the RM-433 remote controller, but the price is still not mad. Here is its manual:

https://sonoff.tech/wp-content/uploads/2021/03/%E8%AF%B4%E6%98%8E%E4%B9%A6-RM433-V1.1-20210305.pdf

The first thing to do is to pair the controller with the D1:

  1. Connect the D1 to a socket.
  2. Hold button 7 for some 5 seconds, until you hear a beep (this removes the previous radio-frequency assignment).
  3. Unplug and plug the D1 to get it restarted.
  4. Press any button on the controller so it's assigned to the D1.
  5. You'll hear another beep and the controller is now paired and can be used to control the D1.

Restore WIFI network

Now you need to restore the network assigned to the D1.

Hold button 8 for some 5 seconds, or basically, until the led starts blinking this way:

Breathing mode. Two fast blinks, one slow blink.

You removed the previous network. Now set it to pairing mode.

Again, hold button 8 for some 5 seconds, or until the led starts blinking continuously:

Pairing mode. Constant blinking.

This way, the device starts a WIFI Access Point (WIFI AP) with a name in the form ITEAD-XXXXXXXXXX.

Pairing with e-weLink

From here, if you want the easy route, just download the e-weLink app and press the quick pairing button. You'll then have your D1 accessible from this app.

Pairing in DIY mode

But I want the complicated way and enable DIY mode to access the device network and control it using commands from the HTTP API in a web app.

We need to find the WIFI network named ITEAD-XXXXXXXXXX set up by the device and connect to it using the password 12345678.

Now open a web browser and access this address http://10.10.7.1 where you'll find the following screens.

Introduce the name (SSID) and password of your WIFI network, and the device is now linked to it.

Assembly

Before getting into the detail of the HTTP API, I'll show you a 3D printed case design to avoid the cables and connections being completely exposed.

It consists of two PLA pieces (base and top) which can be screwed together and which you can download from this server:

https://theroamingworkshop.cloud/demos/D1case_v1_base.stl

https://theroamingworkshop.cloud/demos/D1case_v1_top.stl

You can also preview here:

D1 HTTP API usage

To use the command of the HTTP API you must know the device IP in your local network.

Find IP in the router

You can access the network map in your router, usually from the address http://192.168.1.1

The address and the password should be in some sticker in your router. Then you'll see your device with a name like ESP-XXXX which derives from the WIFI module it holds (I already renamed it here):

Find IP using nmap

In a terminal you can use the tool nmap to scan your local network.

  • Download it if not done yet:
    sudo apt-get update
    sudo apt-get install nmap
  • Scan your network (using sudo you'll get the MAC address, which is useful as the IP could change when restarting the router)
    sudo nmap -v -sn 192.168.1.0/24

Send HTTP requests to the D1

Sonoff's D1 HTTP API is documented in their website:

https://sonoff.tech/sonoff-diy-developer-documentation-d1-http-api/

In order to communicate with the device, you need to send HTTP requests using some software like Postman or using curl or wget in a terminal.

The request is sent to the device IP, to the default port 8081, and we also have to include the device id in the request body (this id matches the XXXXXXXXXX coding in the WIFI network name ITEAD-XXXXXXXXXX).

Let's see some use cases with curl and Postman.

Device information

http://[ip]:[port]/zeroconf/info

  • curl

curl -X POST 'http://192.168.1.34:8081/zeroconf/info' --data-raw '{"deviceid": "XXXXXXXXXX","data": {}}'

  • Postman
  • Response
{
    "seq": 6,
    "error": 0,
    "data": {
        "deviceid": "XXXXXXXXXX",
        "switch": "off",
        "startup": "off",
        "brightness": 60,
        "brightMin": 0,
        "brightMax": 100,
        "mode": 0,
        "otaUnlock": false,
        "fwVersion": "3.5.0",
        "ssid": "TU_RED_WIFI",
        "bssid": "XX:XX:XX:XX:XX:XX",
        "signalStrength": -58
    }
}

Turn on/off

http://[ip]:[port]/zeroconf/switch

  • curl

curl -X POST 'http://192.168.1.34:8081/zeroconf/switch' --data-raw '{"deviceid": "XXXXXXXXXX","data": {"switch":"on"}}'

  • Postman
  • Response
{
    "seq": 9,
    "error": 0
}

Brightness adjustment

http://[ip]:[port]/zeroconf/dimmable

  • curl

curl -X POST 'http://192.168.1.34:8081/zeroconf/dimmable' --data-raw '{"deviceid": "XXXXXXXXXX","data": {"switch":"on","brightness":50,"mode":0,"brightmin":0,"brightmax":100}}'

  • Postman
  • Response
{
    "seq": 14,
    "error": 0
}

Now you're ready to program your own app and control your D1 to your like in a completely private way. I hope this was useful, but if you find any doubts or comments, don't hesitate to drop them on Twitter 🐦!

🐦 @RoamingWorkshop

Temps-i 7: DIY desktop clock with WIFI, temperature-pressure sensor and +48h autonomy.

After lots of comings and goings, trials, redesigns, burnts, cuts and some minor explosions, finally I can bring a smart desktop clock I have been working on for the last 2 years. Making a detailed tutorial can be even longer and tedious, so I hope these traces can help make your own. I warn you this takes a lot of time and practice, and can’t be done carelessly…

What's Temps-i 7?

Let's break down its name:

  • Temps mean time in the Valencia dialect,
  • i for internet, where it gets the time,
  • 7 for the display, which uses 7 segments for every digit.

These three concepts define this compact desktop clock, with WIFI connectivity, temperature sensor and good autonomy.

Let's see how it's made!

Components

First, let's see the recipe ingredients and what each one does:

  • Sparkfun ESP32-Thing board
    >>Top performance microcontroller with WIFI and Bluetooth connectivity thanks to the ESP32 integrated chip, ideal for IoT projects.
  • 4 digits 7 segments red color display.
    >>Shows time and temperature.
  • BMP-280 module.
    >>Temperature and barometric pressure compact digital sensor.
  • 100 Ohm resistors
    >>Needed to reduce display current, without lowering brightness excessively (admits up to 1k Ohm, but leds would be hardly visible with daylight).
  • PCB with custom circuit.
    >>Simplifies display and resistors connectiont to the microcontroller.
  • 1000mAh 3.7v LiPo battery
    >>Ensures an autonomy up to 48 hours without external voltage.
  • Jumper cables.
    >>For additional connections of external modules.
  • Push button
    >>Used to switch the program on display.

Electronic design

It's very important to study the electronic components being used, read all the specifications and prototype with all precautions before we start soldering like crazy.

Component selection

The components listed earlier are not a mere coincidence or copied from elsewhere. They're the most successful trial of many others and meets the project needs:

  • The 4 digit display shows exactly what I'm after: the time. If I can also use it to show temperature, that's fine. But a better quality display, like LCD, would be unnecessarily demanding, and autonomy is another key requirement.
  • The microcontroller includes internet connectivity, as well as enough computing capacity. It also has sufficient in/out pins to control the display without a gpio expansor. Some other options I've tried:
    • More compact microcontrollers: Teensy 4, Digispark, SparkFun Pro Micro. They need a GPIO expansor (like PCF8574) and/or a WIFI module (like ESP-01). This also involves too many more connections.
    • Microcontrollers integrating WIFI and sufficient I/O, like NodeMCU ESP8266. Got out-dated and lacks processing capacity as the counter delayed almost 4 seconds every minute.

Prototyping electronic circuit

Having researched and obtained the components, connect them in a prototype board (protoboard) to test their operation.

In my case, after different pin combinations, the most organised way is the following:

ESP32-ThingComponent
VBATLiPo +
3V3BMP-280 3V3
GNDLiPo -
BMP GND
BMP SD0
Push -
GPIO21BMP SDA
GPIO04BMP SCL
GPIO32Push +
GPIO17Display Digit 1
GPIO23Display Digit 2
GPIO19Display Digit 3
GPIO25Display Digit 4
GPIO15Display Segment A
GPIO22Display Segment B
GPIO27Display Segment C
GPIO12Display Segment D
GPIO13Display Segment E
GPIO18Display Segment F
GPIO26Display Segment G
GPIO14Display Segment P (dot)

Pinout schematic

Once the prototype is achieved, you should save it in a schematic diagram using EDA software (electronic design automation) like Kicad, where you can also generate a PCB design that can be sent for manufacturing. Otherwise, you can always save the schematic in paper, as you wont remember where every cable was going in a couple months time...

Kicad is a bit tricky and it's good to practice with simpler projects. Despite this, it's quite manageable for medium users as it basically consists of searching and choosing symbols for our components and connect their pins accordingly to specifications.

To avoid messing the sketch up with cables, I used names in every connection, which is also valid in Kicad. Also, you'll see that the ESP32-Thing is made up of two 20x pin headers, as I didn't find a working symbol and didn't have time to design one properly. What really matters is that the design is working and coherent with reality.

Kicad shematic

Next step is to assign footprints that are realistic for each symbol, so then we can design a printed circuit board that we can order (usually in China) for 15€ / 5 boards.

You don't need to go crazy on this, specially if you're not experienced. I only need to make soldering connections simpler, as in this case you need about 90 of them but keeping a compact design.

Clock programming

Most microcontrollers, like the ESP32-Thing, are compatible with the Arduino IDE, which makes it simpler to connect the board to a PC and load a clock program.

Before starting, it's important to make a list of tasks and functions that we want to include in the program and modify it as we code. This way you can try different functions separately and debug every step to find errors quickly. In my case, and after many trials, the program will consist of the following:

  1. Define libraries and variables.
  2. Configure pins.
  3. Connect WiFi.
  4. Get date via SNTP.
  5. Disconnect and turn off WiFi (saves battery).
  6. Convert date into digits.
  7. Show digits on display.
  8. Start timer.
  9. Start reading program change pin.
  10. Change program on pushbutton activation.
    1. Read sensors.
    2. Show temperature on display.
  11. Update time after timer ending (every minute).
  12. Restart timer.

I don't want to spend too long on the code, and it's also not the most tidy I have, but here it is for anyone who wants to copy it, and also on github:

https://github.com/TheRoam/Tempsi-7

//----------------------------------------------------------------//
//    Temps-i 7 WiFi clock and temperature in 4 digit display     //
//  v 1.0.1                                                       //
//                                                                //
//  Interfaces:                                                   //
//  - Sparkfun ESP32-Thing micocontroller                         //
//  - BMP-280 temperature and pressure digital sensor             //
//  - 4 digit 7 segment display                                   //
//  - Programm push button                                        //
//  - LiPo Battery                                                //
//                                                                //
//  Detailed documentation:                                       //
//  https://theroamingworkshop.cloud                              //
//                                                                //
//                © THE ROAMING WORKSHOP 2022                     //
//----------------------------------------------------------------//
#include < esp_wifi.h >
#include < WiFi.h >
#include < WiFiMulti.h >
#include "time.h"
#include "sntp.h"
#include < Wire.h >
#include < Adafruit_BMP280.h >
// Spaces inside <> only shown for correct html display. Remove them in Arduino IDE
//BMP280 sensor using I2C interface
Adafruit_BMP280 bmp;
#define BMP_SCK  (4)
#define BMP_MISO (21)
#define BMP_MOSI (4)
#define BMP_CS   (21)
//Sensor variables
float TEMP=0;     //temperature variable
float ALT=0;      //altitude variable
float PRES=0;     //pressure variable
float hREF=1020.0;//sea level reference pressure in hPa
//define time variables
RTC_DATA_ATTR long long TIME=0; //concatenated time
RTC_DATA_ATTR long long d=0;  //day
RTC_DATA_ATTR long long m=0;  //month
RTC_DATA_ATTR long long Y=0;  //year
RTC_DATA_ATTR long long H=0;  //hour
RTC_DATA_ATTR long long M=0;  //minute
RTC_DATA_ATTR long long S=0;  //second
RTC_DATA_ATTR uint32_t dS=0;  //seconds counter for dot
RTC_DATA_ATTR struct tm timeinfo; //saves full date variable
long long inicio=0; //saves start time
long long ahora=0;  //saves current time
//Define digit pins in an array, in display order, for looping
//Numbers match ESP32-Thing GPIO number
int DigPins[4]{
  17,// first digit (GPIO 17)
  23,//second digit (GPIO 23)
  19,//third digit  (GPIO 19)
  25//fourth digit  (GPIO  25)
};
//Define segment pins
//Numbers match ESP32-Thing GPIO number
int SegPins[8]{
  14,   //P
  26,   //g
  18,   //f
  13,   //e
  12,   //d
  27,   //c
  22,   //b
  15    //a
};
//Auxiliary variables
//Numbers match ESP32-Thing GPIO number
int ProgPin=32;   //Pin no used for program
int ButtonStatus=1;
int ledPin=5;     //Used to blink the ESP32-Thing blue led
int ProgNum=-1;   //Define a variable to keep track of current program number
// WIFI
// Define your wifi network and credentials
char ssid1[] = "YOUR_WIFI_SSID1";
char pass1[] = "YOUR_WIFI_PASS1";
char ssid1[] = "YOUR_WIFI_SSID2";
char pass1[] = "YOUR_WIFI_PASS2";
WiFiMulti wifiMulti;
// NTP variables
// Update time zone if needed
const char* ntpServer1 = "pool.ntp.org";
const char* ntpServer2 = "time.nist.gov";
const long  gmtOffset_sec = 3600;
const int   daylightOffset_sec = 0; //this will be corrected later with software
const char* time_zone = "CET-1CEST,M3.5.0,M10.5.0/3";  // TimeZone rule for Europe/Rome including daylight adjustment rules (optional)
// Displayed characters in every digit are a byte array indicating ON (1) and OFF (0) segments
// Use a wiring pattern that matches an understandable byte chain so you can make them up easily
// Segment/byte pattern: Babcdefgp --> where 1 is HIGH (ON) and 0 is LOW (OFF)
// Refer to a character by calling this array, i.e.:
//  - call number 3 by calling ns[3]
//  - call letter A by calling ns[20]
//  - call underscore symbol (_) by calling ns[49]
byte ns[50]{ // Array Position - Byte character
  B11111100,// 0-0
  B01100000,// 1-1
  B11011010,// 2-2
  B11110010,// 3-3
  B01100110,// 4-4
  B10110110,// 5-5
  B10111110,// 6-6
  B11100000,// 7-7
  B11111110,// 8-8
  B11110110,// 9-9
  B11111101,// 10-0.
  B01100001,// 11-1.
  B11011011,// 12-2.
  B11110011,// 13-3.
  B01100111,// 14-4.
  B10110111,// 15-5.
  B10111111,// 16-6.
  B11100001,// 17-7.
  B11111111,// 18-8.
  B11110111,// 19-9.
  B11101110,// 20-A
  B00111110,// 21-b
  B10011100,// 22-C
  B01111010,// 23-d
  B10011110,// 24-e
  B10001110,// 25-f
  B10111100,// 26-G
  B00101110,// 27-h
  B00001100,// 28-I
  B11111000,// 29-J
  B01101110,// 30-K(H)
  B00011100,// 31-L
  B00101010,// 32-m(n)
  B00101010,// 33-n
  B00111010,// 34-o
  B11001110,// 35-P
  B11100110,// 36-q
  B00001010,// 37-r
  B10110110,// 38-S
  B00011110,// 39-t
  B01111100,// 40-U
  B00111000,// 41-v
  B00111000,// 42-w(v)
  B01101110,// 43-X(H)
  B01110110,// 44-y
  B11011010,// 45-Z
  B00000001,// 46-. (dot)
  B11000110,// 47-* (astherisc)
  B00000010,// 48-- (hyphon)
  B00010000,// 49-_ (underscore)
};
//array to store displayed digits
int digits[4];
//digit calculation variables
int first_digit = 0;
int second_digit = 0;
int third_digit = 0;
int fourth_digit = 0;
//counters for looping digits and current number
int dig=0;
int n=0;
int dot=1;
void setup()
{
  // Start disabling bluetooth and WIFI to save energy
  // Just power WIFI later, when needed.
  esp_err_t esp_bluedroid_disable(void);
  esp_err_t esp_bt_controller_disable(void);
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  
  // Start serial communication for any debug messages
  // Commented for production; uncomment for debugging
  //Serial.begin(115200);
  //Define I2C pins, as we are not using standard ones
  Wire.begin(21,4);
  // Activate digit pins looping the pin array.
  for (dig=0; dig<4; dig++){
    pinMode(DigPins[dig], OUTPUT);
  }
  for (dig=0; dig<4; dig++){  //set them LOW (turn them OFF)
    digitalWrite(DigPins[dig], LOW);
  }
  // Activate segment pins
  for (int i=0; i<8; i++){
    pinMode(SegPins[i],OUTPUT);
  }
  // Activate LED pin
  pinMode(ledPin, OUTPUT);
  // Activate PROG pin
  pinMode(ProgPin, INPUT_PULLUP);
  // Turn ON Wifi
  wifiON();
  
  // Setup NTP parameters
  sntp_set_time_sync_notification_cb( timeavailable );
  sntp_servermode_dhcp(1);
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2);
  //wait for date
  do{
    delay(100);
  }while(TIME==0);
  delay(500);
  //WIFI can be turned off now
  wifiOFF();  
  // Setup BMP280
  unsigned status;
  //BMP-280 I2C Address:
  // 0x76 if SD0 is grounded
  // 0x77 if SD0 is high
  status = bmp.begin(0x76);
  if (!status) {
    Serial.println(F("Could not find a valid BMP280 sensor, check wiring or "
                      "try a different address!"));
    Serial.print("SensorID was: 0x"); Serial.println(bmp.sensorID(),16);
    Serial.print("        ID of 0xFF probably means a bad address, a BMP 180 or BMP 085\n");
    Serial.print("   ID of 0x56-0x58 represents a BMP 280,\n");
    Serial.print("        ID of 0x60 represents a BME 280.\n");
    Serial.print("        ID of 0x61 represents a BME 680.\n");
    ESP.restart();
    while (1) delay(10);
  }
  //Default settings from datasheet.
  bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,     // Operating Mode.
                  Adafruit_BMP280::SAMPLING_X2,     // Temp. oversampling
                  Adafruit_BMP280::SAMPLING_X16,    // Pressure oversampling
                  Adafruit_BMP280::FILTER_X16,      // Filtering.
                  Adafruit_BMP280::STANDBY_MS_500); // Standby time.
  //get initial readings
  readSensors();
  
  // Get correct time
  sync_clock();
  num_shift();
  // Set start time
  inicio = millis();
  // Set dot time
  dS=millis();
}
void loop(){
  // After setup, this function will loop until shutdown.
  
  // Start time counter using chip's milisecond counter
  uint32_t ahora=millis();
  
  // Check if the counter has reached 60000 miliseconds.
  // Calibrate if you realize that time desyncs
  // This depends on processor calculation, which can vary with input voltage (if on batteries) and temperature.
  while ( (ahora-inicio+(S*1000)) < 59500){
    // Run dot blinker when not showing temperature
    //commented as it sometimes desyncs the hour digit (needs fixing)
    /*if(ProgNum==-1){
      dot_blinker();
    }*/
    
    // Reading push button:
    // when pressed change status
    if(digitalRead(ProgPin)==1){
      ButtonStatus=1;
    }
    // when released after press, set "program change" status
    // this avoids constant change when holding
    if(digitalRead(ProgPin)==0 && ButtonStatus==1){
      ButtonStatus=2;
    }
    // Status 2 -> Programm change
    if(ButtonStatus==2){
      //reset status
      ButtonStatus=0;
      //update programm number
      ProgNum=-ProgNum;
      Serial.println(ProgNum);
      //change to temp
      if(ProgNum==1){
       //light LED
       digitalWrite(ledPin, HIGH); 
       //save TIME
       TIME=digits[0]*1000+(digits[1]-10)*100+digits[2]*10+digits[3];
       //show temp
       room_temp();
      }else if(ProgNum==-1){  //change to time
        //turn off LED
        digitalWrite(ledPin, LOW);
        //restore time
        split_time(TIME);
      }
    }
    
    //update time counter
    ahora=millis();
    num_shift();
    delay(6);
  }
  // End of while() after 60 seconds
  // Reset initial time
  S=0;
  inicio=millis();
  //make sure there's dot at the end
  if(digits[1]<=10){
    digits[1]+10;
    dot=0;
  }
  //update time
  //if program is in temperature, change to time
  if(ProgNum==1){
    split_time(TIME);
    ProgNum=-1;
  }
  updateMinutes();
}
//-WIFI function
//--Setup and turn Wifi ON
void wifiON(){
  // Set up WIFI (ESP32 Thing only working with wifiMulti library)
  Serial.println("Conectando");
  wifiMulti.addAP(ssid1,pass1);
  //wifiMulti.addAP(ssid2,pass2);
  int led=1;
  int boot=1;
  wifiMulti.run();
  //WiFi.disconnect(true);
  
  while((WiFi.status() != WL_CONNECTED)){
    digitalWrite(ledPin, HIGH);
    delay(500);
    digitalWrite(ledPin,LOW);
    delay(500);
    wifiMulti.run();
  }
  // Make sure LED is off when finished
  digitalWrite(ledPin,LOW);
  // When CONNECTED, while loop ends.
  Serial.print("Conectado a ");
  Serial.println(WiFi.SSID());
  Serial.println(WiFi.localIP());
}
//-Disable WIFI
void wifiOFF(){
  //turn WIFI off as it's not needed any more
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  //disable wifi (deinit clears all flash data)
  esp_wifi_deinit();
}
//----BMP280 functions
//-Read sensor data
void readSensors(){
  TEMP=bmp.readTemperature();
  Serial.println("T: "+(String)TEMP+"ºC");
  PRES=bmp.readPressure();
  Serial.println("P: "+(String)PRES+"hPa");
  ALT=bmp.readAltitude(hREF);
  Serial.println("h: "+(String)ALT+"msnm");
}
//----NTP functions
//-Callback function (get's called when time adjusts via NTP)
void timeavailable(struct timeval *t)
{
  Serial.println("Got time adjustment from NTP!");
  simpleTime();
}
void simpleTime()
{ 
  if(!getLocalTime(&timeinfo)){
    Serial.println("No time available (yet)");
    return;
  }
  Serial.print("Synced time: ");
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
  //save time
  sync_clock();
}
//-
//PROGRAM #0: HELLO at startup
//function to say "HOLA" at start up while connecting to WiFi
void ini_HOLA(){
  int h=30;
  int o=34;
  int l=31;
  int a=20;
  digits[0]=46;
  digits[1]=46;
  digits[2]=46;
  digits[3]=46;
}
//PROGRAM #1: WiFi synced time
//function that gets current time via WiFi
void sync_clock(){
  Y=timeinfo.tm_year;
    TIME=TIME+Y*10000000000;
  m=timeinfo.tm_mon;
  m=m+1;
    TIME=TIME+m*100000000;
  d=timeinfo.tm_mday;
    TIME=d*1000000;
  H=timeinfo.tm_hour;
  //check daylight saving time and correct hour
  //"27 Mar (03 27) +1 hour; 30 Oct (10 30) back -1 hour"
  if( ( m*100+d >= 327 ) && ( m*100+d < 1030 ) ){
    H=H+1;
  }else{
    H=H;
  }
  TIME=TIME+H*10000;
  M=timeinfo.tm_min;
    TIME=TIME+M*100;
  S=timeinfo.tm_sec;
    TIME=TIME+S;
    
  //split current 4 digit time into sigle digits
  split_time((TIME/100)-((TIME/100)/10000)*10000);
}
//number splitting function to separate time string into digits
void split_time(long long num) {
  first_digit = num / 1000;
  digits[0] = first_digit;
  int first_left = num - (first_digit * 1000);
  second_digit = first_left / 100;
  digits[1] = second_digit;
  //añadimos el segundero fijo (sumamos 10)
  digits[1] = digits[1]+10;
  int second_left = first_left - (second_digit * 100);
  third_digit = second_left / 10;
  digits[2] = third_digit;
  fourth_digit = second_left - (third_digit * 10);
  digits[3] = fourth_digit;
}
// number shifting function
void num_shift(){
  for (dig=0; dig<4; dig++){// turn digits off
    digitalWrite(DigPins[dig], HIGH);
  }
  
  //turn them ON (LOW) one by one
    digitalWrite(DigPins[n], LOW);
    for(int seg=7; seg>=0; seg--){
      //read byte array for digit
      int x = bitRead(ns[digits[n]],seg);
      //turn the segments ON or OFF
      digitalWrite(SegPins[seg],x);
    }
    n++;// move to next no.
    if (n==4){// if no. is 4, restart
      n=0;
    }
}
//Getting room temperature from LM35 sensor via NodeMCU analog input pin (ADC)
void room_temp(){
  readSensors();
  //This will return temperature in XY.Z format
  //We don't want the integer value, so we can use the four digits to display temperature units as well "XYºC"
  //Getting first digit in byte format ns[X] where X=int(XY.Z/10)=int(X.YZ)=X
  digits[0]=int(TEMP/10)-int(TEMP/100)*10;
  //Getting second digit in byte format ns[Y] where Y=int(XY.Z)-X*10=int(XY.Z)-int(XY.Z/10)*10
  digits[1]=int(TEMP/1)-int(TEMP/10)*10;
  //Setting temperature units as degree celsius (ºC) in byte format
  digits[2]=47; //astherisc *
  digits[3]=22; //character C
}
void updateMinutes(){
  //add 1 to the last digit
  digits[3]=digits[3]+1;
  //if greater than 9, reset to 0
  if (digits[3]>9){
    digits[3]=0;
    //then add 1 to the third digit
    updateM0();
  }
  //send last digit to display
  digitalWrite(DigPins[3], LOW);
}
void updateM0(){
  //add 1 to third digit
  digits[2]=digits[2]+1;
  //if greater than 5, reset to 0
  if (digits[2]>5){
    digits[2]=0;
    //then add 1 to current hours
    updateH1();
  }
  //send digit to display
  digitalWrite(DigPins[2], LOW);
}
void updateH1(){
  //add 1 hour
  digits[1]=digits[1]+1;
  // if greater than 19, reset to 10
  // (instead of number 0-9, we use numbers 10-19 in order to add the "dot" to the display)
  if (digits[1]>19){
    digits[1]=10;
    //then add 1 to the first digit
    updateH0();
  }// reset when it's 24h (go back to 00)
  if(digits[1]>13 && digits[0]==2){
    digits[0]=0;
    digits[1]=10;
  }
  //display digits
  digitalWrite(DigPins[1], LOW);
  digitalWrite(DigPins[0], LOW);
}
void updateH0(){
  //add 1 to first digit
  digits[0]=digits[0]+1;
  //if greater than 2, reset to 0 (it shouldn't happen as we reset earlier, but just in case..)
  if (digits[0]>2){
    digits[0]=0;
  }
  //display digit
  digitalWrite(DigPins[0], LOW);
}
void dot_blinker(){
  //every second, blink the dot
    if( millis()-dS > 1000 && dot == 1){
      digits[1]=digits[1]-10;
      dS=millis();
      dot=0;
    }
    if( millis()-dS > 250 && dot == 0){
      digits[1]=digits[1]+10;
      dot=1;
    }
}

Notice you'll need these additional libraries installed using the Arduino IDE:

  • esp32 board manager by Espressif
  • WiFiMulti library
  • Adafruit_BMP280 library
    • (the rest of the libraries derive from these ones)

You should constantly try the performance of the code during prototyping so you can change any pin assignment in case of any malfunctioning. If it's all soldered and something fails, it will be really hard to find and solve the error, if it's from a connection.

Assembly

Soldering

Once the code is checked and the PCB is designed, you can start soldering with caution, as a wrong movement can damage your modules or produce errors in the program.

Case design

Having it all soldered you'll have a better idea of the final volume of the device. I measure it all with precision using a digital caliper to make a 3D model of it.

Blender 3D model of all components in their final position

This way you can now design a case around the model so you can craft it using a 3D printer. You could also use other types of assembling materials like plywood or metal.

I usually craft two pieces (one as a base and another one as a cover) so they can be screwed together. I also include different voids to allow for the connection of the USB cable, to add the program switch button and to add a rotary support to hold the clock in a slot in the television.

I will also use white PLA in one piece and grey in the other to bring more life to the design, ending it all like this:

And that's it. I hope you liked it and find it useful. Now you can get your own WIFI clock crafted! Any doubt about this clock can be dropped on Twitter! 🐦

🐦 @RoamingWorkshop