标签: code

  • 【ESP32】ESP32S3通过USB+WIFI透传串口GPS信息并自动切换波特率

    基于微雪ESP32-S3-Touch-LCD-1.47,基于ESP-IDF 5.5.x,该代码目的适配ATGM和UBLOX部分GPS模块,可以热切换115200、38400波特率,实测115200波特率下可以实现5Hz下的NMEA语句持续平稳透传,10Hz下不太稳定。

    #include <stdio.h>
    #include <string.h>
    #include <stdbool.h>
    
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "freertos/semphr.h"
    #include "freertos/ringbuf.h"
    
    #include "driver/uart.h"
    #include "driver/usb_serial_jtag.h"
    
    #include "esp_log.h"
    #include "esp_wifi.h"
    #include "esp_event.h"
    #include "nvs_flash.h"
    #include "lwip/sockets.h"
    #include "esp_timer.h"
    
    /* ================== 配置常量 ================== */
    #define TAG                "GPS_BRIDGE"
    
    // 物理串口
    #define GPS_UART_NUM       UART_NUM_1
    #define GPS_TX_PIN         43
    #define GPS_RX_PIN         44
    
    // 缓存大小
    #define UART_BUF_SIZE      2048
    #define RINGBUF_SIZE       (16 * 1024)
    
    // 网络配置
    #define TCP_PORT           8080
    #define WIFI_SSID          "ESP32S3_GPS"
    #define WIFI_PASS          "12345678"
    
    // GPS参数
    static const int baud_list[] = {115200, 38400};
    #define BAUD_NUM            (sizeof(baud_list) / sizeof(baud_list[0]))
    #define DETECT_TIMEOUT_MS   1500  // 每个波特率停留尝试时间
    #define LOSS_TIMEOUT_SEC    5     // 5秒无NMEA数据则判定丢失
    
    /* ================== 全局句柄 ================== */
    static RingbufHandle_t uart_rb;
    static SemaphoreHandle_t sock_mutex;
    static SemaphoreHandle_t uart_mutex;
    
    static int  client_sock = -1;
    static int  current_baud_idx = 0;
    static bool baud_locked = false;
    
    /* ================== 工具函数 ================== */
    
    // 安全地重新初始化 UART
    static void uart_reinit(int baudrate)
    {
        xSemaphoreTake(uart_mutex, portMAX_DELAY);
        
        if (uart_is_driver_installed(GPS_UART_NUM)) {
            uart_driver_delete(GPS_UART_NUM);
        }
    
        uart_config_t cfg = {
            .baud_rate = baudrate,
            .data_bits = UART_DATA_8_BITS,
            .parity    = UART_PARITY_DISABLE,
            .stop_bits = UART_STOP_BITS_1,
            .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
            .source_clk = UART_SCLK_DEFAULT,
        };
    
        // 安装驱动:缓冲区设为 UART_BUF_SIZE 的 2 倍
        uart_driver_install(GPS_UART_NUM, UART_BUF_SIZE * 2, 0, 0, NULL, 0);
        uart_param_config(GPS_UART_NUM, &cfg);
        uart_set_pin(GPS_UART_NUM, GPS_TX_PIN, GPS_RX_PIN, -1, -1);
    
        ESP_LOGW(TAG, "UART re-init: %d baud, scanning for GPS...", baudrate);
        
        xSemaphoreGive(uart_mutex);
    }
    
    // 协议特征检测
    static bool is_valid_nmea(const uint8_t *buf, int len)
    {
        if (len < 6) return false;
        // NMEA 标准报文必须以 $ 开头
        if (buf[0] != '$') return false;
    
        // 检查常见的 NMEA 类型标识符
        if (memmem(buf, len, "GGA", 3) || 
            memmem(buf, len, "RMC", 3) || 
            memmem(buf, len, "GSA", 3) ||
            memmem(buf, len, "GSV", 3)) {
            return true;
        }
        return false;
    }
    
    /* ================== 核心任务 ================== */
    
    /**
     * 任务1: UART 接收与波特率自动匹配
     * 优先级: 高 (10)
     */
    static void gps_rx_task(void *arg)
    {
        static uint8_t rx_buf[UART_BUF_SIZE];
        int64_t last_baud_switch = 0;
        int64_t last_valid_data  = 0;
    
        uart_reinit(baud_list[current_baud_idx]);
        last_baud_switch = esp_timer_get_time();
    
        while (1) {
            // 使用锁保护 UART 读取过程
            xSemaphoreTake(uart_mutex, portMAX_DELAY);
            int len = uart_read_bytes(GPS_UART_NUM, rx_buf, sizeof(rx_buf), pdMS_TO_TICKS(20));
            xSemaphoreGive(uart_mutex);
    
            int64_t now = esp_timer_get_time();
    
            if (len > 0) {
                if (is_valid_nmea(rx_buf, len)) {
                    last_valid_data = now;
                    if (!baud_locked) {
                        baud_locked = true;
                        ESP_LOGI(TAG, ">>> GPS Locked @ %d baud <<<", baud_list[current_baud_idx]);
                    }
                }
                
                // 只有锁定了正确的波特率才转发数据,过滤扫描时的乱码
                if (baud_locked) {
                    xRingbufferSend(uart_rb, rx_buf, len, 0);
                }
            }
    
            /* 状态机切换逻辑 */
            if (!baud_locked) {
                // 未锁定时:超时轮换波特率
                if ((now - last_baud_switch) / 1000 > DETECT_TIMEOUT_MS) {
                    current_baud_idx = (current_baud_idx + 1) % BAUD_NUM;
                    uart_reinit(baud_list[current_baud_idx]);
                    last_baud_switch = now;
                }
            } else {
                // 已锁定时:长时间无有效 NMEA 信号判定为丢失
                if ((now - last_valid_data) / 1000000 > LOSS_TIMEOUT_SEC) {
                    ESP_LOGE(TAG, "GPS signal lost, restarting scan...");
                    baud_locked = false;
                    last_baud_switch = now;
                }
            }
        }
    }
    
    /**
     * 任务2: 数据多路分发 (UART -> USB & WiFi)
     * 优先级: 中 (6)
     */
    static void tx_dispatcher_task(void *arg)
    {
        size_t len;
        uint8_t *data;
    
        while (1) {
            // 从环形缓冲区提取数据,最大等待
            data = (uint8_t *)xRingbufferReceive(uart_rb, &len, portMAX_DELAY);
            if (!data) continue;
    
            // 1. 发送到物理 USB (CDC)
            if (usb_serial_jtag_is_connected()) {
                usb_serial_jtag_write_bytes(data, len, 0);
            }
    
            // 2. 发送到 TCP 客户端
            if (xSemaphoreTake(sock_mutex, 0) == pdTRUE) {
                if (client_sock != -1) {
                    // 使用非阻塞发送,防止 WiFi 拥塞卡住串口读写
                    int sent = send(client_sock, data, len, MSG_DONTWAIT);
                    if (sent < 0 && (errno != EAGAIN && errno != EWOULDBLOCK)) {
                        ESP_LOGW(TAG, "TCP send error, client might be dead");
                    }
                }
                xSemaphoreGive(sock_mutex);
            }
    
            vRingbufferReturnItem(uart_rb, data);
        }
    }
    
    /**
     * 任务3: 反向路径 USB -> UART
     */
    static void usb_rx_task(void *arg)
    {
        static uint8_t buf[512];
        while (1) {
            int len = usb_serial_jtag_read_bytes(buf, sizeof(buf), pdMS_TO_TICKS(10));
            if (len > 0 && baud_locked) {
                xSemaphoreTake(uart_mutex, portMAX_DELAY);
                uart_write_bytes(GPS_UART_NUM, (char *)buf, len);
                xSemaphoreGive(uart_mutex);
            }
        }
    }
    
    /**
     * 任务4: TCP 服务端 (WiFi -> UART)
     */
    static void tcp_server_task(void *arg)
    {
        struct sockaddr_in addr = {
            .sin_family = AF_INET,
            .sin_port = htons(TCP_PORT),
            .sin_addr.s_addr = htonl(INADDR_ANY)
        };
    
        int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
        int opt = 1;
        setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        bind(listen_sock, (struct sockaddr *)&addr, sizeof(addr));
        listen(listen_sock, 1);
    
        static uint8_t buf[512];
        while (1) {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sock = accept(listen_sock, (struct sockaddr *)&client, &len);
            if (sock < 0) continue;
    
            xSemaphoreTake(sock_mutex, portMAX_DELAY);
            if (client_sock != -1) close(client_sock);
            client_sock = sock;
            xSemaphoreGive(sock_mutex);
    
            ESP_LOGI(TAG, "New TCP client connected");
    
            while (1) {
                int r = recv(sock, buf, sizeof(buf), 0);
                if (r <= 0) break;
    
                if (baud_locked) {
                    xSemaphoreTake(uart_mutex, portMAX_DELAY);
                    uart_write_bytes(GPS_UART_NUM, (char *)buf, r);
                    xSemaphoreGive(uart_mutex);
                }
            }
    
            ESP_LOGI(TAG, "TCP client disconnected");
            xSemaphoreTake(sock_mutex, portMAX_DELAY);
            if (client_sock == sock) client_sock = -1;
            close(sock);
            xSemaphoreGive(sock_mutex);
        }
    }
    
    /* ================== 系统初始化 ================== */
    
    static void wifi_init_softap(void)
    {
        ESP_ERROR_CHECK(esp_netif_init());
        ESP_ERROR_CHECK(esp_event_loop_create_default());
        esp_netif_create_default_wifi_ap();
    
        wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
        ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    
        wifi_config_t ap_cfg = {
            .ap = {
                .ssid = WIFI_SSID,
                .ssid_len = strlen(WIFI_SSID),
                .password = WIFI_PASS,
                .channel = 1,
                .max_connection = 4,
                .authmode = WIFI_AUTH_WPA2_PSK
            }
        };
    
        ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
        ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_cfg));
        ESP_ERROR_CHECK(esp_wifi_start());
    }
    
    void app_main(void)
    {
        // 1. 初始化存储
        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);
    
        // 2. 初始化同步组件与缓冲区
        uart_rb    = xRingbufferCreate(RINGBUF_SIZE, RINGBUF_TYPE_BYTEBUF);
        sock_mutex = xSemaphoreCreateMutex();
        uart_mutex = xSemaphoreCreateMutex();
    
        // 3. 初始化 USB 驱动
        usb_serial_jtag_driver_config_t usb_cfg = {
            .rx_buffer_size = 2048,
            .tx_buffer_size = 2048
        };
        usb_serial_jtag_driver_install(&usb_cfg);
    
        // 4. 初始化 WiFi
        wifi_init_softap();
    
        // 5. 创建任务集群
        // 核心 0 处理实时性最强的串口 IO
        xTaskCreatePinnedToCore(gps_rx_task,        "gps_rx",  4096, NULL, 10, NULL, 0);
        xTaskCreatePinnedToCore(usb_rx_task,        "usb_rx",  4096, NULL, 5,  NULL, 0);
    
        // 核心 1 处理数据分发和网络服务
        xTaskCreatePinnedToCore(tx_dispatcher_task, "tx_dis",  4096, NULL, 6,  NULL, 1);
        xTaskCreatePinnedToCore(tcp_server_task,    "tcp_srv", 4096, NULL, 4,  NULL, 1);
    
        ESP_LOGI(TAG, "Bridge System Started. Use TCP 192.168.4.1:8080 or USB JTAG Serial.");
    }

  • 【网站】Nginx[warn]错误: an upstream response is buffered to a temporary file xxx while reading upstream的解决方法

    如果采用的Nginx+Apache,请求会通过proxy到Apache。

    例如:

        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto https;
            proxy_redirect off;
        }

    这就存在一个问题,也是导致服务器几次宕机的问题,在遇到突发的大频率请求下,尤其是对一些大图、视频之类的体积较大的文件进行请求,Nginx会写入、读取大量的缓存,出现类似的错误,导致硬盘IO暴涨,进而让低配置服务器卡死。

    在Nginx的错误日志中就表现为大量的an upstream response is buffered to a temporary file XXX while reading upstream之类的日志,观察发现基本都是较大的视频、图片、JS文件等。

    解决方法:

    1、可以在Nginx.conf配置适当的缓存,启用proxy buffer(不写on似乎也是默认启用的),然后观察是否依旧存在类似的问题。

    2、如果有大文件例如视频之类静态文件,可以将其进行静态处理,直接由Nginx进行处理,避免频繁的IO读写。

    官方文档:https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers

    proxy_buffering on;
    proxy_buffer_size 16k;
    proxy_buffers 16 32k;
    proxy_busy_buffers_size 256k;
    proxy_temp_file_write_size 256k;

    在站点的Nginx配置中对静态文件进行单独处理,要指定网站的目录位置。

    参考WordPress相关设置:https://developer.wordpress.org/advanced-administration/server/web-server/nginx/

    root /var/html/wp; #定义网站根目录
        location ~ /\. {
        deny all;#拒绝访问隐藏文件
        }
        location ~* /(?:uploads|files)/.*\.php$ {
        deny all;#拒绝uploads、filrs文件夹的php执行
        }
        location = /favicon.ico {
        log_not_found off;#网站图标文件不记录日志
        access_log off;
        }
        location = /robots.txt {
        allow all;#允许robots文件访问
        log_not_found off;
        access_log off;
        }
        location ~* \.(?:jpg|jpeg|png|gif|webp|ico|css|js|woff2?|ttf|svg|eot|mp4|webm|mov|mp3)$ {
            access_log off;#对静态资源做nginx处理
            expires 30d;
            add_header Cache-Control "public";
            try_files $uri =404;
        }
       location / {#其他请求转发到apache
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
        }
  • 【网站】为SSL证书自动续期设定正确的Nginx配置

    使用certbot进行renew证书操作,基于Nginx+Apache。

    在webroot模式下的Nginx配置,需要对80端口和443端口进行配置,以保证验证程序可以读取/.well-known/acme-challenge/路径

    
    # 80端口
     # ACME challenge
        location ^~ /.well-known/acme-challenge/ {
            root  #webroot路径;
            allow all;
        }
       # 其他请求跳转 HTTPS
        location / {
            return 301 https://example.cn$request_uri;
        }
    
    
    # 443端口
    # ACME challenge
        location ^~ /.well-known/acme-challenge/ {
            root #webroot路径;
            allow all;
        }
    
  • 【网站】为WordPress配置正确的文件夹权限

    参考链接

    Hardening WordPress – Advanced Administration Handbook | Developer.WordPress.org

    # 切换到WP目录
    cd /var/www/wordpress
    
    # 将所有权设置为 www-data(用户和组)
    sudo chown -R www-data:www-data .
    
    # 所有目录设为 755
    sudo find . -type d -exec chmod 755 {} \;
    
    # 所有文件设为 644
    sudo find . -type f -exec chmod 644 {} \;
    
    # 强化 wp-config.php 权限
    sudo chmod 640 wp-config.php
    
  • 【网站】如何用Docker容器运行供应商的定制版WordPress

    一、一个烂摊子

    部门此前寻找网站供应商制作了产品网站,其交付网站架构较为老旧,网站基于WordPress 5.0版本构建,PHP版本要求为不高于7.3,MySQL版本5.7。

    就是这种要求在回迁公司服务器时也出现了各种问题。由于公司服务器环境过于杂乱,连mysql5.7的数据库都无法顺利导入。

    这里面也有供应商的一些问题,例如交付的WordPress配置文件与数据库表头不匹配,数据库时间配置错误等。

    在我的帮助下,数据库和配置文件都得以修改正常,网站终于部署上线,但是系统环境导致包含中文文件名的文件无法被解析,在调整Nginx配置无效后,IT让我修改中文名文件,我。。。

    二、服务器和Docker

    借由新ECS服务器和公司官网的搭建,考虑把产品网站也纳入部门服务器的管理。

    但供应商的环境版本老久,为了避免产品网站潜藏的漏洞影响整体服务器的稳定,我使用了Docker来为产品网站配置环境。

    中间也考虑使用LXD来实现,但是网络环境始终无法配置完成,因此不再浪费时间,使用Docker实现功能。

    三、Docker的问题

    最大的问题是镜像源,国内能搜索到的镜像源很多都停止了服务。因此需要在/etc/docker/daemon.json里配置可以访问的镜像源,这个不再赘述。

    四、部署

    将供应商的WP文件夹上传到服务器,可以在同位置配置Docker的yml文件。

    version: '3.7'
    
    services:
      wordpress:
        build: .
        container_name: wordpress-container
        ports:
          - "8000:80"  # WordPress 端口
        volumes:
          - /# 本地 WordPress 目录 wp:/var/www/html  # 本地 WordPress 目录
        networks:
          - wordpress_network
        depends_on:
          - mariadb  # 依赖 MariaDB 服务
        environment:
          WORDPRESS_DB_HOST: mariadb:3306
          WORDPRESS_DB_NAME: wordpress
          WORDPRESS_DB_USER: # 用户名
          WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}  # 从 .env 读取密码
        restart: always
    
      mariadb:
        image: mariadb:latest
        container_name: mariadb-container
        environment:
          MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}  # 从 .env 读取密码
          MYSQL_DATABASE: wordpress
          MYSQL_USER: # 用户名
          MYSQL_PASSWORD: ${MYSQL_PASSWORD}
        volumes:
          - mariadb_data:/var/lib/mysql  # 使用 Docker Volume 代替宿主机路径
          - # 初始数据库地址:/docker-entrypoint-initdb.d/#初始数据库.sql  # 初始 SQL 数据
        ports:
          - "3307:3306"
        networks:
          - wordpress_network
        restart: always
    
      phpmyadmin:
        image: phpmyadmin/phpmyadmin
        container_name: phpmyadmin-container
        environment:
          PMA_HOST: mariadb
          MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}  # 从 .env 读取密码
        ports:
          - "8081:80"
        networks:
          - wordpress_network
        restart: always
    
    networks:
      wordpress_network:
        driver: bridge
    
    volumes:
      mariadb_data:

    从.env文件读取密码:

    MYSQL_ROOT_PASSWORD=# 密码1
    MYSQL_PASSWORD=# 密码2

    如果是使用mariadb,则需要在WP的Config文件中修改配置:

    define('DB_HOST', 'mariadb');

    后端启动之后,我们可以通过IP+端口来进行服务的测试,如果没有问题,就可以通过主机的Nginx代理,来配置域名访问。

    server {
        listen 80;
        server_name #域名;
        return 301 https://$host$request_uri;
    }
    
    server {
        listen 443 ssl;
        server_name #域名;
    
        ssl_certificate /etc/letsencrypt/live/#域名/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/#域名/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
    
        location / {
            proxy_pass http://127.0.0.1:8000;#映射docker端口
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Host $server_name;
            proxy_set_header X-Forwarded-Proto https;
            proxy_redirect off;
        }
    }
    

    五、其他注意事项:

    1、如果WP的重写规则不可用,检查WP文件夹权限。

    2、如果配置HTTPS发现页面正常,但是管理界面出现问题,考虑是配置文件问题,参考官方说明配置

    define( 'FORCE_SSL_ADMIN', true );
    // in some setups HTTP_X_FORWARDED_PROTO might contain 
    // a comma-separated list e.g. http,https
    // so check for https existence
    if( strpos( $_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false )
        $_SERVER['HTTPS'] = 'on';
  • 【网站】如何把多站点的WordPress网站从Nginx+PHP-FPM切换到Apache

    最近手搓基于WP架构的企业官网,发现用Nginx+PHP-FPM时,在多站点的情况下可能会有一些地方不容易调试,所以尝试使用Nginx作为代理服务器,Apache作为后端,发现这对WordPress有很好的兼容性。

    那么此前的网站如果已经完成在Nginx+PHP-FPM的部署,如何切换到Nginx+Apache呢?

    这里省略Apache和其PHP模块的配置说明,需要重点修改的有以下几点:

    1、Apache的配置:

    Apache的端口设置,因为我们要使用Nginx转发HTTPS,因此设置Apache的监听端口为8080,避免和Nginx的端口冲突。

    <VirtualHost *:8080>
        DocumentRoot /var/www/html/wordpress
        ServerName yourdomain.com
        <Directory /var/www/html/wordpress>
            AllowOverride All
            Require all granted
        </Directory>
        
        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
    </VirtualHost>

    2、WP-Config的配置修改:

    如果网站使用了HTTPS,那么需要按照WordPress的官方设置来进行修改WP-Config:

    define( 'FORCE_SSL_ADMIN', true );
    // in some setups HTTP_X_FORWARDED_PROTO might contain 
    // a comma-separated list e.g. http,https
    // so check for https existence
    if( strpos( $_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false )
        $_SERVER['HTTPS'] = 'on';

    3、修改Nginx的站点配置文件:

    删除此前用于PHP-FPM的配置代码,把HTTPS转发到Apache处理。

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
    }

    4、修改.htaccess文件:

    如果使用子文件夹模式:

    # BEGIN WordPress Multisite
    # Using subfolder network type: https://wordpress.org/documentation/article/htaccess/#multisite
    
    RewriteEngine On
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
    RewriteBase /
    RewriteRule ^index\.php$ - [L]
    
    # add a trailing slash to /wp-admin
    RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]
    
    RewriteCond %{REQUEST_FILENAME} -f [OR]
    RewriteCond %{REQUEST_FILENAME} -d
    RewriteRule ^ - [L]
    RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
    RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
    RewriteRule . index.php [L]
    
    # END WordPress Multisite
    

    如果使用子域模式:

    # BEGIN WordPress Multisite
    # Using subdomain network type: https://wordpress.org/documentation/article/htaccess/#multisite
    
    RewriteEngine On
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
    RewriteBase /
    RewriteRule ^index\.php$ - [L]
    
    # add a trailing slash to /wp-admin
    RewriteRule ^wp-admin$ wp-admin/ [R=301,L]
    
    RewriteCond %{REQUEST_FILENAME} -f [OR]
    RewriteCond %{REQUEST_FILENAME} -d
    RewriteRule ^ - [L]
    RewriteRule ^(wp-(content|admin|includes).*) $1 [L]
    RewriteRule ^(.*\.php)$ $1 [L]
    RewriteRule . index.php [L]
    
    # END WordPress Multisite

    5、修改PHP配置文件:

    选择适合自己的媒体处理限制,因为此前处理媒体的PHP服务是PHP-FPM,切换到Apache后需要配置对应的PHP服务。

    6、检验Nginx配置文件,重启Nginx服务和Apache服务。

  • 【网站】在WP区块中添加CSS+JS动画

    类似如下效果:

    H armony

    O ptimize

    N ature

    E nergy

    S ustainability

    S olutions

    ALL In

    HONESS.

    可以直接在区块代码编辑器中进行添加。

    <p class="has-text-align-left has-text-color"
        style="color:#ffffff61;font-size:72px;font-style:normal;font-weight:700;letter-spacing:-2px;line-height:1;text-transform:uppercase">
        <em>H</em> <strong>armony</strong></p>

    例如以上是第一行动画文字的代码,因为在区块中我们给不同的文字设定了不同的标签,我们可以直接在代码中修改后面strong标签的类,改成如下:

    <p class="has-text-align-left has-text-color"
        style="color:#ffffff61;font-size:72px;font-style:normal;font-weight:700;letter-spacing:-2px;line-height:1;text-transform:uppercase">
        <em>H</em> <strong class="change-word">armony</strong></p>

    这样我们就可以利用CSS+JS动画实现该标签的动画效果。

    CSS:

    <style>
        .change-word {
            display: inline-block;
            transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
        }
        .hidden {
            opacity: 0;
            transform: translateY(-10px);
        }
    </style>

    JS:

    <script>
        document.addEventListener("DOMContentLoaded", function () {
            const words = [
                ["armony", "ope", "ealth", "appiness"], // H
                ["ptimize", "pportunity", "rganic", "vergreen"], // O
                ["ature", "urture", "eeds", "urturing"], // N
                ["nergy", "arth", "cology", "nvironment"], // E
                ["ustainability", "ynergy", "upport", "olutions"], // S
                ["olutions", "trategy", "uccess", "ymbiosis"] // S
            ];
    
            function changeWord(index, element, delay) {
                setTimeout(() => {
                    element.classList.add("hidden"); // 先隐藏
                    setTimeout(() => {
                        let nextWord = words[index].shift();
                        words[index].push(nextWord);
                        element.textContent = nextWord;
                        element.classList.remove("hidden"); // 显示
                    }, 400);
                }, delay);
            }
    
            function startAnimation() {
                let elements = document.querySelectorAll(".change-word");
                let delay = 100;
                elements.forEach((el, index) => {
                    setInterval(() => changeWord(index, el, index * delay), 2500);
                });
            }
    
            startAnimation();
        });
    </script>
  • 【网站】让WordPress封面区块的视频背景支持在微信环境下自动切换为照片

    移动版本的微信,在其内置的微信WEB环境中,对视频播放有很多限制,如果我们的封面区块想设置视频,就会出现无法加载播放的情况。

    为了解决这个问题,我去看了一下一些大厂的做法,当然大厂用的肯定不是WP,不过思路是可以借鉴的。

    华为的做法是用一个JS来判断是否是微信环境,如果是微信环境则切换到图片。OPPO的做法更加直接,他们直接用图片+动画的形式解决了视频这个问题,emm通用性确实刚刚的。

    那么我们在WP下,使用原版的封面区块是否可以进行类似的切换呢?也是可以的,原理是在区块代码中插入一个图片,利用一个简单的IF判断来实现微信环境下的自动切换。

    具体代码如下:

    <!-- wp:cover {"url":"//默认图片地址","id":588,"dimRatio":50,"overlayColor":"contrast","isUserOverlayColor":true,"backgroundType":"video","minHeight":840,"minHeightUnit":"px","isDark":false,"className":"main-container","layout":{"type":"constrained"}} -->
    <div class="wp-block-cover is-light main-container" style="min-height:840px">
        <span aria-hidden="true" class="wp-block-cover__background has-contrast-background-color has-background-dim"></span>
    
        <!-- 视频背景,添加 id="background-video" -->
        <video id="background-video" class="wp-block-cover__video-background intrinsic-ignore" autoplay muted loop playsinline 
            src="//视频地址" data-object-fit="cover">
        </video>
    
        <!-- 备用静态图片,默认隐藏 -->
        <img id="background-image" src="//图片地址" 
            alt="Background Image" style="display:none; width: 100%; height: 100%; object-fit: cover; position: absolute; top: 0; left: 0;">
    
        <div class="wp-block-cover__inner-container">
            <!-- 可添加文本或其他内容 -->
        </div>
    </div>
    <!-- /wp:cover -->
    
    <script>
    document.addEventListener("DOMContentLoaded", function () {
        function isWeChatBrowser() {
            return /MicroMessenger/i.test(navigator.userAgent);
        }
    
        if (isWeChatBrowser()) {
            let video = document.getElementById("background-video");
            let image = document.getElementById("background-image");
    
            if (video && image) {
                video.style.display = "none"; // 隐藏视频
                image.style.display = "block"; // 显示备用图片
            }
        }
    });
    </script>
    
  • 【网站】WordPress中如何获得无间隔的区块

        "spacing": {
            "blockGap": "1.2rem",
            "padding": {
                "left": "var:preset|spacing|50",
                "right": "var:preset|spacing|50"
            }
        },

    6.X版本的Wordpress中,区块之间会有一个默认1.2rem的间隔,这个间隔在页眉页脚设置中会与其他不同色彩的区块产生非常显眼的割裂,或者同一种非白色区块中间产生间隔缝隙。

    如何修改这个问题?很简单,我们可以在主题样式和区块设置 (theme.json)中进行更改,去掉这个间隔,让我们的区块背景更加融合或者分离的更明显,具体如上代码,找到 “spacing” 中的 “blockGap” 部分,将预设的 “1.2rem” 改为null即可(不用带引号),修改完毕的代码如下所示↓

    修改完毕后最好检查一下页面是否有其他区块被影响,目前观察容易受到影响的有导航栏,可以在其单独的样式中针对性调整区块间隔。

        "spacing": {
            "blockGap": null,
            "padding": {
                "left": "var:preset|spacing|50",
                "right": "var:preset|spacing|50"
            }
        },
  • 【Python】提取视频画面并生成PPT

    比较笨的方法,用来提取PPT课程视频画面,并生成对应的PPT,代码检测黑屏但没有检测白屏,没有检测重复画面(因为有些人讲课会来回翻PPT),因此还有优化空间。内存占用会逐渐增多,不过测试没有出现崩溃的情况。

    PS:做完发现可以直接问讲课人要PPT原件,我,,,

    import cv2
    import os
    import numpy as np
    from pptx import Presentation
    from pptx.util import Inches
    from skimage.metrics import structural_similarity as ssim
    import tkinter as tk
    from tkinter import filedialog, messagebox
    
    # 选择视频和输出目录
    def select_video_and_output():
        video_path = filedialog.askopenfilename(title="选择视频文件", filetypes=[("MP4 files", "*.mp4")])
        if not video_path:
            messagebox.showwarning("选择视频", "未选择视频文件")
            return None, None
        
        output_dir = filedialog.askdirectory(title="选择输出目录")
        if not output_dir:
            messagebox.showwarning("选择输出目录", "未选择输出目录")
            return None, None
    
        pptx_path = os.path.join(output_dir, "output_presentation.pptx")
        return video_path, pptx_path
    
    # 处理视频并生成 PPT
    def process_video_to_ppt(video_path, pptx_path):
        os.makedirs("ppt_images", exist_ok=True)
        
        cap = cv2.VideoCapture(video_path)
        _, prev_frame = cap.read()
        prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
    
        frame_count = 0
        slide_count = 0
        images = []
        similarity_threshold = 0.95  # 提高 SSIM 阈值,减少相似图片
        brightness_threshold = 10  # 黑屏检测(平均亮度 < 10 认为是黑屏)
    
        def process_frame(frame):
            """ 计算 SSIM 相似度,判断是否保存该帧 """
            nonlocal prev_gray, slide_count
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            score = ssim(prev_gray, gray)
    
            # 计算平均亮度,过滤黑屏
            avg_brightness = np.mean(gray)
            if avg_brightness < brightness_threshold:
                return  # 跳过黑屏帧
    
            if score < similarity_threshold:  
                img_path = os.path.join("ppt_images", f"slide_{slide_count}.jpg")
    
                # 确保不同的幻灯片才保存
                if len(images) == 0 or images[-1] != img_path:  
                    cv2.imwrite(img_path, frame)
                    images.append(img_path)
                    slide_count += 1
                    prev_gray = gray  # 只在确认变化时更新参考帧
    
        while cap.isOpened():
            ret, frame = cap.read()
            if not ret:
                break
    
            # 仅每隔 15 帧处理一次
            if frame_count % 15 == 0:
                process_frame(frame)
    
            frame_count += 1
    
        cap.release()
        # cv2.destroyAllWindows()
    
        # 创建 PPT
        prs = Presentation()
        for img in images:
            slide = prs.slides.add_slide(prs.slide_layouts[5])  # 空白幻灯片
            left, top, width, height = Inches(0), Inches(0), Inches(10), Inches(7.5)
            slide.shapes.add_picture(img, left, top, width, height)
    
        prs.save(pptx_path)
        messagebox.showinfo("完成", f"PPTX 生成完成: {pptx_path}")
    
    # 主函数
    def main():
        root = tk.Tk()
        root.withdraw()  # 隐藏主窗口
        video_path, pptx_path = select_video_and_output()
        if video_path and pptx_path:
            process_video_to_ppt(video_path, pptx_path)
    
    if __name__ == "__main__":
        main()