diff --git a/esp-demo/esp-demo.ino b/esp-demo/esp-demo.ino deleted file mode 100644 index e885e8d..0000000 --- a/esp-demo/esp-demo.ino +++ /dev/null @@ -1,308 +0,0 @@ -#include -#include -#include -#include -#include - - -const char *WIFI_SSID = "seminardemo"; -const char *WIFI_PASS = "seminardemo"; - -const char *MQTT_HOST = "192.168.1.16"; // broker -const uint16_t MQTT_PORT = 1883; -const char *MQTT_USER = "testuser"; // from test/.env -const char *MQTT_PASS = "testpass"; // from test/.env - -const char *MQTT_TOPIC = "lambdaiot"; // main topic - -// GPIO -const uint8_t ACTOR_PIN = 14; // digital output - -// Publishing interval (ms) -const unsigned long SENSOR_PUBLISH_INTERVAL = 5000; - -WiFiClient wifiClient; -PubSubClient mqtt(wifiClient); - -String macAddress; -String deviceId; -String sensorId; -String actorId; - -unsigned long lastPublishAt = 0; - -// DNS namespace UUID bytes: 6ba7b810-9dad-11d1-80b4-00c04fd430c8 -const char *DNS_NAMESPACE_UUID = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; - -// ---- UUID helpers ---- - -bool parseUuidBytes(const String &uuidStr, uint8_t out[16]) { - String hex = uuidStr; - hex.replace("-", ""); - if (hex.length() != 32) { - return false; - } - for (int i = 0; i < 16; i++) { - char c1 = hex[i * 2]; - char c2 = hex[i * 2 + 1]; - auto hexVal = [](char c) -> int { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; - }; - int v1 = hexVal(c1); - int v2 = hexVal(c2); - if (v1 < 0 || v2 < 0) { - return false; - } - out[i] = (uint8_t)((v1 << 4) | v2); - } - return true; -} - -String formatUuid(const uint8_t bytes[16]) { - char buf[37]; - snprintf(buf, sizeof(buf), - "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", - bytes[0], bytes[1], bytes[2], bytes[3], - bytes[4], bytes[5], - bytes[6], bytes[7], - bytes[8], bytes[9], - bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]); - return String(buf); -} - -bool parseHexBytes(const String &hex, uint8_t *out, size_t outLen) { - if (hex.length() < (int)(outLen * 2)) { - return false; - } - for (size_t i = 0; i < outLen; i++) { - char c1 = hex[i * 2]; - char c2 = hex[i * 2 + 1]; - auto hexVal = [](char c) -> int { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; - }; - int v1 = hexVal(c1); - int v2 = hexVal(c2); - if (v1 < 0 || v2 < 0) { - return false; - } - out[i] = (uint8_t)((v1 << 4) | v2); - } - return true; -} - -String uuidV5(const uint8_t namespaceBytes[16], const String &name) { - // SHA1(namespace + name) - size_t totalLen = 16 + name.length(); - uint8_t *buf = (uint8_t *)malloc(totalLen); - if (!buf) { - return String(""); - } - memcpy(buf, namespaceBytes, 16); - memcpy(buf + 16, name.c_str(), name.length()); - - String hashHex = sha1(buf, totalLen); - free(buf); - - uint8_t out[16]; - if (!parseHexBytes(hashHex, out, sizeof(out))) { - return String(""); - } - - // Set version (5) and variant (RFC 4122) - out[6] = (out[6] & 0x0F) | 0x50; - out[8] = (out[8] & 0x3F) | 0x80; - - return formatUuid(out); -} - -// ---- MQTT messaging ---- - -void publishDiscovery() { - StaticJsonDocument<1024> doc; - doc["mac_address"] = macAddress; - - JsonObject dev = doc.createNestedObject("device"); - dev["id"] = deviceId; - dev["name"] = "ESP8266 Demo"; - dev["description"] = "ESP8266 MQTT demo device"; - dev["location"] = "Lab"; - dev["status_id"] = 1; - - JsonArray sensors = doc.createNestedArray("sensors"); - JsonObject s0 = sensors.createNestedObject(); - s0["id"] = sensorId; - s0["name"] = "Temperature"; - s0["type"] = "ADC"; - s0["data_type_id"] = 2; - - JsonArray actors = doc.createNestedArray("actors"); - JsonObject a0 = actors.createNestedObject(); - a0["id"] = actorId; - a0["name"] = "DigitalOut"; - a0["type"] = "GPIO"; - a0["data_type_id"] = 1; - - char buf[1024]; - size_t n = serializeJson(doc, buf, sizeof(buf)); - - Serial.println(n); - Serial.println(buf); - - String topic = String(MQTT_TOPIC) + "/discovery"; - Serial.println(topic); - int result = mqtt.publish(topic.c_str(), buf, n); - Serial.println(result); -} - -void publishSensorReading() { - int adc = analogRead(A0); - float value = (float)adc; // raw ADC value - - StaticJsonDocument<256> doc; - doc["type"] = "sensor_reading"; - doc["sensor_id"] = sensorId; - doc["value"] = value; - - char buf[256]; - size_t n = serializeJson(doc, buf, sizeof(buf)); - mqtt.publish(MQTT_TOPIC, buf, n); -} - -void publishDeviceCheckResponse() { - StaticJsonDocument<256> doc; - doc["type"] = "device_check_response"; - doc["device_id"] = deviceId; - doc["status"] = "ok"; - - char buf[256]; - size_t n = serializeJson(doc, buf, sizeof(buf)); - mqtt.publish(MQTT_TOPIC, buf, n); -} - -void handleActorCommand(JsonObject payload) { - if (!payload.containsKey("actor_id")) return; - String id = payload["actor_id"].as(); - if (id != actorId) return; - - int value = 0; - if (payload.containsKey("value")) { - if (payload["value"].is()) { - value = payload["value"].as() ? 1 : 0; - } else if (payload["value"].is()) { - value = payload["value"].as() != 0 ? 1 : 0; - } else if (payload["value"].is()) { - value = payload["value"].as() != 0 ? 1 : 0; - } else if (payload["value"].is()) { - String v = payload["value"].as(); - v.toLowerCase(); - if (v == "true" || v == "1" || v == "on") value = 1; - else value = 0; - } - } - - digitalWrite(ACTOR_PIN, value ? HIGH : LOW); -} - -void handleSensorTrigger(JsonObject payload) { - if (!payload.containsKey("sensor_id")) return; - String id = payload["sensor_id"].as(); - if (id != sensorId) return; - publishSensorReading(); -} - -void handleDeviceCheckRequest(JsonObject payload) { - if (!payload.containsKey("device_id")) return; - String id = payload["device_id"].as(); - if (id != deviceId) return; - publishDeviceCheckResponse(); -} - -void mqttCallback(char *topic, uint8_t *payload, unsigned int length) { - StaticJsonDocument<512> doc; - DeserializationError err = deserializeJson(doc, payload, length); - if (err) { - return; - } - - const char *type = doc["type"] | ""; - String msgType = String(type); - msgType.toLowerCase(); - - if (msgType == "actor_command") { - handleActorCommand(doc.as()); - return; - } - if (msgType == "sensor_trigger") { - handleSensorTrigger(doc.as()); - return; - } - if (msgType == "device_check_request") { - handleDeviceCheckRequest(doc.as()); - return; - } -} - -// ---- Setup/loop ---- - -void ensureMqtt() { - while (!mqtt.connected()) { - String clientId = "esp-demo-" + String(ESP.getChipId()); - if (mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS)) { - mqtt.subscribe(MQTT_TOPIC); - delay(1000); - publishDiscovery(); - } else { - delay(2000); - } - } -} - -void setup() { - pinMode(ACTOR_PIN, OUTPUT); - digitalWrite(ACTOR_PIN, LOW); - Serial.begin(9600); - - WiFi.mode(WIFI_STA); - WiFi.begin(WIFI_SSID, WIFI_PASS); - while (WiFi.status() != WL_CONNECTED) { - delay(500); - } - - macAddress = WiFi.macAddress(); - macAddress.toLowerCase(); - - uint8_t dnsNs[16]; - parseUuidBytes(String(DNS_NAMESPACE_UUID), dnsNs); - String macNoSep = macAddress; - macNoSep.replace(":", ""); - deviceId = uuidV5(dnsNs, macNoSep); - - uint8_t deviceNs[16]; - parseUuidBytes(deviceId, deviceNs); - sensorId = uuidV5(deviceNs, "sensor-0"); - actorId = uuidV5(deviceNs, "actor-0"); - - mqtt.setBufferSize(4096); // Otherwise discovery fails spectacularly - mqtt.setServer(MQTT_HOST, MQTT_PORT); - mqtt.setCallback(mqttCallback); - - ensureMqtt(); - lastPublishAt = millis(); -} - -void loop() { - ensureMqtt(); - mqtt.loop(); - - unsigned long now = millis(); - if (now - lastPublishAt >= SENSOR_PUBLISH_INTERVAL) { - lastPublishAt = now; - publishSensorReading(); - } -} diff --git a/test/device_emulator.py b/test/device_emulator.py deleted file mode 100644 index 76d3104..0000000 --- a/test/device_emulator.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python3 -import json -import os -import random -import time -import uuid -from datetime import datetime, timezone - -import paho.mqtt.client as mqtt - -BROKER = "localhost" -PORT = 1883 -TOPIC = "lambdaiot" -DISCOVERY_TOPIC = f"{TOPIC}/discovery" -DNS_NAMESPACE = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8") - - -def generate_mac() -> str: - return ":".join(f"{random.randint(0, 255):02x}" for _ in range(6)) - - -def mac_to_device_uuid(mac: str) -> uuid.UUID: - return uuid.uuid5(DNS_NAMESPACE, mac.replace(":", "").lower()) - - -def now_rfc3339() -> str: - return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - - -def parse_actor_value(value): - if isinstance(value, bool): - return 1 if value else 0 - if isinstance(value, (int, float)): - return 1 if value != 0 else 0 - if isinstance(value, str): - if value.strip().lower() in {"true", "on", "1"}: - return 1 - if value.strip().lower() in {"false", "off", "0"}: - return 0 - return 0 - - -def parse_actor_float(value): - if isinstance(value, bool): - return 1.0 if value else 0.0 - if isinstance(value, (int, float)): - return float(value) - if isinstance(value, str): - if value.strip().lower() in {"true", "on"}: - return 1.0 - if value.strip().lower() in {"false", "off"}: - return 0.0 - try: - return float(value) - except ValueError: - return 0.0 - return 0.0 - - -def load_env_file(path: str) -> dict: - env = {} - try: - with open(path, "r", encoding="utf-8") as handle: - for line in handle: - line = line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, value = line.split("=", 1) - env[key.strip()] = value.strip() - except FileNotFoundError: - return {} - return env - - -class DeviceEmulator: - def __init__(self): - self.mac_address = generate_mac() - self.device_id = mac_to_device_uuid(self.mac_address) - self.sensor_id = uuid.uuid5(self.device_id, "sensor-0") - self.sensor_bool_id = uuid.uuid5(self.device_id, "sensor-1") - self.actor_id = uuid.uuid5(self.device_id, "actor-0") - self.actor_float_id = uuid.uuid5(self.device_id, "actor-1") - - self.actor_value = 0 - self.actor_float_value = 0.0 - self.temperature = 22.5 - self.switch_state = False - self.publish_interval = 5 - - env_path = os.path.join(os.path.dirname(__file__), ".env") - env = load_env_file(env_path) - self.mqtt_user = env.get("MOSQ_USER", "").strip() - self.mqtt_pass = env.get("MOSQ_PASS", "").strip() - - self.client = mqtt.Client(client_id=f"emulator-{self.device_id}") - if self.mqtt_user or self.mqtt_pass: - self.client.username_pw_set(self.mqtt_user, self.mqtt_pass) - self.client.on_connect = self.on_connect - self.client.on_message = self.on_message - - def discovery_payload(self): - return { - "mac_address": self.mac_address, - "device": { - "id": str(self.device_id), - "name": "Emulated Device", - "description": "Python MQTT emulator", - "location": "Localhost", - "status_id": 1, - }, - "sensors": [ - { - "id": str(self.sensor_id), - "name": "Temperature", - "type": "DHT22", - "data_type_id": 2, - }, - { - "id": str(self.sensor_bool_id), - "name": "Presence", - "type": "PIR", - "data_type_id": 1, - } - ], - "actors": [ - { - "id": str(self.actor_id), - "name": "Binary Switch", - "type": "Relay", - "data_type_id": 1, - }, - { - "id": str(self.actor_float_id), - "name": "Dimmer", - "type": "PWM", - "data_type_id": 2, - } - ], - } - - def publish_discovery(self): - payload = self.discovery_payload() - self.client.publish(DISCOVERY_TOPIC, json.dumps(payload), qos=1) - print(f"discovery published: device_id={self.device_id}") - - def publish_sensor_reading(self): - self.temperature = round(self.temperature + random.uniform(-0.5, 0.5), 2) - payload = { - "type": "sensor_reading", - "sensor_id": str(self.sensor_id), - "value": self.temperature, - "value_at": now_rfc3339(), - } - self.client.publish(TOPIC, json.dumps(payload), qos=0) - print(f"sensor_reading published: {self.temperature}C") - - def publish_bool_sensor_reading(self): - self.switch_state = not self.switch_state - payload = { - "type": "sensor_reading", - "sensor_id": str(self.sensor_bool_id), - "value": self.switch_state, - "value_at": now_rfc3339(), - } - self.client.publish(TOPIC, json.dumps(payload), qos=0) - print(f"sensor_reading published: presence={self.switch_state}") - - def publish_device_state(self): - payload = { - "type": "device_check_response", - "device_id": str(self.device_id), - "status": "ok", - } - self.client.publish(TOPIC, json.dumps(payload), qos=0) - print("device_check_response published") - - def on_connect(self, client, userdata, flags, rc): - if rc != 0: - print(f"mqtt connect failed: rc={rc}") - return - print("mqtt connected") - client.subscribe(TOPIC, qos=0) - self.publish_discovery() - - def on_message(self, client, userdata, msg): - try: - payload = json.loads(msg.payload.decode("utf-8")) - except Exception: - return - - msg_type = str(payload.get("type", "")).strip().lower() - if msg_type == "device_check_request": - if payload.get("device_id") == str(self.device_id): - self.publish_device_state() - return - - if msg_type == "sensor_trigger": - if payload.get("sensor_id") == str(self.sensor_id): - self.publish_sensor_reading() - return - if payload.get("sensor_id") == str(self.sensor_bool_id): - self.publish_bool_sensor_reading() - return - - if msg_type == "actor_command": - if payload.get("actor_id") == str(self.actor_id): - self.actor_value = parse_actor_value(payload.get("value")) - print(f"actor_command received; state={self.actor_value}") - return - if payload.get("actor_id") == str(self.actor_float_id): - self.actor_float_value = parse_actor_float(payload.get("value")) - print(f"actor_command received; dimmer={self.actor_float_value}") - return - - def run(self): - self.client.connect(BROKER, PORT, keepalive=30) - self.client.loop_start() - try: - while True: - time.sleep(self.publish_interval) - self.publish_sensor_reading() - self.publish_bool_sensor_reading() - finally: - self.client.loop_stop() - - -if __name__ == "__main__": - DeviceEmulator().run()