More than 1 year has passed since last update.

経緯

会社で某コーヒーサーバーを導入したのですが、その際に利用者から、どうやって集金するかが問題に。
毎月定額を利用予定者に払ってもらうということも考えましたが、やっぱり利用しただけ集金したいということで、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] = {
      ...
  }
  ...