Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull request to integrate Granboard #8

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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 <br>
`PORT` Defines the server port, default: 3000 <br>
`PROTOCOL` Defines the server protocol, default: http <br>
`UUID` Can be defined if you want to use another UUID than the one provided in the kcapp venue settings. e.g. =f6a83f1136d3 <br>
`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 <br>
`DEBUG` Can be used to display debug messages: =kcapp* will display all messages from kcapp services. <br>


### Connect
```javascript
const smartboard = require('./smartboard')();
Expand Down Expand Up @@ -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
```

185 changes: 185 additions & 0 deletions granboard.js
Original file line number Diff line number Diff line change
@@ -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];
}
}
thordy marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@S-I-M-O-N any idea what this specific value is? Maybe add a comment to explain why we ignore it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is an initializing value the board sends after subscribing to the notify. As we do not know if this message is the same for all versions of the board, I updated the code to reject all undefined messages. See new commit.

} 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;
};
77 changes: 73 additions & 4 deletions kcapp-smartboard.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,14 +33,15 @@ 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;
thordy marked this conversation as resolved.
Show resolved Hide resolved
if (match.venue && config.has_smartboard) {
debug(`Connected to match ${match.id}`);
kcapp.connectLegNamespace(legId, (leg) => {
debug(`Connected to leg ${legId}`);
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;

Expand Down Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the GranBoard really require us to reconnect here?
For the Unicorn board, I believe this won't work, since we are already connected.

If it does we should check here for which board currently is in use, and then only initialize if it's a GranBoard, otherwise skip.

It also seems like this is exactly the same as lines 48:87, so we could also extract those out into a separate method to reduce code duplication

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the majority of the code of the first branch is duplicated here.

Reconnection does not seem necessary, but the listener is also in the initialize function and thus it was the easiest way of establishing this.

It might work with less code, but except for one error thrown because we are already connected the code just works.

It also allows to keep kcapp-smartboard running as a daemon which will connect to new legs and to the board if you switch it on again. No need to launch it for each session.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you need to run the initialize function for each leg on the Granboard? Does it also keep track of legs played and stuff like that and needs a reset?
For the Unicorn Smartboard it just emits whatever you throw, so it doesn´t need to reestablish it. I also don´t currently have easy access to a Unicorn board to test this change, but I'm a bit worried that calling initialize again here might cause issues, since we are already connected and established above.

Regarding reconnecting, this was solved by having the "Reconnect Smartboard" button in the Frontend

  • From a Leg, click "Options" => "Change Order", this will bring up the following dialog
    image
    Here you can click "Reconnect Smartboard" and it should make the request to reconnect the board if it was restarted/disconnected somehow.

This button will only be available if a Venue is selected when starting the match with smartboard = true, so make sure you configure your venues correctly by going to <kcapp>/offices and adding/editing a venue and enabling smartboard
image

Could you see if this works for your case as well?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reconnect button only triggers the message that the board has already been connected. In fact it is (showing a green light instead of red), but kcapp is not listening for any events from the board and thus not registering them. Besides on a smartphone the Change order menu is not usable as the pop-up is too large to be completely visible. Panning or zooming is also not possible.

Are you sure it works with more than one leg on the Unicorn board? The score is in the object dart which is handled inside the initialize function.

In fact the the if else only handles two options:

  1. No connection has been established and thus scanning and connecting is triggered.
  2. A connection has already been established and scanning and connecting can be omitted.

But in either case you should do the rest. To be honest I thus do not understand the original code, in my opinion there is something missing in the else part.

(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)();
});
}
});
}
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down