Сервер для сбора данных с термодатчиковых модулей
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
using NLog;
|
||||
|
||||
namespace ESPServer
|
||||
{
|
||||
public static class Logging
|
||||
{
|
||||
public static void Setup()
|
||||
{
|
||||
var config = new NLog.Config.LoggingConfiguration();
|
||||
|
||||
var logConsole = new NLog.Targets.ConsoleTarget("logconsole");
|
||||
config.AddRuleForAllLevels(logConsole);
|
||||
LogManager.Configuration = config;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
||||
namespace ESPServer
|
||||
{
|
||||
class ModbusServer
|
||||
{
|
||||
private RegistersContainer registers;
|
||||
|
||||
public int Port { get; set; }
|
||||
|
||||
public ModbusServer(RegistersContainer registers)
|
||||
{
|
||||
this.registers = registers;
|
||||
}
|
||||
|
||||
internal void Run()
|
||||
{
|
||||
var logger = LogManager.GetCurrentClassLogger();
|
||||
logger.Info("Accepting connections on port " + Port);
|
||||
var listener = new TcpListener(IPAddress.Any, Port);
|
||||
listener.Start();
|
||||
while (true)
|
||||
{
|
||||
var client = listener.AcceptTcpClient();
|
||||
logger.Info("Accepted connection from " + client.Client.RemoteEndPoint);
|
||||
var worker = new ModbusServerWorker(registers, client);
|
||||
Thread clientThread = new Thread(new ThreadStart(worker.Run));
|
||||
clientThread.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ModbusServerWorker
|
||||
{
|
||||
const int RECEIVE_TIMEOUT = 10000; // 10 seconds
|
||||
|
||||
private RegistersContainer registers;
|
||||
private TcpClient client;
|
||||
private static Logger logger = LogManager.GetCurrentClassLogger();
|
||||
public ModbusServerWorker(RegistersContainer registers, TcpClient client)
|
||||
{
|
||||
this.registers = registers;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
List<byte> received;
|
||||
ManualResetEvent readCompleted;
|
||||
int currentReceived;
|
||||
NetworkStream stream;
|
||||
byte[] buffer = new byte[256];
|
||||
|
||||
public void Run()
|
||||
{
|
||||
logger.Debug("Working with client");
|
||||
received = new List<byte>();
|
||||
try
|
||||
{
|
||||
client.ReceiveTimeout = RECEIVE_TIMEOUT;
|
||||
stream = client.GetStream();
|
||||
readCompleted = new ManualResetEvent(false);
|
||||
while (true)
|
||||
{
|
||||
readCompleted.Reset();
|
||||
stream.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(this.OnReadCompleted), this);
|
||||
readCompleted.WaitOne();
|
||||
if (currentReceived <= 0)
|
||||
{
|
||||
client.Close();
|
||||
return;
|
||||
}
|
||||
ProcessReceivedData();
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
client.Close();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (stream != null) stream.Dispose();
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessReceivedData()
|
||||
{
|
||||
lock (received)
|
||||
{
|
||||
if (received.Count < 6) return;
|
||||
int len = (((int)received[4] << 8) | (int)received[5]);
|
||||
if (received.Count < 6 + len) return;
|
||||
if (len < 2) throw new Exception("Wrong length");
|
||||
int transactionId = (((int)received[0] << 8) | (int)received[1]);
|
||||
byte unitId = received[6];
|
||||
byte function = received[7];
|
||||
List<byte> response = new List<byte>();
|
||||
if (function == 0x03) // read holding registers
|
||||
{
|
||||
int addr = (((int)received[8] << 8) | (int)received[9]);
|
||||
int amount = (((int)received[10] << 8) | (int)received[11]);
|
||||
for (int a = addr; a < addr + amount; a++)
|
||||
{
|
||||
int value = registers[a];
|
||||
response.Add((byte)((value >> 8) & 0xFF));
|
||||
response.Add((byte)(value & 0xFF));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unsupported function: " + function);
|
||||
}
|
||||
List<byte> fullResponse = new List<byte>();
|
||||
fullResponse.Add(received[0]);
|
||||
fullResponse.Add(received[1]); // transaction id
|
||||
fullResponse.Add(received[2]);
|
||||
fullResponse.Add(received[3]); // protocol id
|
||||
int responseLength = 3 + response.Count;
|
||||
fullResponse.Add((byte)((responseLength >> 8) & 0xFF));
|
||||
fullResponse.Add((byte)((responseLength) & 0xFF));
|
||||
fullResponse.Add(unitId);
|
||||
fullResponse.Add(function);
|
||||
fullResponse.Add((byte)response.Count);
|
||||
fullResponse.AddRange(response);
|
||||
received.RemoveRange(0, 6 + len);
|
||||
stream.Write(fullResponse.ToArray(), 0, fullResponse.Count);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReadCompleted(IAsyncResult ar)
|
||||
{
|
||||
try
|
||||
{
|
||||
currentReceived = stream.EndRead(ar);
|
||||
lock (received)
|
||||
{
|
||||
for (int i = 0; i < currentReceived; i++) received.Add(buffer[i]);
|
||||
}
|
||||
readCompleted.Set();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
logger.Info("Error receiving " + e);
|
||||
currentReceived = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace ESPServer
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static JsonDocument config;
|
||||
static UnitsDatabase db;
|
||||
static RegistersContainer registers;
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
config = LoadConfig();
|
||||
Logging.Setup();
|
||||
db = new UnitsDatabase(config.RootElement.GetProperty("units"));
|
||||
registers = new RegistersContainer();
|
||||
var rootCommand = new RootCommand();
|
||||
var runCommand = new Command("run")
|
||||
{
|
||||
};
|
||||
runCommand.Handler = CommandHandler.Create<string>(RunService);
|
||||
rootCommand.AddCommand(runCommand);
|
||||
var openhabConfigCommand = new Command("openhab-config")
|
||||
{
|
||||
};
|
||||
openhabConfigCommand.Handler = CommandHandler.Create<string>(GenerateOpenhabConfig);
|
||||
rootCommand.AddCommand(openhabConfigCommand);
|
||||
rootCommand.InvokeAsync(args).Wait();
|
||||
}
|
||||
|
||||
private static void GenerateOpenhabConfig(string obj)
|
||||
{
|
||||
var root = config.RootElement;
|
||||
StringBuilder thingsConfig = new StringBuilder();
|
||||
StringBuilder itemsConfig = new StringBuilder();
|
||||
thingsConfig.AppendFormat("Bridge modbus:tcp:{0} [ host=\"{1}\", port={2}, id=1 ] {{\n",
|
||||
"esp_server",
|
||||
root.GetProperty("IP").GetString(),
|
||||
root.GetProperty("modbusPort").GetInt32());
|
||||
//
|
||||
db.GenerateThingsConfig(thingsConfig);
|
||||
db.GenerateItemsConfig(itemsConfig);
|
||||
//
|
||||
thingsConfig.Append("}\n");
|
||||
|
||||
File.WriteAllText("esp_server.things", thingsConfig.ToString());
|
||||
File.WriteAllText("esp_server.items", itemsConfig.ToString());
|
||||
}
|
||||
|
||||
private static void RunService(string obj)
|
||||
{
|
||||
var unitReceiver = new UnitDataReceiver(config.RootElement.GetProperty("port").GetInt32(), db, registers);
|
||||
unitReceiver.Start();
|
||||
var modbusServer = new ModbusServer(registers);
|
||||
modbusServer.Port = config.RootElement.GetProperty("modbusPort").GetInt32();
|
||||
modbusServer.Run();
|
||||
}
|
||||
|
||||
private static JsonDocument LoadConfig()
|
||||
{
|
||||
string assemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
var config = Path.Combine(assemblyFolder, "esp_server.json");
|
||||
string configStr = File.ReadAllText(config);
|
||||
return JsonDocument.Parse(configStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\Release\netcoreapp3.1\publish\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<SelfContained>false</SelfContained>
|
||||
<PublishSingleFile>False</PublishSingleFile>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
</Project>
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ESPServer
|
||||
{
|
||||
class RegistersContainer
|
||||
{
|
||||
private Dictionary<int, int> registers;
|
||||
|
||||
public RegistersContainer()
|
||||
{
|
||||
registers = new Dictionary<int, int>();
|
||||
}
|
||||
|
||||
public int this[int address]
|
||||
{
|
||||
get
|
||||
{
|
||||
lock(registers)
|
||||
{
|
||||
if (registers.ContainsKey(address)) return registers[address];
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (registers)
|
||||
{
|
||||
registers[address] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetFloat(int address, float value)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
registers[address + 1] = (int)bytes[0] + (((int)bytes[1]) << 8);
|
||||
registers[address] = (int)bytes[2] + (((int)bytes[3]) << 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace ESPServer
|
||||
{
|
||||
class UnitDataReceiver
|
||||
{
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
UnitsDatabase db;
|
||||
RegistersContainer registers;
|
||||
int port;
|
||||
|
||||
public UnitDataReceiver(int port, UnitsDatabase db, RegistersContainer registers)
|
||||
{
|
||||
this.db = db;
|
||||
this.port = port;
|
||||
this.registers = registers;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
var t = new Thread(new ThreadStart(this.Run));
|
||||
t.Start();
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
UdpClient listener = new UdpClient(port);
|
||||
IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, port);
|
||||
|
||||
try
|
||||
{
|
||||
logger.Info("Listening for UDP datagrams on port " + port);
|
||||
while (true)
|
||||
{
|
||||
byte[] bytes = listener.Receive(ref groupEP);
|
||||
string t = Encoding.ASCII.GetString(bytes, 0, bytes.Length);
|
||||
logger.Debug("Received: " + t);
|
||||
try
|
||||
{
|
||||
var received = JsonDocument.Parse(t);
|
||||
var id = received.RootElement.GetProperty("id").GetString();
|
||||
var unit = db.GetUnit(id);
|
||||
if (unit == null)
|
||||
{
|
||||
logger.Info("Packet from unknown unit " + id);
|
||||
unit = db.AddUnit(id);
|
||||
}
|
||||
ProcessUnit(unit, received.RootElement);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
listener.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessUnit(Unit unit, JsonElement data)
|
||||
{
|
||||
var ip = data.GetProperty("ip").GetString();
|
||||
lock (unit)
|
||||
{
|
||||
unit.ip = ip;
|
||||
foreach (var v in data.EnumerateObject())
|
||||
{
|
||||
if (v.Name == "id" || v.Name == "ip") continue;
|
||||
var uv = unit.GetValue(v.Name);
|
||||
if (uv == null)
|
||||
{
|
||||
logger.Info("Unit " + unit.id + " unknown value " + v.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (v.Value.ValueKind == JsonValueKind.Null) uv.value = float.NaN;
|
||||
if (v.Value.ValueKind == JsonValueKind.Number) uv.value = v.Value.GetSingle();
|
||||
else
|
||||
{
|
||||
if (!float.TryParse(v.Value.GetString(), out uv.value))
|
||||
{
|
||||
uv.value = float.NaN;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FillRegisters(unit);
|
||||
}
|
||||
}
|
||||
|
||||
private void FillRegisters(Unit unit)
|
||||
{
|
||||
foreach (var uv in unit.values)
|
||||
{
|
||||
registers.SetFloat(unit.baseAddress + uv.Value.addressOffset, uv.Value.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ESPServer
|
||||
{
|
||||
/*
|
||||
* Описание параметра, который может придти от юнита
|
||||
*/
|
||||
class UnitValue
|
||||
{
|
||||
// Название параметра
|
||||
public string name;
|
||||
// Текущее значение
|
||||
public float value;
|
||||
// Сдвиг адреса, по которому это значение будет храниться в modbus
|
||||
public int addressOffset;
|
||||
}
|
||||
|
||||
/*
|
||||
* Описание конкретного модуля (релейный, датчик, whatever)
|
||||
*/
|
||||
class Unit
|
||||
{
|
||||
// Айдишник юнита (мак-адрес)
|
||||
public string id;
|
||||
// Название (для генерации конфигов например)
|
||||
public string name;
|
||||
// Базовый адрес в модбасе
|
||||
public int baseAddress;
|
||||
// Значения которые есть в юните
|
||||
public Dictionary<string, UnitValue> values;
|
||||
// текущий IP юнита
|
||||
public string ip;
|
||||
|
||||
public UnitValue GetValue(string name)
|
||||
{
|
||||
if (!values.ContainsKey(name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return values[name];
|
||||
}
|
||||
|
||||
public int MinOffset
|
||||
{
|
||||
get
|
||||
{
|
||||
int min = int.MaxValue;
|
||||
foreach (var u in values) if (u.Value.addressOffset < min) min = u.Value.addressOffset;
|
||||
return min;
|
||||
}
|
||||
}
|
||||
|
||||
public int MaxOffset
|
||||
{
|
||||
get
|
||||
{
|
||||
int max = int.MinValue;
|
||||
foreach (var u in values) if (u.Value.addressOffset > max) max = u.Value.addressOffset;
|
||||
return max;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UnitsDatabase
|
||||
{
|
||||
List<Unit> units;
|
||||
|
||||
public UnitsDatabase(JsonElement config)
|
||||
{
|
||||
units = new List<Unit>();
|
||||
foreach (var unitConfig in config.EnumerateArray())
|
||||
{
|
||||
var u = new Unit();
|
||||
u.id = unitConfig.GetProperty("id").GetString();
|
||||
u.name = unitConfig.GetProperty("name").GetString();
|
||||
u.baseAddress = unitConfig.GetProperty("address").GetInt32();
|
||||
u.values = new Dictionary<string, UnitValue>();
|
||||
foreach (var knownValue in unitConfig.GetProperty("values").EnumerateArray())
|
||||
{
|
||||
var uv = new UnitValue();
|
||||
uv.name = knownValue.GetProperty("name").GetString();
|
||||
uv.addressOffset = knownValue.GetProperty("offset").GetInt32();
|
||||
u.values[uv.name] = uv;
|
||||
}
|
||||
units.Add(u);
|
||||
}
|
||||
}
|
||||
|
||||
internal Unit AddUnit(string id)
|
||||
{
|
||||
lock (units)
|
||||
{
|
||||
var u = new Unit();
|
||||
u.id = id;
|
||||
u.name = id;
|
||||
u.baseAddress = -1;
|
||||
u.values = new Dictionary<string, UnitValue>();
|
||||
units.Add(u);
|
||||
return u;
|
||||
}
|
||||
}
|
||||
|
||||
public Unit GetUnit(string id)
|
||||
{
|
||||
lock (units)
|
||||
{
|
||||
foreach (var u in units)
|
||||
{
|
||||
if (u.id == id) return u;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#region OpenHAB config generation
|
||||
internal void GenerateItemsConfig(StringBuilder config)
|
||||
{
|
||||
foreach (var u in units)
|
||||
{
|
||||
foreach (var uv in u.values)
|
||||
{
|
||||
var v = uv.Value;
|
||||
config.Append(string.Format("Number {0}_{1} \"{0} {1}\"", u.name, v.name));
|
||||
config.Append(" {channel=\"");
|
||||
config.AppendFormat("modbus:data:esp_server:esp_server:");
|
||||
config.AppendFormat("{0}_{1}:number", u.name, v.name);
|
||||
config.Append("\"}\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void GenerateThingsConfig(StringBuilder config)
|
||||
{
|
||||
int minAddress = int.MaxValue;
|
||||
int maxAddress = int.MinValue;
|
||||
foreach (var u in units)
|
||||
{
|
||||
if (u.baseAddress + u.MinOffset < minAddress)
|
||||
{
|
||||
minAddress = u.baseAddress + u.MinOffset;
|
||||
}
|
||||
if (u.baseAddress + u.MaxOffset > maxAddress)
|
||||
{
|
||||
maxAddress = u.baseAddress + u.MaxOffset;
|
||||
}
|
||||
}
|
||||
// поскольку читаем флоаты, нужен ещё регистр запаса
|
||||
maxAddress++;
|
||||
config.Append("\tBridge poller ");
|
||||
config.Append("esp_server");
|
||||
config.AppendFormat("[start = {0}, length = {1}, type = \"holding\", refresh = 500] {{\n", minAddress, maxAddress - minAddress + 1);
|
||||
foreach (var u in units)
|
||||
{
|
||||
foreach (var uv in u.values)
|
||||
{
|
||||
var v = uv.Value;
|
||||
config.Append("\t\tThing data ");
|
||||
config.Append(u.name + "_" + v.name);
|
||||
config.AppendFormat("[readStart = \"{0}\", readValueType = \"float32\"]\n", u.baseAddress + v.addressOffset);
|
||||
}
|
||||
}
|
||||
config.Append("\t}\n");
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NLog" Version="4.7.9" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<_LastSelectedProfileId>C:\Work\heat_control\esp_server\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"IP": "192.168.0.10",
|
||||
"port": 7000,
|
||||
"modbusPort": 7001,
|
||||
"units": [
|
||||
{
|
||||
"name": "unit1",
|
||||
"id": "8c:aa:b5:7d:6e:6c",
|
||||
"address": 10,
|
||||
"values": [
|
||||
{ "name": "t", "offset": 0 },
|
||||
{ "name": "h", "offset": 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30804.86
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "esp_server", "esp_server.csproj", "{A9FC0FC1-AB9D-4395-A3E2-721EF901226B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A9FC0FC1-AB9D-4395-A3E2-721EF901226B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9FC0FC1-AB9D-4395-A3E2-721EF901226B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9FC0FC1-AB9D-4395-A3E2-721EF901226B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A9FC0FC1-AB9D-4395-A3E2-721EF901226B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {75B90B6A-0F87-466B-B503-069E4A09A77C}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Reference in New Issue
Block a user