Logging Sensor Data to Cloud PostgreSQL Database with Raspberry Pi Pico W

Introduction

This project utilizes a Raspberry Pi Pico W to continuously monitor and log internal temperature readings into a PostgreSQL database over a Wi-Fi connection. The setup involves the following key components and functionalities:

Key Features

  • Wi-Fi Connectivity:

    • The Raspberry Pi Pico W connects to a specified Wi-Fi network using the provided SSID and password, allowing it to communicate with remote servers.
  • PostgreSQL Database Connection:

    • The device establishes a connection to a PostgreSQL database hosted on a remote server, handling any connection errors gracefully while providing feedback on the connection status.
  • Temperature Reading:

    • The internal temperature of the Raspberry Pi Pico W is read using the on-board ADC (Analog-to-Digital Converter). The raw ADC value is converted into a Celsius temperature value using a simple calibration formula.
  • Data Logging:

    • Temperature readings are inserted into a database table named sensor_data every 10 seconds. The system checks for any errors during data insertion and reports them.
  • Continuous Monitoring:

    • The program runs in an infinite loop, continuously reading the temperature and logging the data until manually interrupted by the user.

Compatibility with Other Microcontrollers

This project can also be adapted to work with ESP32 and ESP8266 microcontrollers:

  • Wi-Fi Connectivity: Both the ESP32 and ESP8266 have built-in Wi-Fi capabilities, allowing for similar Wi-Fi connections.
  • Database Connection: The micropg_lite library is compatible with ESP32 and ESP8266, allowing for PostgreSQL database connections.
  • Temperature Reading: The ESP32 can use its internal sensor, while the ESP8266 can interface with external temperature sensors (e.g., DHT11, DS18B20).

Conclusion

This project serves as a practical example of IoT (Internet of Things) data logging, showcasing how microcontrollers can be integrated with cloud-based databases for real-time data monitoring and analysis. It can be further expanded with features such as data visualization, alerting systems, or additional sensors for comprehensive environmental monitoring.

Create Table Query if Table do not Exists in PostgreSQL DB

				
					CREATE TABLE IF NOT EXISTS sensor_data ( id SERIAL PRIMARY KEY, temperature FLOAT NOT NULL, recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )
				
			

main.py

				
					import network  # Handles the Wi-Fi connection
import micropg_lite
import machine
import time

### Updated Wi-Fi connection data
ssid = 'WIFI_SSID'  # Replace with your Wi-Fi SSID
password = 'WIFI_PASSWORD'  # Replace with your Wi-Fi password

# Connect to the network
print("Connecting to Wi-Fi...")
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)

# Wait for connection
while not wlan.isconnected():
    time.sleep(1)

print("Wi-Fi connected:", wlan.ifconfig())

# Connect to PostgreSQL
print("Connecting to PostgreSQL database...")
try:
    conn = micropg_lite.connect(
        host='52.90.199.127',  # Replace with your server IP address
        user='myuser',  # Replace with your username
        password='mypassword',  # Replace with your password
        database='mydatabase'
    )
    print("Database connection successful.")
except Exception as e:
    print("Database connection failed:", e)
    raise

cur = conn.cursor()

# Function to read internal temperature
def read_internal_temperature():
    print("Reading internal temperature...")
    adc = machine.ADC(4)  # Internal temperature sensor is usually on GPIO 4
    raw_value = adc.read_u16()  # Read raw value (0-65535)
    
    # Convert raw value to temperature in Celsius
    temperature = (raw_value / 65535) * 3.3  # Convert to voltage (0 to 3.3V)
    temperature = (temperature - 0.5) * 100  # Convert to Celsius (0.5V corresponds to 0°C)
    
    print(f"Raw value: {raw_value}, Temperature: {temperature:.2f}°C")
    return round(temperature, 2)

# Loop to insert temperature data every 10 seconds
try:
    while True:
        # Record the start time of the loop
        start_time = time.time()
        
        # Read internal temperature
        temperature = read_internal_temperature()
        
        # Insert temperature into the sensor_data table
        print("Inserting temperature data into the database...")
        try:
            # Convert temperature to string before inserting
            cur.execute('INSERT INTO sensor_data (temperature) VALUES (%s)', (str(temperature),))
            conn.commit()
            print(f"Inserted temperature: {temperature:.2f}°C")
        except Exception as e:
            print("Error inserting data:", e)

        # Calculate how long the operations took
        elapsed_time = time.time() - start_time

        # Sleep for the remaining time to ensure the loop runs every 10 seconds
        sleep_time = 10 - elapsed_time
        if sleep_time > 0:
            time.sleep(sleep_time)

except KeyboardInterrupt:
    print("Program stopped by user.")
finally:
    conn.close()
    print("Connection closed.")


				
			
				
					#############################################################################
# The MIT License (MIT)
#
# Copyright (c) 2014-2019, 2021-2024 Hajime Nakagami (micropg)
# Copyright (c) 2023-2025 TimonW-Dev, BetaFloof, MikeRoth93 (micropg_lite based on micropg)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
##############################################################################
# PostgreSQL driver for micropython https://github.com/micropython/micropython but it's more lightweight and made for ESP8266
# It's a micropg (https://github.com/nakagami/micropg) subset.
# micropg (https://github.com/nakagami/micropg) a minipg (https://github.com/nakagami/minipg) subset.
##############################################################################

### Version 3.1.0

import ssl, hashlib, socket, binascii, random

# -----------------------------------------------------------------------------

def raiseExceptionLostConnection():
    raise Exception("08003:Lost connection")

def hmac_sha256_digest(key, msg):
    pad_key = key + b'\x00' * (64 - len(key) % 64)
    return hashlib.sha256(bytes(0x5c ^ b for b in pad_key) + hashlib.sha256(bytes(0x36 ^ b for b in pad_key) + msg).digest()).digest()

class Cursor:
    def __init__(self, connection):
        self.connection = connection
        
    def execute(self, q, a=()):
        if not self.connection or not bool(self.connection.sock): raiseExceptionLostConnection()
        self._rows = []
        if a: q = q.replace('%', '%%').replace('%%s', '%s') % tuple(('NULL' if i is None else "'" + i.replace("'", "''") + "'" if isinstance(i, str) else "'" + ''.join(['\\%03o' % c for c in i]) + "'" for i in a))
        self.connection.execute(q, self)

    def fetchall(self):
        rows = self._rows
        self._rows = []
        return rows

    def close(self):
        self.connection = None

class connect:
    def __init__(self, host, user, password, database, port=5432, use_ssl=False):
        self.user = user
        self.password = password
        self.database = database
        self.host = host
        self.port = port
        self.use_ssl = use_ssl
        self.encoding = 'UTF8'
        self.autocommit = False
        self._ready_for_query = b'I'
        
        # Inlined _open() function
        self.sock = socket.socket()
        self.sock.connect(socket.getaddrinfo(self.host, self.port)[0][-1])
        if self.use_ssl:
            self._write((8).to_bytes(4, 'big') + (80877103).to_bytes(4, 'big'))
            if self._read(1) == b'S': self.sock = ssl.wrap_socket(self.sock)
            else: raiseExceptionLostConnection()
        v = b'\x00\x03\x00\x00user\x00' + self.user.encode('ascii') + b'\x00'
        if self.database: v += b'database\x00' + self.database.encode('ascii') + b'\x00'
        v += b'\x00'
        self._write((len(v) + 4).to_bytes(4, 'big') + v)
        self._process_messages(None)

    def _send_message(self, message, data):
        self._write(b''.join([message, (len(data) + 4).to_bytes(4, 'big'), data, b'H\x00\x00\x00\x04']))

    def _process_messages(self, obj):
        while True:
            try: code = ord(self._read(1))
            except: raiseExceptionLostConnection()
            data = self._read(int.from_bytes(self._read(4), 'big') - 4)
            if code == 90: 
                self._ready_for_query = data
                break
            elif code == 82:
                nonce = str(random.getrandbits(32))
                first = f'n,,n=,r={nonce}'.encode('utf-8')
                msg = b'SCRAM-SHA-256\x00' + (len(first)).to_bytes(4, 'big') + first
                self._write(b'p' + (len(msg) + 4).to_bytes(4, 'big') + msg)
                assert ord(self._read(1)) == 82
                data = self._read(int.from_bytes(self._read(4), 'big') - 4)
                server = dict(kv.split('=', 1) for kv in data[4:].decode('utf-8').split(','))
                pw_bytes = self.password.encode('utf-8')
                iters = int(server['i'])
                u1 = hmac_sha256_digest(pw_bytes, binascii.a2b_base64(server['s']) + b'\x00\x00\x00\x01')
                ui = int.from_bytes(u1, 'big')
                for _ in range(iters - 1):
                    u1 = hmac_sha256_digest(pw_bytes, u1)
                    ui ^= int.from_bytes(u1, 'big') 
                client_key = hmac_sha256_digest(ui.to_bytes(32, 'big'), b"Client Key")
                auth_msg = f"n=,r={nonce},r={server['r']},s={server['s']},i={server['i']},c=biws,r={server['r']}"
                proof = binascii.b2a_base64(bytes(x ^ y for x, y in zip(client_key, hmac_sha256_digest(hashlib.sha256(client_key).digest(), auth_msg.encode('utf-8'))))).rstrip(b'\n')
                final = f"c=biws,r={server['r']},p={proof.decode('utf-8')}".encode('utf-8')
                self._write(b'p' + (len(final) + 4).to_bytes(4, 'big') + final)
                for _ in range(3):
                    assert ord(self._read(1)) == 82
                    data = self._read(int.from_bytes(self._read(4), 'big') - 4)
                    if int.from_bytes(data[:4], 'big') == 0: break
            elif code == 67 and obj:
                parts = data[:-1].decode('ascii').split()
                if parts and parts[-1].isdigit(): obj._rowcount = int(parts[-1])
            elif code == 84 and obj:
                count = int.from_bytes(data[:2], 'big')
                obj.description = [None] * count
                n = 2
                for i in range(count):
                    name_end = data.index(b'\x00', n)
                    name = data[n:name_end]
                    n = name_end + 1
                    try: name = name.decode(self.encoding)
                    except: pass
                    type_code = int.from_bytes(data[n+6:n+10], 'big')
                    size, precision, scale = int.from_bytes(data[n+10:n+12], 'big'), -1, -1
                    obj.description[i] = (name, type_code, None, size, precision, scale, None)
                    n += 18
            elif code == 68 and obj:
                n, row = 2, []
                while n < len(data):
                    if data[n:n+4] == b'\xff\xff\xff\xff': row.append(None); n += 4
                    else:
                        ln = int.from_bytes(data[n:n+4], 'big')
                        col_data = data[n+4:n+4+ln]
                        col_oid = obj.description[len(row)][1]
                        decoded_data = col_data.decode(self.encoding) if col_data else None
                        if col_oid == 16: decoded_data = (decoded_data == 't')
                        elif col_oid in (21, 23, 20, 26): decoded_data = int(decoded_data)
                        elif col_oid in (700, 701): decoded_data = float(decoded_data)
                        row.append(decoded_data)
                        n += ln + 4
                obj._rows.append(tuple(row))
            elif code == 69: raiseExceptionLostConnection()
            elif code == 100: obj.write(data)
            elif code == 71:
                while True:
                    buf = obj.read(8192)
                    if not buf: break
                    self._write(b'd' + (len(buf) + 4).to_bytes(4, 'big') + buf)
                self._write(b'c\x00\x00\x00\x04S\x00\x00\x00\x04')
                
    def _read(self, ln):
        if not self.sock: raiseExceptionLostConnection()
        r = bytearray(ln)
        pos = 0
        while pos < ln:
            chunk = self.sock.read(ln - pos) if hasattr(self.sock, "read") else self.sock.recv(ln - pos)
            if not chunk: raiseExceptionLostConnection()
            r[pos:pos+len(chunk)] = chunk
            pos += len(chunk)
        return bytes(r)

    def _write(self, b):
        if not self.sock:
            raiseExceptionLostConnection()
        pos = 0
        while pos < len(b):
            pos += self.sock.write(b[pos:]) if hasattr(self.sock, "write") else self.sock.send(b[pos:])

    def cursor(self):
        return Cursor(self)

    def execute(self, query, obj=None):
        if self._ready_for_query != b'T':
            self.begin()
        self._send_message(b'Q', query.encode(self.encoding) + b'\x00')
        self._process_messages(obj)
        if self.autocommit:
            self.commit()

    def begin(self):
        if self._ready_for_query == b'E':
            self._rollback()
        self._send_message(b'Q', b"BEGIN\x00")
        self._process_messages(None)

    def commit(self):
        if self.sock:
            self._send_message(b'Q', b"COMMIT\x00")
            self._process_messages(None)
            self.begin()

    def _rollback(self):
        if self.sock:
            self._send_message(b'Q', b"ROLLBACK\x00")
            self._process_messages(None)

    def rollback(self):
        self._rollback()
        self.begin()

    def close(self):
        if self.sock:
            self._write(b'X\x00\x00\x00\x04')
            self.sock.close()
            self.sock = None

def create_database(host, user, password, database, port=5432, use_ssl=False):
    conn = connect(host, user, password, None, port, use_ssl)
    conn._send_message(b'Q', 'CREATE DATABASE {}'.format(database).encode('utf-8') + b'\x00')
    conn.close()

def drop_database(host, user, password, database, port=5432, use_ssl=False):
    conn = connect(host, user, password, None, port, use_ssl)
    conn._send_message(b'Q', 'DROP DATABASE {}'.format(database).encode('utf-8') + b'\x00')
    conn.close()