cleanup: move esp and python MQTT clients to different repos
This commit is contained in:
@@ -1,308 +0,0 @@
|
||||
#include <Arduino.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <Hash.h>
|
||||
|
||||
|
||||
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<String>();
|
||||
if (id != actorId) return;
|
||||
|
||||
int value = 0;
|
||||
if (payload.containsKey("value")) {
|
||||
if (payload["value"].is<bool>()) {
|
||||
value = payload["value"].as<bool>() ? 1 : 0;
|
||||
} else if (payload["value"].is<int>()) {
|
||||
value = payload["value"].as<int>() != 0 ? 1 : 0;
|
||||
} else if (payload["value"].is<float>()) {
|
||||
value = payload["value"].as<float>() != 0 ? 1 : 0;
|
||||
} else if (payload["value"].is<const char *>()) {
|
||||
String v = payload["value"].as<String>();
|
||||
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<String>();
|
||||
if (id != sensorId) return;
|
||||
publishSensorReading();
|
||||
}
|
||||
|
||||
void handleDeviceCheckRequest(JsonObject payload) {
|
||||
if (!payload.containsKey("device_id")) return;
|
||||
String id = payload["device_id"].as<String>();
|
||||
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<JsonObject>());
|
||||
return;
|
||||
}
|
||||
if (msgType == "sensor_trigger") {
|
||||
handleSensorTrigger(doc.as<JsonObject>());
|
||||
return;
|
||||
}
|
||||
if (msgType == "device_check_request") {
|
||||
handleDeviceCheckRequest(doc.as<JsonObject>());
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user