bitcoin-atm
bitcoin atm for pyc inc.
git clone https://9o.is/git/bitcoin-atm.git
commit c141e2a0b86a6706934e07ff5e075a878550e48f parent 13f4fa67f24a1238ffee71a0227a2bca2d65aa17 Author: Jul <jul@9o.is> Date: Sun, 10 Aug 2014 15:37:51 -0700 got started on the bill acceptor driver including ID003 Diffstat:
11 files changed, 404 insertions(+), 49 deletions(-)
diff --git a/project/Build.scala b/project/Build.scala @@ -19,6 +19,8 @@ object LiftProjectBuild extends Build { "net.databinder.dispatch" %% "dispatch-lift-json" % "0.11.0", "com.markgoldenstein" %% "bitcoin-akka" % "0.1-SNAPSHOT", "com.palletops" % "java-websocket" % "1.3.1-SNAPSHOT", + "com.github.jodersky" %% "flow" % "2.0.3", + "com.github.jodersky" % "flow-native" % "2.0.3", //"io.kamon" %% "kamon-core" % "0.3.2", //"io.kamon" %% "kamon-log-reporter" % "0.3.2", //"io.kamon" %% "kamon-system-metrics" % "0.3.2", diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf @@ -1,11 +1,20 @@ chimera { - id = 12345 - name = test - currency = USD - bill-acceptor = JCM + id = "12345" + name = "test" + currency = "USD" } +bill-acceptor { + driver = "ID003" + port = "/dev/tty1" + baud = 9600 + parity = 2 + char-size = 8 + two-stop-bits = false +} + + bitcoin { btcwallet { diff --git a/src/main/scala/inc/pyc/chimera/config/Machine.scala b/src/main/scala/inc/pyc/chimera/config/Machine.scala @@ -4,7 +4,6 @@ package config import model._ import lib._ import bill.acceptor._ -import driver._ import currency._ import akka.actor._ import akka.agent._ @@ -20,7 +19,6 @@ object Machine { val id: String = config.getString("id") val name: String = config.getString("name") val currency = findCurrency(config.getString("currency")) - val billAcceptorDriver = findBillAcceptorDriver(config.getString("bill-acceptor")) /** * Hardware Actor System diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/BillAcceptor.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/BillAcceptor.scala @@ -8,9 +8,10 @@ import driver._ import akka.actor._ import akka.pattern.ask import akka.util.Timeout +import SupervisorStrategy._ import scala.concurrent.duration._ import inc.pyc.lift_akka.EventUpdate - +import com.typesafe.config._ object BillAcceptor extends ExtensionId[BillAcceptorImpl] with ExtensionIdProvider { override def lookup = BillAcceptor @@ -20,6 +21,12 @@ object BillAcceptor extends ExtensionId[BillAcceptorImpl] with ExtensionIdProvid * Subscription topics for event bus. */ val insertedBill = "insertedBill" + + + /** + * Configuration for the bill acceptor. + */ + val config = ConfigFactory.load().getConfig("bill-acceptor") } @@ -48,42 +55,91 @@ class BillAcceptor extends Actor with ActorLogging { implicit val timeout = Timeout(2 seconds) + override val supervisorStrategy = + OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { + case ex => + driverMalfunction() + Restart + } + + /** The currency that is currently accepted. */ private var currency: Currency = Machine.currency /** Bill Acceptor Driver */ - private val driver = initDriver(Machine.billAcceptorDriver) + private val driver = BillAcceptor.config.getString("driver") + + /** Whether the bill acceptor is working. */ + private var operating = true + /** + * Messages when not listening for inserted bills. + */ def receive = { + + case Listen => + val driver = initDriver + driver ! Listen + + case Ready => + val driver = sender + + if(!operating) { + operating = true + log.info("Bill acceptor is operating again. Redirecting to index...") + bus.publish(EventUpdate(redirectTo, "/index")) + driver ! UnListen + } else { + log.info("Listening for bills") + context become listening(driver) + context watch driver + } + + case _ => + log.warning("Received Unknown Message") + } + + + /** + * Bill Acceptor is in listening mode, + * ready for someone to insert a bill. + */ + def listening(driver: ActorRef): Receive = { case Inserted(bill) => log.info(s"Inserted bill: $bill $currency") bus.publish(EventUpdate(BillAcceptor.insertedBill, bill)) - - case Listen => - (driver ? Listen).mapTo[Boolean] map { success => - if(!success) { - log.error("Failed to listen for bills") - bus.publish(EventUpdate(redirectTo, "/malfunction")) - } else { - log.info("Listening for bills") - } - } case UnListen => - (driver ? UnListen).mapTo[Boolean] map { success => - if(!success) { - log.error("Failed to stop listening for bills") - bus.publish(EventUpdate(redirectTo, "/malfunction")) - } else { - log.info("Stopped listening for bills") - } - } + log.info("Stopped listening for bills") + context unwatch driver + context become receive + + case Terminated(`driver`) => + log.error("Driver unexpectedly crashed") + driverMalfunction() case _ => log.warning("Received Unknown Message") } + + /** + * Run this function when driver fails for any reason. + */ + def driverMalfunction(): Unit = { + log.error("Bill acceptor not operating. Redirecting to malfunction... ") + operating = false + bus.publish(EventUpdate(redirectTo, "/malfunction")) + context.system.scheduler.scheduleOnce(10 second){ self ! Listen } + } + + + + private def initDriver : ActorRef = { + initDriver( findBillAcceptorDriver(driver) ) + } + private def initDriver(service: BillAcceptorDriver.Value): ActorRef = { context.actorOf(BillAcceptorDriver.getDriverActor(service), service.toString) } diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/APEX.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/APEX.scala @@ -4,6 +4,13 @@ package driver import akka.actor._ + +/** + * One of Pyramid Acceptor's Bill Acceptor models: + * http://www.pyramidacceptors.com/files/Apex_Manual.pdf + * + * Used by Skyhook. + */ class APEX extends Actor { def receive = { diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/CRC.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/CRC.scala @@ -0,0 +1,52 @@ +package inc.pyc.chimera.lib.bill.acceptor.driver + +/** + * Computes the CCITT-KERMIT CRC + * http://stackoverflow.com/questions/5059268/c-sharp-crc-implementation + */ +class CRC { + + val table = Array( + 0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, + 0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, + 0x1081, 0x0108, 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, + 0x9CC9, 0x8D40, 0xBFDB, 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, + 0x2102, 0x308B, 0x0210, 0x1399, 0x6726, 0x76AF, 0x4434, 0x55BD, + 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, 0xFAE7, 0xC87C, 0xD9F5, + 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, 0x54B5, 0x453C, + 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, 0xC974, + 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB, + 0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, + 0x5285, 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, + 0xDECD, 0xCF44, 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, + 0x6306, 0x728F, 0x4014, 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, + 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, 0xA96A, 0xB8E3, 0x8A78, 0x9BF1, + 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, 0x242A, 0x16B1, 0x0738, + 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, 0x9AF9, 0x8B70, + 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, 0xF0B7, + 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF, + 0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, + 0x18C1, 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, + 0xA50A, 0xB483, 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, + 0x2942, 0x38CB, 0x0A50, 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, + 0xB58B, 0xA402, 0x9699, 0x8710, 0xF3AF, 0xE226, 0xD0BD, 0xC134, + 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, 0x6E6E, 0x5CF5, 0x4D7C, + 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, 0xA33A, 0xB2B3, + 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, 0x3EFB, + 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232, + 0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, + 0xE70E, 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, + 0x6B46, 0x7ACF, 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, + 0xF78F, 0xE606, 0xD49D, 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, + 0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78) + + def compute(buf: Array[Byte]) = { + var crc = 0x00 + + for(i <- 0 until buf.length) { + crc = (crc >> 8) ^ table((crc ^ buf(i)) & 0xff) + } + + crc + } +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/Driver.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/Driver.scala @@ -2,22 +2,126 @@ package inc.pyc.chimera.lib package bill.acceptor package driver +import bill.acceptor.commands._ import akka.actor._ - +import akka.io.IO +import akka.util.ByteString +import com.github.jodersky.flow._ +import com.github.jodersky.flow.Serial._ /** * The different choices for bitcoin services. */ object BillAcceptorDriver extends Enumeration { type BillAcceptorDriver = Value - val JCM, APEX = Value - + val ID003, APEX = Value + def drivers: Map[BillAcceptorDriver, Props] = Map( - JCM -> Props[JCM], - APEX -> Props[APEX] - ) + ID003 -> Props[ID003], + APEX -> Props[APEX]) def getDriverActor(s: Value): Props = { drivers.filter(_._1 == s).map(_._2).head } +} + + +/** + * Main Bill Acceptor Driver trait. + */ +private[driver] trait Driver { + this: Actor with ActorLogging => + + import commands._ + import context.system + + /** + * Serial port (/dev/tty*) + */ + val port: String = BillAcceptor.config.getString("port") + + /** + * Settings for serial. + */ + val settings = SerialSettings( + baud = BillAcceptor.config.getInt("baud"), + characterSize = BillAcceptor.config.getInt("char-size"), + twoStopBits = BillAcceptor.config.getBoolean("two-stop-bits"), + parity = Parity(BillAcceptor.config.getInt("parity")) + ) + + /** + * Handle data passing with this partial function + * when it's in listening mode. + */ + val handleData: Receive + + + /** + * When driver is not in listening mode. + */ + def receive: Receive = { + + case Listen => + log.info("Requesting manager to open port: {}, baud: {}", port, settings.baud) + IO(Serial) ! Open(port, settings) + + + case CommandFailed(cmd, reason) => + log.error("Connection failed, stopping driver. Reason: {}", reason) + throw new CommandFailedException + + + case Opened(port) => + log.info("Port {} is now open", port) + val operator = sender + context become opened(operator) + context watch operator + context.parent ! Ready + } + + + /** + * When driver is in listening mode. + */ + def opened(operator: ActorRef): Receive = { + + case Received(data) => + log.debug("Received data: {}", formatData(data)) + + + case Closed => + log.info("Operator closed normally, exiting driver") + context unwatch operator + context stop self + + + case Terminated(`operator`) => + log.error("Operator crashed, exiting driver") + throw new OperatorCrashException + + + case UnListen => + log.info("Driver's serial connection is closing") + operator ! Close + + + case Input(input) => + val data = ByteString(input.getBytes) + log.debug("Writing data: {}", formatData(data)) + operator ! Write(data, length => Wrote(data.take(length))) + + } + + private def formatData(data: ByteString) = + data.mkString("[", ",", "]") + " " + (new String(data.toArray, "UTF-8")) +} + + +private[driver] object commands { + + /** + * Command to send data to serial. + */ + case class Input(input: String) } \ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/ID003.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/ID003.scala @@ -0,0 +1,117 @@ +package inc.pyc.chimera.lib +package bill.acceptor +package driver + +import akka.actor._ +import com.github.jodersky.flow.Serial._ +import scala.collection._ +import akka.util.ByteString + +/** + * One of JCM's Bill Acceptor protocol: + * ftp://67.205.101.207/Pripherals/BillAcceptors/JCM/ID003/ID-003%20Protocol%20Spec.pdf + * + * Used by Lamassu. + */ +class ID003 extends Actor with ActorLogging with Driver { + + /** Buffer incoming from serial */ + var buf = ByteString() + + /** Message start code */ + val SYNC = 0xfc; + + + val handleData: Receive = { + case Received(data) => + buf ++ data + buf = aquireSync(buf) + + if(buf.length > 1 && buf.length >= buf(1)) { + val responseSize = buf(1) + val packet = buf slice(0, responseSize) + buf = buf drop(responseSize) + parse(packet) + } + } + + + /** + * Tries to parse the byte string. + */ + def parse(data: ByteString) = { + + } + + /** + * Finds `SYNC` in data and returns all bytes after that. + */ + def aquireSync(data: ByteString): ByteString = { + val i = data indexOf SYNC + + if(i > -1) data drop i + else ByteString() + } + + /** Command codes */ + val CMD = Map( + "denominations" -> Array(0x8a), + "status" -> Array(0x11), + "stack" -> Array(0x41), + "ack" -> Array(0x50), + "inhibit" -> Array(0xc3, 0x01), + "unInhibit" -> Array(0xc3, 0x00), + "reset" -> Array(0x40), + "reject" -> Array(0x43), + "enableAll" -> Array(0xc0, 0x00, 0x00), + "getEnabled" -> Array(0x80)) + + + /** Response codes */ + val RSP = Map( + 0x40 -> "powerUp", + 0x1b -> "initialize", + 0x1a -> "disable", + 0x11 -> "enable", + 0x12 -> "accepting", + 0x13 -> "escrow", + 0x14 -> "stacking", + 0x15 -> "vendValid", + 0x16 -> "stacked", + 0x17 -> "rejecting", + 0x18 -> "returning", + 0x43 -> "stackerFull", + 0x44 -> "stackerOpen", + 0x45 -> "acceptorJam", + 0x46 -> "stackerJam", + 0x47 -> "pause", + 0x48 -> "cheated", + 0x49 -> "failure", + 0x50 -> "ack", + 0x80 -> "getEnabled", + 0x88 -> "version", + 0x8a -> "denominations", + 0xc0 -> "setEnabled", + 0xc3 -> "inhibit") + + + /** Rejection reason codes */ + val REJECTION_REASONS = Map( + 0x71 -> "insertion", + 0x72 -> "mug", + 0x73 -> "head", + 0x74 -> "calibration", + 0x75 -> "conveying", + 0x76 -> "discrimination", + 0x77 -> "photoPattern", + 0x78 -> "photoLevel", + 0x79 -> "inhibit", + 0x7a -> "unknown", + 0x7b -> "operation", + 0x7c -> "stacker", + 0x7d -> "length", + 0x7e -> "photoPattern", + 0x7f -> "trueBill") + + +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/JCM.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/JCM.scala @@ -1,12 +0,0 @@ -package inc.pyc.chimera.lib -package bill.acceptor -package driver - -import akka.actor._ - -class JCM extends Actor { - - def receive = { - case _ => - } -} -\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/package.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/package.scala @@ -1,11 +1,32 @@ package inc.pyc.chimera.lib.bill.acceptor +import driver._ +import BillAcceptorDriver._ +import com.github.jodersky.flow.Serial._ +import akka.util.ByteString + package object driver { - import driver._ - import BillAcceptorDriver._ - + /** + * Given a string, the bill acceptor driver is returned. + */ def findBillAcceptorDriver(s: String): BillAcceptorDriver = { drivers.filter(_._1.toString == s).map(_._1).head } + + + /** + * Command to notify that the driver is ready to receive messages + */ + case object Ready + + /** + * Command when writing to the serial actor. + */ + case class Wrote(data: ByteString) extends Event + + + // exceptions + class CommandFailedException extends Exception + class OperatorCrashException extends Exception } \ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/snippet/Comet.scala b/src/main/scala/inc/pyc/chimera/snippet/Comet.scala @@ -42,7 +42,7 @@ class BillAcceptor extends EventRegister { override val receive: Receive = { case JString("listen") => - Machine.billAcceptor.listen() + //Machine.billAcceptor.listen() case JString("unlisten") => Machine.billAcceptor.unlisten()