MVP
This commit is contained in:
parent
418ec8a2c7
commit
7c047f8231
|
@ -1,3 +1,6 @@
|
||||||
|
# Compiled
|
||||||
|
build
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
src
|
||||||
|
example
|
23
README.md
23
README.md
|
@ -0,0 +1,23 @@
|
||||||
|
# Redux Bluetooth
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ npm install redux-bluetooth
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { dummy } from 'redux-bluetooth';
|
||||||
|
|
||||||
|
console.log(dummy());
|
||||||
|
// hello world!
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
- redux-observable
|
||||||
|
- CI: CircleCI | Travis
|
||||||
|
- Coverage: Coverall | https://codecov.io/
|
||||||
|
- Eslint
|
||||||
|
- bundlesize
|
||||||
|
- Badges: ci, coverage, npm
|
||||||
|
- https://greenkeeper.io/
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "redux-bluetooth-counter-peripheral",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Redux Bluetooth example: Counter peripheral",
|
||||||
|
"main": "build/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"prestart": "npm run build",
|
||||||
|
"start": "node build/index.js",
|
||||||
|
"dev": "watch 'npm run build' src",
|
||||||
|
"build": "babel src -d build",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "npm test -- --watch"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"presets": [
|
||||||
|
"latest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+ssh://git@github.com/jvallelunga/redux-bluetooth.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"redux-bluetooth",
|
||||||
|
"example",
|
||||||
|
"counter",
|
||||||
|
"peripheral"
|
||||||
|
],
|
||||||
|
"author": "Jeronimo Vallelunga",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/jvallelunga/redux-bluetooth/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/jvallelunga/redux-bluetooth#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "^6.24.1",
|
||||||
|
"babel-preset-latest": "^6.24.1",
|
||||||
|
"jest": "^20.0.4",
|
||||||
|
"watch": "^1.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"redux": "^3.7.1",
|
||||||
|
"redux-bluetooth": "file:../../"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { createStore } from 'redux';
|
||||||
|
import { startPeripheral } from 'redux-bluetooth/build/peripheral';
|
||||||
|
|
||||||
|
import reducer from './reducer';
|
||||||
|
|
||||||
|
let store = createStore(reducer);
|
||||||
|
|
||||||
|
startPeripheral('Counter', store);
|
|
@ -0,0 +1,13 @@
|
||||||
|
export default function counter(state = 0, { type }) {
|
||||||
|
console.log('Counter: ---------------------------');
|
||||||
|
console.log(type)
|
||||||
|
console.log(state)
|
||||||
|
switch (type) {
|
||||||
|
case 'INCREMENT':
|
||||||
|
return state + 1
|
||||||
|
case 'DECREMENT':
|
||||||
|
return state - 1
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "redux-bluetooth-counter-webapp",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Redux Bluetooth example: Counter webapp",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test --env=jsdom",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
|
"predeploy": "npm run build",
|
||||||
|
"deploy": "gh-pages -d build"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+ssh://git@github.com/jvallelunga/redux-bluetooth.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"redux-bluetooth",
|
||||||
|
"example",
|
||||||
|
"counter",
|
||||||
|
"webapp"
|
||||||
|
],
|
||||||
|
"author": "Jeronimo Vallelunga",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/jvallelunga/redux-bluetooth/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://jvallelunga.github.io/redux-bluetooth",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"react": "^15.6.1",
|
||||||
|
"react-dom": "^15.6.1",
|
||||||
|
"react-redux": "^5.0.5",
|
||||||
|
"redux": "^3.7.1",
|
||||||
|
"redux-bluetooth": "file:../../",
|
||||||
|
"redux-thunk": "^2.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"gh-pages": "^1.0.0",
|
||||||
|
"react-scripts": "1.0.10"
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -0,0 +1,40 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="theme-color" content="#000000">
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is added to the
|
||||||
|
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
You need to enable JavaScript to run this app.
|
||||||
|
</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "./index.html",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import * as TYPES from './types';
|
||||||
|
|
||||||
|
export function increment() {
|
||||||
|
return { type: TYPES.INCREMENT };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrement() {
|
||||||
|
return { type: TYPES.DECREMENT };
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const INCREMENT = 'INCREMENT';
|
||||||
|
export const DECREMENT = 'DECREMENT';
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
export default class App extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handlerConnect = this.handlerConnect.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerConnect(){
|
||||||
|
const { onConnect } = this.props;
|
||||||
|
onConnect('Counter');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
store, status,
|
||||||
|
onIncrement, onDecrement
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
|
||||||
|
{ (status === 'CONNECTED') &&
|
||||||
|
<div className="app-counter">{store}</div>
|
||||||
|
}
|
||||||
|
<div className="app-actions">
|
||||||
|
{ (status !== 'CONNECTED') &&
|
||||||
|
<button className="app-actions__buton" onClick={this.handlerConnect}>Connect</button>
|
||||||
|
}
|
||||||
|
{ (status === 'CONNECTED') &&
|
||||||
|
<button className="app-actions__buton" onClick={onIncrement}>+</button>
|
||||||
|
}
|
||||||
|
{ (status === 'CONNECTED') &&
|
||||||
|
<button className="app-actions__buton" onClick={onDecrement}>-</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
App.propTypes = {
|
||||||
|
store: PropTypes.number,
|
||||||
|
status: PropTypes.string,
|
||||||
|
onConnect: PropTypes.func,
|
||||||
|
onIncrement: PropTypes.func,
|
||||||
|
onDecrement: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
App.defaultProps = {
|
||||||
|
onConnect: () => true,
|
||||||
|
onIncrement: () => true,
|
||||||
|
onDecrement: () => true,
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { actions } from 'redux-bluetooth/build/webapp';
|
||||||
|
|
||||||
|
import { increment, decrement } from '../actions';
|
||||||
|
|
||||||
|
import Component from './component';
|
||||||
|
|
||||||
|
const mapState = (state) => {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapAction = {
|
||||||
|
onConnect: actions.connectStore,
|
||||||
|
onIncrement: increment,
|
||||||
|
onDecrement: decrement,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Component };
|
||||||
|
export default connect(mapState, mapAction)(Component);
|
|
@ -0,0 +1,28 @@
|
||||||
|
.app {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-counter {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
line-height: calc(100vh - 200px);
|
||||||
|
font-size: 80vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-actions {
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-actions__buton {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 20px;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { createSyncStore } from 'redux-bluetooth/build/webapp';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
import * as TYPES from './actions/types';
|
||||||
|
|
||||||
|
import App from './app';
|
||||||
|
|
||||||
|
const ACTIONS = Object.keys(TYPES);
|
||||||
|
|
||||||
|
const store = createSyncStore(ACTIONS);
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"name": "redux-bluetooth",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Redux middleware to dispatch actions via bluetooth to a peripheral store",
|
||||||
|
"main": "build/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "watch 'npm run build' src",
|
||||||
|
"build": "babel src -d build",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "npm test -- --watch",
|
||||||
|
"prepublish": "npm run build",
|
||||||
|
"release": "np"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"presets": [
|
||||||
|
"latest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+ssh://git@github.com/jvallelunga/redux-bluetooth.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"redux",
|
||||||
|
"middleware",
|
||||||
|
"bluetooth",
|
||||||
|
"iot",
|
||||||
|
"bleno",
|
||||||
|
"peripheral",
|
||||||
|
"web-bluetooth",
|
||||||
|
"central"
|
||||||
|
],
|
||||||
|
"author": "Jeronimo Vallelunga",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/jvallelunga/redux-bluetooth/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/jvallelunga/redux-bluetooth#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "^6.24.1",
|
||||||
|
"babel-preset-latest": "^6.24.1",
|
||||||
|
"jest": "^20.0.4",
|
||||||
|
"np": "^2.16.0",
|
||||||
|
"watch": "^1.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bleno": "^0.4.2",
|
||||||
|
"text-encoding": "^0.6.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
const CONFIG = {
|
||||||
|
SERVICE_UUID: '13333333-3333-3333-3333-333333333337',
|
||||||
|
CHARACTERISTIC_UUID: '13333333-3333-3333-3333-333333330001'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CENTRAL_CONFIG = CONFIG;
|
||||||
|
export const BLENO_CONFIG = {
|
||||||
|
SERVICE_UUID: CONFIG.SERVICE_UUID.replace(/-/g, ''),
|
||||||
|
CHARACTERISTIC_UUID: CONFIG.CHARACTERISTIC_UUID.replace(/-/g, ''),
|
||||||
|
DESCRIPTOR_UUID: '2901'
|
||||||
|
};
|
|
@ -0,0 +1,17 @@
|
||||||
|
export default function Encoder({ TextEncoder, TextDecoder}) {
|
||||||
|
const encoder = new TextEncoder('utf-8');
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
|
||||||
|
const encode = (json) => {
|
||||||
|
const string = JSON.stringify(json);
|
||||||
|
return encoder.encode(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decode = (data) => {
|
||||||
|
const string = decoder.decode(data);
|
||||||
|
const json = JSON.parse(string);
|
||||||
|
return typeof json === 'string' ? JSON.parse(json) : json;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { encode, decode };
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import TextEncoding from 'text-encoding';
|
||||||
|
|
||||||
|
import Encoder from '.';
|
||||||
|
|
||||||
|
const { encode , decode } = new Encoder(TextEncoding);
|
||||||
|
|
||||||
|
test('encode / decode', () => {
|
||||||
|
const data = encode({ type: 'TEST', payload: 'PAYLOAD' });
|
||||||
|
const result = decode(data);
|
||||||
|
|
||||||
|
expect(result).toEqual({ type: 'TEST', payload: 'PAYLOAD' });
|
||||||
|
});
|
|
@ -0,0 +1,4 @@
|
||||||
|
import * as peripheral from './peripheral';
|
||||||
|
import * as webapp from './webapp';
|
||||||
|
|
||||||
|
export default { peripheral, webapp };
|
|
@ -0,0 +1,67 @@
|
||||||
|
export default function Characteristic(
|
||||||
|
uuid,
|
||||||
|
Parent,
|
||||||
|
util,
|
||||||
|
descriptor,
|
||||||
|
{ encode, decode } ) {
|
||||||
|
|
||||||
|
function ReduxCharacteristic() {
|
||||||
|
Parent.call(this, {
|
||||||
|
uuid,
|
||||||
|
properties: ['read', 'write', 'notify'],
|
||||||
|
descriptors: [ descriptor ]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.store = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
util.inherits(ReduxCharacteristic, Parent);
|
||||||
|
|
||||||
|
ReduxCharacteristic.prototype.connect = function(store) {
|
||||||
|
this.store = store;
|
||||||
|
this.store.subscribe(() => {
|
||||||
|
if ( this.updateValueCallback && this.store ) {
|
||||||
|
const state = this.store.getState();
|
||||||
|
this.updateValueCallback(encode(state));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ReduxCharacteristic.prototype.disconnect = function() {
|
||||||
|
this.store = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReduxCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) {
|
||||||
|
if (offset) {
|
||||||
|
callback(this.RESULT_ATTR_NOT_LONG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store && this.store.dispatch(decode(data));
|
||||||
|
|
||||||
|
callback(this.RESULT_SUCCESS);
|
||||||
|
};
|
||||||
|
|
||||||
|
ReduxCharacteristic.prototype.onReadRequest = function(offset, callback) {
|
||||||
|
if (offset) {
|
||||||
|
callback(this.RESULT_ATTR_NOT_LONG, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !this.store ) {
|
||||||
|
callback(this.RESULT_SUCCESS, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(this.RESULT_SUCCESS, encode(this.store.getState()));
|
||||||
|
};
|
||||||
|
|
||||||
|
ReduxCharacteristic.prototype.onSubscribe = function(maxValueSize, updateValueCallback) {
|
||||||
|
this.updateValueCallback = updateValueCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReduxCharacteristic.prototype.onUnsubscribe = function() {
|
||||||
|
this.updateValueCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ReduxCharacteristic();
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function Descriptor(uuid, Parent) {
|
||||||
|
return new Parent({ uuid, value: 'Redux Characteristic.' });
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import util from 'util';
|
||||||
|
import bleno from 'bleno';
|
||||||
|
import TextEncoding from 'text-encoding';
|
||||||
|
|
||||||
|
import { BLENO_CONFIG } from '../../common/config';
|
||||||
|
import Encoder from '../../common/encoder';
|
||||||
|
|
||||||
|
import Service from './service';
|
||||||
|
import Characteristic from './characteristic';
|
||||||
|
import Descriptor from './Descriptor';
|
||||||
|
|
||||||
|
export function Bleno(
|
||||||
|
bleno,
|
||||||
|
encoder,
|
||||||
|
{ SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID }) {
|
||||||
|
|
||||||
|
const descriptor = Descriptor(
|
||||||
|
DESCRIPTOR_UUID,
|
||||||
|
bleno.Descriptor
|
||||||
|
);
|
||||||
|
|
||||||
|
const characteristic = Characteristic(
|
||||||
|
CHARACTERISTIC_UUID,
|
||||||
|
bleno.Characteristic,
|
||||||
|
util,
|
||||||
|
descriptor,
|
||||||
|
encoder);
|
||||||
|
|
||||||
|
const service = Service(
|
||||||
|
SERVICE_UUID,
|
||||||
|
bleno.PrimaryService,
|
||||||
|
util,
|
||||||
|
characteristic);
|
||||||
|
|
||||||
|
const start = (name, store) => {
|
||||||
|
bleno.on('stateChange', function(state) {
|
||||||
|
if (state === 'poweredOn') {
|
||||||
|
bleno.startAdvertising(name, [SERVICE_UUID], function(err) {
|
||||||
|
err && console.log('startAdvertising.err: ', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
bleno.stopAdvertising();
|
||||||
|
characteristic.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bleno.on('advertisingStart', function(err) {
|
||||||
|
if (!err) {
|
||||||
|
bleno.setServices([ service ]);
|
||||||
|
characteristic.connect(store);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { start };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Bleno(
|
||||||
|
bleno,
|
||||||
|
Encoder(TextEncoding),
|
||||||
|
BLENO_CONFIG
|
||||||
|
);
|
|
@ -0,0 +1,16 @@
|
||||||
|
export default function Service(
|
||||||
|
uuid,
|
||||||
|
Parent,
|
||||||
|
util,
|
||||||
|
characteristic) {
|
||||||
|
|
||||||
|
function ReduxService() {
|
||||||
|
Parent.call(this, {
|
||||||
|
uuid,
|
||||||
|
characteristics: [characteristic],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
util.inherits(ReduxService, Parent);
|
||||||
|
|
||||||
|
return new ReduxService();
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import bleno from './bleno';
|
||||||
|
|
||||||
|
export const startPeripheral = (name, store) => bleno.start(name, store);
|
|
@ -0,0 +1,31 @@
|
||||||
|
export default function Actions(central, TYPES) {
|
||||||
|
|
||||||
|
const syncState = (state) => ({
|
||||||
|
type: TYPES.BLUETOOTH_SYNC,
|
||||||
|
payload: state
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectStore = (name) => dispatch => {
|
||||||
|
dispatch({ type: TYPES.BLUETOOTH_CONNECTING });
|
||||||
|
return central.connect(name)
|
||||||
|
.then(() => central.handler((state) => dispatch(syncState(state))))
|
||||||
|
.then(() => dispatch({ type: TYPES.BLUETOOTH_CONNECTED }))
|
||||||
|
.then(() => dispatch(syncStore()));
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncStore = () => dispatch => {
|
||||||
|
return central.read()
|
||||||
|
.then(state => dispatch(syncState(state)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendAction = (action) => _ => {
|
||||||
|
return central.write(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectStore,
|
||||||
|
syncStore,
|
||||||
|
syncState,
|
||||||
|
sendAction,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
import * as TYPES from './types';
|
||||||
|
import Actions from './actions';
|
||||||
|
|
||||||
|
let dispatch = null;
|
||||||
|
let central = null;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dispatch = jest.fn();
|
||||||
|
central = {
|
||||||
|
connect: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
handler: jest.fn(),
|
||||||
|
read: jest.fn().mockReturnValue(Promise.resolve('mockState')),
|
||||||
|
write: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
dispatch = null;
|
||||||
|
central = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('syncState', () => {
|
||||||
|
const { syncState } = Actions(central, TYPES);
|
||||||
|
const action = syncState('mockState');
|
||||||
|
|
||||||
|
expect(action).toEqual({
|
||||||
|
type: TYPES.BLUETOOTH_SYNC,
|
||||||
|
payload: 'mockState'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('connectStore', () => {
|
||||||
|
const { connectStore } = Actions(central, TYPES);
|
||||||
|
expect.assertions(6);
|
||||||
|
|
||||||
|
const promise = connectStore('mockName')(dispatch).then(_ => {
|
||||||
|
expect(central.connect).toBeCalled();
|
||||||
|
expect(central.handler).toBeCalled();
|
||||||
|
|
||||||
|
expect(dispatch.mock.calls.length).toBe(2);
|
||||||
|
expect(dispatch.mock.calls[0][0]).toEqual({ type: TYPES.BLUETOOTH_CONNECTING });
|
||||||
|
expect(dispatch.mock.calls[1][0]).toEqual({ type: TYPES.BLUETOOTH_CONNECTED });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return expect(promise).resolves.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('syncStore', () => {
|
||||||
|
const { syncStore } = Actions(central, TYPES);
|
||||||
|
expect.assertions(3);
|
||||||
|
|
||||||
|
const promise = syncStore()(dispatch).then(_ => {
|
||||||
|
expect(central.read).toBeCalled();
|
||||||
|
expect(dispatch).toBeCalledWith({ type: TYPES.BLUETOOTH_SYNC, payload: 'mockState' });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return expect(promise).resolves.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sendAction', () => {
|
||||||
|
const { sendAction } = Actions(central, TYPES);
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const promise = sendAction('mockAction')(dispatch).then(_ => {
|
||||||
|
expect(central.write).toBeCalledWith('mockAction');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return expect(promise).resolves.toBe(true);
|
||||||
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
import central from '../central';
|
||||||
|
|
||||||
|
import * as TYPES from './types';
|
||||||
|
import Actions from './actions';
|
||||||
|
|
||||||
|
export default Actions(central, TYPES);
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const BLUETOOTH_CONNECTING = '@@bluetooth/CONNECTING';
|
||||||
|
export const BLUETOOTH_CONNECTED = '@@bluetooth/CONNECTED';
|
||||||
|
export const BLUETOOTH_ERROR = '@@bluetooth/ERROR';
|
||||||
|
export const BLUETOOTH_READ = '@@bluetooth/READ';
|
||||||
|
export const BLUETOOTH_SYNC = '@@bluetooth/SYNC';
|
||||||
|
export const BLUETOOTH_SEND = '@@bluetooth/SEND';
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { CENTRAL_CONFIG } from '../../common/config';
|
||||||
|
import Encoder from '../../common/encoder';
|
||||||
|
|
||||||
|
export function Central(
|
||||||
|
bluetooth,
|
||||||
|
{ encode, decode },
|
||||||
|
{ SERVICE_UUID, CHARACTERISTIC_UUID }) {
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
server: null,
|
||||||
|
characteristic: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const connect = (name) => {
|
||||||
|
return bluetooth.requestDevice({
|
||||||
|
filters: [{ services: [ SERVICE_UUID ], name: name }]
|
||||||
|
})
|
||||||
|
.then((device) => device.gatt.connect())
|
||||||
|
.then((server) => {
|
||||||
|
state.server = server;
|
||||||
|
return server.getPrimaryService(SERVICE_UUID)
|
||||||
|
})
|
||||||
|
.then((service) => service.getCharacteristic(CHARACTERISTIC_UUID))
|
||||||
|
.then((characteristic) => {
|
||||||
|
state.characteristic = characteristic;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = ( callback ) => {
|
||||||
|
return state.characteristic.startNotifications()
|
||||||
|
.then(() => {
|
||||||
|
state.characteristic.addEventListener('characteristicvaluechanged', (event) => {
|
||||||
|
callback(decode(event.target.value));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const read = () => {
|
||||||
|
if ( state.server && state.server.connected && state.characteristic ) {
|
||||||
|
return state.characteristic.readValue().then(data => {
|
||||||
|
return decode(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('Bluetooth: Not Connected'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const write = (action) => {
|
||||||
|
if ( state.server && state.server.connected && state.characteristic ) {
|
||||||
|
const stringify = JSON.stringify(action);
|
||||||
|
const serialized = encode(stringify);
|
||||||
|
|
||||||
|
return state.characteristic.writeValue(serialized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connected: state.server && state.server.connected,
|
||||||
|
connect,
|
||||||
|
handler,
|
||||||
|
read,
|
||||||
|
write
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Central(
|
||||||
|
navigator.bluetooth,
|
||||||
|
Encoder({ TextEncoder, TextDecoder }),
|
||||||
|
CENTRAL_CONFIG
|
||||||
|
);
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const INIT = 'INIT';
|
||||||
|
export const DISCONNECTED = 'DISCONNECTED';
|
||||||
|
export const DISCONNECTING = 'DISCONNECTING';
|
||||||
|
export const CONNECTING = 'CONNECTING';
|
||||||
|
export const CONNECTED = 'CONNECTED';
|
||||||
|
export const ERROR = 'ERROR';
|
|
@ -0,0 +1,15 @@
|
||||||
|
import * as TYPES from './actions/types';
|
||||||
|
import * as STATUS from './central/status';
|
||||||
|
import ACTIONS from './actions';
|
||||||
|
import MIDDLEWARE from './middleware';
|
||||||
|
import REDUCERS from './reducers';
|
||||||
|
import STORE from './store';
|
||||||
|
|
||||||
|
const { connectStore, syncStore } = ACTIONS;
|
||||||
|
|
||||||
|
export const types = TYPES;
|
||||||
|
export const status = STATUS;
|
||||||
|
export const actions = { connectStore, syncStore };
|
||||||
|
export const reducers = REDUCERS;
|
||||||
|
export const middleware = MIDDLEWARE;
|
||||||
|
export const createSyncStore = STORE;
|
|
@ -0,0 +1,8 @@
|
||||||
|
import actions from '../actions';
|
||||||
|
const { sendAction } = actions;
|
||||||
|
|
||||||
|
export default (actions = []) => store => next => action => {
|
||||||
|
const { type } = action;
|
||||||
|
actions.includes(type) && store.dispatch(sendAction(action));
|
||||||
|
return next(action);
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import * as TYPES from '../actions/types';
|
||||||
|
import * as STATUS from '../central/status';
|
||||||
|
|
||||||
|
const initial = {
|
||||||
|
status: STATUS.INIT
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (autosync = true) => (state = initial, { type, payload }) => {
|
||||||
|
switch (type) {
|
||||||
|
case TYPES.BLUETOOTH_CONNECTING:
|
||||||
|
return Object.assign({}, state, { status: STATUS.CONNECTING });
|
||||||
|
case TYPES.BLUETOOTH_CONNECTED:
|
||||||
|
return Object.assign({}, state, { status: STATUS.CONNECTED });
|
||||||
|
case TYPES.BLUETOOTH_SYNC:
|
||||||
|
return autosync ? Object.assign({}, state, { store: payload }) : state;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
|
||||||
|
import middleware from '../middleware';
|
||||||
|
import reducers from '../reducers';
|
||||||
|
|
||||||
|
export default (actions) => {
|
||||||
|
const middlewares = [
|
||||||
|
middleware(actions),
|
||||||
|
thunk
|
||||||
|
];
|
||||||
|
const enhancers = [
|
||||||
|
applyMiddleware(...middlewares),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION__) {
|
||||||
|
enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__());
|
||||||
|
}
|
||||||
|
|
||||||
|
return createStore(
|
||||||
|
reducers(),
|
||||||
|
compose(...enhancers),
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue