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 TRIGGER_PIN GPIO_NUM_23
|
||||
#define BLE_SERVICE_UUID "deea3136-d728-4f23-823a-1042909dd100"
|
||||
#define BLE_CURRENTTIME_CHARACTERISTIC_UUID "deea3136-d728-4f23-823a-1042909dd101"
|
||||
#define BLE_LASTTRIGGERTIME_CHARACTERISTIC_UUID "deea3136-d728-4f23-823a-1042909dd102"
|
||||
#define BLE_SERVICE_UUID "deea3136-d728-4f23-823a-104290000000"
|
||||
#define BLE_CURRENTTIME_CHARACTERISTIC_UUID "deea3136-d728-4f23-823a-104290000001"
|
||||
#define BLE_LASTTRIGGERTIME_CHARACTERISTIC_UUID "deea3136-d728-4f23-823a-104290000002"
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
@ -29,6 +29,7 @@ NimBLECharacteristic *bleCurrentTimeCharacteristic;
|
|||
NimBLECharacteristic *bleLastTriggerTimeCharacteristic;
|
||||
|
||||
QueueHandle_t triggerQueue;
|
||||
uint64_t lastTriggerTime = 0;
|
||||
|
||||
class LocalServerCallbacks : public NimBLEServerCallbacks
|
||||
{
|
||||
|
@ -56,15 +57,22 @@ class LocalCharacteristicCallbacks : public NimBLECharacteristicCallbacks
|
|||
{
|
||||
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
|
||||
{
|
||||
bleCurrentTimeCharacteristic->setValue(esp_timer_get_time());
|
||||
bleCurrentTimeCharacteristic->setValue(esp_timer_get_time() / 1000);
|
||||
bleCurrentTimeCharacteristic->notify();
|
||||
ESP_LOGI(TAG, "Characteristic written!");
|
||||
// ESP_LOGI(TAG, "Characteristic written!");
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
xQueueSendFromISR(triggerQueue, ¤tTime, &xHigherPriorityTaskWoken);
|
||||
if (xHigherPriorityTaskWoken)
|
||||
|
@ -123,7 +131,7 @@ void app_main()
|
|||
{
|
||||
bleLastTriggerTimeCharacteristic->setValue(currentTime);
|
||||
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