Last active
October 25, 2025 19:50
-
-
Save Yuikawa-Akira/11b63d5a68d71ceaee3f24d3cfb35e0c to your computer and use it in GitHub Desktop.
Pict_Camera_Stream
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <WiFi.h> | |
| #include <utility> | |
| #include <esp_http_server.h> | |
| #include <esp_camera.h> | |
| #include <FastLED.h> | |
| #include <SPI.h> | |
| #include <SD.h> | |
| #include <M5Unified.h> | |
| #define KEY_PIN 1 | |
| #define LED_PIN 2 | |
| #define POWER_GPIO_NUM 18 | |
| CRGB LED[1]; | |
| camera_fb_t *fb; | |
| M5Canvas canvas_565; | |
| M5Canvas canvas_888; | |
| M5Canvas canvas_dihter; | |
| const char *ssid = "PictCam"; | |
| const char *password = "password"; | |
| const uint16_t disp_width_pix = 96, disp_height_pix = 96; | |
| // SDカード保存用 | |
| char filename[64]; | |
| int filecounter = 1; | |
| uint8_t graydata[disp_width_pix * disp_height_pix]; | |
| // 最大8色のカラーパレット | |
| uint32_t ColorPalettes[8][8] = { | |
| { // パレット0 slso8 | |
| 0x0D2B45, 0x203C56, 0x544E68, 0x8D697A, 0xD08159, 0xFFAA5E, 0xFFD4A3, 0xFFECD6 }, | |
| { // パレット1 都市伝説解体センター風 | |
| 0x000000, 0x000B22, 0x112B43, 0x437290, 0x437290, 0xE0D8D1, 0xE0D8D1, 0xFFFFFF }, | |
| { // パレット2 ファミレスを享受せよ風 | |
| 0x010101, 0x33669F, 0x33669F, 0x33669F, 0x498DB7, 0x498DB7, 0xFBE379, 0xFBE379 }, | |
| { // パレット3 gothic-bit | |
| 0x0E0E12, 0x1A1A24, 0x333346, 0x535373, 0x8080A4, 0xA6A6BF, 0xC1C1D2, 0xE6E6EC }, | |
| { // パレット4 noire-truth | |
| 0x1E1C32, 0x1E1C32, 0x1E1C32, 0x1E1C32, 0xC6BAAC, 0xC6BAAC, 0xC6BAAC, 0xC6BAAC }, | |
| { // パレット5 2BIT DEMIBOY | |
| 0x252525, 0x252525, 0x4B564D, 0x4B564D, 0x9AA57C, 0x9AA57C, 0xE0E9C4, 0xE0E9C4 }, | |
| { // パレット6 deep-maze | |
| 0x001D2A, 0x085562, 0x009A98, 0x00BE91, 0x38D88E, 0x9AF089, 0xF2FF66, 0xF2FF66 }, | |
| { // パレット7 night-rain | |
| 0x000000, 0x012036, 0x3A7BAA, 0x7D8FAE, 0xA1B4C1, 0xF0B9B9, 0xFFD159, 0xFFFFFF }, | |
| }; | |
| int currentPalettelndex = 0; // 現在のパレットのインデックス | |
| int maxPalettelndex = 7; // パレット総数 | |
| int ditherLevels = 0; // ディザ階調数 2以上 | |
| camera_config_t camera_config = { | |
| .pin_pwdn = -1, | |
| .pin_reset = -1, | |
| .pin_xclk = 21, | |
| .pin_sscb_sda = 12, | |
| .pin_sscb_scl = 9, | |
| .pin_d7 = 13, | |
| .pin_d6 = 11, | |
| .pin_d5 = 17, | |
| .pin_d4 = 4, | |
| .pin_d3 = 48, | |
| .pin_d2 = 46, | |
| .pin_d1 = 42, | |
| .pin_d0 = 3, | |
| .pin_vsync = 10, | |
| .pin_href = 14, | |
| .pin_pclk = 40, | |
| .xclk_freq_hz = 20000000, | |
| .ledc_timer = LEDC_TIMER_0, | |
| .ledc_channel = LEDC_CHANNEL_0, | |
| .pixel_format = PIXFORMAT_RGB565, | |
| .frame_size = FRAMESIZE_96X96, | |
| // FRAMESIZE_96X96, // 96x96 | |
| // FRAMESIZE_128X128, // 128x128 | |
| // FRAMESIZE_QQVGA, // 160x120 | |
| // FRAMESIZE_QCIF, // 176x144 | |
| // FRAMESIZE_HQVGA, // 240x176 | |
| // FRAMESIZE_240X240, // 240x240 | |
| // FRAMESIZE_QVGA, // 320x240 | |
| .jpeg_quality = 0, | |
| .fb_count = 2, | |
| .fb_location = CAMERA_FB_IN_PSRAM, | |
| .grab_mode = CAMERA_GRAB_LATEST, | |
| .sccb_i2c_port = 0, | |
| }; | |
| bool loadPaletteFromSD(int paletteIndex) { | |
| if (paletteIndex < 0 || paletteIndex > maxPalettelndex) { | |
| return false; | |
| } | |
| // ファイル名を生成 (例: /ColorPalette0.txt) | |
| String filename = "/ColorPalette" + String(paletteIndex) + ".txt"; | |
| // ファイルが存在するか確認 | |
| if (!SD.exists(filename)) { | |
| return false; // ファイルが存在しない場合はデフォルトを使うのでfalseを返す | |
| } | |
| // ファイルを開く | |
| File file = SD.open(filename, FILE_READ); | |
| if (!file) { | |
| return false; // ファイルオープン失敗 | |
| } | |
| // ファイルから8つのカラーコードを読み込む | |
| int colorCount = 0; | |
| while (file.available() && colorCount < 8) { | |
| String line = file.readStringUntil('\n'); // 1行読み込む | |
| line.trim(); // 前後の空白や改行文字を削除 | |
| if (line.length() > 0) { | |
| uint32_t colorValue = strtoul(line.c_str(), NULL, 0); | |
| ColorPalettes[paletteIndex][colorCount] = colorValue; | |
| colorCount++; | |
| } | |
| } | |
| file.close(); // ファイルを閉じる | |
| // 8色読み込めたか確認 | |
| if (colorCount == 8) { | |
| return true; // 成功 | |
| } else { | |
| // もし8色以下の場合は読み込めた分だけ反映して残りはデフォルトを使用する | |
| return false; // 読み込み失敗(色が足りない) | |
| } | |
| } | |
| bool CameraBegin() { | |
| esp_err_t err = esp_camera_init(&camera_config); | |
| if (err != ESP_OK) { | |
| return false; | |
| } | |
| //カメラ追加設定 | |
| sensor_t *s = esp_camera_sensor_get(); | |
| s->set_vflip(s, 0); //上下反転 0無効 1有効 | |
| s->set_hmirror(s, 0); //左右反転 0無効 1有効 | |
| // s->set_colorbar(s, 1); //カラーバー 0無効 1有効 | |
| // s->set_brightness(s, 1); // up the brightness just a bit | |
| // s->set_saturation(s, 0); // lower the saturation | |
| return true; | |
| } | |
| bool CameraGet() { | |
| fb = esp_camera_fb_get(); | |
| if (!fb) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool CameraFree() { | |
| if (fb) { | |
| esp_camera_fb_return(fb); | |
| return true; | |
| } | |
| return false; | |
| } | |
| uint16_t swap16(uint16_t value) { | |
| return (value << 8) | (value >> 8); | |
| } | |
| void convertColor_canvas(M5Canvas &srcSprite, M5Canvas &dstSprite) { | |
| int width = std::min(srcSprite.width(), dstSprite.width()); | |
| int height = std::min(srcSprite.height(), dstSprite.height()); | |
| uint16_t *src_buf = (uint16_t *)srcSprite.getBuffer(); | |
| uint8_t *dst_buf = (uint8_t *)dstSprite.getBuffer(); | |
| int buf_size = width * height; | |
| for (int i = 0; i < buf_size; ++i) { | |
| uint32_t rgb565Color = swap16(src_buf[i]); // 色を取得 | |
| uint32_t rgb888Color = srcSprite.color16to24(rgb565Color); // RGB565からRGB888へ変換 | |
| uint8_t r = (rgb888Color >> 16) & 0xFF; | |
| uint8_t g = (rgb888Color >> 8) & 0xFF; | |
| uint8_t b = rgb888Color & 0xFF; | |
| uint16_t luminance = (uint16_t)(0.2126 * r + 0.7152 * g + 0.0722 * b); // 輝度の計算 BT.709の係数を使用 | |
| uint8_t grayLevel = luminance / 32; // 輝度を8階調のグレースケールに変換 | |
| uint32_t newColor = ColorPalettes[currentPalettelndex][grayLevel]; // カラーパレットから色を取得 | |
| uint8_t r_n = (newColor >> 16) & 0xFF; | |
| uint8_t g_n = (newColor >> 8) & 0xFF; | |
| uint8_t b_n = newColor & 0xFF; | |
| dst_buf[3 * i] = r_n; // 取得した色を書込 | |
| dst_buf[3 * i + 1] = g_n; | |
| dst_buf[3 * i + 2] = b_n; | |
| } | |
| } | |
| void BayerDither4x4(M5Canvas &srcSprite, M5Canvas &dstSprite, int ditherLevelsPerChannel) { | |
| static const uint8_t bayer4x4[4][4] = { | |
| { 0, 8, 2, 10 }, { 12, 4, 14, 6 }, { 3, 11, 1, 9 }, { 15, 7, 13, 5 } | |
| }; | |
| static const float bayerDivisor = 16.0f; | |
| int width = std::min(srcSprite.width(), dstSprite.width()); | |
| int height = std::min(srcSprite.height(), dstSprite.height()); | |
| uint16_t *src_buf = (uint16_t *)srcSprite.getBuffer(); | |
| uint16_t *dst_buf = (uint16_t *)dstSprite.getBuffer(); | |
| float step = 255.0f / (float)(ditherLevelsPerChannel - 1); | |
| for (int y = 0; y < height; ++y) { | |
| for (int x = 0; x < width; ++x) { | |
| uint16_t src_color = swap16(src_buf[y * width + x]); // 元画像のピクセル色を取得 | |
| uint32_t originalColorValue = srcSprite.color16to24(src_color); | |
| uint8_t r_src = (originalColorValue >> 16) & 0xFF; | |
| uint8_t g_src = (originalColorValue >> 8) & 0xFF; | |
| uint8_t b_src = originalColorValue & 0xFF; | |
| uint8_t r_dst, g_dst, b_dst; | |
| uint8_t channels_src[3] = { r_src, g_src, b_src }; | |
| uint8_t channels_dst[3]; | |
| float bayerThreshold = (float)bayer4x4[y % 4][x % 4] / bayerDivisor; // 各チャンネルに対してディザリングを適用 | |
| for (int ch = 0; ch < 3; ++ch) { | |
| uint8_t val_src = channels_src[ch]; | |
| int level_index = floor((float)val_src / step); | |
| if (level_index >= ditherLevelsPerChannel - 1) { level_index = ditherLevelsPerChannel - 2; } | |
| float level_low = (float)level_index * step; | |
| float error = (float)val_src - level_low; | |
| float normalized_error = (step > 0) ? (error / step) : 0.0f; | |
| if (normalized_error < 0.0f) normalized_error = 0.0f; | |
| if (normalized_error > 1.0f) normalized_error = 1.0f; | |
| uint8_t val_dst; | |
| if (normalized_error >= bayerThreshold) { | |
| val_dst = (uint8_t)round(((float)level_index + 1.0f) * step); | |
| } else { | |
| val_dst = (uint8_t)round(level_low); | |
| } | |
| channels_dst[ch] = std::max(0, std::min(255, (int)val_dst)); | |
| } | |
| r_dst = channels_dst[0]; | |
| g_dst = channels_dst[1]; | |
| b_dst = channels_dst[2]; | |
| uint16_t ditheredColor = dstSprite.swap565(channels_dst[0], channels_dst[1], channels_dst[2]); // 新しいRGB値で出力先スプライトに描画 | |
| dst_buf[y * width + x] = ditheredColor; | |
| } | |
| } | |
| } | |
| //---------------------------------- | |
| boolean canStartStream = false; | |
| boolean canSendImage = false; | |
| boolean shouldClear = true; | |
| uint32_t frame_last_time = 0; //for display FPS | |
| uint8_t slider_value1 = 0; // スライダーの値を保持 (0-7) | |
| uint8_t slider_value2 = 0; | |
| // | |
| void *png_data_ptr = NULL; | |
| size_t png_data_len = 0; | |
| //---------------------------------- | |
| httpd_handle_t stream_httpd = NULL; | |
| httpd_handle_t control_httpd = NULL; | |
| void setup() { | |
| M5.begin(); | |
| Serial.begin(115200); | |
| Serial.println(); | |
| delay(1000); | |
| pinMode(POWER_GPIO_NUM, OUTPUT); | |
| digitalWrite(POWER_GPIO_NUM, LOW); | |
| delay(500); | |
| pinMode(KEY_PIN, INPUT_PULLUP); | |
| FastLED.addLeds<SK6812, LED_PIN, GRB>(LED, 1); | |
| LED[0] = CRGB::Red; | |
| FastLED.setBrightness(200); | |
| SPI.begin(7, 8, 6, -1); // 一度SDカードをマウントして確認 | |
| if (!SD.begin(15, SPI, 10000000)) { | |
| FastLED.show(); // エラー | |
| delay(500); | |
| return; | |
| } else { | |
| // パレット0から7までループ | |
| for (int i = 0; i <= maxPalettelndex; i++) { | |
| if (loadPaletteFromSD(i)) { | |
| Serial.printf("Palette %d loaded from SD.\n", i); | |
| } else { | |
| Serial.printf("Palette %d use default.\n", i); | |
| } | |
| delay(100); | |
| } | |
| } | |
| SD.end(); // 一旦ENDしておく | |
| if (psramFound()) { | |
| camera_config.pixel_format = PIXFORMAT_RGB565; | |
| camera_config.fb_location = CAMERA_FB_IN_PSRAM; | |
| camera_config.fb_count = 2; | |
| } else { | |
| FastLED.show(); // エラー | |
| delay(500); | |
| } | |
| if (!CameraBegin()) { | |
| FastLED.show(); // エラー | |
| delay(1000); | |
| ESP.restart(); | |
| } | |
| delay(500); | |
| LED[0] = CRGB::LimeGreen; | |
| FastLED.setBrightness(200); | |
| FastLED.show(); | |
| canvas_565.setColorDepth(16); | |
| canvas_565.createSprite(disp_width_pix, disp_height_pix); | |
| canvas_dihter.setColorDepth(16); | |
| canvas_dihter.createSprite(disp_width_pix, disp_height_pix); | |
| canvas_888.setColorDepth(24); | |
| canvas_888.createSprite(disp_width_pix, disp_height_pix); | |
| TaskHandle_t taskHTTP_handl; | |
| if (!xTaskCreatePinnedToCore(&taskHTTP, "taskHTTP", 9216, NULL, 24, &taskHTTP_handl, 1)) { | |
| Serial.println("Failed to create taskHTTP"); | |
| } | |
| while (!canStartStream) { | |
| delay(1); | |
| } | |
| } | |
| void loop() { | |
| if (shouldClear) { | |
| clearAll(); | |
| shouldClear = false; | |
| } | |
| if (canStartStream) { | |
| if (!canSendImage) { | |
| CameraGet(); | |
| canvas_565.pushImage(0, 0, disp_width_pix, disp_height_pix, (uint16_t *)fb->buf); | |
| CameraFree(); | |
| if (ditherLevels == 0) { | |
| convertColor_canvas(canvas_565, canvas_888); | |
| } else { | |
| BayerDither4x4(canvas_565, canvas_dihter, ditherLevels + 1); | |
| convertColor_canvas(canvas_dihter, canvas_888); | |
| } | |
| if (png_data_ptr != NULL) { | |
| free(png_data_ptr); | |
| png_data_ptr = NULL; | |
| png_data_len = 0; | |
| } | |
| png_data_ptr = canvas_888.createPng(&png_data_len, 0, 0, disp_width_pix, disp_height_pix); | |
| if (png_data_ptr != NULL) { | |
| canSendImage = true; // 送信準備完了 | |
| } else { | |
| Serial.println("PNG creation failed."); | |
| } | |
| } | |
| } | |
| } | |
| void taskHTTP(void *pvParameters) { | |
| connectToWiFi(ssid, password); | |
| startHttpd(); | |
| while (true) { | |
| delay(1); | |
| } | |
| } | |
| void startHttpd() { | |
| httpd_config_t config = HTTPD_DEFAULT_CONFIG(); | |
| httpd_uri_t index_uri = { | |
| .uri = "/", | |
| .method = HTTP_GET, | |
| .handler = index_handler, | |
| .user_ctx = NULL | |
| }; | |
| httpd_uri_t cmd_uri = { | |
| .uri = "/command", | |
| .method = HTTP_GET, | |
| .handler = cmd_handler, | |
| .user_ctx = NULL | |
| }; | |
| httpd_uri_t stream_uri = { | |
| .uri = "/stream", | |
| .method = HTTP_GET, | |
| .handler = stream_handler, | |
| .user_ctx = NULL | |
| }; | |
| // 1. コントロールサーバーをポート80 (デフォルト) で開始 | |
| if (httpd_start(&control_httpd, &config) == ESP_OK) { | |
| httpd_register_uri_handler(control_httpd, &index_uri); | |
| httpd_register_uri_handler(control_httpd, &cmd_uri); | |
| } | |
| // 2. ストリームサーバーを別のポート (81) で開始 | |
| config.server_port += 1; // ポートを 80 から 81 に変更 | |
| config.ctrl_port += 1; | |
| stream_httpd = NULL; // 明示的に初期化 | |
| if (httpd_start(&stream_httpd, &config) == ESP_OK) { | |
| httpd_register_uri_handler(stream_httpd, &stream_uri); | |
| } | |
| } | |
| static esp_err_t stream_handler(httpd_req_t *req) { | |
| canStartStream = true; | |
| esp_err_t res = ESP_OK; | |
| char *part_buf[64]; | |
| // MIMEタイプを image/png に変更 | |
| static const char *stream_part = "Content-Type: image/png\r\nContent-Length: %u\r\n\r\n"; | |
| #define PART_BOUNDARY "myboundary" | |
| static const char *stream_content_type = "multipart/x-mixed-replace;boundary=--" PART_BOUNDARY; | |
| // ヘッダ設定は変更なし | |
| res = httpd_resp_set_type(req, stream_content_type); | |
| if (res != ESP_OK) { | |
| return res; | |
| } | |
| httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); | |
| while (true) { | |
| if (canSendImage) { | |
| void *send_data_ptr = NULL; | |
| size_t send_data_len = 0; | |
| // グローバル変数からPNGデータを取得し、送信準備完了フラグをリセット | |
| // 排他制御 (Semaphore/Mutex) が必要だが、ここでは簡単のためグローバル変数を直接操作 | |
| send_data_ptr = png_data_ptr; | |
| send_data_len = png_data_len; | |
| png_data_ptr = NULL; | |
| png_data_len = 0; | |
| canSendImage = false; // 送信開始と同時にフラグを下げる | |
| if (send_data_ptr != NULL && send_data_len > 0) { | |
| // 1. 区切りヘッダの送信 | |
| static const char *stream_boundary = "\r\n--" PART_BOUNDARY "\r\n"; | |
| res = httpd_resp_send_chunk(req, stream_boundary, strlen(stream_boundary)); | |
| // 2. PNG画像情報のヘッダ送信 | |
| if (res == ESP_OK) { | |
| size_t hlen = snprintf((char *)part_buf, 64, stream_part, send_data_len); | |
| res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen); | |
| } | |
| // 3. PNGデータの送信 | |
| if (res == ESP_OK) { | |
| res = httpd_resp_send_chunk(req, (const char *)send_data_ptr, send_data_len); | |
| } | |
| // 4. 送信が完了したらメモリを解放 | |
| free(send_data_ptr); | |
| // FPS表示 (表示のためにここで実行) | |
| if (res == ESP_OK) { | |
| float fps = 1000.0 / (millis() - (float)frame_last_time); | |
| Serial.printf("%.02lf(fps) PNG Size: %u bytes\r\n", fps, send_data_len); | |
| frame_last_time = millis(); | |
| } | |
| } else { | |
| // データ生成失敗時またはサイズが0の場合 | |
| res = ESP_FAIL; | |
| } | |
| if (res != ESP_OK) { | |
| break; | |
| } | |
| } | |
| delay(1); | |
| } | |
| // ループを抜けた場合、未送信のPNGデータがあれば解放 | |
| if (png_data_ptr != NULL) { | |
| free(png_data_ptr); | |
| png_data_ptr = NULL; | |
| png_data_len = 0; | |
| } | |
| canSendImage = false; // フラグをリセット | |
| return res; | |
| } | |
| static esp_err_t index_handler(httpd_req_t *req) { | |
| String html_body = "<!DOCTYPE html>\r\n"; | |
| html_body += "<html><head></head><body style='text-align:center;'>\r\n"; | |
| html_body += "<div style='margin-top: 20px;'>\r\n"; | |
| html_body += "<img id='pic_place' width='960' height='960' style='transform:scale(1, 1); image-rendering:pixelated;' crossorigin='anonymous'>\r\n"; | |
| html_body += "<canvas id='capture_canvas' width='960' height='960' style='display:none;'></canvas>\r\n"; | |
| html_body += "</div>\r\n"; | |
| html_body += "<div style='display:flex; justify-content:center; gap:10px; flex-wrap:wrap; margin-top: 10px;'>\r\n"; | |
| html_body += "<button style='border-radius:25px; font-size: 64px; padding: 20px 40px;' onclick='startStream()'>START</button>\r\n"; | |
| html_body += "<button style='border-radius:25px; font-size: 64px; padding: 20px 40px;' onclick='changeControl(\"re_start_stream\",1)'>RESET</button>\r\n"; | |
| html_body += "<button style='border-radius:25px; font-size: 64px; padding: 20px 40px;' onclick='changeControl(\"stop_stream\",1)'>PAUSE</button>\r\n"; | |
| html_body += "<button style='border-radius:25px; font-size: 64px; padding: 20px 40px;' onclick='captureAndSave()'>CAPTURE & SAVE</button>\r\n"; | |
| html_body += "</div>\r\n"; | |
| html_body += "<style>\r\n"; | |
| html_body += "#slider1 , #slider2 { width:800px; height: 80px; appearance: none; -webkit-appearance: none; }\r\n"; | |
| html_body += "#slider1::-webkit-slider-runnable-track , #slider2::-webkit-slider-runnable-track { height: 80px; background: #ddd; border-radius: 5px; }\r\n"; | |
| html_body += "#slider1::-moz-range-track , #slider2::-moz-range-track { height: 80px; background: #ddd; border-radius: 5px; }\r\n"; | |
| html_body += "#slider1::-webkit-slider-thumb , #slider2::-webkit-slider-thumb { -webkit-appearance: none; height: 80px; width: 80px; margin-top: 0px; background: #4CAF50; border-radius: 5%; cursor: pointer; }\r\n"; | |
| html_body += "#slider1::-moz-range-thumb , #slider2::-moz-range-thumb{ height: 80px; width: 80px; background: #4CAF50; border-radius: 5%; cursor: pointer; }\r\n"; | |
| html_body += "</style>\r\n"; | |
| html_body += "<div style='margin-top: 20px;'>\r\n"; | |
| html_body += "<span id='slider_val1' style='font-size:32px;'>PALLET: 0</span><br>\r\n"; | |
| html_body += "<input type='range' min='0' max='7' value='0' step='1' id='slider1' onchange='updateSlider1(this.value)'>\r\n"; // スライダー1 | |
| html_body += "</div>\r\n"; | |
| html_body += "<div style='margin-top: 20px;'>\r\n"; | |
| html_body += "<span id='slider_val2' style='font-size:32px;'>DITHER: 0</span><br>\r\n"; | |
| html_body += "<input type='range' min='0' max='7' value='0' step='1' id='slider2' onchange='updateSlider2(this.value)'>\r\n"; // スライダー2 | |
| html_body += "<script>\r\n"; | |
| html_body += "var base_url = document.location.origin;\r\n"; | |
| html_body += "var url_stream = base_url + ':81';\r\n"; | |
| html_body += "function startStream() {\r\n"; | |
| html_body += "var pic = document.getElementById('pic_place');\r\n"; | |
| html_body += "pic.src = url_stream+'/stream';};\r\n"; | |
| html_body += "function changeControl(id_txt, value_txt){\r\n"; | |
| html_body += "var new_url = base_url+'/command?id=';\r\n"; | |
| html_body += "new_url += id_txt + '&';\r\n"; | |
| html_body += "new_url += 'value=' + value_txt;\r\n"; | |
| html_body += "fetch(new_url, {method: 'GET'})\r\n"; | |
| html_body += ".then((response) => {\r\n"; | |
| html_body += " if(response.ok){console.log('Command ' + id_txt + ' sent.');}\r\n"; | |
| html_body += " else {console.error('Command failed with status: ' + response.status);}\r\n"; | |
| html_body += "})\r\n"; | |
| html_body += ".catch((error) => console.error('Network error during command send:', error));\r\n"; | |
| html_body += "};\r\n"; | |
| html_body += "function updateSlider1(value_txt){\r\n"; | |
| html_body += "document.getElementById('slider_val1').innerText = 'PALLET: ' + value_txt;\r\n"; | |
| html_body += "changeControl('slider1', value_txt);\r\n"; | |
| html_body += "};\r\n"; | |
| html_body += "function updateSlider2(value_txt){\r\n"; | |
| html_body += "document.getElementById('slider_val2').innerText = 'DITHER: ' + value_txt;\r\n"; | |
| html_body += "changeControl('slider2', value_txt);\r\n"; | |
| html_body += "};\r\n"; | |
| html_body += "function captureAndSave() {\r\n"; | |
| html_body += " const img = document.getElementById('pic_place');\r\n"; | |
| html_body += " const canvas = document.getElementById('capture_canvas');\r\n"; | |
| html_body += " if (!img.src || img.src.indexOf('/stream') === -1) {\r\n"; | |
| html_body += " alert('Streaming is not started.');\r\n"; | |
| html_body += " return;\r\n"; | |
| html_body += " }\r\n"; | |
| html_body += " try {\r\n"; | |
| html_body += " // Get the latest cached image data from the <img> tag and draw it onto the hidden canvas.\r\n"; | |
| html_body += " const ctx = canvas.getContext('2d');\r\n"; | |
| html_body += " // Set the canvas context to use Nearest-Neighbor scaling for pixelated effect\r\n"; | |
| html_body += " ctx.imageSmoothingEnabled = false;\r\n"; | |
| html_body += " ctx.webkitImageSmoothingEnabled = false;\r\n"; | |
| html_body += " ctx.mozImageSmoothingEnabled = false;\r\n"; | |
| html_body += " // Draw the image onto the canvas using the display size (960x960) to preserve scaling.\r\n"; | |
| html_body += " ctx.drawImage(img, 0, 0, 960, 960);\r\n"; | |
| html_body += " // Get the canvas content as a PNG data URL\r\n"; | |
| html_body += " const dataURL = canvas.toDataURL('image/png');\r\n"; | |
| html_body += " // Create a temporary link element for download\r\n"; | |
| html_body += " const a = document.createElement('a');\r\n"; | |
| html_body += " a.href = dataURL;\r\n"; | |
| html_body += " // Generate a file name: capture_YYYYMMDD_HHMMSS.png\r\n"; | |
| html_body += " const now = new Date();\r\n"; | |
| html_body += " const filename = `capture_${now.getFullYear()}${('0'+(now.getMonth()+1)).slice(-2)}${('0'+now.getDate()).slice(-2)}_${('0'+now.getHours()).slice(-2)}${('0'+now.getMinutes()).slice(-2)}${('0'+now.getSeconds()).slice(-2)}.png`;\r\n"; | |
| html_body += " a.download = filename;\r\n"; | |
| html_body += " // Simulate a click to start the download\r\n"; | |
| html_body += " document.body.appendChild(a);\r\n"; | |
| html_body += " a.click();\r\n"; | |
| html_body += " document.body.removeChild(a);\r\n"; | |
| html_body += " console.log(`Image captured and saved as: ${filename}`);\r\n"; | |
| html_body += " } catch (error) {\r\n"; | |
| html_body += " console.error('Capture failed:', error);\r\n"; | |
| html_body += " alert('Failed to capture the image. Please ensure streaming is active.');\r\n"; | |
| html_body += " }\r\n"; | |
| html_body += "};\r\n"; | |
| html_body += "</script></body></html>\r\n"; | |
| httpd_resp_set_type(req, "text/html"); | |
| httpd_resp_set_hdr(req, "Accept-Charset", "UTF-8"); | |
| return httpd_resp_send(req, html_body.c_str(), html_body.length()); | |
| } | |
| static esp_err_t cmd_handler(httpd_req_t *req) { | |
| char *buf; | |
| size_t buf_len; | |
| char id_txt[32] = { | |
| 0, | |
| }; | |
| char value_txt[32] = { | |
| 0, | |
| }; | |
| buf_len = httpd_req_get_url_query_len(req) + 1; | |
| if (buf_len > 1) { | |
| buf = (char *)malloc(buf_len); | |
| if (!buf) { | |
| httpd_resp_send_500(req); | |
| return ESP_FAIL; | |
| } | |
| if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) { | |
| Serial.println(buf); | |
| if (httpd_query_key_value(buf, "id", id_txt, sizeof(id_txt)) == ESP_OK && httpd_query_key_value(buf, "value", value_txt, sizeof(value_txt)) == ESP_OK) { | |
| } else { | |
| free(buf); | |
| httpd_resp_send_404(req); | |
| return ESP_FAIL; | |
| } | |
| } else { | |
| Serial.println(buf); | |
| free(buf); | |
| httpd_resp_send_404(req); | |
| return ESP_FAIL; | |
| } | |
| free(buf); | |
| } else { | |
| httpd_resp_send_404(req); | |
| return ESP_FAIL; | |
| } | |
| uint8_t val = atoi(value_txt); | |
| int res = 0; | |
| if (!strcmp(id_txt, "re_start_stream")) { | |
| canStartStream = true; | |
| shouldClear = true; | |
| Serial.printf("%s = %d\r\n", id_txt, val); | |
| } else if (!strcmp(id_txt, "stop_stream")) { | |
| canStartStream = false; | |
| if (png_data_ptr != NULL) { | |
| free(png_data_ptr); | |
| png_data_ptr = NULL; | |
| png_data_len = 0; | |
| canSendImage = false; | |
| } | |
| Serial.printf("%s = %d\r\n", id_txt, val); | |
| } else if (!strcmp(id_txt, "slider1")) { | |
| slider_value1 = val; | |
| Serial.printf("%s = %d\r\n", id_txt, slider_value1); | |
| currentPalettelndex = slider_value1; | |
| } else if (!strcmp(id_txt, "slider2")) { | |
| slider_value2 = val; | |
| Serial.printf("%s = %d\r\n", id_txt, slider_value2); | |
| ditherLevels = slider_value2; | |
| } else { | |
| res = -1; | |
| } | |
| if (res) { | |
| return httpd_resp_send_500(req); | |
| } | |
| httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); | |
| return httpd_resp_send(req, NULL, 0); | |
| } | |
| void clearAll() { | |
| canvas_565.fillScreen(TFT_BLACK); | |
| canvas_888.fillScreen(TFT_BLACK); | |
| canvas_dihter.fillScreen(TFT_BLACK); | |
| } | |
| void connectToWiFi(const char *ssid, const char *pwd) { | |
| Serial.println("Setting up ESP32 as an Access Point..."); | |
| WiFi.mode(WIFI_AP); // Wi-FiモードをAP (Access Point) に設定 | |
| if (WiFi.softAP(ssid, password)) { | |
| Serial.println("Access Point started!"); | |
| IPAddress myIP = WiFi.softAPIP(); // APのIPアドレスを取得 (通常は 192.168.4.1) | |
| Serial.print("Access Point SSID: "); | |
| Serial.println(ssid); | |
| Serial.print("Access Point IP address: "); | |
| Serial.println(myIP); | |
| delay(1000); // HTTPサーバがIPアドレスをバインドできるように少し待機 | |
| } else { | |
| Serial.println("Failed to start Access Point!"); | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment