User Tools

Site Tools


amc:ss2025:group-a:start

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
amc:ss2025:group-a:start [2025/07/29 08:54] – created 35120_students.hsrwamc:ss2025:group-a:start [2025/07/29 15:11] (current) – [Data analysis] 35120_students.hsrw
Line 1: Line 1:
 +====== Gießwagen - Plant Detection ======
 +
 +By Dylan Elian Huete Arbizu (35120)
 ==== Introduction ==== ==== Introduction ====
  
Line 10: Line 13:
  
 This project demonstrates how low-cost, network-connected sensors and automation hardware can contribute to sustainable practices in agriculture, urban gardening, or landscape management. The following report details both the hardware and software necessary to build the system, so others can replicate and further adapt it to address environmental needs in their own communities. This project demonstrates how low-cost, network-connected sensors and automation hardware can contribute to sustainable practices in agriculture, urban gardening, or landscape management. The following report details both the hardware and software necessary to build the system, so others can replicate and further adapt it to address environmental needs in their own communities.
 +
 +===== Materials and Methods =====
 +
 +==== Materials ====
 +
 +  * ESP32 Development Board: Primary controller running FreeRTOS.
 +
 +  * VL53L8CX ToF Sensor Module: Delivers 8x8 grid distance measurements for object/plant detection.
 +
 +  * Push-Button Switch: User input, event annotation.
 +
 +  * LED, relay, or actuator (connected to GPIO7): Controls irrigation.
 +
 +  * Wiring/Breadboard or PCB: For sensor, switch, and actuator connections.
 +
 +  * Client computer/device: Receives sensor data via TCP.
 +
 +  * Power Supply: For ESP32 and peripherals.
 +
 +  * Wi-Fi Network: For ESP32 to connect and transmit data.
 +
 +==== Pin Assignments ====
 +
 +^ Function                          ^ ESP32 GPIO  ^ Notes                          ^
 +| I2C SCL                           | 9           | ToF sensor                     |
 +| I2C SDA                           | 8           | ToF sensor                     |
 +| ToF sensor reset                  | 5           | XSHUT line                     |
 +| Output (Actuator)                 | 7           | Controls valve/LED/relay       |
 +| Input (positioning marks reader)  | 4           | With internal pull-up enabled  |
 +
 +===== Methods =====
 +
 +The proposed system integrates an ESP32 microcontroller, a VL53L8CX ToF sensor, actuator control, and Wi-Fi-based TCP communication in order to enable intelligent, sustainable irrigation. Below, the implementation approach is detailed in a narrative format, with illustrative code excerpts highlighting key software components.
 +
 +==== System Configuration and Setup ====
 +
 +  * Wi-Fi Networking
 +The ESP32 is configured to join an existing Wi-Fi network as a station. Wi-Fi credentials are embedded in the code, allowing for easy adjustment depending on deployment site:
 +
 +<code c>
 +#define EXAMPLE_ESP_WIFI_SSID "iotlab"
 +#define EXAMPLE_ESP_WIFI_PASS "iotlab18"
 +</code>
 +Connection status is monitored and maintained using ESP-IDF’s event loop and FreeRTOS event groups. This ensures reliable operation even if the access point is temporarily unavailable:
 +
 +<code c>
 +s_wifi_event_group = xEventGroupCreate();
 +ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
 +ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
 +ESP_ERROR_CHECK(esp_wifi_start());
 +</code>
 +  * TCP Server for Data Streaming
 +Once connected to Wi-Fi, the ESP32 runs a TCP server on port 5055. This server streams processed sensor data and event annotations to any client on the local network. The TCP task listens for and accepts new client connections, and handles connection loss gracefully:
 +
 +<code c>
 +server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
 +bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
 +listen(server_sock, 1);
 +// Accept and serve clients
 +</code>
 +  * I2C and Sensor Initialization
 +The VL53L8CX sensor communicates over I²C, using dedicated pins:
 +
 +<code c>
 +#define I2C_SCL GPIO_NUM_9
 +#define I2C_SDA GPIO_NUM_8
 +</code>
 +
 +The code first brings the sensor out of reset via GPIO5 (XSHUT), then initializes it for 8x8 ranging at 10 Hz, ensuring it's ready for environmental monitoring:
 +
 +<code c>
 +Dev.platform.reset_gpio = GPIO_NUM_5;
 +VL53L8CX_Reset_Sensor(&(Dev.platform));
 +ret = vl53l8cx_init(&Dev);
 +ret = vl53l8cx_set_resolution(&Dev, VL53L8CX_RESOLUTION_8X8);
 +ret = vl53l8cx_set_ranging_frequency_hz(&Dev,10);
 +</code>
 +
 +  * Data Acquisition and Plant/Object Detection
 +Periodically (every 100 ms), the ESP32 queries the VL53L8CX for a new distance frame. Object or plant detection is performed by comparing each value to the median of the frame, considering any pixel "close" if it is significantly less than the median (i.e., background distance):
 +
 +<code c>
 +// Compute median as background
 +int bg = median(distance);
 +// Identify pixels indicating presence (e.g., plant detected)
 +if (distance[i] < bg - offset) { object_mask[i] = true; }
 +</code>
 +A centroid is computed for detected zones, estimating the plant's position beneath the sensor.
 +
 +  * Actuator (Irrigation) Control
 +If the detected object is centered (i.e., the plant is beneath the sensor), the output GPIO (GPIO7) is asserted to trigger irrigation; otherwise it remains low, ensuring only occupied zones are watered:
 +
 +<code c>
 +if (/* central pixels detect presence */) {
 +    gpio_set_level(GPIO_NUM_7, 1); // Open valve/activate relay
 +} else {
 +    gpio_set_level(GPIO_NUM_7, 0); // Close valve
 +}
 +</code>
 +  * Button Handling and Event Annotation
 +A push-button (GPIO4) enables manual event annotation. Button interrupts are debounced using a hardware timer for reliability:
 +
 +<code c>
 +gpio_isr_handler_add(BUTTON_PIN, interr_handler, (void*)BUTTON_PIN);
 +// In ISR task:
 +if (current_state == 0 && last_state == 1 && (timestamp - last_change) >= DEBOUNCE_uS) {
 +    // Send a "mark" frame to client
 +}
 +</code>
 +  * Data Transmission and Logging
 +Each sensor frame (including timestamps and any event marks) is immediately transmitted to any connected client via TCP. The raw data (typically a timestamp and 64 distance readings) can be received, visualized, and logged using a Python client.
 +
 +Example packet preparation:
 +
 +<code c>
 +// Prepare buffer for [timestamp][frame]
 +uint8_t sendbuf[8 + FRAME_SIZE * 2];
 +memcpy(sendbuf, &timestamp, 8);
 +memcpy(sendbuf + 8, frame, FRAME_SIZE * 2);
 +// Send to client
 +send(client_sock, sendbuf, sizeof(sendbuf), 0);
 +</code>
 +
 +==== Software Flow ====
 +
 +**Written in C using ESP-IDF framework.**
 +
 +  * Uses FreeRTOS for multitasking: independent tasks for TCP communication, sensor polling, and button handling.
 +
 +  * Hardware timer (GPTimer) ensures accurate event timestamps and debouncing.
 +
 +  * All configuration parameters (SSID, pins, thresholds) are user-adjustable.
 +
 +==== Complete ESP32 code ====
 +<code c>
 +#include <stdbool.h>
 +#include <stdlib.h>
 +#include <stdio.h>
 +#include <string.h>
 +#include "vl53l8cx_api.h"
 +#include "freertos/FreeRTOS.h"
 +#include "freertos/task.h"
 +#include "freertos/event_groups.h"
 +#include "esp_system.h"
 +#include "esp_wifi.h"
 +#include "esp_event.h"
 +#include "esp_log.h"
 +#include "nvs_flash.h"
 +#include "esp_mac.h"
 +#include "lwip/err.h"
 +#include "lwip/sys.h"
 +#include "lwip/sockets.h"
 +#include "driver/gptimer.h"
 +#include "led_indicator.h"
 +
 +#define OUT_GPIO GPIO_NUM_7
 +#define BUTTON_PIN 4
 +#define DEBOUNCE_uS 100000
 +
 +
 +#define EXAMPLE_ESP_WIFI_SSID      "iotlab"
 +#define EXAMPLE_ESP_WIFI_PASS      "iotlab18"
 +#define EXAMPLE_ESP_MAXIMUM_RETRY  5
 +
 +#define TCP_PORT 5055
 +#define FRAME_SIZE 64
 +
 +static EventGroupHandle_t s_wifi_event_group;
 +#define WIFI_CONNECTED_BIT BIT0
 +#define WIFI_FAIL_BIT      BIT1
 +
 +static const char *TAG = "wifi station";
 +static int s_retry_num = 0;
 +
 +// TCP server global socket
 +static int client_sock = -1;
 +static int server_sock = -1;
 +static struct sockaddr_in client_addr;
 +static socklen_t client_addr_len = sizeof(client_addr);
 +
 +// VL53L8CX variables
 +VL53L8CX_Configuration  Dev;
 +VL53L8CX_ResultsData    results;
 +esp_err_t ret;
 +uint16_t frame[FRAME_SIZE];
 +uint16_t mark[FRAME_SIZE] = { [0 ... 63] = 0 };
 +
 +
 +// GPTimer handle
 +static gptimer_handle_t gptimer = NULL;
 +
 +int compare(const void *a, const void *b) {
 +    return (*(int*)a - *(int*)b);
 +}
 +
 +// Returns median of a 64-element array
 +int median(int *data) {
 +    int tmp[64];
 +    memcpy(tmp, data, sizeof(tmp));
 +    qsort(tmp, 64, sizeof(int), compare);
 +    return tmp[31]; // 32nd element is the median
 +}
 +
 +// Converts 1D index to row, col for 8x8
 +void idx_to_rowcol(int idx, int *row, int *col) {
 +    *row = idx / 8;
 +    *col = idx % 8;
 +}
 +
 +// weight: background_distance - value (if object), else 0.0
 +float calculate_weight(int value, int bg, int offset, bool *object) {
 +    if (value < bg - offset) {
 +        *object = true;
 +        return (float)(bg - value);
 +    } else {
 +        *object = false;
 +        return 0.0f;
 +    }
 +}
 +
 +// Find object centroid from 1D (row-major) 64-element array
 +bool find_object_center(
 +    int *distance,        // input: 1D 64-element row-major array
 +    int offset,           // input: threshold offset from background
 +    float *y_c,           // output: centroid y (0-7, row)
 +    float *x_c,           // output: centroid x (0-7, col)
 +    bool *object_mask     // output: 64-element array (true if object)
 +) {
 +    // 1. Find background
 +    int bg = median(distance);
 +
 +    // 2. Initialize and calculate weights and mask
 +    float weights[64] = {0.0f};
 +    bool any_object = false;
 +
 +    for (int i = 0; i < 64; ++i) {
 +        object_mask[i] = false;
 +        weights[i] = calculate_weight(distance[i], bg, offset, &object_mask[i]);
 +        if (object_mask[i]) any_object = true;
 +    }
 +
 +    if (!any_object) {
 +        return false; // no object detected
 +    }
 +
 +    // 3. Calculate weighted centroid
 +    float sum_y = 0.0f, sum_x = 0.0f, sum_w = 0.0f;
 +    for (int i = 0; i < 64; ++i) {
 +        if (weights[i] > 0.0f) {
 +            int row, col;
 +            idx_to_rowcol(i, &row, &col);
 +            sum_y += (float)row * weights[i];
 +            sum_x += (float)col * weights[i];
 +            sum_w += weights[i];
 +        }
 +    }
 +    *y_c = sum_y / sum_w;
 +    *x_c = sum_x / sum_w;
 +
 +    return true; // object detected, centroid calculated
 +}
 +
 +// --- Wi-Fi event handler
 +static void event_handler(void* arg, esp_event_base_t event_base,
 +                                int32_t event_id, void* event_data)
 +{
 +    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
 +        esp_wifi_connect();
 +    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
 +        if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
 +            esp_wifi_connect();
 +            s_retry_num++;
 +            ESP_LOGI(TAG, "retry to connect to the AP");
 +        } else {
 +            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
 +        }
 +        ESP_LOGI(TAG,"connect to the AP fail");
 +    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
 +        ip_event_got_ip_t* event = event_data;
 +        ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
 +        s_retry_num = 0;
 +        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
 +    }
 +}
 +
 +void wifi_init_sta(void)
 +{
 +    s_wifi_event_group = xEventGroupCreate();
 +    ESP_ERROR_CHECK(esp_netif_init());
 +    ESP_ERROR_CHECK(esp_event_loop_create_default());
 +    esp_netif_create_default_wifi_sta();
 +
 +    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
 +    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
 +
 +    esp_event_handler_instance_t instance_any_id;
 +    esp_event_handler_instance_t instance_got_ip;
 +    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
 +                                                        ESP_EVENT_ANY_ID,
 +                                                        &event_handler,
 +                                                        NULL,
 +                                                        &instance_any_id));
 +    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
 +                                                        IP_EVENT_STA_GOT_IP,
 +                                                        &event_handler,
 +                                                        NULL,
 +                                                        &instance_got_ip));
 +
 +    wifi_config_t wifi_config = {
 +        .sta = {
 +            .ssid = EXAMPLE_ESP_WIFI_SSID,
 +            .password = EXAMPLE_ESP_WIFI_PASS,
 +            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
 +        },
 +    };
 +    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
 +    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
 +    ESP_ERROR_CHECK(esp_wifi_start() );
 +
 +    ESP_LOGI(TAG, "wifi_init_sta finished.");
 +
 +    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
 +            WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
 +            pdFALSE,
 +            pdFALSE,
 +            portMAX_DELAY);
 +
 +    if (bits & WIFI_CONNECTED_BIT) {
 +        ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
 +                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
 +    } else if (bits & WIFI_FAIL_BIT) {
 +        ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
 +                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
 +    } else {
 +        ESP_LOGE(TAG, "UNEXPECTED EVENT");
 +    }
 +}
 +
 +// --- GPTimer Setup ---
 +void gptimer_setup(void)
 +{
 +    gptimer_config_t timer_config = {
 +        .clk_src = GPTIMER_CLK_SRC_DEFAULT,
 +        .direction = GPTIMER_COUNT_UP,
 +        .resolution_hz = 1000000, // 1 MHz = 1 tick per microsecond
 +    };
 +    ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer));
 +    ESP_ERROR_CHECK(gptimer_enable(gptimer));
 +    ESP_ERROR_CHECK(gptimer_start(gptimer));
 +}
 +
 +// --- TCP Server Task ---
 +void tcp_server_task(void *pvParameters)
 +{
 +    struct sockaddr_in server_addr;
 +
 +    server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
 +    if (server_sock < 0) {
 +        ESP_LOGE("TCP", "Unable to create socket: errno %d", errno);
 +        vTaskDelete(NULL);
 +        return;
 +    }
 +
 +    int opt = 1;
 +    setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
 +
 +    server_addr.sin_family = AF_INET;
 +    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
 +    server_addr.sin_port = htons(TCP_PORT);
 +
 +    if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
 +        ESP_LOGE("TCP", "Socket unable to bind: errno %d", errno);
 +        close(server_sock);
 +        vTaskDelete(NULL);
 +        return;
 +    }
 +
 +    if (listen(server_sock, 1) < 0) {
 +        ESP_LOGE("TCP", "Error occurred during listen: errno %d", errno);
 +        close(server_sock);
 +        vTaskDelete(NULL);
 +        return;
 +    }
 +
 +    ESP_LOGI("TCP", "TCP server listening on port %d", TCP_PORT);
 +
 +    while (1) {
 +        ESP_LOGI("TCP", "Waiting for client connection...");
 +        client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len);
 +        if (client_sock < 0) {
 +            ESP_LOGE("TCP", "Unable to accept connection: errno %d", errno);
 +            continue;
 +        }
 +        ESP_LOGI("TCP", "Client connected!");
 +        // Block here until client disconnects; actual sending is done in sensor task
 +        while (1) {
 +            char buf[8];
 +            int len = recv(client_sock, buf, sizeof(buf), MSG_DONTWAIT);
 +            if (len == 0) {
 +                ESP_LOGI("TCP", "Client disconnected");
 +                close(client_sock);
 +                client_sock = -1;
 +                break;
 +            }
 +            vTaskDelay(pdMS_TO_TICKS(100));
 +        }
 +    }
 +}
 +
 +// --- VL53L8CX Task ---
 +void vl53l8cx_task(void *pvParameters) {
 +    uint8_t isReady;
 +    bool nozzel_state = true;
 +    while (1) {
 +        ret = vl53l8cx_check_data_ready(&Dev, &isReady);
 +        if (ret == ESP_OK && isReady) {
 +            vl53l8cx_get_ranging_data(&Dev, &results);
 +            for (int i = 0; i < FRAME_SIZE; ++i) {
 +                frame[i] = results.distance_mm[VL53L8CX_NB_TARGET_PER_ZONE*i];
 +            }
 +            // Get timestamp from GPTimer
 +            uint64_t timestamp = 0;
 +            gptimer_get_raw_count(gptimer, &timestamp); // 1 tick = 1 microsecond[1][2][5]
 +            // Prepare buffer: [timestamp][frame]
 +            uint8_t sendbuf[8 + FRAME_SIZE * 2];
 +            memcpy(sendbuf, &timestamp, 8);
 +            memcpy(sendbuf + 8, frame, FRAME_SIZE * 2);
 +            if(frame[35] <= 300 && frame[36] <= 300 && frame[43] <= 300 && frame[44] <= 300 && nozzel_state){
 +                gpio_set_level(OUT_GPIO, 1);
 +                ESP_LOGI("GPIO_TEST", "GPIO set to HIGH");
 +                nozzel_state = false;
 +                
 +            }else if(frame[35] >= 300 && frame[36] >= 300 && frame[43] >= 300 && frame[44] >= 300 && !nozzel_state){
 +                gpio_set_level(OUT_GPIO, 0);
 +                ESP_LOGI("GPIO_TEST", "GPIOs set to LOW");
 +                nozzel_state = true;
 +            }
 +            // Send to TCP client if connected
 +            if (client_sock >= 0) {
 +                int to_send = sizeof(sendbuf);
 +                int sent = send(client_sock, sendbuf, to_send, 0);
 +                if (sent < 0) {
 +                    ESP_LOGE("TCP", "Send failed: errno %d", errno);
 +                    close(client_sock);
 +                    client_sock = -1;
 +                }
 +            }
 +
 +        }
 +        //vTaskDelay(pdMS_TO_TICKS(100)); // 10Hz
 +    }
 +}
 +
 +void config_gpio(){
 +    gpio_config_t io_conf = {
 +        .pin_bit_mask = (1ULL << BUTTON_PIN),
 +        .mode = GPIO_MODE_INPUT,
 +        .intr_type = GPIO_INTR_NEGEDGE,
 +        .pull_up_en = 1
 +    };
 +    gpio_config(&io_conf);
 +    gpio_config_t out_conf = {
 +        .pin_bit_mask = (1ULL << OUT_GPIO),
 +        .mode = GPIO_MODE_OUTPUT,
 +    };
 +    gpio_config(&out_conf);
 +}
 +static QueueHandle_t interr_queue = NULL;
 +
 +void IRAM_ATTR interr_handler(void* arg) {
 +    uint32_t pin = (uint32_t) arg;
 +    xQueueSendFromISR(interr_queue, &pin, NULL);
 +}
 +
 +static uint64_t last_change = 0;
 +static int last_state = 1; // Asssuming pull-up: 1 = no pressed, 0 = pressed
 +
 +void task_pin_reading(void* params) {
 +    uint32_t pin_received;
 +    while (1) {
 +        if (xQueueReceive(interr_queue, &pin_received, portMAX_DELAY)) {
 +            // Read pin state
 +            int current_state = gpio_get_level(pin_received);
 +            // Get timestamp from GPTimer
 +            uint64_t timestamp = 0;
 +            gptimer_get_raw_count(gptimer, &timestamp); // 1 tick = 1 microsecond[1][2][5]
 +            // If the state has changed and enough time has passed
 +            if (current_state == 0 && last_state == 1 && (timestamp - last_change) >= DEBOUNCE_uS) {
 +                last_state = 0;
 +                last_change = timestamp;
 +                // Prepare buffer: [timestamp][frame]
 +                uint8_t sendbuf[8 + FRAME_SIZE * 2];
 +                memcpy(sendbuf, &timestamp, 8);
 +                memcpy(sendbuf + 8, mark, FRAME_SIZE * 2);
 +                // Send to TCP client if connected
 +                if (client_sock >= 0) {
 +                    int to_send = sizeof(sendbuf);
 +                    int sent = send(client_sock, sendbuf, to_send, 0);
 +                    if (sent < 0) {
 +                        ESP_LOGE("TCP", "Send failed: errno %d", errno);
 +                        close(client_sock);
 +                        client_sock = -1;
 +                    }
 +                }
 +            }else if(current_state == 1 && last_state == 0){
 +                last_state = 1;
 +                last_change = timestamp;
 +            }
 +        }
 +    }
 +}
 +
 +
 +void app_main(void)
 +{
 +    config_gpio();
 +    
 +    // create queue for 10 events
 +    interr_queue = xQueueCreate(10, sizeof(uint32_t));
 +    
 +    // install service for ISR
 +    gpio_install_isr_service(0);
 +    gpio_isr_handler_add(BUTTON_PIN, interr_handler, (void*)BUTTON_PIN);
 +
 +    //Initialize NVS
 +    esp_err_t ret = nvs_flash_init();
 +    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
 +      ESP_ERROR_CHECK(nvs_flash_erase());
 +      ret = nvs_flash_init();
 +    }
 +    ESP_ERROR_CHECK(ret);
 +
 +    ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
 +    wifi_init_sta();
 +
 +    // Setup GPTimer
 +    gptimer_setup();
 +
 +    //Define the i2c bus configuration
 +    i2c_port_t i2c_port = I2C_NUM_1;
 +    i2c_master_bus_config_t i2c_mst_config = {
 +            .clk_source = I2C_CLK_SRC_DEFAULT,
 +            .i2c_port = i2c_port,
 +            .scl_io_num = 9,
 +            .sda_io_num = 8,
 +            .glitch_ignore_cnt = 7,
 +            .flags.enable_internal_pullup = true,
 +    };
 +
 +    i2c_master_bus_handle_t bus_handle;
 +    ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_mst_config, &bus_handle));
 +
 +    //Define the i2c device configuration
 +    i2c_device_config_t dev_cfg = {
 +            .dev_addr_length = I2C_ADDR_BIT_LEN_7,
 +            .device_address = VL53L8CX_DEFAULT_I2C_ADDRESS >> 1,
 +            .scl_speed_hz = VL53L8CX_MAX_CLK_SPEED,
 +    };
 +
 +    Dev.platform.bus_config = i2c_mst_config;
 +    i2c_master_bus_add_device(bus_handle, &dev_cfg, &Dev.platform.handle);
 +
 +    Dev.platform.reset_gpio = GPIO_NUM_5;
 +    VL53L8CX_Reset_Sensor(&(Dev.platform));
 +
 +    uint8_t isAlive = 0;
 +    ret = vl53l8cx_is_alive(&Dev, &isAlive);
 +    if(!isAlive || ret != ESP_OK)
 +    {
 +        printf("VL53L8CX not detected at requested address\n");
 +        return;
 +    }
 +
 +    ret = vl53l8cx_init(&Dev);
 +    if (ret != ESP_OK) {
 +        printf("Sensor init failed: %d\n", ret);
 +        return;
 +    }
 +
 +    ret = vl53l8cx_set_resolution(&Dev, VL53L8CX_RESOLUTION_8X8);
 +    if (ret != ESP_OK) {
 +        printf("Set resolution failed: %d\n", ret);
 +        return;
 +    }
 +    printf("VL53L8CX ULD ready ! (Version : %s)\n", VL53L8CX_API_REVISION);
 +
 +    ret = vl53l8cx_set_ranging_frequency_hz(&Dev,10);
 +    if (ret != ESP_OK) {
 +        printf("Set ranging frequency failed: %d\n", ret);
 +        return;
 +    }
 +    ret = vl53l8cx_set_sharpener_percent(&Dev, 40);
 +    if (ret != ESP_OK) {
 +        printf("Set sharpener percent failed: %d\n", ret);
 +        return;
 +    }
 +    ret = vl53l8cx_set_target_order(&Dev, VL53L8CX_TARGET_ORDER_STRONGEST);
 +    if (ret != ESP_OK) {
 +        printf("Set target order failed: %d\n", ret);
 +        return;
 +    }
 +    ret = vl53l8cx_start_ranging(&Dev);
 +    if (ret != ESP_OK) {
 +        printf("Set start ranging failed: %d\n", ret);
 +        return;
 +    }
 +
 +    // Start TCP server and sensor tasks
 +    xTaskCreate(tcp_server_task, "tcp_server", 4096, NULL, 5, NULL);
 +    xTaskCreate(vl53l8cx_task, "vl53l8cx_task", 6144, NULL, 5, NULL);
 +    xTaskCreate(task_pin_reading, "pin_reading", 2048, NULL, 5, NULL);
 +}
 +
 +</code>
 +==== Assembly ====
 +
 +  * Wire the ESP32 to the VL53L8CX using I2C (SCL: 9, SDA: 8) and sensor XSHUT to GPIO5.
 +
 +  * Connect button to GPIO4 (with internal or external pull-up to 3.3V).
 +
 +  * Connect actuator (e.g., relay valve) control input to GPIO7.
 +
 +  * Flash the ESP32 with the provided code, making any adjustments for your setup.
 +
 +  * Start ESP32; ensure it connects to Wi-Fi.
 +
 +  * Connect a client to ESP32's IP on TCP port 5055 to view or log streaming data.
 +==== Circuit diagram ====
 +
 +{{ :amc:ss2025:group-a:satel-vl53l8cx-circuit.png?400 |VL53L8CX connected to ESP32}}
 +
 +==== Client Software for Data Reception and Visualization ====
 +A fully functional Python client application logs incoming data and visualizes it as a heatmap in real time:
 +
 +  * Raw Data Reception: Receives packets of 136 bytes each (64 x 2-byte sensor readings + 8 bytes timestamp) from the ESP32 over a TCP socket.
 +
 +  * Data Logging: Writes each received frame with microsecond-precision timestamps to a CSV file for later analysis.
 +
 +  * Live Visualization: Uses Matplotlib (embedded in Tkinter) to display a color-mapped 8x8 (upscaled to 64x64) heatmap of the measured distances.
 +
 +  * Threading/Concurrency: Uses a background thread to handle data reception without blocking the GUI.
 +
 +  * Safe Shutdown: Ensures sockets and files are properly closed when the application exits.
 +
 +<code python>
 +import socket
 +import struct
 +import csv
 +import tkinter as tk
 +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
 +import matplotlib.pyplot as plt
 +import numpy as np
 +from scipy.ndimage import zoom
 +import threading
 +
 +ESP32_IP = "192.168.2.189"      # Set your ESP32 IP address
 +ESP32_PORT = 5055
 +CSV_FILENAME = "vl53l8cx_data.csv"
 +FRAME_SIZE = 8 + 64 * 2         # 8 bytes timestamp + 128 bytes frame data
 +UPSCALE_FACTOR = 8              # For smooth 8x8 -> 64x64 heatmap
 +
 +latest_matrix = None
 +latest_timestamp = None
 +lock = threading.Lock()
 +
 +def re_range(pMatrix):
 +    pMatrix = np.array(pMatrix)
 +    nMax = pMatrix.max()
 +    rat = nMax / 5000 if nMax != 0 else 1
 +    r_matrix = pMatrix / rat
 +    return r_matrix.astype(int)
 +
 +def data_receiver(sock, writer, csvfile):
 +    global latest_matrix, latest_timestamp
 +    while True:
 +        data = b''
 +        while len(data) < FRAME_SIZE:
 +            try:
 +                packet = sock.recv(FRAME_SIZE - len(data))
 +            except socket.timeout:
 +                continue
 +            if not packet:
 +                print("Connection closed by ESP32.")
 +                return
 +            data += packet
 +        timestamp_us = struct.unpack('<Q', data[:8])[0]
 +        distances = struct.unpack('<64H', data[8:])
 +        matrix = np.array(distances, dtype=np.uint16).reshape(8, 8)
 +        writer.writerow([timestamp_us] + list(matrix.flatten()))
 +        csvfile.flush()
 +        with lock:
 +            latest_matrix = matrix
 +            latest_timestamp = timestamp_us
 +
 +def update_gui():
 +    with lock:
 +        matrix = None if latest_matrix is None else latest_matrix.copy()
 +        timestamp = latest_timestamp
 +    if matrix is not None:
 +        matrix = re_range(matrix)
 +        high_res_matrix = zoom(matrix, UPSCALE_FACTOR, order=3)
 +        im.set_array(high_res_matrix)
 +        ax.set_title(f"Timestamp: {timestamp}")
 +        canvas.draw()
 +    root.after(100, update_gui)  # 10 Hz refresh rate
 +
 +def clean_exit():
 +    global running
 +    running = False
 +    try:
 +        sock.close()
 +    except Exception:
 +        pass
 +    try:
 +        csvfile.close()
 +    except Exception:
 +        pass
 +    root.destroy()
 +
 +# Socket connection and main setup
 +sock = socket.create_connection((ESP32_IP, ESP32_PORT))
 +sock.settimeout(1.0)
 +csvfile = open(CSV_FILENAME, mode='w', newline='')
 +writer = csv.writer(csvfile)
 +header = ["timestamp_us"] + [f"zone_{i}" for i in range(64)]
 +writer.writerow(header)
 +
 +receiver_thread = threading.Thread(target=data_receiver, args=(sock, writer, csvfile), daemon=True)
 +receiver_thread.start()
 +
 +root = tk.Tk()
 +root.title("Live Sensor Heatmap")
 +
 +fig, ax = plt.subplots()
 +im = ax.imshow(np.zeros((8 * UPSCALE_FACTOR, 8 * UPSCALE_FACTOR)), cmap='viridis', vmin=0, vmax=5000)
 +canvas = FigureCanvasTkAgg(fig, master=root)
 +canvas.get_tk_widget().pack()
 +
 +close_button = tk.Button(root, text="Close", command=clean_exit, font=("Arial", 12), fg="red")
 +close_button.pack(pady=10)
 +
 +root.after(100, update_gui)
 +root.protocol("WM_DELETE_WINDOW", clean_exit)
 +root.mainloop()
 +</code>
 +
 +**Graphic result**
 +{{ :amc:ss2025:group-a:screenshot_2025-07-29_094906.png?400 |Heatmap created from sensor readings where a plant is detected}}
 +
 +===== Results =====
 +
 +==== Functional Testing ====
 +
 +**When the ESP32 is powered and connected to Wi-Fi:**
 +
 +  * The VL53L8CX sensor continuously scans its field, providing an 8x8 distance map.
 +
 +  * The ESP32 detects objects (e.g., plant) based on pixels closer than the background by a given threshold.
 +
 +  * When an object is close to the central region of the sensor frame (representing a plant directly under the sensor), GPIO7 is activated—demonstrating selective and efficient irrigation.
 +
 +  * Actuator remains off when no plant is detected or it is not near the center, preventing watering of bare soil and reducing excessive chemical/fertilizer application.
 +
 +  * All sensor frames and button events are timestamped and streamed live to TCP clients for monitoring or data analysis.
 +
 +==== Environmental Impact ====
 +
 +  * **Water Use Reduction:** System only waters when plant presence is confirmed and in the precise location, minimizing waste.
 +
 +  * **Reduced Chemical Runoff:** As irrigation is limited to when and where needed, less fertilizer is washed into groundwater.
 +
 +  * **Data Gathering:** Collected distance/time data supports further optimization, trend analysis, and integration with weather/fertilization schedules.
 +
 +==== Reliability ====
 +
 +  * Button interrupts reliably send annotated "mark" frames for event logging or manual input.
 +
 +  * The system is resilient to network disconnects, with reconnection and data buffering as programmed.
 +==== Pictures of the prototype ====
 +<imgcaption image1|>{{:amc:ss2025:group-a:20250703_180639.jpg?nolink&200|}}</imgcaption>
 +<imgcaption image2|>{{:amc:ss2025:group-a:20250703_180645.jpg?nolink&200|}}</imgcaption>
 +<imgcaption image3|>{{:amc:ss2025:group-a:20250703_180651.jpg?nolink&200|}}</imgcaption>
 +
 +==== Data analysis ====
 +
 +**Data Cleaning:**
 +Selection of a segment of interest, discarding outliers or unreliable data.
 +Due some hardware limitations, the reading of the marks was not reliable enough, therefore the data had to be conditioned manually.
 +The expected data is 12 marks readings, but due to problems in the data acquisition there were just 8 marks usable, and there were also some duplicated readings, this was determined manually based on the time and pattern expected.
 +
 +**Path Segmentation:**
 +getPath(df) constructs segments ("paths") marked by zero in the zone_1 column, grouping each set of four zeros as a new path.
 +
 +**Speed Calculation:**
 +mean_speed(data) estimates the average speed between time marks by measuring intervals between zeros.
 +
 +  * After interpolating sensor data along the location axis (temporal/spatial sequence), each 8×8 frame is upscaled to 64×64 pixels using cubic spline interpolation (zoom with order=3).
 +
 +  * This spatial interpolation significantly enhances intra-frame resolution.
 +
 +  * The aggregation step then merges these larger frames horizontally with value averaging over overlapping columns, preserving continuity.
 +
 +  * The plot displays a much higher-resolution heatmap representing the sensor data over the scanned path.
 +
 +<code python>
 +import pandas as pd
 +import matplotlib.pyplot as plt
 +import numpy as np
 +from scipy.ndimage import zoom
 +from scipy.interpolate import interp1d
 +
 +fname1 = "vl53l8cx_data_third_test.csv"
 +df = pd.read_csv(fname1)
 +#Since the marks acquisition is not reliable enough, the data has to be treated manually to discard unuseful data
 +dataFrame = df.loc[1419:2618].drop(2360)
 +
 +def getPath(df):
 +    idx = list(df.loc[df["zone_1"]==0].index)
 +    path_list = []
 +    for i in range(len(idx)):
 +        if (i+1)%4 == 0 and i != 0:
 +            path_list.append(df.loc[idx[i-3]:idx[i]])
 +            #print(f"{i} : [{idx[i-3]}:{idx[i]}]")
 +    return path_list
 +
 +def mean_speed(data):
 +    idx = list(data.loc[data["zone_1"]==0].index)
 +    t1 = data.loc[idx[1]]["timestamp_us"] - data.loc[idx[0]]["timestamp_us"]
 +    t2 = data.loc[idx[2]]["timestamp_us"] - data.loc[idx[1]]["timestamp_us"]
 +    t3 = data.loc[idx[3]]["timestamp_us"] - data.loc[idx[2]]["timestamp_us"]
 +    spd1 = 200/t1
 +    spd2 = 1000/t2
 +    spd3 = 1000/t3
 +    return (spd1+spd2+spd3)/3
 +
 +paths = getPath(dataFrame)
 +
 +# 1. Calculate continuous locations and concatenate all paths as before:
 +for i in range(len(paths)):
 +    loc = (paths[i]["timestamp_us"] - paths[i].iloc[0]["timestamp_us"]) * mean_speed(paths[i])
 +    paths[i] = pd.concat([paths[i], loc.to_frame('location')], axis=1)
 +    paths[i] = paths[i].drop(list(paths[i].loc[paths[i]["zone_1"] == 0].index))
 +
 +path_t = pd.concat(paths)
 +path_t = path_t.sort_values("location")
 +
 +locations = path_t['location'].values
 +data_values = path_t.iloc[:, 1:-1].values  # Adjust indices if your columns differ
 +
 +# 2. Interpolate sensor columns independently on a uniform location grid:
 +min_loc, max_loc = np.min(locations), np.max(locations)
 +num_interp_points = int(np.ceil(max_loc - min_loc)) + 1
 +interp_locations = np.linspace(min_loc, max_loc, num_interp_points)
 +
 +interp_data = np.zeros((num_interp_points, data_values.shape[1]))
 +for col in range(data_values.shape[1]):
 +    interp_func = interp1d(locations, data_values[:, col], kind='linear', fill_value='extrapolate')
 +    interp_data[:, col] = interp_func(interp_locations)
 +
 +# 3. Reshape each row into 8x8 frames:
 +num_frames = interp_data.shape[0]
 +frame_height, frame_width = 8, 8
 +frames_8x8 = [interp_data[i].reshape(frame_height, frame_width) for i in range(num_frames)]
 +
 +# 4. Interpolate each 8x8 frame to 64x64 using scipy.ndimage.zoom:
 +zoom_factor = 64 / 8  # 8x to 64x scaling
 +
 +frames_64x64 = [zoom(frame, zoom_factor, order=3) for frame in frames_8x8]  # cubic spline interpolation (order=3)
 +
 +# 5. Aggregate frames horizontally with averaging over overlaps (same as before):
 +max_offset = num_frames - 1
 +final_width = max_offset + 64  # width after scaling frames to 64 wide
 +final_frame_64 = np.zeros((64, final_width))
 +count_64 = np.zeros((64, final_width))
 +
 +for i, frame in enumerate(frames_64x64):
 +    offset = i
 +    final_frame_64[:, offset:offset+64] += frame
 +    count_64[:, offset:offset+64] += 1
 +
 +aggregated_64 = np.divide(final_frame_64, count_64, out=np.zeros_like(final_frame_64), where=count_64 != 0)
 +
 +# 6. Plot the aggregated 64x wide frame:
 +plt.figure(figsize=(final_width / 16, 8))  # Adjust size for clarity
 +plt.imshow(aggregated_64, cmap='viridis', aspect='auto')
 +plt.colorbar(label='Value')
 +plt.title("Aggregated Large Frame with 8x8 to 64x64 Spatial Interpolation")
 +plt.xlabel('Columns (scaled)')
 +plt.ylabel('Rows (scaled)')
 +plt.show()
 +</code>
 +
 +{{ :amc:ss2025:group-a:screenshot_2025-07-29_142824.png?800 |Map of the path of the Gießwagen}}
 +===== Discussion =====
 +
 +  * This system showcases the potential of integrating low-cost sensor networks and automation for sustainable environmental stewardship:
 +
 +  * Precision Irrigation: Only waters when plant is actually present, avoiding traditional timer-based schemes that can waste water and leach chemicals.
 +
 +  * Scalability: Multiple ESP32/sensor nodes can be deployed across large fields or greenhouses, each acting independently but monitored from a central server.
 +
 +  * Customization: Sensor thresholds, actuator logic, and even fertilization scheduling can be tailored using the streamed data, allowing fine-grained environmental control.
 +
 +==== Limitations & Improvements: ====
 +
 +
 +  * Current system is distance-based; integrating soil moisture or plant health sensors could further refine watering decisions.
 +
 +  * Wireless reliability is dependent on network strength; alternative protocols (e.g., LoRa) could be used in rural deployments.
 +
 +  * Data encryption/authentication could be added for more secure remote management.
 +
 +In summary, this project presents a practical, adaptable example of how sensor-driven automation can help address water and chemical conservation challenges in environmental and agricultural settings. The design and methods are fully replicable, providing a baseline for further innovation and environmental impact.
amc/ss2025/group-a/start.1753772052.txt.gz · Last modified: 2025/07/29 08:54 by 35120_students.hsrw