using SSPCTester.Devices.Config; using SSPCTester.Devices.Protocol; namespace SSPCTester.Tests; public sealed class ModbusProfileTests { [Fact] public void FieldProfile_IsValidAndCoversPhysicalSlots1To24() { string path = FindRepoFile("Docs", "Modbus接口规范", "device-profile.json"); var profile = ModbusDeviceProfileLoader.Load(path); Assert.Equal(Enumerable.Range(1, 24), profile.Channels.OrderBy(x => x.PhysicalSlot).Select(x => x.PhysicalSlot)); Assert.Equal((byte)1, profile.Channels.Single(x => x.PhysicalSlot == 1).Relay.Slave); Assert.Equal((ushort)7, profile.Channels.Single(x => x.PhysicalSlot == 1).Relay.Coil); Assert.Equal(12, profile.Channels.Single(x => x.PhysicalSlot == 24).Current.RegisterOffset); } [Fact] public void Crc16_UsesModbusLowByteFirst() { byte[] request = ModbusRtuProtocol.BuildRequest(4, 3, 0, 24); Assert.Equal(new byte[] { 0x04, 0x03, 0x00, 0x00, 0x00, 0x18 }, request[..6]); Assert.Equal((byte)(ModbusRtuProtocol.ComputeCrc(request.AsSpan(0, 6)) & 0xFF), request[6]); ModbusRtuProtocol.ValidateCrc(request); } [Fact] public void SignedCurrent_UsesInt16TwosComplementAndFormulaA() { ushort rawBits = unchecked((ushort)(short)-1000); double amps = ModbusValueConverter.ConvertRaw(rawBits, true, "A", 60.0); Assert.Equal(-6.0, amps, 6); } [Fact] public void InvalidDuplicateRegisterMapping_IsRejected() { var profile = ModbusDeviceProfileLoader.Load(ProfilePath()); profile.Current.ChannelsForTestDuplicate(profile.Channels); var error = Assert.Throws(() => ModbusDeviceProfileLoader.Validate(profile)); Assert.Contains("current.registerOffset 存在重复", error.Message); } private static string ProfilePath() => FindRepoFile("Docs", "Modbus接口规范", "device-profile.json"); private static string FindRepoFile(params string[] parts) { var directory = new DirectoryInfo(AppContext.BaseDirectory); while (directory != null) { string candidate = Path.Combine(new[] { directory.FullName }.Concat(parts).ToArray()); if (File.Exists(candidate)) return candidate; directory = directory.Parent; } throw new FileNotFoundException($"找不到仓库文件:{Path.Combine(parts)}"); } } internal static class ProfileTestExtensions { public static void ChannelsForTestDuplicate( this MeasurementProtocol _, IReadOnlyList channels) { channels[1].Current.RegisterOffset = channels[0].Current.RegisterOffset; } }