// Spludlow Software
// Copyright © Samuel P. Ludlow 2020 All Rights Reserved
// Distributed under the terms of the GNU General Public License version 3
// Distributed WITHOUT ANY WARRANTY; without implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE
// https://www.spludlow.co.uk/LICENCE.TXT
// The Spludlow logo is a registered trademark of Samuel P. Ludlow and may not be used without permission
// v1.14
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Data;
using System.Net.Sockets;
using System.Threading;
using System.ServiceModel;
namespace Spludlow.Trains
{
///
/// Server for XnTcp
/// http://www.terdina.net/rails/
///
public class TrainServer : ITrainServer, IDisposable
{
private class Configuration
{
public string Host;
public int Port;
public TimeSpan Wait;
public int OffCount;
public TrainsDataSet.TrainsDataTable Trains;
public TrainsDataSet.PointsDataTable Points;
}
public class StateObject
{
public Socket Socket;
public byte[] Buffer = new byte[256];
public List Data = new List();
}
private Configuration Config;
private TrainsDataSet DataSet;
private Socket Socket;
public TrainServer()
{
this.LoadConfiguration();
this.Initialize();
}
public void Dispose()
{
Spludlow.Log.Info("TrainServer, Dispose()");
this.Close();
}
private void LoadConfiguration()
{
this.Config = new Configuration();
this.Config.OffCount = 3;
this.Config.Wait = new TimeSpan(0, 0, 0, 0, 250);
this.Config.Trains = new TrainsDataSet.TrainsDataTable();
this.Config.Points = new TrainsDataSet.PointsDataTable();
foreach (string rawLine in File.ReadAllLines(Spludlow.Config.ProgramData + @"\Data\Trains\TrainServer.txt"))
{
string line = rawLine.Trim();
if (line.Length == 0 || line[0] == '#')
continue;
string[] words = Spludlow.Text.Split(line, '\t', true, false);
string command = words[0];
if (command == "X")
{
string[] parts = words[1].Split(new char[] { ':' });
if (parts.Length != 2)
throw new ApplicationException("XnTcp config expected config host:port");
this.Config.Host = parts[0];
this.Config.Port = Int32.Parse(parts[1]);
continue;
}
if (command == "W")
{
this.Config.Wait = new TimeSpan(0, 0, 0, 0, Int32.Parse(words[1]));
continue;
}
if (command == "F")
{
this.Config.OffCount = Int32.Parse(words[1]);
continue;
}
if (command[0] == 'T')
{
TrainsDataSet.TrainsRow trainRow = this.Config.Trains.NewTrainsRow();
trainRow.Address = Int32.Parse(words[0].Substring(1));
trainRow.Name = words[1];
trainRow.Image = "Images/Trains/" + words[2];
//trainRow.Speed;
this.Config.Trains.Rows.Add(trainRow);
continue;
}
if (command[0] == 'P')
{
TrainsDataSet.PointsRow pointsRow = this.Config.Points.NewPointsRow();
pointsRow.Address = Int32.Parse(words[0].Substring(1));
pointsRow.Name = words[1];
pointsRow.Image = "Images/Trains/" + words[2];
//pointsRow.Direction;
this.Config.Points.Rows.Add(pointsRow);
continue;
}
throw new ApplicationException("Unknown config command:\t" + words[0]);
}
}
private void Initialize()
{
this.Close();
this.DataSet = new TrainsDataSet();
foreach (TrainsDataSet.TrainsRow row in this.Config.Trains.Rows)
this.DataSet.Trains.ImportRow(row);
foreach (TrainsDataSet.PointsRow row in this.Config.Points.Rows)
this.DataSet.Points.ImportRow(row);
this.Connect();
}
private void Connect()
{
if (this.Config.Host == null)
{
Spludlow.Log.Warning("TrainServer, XnTcp Test mode.");
}
else
{
this.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
this.Socket.Connect(this.Config.Host, this.Config.Port);
StateObject state = new StateObject();
state.Socket = this.Socket;
this.BeginReceive(state);
Spludlow.Log.Info("TrainServer, XnTcp Connected:\t" + this.Config.Host + "\t" + this.Config.Port);
}
}
private void BeginReceive(StateObject state)
{
this.Socket.BeginReceive(state.Buffer, 0, state.Buffer.Length, 0, new AsyncCallback(ReceiveCallback), state);
}
public void Close()
{
if (this.Socket != null)
{
Spludlow.Log.Info("TrainServer, Closing Socket.");
this.Socket.Close();
this.Socket.Dispose();
this.Socket = null;
Spludlow.Log.Info("TrainServer, Closed Socket.");
}
else
{
Spludlow.Log.Info("TrainServer, Closing socket NULL.");
}
}
public static Spludlow.Trains.ITrainServer MakeClient(string serviceAppKey)
{
string address = Spludlow.Config.RemotingAddress(serviceAppKey);
NetTcpBinding binding = new NetTcpBinding(SecurityMode.None);
binding.MaxReceivedMessageSize = 1 * 1024 * 1024 * 1024;
EndpointAddress endpointAddress = new EndpointAddress(address);
Spludlow.Trains.ITrainServer channel = ChannelFactory.CreateChannel(binding, endpointAddress);
return channel;
}
private void Send(byte[] data)
{
bool resend = true;
try
{
if (this.Socket == null)
throw new ApplicationException("Socket is NULL.");
this.Socket.Send(data);
resend = false;
}
catch (Exception ee)
{
Spludlow.Log.Error("TrainServer, Send(), Re-Starting.", ee);
this.Close();
this.Connect();
}
if (resend == true)
{
Spludlow.Log.Warning("TrainServer, Send(), Re-Sending Data.");
this.Socket.Send(data);
Spludlow.Log.Info("TrainServer, Send(), Re-Sent Data.");
}
}
private void ReceiveCallback(IAsyncResult ar)
{
try
{
StateObject state = (StateObject)ar.AsyncState;
Socket client = state.Socket;
int bytesRead = client.EndReceive(ar);
if (bytesRead > 0)
{
byte[] data = new byte[bytesRead];
Array.Copy(state.Buffer, data, data.Length);
this.Log("ReceiveCallback", data, "");
this.BeginReceive(state);
}
else
{
this.Log("ReceiveCallback", new byte[0], "No Data");
}
}
catch (Exception ee)
{
Spludlow.Log.Error("TrainServer, ReceiveCallback(), Re-Starting.", ee);
this.Close();
this.Connect();
}
}
private void Log(string source, byte[] header, byte[] body, byte[] checkSum, string info)
{
byte[] data = new byte[body.Length + 2];
data[0] = header[0];
body.CopyTo(data, 1);
data[data.Length - 1] = checkSum[0];
this.Log(source, data, info);
}
private void Log(string source, byte[] data, string info)
{
lock (this.DataSet)
{
TrainsDataSet.LogsRow logRow = this.DataSet.Logs.NewLogsRow();
logRow.EventTime = DateTime.Now;
logRow.Source = source;
logRow.Data = BytesToTabLine(data);
logRow.Info = info;
this.DataSet.Logs.Rows.Add(logRow);
int maxLogs = 32;
DataView view = new DataView(this.DataSet.Logs);
view.Sort = "EventTime DESC";
if (view.Count > maxLogs)
{
for (int index = maxLogs; index < view.Count; ++index)
{
DataRowView rowView = view[index];
rowView.Row.Delete();
}
this.DataSet.Logs.AcceptChanges();
}
}
}
public void PowerOff()
{
byte[] buffer = new byte[] { 0x21, 0x80, 0xA1 };
//this.CheckSum(buffer);
this.Send(buffer);
this.Log("Send", buffer, "PowerOff");
}
public void Emergency()
{
byte[] buffer = new byte[] { 0x80, 0x80 };
//this.CheckSum(buffer);
this.Send(buffer);
this.Log("Send", buffer, "Emergency");
}
public void Normal()
{
byte[] buffer = new byte[] { 0x21, 0x81, 0xA0 };
//this.CheckSum(buffer);
this.Send(buffer);
this.Log("Send", buffer, "Normal");
}
private byte[] PointsData(int address, bool direction, bool motorOn)
{
--address;
byte[] buffer = new byte[4];
buffer[0] = 0x52;
buffer[1] = (byte)(address / 4);
int data = 0x80;
data |= ((address % 4) << 1);
if (direction == true)
data |= 0x01;
if (motorOn == true)
data |= 0x08;
buffer[2] = (byte)data;
this.CheckSum(buffer);
return buffer;
}
public void Points(int address, bool direction)
{
byte[] onBuffer = this.PointsData(address, direction, true);
byte[] offBuffer = this.PointsData(address, direction, false);
lock (this.DataSet)
{
this.Send(onBuffer);
for (int off = 0; off < this.Config.OffCount; ++off)
{
Thread.Sleep(this.Config.Wait);
this.Send(offBuffer);
}
TrainsDataSet.PointsRow[] pointsRows = (TrainsDataSet.PointsRow[])this.DataSet.Points.Select("Address = " + address);
if (pointsRows.Length == 0)
throw new ApplicationException("Points Row not found:\t" + address);
if (pointsRows.Length > 1)
throw new ApplicationException("Points Row multiple address match found:\t" + address);
pointsRows[0].Direction = direction;
}
StringBuilder info = new StringBuilder();
info.Append("Address:");
info.Append(address.ToString("X2"));
info.Append(", Direction:");
info.Append(direction);
this.Log("Points On", onBuffer, info.ToString());
this.Log("Points Off X " + this.Config.OffCount, offBuffer, info.ToString());
}
public void Send(string data)
{
string[] words = Spludlow.Text.Split(data, '\t', true, false);
if (words.Length == 0)
throw new ApplicationException("Empty Data.");
byte[] buffer = new byte[words.Length + 1];
for (int index = 0; index < words.Length; ++index)
buffer[index] = (byte)ParseDecimalOrHex(words[index]);
this.CheckSum(buffer);
this.Send(buffer);
this.Log("Send Data", buffer, "");
}
public void Speed(int address, int speed)
{
if (speed < -100)
speed = -100;
if (speed > 100)
speed = 100;
int speedData = (int)Math.Round((31.0M / 100.0M) * (decimal)Math.Abs(speed), 0);
if (speed < 0)
speedData = speedData | 0x80;
byte[] buffer = new byte[6];
buffer[0] = 0xE4;
buffer[1] = 0x12; // Speed and direction instruction ?
buffer[2] = 0x00; // Address High
buffer[3] = (byte)address; // Address Low
buffer[4] = (byte)speedData; // Speed
this.CheckSum(buffer);
lock (this.DataSet)
{
this.Send(buffer);
TrainsDataSet.TrainsRow[] trainRows = (TrainsDataSet.TrainsRow[])this.DataSet.Trains.Select("Address = " + address);
if (trainRows.Length == 0)
throw new ApplicationException("Train Row not found:\t" + address);
if (trainRows.Length > 1)
throw new ApplicationException("Train Row multiple address match found:\t" + address);
trainRows[0].Speed = speed;
}
StringBuilder info = new StringBuilder();
info.Append("Address:");
info.Append(address.ToString("X2"));
info.Append(", speed:");
info.Append(speed);
info.Append("%");
this.Log("Send", buffer, info.ToString());
}
private void CheckSum(byte[] buffer)
{
byte checkSum = 0;
for (int index = 0; index < (buffer.Length - 1); ++index)
checkSum ^= buffer[index];
buffer[buffer.Length - 1] = checkSum;
}
private byte CheckSum(byte[] header, byte[] body)
{
byte checkSum = 0;
checkSum ^= header[0];
foreach (byte b in body)
checkSum ^= b;
return checkSum;
}
public TrainsDataSet Query(bool includeLogs)
{
TrainsDataSet dataSet;
lock (this.DataSet)
{
if (includeLogs == true)
{
dataSet = (TrainsDataSet)this.DataSet.Copy();
}
else
{
dataSet = new TrainsDataSet();
foreach (DataRow row in this.DataSet.Trains.Rows)
dataSet.Trains.ImportRow(row);
foreach (DataRow row in this.DataSet.Points.Rows)
dataSet.Points.ImportRow(row);
}
}
return dataSet;
}
public void ClearLog()
{
lock (this.DataSet)
{
this.DataSet.Logs.Clear();
}
}
private static int ParseDecimalOrHex(string text)
{
if (text.StartsWith("0x") == true)
return Int32.Parse(text.Substring(2), System.Globalization.NumberStyles.AllowHexSpecifier);
else
return Int32.Parse(text);
}
private static string BytesToTabLine(byte[] buffer)
{
StringBuilder text = new StringBuilder();
text.Append(buffer.Length);
text.Append("\t:");
foreach (byte b in buffer)
{
text.Append("\t");
text.Append("0x");
text.Append(b.ToString("X2"));
}
return text.ToString();
}
}
}