Télécommande IR
Projet réalisé par Fma38.
En cours.
Présentation
Martÿn est le papa d'un petit garçon qui ne peut communiquer que via le regard, en utilisant du matériel et logiciel conçu pour ça (marque Tobii).
Il souhaiterait pouvoir donner la possibilité à son fils de contrôler des jouets réels, plutôt que de le laisser uniquement interagir sur ordinateur. Il est donc venu à La Casemate pour demande de l'aide, et Steeve, le responsable des Open Labs, l'a renvoyé vers nous.
Après avoir acheté un jeu de construction sur une brocante, dont les moteurs sont pilotables par une télécommande IR, il est rapidement apparu que la solution la plus simple était de reproduire la télécommande. Un proto a été créé avec succés :
Pour aider Martÿn, et toutes les familles dans des situations similaires, il serait intéressant de construire un appareil universel, facile d'emploi, qui pourrait contrôler tout un tas d'appareils IR. La configuration devrait pouvoir être faite par des non spécialistes, les parents dans ce genre de situation n'ont pas franchement le temps de recompiler un firmware !
La première idée était de développer une module wifi, que le logiciel Tobii pourrait contrôler. Mais il s'avère que ce logiciel ne permet pas de lier un bouton à une url ! Le plus simple est donc de faire quelque chose d'autonome, genre interface à boutons. Et plutôt que de déployer une appli sur le PC, il vaudrait mieux tout intégrer dans le module, lequel proposerait une interface web pour d'une part la partir pilotage, et d'autre part la partie configuration.
Cahier des charges
En vrac.
- communication wifi
- interface web pour le contrôle et la configuration
- chargeur de batterie intégré
- batterie remplaçable rapidement si pas le temps de charger
- codes IR configurables, via apprentissage
- sorties à relais additionnelles
- ergonomique et robuste
- projet libre
Pour Marc :
- utilisation de W3.CSS
- une page présentant 6 boutons (2 lignes / 3 colonnes) aussi gros que possible sur la page :
- 2 boutons verts à gauche
- 2 boutons bleus au milieu
- 2 boutons rouges à droite
- une page de configuration permettant, pour chaque bouton, de sélectionner :
- le mode (toggle ou tempo) TODO: trouver des noms en français
- le délai (mode tempo uniquement)
Électronique
Une première version a été réalisée en utilisant un petit ATOM Lite, de M5Stack. Ce module intègre une led IR, un bouton, et une led RGB.
Firmware
Merci à Marc pour la partie html !
Les fichiers index.html, config.html, et ir.css doivent être mis dans un répertoire data/ afin de pouvoir être uploadés par les outils Arduino IDE.
IR_remote_ESP32.ino
#include <Arduino.h>
#include <stdint.h>
// Wifi stuff
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <EspHtmlTemplateProcessor.h>
//#include <ESPmDNS.h>
//#include <ArduinoOTA.h>
// IR stuff
#include <IrSenderPwmSoftDelay.h>
// JSON stuff
#include <ArduinoJson.h>
// EEPROM management
#include <EEPROM.h>
// -------------------------------------------
#define AP_SSID "IR Remote"
#define AP_PASSWORD "12345678"
#define AP_IP_ADDRESS IPAddress(192, 168, 187, 8)
#define AP_GATEWAY IPAddress(0, 0, 0, 0)
#define AP_SUBNET IPAddress(255, 255, 255, 0)
#define AP_PORT 5
#define AP_HIDDEN true
#define AP_MAX_CONNECTION 1
// -------------------------------------------
WebServer server(80);
EspHtmlTemplateProcessor templateProcessor(&server);
static constexpr frequency_t frequency = 36800U;
static const uint8_t PIN_IR = G12;
static const uint16_t SHORT = 800;
static const uint16_t LONG = 2400;
static const uint16_t stopArray[] = {
SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT
};
static const uint16_t greenUpArray[] = {
SHORT, SHORT, LONG, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, LONG, SHORT
};
static const uint16_t greenDownArray[] = {
SHORT, SHORT, LONG, SHORT, LONG, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT
};
static const uint16_t blueUpArray[] = {
SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, LONG, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, LONG, SHORT
};
static const uint16_t blueDownArray[] = {
SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, LONG, SHORT, LONG, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT
};
static const uint16_t redUpArray[] = {
SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, LONG, SHORT, SHORT, SHORT, LONG, SHORT
};
static const uint16_t redDownArray[] = {
SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, SHORT, LONG, SHORT, LONG, SHORT, SHORT, SHORT
};
static IrSequence stopSeq(stopArray, sizeof(stopArray)/sizeof(microseconds_t));
static IrSequence greenUpSeq(greenUpArray, sizeof(greenUpArray)/sizeof(microseconds_t));
static IrSequence greenDownSeq(greenDownArray, sizeof(greenDownArray)/sizeof(microseconds_t));
static IrSequence blueUpSeq(blueUpArray, sizeof(blueUpArray)/sizeof(microseconds_t));
static IrSequence blueDownSeq(blueDownArray, sizeof(blueDownArray)/sizeof(microseconds_t));
static IrSequence redUpSeq(redUpArray, sizeof(redUpArray)/sizeof(microseconds_t));
static IrSequence redDownSeq(redDownArray, sizeof(redDownArray)/sizeof(microseconds_t));
static IrSequence sequences[] = {
greenUpSeq, greenDownSeq, blueUpSeq, blueDownSeq, redUpSeq, redDownSeq
};
static IrSender* irSender;
long lastSeqStartTime = millis();
int8_t ir = -1;
uint16_t counter = 0;
// Vars saved in EEPROM
uint16_t buttonsSize = 300;
uint16_t buttonsFontSize = 200;
String buttonMode[6] = {
"delay",
"delay",
"delay",
"delay",
"delay",
"delay"
};
uint16_t buttonDelay[6] = {
2000,
2000,
2000,
2000,
2000,
2000
};
String buttonText[6] = {
"+",
"-",
"+",
"-",
"+",
"-"
};
StaticJsonDocument<200> configDoc;
// -------------------------------------------
void setup()
{
// Init serial coms
Serial.begin(115200);
while (!Serial);
Serial.println();
yield();
// Init EEPROM
// EEPROM.begin();
// Start file support
SPIFFS.begin();
delay(500);
// Starting AP
Serial.println(F("Server: configuring AP..."));
if (!WiFi.softAPConfig(AP_IP_ADDRESS, AP_GATEWAY, AP_SUBNET)) {
Serial.println(F("WiFi.softAPConfig() failed"));
errorBlinkForever();
}
Serial.println(F("Server: AP configured"));
yield();
Serial.println(F("Server: starting AP..."));
if (!WiFi.softAP(AP_SSID, AP_PASSWORD)) { //, AP_PORT, AP_HIDDEN, AP_MAX_CONNECTION)) {
Serial.println(F("WiFi.softAP() failed"));
errorBlinkForever();
}
IPAddress myIP = WiFi.softAPIP();
Serial.print(F("Server: AP IP address: "));
Serial.println(myIP);
Serial.println(F("Server: AP started"));
yield();
// Start webserver
Serial.println(F("Server: starting Web server..."));
server.on("/", handleRoot);
server.on("/config", handleConfig);
server.on("/getConfig", handleGetConfig);
server.on("/ir", handleIr);
server.serveStatic("/ir.css", SPIFFS, "/ir.css");
server.begin();
Serial.println(F("Server: web server started"));
yield();
// Create IR Sender
irSender = new IrSenderPwmSoftDelay(PIN_IR);
Serial.print(F("Shooting @ pin "));
Serial.println(irSender->getPin(), DEC);
// TODO: get config from EEPROM
}
void loop()
{
unsigned long prevMillis = millis();
server.handleClient();
yield();
if (ir == -1) {
sendSeq(&stopSeq);
}
else {
Serial.print("active ir=");
Serial.println(ir);
sendSeq(&sequences[ir]);
// Button mode is delay
if (buttonMode[ir] == "delay") {
Serial.print("counter=");
Serial.println(counter);
uint16_t elapsed = millis() - prevMillis; // compute elapsed time since previous iteration
prevMillis = millis();
// Counter big enough
if (counter > elapsed) {
counter -= elapsed; // decrement counter with elapsed time
}
// End of counter
else {
ir = -1; // stop IR
}
}
}
}
void sendSeq(IrSequence *seq)
{
// Ensure delay between sequences
while ((millis()-lastSeqStartTime) < 101);
lastSeqStartTime = millis();
irSender->send(*seq, frequency);
}
String processor(const String& key)
{
if (key == "BUTTONS_SIZE") {
return String(buttonsSize);
}
else if (key == "BUTTONS_FONT_SIZE") {
return String(buttonsFontSize);
}
else if (key == "BUTTON_0_DELAY") {
return String(buttonDelay[0]);
}
else if (key == "BUTTON_1_DELAY") {
return String(buttonDelay[1]);
}
else if (key == "BUTTON_2_DELAY") {
return String(buttonDelay[2]);
}
else if (key == "BUTTON_3_DELAY") {
return String(buttonDelay[3]);
}
else if (key == "BUTTON_4_DELAY") {
return String(buttonDelay[4]);
}
else if (key == "BUTTON_5_DELAY") {
return String(buttonDelay[5]);
}
else if (key == "BUTTON_0_TEXT") {
return String(buttonText[0]);
}
else if (key == "BUTTON_1_TEXT") {
return String(buttonText[1]);
}
else if (key == "BUTTON_2_TEXT") {
return String(buttonText[2]);
}
else if (key == "BUTTON_3_TEXT") {
return String(buttonText[3]);
}
else if (key == "BUTTON_4_TEXT") {
return String(buttonText[4]);
}
else if (key == "BUTTON_5_TEXT") {
return String(buttonText[5]);
}
else {
return "{{" + key + "}}";
}
}
void handleRoot()
{
templateProcessor.processAndSend("/index.html", processor);
}
void handleConfig()
{
bool saveToEEPROM = false;
if (server.hasArg("buttonsSize")) {
buttonsSize = server.arg("buttonsSize").toInt();
Serial.print("new buttonsSize=");
Serial.println(buttonsSize);
saveToEEPROM = true;
}
if (server.hasArg("buttonsFontSize")) {
buttonsFontSize = server.arg("buttonsFontSize").toInt();
Serial.print("new buttonsFontSize=");
Serial.println(buttonsFontSize);
saveToEEPROM = true;
}
if (server.hasArg("button0Mode")) {
buttonMode[0] = server.arg("button0Mode");
Serial.print("new buttonMode[0]=");
Serial.println(buttonMode[0]);
saveToEEPROM = true;
}
if (server.hasArg("button0Delay")) {
buttonDelay[0] = server.arg("button0Delay").toInt();
Serial.print("new buttonDelay[0]=");
Serial.println(buttonDelay[0]);
saveToEEPROM = true;
}
if (server.hasArg("button0Text")) {
buttonText[0] = server.arg("button0Text");
Serial.print("new buttonText[0]=");
Serial.println(buttonText[0]);
saveToEEPROM = true;
}
if (server.hasArg("button1Mode")) {
buttonMode[1] = server.arg("button1Mode");
Serial.print("new buttonMode[1]=");
Serial.println(buttonMode[1]);
saveToEEPROM = true;
}
if (server.hasArg("button1Delay")) {
buttonDelay[1] = server.arg("button1Delay").toInt();
Serial.print("new buttonDelay[1]=");
Serial.println(buttonDelay[1]);
saveToEEPROM = true;
}
if (server.hasArg("button1Text")) {
buttonText[1] = server.arg("button1Text");
Serial.print("new buttonText[1]=");
Serial.println(buttonText[1]);
saveToEEPROM = true;
}
if (server.hasArg("button2Mode")) {
buttonMode[2] = server.arg("button2Mode");
Serial.print("new buttonMode[2]=");
Serial.println(buttonMode[2]);
saveToEEPROM = true;
}
if (server.hasArg("button2Delay")) {
buttonDelay[2] = server.arg("button2Delay").toInt();
Serial.print("new buttonDelay[2]=");
Serial.println(buttonDelay[2]);
saveToEEPROM = true;
}
if (server.hasArg("button2Text")) {
buttonText[2] = server.arg("button2Text");
Serial.print("new buttonText[2]=");
Serial.println(buttonText[2]);
saveToEEPROM = true;
}
if (server.hasArg("button3Mode")) {
buttonMode[3] = server.arg("button3Mode");
Serial.print("new buttonMode[3]=");
Serial.println(buttonMode[3]);
saveToEEPROM = true;
}
if (server.hasArg("button3Delay")) {
buttonDelay[3] = server.arg("button3Delay").toInt();
Serial.print("new buttonDelay[3]=");
Serial.println(buttonDelay[3]);
saveToEEPROM = true;
}
if (server.hasArg("button3Text")) {
buttonText[3] = server.arg("button3Text");
Serial.print("new buttonText[3]=");
Serial.println(buttonText[3]);
saveToEEPROM = true;
}
if (server.hasArg("button4Mode")) {
buttonMode[4] = server.arg("button4Mode");
Serial.print("new buttonMode[4]=");
Serial.println(buttonMode[4]);
saveToEEPROM = true;
}
if (server.hasArg("button4Delay")) {
buttonDelay[4] = server.arg("button4Delay").toInt();
Serial.print("new buttonDelay[4]=");
Serial.println(buttonDelay[4]);
saveToEEPROM = true;
}
if (server.hasArg("button4Text")) {
buttonText[4] = server.arg("button4Text");
Serial.print("new buttonText[4]=");
Serial.println(buttonText[4]);
saveToEEPROM = true;
}
if (server.hasArg("button5Mode")) {
buttonMode[5] = server.arg("button5Mode");
Serial.print("new buttonMode[5]=");
Serial.println(buttonMode[5]);
saveToEEPROM = true;
}
if (server.hasArg("button5Delay")) {
buttonDelay[5] = server.arg("button5Delay").toInt();
Serial.print("new buttonDelay[5]=");
Serial.println(buttonDelay[5]);
saveToEEPROM = true;
}
if (server.hasArg("button5Text")) {
buttonText[5] = server.arg("button5Text");
Serial.print("new buttonText[5]=");
Serial.println(buttonText[5]);
saveToEEPROM = true;
}
if (saveToEEPROM == true) {
// TODO
}
templateProcessor.processAndSend("/config.html", processor);
}
void handleGetConfig()
{
Serial.println("handleGetConfig()");
configDoc["button0Mode"] = buttonMode[0];
configDoc["button1Mode"] = buttonMode[1];
configDoc["button2Mode"] = buttonMode[2];
configDoc["button3Mode"] = buttonMode[3];
configDoc["button4Mode"] = buttonMode[4];
configDoc["button5Mode"] = buttonMode[5];
configDoc["button0Text"] = buttonText[0];
configDoc["button1Text"] = buttonText[1];
configDoc["button2Text"] = buttonText[2];
configDoc["button3Text"] = buttonText[3];
configDoc["button4Text"] = buttonText[4];
configDoc["button5Text"] = buttonText[5];
String response;
serializeJson(configDoc, response);
server.send(200, "application/json", response);
}
void handleIr()
{
// Serial.println("handleIr()");
if (server.hasArg("channel")) {
uint8_t ir_ = server.arg("channel").toInt();
Serial.print("ir_=");
Serial.println(ir_);
// Button mode is 'delay'
if (buttonMode[ir_] == "delay") {
// Button was not active
if (ir_ != ir) {
ir = ir_; // set current IR
counter = buttonDelay[ir_]; // load counter with button delay
}
}
// Button mode is 'toggle'
else {
if (ir_ == ir) {
ir = -1; // switch off IR
}
else {
ir = ir_; // set new IR
}
}
Serial.print("new ir=");
Serial.println(ir);
}
server.send(200, "text/plain", String(ir).c_str());
}index.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="ir.css">
<script>
function onLoad()
{
// Register getIr() function
setInterval (function() {
getIr();
}, 250);
}
function getIr()
{
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = irChanged;
xhttp.open("POST", "/ir", true);
xhttp.send();
}
function setIr(irChannel)
{
let xhttp = new XMLHttpRequest();
xhttp.open("POST", "/ir", true);
xhttp.timeout = 500;
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send("channel=" + irChannel);
}
// Callbacks
function irChanged()
{
if (this.readyState == XMLHttpRequest.DONE && this.status == 200) {
let ir = this.responseText;
document.getElementById("ir_0").style.color = "black"; // restore color
document.getElementById("ir_1").style.color = "black"; // restore color
document.getElementById("ir_2").style.color = "black"; // restore color
document.getElementById("ir_3").style.color = "black"; // restore color
document.getElementById("ir_4").style.color = "black"; // restore color
document.getElementById("ir_5").style.color = "black"; // restore color
if (ir != -1) {
document.getElementById("ir_"+ir).style.color = "white"; // set color
}
}
}
</script>
<title>Télécommande IR</title>
</head>
<body onload="onLoad()">
<button class="button green" style="width:{{BUTTONS_SIZE}}px; height:{{BUTTONS_SIZE}}px; font-size:{{BUTTONS_FONT_SIZE}}px" id="ir_0" onclick="setIr(0)">{{BUTTON_0_TEXT}}</button>
<button class="button red" style="width:{{BUTTONS_SIZE}}px; height:{{BUTTONS_SIZE}}px; font-size:{{BUTTONS_FONT_SIZE}}px" id="ir_2" onclick="setIr(2)">{{BUTTON_2_TEXT}}</button>
<button class="button blue" style="width:{{BUTTONS_SIZE}}px; height:{{BUTTONS_SIZE}}px; font-size:{{BUTTONS_FONT_SIZE}}px" id="ir_4" onclick="setIr(4)">{{BUTTON_4_TEXT}}</button>
<br/>
<button class="button green" style="width:{{BUTTONS_SIZE}}px; height:{{BUTTONS_SIZE}}px; font-size:{{BUTTONS_FONT_SIZE}}px" id="ir_1" onclick="setIr(1)">{{BUTTON_1_TEXT}}</button>
<button class="button red" style="width:{{BUTTONS_SIZE}}px; height:{{BUTTONS_SIZE}}px; font-size:{{BUTTONS_FONT_SIZE}}px" id="ir_3" onclick="setIr(3)">{{BUTTON_3_TEXT}}</button>
<button class="button blue" style="width:{{BUTTONS_SIZE}}px; height:{{BUTTONS_SIZE}}px; font-size:{{BUTTONS_FONT_SIZE}}px" id="ir_5" onclick="setIr(5)">{{BUTTON_5_TEXT}}</button>
<br/>
</body>
</html>config.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- W3.CSS support -->
<link rel="stylesheet" href="ir.css">
<script>
function onLoad()
{
getConfig();
updatePreview();
document.getElementById("buttonPreview").disabled = true;
}
function updatePreview(){
ButtonsSize = document.getElementById("buttonsSize").value //document.forms['settings']['buttonsSize'].value;
buttonsFontSize = document.getElementById("buttonsFontSize").value //document.forms['settings']['buttonsFontSize'].value;
document.getElementById("buttonPreview").style.width = ButtonsSize + "px";
document.getElementById("buttonPreview").style.height = ButtonsSize + "px";
document.getElementById("buttonPreview").style.fontSize = buttonsFontSize + "px";
}
function getConfig()
{
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = configChanged;
xhttp.open("POST", "/getConfig", true);
xhttp.responseType = 'json';
xhttp.send();
}
function saveConfig(){
settings = new FormData(document.getElementById("settings"));
let xhttp = new XMLHttpRequest();
xhttp.open("POST", "/config", true);
xhttp.timeout = 500;
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send(settings);
}
// Callbacks
function configChanged()
{
if (this.readyState == XMLHttpRequest.DONE && this.status == 200) {
let responseObj = this.response;
document.getElementById("button0Mode").selectedIndex = ((responseObj.button0Mode == "toggle") ? 0 : 1);
document.getElementById("button1Mode").selectedIndex = ((responseObj.button1Mode == "toggle") ? 0 : 1);
document.getElementById("button2Mode").selectedIndex = ((responseObj.button2Mode == "toggle") ? 0 : 1);
document.getElementById("button3Mode").selectedIndex = ((responseObj.button3Mode == "toggle") ? 0 : 1);
document.getElementById("button4Mode").selectedIndex = ((responseObj.button4Mode == "toggle") ? 0 : 1);
document.getElementById("button5Mode").selectedIndex = ((responseObj.button5Mode == "toggle") ? 0 : 1);
}
}
</script>
<title>Télécommande IR - Configuration</title>
</head>
<body onload="onLoad()">
<form id="settings" action="/config" method="POST">
<!-- Buttons -->
<table>
<tbody>
<!-- Bouton 0 -->
<tr>
<td>
<div class="buttonConfig green">{{BUTTON_0_TEXT}}</div>
</td>
<td>
<select name="button0Mode" id="button0Mode">
<option value="toggle">Bascule</option>
<option value="delay">Délai</option>
</select>
</td>
<td>
<input name="button0Delay" size="4" type="text" value={{BUTTON_0_DELAY}}>
</td>
<td>ms</td>
</tr>
</tbody>
<!-- Bouton 1 -->
<tr>
<td>
<div class="buttonConfig green">{{BUTTON_1_TEXT}}</div>
</td>
<td>
<select name="button1Mode" id="button1Mode">
<option value="toggle">Bascule</option>
<option value="delay">Délai</option>
</select>
</td>
<td>
<input name="button1Delay" size="4" type="text" value={{BUTTON_1_DELAY}}>
</td>
<td>ms</td>
</tr>
<!-- Bouton 2 -->
<tr>
<td>
<div class="buttonConfig red">{{BUTTON_2_TEXT}}</div>
</td>
<td>
<select name="button2Mode" id="button2Mode">
<option value="toggle">Bascule</option>
<option value="delay">Délai</option>
</select>
</td>
<td>
<input name="button2Delay" size="4" type="text" value={{BUTTON_2_DELAY}}>
</td>
<td>ms</td>
</tr>
<!-- Bouton 3 -->
<tr>
<td>
<div class="buttonConfig red">{{BUTTON_3_TEXT}}</div>
</td>
<td>
<select name="button3Mode" id="button3Mode">
<option value="toggle">Bascule</option>
<option value="delay">Délai</option>
</select>
</td>
<td>
<input name="button3Delay" size="4" type="text" value={{BUTTON_3_DELAY}}>
</td>
<td>ms</td>
</tr>
<!-- Bouton 4 -->
<tr>
<td>
<div class="buttonConfig blue">{{BUTTON_4_TEXT}}</div>
</td>
<td>
<select name="button4Mode" id="button4Mode">
<option value="toggle">Bascule</option>
<option value="delay">Délai</option>
</select>
</td>
<td>
<input name="button4Delay" size="4" type="text" value={{BUTTON_4_DELAY}}>
</td>
<td>ms</td>
</tr>
<!-- Bouton 5 -->
<tr>
<td>
<div class="buttonConfig blue">{{BUTTON_5_TEXT}}</div>
</td>
<td>
<select name="button5Mode" id="button5Mode">
<option value="toggle">Bascule</option>
<option value="delay">Délai</option>
</select>
</td>
<td>
<input name="button5Delay" size="4" type="text" value={{BUTTON_5_DELAY}}>
</td>
<td>ms</td>
</tr>
</table>
<!-- Global -->
<br />
<table>
<tbody>
<tr>
<td>Taille des boutons</td>
<td><input name="buttonsSize" id="buttonsSize" size="4" type="text" value={{BUTTONS_SIZE}} /></td> <!--onkeyup=updatePreview()>-->
</tr>
<tr>
<td>Taille de police</td>
<td><input name="buttonsFontSize" id="buttonsFontSize" size="4" type="text" value={{BUTTONS_FONT_SIZE}} /></td> <!--onkeyup=updatePreview()>-->
</tr>
</tbody>
</table>
<button id="buttonPreview" class="button green">+</button>
<br />
<button>Save</button>
</form>
</body>
</html>ir.css
body {
background-color: darkgray;
}
.button {
border-color: gray;
border-width: 10px;
border-radius: 20px;
font-weight: bold;
color: black;
margin: 20px;
text-align: center;
vertical-align: middle;
}
.buttonConfig {
width: 32px;
height: 32px;
border-color: gray;
border-width: 5px;
border-radius: 5px;
font-weight: bold;
color: black;
display: flex;
align-items: center;
justify-content: center;
}
.green {
background-color: green;
}
.red {
background-color: red;
}
.blue {
background-color: blue;
}
:hover {
border-color: yellow
}Prototype
Liens
Matériel
- https://www.gotronic.fr/art-module-firebeetle-2-esp32-e-dfr0654-34177.htm
- https://www.gotronic.fr/art-accu-lipo-3-7-vcc-400-mah-pr502535-5812.htm
- https://www.gotronic.fr/art-accu-lipo-3-7-vcc-1000-mah-pr523450-5813.htm
- https://www.gotronic.fr/art-module-atom-lite-c008-32196.htm
- https://www.gotronic.fr/art-module-atom-tailbat-t001-32201.htm
- https://flipperzero.one (pourrait faire une super plateforme permettant de contrôler tout un tas de choses)