Feat: restructure and add basic web ui
This commit is contained in:
parent
c55f84d690
commit
e16904fae9
31 changed files with 4793 additions and 10 deletions
13
.eslintignore
Normal file
13
.eslintignore
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
31
.eslintrc.cjs
Normal file
31
.eslintrc.cjs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/** @type { import("eslint").Linter.FlatConfig } */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:svelte/recommended',
|
||||||
|
'prettier'
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
extraFileExtensions: ['.svelte']
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.svelte'],
|
||||||
|
parser: 'svelte-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
engine-strict=true
|
13
.prettierignore
Normal file
13
.prettierignore
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
15
.prettierrc
Normal file
15
.prettierrc
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
38
README.md
Normal file
38
README.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# create-svelte
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npm create svelte@latest
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npm create svelte@latest my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
3
firmware/.gitmodules
vendored
3
firmware/.gitmodules
vendored
|
@ -1,3 +0,0 @@
|
||||||
[submodule "lib/esp-nimble-cpp"]
|
|
||||||
path = lib/esp-nimble-cpp
|
|
||||||
url = https://itsblue.dev/ScStw/esp-nimble-cpp.git
|
|
10
firmware/.vscode/extensions.json
vendored
Normal file
10
firmware/.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"ms-vscode.cpptools-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
|
@ -14,9 +14,9 @@
|
||||||
|
|
||||||
#define TAG "main"
|
#define TAG "main"
|
||||||
#define TRIGGER_PIN GPIO_NUM_23
|
#define TRIGGER_PIN GPIO_NUM_23
|
||||||
#define BLE_SERVICE_UUID "deea3136-d728-4f23-823a-1042909dd100"
|
#define BLE_SERVICE_UUID "deea3136-d728-4f23-823a-104290000000"
|
||||||
#define BLE_CURRENTTIME_CHARACTERISTIC_UUID "deea3136-d728-4f23-823a-1042909dd101"
|
#define BLE_CURRENTTIME_CHARACTERISTIC_UUID "deea3136-d728-4f23-823a-104290000001"
|
||||||
#define BLE_LASTTRIGGERTIME_CHARACTERISTIC_UUID "deea3136-d728-4f23-823a-1042909dd102"
|
#define BLE_LASTTRIGGERTIME_CHARACTERISTIC_UUID "deea3136-d728-4f23-823a-104290000002"
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
{
|
{
|
||||||
|
@ -29,6 +29,7 @@ NimBLECharacteristic *bleCurrentTimeCharacteristic;
|
||||||
NimBLECharacteristic *bleLastTriggerTimeCharacteristic;
|
NimBLECharacteristic *bleLastTriggerTimeCharacteristic;
|
||||||
|
|
||||||
QueueHandle_t triggerQueue;
|
QueueHandle_t triggerQueue;
|
||||||
|
uint64_t lastTriggerTime = 0;
|
||||||
|
|
||||||
class LocalServerCallbacks : public NimBLEServerCallbacks
|
class LocalServerCallbacks : public NimBLEServerCallbacks
|
||||||
{
|
{
|
||||||
|
@ -56,15 +57,22 @@ class LocalCharacteristicCallbacks : public NimBLECharacteristicCallbacks
|
||||||
{
|
{
|
||||||
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
|
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
|
||||||
{
|
{
|
||||||
bleCurrentTimeCharacteristic->setValue(esp_timer_get_time());
|
bleCurrentTimeCharacteristic->setValue(esp_timer_get_time() / 1000);
|
||||||
bleCurrentTimeCharacteristic->notify();
|
bleCurrentTimeCharacteristic->notify();
|
||||||
ESP_LOGI(TAG, "Characteristic written!");
|
// ESP_LOGI(TAG, "Characteristic written!");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void handleTrigger(void *)
|
void handleTrigger(void *)
|
||||||
{
|
{
|
||||||
uint64_t currentTime = esp_timer_get_time();
|
uint64_t currentTime = esp_timer_get_time() / 1000;
|
||||||
|
|
||||||
|
if (currentTime - lastTriggerTime < 1000)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTriggerTime = currentTime;
|
||||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||||
xQueueSendFromISR(triggerQueue, ¤tTime, &xHigherPriorityTaskWoken);
|
xQueueSendFromISR(triggerQueue, ¤tTime, &xHigherPriorityTaskWoken);
|
||||||
if (xHigherPriorityTaskWoken)
|
if (xHigherPriorityTaskWoken)
|
||||||
|
@ -123,7 +131,7 @@ void app_main()
|
||||||
{
|
{
|
||||||
bleLastTriggerTimeCharacteristic->setValue(currentTime);
|
bleLastTriggerTimeCharacteristic->setValue(currentTime);
|
||||||
bleLastTriggerTimeCharacteristic->notify();
|
bleLastTriggerTimeCharacteristic->notify();
|
||||||
ESP_LOGI(TAG, "Characteristic written!");
|
// ESP_LOGI(TAG, "Characteristic written!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4057
package-lock.json
generated
Normal file
4057
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
43
package.json
Normal file
43
package.json
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "bluetooth-buzzer",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"format": "prettier --write ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^2.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^2.0.3",
|
||||||
|
"@sveltejs/kit": "^1.27.4",
|
||||||
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
|
"@types/web-bluetooth": "^0.0.20",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
|
"flowbite": "^2.2.0",
|
||||||
|
"flowbite-svelte": "^0.44.20",
|
||||||
|
"postcss": "^8.4.24",
|
||||||
|
"postcss-load-config": "^4.0.1",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||||
|
"svelte": "^4.2.7",
|
||||||
|
"svelte-check": "^3.6.0",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
|
"tslib": "^2.4.1",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^4.4.2"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"ua-parser-js": "^1.0.37"
|
||||||
|
}
|
||||||
|
}
|
13
postcss.config.cjs
Normal file
13
postcss.config.cjs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
const tailwindcss = require('tailwindcss');
|
||||||
|
const autoprefixer = require('autoprefixer');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
plugins: [
|
||||||
|
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||||
|
tailwindcss(),
|
||||||
|
//But others, like autoprefixer, need to run after,
|
||||||
|
autoprefixer
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
12
src/app.d.ts
vendored
Normal file
12
src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
16
src/app.html
Normal file
16
src/app.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body data-sveltekit-preload-data="hover"
|
||||||
|
class="bg-white dark:bg-gray-900 text-gray-600 dark:text-gray-400 antialiased">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
4
src/app.pcss
Normal file
4
src/app.pcss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/* Write your global styles here, in PostCSS syntax */
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
150
src/lib/bluetooth.ts
Normal file
150
src/lib/bluetooth.ts
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import { bluetoothState, buzzerState } from '../stores';
|
||||||
|
import { Ntp } from './ntp';
|
||||||
|
|
||||||
|
export type DeepronTimerButton = 'OK' | 'ESC' | '+' | '-' | 'RDY' | 'RST' | 'FULL_RESET';
|
||||||
|
|
||||||
|
const BLUETOOTH_BASE_UUID = 'deea3136-d728-4f23-823a-104290';
|
||||||
|
|
||||||
|
const BLUETOOTH_BUZZER_SERVICE_BASE_UUID = BLUETOOTH_BASE_UUID + '00';
|
||||||
|
const BLUETOOTH_BUZZER_SERVICE_UUID = BLUETOOTH_BUZZER_SERVICE_BASE_UUID + '0000';
|
||||||
|
|
||||||
|
let bluetoothDevice: BluetoothDevice | undefined = undefined;
|
||||||
|
let bluetoothService: BluetoothRemoteGATTService | undefined = undefined;
|
||||||
|
|
||||||
|
let bluetoothCharacteristics: {
|
||||||
|
currentTime: BluetoothRemoteGATTCharacteristic | undefined;
|
||||||
|
lastTriggerTime: BluetoothRemoteGATTCharacteristic | undefined;
|
||||||
|
} = {
|
||||||
|
currentTime: undefined,
|
||||||
|
lastTriggerTime: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
let ntp: Ntp | undefined = undefined;
|
||||||
|
let timeSyncResponse: ((time: bigint) => void) | undefined = undefined;
|
||||||
|
let timeSyncInterval: number | undefined = undefined;
|
||||||
|
|
||||||
|
function checkAvailability() {
|
||||||
|
if (!navigator.bluetooth) {
|
||||||
|
bluetoothState.set('UNAVAILABLE');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bluetoothState.set('DISCONNECTED');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startBluetooth() {
|
||||||
|
bluetoothState.set('CONNECTING');
|
||||||
|
try {
|
||||||
|
bluetoothDevice = await navigator.bluetooth.requestDevice({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
services: [BLUETOOTH_BUZZER_SERVICE_UUID]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
optionalServices: [BLUETOOTH_BUZZER_SERVICE_UUID]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
bluetoothState.set('DISCONNECTED');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('> Requested ' + bluetoothDevice.name + ' (' + bluetoothDevice.id + ')');
|
||||||
|
|
||||||
|
let server;
|
||||||
|
try {
|
||||||
|
server = await bluetoothDevice.gatt!.connect();
|
||||||
|
} catch (error) {
|
||||||
|
console.log('> Error connecting to ' + bluetoothDevice.name + ' (' + bluetoothDevice.id + ')');
|
||||||
|
bluetoothState.set('DISCONNECTED');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected);
|
||||||
|
|
||||||
|
bluetoothService = await server.getPrimaryService(BLUETOOTH_BUZZER_SERVICE_UUID);
|
||||||
|
|
||||||
|
bluetoothCharacteristics.currentTime = await bluetoothService.getCharacteristic(BLUETOOTH_BUZZER_SERVICE_BASE_UUID + '000' + (1));
|
||||||
|
await bluetoothCharacteristics.currentTime.startNotifications();
|
||||||
|
bluetoothCharacteristics.currentTime.addEventListener('characteristicvaluechanged', handleNotifications);
|
||||||
|
|
||||||
|
bluetoothCharacteristics.lastTriggerTime = await bluetoothService.getCharacteristic(BLUETOOTH_BUZZER_SERVICE_BASE_UUID + '000' + (2));
|
||||||
|
await bluetoothCharacteristics.lastTriggerTime.startNotifications();
|
||||||
|
bluetoothCharacteristics.lastTriggerTime.addEventListener('characteristicvaluechanged', handleNotifications);
|
||||||
|
|
||||||
|
ntp = new Ntp();
|
||||||
|
|
||||||
|
timeSyncInterval = setInterval(doOneTimeSync, 1000);
|
||||||
|
|
||||||
|
bluetoothState.set('CONNECTED');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnected() {
|
||||||
|
console.log('> Bluetooth Device disconnected');
|
||||||
|
bluetoothDevice = undefined;
|
||||||
|
bluetoothService = undefined;
|
||||||
|
ntp = undefined;
|
||||||
|
timeSyncResponse = undefined;
|
||||||
|
if (timeSyncInterval !== undefined)
|
||||||
|
clearInterval(timeSyncInterval);
|
||||||
|
bluetoothState.set('DISCONNECTED');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNotifications(this: BluetoothRemoteGATTCharacteristic, event: Event) {
|
||||||
|
if (!event.target) return;
|
||||||
|
|
||||||
|
const characteristic = event.target as BluetoothRemoteGATTCharacteristic;
|
||||||
|
|
||||||
|
if (characteristic.uuid == bluetoothCharacteristics.currentTime?.uuid)
|
||||||
|
handleNewCurrentTime(new DataView(characteristic.value!.buffer, 0).getBigUint64(0, true));
|
||||||
|
else if (characteristic.uuid == bluetoothCharacteristics.lastTriggerTime?.uuid)
|
||||||
|
handleNewLastTriggerTime(new DataView(characteristic.value!.buffer, 0).getBigUint64(0, true));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doOneTimeSync() {
|
||||||
|
if (ntp === undefined) return false;
|
||||||
|
if (bluetoothCharacteristics.currentTime === undefined) return false;
|
||||||
|
if (timeSyncResponse !== undefined) return false;
|
||||||
|
|
||||||
|
let sentAt = performance.now();
|
||||||
|
|
||||||
|
const remoteTime = new Promise<bigint>((resolve) => {
|
||||||
|
timeSyncResponse = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
await bluetoothCharacteristics.currentTime.writeValueWithResponse(new ArrayBuffer(0));
|
||||||
|
|
||||||
|
let time = await remoteTime;
|
||||||
|
let receivedAt = performance.now();
|
||||||
|
|
||||||
|
ntp.handleTimeSync(BigInt(Math.floor(sentAt)), BigInt(Math.floor(receivedAt)), time);
|
||||||
|
|
||||||
|
buzzerState.update(state => {
|
||||||
|
state.offset = ntp!.currentOffset();
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNewCurrentTime(time: bigint) {
|
||||||
|
if (timeSyncResponse === undefined)
|
||||||
|
return;
|
||||||
|
|
||||||
|
timeSyncResponse(time);
|
||||||
|
timeSyncResponse = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNewLastTriggerTime(time: bigint) {
|
||||||
|
if (ntp === undefined) return;
|
||||||
|
|
||||||
|
const currentOffset = ntp.currentOffset();
|
||||||
|
if (currentOffset === undefined) return;
|
||||||
|
|
||||||
|
buzzerState.update(state => {
|
||||||
|
state.lastTriggerTime = time - currentOffset;
|
||||||
|
state.connectionQuality = ntp!.currentAcceptanceRate();
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkAvailability, startBluetooth, doOneTimeSync };
|
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
108
src/lib/ntp.ts
Normal file
108
src/lib/ntp.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
export class Ntp {
|
||||||
|
|
||||||
|
__currentOffset: bigint | undefined;
|
||||||
|
__currentFluctuation: bigint | undefined;
|
||||||
|
// between 0 and 100
|
||||||
|
__currentAcceptanceRate: bigint | undefined;
|
||||||
|
|
||||||
|
__acceptedFluctuationFactor: bigint = 4n;
|
||||||
|
|
||||||
|
__latestOffsets: bigint[] = [];
|
||||||
|
__latestFluctuations: bigint[] = [];
|
||||||
|
__latestAcceptanceResults: bigint[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.__currentOffset = undefined;
|
||||||
|
this.__currentFluctuation = undefined;
|
||||||
|
this.__latestOffsets = [];
|
||||||
|
this.__latestFluctuations = [];
|
||||||
|
this.__latestAcceptanceResults = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
currentOffset() {
|
||||||
|
return this.__currentOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAcceptanceRate() {
|
||||||
|
return this.__currentAcceptanceRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTimeSync(sentAt: bigint, receivedAt: bigint, time: bigint) {
|
||||||
|
let roundTripTime = receivedAt - sentAt;
|
||||||
|
let newOffset = time + roundTripTime / 2n - receivedAt;
|
||||||
|
this.__handleNewOffset(newOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
__handleNewOffset(newOffset: bigint) {
|
||||||
|
const offsetInMargin = this.__isOffsetInMargin(newOffset);
|
||||||
|
|
||||||
|
this.__handleAcceptanceResult(offsetInMargin);
|
||||||
|
|
||||||
|
if (!offsetInMargin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__currentOffset = this.__pushValueAndCaluclateAverage(this.__latestOffsets, newOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
__handleAcceptanceResult(result: boolean) {
|
||||||
|
this.__currentAcceptanceRate = this.__pushValueAndCaluclateAverage(
|
||||||
|
this.__latestAcceptanceResults,
|
||||||
|
result ? 100n : 0n
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.__currentAcceptanceRate > 90n) {
|
||||||
|
this.__acceptedFluctuationFactor = 4n;
|
||||||
|
}
|
||||||
|
else if (this.__currentAcceptanceRate > 80n) {
|
||||||
|
this.__acceptedFluctuationFactor = 6n;
|
||||||
|
}
|
||||||
|
else if (this.__currentAcceptanceRate > 50n) {
|
||||||
|
this.__acceptedFluctuationFactor = 10n;
|
||||||
|
} else {
|
||||||
|
this.__acceptedFluctuationFactor = 100n;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
__isOffsetInMargin(newOffset: bigint) {
|
||||||
|
if (!this.__currentOffset) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fluctuation = this.__currentOffset - newOffset;
|
||||||
|
if (fluctuation < 0) {
|
||||||
|
fluctuation = -fluctuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.__currentFluctuation &&
|
||||||
|
this.__latestFluctuations.length > 5 &&
|
||||||
|
fluctuation > this.__currentFluctuation * this.__acceptedFluctuationFactor
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__currentFluctuation = this.__pushValueAndCaluclateAverage(this.__latestFluctuations, fluctuation);
|
||||||
|
|
||||||
|
if (this.__latestFluctuations.length < 10) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fluctuation < this.__currentFluctuation * 2n;
|
||||||
|
};
|
||||||
|
|
||||||
|
__pushValueAndCaluclateAverage(values: bigint[], newValue: bigint): bigint {
|
||||||
|
values.push(newValue);
|
||||||
|
if (values.length > 10) {
|
||||||
|
values.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
let sum = 0n;
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
sum += values[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum / BigInt(values.length);
|
||||||
|
};
|
||||||
|
}
|
5
src/lib/types.ts
Normal file
5
src/lib/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export interface BuzzerState {
|
||||||
|
offset?: bigint;
|
||||||
|
lastTriggerTime?: bigint;
|
||||||
|
connectionQuality?: bigint;
|
||||||
|
}
|
5
src/routes/+layout.svelte
Normal file
5
src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import '../app.pcss';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
2
src/routes/+layout.ts
Normal file
2
src/routes/+layout.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const prerender = false;
|
||||||
|
export const ssr = false;
|
46
src/routes/+page.svelte
Normal file
46
src/routes/+page.svelte
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { checkAvailability, doOneTimeSync, startBluetooth } from '$lib/bluetooth';
|
||||||
|
import { A, Alert, Button } from 'flowbite-svelte';
|
||||||
|
import { bluetoothState } from '../stores';
|
||||||
|
import { UAParser } from 'ua-parser-js';
|
||||||
|
import Timer from './Timer.svelte';
|
||||||
|
|
||||||
|
const uap = new UAParser();
|
||||||
|
|
||||||
|
$: {
|
||||||
|
checkAvailability();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-8 flex flex-col">
|
||||||
|
{#if $bluetoothState == 'DISCONNECTED'}
|
||||||
|
<Button
|
||||||
|
on:click={() => {
|
||||||
|
startBluetooth();
|
||||||
|
}}>Connect</Button
|
||||||
|
>
|
||||||
|
{:else if $bluetoothState == 'UNAVAILABLE'}
|
||||||
|
<span
|
||||||
|
class="text-xl text-white rounded-md font-bold border-primary-600 border-4 border-r-4 p-3"
|
||||||
|
>
|
||||||
|
Your Browser is not compatible with this website, as it does not support <A
|
||||||
|
class="font-bold"
|
||||||
|
href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility"
|
||||||
|
>
|
||||||
|
web bluetooth
|
||||||
|
</A>.
|
||||||
|
</span>
|
||||||
|
<span class="text-xl mt-3">
|
||||||
|
Please use a browser which supports web bluetooth, for example
|
||||||
|
{#if uap.getOS().name === 'iOS'}
|
||||||
|
<A href="https://apps.apple.com/us/app/bluefy-web-ble-browser/id1492822055">Bluefy</A>
|
||||||
|
{:else}
|
||||||
|
<A href="https://www.google.com/chrome/">Google Chrome</A>
|
||||||
|
{/if}.
|
||||||
|
</span>
|
||||||
|
{:else if $bluetoothState == 'CONNECTED'}
|
||||||
|
<Timer />
|
||||||
|
{:else}
|
||||||
|
<Alert color="yellow">Connecting...</Alert>
|
||||||
|
{/if}
|
||||||
|
</div>
|
2
src/routes/+page.ts
Normal file
2
src/routes/+page.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const prerender = false;
|
||||||
|
export const ssr = false;
|
69
src/routes/Timer.svelte
Normal file
69
src/routes/Timer.svelte
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import { buzzerState } from '../stores';
|
||||||
|
import { Button } from 'flowbite-svelte';
|
||||||
|
|
||||||
|
let timerStartedAt: bigint | undefined;
|
||||||
|
let timerStoppedAt: bigint | undefined;
|
||||||
|
|
||||||
|
let currentTimerValue: bigint | undefined;
|
||||||
|
|
||||||
|
let timerInterval: number | undefined;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
timerInterval = setInterval(() => {
|
||||||
|
if (timerStartedAt) {
|
||||||
|
let now = BigInt(Math.floor(performance.now()));
|
||||||
|
const timerValue = (timerStoppedAt ?? now) - timerStartedAt;
|
||||||
|
currentTimerValue =
|
||||||
|
timerStoppedAt === undefined ? timerValue - (timerValue % 100n) : timerValue;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getTimerColor = (timerStartedAt?: bigint, timerStoppedAt?: bigint) => {
|
||||||
|
if (timerStartedAt === undefined) {
|
||||||
|
return 'white';
|
||||||
|
} else if (timerStartedAt !== undefined && timerStoppedAt === undefined) {
|
||||||
|
return 'red';
|
||||||
|
} else if (timerStartedAt !== undefined && timerStoppedAt !== undefined) {
|
||||||
|
return 'green';
|
||||||
|
} else {
|
||||||
|
return 'white';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (!timerStartedAt || !$buzzerState.lastTriggerTime) {
|
||||||
|
break $;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($buzzerState.lastTriggerTime > timerStartedAt) {
|
||||||
|
timerStoppedAt = $buzzerState.lastTriggerTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-3">
|
||||||
|
<div class="w-full flex flex-row flex-wrap gap-3 justify-center">
|
||||||
|
<div class="flex flex-grow flex-col items-center">
|
||||||
|
<span
|
||||||
|
class="text-9xl font-bold"
|
||||||
|
style="color: {getTimerColor(timerStartedAt, timerStoppedAt)};"
|
||||||
|
>
|
||||||
|
{(Number(currentTimerValue ?? 0) / 1000).toFixed(3)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
on:click={() => {
|
||||||
|
timerStartedAt = BigInt(Math.floor(performance.now()));
|
||||||
|
timerStoppedAt = undefined;
|
||||||
|
}}>Start!</Button
|
||||||
|
>
|
7
src/stores.ts
Normal file
7
src/stores.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { BuzzerState } from '$lib/types';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
const bluetoothState = writable<'UNAVAILABLE' | 'DISCONNECTED' | 'CONNECTING' | 'CONNECTED'>('DISCONNECTED');
|
||||||
|
let buzzerState = writable<BuzzerState>({});
|
||||||
|
|
||||||
|
export { bluetoothState, buzzerState }
|
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
18
svelte.config.js
Normal file
18
svelte.config.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
|
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: [vitePreprocess({})],
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
|
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
|
adapter: adapter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
65
tailwind.config.cjs
Normal file
65
tailwind.config.cjs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
const config = {
|
||||||
|
content: [
|
||||||
|
'./src/**/*.{html,js,svelte,ts}',
|
||||||
|
'./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}'
|
||||||
|
],
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
'dark-white': '#F7F7F7',
|
||||||
|
black: '#111827',
|
||||||
|
'dark-grey': '#1F2937',
|
||||||
|
grey: '#374151',
|
||||||
|
'light-grey': '#6B7280',
|
||||||
|
red: '#DA3C2B',
|
||||||
|
yellow: '#DD972A',
|
||||||
|
green: '#057A55',
|
||||||
|
primary: {
|
||||||
|
50: '#FFF5F2',
|
||||||
|
100: '#FFF1EE',
|
||||||
|
200: '#FFE4DE',
|
||||||
|
300: '#FFD5CC',
|
||||||
|
400: '#FFBCAD',
|
||||||
|
500: '#FE795D',
|
||||||
|
600: '#EF562F',
|
||||||
|
700: '#EB4F27',
|
||||||
|
800: '#CC4522',
|
||||||
|
900: '#A5371B'
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
Roboto: ['Roboto', 'sans-serif'],
|
||||||
|
Raleway: ['Raleway', 'sans-serif']
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
blink: 'blink 4s linear infinite',
|
||||||
|
eyes: 'eyes 10s ease-in-out infinite'
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
blink: {
|
||||||
|
'0%, 96%': { transform: 'scaleY(1)' },
|
||||||
|
'98%': { transform: 'scaleY(0.1)' }
|
||||||
|
},
|
||||||
|
eyes: {
|
||||||
|
'0%, 25%, 50%, 75%': { transform: 'translate(0, 0)' },
|
||||||
|
'30%, 45%': { transform: 'translate(-10px, 0)' },
|
||||||
|
'80%, 95%': { transform: 'translate(10px, 0)' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'box-light': '0 10px 50px 0 rgba(17, 24, 39, .05)',
|
||||||
|
'box-dark': '0 10px 50px 0 rgba(247, 247, 247, .02)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
safelist: [
|
||||||
|
],
|
||||||
|
|
||||||
|
plugins: [require('flowbite/plugin')],
|
||||||
|
darkMode: 'media'
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
});
|
Loading…
Reference in a new issue