経緯
会社で某コーヒーサーバーを導入したのですが、その際に利用者から、どうやって集金するかが問題に。
毎月定額を利用予定者に払ってもらうということも考えましたが、やっぱり利用しただけ集金したいということで、NFCを利用した専用端末を作ってみることにしました。
個々の利用者の識別方法
利用者の識別は、新たにカードなどを発行することはしたくなかったので、個々人が所有している任意のFeliCaカードを利用することに。
仕組み
- 端末にFeliCaカードをタッチ
- 1タッチ毎に決められた定額がサーバ上の残高より引かれる
- 電源投入時、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で出力しました。
配線
FeliCaリーダとWi-Fi内蔵mbedモジュール、LCDモジュールとWi-Fi内蔵mbedモジュールを接続します。
また、電源はWi-Fi内蔵mbedモジュール上のmicroUSBコネクタから給電します。
組み立て
まず、FeliCaリーダとLCDモジュールをケースにはめ込みます。
その後、Wi-Fi内蔵mbedモジュールをはめ込み、底蓋をしめます。
完成!
ソフトウェア
端末
ARMが提供している、mbedで開発しました。
サーバ
タッチ後のユーザ認証などを行いますが、今回は、次のような構成にしました。
- プログラム Scala
- 端末とのHTTP通信 Akka HTTP
- データベース PostgreSQL
- サーバ heroku
テスト時には、RESTクライアントとして、Postmanを利用。これ、便利でした。
ちなみに、プログラムはこんな感じです。
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] = {
...
}
...