amc:ss2025:group-a:start
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
amc:ss2025:group-a:start [2025/07/29 09:07] – 35120_students.hsrw | amc: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 | ^ Function | ||
Line 40: | Line 43: | ||
| Input (positioning marks reader) | | Input (positioning marks reader) | ||
+ | ===== Methods ===== | ||
+ | |||
+ | The proposed system integrates an ESP32 microcontroller, | ||
+ | |||
+ | ==== 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 " | ||
+ | #define EXAMPLE_ESP_WIFI_PASS " | ||
+ | </ | ||
+ | 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, | ||
+ | ESP_ERROR_CHECK(esp_wifi_start()); | ||
+ | </ | ||
+ | * 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, | ||
+ | |||
+ | <code c> | ||
+ | server_sock = socket(AF_INET, | ||
+ | bind(server_sock, | ||
+ | listen(server_sock, | ||
+ | // Accept and serve clients | ||
+ | </ | ||
+ | * 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 | ||
+ | </ | ||
+ | |||
+ | 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(& | ||
+ | ret = vl53l8cx_init(& | ||
+ | ret = vl53l8cx_set_resolution(& | ||
+ | ret = vl53l8cx_set_ranging_frequency_hz(& | ||
+ | </ | ||
+ | |||
+ | * Data Acquisition and Plant/ | ||
+ | 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 " | ||
+ | |||
+ | <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; } | ||
+ | </ | ||
+ | A centroid is computed for detected zones, estimating the plant' | ||
+ | |||
+ | * 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, | ||
+ | } else { | ||
+ | gpio_set_level(GPIO_NUM_7, | ||
+ | } | ||
+ | </ | ||
+ | * 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, | ||
+ | // In ISR task: | ||
+ | if (current_state == 0 && last_state == 1 && (timestamp - last_change) >= DEBOUNCE_uS) { | ||
+ | // Send a " | ||
+ | } | ||
+ | </ | ||
+ | * 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, | ||
+ | memcpy(sendbuf + 8, frame, FRAME_SIZE * 2); | ||
+ | // Send to client | ||
+ | send(client_sock, | ||
+ | </ | ||
+ | |||
+ | ==== Software Flow ==== | ||
+ | |||
+ | **Written in C using ESP-IDF framework.** | ||
+ | |||
+ | * Uses FreeRTOS for multitasking: | ||
+ | |||
+ | * Hardware timer (GPTimer) ensures accurate event timestamps and debouncing. | ||
+ | |||
+ | * All configuration parameters (SSID, pins, thresholds) are user-adjustable. | ||
+ | |||
+ | ==== Complete ESP32 code ==== | ||
+ | <code c> | ||
+ | #include < | ||
+ | #include < | ||
+ | #include < | ||
+ | #include < | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | #include " | ||
+ | |||
+ | #define OUT_GPIO GPIO_NUM_7 | ||
+ | #define BUTTON_PIN 4 | ||
+ | #define DEBOUNCE_uS 100000 | ||
+ | |||
+ | |||
+ | #define EXAMPLE_ESP_WIFI_SSID | ||
+ | #define EXAMPLE_ESP_WIFI_PASS | ||
+ | #define EXAMPLE_ESP_MAXIMUM_RETRY | ||
+ | |||
+ | #define TCP_PORT 5055 | ||
+ | #define FRAME_SIZE 64 | ||
+ | |||
+ | static EventGroupHandle_t s_wifi_event_group; | ||
+ | #define WIFI_CONNECTED_BIT BIT0 | ||
+ | #define WIFI_FAIL_BIT | ||
+ | |||
+ | 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 | ||
+ | VL53L8CX_ResultsData | ||
+ | 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), | ||
+ | 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, | ||
+ | int offset, | ||
+ | float *y_c, // output: centroid y (0-7, row) | ||
+ | float *x_c, // output: centroid x (0-7, col) | ||
+ | bool *object_mask | ||
+ | ) { | ||
+ | // 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], | ||
+ | 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, | ||
+ | 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, | ||
+ | } else { | ||
+ | xEventGroupSetBits(s_wifi_event_group, | ||
+ | } | ||
+ | ESP_LOGI(TAG," | ||
+ | } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { | ||
+ | ip_event_got_ip_t* event = event_data; | ||
+ | ESP_LOGI(TAG, | ||
+ | s_retry_num = 0; | ||
+ | xEventGroupSetBits(s_wifi_event_group, | ||
+ | } | ||
+ | } | ||
+ | |||
+ | 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(& | ||
+ | |||
+ | 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, | ||
+ | & | ||
+ | NULL, | ||
+ | & | ||
+ | ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, | ||
+ | IP_EVENT_STA_GOT_IP, | ||
+ | & | ||
+ | NULL, | ||
+ | & | ||
+ | |||
+ | 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, | ||
+ | ESP_ERROR_CHECK(esp_wifi_start() ); | ||
+ | |||
+ | ESP_LOGI(TAG, | ||
+ | |||
+ | 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, | ||
+ | | ||
+ | } else if (bits & WIFI_FAIL_BIT) { | ||
+ | ESP_LOGI(TAG, | ||
+ | | ||
+ | } else { | ||
+ | ESP_LOGE(TAG, | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // --- 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(& | ||
+ | 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, | ||
+ | if (server_sock < 0) { | ||
+ | ESP_LOGE(" | ||
+ | vTaskDelete(NULL); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | int opt = 1; | ||
+ | setsockopt(server_sock, | ||
+ | |||
+ | 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, | ||
+ | ESP_LOGE(" | ||
+ | close(server_sock); | ||
+ | vTaskDelete(NULL); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (listen(server_sock, | ||
+ | ESP_LOGE(" | ||
+ | close(server_sock); | ||
+ | vTaskDelete(NULL); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | ESP_LOGI(" | ||
+ | |||
+ | while (1) { | ||
+ | ESP_LOGI(" | ||
+ | client_sock = accept(server_sock, | ||
+ | if (client_sock < 0) { | ||
+ | ESP_LOGE(" | ||
+ | continue; | ||
+ | } | ||
+ | ESP_LOGI(" | ||
+ | // Block here until client disconnects; | ||
+ | while (1) { | ||
+ | char buf[8]; | ||
+ | int len = recv(client_sock, | ||
+ | if (len == 0) { | ||
+ | ESP_LOGI(" | ||
+ | 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(& | ||
+ | if (ret == ESP_OK && isReady) { | ||
+ | vl53l8cx_get_ranging_data(& | ||
+ | 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, | ||
+ | // Prepare buffer: [timestamp][frame] | ||
+ | uint8_t sendbuf[8 + FRAME_SIZE * 2]; | ||
+ | memcpy(sendbuf, | ||
+ | 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, | ||
+ | ESP_LOGI(" | ||
+ | nozzel_state = false; | ||
+ | | ||
+ | }else if(frame[35] >= 300 && frame[36] >= 300 && frame[43] >= 300 && frame[44] >= 300 && !nozzel_state){ | ||
+ | gpio_set_level(OUT_GPIO, | ||
+ | ESP_LOGI(" | ||
+ | nozzel_state = true; | ||
+ | } | ||
+ | // Send to TCP client if connected | ||
+ | if (client_sock >= 0) { | ||
+ | int to_send = sizeof(sendbuf); | ||
+ | int sent = send(client_sock, | ||
+ | if (sent < 0) { | ||
+ | ESP_LOGE(" | ||
+ | close(client_sock); | ||
+ | client_sock = -1; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | } | ||
+ | // | ||
+ | } | ||
+ | } | ||
+ | |||
+ | 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(& | ||
+ | gpio_config_t out_conf = { | ||
+ | .pin_bit_mask = (1ULL << OUT_GPIO), | ||
+ | .mode = GPIO_MODE_OUTPUT, | ||
+ | }; | ||
+ | gpio_config(& | ||
+ | } | ||
+ | static QueueHandle_t interr_queue = NULL; | ||
+ | |||
+ | void IRAM_ATTR interr_handler(void* arg) { | ||
+ | uint32_t pin = (uint32_t) arg; | ||
+ | xQueueSendFromISR(interr_queue, | ||
+ | } | ||
+ | |||
+ | 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, | ||
+ | // Read pin state | ||
+ | int current_state = gpio_get_level(pin_received); | ||
+ | // Get timestamp from GPTimer | ||
+ | uint64_t timestamp = 0; | ||
+ | gptimer_get_raw_count(gptimer, | ||
+ | // 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, | ||
+ | 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, | ||
+ | if (sent < 0) { | ||
+ | ESP_LOGE(" | ||
+ | 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, | ||
+ | | ||
+ | // install service for ISR | ||
+ | gpio_install_isr_service(0); | ||
+ | gpio_isr_handler_add(BUTTON_PIN, | ||
+ | |||
+ | // | ||
+ | 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, | ||
+ | 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(& | ||
+ | |||
+ | //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.platform.reset_gpio = GPIO_NUM_5; | ||
+ | VL53L8CX_Reset_Sensor(& | ||
+ | |||
+ | uint8_t isAlive = 0; | ||
+ | ret = vl53l8cx_is_alive(& | ||
+ | if(!isAlive || ret != ESP_OK) | ||
+ | { | ||
+ | printf(" | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | ret = vl53l8cx_init(& | ||
+ | if (ret != ESP_OK) { | ||
+ | printf(" | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | ret = vl53l8cx_set_resolution(& | ||
+ | if (ret != ESP_OK) { | ||
+ | printf(" | ||
+ | return; | ||
+ | } | ||
+ | printf(" | ||
+ | |||
+ | ret = vl53l8cx_set_ranging_frequency_hz(& | ||
+ | if (ret != ESP_OK) { | ||
+ | printf(" | ||
+ | return; | ||
+ | } | ||
+ | ret = vl53l8cx_set_sharpener_percent(& | ||
+ | if (ret != ESP_OK) { | ||
+ | printf(" | ||
+ | return; | ||
+ | } | ||
+ | ret = vl53l8cx_set_target_order(& | ||
+ | if (ret != ESP_OK) { | ||
+ | printf(" | ||
+ | return; | ||
+ | } | ||
+ | ret = vl53l8cx_start_ranging(& | ||
+ | if (ret != ESP_OK) { | ||
+ | printf(" | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // Start TCP server and sensor tasks | ||
+ | xTaskCreate(tcp_server_task, | ||
+ | xTaskCreate(vl53l8cx_task, | ||
+ | xTaskCreate(task_pin_reading, | ||
+ | } | ||
+ | |||
+ | </ | ||
+ | ==== 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' | ||
+ | ==== Circuit diagram ==== | ||
+ | |||
+ | {{ : | ||
+ | |||
+ | ==== 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: | ||
+ | |||
+ | * Threading/ | ||
+ | |||
+ | * 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 = " | ||
+ | ESP32_PORT = 5055 | ||
+ | CSV_FILENAME = " | ||
+ | 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, | ||
+ | global latest_matrix, | ||
+ | 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(" | ||
+ | return | ||
+ | data += packet | ||
+ | timestamp_us = struct.unpack('< | ||
+ | distances = struct.unpack('< | ||
+ | matrix = np.array(distances, | ||
+ | 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, | ||
+ | im.set_array(high_res_matrix) | ||
+ | ax.set_title(f" | ||
+ | canvas.draw() | ||
+ | root.after(100, | ||
+ | |||
+ | 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, | ||
+ | sock.settimeout(1.0) | ||
+ | csvfile = open(CSV_FILENAME, | ||
+ | writer = csv.writer(csvfile) | ||
+ | header = [" | ||
+ | writer.writerow(header) | ||
+ | |||
+ | receiver_thread = threading.Thread(target=data_receiver, | ||
+ | receiver_thread.start() | ||
+ | |||
+ | root = tk.Tk() | ||
+ | root.title(" | ||
+ | |||
+ | fig, ax = plt.subplots() | ||
+ | im = ax.imshow(np.zeros((8 * UPSCALE_FACTOR, | ||
+ | canvas = FigureCanvasTkAgg(fig, | ||
+ | canvas.get_tk_widget().pack() | ||
+ | |||
+ | close_button = tk.Button(root, | ||
+ | close_button.pack(pady=10) | ||
+ | |||
+ | root.after(100, | ||
+ | root.protocol(" | ||
+ | root.mainloop() | ||
+ | </ | ||
+ | |||
+ | **Graphic result** | ||
+ | {{ : | ||
+ | |||
+ | ===== 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/ | ||
+ | |||
+ | * All sensor frames and button events are timestamped and streamed live to TCP clients for monitoring or data analysis. | ||
+ | |||
+ | ==== Environmental Impact ==== | ||
+ | |||
+ | * **Water Use Reduction: | ||
+ | |||
+ | * **Reduced Chemical Runoff:** As irrigation is limited to when and where needed, less fertilizer is washed into groundwater. | ||
+ | |||
+ | * **Data Gathering: | ||
+ | |||
+ | ==== Reliability ==== | ||
+ | |||
+ | * Button interrupts reliably send annotated " | ||
+ | |||
+ | * The system is resilient to network disconnects, | ||
+ | ==== Pictures of the prototype ==== | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | |||
+ | ==== Data analysis ==== | ||
+ | |||
+ | **Data Cleaning:** | ||
+ | Selection of a segment of interest, discarding outliers or unreliable data. | ||
+ | Due some hardware limitations, | ||
+ | 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 (" | ||
+ | |||
+ | **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/ | ||
+ | |||
+ | * 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 = " | ||
+ | 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: | ||
+ | |||
+ | def getPath(df): | ||
+ | idx = list(df.loc[df[" | ||
+ | path_list = [] | ||
+ | for i in range(len(idx)): | ||
+ | if (i+1)%4 == 0 and i != 0: | ||
+ | path_list.append(df.loc[idx[i-3]: | ||
+ | # | ||
+ | return path_list | ||
+ | |||
+ | def mean_speed(data): | ||
+ | idx = list(data.loc[data[" | ||
+ | t1 = data.loc[idx[1]][" | ||
+ | t2 = data.loc[idx[2]][" | ||
+ | t3 = data.loc[idx[3]][" | ||
+ | spd1 = 200/t1 | ||
+ | spd2 = 1000/t2 | ||
+ | spd3 = 1000/t3 | ||
+ | return (spd1+spd2+spd3)/ | ||
+ | |||
+ | paths = getPath(dataFrame) | ||
+ | |||
+ | # 1. Calculate continuous locations and concatenate all paths as before: | ||
+ | for i in range(len(paths)): | ||
+ | loc = (paths[i][" | ||
+ | paths[i] = pd.concat([paths[i], | ||
+ | paths[i] = paths[i].drop(list(paths[i].loc[paths[i][" | ||
+ | |||
+ | path_t = pd.concat(paths) | ||
+ | path_t = path_t.sort_values(" | ||
+ | |||
+ | locations = path_t[' | ||
+ | data_values = path_t.iloc[:, | ||
+ | |||
+ | # 2. Interpolate sensor columns independently on a uniform location grid: | ||
+ | min_loc, max_loc = np.min(locations), | ||
+ | num_interp_points = int(np.ceil(max_loc - min_loc)) + 1 | ||
+ | interp_locations = np.linspace(min_loc, | ||
+ | |||
+ | interp_data = np.zeros((num_interp_points, | ||
+ | for col in range(data_values.shape[1]): | ||
+ | interp_func = interp1d(locations, | ||
+ | interp_data[:, | ||
+ | |||
+ | # 3. Reshape each row into 8x8 frames: | ||
+ | num_frames = interp_data.shape[0] | ||
+ | frame_height, | ||
+ | frames_8x8 = [interp_data[i].reshape(frame_height, | ||
+ | |||
+ | # 4. Interpolate each 8x8 frame to 64x64 using scipy.ndimage.zoom: | ||
+ | zoom_factor = 64 / 8 # 8x to 64x scaling | ||
+ | |||
+ | frames_64x64 = [zoom(frame, | ||
+ | |||
+ | # 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, | ||
+ | count_64 = np.zeros((64, | ||
+ | |||
+ | for i, frame in enumerate(frames_64x64): | ||
+ | offset = i | ||
+ | final_frame_64[:, | ||
+ | count_64[:, offset: | ||
+ | |||
+ | aggregated_64 = np.divide(final_frame_64, | ||
+ | |||
+ | # 6. Plot the aggregated 64x wide frame: | ||
+ | plt.figure(figsize=(final_width / 16, 8)) # Adjust size for clarity | ||
+ | plt.imshow(aggregated_64, | ||
+ | plt.colorbar(label=' | ||
+ | plt.title(" | ||
+ | plt.xlabel(' | ||
+ | plt.ylabel(' | ||
+ | plt.show() | ||
+ | </ | ||
+ | |||
+ | {{ : | ||
+ | ===== 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: | ||
+ | |||
+ | * Customization: | ||
+ | |||
+ | ==== Limitations & Improvements: | ||
+ | |||
+ | |||
+ | * Current system is distance-based; | ||
+ | |||
+ | * Wireless reliability is dependent on network strength; alternative protocols (e.g., LoRa) could be used in rural deployments. | ||
+ | |||
+ | * Data encryption/ | ||
+ | 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.1753772843.txt.gz · Last modified: 2025/07/29 09:07 by 35120_students.hsrw