CORE-POS/IS4C

View on GitHub
pos/is4c-nf/scale-drivers/drivers/NewMagellan/Magellan.cs

Summary

Maintainability
C
1 day
Test Coverage
/*******************************************************************************

    Copyright 2009 Whole Foods Co-op

    This file is part of IT CORE.

    IT CORE is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    IT CORE is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    in the file license.txt along with IT CORE; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*********************************************************************************/

/*************************************************************
 * Magellan
 *     Main app. Starts all requested Serial Port Handlers
 * and monitors UDP for messages
 *
 * Note that exit won't work cleanly if a SerialPortHandler
 * blocks indefinitely. Use timeouts in polling reads.
*************************************************************/
using System;
using System.Threading;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;

#if NEWTONSOFT_JSON
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
#endif

#if CORE_RABBIT
using RabbitMQ.Client;
#endif

#if CORE_MYSQL
using MySqlPipe;
#endif

using CustomForms;
using CustomUDP;
using SPH;
using Discover;

[assembly: AssemblyVersion("1.0.*")]

public class Magellan : DelegateForm 
{
    private List<SerialPortHandler> sph;
    private UDPMsgBox u;
    private bool asyncUDP = true;
    private bool disableRBA = false;
    private bool disableButtons = false;
    private bool logXML = false;
    private Object msgLock = new Object();
    private ushort msgCount = 0;
    private static string log_path = "debug_lane.log";

    private bool mq_enabled = false;
    private bool full_udp = false;

    #if CORE_RABBIT
    private bool mq_available = true;
    ConnectionFactory rabbit_factory;
    IConnection rabbit_con;
    IModel rabbit_channel;
    #else
    private bool mq_available = false;
    #endif

    #if CORE_MYSQL
    private MySqlPipe.MySqlPipe mpipe;
    #endif

    // read deisred modules from config file
    public Magellan(int verbosity)
    {
        var d = new Discover.Discover();
        var modules = d.GetSubClasses("SPH.SerialPortHandler");
        var my_location = AppDomain.CurrentDomain.BaseDirectory;
        var sep = Path.DirectorySeparatorChar;
        Magellan.log_path = my_location + sep + ".." + sep + ".." + sep + ".." + sep + "log" + sep + "debug_lane.log";

        List<MagellanConfigPair> conf = ReadConfig();
        sph = new List<SerialPortHandler>();
        foreach (var pair in conf) {
            try {
                if (modules.Any(m => m.Name == pair.module)) {
                    var type = d.GetType("SPH." + pair.module);
                    Console.WriteLine(pair.module + ":" + pair.port);
                    SerialPortHandler s = (SerialPortHandler)Activator.CreateInstance(type, new Object[]{ pair.port });
                    s.SetParent(this);
                    s.SetVerbose(verbosity);
                    s.SetConfig("disableRBA", this.disableRBA ? "true" : "false");
                    s.SetConfig("disableButtons", this.disableButtons ? "true" : "false");
                    s.SetConfig("logXML", this.logXML ? "true" : "false");
                    s.SetConfig(pair.settings);
                    sph.Add(s);
                } else {
                    throw new Exception("unknown module: " + pair.module);
                }
            } catch (Exception ex) {
                Console.WriteLine(ex);
                Console.WriteLine("Warning: could not initialize "+pair.port);
                Console.WriteLine("Ensure the device is connected and you have permission to access it.");
            }
        }
        MonitorSerialPorts();
        UdpListen();

        factorRabbits();
    }

    // alternate constructor for specifying
    // desired modules at compile-time
    public Magellan(SerialPortHandler[] args)
    {
        this.sph = new List<SerialPortHandler>(args);
        MonitorSerialPorts();
        UdpListen();
    }

    private void factorRabbits()
    {
        #if CORE_RABBIT
        try {
            rabbit_factory = new ConnectionFactory();
            rabbit_factory.HostName = "localhost";
            rabbit_con = rabbit_factory.CreateConnection();
            rabbit_channel = rabbit_con.CreateModel();
            rabbit_channel.QueueDeclare("core-pos", false, false, false, null);
        } catch (Exception) {
            mq_available = false;
        }
        #endif
    }


    private UdpClient udp_client = null;
    private UdpClient getClient()
    {
        if (udp_client == null) {
            udp_client = new UdpClient();
            udp_client.Connect(System.Net.IPAddress.Parse("127.0.0.1"), 9451);
        }

        return udp_client;
    }

    private void UdpListen()
    {
        try {
            u = new UDPMsgBox(9450, this.asyncUDP);
            u.SetParent(this);
            u.My_Thread.Start();
        } catch (Exception ex) {
            Magellan.LogMessage(ex.ToString());
            Console.WriteLine("Failed to start UDP server");
            Console.WriteLine(ex);
        }
    }

    private void MonitorSerialPorts()
    {
        try {
            var valid = sph.Where(s => s != null);
            valid.ToList().ForEach(s => { s.SPH_Thread.Start(); });
        } catch (Exception ex) {
            Magellan.LogMessage(ex.ToString());
        }
    }

    public void MsgRecv(string msg, System.Net.IPEndPoint ep=null)
    {
        try {
            if (msg == "exit") {
                this.ShutDown();
            } else if (msg == "die!") {
                new Thread(() => this.ShutDown()).Start();
                Thread.Sleep(500);
                Environment.Exit(0);
            } else if (msg == "full_udp") {
                full_udp = true;
            } else if (msg == "mq_up" && mq_available) {
                mq_enabled = true;
            } else if (msg == "mq_down") {
                mq_enabled = false;
            } else if (msg == "status") {
                byte[] body = System.Text.Encoding.ASCII.GetBytes(Status());
                getClient().Send(body, body.Length); 
            } else if (msg == "core_detect") {
                byte[] body = System.Text.Encoding.ASCII.GetBytes(GetIP() + ";;");
                var client = new UdpClient(); 
                client.Connect(ep.Address, 9451);
                client.Send(body, body.Length); 
                client.Close();
            } else {
                sph.ForEach(s => { s.HandleMsg(msg); });
            }
        } catch (Exception ex) {
            Magellan.LogMessage(ex.ToString());
        }
    }

    private string Status()
    {
        string ret = "";
        foreach (var s in sph) {
            ret += s.Status() + "\n";
        }

        return ret;
    }

    private string GetIP()
    {
        var host = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
        foreach (var ip in host.AddressList) {
            if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) {
                return ip.ToString();
            }
        }

        return "127.0.0.1";
    }

    public void MsgSend(string msg)
    {
        try {
            if (full_udp) {
                byte[] body = System.Text.Encoding.UTF8.GetBytes(msg);
                getClient().Send(body, body.Length); 
            } else if (mq_available && mq_enabled) {
                #if CORE_RABBIT
                byte[] body = System.Text.Encoding.UTF8.GetBytes(msg);
                rabbit_channel.BasicPublish("", "core-pos", null, body);
                #endif
            } else {
                lock (msgLock) {
                    string filename = System.Guid.NewGuid().ToString();
                    string my_location = AppDomain.CurrentDomain.BaseDirectory;
                    char sep = Path.DirectorySeparatorChar;
                    /**
                      Depending on msg rate I may replace "1" with a bigger value
                      as long as the counter resets at least once per 65k messages
                      there shouldn't be sequence issues. But real world disk I/O
                      may be trivial with a serial message source
                    */
                    if (msgCount % 1 == 0 && Directory.GetFiles(my_location+sep+"ss-output/").Length == 0) {
                        msgCount = 0;
                    }
                    filename = msgCount.ToString("D5") + filename;
                    msgCount++;

                    TextWriter sw = new StreamWriter(my_location + sep + "ss-output/" +sep+"tmp"+sep+filename);
                    sw = TextWriter.Synchronized(sw);
                    sw.WriteLine(msg);
                    sw.Close();
                    File.Move(my_location+sep+"ss-output/" +sep+"tmp"+sep+filename,
                          my_location+sep+"ss-output/" +sep+filename);
                }
            }
        } catch (Exception ex) {
            Magellan.LogMessage(ex.ToString());
        }
    }

    public void SqlLog(string k, string s)
    {
        try {
            #if CORE_MYSQL
            this.mpipe.logValue(k, s);
            #endif
        } catch (Exception ex) {
            Magellan.LogMessage(ex.ToString());
        }
    }

    public void ShutDown()
    {
        try {
            u.Stop();
            sph.ForEach(s => { s.Stop(); });
        }
        catch(Exception ex) {
            Magellan.LogMessage(ex.ToString());
            Console.WriteLine(ex);
        }
    }

    private static void LogMessage(string msg)
    {
        try {
            using (StreamWriter sw = File.AppendText(Magellan.log_path)) {
                sw.WriteLine(DateTime.Now.ToString() + ": " + msg);
            }
        } catch (Exception ex) {
            Console.WriteLine("Failed to log: " + ex.ToString());
        }
    }

    #if NEWTONSOFT_JSON
    private List<MagellanConfigPair> JsonConfig()
    {
        string my_location = AppDomain.CurrentDomain.BaseDirectory;
        char sep = Path.DirectorySeparatorChar;
        string ini_file = my_location + sep + ".." + sep + ".." + sep + ".." + sep + "ini.json";
        List<MagellanConfigPair> conf = new List<MagellanConfigPair>();
        if (!File.Exists(ini_file)) {
            return conf;
        }

        string ini_json = File.ReadAllText(ini_file);
        try {
            JObject o = JObject.Parse(ini_json);
            // filter list to valid entries
            var valid = o["NewMagellanPorts"].Where(p=> p["port"] != null && p["module"] != null);
            // map entries to ConfigPair objects
            var pairs = valid.Select(p => new MagellanConfigPair(){port=(string)p["port"], module=(string)p["module"], settings=p.ToObject<Dictionary<string,string>>()});
            conf = pairs.ToList();

            // print errors for invalid entries
            o["NewMagellanPorts"].Where(p => p["port"] == null).ToList().ForEach(p => {
                Console.WriteLine("Missing the \"port\" setting. JSON:");
                Console.WriteLine(p);
            });

            // print errors for invalid entries
            o["NewMagellanPorts"].Where(p => p["module"] == null).ToList().ForEach(p => {
                Console.WriteLine("Missing the \"module\" setting. JSON:");
                Console.WriteLine(p);
            });

            string host = (string)o["localhost"]; 
            string user = (string)o["localUser"]; 
            string pass = (string)o["localPass"]; 
            string dbn = (string)o["tDatabase"]; 
            #if CORE_MYSQL
            Console.WriteLine("Starting SQL Pipe");
            this.mpipe = new MySqlPipe.MySqlPipe(host, user, pass, dbn);
            #endif

        } catch (NullReferenceException) {
            // probably means no NewMagellanPorts key in ini.json
            // not a fatal problem
        } catch (Exception ex) {
            // unexpected exception
            Magellan.LogMessage(ex.ToString());
            Console.WriteLine(ex);
        }
        try {
            JObject o = JObject.Parse(ini_json);
            var ua = (bool)o["asyncUDP"];
            this.asyncUDP = ua;
        } catch (Exception) {}
        try {
            JObject o = JObject.Parse(ini_json);
            var drb = (bool)o["disableRBA"];
            this.disableRBA = drb;
        } catch (Exception) {}
        try {
            JObject o = JObject.Parse(ini_json);
            var dbt = (bool)o["disableButtons"];
            this.disableButtons = dbt;
        } catch (Exception) {}
        try {
            JObject o = JObject.Parse(ini_json);
            var lx = (bool)o["logXML"];
            this.logXML = lx;
        } catch (Exception) {}

        return conf;
    }
    #endif

    private List<MagellanConfigPair> ReadConfig()
    {
        /**
         * Look for settings in ini.json if it exists
         * and the library DLL exists
         */
        #if NEWTONSOFT_JSON
        List<MagellanConfigPair> json_ports = JsonConfig();
        if (json_ports.Count > 0) {
            return json_ports;
        }
        #endif

        string my_location = AppDomain.CurrentDomain.BaseDirectory;
        char sep = Path.DirectorySeparatorChar;
        StreamReader fp = new StreamReader(my_location + sep + "ports.conf");
        List<MagellanConfigPair> conf = new List<MagellanConfigPair>();
        HashSet<string> hs = new HashSet<string>();
        string line;
        while( (line = fp.ReadLine()) != null) {
            line = line.TrimStart(null);
            if (line == "" || line[0] == '#') continue;
            string[] pieces = line.Split(null);
            if (pieces.Length != 2) {
                Console.WriteLine("Warning: malformed port.conf line: "+line);
                Console.WriteLine("Format: <port_string> <handler_class_name>");
            } else if (hs.Contains(pieces[0])) {
                Console.WriteLine("Warning: device already has a module attached.");
                Console.WriteLine("Line will be ignored: "+line);
            } else {
            var pair = new MagellanConfigPair();
                pair.port = pieces[0];
                pair.module = pieces[1];
                conf.Add(pair);
                hs.Add(pieces[0]);
            }
        }    

        return conf;
    }

    // log unhandled exception before app dies
    static void LastRites(object sender, UnhandledExceptionEventArgs args) 
    {
        Exception ex = (Exception) args.ExceptionObject;
        Magellan.LogMessage(ex.ToString());
        Environment.Exit(1);
    }

    static public int Main(string[] args)
    {
        int verbosity = 0;
        for (int i=0;i<args.Length;i++){
            if (args[i] == "-v"){
                verbosity = 1;    
                if (i+1 < args.Length){
                    try { verbosity = Int32.Parse(args[i+1]); }
                    catch{}
                }
            } else if (args[i] == "-m") {
                // Reference a class in SPH to force SPH.dll to load
                var load = new List<SerialPortHandler>();
                var d = new Discover.Discover();
                var modules = d.GetSubClasses("SPH.SerialPortHandler").Where(t => !t.IsAbstract).ToList();
                modules.Sort((a,b) => a.ToString().CompareTo(b.ToString()));
                Console.WriteLine("Available modules:");
                foreach (var m in modules) {
                    Console.WriteLine("  " + m);
                }
                return 0;
            }
        }
        AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(LastRites);
        new Magellan(verbosity);
        Thread.Sleep(Timeout.Infinite);

        return 0;
    }
}

/**
 Helper class representing a config setting
*/
public class MagellanConfigPair
{
    public string port { get; set; }
    public string module { get; set; }
    public Dictionary<string,string> settings { get; set; }
}