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

Both sides previous revisionPrevious revision
Next revision
Previous revision
amc:ss2025:group-a:start [2025/07/29 09:36] – [System Configuration and Setup] 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 31: Line 34:
   * Wi-Fi Network: For ESP32 to connect and transmit data.   * Wi-Fi Network: For ESP32 to connect and transmit data.
  
-===== Pin Assignments =====+==== Pin Assignments ====
  
 ^ Function                          ^ ESP32 GPIO  ^ Notes                          ^ ^ Function                          ^ ESP32 GPIO  ^ Notes                          ^
Line 143: Line 146:
   * All configuration parameters (SSID, pins, thresholds) are user-adjustable.   * 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 ==== ==== Assembly ====
  
Line 156: Line 640:
  
   * Connect a client to ESP32's IP on TCP port 5055 to view or log streaming data.   * 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 ===== ===== Results =====
Line 186: Line 791:
  
   * The system is resilient to network disconnects, with reconnection and data buffering as programmed.   * 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>
  
-==== Discussion ====+{{ :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:   * This system showcases the potential of integrating low-cost sensor networks and automation for sustainable environmental stewardship:
amc/ss2025/group-a/start.1753774617.txt.gz · Last modified: 2025/07/29 09:36 by 35120_students.hsrw