diff --git a/platformio.ini b/platformio.ini deleted file mode 100644 index cc4d564..0000000 --- a/platformio.ini +++ /dev/null @@ -1,11 +0,0 @@ -[env:esp32-c6-devkitc-1] -platform = espressif32 -board = esp32-c6-devkitc-1 -framework = arduino, espidf -monitor_speed = 115200 -lib_deps = - adafruit/Adafruit PWM Servo Driver Library@2.4.1 - -build_flags = - -DARDUINO_USB_MODE=1 - -DARDUINO_USB_CDC_ON_BOOT=1 diff --git a/reptile_feeder.ino b/reptile_feeder.ino new file mode 100644 index 0000000..88cf70a --- /dev/null +++ b/reptile_feeder.ino @@ -0,0 +1,256 @@ +#include +#include +#include +#include +#include +#include +#include // For Over-the-Air updates +#include // For DHT sensors +#include // For DHT sensors + +// --- Wi-Fi Configuration --- +const char* ssid = "Aussie Broadband 8729"; +const char* password = "Ffdfmunfca"; + +// --- MQTT Configuration --- +const char* mqtt_server = "192.168.20.30"; +const int mqtt_port = 1883; +const char* mqtt_user = "mqtt-user"; +const char* mqtt_password = "sam4jo"; +const char* mqtt_control_topic = "homeassistant/reptile_feeder/control"; +const char* mqtt_state_topic = "homeassistant/reptile_feeder/state"; +const char* ota_password = "sam4jo"; // Password for OTA updates + +// --- Hardware Pins --- +#define I2C_SDA_PIN 10 +#define I2C_SCL_PIN 11 +#define DHTPIN_1 6 // Pin for the first DHT22 sensor +#define DHTPIN_2 7 // Pin for the second DHT22 sensor +#define DHTPIN_3 18 // Pin for the third DHT22 sensor +#define DHTTYPE DHT22 + +// --- Timing Configuration --- +#define SENSOR_READ_INTERVAL 30000 // Time in ms between sensor reads (N). 30 seconds. + +// --- Servo Configuration --- +#define SERVO_COUNT 16 +#define SERVO_FREQ 50 +#define SERVOMIN 150 +#define SERVOMAX 600 + +// --- Global Objects --- +WebServer server(80); +Adafruit_PWMServoDriver pca = Adafruit_PWMServoDriver(); +WiFiClient espClient; +PubSubClient mqttClient(espClient); +DHT dht_sensors[] = { DHT(DHTPIN_1, DHTTYPE), DHT(DHTPIN_2, DHTTYPE), DHT(DHTPIN_3, DHTTYPE) }; +const int num_sensors = sizeof(dht_sensors) / sizeof(dht_sensors[0]); +uint8_t current_angles[SERVO_COUNT]; +unsigned long previousSensorMillis = 0; + +// --- Function Declarations --- +void move_servo_smoothly(uint8_t servo_num, uint8_t target_angle, uint8_t speed); +void parse_serial_command(String cmd); +void handle_web_move(); +void handle_root(); +void reconnectMQTT(); +void mqtt_callback(char* topic, byte* payload, unsigned int length); +void readAndPublishSensors(); +void setupOTA(); + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println("\nReptile Feeder booting up..."); + + // Initialize Servos + Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); + pca.begin(); + pca.setPWMFreq(SERVO_FREQ); + for (uint8_t i = 0; i < SERVO_COUNT; i++) { + current_angles[i] = 0; + uint16_t pulselen = map(0, 0, 180, SERVOMIN, SERVOMAX); + pca.setPWM(i, 0, pulselen); + } + Serial.println("Servos initialized."); + + // Initialize DHT Sensors + for (int i = 0; i < num_sensors; i++) { + dht_sensors[i].begin(); + } + Serial.println("DHT sensors initialized."); + + // Connect to Wi-Fi + Serial.printf("Connecting to %s ", ssid); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println(" Connected!"); + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + + // Setup MQTT + mqttClient.setServer(mqtt_server, mqtt_port); + mqttClient.setCallback(mqtt_callback); + + // Setup Web Server + server.on("/", handle_root); + server.on("/move", handle_web_move); + server.begin(); + Serial.println("Web server started."); + + // Setup OTA Updates + setupOTA(); + Serial.println("OTA ready. Hostname: reptile-feeder"); +} + +void loop() { + ArduinoOTA.handle(); // Handle OTA update requests + + if (!mqttClient.connected()) { + reconnectMQTT(); + } + mqttClient.loop(); + + server.handleClient(); + + if (Serial.available() > 0) { + String command = Serial.readStringUntil('\n'); + command.trim(); + parse_serial_command(command); + } + + // Non-blocking sensor read timer + unsigned long currentMillis = millis(); + if (currentMillis - previousSensorMillis >= SENSOR_READ_INTERVAL) { + previousSensorMillis = currentMillis; + readAndPublishSensors(); + } +} + +// --- MQTT Functions --- +void reconnectMQTT() { + while (!mqttClient.connected()) { + Serial.print("Attempting MQTT connection..."); + String clientId = "ReptileFeeder-"; clientId += String(random(0xffff), HEX); + if (mqttClient.connect(clientId.c_str(), mqtt_user, mqtt_password)) { + Serial.println("connected"); + mqttClient.subscribe(mqtt_control_topic); + mqttClient.publish(mqtt_state_topic, "online"); + } else { + Serial.print("failed, rc="); Serial.print(mqttClient.state()); + Serial.println(" try again in 5 seconds"); + delay(5000); + } + } +} + +void mqtt_callback(char* topic, byte* payload, unsigned int length) { + Serial.print("Message arrived on topic: "); Serial.println(topic); + JsonDocument doc; + DeserializationError error = deserializeJson(doc, payload, length); + if (error) { Serial.print("deserializeJson() failed: "); Serial.println(error.c_str()); return; } + if (!doc.containsKey("servo") || !doc.containsKey("angle")) { Serial.println("Error: JSON must contain 'servo' and 'angle'"); return; } + int servo_num = doc["servo"]; + int angle = doc["angle"]; + int speed = doc.containsKey("speed") ? doc["speed"].as() : 10; + move_servo_smoothly((uint8_t)servo_num, (uint8_t)angle, (uint8_t)speed); +} + +// --- Sensor Reading Function --- +void readAndPublishSensors() { + for (int i = 0; i < num_sensors; i++) { + float h = dht_sensors[i].readHumidity(); + float t = dht_sensors[i].readTemperature(); + + if (isnan(h) || isnan(t)) { + Serial.printf("Failed to read from DHT sensor #%d\n", i + 1); + continue; + } + + JsonDocument doc; + doc["temperature"] = t; + doc["humidity"] = h; + + char buffer[128]; + serializeJson(doc, buffer); + + String topic = String(mqtt_state_topic) + "/sensor" + String(i + 1); + if (mqttClient.publish(topic.c_str(), buffer)) { + Serial.printf("Published to %s: %s\n", topic.c_str(), buffer); + } else { + Serial.printf("Failed to publish to %s\n", topic.c_str()); + } + } +} + +// --- OTA Setup Function --- +void setupOTA() { + ArduinoOTA.setHostname("reptile-feeder"); + ArduinoOTA.setPassword(ota_password); + ArduinoOTA + .onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + type = "sketch"; + else // U_SPIFFS + type = "filesystem"; + Serial.println("Start updating " + type); + }) + .onEnd([]() { + Serial.println("\nEnd"); + }) + .onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }) + .onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) Serial.println("End Failed"); + }); + ArduinoOTA.begin(); +} + + +// --- Web, Serial, and Servo Functions (Unchanged) --- +void handle_root() { server.send(200, "text/html", "

Reptile Feeder

"); } +void handle_web_move() { + int s = server.arg("servo").toInt(); + int a = server.arg("angle").toInt(); + int v = server.arg("speed").toInt(); + move_servo_smoothly(s, a, v); + server.send(200, "text/plain", "OK"); +} +void parse_serial_command(String cmd) { + char buf[50]; + cmd.toCharArray(buf, 50); + int s, a, v; + if (sscanf(buf, "move %d %d %d", &s, &a, &v) == 3) { + move_servo_smoothly(s, a, v); + } else { + Serial.println("Invalid format"); + } +} +void move_servo_smoothly(uint8_t servo_num, uint8_t target_angle, uint8_t speed) { + if (servo_num >= SERVO_COUNT || target_angle > 180 || speed < 1 || speed > 10) { + Serial.println("Error: Move parameters out of range."); + return; + } + uint8_t start_angle = current_angles[servo_num]; + if (target_angle == start_angle) return; + uint8_t step_delay = (11 - speed) * 5; + Serial.printf("Moving servo %d from %d to %d at speed %d\n", servo_num, start_angle, target_angle, speed); + int step = (target_angle > start_angle) ? 1 : -1; + for (int i = start_angle; i != (int)target_angle + step; i += step) { + uint16_t pulselen = map(i, 0, 180, SERVOMIN, SERVOMAX); + pca.setPWM(servo_num, 0, pulselen); + delay(step_delay); + } + current_angles[servo_num] = target_angle; + Serial.println("Move complete."); +} diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index cb01480..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include -#include -#include -#include "esp_zigbee_core.h" - -// --- Configuration --- -#define CHANNEL_COUNT 16 // 16 servos -#define ZIGBEE_ENDPOINT_START 1 - -// PCA9685 Settings -Adafruit_PWMServoDriver pca9685 = Adafruit_PWMServoDriver(); -#define SERVO_FREQ 50 // Analog servos run at ~50 Hz -#define SERVO_MIN_PULSE 150 // Corresponds to 0 degrees -#define SERVO_MAX_PULSE 600 // Corresponds to 180 degrees -#define SERVO_OPEN_ANGLE 90 // Angle in degrees for "open" state -#define SERVO_CLOSED_ANGLE 0 // Angle in degrees for "closed" state - -// --- Zigbee Device Setup --- -// Basic Zigbee device configuration -#define ESP_ZB_PRIMARY_CHANNEL_MASK (1l << 15) // Use Zigbee channel 15 -#define ESP_ZB_MANUFACTURER_NAME "T3-Labs" -#define ESP_ZB_MODEL_NAME "ReptileFeeder.16x" - -// --- Function Declarations --- -void move_servo(uint8_t servo_num, uint8_t angle); -static void bdb_start_top_level_commissioning_cb(uint8_t mode_mask); -static esp_err_t zb_attribute_handler(const esp_zb_zcl_set_attr_value_message_t *message); -static esp_err_t zb_action_handler(esp_zb_core_action_signal_t *signal_struct); -void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct); - -// --- Main Application --- - -void setup() -{ - Serial.begin(115200); - Wire.begin(); // Default I2C pins for ESP32-C6 are typically GPIO8/GPIO9 - - // Initialize PCA9685 - pca9685.begin(); - pca9685.setPWMFreq(SERVO_FREQ); - Serial.println("PCA9685 Initialized. Setting all servos to closed position."); - for (int i = 0; i < CHANNEL_COUNT; i++) { - move_servo(i, SERVO_CLOSED_ANGLE); - delay(50); - } - Serial.println("Servos initialized."); - - // Initialize Zigbee stack - esp_zb_cfg_t zb_cfg = ESP_ZB_DEFAULT_CFG(); - zb_cfg.esp_zb_role = ESP_ZB_DEVICE_TYPE_ROUTER; - zb_cfg.install_code_policy = false; - zb_cfg.nwk_cfg.max_children = 0; // Router doesn't need to manage children - esp_zb_init(&zb_cfg); - - // Create On/Off clusters for each servo - for (int i = 0; i < CHANNEL_COUNT; ++i) { - esp_zb_cluster_list_t *cluster_list = esp_zb_zcl_cluster_list_create(); - - // On/Off cluster - esp_zb_on_off_cluster_cfg_t on_off_cfg; - on_off_cfg.on_off = false; // Default to off - esp_zb_attribute_list_t *on_off_cluster = esp_zb_on_off_cluster_create(&on_off_cfg); - esp_zb_cluster_list_add_cluster(cluster_list, on_off_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); - - // Basic cluster - esp_zb_basic_cluster_cfg_t basic_cfg; - basic_cfg.zcl_version = ESP_ZB_ZCL_VERSION_DEFAULT; - basic_cfg.power_source = ESP_ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN; - esp_zb_attribute_list_t *basic_cluster = esp_zb_basic_cluster_create(&basic_cfg); - esp_zb_cluster_list_add_cluster(cluster_list, basic_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); - - // Create an endpoint for the servo - esp_zb_endpoint_config_t endpoint_cfg = { - .endpoint = (uint8_t)(ZIGBEE_ENDPOINT_START + i), - .app_profile_id = ESP_ZB_AF_HA_PROFILE_ID, - .app_device_id = ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID, - .app_device_version = 0 - }; - esp_zb_ep_list_add_ep(esp_zb_get_ep_list(), cluster_list, endpoint_cfg); - } - - // Set Zigbee stack signal handler - esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK); - ESP_ERROR_CHECK(esp_zb_start(false)); - esp_zb_set_app_signal_handler(esp_zb_app_signal_handler); - - Serial.println("Zigbee Initialized and Started."); -} - -void loop() -{ - // The Zigbee stack runs in its own task. - // The loop can be used for other non-blocking tasks if needed. - delay(1000); -} - -// --- Function Definitions --- - -/** - * @brief Moves a servo to a specific angle. - * @param servo_num The servo channel (0-15). - * @param angle The target angle (0-180). - */ -void move_servo(uint8_t servo_num, uint8_t angle) -{ - if (servo_num >= CHANNEL_COUNT || angle > 180) { - return; // Invalid input - } - uint16_t pulselen = map(angle, 0, 180, SERVO_MIN_PULSE, SERVO_MAX_PULSE); - pca9685.setPWM(servo_num, 0, pulselen); -} - -/** - * @brief Zigbee application signal handler. - */ -void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) -{ - uint32_t *p_sg_p = signal_struct->p_app_signal; - esp_err_t err_status = signal_struct->esp_err_status; - - switch (signal_struct->signal_type) { - case ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP: - Serial.println("Zigbee stack initialized"); - esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_INITIALIZATION); - break; - case ESP_ZB_BDB_SIGNAL_DEVICE_FIRST_START: - case ESP_ZB_BDB_SIGNAL_DEVICE_REBOOT: - if (err_status == ESP_OK) { - Serial.println("Device started, commissioning."); - esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING); - } else { - Serial.println("Failed to initialize Zigbee stack"); - } - break; - case ESP_ZB_BDB_SIGNAL_STEERING_COMPLETE: - if (err_status == ESP_OK) { - esp_zb_ieee_addr_t extended_pan_id; - esp_zb_get_extended_pan_id(extended_pan_id); - Serial.printf("Successfully joined network. PAN ID: %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x\n", - extended_pan_id[7], extended_pan_id[6], extended_pan_id[5], extended_pan_id[4], - extended_pan_id[3], extended_pan_id[2], extended_pan_id[1], extended_pan_id[0]); - } else { - Serial.printf("Network steering failed, status: %d\n", err_status); - } - break; - case ESP_ZB_ZCL_SIGNAL_SET_ATTR_VALUE: - zb_attribute_handler((esp_zb_zcl_set_attr_value_message_t *)p_sg_p); - break; - default: - // Serial.printf("Received Zigbee signal: %d\n", signal_struct->signal_type); - break; - } -} - -/** - * @brief Handles incoming Zigbee attribute changes (e.g., On/Off commands). - */ -static esp_err_t zb_attribute_handler(const esp_zb_zcl_set_attr_value_message_t *message) -{ - if (message->info.status != ESP_ZB_ZCL_STATUS_SUCCESS) { - return ESP_OK; - } - - uint8_t endpoint = message->info.dst_endpoint; - uint16_t cluster_id = message->info.cluster; - uint16_t attr_id = message->attribute.id; - bool is_on = false; - - if (cluster_id == ESP_ZB_ZCL_CLUSTER_ID_ON_OFF && attr_id == ESP_ZB_ZCL_ATTR_ON_OFF_ON_OFF_ID) { - is_on = *(bool *)message->attribute.data.value; - uint8_t servo_num = endpoint - ZIGBEE_ENDPOINT_START; - - Serial.printf("Endpoint %d (Servo %d) command: %s\n", endpoint, servo_num, is_on ? "ON" : "OFF"); - - if (servo_num < CHANNEL_COUNT) { - move_servo(servo_num, is_on ? SERVO_OPEN_ANGLE : SERVO_CLOSED_ANGLE); - } - } - return ESP_OK; -}