maj bluetooth compatible webapp

This commit is contained in:
thomas committed 2026-10-24 00:54:19 +02:00
1 parent 3757975e51
commit e94499ba1e
22 files changed
+68217 -67436

No files matched your search

File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
@@ -16,6 +16,8 @@
#include <touchgfx/widgets/TextAreaWithWildcard.hpp> #include <touchgfx/widgets/TextAreaWithWildcard.hpp>
#include <touchgfx/widgets/ToggleButton.hpp> #include <touchgfx/widgets/ToggleButton.hpp>
#include <texts/TextKeysAndLanguages.hpp> #include <texts/TextKeysAndLanguages.hpp>
#include <audio_effects.h>
#include <string.h>
namespace ui namespace ui
{ {
@@ -144,6 +146,44 @@ inline void layoutParamRow(touchgfx::TextArea& label, touchgfx::Slider& slider,
slider.setXY(184, y); /* curseur 456x52 */ slider.setXY(184, y); /* curseur 456x52 */
value.init(touchgfx::TypedText(T_VALUE_M_LEFT), accentColor, 656, y + 10, 136, 34); 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 } // namespace ui
#endif // UITHEME_HPP #endif // UITHEME_HPP
@@ -12,12 +12,16 @@ public:
virtual ~EchoView() {} virtual ~EchoView() {}
virtual void setupScreen(); virtual void setupScreen();
virtual void tearDownScreen(); virtual void tearDownScreen();
virtual void handleTickEvent();
virtual void toggleClicked(); virtual void toggleClicked();
virtual void sliderDelaiChanged(int value); virtual void sliderDelaiChanged(int value);
virtual void sliderFeedbackChanged(int value); virtual void sliderFeedbackChanged(int value);
virtual void sliderMixChanged(int value); virtual void sliderMixChanged(int value);
protected: protected:
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
void syncFromParams();
ui::ParamsWatcher watcher;
ui::Header header; ui::Header header;
ui::ValueText values[3]; ui::ValueText values[3];
}; };
@@ -12,6 +12,7 @@ public:
virtual ~EqualizerView() {} virtual ~EqualizerView() {}
virtual void setupScreen(); virtual void setupScreen();
virtual void tearDownScreen(); virtual void tearDownScreen();
virtual void handleTickEvent();
virtual void toggleClicked(); virtual void toggleClicked();
virtual void sliderBand1Changed(int value); virtual void sliderBand1Changed(int value);
@@ -25,6 +26,9 @@ public:
virtual void sliderBand9Changed(int value); virtual void sliderBand9Changed(int value);
virtual void sliderBand10Changed(int value); virtual void sliderBand10Changed(int value);
protected: protected:
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
void syncFromParams();
ui::ParamsWatcher watcher;
ui::Header header; ui::Header header;
ui::ValueText values[10]; ui::ValueText values[10];
touchgfx::Box zeroDbLine; touchgfx::Box zeroDbLine;
@@ -12,6 +12,7 @@ public:
virtual ~OctaverView() {} virtual ~OctaverView() {}
virtual void setupScreen(); virtual void setupScreen();
virtual void tearDownScreen(); virtual void tearDownScreen();
virtual void handleTickEvent();
virtual void toggleClicked(); virtual void toggleClicked();
virtual void slider1Clicked(int value); virtual void slider1Clicked(int value);
@@ -20,6 +21,9 @@ public:
virtual void slider4Clicked(int value); virtual void slider4Clicked(int value);
virtual void slider5Clicked(int value); virtual void slider5Clicked(int value);
protected: protected:
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
void syncFromParams();
ui::ParamsWatcher watcher;
ui::Header header; ui::Header header;
ui::ValueText values[5]; ui::ValueText values[5];
@@ -12,12 +12,16 @@ public:
virtual ~OverdriveView() {} virtual ~OverdriveView() {}
virtual void setupScreen(); virtual void setupScreen();
virtual void tearDownScreen(); virtual void tearDownScreen();
virtual void handleTickEvent();
virtual void toggleClicked(); virtual void toggleClicked();
virtual void sliderDriveChanged(int value); virtual void sliderDriveChanged(int value);
virtual void sliderToneChanged(int value); virtual void sliderToneChanged(int value);
virtual void sliderLevelChanged(int value); virtual void sliderLevelChanged(int value);
protected: protected:
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
void syncFromParams();
ui::ParamsWatcher watcher;
ui::Header header; ui::Header header;
ui::ValueText values[3]; ui::ValueText values[3];
}; };
@@ -33,6 +33,9 @@ protected:
}; };
void onCardOpen(const EffectCard& card); 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); void setLooperState(LooperState state);
/* Buffer du CanvasWidgetRenderer (voyant du looper), propre a cet ecran comme /* Buffer du CanvasWidgetRenderer (voyant du looper), propre a cet ecran comme
@@ -12,12 +12,16 @@ public:
virtual ~ReverbView() {} virtual ~ReverbView() {}
virtual void setupScreen(); virtual void setupScreen();
virtual void tearDownScreen(); virtual void tearDownScreen();
virtual void handleTickEvent();
virtual void toggleClicked(); virtual void toggleClicked();
virtual void sliderTailleChanged(int value); virtual void sliderTailleChanged(int value);
virtual void sliderAmortiChanged(int value); virtual void sliderAmortiChanged(int value);
virtual void sliderMixChanged(int value); virtual void sliderMixChanged(int value);
protected: protected:
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
void syncFromParams();
ui::ParamsWatcher watcher;
ui::Header header; ui::Header header;
ui::ValueText values[3]; ui::ValueText values[3];
}; };
@@ -12,11 +12,15 @@ public:
virtual ~SaturationView() {} virtual ~SaturationView() {}
virtual void setupScreen(); virtual void setupScreen();
virtual void tearDownScreen(); virtual void tearDownScreen();
virtual void handleTickEvent();
virtual void toggleClicked(); virtual void toggleClicked();
virtual void sliderDriveClicked(int value); virtual void sliderDriveClicked(int value);
virtual void sliderLevelClicked(int value); virtual void sliderLevelClicked(int value);
protected: protected:
/* Relit les parametres d'effets dans les widgets (setupScreen + changements distants) */
void syncFromParams();
ui::ParamsWatcher watcher;
ui::Header header; ui::Header header;
ui::ValueText values[2]; ui::ValueText values[2];
}; };
@@ -20,14 +20,30 @@ void EchoView::setupScreen()
add(values[i]); add(values[i]);
} }
syncFromParams();
}
void EchoView::syncFromParams()
{
FxEchoParams& p = AudioEffects_GetParams()->echo; FxEchoParams& p = AudioEffects_GetParams()->echo;
toggleButton1.forceState(p.enabled); toggleButton1.forceState(p.enabled);
toggleButton1.invalidate();
slider1.setValue((int16_t)(p.delay_ms / 10.0f + 0.5f)); slider1.setValue((int16_t)(p.delay_ms / 10.0f + 0.5f));
slider2.setValue((int16_t)(p.feedback / 0.95f * 100.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)); slider3.setValue((int16_t)(p.mix * 100.0f + 0.5f));
values[0].show("%d ms", slider1.getValue() * 10); values[0].show("%d ms", slider1.getValue() * 10);
values[1].show("%d %%", slider2.getValue()); values[1].show("%d %%", slider2.getValue());
values[2].show("%d %%", slider3.getValue()); values[2].show("%d %%", slider3.getValue());
watcher.snapshot();
}
void EchoView::handleTickEvent()
{
EchoViewBase::handleTickEvent();
if (watcher.changed())
{
syncFromParams();
}
} }
void EchoView::tearDownScreen() void EchoView::tearDownScreen()
@@ -42,8 +42,6 @@ void EqualizerView::setupScreen()
&bandSlider6, &bandSlider7, &bandSlider8, &bandSlider9, &bandSlider10 }; &bandSlider6, &bandSlider7, &bandSlider8, &bandSlider9, &bandSlider10 };
touchgfx::TextArea* labels[10] = { &textArea2, &textArea2_1, &textArea2_2, &textArea2_3, &textArea2_4, touchgfx::TextArea* labels[10] = { &textArea2, &textArea2_1, &textArea2_2, &textArea2_3, &textArea2_4,
&textArea2_5, &textArea2_6, &textArea2_7, &textArea2_8, &textArea2_9 }; &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++) for (int i = 0; i < 10; i++)
{ {
@@ -53,10 +51,34 @@ void EqualizerView::setupScreen()
labels[i]->setColor(ui::textMuted()); labels[i]->setColor(ui::textMuted());
values[i].init(touchgfx::TypedText(T_VALUE_S_CENTER), accent, x - 10, VALUE_Y, 76, 24); values[i].init(touchgfx::TypedText(T_VALUE_S_CENTER), accent, x - 10, VALUE_Y, 76, 24);
add(values[i]); 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)); 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())); values[i].showDb(sliderToDb(sliders[i]->getValue()));
} }
watcher.snapshot();
}
void EqualizerView::handleTickEvent()
{
EqualizerViewBase::handleTickEvent();
if (watcher.changed())
{
syncFromParams();
}
} }
void EqualizerView::tearDownScreen() void EqualizerView::tearDownScreen()
@@ -33,17 +33,34 @@ void OctaverView::setupScreen()
add(values[i]); add(values[i]);
} }
syncFromParams();
}
void OctaverView::syncFromParams()
{
FxOctaverParams& p = AudioEffects_GetParams()->octaver; FxOctaverParams& p = AudioEffects_GetParams()->octaver;
toggleButton1.forceState(p.enabled); toggleButton1.forceState(p.enabled);
toggleButton1.invalidate();
slider1.setValue((int16_t)(p.level_down2 * 100.0f + 0.5f)); slider1.setValue((int16_t)(p.level_down2 * 100.0f + 0.5f));
slider2.setValue((int16_t)(p.level_down1 * 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)); slider3.setValue((int16_t)(p.level_dry * 100.0f + 0.5f));
slider4.setValue((int16_t)(p.level_up1 * 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)); 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++) for (int i = 0; i < 5; i++)
{ {
showLevel(i, sliders[i]->getValue()); showLevel(i, sliders[i]->getValue());
} }
watcher.snapshot();
}
void OctaverView::handleTickEvent()
{
OctaverViewBase::handleTickEvent();
if (watcher.changed())
{
syncFromParams();
}
} }
void OctaverView::tearDownScreen() void OctaverView::tearDownScreen()
@@ -20,14 +20,30 @@ void OverdriveView::setupScreen()
add(values[i]); add(values[i]);
} }
syncFromParams();
}
void OverdriveView::syncFromParams()
{
FxOverdriveParams& p = AudioEffects_GetParams()->overdrive; FxOverdriveParams& p = AudioEffects_GetParams()->overdrive;
toggleButton1.forceState(p.enabled); toggleButton1.forceState(p.enabled);
toggleButton1.invalidate();
slider1.setValue((int16_t)(p.drive * 100.0f + 0.5f)); slider1.setValue((int16_t)(p.drive * 100.0f + 0.5f));
slider2.setValue((int16_t)(p.tone * 100.0f + 0.5f)); slider2.setValue((int16_t)(p.tone * 100.0f + 0.5f));
slider3.setValue((int16_t)(p.level * 100.0f + 0.5f)); slider3.setValue((int16_t)(p.level * 100.0f + 0.5f));
values[0].show("%d %%", slider1.getValue()); values[0].show("%d %%", slider1.getValue());
values[1].show("%d %%", slider2.getValue()); values[1].show("%d %%", slider2.getValue());
values[2].show("%d %%", slider3.getValue()); values[2].show("%d %%", slider3.getValue());
watcher.snapshot();
}
void OverdriveView::handleTickEvent()
{
OverdriveViewBase::handleTickEvent();
if (watcher.changed())
{
syncFromParams();
}
} }
void OverdriveView::tearDownScreen() void OverdriveView::tearDownScreen()
@@ -95,11 +95,23 @@ void OverviewView::setupScreen()
buttonWithLabel7.setLabelText(touchgfx::TypedText(T_SPECTRUM)); buttonWithLabel7.setLabelText(touchgfx::TypedText(T_SPECTRUM));
/* --- Etat initial --- */ /* --- Etat initial --- */
syncFromParams();
setLooperState(LOOPER_IDLE);
}
void OverviewView::syncFromParams()
{
const AudioEffectsParams* params = AudioEffects_GetParams();
toggleButton1.forceState(params->audio_on_off); toggleButton1.forceState(params->audio_on_off);
toggleButton1.invalidate();
const int volume = (int)(params->master_volume * 100.0f + 0.5f); const int volume = (int)(params->master_volume * 100.0f + 0.5f);
slider1.setValue((int16_t)volume); slider1.setValue((int16_t)volume);
volumeValue.show("%d %%", volume); volumeValue.show("%d %%", volume);
setLooperState(LOOPER_IDLE); for (int i = 0; i < 6; i++)
{
cards[i].refresh();
}
watcher.snapshot();
} }
void OverviewView::tearDownScreen() void OverviewView::tearDownScreen()
@@ -152,6 +164,11 @@ void OverviewView::handleTickEvent()
{ {
OverviewViewBase::handleTickEvent(); OverviewViewBase::handleTickEvent();
if (watcher.changed())
{
syncFromParams();
}
/* repeatButton1 declenche une action a l'appui puis en continu tant qu'il est /* 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 * maintenu (comportement standard TouchGFX d'un RepeatButton) : ca ne donne pas
* directement un evenement "relache". On detecte donc nous-memes le front sur son * directement un evenement "relache". On detecte donc nous-memes le front sur son
@@ -24,14 +24,30 @@ void ReverbView::setupScreen()
add(values[i]); add(values[i]);
} }
syncFromParams();
}
void ReverbView::syncFromParams()
{
FxReverbParams& p = AudioEffects_GetParams()->reverb; FxReverbParams& p = AudioEffects_GetParams()->reverb;
toggleButton1.forceState(p.enabled); toggleButton1.forceState(p.enabled);
toggleButton1.invalidate();
slider1.setValue((int16_t)(p.room_size * 100.0f + 0.5f)); slider1.setValue((int16_t)(p.room_size * 100.0f + 0.5f));
slider2.setValue((int16_t)(p.damping * 100.0f + 0.5f)); slider2.setValue((int16_t)(p.damping * 100.0f + 0.5f));
slider3.setValue((int16_t)(p.mix * 100.0f + 0.5f)); slider3.setValue((int16_t)(p.mix * 100.0f + 0.5f));
values[0].show("%d %%", slider1.getValue()); values[0].show("%d %%", slider1.getValue());
values[1].show("%d %%", slider2.getValue()); values[1].show("%d %%", slider2.getValue());
values[2].show("%d %%", slider3.getValue()); values[2].show("%d %%", slider3.getValue());
watcher.snapshot();
}
void ReverbView::handleTickEvent()
{
ReverbViewBase::handleTickEvent();
if (watcher.changed())
{
syncFromParams();
}
} }
void ReverbView::tearDownScreen() void ReverbView::tearDownScreen()
@@ -17,12 +17,28 @@ void SaturationView::setupScreen()
add(values[0]); add(values[0]);
add(values[1]); add(values[1]);
syncFromParams();
}
void SaturationView::syncFromParams()
{
FxSaturationParams& p = AudioEffects_GetParams()->saturation; FxSaturationParams& p = AudioEffects_GetParams()->saturation;
toggleButton1.forceState(p.enabled); toggleButton1.forceState(p.enabled);
toggleButton1.invalidate();
slider1.setValue((int16_t)(p.drive * 100.0f + 0.5f)); slider1.setValue((int16_t)(p.drive * 100.0f + 0.5f));
slider2.setValue((int16_t)(p.level * 100.0f + 0.5f)); slider2.setValue((int16_t)(p.level * 100.0f + 0.5f));
values[0].show("%d %%", slider1.getValue()); values[0].show("%d %%", slider1.getValue());
values[1].show("%d %%", slider2.getValue()); values[1].show("%d %%", slider2.getValue());
watcher.snapshot();
}
void SaturationView::handleTickEvent()
{
SaturationViewBase::handleTickEvent();
if (watcher.changed())
{
syncFromParams();
}
} }
void SaturationView::tearDownScreen() void SaturationView::tearDownScreen()
+55 -19
View File
@@ -69,7 +69,7 @@ const SECTIONS = [
const state = new Map(); /* id -> valeur fil */ const state = new Map(); /* id -> valeur fil */
const widgets = new Map(); /* id -> fonction de mise a jour de l'UI */ 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 $ = s => document.querySelector(s);
const msg = t => { $("#msg").textContent = t || ""; }; const msg = t => { $("#msg").textContent = t || ""; };
@@ -124,45 +124,81 @@ function send(id, value) {
async function flush() { async function flush() {
timer = 0; timer = 0;
const items = [...pending]; pending = new Map(); const items = [...pending]; pending = new Map();
if (!ctrl) return; if (!link) return;
try { try {
for (let i = 0; i < items.length; i += MAX_RECORDS) { for (let i = 0; i < items.length; i += MAX_RECORDS) {
const chunk = items.slice(i, i + MAX_RECORDS); const chunk = items.slice(i, i + MAX_RECORDS);
const buf = new DataView(new ArrayBuffer(chunk.length * 3)); 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); }); 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); } } catch (e) { msg("Envoi impossible : " + e.message); }
} }
function onState(ev) { function onState(dv) {
const dv = ev.target.value;
for (let o = 0; o + 3 <= dv.byteLength; o += 3) { for (let o = 0; o + 3 <= dv.byteLength; o += 3) {
const id = dv.getUint8(o), v = dv.getInt16(o + 1, true); const id = dv.getUint8(o), v = dv.getInt16(o + 1, true);
state.set(id, v); widgets.get(id)?.(v); 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"; } 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() { 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 { try {
msg("Recherche de CelloFX…"); msg("Recherche de CelloFX…");
device = await navigator.bluetooth.requestDevice({filters: [{name: "CelloFX"}], optionalServices: [SERVICE]}); await (native ? connectNative() : connectWeb());
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 */
setConnected(true); msg("Connecté."); 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())); $("#btn").addEventListener("click", () => ($("#dot").classList.contains("on") ? disconnect() : connect()));
build(); build();
+18
View File
@@ -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