Scala と mbed でプリペイドサービスを作ってみた

# 経緯
会社で某コーヒーサーバーを導入したのですが、その際に利用者から、どうやって集金するかが問題に。
毎月定額を利用予定者に払ってもらうということも考えましたが、やっぱり利用しただけ集金したいということで、NFCを利用した専用端末を作ってみることにしました。

# 個々の利用者の識別方法
利用者の識別は、新たにカードなどを発行することはしたくなかったので、個々人が所有している任意のFeliCaカードを利用することに。

# 仕組み
1. 端末にFeliCaカードをタッチ
2. 1タッチ毎に決められた定額がサーバ上の残高より引かれる
3. 電源投入時、Wi-Fi接続時、タッチ時には端末上のLCDディスプレイにその情報を表示

# 専用端末
専用端末は、次のものを組み合わせて作りました。

* FeliCa リーダー モジュール
* Wi-Fi内蔵 mbed モジュール 
* LCD モジュール
* 自作ケース

# FeliCa リーダー モジュール
組込用のFeliCaリーダー・ライターである[RC-S620S](https://www.switch-science.com/catalog/353/)を使いました。

# Wi-Fi内蔵 mbed モジュール
リーダーの制御と通信については、Wi-Fi内蔵 mbed モジュール[WiFi DipCortex](https://developer.mbed.org/platforms/WiFi-DipCortex/)を使いました。
（ちなみに、現在は販売停止しているみたいです）

# LCD モジュール
表示のためのLCDモジュールは、秋月電子で販売している、[I2C接続小型キャラクタLCDモジュール](http://akizukidenshi.com/catalog/g/gK-08896/)を使いました。

# 自作ケース
ケースは、iMacで[Fusion360](http://www.autodesk.co.jp/products/fusion-360/)を使って3Dモデルを設計し、会社にある3Dプリンター[ダヴィンチ 1.0 AiO](https://jp.xyzprinting.com/product/da-Vinci-1.0-AiO)で出力しました。

![150710-0002.png](https://qiita-image-store.s3.amazonaws.com/0/21380/857f58fb-32d3-f8d1-faee-028ddcec992a.png)
3Dモデル(設計中)

![aa35be61-6a50-c730-2aeb-3e7132950540.png](https://qiita-image-store.s3.amazonaws.com/0/21380/a7eacb23-33db-61c2-668a-8f30be1a8845.png)
３Dモデル(完成)

# 配線
FeliCaリーダとWi-Fi内蔵mbedモジュール、LCDモジュールとWi-Fi内蔵mbedモジュールを接続します。
また、電源はWi-Fi内蔵mbedモジュール上のmicroUSBコネクタから給電します。
![aEg0vDLUgRr5.jpg](https://qiita-image-store.s3.amazonaws.com/0/21380/dcaed4d1-3bc0-d210-69db-4c93625014c3.jpeg)

# 組み立て
まず、FeliCaリーダとLCDモジュールをケースにはめ込みます。
その後、Wi-Fi内蔵mbedモジュールをはめ込み、底蓋をしめます。

![jyXMaIN2Vi15.jpg](https://qiita-image-store.s3.amazonaws.com/0/21380/3c3aa466-4869-0456-289c-e6e84daa3c00.jpeg)

# 完成！
![i7x6VRMYwNz6.jpg](https://qiita-image-store.s3.amazonaws.com/0/21380/bfc606d9-23af-f9e5-775e-95e7f0d3c91d.jpeg)

# ソフトウェア
## 端末
ARMが提供している、[mbed](https://developer.mbed.org/)で開発しました。
## サーバ
タッチ後のユーザ認証などを行いますが、今回は、次のような構成にしました。
 * プログラム [Scala](http://www.scala-lang.org/)
 * 端末とのHTTP通信 [Akka HTTP](http://doc.akka.io/docs/akka-stream-and-http-experimental/current/scala/http/)
 * データベース [PostgreSQL](http://www.postgresql.org/)
 * サーバ [heroku](https://www.heroku.com/)
テスト時には、RESTクライアントとして、[Postman](http://www.getpostman.com/)を利用。これ、便利でした。
ちなみに、プログラムはこんな感じです。

```scala:Server.scala
import java.net.URI
import java.nio.ByteBuffer
import java.security.AlgorithmParameters
import java.util.{Base64, Date}
import javax.crypto.spec.{PBEKeySpec, IvParameterSpec, SecretKeySpec}
import javax.crypto.{Cipher, SecretKeyFactory}
import java.security.SecureRandom

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
import akka.http.scaladsl.marshalling.Marshal
import akka.http.scaladsl.model.ResponseEntity
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import akka.http.scaladsl.util.FastFuture.EnhancedFuture

import akka.stream.ActorMaterializer
import akka.util.Timeout
import spray.json.DefaultJsonProtocol

import scala.util.Properties

import slick.driver.PostgresDriver.api._

import scala.concurrent.Await
import scala.concurrent.duration._

case class UpdateRequest(encryptedMessage: String)


object Main extends App {
  val keyPassword = "..."
  val dPassword = "..."
  val repeat = ...

  val port = Properties.envOrElse("PORT", "...").toInt
  implicit val system = ActorSystem("...")
  implicit val materializer = ActorMaterializer()
  implicit val timeout = Timeout(20 seconds)

  val dbUri = new URI(System.getenv("DATABASE_URL"))
  val username = dbUri.getUserInfo.split(":")(0)
  val password = dbUri.getUserInfo.split(":")(1)
  var dbUrl = s"jdbc:postgresql://${dbUri.getHost}:${dbUri.getPort}${dbUri.getPath}"
  if (System.getenv("STACK") == null) {
    dbUrl = s"${dbUrl}?ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory"
  }
  val db = Database.forURL(dbUrl, driver = "org.postgresql.Driver", user = username, password = password)
  val balances = TableQuery[Balances]
  val transactions = TableQuery[Transactions]
  val permissions = TableQuery[Permissions]

  import system.dispatcher

  val route = {
    path("a") {
      post {
        formFields('encryptedRequest) { encryptedRequest => {
          complete {
            val decrypted = decodeToken(encryptedRequest)
            val q = balances.filter(_.token === decrypted._2)
            val select = q.result.headOption
            val f = db.run(select)
            val result = Await.result(f, Duration.Inf)
            result match {
              case Some(someone) => <r>NG</r>
              case None =>
                db.run(balances += Balance(decrypted._1, decrypted._2, decrypted._3))
                <r>OK</r>
            }
          }
        }
        }
      }
    } ~ path("v") {
      post {
        formFields('encryptedRequest) { encryptedRequest => {
          complete {
            val decrypted = decodeToken(encryptedRequest)
            val select = balances.filter(_.token === decrypted._2).result.headOption
            val f = db.run(select)
            val result = Await.result(f, Duration.Inf)
            result match {
              case Some(someone) => {
                <r>OK</r>
              }
              case None => <r>NG</r>
            }
          }
        }
        }
      }
    } ~ path("w") {
      post {
        formFields('encryptedRequest) {
          encryptedRequest => {
            complete {
              val decrypted = decodeToken(encryptedRequest)
              val q = balances.filter(_.token === decrypted._2)
              val select = q.result.headOption
              val f2 = db.run(select)
              val result2 = Await.result(f2, Duration.Inf)
              result2 match {
                case Some(someone) => {
                  val newBalance = someone.balance + decrypted._3
                  if (newBalance >= 0) {
                    db.run(q.update(someone.copy(timeStamp = decrypted._1, balance = newBalance)))
                    val transaction = Transaction(decrypted._1, decrypted._2, decrypted._3)
                    val f = db.run(transactions += transaction)
                    val result = Await.result(f, Duration.Inf)
                    <r>OK:Withdraw,{newBalance}</r>
                  } else
                    <r>NG:Overdraw,{someone.balance}</r>
                }
                case None =>
                  <r>NG:Invalid,-1</r>
              }
            }
          }
        }
      }
    }
    ...
  }

  val bindingFuture = Http().bindAndHandle(route, "0.0.0.0", port)

  println("Starting on port: " + port)

  def decodeToken(message: String): (Int, String, Int) = {
    ...
    (timeStamp, token, amount)
  }

  def hex2byte(hex: String): Array[Byte] = {
    hex.sliding(2,2).toArray.map(Integer.parseInt(_, 16).toByte)
  }

  def getHashedToken(token :Array[Byte]): Array[Byte] = {
    val salt = createSalt()
    val keySpec = new PBEKeySpec(token.map(_.toChar), salt, ...)
    val factory = SecretKeyFactory.getInstance(...)
    factory.generateSecret(keySpec).getEncoded()
  }

  def createSalt(): Array[Byte] = {
      ...
  }
  ...
```