bitcoin-atm
bitcoin atm for pyc inc.
git clone https://9o.is/git/bitcoin-atm.git
commit f708f5372ba5214a52cd0c51cb5dde061cae7c95 parent c141e2a0b86a6706934e07ff5e075a878550e48f Author: Jul <jul@9o.is> Date: Wed, 3 Sep 2014 12:48:05 -0400 almost working ID003 bill acceptor protocol Diffstat:
20 files changed, 1612 insertions(+), 328 deletions(-)
diff --git a/npm-debug.log b/npm-debug.log Binary files differ. diff --git a/setup.sh b/setup.sh @@ -9,4 +9,4 @@ fi sudo npm install grunt grunt-cli bower grunt-contrib-jshint grunt-contrib-concat grunt-contrib-copy grunt-contrib-uglify grunt-contrib-less grunt-contrib-jasmine grunt-contrib-watch grunt-contrib-clean grunt-hash grunt-contrib-htmlmin -bower install jquery jasmine-jquery angular-mocks +#bower install jquery jasmine-jquery angular-mocks diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf @@ -7,10 +7,12 @@ chimera { bill-acceptor { driver = "ID003" - port = "/dev/tty1" + currency = "USD" + port = "/dev/ttyUSB0" baud = 9600 parity = 2 char-size = 8 + buffer-size = 10 two-stop-bits = false } diff --git a/src/main/scala/inc/pyc/chimera/config/Machine.scala b/src/main/scala/inc/pyc/chimera/config/Machine.scala @@ -20,6 +20,7 @@ object Machine { val name: String = config.getString("name") val currency = findCurrency(config.getString("currency")) + /** * Hardware Actor System */ @@ -31,6 +32,7 @@ object Machine { */ val bus = new LookupSystem + /** * Subscription topics for event bus. */ @@ -39,27 +41,33 @@ object Machine { val transactionUpdate = "transactionUpdate" } + /** * The main bitcoin wallet */ val wallet = Wallet(system) + /** * The main bitcoin price ticker */ val priceTicker = PriceTicker(system) + /** * The transaction of the current user. */ val currentTransaction: Agent[Transaction] = Agent(Transaction.create) + /** * The machine's bill acceptor */ val billAcceptor = BillAcceptor(system) } + + /** * PubSub for hardware related topics. */ diff --git a/src/main/scala/inc/pyc/chimera/lib/CRC.scala b/src/main/scala/inc/pyc/chimera/lib/CRC.scala @@ -0,0 +1,64 @@ +package inc.pyc.chimera.lib + +/** + * 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]): Array[Byte] = { + var crc = 0x00 + + for(i <- 0 until buf.length) { + crc = (crc >> 8) ^ table((crc ^ buf(i)) & 0xff) + } + + intToByteArray(crc) + } + + private def intToByteArray(value: Int): Array[Byte] = { + var b = Array[Byte](0,0,0,0) + + for (i <- 0 until b.length) { + val offset = (b.length - 1 - i) * 8; + b(i) = ((value >>> offset) & 0xFF).asInstanceOf[Byte] + } + + b + } + +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/AcceptorCommands.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/AcceptorCommands.scala @@ -0,0 +1,54 @@ +package inc.pyc.chimera +package lib +package bill.acceptor + +import currency.Currency +import driver.DriverCommands._ + +object AcceptorCommands { + + /** + * Command to notify that a bill was inserted + * and the value of the bill is `bill` + */ + case class Inserted(bill: Currency#Value) extends Data + + + /** + * Command to notify that a bill has been accepted + * and cannot be returned. + */ + case class Confirmed(bill: Currency#Value) + + + /** + * Command to turn on the driver and + * listen for inserted bills. + */ + case object Listen + + + /** + * Command to turn off the driver and + * stop listening for inserted bills. + */ + case object UnListen + + + /** + * Accept a bill in bill acceptor (escrow mode). + */ + case object Accept + + + /** + * Reject a bill in bill acceptor (escrow mode). + */ + case object Reject + + + /** + * Notification hat the bill acceptor can now be configured + */ + case object Configurable +} +\ No newline at end of file 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 @@ -2,16 +2,18 @@ package inc.pyc.chimera package lib package bill.acceptor -import config._ import currency._ +import AcceptorCommands._ import driver._ +import DriverCommands._ 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._ +import inc.pyc.lift_akka.LiftActorEventBus + object BillAcceptor extends ExtensionId[BillAcceptorImpl] with ExtensionIdProvider { override def lookup = BillAcceptor @@ -21,6 +23,13 @@ object BillAcceptor extends ExtensionId[BillAcceptorImpl] with ExtensionIdProvid * Subscription topics for event bus. */ val insertedBill = "insertedBill" + val confirmedBill = "confirmedBill" + + + /** + * Event Bus for Chimera internal. + */ + val bus = new LookupBillAcceptor /** @@ -33,8 +42,7 @@ object BillAcceptor extends ExtensionId[BillAcceptorImpl] with ExtensionIdProvid /** * Bill Acceptor API implementation */ -class BillAcceptorImpl(system: ExtendedActorSystem) extends Extension { - import commands._ +sealed class BillAcceptorImpl(system: ExtendedActorSystem) extends Extension { /** * The main actor that handles communications. @@ -48,55 +56,79 @@ class BillAcceptorImpl(system: ExtendedActorSystem) extends Extension { class BillAcceptor extends Actor with ActorLogging { - import commands._ - import Machine.bus - import Machine.topics._ + import config.Machine.topics._ // TODO get rid of this dependency + import BillAcceptor._ import context.dispatcher + import context.system implicit val timeout = Timeout(2 seconds) + override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { case ex => + log.error("Driver unexpectedly crashed: {}", ex.getMessage) + context become receive driverMalfunction() Restart } - + /** The currency that is currently accepted. */ - private var currency: Currency = Machine.currency + private var currency: Currency = findCurrency(config.getString("currency")) + /** Bill Acceptor Driver */ - private val driver = BillAcceptor.config.getString("driver") + private val driverName = config.getString("driver") + /** Whether the bill acceptor is working. */ private var operating = true + + /** Tries to get the driver to operate if it's not operating. */ + private val tryTicker = system.scheduler.schedule(0 seconds, 1 minute)(tryDriver) + + /** * Messages when not listening for inserted bills. */ def receive = { - case Listen => - val driver = initDriver - driver ! Listen + case Listen => + val fsm = initDriver(driverName) + context become waiting(fsm) + context watch fsm + fsm ! Listen + + case m => + log.warning("Received Unknown Message: {}", m) + } + + + /** + * Bill Acceptor is waiting for the FSM to respond + * so it can start listening + */ + def waiting(fsm: ActorRef): Receive = { + 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") + val fsm = sender + startListening(fsm) + + + case Terminated(`fsm`) => + context become receive + driverMalfunction() + + + case Configurable => + context become configurable + + + case m => + log.warning("Received Unknown Message (Waiting): {}", m) } @@ -104,65 +136,113 @@ class BillAcceptor extends Actor with ActorLogging { * Bill Acceptor is in listening mode, * ready for someone to insert a bill. */ - def listening(driver: ActorRef): Receive = { + def listening(fsm: ActorRef): Receive = { + + case Accept => + fsm ! Accept + + + case Reject => + fsm ! Reject + + + case Configurable => + context become configurable + case Inserted(bill) => log.info(s"Inserted bill: $bill $currency") - bus.publish(EventUpdate(BillAcceptor.insertedBill, bill)) + bus.publish(EventUpdate(insertedBill, bill)) + + + case Confirmed(bill) => + log.info(s"Confirmed bill: $bill $currency") + bus.publish(EventUpdate(confirmedBill, bill)) + case UnListen => log.info("Stopped listening for bills") - context unwatch driver + fsm ! UnListen + + + case Disconnected => + context unwatch fsm + fsm ! Shutdown context become receive - case Terminated(`driver`) => + + case Terminated(`fsm`) => log.error("Driver unexpectedly crashed") + context become receive driverMalfunction() - case _ => - log.warning("Received Unknown Message") + + case m => + log.warning("Received Unknown Message (Listening): {}", m) + } + + + /** + * State where bill acceptor can be given + * commands so it can be configured. + */ + def configurable: Receive = { + + case Ready => + val fsm = sender + startListening(fsm) + } /** - * Run this function when driver fails for any reason. + * Bill acceptor will begin to listen for bills. + */ + def startListening(fsm: ActorRef): Unit = + if (!operating) { + operating = true + log.info("Bill acceptor is operating again. Redirecting to index...") + bus.publish(EventUpdate(redirectTo, "/index")) + fsm ! UnListen + } else { + log.info("Listening for bills") + context become listening(fsm) + context watch fsm + } + + + /** + * Run this function when driver fails for whatever 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 } } + /** + * Tries to get the driver to operate if it's not operating. + */ + def tryDriver(): Unit = + if(!operating) self ! Listen + - private def initDriver : ActorRef = { - initDriver( findBillAcceptorDriver(driver) ) - } - private def initDriver(service: BillAcceptorDriver.Value): ActorRef = { - context.actorOf(BillAcceptorDriver.getDriverActor(service), service.toString) + /** + * Initiates a bill acceptor finite state machine actor that will communicate with the driver + * given the driver's protocol name. + */ + private def initDriver(service: String): ActorRef = { + context.actorOf(BillAcceptorDriver.getDriver(service), service.toString) } } -private[acceptor] object commands { - /** - * Command to notify that a bill was inserted - * and the value of the bill is `bill` - */ - case class Inserted(bill: Int) - - /** - * Command to turn on the driver and - * listen for inserted bills. - */ - case object Listen - - /** - * Command to turn off the driver and - * stop listening for inserted bills. - */ - case object UnListen +/** + * PubSub for Bill Acceptor topics. + */ +class LookupBillAcceptor extends LiftActorEventBus { + override protected def mapSize: Int = 10 } \ No newline at end of file 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 @@ -1,19 +0,0 @@ -package inc.pyc.chimera.lib -package bill.acceptor -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 = { - case _ => - } -} -\ No newline at end of file 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 @@ -1,52 +0,0 @@ -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,26 +2,28 @@ package inc.pyc.chimera.lib package bill.acceptor package driver -import bill.acceptor.commands._ +import AcceptorCommands._ import akka.actor._ import akka.io.IO import akka.util.ByteString import com.github.jodersky.flow._ import com.github.jodersky.flow.Serial._ +import scala.concurrent.duration._ + /** - * The different choices for bitcoin services. + * The different supported of bill acceptors. */ object BillAcceptorDriver extends Enumeration { type BillAcceptorDriver = Value val ID003, APEX = Value + + def drivers = Map( + ID003 -> Props[id003.ID003FSM] + ) - def drivers: Map[BillAcceptorDriver, Props] = Map( - ID003 -> Props[ID003], - APEX -> Props[APEX]) - - def getDriverActor(s: Value): Props = { - drivers.filter(_._1 == s).map(_._2).head + def getDriver(s: String): Props = { + drivers.filter(_._1.toString == s).map(_._2).head } } @@ -32,17 +34,30 @@ object BillAcceptorDriver extends Enumeration { private[driver] trait Driver { this: Actor with ActorLogging => - import commands._ + import DriverCommands._ import context.system + import context.dispatcher + + + /** The FSM is the the parent actor */ + val fsm = context parent + /** - * Serial port (/dev/tty*) + * Command to poll bill acceptor's status. */ + val poll: Input + + + /** Serial port (/dev/tty*) */ val port: String = BillAcceptor.config.getString("port") + + + /** Size of buffer */ + val bufferSize: Int = BillAcceptor.config.getInt("buffer-size") - /** - * Settings for serial. - */ + + /** Settings for serial */ val settings = SerialSettings( baud = BillAcceptor.config.getInt("baud"), characterSize = BillAcceptor.config.getInt("char-size"), @@ -50,11 +65,16 @@ private[driver] trait Driver { parity = Parity(BillAcceptor.config.getInt("parity")) ) + /** * Handle data passing with this partial function * when it's in listening mode. */ val handleData: Receive + + + lazy val startPoll = system.scheduler.schedule( + 1 second, 100 milliseconds, self, poll) /** @@ -63,65 +83,93 @@ private[driver] trait Driver { def receive: Receive = { case Listen => - log.info("Requesting manager to open port: {}, baud: {}", port, settings.baud) - IO(Serial) ! Open(port, settings) + log.debug("Requesting manager to open port: {}, baud: {}", port, settings.baud) + IO(Serial) ! Open(port, settings, bufferSize) case CommandFailed(cmd, reason) => - log.error("Connection failed, stopping driver. Reason: {}", reason) - throw new CommandFailedException + throw new CommandFailedException(reason) case Opened(port) => - log.info("Port {} is now open", port) + log.debug("Port {} is now open", port) val operator = sender context become opened(operator) context watch operator - context.parent ! Ready + fsm ! Ready + startPoll + + + case Input(_) => + // ignore; not connected + + + case m => + log.warning("Received unknown message with port closed: {}", m) } /** * When driver is in listening mode. */ - def opened(operator: ActorRef): Receive = { + def opened(operator: ActorRef): Receive = handleData orElse { case Received(data) => - log.debug("Received data: {}", formatData(data)) + log.debug("Received data: {}", formatData(data)) case Closed => - log.info("Operator closed normally, exiting driver") + log.debug("Operator closed normally, exiting driver") context unwatch operator - context stop self + context become receive + fsm ! Closed case Terminated(`operator`) => - log.error("Operator crashed, exiting driver") throw new OperatorCrashException + + + case Shutdown => + log.debug("Trying to shutdown operator") + startPoll.cancel + operator ! PoisonPill + context become shuttingdown(operator) + + + case ForceShutdown => + log.debug("Trying to terminate operator") + startPoll.cancel + operator ! Kill + self ! Kill case UnListen => - log.info("Driver's serial connection is closing") + log.debug("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))) + case Input(data) => + if(data != poll.input) log.info("Writing data: {}", data) + operator ! Write(data) + + + case m => + log.warning("Received unknown message with port open: {}", m) } - 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) + def shuttingdown(operator: ActorRef): Receive = { + + case Terminated(`operator`) => + log.debug("Operator terminated normally, exiting driver") + context stop self + + case m => + log.warning("Received unknown message while shutting down: {}", m) + } + + + protected def formatData(data: ByteString) = + data.map("0x"+Integer.toHexString(_)).mkString("[", ",", "]") } \ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/DriverCommands.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/DriverCommands.scala @@ -0,0 +1,82 @@ +package inc.pyc.chimera +package lib +package bill.acceptor +package driver + +import akka.AkkaException +import scala.util.control.NoStackTrace +import akka.util.ByteString +import com.github.jodersky.flow.Serial.Event +import currency.Currency + + +private[acceptor] object DriverCommands { + + /** + * Shutdown actor + */ + case object Shutdown + + + /** + * Force Shutdown actor + */ + case object ForceShutdown + + + /** + * When the driver is ready to send out commands. + */ + case object Ready + + + /** + * Command to send data to serial. + */ + case class Input(input: ByteString) + + + /** + * Command when writing to the serial actor. + */ + case class Wrote(data: ByteString) extends Event + + + /** + * Data for Driver FSM's + */ + trait Data + + + /** + * The driver sent a message with no data. + */ + case object NoData extends Data + + + /** + * String message to send as data. + */ + case class Message(data: String) extends Data + + + /** + * Status (or state) for Driver FSM's + */ + trait Status + + + /** + * When the driver is not listening to the serial port. + */ + case object Disconnected extends Status + + + // exceptions + class CommandFailedException(reason: Throwable) extends + AkkaException("Connection failed, stopping driver. Reason: "+reason , reason) with NoStackTrace + + class OperatorCrashException extends + AkkaException("Operator crashed, exiting driver", new Throwable) with NoStackTrace + +} +\ 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 @@ -1,117 +0,0 @@ -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/id003/Commands.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/id003/Commands.scala @@ -0,0 +1,525 @@ +package inc.pyc.chimera +package lib +package bill.acceptor +package driver +package id003 + +import currency._ +import ID003Decoder._ +import DriverCommands._ +import akka.util.ByteString +import scala.collection._ + + +/** + * Refer to page 9 of the PDF. + */ +private[id003] object ID003Commands { + + /** + * Messages from Host to Acceptor + */ + sealed trait Request { + val cmd: ByteString + } + + + /** + * Messages from Acceptor to Host + */ + sealed trait Response + + + /** + * Poll message requesting for status + */ + case object StatusRequest extends Request { + val cmd = ByteString(0x11) + } + + + /** + * Acknowledgement + */ + case object Ack extends Request with Response { + val cmd = ByteString(0x50) + } + + + /** + * Operation command from the Host to the Acceptor. + */ + sealed trait OperationCommand extends Request + + + /** + * Reset the acceptor. + * Command is used to clear power up status and enter INITIALIZING status + */ + case object Reset extends OperationCommand { + val cmd = ByteString(0x40) + } + + + /** + * Stack the bill in escrow (or accept the bill). + * Command is valid only when Acceptor status is ESCROW + */ + case object Stack extends OperationCommand { + val cmd = ByteString(0x41) + } + + + /** + * Return the bill in escrow (or reject the bill). + * Command is valid only when Acceptor status is ESCROW + */ + case object Return extends OperationCommand { + val cmd = ByteString(0x43) + } + + + /** + * A bill in the escrow position will be held for 10 seconds. + * Command is valid only when Acceptor status is ESCROW + */ + case object Hold extends OperationCommand { + val cmd = ByteString(0x44) + } + + + /** + * Acceptor will stop and wait for 3 seconds. + * Command is valid while a bill is being processed. + * (ACCEPTING, ESCROW, STACKING, STACKED etc) + */ + case object Wait extends OperationCommand { + val cmd = ByteString(0x45) + } + + + /** + * ..... ? + */ + case object Signature extends OperationCommand { + val cmd = ByteString(0xdc) + } + + + /** + * Any request that requires data input. + */ + sealed trait SetRequest extends Request { + def data: ByteString + } + + + /** + * Response Echo + */ + sealed trait EchoResponse extends Response with SetRequest + + + /** + * Set Command is used to change the functionality of the Acceptor. + * Most of these Set Commands are valid only when the Acceptor status is INITIALIZING + * and will respond with an echo of the command. + */ + sealed trait SetCommand extends SetRequest + + + /** + * Commands are used to interrogate the acceptor + * for current setting information + */ + sealed trait SetStatus extends SetRequest + + + /** + * Enable/disable each denomination that the Acceptor is programmed to recognize. + */ + trait DenominationCommand extends SetCommand with SetStatus { + val cmd = ByteString(0xc0) + } + + + case object Denominations extends DenominationCommand with EchoResponse { + def data = ByteString() + } + + + case object EnableAllUSD extends DenominationCommand { + def data = ByteString(0x82, 0x00) + } + + + /** + * To enable only a specific values of a currency, send this command. + * + * ie. Allow only $10, $20, $50 and $100 bills. + * + * val disable = USD.`1` + USD.`5` + * BillAcceptor ! Denominations(USD, disable) + */ + case class Denominations[T <: Currency](currency: T, values: Currency#ValueSet) extends DenominationCommand with SetCommand with SetStatus { + + def data: ByteString = { + + val currencyValues = CURRENCY_VALUE(currency) + + // don't mind the confusion + val indices = values.map((v: Currency#Value) => v.id) map { + value => currencyValues.find(_._2 == value) + } flatMap (_.map(_._1)) map (currencyValues.keyIndex) + + val byte = indices map (1 << _) sum + + // java will reset to 0x00 if greater than 0xff :( + val byte2 = byte - 0xff + + if(byte2 < 0) ByteString(byte, 0x00) + else ByteString(byte, byte2) + } + } + + + /** + * Validation security level for each denomination + */ + trait Security extends SetCommand with SetStatus { + val cmd = ByteString(0xc1) + } + + + object Security extends Security with EchoResponse { + def data = ByteString() + } + + + case object StandardSecurity extends Security { + def data = ByteString(0x00, 0x00) + } + + + case object SecurityHighUSD extends Security { + def data = ByteString(0x7d, 0x00) + } + + + /** + * Set the communication mode to be used + */ + trait CommunicationMode extends SetCommand { + val cmd = ByteString(0xc2) + } + + + object CommunicationMode extends CommunicationMode with EchoResponse { + def data = ByteString() + } + + + case object CommunicationPolled extends CommunicationMode { + def data = ByteString(0x00) + } + + + case object CommunicationInterrupt1 extends CommunicationMode { + def data = ByteString(0x01) + } + + + case object CommunicationInterrupt2 extends CommunicationMode { + def data = ByteString(0x02) + } + + + /** + * Turn on/off acceptance of all currency and coupons. + * Inhibit enables and uninhibit disables. + */ + trait InhibitCMD extends SetCommand with SetStatus { + val cmd = ByteString(0xc3) + } + + + object InhibitCMD extends InhibitCMD with EchoResponse { + def data = ByteString() + } + + + case object Inhibit extends InhibitCMD { + def data = ByteString(0x01) + } + + + case object UnInhibit extends InhibitCMD { + def data = ByteString(0x00) + } + + + /** + * Set the directions in which currency will be accepted + */ + trait DirectionCMD extends SetCommand with SetStatus { + val cmd = ByteString(0xc4) + } + + + case object DirectionCMD extends DirectionCMD with EchoResponse { + def data = ByteString() + } + + + case object DirectionAll extends DirectionCMD { + def data = ByteString(0x00) + } + + + case class Direction( + faceUpLeft: Boolean = true, + faceUpRight: Boolean = true, + faceDownLeft: Boolean = true, + faceDownRight: Boolean = true) extends DirectionCMD { + + def data = { + var result = 0x00 + if(!faceUpLeft) result += 0x01 + if(!faceUpRight) result += 0x02 + if(!faceDownLeft) result += 0x04 + if(!faceDownRight) result += 0x08 + ByteString(result) + } + } + + + /** + * Request Escrow code / Denomination assignments from the Acceptor + * TODO Not yet implemented.... + */ + case class CurrencyAssignment(bytes: Byte) extends SetStatus { + val cmd = ByteString(0x8a) + def data = ByteString() + } + + + /** + * Command sent by the Host is not valid. + */ + case object InvalidCommand extends Response + + + /** + * following messages are used to reply to a + * STATUS REQUEST [11H] message from the Host + */ + sealed trait StatusResponse extends Status with Response + + + /** + * The Acceptor is ready to accept currency + */ + case object Idle extends StatusResponse + + + /** + * The Acceptor is drawing the bill in and + * examining it with validation sensors + */ + case object Accepting extends StatusResponse + + + /** + * Bill validation is complete. + * One byte of data is sent with this status + * to indicate the inserted denomination. + */ + case object Escrow extends StatusResponse { + + def extract(data: Byte, currency: Currency) = { + CURRENCY_VALUE(currency)(data) + } + + } + + + /** + * A bill is being transported to the cashbox + */ + case object Stacking extends StatusResponse + + + /** + * Bill accepted confirmation signal + * (The Acceptor can no longer return the bill). + * Vend Valid status is held until an ACK message is received from the Host. + */ + case object VendValid extends StatusResponse + + + /** + * Status is reported after VEND VALID status + * until the Acceptor returns to IDLE status + */ + case object Stacked extends StatusResponse + + + /** + * The Acceptor judged a bill invalid or the Host + * disabled acceptance of a specific denomination via command + */ + case object Rejecting extends StatusResponse { + + def extract(data: Byte) = { + + 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") + + REJECTION_REASONS(data) + } + } + + + /** + * During ESCROW status, the acceptor received a RETURN + * command from the Host; the bill is being returned. + */ + case object Returning extends StatusResponse + + + /** + * During ESCROW status, the Acceptor received a HOLD + * command from the Host; the acceptor is holding the bill in escrow + */ + case object Holding extends StatusResponse + + + /** + * The Acceptor will not draw any notes into the path during this status. + */ + case object Disabled extends StatusResponse + + + /** + * The Acceptor is conducting some initial self-tests + */ + case object Initializing extends StatusResponse + + + /** + * The Acceptor is calculating a CRC for the Host. + */ + case object SigBusy extends StatusResponse + + + /** + * The Acceptor has finished calculating a CRC for the Host + */ + case object SigEnd extends StatusResponse + + + /** + * Standard response when the acceptor receives power + */ + case object PowerUp extends StatusResponse + + + /** + * Upon receiving power, the acceptor detected a bill in the head unit. + * When a RESET message is received from the Host, the Acceptor will + * first return the bill and then enter INITIALIZING status. + */ + case object PowerUpBillInAcceptor extends StatusResponse + + + /** + * Upon receiving power, the Acceptor detected a bill in the transport unit. + * When a RESET command is received from the Host, the Acceptor will finish + * stacking the bill and then enter INITIALIZING status + */ + case object PowerUpBillInStacker extends StatusResponse + + + /** + * The cashbox is full + */ + case object StackerFull extends StatusResponse + + + /** + * The cashbox is either removed or not completely installed + */ + case object StackerOpen extends StatusResponse + + + /** + * A bill is jammed inside the Acceptor head + */ + case object JamInAcceptor extends StatusResponse + + + /** + * A bill jam occurred in the transport section of the Acceptor or + * an abnormality was detected while the bill was being pushed inside the cashbox + */ + case object JamInStacker extends StatusResponse + + + /** + * The Acceptor has stopped because a second bill was inserted while + * the first bill is being transported and stacked. + * When the second bill is removed, the Acceptor will continue + * with transporting and stacking the first bill. + */ + case object Pause extends StatusResponse + + + /** + * The Acceptor has detected an action thought to be mischievous + */ + case object Cheated extends StatusResponse + + + /** + * Normal Acceptor operation can not continue because of a + * failure, abnormal condition or incorrect setting + */ + case object Failure extends StatusResponse { + + def extract(data: Byte) = { + + val FAILURE_REASONS = Map( + 0xa2 -> "stack motor failure", + 0xa5 -> "Transport (feed) Motor speed failure", + 0xa6 -> "Transport (feed) Motor failure", + 0xab -> "Cashbox not ready", + 0xaf -> "Validator head is removed or wrong type isinstalled", + 0xb0 -> "Boot ROM failure", + 0xb1 -> "External ROM failure", + 0xb2 -> "ROM failure", + 0xb3 -> "External ROM writing failure") + + FAILURE_REASONS(data) + } + } + + + /** + * An error has developed in the communication data + */ + case object CommunicationError extends StatusResponse +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/id003/Decoder.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/id003/Decoder.scala @@ -0,0 +1,90 @@ +package inc.pyc.chimera +package lib +package bill.acceptor +package driver +package id003 + +import currency._ +import ID003Commands._ +import scala.collection.immutable.SortedMap + +private[id003] object ID003Decoder { + + + /** + * Status codes + */ + val STATUS = Map( + 0x11 -> Idle, + 0x12 -> Accepting, + 0x13 -> Escrow, + 0x14 -> Stacking, + 0x15 -> VendValid, + 0x16 -> Stacked, + 0x17 -> Rejecting, + 0x18 -> Returning, + 0x19 -> Holding, + 0x1a -> Disabled, + 0x1b -> Initializing, + 0xde -> SigBusy, + 0xdf -> SigEnd, + 0x40 -> PowerUp, + 0x41 -> PowerUpBillInAcceptor, + 0x42 -> PowerUpBillInStacker, + 0x43 -> StackerFull, + 0x44 -> StackerOpen, + 0x45 -> JamInAcceptor, + 0x46 -> JamInStacker, + 0x47 -> Pause, + 0x48 -> Cheated, + 0x49 -> Failure, + 0x4a -> CommunicationError) + + + + val RESPONSE = Map( + 0xc0 -> Denominations, + 0xc1 -> Security, + 0xc2 -> CommunicationMode, + 0xc3 -> InhibitCMD, + 0xc4 -> DirectionCMD + ) + + + + /** + * Decode and encode currency value + */ + def CURRENCY_VALUE(currency: Currency) = SortedMap( + 0x61 -> CURRENCY_VALUES(currency)(0), + 0x62 -> CURRENCY_VALUES(currency)(1), + 0x63 -> CURRENCY_VALUES(currency)(2), + 0x64 -> CURRENCY_VALUES(currency)(3), + 0x65 -> CURRENCY_VALUES(currency)(4), + 0x66 -> CURRENCY_VALUES(currency)(5), + 0x67 -> CURRENCY_VALUES(currency)(6), + 0x68 -> CURRENCY_VALUES(currency)(7)) + + + /** + * Country specific data codes. + * The values of the array is the value of the bill. + * If value is zero, bit is reserved. + * + * Refer to Appendix E in the PDF. + */ + val CURRENCY_VALUES: Map[Currency, Array[Int]] = Map( + USD -> Array( 1, 0, 5, 10, 20, 50, 100, 0) + ) + + + /** + * Country Number Code + */ + val CURRENCY: Map[Int, Currency] = Map( + 0x01 -> USD, + 0x27 -> USD, + 0x46 -> USD + ) + +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/id003/Driver.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/id003/Driver.scala @@ -0,0 +1,195 @@ +package inc.pyc.chimera.lib +package bill.acceptor +package driver +package id003 + +import ID003Decoder._ +import AcceptorCommands._ +import DriverCommands._ +import ID003Commands._ +import akka.actor._ +import FSM.Event +import com.github.jodersky.flow.Serial._ +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 ID003Driver extends Actor with ActorLogging with Driver { + + import context.system + import context.dispatcher + + + val handleData: Receive = { + + case Received(data) => + buffer = buffer ++ data + //log.info("putting {} into {}", data, buffer) + buffer = syncrhronize(buffer) + + if(bufferIsReady) + parsePacket(buffer(1)) + + + case request: SetRequest => + send(request) + + + case request: Request => + send(request) + + } + + + /** Data buffer incoming from serial */ + var buffer = ByteString() + + + /** + * Currency that is currently being accepted. + * USD (0x01) is default. + */ + var currency = CURRENCY(0x01) + + + /** Message start code */ + val SYNC = ByteString(0xfc) + + + /** Cyclic redundancy check */ + val CRC = new CRC + + + /** sync + length + command + CRC */ + val poll = Input( + SYNC ++ + ByteString(0x05) ++ + StatusRequest.cmd ++ + ByteString(0x27, 0x56) + ) + + + /** + * Checks if buffer is ready to be parsed. + * Byte after `SYNC` is length of packet. + * + * Requirements are: + * 1. Buffer data must begin with a legitimate packet or `SYNC` + * 2. Entire packet must be available in the buffer. + */ + def bufferIsReady = + buffer.length > 1 && + buffer.length >= buffer(1) && + ByteString(buffer(0)) == SYNC + + + /** + * Tries to parse the next available packet in the buffer. + * @length the length of the packet + */ + def parsePacket(length: Int): Unit = { + val packet = buffer slice (0, length) + buffer = buffer drop length + parse (packet) + } + + + /** + * Tries to parse the packet. + * State is available in the 3rd byte. + */ + def parse(packet: ByteString): Unit = + if(validPacket(packet)) { + + STATUS get(packet(2)) map(_ match { + + case Escrow => + val value = Escrow.extract(packet(3), currency) + if(value != 0) { + val bill = currency(value) + fsm ! Event(Escrow, Inserted(bill)) + } + + + case Rejecting => + val reason = Rejecting.extract(packet(3)) + fsm ! Event(Rejecting, Message(reason)) + + + case Failure => + val reason = Failure.extract(packet(3)) + fsm ! Event(Failure, Message(reason)) + + + case status => + fsm ! Event(status, NoData) + }) + + + RESPONSE get(packet(2)) map(_ match { + case r => fsm ! r + }) + + + // TODO handle Ack and InvalidCommand + } + + + /** + * Validates the packet by computing and checking the CRC. + * CRC is 2 bytes in length. + */ + def validPacket(packet: ByteString): Boolean = { + val payload = packet dropRight 2 + val crc = packet takeRight 2 + val compute = CRC.compute(payload toArray) takeRight 2 + val realcrc = ByteString(compute) + realcrc == crc.reverse + } + + + /** + * Generates CRC for the payload. + * CRC is 2 bytes in length. + */ + def crc(payload: ByteString): ByteString = { + val compute = CRC.compute(payload toArray) takeRight 2 + val crc = ByteString(compute) + crc.reverse + } + + + /** + * Synchronizes the data in the buffer by finding + * `SYNC` in data and returning all bytes after that. + * + * ie. + * jiberish + garbage + sync + data => sync + data + */ + def syncrhronize(buffer: ByteString): ByteString = { + val i = buffer indexOf SYNC(0) + if(i < 0) buffer + else buffer drop i + } + + + /** + * Builds a packet and sends it to the bill acceptor. + */ + def send(request: Request, data: ByteString = ByteString()): Unit = { + val length = ByteString(0x05 + data.size) + val payload = SYNC ++ length ++ request.cmd ++ data + self ! Input(payload ++ crc(payload)) + } + + + /** + * Builds a packet and sends it to the bill acceptor. + */ + def send(request: SetRequest): Unit = send(request, request.data) +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/id003/Fsm.scala b/src/main/scala/inc/pyc/chimera/lib/bill/acceptor/driver/id003/Fsm.scala @@ -0,0 +1,313 @@ +package inc.pyc.chimera +package lib +package bill.acceptor +package driver +package id003 + +import currency._ +import AcceptorCommands._ +import DriverCommands._ +import ID003Commands._ +import akka.actor._ +import akka.pattern._ +import scala.concurrent._, duration._ +import com.github.jodersky.flow.Serial.Closed + + +/** + * The interface to communicate with the ID003 Bill Acceptor. + * @master Actor to report all updates from the driver + * @driver Actor that communicates with ID003 device + */ +class ID003FSM extends Actor with FSM[Status, Data] { + + + /** + * Status where bill acceptor is trying to recover. + */ + case object Trying extends Status + + + /** + * The last request is kept to re-send in case of a communication error + */ + private var lastRequest: Any = Listen + + + /** + * The last inserted bill. + */ + private var lastBill: Option[Currency#Value] = None + + + /** + * Report important updates from the bill acceptor to the master. + */ + val master: ActorRef = context parent + + + /** + * The driver that communicates with the bill acceptor. + */ + val driver: ActorRef = context.actorOf(Props[ID003Driver], "ID003Driver") + + + context.watch(driver) + + + startWith(Disconnected, NoData) + + + when (Disconnected) { + case Event(Listen, _) => + requestDriver (Listen) + stay + + case Event(Ready, _) => + stay forMax (4 second) + + + case Event(Shutdown, _) => + context stop driver + context stop self + stay + + case Event(StateTimeout, _) => + requestDriver(Reset) + goto (Trying) + } + + + when(Trying, stateTimeout = 2 second) { + case Event(StateTimeout, _) => + driver ! ForceShutdown + failFSM("Driver is not responding") + } + + + when (Accepting) { + case Event(Escrow, Inserted(bill)) => + lastBill = Some(bill) + master ! Inserted(bill) + goto (Escrow) using Inserted(bill) + } + + + when (Escrow, stateTimeout = 8 second) { + case Event(Accept, _) => + requestDriver (Stack) + stay + + case Event(Reject, _) => + requestDriver (Return) + stay + + case Event(StateTimeout, _) => + requestDriver(Hold) + stay forMax (9 second) + } + + + // re-send VendValid Ack + when(VendValid, stateTimeout = 1 second) { + case Event(StateTimeout, _) => + requestDriver(Ack) + stay forMax (1 second) + } + + + when(CommunicationError, stateTimeout = 2 second) { + case Event(StateTimeout, _) => + requestDriver(Reset) + stay + } + + + when(Disabled)(FSM.NullFunction) + when(Idle)(FSM.NullFunction) + when(Stacking)(FSM.NullFunction) + when(Stacked)(FSM.NullFunction) + when(Rejecting)(FSM.NullFunction) + when(Returning)(FSM.NullFunction) + when(Holding)(FSM.NullFunction) + when(Initializing)(FSM.NullFunction) + when(SigBusy)(FSM.NullFunction) + when(SigEnd)(FSM.NullFunction) + when(PowerUp)(FSM.NullFunction) + when(PowerUpBillInAcceptor)(FSM.NullFunction) + when(PowerUpBillInStacker)(FSM.NullFunction) + when(StackerFull)(FSM.NullFunction) + when(StackerOpen)(FSM.NullFunction) + when(JamInAcceptor)(FSM.NullFunction) + when(JamInStacker)(FSM.NullFunction) + when(Pause)(FSM.NullFunction) + when(Cheated)(FSM.NullFunction) + when(Failure)(FSM.NullFunction) + + + onTransition { + + case _ -> VendValid => + log.info("VendValid Mode") + requestDriver(Ack) + lastBill.map(master ! Confirmed(_)) + lastBill = None + + case _ -> Escrow => // justa testing + log.info("Going in Escrow Mode...") + master ! nextStateData + + case _ -> Initializing => + log.info("Initializing") + master ! Configurable + + + case Disconnected -> Idle => + log.info("Disconnected to Idle Mode") + master ! Ready + + + case Initializing -> Idle => + log.info("Initializing to Idle Mode") + master ! Ready + + + case _ -> Disabled => + log.info("Disabled") + requestDriver(UnInhibit) + + + case _ -> PowerUpBillInStacker => + log.info("Powering up bill acceptor with bill in stacker. Trying to reset ...") + driver ! Reset + + + case PowerUpBillInStacker -> Initializing => + log.info("Bill stacker has been reset successfully") + + + case _ -> PowerUpBillInAcceptor => + log.info("Powering up bill acceptor with bill in acceptor. Trying to reset ...") + driver ! Reset + + + case PowerUpBillInAcceptor -> Initializing => + log.info("Bill acceptor has been reset successfully") + + + case _ -> StackerFull => + stackerUnavailable("Stacker is full", fail = false) + + + case Rejecting -> JamInAcceptor => + stackerUnavailable("Acceptor is jammed (while returning the bill)") + + + case _ -> JamInAcceptor => + stackerUnavailable("Acceptor is jammed") + + + case _ -> JamInStacker => + stackerUnavailable("Stacker is jammed") + + + case _ -> Rejecting => + log.warning("Bill is being rejected") + + + case _ -> Cheated => + log.warning("Someone is cheating") + + + case _ -> StackerOpen => + log.info("Cashbox is open") + // TODO reset amount of cash to zero + + + case StackerOpen -> _ => + log.info("Cash box is closed") + + + case a -> b => + log.info("Transition from {} to {}", a, b) + + } + + + whenUnhandled { + + case Event(UnListen, _) => + driver ! UnListen + stay + + + case Event(Closed, _) => + master ! Disconnected + goto (Disconnected) + + + case Event(e: EchoResponse, _) => + log.info("echo: cmd = {} data = {}", e.cmd.toArray, e.data.toArray) + stay + + + case Event(Terminated(`driver`), _) => + context stop self + goto (Disconnected) + + + case Event(e, s) => e match { + + + case Event(Failure, Message(reason)) => + failFSM("Failure in bill acceptor: " + reason) + + + case Event(CommunicationError, _) => + requestDriver(lastRequest) + stay + + + case Event(s: Status, _) => + if (stateName == s) stay + else goto (s) using (stateData) + + + case _ => + log.warning("received unhandled request {} in state {}/{}", e, stateName, s) + stay + } + + + } + + + /** + * Reports the problem to Lycia when the stacker + * is unavailable for whatever reason. + */ + private def stackerUnavailable(reason: String, fail: Boolean = true) = { + // TODO notify lycia + if(fail) failFSM(reason) + } + + + /** + * Fails this Finite State Machine. + */ + private def failFSM(reason: String) = { + stop(FSM.Failure(reason)) + } + + + /** + * Sends a request to the driver actor. + */ + private def requestDriver(any: Any): Unit = { + driver ! any + lastRequest = any + } + + + initialize() +} +\ 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,32 +0,0 @@ -package inc.pyc.chimera.lib.bill.acceptor - -import driver._ -import BillAcceptorDriver._ -import com.github.jodersky.flow.Serial._ -import akka.util.ByteString - -package object driver { - - /** - * 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/lib/currency/Currency.scala b/src/main/scala/inc/pyc/chimera/lib/currency/Currency.scala @@ -2,11 +2,11 @@ package inc.pyc.chimera.lib package currency abstract class Currency extends Enumeration { + def self = this + /** * Symbol of the currency */ val symbol: String - - implicit def toInt(c: Value): Int = c.toString.toInt } \ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/currency/USD.scala b/src/main/scala/inc/pyc/chimera/lib/currency/USD.scala @@ -2,7 +2,11 @@ package inc.pyc.chimera.lib package currency object USD extends Currency { - type USD = Value val symbol = "$" - val `1`, `5`, `10`, `20`, `50`, `100` = Value + val `1` = Value(1, "$1") + val `5` = Value(5, "$5") + val `10` = Value(10, "$10") + val `20` = Value(20, "$20") + val `50` = Value(50, "$50") + val `100` = Value(100, "$100") } \ No newline at end of file diff --git a/src/main/scala/inc/pyc/chimera/lib/currency/package.scala b/src/main/scala/inc/pyc/chimera/lib/currency/package.scala @@ -1,11 +1,47 @@ package inc.pyc.chimera.lib +import scala.collection.SortedSet + + package object currency { + + /** + * All supported currencies. + */ val currencies: Map[String, Currency] = Map( "USD" -> USD ) + + /** + * Finds the currency given the string value. + */ def findCurrency(s: String): Currency = currencies.filter(_._1 == s).map(_._2).head + + + /** + * Finds the sum of a set of currency values. + */ + def sum(set: Currency#ValueSet): Int = + set.map((x: Currency#Value) => x.id).sum + + + /** + * Finds the index of a map value given the key or key and value. + * TODO this belongs somewhere else + */ + implicit class keyIndexing[A, B](val m: Map[A, B]) extends AnyVal { + + /** Find index with key */ + def keyIndex(key: A) = { + m.toArray.indexWhere { case (k, v) => key == k } + } + + /** Find index with key and value */ + def keyIndex(key: A, value: B) = { + m.toArray.indexWhere { case (k, v) => key == k && value == v } + } + } } \ No newline at end of file