From c077499beba4be8c4c17ed87d8dbdcca0325715e Mon Sep 17 00:00:00 2001 From: S-I-M-O-N Date: Wed, 20 Nov 2024 16:37:33 +0100 Subject: [PATCH 1/4] Integration of Granboard --- README.md | 54 ++++++++++++- granboard.js | 185 ++++++++++++++++++++++++++++++++++++++++++++ kcapp-smartboard.js | 77 +++++++++++++++++- package.json | 6 +- 4 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 granboard.js diff --git a/README.md b/README.md index c7205d9..8e7a714 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..ab80d47 --- /dev/null +++ b/granboard.js @@ -0,0 +1,185 @@ +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; i++) { + 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(); + if (rawValue == "BTN@") { + playerChangeCallback(); + } else if (rawValue == "GB5;101") { + } else { + var value = translate(rawValue); + 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" From 9aca35c4b75b3ce74e0387dd18d015622fe7db5b Mon Sep 17 00:00:00 2001 From: S-I-M-O-N Date: Wed, 20 Nov 2024 16:40:36 +0100 Subject: [PATCH 2/4] Update README.md fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e7a714..9ae943f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ NODE_ENV="gran" UUID="f6a83f1136d3" PORT=443 KCAPP_API="remoteserver.net" PROTOC 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) +For additional information on the Granboard protocol, see [ESP32-GranBoard-Client](https://github.com/SoftCyD/ESP32-GranBoard-Client) ### Environment variables From 8a9acad9e3949fa72f4a0fd4cd6a801bcf0d7a54 Mon Sep 17 00:00:00 2001 From: S-I-M-O-N Date: Thu, 21 Nov 2024 13:41:45 +0100 Subject: [PATCH 3/4] Update of the lookup code for the board messages. It will now reject all messages which have not been predefined and trigger a debug message. --- granboard.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/granboard.js b/granboard.js index ab80d47..66545eb 100644 --- a/granboard.js +++ b/granboard.js @@ -30,8 +30,10 @@ const CHARACTERISTIC_THROW_NOTIFICATIONS = "442f15718a009a28cbe1e1d4212d53eb"; * @param {string} message - Message sent by board */ function translate(message) { - for (let i = 0; i < BOARD.length; i++) { - if (BOARD[i] === message) { + for (let i = 0; i < BOARD.length+1; i++) { + if (i == BOARD.length) { + return "ERROR"; + } else if (BOARD[i] === message) { return VALUES[i]; } } @@ -105,11 +107,12 @@ exports.initialize = (peripheral, buttonNumber, throwCallback, playerChangeCallb throwNotifyCharacteristic.on('data', (data, isNotification) => { var rawValue = data.toString(); - if (rawValue == "BTN@") { + var value = translate(rawValue); + if (value == "BTN") { playerChangeCallback(); - } else if (rawValue == "GB5;101") { + } else if (value == "ERROR") { + debug(`Message from board could not be translated: ${rawValue}`); } else { - var value = translate(rawValue); var dart = { score: parseInt(value.split("-")[0]), multiplier: parseInt(value.split("-")[1]) From e185607110438867ef17bf6abc8da37165bfc2fc Mon Sep 17 00:00:00 2001 From: S-I-M-O-N Date: Tue, 26 Nov 2024 13:51:57 +0100 Subject: [PATCH 4/4] Update of require statement to point noble to the abandonware library. --- smartboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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];