ESP8266でBトレ(第10回 ソフトウェア編その1)再開しました

ATS動かしてみました。

こんにちはけろけろおじさんです ずいぶん長いこと放置していてすみません、最近またいじり始めました。 書きたいことはいろいろあるのですが、まとまらないのでとりあえずビデオだけでもと思って最近実験したときの映像をアップしました。

www.youtube.com

ATSを動作させるために必要な在線検知機能を使うため、列車がいない(はずの)セクションにも低いDutyで電圧をかけています。 ほかに、列車の進行に合わせてセクションに走行電流を給電する機能ももたせています。 コードに汎用性をもたせるために、セクションに直に給電指示をするのではなく、各列車に対応するプログラム上のインスタンスから、自列車のいるセクションと、一つ前方のセクション(空いたら確保する)に対して給電を指示するようにしています。こうすると、今後、走行速度を外部から制御するように拡張するときにあまり悩まなくて済みます。コード(C#)を下に載せますので興味のある方は読んでみてください(上のビデオはこのコード動いてるものです、まだ骨格だけなのでおじさんが考えている方法が理解していただきやすいかと...)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Reactive;
using System.Reactive.Subjects;
using System.Reactive.Linq;
using System.Reactive.Disposables;
using ZcProxy;
using log4net;


namespace LayoutProxyA {
    //★★★★★★★★★★★★起動処理(抜粋)
    public partial class Form1 : Form {

        public List<TrainE> Trains = null;// new List<TrainC>();

        public TrainE MyTrain;
        private void initTrains() {
            if (Trains != null) {
                Trains.ForEach(t => { t.CurrSection = null; t = null; });
            }
            this.Trains = null;
            this.Trains = new List<TrainE>();
            //レイアウトに置いた編成の列車番号とセクション番号を設定する
            Trains.Add(new TrainE("347M", "3"));
            Trains.Add(new TrainE("345M", "5"));
            Trains.Add(new TrainE("7111", "8"));
        }

        private void initLayout() {
            ZcProxy.ZcProxy.InitZCS("myZcs10.ini");
            SimpleLayoutProxyD.Init();
            SimpleLayoutProxyD.Sections.ForEach(s => s.Run(true));
        }
    }

    //★★★★★★★★★★★★レイアウト全体に対応するクラス
    public class SimpleLayoutProxyD {
        public static ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        public static List<ISectionD> Sections = new List<ISectionD>();
        public static ISectionD GetSectionById(string id) {
            return Sections.Where(s => s.Id == id).FirstOrDefault();
        }
        public static ISectionD NullSection = null;

        public static void Init() {


            //Define ZCs ゾーンコントローラのハードウェアに対応するサーバー上のクラス    ハードコーディング許してください
            var asakaze = ZcProxy.ZcProxy.Zcs.Where(z => z.Id == "asakaze").First();//一号機の名前は「あさかぜ」
            var mizuho = ZcProxy.ZcProxy.Zcs.Where(z => z.Id == "mizuho").First();//二号機の名前は「みずほ」//使っていないけど零号機は「さくら」
            //Define Sections
            Tuple<ZoneController, String[]>[] zcs = {/
                Tuple.Create(asakaze, new String[] { "0,a-5", "1,a-1", "2,a-2", "3,a-3" , "9,a-0"}),
                Tuple.Create(mizuho, new String[] { "4,m-3", "5,m-2", "6,m-1", "7,m-0" , "8,m-5"})
            };
            //Sectionsを作る Idを設定する ZCSのZc.Feederを割り当てながら。 Sectionに割り当てるZCSのZCのFeederはZCName+DispNameで牽く
            for (int i = 0; i <= 9; i++) {
                var section = new SimpleSectionD();
                section.Id = i.ToString();
                var fdName = "外廻0" + i.ToString();
                var fd = asakaze.Feeders.Where(f => f.DispName == fdName).FirstOrDefault();
                if (fd == null) { fd = mizuho.Feeders.Where(f => f.DispName == fdName).FirstOrDefault(); }
                section.Feeder = fd;
                Sections.Add(section);
            }
            //位相同期のパラメータ設定(これもちゃんと動くようになりました)
            asakaze.Synchronizer.LastSendChannel = 0;
            asakaze.Synchronizer.LastSendAdjVal = 180;
            mizuho.Synchronizer.LastSendChannel = 3;
            mizuho.Synchronizer.LastSendAdjVal = 180;

            //両隣のセクションへの参照を設定する
            for (int i = 0; i <= 9; i++) {
                Sections[i].ForwardSection = Sections[(i + 1) % 10];
                Sections[(i + 1) % 10].BackwardSection = Sections[i];
            }
            for (int i = 0; i <= 9; i++) {
                Console.WriteLine("Me:{0} ForWD:{1} BckWD:{2}", Sections[i].Id, Sections[i].ForwardSection.Id, Sections[i].BackwardSection.Id);
            }
        }
    }


    //★★★★★★★★★★★★レイアウト上のセクションを表すインターフェイス
    //(今後の拡張で動的セクションや複合セクションもでてきそうなためインターフェイスを用意した)
    public interface ISectionD {
        ISectionD ForwardSection { get; set; }
        ISectionD BackwardSection { get; set; }
        TrainE Train { get; set; }
        int Signal { get; }
        string Id { get; set; }
        Subject<ISectionD> ExitingPublisher { get; }
        Subject<ISectionD> EnteringPublisher { get; }
        short FeederValue { get; set; }
        bool LastExisting { get; set; }
        int EmptySectionsCount(int maxCount);
        void Run(bool run);
    }


    //★★★★★★★★★★★★レイアウト上の一セクション
    public class SimpleSectionD : ISectionD {
        //セクションのId
        public string Id { get; set; }

        //前方のセクションへの参照(進路構成によって変化する)
        public ISectionD ForwardSection { get; set; }
        //後方のセクションへの参照(進路構成によって変化する)
        public ISectionD BackwardSection { get; set; }

        //検出カウントをつぶやきつづける(0..50)
        public BehaviorSubject<int> ExitstingPublisher = new BehaviorSubject<int>(0);
        //列車が進入したときにつぶやく(その列車が聞いてくれるはず)
        public Subject<ISectionD> _enteringPublisher = new Subject<ISectionD>();
        public Subject<ISectionD> EnteringPublisher { get { return _enteringPublisher; } }
        //列車が退出したときにつぶやく(その列車が聞いてくれるはず)
        public Subject<ISectionD> _exitingPublisher = new Subject<ISectionD>();
        public Subject<ISectionD> ExitingPublisher { get { return _exitingPublisher; } }

        //信号
        public int Signal { get; set; }
        //自分を含めて前方がいくつ空いているか数える
        public int EmptySectionsCount(int maxCount) {
            maxCount--;
            if (maxCount < 0) { return 0; }
            try {
                if (ForwardSection.LastExisting) {
                    return 1;
                }
                else {
                    return ForwardSection.EmptySectionsCount(maxCount) + 1;
                }
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
                return -1;
            }
        }

        //自セクションの前方の信号の監視
        public SimpleSectionD() {
            Task.Run(async () => {
                while (true) {
                    try {
                        if (LastExisting) { Signal = 0; }
                        else { Signal = EmptySectionsCount(3); }                    //★
                        await Task.Delay(50);
                        SimpleLayoutProxyD.Sections.ForEach((s) => { Console.Write("{0}-{1}-{2} ", s.Id, s.Signal, s.LastExisting ? "●" : "-"); });
                        Console.WriteLine();
                    }
                    catch (Exception e) {
                        Console.WriteLine(e.Message);
                    }
                }
            });
        }


        //自セクションにいる列車
        public TrainE Train { get; set; }


        //セクションに対応するフィーダー
        private KFeeder _feeder;
        public KFeeder Feeder {
            get { return _feeder; }
            set {
                _feeder = value;
                if (_feeder != null) {
                    //フィーダーからのイベントを2つセットにする
                    var bufferdFeederEvents = _feeder.ExistPublisher.Buffer(2);//.Where(r => true).Buffer(2);
                    //自セクションに列車が存在しているときに2回連続して列車が検出されなければ、列車が退出した(または車両が持ちあげられた)と判断してイベントを発生
                    bufferdFeederEvents.Where(r => LastExisting && r.Count == 2 && (r[0] <= 3 && r[1] <= 3)).Subscribe(ExitOrDisappearObserved);
                    //自セクションに列車が存在していないときに2回連続して列車が検出されれば、列車が進入した(または車両が置かれた)と判断してイベントを発生
                    bufferdFeederEvents.Where(r => !LastExisting && r.Count == 2 && (r[0] > 3 && r[1] > 3)).Subscribe(EnterOrAppearObserved);
                }
            }
        }
        public bool LastExisting { get; set; } = false;

        //定期的に電圧を印加して在線をチェックする
        public void Run(bool run) {
            Task.Run(async () => {
                //(Trainがnullでも在線していることはあるので注意)
                while (run) {
                    try {
                        await Task.Delay(50);
                        short speedValue = 0;
                        //セクションのどちら側に進むのが前進だとみなすか(まだ未対応)
                        short dirValue = 0;
                        if (Train != null) {
                            speedValue = Train.SpeedValue;
                        }
                        if (speedValue <= 0) { speedValue = 100; }//止まらないようならここを調整する
                        _feeder.LastSendValue = speedValue;
                    }
                    catch (Exception e) {
                        Console.WriteLine(e.Message);
                    }
                }
            });
        }

        //セクションから列車が退出したとき(持ち上げられたとき、脱線したとき?)
        public void ExitOrDisappearObserved(IList<int> les) {
            LastExisting = false;
            var id = this.Id;
            ExitingPublisher.OnNext(this);
        }

        //セクションに列車が進入したとき(置かれたとき)
        public void EnterOrAppearObserved(IList<int> les) {
            LastExisting = true;
            Signal = 0;
            var id = this.Id;
            EnteringPublisher.OnNext(this);
        }
        public short FeederValue {
            get { if (Feeder != null) { return Feeder.LastSendValue; } else { return 0; } }
            set {
                if (Feeder != null) { Feeder.LastSendValue = value; }
            }
        }
v   }
 

    //★★★★★★★★★★★★列車のインスタンス
    public class TrainE {

        //列車番号
        public string Id { get; set; }

        //現在のセクション
        public ISectionD CurrSection { get; set; }
        //信号を監視する対象のセクション
        public ISectionD WatchSection { get; set; }
        //もうすぐ抜け切るセクション
        public ISectionD ExitingSection { get; set; }

        //信号の値ごとの制限速度{何灯式でも設定可能です}
        //private short[] SpeedSteps = { 200, 250, 320, 580, 800 };
        //private short[] SpeedSteps = { 200, 250, 560 };
        //private short[] SpeedSteps = { 100, 200, 350, 600, 800 };
        private short[] SpeedSteps = { 100, 200, 350, 550, 800 };
        //スピード
        public short SpeedValue { get; set; }

        //自分が見ている信号の色
        private int MySignal { get; set; }

        //運転士が設定したスピード
        public short CSpeed { get; set; } = 1024;

        //コンストラクタ
        public TrainE(string id, string sectionId) {
            Id = id;
            CurrSection = SimpleLayoutProxyD.GetSectionById(sectionId);
            CurrSection.Train = this;
            WatchSection = CurrSection.ForwardSection;
            sectionEnteringUnSubscriber = WatchSection.EnteringPublisher.Subscribe(SectionEnterObserver);

            //信号を監視するタスクを起動
            Task.Run(async () => {
                var lastSignal = 0;
                ISectionD lastWatchSection = null;
                while (true) {
                    try {
                        MySignal = WatchSection.Signal;
                        //進んでいいならそのセクションを占有し、進入タイミングを監視する
                        if (lastSignal > 0 && MySignal > 0) {
                            if (WatchSection != null && WatchSection.Train == null) {// || WatchSection.Train == this) {
                                WatchSection.Train = this;
                            }
                        }
                        lastSignal = MySignal;
                        if (lastWatchSection != WatchSection) {
                            sectionEnteringUnSubscriber.Dispose();
                            sectionEnteringUnSubscriber = WatchSection.EnteringPublisher.Subscribe(SectionEnterObserver);
                            lastWatchSection = WatchSection;
                        }
                        await Task.Delay(10);
                    }
                    catch (Exception e) {
                        Console.WriteLine(e.Message);
                    }
                }
            });

            //信号の色によって速度を調整するタスクを起動
            const short LOW_LIMIT = 160;
            Task.Run(async () => {
                while (true) {
                    try {
                        if (SpeedSteps[MySignal] > SpeedValue) {
                            SpeedValue = (short)(SpeedValue + 15);
                            if (SpeedValue < LOW_LIMIT) { SpeedValue = LOW_LIMIT; }//下のほうはスキップする
                        }
                        else if (SpeedSteps[MySignal] < SpeedValue) {
                            SpeedValue = (short)(SpeedValue - 15);
                            if (SpeedValue < LOW_LIMIT) { SpeedValue = LOW_LIMIT; }
                        }
                        if (CurrSection != null) {

                            CurrSection.FeederValue = SpeedValue;
                        }
                        if (WatchSection != null && WatchSection.Train == this) {
                            WatchSection.FeederValue = SpeedValue;
                        }
                        if (ExitingSection != null) {
                            ExitingSection.FeederValue = SpeedValue;
                        }
                        await Task.Delay(100);
                    }
                    catch (Exception e) {
                        Console.WriteLine(e.Message);
                    }
                }
            });


        }

        private IDisposable sectionEnteringUnSubscriber = null;

        //新たなセクションに入ったときの処理(入ったセクションのIdを受け取る)
        public void SectionEnterObserver(ISectionD section) {
            //今のセクションのExistanceを監視して抜けたらセクションの占有を解除する
            var backsec = section.BackwardSection;//(CurrSection)
            var sw = new System.Diagnostics.Stopwatch();
            var disposable = new SingleAssignmentDisposable();
            disposable.Disposable = (backsec as SimpleSectionD).Feeder.ExistPublisher.Buffer(2).Subscribe((v) => {
                if (v[0] < 3 && v[1] < 3) {
                    backsec.Train = null;
                    disposable.Dispose();
                }
            });
            //新しく入ったセクションをカレントセクションとする
            CurrSection = WatchSection;
            //今入ったセクションの次のセクションの信号を監視しはじめる
            //ToDo 監視し始めた後にForwardSectionが切り替えられる(進路設定などで)ことがあるのでその対応
            WatchSection = section.ForwardSection;
        }
    }
}


この制御プログラムとレイアウトの間にはゾーンコントローラ上のArduinoのプログラムと、そのプログラムとUDPで通信するパソコン側のライブラリがあるのですが、それはまた改めて書きますね。

P.S.駅長も元気です、最近10歳になりました。