2026年2月19日 星期四

[Django 6.0] 把尚虎雲生產端佈署到Render平台

1.下載尚虎雲生產端的軟體

載點:https://github.com/shanghuyun-team/Shanghuyun-Production-side/releases

2.解開後的目錄


3.增加render.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
databases:
  - name: Shanghuyundb          # 資料庫服務名稱(可自訂)
    plan: free              # free / starter 等,免費版夠用
    databaseName: Shanghuyun    # 資料庫名稱
    user: Shanghuyun            # 使用者名稱(Render 會自動產生密碼)

services:
  - type: web
    name: Shanghuyun-Production-Side           # Web Service 名稱
    env: python             # Python 環境
    plan: free              # 免費方案
    region: singapore       # 建議選離台灣近的(如 singapore 或 oregon),跟資料庫同區域避免延遲
    buildCommand: pip install -r requirements.txt && python manage.py collectstatic --noinput
    startCommand: gunicorn core.wsgi:application --bind 0.0.0.0:$PORT
    # 如果你的專案資料夾是 Shanghuyun-Production-Side/core(內層),startCommand 改成:gunicorn Shanghuyun-Production-Side.wsgi:application ...

    envVars:
      - key: PYTHON_VERSION
        value: 3.14.1           # 或你的 Python 版本
      - key: SECRET_KEY
        generateValue: true   # Render 自動產生安全的 SECRET_KEY
      - key: DEBUG
        value: "False"
      - key: DATABASE_URL
        fromDatabase:
          name: mysitedb      # 對應上面的資料庫 name
          property: connectionString  # 自動注入 postgresql://... 格式
      - key: ALLOWED_HOSTS
        value: "*"            # 測試用,之後改成你的域名

4.在Github建Rep
Shanghuyun-Production-side

5.把軟體上傳到Github
  • 如果還沒 repo:
git init
git add .
git commit -m "Initial commit"
  • 建立 GitHub repo(private 或 public 都行)
  • push:
git remote add origin https://github.com/你的帳號/你的專案.git
git branch -M main
git push -u origin main

6. 建立新的藍圖

https://dashboard.render.com/blueprints




7.在Start Command中填入:
gunicorn core.wsgi:application --bind 0.0.0.0:$PORT

按下佈署鍵

8.佈署完成後,按下連結打開

2026年2月18日 星期三

[Django 6.0] 佈署Django到Render平台

1. 安裝虛擬環境工具
pip install virtualenvwrapper-win


2.建立虛擬環境

mkvirtualenv django_env

執行後畫面


環境都存在哪裡?

預設位置:

C:\Users\你的帳號\Envs


3.熟悉虛擬環境的指令

python --version → 確認是乾淨環境

deactivate → 離開

lsvirtualenv 或 workon → 看到 testenv 了嗎?

workon testenv → 再切回去


4.安裝Django

pip install django


5.製作安裝套件的文件requirements.txt

pip freeze > requirements.txt


6.初始化mysite Django 專案

django-admin startproject mysite


7.更換工作目錄
cd mysite

8.執行本地端網站
python manage.py runserver


9.打開瀏覽器
http://127.0.0.1:8000/

10.建立一個新應用程式

python manage.py startapp myapp


11.添加應用程式,mysite/mysite/settings.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
"""
Django settings for mysite project.

Generated by 'django-admin startproject' using Django 6.0.2.

For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-!mi(werx6fq32e)h4we%q(d5k=(=coxpx^ux9z!#pqv=*-il13'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'mysite.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'mysite.wsgi.application'


# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/

STATIC_URL = 'static/'

12.建立網頁 mysite/templates/index.html 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <title>Hello Django on Render!</title>

    <link rel="stylesheet"
          href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
          integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
          crossorigin="anonymous">
</head>
<body>
<main class="container">
    <div class="row text-center justify-content-center">
        <div class="col">
            <h1 class="display-4">Hello World!</h1>
        </div>
    </div>
</main>
</body>
</html>


13.建立一個簡單的視圖,mysite/myapp/views.py

1
2
3
4
5
from django.shortcuts import render


def index(request):
    return render(request, 'index.html', {})

14.設定路徑,mysite/mysite/uels.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""
URL configuration for mysite project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/6.0/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from myapp import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index, name='index'),
]

15.重新啟動網站
python manage.py runserver

16.用render.yaml部署,放在mysite目錄下,和manage.py在同一個目錄。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
databases:
  - name: mysitedb          # 資料庫服務名稱(可自訂)
    plan: free              # free / starter 等,免費版夠用
    databaseName: mysite    # 資料庫名稱
    user: mysite            # 使用者名稱(Render 會自動產生密碼)

services:
  - type: web
    name: mysite            # Web Service 名稱
    env: python             # Python 環境
    plan: free              # 免費方案
    region: singapore       # 建議選離台灣近的(如 singapore 或 oregon),跟資料庫同區域避免延遲
    buildCommand: pip install -r requirements.txt && python manage.py collectstatic --noinput
    startCommand: gunicorn mysite.wsgi:application --bind 0.0.0.0:$PORT
    # 如果你的專案資料夾是 mysite/mysite(內層),startCommand 改成:gunicorn mysite.wsgi:application ...

    envVars:
      - key: PYTHON_VERSION
        value: 3.14.1           # 或你的 Python 版本
      - key: SECRET_KEY
        generateValue: true   # Render 自動產生安全的 SECRET_KEY
      - key: DEBUG
        value: "False"
      - key: DATABASE_URL
        fromDatabase:
          name: mysitedb      # 對應上面的資料庫 name
          property: connectionString  # 自動注入 postgresql://... 格式
      - key: ALLOWED_HOSTS
        value: "*"            # 測試用,之後改成你的域名

17.先把專案推到 GitHub
  • 如果還沒 repo:
git init
git add .
git commit -m "Initial commit"
  • 建立 GitHub repo(private 或 public 都行)
  • push:
git remote add origin https://github.com/你的帳號/你的專案.git
git branch -M main
git push -u origin main

18.建立新的藍圖
https://dashboard.render.com/blueprints


19.選擇包含您的藍圖的儲存庫,然後按一下「連線」。


20.命名後,按下部署鍵

21.部署過程,顯示:ModuleNotFoundError: No module named 'app'
在 Render Dashboard 修正 Start Command 登入 https://dashboard.render.com → 選你的 Web Service(mysite) 點 Settings(或 Edit) 找到 Start Command 欄位 改成:gunicorn mysite.wsgi:application

22.再度部署,部署成功後,瀏覽器訪問時,收到 400 Bad Request
127.0.0.1 - - [18/Feb/2026:08:20:57 +0000] "HEAD / HTTP/1.1" 400 143 ... 127.0.0.1 - - [18/Feb/2026:08:21:01 +0000] "GET / HTTP/1.1" 400 143 ...
這不是 Gunicorn 或 port 綁定問題,而是 Django 的安全檢查 觸發:你的 ALLOWED_HOSTS 設定沒包含 Render 給的域名(這裡是 mysite-1-p20l.onrender.com)。
Django 在 production 模式(DEBUG=False)下,會嚴格驗證 Host header。如果 Host 不匹配 ALLOWED_HOSTS,就直接回 400 Bad Request(不會給詳細錯誤頁面)。

23.修改mysite/mysite/setting.py
ALLOWED_HOSTS = [
    'mysite-1-p20l.onrender.com',   # 你的實際 Render URL(從 log 抄)
    '.onrender.com',                 # 萬用(wildcard),涵蓋所有子域名,測試超方便
    'localhost',                     # 本地開發用,可選
    '127.0.0.1',
]

24.重新把修改後的程式,上傳到Github
git add mysite/settings.py
git commit -m "Fix ALLOWED_HOSTS for Render deployment"
git push origin main

25.自動重新部署
Render 會自動偵測 push → 自動 redeploy(如果開了 auto-deploy)
也可以用手動 redeploy(如果等不及)
去 Render Dashboard → 你的服務(mysite-1-p20l)→ Manual Deploy → Deploy latest commit

26.開啟網站



2026年2月17日 星期二

[OTTO GO] 動畫測試-LVGL + lv_spinner Loading 動畫



arduino:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <lvgl.h>

// TFT 腳位(保持原樣)
#define TFT_MOSI 19
#define TFT_SCLK 18
#define TFT_CS   5
#define TFT_DC   16
#define TFT_RST  23

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);

// LVGL v9 需要的緩衝區(大小單位:bytes,不是 pixels)
#define DRAW_BUF_SIZE (240 * 40 * sizeof(lv_color_t))   // 約 19KB,1/10 螢幕
// 緩衝區(v9 用 uint8_t 陣列)
#define DRAW_BUF_SIZE (320 * 240 / 10 * sizeof(lv_color_t))  // 約 1/10 螢幕大小
static uint8_t draw_buf1[DRAW_BUF_SIZE];
static uint8_t draw_buf2[DRAW_BUF_SIZE];  // 雙緩衝更好(防閃爍),RAM 夠的話用

void my_disp_flush(lv_display_t * display, const lv_area_t * area, uint8_t * px_map) {
  uint32_t w = lv_area_get_width(area);
  uint32_t h = lv_area_get_height(area);

  tft.startWrite();
  tft.setAddrWindow(area->x1, area->y1, w, h);
  tft.writePixels((uint16_t *)px_map, w * h, true, false);
  tft.endWrite();

  lv_display_flush_ready(display);
}

void setup() {
  Serial.begin(115200);
  delay(100);
  Serial.println("Starting LVGL v9 + Adafruit ST7789");

  SPI.begin(TFT_SCLK, -1, TFT_MOSI, TFT_CS);
  tft.init(240, 320);           // 你的螢幕解析度(如果 240x240 就改)
  tft.setRotation(1);
  tft.fillScreen(ST77XX_BLACK);

  lv_init();

  // 建立 display(注意寬高依 rotation 調整:寬 320、高 240)
  lv_display_t * disp = lv_display_create(320, 240);

  // 設定緩衝區(PARTIAL 模式最常用,適合小緩衝)
  lv_display_set_buffers(disp, draw_buf1, draw_buf2, DRAW_BUF_SIZE, LV_DISPLAY_RENDER_MODE_PARTIAL);

  // 設定 flush callback
  lv_display_set_flush_cb(disp, my_disp_flush);

  // 建立 spinner 測試
  lv_obj_t * spinner = lv_spinner_create(lv_screen_active());
  lv_obj_set_size(spinner, 100, 100);
  lv_obj_center(spinner);
  lv_obj_set_style_arc_width(spinner, 12, 0);
  lv_obj_set_style_arc_color(spinner, lv_color_hex(0x2196F3), LV_PART_INDICATOR);

  Serial.println("Setup complete - spinner should rotate now!");
}

unsigned long last_tick = 0;
void loop() {
  unsigned long now = millis();
  if (now - last_tick >= 5) {
    lv_tick_inc(now - last_tick);   // 更新時間
    lv_timer_handler();
    last_tick = now;
  }
  // delay(1);  // 可選,小 delay 避免 CPU 100%
}

[OTTO GO] 動畫測試-利用LVGL技術,一次播4張

 

1.請參閱前一篇文章,轉換成4張圖檔。

2.並按照前一篇文章,把產生的4個.c檔案變成4個.h和4個.c的檔案,然後放到主檔案的目錄。

3.arduino程式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/*
  ESP32 + Adafruit_ST7789(240x320) 播放 tile000~tile003 的 RGB565 圖 (320x240)

  TFT pins:
    MOSI=19, SCLK=18, CS=5, DC=16, RST=23

  前提:
  - tile000.c ~ tile003.c 各自只保留像素陣列(uint8_t[]),不要包含 LVGL 的 lv_img_dsc_t
  - 若有 LV_ATTRIBUTE_* 巨集,需在 .c 最上方補上空巨集定義
*/

#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>

#include "tile000.h"
#include "tile001.h"
#include "tile002.h"
#include "tile003.h"

// ===== TFT pins =====
#define TFT_MOSI 19
#define TFT_SCLK 18
#define TFT_CS    5
#define TFT_DC   16
#define TFT_RST  23

Adafruit_ST7789 tft(TFT_CS, TFT_DC, TFT_RST);

// 固定尺寸
static const uint16_t W = 320;
static const uint16_t H = 240;
static const uint32_t FRAME_BYTES = (uint32_t)W * (uint32_t)H * 2u;

// 一行 buffer:320*2 = 640 bytes
static uint16_t lineBuf[W];

// 你的 .c 匯出多數是 little-endian RGB565(低位元組在前)
static const bool SOURCE_IS_LITTLE_ENDIAN = true;

static inline uint16_t read565(const uint8_t *p) {
  if (SOURCE_IS_LITTLE_ENDIAN) return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
  else return ((uint16_t)p[0] << 8) | (uint16_t)p[1];
}

void drawRGB565_320x240(const uint8_t *dataBytes) {
  tft.startWrite();
  tft.setAddrWindow(0, 0, W, H);

  const uint32_t rowBytes = (uint32_t)W * 2u;

  for (uint16_t y = 0; y < H; y++) {
    const uint8_t *row = dataBytes + (uint32_t)y * rowBytes;

    for (uint16_t x = 0; x < W; x++) {
      lineBuf[x] = read565(row + (uint32_t)x * 2u);
    }

    // 一次推一行
    tft.writePixels(lineBuf, W, true);
  }

  tft.endWrite();
}

void setup() {
  Serial.begin(115200);
  delay(200);

  SPI.begin(TFT_SCLK, -1, TFT_MOSI, TFT_CS);

  tft.init(240, 320);
  tft.setRotation(1);          // 320x240
  tft.fillScreen(ST77XX_BLACK);

  Serial.printf("TFT size: %d x %d\n", tft.width(), tft.height());
  Serial.printf("Frame bytes expected: %u\n", (unsigned)FRAME_BYTES);

  tft.setTextColor(ST77XX_YELLOW);
  tft.setTextSize(2);
  tft.setCursor(8, 8);
  tft.print("tile player");
}

void loop() {
  // 依序播放 4 幀
  drawRGB565_320x240(tile000_map);
  delay(60);

  drawRGB565_320x240(tile001_map);
  delay(60);

  drawRGB565_320x240(tile002_map);
  delay(60);

  drawRGB565_320x240(tile003_map);
  delay(60);
}

[OTTO GO] 圖片顯示,利用LVGL技術



1.圖檔轉換工具:先把圖檔轉換成LVGL格式

https://lvgl.io/tools/imageconverter

2.選擇RGB565產生一個.c的圖檔,例子是animation2.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#ifdef __has_include
    #if __has_include("lvgl.h")
        #ifndef LV_LVGL_H_INCLUDE_SIMPLE
            #define LV_LVGL_H_INCLUDE_SIMPLE
        #endif
    #endif
#endif

#if defined(LV_LVGL_H_INCLUDE_SIMPLE)
    #include "lvgl.h"
#else
    #include "lvgl/lvgl.h"
#endif


#ifndef LV_ATTRIBUTE_MEM_ALIGN
#define LV_ATTRIBUTE_MEM_ALIGN
#endif

#ifndef LV_ATTRIBUTE_IMAGE_TILE000
#define LV_ATTRIBUTE_IMAGE_TILE000
#endif

const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMAGE_TILE000 uint8_t tile000_map[] = {
/*圖檔資料太, 省略*/
};

const lv_image_dsc_t tile000 = {
  .header.cf = LV_COLOR_FORMAT_RGB565,
  .header.magic = LV_IMAGE_HEADER_MAGIC,
  .header.w = 320,
  .header.h = 240,
  .data_size = 76800 * 2,
  .data = tile000_map,
};

3.把檔案切割成animation2.h和animation2.c
animation2.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#pragma once
#include <Arduino.h>

#ifdef __cplusplus
extern "C" {
#endif
extern const uint8_t animation2_map[];
#ifdef __cplusplus
}
#endif

static const uint16_t ANIM2_W = 320;
static const uint16_t ANIM2_H = 240;
static const uint32_t ANIM2_SIZE = (uint32_t)ANIM2_W * (uint32_t)ANIM2_H * 2u;

antimation2.c:

1
2
3
4
#include <stdint.h>
const uint8_t animation2_map[] = {
/*圖檔資料太, 省略*/
}

4. arduino主檔程式


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/*
  ESP32 + Adafruit_ST7789(240x320) 顯示 animation2.c 的 RGB565 圖 (320x240)

  TFT pins (你前面驗證可用那組):
    MOSI=19, SCLK=18, CS=5, DC=16, RST=23

  重要:
  - tft.setRotation(1) 讓螢幕座標變成 320x240,剛好完整顯示
  - animation2.c 需移除底部 lv_image_dsc_t descriptor(只留 animation2_map 陣列)
*/

#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>

#include "animation2.h"   // extern animation2_map + W/H

// ===== TFT pins =====
#define TFT_MOSI 19
#define TFT_SCLK 18
#define TFT_CS    5
#define TFT_DC   16
#define TFT_RST  23

Adafruit_ST7789 tft(TFT_CS, TFT_DC, TFT_RST);

// 320 像素一行的暫存(RAM 640 bytes)
static uint16_t lineBuf[ANIM2_W];

// 你的 animation2.c 是 RGB565 的 bytes
// 多數 LVGL 匯出 RGB565 會是 little-endian:低位元組在前
// 若你顏色不對(例如整體偏紫/偏綠),把這個改成 0 試試
static const bool SOURCE_IS_LITTLE_ENDIAN = true;

static inline uint16_t read565(const uint8_t *p) {
  if (SOURCE_IS_LITTLE_ENDIAN) {
    return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
  } else {
    return ((uint16_t)p[0] << 8) | (uint16_t)p[1];
  }
}

void drawRGB565_320x240(const uint8_t *dataBytes) {
  // dataBytes 長度必須 = 320*240*2 = 153600
  tft.startWrite();
  tft.setAddrWindow(0, 0, ANIM2_W, ANIM2_H);

  const uint32_t rowBytes = (uint32_t)ANIM2_W * 2u;

  for (uint16_t y = 0; y < ANIM2_H; y++) {
    const uint8_t *row = dataBytes + (uint32_t)y * rowBytes;

    for (uint16_t x = 0; x < ANIM2_W; x++) {
      lineBuf[x] = read565(row + (uint32_t)x * 2u);
    }

    // 一次推一行
    // Adafruit_ST7789(Adafruit_SPITFT) 提供 writePixels
    tft.writePixels(lineBuf, ANIM2_W, true);
  }

  tft.endWrite();
}

void setup() {
  Serial.begin(115200);
  delay(200);

  // 用你穩定的 SPI begin(Adafruit_ST7789 會使用預設 SPI instance)
  SPI.begin(TFT_SCLK, -1, TFT_MOSI, TFT_CS);

  tft.init(240, 320);
  tft.setRotation(1);          // 變成 320x240
  tft.fillScreen(ST77XX_BLACK);

  Serial.printf("TFT size: %d x %d\n", tft.width(), tft.height());
  Serial.printf("Expect:   %u bytes\n", (unsigned)ANIM2_SIZE);

  // 顯示一次
  drawRGB565_320x240(animation2_map);

  // 顯示文字提示
  tft.setTextColor(ST77XX_YELLOW);
  tft.setTextSize(2);
  tft.setCursor(8, 8);
  tft.print("animation2.c RGB565");
}

void loop() {
  // 你這份 animation2.c 只有單張圖,所以 loop 不會動
  delay(1000);
}