I recently built an IoT Component SDK for developing Cyber Physical Systems (CPS) and Internet of Things applications. The key insight was designing it “simulation first.” The entire system works without any physical hardware, using realistic environmental models instead.
The Problem
When developing IoT systems, you typically need access to sensors, actuators, and microcontrollers. This creates friction: hardware is expensive, might not be available, and setting up a physical testbed takes time. Yet you still need to validate your control logic, test edge cases, and demonstrate the system to stakeholders.
I wanted an SDK where the same component code runs in simulation during development and on real hardware in production, with zero changes.
The Architecture
The SDK is built around a few core concepts:
Components are the building blocks: sensors that read values, actuators that control devices, and processors that transform data or make decisions. Each component has a standard interface for publishing and subscribing to messages.
Hardware Abstraction Layer (HAL) provides multiple backends for each component type:
mockreturns fixed values for unit testingsimulateduses physical models with realistic noise, drift, and failureshardwareconnects to real GPIO, I2C, SPI devices
Message Broker handles pub/sub communication between components using MQTT style topic wildcards. A local in memory broker works for single process systems; an MQTT broker enables distributed deployments.
Simulation Engine orchestrates time (with 1x to 100x acceleration), environment models, and fault injection for testing resilience.
Environment Simulation
The simulation includes realistic physical models:
class SoilMoistureModel:
"""Simulates soil moisture dynamics."""
def update(self, dt_hours: float, temperature: float, humidity: float) -> float:
# Evaporation increases with temperature and decreases with humidity
evap_rate = self.base_evaporation * (1 + 0.05 * (temperature - 20))
evap_rate *= (1 - humidity / 200)
# Irrigation adds moisture
if self.is_irrigating:
self.moisture += self.irrigation_rate * dt_hours
# Evaporation removes moisture
self.moisture -= evap_rate * dt_hours
return self.moisture
Temperature follows a daily sine wave, humidity responds to temperature and rain, and soil moisture accounts for evaporation, irrigation, and drainage. The models are simplified but capture the dynamics needed for testing control logic.
Hysteresis Control
For actuator control, I implemented a hysteresis controller to prevent rapid on/off cycling:
# Turn pump ON when moisture < 30%, OFF when > 60%
irrigation-controller:
type: controller.hysteresis
config:
input_topic: sensors/soil/zone1
output_topic: actuators/pump/command
low_threshold: 30.0
high_threshold: 60.0
The dead band between 30% and 60% ensures the pump does not oscillate when moisture hovers around a single threshold.
Web Dashboard
The SDK includes a React dashboard for monitoring and control:
- System Overview: Component count, message throughput, uptime
- Real time Charts: Sensor readings with Recharts
- Simulation Controls: Start/stop/pause, time acceleration
- Message Inspector: Filter and inspect pub/sub traffic
- Environment Panel: Current temperature, humidity, soil moisture
The backend is FastAPI with WebSocket support for real time updates. The frontend polls the REST API and receives push updates via WebSocket.
YAML Configuration
Systems are defined declaratively in YAML:
system:
name: "smart-greenhouse"
mode: "simulation"
simulation:
time_scale: 10.0
components:
- id: soil-sensor-1
type: sensor.soil_moisture
config:
interval: 30
topic: sensors/soil/zone1
- id: pump-1
type: actuator.pump
config:
command_topic: actuators/pump/command
flow_rate: 10.0
- id: irrigation-controller
type: controller.hysteresis
config:
input_topic: sensors/soil/zone1
output_topic: actuators/pump/command
low_threshold: 30.0
high_threshold: 60.0
A CLI loads and runs the system:
iot-sdk run greenhouse.yaml
The SystemBuilder parses the config, instantiates components via a factory, wires them to the broker and simulation engine, and manages the lifecycle.
Fault Injection
For resilience testing, the simulation supports fault injection:
sensor_stuckmeans the sensor returns the same value repeatedlysensor_driftmeans readings drift from true value over timeactuator_stuck_on/actuator_stuck_offmeans the actuator ignores commandscommunication_delaymeans messages are delayed
This lets you test how control logic handles degraded conditions without physically breaking hardware.
What I Learned
Simulation fidelity matters. Early versions with simple random values did not exercise the control logic meaningfully. Adding realistic dynamics (evaporation that depends on temperature, moisture that saturates and drains) made the simulation useful for finding bugs.
The HAL abstraction pays off. Swapping backend: simulated for backend: hardware in the config is all it takes to run on real devices. The component code stays the same.
YAML configuration is powerful. Defining systems declaratively makes it easy to version control configurations, share them with teammates, and spin up variations for testing.
Code
The full SDK is available on GitHub. It includes:
- Core component framework with pub/sub messaging
- Simulation engine with physical models
- Sensors: temperature, humidity, soil moisture
- Actuators: pump, relay
- Processors: threshold alerts, hysteresis control
- React dashboard with real time charts
- CLI for running systems from YAML
If you are building IoT applications and want to iterate quickly without hardware, this approach might be worth exploring.