diff --git a/README.md b/README.md
index c7205d9..9ae943f 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,46 @@
![logo](https://raw.githubusercontent.com/wiki/kcapp/smartboard/images/logo.png)
# smartboard
-Integration of [Unicorn Smartboard](https://www.unicornsmartboard.com/smartboard.html) into kcapp
+Integration of
+[Unicorn Smartboard](https://www.unicornsmartboard.com/smartboard.html)
+&
+[Granboard](https://granboards.com)
+into kcapp
## Usage
+
+To launch the service first download the dependancies with:
+
+```
+npm install
+```
+
+Then you can launch the default configuration with a Unicorn Smartboard with:
+
+```
+npm start
+```
+
+To connect to a remote kcapp server with an additional Granboard issue something like:
+
+```
+NODE_ENV="gran" UUID="f6a83f1136d3" PORT=443 KCAPP_API="remoteserver.net" PROTOCOL="https" node kcapp-smartboard
+```
+
For detailed information and usage, see the [Wiki](https://github.com/kcapp/smartboard/wiki)
+For additional information on the Granboard protocol, see [ESP32-GranBoard-Client](https://github.com/SoftCyD/ESP32-GranBoard-Client)
+
+
+### Environment variables
+
+`KCAPP_API` Defines the server name, default: localhost
+`PORT` Defines the server port, default: 3000
+`PROTOCOL` Defines the server protocol, default: http
+`UUID` Can be defined if you want to use another UUID than the one provided in the kcapp venue settings. e.g. =f6a83f1136d3
+`NODE_ENV` Can be used to select which board ist installed: =prod if you want to connect a Unicorn Smartboard / =gran if you want to connect a Granboard
+`DEBUG` Can be used to display debug messages: =kcapp* will display all messages from kcapp services.
+
+
### Connect
```javascript
const smartboard = require('./smartboard')();
@@ -48,3 +84,19 @@ smartboard.disconnect(peripheral, () => {
// Disconnected
});
```
+
+## Troubleshooting
+
+On Linux it might be necessary to issue the following command:
+
+```
+sudo setcap cap_net_raw+eip $(eval readlink -f `which node`)
+```
+
+On the Pi the bluetooth module might be blocked. To unblock issue:
+
+```
+rfkill block bluetooth
+rfkill unblock bluetooth
+```
+
diff --git a/granboard.js b/granboard.js
new file mode 100644
index 0000000..66545eb
--- /dev/null
+++ b/granboard.js
@@ -0,0 +1,188 @@
+var debug = require('debug')('kcapp-granboard:board');
+var noble = require('@abandonware/noble');
+
+/** List containing all the messages send by the board. Used to translate to score and multiplier values. */
+const BOARD = ["4.0@", "8.0@", "3.3@", "3.4@", "3.5@", "3.6@", "2.3@", "2.4@", "2.5@", "2.6@",
+ "1.2@", "1.4@", "1.5@", "1.6@", "0.1@", "0.3@", "0.5@", "0.6@", "0.0@", "0.2@",
+ "0.4@", "4.5@", "1.0@", "1.1@", "1.3@", "4.4@", "2.0@", "2.1@", "2.2@", "4.3@",
+ "3.0@", "3.1@", "3.2@", "4.2@", "9.1@", "9.0@", "9.2@", "8.2@", "10.1@", "10.0@",
+ "10.2@", "8.3@", "7.1@", "7.0@", "7.2@", "8.4@", "6.1@", "6.0@", "6.3@", "8.5@",
+ "11.1@", "11.2@", "11.4@", "8.6@", "11.0@", "11.3@", "11.5@", "11.6@", "6.2@", "6.4@",
+ "6.5@", "6.6@", "7.3@", "7.4@", "7.5@", "7.6@", "10.3@", "10.4@", "10.5@", "10.6@",
+ "9.3@", "9.4@", "9.5@", "9.6@", "5.0@", "5.3@", "5.5@", "5.6@", "5.1@", "5.2@", "5.4@", "4.6@", "BTN@", "OUT@"];
+
+const VALUES = ["25-2", "25-0", "20-0", "20-3", "20-1", "20-2", "1-0", "1-3", "1-1", "1-2",
+ "18-0", "18-3", "18-1", "18-2", "4-0", "4-3", "4-1", "4-2", "13-0", "13-3",
+ "13-1", "13-2", "6-0", "6-3", "6-1", "6-2", "10-0", "10-3", "10-1", "10-2",
+ "15-0", "15-3", "15-1", "15-2", "2-0", "2-3", "2-1", "2-2", "17-0", "17-3",
+ "17-1", "17-2", "3-0", "3-3", "3-1", "3-2", "19-0", "19-3", "19-1", "19-2",
+ "7-0", "7-3", "7-1", "7-2", "16-0", "16-3", "16-1", "16-2", "8-0", "8-3",
+ "8-1", "8-2", "11-0", "11-3", "11-1", "11-2", "14-0", "14-3", "14-1", "14-2",
+ "9-0", "9-3", "9-1", "9-2", "12-0", "12-3", "12-1", "12-2", "5-0", "5-3", "5-1", "5-2", "BTN", "OUT"];
+
+/** Service containing board characteristics */
+const SERVICE_SCORING = "442f15708a009a28cbe1e1d4212d53eb";
+/** Characteristic to subscribe to throw notifications */
+const CHARACTERISTIC_THROW_NOTIFICATIONS = "442f15718a009a28cbe1e1d4212d53eb";
+
+/**
+ * Translate the given message
+ * @param {string} message - Message sent by board
+ */
+function translate(message) {
+ for (let i = 0; i < BOARD.length+1; i++) {
+ if (i == BOARD.length) {
+ return "ERROR";
+ } else if (BOARD[i] === message) {
+ return VALUES[i];
+ }
+ }
+}
+
+/**
+ * Start scanning for the board
+ */
+exports.startScan = () => {
+ debug("Started scanning for board");
+ noble.startScanning();
+}
+
+/**
+ * Connect to the dart board
+ * This method will add a callback to the discover method for bluetooth,
+ * and check all peripherals found until it finds one matching the UUID
+ * we are looking for
+ *
+ * @param {string} uuid - UUID of smartboard to connect to
+ * @param {function} callback - Callback when dart is thrown
+ */
+exports.connect = (uuid, callback) => {
+ this.discoverCallback = (peripheral) => {
+ if (peripheral.uuid === uuid) {
+ callback(peripheral);
+ debug("Found device, stopped scanning");
+ noble.stopScanning();
+ this.peripheral = peripheral;
+ }
+ };
+ noble.on('discover', this.discoverCallback);
+}
+
+/**
+ * Initialize the dart board, by setting up notification listeners
+ * for darts thrown, and button presses
+ *
+ * @param {object} - Peripheral object to initialize
+ * @param {int} - Number next to the board button
+ * @param {function} - Callback when dart is thrown
+ * @param {function} - Callback when button is pressed
+ */
+exports.initialize = (peripheral, buttonNumber, throwCallback, playerChangeCallback) => {
+ peripheral.connect((error) => {
+ if (error) {
+ debug(`ERROR: ${error}`);
+ }
+ debug(`Connected to ${peripheral.advertisement.localName} (${peripheral.uuid})`);
+
+ // Get the scoring service
+ peripheral.discoverServices([SERVICE_SCORING], (error, services) => {
+ if (error) {
+ debug(`ERROR: ${error}`);
+ }
+
+ var scoringService = services[0];
+ scoringService.discoverCharacteristics([CHARACTERISTIC_THROW_NOTIFICATIONS], (error, characteristics) => {
+ if (error) {
+ debug(`ERROR: ${error}`);
+ }
+
+ // Subscribe to throw notifications
+ var throwNotifyCharacteristic = characteristics[0];
+ throwNotifyCharacteristic.subscribe((error) => {
+ if (error) {
+ debug(`ERROR: ${error}`);
+ }
+ debug('Subscribed to throw notifications!');
+ });
+
+ throwNotifyCharacteristic.on('data', (data, isNotification) => {
+ var rawValue = data.toString();
+ var value = translate(rawValue);
+ if (value == "BTN") {
+ playerChangeCallback();
+ } else if (value == "ERROR") {
+ debug(`Message from board could not be translated: ${rawValue}`);
+ } else {
+ var dart = {
+ score: parseInt(value.split("-")[0]),
+ multiplier: parseInt(value.split("-")[1])
+ };
+ throwCallback(dart);
+ }
+ });
+ this.throwNotifyCharacteristic = throwNotifyCharacteristic;
+ });
+ });
+ });
+}
+
+/**
+ * Disconnect from the connected peripheral
+ * @param {object} - Connected peripheral
+ * @param {function} - Callback onces disconnected
+ */
+exports.disconnect = (peripheral, callback) => {
+ debug(`Removing 'discover' callback`);
+ noble.removeListener('discover', this.discoverCallback);
+
+ if (this.throwNotifyCharacteristic) {
+ this.throwNotifyCharacteristic.unsubscribe((error) => {
+ if (error) {
+ debug(`ERROR: ${error}`);
+ }
+ debug(`Unsubscribed from throw notifications`);
+ });
+ }
+ if (this.buttonCharacteristic) {
+ this.buttonCharacteristic.write(new Buffer([0x02]), true, (error) => {
+ if (error) {
+ debug(`ERROR: ${error}`);
+ }
+ debug(`Disabled listening on characteristic ${CHARACTERISTIC_BUTTON}`);
+ peripheral.disconnect((error) => {
+ if (error) {
+ debug(`ERROR: ${error}`);
+ }
+ debug(`Disconnected from ${peripheral.advertisement.localName}`);
+ if (callback) {
+ callback();
+ }
+ });
+ });
+ }
+}
+
+function interrupt() {
+ if (this.peripheral) {
+ debug("Caught interrupt signal, Disconnecting...");
+
+ this.disconnect(() => {
+ process.exit();
+ });
+
+ // Give the board 3 seconds to disconnect before we die....
+ setTimeout(() => {
+ process.exit();
+ }, 3000);
+ } else {
+ process.exit();
+ }
+}
+
+/**
+ * Configure the smartboard module
+ */
+module.exports = () => {
+ process.on('SIGINT', interrupt.bind(this));
+ return this;
+};
diff --git a/kcapp-smartboard.js b/kcapp-smartboard.js
index 92356d2..f59be6c 100644
--- a/kcapp-smartboard.js
+++ b/kcapp-smartboard.js
@@ -1,8 +1,16 @@
const debug = require('debug')('kcapp-smartboard:main');
-const smartboard = process.env.NODE_ENV === "prod" ? require('./smartboard')() : require("./smartboard-mock")();
+var smartboard;
+if (process.env.NODE_ENV == "prod"){
+ smartboard = require("./smartboard")();
+} else if (process.env.NODE_ENV == "gran"){
+ smartboard = require("./granboard")();
+} else {
+ smartboard = require("./smartboard-mock")();
+}
const host = process.env.KCAPP_API || "localhost";
const port = process.env.PORT || 3000;
-const kcapp = require('kcapp-sio-client/kcapp')(host, port, 'smartboard', "http");
+const protocol = process.env.PROTOCOL || "http";
+const kcapp = require('kcapp-sio-client/kcapp')(host, port, 'smartboard', protocol);
const X01 = 1;
const SHOOTOUT = 2;
@@ -25,6 +33,7 @@ function connectToMatch(data) {
const match = data.match;
const legId = match.current_leg_id;
const config = match.venue.config;
+ const smartboard_uuid = process.env.UUID || config.smartboard_uuid;
if (match.venue && config.has_smartboard) {
debug(`Connected to match ${match.id}`);
kcapp.connectLegNamespace(legId, (leg) => {
@@ -32,7 +41,7 @@ function connectToMatch(data) {
if (!this.connected) {
leg.emit('announce', { type: 'notify', message: 'Searching for smartboard ...' });
smartboard.startScan();
- smartboard.connect(config.smartboard_uuid, (peripheral) => {
+ smartboard.connect(smartboard_uuid, (peripheral) => {
this.connected = true;
this.peripheral = peripheral;
@@ -102,7 +111,67 @@ function connectToMatch(data) {
} else {
debug("Already connected to board...");
leg.emit('announce', { type: 'success', message: 'Already Connected' });
- leg.on('leg_finished', disconnectListener.bind(this));
+ smartboard.initialize(peripheral, config.smartboard_button_number,
+ (dart) => {
+ const player = leg.currentPlayer;
+ if (dart.multiplier == 0) {
+ dart.multiplier = 1;
+ dart.zone = 'inner';
+ } else if (dart.multiplier == 1) {
+ dart.zone = 'outer';
+ }
+ debug(`Got throw ${JSON.stringify(dart)} for ${player.player.id}`);
+ leg.emitThrow(dart);
+
+ if (match.match_type.id == SHOOTOUT) {
+ player.current_score += dart.score * dart.multiplier;
+ const visits = leg.leg.visits.length;
+ if (visits > 0 && ((visits * 3 + leg.dartsThrown) % (9 * leg.leg.players.length) === 0)) {
+ debug("Match finished! sending visit");
+ leg.emitVisit();
+ } else if (leg.dartsThrown == 3) {
+ leg.emitVisit();
+ }
+ }
+ else if (match.match_type.id == X01) {
+ player.current_score -= dart.score * dart.multiplier;
+
+ if (player.current_score === 0 && dart.multiplier === 2) {
+ debug("Player Checkout! sending visit");
+ leg.emit('announce', { type: 'confirm_checkout', message: "" });
+ } else if (player.current_score <= 1) {
+ debug("Player busted, sending visit");
+ leg.emitVisit();
+ } else if (leg.dartsThrown == 3) {
+ leg.emitVisit();
+ }
+ } else {
+ if (leg.dartsThrown == 3) {
+ leg.emitVisit();
+ }
+ }
+ },
+ () => {
+ debug("Button pressed, sending visit");
+ leg.emitVisit();
+ }
+ );
+
+ leg.on('leg_finished', (data) => {
+ debug(`Got leg_finished event!`);
+ const match = data.match;
+ if (match.is_finished) {
+ debug("Match is finished, disconnecting from board");
+ disconnectListener.bind(this)(data);
+ } else {
+ debug("Leg is finished");
+ }
+ });
+
+ leg.on('cancelled', (data) => {
+ debug("Leg cancelled, disconnecting from board");
+ disconnectListener.bind(this)();
+ });
}
});
}
diff --git a/package.json b/package.json
index 6be5cc3..e73b7b1 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,17 @@
{
"name": "kcapp-smartboard",
- "version": "0.0.2",
+ "version": "0.0.3",
"private": true,
"scripts": {
"start": "NODE_ENV=prod node kcapp-smartboard",
+ "gran": "NODE_ENV=gran node kcapp-smartboard",
"dev": "node kcapp-smartboard"
},
"dependencies": {
"debug": "^4.3.2",
"kcapp-sio-client": "github:kcapp/kcapp-sio-client#4f298b816e01e689d219abd55a6d4548bc2352f4",
- "noble": "^1.9.1"
+ "@abandonware/bluetooth-hci-socket": "^0.5.3-12",
+ "@abandonware/noble": "^1.9.2-25"
},
"optionalDependencies": {
"rpi-gpio": "^2.1.7"
diff --git a/smartboard.js b/smartboard.js
index b3fc36f..12c5c06 100644
--- a/smartboard.js
+++ b/smartboard.js
@@ -1,5 +1,5 @@
var debug = require('debug')('kcapp-smartboard:board');
-var noble = require('noble');
+var noble = require('@abandonware/noble');
/** List containing all numbers on the board. Used to shift scores when board is rotated */
const BOARD = [15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5, 20, 1, 18, 4, 13, 6, 10];