using System; using System.Collections.Generic; namespace AutoFocusTool.Serial { /// /// 下位机串口协议帧构造 + 校验。完全依据原工程 Commander.cs 实测确证: /// 帧: [0]帧头0x5E [1]命令码 [2]序号0x00 [3]整帧长 [4..n-2]辅助码+参数 [n-1]累加和校验 /// 校验 = 前 n-1 字节逐字节相加(byte溢出截断)放末字节。无独立帧尾。 /// 电机绝对运动脉冲值: 32位int, 大端写入帧(高字节在前)。 /// public static class Protocol { public const byte ST = 0x5E; // 帧头 public const byte CMD_MOTOR = 0x05; // 电机控制 public const byte CMD_IO = 0x09; // 设IO(含LED) public const byte CMD_READ_MOTOR = 0x18; // 读电机位置 public const byte CMD_SHAKE = 0x01; // 握手 public const byte CMD_READ_EEPROM = 0x11; // 读EEPROM(回复10字节) /// 累加和校验:前 n-1 字节求和写入末字节 public static byte[] WithChecksum(byte[] frame) { byte sum = 0; for (int i = 0; i < frame.Length - 1; i++) sum += frame[i]; frame[frame.Length - 1] = sum; return frame; } /// 校验回复帧(末字节==前面累加和) public static bool CheckChecksum(byte[] frame) { if (frame == null || frame.Length < 2) return false; byte sum = 0; for (int i = 0; i < frame.Length - 1; i++) sum += frame[i]; return frame[frame.Length - 1] == sum; } /// /// 根据命令码返回下位机回复的固定字节数(依据 Commander.CustomProtocolLength)。 /// 接收端按这个长度收帧,不靠帧尾。 /// public static int ReplyLength(byte cmd) { switch (cmd) { case 0x01: return 6; // 握手 case 0x02: return 6; // 自检 case 0x04: return 7; // 设目标温度 case 0x05: return 6; // 电机控制 case 0x06: return 9; // 读传感器 case 0x08: return 9; case 0x09: return 6; // 设IO(LED开关) case 0x10: return 7; case 0x11: return 10; // 读EEPROM case 0x12: return 6; case 0x18: return 10; // 读电机位置 case 0x19: return 6; case 0x20: return 12; default: return 6; } } // ── 握手:5E 01 00 05 00 + 校验 ── public static byte[] ShakeHands() => WithChecksum(new byte[] { ST, 0x01, 0x00, 0x05, 0x00 }); // ── 读舱室绑定的相机序列号 CCDSN(读EEPROM,原工程 GetModule)── // 5E 11 00 09 00 00 10 04 00 + 校验,回复10字节,[4..7]拼int32 public static byte[] GetCCDSN() => WithChecksum(new byte[] { ST, 0x11, 0x00, 0x09, 0x00, 0x00, 0x10, 0x04, 0x00 }); // ── 读EEPROM灯光亮度(只读,无写命令)── // 5E 11 00 09 00 05 34 04 00 + 校验 public static byte[] ReadLightBrightness() => WithChecksum(new byte[] { ST, 0x11, 0x00, 0x09, 0x00, 0x05, 0x34, 0x04, 0x00 }); /// /// 读第 wellNum(1-16) 个well的水平电机位置(旋转脉冲,存EEPROM)。 /// 培养皿16个well圆形布局,转动培养皿让well对准相机。地址表来自原工程 Commander.cs:284。 /// 命令: 5E 11 00 09 00 04 [地址低] 04 00 + 校验,回复10字节,位置在[4..7]。 /// public static byte[] ReadWellHorizontalPos(int wellNum) { // wellNum → EEPROM 地址低字节(确证自源码) byte addr; switch (wellNum) { case 1: addr = 0x00; break; case 2: addr = 0x50; break; case 3: addr = 0x54; break; case 4: addr = 0x58; break; case 5: addr = 0x5C; break; case 6: addr = 0x60; break; case 7: addr = 0x64; break; case 8: addr = 0x68; break; case 9: addr = 0x6C; break; case 10: addr = 0x70; break; case 11: addr = 0x74; break; case 12: addr = 0x78; break; case 13: addr = 0x7C; break; case 14: addr = 0x80; break; case 15: addr = 0x84; break; case 16: addr = 0x04; break; default: addr = 0x00; break; } return WithChecksum(new byte[] { ST, 0x11, 0x00, 0x09, 0x00, 0x04, addr, 0x04, 0x00 }); } /// /// 读第 wellNum(1-16) 个well的垂直电机焦准脉冲数(Z对焦零点,存EEPROM)。 /// 地址表来自原工程 Commander.cs:530。well1=0x08, 步进4, well16=0x44。 /// public static byte[] ReadWellFocusZero(int wellNum) { byte addr; switch (wellNum) { case 1: addr = 0x08; break; case 2: addr = 0x0C; break; case 3: addr = 0x10; break; case 4: addr = 0x14; break; case 5: addr = 0x18; break; case 6: addr = 0x1C; break; case 7: addr = 0x20; break; case 8: addr = 0x24; break; case 9: addr = 0x28; break; case 10: addr = 0x2C; break; case 11: addr = 0x30; break; case 12: addr = 0x34; break; case 13: addr = 0x38; break; case 14: addr = 0x3C; break; case 15: addr = 0x40; break; case 16: addr = 0x44; break; default: addr = 0x08; break; } return WithChecksum(new byte[] { ST, 0x11, 0x00, 0x09, 0x00, 0x04, addr, 0x04, 0x00 }); } /// 读垂直电机扫描间隔脉冲(每层Z步距,存EEPROM)。Commander.cs:668。 public static byte[] ReadScanStep() => WithChecksum(new byte[] { ST, 0x11, 0x00, 0x09, 0x00, 0x04, 0x48, 0x04, 0x00 }); // ── LED 开/关(固定字节,原工程实测)── public static byte[] OpenLED() => new byte[] { 0x5E, 0x09, 0x00, 0x07, 0x00, 0x01, 0x6F }; public static byte[] CloseLED() => new byte[] { 0x5E, 0x09, 0x00, 0x07, 0x00, 0x00, 0x6E }; // ── 读电机位置:5E 18 00 06 [轴] 00 + 校验。轴: 0x01水平 0x02垂直 ── public static byte[] ReadHorizontalMotor() => WithChecksum(new byte[] { ST, 0x18, 0x00, 0x06, 0x01, 0x00 }); public static byte[] ReadVerticalMotor() => WithChecksum(new byte[] { ST, 0x18, 0x00, 0x06, 0x02, 0x00 }); // ── 电机复位(固定字节,原工程实测)── // 水平复位 5E 05 00 0B 13 00 00 00 0B B8 44 public static byte[] HorizontalReset() => new byte[] { 0x5E, 0x05, 0x00, 0x0B, 0x13, 0x00, 0x00, 0x00, 0x0B, 0xB8, 0x44 }; // 垂直复位 5E 05 00 0B 23 00 00 00 07 D0 68 public static byte[] VerticalReset() => new byte[] { 0x5E, 0x05, 0x00, 0x0B, 0x23, 0x00, 0x00, 0x00, 0x07, 0xD0, 0x68 }; // ── 电机绝对运动:11字节帧,辅助码 0x14水平 0x24垂直,脉冲32位大端 ── public static byte[] HorizontalAbsolute(int pulse) => MotorAbsolute(0x14, pulse); public static byte[] VerticalAbsolute(int pulse) => MotorAbsolute(0x24, pulse); // ── 电机相对正/反转:辅助码 水平0x10/0x11,垂直0x20/0x21 ── public static byte[] HorizontalForward(int pulse) => MotorAbsolute(0x10, pulse); public static byte[] HorizontalBackward(int pulse) => MotorAbsolute(0x11, pulse); public static byte[] VerticalForward(int pulse) => MotorAbsolute(0x20, pulse); public static byte[] VerticalBackward(int pulse) => MotorAbsolute(0x21, pulse); /// /// 构造电机运动帧:5E 05 00 0B [辅助码] [p3 p2 p1 p0 大端] 00 [校验] /// private static byte[] MotorAbsolute(byte auxCode, int pulse) { byte[] p = BitConverter.GetBytes(pulse); // 小端: p[0]低..p[3]高 var cmd = new List { ST, CMD_MOTOR, 0x00, 0x0B, auxCode, p[3], p[2], p[1], p[0], // 大端写入:高字节在前 0x00, // 参数5 0x00 // 校验占位 }; return WithChecksum(cmd.ToArray()); } /// 解析读电机位置回复:字节[4..7]拼 int32(帧内大端)。失败 -1。 public static int ParseMotorPosition(byte[] reply) { if (reply == null || reply.Length < 8) return -1; byte[] t = { reply[7], reply[6], reply[5], reply[4] }; if (!BitConverter.IsLittleEndian) Array.Reverse(t); return BitConverter.ToInt32(t, 0); } /// 解析握手回复:字节[2]即模块号/houseSn。 public static int ParseShakeHands(byte[] reply) { if (reply == null || reply.Length < 3) return -1; return reply[2]; } /// 解析读EEPROM回复:字节[4..7]拼 int32(用于 CCDSN / 亮度)。失败 -1。 public static int ParseEepromInt(byte[] reply) { if (reply == null || reply.Length < 8) return -1; byte[] t = { reply[4], reply[5], reply[6], reply[7] }; if (!BitConverter.IsLittleEndian) Array.Reverse(t); return BitConverter.ToInt32(t, 0); } } }