36
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

フューチャーアーキテクトAdvent Calendar 2015

Day 11

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

Last updated at Posted at 2015-12-11

経緯

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

個々の利用者の識別方法

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

仕組み

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

専用端末

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

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

FeliCa リーダー モジュール

組込用のFeliCaリーダー・ライターであるRC-S620Sを使いました。

Wi-Fi内蔵 mbed モジュール

リーダーの制御と通信については、Wi-Fi内蔵 mbed モジュールWiFi DipCortexを使いました。
(ちなみに、現在は販売停止しているみたいです)

LCD モジュール

表示のためのLCDモジュールは、秋月電子で販売している、I2C接続小型キャラクタLCDモジュールを使いました。

自作ケース

ケースは、iMacでFusion360を使って3Dモデルを設計し、会社にある3Dプリンターダヴィンチ 1.0 AiOで出力しました。

150710-0002.png
3Dモデル(設計中)

aa35be61-6a50-c730-2aeb-3e7132950540.png
3Dモデル(完成)

配線

FeliCaリーダとWi-Fi内蔵mbedモジュール、LCDモジュールとWi-Fi内蔵mbedモジュールを接続します。
また、電源はWi-Fi内蔵mbedモジュール上のmicroUSBコネクタから給電します。
aEg0vDLUgRr5.jpg

組み立て

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

jyXMaIN2Vi15.jpg

完成!

i7x6VRMYwNz6.jpg

ソフトウェア

端末

ARMが提供している、mbedで開発しました。

サーバ

タッチ後のユーザ認証などを行いますが、今回は、次のような構成にしました。

  • プログラム Scala
  • 端末とのHTTP通信 Akka HTTP
  • データベース PostgreSQL
  • サーバ heroku
    テスト時には、RESTクライアントとして、Postmanを利用。これ、便利でした。
    ちなみに、プログラムはこんな感じです。
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] = {
      ...
  }
  ...
36
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?