-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
748 lines (715 loc) · 33.4 KB
/
Copy pathindex.html
File metadata and controls
748 lines (715 loc) · 33.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>libreble — your Bluetooth devices, set free</title>
<meta name="description" content="Web apps that talk to your Bluetooth devices directly over Web Bluetooth in Chrome — no vendor account, no app, no cloud — with every protocol documented.">
<link rel="canonical" href="https://libreble.github.io/">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#f3f5f7" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0e1216" media="(prefers-color-scheme: dark)">
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="apple-touch-icon.png">
<meta property="og:type" content="website">
<meta property="og:site_name" content="libreble">
<meta property="og:title" content="libreble — your Bluetooth devices, set free">
<meta property="og:description" content="Web apps that talk to Bluetooth gadgets directly, in Chrome. No vendor account, no vendor app, no cloud — and every protocol documented.">
<meta property="og:url" content="https://libreble.github.io/">
<meta property="og:image" content="https://libreble.github.io/og/libreble.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="The libreble beacon mark next to the words: Your devices, set free.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="libreble — your Bluetooth devices, set free">
<meta name="twitter:description" content="Web apps that talk to Bluetooth gadgets directly, in Chrome. No vendor account, no vendor app, no cloud — and every protocol documented.">
<meta name="twitter:image" content="https://libreble.github.io/og/libreble.png">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"name": "libreble",
"url": "https://libreble.github.io/",
"description": "Web apps that talk to Bluetooth devices directly over Web Bluetooth in Chrome, with no vendor account, no vendor app and no cloud, and document each device's protocol so anyone can build on it."
},
{
"@type": "ItemList",
"name": "Liberated devices",
"itemListOrder": "https://schema.org/ItemListUnordered",
"numberOfItems": 4,
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"item": {
"@type": "SoftwareApplication",
"name": "Bikefit",
"url": "https://libreble.github.io/bikefit/",
"description": "Live power, cadence, heart rate and zones for the ICG IC-6 indoor bike, with session history on your device and TCX export.",
"applicationCategory": "UtilitiesApplication",
"operatingSystem": "Chrome on Android, Windows, macOS, Linux, ChromeOS",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"isAccessibleForFree": true,
"license": "https://opensource.org/licenses/MIT"
}
},
{
"@type": "ListItem",
"position": 2,
"item": {
"@type": "SoftwareApplication",
"name": "Brushlog",
"url": "https://libreble.github.io/brushlog/",
"description": "Syncs the brushing history an Oral-B iO toothbrush already stores, with live pressure, timer, brush-head wear, streaks and trends. No pairing, no P&G account.",
"applicationCategory": "UtilitiesApplication",
"operatingSystem": "Chrome on Android, Windows, macOS, Linux, ChromeOS",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"isAccessibleForFree": true,
"license": "https://opensource.org/licenses/MIT"
}
},
{
"@type": "ListItem",
"position": 3,
"item": {
"@type": "SoftwareApplication",
"name": "COLLET",
"url": "https://libreble.github.io/collet/",
"description": "Live battery, speed and health from the Dremel 8260 rotary tool, speed control, and an accessory and material speed guide.",
"applicationCategory": "UtilitiesApplication",
"operatingSystem": "Chrome on Android, Windows, macOS, Linux, ChromeOS",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"isAccessibleForFree": true,
"license": "https://opensource.org/licenses/MIT"
}
},
{
"@type": "ListItem",
"position": 4,
"item": {
"@type": "SoftwareApplication",
"name": "Multimeter",
"url": "https://libreble.github.io/multimeter/",
"description": "Live readout, charting, statistics, recording and CSV/PNG export for Bluetooth multimeters from UNI-T, OWON, Aneng, BSIDE/ZOYI, Voltcraft and AICARE.",
"applicationCategory": "UtilitiesApplication",
"operatingSystem": "Chrome on Android, Windows, macOS, Linux, ChromeOS",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"isAccessibleForFree": true,
"license": "https://opensource.org/licenses/MIT"
}
}
]
}
]
}
</script>
<!-- Theme override hook for testing: ?theme=dark or ?theme=light sets html[data-theme]. Harmless otherwise. -->
<script>
(function () {
var t = new URLSearchParams(location.search).get("theme");
if (t === "dark" || t === "light") document.documentElement.setAttribute("data-theme", t);
})();
</script>
<link rel="preload" href="fonts/archivo-latin.woff2" as="font" type="font/woff2" crossorigin>
<style>
/* Self-hosted, latin subset, SIL OFL — see fonts/OFL-*.txt. Archivo is one variable file
(width 62–125, weight 400–800); Plex Mono is two static weights. Nothing loads from
anyone else's server. */
@font-face {
font-family: Archivo; font-style: normal; font-weight: 400 800; font-stretch: 62% 125%;
font-display: swap; src: url(fonts/archivo-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400;
font-display: swap; src: url(fonts/ibm-plex-mono-400-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "IBM Plex Mono"; font-style: normal; font-weight: 500;
font-display: swap; src: url(fonts/ibm-plex-mono-500-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* ------------------------------------------------------------------
Tokens. Light is the base; dark redefines the same names twice:
once for the OS preference (unless data-theme="light" forces light),
once for an explicit data-theme="dark".
------------------------------------------------------------------ */
:root {
--paper: #f3f5f7;
--surface: #ffffff;
--ink: #151a20;
--ink-2: #3a444f;
--muted: #5b6673;
--line: #d8dee5;
--line-strong: #aeb8c3;
--led: #c2410c; /* accent for text and links */
--led-fill: #f26b1d; /* accent for fills: the LED itself */
--on-led: #180d04;
--led-soft: rgba(242, 107, 29, 0.13);
--ok: #15803d; /* the green checkmark */
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--paper: #0e1216;
--surface: #161b21;
--ink: #e9edf1;
--ink-2: #c4cdd6;
--muted: #98a4b1;
--line: #2a323c;
--line-strong: #46515d;
--led: #ffa259;
--led-fill: #ff8a3d;
--on-led: #180d04;
--led-soft: rgba(255, 138, 61, 0.16);
--ok: #4ade80;
}
}
:root[data-theme="dark"] {
--paper: #0e1216;
--surface: #161b21;
--ink: #e9edf1;
--ink-2: #c4cdd6;
--muted: #98a4b1;
--line: #2a323c;
--line-strong: #46515d;
--led: #ffa259;
--led-fill: #ff8a3d;
--on-led: #180d04;
--led-soft: rgba(255, 138, 61, 0.16);
--ok: #4ade80;
}
/* ---- base ---- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: Archivo, "Helvetica Neue", Arial, sans-serif;
font-size: 1.0625rem;
line-height: 1.55;
padding-inline: clamp(16px, 4.5vw, 48px);
font-feature-settings: "kern";
}
main { max-width: 1120px; margin: 0 auto; }
a { color: var(--led); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover { text-decoration-thickness: 2px; }
:focus-visible { outline: 2px solid var(--led); outline-offset: 3px; border-radius: 2px; }
h1, h2, h3 { margin: 0; text-wrap: balance; }
p { margin: 0; }
.mono { font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* ---- the mark ---- */
.mark { display: block; width: 100%; height: auto; }
.mark .ring { fill: none; stroke: currentColor; stroke-width: 5.5; stroke-linecap: round; }
.mark .led { fill: var(--led-fill); }
@keyframes advertise {
0%, 100% { opacity: 1; }
50% { opacity: 0.55; }
}
.hero .mark .led { animation: advertise 2.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .hero .mark .led { animation: none; } }
/* ---- hero ---- */
.hero {
padding-block: clamp(40px, 7vw, 88px) clamp(40px, 6vw, 72px);
display: grid;
gap: 40px 56px;
grid-template-columns: minmax(0, 1fr);
align-items: end;
}
@media (min-width: 900px) {
.hero { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
}
.brand { display: flex; align-items: center; gap: 14px; }
.hero .brand { grid-column: 1 / -1; align-self: start; margin-bottom: clamp(8px, 2vw, 24px); }
.brand .mark { width: 48px; }
.wordmark {
font-weight: 800;
font-stretch: 112%;
font-size: 1.625rem;
letter-spacing: -0.02em;
line-height: 1;
}
.ble { color: var(--led); }
.wordmark.inline { font-size: inherit; line-height: inherit; color: var(--ink); }
.hero h1 {
font-weight: 700;
font-stretch: 108%;
font-size: clamp(2.4rem, 1.1rem + 4.4vw, 4.25rem);
line-height: 1.02;
letter-spacing: -0.035em;
max-width: 12ch;
margin-bottom: clamp(20px, 3vw, 28px);
}
.lede {
font-size: clamp(1.0625rem, 0.95rem + 0.6vw, 1.3125rem);
line-height: 1.5;
color: var(--ink-2);
max-width: 36em;
text-wrap: pretty;
}
.lede strong { color: var(--ink); font-weight: 600; }
/* libreble vs the vendor app, at a glance */
.vs {
width: 100%;
border-collapse: collapse;
border-top: 2px solid var(--ink);
font-size: 0.9375rem;
}
.vs th, .vs td {
padding: 10px 8px;
border-bottom: 1px solid var(--line);
vertical-align: top;
text-align: left;
}
.vs th:first-child, .vs td:first-child { padding-left: 0; }
.vs thead th {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
padding-top: 8px;
}
.vs thead th.us { text-transform: none; letter-spacing: 0; padding-top: 4px; }
.vs thead th.us .wordmark { font-family: Archivo, "Helvetica Neue", Arial, sans-serif; font-size: 1.125rem; color: var(--ink); }
.vs tbody th { font-weight: 500; color: var(--ink); width: 30%; }
.vs td { color: var(--muted); }
.vs td.us { color: var(--ink); }
.vs .ok, .vs .no {
display: inline-block;
width: 1.35em;
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-weight: 700;
}
.vs .ok { color: var(--ok); }
.vs .no { color: var(--muted); }
/* ---- datasheet rows: rail left, content right ---- */
.row {
border-top: 1px solid var(--line-strong);
padding-block: clamp(36px, 5vw, 64px);
display: grid;
gap: 24px 56px;
grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 860px) {
.row { grid-template-columns: 232px minmax(0, 1fr); }
}
.rail h2 {
font-weight: 700;
font-stretch: 108%;
font-size: 1.625rem;
line-height: 1.1;
letter-spacing: -0.02em;
margin-bottom: 10px;
}
.rail .sub { color: var(--muted); font-size: 0.9375rem; line-height: 1.5; text-wrap: pretty; }
/* ---- device cards ---- */
.grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 6px;
padding: 20px 20px 18px;
display: flex;
flex-direction: column;
gap: 10px;
transition: border-color 0.15s ease;
}
.card:hover { border-color: var(--line-strong); }
.card-top {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
flex-wrap: wrap;
}
.device {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.8125rem;
color: var(--muted);
line-height: 1.4;
min-width: 0;
flex: 1 1 12em;
}
.card h3 {
font-weight: 700;
font-stretch: 106%;
font-size: 1.5rem;
line-height: 1.1;
letter-spacing: -0.02em;
}
.card p.what { color: var(--ink-2); font-size: 0.9375rem; line-height: 1.5; flex: 1; text-wrap: pretty; }
.links {
display: flex;
align-items: center;
gap: 6px 16px;
flex-wrap: wrap;
margin-top: 6px;
font-size: 0.9375rem;
}
.links .primary {
background: var(--led-fill);
color: var(--on-led);
text-decoration: none;
font-weight: 600;
padding: 8px 14px;
border-radius: 999px;
line-height: 1.2;
white-space: nowrap;
}
.links .primary:hover { filter: brightness(1.06); }
.links .primary::after { content: " \2192"; }
.links a:not(.primary) { color: var(--ink); white-space: nowrap; }
.links a:not(.primary)::after { content: "\2197"; font-size: 0.85em; margin-left: 0.15em; color: var(--muted); }
/* ---- status badges: an LED per state ---- */
.badge {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.6875rem;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink);
display: inline-flex;
align-items: center;
gap: 7px;
white-space: nowrap;
line-height: 1;
}
.badge::before { content: ""; width: 8px; height: 8px; border-radius: 50%; flex: none; box-sizing: border-box; }
.badge.live::before { background: var(--led-fill); box-shadow: 0 0 0 3px var(--led-soft); }
.badge.soon::before { background: var(--ink); }
.badge.wip::before { border: 2px solid var(--ink); }
.badge.plan { color: var(--muted); }
.badge.plan::before { border: 2px solid var(--line-strong); }
.also { margin-top: 24px; color: var(--muted); font-size: 0.9375rem; text-wrap: pretty; }
.also strong { color: var(--ink); font-weight: 600; }
/* ---- how it works ---- */
.rules {
display: grid;
gap: 24px 28px;
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
}
.rules > div { border-top: 2px solid var(--ink); padding-top: 14px; }
.rules h3 { font-weight: 700; font-stretch: 106%; font-size: 1.125rem; letter-spacing: -0.01em; margin-bottom: 8px; }
.rules p { color: var(--ink-2); font-size: 0.9375rem; line-height: 1.5; text-wrap: pretty; }
/* ---- what you need ---- */
.req { list-style: none; margin: 0; padding: 0; border-top: 2px solid var(--ink); max-width: 36em; }
.req li {
display: grid;
grid-template-columns: 1.6em 1fr;
gap: 10px;
align-items: baseline;
padding-block: 10px;
border-bottom: 1px solid var(--line);
}
.req li::before {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-weight: 500;
text-align: center;
}
.req li.yes::before { content: "\2713"; color: var(--led); }
.req li.no::before { content: "\2715"; color: var(--muted); }
.req li.no { color: var(--muted); }
.req li small { display: block; font-size: 0.875rem; color: var(--muted); }
.note { color: var(--muted); font-size: 0.9375rem; margin-top: 16px; max-width: 36em; text-wrap: pretty; }
/* ---- liberate a device ---- */
.steps { list-style: none; margin: 0; padding: 0; counter-reset: step; max-width: 40em; }
.steps li {
counter-increment: step;
display: grid;
grid-template-columns: 2.2em 1fr;
gap: 12px;
padding-block: 14px;
border-top: 1px solid var(--line);
color: var(--ink-2);
font-size: 0.9375rem;
line-height: 1.5;
}
.steps li:first-child { border-top: 2px solid var(--ink); }
.steps li::before {
content: counter(step);
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-weight: 500;
font-size: 1.25rem;
color: var(--led);
line-height: 1.15;
}
.ask { margin-top: 36px; max-width: 40em; border-top: 2px solid var(--ink); padding-top: 14px; }
.ask h3 { font-weight: 700; font-stretch: 106%; font-size: 1.125rem; letter-spacing: -0.01em; margin-bottom: 8px; }
.ask p { color: var(--ink-2); font-size: 0.9375rem; line-height: 1.5; text-wrap: pretty; }
.ask .links { margin-top: 14px; }
.steps li strong { display: block; color: var(--ink); font-weight: 700; font-stretch: 106%; font-size: 1.0625rem; margin-bottom: 2px; }
code {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.85em;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 4px;
padding: 0.05em 0.35em;
color: var(--ink);
}
/* ---- footer ---- */
footer {
border-top: 1px solid var(--line-strong);
padding-block: 36px 56px;
color: var(--muted);
font-size: 0.875rem;
line-height: 1.55;
display: grid;
gap: 20px 56px;
grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 860px) { footer { grid-template-columns: 232px minmax(0, 1fr); } }
footer .brand { margin: 0; align-items: center; }
footer .brand .mark { width: 28px; }
footer .wordmark { font-size: 1.125rem; }
footer .legal { display: grid; gap: 10px; max-width: 60em; }
footer .legal p { text-wrap: pretty; }
footer .support { color: var(--ink-2); margin-bottom: 6px; }
footer a { color: var(--ink); }
footer .meta { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px 20px; }
footer .meta a { color: var(--ink); }
</style>
</head>
<body>
<main>
<header class="hero">
<div class="brand">
<svg class="mark" viewBox="0 0 64 64" width="48" height="48" role="img" aria-label="libreble mark: a beacon inside two rings that open toward the top right">
<g transform="rotate(-45 32 32)">
<path class="ring" d="M44.36 40.50A15 15 0 1 1 44.36 23.50"/>
<path class="ring" d="M54.69 42.50A25 25 0 1 1 54.69 21.50"/>
<circle class="led" cx="32" cy="32" r="6.5"/>
</g>
</svg>
<span class="wordmark">libre<span class="ble">ble</span></span>
</div>
<div>
<h1>Your devices, set free.</h1>
<p class="lede">
Your gadget was fine before it got smart. Now it has Bluetooth, an app, an account, a
cloud, and a privacy policy longer than the manual. <span class="wordmark inline">libre<span class="ble">ble</span></span> puts the radio to better use:
web apps that talk to the device <strong>directly, in your browser</strong>. Your data
stays on your device, it works offline, and nothing breaks when the vendor loses interest.
No account, no app store, no server. Freeing a gadget that never needed freeing is a lot of
work, so each app also <strong>documents the device's protocol</strong> —
nobody should have to do it twice.
</p>
</div>
<table class="vs" aria-label="libreble compared with the vendor app">
<thead>
<tr><th scope="col"><span class="sr">Feature</span></th><th scope="col" class="us"><span class="wordmark">libre<span class="ble">ble</span></span></th><th scope="col">Vendor app</th></tr>
</thead>
<tbody>
<tr><th scope="row">Account</th><td class="us"><span class="ok">✓</span>None</td><td><span class="no">✕</span>Sign up first</td></tr>
<tr><th scope="row">Your data</th><td class="us"><span class="ok">✓</span>On your device</td><td><span class="no">✕</span>Their cloud</td></tr>
<tr><th scope="row">Install</th><td class="us"><span class="ok">✓</span>Open a link</td><td><span class="no">✕</span>App store</td></tr>
<tr><th scope="row">Offline</th><td class="us"><span class="ok">✓</span>Works</td><td><span class="no">✕</span>Usually not</td></tr>
<tr><th scope="row">Protocol</th><td class="us"><span class="ok">✓</span>Documented</td><td><span class="no">✕</span>Secret</td></tr>
<tr><th scope="row">Source</th><td class="us"><span class="ok">✓</span>Open, MIT</td><td><span class="no">✕</span>Closed</td></tr>
</tbody>
</table>
</header>
<section id="devices" class="row">
<div class="rail">
<h2>Liberated devices</h2>
<p class="sub">Every app runs on its own page under this site and is a standalone repo on GitHub.</p>
</div>
<div>
<div class="grid">
<!--
To add a device, copy one <article class="card"> block.
.device what it liberates (device line)
.badge optional <span class="badge soon|wip|plan"> in .card-top for an app that
isn't live yet; live apps carry no badge
h3 the app name
.what one or two sentences on what it does
.links Open app (class="primary"), Protocol doc, Source repo
-->
<article class="card">
<div class="card-top">
<span class="device">ICG IC-6 indoor bike</span>
</div>
<h3>Bikefit</h3>
<p class="what">Live power, cadence, heart rate and zones; full session history on your
device; TCX export to Strava, intervals.icu and friends.</p>
<div class="links">
<a class="primary" href="https://libreble.github.io/bikefit/">Open app<span class="sr">: Bikefit</span></a>
<a href="https://github.com/libreble/bikefit/blob/main/PROTOCOL.md" target="_blank" rel="noopener">Protocol</a>
<a href="https://github.com/libreble/bikefit" target="_blank" rel="noopener">Source</a>
</div>
</article>
<article class="card">
<div class="card-top">
<span class="device">Oral-B iO toothbrush</span>
</div>
<h3>Brushlog</h3>
<p class="what">Syncs the brushing history the brush already stores — no pairing, no P&G
account. Live pressure and timer, brush-head wear, streaks and trends. Verified on an
iO; Genius and Smart lines covered too.</p>
<div class="links">
<a class="primary" href="https://libreble.github.io/brushlog/">Open app<span class="sr">: Brushlog</span></a>
<a href="https://github.com/libreble/brushlog/blob/main/docs/PROTOCOL.md" target="_blank" rel="noopener">Protocol</a>
<a href="https://github.com/libreble/brushlog" target="_blank" rel="noopener">Source</a>
</div>
</article>
<article class="card">
<div class="card-top">
<span class="device">Dremel 8260 rotary tool</span>
</div>
<h3>COLLET</h3>
<p class="what">Live battery, speed and health from the tool, speed control with the tool's
own confirm handshake, and an accessory & material speed guide — which bit at which
speed. A built-in mock tool runs the whole app without hardware.</p>
<div class="links">
<a class="primary" href="https://libreble.github.io/collet/">Open app<span class="sr">: COLLET</span></a>
<a href="https://github.com/libreble/collet/blob/main/docs/PROTOCOL.md" target="_blank" rel="noopener">Protocol</a>
<a href="https://github.com/libreble/collet" target="_blank" rel="noopener">Source</a>
</div>
</article>
<article class="card">
<div class="card-top">
<span class="device">Bluetooth multimeters — UNI-T, OWON, Aneng, BSIDE/ZOYI, Voltcraft, AICARE</span>
</div>
<h3>Multimeter</h3>
<p class="what">Live full-screen readout, charting, statistics, recording and CSV/PNG
export — several meters at once, with derived channels. One app across six vendors,
each meter family's protocol documented.</p>
<div class="links">
<a class="primary" href="https://libreble.github.io/multimeter/">Open app<span class="sr">: Multimeter</span></a>
<a href="https://github.com/libreble/multimeter/tree/main/docs/protocols" target="_blank" rel="noopener">Protocols</a>
<a href="https://github.com/libreble/multimeter" target="_blank" rel="noopener">Source</a>
</div>
</article>
</div>
<p class="also">Looking for liberated <strong>label printers</strong>? That's a sibling
project: <a href="https://thermal-label.github.io/" target="_blank" rel="noopener">thermal-label.github.io</a> — same idea,
organised around thermal printers rather than around Bluetooth.</p>
</div>
</section>
<section id="how" class="row">
<div class="rail">
<h2>How it works</h2>
<p class="sub">Four rules every <span class="wordmark inline">libre<span class="ble">ble</span></span> app follows.</p>
</div>
<div class="rules">
<div>
<h3>Direct to the device</h3>
<p>The browser speaks Bluetooth to the device itself through
<a href="https://developer.mozilla.org/docs/Web/API/Web_Bluetooth_API" target="_blank" rel="noopener">Web Bluetooth</a>.
Nothing sits in between — no vendor server, no relay, no login.</p>
</div>
<div>
<h3>Your data stays yours</h3>
<p>Everything is stored in the browser on your device. Export it as a plain JSON file,
import it elsewhere, delete it whenever. No network requests at runtime.</p>
</div>
<div>
<h3>Installable, works offline</h3>
<p>Each app is a PWA: add it to your home screen once and it opens without a connection,
like any other app — minus the app store.</p>
</div>
<div>
<h3>No ads, no trackers</h3>
<p>No ads, no analytics, no crash reporting, no "anonymous" telemetry, no third-party
anything. We have no idea how many people use these apps or what they do with them. Yes,
we're curious. No, we're not adding a counter — if an app earned it, a star on
<a href="https://github.com/libreble" target="_blank" rel="noopener">GitHub</a> is the
whole feedback loop.</p>
</div>
</div>
</section>
<section id="requirements" class="row">
<div class="rail">
<h2>What you need</h2>
<p class="sub">Web Bluetooth is a Chromium feature, so the browser matters more than the device.</p>
</div>
<div>
<ul class="req">
<li class="yes"><span><span class="sr">Supported: </span>Chrome on Android</span></li>
<li class="yes"><span><span class="sr">Supported: </span>Chrome or Edge on desktop <small>Windows, macOS, Linux, ChromeOS</small></span></li>
<li class="no"><span><span class="sr">Not supported: </span>Safari <small>including every browser on iOS and iPadOS</small></span></li>
<li class="no"><span><span class="sr">Not supported: </span>Firefox</span></li>
</ul>
<p class="note">On iPhone and iPad a Web Bluetooth-capable browser such as Bluefy can work,
but it isn't a supported target.</p>
</div>
</section>
<section id="contribute" class="row">
<div class="rail">
<h2>Liberate a device</h2>
<p class="sub">Have a gadget held hostage by its app? If it talks Bluetooth Low Energy — the
<span class="ble">ble</span> in <span class="wordmark inline">libre<span class="ble">ble</span></span> — anyone
can liberate it. This is the shape, and how to get it listed here.</p>
</div>
<div>
<ol class="steps">
<li><span><strong>Document the protocol</strong> Capture what the device says on the air,
work out the framing, and write it up as a <code>PROTOCOL.md</code> other people can
implement from. The write-up counts as much as the app.</span></li>
<li><span><strong>Build the PWA</strong> A pure protocol layer with no UI dependencies, a
real Web Bluetooth transport plus a mock so the UI works without hardware, local storage
with JSON export, a manifest and a service worker.</span></li>
<li><span><strong>Publish it, then tell us</strong> Deploy it anywhere — your own GitHub
Pages is fine. <a href="https://github.com/libreble/libreble.github.io/issues/new?template=liberated-device.yml" target="_blank" rel="noopener">Open an issue</a>
with the app, the source and the protocol write-up, and it gets a card above.</span></li>
</ol>
<p class="note">The apps above all follow this shape — borrow whatever helps. Their source
lives at <a href="https://github.com/libreble" target="_blank" rel="noopener">github.com/libreble</a>.</p>
<div class="ask">
<h3>Not a builder? Ask for a device.</h3>
<p>Anything with a Bluetooth LE radio qualifies: say which make and model, and what its
app holds hostage. Requests are a wish list, not a queue — but if you own the device and
can help capture what it says on the air, things move a lot faster.</p>
<p class="links">
<a class="primary" href="https://github.com/libreble/libreble.github.io/issues/new?template=device-request.yml" target="_blank" rel="noopener">Request a device</a>
<a href="https://github.com/libreble/libreble.github.io/labels/device%20request" target="_blank" rel="noopener">Open requests</a>
</p>
</div>
</div>
</section>
<footer>
<div>
<div class="brand">
<svg class="mark" viewBox="0 0 64 64" width="28" height="28" aria-hidden="true">
<g transform="rotate(-45 32 32)">
<path class="ring" d="M44.36 40.50A15 15 0 1 1 44.36 23.50"/>
<path class="ring" d="M54.69 42.50A25 25 0 1 1 54.69 21.50"/>
<circle class="led" cx="32" cy="32" r="6.5"/>
</g>
</svg>
<span class="wordmark">libre<span class="ble">ble</span></span>
</div>
<p class="meta">
<a href="https://github.com/libreble" target="_blank" rel="noopener">GitHub</a>
<a href="https://ko-fi.com/mannes" target="_blank" rel="noopener">Ko-fi</a>
</p>
</div>
<div class="legal">
<p class="support">The apps are free and stay that way. If you'd like to support the work
anyway: a coffee on <a href="https://ko-fi.com/mannes" target="_blank" rel="noopener">Ko-fi</a>,
or — honestly more useful — the Bluetooth gadget you'd like liberated. Hardware on the desk
is how a device gets an app; say so in a
<a href="https://github.com/libreble/libreble.github.io/issues/new?template=device-request.yml" target="_blank" rel="noopener">device request</a>.</p>
<p><span class="wordmark inline">libre<span class="ble">ble</span></span> is an independent, unofficial project. It is not affiliated with or endorsed by
any device maker; product names belong to their respective owners.</p>
<p>The apps reproduce only the on-air Bluetooth protocols, derived from public
reverse-engineering for interoperability and personal-data ownership. No vendor code is
included.</p>
<p>All code is released under the
<a href="https://github.com/libreble/libreble.github.io/blob/main/LICENSE" target="_blank" rel="noopener">MIT License</a>.</p>
</div>
</footer>
</main>
</body>
</html>