// 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(); } } }