maj bluetooth compatible webapp
This commit is contained in:
1 parent
3757975e51
commit
e94499ba1e
22 files changed
+68217
-67436
No files matched your search
+7316
-6900
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+30319
-30257
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+30319
-30257
File diff suppressed because it is too large.
Load diff
@@ -16,6 +16,8 @@
|
||||
#include <touchgfx/widgets/TextAreaWithWildcard.hpp>
|
||||
#include <touchgfx/widgets/ToggleButton.hpp>
|
||||
#include <texts/TextKeysAndLanguages.hpp>
|
||||
#include <audio_effects.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
@@ -144,6 +146,44 @@ inline void layoutParamRow(touchgfx::TextArea& label, touchgfx::Slider& slider,
|
||||
slider.setXY(184, y); /* curseur 456x52 */
|
||||
value.init(touchgfx::TypedText(T_VALUE_M_LEFT), accentColor, 656, y + 10, 136, 34);
|
||||
}
|
||||
|
||||
/* Detecte qu'un autre acteur que l'ecran (controle a distance BLE) a modifie les parametres
|
||||
* d'effets. snapshot() memorise l'etat affiche ; changed() compare tous les PERIOD ticks IHM
|
||||
* (~100 ms a 60 Hz) et renvoie true une fois par changement : l'ecran relit alors les parametres. */
|
||||
class ParamsWatcher
|
||||
{
|
||||
public:
|
||||
ParamsWatcher() : ticks(0)
|
||||
{
|
||||
memset(&last, 0, sizeof(last));
|
||||
}
|
||||
|
||||
void snapshot()
|
||||
{
|
||||
last = *AudioEffects_GetParams();
|
||||
ticks = 0;
|
||||
}
|
||||
|
||||
bool changed()
|
||||
{
|
||||
if (++ticks < PERIOD)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ticks = 0;
|
||||
if (memcmp(&last, AudioEffects_GetParams(), sizeof(last)) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
snapshot();
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
static const uint8_t PERIOD = 6;
|
||||
AudioEffectsParams last;
|
||||
uint8_t ticks;
|
||||
};
|
||||
} // namespace ui
|
||||
|
||||
#endif // UITHEME_HPP
|
||||
@@ -12,12 +12,16 @@ public:
|
||||
virtual ~EchoView() {}
|
||||
virtual void setupScreen();
|
||||
virtual void tearDownScreen();
|
||||
virtual void handleTickEvent();
|
||||
|
||||
virtual void toggleClicked();
|
||||
virtual void sliderDelaiChanged(int value);
|
||||
virtual void sliderFeedbackChanged(int value);
|
||||
virtual void sliderMixChanged(int value);
|
||||
protected:
|
||||
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
|
||||
void syncFromParams();
|
||||
ui::ParamsWatcher watcher;
|
||||
ui::Header header;
|
||||
ui::ValueText values[3];
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ public:
|
||||
virtual ~EqualizerView() {}
|
||||
virtual void setupScreen();
|
||||
virtual void tearDownScreen();
|
||||
virtual void handleTickEvent();
|
||||
|
||||
virtual void toggleClicked();
|
||||
virtual void sliderBand1Changed(int value);
|
||||
@@ -25,6 +26,9 @@ public:
|
||||
virtual void sliderBand9Changed(int value);
|
||||
virtual void sliderBand10Changed(int value);
|
||||
protected:
|
||||
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
|
||||
void syncFromParams();
|
||||
ui::ParamsWatcher watcher;
|
||||
ui::Header header;
|
||||
ui::ValueText values[10];
|
||||
touchgfx::Box zeroDbLine;
|
||||
|
||||
@@ -12,6 +12,7 @@ public:
|
||||
virtual ~OctaverView() {}
|
||||
virtual void setupScreen();
|
||||
virtual void tearDownScreen();
|
||||
virtual void handleTickEvent();
|
||||
|
||||
virtual void toggleClicked();
|
||||
virtual void slider1Clicked(int value);
|
||||
@@ -20,6 +21,9 @@ public:
|
||||
virtual void slider4Clicked(int value);
|
||||
virtual void slider5Clicked(int value);
|
||||
protected:
|
||||
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
|
||||
void syncFromParams();
|
||||
ui::ParamsWatcher watcher;
|
||||
ui::Header header;
|
||||
ui::ValueText values[5];
|
||||
|
||||
|
||||
@@ -12,12 +12,16 @@ public:
|
||||
virtual ~OverdriveView() {}
|
||||
virtual void setupScreen();
|
||||
virtual void tearDownScreen();
|
||||
virtual void handleTickEvent();
|
||||
|
||||
virtual void toggleClicked();
|
||||
virtual void sliderDriveChanged(int value);
|
||||
virtual void sliderToneChanged(int value);
|
||||
virtual void sliderLevelChanged(int value);
|
||||
protected:
|
||||
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
|
||||
void syncFromParams();
|
||||
ui::ParamsWatcher watcher;
|
||||
ui::Header header;
|
||||
ui::ValueText values[3];
|
||||
};
|
||||
|
||||
@@ -33,6 +33,9 @@ protected:
|
||||
};
|
||||
|
||||
void onCardOpen(const EffectCard& card);
|
||||
/* Relit marche/arret, volume et etat des cartes (setupScreen + changements distants) */
|
||||
void syncFromParams();
|
||||
ui::ParamsWatcher watcher;
|
||||
void setLooperState(LooperState state);
|
||||
|
||||
/* Buffer du CanvasWidgetRenderer (voyant du looper), propre a cet ecran comme
|
||||
|
||||
@@ -12,12 +12,16 @@ public:
|
||||
virtual ~ReverbView() {}
|
||||
virtual void setupScreen();
|
||||
virtual void tearDownScreen();
|
||||
virtual void handleTickEvent();
|
||||
|
||||
virtual void toggleClicked();
|
||||
virtual void sliderTailleChanged(int value);
|
||||
virtual void sliderAmortiChanged(int value);
|
||||
virtual void sliderMixChanged(int value);
|
||||
protected:
|
||||
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
|
||||
void syncFromParams();
|
||||
ui::ParamsWatcher watcher;
|
||||
ui::Header header;
|
||||
ui::ValueText values[3];
|
||||
};
|
||||
|
||||
@@ -12,11 +12,15 @@ public:
|
||||
virtual ~SaturationView() {}
|
||||
virtual void setupScreen();
|
||||
virtual void tearDownScreen();
|
||||
virtual void handleTickEvent();
|
||||
|
||||
virtual void toggleClicked();
|
||||
virtual void sliderDriveClicked(int value);
|
||||
virtual void sliderLevelClicked(int value);
|
||||
protected:
|
||||
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
|
||||
void syncFromParams();
|
||||
ui::ParamsWatcher watcher;
|
||||
ui::Header header;
|
||||
ui::ValueText values[2];
|
||||
};
|
||||
|
||||
@@ -20,14 +20,30 @@ void EchoView::setupScreen()
|
||||
add(values[i]);
|
||||
}
|
||||
|
||||
syncFromParams();
|
||||
}
|
||||
|
||||
void EchoView::syncFromParams()
|
||||
{
|
||||
FxEchoParams& p = AudioEffects_GetParams()->echo;
|
||||
toggleButton1.forceState(p.enabled);
|
||||
toggleButton1.invalidate();
|
||||
slider1.setValue((int16_t)(p.delay_ms / 10.0f + 0.5f));
|
||||
slider2.setValue((int16_t)(p.feedback / 0.95f * 100.0f + 0.5f));
|
||||
slider3.setValue((int16_t)(p.mix * 100.0f + 0.5f));
|
||||
values[0].show("%d ms", slider1.getValue() * 10);
|
||||
values[1].show("%d %%", slider2.getValue());
|
||||
values[2].show("%d %%", slider3.getValue());
|
||||
watcher.snapshot();
|
||||
}
|
||||
|
||||
void EchoView::handleTickEvent()
|
||||
{
|
||||
EchoViewBase::handleTickEvent();
|
||||
if (watcher.changed())
|
||||
{
|
||||
syncFromParams();
|
||||
}
|
||||
}
|
||||
|
||||
void EchoView::tearDownScreen()
|
||||
|
||||
@@ -42,8 +42,6 @@ void EqualizerView::setupScreen()
|
||||
&bandSlider6, &bandSlider7, &bandSlider8, &bandSlider9, &bandSlider10 };
|
||||
touchgfx::TextArea* labels[10] = { &textArea2, &textArea2_1, &textArea2_2, &textArea2_3, &textArea2_4,
|
||||
&textArea2_5, &textArea2_6, &textArea2_7, &textArea2_8, &textArea2_9 };
|
||||
const FxEqualizerParams& p = AudioEffects_GetParams()->equalizer;
|
||||
toggleButton1.forceState(p.enabled);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
@@ -53,10 +51,34 @@ void EqualizerView::setupScreen()
|
||||
labels[i]->setColor(ui::textMuted());
|
||||
values[i].init(touchgfx::TypedText(T_VALUE_S_CENTER), accent, x - 10, VALUE_Y, 76, 24);
|
||||
add(values[i]);
|
||||
}
|
||||
|
||||
syncFromParams();
|
||||
}
|
||||
|
||||
void EqualizerView::syncFromParams()
|
||||
{
|
||||
touchgfx::Slider* sliders[10] = { &bandSlider1, &bandSlider2, &bandSlider3, &bandSlider4, &bandSlider5,
|
||||
&bandSlider6, &bandSlider7, &bandSlider8, &bandSlider9, &bandSlider10 };
|
||||
const FxEqualizerParams& p = AudioEffects_GetParams()->equalizer;
|
||||
toggleButton1.forceState(p.enabled);
|
||||
toggleButton1.invalidate();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
sliders[i]->setValue((int16_t)((p.bands[i].gain_db + GAIN_RANGE_DB / 2.0f) / GAIN_RANGE_DB * 100.0f + 0.5f));
|
||||
values[i].showDb(sliderToDb(sliders[i]->getValue()));
|
||||
}
|
||||
watcher.snapshot();
|
||||
}
|
||||
|
||||
void EqualizerView::handleTickEvent()
|
||||
{
|
||||
EqualizerViewBase::handleTickEvent();
|
||||
if (watcher.changed())
|
||||
{
|
||||
syncFromParams();
|
||||
}
|
||||
}
|
||||
|
||||
void EqualizerView::tearDownScreen()
|
||||
|
||||
@@ -33,17 +33,34 @@ void OctaverView::setupScreen()
|
||||
add(values[i]);
|
||||
}
|
||||
|
||||
syncFromParams();
|
||||
}
|
||||
|
||||
void OctaverView::syncFromParams()
|
||||
{
|
||||
FxOctaverParams& p = AudioEffects_GetParams()->octaver;
|
||||
toggleButton1.forceState(p.enabled);
|
||||
toggleButton1.invalidate();
|
||||
slider1.setValue((int16_t)(p.level_down2 * 100.0f + 0.5f));
|
||||
slider2.setValue((int16_t)(p.level_down1 * 100.0f + 0.5f));
|
||||
slider3.setValue((int16_t)(p.level_dry * 100.0f + 0.5f));
|
||||
slider4.setValue((int16_t)(p.level_up1 * 100.0f + 0.5f));
|
||||
slider5.setValue((int16_t)(p.level_up2 * 100.0f + 0.5f));
|
||||
touchgfx::Slider* sliders[5] = { &slider1, &slider2, &slider3, &slider4, &slider5 };
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
showLevel(i, sliders[i]->getValue());
|
||||
}
|
||||
watcher.snapshot();
|
||||
}
|
||||
|
||||
void OctaverView::handleTickEvent()
|
||||
{
|
||||
OctaverViewBase::handleTickEvent();
|
||||
if (watcher.changed())
|
||||
{
|
||||
syncFromParams();
|
||||
}
|
||||
}
|
||||
|
||||
void OctaverView::tearDownScreen()
|
||||
|
||||
@@ -20,14 +20,30 @@ void OverdriveView::setupScreen()
|
||||
add(values[i]);
|
||||
}
|
||||
|
||||
syncFromParams();
|
||||
}
|
||||
|
||||
void OverdriveView::syncFromParams()
|
||||
{
|
||||
FxOverdriveParams& p = AudioEffects_GetParams()->overdrive;
|
||||
toggleButton1.forceState(p.enabled);
|
||||
toggleButton1.invalidate();
|
||||
slider1.setValue((int16_t)(p.drive * 100.0f + 0.5f));
|
||||
slider2.setValue((int16_t)(p.tone * 100.0f + 0.5f));
|
||||
slider3.setValue((int16_t)(p.level * 100.0f + 0.5f));
|
||||
values[0].show("%d %%", slider1.getValue());
|
||||
values[1].show("%d %%", slider2.getValue());
|
||||
values[2].show("%d %%", slider3.getValue());
|
||||
watcher.snapshot();
|
||||
}
|
||||
|
||||
void OverdriveView::handleTickEvent()
|
||||
{
|
||||
OverdriveViewBase::handleTickEvent();
|
||||
if (watcher.changed())
|
||||
{
|
||||
syncFromParams();
|
||||
}
|
||||
}
|
||||
|
||||
void OverdriveView::tearDownScreen()
|
||||
|
||||
@@ -95,11 +95,23 @@ void OverviewView::setupScreen()
|
||||
buttonWithLabel7.setLabelText(touchgfx::TypedText(T_SPECTRUM));
|
||||
|
||||
/* --- Etat initial --- */
|
||||
syncFromParams();
|
||||
setLooperState(LOOPER_IDLE);
|
||||
}
|
||||
|
||||
void OverviewView::syncFromParams()
|
||||
{
|
||||
const AudioEffectsParams* params = AudioEffects_GetParams();
|
||||
toggleButton1.forceState(params->audio_on_off);
|
||||
toggleButton1.invalidate();
|
||||
const int volume = (int)(params->master_volume * 100.0f + 0.5f);
|
||||
slider1.setValue((int16_t)volume);
|
||||
volumeValue.show("%d %%", volume);
|
||||
setLooperState(LOOPER_IDLE);
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
cards[i].refresh();
|
||||
}
|
||||
watcher.snapshot();
|
||||
}
|
||||
|
||||
void OverviewView::tearDownScreen()
|
||||
@@ -152,6 +164,11 @@ void OverviewView::handleTickEvent()
|
||||
{
|
||||
OverviewViewBase::handleTickEvent();
|
||||
|
||||
if (watcher.changed())
|
||||
{
|
||||
syncFromParams();
|
||||
}
|
||||
|
||||
/* repeatButton1 declenche une action a l'appui puis en continu tant qu'il est
|
||||
* maintenu (comportement standard TouchGFX d'un RepeatButton) : ca ne donne pas
|
||||
* directement un evenement "relache". On detecte donc nous-memes le front sur son
|
||||
|
||||
@@ -24,14 +24,30 @@ void ReverbView::setupScreen()
|
||||
add(values[i]);
|
||||
}
|
||||
|
||||
syncFromParams();
|
||||
}
|
||||
|
||||
void ReverbView::syncFromParams()
|
||||
{
|
||||
FxReverbParams& p = AudioEffects_GetParams()->reverb;
|
||||
toggleButton1.forceState(p.enabled);
|
||||
toggleButton1.invalidate();
|
||||
slider1.setValue((int16_t)(p.room_size * 100.0f + 0.5f));
|
||||
slider2.setValue((int16_t)(p.damping * 100.0f + 0.5f));
|
||||
slider3.setValue((int16_t)(p.mix * 100.0f + 0.5f));
|
||||
values[0].show("%d %%", slider1.getValue());
|
||||
values[1].show("%d %%", slider2.getValue());
|
||||
values[2].show("%d %%", slider3.getValue());
|
||||
watcher.snapshot();
|
||||
}
|
||||
|
||||
void ReverbView::handleTickEvent()
|
||||
{
|
||||
ReverbViewBase::handleTickEvent();
|
||||
if (watcher.changed())
|
||||
{
|
||||
syncFromParams();
|
||||
}
|
||||
}
|
||||
|
||||
void ReverbView::tearDownScreen()
|
||||
|
||||
@@ -17,12 +17,28 @@ void SaturationView::setupScreen()
|
||||
add(values[0]);
|
||||
add(values[1]);
|
||||
|
||||
syncFromParams();
|
||||
}
|
||||
|
||||
void SaturationView::syncFromParams()
|
||||
{
|
||||
FxSaturationParams& p = AudioEffects_GetParams()->saturation;
|
||||
toggleButton1.forceState(p.enabled);
|
||||
toggleButton1.invalidate();
|
||||
slider1.setValue((int16_t)(p.drive * 100.0f + 0.5f));
|
||||
slider2.setValue((int16_t)(p.level * 100.0f + 0.5f));
|
||||
values[0].show("%d %%", slider1.getValue());
|
||||
values[1].show("%d %%", slider2.getValue());
|
||||
watcher.snapshot();
|
||||
}
|
||||
|
||||
void SaturationView::handleTickEvent()
|
||||
{
|
||||
SaturationViewBase::handleTickEvent();
|
||||
if (watcher.changed())
|
||||
{
|
||||
syncFromParams();
|
||||
}
|
||||
}
|
||||
|
||||
void SaturationView::tearDownScreen()
|
||||
|
||||
+55
-19
@@ -69,7 +69,7 @@ const SECTIONS = [
|
||||
|
||||
const state = new Map(); /* id -> valeur fil */
|
||||
const widgets = new Map(); /* id -> fonction de mise a jour de l'UI */
|
||||
let ctrl = null, pending = new Map(), timer = 0, demo = false;
|
||||
let link = null, pending = new Map(), timer = 0, demo = false;
|
||||
const $ = s => document.querySelector(s);
|
||||
const msg = t => { $("#msg").textContent = t || ""; };
|
||||
|
||||
@@ -124,45 +124,81 @@ function send(id, value) {
|
||||
async function flush() {
|
||||
timer = 0;
|
||||
const items = [...pending]; pending = new Map();
|
||||
if (!ctrl) return;
|
||||
if (!link) return;
|
||||
try {
|
||||
for (let i = 0; i < items.length; i += MAX_RECORDS) {
|
||||
const chunk = items.slice(i, i + MAX_RECORDS);
|
||||
const buf = new DataView(new ArrayBuffer(chunk.length * 3));
|
||||
chunk.forEach(([id, v], k) => { buf.setUint8(k * 3, id); buf.setInt16(k * 3 + 1, v, true); });
|
||||
await ctrl.writeValueWithoutResponse(buf);
|
||||
await link.write(new Uint8Array(buf.buffer), false);
|
||||
}
|
||||
} catch (e) { msg("Envoi impossible : " + e.message); }
|
||||
}
|
||||
|
||||
function onState(ev) {
|
||||
const dv = ev.target.value;
|
||||
function onState(dv) {
|
||||
for (let o = 0; o + 3 <= dv.byteLength; o += 3) {
|
||||
const id = dv.getUint8(o), v = dv.getInt16(o + 1, true);
|
||||
state.set(id, v); widgets.get(id)?.(v);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- connexion ---- */
|
||||
/* ---- connexion ----
|
||||
* Deux transports, meme interface {write(bytes, avecReponse), close()} :
|
||||
* - app WebApp : pont natif Android.ble (Web Bluetooth n'existe pas dans sa WebView)
|
||||
* - Chrome : Web Bluetooth (page en contexte securise) */
|
||||
function setConnected(on) { $("#dot").classList.toggle("on", on); $("#btn").textContent = on ? "Déconnecter" : "Connecter"; }
|
||||
let device = null;
|
||||
function lost() { link = null; setConnected(false); msg("Déconnecté."); }
|
||||
|
||||
async function connectNative() {
|
||||
const B = window.Android.ble;
|
||||
const dev = await B.connect({name: "CelloFX", service: SERVICE, timeout: 10000});
|
||||
Android.on("ble.disconnect", e => { if (e.id === dev.id) lost(); });
|
||||
await B.subscribe(dev.id, SERVICE, CHAR_STATE, bytes => onState(new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)));
|
||||
link = {
|
||||
write: (bytes, resp) => B.write(dev.id, SERVICE, CHAR_CTRL, bytes, {response: resp}),
|
||||
close: () => B.disconnect(dev.id),
|
||||
};
|
||||
await link.write(new Uint8Array([SNAPSHOT, 0, 0]), true);
|
||||
}
|
||||
|
||||
async function connectWeb() {
|
||||
const device = await navigator.bluetooth.requestDevice({filters: [{name: "CelloFX"}], optionalServices: [SERVICE]});
|
||||
device.addEventListener("gattserverdisconnected", lost);
|
||||
const server = await device.gatt.connect();
|
||||
const svc = await server.getPrimaryService(SERVICE);
|
||||
const ctrl = await svc.getCharacteristic(CHAR_CTRL);
|
||||
const st = await svc.getCharacteristic(CHAR_STATE);
|
||||
st.addEventListener("characteristicvaluechanged", ev => onState(ev.target.value));
|
||||
await st.startNotifications(); /* le module envoie alors un instantane */
|
||||
link = {
|
||||
write: (bytes, resp) => resp ? ctrl.writeValueWithResponse(bytes) : ctrl.writeValueWithoutResponse(bytes),
|
||||
close: () => device.gatt.connected && device.gatt.disconnect(),
|
||||
};
|
||||
await link.write(new Uint8Array([SNAPSHOT, 0, 0]), true); /* au cas ou */
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
if (!navigator.bluetooth) { demo = true; setConnected(true); msg("Web Bluetooth indisponible ici : mode démo (rien n'est envoyé). Ouvre la page dans Chrome Android en HTTPS."); return; }
|
||||
const native = window.Android?.isNative && window.Android.ble;
|
||||
if (!native && !navigator.bluetooth) {
|
||||
demo = true; setConnected(true);
|
||||
msg(window.isSecureContext
|
||||
? "Web Bluetooth indisponible dans ce navigateur : mode démo (rien n'est envoyé). Utilise l'app WebApp ou Chrome Android."
|
||||
: "Page non sécurisée : Web Bluetooth est bloqué (mode démo). Sers la page en HTTPS ou autorise l'origine dans chrome://flags (voir remote/serve.sh).");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
msg("Recherche de CelloFX…");
|
||||
device = await navigator.bluetooth.requestDevice({filters: [{name: "CelloFX"}], optionalServices: [SERVICE]});
|
||||
device.addEventListener("gattserverdisconnected", () => { ctrl = null; setConnected(false); msg("Déconnecté."); });
|
||||
const server = await device.gatt.connect();
|
||||
const svc = await server.getPrimaryService(SERVICE);
|
||||
ctrl = await svc.getCharacteristic(CHAR_CTRL);
|
||||
const st = await svc.getCharacteristic(CHAR_STATE);
|
||||
st.addEventListener("characteristicvaluechanged", onState);
|
||||
await st.startNotifications(); /* le module envoie alors un instantane */
|
||||
await ctrl.writeValueWithResponse(new Uint8Array([SNAPSHOT, 0, 0])); /* au cas ou */
|
||||
await (native ? connectNative() : connectWeb());
|
||||
setConnected(true); msg("Connecté.");
|
||||
} catch (e) { setConnected(false); msg(e.name === "NotFoundError" ? "Aucun appareil choisi." : "Connexion échouée : " + e.message); }
|
||||
} catch (e) {
|
||||
link = null; setConnected(false);
|
||||
msg(e.name === "NotFoundError" ? "Aucun appareil choisi."
|
||||
: e.code === "not_found" ? "CelloFX introuvable : la carte est-elle allumée et à portée ?"
|
||||
: e.code === "permission_denied" ? "Autorisation Bluetooth refusée (menu WebApp → Autorisations)."
|
||||
: "Connexion échouée : " + e.message);
|
||||
}
|
||||
}
|
||||
function disconnect() { device?.gatt?.connected && device.gatt.disconnect(); ctrl = null; demo = false; setConnected(false); msg(""); }
|
||||
function disconnect() { link?.close(); link = null; demo = false; setConnected(false); msg(""); }
|
||||
$("#btn").addEventListener("click", () => ($("#dot").classList.contains("on") ? disconnect() : connect()));
|
||||
|
||||
build();
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sert la telecommande CelloFX sur le reseau local (telephone et PC sur le meme Wi-Fi).
|
||||
# Web Bluetooth exige un contexte securise : voir le message affiche ci-dessous.
|
||||
# Usage: remote/serve.sh [port]
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
PORT="${1:-8000}"
|
||||
echo "Page : cellofx-remote.html (port $PORT)"
|
||||
for ip in $(hostname -I 2>/dev/null); do echo " http://$ip:$PORT/cellofx-remote.html"; done
|
||||
cat <<MSG
|
||||
|
||||
Sur le telephone (Chrome Android, une seule fois) :
|
||||
1. chrome://flags/#unsafely-treat-insecure-origin-as-secure
|
||||
2. y ajouter l'adresse http://<ip>:$PORT ci-dessus, passer le drapeau sur Enabled, relancer Chrome
|
||||
3. Bluetooth + localisation actives, ouvrir l'adresse, "Connecter"
|
||||
Ensuite : menu Chrome > "Ajouter a l'ecran d'accueil" (la page est alors lancee comme une app).
|
||||
MSG
|
||||
exec python3 -m http.server "$PORT" --bind 0.0.0.0
|
||||
Reference in new issue
Block a user