Using Bluetooth on mobile

This commit is contained in:
aronmal 2022-09-25 00:28:29 +02:00
parent 43d7de56b8
commit 6ba0802caf
Signed by: aronmal
GPG key ID: 816B7707426FC612
7 changed files with 2200 additions and 6382 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
platform-tools

View file

@ -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)

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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
View 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

View file

@ -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;