VisualStudioCodeでc#+MS-SQLのWebApplicationを開発する環境を作ったときのメモ-1(自分用)

Webアプリケーション(MVC)を.NET Coreで開発するために環境を構築したときのメモ(2020/5時点)。

経緯

まかない用で古い.NET Framework+初期のMVCで作ったWebアプリケーションを使っていたが、そのアプリが居候していた古いサーバーを撤去することになり、別のサーバーに引っ越す必要が生じた。が、引っ越し先のサーバーに古い.NetFrameworkを入れるわけにもいかないので、.NET Core3.1で作り直すことにした。開発現場ではないのでVSはExpress版しかなかったが、.NET CoreはExpress版では開発できないので、VisualStudio Codeを使うことにした。VisualStudio Codeを本格的に使うのは初めてなので、いろいろ躓いた(これからも躓く)が、せっかくなので記録に残すことにした。(あくまで個人的なメモであり無保証です)

用意したもの(開発環境)

  • VisualStudio Code
  • DotNetCore3.1
  • SQLExpress2019
  • SSMS 18.5
  • IIS Express(有効化する)

(開発用の端末はWIndows10。SQLExpress2019とSSMSは、他のWindows10端末にすでに入っているものに接続して使うことにした。)

VisualStudioCodeの設定

追加するパッケージ

  • c#
  • 追加する都度書き足す

他の端末からSQLExpressに接続できるようにする設定

開発用端末に開発用DBを置くならこれらの設定は不要。

  • DBの設定(DBのある端末上のSSMSから設定した)
    • DBサーバーのプロパティで認証方法を混合認証に切り替える
    • DBサーバーのプロパティでリモートからのログインを許可するように変更する
  • DBのある端末上のSqlServerConfigurationManagerで以下を設定する
    • TCP/IPによる通信を有効にする
    • TCP/IPのポートを固定する(SQLExpressをインストールするときに「既定のインスタンス」として入れた場合は1433になるようだが、普通に入れると「端末名\SQLExpress」など名前付きのインスタンスになる。その場合は、TCP/IPでの通信は無効になっていてポートも「動的」になっている。そこで、49152–65535の範囲で任意の(他と被らない)ポートに固定する(「動的」のまま設定できるかもしれないが、調べてない)。IPAllについて通信を有効化する(使いたいIPアドレスだけ有効にしてもダメだった)。
  • F/Wの設定。上であけたTCPのポート番号について、外部からの通信を許可する。netshコマンドかコントロールパネルの「セキュリティが強化されたWIndows Defender Firewallの設定」の画面からおこなう。

dotnetコマンドでプロジェクトを作って必要なパッケージを追加する手順

  • まずプロジェクトを作る コマンドプロンプトからプロジェクトのフォルダを作りたい場所に移動して
dotnet new mvc -n myApp

とすると、カレントフォルダ内にmyAppというフォルダが作成されてその下にプロジェクトのひな形ができる

  • VSCodeを起動して拡張機能のボタンを押してc#を探して入れる。 直接インストールできない環境の場合は、いったんダウンロードして、拡張機能タブ上部の...を押して、「vsixのインストール」でダウンロードしたファイルを選択して入れる(そもそもダウンロード先までたどり着けないときは、プロキシの設定が必要かもしれないので確認して設定する)
  • 「ファイル」>「フォルダを開く」で先ほど作ったフォルダを選択する
  • とりあえずF5を押して実行してみる launch.jsonとtask.jsonが自動生成されればそのまま実行でき、ブラウザでlocalhost:5000にアクセスするとhttpsの5001にリダイレクトされてWelcomeページが表示される。自動生成されないときは、以下の手順(2つ環境を作ったが1台は自動生成されなかった、謎)。

    • 自動生成されない場合はエラーが出て設定しろと言われるので、launch.jsonを開いて、プルダウンで.Net Coreを選んで、生成されたConfig内容ののprogramキーの値として、作成するアプリのdllへのフルパスの部分を書く。
    • 再度実行すると今度はtask.jsonを設定しろと言われるので、ConfigureTasksを押してデフォルトのまま追加する。これでもう一度実行すると起動出来た。
  • EFのパッケージを追加する(プロジェクトごとに必要)参考リンク

   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet tool install --global dotnet-ef
//「すでに入っている」と言われたときは念のため以下のコマンドで最新版に更新する
   dotnet tool update --global dotnet-ef
  • dotnet ef コマンドを使えるようにする
dotnet add package Microsoft.EntityFrameworkCore.Design
  • これも入れるらしいが、「指定された検索条件とパッケージ名 'Microsoft.EntityFrameworkCore.Tools' と一致するものが見つかりませんでした。」と言われたので保留 nugetパッケージをVS Code上で追加するときはdotnet add package ...で入れる
//これはnugetパッケージをVisual Studio上でインストールする場合↓
Install-Package Microsoft.EntityFrameworkCore.Tools
//VS Code上でnugetパッケージを入れるときはこのコマンドを使う↓
 dotnet add package Microsoft.EntityFrameworkCore.Tools

ここまで入れてdotnet ef と入力してみてロゴが表示されたのでとりあえずOKとした

既存のDBからクラスを生成(DbContext,Entity)し、エンティティを使ってCRUD画面を生成する。

  • スキャフォールディング用のツールを入れる
dotnet tool install -g dotnet-aspnet-codegenerator

これも必要?Microsoft.VisualStudio.Web.CodeGeneration.Design

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
  • DBコンテキスト用 > 上でインストールしたdotnet ef コマンドでおこなう

  • コマンド

    • スキーマからDbContext、EntityClassを生成するコマンド
dotnet ef dbcontext scaffold "server=x.x.x.x,port\sqlexpress;database=db名;Persist Security Info=True;User ID=xxx;Password=xxx" Microsoft.EntityFrameworkCore.Sqlserver -o Models 

これでプロジェクトにModelsフォルダが作られ、DbContextとエンティティクラスが追加される。 (DB側を更新した場合にちゃんと追随できるかは未検証)

dotnet aspnet-codegenerator controller -name XXXController -m XXX -dc XXXContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries

これでCRUDのコントローラーとビュー(cshtmlファイル)が生成される、が実行するとエラーになる。

error CS1061: 'TXXX' に 'Id' の定義が含まれておらず、型 'TXXX' の最初の引数 を受け付けるアクセス可能な拡張メソッド 'Id' が見つかりませんでした。

ジェネレータで生成されたエンティティクラスのIdに[Key]アトリビュートを追記したら解消した。

using System.ComponentModel.DataAnnotations;//←これを追加しないと[Key]が使えない
...
    public partial class TXXX
    {
        [Key]   //←これを追加
        public int Id { get; set; }
        ...

KeyAttributeを使うためにusingにusing System.ComponentModel.DataAnnotationsを書き足す必要がある。

  • サービスの登録と接続文字列の設定 DbContextをコントローラから使えるようにサービスへ登録する。startup.csに以下を書き足す
using Microsoft.EntityFrameworkCore;//これを追加
...
        public void ConfigureServices(IServiceCollection services){//この中に以下を追加
          ...
             services.AddDbContext<Models.TEST_DBContext>(optionsAction=>
                    optionsAction.UseSqlServer(Configuration.GetConnectionString("MyConnectionstring")));

スキャフォールディングした直後は接続文字列がDbContextに直書きなので、appsettings.jsonにConnectionStringsというセクションを追加してそこに移動する。たぶんここを読む必要あり(まだ読んでない)

{...//キーを追加して接続文字列を書く
  "ConnectionStrings":{
      "MyConnectionstring":"server=x.x.x.x,port\sqlexpress;database=db名;Persist Security Info=True;User ID=xxx;Password=xxx"  }
}

dotnet efのスキャフォールディングで生成されたDbContext内のOnConfiguringメソッド内のconnectionStringを設定している行(またはメソッドごと)を削除する。 たぶん入っているはずだけど、Configurationの参照がない場合は以下のパッケージをインストールする。

dotnet add package Microsoft.Extensions.Configuration

そろそろ編集が大変になってきたので続きは別エントリで

ESP8266でBトレ(第13回 アクセサリ編その1)PICで信号機を作るよ(1)

こんにちはけろけろおじさんです。あいかわらず全然進んでいないのですが、最近、おうちで電子工作することが国をあげて推奨されている(?)ので、ちょっとだけ進めました。といっても本線とはあまり関係ない部分ですが...

そのまえにちょっと前回の補足です

前回の記事にリンクを貼ってなかったのですが、簡単な自動運転(3列車でダブルホーム運転)をやったときのビデオをYoutubeに載せてあります。ビデオにテロップを入れるなどという高度な技が使えないのでここで解説しますね(全体のシステム構成などは前回以前の記事をご覧ください)。

youtu.be

  • エンドレス10閉塞+側線1の11閉塞でダブルホーム運転しています。
  • ごくシンプルに、次のルールで制御しています。
    1. 出発番線は交互に切り替える。切り替えタイミングは先発列車が出発区間を抜けたとき。
    2. 出発信号は次の条件で進行(または注意または減速)現示にする
      • 停車してから規程秒数経過した
      • 自分の番線が開通している
      • 上記の場合に、自区間の閉塞値を表示する
    3. 到着番線は交互に切り替える。到着番線は、駅から数えて第三コーナーの入口すぐ手前の直線線路に入ったときに決め、切り替えは、場内~自列車までの区間に列車がいなくなったときにおこなう

閉塞信号は自律的に動作するので、これだけのルールで交互発着が行えます。イメージとしては「傾斜のついた溝を転がるビー玉を、溝の分岐や合流の手前につけたゲートを操作して制御する」ような感じです。

制御側のPCでは、列車ごとに電圧-速度の関係を記述したパラメータを用意し、特性の違いを吸収しようとしているのですが、京急(赤い電車)は減速が効きすぎて場内区間を抜けきれずに止まってしまうことがあり、そのときは、後続列車が駅手前で信号待ちさせられています。

PICで信号機 ~方式検討

さてここからが今回の本題です。 ゾーンコントローラ方式をやってみようと思いついたとき、「信号機もゾーンコントローラから動かしたいな」と考えてました。また、信号機を作るなら、信号機ヘッド内に周辺回路も内蔵したいなと考えていました。両方とも、配線を削減したいがためです。ただ、ぼんやり考えていただけで、方法についてはノーアイデアでした。とりあえずゾーンコントローラにIOExpanderの8bit分を用意してはありましたが...。

どうやろうかな

  • 最初に考えたのはパラレル伝送方式です。これだと、4灯式1機あたり2本の信号線が必要なので、8bitでは4機分にしかなりません。また、信号機への配線も1機あたり4本(電源/GND/信号線×2)必要となり結構大げさです。レイアウトが配線であふれそうですね。さらに、信号機側のデコーダIC(74HCシリーズ)はフラットパッケージでも信号機のヘッドに内蔵できるほどは小さくありません。ですので、デコーダから信号機までは5本の配線が必要で大量生産には向きません。というわけでこの案はボツになりました。

    パラレル伝送
    パラレル伝送だと線の数が多いですね。

  • 次に、DCCを使うことを検討してみました。DCCなら一組2本の配線を引き回せばすみます。配線は途中で分岐させることもできますので、「ゾーンコントローラで信号も制御する」という目論見からははずれますが、電線のぐちゃぐちゃからは解放されます。DCCの送信側は既製品を使い、受信側はオープンソースのものを利用させてもらえば、コストもそれほどかからなさそうです。ですが、よく考えてみると信号を点けるためだけにDCC規格のデコーダを使うのはやや大げさな気がします(そもそもやりたいことに比べてDCCの仕様は多機能すぎで、中身を理解する前に挫折してしまいます)。また、デコーダに使用するチップも、ある程度の処理能力(といってもPIC12F程度?)が必要で、信号機ヘッドへの内蔵はむつかしく、デコーダから信号機ヘッドへの配線は相変わらず残ってしまいます。

  • そこで、「電源とシリアルデータ線を兼用する」というアイデアはDCCからいただきつつ、アクセサリ用に特化した軽量なオレオレプロトコルを使うことにしました。通信手順を単純にすることで、非力ながらもパッケージが小さい(SOT-23)のPIC10Fシリーズを受信側に使うことができます。SOT-23と1608サイズのLEDの組み合わせにより、信号機ヘッドと受信機を一体化しても、なんとかサマになる大きさに収まりそうで、そうすると、信号灯と制御部を結ぶ外付け配線が不要になり、レイアウトへの設置がラクチンになります。これで、すきなだけ信号機をおったてることができますね。送信側は、ゾーンコントローラ上でシリアルポート用に確保してあるGPIOがヒマ(となりのゾーンコントローラと位相同期をするとき以外は遊んでいる)なのでこれを使います。

    シリアル伝送方式
    シリアル伝送だと配線が節約できます

    PIC初体験

    というわけで、前に買ってあったPIC10F222を引っ張り出してきました。どうです、小さいでしょ。

    PIC10F222
    PIC10F222(SOT-23パッケージ)と1円玉
    PICは名前だけは前から知っていたのですが、使うのは初めてです。以前は無償の開発環境はアセンブラだったので、おじさんは手が出せなかったのですが、今は開発環境としてMPLAB X IDEがフリーで使え、これにはCコンパイラもついています。書き込みツールもPICKit3の中華コンパチ品がお手頃価格で買えますのでプログラム開始までのハードルはESP8266よりも低いくらいですね。

あれ、タイマー割り込みがない

コードを書き始めて分かったのですが、PIC10F222はタイマーは内蔵していますが、割り込みはかけられないので、シリアル受信をするにはちょっと面倒です。10F322ならタイマー割り込みが使えるようなので、おとなしくそっちを買いなおせばいいのですが、今回のプログラムは受信+Lチカだけなので、受信に全力を投入しても問題ありません。そこで、こちらなどを参考にさせていただきつつシリアル受信コードをCで書きました。

ピン足りなくない?

PIC10F222は6ピンです。電源/GND/シリアル入力で3pin使うので、出力に使えるのは3pinだけです。「あれ?4灯式以上の信号だとデコーダが必要なんじゃない?」と思うでしょ?(話の流れ上、思ってくださいね)。ところがどっこい、デコーダなしで間に合います。どうやるかというとIOピンの3ステート目(ハイインピーダンス)を使います。LEDは電球と違って点灯最低電圧以下だと全く点灯しませんので、図のような回路にしたうえで、電源電圧をLED1とLED2の最低点灯電圧の和以下にしておきます。この状態で、GPIOをハイインピーダンスにしてやるとLED1,LED2とも消灯するというわけです。この方法だと原理的には6灯式までいけます。

LED切り替え
0,1,Highインピーダンスで切り替える
論理表
GPIOの状態によりLEDがきりかわります

プロトコルについて

PIC10Fのクロック(8MHz)で無理なく処理できるように、1200bps(8ビット、パリティなし、ストップビット1)にしました。LEDの点灯電圧は1.7V程度なのでPICの動作電圧は3~3.2V程度を想定し、信号は3V程度で送るつもりです。が、これでノイズが問題になるようでしたら、高めの電圧で送って、信号機の根元で分圧するように変えるかもしれません。アドレス4bit+データ4bitとして、アドレスの一つはブロードキャスト用としたので、ゾーンコントローラ1台あたり15機の信号機をつなぐことができます。信号機だけならデータに4bitもいらないので今後気が変わるかもしれません。消費電流は実測で1機分あたり7mA程度でしたので、送信側はESP8266のGPIOそのままでは不足で、バスバッファをつける必要があります(そもそもCPUの信号線をそのまま外に出すようなことはしないですね)。

で、動くの?

ブレッドボードに2組分組んで、PCのUSBシリアルで動かしてみた動画を載せます。

ブレッドボード
2組(左:5灯式,右:4灯式)組みたててみました


PIC10F222で鉄道模型用の信号機を作ってみる(その1)

キーボードの入力に応じて色が変化してます。この例では0-3(ASCIIコードでいうと上位4バイトが0b0011)が右の4灯式向けのデータで@~C(〃0b0100)だと左の5灯式のデータになるようにしています(いつにもまして地味な動画ですね)。なんかLEDの明るさがまちまちだったり、Offでも完全に消えていないやつがいたりしますが、試作なのでご容赦ください。ちなみに電圧レベルの1,0とも点灯に使っているのため、通常のようなPWMによる明るさの調節はできません(1/High,0/Highの比率を変えるPWMを書けばいけそうですけど...)

完成イメージ

PCBデザインソフト(今回はKiCadを使ってみました)で作ったイメージです。本物の信号機はメーカーのサイト(PDFです)によると幅60cm弱なので、スケールでは約4mmですが、さすがに厳しいので2割オーバーの4.8mmでデザインしました。1608より小さい1206サイズのLEDを使えば(正面からのサイズは)スケールにできそうですが、老眼がつらいので今回はパスしました。基板の製造はSeeedStudioに発注しました。でも、小さすぎて外形カットはオーダーできないので、間隔を大きめにとって電源/データ線を結線した状態でレイアウトしています。パーツを取り付けて動作テストした後に切り出すつもりです。 信号機ヘッドの前面は液晶3Dプリンタで出力する予定ですが、こちらは基板ができてからそれに合わせて考えます。

課題など

今後解決する必要のある点がまだいくつかあります。

  • 量産するとなると(単品でも)手はんだではむつかしいので、リフローでやる必要がありそうですが、両面なのでどうしたもんか思案中です。SeeedStudioは部品の実装を含めて注文できるそうなので、PICのコードや部品の定数が決まっているのならこっちのほうがよさそうですね。今回はLEDの明るさを決めるRの値や、使うPICの型番、コードをいろいろ試したいので、ボートとメタルマスクの発注だけにしました。
  • 現状のコードでは、表示色を切り替えるときに今の色を一旦消灯し、ワンテンポおいて新たな色を点けるようにしていますが、横着してdelay関数を使ったので、この間シリアルデータの受信処理を行っていません。通常は同じ信号機に連続して指示を送信することはないはずですが、ブロードキャスト(発報信号などを想定)を取りこぼす恐れがあります。ちゃんと書きなおせばいいのでしょうが、京成や京急フリッカー信号も表示したいななどと考えると、素直にPIC10F322を使うのが早そうです(ピン配置は同じなので簡単に試せそうです)。
  • 踏切などもこれで動かせたらいいなと思っているのですが、そうすると、踏切表示灯(線路脇にあって、踏切が閉じると点くやつ)用や、障害物検知のデータを返すための3本目の線が必要ですね。そこまでやるならポイントの制御もこの通信方式でおこなえば、配線やゾーンコントローラ側の回路が簡単にできそうです。

回路図とか(暫定)コードとか

一応回路図(5灯式)と暫定版のコード(〃)を載せておきます(コードは一番下に載せました)。データと電源は兼用なので電源用にはダイオードを挟んでコンデンサを入れています。信号は「0」のときに電圧がかっています(232Cそのまま)。LEDの抵抗をHigh側とLow側で兼用にしたり、信号電圧がVddよりダイオードの分だけ高かったりしてますが、狭いところに配置するためにパーツを減らした結果ですので見逃してください。

回路図
回路図

本線についての近況など

ここのところずっと停滞していて、いつになったら開業するのか、やってる本人にもワカラナイのですが、現況は次のような感じです。

  • ゾーンコントローラを追加で作成して全部で8台になりましたので、数の面からはPoint to Pointの小規模な鉄道路線が実現できそうです(はんだ付けが修行のようでした。これ以上必要になるようなら追加分は周辺回路をCPLD化します)。
  • コントロールボードについては今までSVG+jQuery+SignalRでやってきましたが、昨年末あたりにBlazorというのが正式リリース(ServerSideのほう)されたので試してみたところ、用途に適していそうでしたので、今後はこれで書こうと思っています。従来のWebシステムは、サーバー側と画面側を別々に作って、この間のデータの受け渡しはプログラマの責任で結構原始的な方法によっていたのですが、(Server side)Blazorで(プッシュに)Rxなどを使えば、サーバーと画面の通信が双方向とも型安全にc#で書けます。IDEでピリオドを打つのに頼り切っているおじさんは大喜びです。で、今後はそっちで進めることにしました(Blazor流行るといいな...)。
    ゾーンコントローラ8台
    修行の成果です...

コード

/*
 * File:   newmain.c
 * Author: KERO
 *
 * Created on 2020/04/26, 21:43
 */
// CONFIG

#pragma config IOSCFS = 8MHZ    // Internal Oscillator Frequency Select bit (8 MHz)
#pragma config MCPU = ON        // Master Clear Pull-up Enable bit (Pull-up enabled)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled)
#pragma config CP = OFF         // Code protection bit (Code protection off)
#pragma config MCLRE = OFF      // GP3/MCLR Pin Function Select bit (GP3/MCLR pin function is digital I/O, MCLR internally tied to VDD)

#define _XTAL_FREQ 8000000
#define MY_ADDR 0b0101 //0b0011 0-3  0b0100 @-D

#include <xc.h>
#include "newmain.h"
void main(void) {
    ADCON0bits.ANS0=0;//use pin5(1) as GPIO0
    ADCON0bits.ANS1=0;//use pin4(3) as GPIO1
    OSCCALbits.FOSC4=0;//use pin3(4) as GPIO2
    TMR0=0x00;
    OPTION=0b10011001;//(bit0-2 TMR0 divide 1:4 (bit5 use pin5(1) as GPIO0)
    TRISGPIO=0b1000;//bit3は入力用 bit2.H:R bit1.H:G bit0.H:Y0/L:Y1/
    TestBlink();

    unsigned char bitsRead=0;
    unsigned char buf=0;
    unsigned char status=0;//0:idle 1:prepare 2:reading 
    unsigned char cnt=0;
    unsigned char chkstart=0;
    while(1){
        //9600の8分周のとき:170
        if(TMR0>170){
            TMR0=0;
            cnt++;
            if(status==0){//待機状態
                if(GPIObits.GP3==0){//アイドル状態のとき0になれば
                    status=1;//開始準備状態へ遷移
                    cnt=0;//カウンタリセット(4サイクル待つ用)
                    chkstart=0;//スタートビット確認用カウンタをリセット
                }
            }
            else if(status==1){//開始準備状態
                    if(GPIObits.GP3==0){
                        chkstart++;
                        if(chkstart==4){//4回続けて0だったらスタートビットとみなす
                            buf=0;
                            bitsRead=0;
                            status=2;//読込中状態へ遷移
                            cnt=0;//カウンタリセット(8サイクル待つ用)
                        }
                    }
                    else{
                        status=0;//
                    }
                //}
            }
            else if(status==2){//読込中状態
                if(cnt==8){
                    if(bitsRead<8){
                        unsigned char v=GPIObits.GP3;//負論理なのであとで反転すること
                        unsigned char w=v<<bitsRead;
                        buf=buf | w;
                    }
                    else if(bitsRead==8){
                        /* 格納データチェック
                        for(unsigned char a=0;a<8 ;a++){
                            unsigned char b=0;
                            b=buf & 0b1<<(7-a);
                            GPIObits.GP2=(b>0);
                                GPIObits.GP1=(!(b>0));
                                __delay_ms(400);
                                GPIO=0b00000000;
                                __delay_ms(100);
                        }
                        */
                        SetSignal(buf);
                        buf=0;
                    }
                    else if(bitsRead>8){//Stopビット読込
                        status=0;//待機状態へ遷移
                    }
                    bitsRead++;
                    cnt=0;//カウンタリセット
                }
            }
        }
    }
    return;
}
inline void Off(){
    TRISGPIO=0b1101;//all high
    GPIO=0b0000;
}
inline void TestBlink(){
    Red();
    __delay_ms(500);
    Off();
    __delay_ms(100);
    YellowYellow();
    __delay_ms(500);
    Off();
    __delay_ms(100);    
    Yellow();
    __delay_ms(500);
    Off();
    __delay_ms(100);
    YellowGreen();
    __delay_ms(500);
    Off();
    __delay_ms(100);
    Green();
    __delay_ms(500);
    Off();
    __delay_ms(100);
    Red();
}
inline void GYR(){
        TRISGPIO=0b1000;//bit0-2を出力に
        GPIO=0b0110;//
}
inline void Red(){
        TRISGPIO=0b1101;//bit0,2をハイインピーダンスに
        GPIO=0b1111;//
}
inline void YellowYellow(){
        TRISGPIO=0b1000;//
        GPIO=0b1101;//
}
inline void Yellow(){
        TRISGPIO=0b1001;//bit0をハイインピーダンスに
        GPIO=0b1101;//
}
inline void YellowGreen(){
        TRISGPIO=0b1000;//
        GPIO=0b1000;// 
}
inline void Green(){
        TRISGPIO=0b1001;//bit0をハイインピーダンスに
        GPIO=0b1001;//bit1=Low G点灯
}
inline void SetSignal(unsigned char buf){
    unsigned b;
    b=(buf & 0b11110000)>>4;
    if(b== 0b00000000){Red();return;}//ブロードキャスト(とりあえず全部赤)
    else if(b!=MY_ADDR){return;}
    else{ //自分宛なら色を設定する
        Off();
        __delay_ms(150);//仮(ブロックしないようにループ内でチェックするように変える)
        b=buf & 0b00001111;//下位2ビットのみ使用(現示用に下位4ビット割り当てのうち)
        if(b==0b00000100){Green();}
        else if(b==0b00000011){YellowGreen();}
        else if(b==0b00000010){Yellow();}
        else if(b==0b00000001){YellowYellow();}
        else {Red();}
    }
    return;
}

ESP8266でBトレ(第12回 ソフトウェア編その3)レイアウトプロキシの機能追加と、WebUIの作成

ちょっとだけ駅長気分が味わえました(^^)

こんにちはけろけろおじさんです またまた前回から半年以上たってしまいましたが、最近少し進んだのでその報告です。

前回記事を書いたのは秋でしたが、そのあと今まで冬眠していたわけではなく、3Dプリンタにはまったり、日曜大工に精を出したりしていたのでした。 脱線ばかりしているようですが、おじさんが的にはこれらはお互いに無関係なわけではなくて、最終的には全てがつながり、伏線もすべて回収され、遠大なストーリーが完成する予定なのです。(ひょっとすると全部中途半端に終わる可能性もあります。なにしろおじさんのおもちゃ箱には、数十年来の仕掛品が地層のように積み重なっているので...(これらも全部伏線?))

今回は何?

今回の概要は以下のようなものです。最近電子工作がないので、ちょっと物足りない感じですね。

  • テストのたびに毎回配線するのが面倒なので、実験線を作りました。ただし以前に作りかけていた実験線からはかなり規模を縮小して、エンドレス+待避線だけのものです。
  • レイアウトプロキシに場内信号、出発信号、およびルート設定機能を追加しました。これは自動運転のための準備です。
  • 実験専用にWebUIを作成しました。これまではプロキシ類のテストや操作はWindowsFormアプリケーションでおこなっていましたが、実験専用のものはWeb+SignalRで作りました。

新実験線

机の上に都度広げていると毎回結構手間なので、木枠を組んだ上にベニヤ板をねじ止めして実験線用地とし、ユニトラックを敷設しました。エンドレスの内側にはゾーンコントローラーを置くためのくぼみを作って、ここにゾーンコントローラーをスペーサーを介してねじ止めしてあります。セクション数はエンドレスに10+待避線1で11です。ゾーンコントローラーは2台使用しています(くぼみはあと2台分あります)。

f:id:cacao1:20190625211816j:plain
新実験線
レールとゾーンコントローラーとの配線はコネクタを介して接続しています。フィーダー一か所につき、ゾーンコントローラ-アダプタケーブル間、アダプタケーブル-延長ケーブル間、延長ケーブル-フィーダー間と3つもコネクタがあります。コネクタと言っても、ピンヘッダ、ピンソケット利用の自作品です。安価にできてわりとしっかりつながるのですが、数が多いと外れる可能性も高まりますので、つないだ後でマスキングテープを巻きました。
f:id:cacao1:20190625211438j:plain
ピンヘッダ・ソケット利用のコネクタ
配線後に通電したところ、配線同士の隣接のしかたによっては車両がいないのに誤検出するケースがありました。フィーダー線の誘導障害のようです(が詳しくは追ってません)。ACアダプタからの線や、異なるゾーンコントローラからのフィーダー同士はなるべく違づけないようにして回避しました。
f:id:cacao1:20190625212008j:plain
新実験線(裏面)
レールは今のところ固定していません。フィーダー線とポイントマシンの線が合計で13本もあるので、固定しなくても線に支えられてなんとなくとまっています(ボード全体を立てかけても大丈夫です)。各配線はベニヤ板に穴をあけて通していますが、穴にコルク片をつめてずれにくいように固定しています。

板の上に線路をおくと、次はホームを作ったり駅舎を建てたりしたくなりますが、そっち方面に脱線すると帰ってこられなくなるのでぐっと我慢します。

レイアウトプロキシの機能追加

前回は閉塞信号しかなかったのですが、今回、場内信号と出発信号、それに進路設定の機能(※)を追加しました。これらを制御することにより、列車の入線順や出発順の制御が可能になります(前回は「前が空いている限り前進する」という機能しかありませんでした)。これで「画面をプチプチすれば、個々の列車を運転しなくても、車両を思うように走らせることができる」ようになりました。運転指令とまではいきませんが、駅長気分が味わえます。

(※)「進路設定って言ってもただのポイント切り替えじゃん」と思われるかもしれませんが、実はポイント切り替えに応じてセクションのつながりの定義も切り替えています。というか、セクション間のつながりの定義を変更するのに合わせて、その通りに進めるようにポイントを切り替えています。場内からホームまでに複数のポイントがある場合にはこの考え方がよさげです。

コントロールボード

どうも腰が上がらなくてずっと着手していなかったのですが、ようやくコントロールボードのWebアプリ(の先行試作品)を作りました。Web界隈は技術の移り変わりが激しくて、おじさんは全くフォローできていないのですが、現時点(2019年前半)でc#で作るなら、.NetCoreでRx+React(with TypeScript)+SignalRくらいが向いているような気がします。ですが、今回は.NetFramework4.7でRx+jQuery+SignalRで作りました。おじさんがReactを使えないせいでもありますが(そもそも開発環境の構築で躓いてしまいます)、特定のレイアウト専用にゴリゴリハードコーディングするならjQuerySVGエレメントを直接いじるのが簡単です(jQuerySVG対応が進んで、HTML要素とほぼ同じように扱えるようになっているようです)。「『なるべく汎用的に作る』という当初の目標はどこへ?」というセルフツッコミもありますが、「とにかく動くものを一回作ってみる」というのも必要です。でないと何をどうコンポーネント化していいかもわかりませんしね(半分くらい言い訳です...)。というわけで、サーバー側もレイアウトプロキシから上はゴリゴリです。

f:id:cacao1:20190625212612p:plain
コントロールボード(間違い(初期化漏れ)が一か所ありますね^^;)
レイアウトプロキシとコントロールボードWebの接続についてですが、以前やった在線検知の動作確認のとき(第3回のとき)は、Web側とレイアウト側を別アプリにして、間をプロセス間通信でつなげたのですが、今回はWebアプリ内のタスクとしてレイアウトプロキシ以下のもろもろの処理を起動しています。安普請な感じがしないでもないですが、画面側とレイアウトプロキシ側がお互いにRxで呼び合えるお手軽さに負けてこういう作りになりました。

で、動くの?

というわけで、動作させてみます。画面上でセクションの境界付近にある丸いのが信号です。ポイントの下にある丸いのは進路選択ボタンで、その下の平たいボタンは場内、出発の許可ボタンです。場内・出発信号は、閉塞の値とこの許可ボタンの値の論理積になります。

まずは一列車をエンドレスで走行させてみます。


EPS8266でBトレを動かしてみた(その5(の1)コントロールボード)

各セクションの信号の値を4灯式の色で表しています。列車通過後の閉塞信号の変化の様子がわかりますね。

次に2列車です。


ESP8266でBトレを動かしてみた(その5(の2) 閉塞運転)

先行列車が出発を抜けたあと進路を切り替えると後続列車が出発します。場内信号を赤に変えると駅の手前で停車します。後続列車は閉塞信号をみて減速します...が、寸止めのつもりが減速しきれずに追突してますね。

次に3列車です。


ESP8266でBトレを動かしてみた(その5(の3) 3列車同時運転)

10閉塞で3列車で信号が4灯式なので、ほぼすべての区間で減速以下の信号になってます。 ダブルホーム運用をすればスムースに流れると思いますが、おじさんがやると場内を停止現示にするのをわすれて後続が同じ番線に入ってきたり、出発信号を停止にし忘れて駅を通過されたり、あげくは列車が出発セクションを抜けきる前に進路を切り替えてしまって脱線させてしまい、そこへ後発列車が突っ込んだりとさんざんです。信号を停止定位にしたり、鎖錠機能をつけたりしないとダメですね。

今後について

次は(ダイヤ運転の手前の)自動運転をやってるつもりです。今回、場内・出発信号と進路選択を手で触って動かしてみて感じたのですが、自動運転では「個々の列車を制御する」よりも、「駅にやって来る列車・駅から出ていく列車の入出場を制御する」のがよさそうです、駅長の役割のタスク化ですね。

ESP8266でBトレ(第11回 ソフトウェア編その2)ZoneController基板のソフトと、対向するサーバー側のソフト

ZoneController(基板)のソフトウェアとパソコン側(ZcProxy)のソフトについて

おじさんが考えているシステムの全体構成はこうなっていますが、 f:id:cacao1:20151128235918p:plain レイヤ(層)でみるとこうなっています。 f:id:cacao1:20181023222757p:plain 上半分と下半分は、ちょうど鏡に映したような関係になっています。 で、前回は下の図のレイアウトとLayoutProxyの部分のはなしを少し書きました。 今回はその内側のZoneController(基板)とZcProxyの部分について書きます。

ZoneController(基板)側

ZoneControllerのソフトウェアはArduinoで書きました。ただ、Arduinoのメインループにいろいろ書くと、一画面に入らない処理が理解できないおじさんには、あとで保守できなくなるのが見えていたので、フィーダーやスイッチはクラスにして外に出しました。ですのでメインループはこれだけ↓になりました。

//ZoneController(基板)のArduinoのメインループ
void loop() {//メインループ
    if (!AppConfig.isConfigMode()) { loop1(); }//通常モード
    else { AppConfig.process(); }//設定モード
}

void loop1(){//通常モード
    long ctime = millis();
    if (ctime - ms >= 150) {
        //Serial.print("Exec...");
        //Serial.println(ctime);
        String cmds = recieveCmds();
        yield();
        if (isCmds4me(cmds)) {
            String cmdsBody = getCmdsBody(cmds);
            SynchronizerA::DelivCmds(cmdsBody);
            FeederA::DelivCmds(cmdsBody);
            SwitchA::DelivCmds(cmdsBody);
        }
        else {
            Serial.println("not for me " + cmds.substring(20));
        }
        FeederA::ExecCmds();
        String results0 = FeederA::gatherResults();
        String results1 = FeederA::gatherExistence();
        String results2 = SwitchA::gatherResults();
        String results3 = SynchronizerA::gatherResults();
        String result = results0 + results1 + results2 +results3;
        yield();
        ms = ctime;
        long t = millis() - ms;
        //Serial.println(t);
        int v = analogRead(A0);
        //Serial.print("v=");
        //Serial.println(v);
        sendResults(result);
    }
    yield();
}

サーバーとの通信方式はUDPにしました。当初はTCPで、WebSocket>Http>生ソケットと順に試したのですが、ESP8266側でどうしても不定期に遅延が発生してしまいます。ライブラリの中身はおじさんのスキルではとても追えません。困りましたが、考えてみたら状態値はサーバーへ送り返すつもりでした。ですので、このレイヤで到達保証がなくても、返り値をチェックして指定した状態になるまで繰り返し送り続ければ用は足せます。そこで、TCPはやめてUDPにしました。 UDPだと軽快に動きます。が、反面、送信元の偽装もたやすいので、ジャックされるリスクはあります。

ZoneControllerの処理周期は約150msとしています。150msのうちに次の処理をします。

  • コマンド文字列の受信
  • 受信したコマンド文字列を同期処理、フィーダー、スイッチマシン処理クラスへ配布
  • フィーダークラスへ配布したコマンドの実行(方向とDutyの設定)
  • フィーダー、スイッチマシン、の処理結果文字列の収集、在線情報の収集
  • スイッチマシン用コンデンサの充電電圧の取得
  • 返送する結果文字列の組み立て
  • 結果文字列の送信

位相同期やスイッチマシンの切り替えはループの中では行わずに割込み処理の中で行っています。

ZcProxy(サーバー)側

次にこれと対向するサーバー(パソコン)側ソフトです。 サーバー一台で複数のZoneController(基板)を相手にします。従来型のクライアント-サーバー方式的な発想だと、接続相手ごとにスレッドを起こして各スレッドが1台のクライアント(Zoncontroller)を相手にすることになります。仮にZoneControllerが20台あるとすると、サーバー側で20スレッドが必要になります。最近のパソコンなら問題ないとも言えますが、クライアントの数だけスレッドを起こすなんて、ちょっと手法的に残念な感じがします。そこで、現代的にTaskを使うことにしました(※)。Taskは「小さい処理のカタマリを記述するだけで、環境に内蔵されたスケジューラがいい具合に空いているスレッドを使って実行し、別のところで待っていれば実行結果を教えてくれる」というものです(おじさんの理解はこの程度です)。昔はこんなのなかったのですが、便利な世の中になりましたね。

で、構成は次のようになっています。まず、プログラムを起動すると、最初にZoneController(基板)に対応するゾーンコントローラクラスのインスタンスを生成します(ここは設定ファイルを読み込みそのとおりインスタンス化します)。その後、プログラムは、生成されたインスタンスが内包しているフィーダーとスイッチなどの状態値(LastSendValueという名前にしました)を読みだして一つ文字列に編集し、自分の担当するZoneController(基板)に向けて送信する動作を始めます(これはTask)。同様に、ZoneControllerから返ってくる現在の値の文字列をインスタンスのフィーダーやスイッチのLastRespValueに格納する動作もはじめます(これもTask)。送信は300msごと、受信は受信の都度(=ZoneController側の送信間隔の150ms)おこないます(送信はもう少し詰めるつもりです)。 このような構造にすると、サーバー側の上位プログラム(前回のレイアウトプロキシのような)からは、ZoneControllerのハードウェアを直接操作せずとも、ZoneControllerクラスのインスタンスが内包するフィーダーなどのプロパティ(LastSendValue,LastRespValue)を読み書きすれば物理的なゾーンコントローラの状態の取得・設定が行えます(読み出しについてはプロパティの他にRxでのイベント通知もおこなうようにしました)。 というわけで、このライブラリはZoneControllerのプロキシという意味でZcProxyという名前にしました。

(※)なんていいつつ、Taskの中で、"Thread.Sleep(10)"とか書いていたのは内緒です(これだとタスクが自身を実行中のスレッドをブロックしてしまいますので、Taskにした意味がないですね)。

//サーバー側(ZcProxy)の送受信部分
        public static void StartSend() {
            IsSendCanceled = false;
            ZcProxy.Zcs.AsParallel().Select(z => {//Zcごとにタスクを起動する
                var uc = new UdpClient();
                Task.Run(async() => {
                    while (!IsSendCanceled) {
                        if (z.remoteEP == null)
                        {
                            await Task.Delay(200);
                            continue;
                        }
                        var s = z.Id + ':' + z.GatherCmds4Snd();
                        byte[] buf = Encoding.ASCII.GetBytes(s);
                        uc.Connect(z.remoteEP);
                        uc.Send(buf, buf.Length);
                        await Task.Delay(300);
                    }
                    Console.WriteLine("Send Canceled "+z.Id);
                });
                return "";
            }).ToList();
        }

        public static void StartListen() {
            IsListenCanceled = false;
            Task.Run(async () => {
                using (var udpClient = new UdpClient(2012)) {
                    while (!IsListenCanceled) {
                        var rcvResult = await udpClient.ReceiveAsync();
                        string rcvMsg = Encoding.UTF8.GetString(rcvResult.Buffer);
                        var ms = rcvMsg.Split(':');
                        var zc = GetZcByRcvResult(rcvResult.RemoteEndPoint, ms[0]);
                        try {
                            var msg = ms[1];//ex:kf0-s30,kf0-d0....
                            zc.SetRespString(msg);
                        }
                        catch (Exception ex) {
                            Console.Write(".");
                            Console.WriteLine(ex.ToString());
                            Console.WriteLine(rcvMsg);
                        }
                    }
                    Console.WriteLine("Listen Canceled ");

                }
            });
        }

位相同期の顛末

位相同期がまだ調整中ということを第8回で書きましたが、その後解決したので、その報告を書いておきますね(あれから2か所直しました)。

フィーダー電圧検出時のマスク期間の延長

PWMがOffになってから100usでモータードライバの出力がOffになるというBD6231Fの仕様に従ってワンショットマルチで100usだけGPIOのOn期間を延長し、それをフィーダーからの信号のマスクに使っていましたが、設計上で100usピッタリにするとマスクしきれないことがあって(素子に誤差があることを考えれば当たり前ですよね(汗))、意図しない割込みが大量に発生していました。そこで時定数を決めているコンデンサを増量(並列に追加)し、マスクの期間を延ばしたところ、不要な割込みが発生しなくなり安定動作するようになりました。

タイミング調整不可パターンでのタイミングずらし

第5回でいうところの「あなただけがいる」が成立→不成立に変化するケースには2パターンがありますが、このうち、「私」がその原因である場合はどれだけタイミングをずらせばいいか判断できません。従来はこのケースでは何もせずにスルーしていましたが、それだと短時間のうちには同期できませんので、この場合には無条件に90usタイミングを遅らせるようにしました。こうすれば、理屈のうえでは、最大10回(900us)くらい繰り返すうちにどこかでタイミングがはかれるケースに当たり、同期できます。(在線検知のために最低でもduty10%以上で電圧を印加しているため1000usは待たなくていい)

void SynchronizerA::syncB(){
    //同期するかどうかの判定
    FeederA *fd = getFeederByChannel(Channel);
    short targetPort = fd->getAnalogPort();
    short targetCh = fd->getId();
    if (targetCh > 3) { return; }//0-3ch以外なら処理しない
    short duty = fd->getAnalogValue();
    if (duty > 500) { return; }//高速走行中なら同期しない(しなくても目立たないので)


    //同期開始
    detachInterrupt(1);//再入を防ぐため割り込みをやめる
    int isRAISING = digitalRead(1) == 1;
    if (isRAISING) {//あなただけがいる状態から あなたと私がいる状態、誰もいない状態)になるケース
        if (digitalRead(targetPort) == HIGH) {//現在の私のPWM出力がH(あなただけだったところに私が現れたケース)
            up++;
            timer1_disable();
            delayMicroseconds(90);
            timer1_write(1);
            timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE);
        }
        else {//現在のPWM出力がL(あなただけがいる状態からあなたが去ったケース ★この状態変化を利用する(次にあなたが現れるタイミングに自分を合わせる))
            upA++;
            timer1_disable();
            int duty1024 = static_cast<double>(duty / 1.024);
            delayMicroseconds(1000 - duty1024 - 300 + adj);//実行時にadjに100程度を設定する()
            timer1_write(1);
            timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE);
        }
    }
    else {//FALLING
        if (digitalRead(targetPort) == LOW) {//誰もいないところにあなたが現れたケースまたは2人いる状態で私が去ったケース ??この状態変化を利用できる?)
            dn++;
        }
        else {//ここには来ない(自分がいる場合はかならず条件を満たさないのでNOT ONLYYOUがLOWではないのでFALLINGではこの条件にはいらない)
        }
    }
label1:
    attachInterrupt(1, syncB, CHANGE);//割り込みを許可する

}

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歳になりました。

番外編「ESP8266を使っておかんがエアコンをつけるようにするシステムをつくる」の巻

ゾーンコントローラがやっと3台になったので、今度こそサーバー側のソフトを書こうと思っていたのですが、またもや脱線です、毎回すみません。

今度はなに?

おじさんの実家では母親が一人暮らししているのですが、昔の人なのでなかなかエアコンをつけなくて心配です。電話で話しても「暑くないんだもん」などと言うので困ります。年寄りは感覚が鈍っていて知らないうちに熱中症になるっていうのに... そこで、「暑いを見える化」し「だからエアコンつけてね」と説得力をもって言えるような仕組みを導入することにしました。

しくみ

ゾーンコントローラで使っているESP-WROOM-02をIoT的に使います(こっちがESP-WROOM-02の普通の使い方かもしれません)。温度湿度センサーで得た情報をインターネット上のサービスに投げて可視化し、おかんが使っているiPadとおじさんのパソコンの両方で表示します。でもって「ほら、こんなに暑いでしょ」と言えば、納得してエアコンをつけるんじゃないかという目論見です。

f:id:cacao1:20160710132224j:plain:w400

機械のしくみ

ESP-WROOM-02と温度湿度センサHDC1000を使います。IoTぽくするならプロトコルにMQTTなどを使ってセンサー側とサーバー側を作りこむのでしょうが、センサーからデータを投げるだけならともかく、投げた結果をグラフ化するのにはサーバー側に表示の仕組みを用意する必要があり、おじさんのペースで作っていると、きっと出来上がる前に夏が終わってしまいます。困ったなと思いつつネットを探していると、なんと、IoT用のサーバー機能のほかに可視化までしてくれるすてきなサービス"Ambient"がありました。Ambientには、ESP8266用にはAPIのライブラリもあり、わずか2,3行書くだけでデータを投げることができます。さらにすごいことに、サーバー側では設定画面をポチポチするだけでグラフ化できます。すごすぎですね。詳細はリンク先(作者の方のページです)をご覧ください。

f:id:cacao1:20160710133825j:plain

こんなグラフが簡単に作れてパスワードさえ覚えていればどこからでも参照できます。

作ったもの

HDC1000を使った回路とソースはAmbientのドキュメントページにあるので(おじさんは知らずにHDC1000にしたのですが、どこまでいたれりつくせりなんでしょう)、サービスを利用するならそのままでいいのですが、そこまで人様に依存してしまうと脳が退化しておかんより先に自分がぼけてしまいますので、I2C接続のLCDと、アラーム用にLEDを追加ました(といっても相変わらず人様からのコピペです)。また、ビルドしなおさなくてもWiFiルータの設定や情報を投げる先のキーなどを変更できるよう、設定モードを設けましたので、そのモードに入るための押ボタンスイッチもつけました(この機能はゾーンコントローラからの流用です)。I2C液晶のドライバはオレ工房さんのソースを利用させていただきました。 小さいケースに収めるため、ESP-WROOM-02に直接リード線をはんだ付けして両面スルーホールの基板取り付けたり、チップ抵抗を使ったりしてみました。 ソースはGitに置きました。

f:id:cacao1:20160710133324j:plain

一応、回路図です(これだけですが)。

f:id:cacao1:20160710134024j:plain:w400

チップ抵抗使いました(写真の黒い小さいの)。このくらいのサイズなら手はんだでも何とかなります。

f:id:cacao1:20160710134243j:plain:w400

おもて面 変換基板なしだと小さく収まります。

動作

約5秒ごとに温度・湿度と不快指数を切り替えて液晶表示するようにしました。不快指数が設定した値を超えると赤色LEDが点灯するようにしました。Ambientへは設定した時間ごとに温度・湿度・不快指数を送信するようにしました。

f:id:cacao1:20160710134444j:plain:w400

できました。箱に入れるなんて久しぶり

メモ

  1. HDC1000モジュールのセンサー部はケースの外へつき出すように配置しましたが、ケースにほかに開口部がないと排熱がここを通り測定値が不正確になりそうです。私が作ったものはLEDや電源コネクタ部に大きな穴が開いているので大丈夫なようです。
  2. MicroUSBのACアダプタがたくさん余っているのでこれを降圧して使ったのですが、550mAのものでは起動せず750mAのものにしたら動きました。550mAのACアダプタがヘタっていただけかもしれませんが(無負荷だと電圧が出ているのですが)、動かないときは電源も疑ってみてください。。
  3. 当初は半日ちょっとでハングアップしていたのですが、HDC1000から読み込む処理と液晶に出力する処理の間にDelayを入れたら連続運転できるようになりました。
  4. I2Cのプルアップ抵抗はHDC1000に内蔵されていますが、試験のときなどにHDC1000を外すこともある(った)ので、別にプルアップ抵抗を入れておきました。
  5. いつも使っているラッピングワイヤのほかにポリウレタン銅線を使ってみたのですが、20Wのハンダごてでは被覆がなかなか溶けないようです。被覆に少し傷をつけてからハンダに浸けるとポリウレタンの溶ける臭い?がしてくっつきます。

設置してみた結果

来週お盆に帰ったときに設置してみます。ちゃんと動いたら利用結果を報告します。

2016/08/22追記

だいぶ遅くなってしまいましたが、結果報告です。 自宅で試験しているときは平気だったのですが、実家に持ち帰って動かしたら、ESP8266が暖かくなってしまいました。試験環境よりWiFiの電波が弱いせいでしょうか?そのため、実際の温度より1.5℃~2.0℃くらい高めの測定値がでてしまいます。おかんには「あてにならないわね」などと言われてしまいましたが、気にしているということは伝わったようで、昨年よりはエアコンを使っているようです。ですので、とりえあず目的は果たせました(^^;;。また、上記設計ミスで測定値がずれるほかは、ESP8266+Ambientの組み合わせで1か月以上快調に動作し続けています。

f:id:cacao1:20160821223822j:plain:w600

実験線進捗状況 用地取得が完了しました。

ESP8266でBトレ(第9回 ハードウェア編その9)先行試作版完成?

3月以降少し忙しくて中断していましたが、先月からぼちぼち再開しました。まだまだ続きますがスローペースで申し訳ないです。

今回の内容

5月~6月上旬にかけては次のようなことをやりましたのでその報告です。

(図や写真などはあとで追加します)

ハードウェアの落穂ひろい

早くPC側ソフトウェアを書き始めたいのですが、それにはハードがちゃんと動く必要があります。前回まででだいたい動くことまで確認したのですが、位相同期の動作確認と昇圧回路の組み込みと動作確認がまだでした。ですのでこのあたりをやりました。

ソフトウェアの作成

今までゾーンコントローラと通信するパソコン側ソフトはコマンドラインで操作するものでしたが(検証だけはWebサーバーまで連動させてみましたが)、これをもとにして本格的に試すにための拡張をするには、つくりに難がありました。また、複数枚のゾーンコントローラへの対応も考慮していませんでした。そこで、複数枚のゾーンコントローラに対応するパソコン側ソフトの作成と、それに合わせたコマンド体系(ソフトウェアレベルのプロトコル)を定義し、あわせてゾーンコントローラ側ソフトも改修(というか作り直し)をしました。

ハードウェアの落穂ひろい

位相同期機能

どうせ一発では動かないだろうなという予感がしていたのですが、やっぱり動きませんでした。上流側から順にオシロのプローブをあてていったところ、検出信号の論理が逆でした。インバータがいらないようにコンパレータの+と-入れ替えたのに、ロジック側を書くときにそれを忘れてました。そこを直したら、今度は信号を引き延ばすためのワンショットマルチの出力部分で、notQを取り出すべきところをQを取り出していたがわかりました。というわけで、プリント基板オーダー初体験の次は、さっそくパターンカットとジャンパの引き回しを初体験しました。

これで回路的には設計どおりなったので、調整しようと思っていた部分にとりかかりました。課題は2つあり、一つはフィーダーの境界をまたぐときに割り込みが数十回以上発生すること、もう一つは、同期している側のフィーダー内を動力車が走るとノイズ(逆起電力か空走中の発電電圧?)でパラパラと割り込みが発生する点です。ですが、前者は特に悪影響はないようでしたので今のところ未対応です。後者は、走りに影響するのでちゃんと対策したかったのですが、うまいアイデアが思いつきませんでしたので、コンパレータの基準電圧側を半固定抵抗にして、PWMのON電圧以下かつ逆起電力や発電電圧以上のところに閾値が来るように調整してお茶を濁しました。これでは、動力車の特性によって調整が必要になりそうですが...。

昇圧回路

6VのACアダプタを使うことにしたため、ポイントマシン用の12Vは昇圧回路で生成することにしました。基板の端に信号用にと思って残してあったユニバーサル基板状の部分に昇圧回路を組み込みます。FETへのスイッチング信号供給も昇圧回路のON/OFF判定もCPUでやればよさそうですが、プログラムミスなどで出力が過電圧になったり電源を短絡したりすると困るので、両方とも回路を組みました。ただ、十分に充電されたことを確認してからポイント転換を行うために、出力側コンデンサの電圧を分圧したものをESP8266のTOUTに引き込んA/D変換して使用しています(タイマーなどで見当をつけるよりスマートでしょ?)。これでESP8266の全ピンを完食しました。

ソフトウェアの作成

ゾーンコントローラ側のソフトウェア(開発環境)

ZoneControllerのソフトはArduinoで書きました。Arduinoは今回初めて使ったのですが、loop()とsetup()と割り込みルーチンに処理を書けばいいのでわかりやすくていいですね。開発環境はVisualStudio2015にVisualMicroのプラグインを入れて使っています(どちらも無料版です)。ZoneControllerは大きなプログラムではありませんが、このくらいの規模でもArduinoIDEよりはかなり楽にコーディングできます。このブログの一番古いエントリに導入方法などをまとめてありますので、興味のある方はご覧ください。 ソフト本体については次回以降に書こうと思いますが、次のような点が特徴といえば特徴です。

  1. 機能をクラス化して外出しし、Arduinoでは骨格だけを書きました。
  2. 起動時のピン(GPIO14)の状態を見て通常モードか設定モードのいずれかで立ち上がるようにし、設定モードの場合はシリアルで対話入力した値をEEP-ROMに設定を書き込むようにしました。

サーバー側ソフトウェア

サーバー側はC#で書きました。開発環境はこちらもVisualStudio2015(無料版)です。プログラム本体についてですが、これまではコンソールと通信が一体化した掘立小屋みたいなコードでしたが、今回は少し役割分担を考えて作り始めました。これまたさわりだけ説明を書くと次のような感じです。

  1. ゾーンコントローラとの通信部分だけをクラスライブラリにしました。
  2. 通信方式はUDPとし、データの到達保証はアプリケーションレイヤで考えることにしました。
  3. クラスライブラリは、アプリケーション側からはゾーンコントローラのプロキシとして見えるようにしました(ハードウェアを意識しなくてもいいようにしました)。
  4. クラスライブラリとゾーンコントローラハードウェアのテストのために簡単なWindowsFormsアプリケーションを作りました。

これからの予定

6月中旬以降、ZoneController方式での鉄道運行が可能かどうかを検証するために、ZoneControllerの量産(といっても4、5台ですが)にとりかかってます。最初の1台は楽しかったですが、2台目は普通でした。これから作る3台め以降のことを考えるとちょっと気が重いです。電車の模型を作るときの窓抜き作業みたいな気分です。あと、レールにフィーダーをつけるのとフィーダやポイントのケーブルに端子をはんだ付けするのも考えるとウンザリしますね。昔は全然平気だったのに...