Skip to main content

7.1 Smart Weather Station

Nothing in this project is new. The sensor is the one from section 5, the display is the one from section 4, and the Wi-Fi is the one from section 6. What is new is that all three run at once, in one script, on one pair of wires: a board that measures the room, shows the reading on its own screen, and puts the same numbers on the internet where you can read them from anywhere.

That combination is what people mean by IoT: a small thing that senses something local and reports it somewhere else.

In this documentation you will learn:

  • How to put two Qwiic modules on one bus, and why they do not interfere with each other.
  • What an I2C address is for, in the one situation where it finally matters.
  • How to send a measurement to a web server with an HTTP POST request.
  • Why the display stays blank for the first thirty seconds, and why that is not a fault.
  • Why the same reading is formatted three different ways in the same script.
  • How to write the degree symbol on a display that does not know your character set.

Hardware required:

  • 1x Soldered NULA MINI board
  • 1x Breadboard
  • 1x Soldered SHTC3 temperature and humidity sensor
  • 1x 16x2 LCD display with I2C adapter
  • 2x Qwiic cables
  • 1x USB-C cable
ℹ️
Still no resistors and no jumper wires. Both modules are Qwiic, so the entire circuit is two cables that only fit one way round. The one thing to check before you start is that you have two Qwiic cables rather than one. Every earlier example needed only a single cable.

Putting the components together

Five steps, and three of them are just plugging a cable in.

1. Insert the NULA MINI board on the breadboard

Push the board into one end of the breadboard so that its two rows of pins sit on either side of the centre channel. In the photos it occupies rows 25 to 30.

NULA MINI board seated on the breadboard
Step 1: the board seated on the breadboard
ℹ️
As in examples 3.1, 4.1 and 5.1, the breadboard does no electrical work in this project. Nothing connects to the pins of the board at all. It is there to hold the board flat while you push two cables into it.

2. Plug the first Qwiic cable into the board

The NULA MINI has one Qwiic connector, on the edge of the board between the USER and RST buttons, marked qwiic on the silkscreen. Push the cable in until it clicks.

Qwiic cable plugged into the NULA MINI board, with the other end still loose
Step 2: the first cable in the Qwiic connector of the board, other end still free
⚠️
Do not confuse it with the smaller two-contact connector on the opposite edge, marked - and +. That one is the battery connector, and a Qwiic cable will not fit it.

If you would like to see exactly which connector this is on a bare board, it is highlighted here:

The NULA MINI board with its Qwiic connector highlighted
The Qwiic connector on the NULA MINI, highlighted in blue

3. Plug the other end into the SHTC3 sensor

The sensor is the small purple board silkscreened SHTC3 BREAKOUT. It has two Qwiic connectors, one at each end. Plug the cable into either one, and leave the other free. This is the first time in the kit that the spare connector actually gets used.

Qwiic cable running from the NULA MINI board to the SHTC3 sensor
Step 3: the first cable joining the board to the sensor

4. Chain the display onto the sensor

Take the second Qwiic cable. Plug one end into the free connector of the SHTC3, and the other into either of the two connectors on the purple I2C LCD ADAPTER on the back of the display.

The full Qwiic chain: NULA MINI on the breadboard, a cable to the SHTC3 breakout, and a second cable from the SHTC3 to the I2C LCD adapter on the back of the display
Step 4: the display chained onto the sensor with the second cable. That is the entire circuit

This is the only genuinely new connection in the project, and it is worth being clear about what it is not. The second cable does not run the display through the sensor, with the sensor passing data along. Both connectors on the SHTC3 are wired to the same four conductors: they are one junction with two sockets. Chaining simply saves you needing a board with three Qwiic connectors on it.

ℹ️
Because of that, the order does not matter. Board to display, then display to sensor, works exactly as well. So does plugging both modules into a Qwiic hub, if you have one. All four conductors end up joined either way.

5. Connect the USB-C cable

Plug the USB-C cable into the board and into your computer. The purple PWR light comes on, and both modules are powered too. Everything on the chain takes what it needs through those same two cables.

The finished build powered up before running the script: the display backlight is lit and its top row shows sixteen solid blocks
Step 5: powered up, before running the script. Backlight on, top row full of blocks

That row of solid blocks is worth a moment. It is what an HD44780 display shows when it has power but has never been initialized, the state it wakes up in before any code has spoken to it. You saw the other version of this in example 4.1, where the screen was lit but completely empty. Either appearance means the same thing: the display has power, and nothing beyond that can be concluded yet.


Two modules, one pair of wires

Every Qwiic example so far has used a single module, which meant the address printed on these boards never mattered. Here it does.

I2C is a bus. The two signal wires are shared by everything plugged into them, so when the board sends a byte, both the sensor and the display receive it. What stops the chaos is that every message begins with an address, and a module only reacts to messages carrying its own:

ModuleAddressWhere it is written
16x2 LCD, through its adapter0x20I2C ADDR 0X20, on the purple adapter
SHTC3 sensor0x70not printed, fixed inside the chip

0x20 and 0x70 are different, so the two modules share one pair of wires and neither ever answers for the other. That is the whole trick, and it is why the script hands the same i2c object to both drivers and then forgets about the chaining entirely:

i2c = I2C(0, scl=Pin(7), sda=Pin(6))

shtc3 = SHTC3(i2c)
lcd = LCD_I2C(i2c)

One connection, created once, shared by both. lcd.begin() and shtc3.begin() are then written exactly as they were in 4.1 and 5.1.

ℹ️
This is where the explicit I2C() line finally pays for itself. In an Arduino sketch each library calls Wire.begin() on its own and you have to trust that they agree; here there is visibly one bus object, and you can see both modules being handed it.
⚠️
Two modules with the same address on one bus is the situation that does not work: both answer at once and neither reading is trustworthy. That is what the A0, A1 and A2 pads on the LCD adapter are for: bridging them moves the display up to 0x27, so a second identical display could join the same cable. You will not need them here, since 0x20 and 0x70 were never going to collide.

Setting up webhook.site

The script needs somewhere on the internet to send its readings, and webhook.site provides one for free with no account.

  1. Open webhook.site in a browser. It generates a unique URL for you the moment the page loads.
  2. Copy the address shown under Your unique URL. It looks like https://webhook.site/fce7fced-1f3b-4ea1-8ed7-498130bbe714.
  3. Leave that browser tab open. Every reading your board sends will appear in it.
A fresh webhook.site page showing the unique URL and an empty inbox waiting for the first request
A fresh webhook.site page. The inbox reads 0/50 and is waiting for the first request

Then paste it into the script, replacing the placeholder text between the quotation marks, but change the https at the front to http as you do it:

webhook_url = "http://webhook.site/your-unique-id"
⚠️

Change https to http, or the project will appear to freeze. The address webhook.site shows you begins with https, and on this board an https request never finishes: the board opens the connection, starts the encrypted handshake, and then waits for it forever. There is no error and no timeout. The script simply stops at the first urequests.post() call, so you get one temperature line in the console, nothing on webhook.site, and no further readings, while the display keeps showing that first measurement.

Dropping the s sends the reading unencrypted instead, which webhook.site accepts perfectly happily, and everything then works as described below. This is a limitation of the MicroPython build on the board rather than anything to do with your wiring or your webhook page.

ℹ️
Unencrypted means anyone able to watch the traffic between your board and the server could read the temperature of your room, which is not much of a secret. It is worth knowing that the distinction exists, though: https is what protects a password or a card number, and a real product sending real data would need it to work.

While you are there, fill in your network details too:

ssid = "your ssid"
password = "your password"
⚠️
Three placeholders, and forgetting any one of them fails in a different way. A wrong password leaves the board printing dots forever with no error at all, exactly as in example 6.1. A wrong URL still connects and still returns a response code, just from somewhere that is not your page. A URL left as https stops the script dead at the first reading, as described above. Only the sensor tells you plainly when it is wrong.
ℹ️
Two limits on the free page, both visible in the screenshots. It keeps requests for 7 days, which is the 7d badge beside the URL, and it holds 50 of them, which is the 0/50 counter. At one reading every thirty seconds, fifty requests is about 25 minutes before it quietly stops recording new ones.

Stopping when the sensor is missing

There is one thing this script does that none of the earlier ones do. If the sensor does not answer, it refuses to carry on:

if not shtc3.begin():
print("SHTC3 init failed!")
lcd.print("SHTC3 error!")
while True:
time.sleep(0.1)

That last loop does nothing forever. It is a deliberate full stop: there is no point connecting to Wi-Fi and sending readings when there is nothing to read, so the script says what went wrong on both outputs and parks.

ℹ️
An empty while True with a small sleep in it is the MicroPython way of writing "go no further". The sleep matters: a completely empty loop would spin the processor at full speed for nothing.

The first thirty seconds

There is one moment in this project that looks broken and is not, so it is worth knowing about before you run it rather than after.

Once the start-up is finished, the script clears the display and drops into its loop. But a reading is only taken when time.ticks_ms() has moved UPDATE_MS past the last one, and last_update starts at zero. So the first reading always lands thirty seconds after the script starts, however quickly the rest went.

In between, the display is lit and completely blank. It looks precisely like the contrast fault from example 4.1. It is not. Wait it out, and Temp: and Hum: appear together.

ℹ️
If you would rather not wait while testing, lower UPDATE_MS to something like 5000. Put it back before leaving the board running, though: at five seconds a reading, the free webhook page fills its fifty slots in about four minutes.

The same reading, three ways

One measurement leaves the board by three different routes in this script, and each formats it differently:

print("Temperature: {:.2f} °C, Humidity: {:.2f} %".format(temperature, humidity))

lcd.print("Temp: {:.1f}\xDFC".format(temperature))

post_data = "temperature={:.2f}&humidity={:.2f}".format(temperature, humidity)

The console gets two decimal places, because there is no shortage of room. The display gets one, because a 16-character row has to hold Temp: , the number, a degree symbol and a C. The POST gets two, because those numbers are going to a computer rather than to a person, and may as well arrive at full precision.

ℹ️
{:.2f} and {:.1f} are the same format specifier with a different precision: a decimal number with this many digits after the point. It is the same mechanism as the {:02d} used for zero-padding in the alarm clock, which is worth noticing: one syntax covers both jobs.

The degree symbol

The other difference is that one line writes ° and the other writes \xDF:

print("Temperature: {:.2f} °C, ...") # console
lcd.print("Temp: {:.1f}\xDFC".format(temperature)) # display

Your computer and the HD44780 do not use the same character set. Writing ° straight to the display would produce the wrong symbol, so the script asks for character 0xDF instead, which is where the degree sign lives in the built-in font of the display.

ℹ️
\xDF inside a string means "the character with code 0xDF", which is 223 in ordinary numbers. The Arduino version of this project does the same thing by calling lcd.write(223) as a separate statement; in Python it can sit inside the string itself.
ℹ️
There is a fourth route worth noticing: during the Wi-Fi wait the script prints a dot to both outputs. In the console the dots build into a row, because print(".", end="") continues where the last one stopped. On the display they do not, because lcd.setCursor(0, 1) sends the cursor back to the start of the row every time, so you get a single dot that sits there and never moves.

Code

# I2C is what the Qwiic connector carries, and both the sensor and the display are Qwiic modules.
from machine import I2C, Pin

# The Soldered drivers for the SHTC3 sensor and the LCD display, both found in the lib folder of the examples
# repository.
from SHTC3 import SHTC3
from LCD import LCD_I2C

# The network module joins a Wi-Fi network, and urequests lets us speak HTTP.
import network
import urequests
import time

# These two variables hold the name of your Wi-Fi network (the SSID) and its password.
ssid = "your ssid"
password = "your password"

# This variable holds the address we send our readings to. Open https://webhook.site in a browser, copy the unique
# link it shows you, and paste it between the quotation marks below.
webhook_url = "your unique url"

# Here we set up the I2C connection. On the NULA MINI, I2C uses IO6 for the data line (SDA) and IO7 for the clock
# line (SCL), which are the pins the Qwiic connector is wired to. Notice that both modules share this one
# connection: that is what lets you chain several Qwiic modules together without defining any pins for them.
i2c = I2C(0, scl=Pin(7), sda=Pin(6))

# Here we create our two objects, one for the sensor and one for the display, and hand both of them the same I2C
# connection.
shtc3 = SHTC3(i2c)
lcd = LCD_I2C(i2c)

# These two variables hold the latest readings, and the two below them control how much time passes between
# readings. 30000 milliseconds is thirty seconds.
temperature = 0.0
humidity = 0.0
last_update = 0
UPDATE_MS = 30000

# Here we prepare the display. begin() starts the communication and has to come first. backlight() then turns on the
# light behind the screen, and it has to come after begin(), which would otherwise switch it back off.
lcd.begin()
lcd.backlight()
lcd.clear()
lcd.setCursor(0, 0)
lcd.print("Weather Station")
lcd.setCursor(0, 1)
lcd.print("Starting...")
time.sleep(1)
lcd.clear()

# begin() on the sensor prepares it for use and tells us whether it answered, giving back True on success and False
# on failure. If the sensor is missing there is nothing left for this project to measure, so instead of continuing we
# print the problem and stop here. The while True loop below never ends, which is a simple way of saying "go no
# further".
if not shtc3.begin():
print("SHTC3 init failed!")
lcd.print("SHTC3 error!")
while True:
time.sleep(0.1)

# Here we create our network object and switch the Wi-Fi hardware on, then start the connection attempt. connect()
# does not wait for the connection to finish, so we tell the user what is going on both in the console and on the
# display.
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
print("Connecting to Wi-Fi", end="")
lcd.setCursor(0, 0)
lcd.print("Connecting WiFi")
wlan.connect(ssid, password)

# Here we wait for the connection ourselves. isconnected() tells us whether we are online yet.
while not wlan.isconnected():
time.sleep(0.5)
print(".", end="")
lcd.setCursor(0, 1)
lcd.print(".")

# ifconfig() returns the address the router handed out to our board as its first value.
print()
print("Wi-Fi connected!")
print("IP:", wlan.ifconfig()[0])

# Let the user know we are online, then clear the display so the readings start on an empty screen.
lcd.clear()
lcd.print("WiFi Connected!")
time.sleep(0.8)
lcd.clear()

print("Smart Weather Station ready!")

while True:

# time.ticks_ms() returns the number of milliseconds passed since the board was powered on. We use it instead of
# a plain sleep so the board stays free to do other work between readings.
now = time.ticks_ms()

# Here we check how much time has passed since the last reading. Only when UPDATE_MS milliseconds have gone by do
# we take a new one, and we immediately remember the current time as the new starting point.
if time.ticks_diff(now, last_update) >= UPDATE_MS:
last_update = now

# sample() tells the sensor to perform a fresh measurement, and the two read functions then hand us the
# results. We have to call sample() first, otherwise we would keep getting the previous measurement.
shtc3.sample()
temperature = shtc3.readTemperature()
humidity = shtc3.readHumidity()

# Print the readings to the console. ":.2f" tells Python to show a number with two decimal places.
print("Temperature: {:.2f} °C, Humidity: {:.2f} %".format(temperature, humidity))

# Now we show the same readings on the display. We clear it first, because writing shorter text over longer
# text would leave leftover characters behind.
# "\xDF" is the character code the display uses for the degree symbol. The console and the LCD do not use the
# same character set, which is why we write the degree sign one way above and another way here.
lcd.clear()
lcd.setCursor(0, 0)
lcd.print("Temp: {:.1f}\xDFC".format(temperature))
lcd.setCursor(0, 1)
lcd.print("Hum: {:.1f} %".format(humidity))

# Before sending anything we check that we are still online. A Wi-Fi connection can drop at any time.
if wlan.isconnected():

# Here we build the data we are going to send. The format "name=value&name=value" is the same one a
# browser uses when you submit a simple web form: the ampersand ("&") separates one value from the next.
post_data = "temperature={:.2f}&humidity={:.2f}".format(temperature, humidity)

# We wrap the request in a try block because anything that goes over a network can fail.
try:

# post() sends the request together with our data and waits for the answer. The header we pass along
# describes the format our data is written in, so the server knows how to read it.
response = urequests.post(
webhook_url,
data=post_data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)

print("POST successful! Response code:", response.status_code)

# close() releases the memory the response was using. Always close a response once you are done.
response.close()

except Exception as e:
print("POST failed. Error:", e)

else:

# If we lost the connection, start a new attempt and try again on the next reading.
print("Wi-Fi disconnected. Trying to reconnect...")
wlan.connect(ssid, password)

# A very short pause leaves the processor a moment to handle its own background work.
time.sleep_ms(10)

What you should see

Press Run and keep your webhook.site tab open beside the editor. There are three places to watch, and they should agree with each other.

On the display

After the Weather Station / Starting... greeting, the Connecting WiFi message, and the thirty-second wait, the readings appear:

The 16x2 display showing Temp: 27.9 degrees C on the top row and Hum: 40.8 percent on the bottom row
The finished weather station: 27.9 °C and 40.8 % relative humidity

Breathe gently on the sensor from a few centimetres away and the humidity figure climbs within a second or two, but you will only see it on the next reading, up to thirty seconds later. That delay is the clearest reminder that the display is not connected to the sensor in any live sense: it is showing whatever the last trip round the loop put there.

In the console

Connecting to Wi-Fi....
Wi-Fi connected!
IP: 192.168.75.75
Smart Weather Station ready!
Temperature: 27.65 °C, Humidity: 44.13 %
POST successful! Response code: 200
Temperature: 28.20 °C, Humidity: 43.77 %
POST successful! Response code: 200

Reading down:

  • Connecting to Wi-Fi.... shows four dots, one every 500 ms, so this board took about two seconds to join the network. The row of dots is a usable stopwatch, and the count varies a little from one start to the next. Anything from about four to eight dots is normal.
  • IP: 192.168.75.75 is the address your router handed the board. It is a private address, only meaningful inside your own network.
  • Smart Weather Station ready! is the last line before the loop starts. Everything after this repeats.
  • Temperature: 27.65 °C, Humidity: 44.13 % is the reading, at two decimal places. Your own numbers will differ, of course. The console always gives two decimals where the display gives one, so a console line of 27.65 is the same measurement the screen writes as 27.6.
  • In POST successful! Response code: 200, the 200 is the HTTP code for "fine, I have it".
  • Then the same two lines again, thirty seconds later. That pair is the whole loop, and it repeats for as long as the board is left running.

On webhook.site

The webhook.site page showing two received POST requests thirty seconds apart, with the form values temperature 27.93 and humidity 40.80
Two readings arrived, thirty seconds apart. The public IP address has been painted over

This page has more to say than the console does:

  • The timestamps are 10:58:40 and 10:59:10: exactly thirty seconds apart. That is far better proof that UPDATE_MS is doing its job than anything printed on the board itself.
  • Form values lists temperature and humidity as separate named fields. They are only broken out like that because of the Content-Type header. Without it the server would receive the same characters and have no idea they were meant as a form.
  • Raw Content shows what was actually sent: temperature=27.93&humidity=40.80. That is exactly the string post_data was built from: the same two numbers the console printed on that run, joined by an &. (This screenshot is from a different run than the console output above, so the figures do not match it; on your own board they will.)
  • Size: 32 bytes is the length of that string. Count it if you like: the & and the = signs are part of the payload too.
ℹ️
The Host field is greyed out in this screenshot on purpose. It shows the public address of the router the board sits behind, not the 192.168.75.75 the board printed. Both are correct: your router replaces the private address with its own public one on the way out, a process called NAT, and the server on the far side can only ever see the router. Webhook.site puts Whois and Shodan links beside it, which is a fair reminder that a public IP is worth being careful with.
⚠️
If the console prints one reading and then nothing else, and webhook.site stays empty, your webhook_url still starts with https. Change it to http and run again. The full explanation is in the webhook.site section above. The giveaway is that the display keeps showing that first measurement quite happily: the board has not crashed, it is stuck waiting inside the request.
⚠️
If you see SHTC3 init failed! in the console and SHTC3 error! on the display, the script stops there deliberately and never reaches the Wi-Fi part. Check the Qwiic cables are pushed fully home at all four ends, then check the scl=Pin(7), sda=Pin(6) in the I2C() line.
⚠️
If the display stays blank but the console is printing readings normally, the contrast trimpot of the display is turned down. It is the small screw on the purple adapter beside the CONTRAST silkscreen. Turn it slowly with a screwdriver until the characters appear. See 4.1 LCD Message Display for the full description of this fault, but remember to rule out the thirty-second wait first.

Where to take it next

Everything in this project is one variable away from being something else:

  • Change UPDATE_MS and you change how often the room is sampled.
  • Swap the two lcd.print lines and the display shows humidity first.
  • Add a third name and value to post_data and the webhook page grows a third form field.

The one thing you cannot do from here is read the data back: webhook.site shows you requests as they arrive, but it is not a place to keep them. Sending the same POST to a spreadsheet service or a database instead is the natural next step, and nothing about the board or the wiring would have to change.


Full example

Check out the full example code on the link below:

7.1_Smart_Weather_Station.py

Project that measures temperature and humidity with the SHTC3 sensor, displays the readings on a 16x2 LCD, and sends them to webhook.site every 30 seconds.