Skip to content
Portfolio: <Bantonis/>
HomeProfileInternshipProjects
Back to projects

Pico Door Alarm

Summary

A Pico W/H door alarm prototype that evolved from a breadboard proof of concept into a soldered, 3D-printed MicroPython build with local feedback and Telegram alerts through a REST endpoint.

Role
Embedded / Home Automation Developer
Timeline
Personal project: 2024 — 2025
Tags
MicroPythonPico W/HRESTIoT

Preview

First breadboard prototype with Raspberry Pi Pico W/H, buzzer, LEDs and an early distance-sensor setup.
Second prototype on a soldered board, next to the magnetic contact sensor used for door state detection.
Working USB-powered prototype mounted near the door, with local buttons, buzzer and LED feedback.
Final electronics layout with a separate power board and the Pico control board before enclosure assembly.
FreeCAD enclosure design with space for the electronics, ventilation, cable routing and mounting points.
3D-printed enclosure parts during production on a Creality printer.
Final mounted prototype with 3D-printed housing and visible control board beside the magnetic contact sensor.
Telegram alert flow from the Home Telegram Bot, showing selected door alarm and safety-mode messages.
PreviousNext

Overview

  • The project combines a Raspberry Pi Pico W/H, MicroPython firmware, a magnetic contact sensor, LEDs, a buzzer and REST-based Telegram notifications.
  • The build evolved through multiple iterations: breadboard prototype, soldered board, improved power setup and 3D-printed enclosure.
  • The final version provides both local status feedback and remote alert messages through the Home Telegram Bot.

My contribution

  • Built the Pico W/H firmware for sensor input, mode handling, LEDs and buzzer feedback.
  • Implemented WiFi connectivity and REST calls for Telegram alert delivery.
  • Reworked the hardware from a breadboard setup to a soldered prototype board.
  • Designed and tested a 3D-printed enclosure for the final mounted prototype.

Outcome

  • Working door alarm prototype with local LED and buzzer feedback.
  • Stable WiFi notifications through the Home Telegram Bot REST API endpoint.
  • Mounted prototype with soldered electronics, separate power board and 3D-printed housing.

Public demo scope

The public portfolio intentionally limits the technical detail shown for this alarm project. Full source code, exact wiring, timing values, internal mode logic and the complete operating sequence are not published. The demo focuses on the concept, architecture, build evolution, sanitized code excerpts and selected media, while the full technical explanation can be discussed privately during the jury moment.

Goal

  • Detect door activity with a small embedded setup.
  • Provide local visual and audio feedback for alarm and status states.
  • Send selected notifications through the Home Telegram Bot REST API endpoint.
  • Create a more robust physical prototype with soldered electronics and a 3D-printed enclosure.

Approach

  • Built v1 around September 2024 as a breadboard proof of concept with distance-based detection and Telegram alerts.
  • Reworked v2 around November 2024 with a magnetic contact sensor and a soldered prototype board.
  • Moved from CircuitPython to MicroPython after reliability issues with the first WiFi implementation on my Omada network.
  • Designed the enclosure in FreeCAD around December 2024 and refined the case, wiring and firmware by May 2025.

Artifacts

  • Public code snippets for mode timing, feedback patterns and rate-limited REST notifications.
  • Photos showing the evolution from breadboard prototype to soldered board, power board and mounted enclosure.
  • Telegram message screenshot showing selected alert delivery through the Home Telegram Bot.

Code examples/ Source Code

Mode orchestration excerpt

main.py

py / 97 lines

Sanitized firmware excerpt showing how alarm, status and feedback modes are coordinated.

"""Public excerpt from the Pico door alarm firmware.

The full operating sequence, exact timings and wiring details are omitted from
the portfolio. This excerpt shows the parts that are useful to discuss: mode
priority, local feedback and rate-limited REST notifications.
"""

import gc
import time

from lib.buzzer import AlarmBuzzer
from lib.connection import ApiClient
from lib.led import BlinkLed, PauseLed
from lib.mode import Mode
from lib.switch import Switch


door_contact = Switch("DOOR_CONTACT_PIN")
green_led = PauseLed("GREEN_LED_PIN", blink_time=0.1, pause_time=1.5)
blue_led = PauseLed("BLUE_LED_PIN", blink_time=0.2, pause_time=1.0)
red_led = BlinkLed("RED_LED_PIN", blink_time=0.08)
buzzer = AlarmBuzzer("BUZZER_PIN")
api_client = ApiClient()

sleep_mode = Mode("sleep")
check_mode = Mode("check")
safety_mode = Mode("safety", duration=20.0)
alarm_mode = Mode("alarm", init_duration=5.0)

alarm_was_active = False
loop_count = 0


def update_modes(sleep_enabled: bool, safety_button_pressed: bool) -> None:
    sleep_mode.set_active(sleep_enabled)

    if sleep_mode.is_active():
        check_mode.set_active(False)
        safety_mode.set_active(False)
        alarm_mode.cancel_pending()
        return

    if safety_button_pressed:
        safety_mode.start()
        alarm_mode.cancel_pending()

    safety_mode.update()
    check_mode.set_active(not safety_mode.is_active() and not alarm_mode.is_active())

    if safety_mode.is_active():
        alarm_mode.cancel_pending()
    elif door_contact.is_opened() and not alarm_mode.is_init_active():
        alarm_mode.start_pending()

    alarm_mode.update_init()


def update_feedback(silent_enabled: bool) -> None:
    green_led.paused_blink() if check_mode.is_active() else green_led.on(False)
    blue_led.on(safety_mode.is_active())
    red_led.blink() if alarm_mode.is_active() else red_led.on(False)

    if silent_enabled:
        buzzer.stop_alarm()
    elif alarm_mode.is_active():
        buzzer.start_or_update_alarm()
    else:
        buzzer.stop_alarm()


def send_notifications() -> None:
    global alarm_was_active

    if alarm_mode.is_active():
        alarm_was_active = True
        api_client.timed_post("Door alarm state changed", "DANGER")

    if safety_mode.is_active() and alarm_was_active:
        alarm_was_active = False
        api_client.post("Door alarm acknowledged by safety mode", "INFO")


def loop_once(inputs: dict[str, bool]) -> None:
    global loop_count

    loop_count += 1
    if loop_count >= 100000:
        loop_count = 0
        gc.collect()

    update_modes(
        sleep_enabled=inputs["sleep_enabled"],
        safety_button_pressed=inputs["safety_button_pressed"],
    )
    update_feedback(silent_enabled=inputs["silent_enabled"])
    send_notifications()
    time.sleep(0.005)

Timed mode helper

mode.py

py / 59 lines

Reusable MicroPython state helper for active and pending modes with tick-based timers.

import time


class Mode:
    """Small MicroPython state helper with optional active and pending timers."""

    def __init__(self, name, duration=None, init_duration=None):
        self._name = name
        self._active = False
        self._init_active = False
        self._time = time.ticks_ms() if (duration is not None or init_duration is not None) else None
        self._dur_ms = int(duration * 1000) if duration is not None else None
        self._init_dur_ms = int(init_duration * 1000) if init_duration is not None else None

    def info(self):
        return self._name

    def is_active(self):
        return self._active

    def is_init_active(self):
        return self._init_active

    def set_active(self, state):
        self._active = state

    def set_init_active(self, state):
        self._init_active = state

    def start(self):
        self.reset_time()
        self.set_active(True)

    def start_pending(self):
        self.reset_time()
        self.set_init_active(True)

    def cancel_pending(self):
        self.set_init_active(False)
        self.set_active(False)

    def reset_time(self):
        self._time = time.ticks_ms()

    def update(self):
        if self._dur_ms is not None and self._time is not None:
            now = time.ticks_ms()
            if time.ticks_diff(now, self._time) >= self._dur_ms:
                self._time = now
                self._active = False

    def update_init(self):
        if self._init_dur_ms is not None and self._time is not None:
            now = time.ticks_ms()
            if time.ticks_diff(now, self._time) >= self._init_dur_ms:
                self._time = now
                if self._init_active:
                    self._active = True
                    self._init_active = False

WiFi and REST client

connection.py

py / 114 lines

Sanitized client code for WiFi connection handling, reconnect checks and rate-limited alert posts.

import time
import network
import usocket as socket
import urequests as requests
import ujson as json
import gc

import settings


class ConnectionException(Exception):
    pass


class WifiClient:
    def __init__(self, hostname: str = "") -> None:
        self.wlan = network.WLAN(network.STA_IF)
        self._hostname: str = hostname if hostname else settings.WIFI_HOSTNAME

    def _apply_hostname(self) -> None:
        if not self._hostname:
            return
        try:
            network.hostname(self._hostname)
            return
        except Exception:
            pass

        try:
            self.wlan.config(hostname=self._hostname)
            return
        except Exception:
            pass

        try:
            self.wlan.config(dhcp_hostname=self._hostname)
            return
        except Exception:
            print("Hostname config not supported on this firmware.")

    def connect(self, timeout: int = 20) -> None:
        self.wlan.active(False)
        self._apply_hostname()
        self.wlan.active(True)
        self.wlan.connect(settings.WIFI_SSID, settings.WIFI_PASSWORD)
        start = time.ticks_ms()
        while time.ticks_diff(time.ticks_ms(), start) < timeout * 1000:
            if self.wlan.isconnected():
                ip = self.wlan.ifconfig()[0]
                print(f"Wi-Fi connected (hostname={self._hostname}), IP: {ip}")
                return
            time.sleep(1)
        raise ConnectionException("Wi-Fi connection FAILED")


class ApiClient:
    def __init__(self) -> None:
        self.wifi = WifiClient()
        self.wifi.connect()
        protocol = settings.API_PROTOCOL if hasattr(settings, "API_PROTOCOL") else "https"
        self._base = f"{protocol}://{settings.API_URL}:{settings.API_PORT}{settings.API_SUFFIX}"
        self._send_ms = int(settings.SEND_FREQUENCY * 1000)
        self._last_ms = time.ticks_ms()
        self._sent = False

    def check_connection(self, timeout: float = 1.0) -> bool:
        try:
            addr = socket.getaddrinfo(settings.API_URL, settings.API_PORT)[0][-1]
            s = socket.socket()
            s.settimeout(timeout)
            s.connect(addr)
            s.close()
            return True
        except Exception as e:
            print("Socket check failed:", e)
            return False

    def post(self, text: str, message_type: str) -> None:
        if not self.check_connection():
            print("Re-connecting Wi-Fi...")
            try:
                self.wifi.connect()
            except Exception as ex:
                print("Connecting to Wi-Fi unsuccessful:", ex)

        payload: dict = {"text": text, "type": message_type}
        body = json.dumps(payload)
        headers = {"Content-Type": "application/json"}

        try:
            response = requests.post(self._base, data=body, headers=headers)
            print("POST status:", response.status_code)
            response.close()
            gc.collect()
            if response.status_code >= 400:
                try:
                    err = response.json()
                except Exception:
                    err = "No exception message..."
                raise ConnectionException(
                    f"Server error {response.status_code}: {err}"
                )
        except Exception as e:
            print("POST failed:", e)

    def timed_post(self, text: str, message_type: str) -> None:
        now = time.ticks_ms()

        if not self._sent:
            self._last_ms = now
            self._sent = True
            self.post(text, message_type)
        elif time.ticks_diff(now, self._last_ms) >= self._send_ms:
            self._sent = False
Technology stack
Raspberry Pi Pico W/HMicroPythonCircuitPythonWiFiRESTMagnetic SwitchLEDsBuzzerFreeCAD3D PrintingHome Telegram Bot
© 2026 Bryan Antonis — Built with Next.js + React-Bootstrap + AI assistance for styling and insight