俺のラズパイ3で.NET CoreなF#が動かないわけがない

と、思っていましたが、動きません orz.

dotnet/coreclr:
https://github.com/dotnet/coreclr/
dotnet/cli:
https://github.com/dotnet/cli

.NET Coreをビルドしようと思うと、

な感じで拒否られるし、じゃあ、dotnet コマンドだけでもビルドできないかと思うと、

20160831_06

な感じで、前回ビルド済みの dotnet コマンドを要求されるし、どうにもなりません。

Problem install .Net Core on Raspberry Pi Model B ・ Issue #140 ・ dotnet/core
https://github.com/dotnet/core/issues/140
How to use dotnet cli in debian linux on arm. ・ Issue #3197 ・ dotnet/cli
https://github.com/dotnet/cli/issues/3197

を見る限り、Raspberry Pi で使われてる ARMv7 ではビルドできませんという具合です。.NET CoreのターゲットとしてARMは入っているので、いずれ対応するのかという雰囲気だけはあるんですが、進展はしているのかは不明。

でも、元 dnvm は動いていたよね?

しかし、Raspberry Pi2 に Windows IoT Core がインストールできるわけだし、最初から ARMv7 が開発ターゲットから外されているというのも変名は話なので、いつくか遡ってみると、

Raspberry Pi 2 にインストールした Windows 10 IoT Core 上で DNX と ASP.NET 5 アプリケーションを動かしてみた – しばやん雑記
http://blog.shibayan.jp/entry/20150629/1435546231
How to run .NET Core Application on Pi2 – Win10 IoT Core? – Damir Dobric Posts – developers.de
http://developers.de/blogs/damir_dobric/archive/2015/09/22/how-to-run-net-core-application-on-pi2-win10-iot-core.aspx

を読むと、dotnet コマンドの前身の dnvm コマンドでは Raspberry Pi で(Windows IoT Core限定カモ)動いていた模様です。
「模様」ですというのは、現在、ARMのランタイムを取ろうとすると、エラーが発生します。

dnvm install latest -r coreclr -arch x64 は通るけど、
dnvm install latest -r coreclr -arch ARM は通らなくなっているので、

サーバー自体から消えてしまった感じ。うーん、古いバイナリでよいので残してほしかったのですが、ここが OSS の辛いところ。

ややこしい、3つの Core な関係

でもって、やりたいことは、F# な Web APIアプリをラズパイ上で動かしたいだけなので、別に mono でもいいんだよな、と思い直して .NET Core なところをあれこれと弄っていこうとすると、

– .NET Core なランタイム
– ASP.NET Core
– ASP.NET Core MVC

な、ややこしい関係にぶち当たります。

素直に、.NET Core 環境だけで作るとか、素直に .NET Framework 4.6 な環境で作るとかすればよいのですが、じゃあ、dotnet コマンドは何の環境で動いているのかとか、ASP.NET Core MVC は内部で何がうごいているのかとかとか考えるとややこしいのです。

msbuild に代わるビルド環境としての dotnet コマンドは、dotnet restore してアセンブリを NuGet で取ってきて dotnet run でプロジェクトを実行できます。プロジェクトがある場所に、project.json があって、その中に設定が書いてあるわけですが、この dotnet コマンドが、.NET Core 上なのか .NET Framework 上なのか mono 上なのかが謎です。ただ、dotnet/cli を取って来て Raspi 上でビルドができない(Ubuntu上ではできた)ので、動作はともかくとして、ビルドするのに「動作する dotnet」が必要です。ブートストラップ的になっているの、卵と鶏の関係になっています。

mono 上で動く dotnet コマンドがあればいいのに、と思うんだけど、それがないところみると、たぶん .NET Core 上で動いているんですよね、おそらく。

ASP.NET Core はオープンソースな https://github.com/aspnet/ で開発が進められていますが、内部的に .NET Core 必須なのか?というとそうではなくて、

を見ると、

– ASP.NET Web Application(.NET Frameowork)
– ASP.NET Core Web Application(.NET Core)
– ASP.NET Core Web Application(.NET Framework)

の3種類のASP.NET MVCプロジェクトが作れます。

最初のASP.NETアプリは、従来のASP.NET MVC5とかを作るプロジェクトで .NET Frameworkを使うやつです。
次の、(.NET Core)のほうは、.NET Coreを使っているほうで、dotnet runとかで実行ができるやつです。IISがいらないのでLinuxに移すことができますよね。
じゃあ、最後の「ASP.NET Core Web Application(.NET Framework)」ってのは何でしょう?
.NET Framework上で動く、ASP.NET Core って何?ASP.NET Coreの「Core」って、.NET Coreの「Core」じゃなかったの?と思う訳ですが、どうやら、この2つの Core は違う意味とだということが分かります。どうやら、.NET Framworkを使って、IISのいらない exe ファイルを作って実行することができるですよね。

bin/Dbug/net452/win7-x64 フォルダーにある WebApplication1.exe を実行すると、普通に ASP.NET Core MVC アプリケーションが動きます。

このバイナリをそのまま Raspberry pi に持って行って、mono で動かすと libuv のアセンブリがなくて動きません。Raspberry pi 上に libuv はインストールしたのですが、Linux の動的ライブラリ *.so を探しているかどうかは不明です。

どうやら、ASP.NET MVC のほうは、IIS 上で動いて、
ASP.NET Core MVC のほうは

– Kestrel
– libuv

が肝だということが薄々わかってきました。Kestrel は Webサーバーで、https://github.com/aspnet/KestrelHttpServer からダウンロードが可能です。ただし、これをビルドするときに dotnet コマンドが必要なので、ラズパイ上では頓挫しています。「ASP.NET Core Web Application(.NET Framework)」のプロジェクトが、Kestrel を使っているので、.NET Core 自体は必要ないはずなんですよね(ASP.NET Coreしか必要ない)。
libuv のほうは、Node.js でも使われているクロスプラットフォームな TCP/IP ライブラリです。こっちのほうは、https://github.com/libuv/libuv からダウンロードしてラズパイ上でもビルドができます。

ということは、「dotnet コマンドを使わないでビルドができる Kestrel を .NET Framework 版でビルドすれば、mono 上で動く」ので、ラズパイ上で動くようになるのでは?と思いついたのですがどうでせふ?

オレオレASP.NET Core MVCを作成する

ひとまず、HttpListenerを使ったら mono でも動くので、Raspberry Pi 上に F# な Web APIサーバーが立てられるのではと思って、ちまちまと考えていたり。

type WebHostBuilder() as self =
    let mutable _sv = new System.Net.HttpListener()

    member x.UseStartup() = 
        self
    member x.UseUrls(url) = 
        _sv.Prefixes.Add( url )
        self
    member x.Build() = 
        ()
    member x.Run() =
        if _sv.Prefixes.Count = 0 then
            _sv.Prefixes.Add("http://localhost:5000/")
        _sv.Start()
        while true do
            let cont = _sv.GetContext()
            Console.WriteLine("url: {0}", cont.Request.Url)
            let sw = new System.IO.StreamWriter(cont.Response.OutputStream)
            sw.WriteLine("OK")
            sw.Close()
            cont.Response.Close()
        _sv.Stop()

[<EntryPoint>]
let main argv = 
    printfn "start oreoreHttpServer"

    let host = new WebHostBuilder()
    host.UseStartup()
        .UseUrls("http://+:5000/")
        .Build()
    host.Run()
    0

なんとなく、インターフェースだけ Kestrel に似せています。

カテゴリー: ASP.NET, F#, RaspberryPi | 俺のラズパイ3で.NET CoreなF#が動かないわけがない はコメントを受け付けていません

F#でASP.NET CoreのWeb APIを作ろう(SQLite編)

F#でASP.NET CoreのWeb APIを作ろう | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/7996

の続きで、データベースにSQLiteを使ってWeb APIを作成します。
.NET Core用のSQLite は NuGet で「”Microsoft.EntityFrameworkCore.Sqlite”」があるので、これを使えばokです。
C#の.NET CoreのWeb APIプロジェクトからコピーしたあとから、再スタート。

project.jsonの修正

– “includeFiles” にF#のファイルをひとつずつ追加(面倒だけど)

– “dependencies” に4つの参照をを追加
– “Microsoft.EntityFrameworkCore”
– “Microsoft.EntityFrameworkCore.Sqlite”
– “Microsoft.EntityFrameworkCore.Sqlite.Design”
– “Microsoft.EntityFrameworkCore.Tools”

C#のときは “Microsoft.EntityFrameworkCore” が無くても動くのだけど、F#のプロジェクトには入れます。でも、入れるのが普通な気がする。

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true,
    "debugType": "portable",
    "compilerName": "fsc",
    "compile": {
      "includeFiles": [
        "Models/Person.fs",
        "Data/ApplicationDbContext.fs",
        "Controllers/ValuesController.fs",
        "Controllers/PeopleController.fs",
        "Startup.fs",
        "Program.fs"
      ]
    }
  },
  "tools": {
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final",
    "dotnet-compile-fsc":"1.0.0-preview2-*"
  },

  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ],
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.0"
        },
        "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160629"
      }
    }
  },
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.0.0",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Mvc": "1.0.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
    "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
    "Microsoft.Extensions.Configuration.Json": "1.0.0",
    "Microsoft.Extensions.Logging": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Extensions.Logging.Debug": "1.0.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
    "Microsoft.EntityFrameworkCore": "1.0.0",
    "Microsoft.EntityFrameworkCore.Sqlite": "1.0.0",
    "Microsoft.EntityFrameworkCore.Sqlite.Design": "1.0.0",
    "Microsoft.EntityFrameworkCore.Tools":  "1.0.0-preview2-final"
  },

  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },

  "publishOptions": {
    "include": [
      "wwwroot",
      "Views",
      "Areas/**/Views",
      "appsettings.json",
      "web.config"
    ]
  },

  "scripts": {
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

データベース接続の設定

appsettings.json に “DefaultConnection” を追加。ローカルファイルにSQLiteのデータベースを置きます。

appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Filename=./sample.db"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

StartupクラスのConfigureServicesメソッドでSQLiteに接続できるようにする。

type Startup(env:IHostingEnvironment) as this =
    do 
        let builder = new ConfigurationBuilder()
        builder.SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", true, true)
                .AddJsonFile("appsettings." + env.EnvironmentName + ".json", true)
                .AddEnvironmentVariables() |> ignore
        this.Configuration <- builder.Build()

    [<DefaultValue>]
    val mutable _Configuration:IConfigurationRoot 
    member val Configuration:IConfigurationRoot = null with get, set
    member x.ConfigureServices( services:IServiceCollection ) =
        services.AddDbContext<ApplicationDbContext>( 
            fun options ->
                options.UseSqlite(this._Configuration.GetConnectionString("DefaultConnection")) |> ignore
                ) |> ignore
        services.AddMvc() |> ignore
        ()

    member x.Configure( app:IApplicationBuilder, env:IHostingEnvironment, loggerFactory:ILoggerFactory ) =
        loggerFactory.AddConsole(this._Configuration.GetSection("Logging")) |> ignore
        loggerFactory.AddDebug() |> ignore
        app.UseMvc() |> ignore
        ()

Modelの作成

シンプルに自動生成のプロパティを使えます。

Person.cs

type Person() =
    member val Id = 0 with get, set
    member val Name = "" with get, set
    member val Age = 0 with get, set

ApplicationDbContextの作成

namespace ApiFSharp.Data
open Microsoft.EntityFrameworkCore
open ApiFSharp.Models

type ApplicationDbContext( options:DbContextOptions<ApplicationDbContext> ) =
    inherit DbContext( options )
    [<DefaultValue>] 
    val mutable _Person:DbSet<Person> 
    member x.Person with get() = x._Person and set value = x._Person <- value
    override x.OnModelCreating( builder:ModelBuilder ) =
        base.OnModelCreating(builder)
        ()

実は、「member x.Person with …」のところで随分悩みました。
自己参照のつもりで、this を追加して this._Person で参照させると実行時に、循環参照のエラーがでます。

type ApplicationDbContext( options:DbContextOptions<ApplicationDbContext> ) as this=
    inherit DbContext( options )
    [<DefaultValue>] 
    val mutable _Person:DbSet<Person> 
    member x.Person with get() = this._Person and set value = this._Person <- value

http://localhost:5000/api/people で、Web APIにアクセスしようとすると、System.InvalidOperationException の例外が発生しています。

Hosting environment: Production
Content root path: D:workblogsrcSQLiteWebApiSQLiteWebApiSQLiteFSharp
Now listening on: http://*:5000
Application started. Press Ctrl+C to shut down.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:5000/api/people
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HKUCHBOFLSDB": An unhandled exception was thrown by the application.
System.InvalidOperationException: The initialization of an object or value resulted in an object or value being accessed
 recursively before it was fully initialized.
   at Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicFunctions.FailInit()
   at ApiFSharp.Data.ApplicationDbContext.set_Person(DbSet`1 value)
   at Microsoft.EntityFrameworkCore.Internal.DbSetInitializer.InitializeSets(DbContext context)
   at Microsoft.EntityFrameworkCore.DbContext..ctor(DbContextOptions options)
   at ApiFSharp.Data.ApplicationDbContext..ctor(DbContextOptions`1 options)
...

これを this 使わずに

type ApplicationDbContext( options:DbContextOptions<ApplicationDbContext> ) =
    inherit DbContext( options )
    [<DefaultValue>] 
    val mutable _Person:DbSet<Person> 
    member x.Person with get() = x._Person and set value = x._Person <- value

とすると循環しないので、なんか理由があるんですかね?謎です。

Controllerの作成

まめに ignore を追加します。

[<Route("api/[controller]")>]
type PeopleController(context) =
    inherit Controller() 
    let _context:ApplicationDbContext = context

    [<HttpGet>]
    member x.Get () = 
        if isNull _context.Person then  
            new List<Person>()
        else
             _context.Person.ToList()
    [<HttpGet("{id}")>]
    member x.Get (id:int) = 
        _context.Person.SingleOrDefault( fun m -> m.Id = id )
    [<HttpPost>]
    member x.Post([<FromBody>] person:Person) =
        _context.Add( person ) |> ignore
        _context.SaveChanges() |> ignore
        person.Id
    [<HttpPut("{id}")>]
    member x.Put( id:int, [<FromBody>] person:Person) =
        if id <> person.Id then
            -1
        else 
            _context.Update( person ) |> ignore
            _context.SaveChanges() |> ignore
            person.Id
    [<HttpDelete("{id}")>]
    member x.Delete( id:int ) =
        let person = _context.Person.SingleOrDefault( fun m -> m.Id = id )
        _context.Person.Remove( person ) |> ignore
        _context.SaveChanges() |> ignore
        person.Id

ビルドして実行

実行するとこんな感じです。データベースのファイル「sample.db」は自動では作ってくれないので、何らかの形であらかじめファイルを作っておきます。

サンプルコード

サンプルコードはこちら。C#版も含めてあります。
https://1drv.ms/u/s!AmXmBbuizQkXgfwM4A7bz3cWyThD_Q

次は…

あれこれ追加が必要ですが、ASP.NET Coreな環境で、F#でWeb APIが作れることがわかりました。本当はASP.NET MVCにしたいけど、ViewページのRazorが対応していないので無理かな。このあたりは後で調べます。
開発環境は Visual Studio Codeを使う(というか、むしろ Visual Studio 2015上では開発できない)ので、LinuxはMac上でも開発&動作ができます。IISとかApacheとかに関係なく、マイクロサーバー的にWeb APIを提供できる環境ができるのはよいかもしれません。Azureに乗せたりすると、自前のなんかのプロキシとか作れるんじゃないですかね。いや、C#で書いたほうが楽かもしれませんが、もうちょっとなんか整理して、Controllerだけさくさくと実験&拡張できるようにすると、手軽なWeb APIが作れるかも。

カテゴリー: ASP.NET, F#, SQLite | F#でASP.NET CoreのWeb APIを作ろう(SQLite編) はコメントを受け付けていません

ASP.NET Core MVC で SQLite を使う

.NET Core上でF#とSQLiteを結び付ける前哨戦として、ASP.NET Core MVC で SQLite を使ってみます。
方法としては、Visual Studio で「ASP.NET Core Web Applicaiton(.NET Core)」は、SQL Serverを使う設定になっているので、これをSQLiteに切り替えるだけです。

NuGetでSQLiteをインストール

– Microsoft.EntityFrameworkCore.SQLite
– Microsoft.EntityFrameworkCore.SQLite.Desgin

の2つをインストールします。Desginのほうは、データベースファーストのほうで使うけど、一応インストール。

SQLiteの設定に切り替え

StartupクラスのConfigureServicesメソッドで、UseSqlServerの箇所をUseSqliteに切り替え。

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
    // Add framework services.
    services.AddMvc();
}

appsettings.jsonを開いて、DefaultConnectionの値をSQLiteが使うファイル名にする。

appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Filename=./sample.db"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

Modelクラスを作って、スキャフォールディング

あとは、SQL Serverのときと同じで、ModelクラスになるPersonクラスを使って、スキャフォールディングを実行。

namespace SampleSQLiteMvc.Models
{
    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

マイグレーションとデータベースのアップデートをすれば完了です。

dotnet ef migrations add init
dotnet ef database update

実行

実行するとこんな感じに、SQL Serverのときと同じように動きます。

肝は、NuGet で Microsoft.EntityFrameworkCore.SQLite を入れて、Startupクラスで接続先を変えるだけなので、それほど難しくありません。

サンプルコード

サンプルコードはこちら
https://1drv.ms/u/s!AmXmBbuizQkXgfwL7jS1NrKxv8KbQg

カテゴリー: ASP.NET, SQLite | ASP.NET Core MVC で SQLite を使う はコメントを受け付けていません

F#でASP.NET CoreのWeb APIを作ろう

なぜ、F#で作るのかはさておき、C#のWeb APIアプリをF#にコンバートしてみます。本当のところは、ASP.NET Core MVCのほうをF#に対応させてみたかったのですが、Razor構文がF#に(たぶん)対応していないので無理ってことで、ViewページのないWeb APIで試しています。まあ、F#の関数型なところがステートレスなHTTPプロトコルに合っているんじゃないか、常々思ってはいるのですが。

いわゆる、ASP.NET Core Web Application(.NET Core) をF#に直します。

単純なWebアプリのほうは、いくつかサンプルが見つかるのですが、Web APIのコンバートは見つからなかったので参考になると思います。先行きは、SQLiteを使って、データベースのアクセスまで作りたいところ。

Visual Studio Codeの準備

実は、F#の.NET Coreは、Visual Studio 2015は対応していません。コードのコンバートなのでちまちまとテキストエディタを使ってもよいのですが、さすがにインテリセンスがないとF#のコードは組みづらい。

Visual Studio Code
https://www.visualstudio.com/ja-jp/products/code-vs.aspx

を使うと、.NET CoreのF#でもインテリセンスが使えるようになります。ちなみに、LinuxとMacでもF#の.NET Core環境が使えます。

Visual Studio Codeをダウンロード&インストールして「表示」→「Extentions(拡張)」から、「Ionide-fsharp」をインストールします。
Windows版の場合は、Ionide-fsharpだけでよいのですが、LinuxとMacの場合は別途 mono もインストールします。拡張機能が mono を使っているからだと思うのだけど、そのうち .NET Coreに統一されるかもしれません。

インストール後に「有効」にして、VSCode を再起動すると、F#のコードでインテリセンスが使えるようになります。

C#の.NET CoreのWeb APIプロジェクトからコピー

Visual Studio 2015で.NET CoreのWeb APIプロジェクトを作って、C#のコード(Startup.cs、Program.cs、ValuesController.cs)以外のファイルをごっそりコピーします。

project.jsonの修正

コンソール版のF#コードを「dotnet new –lang F#」で作成して、2つのproject.jsonをうまくマージします。
F#の場合は、ビルドするときに前方参照が必要なので、”buildOptions” – “compile” – “includeFiles” にビルドする順番ファイル名を書いておきます。これが非常に面倒なんですが…先行きはどうなんでしょう?

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true,
    "debugType": "portable",
    "compilerName": "fsc",
    "compile": {
      "includeFiles": [
        "Controllers/ValuesController.fs",
        "Startup.fs",
        "Program.fs"
      ]
    }
  },
  "tools": {
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final",
    "dotnet-compile-fsc":"1.0.0-preview2-*"
  },
  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ],
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.0"
        },
        "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160629"
      }
    }
  },
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.0.0",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Mvc": "1.0.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
    "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
    "Microsoft.Extensions.Configuration.Json": "1.0.0",
    "Microsoft.Extensions.Logging": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Extensions.Logging.Debug": "1.0.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0"
  },

  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },

  "publishOptions": {
    "include": [
      "wwwroot",
      "Views",
      "Areas/**/Views",
      "appsettings.json",
      "web.config"
    ]
  },

  "scripts": {
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

コードをF#に作り変える

3つのC#のファイルをF#にコンバートします。
大体、1対1でコンバートができるのですが、F#のほうが型チェックが厳しいので、戻り値が void になる関数はまめに ignore していきます。戻り値が推論されて、実行時にメソッドが見つからなくてエラーになるのがハマりどころです。

Startup.cs

namespace ApiFSharp

open System
open System.Collections.Generic
open System.Linq
open System.Threading.Tasks
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Logging

type Startup(env:IHostingEnvironment) =
    let mutable _Configuration:IConfigurationRoot = null
    do 
        let builder = new ConfigurationBuilder()
        builder.SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", true, true)
                .AddJsonFile("appsettings." + env.EnvironmentName + ".json", true)
                .AddEnvironmentVariables() |> ignore
        _Configuration <- builder.Build()
    member x.Cnfiguration 
        with get() =  _Configuration 
        and set( value ) = _Configuration <- value
    member x.ConfigureServices( services:IServiceCollection ) =
        services.AddMvc() |> ignore
        ()
    member x.Configure( app:IApplicationBuilder, env:IHostingEnvironment, loggerFactory:ILoggerFactory ) =
        loggerFactory.AddConsole( _Configuration.GetSection("Logging")) |> ignore
        loggerFactory.AddDebug() |> ignore
        app.UseMvc() |> ignore
        ()

Web APIをどのPCからも受け付けられるように UseUrlsメソッドで「”http://*:5000″」のように追加しておきます。こうすると、Linux仮想マシンでWeb APIを動かして、PCのブラウザでチェックする、ということができます。

Program.fs

open System
open System.Collections.Generic
open System.IO
open System.Linq
open System.Threading.Tasks
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Builder
open ApiFSharp

[<EntryPoint>]
let Main argv = 
    let host = new WebHostBuilder()
    let ihost = 
        host.UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>()
            .UseUrls("http://*:5000")
            .Build()
    ihost.Run()
    0

Startup.cs と Program.fs は定番の処理なので、Web APIのプロジェクトテンプレートを作ってしまえばよいと思います。
実際に手を入れていくのは、ValuesControllerクラスです。

ValuesController.cs

namespace ApiFSharp.Controllers 

open System
open System.Collections.Generic
open System.Linq
open System.Threading.Tasks
open Microsoft.AspNetCore.Mvc

[<Route("api/[controller]")>]
type ValuesController() =
    inherit Controller() 

    [<HttpGet>]
    member x.Get () = 
        ["value1"; "value2" ]
    [<HttpGet("{id}")>]
    member x.Get (id:int) = 
        "value"
    [<HttpPost>]
    member x.Post([<FromBody>] value:string) =
        ()
    [<HttpPut("{id}")>]
    member x.Put( id:int, [<FromBody>] value:string) =
        ()
    [<HttpDelete("{id}")>]
    member x.Delete( id:int ) =
        ()

データベースにアクセスしていないので、固定文字列しか返していないけど、SQLite を使ってデータアクセスを作れば結構使えるようになるかなと。

ビルドして実行する

リストアして、ビルドして、実行します。

dotnet restore
dotnet build
dotnet run

VSCodeで、Shift+Ctrl+B を押すとビルドができます。

実行する

PowerShell で dotnet run してブラウザで http://localhost:5000/api/values にアクセスします。

ちなみに Ubuntu 上の VSCode を使って F# プログラミングはこんな感じ。

固定の文字列を返すだけとか、計算するだけだとふつうに HttpListener を使って返すだけでも十分だろうという気もしますが、データベースアクセス付け加えたり、認証とかクッキーとかを使ってアクセス制限を考えたり、Controllerクラスのテストなどを考えると、ASP.NET Core を利用した Web API を F# で作るのもアリかな…という無理矢理な発想で :)

サンプルコード

サンプルコードはこちら
https://1drv.ms/u/s!AmXmBbuizQkXgfwK1qbhxJOHVP0bxQ

カテゴリー: ASP.NET, F# | F#でASP.NET CoreのWeb APIを作ろう はコメントを受け付けていません

SQLite で LINQ を使う

と或るところで、さらっと答えてしまったら間違っていたので、仕切り直しがてらに進呈致します。どうやら、昔、どこかでオンメモリの設定で使っていてその知識がそのままだったようです。ファイルベースで動いていますね、SQLiteは。後述しますが、接続文字列のところで「DataSource=sample.db」のようにファイル名ではなくて「DataSource=:memory:」にするとオンメモリのDBとして動作します。

System.Data.SQLite を使う

SQLiteの本家サイトから落としてもいいのですが、手っ取り早く NuGet で落とします。

.NET Core 用には System.Data.SQLite.Core があるのと、Windows IoT Core のような UWP アプリで内部でデータベースを扱うのに SQLite 一択となるので、一度使ってみるといいかもしれません。NuGet には「Microsoft.Data.SQLite」もあるので、多少乱立ぎみなのかも。

SQLiteにテーブルを作る

実験用に users テーブルを作ります。SQL文は、一度 PupSQLite で作ったものから吐き出しています。

Pup’s Atelier-Software
https://www.eonet.ne.jp/~pup/software.html

オンメモリにする場合は、コネクションをクローズするたびに無くなってしまう(揮発性)なので、アプリが起動したときにコネクションを開いて、終わるときに閉じるという具合でしょう。

{
    string sql = @"
CREATE TABLE [users] (
[id] VARCHAR(256),
[username] VARCHAR(256) NOT NULL,
[email] VARCHAR(256),
[birthday] DATETIME,
[age] INTEGER,
[memo1] VARCHAR(256),
[memo2] VARCHAR(256),
[memo3] VARCHAR(256),
[memo4] VARCHAR(256),
[memo5] VARCHAR(256),
[memo6] VARCHAR(256),
[memo7] VARCHAR(256),
[memo8] VARCHAR(256),
[memo9] VARCHAR(256),
[memo10] VARCHAR(256),
PRIMARY KEY(id)
);";
    var cn = new SQLiteConnection("DataSource=" + db_file);
    var cmd = new SQLiteCommand(sql, cn);
    cn.Open();
    cmd.ExecuteNonQuery();
    cn.Close();
    textMsg.Text = "Users テーブルを作成しました";
}

Memo1 から Memo10 のデータは、ファイルを巨大にするためのダミーデータ用です。このテーブルで作ると10万件で、500MB ぐらいのファイルができあがります。

Userクラスを作る

LINQ で使えるように Entity クラスを作ります。
カラムの型は指定しなくても、うまくマッピングできています。Table属性とColumn属性のNameを指定すればokです。

using System.Data.Linq.Mapping;

[Table(Name ="users")]
class User
{
    [Column(Name ="id", IsPrimaryKey = true )]
    public string Id { get; set; }
    [Column(Name = "username", CanBeNull = false)]
    public string UserName { get; set; }
    [Column(Name = "email",CanBeNull = true)]
    public string Email { get; set; }
    [Column(Name = "birthday", CanBeNull = true)]
    public DateTime? Birthday { get; set; }
    [Column(Name = "age" ,CanBeNull = true)]
    public int? Age { get; set; }

    [Column(Name = "memo1")]
    public string Memo1 { get; set; }
    [Column(Name = "memo2")]
    public string Memo2 { get; set; }
    [Column(Name = "memo3")]
    public string Memo3 { get; set; }
    [Column(Name = "memo4")]
    public string Memo4 { get; set; }
    [Column(Name = "memo5")]
    public string Memo5 { get; set; }
    [Column(Name = "memo6")]
    public string Memo6 { get; set; }
    [Column(Name = "memo7")]
    public string Memo7 { get; set; }
    [Column(Name = "memo8")]
    public string Memo8 { get; set; }
    [Column(Name = "memo9")]
    public string Memo9 { get; set; }
    [Column(Name = "memo10")]
    public string Memo10 { get; set; }
}

検索用の LINQ

System.Data.Linqを参照設定しておいて、DataContextクラスを使います。テーブルを参照するときは、GetTableメソッドで指定のテーブルを取得。

private void clickCount(object sender, RoutedEventArgs e)
{
    var sw = new Stopwatch();
    sw.Start();
    var cn = new SQLiteConnection("DataSource=" + db_file);
    var context = new DataContext(cn);
    var users = context.GetTable&lt;User&gt;();
    int count = users.Count();

    textMsg.Text = $"{count} 件のデータがあります";
    textTime.Text = $"{sw.ElapsedMilliseconds} msec";
}

すると、LINQ が使えるようになるので、where文などを使って条件を指定できるようになります。

private void clickAge(object sender, RoutedEventArgs e)
{
    var sw = new Stopwatch();
    sw.Start();
    var cn = new SQLiteConnection("DataSource=" + db_file);
    var context = new DataContext(cn);
    var users = context.GetTable&lt;User&gt;();
    var q = from t in users
            where 40 &lt;= t.Age &amp;&amp; t.Age &lt; 50
            select t;
    var count = q.Count();
    sw.Stop();
    textMsg.Text = $"{count} 件のデータがあります";
    textTime.Text = $"{sw.ElapsedMilliseconds} msec";
}

10万件のデータを検索したところ(インデックスなし)、300 mesc ちょっとで返って来ます。

メモリの具合はどうかというと、Working set の private が 30 MB 程度。このとき sample.db ファイルの大きさは 500MB 程度あるので、ファイルアクセスをしています。年齢で検索するために clickAge を呼び出した瞬間に HDD アクセスが大量発生してます。

となると検索スピードは HDD/SSD のスピードに依るということですね。

データ作成

データ作成をする INSERT は意外と遅いです。ロジックが多少複雑だというのもあるんでしょうが、1000件作るのに7秒程度かかります。オンメモリにすると 300msec 程度なので insert スピードが 20倍ぐらい違います。

private void clickCreateData(object sender, RoutedEventArgs e)
{
    var cn = new SQLiteConnection("DataSource=" + db_file); //  ";SyncMode=off;JournalMode=Memory");
    var context = new DataContext(cn);
    var users = context.GetTable&lt;User&gt;();
    _rnd = new Random();
    int max = int.Parse(textSize.Text);
    var sw = new Stopwatch();
    sw.Start();
    for ( int i=1; i&lt;=max; i++ )
    {
        users.InsertOnSubmit(makeUser());
        if ( i % 100 == 0 )
        {
            context.SubmitChanges();
            Debug.WriteLine($"{i} 件 挿入...");
        }
    }
    context.SubmitChanges();
    sw.Stop();
    textMsg.Text = $"{max} 件のデータを挿入しました";
    textTime.Text = $"{sw.ElapsedMilliseconds} msec";
}

private User makeUser()
{
    var user = new User();
    user.Id = Guid.NewGuid().ToString("D");
    user.UserName = createName();
    user.Email = createEmail(user.UserName);
    user.Birthday = null;
    user.Age = _rnd.Next(10, 100);
    user.Memo1 = createMemo();
    user.Memo2 = createMemo();
    user.Memo3 = createMemo();
    user.Memo4 = createMemo();
    user.Memo5 = createMemo();
    user.Memo6 = createMemo();
    user.Memo7 = createMemo();
    user.Memo8 = createMemo();
    user.Memo9 = createMemo();
    user.Memo10 = createMemo();

    return user;
}
private string createName()
{
    // xxxxxx xxxxxx
    var name = "Aaaaaa Aaaaaa";
    var ch = name.ToCharArray();
    for (int i = 0; i &lt; ch.Length; i++) {
        if (ch[i] == ' ') continue;
        ch[i] = (char)(ch[i] + _rnd.Next(26));
    }
    return new string(ch);
}
private string createEmail( string username )
{
    // xxxxxx xxxxxx
    var name = username.Split(' ')[0];
    return name + "@mail.com";
}
private string createMemo(int size = 256)
{
    var ch = new char[size];
    for (int i = 0; i &lt; size; i++)
    {
        ch[i] = (char)('A' + _rnd.Next(26));
    }
    return new string(ch);
}

機会を作って、.NET Core のほうの SQLite も試してみるということで。

サンプルコード

サンプルコードはこちら
https://1drv.ms/u/s!AmXmBbuizQkXgfsUYttAgWYXnhiogw

参考先

C# で SQLite を便利に使うサンプルコード(LINQ to SQLite) – 翔星 Be ランド日記
http://shinta0806be.ldblog.jp/archives/9084539.html

In-Memory Databases
https://www.sqlite.org/inmemorydb.html

 

カテゴリー: SQLite | 1件のコメント

ASP.NET Core MVC の Web API で JSON 形式のデータを扱う

Web API の JSON 形式に関しては、

Building Your First Web API with ASP.NET Core MVC and Visual Studio ? ASP.NET documentation
https://docs.asp.net/en/latest/tutorials/first-web-api.html

に詳しい解説があります。
が、クライアント側が書いていないので、先の XML 形式と同じように WFP アプリでクライアントを書いていきます。

送受信の形式

  • WPF アプリで JSON 形式で送信
  • ASP.NET Core Web API で JSON 形式で返信

することを考える。データはいちいち JSON 形式に直すのは面倒なので、C# のクラスから Newtonsoft.Json.JsonSerializer を使ってシリアライズ/デシリアライズをする。

Web API 側の設定

XML 形式の場合は Formatters を追加したが、JSON の場合はもともとロードされているので不要。
JsonOutputFormatter と JsonInputFormatter が初期値で使われている。

Web API の PeopleController クラスを作る

Modelクラスである Person クラスを作っておく。

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

JsonSerializer は、IEnumerable<Person> もでシリアライズしてくれるので、XML 形式のような People クラスは不要で、そのまま使える。

コントローラーを作るときに「Entitiy Frameworkを使用したアクションがあるAPIコントローラー」を選べば、CURD機能のAPIが、ずらっと出力される。

このまま動くので改変しなくてよい。

[Produces("application/json")]
[Route("api/People")]
public class PeopleController : Controller
{
    private readonly ApplicationDbContext _context;

    public PeopleController(ApplicationDbContext context)
    {
        _context = context;
    }

    // GET: api/People
    [HttpGet]
    public IEnumerable<Person> GetPerson()
    {
        return _context.Person;
    }

    // GET: api/People/5
    [HttpGet("{id}")]
    public async Task<IActionResult> GetPerson([FromRoute] int id)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        Person person = await _context.Person.SingleOrDefaultAsync(m => m.Id == id);

        if (person == null)
        {
            return NotFound();
        }

        return Ok(person);
    }

    // PUT: api/People/5
    [HttpPut("{id}")]
    public async Task<IActionResult> PutPerson([FromRoute] int id, [FromBody] Person person)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        if (id != person.Id)
        {
            return BadRequest();
        }

        _context.Entry(person).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!PersonExists(id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        // return NoContent();
        return await GetPerson(person.Id);
    }

    // POST: api/People
    [HttpPost]
    public async Task<IActionResult> PostPerson([FromBody] Person person)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        _context.Person.Add(person);
        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateException)
        {
            if (PersonExists(person.Id))
            {
                return new StatusCodeResult(StatusCodes.Status409Conflict);
            }
            else
            {
                throw;
            }
        }

        return CreatedAtAction("GetPerson", new { id = person.Id }, person);
    }

    // DELETE: api/People/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeletePerson([FromRoute] int id)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        Person person = await _context.Person.SingleOrDefaultAsync(m => m.Id == id);
        if (person == null)
        {
            return NotFound();
        }

        _context.Person.Remove(person);
        await _context.SaveChangesAsync();

        return Ok(person);
    }

    private bool PersonExists(int id)
    {
        return _context.Person.Any(e => e.Id == id);
    }
    /// <summary>
    /// 受け口を POST に変換する
    /// </summary>

    [HttpPost("{id}")]
    [Route("Edit/{id}")]
    public async Task<IActionResult> Edit([FromRoute] int id, [FromBody] Person person)
    {
        return await PutPerson(id, person);
    }
    [HttpPost]
    [Route("Create")]
    public async Task<IActionResult> Create([FromBody] Person person)
    {
        return await PostPerson(person);
    }
}

クライアントからの表示の都合上 PutPerson の戻り値を変えている。
あと、POST 形式だけで通るように、Edit と Create を追加している。

WPF クライアントを作る

XML 形式のときと同じように、Person クラスだけを作る。
JsonSerializer が使えるように、Newtonsoft.Json を NuGet で参照設定させておく。

namespace ClientJson
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private async void clickGet(object sender, RoutedEventArgs e)
        {
            var hc = new HttpClient();
            // hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var res = await hc.GetAsync("http://localhost:5000/api/people");
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;

            var js = new Newtonsoft.Json.JsonSerializer();
            var jr = new Newtonsoft.Json.JsonTextReader( new System.IO.StringReader(str));
            var items = js.Deserialize<IEnumerable<Person>>(jr);
            textPerson.Text = "";
            foreach (var item in items)
            {
                textPerson.Text += $"{item.Id} {item.Name} {item.Age} n";
            }
        }

        private async void clickGetById(object sender, RoutedEventArgs e)
        {
            int id = 2;
            var hc = new HttpClient();
            // hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var res = await hc.GetAsync($"http://localhost:5000/api/people/{id}");
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;

            var js = new Newtonsoft.Json.JsonSerializer();
            var jr = new Newtonsoft.Json.JsonTextReader(new System.IO.StringReader(str));
            var item = js.Deserialize<Person>(jr);
            textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
        }

        private async void clickPutById(object sender, RoutedEventArgs e)
        {
            var person = new Person() { Id = 2, Name = "update person", Age = 99 };
            var js = new Newtonsoft.Json.JsonSerializer();
            var sw = new System.IO.StringWriter();
            js.Serialize(sw, person);
            var hc = new HttpClient();
            // hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var json = sw.ToString();
            var cont = new StringContent(json, Encoding.UTF8, "application/json");
            var res = await hc.PutAsync($"http://localhost:5000/api/people/{person.Id}", cont);
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;

            var jr = new Newtonsoft.Json.JsonTextReader(new System.IO.StringReader(str));
            var item = js.Deserialize<Person>(jr);
            textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
        }

        private async void clickPost(object sender, RoutedEventArgs e)
        {
            var person = new Person() { Id = 0, Name = "new person", Age = 88 };
            var js = new Newtonsoft.Json.JsonSerializer();
            var sw = new System.IO.StringWriter();
            js.Serialize(sw, person);
            var hc = new HttpClient();
            // hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var json = sw.ToString();
            var cont = new StringContent(json, Encoding.UTF8, "application/json");
            var res = await hc.PostAsync($"http://localhost:5000/api/people", cont);
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;

            var jr = new Newtonsoft.Json.JsonTextReader(new System.IO.StringReader(str));
            var item = js.Deserialize<Person>(jr);
            textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
        }

        private void clickDeleteById(object sender, RoutedEventArgs e)
        {
        }

        private async void clickCreate(object sender, RoutedEventArgs e)
        {
            var person = new Person() { Id = 0, Name = "new person", Age = 88 };
            var js = new Newtonsoft.Json.JsonSerializer();
            var sw = new System.IO.StringWriter();
            js.Serialize(sw, person);
            var hc = new HttpClient();
            // hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var json = sw.ToString();
            var cont = new StringContent(json, Encoding.UTF8, "application/json");
            var res = await hc.PostAsync($"http://localhost:5000/api/people/Create", cont);
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;

            var jr = new Newtonsoft.Json.JsonTextReader(new System.IO.StringReader(str));
            var item = js.Deserialize<Person>(jr);
            textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
        }

        private async void clickEdit(object sender, RoutedEventArgs e)
        {
            var person = new Person() { Id = 2, Name = "edit person", Age = 99 };
            var js = new Newtonsoft.Json.JsonSerializer();
            var sw = new System.IO.StringWriter();
            js.Serialize(sw, person);
            var hc = new HttpClient();
            // hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var json = sw.ToString();
            var cont = new StringContent(json, Encoding.UTF8, "application/json");
            var res = await hc.PostAsync($"http://localhost:5000/api/people/Edit/{person.Id}", cont);
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;

            var jr = new Newtonsoft.Json.JsonTextReader(new System.IO.StringReader(str));
            var item = js.Deserialize<Person>(jr);
            textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
        }
    }
}
namespace SampleWebApiXml.Models
{
    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }
}
  • Web APIの戻り値形式が、デフォルトでJSONなので、 hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(“application/json”)); は、設定しなくてもよい。
  • Person クラスと JSON 形式は、JsonSerializer.Serialize と Deserializeを使えばよい。
  • これも、日本語を通すためにはきちんと UTF8 エンコードが必要かも。

でもって、うまく動くと dotnet run でサーバーを起動、WPF クライアントから JSON 形式で送受信ができるようになる。ここまで、できるようになれば、クライアントを Javascript や Ruby に切り替えたり、サーバー側を CakePHP で切り替えて相互に動かせるようになる。

サンプルコード

動作できるサンプルコードはこちら
https://1drv.ms/u/s!AmXmBbuizQkXgfsRYiYpmZzonGIBZw

カテゴリー: ASP.NET | ASP.NET Core MVC の Web API で JSON 形式のデータを扱う はコメントを受け付けていません

ASP.NET Core MVC の Web API で XML 形式のデータを扱う

ASP.NET Core の Web API は標準で JSON 形式を扱うようになっているので、XML 形式を扱おうとすると苦労します…というか、苦労したのでメモ書き。

送受信の形式

Web API を POST で送信する場合 Body に何の形式を使うのか、というのと、受信に何の形式を使うのか、で組み合わせがある。

送信側
– フォーム形式 application/x-www-form-urlencoded
– JSON 形式 application/json
– XML 形式 application/xml あるいは text/xml

受信側
– JSON 形式 application/json
– XML 形式 application/xml あるいは text/xml

で、最近はブラウザ経由で JSON 形式で送受信することが多いので、そっちの情報は比較的多いのだが、XML 形式の情報がない。というか、WCF がそれを担っていたのだけど、WCF 自体が廃盤になっている。
なので、試験的に

– WPF アプリで XML 形式で送信
– ASP.NET Core Web API で XML 形式で返す

ということが考える。

テストプロジェクトを作る

ASP.NET Core Web Applicaiton(.NET Core) を使う.

Web API 側の設定

project.json に

"Microsoft.AspNetCore.Mvc.Formatters.Xml": "1.0.0"

を加える。

Setup.cs の Setup.ConfigureServices に XmlSerializerOutputFormatter と XmlSerializerInputFormatter を追加する。

services.AddMvc();
// add XML output formatter
services.Configure<Microsoft.AspNetCore.Mvc.MvcOptions>(
    options => {
        options.OutputFormatters.Add(
            new Microsoft.AspNetCore.Mvc.Formatters.XmlSerializerOutputFormatter());
        options.InputFormatters.Add(
            new Microsoft.AspNetCore.Mvc.Formatters.XmlSerializerInputFormatter());
    });

OutputFormatters がアウトプット用で、InputFormattersがインプット用なので、フォーム形式で受けてXML形式で返す場合には、OutputFormattersだけでよい。
XML形式には、XmlDataContractSerializerOutputFormatter もあるの。これはクライアントと形式を揃える必要がある。じゃないとデシリアライズができない。

これで ASP.NET Core 側の XML 形式で送受信する設定は完了。

Web API の PeopleController クラスを作ってみる

Modelクラスである Person クラスを作っておく。

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

#if false
// これでは XMLデシリアライズできない
public class People : List<Person> { }
#endif

public class People
{
    // プロパティにして List 化すると通る
    public List<Person> Items { get; set; }
}

実は、List<Person> を使いたいのだが、XML形式でシリアライズするときに ArrayOfPerson のように変換されてデシリアライズでうまくいかない。仕方がないので、People クラスのように、中身に List を含んだラップクラスを作る。

この Person クラスからコードファーストでデータベースを作った後、PeopleController クラスを作る。

 [Produces("application/xml")]
 [Route("api/People")]
public class PeopleController : Controller
{
    private readonly ApplicationDbContext _context;

    public PeopleController(ApplicationDbContext context)
    {
        _context = context;
    }

    // GET: api/People
    [HttpGet]
#if false
	public IEnumerable<Person> GetPerson()
	{
		return _context.Person;
	}
#else
    public async Task<People> GetPerson()
    {
        var people = new People();
        people.Items = new List<Person>();
        await _context.Person.ForEachAsync(p => people.Items.Add(p));
        return people;
    }
#endif
    // GET: api/People/5
    [HttpGet("{id}")]
    public async Task<IActionResult> GetPerson([FromRoute] int id)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        Person person = await _context.Person.SingleOrDefaultAsync(m => m.Id == id);

        if (person == null)
        {
            return NotFound();
        }

        return Ok(person);
    }

    // PUT: api/People/5
    [HttpPut("{id}")]
    public async Task<IActionResult> PutPerson([FromRoute] int id, [FromBody] Person person)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        if (id != person.Id)
        {
            return BadRequest();
        }

        _context.Entry(person).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!PersonExists(id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        // return NoContent();
        // return CreatedAtAction("GetPerson", new { id = person.Id }, person);
        return await GetPerson(person.Id);
    }

    // POST: api/People
    [HttpPost]
    public async Task<IActionResult> PostPerson([FromBody] Person person)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        _context.Person.Add(person);
        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateException)
        {
            if (PersonExists(person.Id))
            {
                return new StatusCodeResult(StatusCodes.Status409Conflict);
            }
            else
            {
                throw;
            }
        }

        return CreatedAtAction("GetPerson", new { id = person.Id }, person);
    }

    // DELETE: api/People/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeletePerson([FromRoute] int id)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        Person person = await _context.Person.SingleOrDefaultAsync(m => m.Id == id);
        if (person == null)
        {
            return NotFound();
        }

        _context.Person.Remove(person);
        await _context.SaveChangesAsync();

        return Ok(person);
    }

    private bool PersonExists(int id)
    {
        return _context.Person.Any(e => e.Id == id);
    }

    /// <summary>
    /// 受け口を POST に変換する
    /// </summary>

    [HttpPost("{id}")]
    [Route("Edit/{id}")]
    public async Task<IActionResult> Edit([FromRoute] int id, [FromBody] Person person)
    {
        return await PutPerson(id, person);
    }
    [HttpPost]
    [Route("Create")]
    public async Task<IActionResult> Create([FromBody] Person person)
    {
        return await PostPerson(person);
    }

}

Entity Framework から取得するように修正してあるので、ややこしくなっているけど、ASP.NET Core MVC のスキャフォールディングと合わせるように、Create や Update で通るようにしてある。
フォーム形式の場合は、Bind 属性で値を取るが、JSONやXML形式の場合は FromBody 属性でバインドする。このあたり、ちょっと混乱しているような気がする。

PeopleController クラスの Produces 属性は、デフォルトで返す Content-type を指定するらしい。

WPF クライアントを作る

本来ならば、Person, People クラスをアセンブリで共有するほうがいいのだが、ASP.NET Core は .NET Core のライブラリで、WPF クライアントは .NET Framework のライブラリなので共有できない。が、XML形式やJSON形式でシリアライズしてやり取りするだけなので、クラス名やプロパティ名だけ合わせておけばよい。

namespace ClientXml
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void clickGet(object sender, RoutedEventArgs e)
        {
            var hc = new HttpClient();
            hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
            var res = await hc.GetAsync("http://localhost:5000/api/people");
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;
#if false
            // ArrayOfPerson は取れない
            var xs = new System.Xml.Serialization.XmlSerializer(typeof(IEnumerable<Person>));
            var items = xs.Deserialize(new System.IO.StringReader(str)) as IEnumerable<Person>;
            textPerson.Text = "";
            foreach ( var item in items )
            {
                textPerson.Text += $"{item.Id} {item.Name} {item.Age} n";
            }
#endif
            var xs = new System.Xml.Serialization.XmlSerializer(typeof(People));
            var people = xs.Deserialize(new System.IO.StringReader(str)) as People;
            textPerson.Text = "";
            foreach (var item in people.Items)
            {
                textPerson.Text += $"{item.Id} {item.Name} {item.Age} n";
            }
        }

        private async void clickGetById(object sender, RoutedEventArgs e)
        {
            int id = 2;
            var hc = new HttpClient();
            hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
            var res = await hc.GetAsync($"http://localhost:5000/api/people/{id}");
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;
            var xs = new System.Xml.Serialization.XmlSerializer(typeof(Person));
            var item = xs.Deserialize(new System.IO.StringReader(str)) as Person;
            textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
        }

        private async void clickPutById(object sender, RoutedEventArgs e)
        {
            var person = new Person() { Id = 2, Name = "update person", Age = 99 };
            var xs = new System.Xml.Serialization.XmlSerializer(typeof(Person));
            var hc = new HttpClient();
            hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
            var sw = new System.IO.StringWriter();
            // 先頭の <?xml ... をカットする
            var settings = new System.Xml.XmlWriterSettings() { OmitXmlDeclaration = true, Encoding = Encoding.UTF8 };
            var xw = System.Xml.XmlWriter.Create(sw, settings);
            xs.Serialize(xw, person);
            var xml = sw.ToString();
            var cont = new StringContent(xml, Encoding.UTF8, "application/xml");
            var res = await hc.PutAsync($"http://localhost:5000/api/people/{person.Id}", cont);
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;
            var item = xs.Deserialize(new System.IO.StringReader(str)) as Person;
            textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
        }

        private async void clickPost(object sender, RoutedEventArgs e)
        {
            var person = new Person() { Id = 0, Name = "new person", Age = 88 };
            var xs = new System.Xml.Serialization.XmlSerializer(typeof(Person));
            var hc = new HttpClient();
            hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
            var sw = new System.IO.StringWriter();
            var settings = new System.Xml.XmlWriterSettings() { OmitXmlDeclaration = true, Encoding = Encoding.UTF8 };
            var xw = System.Xml.XmlWriter.Create(sw, settings);
            xs.Serialize(xw, person);
            var xml = sw.ToString();
            var cont = new StringContent(xml, Encoding.UTF8, "application/xml");
            var res = await hc.PostAsync($"http://localhost:5000/api/people", cont);
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;
            var item = xs.Deserialize(new System.IO.StringReader(str)) as Person;
            textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
        }

        private void clickDeleteById(object sender, RoutedEventArgs e)
        {
        }

        private async void clickCreate(object sender, RoutedEventArgs e)
        {
            var person = new Person() { Id = 0, Name = "new person", Age = 88 };
            var xs = new System.Xml.Serialization.XmlSerializer(typeof(Person));
            var hc = new HttpClient();
            hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
            var sw = new System.IO.StringWriter();
            var settings = new System.Xml.XmlWriterSettings() { OmitXmlDeclaration = true, Encoding = Encoding.UTF8 };
            var xw = System.Xml.XmlWriter.Create(sw, settings);
            xs.Serialize(xw, person);
            var xml = sw.ToString();
            var cont = new StringContent(xml, Encoding.UTF8, "application/xml");
            var res = await hc.PostAsync($"http://localhost:5000/api/people/Create", cont);
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;
            var item = xs.Deserialize(new System.IO.StringReader(str)) as Person;
            textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
        }

        private async void clickEdit(object sender, RoutedEventArgs e)
        {
            var person = new Person() { Id = 2, Name = "update person", Age = 99 };
            var xs = new System.Xml.Serialization.XmlSerializer(typeof(Person));
            var hc = new HttpClient();
            hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
            var sw = new System.IO.StringWriter();
            // 先頭の <?xml ... をカットする
            var settings = new System.Xml.XmlWriterSettings() { OmitXmlDeclaration = true, Encoding = Encoding.UTF8 };
            var xw = System.Xml.XmlWriter.Create(sw, settings);
            xs.Serialize(xw, person);
            var xml = sw.ToString();
            var cont = new StringContent(xml, Encoding.UTF8, "application/xml");
            var res = await hc.PostAsync($"http://localhost:5000/api/people/Edit/{person.Id}", cont);
            var str = await res.Content.ReadAsStringAsync();
            textXml.Text = str;
            var item = xs.Deserialize(new System.IO.StringReader(str)) as Person;
            textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
        }
    }
}
namespace SampleWebApiXml.Models
{
    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }
    public class People
    {
        public List<Person> Items { get; set; }
    }
}

試行錯誤した結果を載せているので、これが一番良いというわけではない。
いくつか、ポイントがあるのでざっと解説をしておくと、

– XML 形式で受信するために hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(“application/xml”)); を付ける。HTTP プロトコルの Accept ヘッダに受信する形式を指定しておくと、Web API が Accept にあわせて送ってくれる。
– POST や PUT で送信するときに、StringContent(xml, Encoding.UTF8, “application/xml”); のように Content-type を指定する。これを指定しないと、Web API 側で XML データとして認識してくれない。
– XmlSerializer は、何故か UTF16 でシリアライズするので、先頭の <?xml … を取り除くために、System.Xml.XmlWriterSettings で設定をする。ASCII 文字でしかテストしていないが、日本語を通す場合はきちんと string 型から UTF8 エンコードをしたほうがいいかもしれない.

うまく実行できると、こんな風にサーバ側を dotnet run で動かして、WPF アプリから送受信できるようになる。

きちっとした説明は別の機会にでも…

サンプルコード

動作できるサンプルコードはこちら
https://1drv.ms/u/s!AmXmBbuizQkXgfsQuu9Ly1NjTHa6nQ

最初に dotnet ef database update を使ってデータベースを作らないといけないかも。

カテゴリー: ASP.NET | ASP.NET Core MVC の Web API で XML 形式のデータを扱う はコメントを受け付けていません

micro:bit 互換機で夏休みの自由研究を

毎度のことながら夏休みの自由研究に親は悩まされるのですが、今年の夏は(建前上ながら)大丈夫です。と、言いますか、子供の自由研究めあてに micro:bit 互換機のモニターに応募して当たりましたので、そのまま有り難く自由研究に使わせて頂きました。micro:bit互換機の試作機モニターへのご応募ありがとうございました【スイッチサイエンスチャンネル】 ちなみに、互換機は MFT2016(今週末の土日です)に販売されるそうなので、MFT2016にて、micro:bit互換機「chibi:bit」のテスト版を販売します | スイッチサイエンス マガジン こっちのほうも是非。

micro:bit とは

BBC が英国の小学生向けに無償配布した電子工作キットです。BBC micro:bit : home から辿ると micro:bit で何ができそうかが分かります。中身が mbed なので、たぶんインターネット上の mbed 環境でも作れるはずですが、子供用に Scratch に似たような Block Editor を使ってプログラミングをします。

image

ブラウザ上で、ぽちぽちとブロックを重ねるのと、「run」ボタンを押すと左に出てるシミュレーターで動かすことができます。なので、1回ごとに実機に転送しなくてよいのです。けど、試行錯誤的にはこれで十分だけど、傾きセンサーとか音を鳴らすとかの動きの実際の動きまでは試せないので、やっぱり実機への転送は必須です。

まったく宣伝されてませんが、実は Block Editor は Microsoft が作っています(日本で話題になってないだけですかね?)。

image

また、使ってはいないのですが、JavaScript や python も使えるのでがりがり組みたい場合(?)は、そっちのほうがいいかもしれません。

まあ、小6の自由研究にいきなりプログラムコードを…と言っても敷居が高すぎるので、ぽちぽちと並べて作ります。このあたり Scratch に喰い付く子であれば子供だけでも進むんでしょうが、そのあたりは「自由研究」として、説明しながら手を出しながらという具合で進めます。

自由研究の様子

30分ほど micro:bit の解説をして、ぽちぽちとプログラミング状態に突入。ほんとうは、加速度センサーとか電子ブザーとか付ければいいんですが、準備もあるし pin の説明をするのもアレなので、A, B ボタンと、シェイクで LED を光らせるところだけやります。

image

5×5 の LED を光らせるのは簡単になっています。show leds のブロックを使うか、文字列(アルファベットだけど)を流れるように表示する show string のブロックを使います。サンプルコードには30分とか書いてあるけど、最初にやるとそれなりに時間がかかるし工作も入ると1日では済みそうもないので、そこは手抜きで(苦笑)。

変数を使ってとか、if文を使って、とかあれこれやると時間が掛かりそうなので、A/B ボタンを押したときの処理だけ羅列した状態です。LED の配置が連なっているだけでもアニメーションっぽく動きます。途中の pause はなくても、一定時間 LED は光る仕様のようなので省略。

実はループを使っていると、途中でボタンを押しても反応しないんですよね…、ってのが分かったので途中で反応するように組み替えてもいいんですが、それはまあ「自由研究」の範疇を超えるので、また時間があったらということで。

image

micro:bit はパソコンから micro USBケーブルで電源供給するか、コネクタに接続するかなんですが、ひとまず携帯用のモバイル電源で光るのでお試しでやってみます。後から micro USB 経由で電池から供給できるように改造します。そうしないと学校に持って行けないし。

image

単体で持って行っても何やら分からないので、micro:bit の解説と、ちょっとだけ仕様っぽいものを書いておきます。裏には先に書いた Block Editor の画面キャプチャを印刷して貼り付けておしまい。micro:bit の説明から開始して2時間弱で「自由研究」の終了です。まあ、この時間で自由研究にしていいかどうかはさておき、夏休みの自由研究の素材としてはよいかなと。

これからの micro:bit

ひとまず、手元の micro:bit 互換機は、夏休みの自由研究の作品として学校に持って行ってしまうので帰ってきたら、ということになりますが、せっかく BLE もついているし、加速度センサーと磁気センサーもついているので、このあたりは試しておきたいですね。あと、圧電ブザーを鳴らすとかワニクリップを使って定番っぽいものをやっておく予定。

カテゴリー: 雑談 | micro:bit 互換機で夏休みの自由研究を はコメントを受け付けていません

.NETラボ勉強会 in 仙台でロボットアームを動かしたよ

.NETラボ 勉強会 2016年6月 in 仙台 | .NETラボ まで手持ちのロボットアームを持って行って1枠話した来たので、そのひとり反省会。みなさまありがとうございました。

仙台での写真を撮り忘れてしまったのですが、

こんな材料を使って

こんなものを動かします。Raspberry Pi – Arduino – Robot Arm というややこしい感じになっていますが、Windows IoT Core in Raspberry Pi から直接は細かくサーボを制御できないので、Arduino を噛まします。Raspberry Pi – Arduino 間はシリアル通信なので、実は PC – Arduino で USB ケーブル接続でも可能なのですが…そこは Windows IoT Core を無理矢理使うということで。この部分をコマンド制御にしておくことで、ワタクシ的には ROS への練習とか PLEN2 への練習とかを兼ねています。あと、かつてロボットアームでスマホゲームを動かしたかったので、それも兼ねて。

プレゼン資料

Windows IoT Core and Robot Arm

[slideshare id=63470614&doc=windowsiotcore-160627045432]

デモ用のコード

https://1drv.ms/u/s!AmXmBbuizQkXgflgZRXM-PnmEla5ng

デモについて

ここ1年ぐらいは、LEGO EV3 を持って行ったり、Raspberry Pi を持って行って Windows IoT Core で Lチカしてみたりする訳ですが、IoT 自体の説明時間とデモの説明時間のバランスが難しいです。普通の PC の場合、キーボードとモニタで完結することが多いので、自分としてはデモで「モニタの外側」に出ることを意識してやってる…んですが、自分でやってても「だから何?」の疑問が付きまといます。なんか、ソフトウェア技術的にすごいものと組み合わせてあれこれとハードを動かせれば「すごい」感じがするんでしょうが(まだできないけど)、でもそれだと「すごい」だけで終わっちゃうしなー、という感じ。

今回のロボットアームの件で言えば、

  • 適当なモーションを作って、暫く動きっぱなしでもよかったかも。
    • 1コマンド打ち込みだけだと、6軸同時に制御する、ってのがわかりづらい。
  • やっぱり、Raspberry Pi から大型モニタに出したほうがよい。
    • HDMI がないところが多いので、VGA変換ケーブルを買って試しておく。
  • 説明なしで、コントローラはタブレット(Xamarin)で良いかも。
    • あまり説明なしだと「何かのおもちゃが動いているだけ」になるけど、ポイントだけ解説するとか。
  • 当日、配線するのは結構大変なので(大抵、事前に30分ぐらいしか取れない)、そこだけは基盤にしないと駄目かも。

 

ソフトウェア屋さんがハードを触るときの最初のハードル

西村さん と話して、盛り上がったのでメモ的に

  • ハード特有の用語がわからん。そもそも、ブレッドボードが何か?とか。
    • これは、ソフトウェアの専門用語と同じだから、電子工作のはじめて本で覚えるしかないかな。私はオライリーの「Arduino をはじめよう」が最初の一冊です。
  • 電子部品は何処で買うのかわからない。
    • 昔は、秋葉原に行けばあったけど(今でもあるけど、数が減ったから探しあたる必要があり)、地方の人はどうしているんだろう。むしろ通販だから前よりよくなった?
  • 電子部品を何を買うのかわからない。
    • いさぎよく、最初は「初心者キット」(ちょっと割高だけど)を買うのがベター
    • その後、古めの電子工作の本を買ったのだけど、電子部品の番号が今は売っていなくて互換品を自分で探す必要あり。
  • だいたい2か月頑張れば、次に進める気がする。
    • プログラミングと同じで「慣れ」が必要かなと。キーボードじゃなくて、ブレットボードとジャンプワイヤーで手を動かすので、そっちの方面の「慣れ」が必要。
  • Raspberry Pi, Arduino で得意分野が違うので、両方やっておいたほうが良い。
    • Raspberry Pi だと画像処理とか通信処理とかが楽。
    • Arduino だとマイクロ秒単位の細かい制御ができる。
    • ブラウザ経由でLチカできる、ってのもアリだけど、直接ハードを触って「どんなことが起きているのか」を知っておいたほうがいいかな。

ロボットアームのその先

実験的に自前シリアル通信を作ったけど、Firmata を使うか ROS にしたいですよね。少しずつそれっぽい感じにする予定。ロボットアームからのフィードバックが必要なので、ポテンショメータの値の取得と、加速度センサーの追加、あと Raspberry Pi 側からカメラを引っ張ってきて画像解析できるように、ってところまでが目標。

カテゴリー: Win IoT | .NETラボ勉強会 in 仙台でロボットアームを動かしたよ はコメントを受け付けていません

PLEN2 が到着して初期設定まで

本当は自作ロボットがいいのですが、チートして Kickstarter 経由で PLEN2 を購入しました。日本では Kibidango  のほうが先なのですが、組み立てキットの早期募集が終わっていたので値段が同じぐらいだった Kickstarter のほうに応募。3D プリンタは持っていないのですが、せめて組み立てるということで。発送は日本郵便できました。

開封

元になった PLEN よりもちょっと小さ目です。身長20cm位なので、サーボも小さいですね。バッテリーは充電式の単四x5なもので動かします。胸の部分に充電器、背中の部分に Arduino が乗っている状態になります。

開けて中身を確認したものの、途中で水圧式のロボットアームの組み立てをしているので間があいていますが、組み立て自体はだいたい半日仕事じゃないかなと。特にロボットの組み立てが始めての場合、サーボの基準値とかはめ込みの方向とかあれこれ悩むのでちょっと時間が掛かります。

組み立て

Tutorials [PLEN Playground – Wiki]
http://plen.jp/playground/wiki/tutorials/index.html

に従って、Firmware のインストールからスタートします。バッテリーは既に充電済みなので、即つなげて試すことができました。Arduino にインストールした後に、サーボを繋いでスイッチを入れてピニオンギアにはめ込んで、を18回繰り返します。サーボが18個ありますからね。

サーボに電源を入れた状態で、

な感じで十字の軸を揃えます。揃えますが、きっちりと揃う訳ではありません。どうしてもサーボの基準点とピニオンギアの歯の山とのずれがあるので、ちょっとだけずれます。

この基準点は後でアプリを使って調節していくので、だいたい揃えておけばokです。

ひとまず、こんな感じまで完成させて、電源を入れない状態で立つぐらいまで仕上げます。ロボットアームだと、アームの部分の自重で電源を入れないと関節部分(サーボ部分)で曲がってしまうのですが、PLEN2 の場合は、足裏に重心が乗っかる感じなので、電源が入らない状態でもそのまま立てます。

サーボの原点位置を調節する

背中のスイッチ(Arduino 側とサーボ電源の両方)に電源を入れた状態だと、先のサーボの原点位置がずれているので、ちょっとずれた感じになります。私の場合は、なんか前傾姿勢になっていて、倒れそうな感じになっているし、足もちょっとがに股っぽくなります。実際、このまま歩きのモーションを入れると倒れます(苦笑)。

Tuning Up Home Positions [PLEN Playground – Wiki]
http://plen.jp/playground/wiki/tutorials/plen2/tune

にある ControlServer を使ってサーボの基準点を調節します。Windows の場合は、ControlServer.exe を起動して、PLENUtilities.url でブラウザから調節します。

こんな風なコンソールのサーバーが立ち上がって USB ケーブル経由で PLEN2 のサーボを調節します。

image

ブラウザのほうで、調節したいサーボの番号(位置)をクリックして、Angle をマウスで動かします。このとき PELN2 の実機が動くのでまっすぐになるようにします。調節が終わったら、Home ボタンを押して、そのサーボの設定を PLEN2 に送ります。これをすべての関節分(18回)繰り返します。現状のバージョンでは、サーボの1と13が逆になっています。モーションは問題ないので、ここの表示だけの問題かなと。

image

サーボから角度が送られてくるわけではなく、一方的にサーボ側に角度を通知するだけなので、再び Reset ボタンを押して0度を送ってから調節します。0度から大きく外れてしまった場合は、サーボを外してピニオンギアへのはめ込みをやり直したほうがよさそうです。単位が1/10 deg.(度)なので、歯車1個分(300位?)を目安にすればいいでしょう。

調節するときは、手足がまっすぐになるように、寝かせた状態か手でつりさげた状態でやるとやりやすいです。

ジーっと音がしなくなるまで調節する

サーボの位置を調節しているときに「ジー」という音が鳴ることがあります。これはサーボが鳴っているのですが、ただ立っているだけなのに鳴っているのはうるさいので、音がでなくなるように調節します。

何故、音が鳴るかというと、理由が2パターンあります。

  1. サーボが位置を変えようとする。
  2. 角度が限界までいっているので、それ以上回せない。
  3. けれども、位置設定まで動かそうとするので、モーターがジーっと鳴る。

 

  1. サーボの位置を設定する。
  2. 歯車と重心位置の関係で、ちょっとだけ行き過ぎる。
  3. サーボが位置を戻そうとするが、重さの関係でぎりぎり戻らない。
  4. でもってモーターがジーっと鳴る。

のパターンです。

最初のパターンは可動域よりも大きく廻し過ぎるときで、PLEN2 の場合、股(4,16)、膝(6,18)、足首(8,20)のところで音が鳴りやすくなっています。股の可動域が PLEN 狭すぎるような気がするので、ここは 3D プリンタで要調節かなと思います。

後者の重心が関わる場合は、サーボの角度の分解能に関わるものなので、完全に調節するのは無理です。PELN2 を立たせた状態で、音が鳴らないように膝などを調節します。歯車の遊びの部分があるので、ここは実機調節になります。

既存のモーションを試してみる

iPhone か Android のアプリを入れて既存のモーションを動かしてみます。いきなり、歩かせるとまだバランスが悪くて(微調節が必要な状態)ばたっと倒れますが、踊りのほうはなんとか動きます。モーションが PELN 用なのか、サッカーのモーションを PELN2 に適用すると倒れます(苦笑)。ここが股関節と足首のサーボの可動域の問題かなと。

 

 

 

自分でモーションを作る

自前のモーションは http://plen.jp/playground/motion-editor/ で作れます。可動範囲の制限がないので、コツがいるのですが、まあ、そのあたりはおいおいに。

できあがったモーションは JSON 形式で保存されるので、角度などをエディタで変更できます。

PLEN2 へのアップロードは Releases · plenprojectcompany/plen-MotionInstaller_Win を使って USB 経由で送信します。Arudino 側のスロットに割り当てる方式なので、モーションの空きスロットに転送します(あるいは上書き)。

PLEN2 の各コードは MIT ライセンスで github で配布されているので、自前で修正が可能です。モーションの実行は、ストアアプリ版がないので、プロトコルを見て自前で作ろうかなと思っているところです。

 

 

そんな訳で、ひとまず PLEN2 の組み立てからちょっと動かすところまで。これから先は自前でアプリを作るところからスタート。

  • Windows アプリでモーションの転送
  • Xamarin で手足を個別に動かすツール(モーションの作成用)
  • 加速度センサー(AccelerationGyroSensor)を使った制御(あるのかな?)

な感じです。

カテゴリー: PLEN2 | PLEN2 が到着して初期設定まで はコメントを受け付けていません