/* * This file is part of the GameBoy-Printer project. * Copyright (C) 2018 Tido Klaassen * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "freertos/queue.h" #include "freertos/event_groups.h" #include "lwip/sockets.h" #include "lwip/dns.h" #include "lwip/netdb.h" #include "lwip/igmp.h" #include "esp_wifi.h" #include "esp_system.h" #include "esp_event.h" #include "esp_event_loop.h" #include "nvs_flash.h" #include "soc/rtc_cntl_reg.h" #include "rom/cache.h" #include "driver/spi_master.h" #include "esp_err.h" #include "esp_log.h" #include "esp_spi_flash.h" #include "esp_spiffs.h" #include "soc/gpio_reg.h" #include "driver/gpio.h" #include "esp_intr_alloc.h" #include #include #include #include #include #include #include #include #include #include #include #include "lodepng.h" #define TAG "GB-Printer" #define GPIO_MISO 4 #define GPIO_MOSI 13 #define GPIO_SCLK 14 #define WIFI_SSID "GB-Printer" #define MAX_STA_CONN 3 #define LISTEN_PORT 80u #define MAX_CONNECTIONS 8u #define NVS_NAMESPC "GBPR" #define IMGDIR "/img" #define IMGTMPL "img%05d.png" #define IMG_WIDTH 160 // Images are always 160 pixels wide #define IMG_HEIGHT 144 // Maximum image height, may be less #define IMG_SIZE 5760 #define MAX_DATA_SIZE 640 #define PR_PARAM_SIZE 4 #define PKT_RING_SIZE 4 #define HTML_HEADER "" \ "" \ "" \ "ESP32 GameBoy Printer" \ "" \ "" /* Useful Linux-like defines */ #define ARRAY_SIZE(x) (sizeof(x) / sizeof(*(x))) #define likely(x) __builtin_expect((x),1) #define unlikely(x) __builtin_expect((x),0) #define mb() asm volatile ("" : : : "memory") #define STATUS_CHKSUM (1u << 0) #define STATUS_BUSY (1u << 1) #define STATUS_FULL (1u << 2) #define STATUS_UNPROC (1u << 3) #define STATUS_ERR0 (1u << 4) #define STATUS_ERR1 (1u << 5) #define STATUS_ERR2 (1u << 6) #define STATUS_BATLOW (1u << 7) enum pr_state { state_sync0 = 0, state_sync1, state_cmd, state_compr, state_len_low, state_len_high, state_data, state_chk_low, state_chk_high, state_ack, state_status, }; enum pr_cmd { cmd_init = 0x01, cmd_print = 0x02, cmd_data = 0x04, cmd_break = 0x08, cmd_inquiry = 0x0f, }; enum pr_owner { own_isr = 0, own_task, }; struct pr_packet { volatile enum pr_owner owner; enum pr_state state; uint8_t cmd; uint8_t compr; uint8_t data[MAX_DATA_SIZE]; size_t data_len; size_t cur_len; uint16_t cur_sum; uint16_t chk_sum; }; struct pr_data { uint8_t data[IMG_SIZE]; size_t data_len; uint8_t params[PR_PARAM_SIZE]; uint8_t status; unsigned int busy_cnt; unsigned int printed; size_t width; size_t hight; }; static struct spi_ctrl { struct pr_packet work_packet; struct pr_packet packets[PKT_RING_SIZE]; size_t clk_cnt; uint64_t clk_last; uint8_t rcv_data; uint8_t snd_data; volatile size_t wr_idx; volatile size_t rd_idx; volatile uint8_t status_isr; volatile uint8_t status_task; xQueueHandle packet_done; } ctrl; static uint8_t render_buff[IMG_SIZE]; static QueueHandle_t img_queue; static EventGroupHandle_t wifi_event_group; static char conn_mem[sizeof(RtosConnType) * MAX_CONNECTIONS]; static HttpdFreertosInstance httpd_instance; CgiStatus cgi_del_all(HttpdConnData *conn); CgiStatus cgi_reset_seq(HttpdConnData *conn); CgiStatus tpl_index(HttpdConnData *conn, char *token, void **arg); HttpdBuiltInUrl builtin_urls[]={ ROUTE_REDIRECT("/", "/index.tpl"), ROUTE_REDIRECT("/index.html", "/index.tpl"), ROUTE_TPL("/index.tpl", tpl_index), ROUTE_CGI("/img/*", cgiEspVfsHook), ROUTE_CGI("/remove_all", cgi_del_all), ROUTE_CGI("/reset_seq", cgi_reset_seq), ROUTE_FILESYSTEM(), ROUTE_END() }; CgiStatus tpl_index(HttpdConnData *conn, char *token, void **arg) { struct dirent *ent; DIR *dir; char buff[128]; CgiStatus result; dir = (DIR *) *arg; result = HTTPD_CGI_DONE; buff[0] = '\0'; if(conn->isConnectionClosed){ ESP_LOGD(TAG, "[%s] Conn closed", __func__); if(dir != NULL){ closedir(dir); } goto err_out; } if(token == NULL){ goto err_out; } if(strcmp(token, "GBPics") != 0){ goto err_out; } if(dir == NULL){ dir = opendir(IMGDIR); if(dir == NULL){ ESP_LOGE(TAG, "[%s] opendir() failed", __func__); goto err_out; } *arg = dir; } ent = readdir(dir); if(ent == NULL){ goto err_out; } snprintf(buff, sizeof(buff), "
" "" "" "", IMGDIR, ent->d_name, IMGDIR, ent->d_name); result = HTTPD_CGI_MORE; ESP_LOGD(TAG, "[%s] Sending: %s", __func__, buff); httpdSend(conn, buff, -1); err_out: if(result == HTTPD_CGI_DONE && dir != NULL){ closedir(dir); *arg = NULL; } return result; } CgiStatus cgi_del_all(HttpdConnData *conn) { struct dirent *ent; DIR *dir; char buff[128]; int result; buff[0] = '\0'; dir = NULL; result = 0; if(conn->isConnectionClosed){ goto err_out; } dir = opendir(IMGDIR); if(dir == NULL){ ESP_LOGE(TAG, "[%s] opendir() failed", __func__); result = -1; goto send_reply; } while((ent = readdir(dir)) != NULL){ snprintf(buff, sizeof(buff), "%s/%s", IMGDIR, ent->d_name); result = unlink(buff); if(result != 0){ ESP_LOGE(TAG, "[%s] Unlink failed for %s", __func__, buff); goto send_reply; } } send_reply: if(result == 0){ httpdRedirect(conn, "/index.tpl"); } else { httpdStartResponse(conn, 500); httpdHeader(conn, "Content-Type", "text/html"); httpdEndHeaders(conn); httpdSend(conn, HTML_HEADER, -1); httpdSend(conn, "" "

ESP32 Gameboy Printer

" "

" "Error while deleting file ", -1); httpdSend(conn, buff, -1); httpdSend(conn, ".
" "Go back" "
" "Retry" "

" "" "", -1); } err_out: if(dir != NULL){ closedir(dir); } return HTTPD_CGI_DONE; } CgiStatus cgi_reset_seq(HttpdConnData *conn) { nvs_handle handle; int result; result = 0; if(conn->isConnectionClosed){ goto err_out; } result = nvs_open(NVS_NAMESPC, NVS_READWRITE, &handle); if(result != ESP_OK){ goto send_reply; } result = nvs_set_u32(handle, "next_idx", 0); nvs_commit(handle); nvs_close(handle); send_reply: if(result == ESP_OK){ httpdRedirect(conn, "/index.tpl"); } else { httpdStartResponse(conn, 500); httpdHeader(conn, "Content-Type", "text/html"); httpdEndHeaders(conn); httpdSend(conn, HTML_HEADER, -1); httpdSend(conn, "" "

ESP32 Gameboy Printer

" "

" "Error resetting image sequence." "
" "Go back" "
" "Retry" "

" "" "", -1); } err_out: return HTTPD_CGI_DONE; } esp_err_t http_srv_init(void) { HttpdInitStatus status; esp_err_t result; result = ESP_OK; if(espFsInit((void*)(webpages_espfs_start)) != ESPFS_INIT_RESULT_OK){ result = ESP_FAIL; goto err_out; } status = httpdFreertosInit(&httpd_instance, builtin_urls, LISTEN_PORT, conn_mem, MAX_CONNECTIONS, HTTPD_FLAG_NONE); if(status != InitializationSuccess){ result = ESP_FAIL; goto err_out; } httpdFreertosStart(&httpd_instance); err_out: return result; } void draw_tile(uint8_t *data, uint8_t *image) { int x, y; uint16_t *tmp; for(y = 0; y < 8; ++y){ tmp = (uint16_t *) &image[40 * y]; *tmp = 0; for(x = 7; x >= 0; --x){ *tmp |= ((data[1] >> x) & 0x1) << (2 * x + 1); *tmp |= ((data[0] >> x) & 0x1) << (2 * x); } *tmp = ~htons(*tmp); data += 2; } } esp_err_t draw_bitmap(struct pr_data *data) { size_t w, h, x, y; uint8_t *tile_data, *tile_dest; unsigned int tile; esp_err_t result; result = ESP_OK; w = IMG_WIDTH; h = (data->data_len * 4) / IMG_WIDTH; if(h > IMG_HEIGHT){ ESP_LOGE(TAG, "[%s] Invalid image hight: %d", __func__, h); result = ESP_ERR_INVALID_SIZE; goto err_out; } memset(render_buff, 0x0, sizeof(render_buff)); for(tile = 0; tile < (w * h / 64); ++tile){ x = tile % 20; y = 8 * (tile / 20); tile_data = &(data->data[tile * 16]); tile_dest = &(render_buff[(x * 2) + (y * 40)]); draw_tile(tile_data, tile_dest); } memmove(data->data, render_buff, data->data_len); data->width = w; data->hight = h; err_out: return result; } void save_data(struct pr_data *data) { struct stat statbuf; uint32_t idx; FILE *file; char path[128]; nvs_handle handle; size_t written, png_size; uint8_t *png; LodePNGState state; esp_err_t result; png = NULL; file = NULL; result = nvs_open(NVS_NAMESPC, NVS_READWRITE, &handle); ESP_ERROR_CHECK(result); result = draw_bitmap(data); if(result != ESP_OK){ goto err_out; } lodepng_state_init(&state); state.info_raw.colortype = LCT_GREY; state.info_raw.bitdepth = 2; state.info_png.color.colortype = LCT_GREY; state.info_png.color.bitdepth = 2; state.encoder.zlibsettings.btype = 0; // disable compression due to lack of memory lodepng_encode(&png, &png_size, data->data, data->width, data->hight, &state); result = state.error; lodepng_state_cleanup(&state); if(result != 0){ ESP_LOGE(TAG, "[%s] lodepng failed: %s", __func__, lodepng_error_text(result)); goto err_out; } result = nvs_get_u32(handle, "next_idx", &idx); if(result != ESP_OK){ ESP_LOGI(TAG, "[%s] No image sequence number found", __func__); idx = 0; } /* Find first free image name. Start at index read from NVS and increment * until we find a free slot. */ errno = 0; do{ snprintf(path, sizeof(path) - 1, IMGDIR "/" IMGTMPL, idx); result = stat(path, &statbuf); if(result == 0){ ESP_LOGD(TAG, "[%s] Found %s", __func__, path); ++idx; } }while(result == 0); if(errno != ENOENT){ ESP_LOGE(TAG, "Saving data failed: %s", strerror(errno)); goto err_out; } file = fopen(path, "w"); if(file == NULL){ ESP_LOGE(TAG, "Failed to open file %s: %s", path, strerror(errno)); goto err_out; } written = fwrite(png, 1, png_size, file); if(written != png_size){ ESP_LOGE(TAG, "Error writing data: %s", strerror(errno)); result = EIO; goto err_out; } fclose(file); file = NULL; if(nvs_set_u32(handle, "next_idx", idx + 1) != ESP_OK){ /* Not super critical. Worst case we get a duplicate image name if * picture storage gets erased before next image is saved */ ESP_LOGW(TAG, "[%s] Error storing image sequence number", __func__); } nvs_commit(handle); err_out: if(file != NULL){ fclose(file); if(result != ESP_OK){ unlink(path); } } if(png != NULL){ free(png); } nvs_close(handle); return; } esp_err_t init_spiffs(void) { esp_err_t result; esp_vfs_spiffs_conf_t cfg; ESP_LOGI(TAG, "Initializing SPIFFS"); cfg.base_path = "/img"; cfg.partition_label = NULL; cfg.max_files = MAX_CONNECTIONS; cfg.format_if_mount_failed = true; result = esp_vfs_spiffs_register(&cfg); switch(result){ case ESP_OK: break; case ESP_FAIL: ESP_LOGE(TAG, "Failed to mount or format filesystem"); break; case ESP_ERR_NOT_FOUND: ESP_LOGE(TAG, "Failed to find SPIFFS partition"); break; default: ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(result)); break; } return result; } static esp_err_t event_handler(void *ctx, system_event_t *event) { switch(event->event_id) { case SYSTEM_EVENT_AP_STACONNECTED: ESP_LOGI(TAG, "station:"MACSTR" join, AID=%d", MAC2STR(event->event_info.sta_connected.mac), event->event_info.sta_connected.aid); break; case SYSTEM_EVENT_AP_STADISCONNECTED: ESP_LOGI(TAG, "station:"MACSTR"leave, AID=%d", MAC2STR(event->event_info.sta_disconnected.mac), event->event_info.sta_disconnected.aid); break; default: break; } return ESP_OK; } void wifi_init_softap() { wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); wifi_config_t wifi_config = { .ap = { .ssid = WIFI_SSID, .ssid_len = strlen(WIFI_SSID), .max_connection = MAX_STA_CONN, .authmode = WIFI_AUTH_OPEN, }, }; wifi_event_group = xEventGroupCreate(); tcpip_adapter_init(); ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config)); ESP_ERROR_CHECK(esp_wifi_start()); } static void IRAM_ATTR sclk_isr_handler(void* arg) { struct spi_ctrl *ctrl; struct pr_packet *packet; uint64_t curr_time; BaseType_t task_woken; ctrl = (struct spi_ctrl *) arg; packet = &(ctrl->work_packet); if(gpio_get_level(GPIO_SCLK) == 0){ /* Falling edge. Check timeouts and set up MISO. */ curr_time = esp_timer_get_time(); /* Check for sync loss during byte transfer. Clock should run */ /* at ~8kHz (125us), so anything above 200us is definitely out */ /* of spec. */ if(unlikely(ctrl->clk_cnt != 0 && (curr_time - ctrl->clk_last) > 200)){ ctrl->clk_cnt = 0; ctrl->rcv_data = 0; ctrl->snd_data = 0; ctrl->status_isr |= STATUS_ERR0; packet->state = state_sync0; } /* According to the Game Boy programming manual the maximum allowed */ /* pause between sending bytes in a packet is 5ms. */ if(unlikely(packet->state != state_sync0 && (curr_time - ctrl->clk_last) > 5100)) { ctrl->status_isr |= STATUS_ERR0; packet->state = state_sync0; } ctrl->clk_last = curr_time; gpio_set_level(GPIO_MISO, (ctrl->snd_data & 0x80u) ? 1 : 0); ctrl->snd_data <<= 1; } else { // Rising edge. Sample MOSI and increase clock count. if(unlikely(ctrl->clk_cnt == 0)){ ctrl->rcv_data = 0; } ctrl->rcv_data <<= 1; if(gpio_get_level(GPIO_MOSI)){ ctrl->rcv_data |= 1; } ++ctrl->clk_cnt; } if(likely(ctrl->clk_cnt <= 7)){ goto done; } ctrl->clk_cnt = 0; if(packet->state >= state_cmd && packet->state < state_chk_low){ packet->cur_sum += ctrl->rcv_data; } /* Process the transferred byte. The state indicates how the byte that */ /* has just completed is interpreted. If data needs to be sent out in */ /* state x, it must be copied to ctrl->snd_data during transition to */ /* that state. */ switch(packet->state){ case state_sync0: if(ctrl->rcv_data == 0x88){ packet->cur_sum = 0; packet->data_len = 0; ctrl->status_isr &= ~(STATUS_CHKSUM | STATUS_BUSY); packet->state = state_sync1; } break; case state_sync1: if(ctrl->rcv_data == 0x33){ packet->state = state_cmd; } else { packet->state = state_sync0; ctrl->status_isr |= STATUS_ERR0; } break; case state_cmd: packet->cmd = ctrl->rcv_data; packet->state = state_compr; break; case state_compr: packet->compr = ctrl->rcv_data; packet->state = state_len_low; break; case state_len_low: packet->cur_len = ctrl->rcv_data; packet->state = state_len_high; break; case state_len_high: packet->cur_len |= (ctrl->rcv_data << 8); if(likely(packet->cur_len <= sizeof(packet->data))){ if(packet->cur_len > 0){ packet->state = state_data; } else { packet->state = state_chk_low; } } else { /* Bogus data length. We could try inferring the real length * from the command byte, but since we probably have lost * synchronisation anyway, we just give up on this packet */ packet->state = state_sync0; ctrl->status_isr |= STATUS_ERR0; } break; case state_data: packet->data[packet->data_len++] = ctrl->rcv_data; --packet->cur_len; if(unlikely(packet->cur_len == 0)){ packet->state = state_chk_low; } break; case state_chk_low: packet->chk_sum = ctrl->rcv_data; packet->state = state_chk_high; break; case state_chk_high: packet->chk_sum |= (ctrl->rcv_data << 8); /* We need to send the ACK byte next. */ ctrl->snd_data = 0x81; packet->state = state_ack; break; case state_ack: /* Packet is almost complete, we need to send out the checksum */ /* in the next byte. Check for errors and update ISR status */ /* accordingly. If everything checks out, try copying packet */ /* to transfer ring. */ if(unlikely(packet->chk_sum != packet->cur_sum)){ ctrl->status_isr |= (STATUS_CHKSUM | STATUS_ERR0); } else { switch(packet->cmd){ case cmd_data: /* We expect to always receive complete bands (20 tiles) */ /* of image data. */ if(packet->data_len % 40 == 0) { ctrl->status_isr |= STATUS_UNPROC; } else { ctrl->status_isr |= STATUS_ERR0; } break; case cmd_print: if(packet->data_len == PR_PARAM_SIZE){ ctrl->status_isr &= ~STATUS_UNPROC; ctrl->status_isr |= STATUS_FULL; } else { ctrl->status_isr |= STATUS_ERR0; } break; case cmd_inquiry: break; case cmd_init: case cmd_break: ctrl->status_isr = 0; break; } if(ctrl->packets[ctrl->wr_idx].owner == own_isr){ memcpy(&(ctrl->packets[ctrl->wr_idx]), packet, sizeof(*packet)); } else { /* Protocol task is too slow, signal busy error. */ ctrl->status_isr |= (STATUS_ERR0 | STATUS_BUSY); } } /* Send combined error status of ISR and task */ ctrl->snd_data = (ctrl->status_isr | ctrl->status_task); packet->state = state_status; break; case state_status: /* Status byte has been sent out, packet transfer is complete. */ /* Hand packet over to protocol task, unless we had a checksum */ /* error or packet ring was full. */ if(likely((ctrl->status_isr & (STATUS_CHKSUM | STATUS_BUSY)) == 0)){ mb(); ctrl->packets[ctrl->wr_idx].owner = own_task; mb(); xSemaphoreGiveFromISR(ctrl->packet_done, &task_woken); if(task_woken){ portYIELD_FROM_ISR(); } ++ctrl->wr_idx; ctrl->wr_idx %= ARRAY_SIZE(ctrl->packets); } /* Get ready to start reception of next packet */ packet->state = state_sync0; ctrl->snd_data = 0; break; default: /* Should never be reached. */ packet->state = state_sync0; ctrl->status_isr |= STATUS_ERR0; break; } done: return; } void IRAM_ATTR packet_proto_task(void *pvParameters) { struct pr_packet *packet; struct pr_data *data = NULL; ESP_LOGI(TAG, "Packet proto task started."); while(1) { /* Make sure we have an image data buffer ready before we accept */ /* any command packages from the ISR. */ if(data == NULL){ data = calloc(1, sizeof(*data)); if(data == NULL){ ESP_LOGE(TAG, "Out of memory"); ctrl.status_task = STATUS_BUSY; continue; } } /* If we have a completed image buffer, try handing it to the main */ /* task for rendering and saving to flash. */ if(data->printed != 0){ if(xQueueSend(img_queue, &data, 0) != pdTRUE){ ctrl.status_task |= STATUS_BUSY; ++data->busy_cnt; if(data->busy_cnt < 5){ /* Give main task a chance to process the queue. */ vTaskDelay(portTICK_PERIOD_MS); } else { /* Give up and signal an error after five tries. */ memset(data, 0x0, sizeof(*data)); ctrl.status_task |= STATUS_ERR0; } } else { /* Data buffer belongs to main task now. We will allocate */ /* a new one on next loop iteration. */ data = NULL; ctrl.status_task &= ~STATUS_BUSY; } /* Restart loop for either retrying this buffer or allocating */ /* a new one. */ continue; } /* We have a non-completed data buffer now. Remove busy signal and */ /* wait for command packet from ISR. */ ctrl.status_task &= ~STATUS_BUSY; packet = &(ctrl.packets[ctrl.rd_idx]); if(packet->owner == own_isr){ xSemaphoreTake(ctrl.packet_done, portMAX_DELAY); } /* No new packet availabe? Restart loop. */ if(packet->owner == own_isr){ continue; } ESP_LOGD(TAG, "Got packet: cmd: 0x%02x status_isr: 0x%02x status_task: 0x%02x", packet->cmd, ctrl.status_isr, ctrl.status_task); switch(packet->cmd){ case cmd_data: /* We expect to always receive complete bands (20 tiles) */ /* of image data. Copy data payload into the image buffer */ /* and update the current offset. */ ESP_LOGD(TAG, "cmd_data: len: 0x%x", packet->data_len); if( (packet->data_len % 40 == 0) && (data->data_len + packet->data_len <= sizeof(data->data))) { memcpy(&(data->data[data->data_len]), packet->data, packet->data_len); data->data_len += packet->data_len; ctrl.status_task |= STATUS_UNPROC; } else { ESP_LOGE(TAG, "Invalid data buffer length: 0x%x", packet->data_len); ctrl.status_task |= STATUS_ERR0; } break; case cmd_print: /* Image data transfer is complete. Prepare to hand data buffer */ /* over to main task. */ ESP_LOGD(TAG, "cmd_print: len: 0x%x", packet->data_len); if(packet->data_len == sizeof(data->params)){ memcpy(data->params, packet->data, packet->data_len); ctrl.status_task &= ~STATUS_UNPROC; ctrl.status_task |= STATUS_FULL; data->busy_cnt = 0; data->printed = 1; } else { ESP_LOGE(TAG, "Parameter buffer overrun"); ctrl.status_task |= STATUS_ERR0; } break; case cmd_inquiry: /* Status inquiry is handled completely in ISR. */ ESP_LOGD(TAG, "cmd_inquiry: len: 0x%x", packet->data_len); break; case cmd_init: case cmd_break: /* Hard reset the whole transaction. This is the only way to */ /* clear possible error states from the task's status indicator. */ ESP_LOGD(TAG, "cmd_init/break"); memset(data, 0x0, sizeof(*data)); ctrl.status_task = 0; break; } /* Hand ownership of command packet back to ISR, move read index */ /* to next element in ring. */ mb(); packet->owner = own_isr; mb(); ++ctrl.rd_idx; ctrl.rd_idx %= ARRAY_SIZE(ctrl.packets); } } void app_main() { esp_err_t result; gpio_config_t io_cfg; struct pr_data *data = NULL; // Initialise NVS result = nvs_flash_init(); if( result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); result = nvs_flash_init(); } ESP_ERROR_CHECK(result); memset(&ctrl, 0x0, sizeof(ctrl)); memset(&io_cfg, 0x0, sizeof(io_cfg)); io_cfg.intr_type = GPIO_INTR_ANYEDGE; io_cfg.mode = GPIO_MODE_INPUT; io_cfg.pull_up_en = 0; io_cfg.pin_bit_mask = (1u << GPIO_SCLK); result = gpio_config(&io_cfg); if(result != ESP_OK){ ESP_LOGE(TAG, "gpio_config() failed for GPIO_SCLK"); goto err_out; } io_cfg.intr_type = GPIO_INTR_DISABLE; io_cfg.mode = GPIO_MODE_INPUT; io_cfg.pull_up_en = 0; io_cfg.pin_bit_mask = (1u << GPIO_MOSI); result = gpio_config(&io_cfg); if(result != ESP_OK){ ESP_LOGE(TAG, "gpio_config() failed for GPIO_MOSI"); goto err_out; } io_cfg.intr_type = GPIO_INTR_DISABLE; io_cfg.mode = GPIO_MODE_OUTPUT_OD; io_cfg.pull_up_en = 0; io_cfg.pin_bit_mask = (1u << GPIO_MISO); result = gpio_config(&io_cfg); if(result != ESP_OK){ ESP_LOGE(TAG, "gpio_config() failed for GPIO_MISO"); goto err_out; } memset(&ctrl, 0x0, sizeof(ctrl)); ctrl.packet_done = xSemaphoreCreateBinary(); if(ctrl.packet_done == NULL){ ESP_LOGE(TAG, "xSemaphoreCreateBinary() failed"); goto err_out; } result = init_spiffs(); if(result != ESP_OK){ ESP_LOGE(TAG, "[%s] init_spiffs() failed", __func__); goto err_out; } result = gpio_install_isr_service(0); if(result != ESP_OK){ ESP_LOGE(TAG, "[%s] gpio_install_isr_service() failed", __func__); goto err_out; } result = gpio_isr_handler_add(GPIO_SCLK, sclk_isr_handler, &ctrl); if(result != ESP_OK){ ESP_LOGE(TAG, "[%s] gpio_isr_handler_add() failed", __func__); goto err_out; } img_queue = xQueueCreate(2, sizeof(struct pr_data *)); if(img_queue == NULL){ ESP_LOGE(TAG, "[%s] Creating packet queue failed.", __func__); goto err_out; } #if 1 wifi_init_softap(); http_srv_init(); #endif xTaskCreate(&packet_proto_task, "packet_proto_task", 3072, NULL, 2, NULL); ESP_LOGI(TAG, "Entering main loop"); while(1){ if(xQueueReceive(img_queue, &data, portMAX_DELAY) == pdTRUE){ save_data(data); free(data); } } err_out: return; }