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
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.

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.

- 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:

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.

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.

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.
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.

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:
| Module | Address | Where it is written |
|---|---|---|
| 16x2 LCD, through its adapter | 0x20 | I2C ADDR 0X20, on the purple adapter |
| SHTC3 sensor | 0x70 | not 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.
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.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.
- Open webhook.site in a browser. It generates a unique URL for you the moment the page loads.
- Copy the address shown under Your unique URL. It looks like
https://webhook.site/fce7fced-1f3b-4ea1-8ed7-498130bbe714. - Leave that browser tab open. Every reading your board sends will appear in it.

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.
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"
https stops the script dead at the first reading, as described above. Only the sensor tells you plainly when it is wrong.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.
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.
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.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:

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.75is 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 of27.65is the same measurement the screen writes as27.6.- In
POST successful! Response code: 200, the200is 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

This page has more to say than the console does:
- The timestamps are
10:58:40and10:59:10: exactly thirty seconds apart. That is far better proof thatUPDATE_MSis doing its job than anything printed on the board itself. Form valuesliststemperatureandhumidityas separate named fields. They are only broken out like that because of theContent-Typeheader. Without it the server would receive the same characters and have no idea they were meant as a form.Raw Contentshows what was actually sent:temperature=27.93&humidity=40.80. That is exactly the stringpost_datawas 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 bytesis the length of that string. Count it if you like: the&and the=signs are part of the payload too.
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.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.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.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_MSand you change how often the room is sampled. - Swap the two
lcd.printlines and the display shows humidity first. - Add a third name and value to
post_dataand 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.