A distributed IoT system for bathroom device management using custom mDNS-based service discovery, gRPC communication, and real-time device monitoring. Built as a microservices architecture demonstrating device registration, health monitoring, and control through a modern web interface.
- Architecture Overview
- Technology Stack
- Project Structure
- Communication Protocols
- Device Types
- Getting Started
- Development
- API Documentation
Badezimmer implements a microservices-based IoT platform where devices self-register through a custom mDNS protocol, communicate via TCP sockets for health checks, and expose gRPC-over-HTTP APIs for frontend control.
graph TB
subgraph "Frontend Layer"
FE[Next.js Frontend<br/>Port 3000]
end
subgraph "Gateway Layer"
GW[Python Gateway<br/>Port 8000<br/>FastAPI + gRPC]
end
subgraph "Service Discovery"
MDNS[mDNS Multicast<br/>224.0.0.251:5369<br/>Custom Protocol]
end
subgraph "Device Layer - Python"
LL[Light Lamp<br/>Actuator]
FD[Fart Detector<br/>Sensor]
TL[Toilet<br/>Sensor]
SK[Sink<br/>Actuator]
end
subgraph "Device Layer - Go"
WL[Water Leak<br/>Sensor]
end
FE -->|gRPC-Web<br/>HTTP/1.1| GW
GW -->|TCP Socket<br/>Protobuf| LL
GW -->|TCP Socket<br/>Protobuf| FD
GW -->|TCP Socket<br/>Protobuf| TL
GW -->|TCP Socket<br/>Protobuf| SK
GW -->|TCP Socket<br/>Protobuf| WL
LL -.->|Register/Update<br/>UDP Multicast| MDNS
FD -.->|Register/Update<br/>UDP Multicast| MDNS
TL -.->|Register/Update<br/>UDP Multicast| MDNS
SK -.->|Register/Update<br/>UDP Multicast| MDNS
WL -.->|Register/Update<br/>UDP Multicast| MDNS
GW -.->|Listen/Query<br/>UDP Multicast| MDNS
GW -->|TCP Health Check<br/>Port Probe| LL
GW -->|TCP Health Check<br/>Port Probe| FD
GW -->|TCP Health Check<br/>Port Probe| TL
GW -->|TCP Health Check<br/>Port Probe| SK
GW -->|TCP Health Check<br/>Port Probe| WL
style MDNS fill:#f9f,stroke:#333,stroke-width:2px
style GW fill:#bbf,stroke:#333,stroke-width:2px
style FE fill:#bfb,stroke:#333,stroke-width:2px
sequenceDiagram
participant Device
participant mDNS
participant Gateway
participant Frontend
Note over Device,mDNS: 1. Service Discovery Phase
Device->>mDNS: Register Service<br/>(UDP 224.0.0.251:5369)
Device->>mDNS: Broadcast Service Info<br/>(Name, Type, Port, Properties)
Gateway->>mDNS: Listen for Services
mDNS->>Gateway: Service Announcement
Note over Gateway,Device: 2. Health Check Phase
Gateway->>Device: TCP Health Check<br/>(Connect to device port)
Device->>Gateway: Connection Accepted
Gateway->>Gateway: Mark Device as ONLINE
Note over Frontend,Device: 3. Control Flow
Frontend->>Gateway: gRPC-Web Request<br/>(List Devices / Send Command)
Gateway->>Gateway: Lookup Device by ID
Gateway->>Device: TCP Request<br/>(Protobuf-encoded command)
Device->>Device: Process Command<br/>(Update state)
Device->>Gateway: TCP Response<br/>(Protobuf-encoded result)
Device->>mDNS: Update Service<br/>(Broadcast new properties)
Gateway->>Frontend: gRPC Response
Frontend->>Frontend: Update UI
Note over Device,mDNS: 4. Periodic Renovation
loop Every 60s
Device->>mDNS: Re-broadcast Service<br/>(TTL renewal)
end
Note over Gateway,Device: 5. Cleanup Phase
loop Every 60s
Gateway->>Device: TCP Health Check
alt Device Offline
Gateway->>Gateway: Mark Device as OFFLINE
end
Gateway->>Gateway: Remove Expired Entries<br/>(TTL exceeded)
end
-
Python 3.12+: Core device services and gateway
fastapi: REST and gRPC-Web gateway servergrpcio: Protocol Buffers and gRPC implementationzeroconf: mDNS service discovery foundationasyncudp: Async UDP socket handling for multicastasyncio: Async I/O for concurrent operationsuv: Fast Python package manager and project manager
-
Go 1.21+: High-performance water leak sensor
- Custom mDNS implementation
- Native protobuf support
- Next.js 14+: React-based UI framework
- TypeScript: Type-safe frontend development
- gRPC-Web: Browser-compatible gRPC client
- Radix UI: Accessible component primitives
- Tailwind CSS: Utility-first styling
- Protocol Buffers: Schema-defined serialization (proto3)
- Docker & Docker Compose: Containerization and orchestration
- Chainguard Images: Minimal, secure container base images
badezimmer/
├── proto/ # Protocol Buffers definitions
│ └── badezimmer.proto # Service, message, and enum schemas
│
├── src/ # Python source code
│ ├── badezimmer/ # Shared library package
│ │ ├── mdns.py # Custom mDNS protocol implementation
│ │ ├── tcp.py # TCP socket utilities and protobuf helpers
│ │ ├── browser.py # Service browser for discovery
│ │ ├── info.py # Service info and cache management
│ │ ├── logger.py # Structured logging setup
│ │ ├── badezimmer_pb2.py # Generated protobuf messages
│ │ └── badezimmer_pb2_grpc.py # Generated gRPC service stubs
│ │
│ ├── gateway/ # Central gateway service
│ │ └── __init__.py # FastAPI app, gRPC server, device management
│ │
│ ├── lightlamp/ # Light actuator device
│ │ └── __init__.py # Lamp control logic, TCP server
│ │
│ ├── fartdetector/ # Fart detection sensor
│ │ └── __init__.py # Sensor logic and periodic updates
│ │
│ ├── toilet/ # Toilet sensor device
│ │ └── __init__.py # Flush detection and monitoring
│ │
│ └── sink/ # Sink actuator device
│ └── __init__.py # Water flow control
│
├── go-water-leak/ # Go-based water leak sensor
│ ├── main.go # Main sensor application
│ ├── mdns.go # mDNS implementation in Go
│ ├── badezimmer/ # Generated Go protobuf code
│ │ └── badezimmer.pb.go
│ └── go.mod # Go module dependencies
│
├── badezimmer-home-page/ # Next.js frontend application
│ ├── app/ # Next.js app router
│ │ ├── page.tsx # Home page with device grid
│ │ └── layout.tsx # Root layout
│ │
│ ├── components/ # React components
│ │ ├── device-card.tsx # Individual device display
│ │ ├── device-controls.tsx # Control panel for devices
│ │ ├── device-grid.tsx # Device list grid
│ │ └── ui/ # Radix UI component library
│ │
│ ├── lib/ # Utility libraries
│ │ ├── grpc-client.ts # gRPC-Web client setup
│ │ ├── device-types.ts # TypeScript type definitions
│ │ └── utils.ts # Helper functions
│ │
│ ├── generated/ # Generated gRPC-Web stubs
│ │ ├── badezimmer_grpc_web_pb.js
│ │ └── badezimmer_pb.js
│ │
│ └── package.json # Frontend dependencies
│
├── docker-compose.yaml # Multi-service orchestration
├── Dockerfile # Python services image
├── Dockerfile.front # Frontend image
├── Dockerfile.water-leak # Go water leak sensor image
├── pyproject.toml # Python project configuration
└── README.md # This file
The system implements a custom multicast DNS protocol for zero-configuration device discovery on the local network.
- Multicast Group:
224.0.0.251:5369 - Transport: UDP multicast
- Encoding: Protocol Buffers (defined in
badezimmer.proto)
message MDNS {
fixed32 transaction_id = 1;
google.protobuf.Timestamp timestamp = 2;
oneof data {
MDNSQueryRequest query_request = 3;
MDNSQueryResponse query_response = 4;
}
}sequenceDiagram
participant Device
participant Network
participant Gateway
Note over Device: Device Starts
Device->>Device: Generate Service Info<br/>(Name, Type, Port, Properties)
Note over Device,Network: Tiebreaking Phase
loop 3 attempts with 100ms intervals
Device->>Network: Multicast Query<br/>"Is name taken?"
Network-->>Device: Responses (if any)
alt Name Conflict
Device->>Device: Randomize name suffix
end
end
Note over Device,Network: Registration Phase
Device->>Network: Multicast Announcement<br/>PTR + SRV + TXT + A records
Gateway->>Gateway: Listening on multicast
Network->>Gateway: Service Announcement
Gateway->>Gateway: Cache service info<br/>(60s TTL)
Gateway->>Gateway: Add to device registry
Note over Device,Network: Maintenance Phase
loop Every 60s
Device->>Network: Re-announce service<br/>(TTL renewal)
end
The mDNS implementation uses four DNS record types:
-
PTR (Pointer) Record: Maps service type to instance name
_lightlamp._tcp.local. → "Light Lamp._lightlamp._tcp.local." -
SRV (Service) Record: Provides port and target host
Service: _lightlamp._tcp.local. Port: 8080 Target: light-lamp-host.local. -
TXT (Text) Record: Key-value properties
is_on=true brightness=75 color=0xFF5733 -
A (Address) Record: IPv4 addresses
light-lamp-host.local. → 192.168.1.100
- Tiebreaking: Devices probe the network 3 times before registering to avoid name conflicts
- Cache Flush: Records include cache-flush flag for immediate updates
- TTL Management: 60-second default TTL with automatic renewal
- Health Monitoring: Gateway performs TCP health checks every 60 seconds
- Goodbye Packets: Devices send TTL=0 announcements when shutting down
All device-to-gateway communication uses TCP sockets with length-prefixed Protobuf messages.
┌─────────────────┬──────────────────────────┐
│ Length (4 bytes) │ Protobuf Message │
│ Big-Endian │ (variable length) │
└─────────────────┴──────────────────────────┘
def prepare_protobuf_request(message: message.Message) -> bytes:
"""Prepares a Protobuf message for TCP transmission"""
serialized_message = message.SerializeToString()
message_length = len(serialized_message)
length_prefix = message_length.to_bytes(4, byteorder="big")
return length_prefix + serialized_message
def get_protobuf_data(data: bytes) -> bytes:
"""Extracts Protobuf message from TCP data"""
message_length = int.from_bytes(data[:4], byteorder="big")
return data[4:4 + message_length]sequenceDiagram
participant Gateway
participant Device
Gateway->>Device: TCP Connect (device_ip:device_port)
Device-->>Gateway: Connection Established
Gateway->>Gateway: Serialize BadezimmerRequest<br/>(Protobuf)
Gateway->>Gateway: Prepend 4-byte length
Gateway->>Device: Send [Length][Protobuf Data]
Device->>Device: Read 4 bytes (length)
Device->>Device: Read N bytes (message)
Device->>Device: Deserialize BadezimmerRequest
Device->>Device: Process command
Device->>Device: Serialize BadezimmerResponse
Device->>Device: Prepend 4-byte length
Device->>Gateway: Send [Length][Protobuf Data]
Gateway->>Gateway: Parse response
Gateway-->>Device: Close connection
The gateway performs periodic TCP health checks to determine device availability:
async def health_check(ip: str, port: int, timeout: float = 1.0) -> bool:
"""Attempts TCP connection to verify device is reachable"""
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(ip, port),
timeout=timeout
)
writer.close()
await writer.wait_closed()
return True
except Exception:
return False- Frequency: Every 60 seconds (configurable)
- Timeout: 1 second default
- Action: Devices failing health checks are marked as
OFFLINE
The frontend communicates with the gateway using gRPC-Web, which encapsulates gRPC in HTTP/1.1.
service BadezimmerService {
rpc ListConnectedDevices(ListConnectedDevicesRequest)
returns (ListConnectedDevicesResponse) {}
rpc SendActuatorCommand(SendActuatorCommandRequest)
returns (SendActuatorCommandResponse) {}
}const badezimmerClient = new BadezimmerServicePromiseClient(
"http://localhost:8000", // Gateway URL
null,
null
);
// List all devices
const devices = await badezimmerClient.listConnectedDevices(request, {});
// Send command to actuator
const response = await badezimmerClient.sendActuatorCommand(request, {});The gateway implements a gRPC-Web bridge using FastAPI:
@app.post("/badezimmer.BadezimmerService/ListConnectedDevices")
async def list_connected_devices(request: Request):
# Parse gRPC-Web request
# Query device registry
# Return gRPC-Web response- Protocol: HTTP/1.1 POST requests
- Content-Type:
application/grpc-web+proto - Encoding: Binary Protobuf
- CORS: Enabled for cross-origin frontend access
- Category:
FART_DETECTOR - mDNS Type:
_fartdetector._tcp.local. - Properties:
{ "smell_level": "0-100", "last_detection": "ISO 8601 timestamp" }
- Category:
TOILET - mDNS Type:
_toilet._tcp.local. - Properties:
{ "flush_count": "integer", "last_flush": "ISO 8601 timestamp" }
- Category:
WATER_LEAK - mDNS Type:
_waterleak._tcp.local. - Properties:
{ "severity": "0-10", "location": "BATHROOM" }
- Category:
LIGHT_LAMP - mDNS Type:
_lightlamp._tcp.local. - Properties:
{ "is_on": "true/false", "brightness": "0-100", "color": "0xRRGGBB" } - Actions:
message LightLampActionRequest { optional bool turn_on = 1; optional int32 brightness = 2; optional Color color = 3; }
- Category:
SINK - mDNS Type:
_sink._tcp.local. - Properties:
{ "is_on": "true/false" } - Actions:
message SinkActionRequest { optional bool turn_on = 1; }
- Docker: Version 20.10 or higher
- Docker Compose: Version 2.0 or higher
-
Clone the repository:
git clone https://github.com/talDoFlemis/badezimmer.git cd badezimmer -
Start all services:
docker-compose up --build
-
Access the frontend: Open your browser to http://localhost:3000
-
Access the gateway API:
- REST API: http://localhost:8000/docs (Swagger UI)
- gRPC endpoint:
http://localhost:8000
gantt
title Service Startup Sequence
dateFormat X
axisFormat %S
section Infrastructure
Docker Network Created :0, 1
section Devices
Light Lamp Starts :1, 3
Light Lamp Registers :2, 4
Fart Detector Starts :1, 3
Fart Detector Registers :2, 4
Toilet Starts :1, 3
Toilet Registers :2, 4
Sink Starts :1, 3
Sink Registers :2, 4
Water Leak Starts :1, 3
Water Leak Registers :2, 4
section Gateway
Gateway Starts :0, 2
Gateway Listens mDNS :2, 15
Discovers Devices :4, 6
Health Checks Begin :6, 15
section Frontend
Next.js Build :0, 5
Frontend Ready :5, 15
The docker-compose.yaml orchestrates 7 services:
services:
gateway: # Central hub (Python FastAPI)
ports: ["8000:8000"]
lightlamp: # Actuator device (Python)
fartdetector: # Sensor device (Python)
toilet: # Sensor device (Python)
sink: # Actuator device (Python)
waterleak: # Sensor device (Go)
frontend: # Next.js UI
ports: ["3000:3000"]All services share the same Docker network, enabling multicast communication.
-
Install uv (fast Python package manager):
curl -LsSf https://astral.sh/uv/install.sh | sh -
Install dependencies:
uv sync
-
Generate Protobuf code:
uv run python -m grpc_tools.protoc \ -I./proto \ --python_out=./src/badezimmer \ --grpc_python_out=./src/badezimmer \ --pyi_out=./src/badezimmer \ ./proto/badezimmer.proto
-
Run individual services:
uv run gateway # Gateway on port 8000 uv run lightlamp # Light lamp service uv run fartdetector # Fart detector service
-
Navigate to frontend directory:
cd badezimmer-home-page -
Install dependencies:
pnpm install
-
Generate gRPC-Web code:
protoc -I=../proto badezimmer.proto \ --js_out=import_style=commonjs:./generated \ --grpc-web_out=import_style=typescript,mode=grpcwebtext:./generated
-
Run development server:
pnpm dev
Frontend available at http://localhost:3000
-
Navigate to Go directory:
cd go-water-leak -
Install dependencies:
go mod download
-
Generate Protobuf code:
protoc -I=../proto \ --go_out=./badezimmer \ --go_opt=paths=source_relative \ ../proto/badezimmer.proto
-
Run the service:
go run .
Key dependencies:
fastapi[standard]: Web framework for gatewaygrpcio+grpcio-tools: gRPC implementationprotobuf: Protocol Buffer supportzeroconf: mDNS foundation libraryasyncudp: Async UDP for multicast
Entry points defined for each service:
[project.scripts]
gateway = "gateway:main"
lightlamp = "lightlamp:main"
fartdetector = "fartdetector:main"
toilet = "toilet:main"
sink = "sink:main"- Python services: Based on
cgr.dev/chainguard/python(minimal, secure) - Go service: Multi-stage build with
golang:alpine - Frontend:
node:alpinewith Next.js standalone build
Lists all devices discovered via mDNS.
Request:
message ListConnectedDevicesRequest {
optional DeviceKind filter_kind = 1; // SENSOR or ACTUATOR
optional string filter_name = 2; // Name substring filter
}Response:
message ListConnectedDevicesResponse {
repeated ConnectedDevice devices = 1;
}
message ConnectedDevice {
string id = 1; // Unique ID: "name@type"
string device_name = 2; // Human-readable name
DeviceKind kind = 3; // SENSOR or ACTUATOR
DeviceStatus status = 4; // ONLINE, OFFLINE, ERROR
repeated string ips = 5; // IPv4 addresses
int32 port = 6; // TCP port
map<string, string> properties = 7; // Device-specific properties
DeviceCategory category = 8; // LIGHT_LAMP, FART_DETECTOR, etc.
TransportProtocol transport_protocol = 9; // TCP or UDP
}Sends a control command to an actuator device.
Request:
message SendActuatorCommandRequest {
string device_id = 1;
oneof action {
LightLampActionRequest light_action = 2;
SinkActionRequest sink_action = 3;
}
}Response:
message SendActuatorCommandResponse {
optional string message = 2; // Human-readable result
}The gateway also exposes FastAPI endpoints:
GET /: Health checkPOST /badezimmer.BadezimmerService/ListConnectedDevices: gRPC-Web endpointPOST /badezimmer.BadezimmerService/SendActuatorCommand: gRPC-Web endpointGET /docs: Swagger UI documentation
All errors are communicated via ErrorDetails:
enum ErrorCode {
UNKNOWN_ERROR = 0;
DEVICE_NOT_FOUND = 1;
INVALID_COMMAND = 2;
DEVICE_OFFLINE = 3;
VALIDATION_ERROR = 4;
}
message ErrorDetails {
ErrorCode code = 1;
string message = 2;
map<string, string> metadata = 3;
}-
Start the system:
docker-compose up
-
Verify device registration: Check gateway logs for device discoveries:
INFO - Adding device: device_id=Light Lamp@_lightlamp._tcp.local. INFO - Adding device: device_id=Fart Detector@_fartdetector._tcp.local. -
Test frontend:
- Open http://localhost:3000
- Verify devices appear in grid
- Toggle light lamp on/off
- Adjust brightness and color
-
Test gRPC directly: Use tools like
grpcurlorBloomRPCto test gateway endpoints
View logs for specific services:
docker-compose logs -f gateway
docker-compose logs -f lightlamp
docker-compose logs -f frontend