标签: code

  • 【摄影】Nikon F90X/N90s F90/N90系列胶片相机串口通信协议逆向项目

    基于尼康官方软件AC-PW,提取其核心 Pcf9032.dll逆向
    由 DeepSeek 4.0 Pro 辅助完成。


    第一章 概述

    1.1 适用范围

    本报告完整描述 F90X/N90s F90/N90 胶片单反相机通过串行接口与计算机通信的底层协议、命令集、寄存器映射、功能编号及所有操作序列。所有内容均来自对官方控制库 Pcf9032.dll 及配套软件 PhotoSecretary的逆向分析,并通过资源提取进行验证。

    1.2 通信架构

    • 物理层:RS-232C 串行通信,5V 电平
    • 波特率:初始 1200 bps,握手后切换至 9600 bps
    • 数据格式:8 数据位,无校验,1 停止位 (8N1)
    • 端口选择:COM1~COM4,由功能 ID 0 设定
    • DLL 文件:Pcf9032.dll(Nikon 相机通信库),由主程序动态加载

    1.3 相机型号识别

    型号标识相机返回字符串地址字节对应型号
    1F90N900x10F90系列/N90(北美发售型号)系列
    2F90xN90s0x20F90X系列/N90S(北美发售型号)系列

    第二章 通信初始化

    2.1 唤醒与型号识别

    1. 打开串口 COM%d,配置为 1200,8N1。
    2. 发送唤醒字节:00(1 字节),等待 200ms,清空接收缓冲区。
    3. 发送型号查询:53 31 30 30 30 05S1000 + ENQ,共 6 字节)。
    4. 接收相机返回的标识字符串(例如 31 30 32 30 46 39 30 58 2F 4E 39 30 53 00 03 06 表示 1020F90X/N90S)。
    5. 根据字符串确定型号:
      • 若包含 F90N90 → 型号 1,地址字节 0x10。
      • 若包含 F90xN90s → 型号 2,地址字节 0x20。

    2.2 波特率升级至 9600

    1. 发送 9 字节命令(型号 1 用 0x10,型号 2 用 0x20):
      01 10 87 05 00 00 00 00 03 或 01 20 87 05 00 00 00 00 03
    2. 等待相机应答 06 00(ACK)。
    3. 将本地串口切换为 9600 bps,等待 200ms 稳定。

    2.3 结束 9600 会话(返回 1200)

    • 发送 04 04(EOT 两次),相机返回 04 04 确认,然后串口恢复到 1200。
    • 若相机自动休眠,波特率也会自动降回 1200。

    第三章 通信帧格式

    校验和仅计算纯数据部分,即从 STX (0x02) 之后的第一个字节开始,到 ETX (0x03) 之前的所有字节,不包括帧头(SOH)、地址、命令码、长度字段等任何头部字节。

    波特率切换等特殊握手指令会使用 0x87 作为操作码,此类特殊指令属于硬编码控制帧,不完全遵循常规寄存器读写帧的载荷结构。

    3.1 标准命令帧(命令码 ≤ 100)

    读请求(无数据)

    偏移 大小 值       说明
    0 1 0x01 帧头 (SOH)
    1 1 ADDR 设备地址 (0x10 或 0x20)
    2 1 0x80 读操作
    3 1 0x00 保留
    4 1 v6 寄存器地址高字节
    5 1 v7 寄存器地址低字节
    6 1 0x00 保留
    7 1 len 期望接收的数据长度
    8 1 0x03 帧尾 (ETX)

    总长 9 字节,无校验。

    写请求(有数据)

    偏移 大小 值       说明
    0    1    0x01     帧头
    1    1    ADDR     设备地址
    2    1    0x81     写操作
    3    1    0x00     保留
    4    1    v6       寄存器地址高字节
    5    1    v7       寄存器地址低字节
    6    1    0x00     保留
    7    1    len      数据长度(实际发送字节数)
    8    1    0x02     数据起始标识 (STX)
    9~   len   DATA    数据体
    9+len  1   CHK     校验和(所有 DATA 字节累加和的低 8 位)
    10+len 1   0x03     帧尾 (ETX)
    11+len 1   0x00     填充

    总长 12 + len 字节。

    3.2 扩展命令帧(命令码 > 100)

    读请求

    偏移 大小 值       说明
    0    1    0x01     帧头
    1    1    ADDR     设备地址
    2    1    0x1B     命令组标识
    3    1    0x90     命令组标识
    4    1    0x82     读操作
    5    1    v6       参数(由命令码决定)
    6    1    len      期望接收长度
    7    1    0x00     保留
    8    1    0x03     帧尾

    总长 9 字节。

    写请求

    偏移 大小 值       说明
    0    1    0x01     帧头
    1    1    ADDR     设备地址
    2    1    0x1B     命令组标识
    3    1    0x90     命令组标识
    4    1    0x81     写操作
    5    1    v6       参数
    6    1    len      数据长度
    7~   len   DATA    数据体
    7+len 1    CHK     校验和
    8+len 1    0x03     帧尾
    9+len 1    0x00     填充

    总长 10 + len 字节。

    3.3 应答帧

    • 短应答(ACK)06 00 表示成功。
    • 特殊同步应答04 04 用于同步。
    • 长应答(数据包)
      • 字节 0:状态码(通常忽略,或为 0x01)
      • 字节 1~(n-2):数据
      • 字节 (n-1):校验和(字节 1 到字节 n-2 的累加和低 8 位)
      • 字节 n:0x03 (ETX)

    3.4 重试机制

    发送命令后,最多重试 4 次(外层循环)× 4 次(内层发送尝试)。每次发送失败等待 200ms,若连续 4 次内层失败则退出内层;内层结束后等待 150ms 再进入下一次外循环。


    第四章 命令码与寄存器地址映射表

    4.1 标准命令码表(≤100)

    从 DLL 函数 sub_1000DB73 的 switch 分支提取。v6v7 以十六进制补码表示(负值转换为无符号字节,例 -3 = 0xFD)。

    命令码v6 (hex)v7 (hex)寄存器地址常用数据长度方向关联功能ID
    0FD19FD191读/写
    1FD17FD172(读)/6(写)读/写
    2FD1AFD1A1读/写
    3FD1CFD1C1读/写
    4FD1BFD1B1读/写
    5FD16FD161读/写
    6FD28FD28113
    7FD26FD26114,15,16
    8FD27FD27114,15,16
    9FD29FD29118
    10FD2BFD2B120
    11FD2AFD2A119
    12FD2DFD2D121
    13FD2EFD2E126
    14FD21FD211
    15FD2CFD2C122,23
    16FE44FE442读/写
    17FEC9/C1FE?1型号相关
    18FD20FD201读/写0x64,0x66
    19FD39FD391读/写
    20FD3AFD3A1读/写0x64,0x66
    21FD3BFD3B1读/写0x64
    22FD3CFD3C2读/写0x64
    23FED0FED01用户设置
    24FDF2FDF22用户设置
    25FDF3FDF31读/写
    26FE94FE942用户设置
    27FE95FE951读/写
    28FE9EFE9E1用户设置
    29FD25FD25117
    30FD36FD363读/写90-95
    31FEC8FEC81用户设置
    32FD33FD333(型号1)读/写0x46-0x52
    33FD34FD341状态检测
    34FD40FD401读/写131
    35FD30FD306(型号2)读/写0x46-0x52
    36FD41FD413读/写
    37FD24FD241状态查询
    38FE50FE501状态检测
    39FE20FE201读/写
    40FEDB/D4FE?1型号相关
    41FD93/8EFD?1型号相关
    42FD9D/90FD?1型号相关
    43FE06FE061
    44FD1FFD1F10x64

    4.2 扩展命令码表(>100)

    从 sub_1000DED3 的 switch 提取,命令码对应 ASCII 字符,v6 为帧内参数。

    命令码ASCIIv6 (hex)常用数据长度方向
    101‘e’003
    102‘f’065
    103‘g’101
    118‘v’701读/写
    119‘w’722读/写
    122‘z’801

    第五章 功能 ID 完整映射

    5.1 设定/读取函数导出索引

    导出序号函数名用途
    1FeatureExists检查功能ID是否有效
    2GetIndexString获取功能选项文本
    3GetMaxIndex获取选项最大索引
    8GetValueBool读取布尔型参数
    9GetValueFloat未实现
    10GetValueIndex读取索引型参数
    11GetValueInteger读取整型参数
    12GetValueString读取字符串参数
    13GetValueStruct读取结构体参数
    14InvalidateReads清除读取缓存
    15SetValueBool设置布尔型参数
    16SetValueFloat未实现
    17SetValueIndex设置索引型参数
    18SetValueInteger设置整型参数
    19SetValueString设置字符串参数
    20SetValueStruct设置结构体参数
    21StartProcess执行动作(拍摄、对焦、删除等)
    23WriteDataToCamera将修改的参数写入相机

    5.2 功能 ID → 命令/寄存器映射

    以下列出全部功能 ID(0~147)的精确技术映射,包括类型、设置/读取函数、所属 WriteDataToCamera 的 case、具体的命令码、寄存器地址(v6, v7)或扩展命令标识,以及在配置块中的位操作说明。

    功能 ID类型设置函数读取函数Case命令码 / 寄存器操作描述
    0IntegerSetValueInteger直接设置全局变量 word_10015218通信端口号 (1~4)
    1未使用
    2StringGetValueString直接返回相机型号字符串指针相机型号字符串 (“F90N90” 或 “F90xN90s”)
    3BoolSetValueBool直接设置 word_100122D4相机型号选择 (0=型号1, 1=型号2)
    4IndexGetValueIndex特殊:读取 word_10012170 数组电子手帐数据读取状态/进度
    5IntegerGetValueInteger直接读取 dword_10012000内部状态值(只读)
    6BoolGetValueBool5通过 sub_10008905 读取 word_100122BC未知布尔状态(只读)
    7~12未使用
    13IndexSetValueIndexGetValueIndex1命令 6 → FD28测光方式 (0=矩阵, 1=中央重点, 2=点)
    14IndexSetValueIndexGetValueIndex1命令 7/8 → FD26, FD27;位操作曝光模式 (0=P,1=S,2=A,3=M,4=CP,5-11=变程序)
    15IndexSetValueIndexGetValueIndex1命令 7/8 → FD26, FD27;位操作曝光模式(与14共用寄存器,不同位)
    16BoolSetValueBoolGetValueBool1命令 7/8 → FD26, FD27;位操作曝光模式扩展布尔
    17IndexSetValueIndexGetValueIndex1命令 29 → FD25快门速度 (B门/定时相关)
    18IndexSetValueIndexGetValueIndex1命令 9 → FD29卷片模式 (0=单张, 1=低速连拍, 2=高速连拍)
    19IndexSetValueIndexGetValueIndex1命令 11 → FD2A闪光同步模式 (0=前帘,1=慢速,2=后帘,3=防红眼)
    20IndexSetValueIndexGetValueIndex1命令 10 → FD2B对焦区域 (0=宽区, 1=点)
    21IndexSetValueIndexGetValueIndex1命令 12 → FD2D曝光补偿范围 (-5.0~+5.0 EV)
    22BoolSetValueBoolGetValueBool1命令 15 → FD2C;位操作DX/手动 ISO 选择
    23IndexSetValueIndexGetValueIndex1命令 15 → FD2CISO 感光度 (0=ISO 6, 1=ISO 8 … 30=ISO 6400)
    24IndexGetValueIndex1命令 15 读回 FD2CISO 扩展读取
    26IndexSetValueIndexGetValueIndex1命令 13 → FD2E闪光补偿/特殊设定
    27~28未使用
    29BoolGetValueBool3word_100123F2(通过 sub_100058C7 读取)MF-26 状态(只读)
    30BoolGetValueBool3word_100123F4(通过 sub_100058C7 读取)MF-26 状态(只读)
    31IndexSetValueIndexGetValueIndex3命令 18,20 等,位操作 FD20, FD3A 等MF-26 曝光模式
    32IndexSetValueIndexGetValueIndex3同上MF-26 曝光模式
    33IndexSetValueIndexGetValueIndex3命令 6 → FD28测光方式 (MF-26)
    34IndexSetValueIndexGetValueIndex3命令 9 → FD29卷片模式 (MF-26)
    35IndexSetValueIndexGetValueIndex3命令 11 → FD2A闪光同步模式 (MF-26)
    36IndexSetValueIndexGetValueIndex3命令 10 → FD2B对焦区域 (MF-26)
    37~39未使用
    40IndexSetValueIndexGetValueIndex1/8命令 1 → FD17用户设置库名称
    41IndexGetValueIndex1/8命令 1/7 读取用户设置库当前选择
    42IndexGetValueIndex8命令 43 → FE06镜头光圈值 (F1~F90)
    43IndexSetValueIndexGetValueIndex8命令 43 → FE06 等镜头光圈值(设定)
    44IndexSetValueIndexGetValueIndex1命令 9 → FD29卷片模式(另一入口)
    45IndexSetValueIndexGetValueIndex8命令 45闪光同步(另一设定)
    46BoolSetValueBoolGetValueBool8命令 15 → FD2C位操作对焦模式 (0=手动, 1=AF 等)
    47IndexGetValueIndex8命令 47ISO 显示(备用)
    48IndexSetValueIndexGetValueIndex8命令 10 → FD2B对焦区域(另一入口)
    49IndexSetValueIndexGetValueIndex8命令 49AF 方式 (0=释放优先, 1=对焦优先, 2=手动)
    50IndexGetValueIndex8命令 50对焦状态选项
    51IndexSetValueIndexGetValueIndex8命令 51对焦状态 (0=前焦,1=后焦,2=合焦,3=无法合焦)
    52IndexSetValueIndexGetValueIndex8命令 52ON/OFF 开关
    53IndexSetValueIndexGetValueIndex8命令 53曝光补偿值 (-20.0~+20.0)
    54IndexSetValueIndexGetValueIndex8命令 54曝光补偿值(另一组)
    55IndexSetValueIndexGetValueIndex8命令 55胶片计数器 (E, 0~99)
    56IndexSetValueIndexGetValueIndex8命令 56电池状态 (0=满电, 1=低电量)
    57IndexGetValueIndex8命令 57特定索引
    58IndexGetValueIndex8命令 58曝光补偿范围 (-5.0~+5.0 EV)
    59IndexSetValueIndexGetValueIndex8命令 59镜头焦距 (5mm~7600mm)
    60IndexSetValueIndexGetValueIndex8命令 60镜头焦距(另一组)
    61IndexSetValueIndexGetValueIndex8命令 61镜头焦距(另一组)
    62IndexGetValueIndex8命令 62光圈值(另一组)
    63IndexGetValueIndex8命令 63特定索引
    64IndexGetValueIndex8命令 64光圈值(另一组)
    65IndexGetValueIndex1/8命令 1 读取用户库读取
    66IndexSetValueIndexGetValueIndex1/8命令 6 → FD28测光方式(另一入口)
    67IndexSetValueIndexGetValueIndex8命令 67闪光同步(另一组)
    68IndexSetValueIndexGetValueIndex8命令 68镜头焦距(另一组)
    69BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)蜂鸣音开关(恢复默认用)
    70 (0x46)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)合焦音开关
    71 (0x47)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)DX 优先设定
    72 (0x48)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)AE/AF 同时锁定
    73 (0x49)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)AF-C 对焦优先
    74 (0x4A)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)AF-S 释放优先
    75 (0x4B)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)AF-S 帧间 AF 驱动
    76 (0x4C)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)0 帧数据印记
    77 (0x4D)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)测光偏差显示
    78 (0x4E)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)A 模式简易曝光补偿
    79 (0x4F)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)(保留)
    80 (0x50)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)闪光灯相关设定
    81 (0x51)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)闪光灯相关设定
    82 (0x52)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)闪光灯相关设定
    83IndexSetValueIndexGetValueIndex4配置块 (命令32/35)闪光灯模式
    84IndexSetValueIndexGetValueIndex4配置块 (命令32/35)长时间曝光时间 (4秒~60秒/B门)
    85IndexSetValueIndexGetValueIndex4配置块 (命令32/35)同步释放模式
    86IndexSetValueIndexGetValueIndex4配置块 (命令32/35)(预留)
    87IndexSetValueIndexGetValueIndex4配置块 (命令32/35)自拍/间隔时间
    88StartProcess(0x58) 调用恢复默认设置(操作码)
    89StartProcess(0x89) 调用触发拍摄(操作码)
    90IndexSetValueIndexGetValueIndex7命令 30 → FD36闪光灯/自定义程序点 (快门)
    91IndexSetValueIndexGetValueIndex7命令 30 → FD36闪光灯/自定义程序点 (光圈)
    92IndexSetValueIndexGetValueIndex7命令 30 → FD36同上
    93IndexSetValueIndexGetValueIndex7命令 30 → FD36同上
    94IndexSetValueIndexGetValueIndex7命令 30 → FD36同上
    95IndexSetValueIndexGetValueIndex7命令 30 → FD36同上
    96StartProcess(0x60) 调用闪光灯操作(操作码)
    97IndexGetValueIndex8命令 97微调值
    98IndexSetValueIndexGetValueIndex8/11命令 98微调值 (0.3~2.0)
    99IndexSetValueIndexGetValueIndex8/11命令 99曝光补偿步长 (-3.0~+1.0)
    100Bool8(保留)
    101IntegerGetValueInteger8读取多个配置寄存器配置块值(只读)
    102~105未使用
    106IndexSetValueIndexGetValueIndex9扩展命令 103, 122 等时区城市 (1~24)
    107未使用
    108Index9扩展命令时区城市(备用)
    109IndexSetValueIndexGetValueIndex10扩展命令时差数值 (-10~+12, -11)
    110IndexSetValueIndexGetValueIndex10扩展命令时差数值 (-11)
    111~114未使用
    115IndexGetValueIndex11扩展命令模式选择
    116IndexSetValueIndexGetValueIndex11扩展命令微调值 (另一入口)
    117未使用
    118IndexSetValueIndexGetValueIndex11扩展命令曝光补偿步长 (同 99)
    119IndexSetValueIndexGetValueIndex11扩展命令特殊设定(自拍/闪光相关)
    120~123未使用
    124StructGetValueStruct11扩展命令读取结构体 (2 字段)
    125StructGetValueStruct11扩展命令读取结构体 (3 字段)
    126未使用
    127~12912扩展命令保留
    130StructGetValueStruct11扩展命令读取结构体 (3 字段)
    131IndexSetValueIndexGetValueIndex5命令 34 → FD40数据存储模式 (0/69/78/95)
    132IndexSetValueIndexGetValueIndex5命令 5 → FD34 bit7数据印记 / 特殊设定位
    133命令 133(内部全消去)内部使用
    134IntegerGetMinMaxInteger只读,范围 0-9999胶片编号
    135未使用
    136StartProcess(0x88) 调用指针复位/状态初始化
    137StartProcess(0x89) 调用触发拍摄(同 89)
    138未使用
    139IndexGetIndexString6字符串资源自定义功能名称/选项
    140~145未使用
    146StructGetValueStruct6扩展命令相机信息(型号、序列号等)
    147StructGetValueStruct6/5扩展命令对焦数据读取

    注释

    • 配置块位操作:功能 ID 69~82 的具体位定义已在第七章详细列出。每个布尔值对应配置块(命令32/35)的某一位,通过读-修改-写流程改变。
    • 扩展命令:功能 ID 106~130 使用命令码 >100 的扩展命令(格式 1B 90 cmd v6 len),具体 v6 和 len 见第四章扩展命令表。
    • 操作码:88, 89, 96, 136, 137 等不直接对应设置/读取函数,而是由 StartProcess 分发执行,内部命令序列已在前文各章详述。
    • 表中粗体为软件中直接面向用户的主要功能。
    • 未使用的功能 ID 在 DLL 中返回错误 2(不支持)。
    • 对于131:
    • 这些值直接写入 FD40 寄存器,对应相机的“数据存储模式”0 (0x00) → 不存储数据(Disabled)
    • 69 (0x45) → 存储最小数据量(Minimum)
    • 78 (0x4E) → 存储中间数据量(Intermediate)
    • 95 (0x5F) → 存储最大数据量(Maximum)
      该映射与我们之前整理的 FD40 寄存器功能一致,且与 DLL 资源中对应的字符串(“記憶しない”、“最小情報量記憶”等)吻合。

    第六章 特殊操作流程

    6.1 拍摄(快门释放)

    • 操作码:StartProcess(0x89)
    • DLL 函数:sub_10007BB0 → 型号1调用 sub_10007C4B,型号2调用 sub_10007CE0
    • 命令序列(以型号1为例):
      1. 读 FD20FD24FD40 获取状态。
      2. 构造写命令 01 10 81 00 FD 41 00 01 02 <data> <cs> 03 00,数据为 0x42(’B’)及校验。
      3. 等待 ACK,再读 FD20 和 FD24 完成清理。

    6.2 自动对焦

    • DLL 中未作为独立导出命令,但 EXE 的“AFの実行”菜单调用 StartProcess,其内部可触发对焦操作,对应的相机命令为 01 20 86 00 00 00 00 00 03(也可被直接发送)。

    6.3 用户设置库(保存/调用)

    • 保存到库:StartProcess(0x25) → sub_10005B79
    • 从库读取:StartProcess(0x26) → sub_10005A2EStartProcess(0x27) → sub_10005D47
    • 这些函数内部发送命令 18、20、31、119 以及 sub_1000E07D/sub_1000E183 等位操作,并读写 FD17、FD26、FD27 等寄存器。

    6.4 恢复默认设置

    • StartProcess(0x58)
    • 先将内部变量设为一组出厂默认值(详见代码中的连续赋值),然后调用 sub_100065A7,发送命令 119 或 18,最后写入配置块(命令 32 或 35)和公共配置(命令 20)。

    6.5 胶卷数据读取

    • 入口:sub_10006F09 → 型号1调用 sub_10007042,型号2调用 sub_1000749C
    • 步骤:
      1. 读 FD20FD24FD40 验证状态和存储模式。
      2. 读 FD41 获取帧数 v19 和相关信息。
      3. 根据 v19 计算动态地址:addr = 2 * v19(可能进行回绕修正)。
      4. 调用 sub_10007A14 发送动态读命令,接收数据包。
      5. 数据以 0xFF 作为卷结束标记,ISO 索引在最后。
      6. 解析帧数据(每帧 3/4/6 字节,取决于存储模式),字段包括快门速度、光圈、曝光模式、测光方式、焦距、曝光补偿等。
      7. 发送确认命令 0x42(’B’)完成本次读取。

    6.6 删除胶卷数据

    • 全消去StartProcess(0x1C) → sub_1000A9CD
      • 检查相机内是否有数据,若无则发送命令 133,相机清空整个环形缓冲区。
    • 部分删除(移动读取指针)StartProcess(0x1B) → sub_1000A7E1
      • 发送命令 134,并操作命令 19(FD39)的位 3,使相机内部的待读指针前移,等效于删除已读取的卷。
    • 指针/状态复位StartProcess(0x88) → sub_10008403
      • 型号1:写 FD41 特定值,清除 FED6 的 bit0,清零 FD40。
      • 型号2:读 FD00,写命令 ‘B’,清除 FECE 的 bit0,清零 FD40。

    第七章 配置块位定义

    从 sub_1000694E 提取(以型号1配置块命令32为例):

    • v34 低字节 = a3[10]
    • v34 高字节:
      • bit0: a3[13] 控制
      • bit1: a3[2] 控制
      • bit2: a3[11] 控制
      • bit3: a3[3] 控制
      • bit5: a3[9] 控制
      • bit6: a3[8] 控制
    • v35 低字节:
      • bit2: a3[12] (1或3置1)
      • bit3: a3[12] (2或3置1)
      • bit4: a3[5] 控制
      • bit5: a3[7] 控制
      • bit7: a3[6] 控制

    型号2配置块(命令35)有类似的位定义,详见原始反编译代码。


    第八章 错误码表

    错误码 (hex)含义
    0x19 (25)参数无效
    0x1D (29)数据块读取校验失败
    0x25 (37)对焦扫描初始化失败
    0x28 (40)功能不可用/状态错误
    0x29 (41)功能不支持
    0x32 (50)通用“不支持此操作”
    0x33 (51)参数冲突
    0x34 (52)参数范围错误
    0x35 (53)参数范围错误
    0x37 (55)模式不匹配
    0x45 (69)初始化失败
    -18校验和不匹配
    -27未收到 ACK (0x06)
    -30接收超时
    -35相机状态错误

    所有错误均通过全局变量 dword_10015220 返回。


    第九章 数值编码与资源字符串

    相机参数值到显示文本的映射完全来自 DLL/EXE 资源,无需第三方数据。例如:

    • 快门速度:EXE 资源 17408 开始 "30"",连续列出 Bulb、30″、25″、… 1/8000。
    • 光圈:资源 17664 起 F1、F1.1、… F90。
    • ISO:资源 19968 起 ISO 6、ISO 8、… ISO 6400。
    • 曝光补偿:资源 19712 起 0.0、-0.2、… +20.0。
    • 测光方式:资源 22190 起 “多分区测光”、”中央重点测光”、”点测光”。
    • 曝光模式:资源 17920 起 “P:多程序自动”、”S:快门优先” 等。

    所有字符串均已在 EXE 和 DLL 的资源提取中完整列出,并翻译为中文。


    第十章 与第三方文档的主要差异说明(主要指f90x-serial-documentation/f90x-serial-documentation.md at trunk · antarktikali/f90x-serial-documentation · GitHubgIcon source 项目等)

    • 地址字节:第三方固定使用 0x20,我们根据 DLL 逻辑动态使用 0x10 或 0x20
    • 扩展命令格式:第三方可能省略 0x90,我们严格采用 DLL 的 1B 90 cmd 格式。
    • 环形缓冲区指针:第三方直接操作 FD44/FD46,DLL 通过动态地址计算和 FED6/FECE 等寄存器间接实现,功能等效。
    • 部分只读状态寄存器(如 FE20、FE22)在 DLL 中未出现立即数,但其信息已通过功能 ID 提供,不影响使用。
    • 所有命令码和寄存器映射完全基于 DLL 的 sub_1000DB73sub_1000DED3,比第三方仅列举的片段更为完整和准确。

    附录 A 数据块格式(胶卷记录)

    卷头:2 字节 0x5A58(“ZX”)
         2 字节 元数据
         2 字节 0x0000
         2 字节 卷号(BCD)
    帧数据:N × (3/4/6 字节) 取决于存储模式
         1 字节 0xFF(结束标记)
         1 字节 ISO 索引

    每帧数据字段(6 字节最大模式):快门速度、光圈、曝光模式、测光模式、闪光同步、焦距、曝光补偿、闪光补偿。具体编码与命令寄存器返回的原始值一致,可参照资源字符串表解析。

    补充:

    • 在 sub_10007042 中,读取胶卷数据时使用的动态地址 v28 = 2 * v19,并且有如下回绕处理:
    v15 = 总字节数 + v28;
    if ( v15 >= 0x200u )
        v15 -= 444;   // 回绕修正,444 = 0x1BC
    • 这说明环形缓冲区的大小为 512 (0x200) 字节,当读取指针超过 0x1FF 时,会回绕到 0x200 - 444 = 0x44(实际基址由相机固定)。DLL 内部自动处理了回绕,开发者只需按此逻辑实现即可。
  • 【摄影】穷鬼如何进入毒德大学?

    Leica FOTOS,徕卡公司推出的一款徕卡相机管理软件,其内置了多款LUT供高贵的SL3及Q3使用,让我等穷鬼十分眼馋。那么穷鬼如何迈入毒德大学呢?

    方法一:HALD LUT,受限于HALD 色块图的分辨率,提取的LUT总让我等穷鬼觉得不够德味。

    方法二:反编译Leica FOTOS,我们将会得到纯正的徕卡LUT,但是万恶的资本主义徕卡,在两年前就发现了这个BUG,为了阻止穷鬼不购买SL3就获得富哥们LUT,将软件中的LUT加密,文件也变为了.CUBE.enc后缀。

    但是穷鬼们没有徕卡,有电脑哇↓

    1、分析加密结构
    将 Leica FOTOS 的 APK 解包,寻找资源文件夹,assets\looks\cube中可看到一批 .CUBE.enc 文件。直接查看其编码,开头 Base64 字符串 +] 分隔符 +密文,反编译寻找lut相关的类,基本确认采用AES加密,开头字符串为初始化向量。

    2、寻找密钥
    不管徕卡的程序员是不是外包的,让人遗憾的是他们都没有把Key明文存储。穷鬼们还是要继续努力,进一步发现Key通过JNI 放在底层 libnative-lib.so 动态库中,并通过一个名为 NativeKeyProvider 的类进行调用。

    3、拦截getkey
    安卓人一般用安卓机,碰巧安卓机还是ROOT的,因此穷鬼们可以使用 Frida 对程序进行动态 Hook,直接拦截 getKey() 方法。
    成功 Hook 后,程序会返回实际使用的密钥:
    bGVp■■■■■■■■■■■■■HRfa2V5
    Base64 解码后内容为:
    leica_camera_encrypt_key(好直白)

    4、解密 LUT 文件
    拿到 Key + IV 后,可使用 Python 的 cryptography 库,实现 AES-GCM 解密即可,将 .CUBE.enc 批量还原为标准 .cube 文件。

    最终,我们迈进了毒德大学。

    就是你这个徕卡LUT怎么还是个17x17x17的,嗯,感觉又不够德味了。

  • 【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>