Intermediate2–3 hours14+4 parts needed

Parent info

Cost: ~$32
Time: 2–3 hours
Age: 14+
Difficulty: ●●●
Soldering: No soldering needed
What they'll learn: Microcontroller programming

Parts you need

Affiliate links — we may earn a small commission

ESP32-S3-DevKitC-1
8×8 LED Matrix with MAX7219 (×2)
Breadboard + Jumper Wires
USB-C Data Cable
🎮

Try this circuit in your browser!

Run the code, press the buttons and watch what happens — before you buy any parts. No account needed.

Open in Simulator →

Your classmates drew a graph. Yours changes in real time.

Imagine this: your Algebra class is learning about linear functions. Everyone draws y = 2x + 1 on graph paper. Neat lines, colored pencils, done.

You pull out a device with a 16×8 LED matrix. You text it: “m=3 b=-2” — and the LED grid instantly redraws the line with the new slope and intercept. You say: “As m increases, the line gets steeper. Watch.”

You change m from 1 to 5, live, in front of the class. Every student sees the slope change in real time.

That’s what we’re building. For about $28.

Wiring diagram for Grade 9 Algebra: Function Visualizer: esp32 s3 devkitc 1 connected to matrix1, matrix2


What you’ll need

Part What it does Price
ESP32-S3-DevKitC-1 Brain — calculates the function and drives the display ~$12
8×8 MAX7219 LED matrix ×2 16×8 pixel grid for graphing functions ~$10
Breadboard + jumper wires Wires everything together ~$5
USB-C data cable Power + code upload ~$5

You also need: home WiFi, Telegram account (free).

Total: ~$28 | Time: ~2–3 hours | Difficulty: ●●●○○


How it works (60 seconds)

A MAX7219 LED matrix is an 8×8 grid of LEDs controlled by a single chip. Two chained together give you 16 columns × 8 rows = 128 pixels. That’s your coordinate system.

The ESP32 calculates y = mx + b for every x value (0–15), converts the y result to a pixel row (0–7), and turns on that LED. When you send a new m or b value via Telegram, it recalculates and redraws instantly.

Think of it as a tiny graphing calculator — but physical, and you control it by texting it.


Step 0: Understand the math

The function y = mx + b has two parts:

  • m (slope): How steep the line is. m=1 is 45°. m=2 is steeper. m=0 is flat. m=-1 goes downhill.
  • b (y-intercept): Where the line crosses the y-axis (x=0).

On a 16×8 display:

  • X axis: columns 0–15 (left to right)
  • Y axis: rows 0–7 (we’ll flip so row 0 = bottom, row 7 = top)
  • Origin (0,0) = bottom-left corner

For each column x, we calculate: row = 4 + m*x + b then map it to 0–7.


Step 1: Wire it up

Time: ~15 minutes

Two MAX7219 matrices are daisy-chained — the first one’s DOUT connects to the second one’s DIN.

ESP32 → First Matrix:

  1. DIN → GPIO 11 (C6: GPIO 22)
  2. CLK → GPIO 12 (C6: GPIO 23)
  3. CS → GPIO 10 (C6: GPIO 18)
  4. VCC → 5V
  5. GND → GND

First Matrix → Second Matrix: 6. First DOUT → Second DIN 7. Second CLK → same CLK wire (GPIO 12, C6: GPIO 23) 8. Second CS → same CS wire (GPIO 10, C6: GPIO 18) 9. Second VCC → 5V 10. Second GND → GND

Good to know: The simulator has no MAX7219 part, so the wiring picture uses a plain LED matrix that only has DIN, VCC and GND. Wire CLK and CS as listed above anyway — the real module needs them.

Check: The two matrices should be physically side-by-side so they form a 16-wide display. First matrix = left half (x 0–7), second = right half (x 8–15).


Step 2: Flash the code

Time: ~20 minutes

Install these libraries in Arduino IDE Library Manager:

  • MD_MAX72XX (by MajicDesigns)
  • UniversalTelegramBot (by Brian Lough)
  • ArduinoJson (by Benoit Blanchon)

The big picture first. This program is a live graphing calculator made of LEDs:

  • The ESP32 does math: for every x position on the display, it calculates y = mx + b and lights up the LED at that position.
  • Telegram is used as a remote control: you text “m=2 b=1” and the ESP32 redraws the line instantly.
  • The display is a 16×8 grid — 16 columns (x) and 8 rows (y).

A program is like a recipe. The computer reads it top to bottom and does exactly what is written, nothing more. Copy this entire recipe into Arduino IDE and upload it:

// ========== CHOOSE YOUR BOARD ==========
// Uncomment the line for YOUR board:
#define BOARD_S3    // ESP32-S3-DevKitC-1
//#define BOARD_C6  // ESP32-C6-DevKitC-1
// ========================================

#ifdef BOARD_S3
  #define PIN_CS    10
  #define PIN_MOSI  11
  #define PIN_CLK   12
#endif
#ifdef BOARD_C6
  #define PIN_CS    18
  #define PIN_MOSI  22
  #define PIN_CLK   23
#endif

#include <WiFi.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>
#include <WiFiClientSecure.h>

#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 2

const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";
const char* BOT_TOKEN = "YOUR_TELEGRAM_BOT_TOKEN";

MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, PIN_MOSI, PIN_CLK, PIN_CS, MAX_DEVICES);
WiFiClientSecure client;
UniversalTelegramBot bot(BOT_TOKEN, client);

float slope = 1.0;
float intercept = 0.0;

void drawFunction() {
  mx.clear();
  
  for (int x = 0; x < 16; x++) {
    float y = slope * (x - 8) + intercept;
    int row = 4 + (int)round(y);
    row = constrain(row, 0, 7);
    
    int col = x;
    mx.setPoint(row, col, true);
  }
  
  for (int x = 0; x < 16; x++) {
    float y = slope * (x - 8) + intercept;
    int row = 4 + (int)round(y);
    if (row != 4) mx.setPoint(4, x, true);
  }
  
  mx.update();
}

unsigned long lastBotCheck = 0;

void setup() {
  Serial.begin(115200);
  mx.begin();
  mx.control(MD_MAX72XX::INTENSITY, 4);
  mx.clear();
  
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConnected!");
  
  client.setInsecure();
  
  drawFunction();
  Serial.println("Ready! Send 'm=2 b=1' in Telegram to change the function.");
}

void loop() {
  if (millis() - lastBotCheck > 2000) {
    lastBotCheck = millis();
    int numNewMessages = bot.getUpdates(bot.last_message_received + 1);
    
    for (int i = 0; i < numNewMessages; i++) {
      String msg = bot.messages[i].text;
      String chatId = bot.messages[i].chat_id;
      
      int mIdx = msg.indexOf("m=");
      int bIdx = msg.indexOf("b=");
      
      if (mIdx >= 0) {
        slope = msg.substring(mIdx + 2).toFloat();
      }
      if (bIdx >= 0) {
        intercept = msg.substring(bIdx + 2).toFloat();
      }
      
      drawFunction();
      
      String reply = "y = " + String(slope, 1) + "x + " + String(intercept, 1);
      reply += "\nSlope: " + String(slope, 1);
      reply += "\nIntercept: " + String(intercept, 1);
      bot.sendMessage(chatId, reply, "");
    }
  }
}

Line-by-line: what every line does and why

Lines 1–6: Borrowing ready-made instruction books

#include <WiFi.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>
#include <WiFiClientSecure.h>

#include means “grab this instruction book.” Each library handles a big job so we do not have to write it from scratch:

  • WiFi — connects the ESP32 to your home network.
  • MD_MAX72xx — controls the LED matrix chips.
  • SPI — the communication protocol the LED matrix uses (faster than I2C).
  • UniversalTelegramBot — handles all the Telegram messaging.
  • ArduinoJson — reads the JSON-formatted replies from Telegram’s servers.
  • WiFiClientSecure — makes secure (HTTPS) connections to Telegram.

Lines 8–12: Giving numbers easy names

#define PIN_CS    10
#define PIN_MOSI  11
#define PIN_CLK   12

#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 2

#define is a sticky note. FC16_HW is the specific hardware type of our LED matrix modules — the library needs to know the exact wiring layout inside. MAX_DEVICES 2 means two modules are daisy-chained. The three PIN names tell the ESP32 which legs connect to the matrix: CS (chip select, leg 10), MOSI (data stream, leg 11), CLK (clock beat, leg 12). On the C6 they are legs 18, 22 and 23.


Lines 14–16: Your login credentials

const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";
const char* BOT_TOKEN = "YOUR_TELEGRAM_BOT_TOKEN";

const char* is a text box that never changes. You must replace these three values with your own. ssid is your WiFi name, password is the WiFi password, and BOT_TOKEN is the secret key Telegram gives you for your bot.


Lines 18–21: Creating the matrix and Telegram objects

MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, PIN_MOSI, PIN_CLK, PIN_CS, MAX_DEVICES);
WiFiClientSecure client;
UniversalTelegramBot bot(BOT_TOKEN, client);

mx is the LED matrix — named so we can command it later. client is a secure internet connection. bot is our Telegram assistant — it uses the token to talk to Telegram’s servers and the client to do it securely.


Lines 23–24: Variables for m and b

float slope = 1.0;
float intercept = 0.0;

float means a number with a decimal point. slope stores the current m value. intercept stores the current b value. They start at 1 and 0 — which draws a 45° diagonal line (y = x).


Lines 26–48: drawFunction() — the heart of the project

void drawFunction() {
  mx.clear();

mx.clear() wipes all 128 LEDs dark — like erasing a whiteboard before drawing a new line.

  for (int x = 0; x < 16; x++) {
    float y = slope * (x - 8) + intercept;
    int row = 4 + (int)round(y);
    row = constrain(row, 0, 7);
    mx.setPoint(row, col, true);
  }

This for loop runs 16 times — once for each column. For each x value it:

  1. Calculates y using y = m(x−8) + b. The x - 8 shifts the origin to the center of the display (column 8) instead of the left edge.
  2. Converts y into a pixel row: row = 4 + y. Row 4 is the middle of the 8-row display. Positive y goes up from center (row numbers decrease), negative y goes down.
  3. constrain(row, 0, 7) clamps the row to valid screen range — like telling a number “you cannot go below 0 or above 7.”
  4. mx.setPoint(row, col, true) lights up that specific LED — row is the height, col is the column.
  for (int x = 0; x < 16; x++) {
    float y = slope * (x - 8) + intercept;
    int row = 4 + (int)round(y);
    if (row != 4) mx.setPoint(4, x, true);
  }

This second loop draws the x-axis (row 4, the center row). It only draws an axis dot if the function line is not already there — otherwise the whole center row would light up and obscure the line.

  mx.update();
}

mx.update() actually sends all the changes to the physical LEDs. Without this, nothing would appear. Think of it like pressing Print after you finish editing a document.


Lines 50–70: setup() — runs once on power-on

mx.begin();
mx.control(MD_MAX72XX::INTENSITY, 4);
mx.clear();

mx.begin() starts the LED matrix. mx.control(INTENSITY, 4) sets brightness to 4 out of 15 — medium brightness. mx.clear() starts with a blank display.

WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
}

Connect to WiFi. while means “keep doing this as long as it is true.” WiFi.status() != WL_CONNECTED means “WiFi is not yet connected.” So the loop keeps waiting, printing a dot every half second, until the connection succeeds.

client.setInsecure();

This tells the secure connection to skip certificate checking — a shortcut that works fine for a school project but would not be acceptable in a bank app. It is needed because storing Telegram’s security certificate on the ESP32 is complicated.

drawFunction();

Draw the starting line (y = x) as soon as WiFi connects.


Lines 72–97: loop() — checks Telegram every 2 seconds

if (millis() - lastBotCheck > 2000) {
  lastBotCheck = millis();
  int numNewMessages = bot.getUpdates(bot.last_message_received + 1);

millis() is a stopwatch. millis() - lastBotCheck is how long since the last check. If more than 2000 milliseconds (2 seconds) have passed, check Telegram for new messages. bot.getUpdates(...) asks Telegram’s servers: “Any new messages since the last one I read?”

  for (int i = 0; i < numNewMessages; i++) {
    String msg = bot.messages[i].text;
    int mIdx = msg.indexOf("m=");
    int bIdx = msg.indexOf("b=");
    if (mIdx >= 0) slope = msg.substring(mIdx + 2).toFloat();
    if (bIdx >= 0) intercept = msg.substring(bIdx + 2).toFloat();

Loop through each new message. msg.indexOf("m=") searches for the text “m=” and returns its position — or -1 if not found. msg.substring(mIdx + 2) cuts the text starting right after “m=”. .toFloat() converts that text (“2.5”) to a number (2.5). This parses a message like “m=2 b=1” and extracts the two numbers.

    drawFunction();
    bot.sendMessage(chatId, reply, "");

Redraw the line with the new values. Send back a confirmation message to Telegram so you know it worked.


The whole thing in one sentence

On power-on, connect to WiFi and draw y = x on the LED matrix. Then every 2 seconds, check Telegram for new m and b values, update the variables, and redraw the line.

First thing to try: Send your bot the message m=0 b=0. The line should become completely flat (horizontal) — zero slope. Then send m=3 b=0 and watch the line become very steep. Then send m=1 b=-3 and watch the line shift down.

Check: After uploading, the matrix should show a diagonal line (y=x, slope=1, intercept=0). If it’s blank, check SPI connections. If it shows garbage, make sure you set HARDWARE_TYPE to FC16_HW.


Step 3: Set up Telegram bot

Time: ~5 minutes

  1. Open Telegram, search @BotFather
  2. Send /newbot — name it “FunctionBot” or similar
  3. BotFather gives you a long token — copy it
  4. Paste it into BOT_TOKEN in the code and re-upload

Test: Send your bot: m=2 b=0
The matrix should redraw with a steeper line. Bot replies: “y = 2.0x + 0.0”


Step 4: Present it!

Commands for your presentation:

Send this What it shows
m=0 b=0 Flat line (zero slope — horizontal)
m=1 b=0 45° diagonal
m=3 b=0 Steep positive slope
m=-1 b=0 Downward slope
m=1 b=3 Shift up 3 units
m=1 b=-3 Shift down 3 units

Presentation tip: Walk your audience through slope changes first (keep b=0, change m from -2 to 2 in steps). Say: “Notice the line rotates around the origin as slope changes — this is why slope is called the ‘rate of change.’” Then change b to show translation: “The b value slides the whole line up or down without rotating it.”


What just happened

You implemented coordinate mapping — converting mathematical coordinates (x, y can be any real number) to physical pixels (0–15, 0–7). The map() and constrain() functions scaled the math-space to screen-space.

You also used SPI (Serial Peripheral Interface) — a fast communication protocol that controls the MAX7219. SPI uses 4 wires and can run at 10 million bits per second, which is why the display updates instantly.

Curriculum connections:

  • Common Core Math 8.F.A.3: Interpret the equation y = mx + b as defining a linear function
  • Common Core Math HSF.IF.C.7: Graph functions expressed symbolically and show key features
  • Common Core Math HSF.BF.B.3: Identify the effect on the graph of replacing f(x) by f(x+k)

You didn’t just draw a line — you built a function machine. Input m and b, output a graph. That’s what graphing calculators do, except yours fits in a backpack and costs $28.


Level Up

Quadratic functions: Change the formula to y = a*x*x + b*x + c and you can visualize parabolas. Send a=1 b=0 c=0 to see y=x².

Two functions at once: Use different brightness levels (MAX7219 supports 16 levels) to show two functions simultaneously — visualize intersections.

Absolute value: Add a mode that graphs y = |mx + b|, showing the V-shape of absolute value functions.


Troubleshooting

Problem Fix
Matrix shows nothing Check 5V power (not 3.3V). Check CS on GPIO 10 (C6: GPIO 18). Try mx.clear(); mx.setPoint(0,0,true); mx.update(); to test.
Only half the matrix works Check DOUT of matrix 1 goes to DIN of matrix 2. Same CLK and CS for both.
Telegram bot never responds Double-check BOT_TOKEN. WiFi must be connected (check Serial Monitor). Add Serial.println(WiFi.status()) to debug.
Line looks wrong/upside down Flip the row calculation: change row = 4 + (int)round(y) to row = 4 - (int)round(y)
Upload fails Hold BOOT button while clicking Upload in Arduino IDE.
Affiliate disclosure: Some links on this page are affiliate links. If you buy through them, we may earn a small commission at no extra cost to you.