Skip to content

Final Project development

This page follows the steps I made to complete my final project during the different week of the Fab Academy.

Week 01 - Plan and sketch for a potential final project

Socialization acceptance device

This idea came to me thinking about people with autistic syndrom for whom socialization is a big difficulty just like the ability to express their mindset. A lot of them have the will to meet and bond with people but they don’t always have the social skill to do so. At first, I thought on made it appear as a message on a t-shirt to be seen by everyone. A less difficult choice would be on a wearable small screen maybe on a bracer or as badges.

first scale idea first idea for output

I imagined a fidget as an input device. Fidgets are often used with persons with autism for regulating themselves, through manipulating, massaging or chewing.

first sketch for input

The person could use to express its mindset, through a mood gauge and socialbility acceptance. With a potentiometer, they could share their mood from angry, sad, neutral, good, or happy. With a bunch of switches, they could share their level of acceptance of socialization from “I need to be alone” to “I need support”. Designing it made me re-think with a dual-scale “mood/interaction acceptance”

second scale idea

Week 02 - Computer Aided Design

Input device

During the Computer aided design week, I designed a 3D remote device for my final project.

second render

The user couldn’t enter its mood through through the potentiometer and enter his interaction acceptance through the switch activated. To validate the state and send the response to an output device, he needs to push the red button.

Output display

For the output, I thought to display a smiley face as it’s an international form of communicating even without reading hability.

I started making a table, combining a mood scale and intercation acceptance scale with smileys. Combining them with Emoji Kitchen could provide a reperesentation of the user’s mindset.

combining smileys table

I also started to make a svg file of smileys to make them appear on the input device with cutting or engraving.

vector smileys

Week 03 - Computer controlled cutting

I decided to make a plywood model of my final project to see the size. I’m aware that living hinge aren’t strong enough for something that will be manipulated a lot. I have rebuilt my model on fusion using metal sheet.

3d plywood model

On my third attempt, I could finally make a nice lasercut and assemble it.

freshly baked lasercut

The hinges don’t completely fit, probably because the pattern make it too loose. The size could be a bit longer to facilitate manipulation between the two hands and i’d like it to be thinner to handed it more easily.

model in the hand

model hero shot

For my final project, I don’t think I will use the model I created this week, beacuse it’s too fragile and absolutely not ergonomic. Anyway, it helps me defining more precisely what I need : a shape more elongated to facilitate manibpulation, a more flexible material like rubber or silicone, a reccessed button to avoid unattended push.

Week 06 - Embedded programming

During this week, I learned about programming on the RP2040 Micro-controller and chose to test potentiometer programming in prevision of my final project.

To connect the potentiometer to my board, I added connexion header on it to have a better access at the 3.3V pin.

A2 and A3 are available but A3 is link to a 1000 ohms resistor. Potentiometer being a variable resistor, I thought using the pin without resistor was more pertinent. But after testing the two of them, they both works well.

I made this setup.

ChatGPT provided me this code to this prompt and some specification to correct issues:

make an arduino code for a potentiometer lighting 1 then 2 then 3 led as it is turn.

const int potPin = A2;  // analog pin for the potentiometer
const int ledPin1 = 26;  // the number of the first LED pin
const int ledPin2 = 0;  // the number of the second LED pin
const int ledPin3 = 1;  // the number of the third LED pin
const int numLEDs = 3;  // number of LEDs

void setup() {
  pinMode(ledPin1, OUTPUT);
  pinMode(ledPin2, OUTPUT);
  pinMode(ledPin3, OUTPUT);
}

void loop() {
  // Read the potentiometer value
  int potValue = analogRead(potPin);

  // Map the potentiometer value to the range of LEDs
  int ledLevel = map(potValue, 0, 1023, 0, numLEDs + 1);

  // Turn on the LEDs based on the mapped value
  digitalWrite(ledPin1, ledLevel >= 1);
  digitalWrite(ledPin2, ledLevel >= 2);
  digitalWrite(ledPin3, ledLevel >= 3);

  delay(100);  // Adjust delay as needed to control the responsiveness
}
potPin constant define the Analog pin on A2 and don’t need to be set as input in the setup part.
In the main program, analogRead will get a value between 0 and 1023 from the potentiometer and make a variable from it. Then it creates from it a ledLevel variable with the map functions. This functions will make corresponding potValue (between 0 and 1023) and ledLevel (between 0 and 4 “pinLEd+1”)

Then it will turn on LED for each ledLevel, corresponding to the potentionmeter value.

The final code is here

Serial Analog test

I also test AnalogInOutSerial example to have a monitor feedback of the value of the potentiometer.

I change the code a bit to turn on all LEDs with the fade.

The final code is here

OLED Screen

For testing an OLED screen, I used the SSD1306 screen with the U8g2 library as recommended in Neil’s documentation.

I used chat GPT to generate a code to display a text. I modified it a little to set the pins SDA and SCL and change the default text.

Here’s the final code :

#include <U8g2lib.h>
#include <Wire.h>

// OLED display constructor
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ 7, /* data=*/ 6);

void setup(void) {
  // Initialize the OLED display
  u8g2.begin();
}

void loop(void) {
  // Clear the screen
  u8g2.clearBuffer();

  // Set font and position
  u8g2.setFont(u8g2_font_ncenB14_tr);
  u8g2.setCursor(0, 20); // Set position (x, y)

  // Write text to the screen
  u8g2.print("It works! _o/");

  // Send the buffer to the display
  u8g2.sendBuffer();

  // Delay to control the refresh rate
  delay(1000);
}

Unfortunately, my I2C connector broke but I learned from datasheet that all of the pins have a functions for I2C. I noticed that not all kind of characters are displayed. I could explore that later. I also should explore image displaying.

Week 11 - Input device

Personnal assignment - I have to test a selector and switches and program different response mixing them. If possible try with screen display then with e-paper

During this week, I made a bunch of tests to understand how the selector works. I also implement what I learned from output device week and a bit more about OLED screen.

I ended up with this code :

#define pinCLK 1       // CLK pin connected to GPIO 1
#define pinDT  2       // DT pin connected to GPIO 2
#define pinSW  3       // SW pin connected to GPIO 3

#include <U8g2lib.h>
#include <Wire.h>

// OLED display constructor
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ 7, /* data=*/ 6);

int count = 0;    // Var to know how many clicks occured (increment clockwise and decrement counter_clockwise)
int previousStateSW;           // Var to memorize previous SW state to compare it with actual state
int previousStateCLK;          // Var to memorize previous CL state to compare it with actual state

void setup() {

    Serial.begin(9600);

    // Initialize the OLED display
    u8g2.begin();

    pinMode(pinSW, INPUT_PULLUP);         // INPUT_PULLUP to avoid 'floating'
    pinMode(pinDT, INPUT);
    pinMode(pinCLK, INPUT);

    previousStateSW  = digitalRead(pinSW);    // initial value for SW
    previousStateCLK = digitalRead(pinCLK);   // initial value for CLK

    delay(200); // delay to stabilize signal before the loop
}

void loop() {

    int actualStateCLK = digitalRead(pinCLK); // reading CLK value
    int actualStateDT  = digitalRead(pinDT);  // reading DT value
    int actualStateSW  = digitalRead(pinSW);  // reading SW value

    // Clear the screen
    u8g2.clearBuffer();

    // *************************
    // Push Button verification
    // *************************

    if (actualStateSW != previousStateSW) {    // Checking if SW state has changed
        previousStateSW = actualStateSW;      // Save new state

        if (actualStateSW == LOW) {
            Serial.println(F("SW Button pushed"));     // Display State on serial monitor

            // Set font and position
            u8g2.setFont(u8g2_font_ncenB14_tr);
            u8g2.setCursor(0, 20); // Set position (x, y)
            // Write text to the screen
            u8g2.print("SW Button :");

            u8g2.setCursor(0, 40); // Set position (x, y)
            // Write text to the screen
            u8g2.print("Pushed");

        } else {
            Serial.println(F("SW Button released"));

            u8g2.setFont(u8g2_font_ncenB14_tr);
            u8g2.setCursor(0, 20); // Set position (x, y)
            // Write text to the screen
            u8g2.print("SW Button :");

            u8g2.setCursor(0, 40); // Set position (x, y)
            // Write text to the screen
            u8g2.print("Released");
        }
        // Send the buffer to the display
        u8g2.sendBuffer();

        delay(10); // Delay to avoid rebounce
    }

    // ***************************
    // Rotary encoder verification
    // ***************************

    if (actualStateCLK != previousStateCLK) {       // Checking if SW state has changed
        previousStateCLK = actualStateCLK;           // Save new state

        if (actualStateCLK == LOW) {

            if (actualStateCLK != actualStateDT) {   // comparing CLK and DT, if they're different, direction is counter-clockwise          
                count--;                            // decrement counter
                Serial.print(F("Direction = counter-clockwise | Counter value = "));   // Display value on Serial Monitor
                Serial.println(count);

                u8g2.setFont(u8g2_font_ncenB10_tr);
                u8g2.setCursor(0, 15); // Set position (x, y)

                // Write text to the screen
                u8g2.print("Direction :");

                u8g2.setFont(u8g2_font_ncenB08_tr);
                u8g2.setCursor(0, 28); // Set position (x, y)
                u8g2.print("counter-clockwise");

                u8g2.setFont(u8g2_font_ncenB10_tr);
                u8g2.setCursor(0, 45); // Set position (x, y)
                // Write text to the screen
                u8g2.print("Counter value = ");

                u8g2.setCursor(0, 60); // Set position (x, y)
                u8g2.println(count);

                // Send the buffer to the display
                u8g2.sendBuffer();
            }
            else {                                   // when CLK and DT are similar, direction is clockwise            
                count++;                             // increment counter
                Serial.print(F("Direction = clockwise | Counter value = "));   // Display value on Serial Monitor
                Serial.println(count);

                 u8g2.setFont(u8g2_font_ncenB10_tr);
                u8g2.setCursor(0, 15); // Set position (x, y)

                // Write text to the screen
                u8g2.print("Direction :");


                u8g2.setCursor(0, 30); // Set position (x, y)
                u8g2.print("clockwise");

                u8g2.setCursor(0, 45); // Set position (x, y)
                // Write text to the screen
                u8g2.print("Counter value = ");

                u8g2.setCursor(0, 60); // Set position (x, y)
                u8g2.println(count);

                // Send the buffer to the display
                u8g2.sendBuffer();
            }

            delay(1);   // delay to avoid CLK rebounce      
        }
    }
}

The results was very satisfactory.

I still need to implement in the code the limit to the counter value to the number I decided (I think of -2 to 2 for the 5 levels of mood) and display an image instead of strings.

I didn’t have time to work on e-paper but I found some WEMOS ePaper 2.13 Shield with driver.

Week 13 - Networking and communication

During this week, I succeeded to display on a distant screen, inputs from remote controller board.

Using the previous rotary encoder code, I modified it with with blocks of code from examples of ESP NOW.

I succeeded to have a feedback of the SW button being pushed or released but not from the rotary encoder.

The cheap yellow screen library also need modification to work.

Week 14 - Interface and application programming

I made an html interface that could be control with an input device through serialport.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>IMU Data and Mindset Selector</title>
    <style>
        .container {
            display: flex;
            justify-content: space-around;
            margin-bottom: 20px;
            max-height: 20%;
        }
        .image-container {
            text-align: center;
        }
        .image-container img {
            max-width: 100%;
            max-height: 100px;
        }
        .btn-container {
            text-align: center;
            margin-bottom: 20px;
        }
        .btn {
            padding: 10px 20px;
            background-color: #007bff;
            color: #fff;
            border: none;
            cursor: pointer;
            margin: 0 10px; /* Add some spacing between buttons */
        }
        #result-container {
            text-align: center;
            margin-top: 20px;
        }
        #result-button-container {
            text-align: center;
            margin-bottom: 20px;
        }
        #result-image {
            max-width: 20%; /* Adjust the maximum width of the result image container */
            margin: 0 auto; /* Center the result image horizontally */
        }
        #result-image img {
            max-width: 100%; /* Ensure the result image fits within its container */
            height: auto; /* Maintain aspect ratio */
        }
    </style>
</head>
<body>

<div class="container">
    <div class="image-container">
        <h2>Mood</h2>
        <div id="image-container1">
            <img src="03_smiley.jpg" alt="Image" data-value="3">
        </div>
        <div class="btn-container">
            <button class="btn" onclick="previousImage(1)">Previous</button>
            <button class="btn" onclick="nextImage(1)">Next</button>
        </div>
    </div>
    <div class="image-container">
        <h2>Interaction acceptance</h2>
        <div id="image-container2">
            <img src="30_smiley.jpg" alt="Image" data-value="30">
        </div>
        <div class="btn-container">
            <button class="btn" onclick="previousImage(2)">Previous</button>
            <button class="btn" onclick="nextImage(2)">Next</button>
        </div>
    </div>
</div>

<div id="result-button-container">
    <button class="btn" onclick="addValues()">Set</button>
</div>

<div id="result-container">
    <h2>Mindset</h2>
    <div id="result-text"></div> 
    <div id="result-image"></div>
</div>

<!-- Button to trigger the open_IMU function -->
<button class="btn" value="Open IMU" onclick="open_IMU()">Open IMU</button>

<!-- Container to display the container index value -->
<div id="container-index-value"></div>

<!-- JavaScript code -->
<script>
    var currentIndex1 = 2; // Index for the first image array
    var currentIndex2 = 2; // Index for the second image array
    var images1 = ["05_smiley.jpg", "04_smiley.jpg", "03_smiley.jpg","02_smiley.jpg", "01_smiley.jpg"];
    var images2 = ["50_smiley.jpg", "40_smiley.jpg", "30_smiley.jpg", "20_smiley.jpg", "10_smiley.jpg"];
    var values1 = [5, 4, 3, 2, 1];
    var values2 = [50, 40, 30, 20, 10];
    var allImages = [
        { src: "smiley_mix11.jpg", value: 11 },
        { src: "smiley_mix12.jpg", value: 12 },
        { src: "smiley_mix13.jpg", value: 13 },
        { src: "smiley_mix14.jpg", value: 14 },
        { src: "smiley_mix15.jpg", value: 15 },
        { src: "smiley_mix21.jpg", value: 21 },
        { src: "smiley_mix22.jpg", value: 22 },
        { src: "smiley_mix23.jpg", value: 23 },
        { src: "smiley_mix24.jpg", value: 24 },
        { src: "smiley_mix25.jpg", value: 25 },
        { src: "smiley_mix31.jpg", value: 31 },
        { src: "smiley_mix32.jpg", value: 32 },
        { src: "smiley_mix33.jpg", value: 33 },
        { src: "smiley_mix34.jpg", value: 34 },
        { src: "smiley_mix35.jpg", value: 35 },
        { src: "smiley_mix41.jpg", value: 41 },
        { src: "smiley_mix42.jpg", value: 42 },
        { src: "smiley_mix43.jpg", value: 43 },
        { src: "smiley_mix44.jpg", value: 44 },
        { src: "smiley_mix45.jpg", value: 45 },
        { src: "smiley_mix51.jpg", value: 51 },
        { src: "smiley_mix52.jpg", value: 52 },
        { src: "smiley_mix53.jpg", value: 53 },
        { src: "smiley_mix54.jpg", value: 54 },
        { src: "smiley_mix55.jpg", value: 55 },
    ];

    async function open_IMU() {
        try {
            // Requesting the serial port
            const port = await navigator.serial.requestPort();

            // Opening the serial port
            await port.open({ baudRate: 115200, bufferSize: 1 });

            // Getting the reader for the port
            const reader = port.readable.getReader();

            while (true) {
                // Waiting for line end
                let lineEnd = false;
                let line = '';
                while (!lineEnd) {
                    const { value, done } = await reader.read();
                    let chr = String.fromCharCode(value[0]);
                    line += chr;
                    if (chr === '\n' || done) lineEnd = true;
                }

            if (line.trim() === "SW Button pushed") {
                // If the button is pushed, call addValues
                addValues();
            } else if (line.trim() === "clockwise") {
                // If the rotation is clockwise, call nextImage
                nextImage(1);
            } else if (line.trim() === "counter-clockwise") {
                // If the rotation is counter-clockwise, call previousImage
                previousImage(1);
            }

                // Extracting container index
                const vars = line.split(',');
                const containerIndex = vars[0].trim().toString(); // Convert to string

                // Updating the container index value on the webpage
                document.getElementById("container-index-value").innerText = "Container Index: " + containerIndex;
            }
        } catch (error) {
            // Handling any errors that occur during the process
            console.error("An error occurred:", error);
        }
    }

    function previousImage(containerIndex) {
        if (containerIndex === 1) {
            if (currentIndex1 === 0) {
                return; // Do nothing if already at the first image
            }
            currentIndex1--;
            updateImage(1);
        } else if (containerIndex === 2) {
            if (currentIndex2 === 0) {
                return; // Do nothing if already at the first image
            }
            currentIndex2--;
            updateImage(2);
        }
    }

    function nextImage(containerIndex) {
        if (containerIndex === 1) {
            if (currentIndex1 === images1.length - 1) {
                return; // Do nothing if already at the last image
            }
            currentIndex1++;
            updateImage(1);
        } else if (containerIndex === 2) {
            if (currentIndex2 === images2.length - 1) {
                return; // Do nothing if already at the last image
            }
            currentIndex2++;
            updateImage(2);
        }
    }

    function updateImage(containerIndex) {
        var displayedImage;
        if (containerIndex === 1) {
            displayedImage = document.getElementById("image-container1").getElementsByTagName("img")[0];
            displayedImage.src = images1[currentIndex1];            
            displayedImage.dataset.value = values1[currentIndex1];
        } else if (containerIndex === 2) {
            displayedImage = document.getElementById("image-container2").getElementsByTagName("img")[0];
            displayedImage.src = images2[currentIndex2];            
            displayedImage.dataset.value = values2[currentIndex2];
        }
    }

    function addValues() {
        var value1 = values1[currentIndex1];
        var value2 = values2[currentIndex2];
        var result = value1 + value2;

        // Find the image object with the matching value
        var resultImage = allImages.find(function(image) {
            return image.value === result;
        });

        if (resultImage) {
            var resultImageSrc = resultImage.src;
            var resultImageValue = resultImage.value; // Get the personalized value
    //        document.getElementById("result-text").innerText = "Result: " + result + " (" + resultImageValue + ")"; //
            document.getElementById("result-image").innerHTML = "<img src='" + resultImageSrc + "' alt='Result Image'>";
        } else {
            document.getElementById("result-text").innerText = "Result: Invalid";
            document.getElementById("result-image").innerHTML = ""; // Clear previous result image
        }
    }
</script>
</body>
</html>
#define pinCLK 1       // CLK pin connected to GPIO 1
#define pinDT  2       // DT pin connected to GPIO 2
#define pinSW  3       // SW pin connected to GPIO 3

int count = 0;    // Var to know how many clicks occured (increment clockwise and decrement counter_clockwise)
int previousStateSW;           // Var to memorize previous SW state to compare it with actual state
int previousStateCLK;          // Var to memorize previous CL state to compare it with actual state

void setup() {

    Serial.begin(9600);

    pinMode(pinSW, INPUT_PULLUP);         // INPUT_PULLUP to avoid 'floating'
    pinMode(pinDT, INPUT);
    pinMode(pinCLK, INPUT);

    previousStateSW  = digitalRead(pinSW);    // initial value for SW
    previousStateCLK = digitalRead(pinCLK);   // initial value for CLK

    delay(200); // delay to stabilize signal before the loop
}

void loop() {

    int actualStateCLK = digitalRead(pinCLK); // reading CLK value
    int actualStateDT  = digitalRead(pinDT);  // reading DT value
    int actualStateSW  = digitalRead(pinSW);  // reading SW value

// *************************
// Push Button verification
// *************************

    if(actualStateSW != previousStateSW) {    // Checking if SW state has changed
        previousStateSW = actualStateSW;      // Save new state

        if(actualStateSW == LOW)
            Serial.println(F("SW Button pushed"));     // Display State on serial monitor
        else
            Serial.println(F("SW Button released"));

        delay(10); // Delay to avoid rebounce
    }

// ***************************
// Rotary encoder verification
// ***************************

    if (actualStateCLK != previousStateCLK) {
        delayMicroseconds(200); // Debounce delay
        if (digitalRead(pinCLK) == actualStateCLK) { // Check if the CLK signal has settled
            previousStateCLK = actualStateCLK;

    if(actualStateCLK == LOW) {

        if(actualStateCLK != actualStateDT) {   // comparing CLK and DT, if they're different, direction is counter-clockwise          
            count++;                            // decrement counter
            Serial.println(F("clockwise"));   // Display value on Serial Monitor
//            Serial.println(count);
        }
    else {                                   // when CLK and DT are similar, direction is clockwise            
        count--;                             // increment counter
        Serial.println(F("counter-clockwise"));   // Display value on Serial Monitor
//        Serial.println(count);
}

    }
    } 

}
}

I create an online version of the interface to test it.

I also wanted to try it on phone but Android Chrome does not allowed serial port communication

Week 15

I started to design my final board using an ESP32WROOM32UE and read the datasheet

Week 16 - System integration

I made the final design for my PCB

Then I made the BOM for Electronics part.

Nb Element Link Price Remarks
1 Micro-controller ESP32-WROOM-32UE
1 KY-040 Rotary Encoder
5 Rocker Switch https://www.sparkinter.com/pdf/230203-0060103.pdf Cut-out 19x13mm
1 Button
1 Regulator 3.3V 1A
1 vertical connector SMD 1x05 2.54mm
1 horizontal connector SMD 1x06 2.54mm FTDI
1 vertical connector SMD 2x05 2.54mm switches
1 resistor 10k
1 resistor 1K
1 LED Clear Blue
2 resistor O ohm
1 capacitor 0.1 uF
1 capacitor 1 uF
1 capacitor 10 uF
1 switch slide
10 cable
1 CCL FR1

And then I modelized the different part of the remote.

And printed the battery holder

Week 17

Electronics

I started by finalizing the electronics production. PCB was milled during previous week and I had to solder components I already prepared on it. ESP32 chip was trickier to solder than previous Xiao Board but I managed to do it. I made quite a mess on the pins near the vertical connectors, almost all them (except the GND one) are connected to each other but hopefully none of them are used on my board. Next times, I will avoid soldering pins that aren’t connected to anything. I tested it and could upload a blink program in it.

I then prepared the wiring for the switches. I modified Dupont wires to solder them on the switches (I found out later I could have make my own connectors).

I added shrinkables tube to avoid contacts. Having no spring to maintain the contact in the battery holder, I chose to use a piece of sponge. To make the actual, I used copper tape with a lot of tin With all this parts the electronics of the remote device was complete.

A bit later, I added a switch between the battery and the board to turn off the device.

Pin references :

SW : IO34
DT : IO35
CLK : IO32
Rocker Switch 1 : IO33 (internal pull-up)
Rocker Switch 2 : IO25 (internal pull-up)
Rocker Switch 3 : IO26 (internal pull-up)
Rocker Switch 4 : IO27 (internal pull-up)
Rocker Switch 5 : IO14 (internal pull-up)
LED : IO23

Shell

I made 2 tries of the outer shell. I printed the design I made previous week. That first one had a good grip but not enough space for the components. The cut out for the switches was good and they perfectly fit in it. I also found out that four rocker switches shoud be sufficient and it would feel more natural when handling it as each fingers will have contact whith one except for the thumb that will maintain the grip.

.

I made another design trying to reduce the volume of it at the same time than making more space in it.

All the compenents fit in the second shell, but the grip sensation was very bad. The shape wasn’t ergonomic at all.

The knob was still printing at the end of this week.

Programming

I could make a WIFI communication and generate an html page.

To display an html page with images from the ESP32, I needed to upload the images in it. The chip had not enough memory to store the 35 jpeg needed. I used Riot to convert the emojis to the smallest yet recognizible PNGs. We see on the screenshot tahat I could go from a ~ 60KB jpg file to a 6KB png file.

I used ESP32FS tutorial to upload the 35 images on the ESP32 chip. this tool only works on older version of Arduino IDE.

Using SPIFFS, I could display the webpage with the images embedded in the chip.

I had to remake all the programming, because the debounce didn’t work anymore and the javascript was using serial communication and that didn’t work anymore with this configuration. Unfortunately, I wrote over the code as I was making progress with suggestion of chatGPT and I haven’t saved all the step that lead to the final code.

#include <WiFi.h>
#include <WebServer.h>

// Define pins for rotary encoder and button
#define pinCLK 32
#define pinDT 35
#define pinSW 34
#define pinLED 23

// Network credentials
const char* ssid = "your_SSID";
const char* password = "your_password";

// Create a web server on port 80
WebServer server(80);

// Variables to track the count and last action
int rotaryCount = 0; // Variable to keep track of the rotary encoder count
int previousStateSW; // Variable to store the previous state of the SW pin
int previousStateCLK; // Variable to store the previous state of the CLK pin

String last_action = "";

// HTML content
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32 Rotary Encoder</title>
</head>
<body>
<h1>ESP32 Rotary Encoder</h1>
<div id="count">Count: </div>
<div id="last-action">Last Action: </div>

<script>
    async function fetchData() {
        try {
            const response = await fetch('/data');
            const data = await response.json();
            document.getElementById('count').innerText = "Count: " + data.rotaryCount;
            document.getElementById('last-action').innerText = "Last Action: " + data.last_action;
        } catch (error) {
            console.error('Error fetching data:', error);
        }
    }

    setInterval(fetchData, 1000);
</script>
</body>
</html>
)rawliteral";

void setup() {
    Serial.begin(115200); // Initialize serial communication at 115200 baud rate
    pinMode(pinSW, INPUT_PULLUP); // Set SW pin as input with internal pull-up resistor
    pinMode(pinDT, INPUT);        // Set DT pin as input
    pinMode(pinCLK, INPUT);       // Set CLK pin as input
    pinMode(pinLED, OUTPUT);      // Set LED pin as output

    previousStateSW  = digitalRead(pinSW);  // Read initial state of the SW pin
    previousStateCLK = digitalRead(pinCLK); // Read initial state of the CLK pin

    // Connect to Wi-Fi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(1000);
        Serial.println("Connecting to WiFi...");
    }
    Serial.println("Connected to WiFi");

    // Print the IP address
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    // Define server routes
    server.on("/", []() {
        server.send_P(200, "text/html", index_html);
    });

    server.on("/data", []() {
        String json = "{\"rotaryCount\":" + String(rotaryCount) + ", \"last_action\":\"" + last_action + "\"}";
        server.send(200, "application/json", json);
    });

    server.begin();
    Serial.println("HTTP server started");
}

void loop() {
    server.handleClient();
    int actualStateCLK = digitalRead(pinCLK); // Read current state of the CLK pin
    int actualStateDT  = digitalRead(pinDT);  // Read current state of the DT pin
    int actualStateSW  = digitalRead(pinSW);  // Read current state of the SW pin

    // Push Button verification
    if (actualStateSW != previousStateSW) { // Check if SW state has changed
        previousStateSW = actualStateSW; // Update the previous SW state
        delay(50); // Debounce delay
        if (actualStateSW == LOW) {
            Serial.println(F("SW Button pushed")); // Print message when button is pressed
            last_action = "SW Button pushed";
        } else {
            Serial.println(F("SW Button released")); // Print message when button is released
            last_action = "SW Button released";
        }    
    }

    // Rotary encoder verification
    if (actualStateCLK != previousStateCLK) {
        delay(10); // Debounce delay
        if (digitalRead(pinCLK) == actualStateCLK) { // Check if the CLK signal has settled
            previousStateCLK = actualStateCLK;

            if (actualStateCLK == LOW) {
                if (actualStateCLK != actualStateDT) { // Comparing CLK and DT, if they're different, direction is counter-clockwise
                    rotaryCount--; // Decrement counter
                    Serial.println(F("counter-clockwise")); // Display value on Serial Monitor
                    last_action = "Counter-clockwise";
                } else { // When CLK and DT are similar, direction is clockwise
                    rotaryCount++; // Increment counter
                    Serial.println(F("clockwise")); // Display value on Serial Monitor
                    last_action = "Clockwise";
                }
                Serial.println(rotaryCount);

                // Ensure count stays within bounds (1-5 for PWM)
                if (rotaryCount < 1) {
                    rotaryCount = 1;
                } else if (rotaryCount > 5) {
                    rotaryCount = 5;
                }
                // Map rotaryCount to PWM range (0-255)
                int pwmValue = map(rotaryCount, 1, 5, 0, 128);
                analogWrite(pinLED, pwmValue); // Set LED brightness
            }
        }
    }
}
#include <WiFi.h>
#include <WebServer.h>

// Define pins for rotary encoder, button, LED, and rocker switches
#define pinCLK 32
#define pinDT  35
#define pinSW  34
#define pinLED 23

#define pinRocker1 25
#define pinRocker2 26
#define pinRocker3 27
#define pinRocker4 14

// Network credentials
const char* ssid = "your_SSID";
const char* password = "your_password";

// Create a web server on port 80
WebServer server(80);

// Variables to track the count, states, and direction
int rotaryCount = 0;
int previousStateSW;
int previousStateCLK;
bool isClockwise = false;

int previousRockerState1;
int previousRockerState2;
int previousRockerState3;
int previousRockerState4;

int activatedRockerCount = 0;

// HTML content with JavaScript for dynamic updates
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mindset Selector</title>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            function fetchData() {
                fetch('/data')
                    .then(response => response.json())
                    .then(data => {
                        document.getElementById('rotary-count').textContent = 'Rotary Count: ' + data.rotaryCount;
                        document.getElementById('rocker-count').textContent = 'Activated Rocker Switches: ' + data.activatedRockerCount;
                        document.getElementById('direction').textContent = 'Direction: ' + data.direction;
                    });
            }
            setInterval(fetchData, 1000);
        });
    </script>
</head>
<body>
<div id="rotary-count">Rotary Count: </div>
<div id="rocker-count">Activated Rocker Switches: </div>
<div id="direction">Direction: </div>
</body>
</html>
)rawliteral";

void setup() {
    Serial.begin(115200);

    pinMode(pinSW, INPUT_PULLUP);
    pinMode(pinDT, INPUT);
    pinMode(pinCLK, INPUT);
    pinMode(pinLED, OUTPUT);

    pinMode(pinRocker1, INPUT_PULLUP);
    pinMode(pinRocker2, INPUT_PULLUP);
    pinMode(pinRocker3, INPUT_PULLUP);
    pinMode(pinRocker4, INPUT_PULLUP);

    previousStateSW = digitalRead(pinSW);
    previousStateCLK = digitalRead(pinCLK);

    previousRockerState1 = digitalRead(pinRocker1);
    previousRockerState2 = digitalRead(pinRocker2);
    previousRockerState3 = digitalRead(pinRocker3);
    previousRockerState4 = digitalRead(pinRocker4);

    analogWrite(pinLED, 0);

    // Connect to Wi-Fi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(1000);
        Serial.println("Connecting to WiFi...");
    }
    Serial.println("Connected to WiFi");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    // Define server routes
    server.on("/", []() {
        server.send_P(200, "text/html", index_html);
    });

    server.on("/data", []() {
        String direction = isClockwise ? "Clockwise" : "Counterclockwise";
        String json = "{\"rotaryCount\":" + String(rotaryCount) + ",\"activatedRockerCount\":" + String(activatedRockerCount) + ",\"direction\":\"" + direction + "\"}";
        server.send(200, "application/json", json);
    });

    server.begin();
    Serial.println("HTTP server started");
}

void loop() {
    server.handleClient();

    int actualStateCLK = digitalRead(pinCLK);
    int actualStateDT  = digitalRead(pinDT);
    int actualStateSW  = digitalRead(pinSW);

    int currentRockerState1 = digitalRead(pinRocker1);
    int currentRockerState2 = digitalRead(pinRocker2);
    int currentRockerState3 = digitalRead(pinRocker3);
    int currentRockerState4 = digitalRead(pinRocker4);

    // Update direction based on the rotary encoder movement
    if (actualStateCLK != previousStateCLK) {
        delayMicroseconds(200); // Debounce delay
        if (digitalRead(pinCLK) == actualStateCLK) {
            previousStateCLK = actualStateCLK;

            if (actualStateCLK == LOW) {
                isClockwise = (actualStateCLK != actualStateDT);
                rotaryCount += isClockwise ? 1 : -1;
                rotaryCount = constrain(rotaryCount, 1, 5);
                Serial.println(rotaryCount);

                int pwmValue = map(rotaryCount, 1, 5, 0, 128);
                analogWrite(pinLED, pwmValue);
            }
        }
    }

    // Update activated rocker switch count
    if (currentRockerState1 != previousRockerState1 || 
        currentRockerState2 != previousRockerState2 || 
        currentRockerState3 != previousRockerState3 || 
        currentRockerState4 != previousRockerState4) {
        delay(50);
        activatedRockerCount = (currentRockerState1 == LOW) + (currentRockerState2 == LOW) + 
                            (currentRockerState3 == LOW) + (currentRockerState4 == LOW);
        Serial.print("Activated rocker switches: ");
        Serial.println(activatedRockerCount);

        previousRockerState1 = currentRockerState1;
        previousRockerState2 = currentRockerState2;
        previousRockerState3 = currentRockerState3;
        previousRockerState4 = currentRockerState4;
    }

    // Push button verification
    if (actualStateSW != previousStateSW) {
        delay(50); // Debounce delay
        previousStateSW = actualStateSW;
        Serial.println(actualStateSW == LOW ? "SW Button pushed" : "SW Button released");
    }
}
#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>

// Define pins for rotary encoder and button
#define pinCLK 32
#define pinDT  35
#define pinSW  34
#define pinLED 23

#define pinRocker1 25   // Rocker switch 1 connected to GPIO 25
#define pinRocker2 26   // Rocker switch 2 connected to GPIO 26
#define pinRocker3 27   // Rocker switch 3 connected to GPIO 27
#define pinRocker4 14   // Rocker switch 4 connected to GPIO 14

// Network credentials
const char* ssid = "your_SSID";
const char* password = "your_password";

// Create a web server on port 80
WebServer server(80);

// Variables to track the count and last action
int rotaryCount = 1;    // Variable to keep track of the rotary encoder count
int previousStateSW;  // Variable to store the previous state of the SW pin
int previousStateCLK; // Variable to store the previous state of the CLK pin

int previousRockerState1; // Variable to store the previous state of Rocker switch 1
int previousRockerState2; // Variable to store the previous state of Rocker switch 2
int previousRockerState3; // Variable to store the previous state of Rocker switch 3
int previousRockerState4; // Variable to store the previous state of Rocker switch 4

int activatedRockerCount = 1; // Counter for the total number of activated rocker switches

int actualStateSW_global; // Global variable to store the state of SW pin

// HTML content
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mindset Selector</title>
    <style>
        .container {
            display: flex;
            justify-content: space-around;
            margin-bottom: 20px;
            max-height: 20%;
        }
        .image-container {
            text-align: center;
        }
        .image-container img {
            max-width: 100%;
            max-height: 100px;
        }
        .btn-container {
            text-align: center;
            margin-bottom: 20px;
        }
        .btn {
            padding: 10px 20px;
            background-color: #007bff;
            color: #fff;
            border: none;
            cursor: pointer;
            margin: 0 10px;
        }
        #result-container {
            text-align: center;
            margin-top: 20px;
        }
        #result-button-container {
            text-align: center;
            margin-bottom: 20px;
        }
        #result-image {
            max-width: 50%;
            margin: 0 auto;
        }
        #result-image img {
            max-width: 100%;
            height: auto;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="image-container">
        <h2>Mood</h2>
        <div id="image-container1">
            <img src="/03_smiley.png" alt="Image" data-value="3">
        </div>
        <div class="btn-container">
            <button class="btn" onclick="previousImage(1)">Previous</button>
            <button class="btn" onclick="nextImage(1)">Next</button>
        </div>
    </div>
    <div class="image-container">
        <h2>Interaction acceptance</h2>
        <div id="image-container2">
            <img src="/10_smiley.png" alt="Image" data-value="30">
        </div>
        <div class="btn-container">
            <button class="btn" onclick="previousImage(2)">Previous</button>
            <button class="btn" onclick="nextImage(2)">Next</button>
        </div>
    </div>
</div>

<div id="result-button-container">
    <button class="btn" onclick="addValues()">Set</button>
</div>

<div id="result-container">
    <h2>Mindset</h2>
    <div id="result-image"></div>
</div>

<script>
    var currentIndex1 = 2;
    var currentIndex2 = 2;
    var images1 = ["/05_smiley.png", "/04_smiley.png", "/03_smiley.png","/02_smiley.png", "/01_smiley.png"];
    var images2 = ["/50_smiley.png", "/40_smiley.png", "/30_smiley.png", "/20_smiley.png", "/10_smiley.png"];
    var values1 = [5, 4, 3, 2, 1];
    var values2 = [50, 40, 30, 20, 10];
    var allImages = [
        { src: "/smiley_mix11.png", value: 11 },
        { src: "/smiley_mix12.png", value: 12 },
        { src: "/smiley_mix13.png", value: 13 },
        { src: "/smiley_mix14.png", value: 14 },
        { src: "/smiley_mix15.png", value: 15 },
        { src: "/smiley_mix21.png", value: 21 },
        { src: "/smiley_mix22.png", value: 22 },
        { src: "/smiley_mix23.png", value: 23 },
        { src: "/smiley_mix24.png", value: 24 },
        { src: "/smiley_mix25.png", value: 25 },
        { src: "/smiley_mix31.png", value: 31 },
        { src: "/smiley_mix32.png", value: 32 },
        { src: "/smiley_mix33.png", value: 33 },
        { src: "/smiley_mix34.png", value: 34 },
        { src: "/smiley_mix35.png", value: 35 },
        { src: "/smiley_mix41.png", value: 41 },
        { src: "/smiley_mix42.png", value: 42 },
        { src: "/smiley_mix43.png", value: 43 },
        { src: "/smiley_mix44.png", value: 44 },
        { src: "/smiley_mix45.png", value: 45 },
        { src: "/smiley_mix51.png", value: 51 },
        { src: "/smiley_mix52.png", value: 52 },
        { src: "/smiley_mix53.png", value: 53 },
        { src: "/smiley_mix54.png", value: 54 },
        { src: "/smiley_mix55.png", value: 55 },
    ];

    async function fetchData() {
        try {
            const response = await fetch('/data');
            const data = await response.json();

            // Update currentIndex based on the response
            currentIndex1 = data.rotaryCount - 1; // Using the rotaryCount for currentIndex1
            currentIndex2 = data.activatedRockerCount - 1; // Using activatedRockerCount for currentIndex2
            // Check the value of triggerAddValues
            if (data.triggerAddValues) {
                addValues(); // Call addValues function if triggerAddValues is true
            }
            console.log("Updated indices:", currentIndex1, currentIndex2); // Debugging log
            updateImage(1);
            updateImage(2);
        } catch (error) {
            console.error('Error fetching data:', error);
        }
    }

    setInterval(fetchData, 100);

    function previousImage(containerIndex) {
        if (containerIndex === 1) {
            if (currentIndex1 === 0) return;
            currentIndex1--;
            updateImage(1);
        } else if (containerIndex === 2) {
            if (currentIndex2 === 0) return;
            currentIndex2--;
            updateImage(2);
        }
    }

    function nextImage(containerIndex) {
        if (containerIndex === 1) {
            if (currentIndex1 === images1.length - 1) return;
            currentIndex1++;
            updateImage(1);
        } else if (containerIndex === 2) {
            if (currentIndex2 === images2.length - 1) return;
            currentIndex2++;
            updateImage(2);
        }
    }

    function updateImage(containerIndex) {
        var displayedImage;
        if (containerIndex === 1) {
            displayedImage = document.getElementById("image-container1").getElementsByTagName("img")[0];
            displayedImage.src = images1[currentIndex1];
            displayedImage.dataset.value = values1[currentIndex1];
        } else if (containerIndex === 2) {
            displayedImage = document.getElementById("image-container2").getElementsByTagName("img")[0];
            displayedImage.src = images2[currentIndex2];
            displayedImage.dataset.value = values2[currentIndex2];
        }
    }

    function addValues() {
        var value1 = values1[currentIndex1];
        var value2 = values2[currentIndex2];
        var resultValue = value1 + value2;
        var resultImage = allImages.find(image => image.value === resultValue);
        if (resultImage) {
            document.getElementById("result-image").innerHTML = `<img src="${resultImage.src}" alt="Result Image">`;
        } else {
            document.getElementById("result-image").innerHTML = "No matching image found.";
        }
    }
</script>
</body>
</html>
)rawliteral";

// Function to initialize the SPIFFS
void initSPIFFS() {
    if (!SPIFFS.begin(true)) {
        Serial.println("An error occurred while mounting SPIFFS");
        return;
    }
    Serial.println("SPIFFS mounted successfully");
}

void setup() {
    Serial.begin(115200); // Initialize serial communication at 115200 baud rate

    pinMode(pinSW, INPUT_PULLUP); // Set SW pin as input with internal pull-up resistor
    pinMode(pinDT, INPUT);        // Set DT pin as input
    pinMode(pinCLK, INPUT);       // Set CLK pin as input
    pinMode(pinLED, OUTPUT);      // Set LED pin as output

    pinMode(pinRocker1, INPUT_PULLUP); // Set Rocker switch 1 as input with internal pull-up resistor
    pinMode(pinRocker2, INPUT_PULLUP); // Set Rocker switch 2 as input with internal pull-up resistor
    pinMode(pinRocker3, INPUT_PULLUP); // Set Rocker switch 3 as input with internal pull-up resistor
    pinMode(pinRocker4, INPUT_PULLUP); // Set Rocker switch 4 as input with internal pull-up resistor

    previousStateSW  = digitalRead(pinSW);  // Read initial state of the SW pin
    previousStateCLK = digitalRead(pinCLK); // Read initial state of the CLK pin

    previousRockerState1 = digitalRead(pinRocker1); // Read initial state of Rocker switch 1
    previousRockerState2 = digitalRead(pinRocker2); // Read initial state of Rocker switch 2
    previousRockerState3 = digitalRead(pinRocker3); // Read initial state of Rocker switch 3
    previousRockerState4 = digitalRead(pinRocker4); // Read initial state of Rocker switch 4

    analogWrite(pinLED, 0); // Initialize the LED with 0 brightness (off)

    // Initialize SPIFFS
    initSPIFFS();

    // Connect to Wi-Fi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(1000);
        Serial.println("Connecting to WiFi...");
    }
    Serial.println("Connected to WiFi");

    // Print the IP address
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    // Define server routes
    server.on("/", []() {
        server.send_P(200, "text/html", index_html);
    });

    server.on("/data", []() {
        actualStateSW_global = digitalRead(pinSW); // Read the current state of SW pin
        bool triggerAddValues = (actualStateSW_global == LOW); // Set triggerAddValues based on actualStateSW_global
        String json = "{\"rotaryCount\":" + String(rotaryCount) + ",\"activatedRockerCount\":" + String(activatedRockerCount) + ",\"triggerAddValues\":" + (actualStateSW_global == LOW ? "true" : "false") + "}";
        server.send(200, "application/json", json);
    });

    server.serveStatic("/", SPIFFS, "/");

    server.begin();
    Serial.println("HTTP server started");
}

void loop() {
    server.handleClient();

    int actualStateCLK = digitalRead(pinCLK); // Read current state of the CLK pin
    int actualStateDT  = digitalRead(pinDT);  // Read current state of the DT pin
    int actualStateSW  = digitalRead(pinSW);  // Read current state of the SW pin

    // Read current states of rocker switches again after debounce delay
    int currentRockerState1 = digitalRead(pinRocker1);
    int currentRockerState2 = digitalRead(pinRocker2);
    int currentRockerState3 = digitalRead(pinRocker3);
    int currentRockerState4 = digitalRead(pinRocker4);

    // Check if the state has changed and is stable
    if (currentRockerState1 != previousRockerState1 || 
        currentRockerState2 != previousRockerState2 || 
        currentRockerState3 != previousRockerState3 || 
        currentRockerState4 != previousRockerState4) {
        // Debounce delay for rocker switches
        delay(50);
        // Update activatedRockerCount only if there is a change in any rocker switch state
        activatedRockerCount = 1;
        if (currentRockerState1 == LOW) {
            activatedRockerCount++;
        }
        if (currentRockerState2 == LOW) {
            activatedRockerCount++;
        }
        if (currentRockerState3 == LOW) {
            activatedRockerCount++;
        }
        if (currentRockerState4 == LOW) {
            activatedRockerCount++;
        }

        // Print the number of activated rocker switches to the Serial Monitor
        Serial.print(F("Activated rocker switches: "));
        Serial.println(activatedRockerCount);

        // Update previous rocker states
        previousRockerState1 = currentRockerState1;
        previousRockerState2 = currentRockerState2;
        previousRockerState3 = currentRockerState3;
        previousRockerState4 = currentRockerState4;
    }

    // Push Button verification
    if (actualStateSW != previousStateSW) { // Check if SW state has changed
        previousStateSW = actualStateSW; // Update the previous SW state
        delay(50); // Debounce delay
        if (actualStateSW == LOW) {
            Serial.println(F("SW Button pushed")); // Print message when button is pressed
        } else {
            Serial.println(F("SW Button released")); // Print message when button is released
        }    
    }

    // Rotary encoder verification

    if (rotaryCount < 1) {
        rotaryCount = 1;
    } else if (rotaryCount > 5) {
        rotaryCount = 5;
    }

    if (actualStateCLK != previousStateCLK) {
        delayMicroseconds(200); // Debounce delay
        if (digitalRead(pinCLK) == actualStateCLK) { // Check if the CLK signal has settled
            previousStateCLK = actualStateCLK;

            if (actualStateCLK == LOW) {
                if (actualStateCLK != actualStateDT) { // Comparing CLK and DT, if they're different, direction is counter-clockwise
                    rotaryCount--; // Decrement counter
                    Serial.println(F("counter-clockwise")); // Display value on Serial Monitor
                } else { // When CLK and DT are similar, direction is clockwise
                    rotaryCount++; // Increment counter
                    Serial.println(F("clockwise")); // Display value on Serial Monitor
                }
                Serial.println(rotaryCount);

                // Ensure count stays within bounds (0-5 for PWM)
                if (rotaryCount < 1) {
                    rotaryCount = 1;
                } else if (rotaryCount > 5) {
                    rotaryCount = 5;
                }
                // Map rotaryCount to PWM range (0-128)
                int pwmValue = map(rotaryCount, 1, 5, 0, 128);
                analogWrite(pinLED, pwmValue); // Set LED brightness
            }
        }
    }
}

Here’s the code working :

Week 18

The Knob

My first print of the knob was too tight for the shaft. At first, I wanted it to just fit on it but I decided to us a screw and a nut instead. I change my design a bit to have a nut inserted in it. I programmed a puse in the printing to put it in but unhopefully the Ender5 printer didn’t pause at height. I made anorther try with the prusa printer and it worked well.

knob printing paused knob mounted on the rotary encoder

Updating the electronics

I didn’t thought before to add the possibility toturn off the remote control so I added an extra Rocker Switch (different color) between the battery and the PCB. As I was redisgning the shell, I added JST connectors to facilitate the assembly. I also modified one of the JST connectors to solder it on the surface of the PCB.

modifying JST connectors modified elctroncis with on/off switch

The Shell

I redesigned the shell based on the first print and adding some space for the rocker switch wiring and for the rotary encoder. I pay attention to keep an ergonomic shape and added a cut-out for the on/off switch. Finally, I add shape to integrate the PCB and a support to constraint the battery holder and letting some space for the wires. I also profited of this redesign to add more offset for the access panel to slide more smoothly.

fusion 360 rotary side of the shell Fusion 360 switch side of the shell

The printing went quite well.

rotary side of the shell inside of the shell switch side of the shell

assembly of the shell

Wanting a better finished shell, I also printed a PET-G version of the device then sand it and cover it with epoxy resin for nice finition.

PETG print of the shell adding Epoxy on the shell

I had to sand it again because of droplets. The result was really good, the surface being really smooth but i found it’s visually not as as good as the PLA one. I should have add colors in the epoxy. I also regret a bit adding epoxy because I found the tactile sensation more pleasant with a good sanding than with a layer of epoxy. Plus I found it one day a bit broken like if someone tried to force the acces panel the wrong way.

final smooth PETG shell Broken part of the PETG shell

For that, I decided to continue with the PLA one.

Substractive design

I figured out I miss some substractive design parts and I decided to make some vinyl cutting to add informations on the device in a fancy way. In Inkscape, I designed a part for the rotary encoder and another near the rocker switches.

SVG design for interaction acceptance information SVG design for mood information

I chose colors that make good contrast with their supports. The cutting went well but I had more difficulties for the weeding, especially for te letters. I finally chose to keep the outside shape of the letters for the switches parts after failing 2 times at eeding theme properly. Despite that, the result is satisfactory and add some nice colors to the device.

cutting for interaction acceptance information on the device cutting for mood information on the knob

Debugging the rotary encoder

After a few time using my new board, I had some difficulties to have correct data from my rotary encoder. I had sometimes the counter going crazy or changing direction without touching it and sometimes. I tested the continuity with the multimeter and it seemed alright. I thought at first there was a bad connexion with programmer and decided to avoid powering the board through it. I tried to modify the debounce delay in the code, and tried other way of debouncing i found on the internet. Then, I tried to change the component but it does nothing. When studying the KY-040 board, we found with instructor there was some floating signal on the oscilloscope so I added a pull-up resistor on the board. Finally, I figured the bad signals wer worse when the rotary encoder was fixed to the shell and that could be due to constraint on the connectors. I decided to desoldered the connectors on my board and the KY-040 Board and connected them directly with wires and everything was solved. Plus, it made the assembly easier.

added resistor on KY-040 board wires soldered between KY-040 and main PCB

electronics integrated to the shell

Updating the phone version

With the rotary encoder working fine, I updated the code to have a better intrafec on phone. I removed the raw data displayed at the top and added titles above each images. I couldn’t removed the button as they’re used in the script.

#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>

// Define pins for rotary encoder and button
#define pinCLK 32
#define pinDT  35
#define pinSW  34
#define pinLED 23

#define pinRocker1 25   // Rocker switch 1 connected to GPIO 25
#define pinRocker2 26   // Rocker switch 2 connected to GPIO 26
#define pinRocker3 27   // Rocker switch 3 connected to GPIO 27
#define pinRocker4 14   // Rocker switch 4 connected to GPIO 14

// Network credentials
const char* ssid = "your_ssid";
const char* password = "you_password";

// Create a web server on port 80
WebServer server(80);

// Variables to track the count and last action
int rotaryCount = 1;    // Variable to keep track of the rotary encoder count
int previousStateSW;  // Variable to store the previous state of the SW pin
int previousStateCLK; // Variable to store the previous state of the CLK pin

int previousRockerState1; // Variable to store the previous state of Rocker switch 1
int previousRockerState2; // Variable to store the previous state of Rocker switch 2
int previousRockerState3; // Variable to store the previous state of Rocker switch 3
int previousRockerState4; // Variable to store the previous state of Rocker switch 4

int activatedRockerCount = 1; // Counter for the total number of activated rocker switches

int actualStateSW_global; // Global variable to store the state of SW pin

// HTML content
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mindset Selector</title>
    <style>
        .container {
            display: flex;
            justify-content: space-around;
            margin-bottom: 20px;
            max-height: 20%;
        }
        .image-container {
            text-align: center;
        }
        .image-container img {
            max-width: 100%;
            max-height: 100px;
        }
        .btn-container {
            text-align: center;
            margin-bottom: 20px;
        }
        .btn {
            padding: 10px 20px;
            background-color: #007bff;
            color: #fff;
            border: none;
            cursor: pointer;
            margin: 0 10px;
        }
        #result-container {
            text-align: center;
            margin-top: 20px;
        }
        #result-button-container {
            text-align: center;
            margin-bottom: 20px;
        }
        #result-image {
            max-width: 50%;
            margin: 0 auto;
        }
        #result-image img {
            max-width: 100%;
            height: auto;
        }
    </style>
</head>
<body>
<div id="rotary-count">Rotary Count: </div>
<div id="rocker-count">Activated Rocker Switches: </div>
<div class="container">
    <div class="image-container">
        <h2>Mood</h2>
        <div id="image-container1">
            <img src="/03_smiley.png" alt="Image" data-value="3">
        </div>
        <div class="btn-container">
            <button class="btn" onclick="previousImage(1)">Previous</button>
            <button class="btn" onclick="nextImage(1)">Next</button>
        </div>
    </div>
    <div class="image-container">
        <h2>Interaction acceptance</h2>
        <div id="image-container2">
            <img src="/10_smiley.png" alt="Image" data-value="30">
        </div>
        <div class="btn-container">
            <button class="btn" onclick="previousImage(2)">Previous</button>
            <button class="btn" onclick="nextImage(2)">Next</button>
        </div>
    </div>
</div>

<div id="result-button-container">
    <button class="btn" onclick="addValues()">Set</button>
</div>

<div id="result-container">
    <h2>Mindset</h2>
    <div id="result-text"></div> 
    <div id="result-image"></div>
</div>

<script>
    var currentIndex1 = 2;
    var currentIndex2 = 2;
    var images1 = ["/05_smiley.png", "/04_smiley.png", "/03_smiley.png","/02_smiley.png", "/01_smiley.png"];
    var images2 = ["/50_smiley.png", "/40_smiley.png", "/30_smiley.png", "/20_smiley.png", "/10_smiley.png"];
    var values1 = [5, 4, 3, 2, 1];
    var values2 = [50, 40, 30, 20, 10];
    var allImages = [
        { src: "/smiley_mix11.png", value: 11 },
        { src: "/smiley_mix12.png", value: 12 },
        { src: "/smiley_mix13.png", value: 13 },
        { src: "/smiley_mix14.png", value: 14 },
        { src: "/smiley_mix15.png", value: 15 },
        { src: "/smiley_mix21.png", value: 21 },
        { src: "/smiley_mix22.png", value: 22 },
        { src: "/smiley_mix23.png", value: 23 },
        { src: "/smiley_mix24.png", value: 24 },
        { src: "/smiley_mix25.png", value: 25 },
        { src: "/smiley_mix31.png", value: 31 },
        { src: "/smiley_mix32.png", value: 32 },
        { src: "/smiley_mix33.png", value: 33 },
        { src: "/smiley_mix34.png", value: 34 },
        { src: "/smiley_mix35.png", value: 35 },
        { src: "/smiley_mix41.png", value: 41 },
        { src: "/smiley_mix42.png", value: 42 },
        { src: "/smiley_mix43.png", value: 43 },
        { src: "/smiley_mix44.png", value: 44 },
        { src: "/smiley_mix45.png", value: 45 },
        { src: "/smiley_mix51.png", value: 51 },
        { src: "/smiley_mix52.png", value: 52 },
        { src: "/smiley_mix53.png", value: 53 },
        { src: "/smiley_mix54.png", value: 54 },
        { src: "/smiley_mix55.png", value: 55 },
    ];

    async function fetchData() {
        try {
            const response = await fetch('/data');
            const data = await response.json();
            document.getElementById('rotary-count').innerText = "Rotary Count: " + data.rotaryCount;
            document.getElementById('rocker-count').innerText = "Activated Rocker Switches: " + data.activatedRockerCount;

            // Update currentIndex based on the response
            currentIndex1 = data.rotaryCount - 1; // Using the rotaryCount is used for currentIndex1
            currentIndex2 = data.activatedRockerCount - 1; // Using activatedRockerCount for currentIndex2
            // Check the value of triggerAddValues
                if (data.triggerAddValues) {
                    addValues(); // Call addValues function if triggerAddValues is true
                }
            console.log("Updated indices:", currentIndex1, currentIndex2); // Debugging log
            updateImage(1);
            updateImage(2);
        } catch (error) {
            console.error('Error fetching data:', error);
        }
    }

    setInterval(fetchData, 100);

    function previousImage(containerIndex) {
        if (containerIndex === 1) {
            if (currentIndex1 === 0) return;
            currentIndex1--;
            updateImage(1);
        } else if (containerIndex === 2) {
            if (currentIndex2 === 0) return;
            currentIndex2--;
            updateImage(2);
        }
    }

    function nextImage(containerIndex) {
        if (containerIndex === 1) {
            if (currentIndex1 === images1.length - 1) return;
            currentIndex1++;
            updateImage(1);
        } else if (containerIndex === 2) {
            if (currentIndex2 === images2.length - 1) return;
            currentIndex2++;
            updateImage(2);
        }
    }

    function updateImage(containerIndex) {
        var displayedImage;
        if (containerIndex === 1) {
            displayedImage = document.getElementById("image-container1").getElementsByTagName("img")[0];
            displayedImage.src = images1[currentIndex1];
            displayedImage.dataset.value = values1[currentIndex1];
        } else if (containerIndex === 2) {
            displayedImage = document.getElementById("image-container2").getElementsByTagName("img")[0];
            displayedImage.src = images2[currentIndex2];
            displayedImage.dataset.value = values2[currentIndex2];
        }
    }

    function addValues() {
        var value1 = values1[currentIndex1];
        var value2 = values2[currentIndex2];
        var resultValue = value1 + value2;
        var resultImage = allImages.find(image => image.value === resultValue);
        document.getElementById("result-text").innerText = "Mindset Value: " + resultValue;
        if (resultImage) {
            document.getElementById("result-image").innerHTML = `<img src="${resultImage.src}" alt="Result Image">`;
        } else {
            document.getElementById("result-image").innerHTML = "No matching image found.";
        }
    }
</script>
</body>
</html>
)rawliteral";

// Function to initialize the SPIFFS
void initSPIFFS() {
    if (!SPIFFS.begin(true)) {
        Serial.println("An error occurred while mounting SPIFFS");
        return;
    }
    Serial.println("SPIFFS mounted successfully");
}

void setup() {
    Serial.begin(115200); // Initialize serial communication at 115200 baud rate

    pinMode(pinSW, INPUT_PULLUP); // Set SW pin as input with internal pull-up resistor
    pinMode(pinDT, INPUT);        // Set DT pin as input
    pinMode(pinCLK, INPUT);       // Set CLK pin as input
    pinMode(pinLED, OUTPUT);      // Set LED pin as output

    pinMode(pinRocker1, INPUT_PULLUP); // Set Rocker switch 1 as input with internal pull-up resistor
    pinMode(pinRocker2, INPUT_PULLUP); // Set Rocker switch 2 as input with internal pull-up resistor
    pinMode(pinRocker3, INPUT_PULLUP); // Set Rocker switch 3 as input with internal pull-up resistor
    pinMode(pinRocker4, INPUT_PULLUP); // Set Rocker switch 4 as input with internal pull-up resistor

    previousStateSW  = digitalRead(pinSW);  // Read initial state of the SW pin
    previousStateCLK = digitalRead(pinCLK); // Read initial state of the CLK pin

    previousRockerState1 = digitalRead(pinRocker1); // Read initial state of Rocker switch 1
    previousRockerState2 = digitalRead(pinRocker2); // Read initial state of Rocker switch 2
    previousRockerState3 = digitalRead(pinRocker3); // Read initial state of Rocker switch 3
    previousRockerState4 = digitalRead(pinRocker4); // Read initial state of Rocker switch 4

    analogWrite(pinLED, 0); // Initialize the LED with 0 brightness (off)

    // Initialize SPIFFS
    initSPIFFS();

    // Connect to Wi-Fi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(1000);
        Serial.println("Connecting to WiFi...");
    }
    Serial.println("Connected to WiFi");

    // Print the IP address
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    // Define server routes
    server.on("/", []() {
        server.send_P(200, "text/html", index_html);
    });

    server.on("/data", []() {
        actualStateSW_global = digitalRead(pinSW); // Read the current state of SW pin
        bool triggerAddValues = (actualStateSW_global == LOW); // Set triggerAddValues based on actualStateSW_global
        String json = "{\"rotaryCount\":" + String(rotaryCount) + ",\"activatedRockerCount\":" + String(activatedRockerCount) + ",\"triggerAddValues\":" + (actualStateSW_global == LOW ? "true" : "false") + "}";
        server.send(200, "application/json", json);
    });

    server.serveStatic("/", SPIFFS, "/");

    server.begin();
    Serial.println("HTTP server started");
}

void loop() {
    server.handleClient();

    int actualStateCLK = digitalRead(pinCLK); // Read current state of the CLK pin
    int actualStateDT  = digitalRead(pinDT);  // Read current state of the DT pin
    int actualStateSW  = digitalRead(pinSW);  // Read current state of the SW pin

    // Read current states of rocker switches again after debounce delay
    int currentRockerState1 = digitalRead(pinRocker1);
    int currentRockerState2 = digitalRead(pinRocker2);
    int currentRockerState3 = digitalRead(pinRocker3);
    int currentRockerState4 = digitalRead(pinRocker4);

    // Check if the state has changed and is stable
    if (currentRockerState1 != previousRockerState1 || 
        currentRockerState2 != previousRockerState2 || 
        currentRockerState3 != previousRockerState3 || 
        currentRockerState4 != previousRockerState4) {
        // Debounce delay for rocker switches
        delay(50);
        // Update activatedRockerCount only if there is a change in any rocker switch state
        activatedRockerCount = 1;
        if (currentRockerState1 == LOW) {
            activatedRockerCount++;
        }
        if (currentRockerState2 == LOW) {
            activatedRockerCount++;
        }
        if (currentRockerState3 == LOW) {
            activatedRockerCount++;
        }
        if (currentRockerState4 == LOW) {
            activatedRockerCount++;
        }

        // Print the number of activated rocker switches to the Serial Monitor
        Serial.print(F("Activated rocker switches: "));
        Serial.println(activatedRockerCount);

        // Update previous rocker states
        previousRockerState1 = currentRockerState1;
        previousRockerState2 = currentRockerState2;
        previousRockerState3 = currentRockerState3;
        previousRockerState4 = currentRockerState4;
    }

    // Push Button verification
    if (actualStateSW != previousStateSW) { // Check if SW state has changed
        previousStateSW = actualStateSW; // Update the previous SW state
        delay(50); // Debounce delay
        if (actualStateSW == LOW) {
            Serial.println(F("SW Button pushed")); // Print message when button is pressed
        } else {
            Serial.println(F("SW Button released")); // Print message when button is released
        }    
    }

    // Rotary encoder verification

    if (rotaryCount < 1) {
        rotaryCount = 1;
    } else if (rotaryCount > 5) {
        rotaryCount = 5;
    }

    if (actualStateCLK != previousStateCLK) {
        delayMicroseconds(200); // Debounce delay
        if (digitalRead(pinCLK) == actualStateCLK) { // Check if the CLK signal has settled
            previousStateCLK = actualStateCLK;

            if (actualStateCLK == LOW) {
                if (actualStateCLK != actualStateDT) { // Comparing CLK and DT, if they're different, direction is counter-clockwise
                    rotaryCount--; // Decrement counter
                    Serial.println(F("counter-clockwise")); // Display value on Serial Monitor
                } else { // When CLK and DT are similar, direction is clockwise
                    rotaryCount++; // Increment counter
                    Serial.println(F("clockwise")); // Display value on Serial Monitor
                }
                Serial.println(rotaryCount);

                // Ensure count stays within bounds (0-5 for PWM)
                if (rotaryCount < 1) {
                    rotaryCount = 1;
                } else if (rotaryCount > 5) {
                    rotaryCount = 5;
                }
                // Map rotaryCount to PWM range (0-128)
                int pwmValue = map(rotaryCount, 1, 5, 0, 128);
                analogWrite(pinLED, pwmValue); // Set LED brightness
            }
        }
    }
}
#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>

// Define pins for rotary encoder and button
#define pinCLK 32
#define pinDT  35
#define pinSW  34
#define pinLED 23

#define pinRocker1 25   // Rocker switch 1 connected to GPIO 25
#define pinRocker2 26   // Rocker switch 2 connected to GPIO 26
#define pinRocker3 27   // Rocker switch 3 connected to GPIO 27
#define pinRocker4 14   // Rocker switch 4 connected to GPIO 14

// Network credentials
const char* ssid = "your-ssid";
const char* password = "your_password";

// Create a web server on port 80
WebServer server(80);

// Variables to track the count and last action
int rotaryCount = 1;    // Variable to keep track of the rotary encoder count
int previousStateSW;  // Variable to store the previous state of the SW pin
int previousStateCLK; // Variable to store the previous state of the CLK pin

int previousRockerState1; // Variable to store the previous state of Rocker switch 1
int previousRockerState2; // Variable to store the previous state of Rocker switch 2
int previousRockerState3; // Variable to store the previous state of Rocker switch 3
int previousRockerState4; // Variable to store the previous state of Rocker switch 4

int activatedRockerCount = 1; // Counter for the total number of activated rocker switches

int actualStateSW_global; // Global variable to store the state of SW pin

// HTML content
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mindset Selector</title>
    <style>
        .container {
            display: flex;
            justify-content: space-around;
            margin-bottom: 20px;
            max-height: 20%;
        }
        .image-container {
            text-align: center;
        }
        .image-container img {
            max-width: 100%;
            max-height: 100px;
        }
        .btn-container {
            text-align: center;
            margin-bottom: 20px;
        }
        .btn {
            padding: 10px 20px;
            background-color: #007bff;
            color: #fff;
            border: none;
            cursor: pointer;
            margin: 0 10px;
        }
        #result-container {
            text-align: center;
            margin-top: 20px;
        }
        #result-button-container {
            text-align: center;
            margin-bottom: 20px;
        }
        #result-image {
            max-width: 50%;
            margin: 0 auto;
        }
        #result-image img {
            max-width: 100%;
            height: auto;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="image-container">
        <h2>Mood</h2>
        <div id="image-container1">
            <img src="/03_smiley.png" alt="Image" data-value="3">
        </div>
        <div class="btn-container">
            <button class="btn" onclick="previousImage(1)">Previous</button>
            <button class="btn" onclick="nextImage(1)">Next</button>
        </div>
    </div>
    <div class="image-container">
        <h2>Interaction acceptance</h2>
        <div id="image-container2">
            <img src="/10_smiley.png" alt="Image" data-value="30">
        </div>
        <div class="btn-container">
            <button class="btn" onclick="previousImage(2)">Previous</button>
            <button class="btn" onclick="nextImage(2)">Next</button>
        </div>
    </div>
</div>

<div id="result-button-container">
    <button class="btn" onclick="addValues()">Set</button>
</div>

<div id="result-container">
    <h2>Mindset</h2>
    <div id="result-image"></div>
</div>

<script>
    var currentIndex1 = 2;
    var currentIndex2 = 2;
    var images1 = ["/05_smiley.png", "/04_smiley.png", "/03_smiley.png","/02_smiley.png", "/01_smiley.png"];
    var images2 = ["/50_smiley.png", "/40_smiley.png", "/30_smiley.png", "/20_smiley.png", "/10_smiley.png"];
    var values1 = [5, 4, 3, 2, 1];
    var values2 = [50, 40, 30, 20, 10];
    var allImages = [
        { src: "/smiley_mix11.png", value: 11 },
        { src: "/smiley_mix12.png", value: 12 },
        { src: "/smiley_mix13.png", value: 13 },
        { src: "/smiley_mix14.png", value: 14 },
        { src: "/smiley_mix15.png", value: 15 },
        { src: "/smiley_mix21.png", value: 21 },
        { src: "/smiley_mix22.png", value: 22 },
        { src: "/smiley_mix23.png", value: 23 },
        { src: "/smiley_mix24.png", value: 24 },
        { src: "/smiley_mix25.png", value: 25 },
        { src: "/smiley_mix31.png", value: 31 },
        { src: "/smiley_mix32.png", value: 32 },
        { src: "/smiley_mix33.png", value: 33 },
        { src: "/smiley_mix34.png", value: 34 },
        { src: "/smiley_mix35.png", value: 35 },
        { src: "/smiley_mix41.png", value: 41 },
        { src: "/smiley_mix42.png", value: 42 },
        { src: "/smiley_mix43.png", value: 43 },
        { src: "/smiley_mix44.png", value: 44 },
        { src: "/smiley_mix45.png", value: 45 },
        { src: "/smiley_mix51.png", value: 51 },
        { src: "/smiley_mix52.png", value: 52 },
        { src: "/smiley_mix53.png", value: 53 },
        { src: "/smiley_mix54.png", value: 54 },
        { src: "/smiley_mix55.png", value: 55 },
    ];

    async function fetchData() {
        try {
            const response = await fetch('/data');
            const data = await response.json();

            // Update currentIndex based on the response
            currentIndex1 = data.rotaryCount - 1; // Using the rotaryCount for currentIndex1
            currentIndex2 = data.activatedRockerCount - 1; // Using activatedRockerCount for currentIndex2
            // Check the value of triggerAddValues
            if (data.triggerAddValues) {
                addValues(); // Call addValues function if triggerAddValues is true
            }
            console.log("Updated indices:", currentIndex1, currentIndex2); // Debugging log
            updateImage(1);
            updateImage(2);
        } catch (error) {
            console.error('Error fetching data:', error);
        }
    }

    setInterval(fetchData, 100);

    function previousImage(containerIndex) {
        if (containerIndex === 1) {
            if (currentIndex1 === 0) return;
            currentIndex1--;
            updateImage(1);
        } else if (containerIndex === 2) {
            if (currentIndex2 === 0) return;
            currentIndex2--;
            updateImage(2);
        }
    }

    function nextImage(containerIndex) {
        if (containerIndex === 1) {
            if (currentIndex1 === images1.length - 1) return;
            currentIndex1++;
            updateImage(1);
        } else if (containerIndex === 2) {
            if (currentIndex2 === images2.length - 1) return;
            currentIndex2++;
            updateImage(2);
        }
    }

    function updateImage(containerIndex) {
        var displayedImage;
        if (containerIndex === 1) {
            displayedImage = document.getElementById("image-container1").getElementsByTagName("img")[0];
            displayedImage.src = images1[currentIndex1];
            displayedImage.dataset.value = values1[currentIndex1];
        } else if (containerIndex === 2) {
            displayedImage = document.getElementById("image-container2").getElementsByTagName("img")[0];
            displayedImage.src = images2[currentIndex2];
            displayedImage.dataset.value = values2[currentIndex2];
        }
    }

    function addValues() {
        var value1 = values1[currentIndex1];
        var value2 = values2[currentIndex2];
        var resultValue = value1 + value2;
        var resultImage = allImages.find(image => image.value === resultValue);
        if (resultImage) {
            document.getElementById("result-image").innerHTML = `<img src="${resultImage.src}" alt="Result Image">`;
        } else {
            document.getElementById("result-image").innerHTML = "No matching image found.";
        }
    }
</script>
</body>
</html>
)rawliteral";

// Function to initialize the SPIFFS
void initSPIFFS() {
    if (!SPIFFS.begin(true)) {
        Serial.println("An error occurred while mounting SPIFFS");
        return;
    }
    Serial.println("SPIFFS mounted successfully");
}

void setup() {
    Serial.begin(115200); // Initialize serial communication at 115200 baud rate

    pinMode(pinSW, INPUT_PULLUP); // Set SW pin as input with internal pull-up resistor
    pinMode(pinDT, INPUT);        // Set DT pin as input
    pinMode(pinCLK, INPUT);       // Set CLK pin as input
    pinMode(pinLED, OUTPUT);      // Set LED pin as output

    pinMode(pinRocker1, INPUT_PULLUP); // Set Rocker switch 1 as input with internal pull-up resistor
    pinMode(pinRocker2, INPUT_PULLUP); // Set Rocker switch 2 as input with internal pull-up resistor
    pinMode(pinRocker3, INPUT_PULLUP); // Set Rocker switch 3 as input with internal pull-up resistor
    pinMode(pinRocker4, INPUT_PULLUP); // Set Rocker switch 4 as input with internal pull-up resistor

    previousStateSW  = digitalRead(pinSW);  // Read initial state of the SW pin
    previousStateCLK = digitalRead(pinCLK); // Read initial state of the CLK pin

    previousRockerState1 = digitalRead(pinRocker1); // Read initial state of Rocker switch 1
    previousRockerState2 = digitalRead(pinRocker2); // Read initial state of Rocker switch 2
    previousRockerState3 = digitalRead(pinRocker3); // Read initial state of Rocker switch 3
    previousRockerState4 = digitalRead(pinRocker4); // Read initial state of Rocker switch 4

    analogWrite(pinLED, 0); // Initialize the LED with 0 brightness (off)

    // Initialize SPIFFS
    initSPIFFS();

    // Connect to Wi-Fi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(1000);
        Serial.println("Connecting to WiFi...");
    }
    Serial.println("Connected to WiFi");

    // Print the IP address
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    // Define server routes
    server.on("/", []() {
        server.send_P(200, "text/html", index_html);
    });

    server.on("/data", []() {
        actualStateSW_global = digitalRead(pinSW); // Read the current state of SW pin
        bool triggerAddValues = (actualStateSW_global == LOW); // Set triggerAddValues based on actualStateSW_global
        String json = "{\"rotaryCount\":" + String(rotaryCount) + ",\"activatedRockerCount\":" + String(activatedRockerCount) + ",\"triggerAddValues\":" + (actualStateSW_global == LOW ? "true" : "false") + "}";
        server.send(200, "application/json", json);
    });

    server.serveStatic("/", SPIFFS, "/");

    server.begin();
    Serial.println("HTTP server started");
}

void loop() {
    server.handleClient();

    int actualStateCLK = digitalRead(pinCLK); // Read current state of the CLK pin
    int actualStateDT  = digitalRead(pinDT);  // Read current state of the DT pin
    int actualStateSW  = digitalRead(pinSW);  // Read current state of the SW pin

    // Read current states of rocker switches again after debounce delay
    int currentRockerState1 = digitalRead(pinRocker1);
    int currentRockerState2 = digitalRead(pinRocker2);
    int currentRockerState3 = digitalRead(pinRocker3);
    int currentRockerState4 = digitalRead(pinRocker4);

    // Check if the state has changed and is stable
    if (currentRockerState1 != previousRockerState1 || 
        currentRockerState2 != previousRockerState2 || 
        currentRockerState3 != previousRockerState3 || 
        currentRockerState4 != previousRockerState4) {
        // Debounce delay for rocker switches
        delay(50);
        // Update activatedRockerCount only if there is a change in any rocker switch state
        activatedRockerCount = 1;
        if (currentRockerState1 == LOW) {
            activatedRockerCount++;
        }
        if (currentRockerState2 == LOW) {
            activatedRockerCount++;
        }
        if (currentRockerState3 == LOW) {
            activatedRockerCount++;
        }
        if (currentRockerState4 == LOW) {
            activatedRockerCount++;
        }

        // Print the number of activated rocker switches to the Serial Monitor
        Serial.print(F("Activated rocker switches: "));
        Serial.println(activatedRockerCount);

        // Update previous rocker states
        previousRockerState1 = currentRockerState1;
        previousRockerState2 = currentRockerState2;
        previousRockerState3 = currentRockerState3;
        previousRockerState4 = currentRockerState4;
    }

    // Push Button verification
    if (actualStateSW != previousStateSW) { // Check if SW state has changed
        previousStateSW = actualStateSW; // Update the previous SW state
        delay(50); // Debounce delay
        if (actualStateSW == LOW) {
            Serial.println(F("SW Button pushed")); // Print message when button is pressed
        } else {
            Serial.println(F("SW Button released")); // Print message when button is released
        }    
    }

    // Rotary encoder verification

    if (rotaryCount < 1) {
        rotaryCount = 1;
    } else if (rotaryCount > 5) {
        rotaryCount = 5;
    }

    if (actualStateCLK != previousStateCLK) {
        delayMicroseconds(200); // Debounce delay
        if (digitalRead(pinCLK) == actualStateCLK) { // Check if the CLK signal has settled
            previousStateCLK = actualStateCLK;

            if (actualStateCLK == LOW) {
                if (actualStateCLK != actualStateDT) { // Comparing CLK and DT, if they're different, direction is counter-clockwise
                    rotaryCount--; // Decrement counter
                    Serial.println(F("counter-clockwise")); // Display value on Serial Monitor
                } else { // When CLK and DT are similar, direction is clockwise
                    rotaryCount++; // Increment counter
                    Serial.println(F("clockwise")); // Display value on Serial Monitor
                }
                Serial.println(rotaryCount);

                // Ensure count stays within bounds (0-5 for PWM)
                if (rotaryCount < 1) {
                    rotaryCount = 1;
                } else if (rotaryCount > 5) {
                    rotaryCount = 5;
                }
                // Map rotaryCount to PWM range (0-128)
                int pwmValue = map(rotaryCount, 1, 5, 0, 128);
                analogWrite(pinLED, pwmValue); // Set LED brightness
            }
        }
    }
}

TFT screen display

Now that the remote control worked properly and I could display and control an interface on phone, the bare minimum for my final project was ready. I decided to use the time left to try to make it work on a dedicated screen, the ESP32-2432S028 or Cheap-yellow-display I tested during week 13.

I wanted at first to display the webpage on the screen but it didn’t seem possible or I haven’t find how. Best solution seemed to be displaying images and make them update.

I searched for examples to display images on it and find this site Test for cheap yellow display. I used the first part of it to understand how to get access to the SD Card using SD.h library and how to add Wifi config in it but as I will use ESP-Now I will not need that feature. I also could access to images on the SD card but struggled a bit to display them. Using JpegDecoder Library and with the help of chatGPT, I found out how to display an image, then display multiple images dispatched on the same screen. To make it easier, I had redimensionned all images to fit part of the screen size and reupload them on the SD card. So base emojis are about half the width of the screen and the mixed one are about 2/3 of the length.

getting access to SD card display multiple images - one aligned to the bottom display multiple images - different x attributes display multiple images - all well dispatched

Then I reimplement arrays of images with values and calculation of value in C++ version because javascript don’t work .

I wanted to add touchscreen to navigate in the arrays but when I uploaded the code, images wouldn’t display. I spend much time to debug it, trying to erase part of the code, changing orders. Finally it seems there was conflict between touchscreen and SD card using both SPI to work. If setup start SD before touchscreen, the touch was detected but it was impossible to load images and if the contrary, the touchscreen seem to constantly being pushed and the images were uploaded.

I decided to abandonned using the touchscreen, and made a first code that automatically change the base images and update the mixed image to verify that the images can be changed.

With that success, I continued progressing by implementing ESP-Now communication. I used the previous test I made to implement ESPNow in my code and add the rocker switches counter reading.

I add to redo the code step by step because there was incompatibility between ESPNow Library and JPEGDecoder Library and couldn’t use the receive data as it is. The displayJpeg command couldn’t be called in the ‘void onReceive’ function used for communication. I needed to update variables that could be used to trigger other functions in the loop. I also edited a starting page and implement a clear screen when changing the value.

Here’s the evolution of the code:

#include <WiFi.h>
#include <FS.h>
#include <SPI.h>
#include <SD.h>
#include <TFT_eSPI.h>
#include <ArduinoJson.h>

struct Config {
char ssid[32];
char password[32];
char ep[32];
char port[32];
};

Config config;
const char *cfg = "/etc/config.json"; //file to create where ssid, password, ep and port are registered
TFT_eSPI tft = TFT_eSPI();

void setup() {
Serial.begin(115200);
delay(1000);

Serial.println("Starting setup...");

// TFT initialization
tft.begin();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);
tft.setCursor(0, 0);
tft.setTextFont(1);
tft.setTextSize(2);
tft.setTextColor(TFT_WHITE, TFT_BLACK);

Serial.println("TFT initialized.");

// SD card initialization
if (!SD.begin()) {
    Serial.println("SD card mount failure");
    return;
}
Serial.println("SD card initialized successfully");
tft.println("SD card initialized successfully");

// Load configuration from SD card
File file = SD.open(cfg);
if (!file) {
    Serial.println("Configuration file not found");
    tft.println("Config file not found");
} else {
    Serial.println("Configuration file found");
    loadConfiguration(config);
    file.close();
}

// Attempt WiFi connection
connectToWiFi();
}

void loop() {
// Placeholder for main loop code
}

void connectToWiFi() {
Serial.println("Attempting to connect to WiFi...");
tft.println("Attempting to connect to WiFi...");

WiFi.begin(config.ssid, config.password);

unsigned long startTime = millis();
while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
    tft.print(".");
    if (millis() - startTime > 20000) { // 20 seconds timeout
    Serial.println("Failed to connect to WiFi");
    tft.println("Failed to connect to WiFi");
    return;
    }
}

Serial.println();
Serial.println("Connected to WiFi");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());

tft.println();
tft.println("Connected to WiFi");
tft.print("IP: ");
tft.println(WiFi.localIP());
}

void loadConfiguration(Config &config) {
File file = SD.open(cfg, FILE_READ);
if (!file) {
    Serial.println("Failed to open config file");
    return;
}

StaticJsonDocument<512> doc;
DeserializationError error = deserializeJson(doc, file);
if (error) {
    Serial.print("Failed to parse config file: ");
    Serial.println(error.c_str());
    return;
}

strlcpy(config.ssid, doc["SSID"] | "", sizeof(config.ssid));
strlcpy(config.password, doc["pass"] | "", sizeof(config.password));
strlcpy(config.ep, doc["ep"] | "", sizeof(config.ep));
strlcpy(config.port, doc["port"] | "", sizeof(config.port));

Serial.println("Configuration loaded:");
Serial.print("SSID: "); Serial.println(config.ssid);
Serial.print("Password: "); Serial.println(config.password);
Serial.print("Endpoint: "); Serial.println(config.ep);
Serial.print("Port: "); Serial.println(config.port);
}
#include <FS.h>
#include <SPI.h>
#include <SD.h>
#include <TFT_eSPI.h>
#include <JPEGDecoder.h>

// File paths
const char *imageFile1 = "/images/01_smiley.jpg";
const char *imageFile2 = "/images/20_smiley.jpg";
const char *imageFile3 = "/images/smiley_mix21.jpg";

// TFT and SD
TFT_eSPI tft = TFT_eSPI();

void setup() {
Serial.begin(115200);
delay(1000);

// Initialize TFT
tft.begin();
tft.setRotation(2);  // Adjust as needed
tft.fillScreen(TFT_WHITE);

// Initialize SD card
if (!SD.begin()) {
    Serial.println("SD card initialization failed!");
    return;
}
Serial.println("SD card initialized.");

// Display images
displayJpeg(imageFile1, 0);
displayJpeg(imageFile2, 1);
displayJpeg(imageFile3, 2);
}

void loop() {
// Main loop code (if any)
}

void displayJpeg(const char *filename, int imageIndex) {
// Open the file
File jpegFile = SD.open(filename, FILE_READ);
if (!jpegFile) {
    Serial.println("Failed to open file for reading");
    return;
}

// Decode the JPEG file
if (JpegDec.decodeSdFile(jpegFile)) {
    jpegRender(imageIndex);
} else {
    Serial.println("JPEG file format not supported!");
}

// Close the file
jpegFile.close();
}

void jpegRender(int imageIndex) {
uint16_t *pImg;
uint16_t mcu_w = JpegDec.MCUWidth;
uint16_t mcu_h = JpegDec.MCUHeight;
uint32_t max_x = JpegDec.width;
uint32_t max_y = JpegDec.height;

// Calculate the vertical offset
int16_t y_offset = 0;

// Calculate the horizontal offset based on the imageIndex
int16_t x_offset = 0;
if (imageIndex == 0) {
    // First image positioned at 1/4 of TFT width
    x_offset = tft.width() / 4 - max_x / 2;
} else if (imageIndex == 1) {
    // Second image positioned at 3/4 of TFT width
    x_offset = 3 * tft.width() / 4 - max_x / 2;
} else {
    // Center third image at the bottom
    x_offset = (tft.width() - max_x) / 2;
    y_offset = tft.height() - max_y;
}

bool swapBytes = tft.getSwapBytes();
tft.setSwapBytes(true);

while (JpegDec.read()) {
    pImg = JpegDec.pImage;

    int mcu_x = JpegDec.MCUx * mcu_w;
    int mcu_y = JpegDec.MCUy * mcu_h;

    uint32_t win_w = (mcu_x + mcu_w <= max_x) ? mcu_w : (max_x % mcu_w);
    uint32_t win_h = (mcu_y + mcu_h <= max_y) ? mcu_h : (max_y % mcu_h);

    if ((mcu_x + win_w) <= tft.width() && (mcu_y + win_h) <= tft.height()) {
    tft.pushImage(mcu_x + x_offset, mcu_y + y_offset, win_w, win_h, pImg);
    } else if ((mcu_y + win_h) >= tft.height()) {
    JpegDec.abort();
    }
}

tft.setSwapBytes(swapBytes);
}
#include <SPI.h>
#include <TFT_eSPI.h>
#include "ESP32_NOW.h"
#include "WiFi.h"
#include <esp_mac.h>  // For the MAC2STR and MACSTR macros
#include <vector>
#include <JPEGDecoder.h>
#include <SD.h>
#include <FS.h>

// Image paths and corresponding values
const char *images1[] = {"/images/05_smiley.jpg", "/images/04_smiley.jpg", "/images/03_smiley.jpg", "/images/02_smiley.jpg", "/images/01_smiley.jpg"};
const int values1[] = {5, 4, 3, 2, 1};

const char *images2[] = {"/images/50_smiley.jpg", "/images/40_smiley.jpg", "/images/30_smiley.jpg", "/images/20_smiley.jpg", "/images/10_smiley.jpg"};
const int values2[] = {50, 40, 30, 20, 10};

struct ImageData {
const char *src;
int value;
};

ImageData allImages[] = {
    {"/images/smiley_mix11.jpg", 11}, {"/images/smiley_mix12.jpg", 12}, {"/images/smiley_mix13.jpg", 13},
    {"/images/smiley_mix14.jpg", 14}, {"/images/smiley_mix15.jpg", 15}, {"/images/smiley_mix21.jpg", 21},
    {"/images/smiley_mix22.jpg", 22}, {"/images/smiley_mix23.jpg", 23}, {"/images/smiley_mix24.jpg", 24},
    {"/images/smiley_mix25.jpg", 25}, {"/images/smiley_mix31.jpg", 31}, {"/images/smiley_mix32.jpg", 32},
    {"/images/smiley_mix33.jpg", 33}, {"/images/smiley_mix34.jpg", 34}, {"/images/smiley_mix35.jpg", 35},
    {"/images/smiley_mix41.jpg", 41}, {"/images/smiley_mix42.jpg", 42}, {"/images/smiley_mix43.jpg", 43},
    {"/images/smiley_mix44.jpg", 44}, {"/images/smiley_mix45.jpg", 45}, {"/images/smiley_mix51.jpg", 51},
    {"/images/smiley_mix52.jpg", 52}, {"/images/smiley_mix53.jpg", 53}, {"/images/smiley_mix54.jpg", 54},
    {"/images/smiley_mix55.jpg", 55},
};

int currentIndex1 = 2;
int currentIndex2 = 2;
int currentIndexAll = 0; // Initialize the index to 0

const int numImages1 = sizeof(images1) / sizeof(images1[0]);
const int numImages2 = sizeof(images2) / sizeof(images2[0]);
const int numAllImages = sizeof(allImages) / sizeof(allImages[0]);

TFT_eSPI tft = TFT_eSPI();

/* Definitions */
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define FONT_SIZE 4

#define ESPNOW_WIFI_CHANNEL 6

bool updateDisplay = false; // Global flag to indicate display update
bool updateResult = false; // Global flag to indicate display update

/* Classes */

// Creating a new class that inherits from the ESP_NOW_Peer class is required.

class ESP_NOW_Peer_Class : public ESP_NOW_Peer {
public:
// Constructor of the class
ESP_NOW_Peer_Class(const uint8_t *mac_addr,
                    uint8_t channel,
                    wifi_interface_t iface,
                    const uint8_t *lmk)
    : ESP_NOW_Peer(mac_addr, channel, iface, lmk) {}

// Destructor of the class
~ESP_NOW_Peer_Class() {}

// Function to register the control peer
bool add_peer() {
    if (!add()) {
    log_e("Failed to register the broadcast peer");
    return false;
    }
    return true;
}

// Function to handle received messages from the control
void onReceive(const uint8_t *data, size_t len, bool broadcast) {
    Serial.printf("Received a message from control " MACSTR " (%s)\n", MAC2STR(addr()), broadcast ? "broadcast" : "unicast");
    Serial.printf("  Message: %.*s\n", len, data);  // Print the message as received

    // Try to extract the numeric part from the message
    String message = String((char *)data);
    int numberPart = message.substring(message.lastIndexOf(' ') + 1).toInt();

    if (message.startsWith("clockwise")) {
    if (numberPart >= 1 && numberPart <= 5) {
        currentIndex1 = constrain(numberPart - 1, 0, numImages1 - 1); // Update currentIndex1 based on rotary count
        Serial.printf("Updated currentIndex1 to %d\n", currentIndex1);
        updateDisplay = true; // Set flag to update display1 in the main loop
    } else {
        Serial.println("Invalid message format or value.");
    }
    } else if (message.startsWith("counter-clockwise")) {
    if (numberPart >= 1 && numberPart <= 5) {
        currentIndex1 = constrain(numberPart - 1, 0, numImages1 - 1); // Update currentIndex1 based on rotary count
        Serial.printf("Updated currentIndex1 to %d\n", currentIndex1);
        updateDisplay = true; // Set flag to update display1 in the main loop
    } else {
        Serial.println("Invalid message format or value.");
    }
    } else if (message.startsWith("Rockers")) {
    if (numberPart >= 1 && numberPart <= 5) {
        currentIndex2 = constrain(numberPart - 1, 0, numImages2 - 1); // Update currentIndex2 based on rotary count
        Serial.printf("Updated currentIndex2 to %d\n", currentIndex2);
        updateDisplay = true; // Set flag to updateDisplay in the main loop
    } else {
        Serial.println("Invalid message format or value.");
    } 
    } else if (message.equals("SW: pressed")) {
    int sum = values1[currentIndex1] + values2[currentIndex2];
    for (int i = 0; i < numAllImages; ++i) {
        if (allImages[i].value == sum) {
        // Update the currentIndexAll to the index of the matching image
        currentIndexAll = i;  
        Serial.printf("Updated currentIndexAll to %d\n", currentIndexAll);
        updateResult = true; // Set flag to updateResult in the main loop
        }
    }
    } else {
    Serial.println("Invalid message format or value.");
    }
}
};

/* Global Variables */

// List of all the controls. It will be populated when a new control is registered
std::vector<ESP_NOW_Peer_Class> masters;

/* Callbacks */

// Callback called when an unknown peer sends a message
void register_new_master(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) {
if (memcmp(info->des_addr, ESP_NOW.BROADCAST_ADDR, 6) == 0) {
    Serial.printf("Unknown peer " MACSTR " sent a broadcast message\n", MAC2STR(info->src_addr));
    Serial.println("Registering the peer as a controller");

    ESP_NOW_Peer_Class new_master(info->src_addr, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, NULL);

    masters.push_back(new_master);
    if (!masters.back().add_peer()) {
    Serial.println("Failed to register the new controller");
    return;
    }
} else {
    // The peripherals will only receive broadcast messages
    log_v("Received a unicast message from " MACSTR, MAC2STR(info->src_addr));
    log_v("Ignoring the message");
}
}

void setup() {
Serial.begin(115200);

    // Initialize SD card
if (!SD.begin()) {
    Serial.println("SD card initialization failed!");
    return;
}
Serial.println("SD card initialized.");

// Initialize the Wi-Fi module
WiFi.mode(WIFI_STA);
WiFi.setChannel(ESPNOW_WIFI_CHANNEL);
while (!WiFi.STA.started()) delay(100);

Serial.println("ESP-NOW Example - Broadcast Peripherals");
Serial.println("Wi-Fi parameters:");
Serial.println("  Mode: STA");
Serial.println("  MAC Address: " + WiFi.macAddress());
Serial.printf("  Channel: %d\n", ESPNOW_WIFI_CHANNEL);

// Initialize the ESP-NOW protocol
if (!ESP_NOW.begin()) {
    Serial.println("Failed to initialize ESP-NOW");
    Serial.println("Rebooting in 5 seconds...");
    delay(5000);
    ESP.restart();
}

// Register the new peer callback
ESP_NOW.onNewPeer(register_new_master, NULL);

Serial.println("Setup complete. Waiting for a control to broadcast a message...");

tft.init();          // Start the TFT display
tft.setRotation(2);  // Set the TFT display rotation in landscape mode

// Clear the screen before writing to it
tft.fillScreen(TFT_WHITE);
tft.setTextColor(TFT_BLACK, TFT_WHITE);

// Display images based on currentIndex
displayJpeg(images1[currentIndex1], 0);
displayJpeg(images2[currentIndex2], 1);


// Set X and Y coordinates for center of display
    int centerX = SCREEN_HEIGHT / 2;
    int centerY = SCREEN_WIDTH / 2;

tft.drawCentreString("SHOULD", centerX, centerY - 100, FONT_SIZE);
tft.drawCentreString("YOU", centerX, centerY - 80, FONT_SIZE);
tft.drawCentreString("TALK", centerX, centerY - 60, FONT_SIZE);
tft.drawCentreString("TO ME", centerX, centerY - 40, FONT_SIZE);
tft.drawCentreString("-O-", centerX, centerY - 20, FONT_SIZE);
tft.drawCentreString("METER", centerX, centerY, FONT_SIZE);

tft.drawCentreString("Communication is ready", centerX, centerY + 40, 2);

tft.drawCentreString("By Alexis", 200, 300, 2);
}

void loop() {
// Update display if the flag is set
if (updateDisplay) {
    tft.fillScreen(TFT_WHITE);
    tft.setTextColor(TFT_BLACK, TFT_WHITE);
    displayJpeg(images1[currentIndex1], 0);
    displayJpeg(images2[currentIndex2], 1);
    tft.drawCentreString("By Alexis", 200, 300, 2);
    updateDisplay = false; // Reset the flag
}

if (updateResult) {
    tft.fillScreen(TFT_WHITE);
    tft.setTextColor(TFT_BLACK, TFT_WHITE);
    displayJpeg(images1[currentIndex1], 0);
    displayJpeg(images2[currentIndex2], 1);
    displayJpeg(allImages[currentIndexAll].src, 2);
    tft.drawCentreString("By Alexis", 200, 300, 2);
    updateResult = false; // Reset the flag
}
}


void displayJpeg(const char *filename, int imageIndex) {
Serial.print("Opening file: ");
Serial.println(filename);

// Open the file
File jpegFile = SD.open(filename, FILE_READ);
if (!jpegFile) {
    Serial.print("Failed to open file ");
    Serial.println(filename);
    return;
}

// Decode the JPEG file
if (JpegDec.decodeSdFile(jpegFile)) {
    Serial.print("Decoding successful for ");
    Serial.println(filename);
    jpegRender(imageIndex);
} else {
    Serial.print("JPEG file format not supported for ");
    Serial.println(filename);
}

// Close the file
jpegFile.close();
}

void jpegRender(int imageIndex) {
uint16_t *pImg;
uint16_t mcu_w = JpegDec.MCUWidth;
uint16_t mcu_h = JpegDec.MCUHeight;
uint32_t max_x = JpegDec.width;
uint32_t max_y = JpegDec.height;

Serial.print("Rendering image at index ");
Serial.print(imageIndex);
Serial.print(", width: ");
Serial.print(max_x);
Serial.print(", height: ");
Serial.println(max_y);

// Calculate the vertical and horizontal offsets
int16_t x_offset = 0;
int16_t y_offset = 0;

if (imageIndex == 0) {
    // First image positioned at 1/4 of TFT width
    x_offset = tft.width() / 4 - max_x / 2;
} else if (imageIndex == 1) {
    // Second image positioned at 3/4 of TFT width
    x_offset = 3 * tft.width() / 4 - max_x / 2;
} else {
    // Center third image at the bottom
    x_offset = (tft.width() - max_x) / 2;
    y_offset = tft.height() - max_y;
}

bool swapBytes = tft.getSwapBytes();
tft.setSwapBytes(true);

while (JpegDec.read()) {
    pImg = JpegDec.pImage;

    int mcu_x = JpegDec.MCUx * mcu_w;
    int mcu_y = JpegDec.MCUy * mcu_h;

    uint32_t win_w = (mcu_x + mcu_w <= max_x) ? mcu_w : (max_x % mcu_w);
    uint32_t win_h = (mcu_y + mcu_h <= max_y) ? mcu_h : (max_y % mcu_h);

    if ((mcu_x + win_w) <= tft.width() && (mcu_y + win_h) <= tft.height()) {
    tft.pushImage(mcu_x + x_offset, mcu_y + y_offset, win_w, win_h, pImg);
    } else if ((mcu_y + win_h) >= tft.height()) {
    JpegDec.abort();
    }
}

tft.setSwapBytes(swapBytes);
}
#include <FS.h>
#include <SPI.h>
#include <SD.h>
#include <TFT_eSPI.h>
#include <JPEGDecoder.h>

// Image paths and corresponding values
const char *images1[] = {"/05_smiley.jpg", "/04_smiley.jpg", "/03_smiley.jpg", "/02_smiley.jpg", "/01_smiley.jpg"};
const int values1[] = {5, 4, 3, 2, 1};

const char *images2[] = {"/50_smiley.jpg", "/40_smiley.jpg", "/30_smiley.jpg", "/20_smiley.jpg", "/10_smiley.jpg"};
const int values2[] = {50, 40, 30, 20, 10};

struct ImageData {
const char *src;
int value;
};

ImageData allImages[] = {
    {"/smiley_mix11.jpg", 11}, {"/smiley_mix12.jpg", 12}, {"/smiley_mix13.jpg", 13},
    {"/smiley_mix14.jpg", 14}, {"/smiley_mix15.jpg", 15}, {"/smiley_mix21.jpg", 21},
    {"/smiley_mix22.jpg", 22}, {"/smiley_mix23.jpg", 23}, {"/smiley_mix24.jpg", 24},
    {"/smiley_mix25.jpg", 25}, {"/smiley_mix31.jpg", 31}, {"/smiley_mix32.jpg", 32},
    {"/smiley_mix33.jpg", 33}, {"/smiley_mix34.jpg", 34}, {"/smiley_mix35.jpg", 35},
    {"/smiley_mix41.jpg", 41}, {"/smiley_mix42.jpg", 42}, {"/smiley_mix43.jpg", 43},
    {"/smiley_mix44.jpg", 44}, {"/smiley_mix45.jpg", 45}, {"/smiley_mix51.jpg", 51},
    {"/smiley_mix52.jpg", 52}, {"/smiley_mix53.jpg", 53}, {"/smiley_mix54.jpg", 54},
    {"/smiley_mix55.jpg", 55},
};

int currentIndex1 = 2;
int currentIndex2 = 2;

const int numImages1 = sizeof(images1) / sizeof(images1[0]);
const int numImages2 = sizeof(images2) / sizeof(images2[0]);
const int numAllImages = sizeof(allImages) / sizeof(allImages[0]);

// TFT and SD
TFT_eSPI tft = TFT_eSPI();

void setup() {
Serial.begin(115200);
delay(1000);

// Initialize TFT
tft.begin();
tft.setRotation(2);  // Adjust as needed
tft.fillScreen(TFT_WHITE);

// Initialize SD card
if (!SD.begin()) {
    Serial.println("SD card initialization failed!");
    return;
}
Serial.println("SD card initialized.");

// Display images based on currentIndex
displayJpeg(images1[currentIndex1], 0);
displayJpeg(images2[currentIndex2], 1);

for (int i = 0; i < numAllImages; i++) {
    displayJpeg(allImages[i].src, 2);
}
}

void loop() {
// Main loop code (if any)
}

void displayJpeg(const char *filename, int imageIndex) {
// Open the file
File jpegFile = SD.open(filename, FILE_READ);
if (!jpegFile) {
    Serial.print("Failed to open file ");
    Serial.println(filename);
    return;
}

// Decode the JPEG file
if (JpegDec.decodeSdFile(jpegFile)) {
    jpegRender(imageIndex);
} else {
    Serial.print("JPEG file format not supported for ");
    Serial.println(filename);
}

// Close the file
jpegFile.close();
}

void jpegRender(int imageIndex) {
uint16_t *pImg;
uint16_t mcu_w = JpegDec.MCUWidth;
uint16_t mcu_h = JpegDec.MCUHeight;
uint32_t max_x = JpegDec.width;
uint32_t max_y = JpegDec.height;

// Calculate the vertical and horizontal offsets
int16_t x_offset = 0;
int16_t y_offset = 0;

if (imageIndex == 0) {
    // First image positioned at 1/4 of TFT width
    x_offset = tft.width() / 4 - max_x / 2;
} else if (imageIndex == 1) {
    // Second image positioned at 3/4 of TFT width
    x_offset = 3 * tft.width() / 4 - max_x / 2;
} else {
    // Center third image at the bottom
    x_offset = (tft.width() - max_x) / 2;
    y_offset = tft.height() - max_y;
}

bool swapBytes = tft.getSwapBytes();
tft.setSwapBytes(true);

while (JpegDec.read()) {
    pImg = JpegDec.pImage;

    int mcu_x = JpegDec.MCUx * mcu_w;
    int mcu_y = JpegDec.MCUy * mcu_h;

    uint32_t win_w = (mcu_x + mcu_w <= max_x) ? mcu_w : (max_x % mcu_w);
    uint32_t win_h = (mcu_y + mcu_h <= max_y) ? mcu_h : (max_y % mcu_h);

    if ((mcu_x + win_w) <= tft.width() && (mcu_y + win_h) <= tft.height()) {
    tft.pushImage(mcu_x + x_offset, mcu_y + y_offset, win_w, win_h, pImg);
    } else if ((mcu_y + win_h) >= tft.height()) {
    JpegDec.abort();
    }
}

tft.setSwapBytes(swapBytes);
}
#include <FS.h>
#include <SPI.h>
#include <SD.h>
#include <TFT_eSPI.h>
#include <JPEGDecoder.h>
#include <XPT2046_Touchscreen.h>

// Image paths and corresponding values
const char *images1[] = {"/images/05_smiley.jpg", "/images/04_smiley.jpg", "/images/03_smiley.jpg", "/images/02_smiley.jpg", "/images/01_smiley.jpg"};
const int values1[] = {5, 4, 3, 2, 1};

const char *images2[] = {"/images/50_smiley.jpg", "/images/40_smiley.jpg", "/images/30_smiley.jpg", "/images/20_smiley.jpg", "/images/10_smiley.jpg"};
const int values2[] = {50, 40, 30, 20, 10};

struct ImageData {
const char *src;
int value;
};

ImageData allImages[] = {
    {"/images/smiley_mix11.jpg", 11}, {"/images/smiley_mix12.jpg", 12}, {"/images/smiley_mix13.jpg", 13},
    {"/images/smiley_mix14.jpg", 14}, {"/images/smiley_mix15.jpg", 15}, {"/images/smiley_mix21.jpg", 21},
    {"/images/smiley_mix22.jpg", 22}, {"/images/smiley_mix23.jpg", 23}, {"/images/smiley_mix24.jpg", 24},
    {"/images/smiley_mix25.jpg", 25}, {"/images/smiley_mix31.jpg", 31}, {"/images/smiley_mix32.jpg", 32},
    {"/images/smiley_mix33.jpg", 33}, {"/images/smiley_mix34.jpg", 34}, {"/images/smiley_mix35.jpg", 35},
    {"/images/smiley_mix41.jpg", 41}, {"/images/smiley_mix42.jpg", 42}, {"/images/smiley_mix43.jpg", 43},
    {"/images/smiley_mix44.jpg", 44}, {"/images/smiley_mix45.jpg", 45}, {"/images/smiley_mix51.jpg", 51},
    {"/images/smiley_mix52.jpg", 52}, {"/images/smiley_mix53.jpg", 53}, {"/images/smiley_mix54.jpg", 54},
    {"/images/smiley_mix55.jpg", 55},
};

int currentIndex1 = 2;
int currentIndex2 = 2;

const int numImages1 = sizeof(images1) / sizeof(images1[0]);
const int numImages2 = sizeof(images2) / sizeof(images2[0]);
const int numAllImages = sizeof(allImages) / sizeof(allImages[0]);

// TFT and SD
TFT_eSPI tft = TFT_eSPI();

// Touchscreen pins
#define XPT2046_IRQ 36   // T_IRQ
#define XPT2046_MOSI 32  // T_DIN
#define XPT2046_MISO 39  // T_OUT
#define XPT2046_CLK 25   // T_CLK
#define XPT2046_CS 33    // T_CS

SPIClass touchscreenSPI = SPIClass(VSPI);
XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);

#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define FONT_SIZE 2

// Touchscreen coordinates: (x, y) and pressure (z)
int x, y, z;

// Print Touchscreen info about X, Y and Pressure (Z) on the Serial Monitor
void printTouchToSerial(int touchX, int touchY, int touchZ) {
Serial.print("X = ");
Serial.print(touchX);
Serial.print(" | Y = ");
Serial.print(touchY);
Serial.print(" | Pressure = ");
Serial.print(touchZ);
Serial.println();
}

// Print Touchscreen info about X, Y and Pressure (Z) on the TFT Display
void printTouchToDisplay(int touchX, int touchY, int touchZ) {
// Clear TFT screen
tft.fillScreen(TFT_WHITE);
tft.setTextColor(TFT_BLACK, TFT_WHITE);

int centerX = SCREEN_WIDTH / 2;
int textY = 80;

String tempText = "X = " + String(touchX);
tft.drawCentreString(tempText, centerX, textY, FONT_SIZE);

textY += 20;
tempText = "Y = " + String(touchY);
tft.drawCentreString(tempText, centerX, textY, FONT_SIZE);

textY += 20;
tempText = "Pressure = " + String(touchZ);
tft.drawCentreString(tempText, centerX, textY, FONT_SIZE);
}
void setup() {
Serial.begin(115200);
delay(1000);

// Initialize TFT
tft.begin();
tft.setRotation(2);  // Adjust as needed
tft.fillScreen(TFT_WHITE);
Serial.println("TFT initialized.");

// Initialize Touchscreen
touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
touchscreen.begin(touchscreenSPI);
touchscreen.setRotation(2);
Serial.println("Touchscreen initialized.");

// Initialize SD card
if (!SD.begin()) {
    Serial.println("SD card initialization failed!");
    return;
}
Serial.println("SD card initialized.");

// Display images based on currentIndex
displayJpeg(images1[currentIndex1], 0);
displayJpeg(images2[currentIndex2], 1);

int sumValue = values1[currentIndex1] + values2[currentIndex2];
const char *thirdImage = findImageByValue(sumValue);
if (thirdImage) {
    displayJpeg(thirdImage, 2);
} else {
    Serial.print("No image found with value ");
    Serial.println(sumValue);
}
Serial.println("Images displayed.");

}

void loop() {
// Checks if Touchscreen was touched, and updates the currentIndex1 and currentIndex2
if (touchscreen.tirqTouched() && touchscreen.touched()) {
    // Get Touchscreen points
    TS_Point p = touchscreen.getPoint();
    // Calibrate Touchscreen points with map function to the correct width and height
    int touchX = map(p.x, 200, 3700, 1, SCREEN_WIDTH);
    int touchY = map(p.y, 240, 3800, 1, SCREEN_HEIGHT);

    // Check if the touch is within the regions where images1 and images2 are displayed
    if (touchX < SCREEN_WIDTH / 2) {
    // Left half of the screen, increment currentIndex1
    currentIndex1 = (currentIndex1 + 1) % numImages1;
    displayJpeg(images1[currentIndex1], 0);
    } else {
    // Right half of the screen, increment currentIndex2
    currentIndex2 = (currentIndex2 + 1) % numImages2;
    displayJpeg(images2[currentIndex2], 1);
    }

    // Calculate the sum value and display the corresponding third image
    int sumValue = values1[currentIndex1] + values2[currentIndex2];
    const char *thirdImage = findImageByValue(sumValue);
    if (thirdImage) {
    displayJpeg(thirdImage, 2);
    } else {
    Serial.print("No image found with value ");
    Serial.println(sumValue);
    }

    delay(100); // debounce delay
}// Main loop code (if any)
}

const char *findImageByValue(int value) {
for (int i = 0; i < numAllImages; i++) {
    if (allImages[i].value == value) {
    return allImages[i].src;
    }
}
return nullptr;  // Return nullptr if no image with the required value is found
} 

void displayJpeg(const char *filename, int imageIndex) {
Serial.print("Opening file: ");
Serial.println(filename);

// Add a delay here if needed
delay(100); // Adjust this delay as needed

// Open the file
File jpegFile = SD.open(filename, FILE_READ);
if (!jpegFile) {
    Serial.print("Failed to open file ");
    Serial.println(filename);
    return;
}

// Decode the JPEG file
if (JpegDec.decodeSdFile(jpegFile)) {
    Serial.print("Decoding successful for ");
    Serial.println(filename);
    jpegRender(imageIndex);
} else {
    Serial.print("JPEG file format not supported for ");
    Serial.println(filename);
}

// Close the file
jpegFile.close();
}

void jpegRender(int imageIndex) {
uint16_t *pImg;
uint16_t mcu_w = JpegDec.MCUWidth;
uint16_t mcu_h = JpegDec.MCUHeight;
uint32_t max_x = JpegDec.width;
uint32_t max_y = JpegDec.height;

Serial.print("Rendering image at index ");
Serial.print(imageIndex);
Serial.print(", width: ");
Serial.print(max_x);
Serial.print(", height: ");
Serial.println(max_y);

// Calculate the vertical and horizontal offsets
int16_t x_offset = 0;
int16_t y_offset = 0;

if (imageIndex == 0) {
    // First image positioned at 1/4 of TFT width
    x_offset = tft.width() / 4 - max_x / 2;
} else if (imageIndex == 1) {
    // Second image positioned at 3/4 of TFT width
    x_offset = 3 * tft.width() / 4 - max_x / 2;
} else {
    // Center third image at the bottom
    x_offset = (tft.width() - max_x) / 2;
    y_offset = tft.height() - max_y;
}

bool swapBytes = tft.getSwapBytes();
tft.setSwapBytes(true);

while (JpegDec.read()) {
    pImg = JpegDec.pImage;

    int mcu_x = JpegDec.MCUx * mcu_w;
    int mcu_y = JpegDec.MCUy * mcu_h;

    uint32_t win_w = (mcu_x + mcu_w <= max_x) ? mcu_w : (max_x % mcu_w);
    uint32_t win_h = (mcu_y + mcu_h <= max_y) ? mcu_h : (max_y % mcu_h);

    if ((mcu_x + win_w) <= tft.width() && (mcu_y + win_h) <= tft.height()) {
    tft.pushImage(mcu_x + x_offset, mcu_y + y_offset, win_w, win_h, pImg);
    } else if ((mcu_y + win_h) >= tft.height()) {
    JpegDec.abort();
    }
}

tft.setSwapBytes(swapBytes);
}
#include <FS.h>
#include <SPI.h>
#include <SD.h>
#include <TFT_eSPI.h>
#include <JPEGDecoder.h>

// Image paths and corresponding values
const char *images1[] = {"/images/05_smiley.jpg", "/images/04_smiley.jpg", "/images/03_smiley.jpg", "/images/02_smiley.jpg", "/images/01_smiley.jpg"};
const int values1[] = {5, 4, 3, 2, 1};

const char *images2[] = {"/images/50_smiley.jpg", "/images/40_smiley.jpg", "/images/30_smiley.jpg", "/images/20_smiley.jpg", "/images/10_smiley.jpg"};
const int values2[] = {50, 40, 30, 20, 10};

struct ImageData {
const char *src;
int value;
};

ImageData allImages[] = {
    {"/images/smiley_mix11.jpg", 11}, {"/images/smiley_mix12.jpg", 12}, {"/images/smiley_mix13.jpg", 13},
    {"/images/smiley_mix14.jpg", 14}, {"/images/smiley_mix15.jpg", 15}, {"/images/smiley_mix21.jpg", 21},
    {"/images/smiley_mix22.jpg", 22}, {"/images/smiley_mix23.jpg", 23}, {"/images/smiley_mix24.jpg", 24},
    {"/images/smiley_mix25.jpg", 25}, {"/images/smiley_mix31.jpg", 31}, {"/images/smiley_mix32.jpg", 32},
    {"/images/smiley_mix33.jpg", 33}, {"/images/smiley_mix34.jpg", 34}, {"/images/smiley_mix35.jpg", 35},
    {"/images/smiley_mix41.jpg", 41}, {"/images/smiley_mix42.jpg", 42}, {"/images/smiley_mix43.jpg", 43},
    {"/images/smiley_mix44.jpg", 44}, {"/images/smiley_mix45.jpg", 45}, {"/images/smiley_mix51.jpg", 51},
    {"/images/smiley_mix52.jpg", 52}, {"/images/smiley_mix53.jpg", 53}, {"/images/smiley_mix54.jpg", 54},
    {"/images/smiley_mix55.jpg", 55},
};

int currentIndex1 = 2;
int currentIndex2 = 2;

const int numImages1 = sizeof(images1) / sizeof(images1[0]);
const int numImages2 = sizeof(images2) / sizeof(images2[0]);
const int numAllImages = sizeof(allImages) / sizeof(allImages[0]);

// TFT and SD
TFT_eSPI tft = TFT_eSPI();

void setup() {
Serial.begin(115200);
delay(1000);

// Initialize TFT
tft.begin();
tft.setRotation(2);  // Adjust as needed
tft.fillScreen(TFT_WHITE);

// Initialize SD card
if (!SD.begin()) {
    Serial.println("SD card initialization failed!");
    return;
}
Serial.println("SD card initialized.");

// Display images based on currentIndex
displayJpeg(images1[currentIndex1], 0);
displayJpeg(images2[currentIndex2], 1);

int sumValue = values1[currentIndex1] + values2[currentIndex2];
const char *thirdImage = findImageByValue(sumValue);
if (thirdImage) {
    displayJpeg(thirdImage, 2);
} else {
    Serial.print("No image found with value ");
    Serial.println(sumValue);
}
}

void loop() {
// Main loop code (if any)
}

const char *findImageByValue(int value) {
for (int i = 0; i < numAllImages; i++) {
    if (allImages[i].value == value) {
    return allImages[i].src;
    }
}
return nullptr;  // Return nullptr if no image with the required value is found
}

void displayJpeg(const char *filename, int imageIndex) {
Serial.print("Opening file: ");
Serial.println(filename);

// Open the file
File jpegFile = SD.open(filename, FILE_READ);
if (!jpegFile) {
    Serial.print("Failed to open file ");
    Serial.println(filename);
    return;
}

// Decode the JPEG file
if (JpegDec.decodeSdFile(jpegFile)) {
    Serial.print("Decoding successful for ");
    Serial.println(filename);
    jpegRender(imageIndex);
} else {
    Serial.print("JPEG file format not supported for ");
    Serial.println(filename);
}

// Close the file
jpegFile.close();
}

void jpegRender(int imageIndex) {
uint16_t *pImg;
uint16_t mcu_w = JpegDec.MCUWidth;
uint16_t mcu_h = JpegDec.MCUHeight;
uint32_t max_x = JpegDec.width;
uint32_t max_y = JpegDec.height;

Serial.print("Rendering image at index ");
Serial.print(imageIndex);
Serial.print(", width: ");
Serial.print(max_x);
Serial.print(", height: ");
Serial.println(max_y);

// Calculate the vertical and horizontal offsets
int16_t x_offset = 0;
int16_t y_offset = 0;

if (imageIndex == 0) {
    // First image positioned at 1/4 of TFT width
    x_offset = tft.width() / 4 - max_x / 2;
} else if (imageIndex == 1) {
    // Second image positioned at 3/4 of TFT width
    x_offset = 3 * tft.width() / 4 - max_x / 2;
} else {
    // Center third image at the bottom
    x_offset = (tft.width() - max_x) / 2;
    y_offset = tft.height() - max_y;
}

bool swapBytes = tft.getSwapBytes();
tft.setSwapBytes(true);

while (JpegDec.read()) {
    pImg = JpegDec.pImage;

    int mcu_x = JpegDec.MCUx * mcu_w;
    int mcu_y = JpegDec.MCUy * mcu_h;

    uint32_t win_w = (mcu_x + mcu_w <= max_x) ? mcu_w : (max_x % mcu_w);
    uint32_t win_h = (mcu_y + mcu_h <= max_y) ? mcu_h : (max_y % mcu_h);

    if ((mcu_x + win_w) <= tft.width() && (mcu_y + win_h) <= tft.height()) {
    tft.pushImage(mcu_x + x_offset, mcu_y + y_offset, win_w, win_h, pImg);
    } else if ((mcu_y + win_h) >= tft.height()) {
    JpegDec.abort();
    }
}

tft.setSwapBytes(swapBytes);
}
#include <SPI.h>
#include <TFT_eSPI.h>
#include "ESP32_NOW.h"
#include "WiFi.h"
#include <esp_mac.h>  // For the MAC2STR and MACSTR macros
#include <vector>
#include <JPEGDecoder.h>
#include <SD.h>
#include <FS.h>

// Image paths and corresponding values
const char *images1[] = {"/images/05_smiley.jpg", "/images/04_smiley.jpg", "/images/03_smiley.jpg", "/images/02_smiley.jpg", "/images/01_smiley.jpg"};
const int values1[] = {5, 4, 3, 2, 1};

const char *images2[] = {"/images/50_smiley.jpg", "/images/40_smiley.jpg", "/images/30_smiley.jpg", "/images/20_smiley.jpg", "/images/10_smiley.jpg"};
const int values2[] = {50, 40, 30, 20, 10};

struct ImageData {
const char *src;
int value;
};

ImageData allImages[] = {
    {"/images/smiley_mix11.jpg", 11}, {"/images/smiley_mix12.jpg", 12}, {"/images/smiley_mix13.jpg", 13},
    {"/images/smiley_mix14.jpg", 14}, {"/images/smiley_mix15.jpg", 15}, {"/images/smiley_mix21.jpg", 21},
    {"/images/smiley_mix22.jpg", 22}, {"/images/smiley_mix23.jpg", 23}, {"/images/smiley_mix24.jpg", 24},
    {"/images/smiley_mix25.jpg", 25}, {"/images/smiley_mix31.jpg", 31}, {"/images/smiley_mix32.jpg", 32},
    {"/images/smiley_mix33.jpg", 33}, {"/images/smiley_mix34.jpg", 34}, {"/images/smiley_mix35.jpg", 35},
    {"/images/smiley_mix41.jpg", 41}, {"/images/smiley_mix42.jpg", 42}, {"/images/smiley_mix43.jpg", 43},
    {"/images/smiley_mix44.jpg", 44}, {"/images/smiley_mix45.jpg", 45}, {"/images/smiley_mix51.jpg", 51},
    {"/images/smiley_mix52.jpg", 52}, {"/images/smiley_mix53.jpg", 53}, {"/images/smiley_mix54.jpg", 54},
    {"/images/smiley_mix55.jpg", 55},
};

int currentIndex1 = 2;
int currentIndex2 = 2;
int currentIndexAll = 0; // Initialize the index to 0

const int numImages1 = sizeof(images1) / sizeof(images1[0]);
const int numImages2 = sizeof(images2) / sizeof(images2[0]);
const int numAllImages = sizeof(allImages) / sizeof(allImages[0]);

TFT_eSPI tft = TFT_eSPI();

/* Definitions */
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define FONT_SIZE 4

#define ESPNOW_WIFI_CHANNEL 6

bool updateDisplay = false; // Global flag to indicate display update
bool updateResult = false; // Global flag to indicate display update

/* Classes */

// Creating a new class that inherits from the ESP_NOW_Peer class is required.

class ESP_NOW_Peer_Class : public ESP_NOW_Peer {
public:
// Constructor of the class
ESP_NOW_Peer_Class(const uint8_t *mac_addr,
                    uint8_t channel,
                    wifi_interface_t iface,
                    const uint8_t *lmk)
    : ESP_NOW_Peer(mac_addr, channel, iface, lmk) {}

// Destructor of the class
~ESP_NOW_Peer_Class() {}

// Function to register the control peer
bool add_peer() {
    if (!add()) {
    log_e("Failed to register the broadcast peer");
    return false;
    }
    return true;
}

// Function to handle received messages from the control
void onReceive(const uint8_t *data, size_t len, bool broadcast) {
    Serial.printf("Received a message from control " MACSTR " (%s)\n", MAC2STR(addr()), broadcast ? "broadcast" : "unicast");
    Serial.printf("  Message: %.*s\n", len, data);  // Print the message as received

    // Try to extract the numeric part from the message
    String message = String((char *)data);
    int numberPart = message.substring(message.lastIndexOf(' ') + 1).toInt();

    if (message.startsWith("clockwise")) {
    if (numberPart >= 1 && numberPart <= 5) {
        currentIndex1 = constrain(numberPart - 1, 0, numImages1 - 1); // Update currentIndex1 based on rotary count
        Serial.printf("Updated currentIndex1 to %d\n", currentIndex1);
        updateDisplay = true; // Set flag to update display1 in the main loop
    } else {
        Serial.println("Invalid message format or value.");
    }
    } else if (message.startsWith("counter-clockwise")) {
    if (numberPart >= 1 && numberPart <= 5) {
        currentIndex1 = constrain(numberPart - 1, 0, numImages1 - 1); // Update currentIndex1 based on rotary count
        Serial.printf("Updated currentIndex1 to %d\n", currentIndex1);
        updateDisplay = true; // Set flag to update display1 in the main loop
    } else {
        Serial.println("Invalid message format or value.");
    }
    } else if (message.startsWith("Rockers")) {
    if (numberPart >= 1 && numberPart <= 5) {
        currentIndex2 = constrain(numberPart - 1, 0, numImages2 - 1); // Update currentIndex2 based on rotary count
        Serial.printf("Updated currentIndex2 to %d\n", currentIndex2);
        updateDisplay = true; // Set flag to updateDisplay in the main loop
    } else {
        Serial.println("Invalid message format or value.");
    } 
    } else if (message.equals("SW: pressed")) {
    int sum = values1[currentIndex1] + values2[currentIndex2];
    for (int i = 0; i < numAllImages; ++i) {
        if (allImages[i].value == sum) {
        // Update the currentIndexAll to the index of the matching image
        currentIndexAll = i;  
        Serial.printf("Updated currentIndexAll to %d\n", currentIndexAll);
        updateResult = true; // Set flag to updateResult in the main loop
        }
    }
    } else {
    Serial.println("Invalid message format or value.");
    }
}
};

/* Global Variables */

// List of all the controls. It will be populated when a new control is registered
std::vector<ESP_NOW_Peer_Class> masters;

/* Callbacks */

// Callback called when an unknown peer sends a message
void register_new_master(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) {
if (memcmp(info->des_addr, ESP_NOW.BROADCAST_ADDR, 6) == 0) {
    Serial.printf("Unknown peer " MACSTR " sent a broadcast message\n", MAC2STR(info->src_addr));
    Serial.println("Registering the peer as a controller");

    ESP_NOW_Peer_Class new_master(info->src_addr, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, NULL);

    masters.push_back(new_master);
    if (!masters.back().add_peer()) {
    Serial.println("Failed to register the new controller");
    return;
    }
} else {
    // The peripherals will only receive broadcast messages
    log_v("Received a unicast message from " MACSTR, MAC2STR(info->src_addr));
    log_v("Ignoring the message");
}
}

void setup() {
Serial.begin(115200);

    // Initialize SD card
if (!SD.begin()) {
    Serial.println("SD card initialization failed!");
    return;
}
Serial.println("SD card initialized.");

// Initialize the Wi-Fi module
WiFi.mode(WIFI_STA);
WiFi.setChannel(ESPNOW_WIFI_CHANNEL);
while (!WiFi.STA.started()) delay(100);

Serial.println("ESP-NOW Example - Broadcast Peripherals");
Serial.println("Wi-Fi parameters:");
Serial.println("  Mode: STA");
Serial.println("  MAC Address: " + WiFi.macAddress());
Serial.printf("  Channel: %d\n", ESPNOW_WIFI_CHANNEL);

// Initialize the ESP-NOW protocol
if (!ESP_NOW.begin()) {
    Serial.println("Failed to initialize ESP-NOW");
    Serial.println("Rebooting in 5 seconds...");
    delay(5000);
    ESP.restart();
}

// Register the new peer callback
ESP_NOW.onNewPeer(register_new_master, NULL);

Serial.println("Setup complete. Waiting for a control to broadcast a message...");

tft.init();          // Start the TFT display
tft.setRotation(2);  // Set the TFT display rotation in landscape mode

// Clear the screen before writing to it
tft.fillScreen(TFT_WHITE);
tft.setTextColor(TFT_BLACK, TFT_WHITE);

// Display images based on currentIndex
displayJpeg(images1[currentIndex1], 0);
displayJpeg(images2[currentIndex2], 1);


// Set X and Y coordinates for center of display
    int centerX = SCREEN_HEIGHT / 2;
    int centerY = SCREEN_WIDTH / 2;

tft.drawCentreString("SHOULD", centerX, centerY - 100, FONT_SIZE);
tft.drawCentreString("YOU", centerX, centerY - 80, FONT_SIZE);
tft.drawCentreString("TALK", centerX, centerY - 60, FONT_SIZE);
tft.drawCentreString("TO ME", centerX, centerY - 40, FONT_SIZE);
tft.drawCentreString("-O-", centerX, centerY - 20, FONT_SIZE);
tft.drawCentreString("METER", centerX, centerY, FONT_SIZE);

tft.drawCentreString("Communication is ready", centerX, centerY + 40, 2);

tft.drawCentreString("By Alexis", 200, 300, 2);
}

void loop() {
// Update display if the flag is set
if (updateDisplay) {
    tft.fillScreen(TFT_WHITE);
    tft.setTextColor(TFT_BLACK, TFT_WHITE);
    displayJpeg(images1[currentIndex1], 0);
    displayJpeg(images2[currentIndex2], 1);
    tft.drawCentreString("By Alexis", 200, 300, 2);
    updateDisplay = false; // Reset the flag
}

if (updateResult) {
    tft.fillScreen(TFT_WHITE);
    tft.setTextColor(TFT_BLACK, TFT_WHITE);
    displayJpeg(images1[currentIndex1], 0);
    displayJpeg(images2[currentIndex2], 1);
    displayJpeg(allImages[currentIndexAll].src, 2);
    tft.drawCentreString("By Alexis", 200, 300, 2);
    updateResult = false; // Reset the flag
}
}


void displayJpeg(const char *filename, int imageIndex) {
Serial.print("Opening file: ");
Serial.println(filename);

// Open the file
File jpegFile = SD.open(filename, FILE_READ);
if (!jpegFile) {
    Serial.print("Failed to open file ");
    Serial.println(filename);
    return;
}

// Decode the JPEG file
if (JpegDec.decodeSdFile(jpegFile)) {
    Serial.print("Decoding successful for ");
    Serial.println(filename);
    jpegRender(imageIndex);
} else {
    Serial.print("JPEG file format not supported for ");
    Serial.println(filename);
}

// Close the file
jpegFile.close();
}

void jpegRender(int imageIndex) {
uint16_t *pImg;
uint16_t mcu_w = JpegDec.MCUWidth;
uint16_t mcu_h = JpegDec.MCUHeight;
uint32_t max_x = JpegDec.width;
uint32_t max_y = JpegDec.height;

Serial.print("Rendering image at index ");
Serial.print(imageIndex);
Serial.print(", width: ");
Serial.print(max_x);
Serial.print(", height: ");
Serial.println(max_y);

// Calculate the vertical and horizontal offsets
int16_t x_offset = 0;
int16_t y_offset = 0;

if (imageIndex == 0) {
    // First image positioned at 1/4 of TFT width
    x_offset = tft.width() / 4 - max_x / 2;
} else if (imageIndex == 1) {
    // Second image positioned at 3/4 of TFT width
    x_offset = 3 * tft.width() / 4 - max_x / 2;
} else {
    // Center third image at the bottom
    x_offset = (tft.width() - max_x) / 2;
    y_offset = tft.height() - max_y;
}

bool swapBytes = tft.getSwapBytes();
tft.setSwapBytes(true);

while (JpegDec.read()) {
    pImg = JpegDec.pImage;

    int mcu_x = JpegDec.MCUx * mcu_w;
    int mcu_y = JpegDec.MCUy * mcu_h;

    uint32_t win_w = (mcu_x + mcu_w <= max_x) ? mcu_w : (max_x % mcu_w);
    uint32_t win_h = (mcu_y + mcu_h <= max_y) ? mcu_h : (max_y % mcu_h);

    if ((mcu_x + win_w) <= tft.width() && (mcu_y + win_h) <= tft.height()) {
    tft.pushImage(mcu_x + x_offset, mcu_y + y_offset, win_w, win_h, pImg);
    } else if ((mcu_y + win_h) >= tft.height()) {
    JpegDec.abort();
    }
}

tft.setSwapBytes(swapBytes);
}

As it is, my final project worked well, some improvements could be added such as text on the screen to help understanding. But The most urgent issue was to improve the integration of this screen.

Screen integration

As I was working on the code, I started printing a case for screen. I found a model on printables that I modified with Fusion360 to add loops for attaching a strap.

To power the screen, I bought a powerbank and modelised an holder to fix it to the screen case.

case for the screen in Fusion360 holder for the case and the battery in Fusion360

case, battery and holder in Fusion360

I made several print of the holder but none of them was really good. I stayed with the first one I mistakenly printed without support. one completely broke when I removed the supports and the other missed collapsed during printing. I had to reinforce the first one with tape because due to lacke of support it was a bit too tight for the screen and the battery and cracked a bit.

broken holder holder for the case and the battery

Sewing

I used a scrap of fabric to sew a strap. I added PETG-printed buckles to tighten it.

sewing the strap strap on the screen buckles modelisation in Fusion360 PETG-printed buckles on the strap

screen straped around arm

Programmed schedule from Midterm :

Week 12 - Molding and casting Create Molding from 3D scan of handshaped model of the fidget for more organic shape. Have to define how every input assembly with it.

Week 13 - Networking and casting
Work on communication between Fidget and Display. Actually working cable, find more easy way to communicate. Wired, distant, inductive, nfc ?

Week 14 - Interface and application programming Send images on micro-controller memory - choose images from switch and selector - display images on screen

Week 15 - Wildcard week Create support for display screen

Week 16 - Apllications and implicatiosn Create support for display screen

Week 17 - Systeme integration Debugging

Week 18 -