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:35] – [System Design] 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 49: | Line 52: | ||
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: | 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: | ||
- | < | + | < |
#define EXAMPLE_ESP_WIFI_SSID " | #define EXAMPLE_ESP_WIFI_SSID " | ||
#define EXAMPLE_ESP_WIFI_PASS " | #define EXAMPLE_ESP_WIFI_PASS " | ||
Line 55: | Line 58: | ||
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: | 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: | ||
- | < | + | < |
s_wifi_event_group = xEventGroupCreate(); | s_wifi_event_group = xEventGroupCreate(); | ||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); | ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); | ||
Line 64: | Line 67: | ||
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, | 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, | ||
- | < | + | < |
server_sock = socket(AF_INET, | server_sock = socket(AF_INET, | ||
bind(server_sock, | bind(server_sock, | ||
Line 73: | Line 76: | ||
The VL53L8CX sensor communicates over I²C, using dedicated pins: | The VL53L8CX sensor communicates over I²C, using dedicated pins: | ||
- | < | + | < |
#define I2C_SCL GPIO_NUM_9 | #define I2C_SCL GPIO_NUM_9 | ||
#define I2C_SDA GPIO_NUM_8 | #define I2C_SDA GPIO_NUM_8 | ||
Line 80: | Line 83: | ||
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: | 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: | ||
- | < | + | < |
Dev.platform.reset_gpio = GPIO_NUM_5; | Dev.platform.reset_gpio = GPIO_NUM_5; | ||
VL53L8CX_Reset_Sensor(& | VL53L8CX_Reset_Sensor(& | ||
Line 91: | Line 94: | ||
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 " | 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 " | ||
- | < | + | < |
// Compute median as background | // Compute median as background | ||
int bg = median(distance); | int bg = median(distance); | ||
Line 102: | Line 105: | ||
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: | 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: | ||
- | < | + | < |
if (/* central pixels detect presence */) { | if (/* central pixels detect presence */) { | ||
gpio_set_level(GPIO_NUM_7, | gpio_set_level(GPIO_NUM_7, | ||
Line 112: | Line 115: | ||
A push-button (GPIO4) enables manual event annotation. Button interrupts are debounced using a hardware timer for reliability: | A push-button (GPIO4) enables manual event annotation. Button interrupts are debounced using a hardware timer for reliability: | ||
- | < | + | < |
gpio_isr_handler_add(BUTTON_PIN, | gpio_isr_handler_add(BUTTON_PIN, | ||
// In ISR task: | // In ISR task: | ||
Line 124: | Line 127: | ||
Example packet preparation: | Example packet preparation: | ||
- | < | + | < |
// Prepare buffer for [timestamp][frame] | // Prepare buffer for [timestamp][frame] | ||
uint8_t sendbuf[8 + FRAME_SIZE * 2]; | uint8_t sendbuf[8 + FRAME_SIZE * 2]; | ||
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 < | ||
+ | #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 ==== | ==== Assembly ==== | ||
Line 156: | Line 640: | ||
* Connect a client to ESP32' | * 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 ===== | ===== Results ===== | ||
Line 186: | Line 791: | ||
* The system is resilient to network disconnects, | * 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 ==== | + | {{ : |
+ | ===== 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.1753774511.txt.gz · Last modified: 2025/07/29 09:35 by 35120_students.hsrw