Skip to content

Commit

Permalink
Merge pull request #1532 from tidepool-org/bluetooth-windows
Browse files Browse the repository at this point in the history
Support for Bluetooth meters on Windows; add support for Fora TN'G Voice (UPLOAD-233, UPLOAD-783)
  • Loading branch information
gniezen authored Mar 20, 2023
2 parents d3d7921 + b96c04b commit 4a167c5
Show file tree
Hide file tree
Showing 25 changed files with 2,728 additions and 3,339 deletions.
7 changes: 7 additions & 0 deletions app/actions/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ export function doAppInit(opts, servicesToInit) {
log('Getting OS details.');
await actionUtils.initOSDetails();

ipcRenderer.on('bluetooth-pairing-request', async (event, details) => {
const displayBluetoothModal = actionUtils.makeDisplayBluetoothModal(dispatch);
displayBluetoothModal((response) => {
ipcRenderer.send('bluetooth-pairing-response', response);
}, details);
});

log('Initializing device');
device.init({
api,
Expand Down
15 changes: 15 additions & 0 deletions app/actions/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,21 @@ export function dismissedAdHocPairingDialog() {
};
}

export function bluetoothPairingRequest(callback, cfg) {
return {
type: ActionTypes.BLUETOOTH_PAIRING_REQUEST,
payload: { callback, cfg },
meta: { source: actionSources[ActionTypes.BLUETOOTH_PAIRING_REQUEST] }
};
}

export function dismissedBluetoothPairingDialog() {
return {
type: ActionTypes.BLUETOOTH_PAIRING_DISMISSED,
meta: { source: actionSources[ActionTypes.BLUETOOTH_PAIRING_DISMISSED] }
};
}

export function fetchPatientsForClinicRequest() {
return {
type: ActionTypes.FETCH_PATIENTS_FOR_CLINIC_REQUEST,
Expand Down
6 changes: 6 additions & 0 deletions app/actions/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export function makeDisplayAdhocModal(dispatch) {
};
}

export function makeDisplayBluetoothModal(dispatch) {
return (cb, cfg) => {
dispatch(syncActions.bluetoothPairingRequest(cb, cfg));
};
}

export function makeUploadCb(dispatch, getState, errCode, utc) {
return async (err, recs) => {
const { devices, uploadsByUser, uploadTargetDevice, uploadTargetUser, version } = getState();
Expand Down
142 changes: 142 additions & 0 deletions app/components/BluetoothModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* == BSD2 LICENSE ==
* Copyright (c) 2022, Tidepool Project
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the associated License, which is identical to the BSD 2-Clause
* License as published by the Open Source Initiative at opensource.org.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the License for more details.
*
* You should have received a copy of the License along with this program; if
* not, you can obtain one from Tidepool Project at tidepool.org.
* == BSD2 LICENSE ==
*/

import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import { sync as syncActions } from '../actions/';

import styles from '../../styles/components/BluetoothModal.module.less';

const remote = require('@electron/remote');
const i18n = remote.getGlobal( 'i18n' );

export class BluetoothModal extends Component {
handleContinue = () => {
const { showingBluetoothPairingDialog, sync } = this.props;
const response = {
confirmed: true,
};

if (this.pin && this.pin.value) {
response.pin = this.pin.value;
}

showingBluetoothPairingDialog.callback(response);
sync.dismissedBluetoothPairingDialog();
};

handleCancel = () => {
const { showingBluetoothPairingDialog, sync } = this.props;
const response = {
confirmed : false,
};
showingBluetoothPairingDialog.callback(response);
sync.dismissedBluetoothPairingDialog();
};

render() {
const { showingBluetoothPairingDialog } = this.props;

if(!showingBluetoothPairingDialog){
return null;
}

const pairingDetails = showingBluetoothPairingDialog.cfg;

switch (pairingDetails.pairingKind) {
case 'confirm': {
return (
<div className={styles.modalWrap}>
<div className={styles.modal}>
<div className={styles.title}>
<div>{i18n.t('Do you want to connect to {{device}}?', { device: pairingDetails.deviceId })}</div>
</div>
<div className={styles.actions}>
<button className={styles.button} onClick={this.handleContinue}>
{i18n.t('Confirm')}
</button>
<button className={styles.buttonSecondary} onClick={this.handleCancel}>
{i18n.t('Cancel')}
</button>
</div>
</div>
</div>
);
}
case 'confirmPin': {
return (
<div className={styles.modalWrap}>
<div className={styles.modal}>
<div className={styles.title}>
<div>{i18n.t('Does the pin {{pin}} match the pin displayed on device {{device}}?', { pin: pairingDetails.pin, device: pairingDetails.deviceId })}</div>
</div>
<div className={styles.actions}>
<button className={styles.button} onClick={this.handleContinue}>
{i18n.t('Confirm')}
</button>
<button className={styles.buttonSecondary} onClick={this.handleCancel}>
{i18n.t('Cancel')}
</button>
</div>
</div>
</div>
);
}
case 'providePin': {
return (
<div className={styles.modalWrap}>
<div className={styles.modal}>
<div className={styles.title}>
<div>{i18n.t('Enter Bluetooth Passkey for device {{device}}:', { device: pairingDetails.deviceId })}</div>
</div>
<div className={styles.textInputWrapper}>
<input
type="text"
ref={(input) => { this.pin = input; }}
className={styles.textInput} />
</div>
<div className={styles.actions}>
<button className={styles.button} onClick={this.handleContinue}>
{i18n.t('Continue')}
</button>
<button className={styles.buttonSecondary} onClick={this.handleCancel}>
{i18n.t('Cancel')}
</button>
</div>
</div>
</div>
);
}
}
}
};

export default connect(
(state, ownProps) => {
return {
showingBluetoothPairingDialog: state.showingBluetoothPairingDialog
};
},
(dispatch) => {
return {
sync: bindActionCreators(syncActions, dispatch)
};
}
)(BluetoothModal);
2 changes: 1 addition & 1 deletion app/components/Upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export default class Upload extends Component {
return this.handleMedtronic600Upload();
}

if (device === 'caresensble' || device === 'onetouchverioble') {
if (device === 'caresensble' || device === 'onetouchverioble' || device === 'foracareble') {
return this.handleBluetoothUpload(_.get(upload, 'key', null));
}

Expand Down
4 changes: 4 additions & 0 deletions app/constants/actionSources.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,7 @@ export const AD_HOC_PAIRING_DISMISSED = USER;
export const FETCH_INFO_REQUEST = UNDER_THE_HOOD;
export const FETCH_INFO_SUCCESS = UNDER_THE_HOOD;
export const FETCH_INFO_FAILURE = USER_VISIBLE;

// bluetooth pairing
export const BLUETOOTH_PAIRING_REQUEST = USER_VISIBLE;
export const BLUETOOTH_PAIRING_DISMISSED = USER;
4 changes: 4 additions & 0 deletions app/constants/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,7 @@ export const KEYCLOAK_AUTH_LOGOUT = 'KEYCLOAK_AUTH_LOGOUT';
export const KEYCLOAK_TOKENS_RECEIVED = 'KEYCLOAK_TOKENS_RECEIVED';
export const SET_KEYCLOAK_REGISTRATION_URL = 'SET_KEYCLOAK_REGISTRATION_URL';
export const KEYCLOAK_INSTANTIATED = 'KEYCLOAK_INSTANTIATED';

// bluetooth pairing
export const BLUETOOTH_PAIRING_REQUEST = 'BLUETOOTH_PAIRING_REQUEST';
export const BLUETOOTH_PAIRING_DISMISSED = 'BLUETOOTH_PAIRING_DISMISSED';
2 changes: 2 additions & 0 deletions app/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import UpdateModal from '../components/UpdateModal';
import UpdateDriverModal from '../components/UpdateDriverModal';
import DeviceTimeModal from '../components/DeviceTimeModal';
import AdHocModal from '../components/AdHocModal';
import BluetoothModal from '../components/BluetoothModal';

import styles from '../../styles/components/App.module.less';

Expand Down Expand Up @@ -221,6 +222,7 @@ export class App extends Component {
<UpdateDriverModal />
<DeviceTimeModal />
<AdHocModal />
<BluetoothModal />
</div>
);
}
Expand Down
14 changes: 13 additions & 1 deletion app/main.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,16 @@ let menu;
let template;
let mainWindow = null;
let serialPortFilter = null;
let bluetoothPinCallback = null;

// Web Bluetooth should only be an experimental feature on Linux
app.commandLine.appendSwitch('enable-experimental-web-platform-features', true);

// SharedArrayBuffer (used by lzo-wasm) requires cross-origin isolation
// in Chrome 92+, but we can't do this for our Electron setup,
// so we have to enable it manually
app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer');
// Confirm-only Bluetooth pairing is still behind a Chromium flag (up until v108 at least)
app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer,WebBluetoothConfirmPairingSupport');

if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support'); // eslint-disable-line
Expand Down Expand Up @@ -302,6 +304,12 @@ operating system, as soon as possible.`,
}
});

mainWindow.webContents.session.setBluetoothPairingHandler((details, callback) => {
bluetoothPinCallback = callback;
console.log('Sending bluetooth pairing request to renderer');
mainWindow.webContents.send('bluetooth-pairing-request', _.omit(details, ['frame']));
});

if (process.env.NODE_ENV === 'development') {
mainWindow.openDevTools();
mainWindow.webContents.on('context-menu', (e, props) => {
Expand Down Expand Up @@ -629,6 +637,10 @@ ipcMain.on('setSerialPortFilter', (event, arg) => {
serialPortFilter = arg;
});

ipcMain.on('bluetooth-pairing-response', (event, response) => {
bluetoothPinCallback(response);
});

if(!app.isDefaultProtocolClient('tidepoolupload')){
app.setAsDefaultProtocolClient('tidepoolupload');
}
Expand Down
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"drivelist": "11.0.0",
"keytar": "7.9.0",
"@ronomon/direct-io": "3.0.1",
"@tidepool/direct-io": "3.0.2",
"usb": "2.4.3"
}
}
15 changes: 10 additions & 5 deletions app/reducers/devices.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ const devices = {
key: 'caresensble',
source: {type: 'device', driverId: 'BluetoothLE'},
enabled: {mac: true, win: false, linux: true}
// PIN pairing for WebBluetooth is not currently supported on Windows 10:
// https://bugs.chromium.org/p/chromium/issues/detail?id=960258
// CareSens Bluetooth pairing is tricky; maybe better to wait for Uploader-in-Web
// before enabling it in Windows with proper on-screen instructions
},
dexcom: {
instructions: i18n.t('Plug in receiver with micro-USB'),
Expand All @@ -104,6 +104,13 @@ const devices = {
source: {type: 'device', driverId: 'Weitai'},
enabled: {mac: true, win: true, linux: true}
},
foracareble: {
instructions: i18n.t('Hold Bluetooth switch on meter until Bluetooth indicator starts to flash'),
key: 'foracareble',
name: 'Fora TN\'G Voice',
source: {type: 'device', driverId: 'BluetoothLE'},
enabled: {mac: true, win: true, linux: true}
},
glucocardexpression: {
instructions: {
text: i18n.t('Plug in meter with cable and set meter to'),
Expand Down Expand Up @@ -201,9 +208,7 @@ const devices = {
name: 'OneTouch Verio Flex, Verio Reflect & Select Plus Flex (with Bluetooth)',
key: 'onetouchverioble',
source: {type: 'device', driverId: 'OneTouchVerioBLE'},
enabled: {mac: true, win: false, linux: true}
// PIN pairing for WebBluetooth is not currently supported on Windows 10:
// https://bugs.chromium.org/p/chromium/issues/detail?id=960258
enabled: {mac: true, win: true, linux: true}
},
onetouchverioiq: {
instructions: i18n.t('Plug in meter with mini-USB'),
Expand Down
1 change: 1 addition & 0 deletions app/reducers/initialState.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const initialState = {
showingDeviceTimePrompt: null,
isTimezoneFocused: false,
showingAdHocPairingDialog: false,
showingBluetoothPairingDialog: null,
keycloakConfig: {},
};

Expand Down
11 changes: 11 additions & 0 deletions app/reducers/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,17 @@ export function showingAdHocPairingDialog(state = initialState.showingAdHocPairi
}
}

export function showingBluetoothPairingDialog(state = initialState.showingBluetoothPairingDialog, action) {
switch (action.type) {
case types.BLUETOOTH_PAIRING_REQUEST:
return { callback: action.payload.callback, cfg: action.payload.cfg };
case types.BLUETOOTH_PAIRING_DISMISSED:
return initialState.showingBluetoothPairingDialog;
default:
return state;
}
}

export const clinics = (state = initialState.clinics, action) => {
switch (action.type) {
case types.FETCH_PATIENTS_FOR_CLINIC_SUCCESS: {
Expand Down
16 changes: 8 additions & 8 deletions app/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
# yarn lockfile v1


"@ronomon/direct-io@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@ronomon/direct-io/-/direct-io-3.0.1.tgz#d7bc72b710030469225bfb9c6b6d57554dedae0c"
integrity sha512-NkKB32bjq7RfMdAMiWayphMlVWzsfPiKelK+btXLqggv1vDVgv2xELqeo0z4uYLLt86fVReLPxQj7qpg0zWvow==
dependencies:
"@ronomon/queue" "^3.0.1"

"@ronomon/queue@^3.0.1":
"@ronomon/queue@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@ronomon/queue/-/queue-3.0.1.tgz#42613e8488289ad452f4cf2244064958fc08d370"
integrity sha512-STcqSvk+c7ArMrZgYxhM92p6O6F7t0SUbGr+zm8s9fJple5EdJAMwP3dXqgdXeF95xWhBpha5kjEqNAIdI0r4w==

"@tidepool/direct-io@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@tidepool/direct-io/-/direct-io-3.0.2.tgz#f1c7e135881076b41c744d68f02ac5cdc2c41a14"
integrity sha512-AfOWIRqUDsHIFoglR5ruf6MX0qvHiqsY8OYf+/0GaWGgUzYzDbsRyjUWX5VuGmfd/WwfjZAUI9OHppdpLnsCZQ==
dependencies:
"@ronomon/queue" "3.0.1"

"@types/w3c-web-usb@1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/w3c-web-usb/-/w3c-web-usb-1.0.6.tgz#5d8560d0d9f585ffc80865bc773db7bc975b680c"
Expand Down
2 changes: 1 addition & 1 deletion lib/core/device.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const device = {

let tandemDriver;
try {
// eslint-disable-next-line global-require, import/no-unresolved
// eslint-disable-next-line global-require, import/no-unresolved, import/extensions
tandemDriver = require('../drivers/tandem/tandemDriver');
} catch (e) {
device.log('Tandem driver is only available to Tidepool developers.');
Expand Down
Loading

0 comments on commit 4a167c5

Please sign in to comment.