Using Bluetooth on mobile
This commit is contained in:
parent
0f03753612
commit
6bd0c26683
7 changed files with 2200 additions and 6382 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
platform-tools
|
25
README.md
25
README.md
|
@ -1,2 +1,27 @@
|
||||||
# leaky-ships
|
# leaky-ships
|
||||||
Battleship web app with react frontend and ASP.NET Core backend
|
Battleship web app with react frontend and ASP.NET Core backend
|
||||||
|
|
||||||
|
## Bluetooth
|
||||||
|
|
||||||
|
Download [Android SDK Platform-Tools](https://developer.android.com/studio/releases/platform-tools)
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
```
|
||||||
|
./adb pair 10.1.0.125:38407
|
||||||
|
./adb connect 10.1.0.125:39099
|
||||||
|
```
|
||||||
|
|
||||||
|
Chrome flags to be enabled:
|
||||||
|
```
|
||||||
|
chrome://flags/#enable-experimental-web-platform-features
|
||||||
|
chrome://flags/#enable-web-bluetooth-new-permissions-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
Dev tool to discover gatt services:
|
||||||
|
```
|
||||||
|
chrome://bluetooth-internals/#devices
|
||||||
|
```
|
||||||
|
Other resources:
|
||||||
|
|
||||||
|
- [GATT Characteristics](https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf)
|
||||||
|
- [Using Web BLE](https://youtu.be/TsXUcAKi790)
|
8388
frontend/package-lock.json
generated
8388
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -16,6 +16,7 @@
|
||||||
"@types/react-dom": "^18.0.5",
|
"@types/react-dom": "^18.0.5",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/sass": "^1.43.1",
|
"@types/sass": "^1.43.1",
|
||||||
|
"@types/web-bluetooth": "^0.0.15",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { faCrosshairs } from '@fortawesome/free-solid-svg-icons';
|
import { faCrosshairs } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { CSSProperties, useEffect, useReducer, useState } from 'react';
|
import { CSSProperties, useEffect, useReducer, useState } from 'react';
|
||||||
|
import Bluetooth from './Bluetooth';
|
||||||
import BorderTiles from './components/BorderTiles';
|
import BorderTiles from './components/BorderTiles';
|
||||||
import FogImages from './components/FogImages';
|
import FogImages from './components/FogImages';
|
||||||
import HitElems from './components/HitElems';
|
import HitElems from './components/HitElems';
|
||||||
|
@ -16,20 +17,20 @@ function App() {
|
||||||
const [target, setTarget] = useState<TargetType>(initlialTarget);
|
const [target, setTarget] = useState<TargetType>(initlialTarget);
|
||||||
const [targetPreview, setTargetPreview] = useState<TargetPreviewType>(initlialTargetPreview);
|
const [targetPreview, setTargetPreview] = useState<TargetPreviewType>(initlialTargetPreview);
|
||||||
const [hits, DispatchHits] = useReducer(hitReducer, [] as HitType[]);
|
const [hits, DispatchHits] = useReducer(hitReducer, [] as HitType[]);
|
||||||
|
|
||||||
// handle visibility and position change of targetPreview
|
// handle visibility and position change of targetPreview
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const {newX, newY, shouldShow, appearOK, eventReady, show, x, y} = targetPreview;
|
const { newX, newY, shouldShow, appearOK, eventReady, show, x, y } = targetPreview;
|
||||||
const positionChange = !(x === newX && y === newY);
|
const positionChange = !(x === newX && y === newY);
|
||||||
// if not ready or no new position
|
// if not ready or no new position
|
||||||
if (!eventReady || (!positionChange && show))
|
if (!eventReady || (!positionChange && show))
|
||||||
return;
|
return;
|
||||||
if (show) {
|
if (show) {
|
||||||
// hide preview to change position when hidden
|
// hide preview to change position when hidden
|
||||||
setTargetPreview(e => ({...e, appearOK: false, eventReady: false, show: false}));
|
setTargetPreview(e => ({ ...e, appearOK: false, eventReady: false, show: false }));
|
||||||
} else if (shouldShow && appearOK && !isHit(hits, newX, newY).length) {
|
} else if (shouldShow && appearOK && !isHit(hits, newX, newY).length) {
|
||||||
// BUT only appear again if it's supposed to (in case the mouse left over the edge) and ()
|
// BUT only appear again if it's supposed to (in case the mouse left over the edge) and ()
|
||||||
setTargetPreview(e => ({...e, appearOK: false, eventReady: false, show: true, x: newX, y: newY}));
|
setTargetPreview(e => ({ ...e, appearOK: false, eventReady: false, show: true, x: newX, y: newY }));
|
||||||
}
|
}
|
||||||
}, [targetPreview, hits])
|
}, [targetPreview, hits])
|
||||||
|
|
||||||
|
@ -38,9 +39,9 @@ function App() {
|
||||||
if (targetPreview.eventReady)
|
if (targetPreview.eventReady)
|
||||||
return;
|
return;
|
||||||
const autoTimeout = setTimeout(() => {
|
const autoTimeout = setTimeout(() => {
|
||||||
setTargetPreview(e => ({...e, eventReady: true}));
|
setTargetPreview(e => ({ ...e, eventReady: true }));
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
// or abort if state has changed early
|
// or abort if state has changed early
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(autoTimeout);
|
clearTimeout(autoTimeout);
|
||||||
|
@ -53,9 +54,9 @@ function App() {
|
||||||
if (!targetPreview.shouldShow)
|
if (!targetPreview.shouldShow)
|
||||||
return;
|
return;
|
||||||
const autoTimeout = setTimeout(() => {
|
const autoTimeout = setTimeout(() => {
|
||||||
setTargetPreview(e => ({...e, appearOK: true}));
|
setTargetPreview(e => ({ ...e, appearOK: true }));
|
||||||
}, 350);
|
}, 350);
|
||||||
|
|
||||||
// or abort if movement is repeated early
|
// or abort if movement is repeated early
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(autoTimeout);
|
clearTimeout(autoTimeout);
|
||||||
|
@ -65,37 +66,60 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<header className="App-header">
|
<header className="App-header">
|
||||||
<div id="game-frame" style={{'--i': count} as CSSProperties}>
|
<Bluetooth />
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
className="App-link"
|
||||||
|
onClick={() => {navigator.clipboard.writeText("chrome://flags/#enable-experimental-web-platform-features")}}
|
||||||
|
// target="_blank"
|
||||||
|
style={{"cursor": "pointer"}}
|
||||||
|
// rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Step 1
|
||||||
|
</span>
|
||||||
|
{" "}
|
||||||
|
<span
|
||||||
|
className="App-link"
|
||||||
|
onClick={() => {navigator.clipboard.writeText("chrome://flags/#enable-web-bluetooth-new-permissions-backend")}}
|
||||||
|
// target="_blank"
|
||||||
|
style={{"cursor": "pointer"}}
|
||||||
|
// rel="noopener noreferrer"
|
||||||
|
|
||||||
|
>
|
||||||
|
Step 2
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div id="game-frame" style={{ '--i': count } as CSSProperties}>
|
||||||
{/* Bordes */}
|
{/* Bordes */}
|
||||||
<BorderTiles count={count} actions={{setTarget, setTargetPreview, hits, DispatchHits}} />
|
<BorderTiles count={count} actions={{ setTarget, setTargetPreview, hits, DispatchHits }} />
|
||||||
|
|
||||||
{/* Collumn lettes and row numbers */}
|
{/* Collumn lettes and row numbers */}
|
||||||
<Labeling count={count} />
|
<Labeling count={count} />
|
||||||
|
|
||||||
{/* Ships */}
|
{/* Ships */}
|
||||||
{/* <Ships /> */}
|
<Ships />
|
||||||
|
|
||||||
<HitElems hits={hits} />
|
<HitElems hits={hits} />
|
||||||
|
|
||||||
{/* Fog images */}
|
{/* Fog images */}
|
||||||
{/* <FogImages /> */}
|
{/* <FogImages /> */}
|
||||||
<div className={`hit-svg target ${target.show ? 'show' : ''}`} style={{'--x': target.x, '--y': target.y} as CSSProperties}>
|
<div className={`hit-svg target ${target.show ? 'show' : ''}`} style={{ '--x': target.x, '--y': target.y } as CSSProperties}>
|
||||||
<FontAwesomeIcon icon={faCrosshairs} />
|
<FontAwesomeIcon icon={faCrosshairs} />
|
||||||
</div>
|
</div>
|
||||||
<div className={`hit-svg target-preview ${targetPreview.show && (target.x !== targetPreview.x || target.y !== targetPreview.y) ? 'show' : ''}`} style={{'--x': targetPreview.x, '--y': targetPreview.y} as CSSProperties}>
|
<div className={`hit-svg target-preview ${targetPreview.show && (target.x !== targetPreview.x || target.y !== targetPreview.y) ? 'show' : ''}`} style={{ '--x': targetPreview.x, '--y': targetPreview.y } as CSSProperties}>
|
||||||
<FontAwesomeIcon icon={faCrosshairs} />
|
<FontAwesomeIcon icon={faCrosshairs} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <p>
|
{/* <p>
|
||||||
Edit <code>src/App.tsx</code> and save to reload.
|
Edit <code>src/App.tsx</code> and save to reload.
|
||||||
</p> */}
|
</p> */}
|
||||||
<a
|
<a
|
||||||
className="App-link"
|
className="App-link"
|
||||||
href="https://www.freepik.com/free-vector/militaristic-ships-set-navy-ammunition-warship-submarine-nuclear-battleship-float-cruiser-trawler-gunboat-frigate-ferry_10704121.htm"
|
href="https://www.freepik.com/free-vector/militaristic-ships-set-navy-ammunition-warship-submarine-nuclear-battleship-float-cruiser-trawler-gunboat-frigate-ferry_10704121.htm"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Battleships designed by macrovector
|
<p>Battleships designed by macrovector</p>
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
|
|
111
frontend/src/Bluetooth.tsx
Normal file
111
frontend/src/Bluetooth.tsx
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
function Bluetooth() {
|
||||||
|
const connectToDevice = async () => {
|
||||||
|
if (!navigator.bluetooth)
|
||||||
|
console.log('Web Bluetooth is not available!');
|
||||||
|
navigator.bluetooth
|
||||||
|
.requestDevice({
|
||||||
|
filters: [
|
||||||
|
{ namePrefix: "Chromecast Remote" }
|
||||||
|
],
|
||||||
|
optionalServices: ["battery_service"],
|
||||||
|
})
|
||||||
|
.then(device => {
|
||||||
|
console.log(device);
|
||||||
|
// console.log(device.id, device.name, device.gatt);
|
||||||
|
// Set up event listener for when device gets disconnected.
|
||||||
|
device.addEventListener('gattserverdisconnected', onDisconnected);
|
||||||
|
|
||||||
|
console.log(1)
|
||||||
|
// Attempts to connect to remote GATT Server.
|
||||||
|
const gatt = device.gatt
|
||||||
|
if (!gatt)
|
||||||
|
throw new Error('no gatt');
|
||||||
|
return gatt.connect();
|
||||||
|
})
|
||||||
|
.then(server => {
|
||||||
|
console.log(2)
|
||||||
|
console.log(server)
|
||||||
|
// Getting Battery Service…
|
||||||
|
return server.getPrimaryService('battery_service');
|
||||||
|
})
|
||||||
|
.then(service => {
|
||||||
|
console.log(3)
|
||||||
|
// Getting Battery Level Characteristic…
|
||||||
|
return service.getCharacteristic('battery_level');
|
||||||
|
// return service.getCharacteristic(0x2a19);
|
||||||
|
})
|
||||||
|
.then(characteristic => {
|
||||||
|
console.log(4)
|
||||||
|
// Reading Battery Level…
|
||||||
|
return characteristic.readValue();
|
||||||
|
})
|
||||||
|
.then(value => {
|
||||||
|
console.log(5)
|
||||||
|
console.log(`Battery percentage is ${value.getUint8(0)}`);
|
||||||
|
})
|
||||||
|
.catch(error => { console.log(error); });
|
||||||
|
}
|
||||||
|
const connectToDevice2 = async () => {
|
||||||
|
if (!navigator.bluetooth)
|
||||||
|
console.log('Web Bluetooth is not available!');
|
||||||
|
let device = await navigator.bluetooth
|
||||||
|
navigator.bluetooth
|
||||||
|
.requestDevice({
|
||||||
|
filters: [
|
||||||
|
{ namePrefix: "Chromecast Remote" }
|
||||||
|
],
|
||||||
|
optionalServices: ["device_information"],
|
||||||
|
})
|
||||||
|
.then(device => {
|
||||||
|
// Set up event listener for when device gets disconnected.
|
||||||
|
device.addEventListener('gattserverdisconnected', onDisconnected);
|
||||||
|
|
||||||
|
console.log(1)
|
||||||
|
// Attempts to connect to remote GATT Server.
|
||||||
|
const gatt = device.gatt
|
||||||
|
if (!gatt)
|
||||||
|
throw new Error('no gatt');
|
||||||
|
return gatt.connect();
|
||||||
|
})
|
||||||
|
.then(server => {
|
||||||
|
console.log(2)
|
||||||
|
console.log(server)
|
||||||
|
// Getting Battery Service…
|
||||||
|
return server.getPrimaryService('device_information');
|
||||||
|
})
|
||||||
|
.then(service => {
|
||||||
|
console.log(3)
|
||||||
|
// Getting Battery Level Characteristic…
|
||||||
|
return service.getCharacteristic('manufacturer_name_string');
|
||||||
|
})
|
||||||
|
.then(characteristic => {
|
||||||
|
console.log(4)
|
||||||
|
// Reading Battery Level…
|
||||||
|
return characteristic.readValue();
|
||||||
|
})
|
||||||
|
.then(value => {
|
||||||
|
console.log(5)
|
||||||
|
let decoder = new TextDecoder('utf-8');
|
||||||
|
console.log(decoder.decode(value));
|
||||||
|
})
|
||||||
|
.catch(error => { console.log(error); });
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDisconnected = (event: any) => {
|
||||||
|
// alert("Device Disconnected");
|
||||||
|
// console.log(event);
|
||||||
|
const device = event.target;
|
||||||
|
console.log(`Device "${device.name}" is disconnected.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button className="bluetooth" onClick={connectToDevice2}>CONNECT</button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bluetooth
|
|
@ -41,6 +41,8 @@
|
||||||
$width: $height;
|
$width: $height;
|
||||||
height: $height;
|
height: $height;
|
||||||
width: $width;
|
width: $width;
|
||||||
|
max-height: 100vw;
|
||||||
|
max-width: 100vw;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue