Wiznet makers

Nekhil

Published August 20, 2023 © 3-Clause BSD License(BSD-3-Clause)

0 UCC

0 VAR

1 Contests

0 Followers

0 Following

OrderEZ: Self-Service Ordering System for cafes

Empowering customers to scan QR codes at their tables and seamlessly place orders.

COMPONENTS
PROJECT DESCRIPTION
 
Hardware 
1 x W5300 TOE Shield
1 x Nucleo 144 F29ZI
1 X DF player mini
1 X 3ohm 4watt speaker
1 x micro sd card
Software
Arduino IDE
Corel Draw
 
OrderEZ: Self-Service Ordering System
 
Are you ready to revolutionize the way cafes operate and enhance customer satisfaction? Introducing the OrderEZ, a groundbreaking self-service ordering system that empowers cafe-goers and streamlines operations. Say goodbye to waiting in lines and searching for a waiter – our innovative solution puts the ordering process in the hands of your customers.
 
The Problem: Traditional cafes often face challenges with order accuracy, long waiting times, and dependency on waitstaff for order taking. Customers are looking for quicker, more efficient ways to place their orders and enjoy their meals.
 
The Solution: The OrderEZ addresses these pain points with a cutting-edge approach. Through strategically placed QR codes on cafe tables, customers gain access to a user-friendly online ordering platform. They can browse the menu, select items, and submit orders effortlessly, all from the comfort of their table.
 
Video
Key Features:
  • QR Code Magic: Customers scan QR codes to initiate the ordering process, enabling a seamless connection between tables and the ordering platform.
  • User-Friendly Interface: An intuitive web interface allows customers to explore the menu, customize their orders, and complete transactions without hassle.
  • Real-Time Updates: Admins receive instant order notifications and updates, ensuring efficient order processing.
  • Audio Alerts: Our integrated audio notification system uses STM32 Nucleo and DF Player Mini to immediately alert administrators of new orders.
  • Lightning-Fast Response:  The system's lightning-fast response ensures that customers' orders are received and processed almost instantly.
  • Handling Multiple tables: The device can handle multiple tables simultaneously, thanks to 8 independent hardware sockets of W5300.
How It Works:
 
QR Code Interaction: Each café table features a QR code with essential details – the Ethernet server's IP address and a table ID. When customers scan the QR code using their phones, their browsers send a request to the server, including the table ID as a parameter.
Ordering Form: The server responds with a form that displays the menu items that are available. This form is designed to be user-friendly and easy to navigate. The table ID, hidden in the form, keeps track of where the order originates.
Placing an Order: Customers select their desired items from the menu and submit the form. The server receives a POST request containing the chosen items and the table ID. This information is then processed by the STM32 Nucleo microcontroller, which manages the orders.
Order Confirmation: The server promptly confirms the order's successful submission, providing peace of mind to customers. Additionally, the STM32 Nucleo produces a pleasant bell sound, giving a tangible confirmation of the order.
 
 
Administrative Management:
 
Admin Portal: Café administrators access a dedicated portal through the web interface itself. This portal acts as the control centre for managing orders. Upon requesting this portal, the server displays an organized list of active orders.
Efficient Order Handling: When an order is fulfilled and served to the customer, administrators can easily remove it from the list with a simple double-tap action. This straightforward process optimizes order management and ensures smooth operations.
 
System Architecture
 
The OrderEZ system is meticulously designed with an emphasis on technical efficiency and reliability:
  • Cafe Tables with QR Codes: QR codes initiate the ordering process for customers at different tables. QR contains server address along with unique table id.
  • STM32 Nucleo + W5300 TOE Shield
This project leverages the power of the W5300 TOE Shield, which brings advanced TCP/IP Offload Engine technology to the table. With 8 independent hardware sockets, the shield handles multiple customer connections concurrently, ensuring rapid order placement. This efficient offloading enhances the entire system's performance, allowing the STM32 Nucleo microcontroller to focus on delivering a seamless user experience. So let me explain this thing.
 

*) TCP/IP Offload Engine (TOE)

The integrated TOE offloads network protocol processing from the microcontroller, resulting in enhanced system performance and responsiveness. The microcontroller can focus on application-specific logic, while the TOE handles low-level networking tasks.
 

*) Independent Hardware Sockets

With 8 independent hardware sockets, the shield facilitates simultaneous communication with multiple clients (tables). Each socket corresponds to a table, ensuring that multiple customers can place orders concurrently without performance degradation.

*) Efficient Socket Multiplexing

The W5300 TOE Shield efficiently employs socket multiplexing, enabling the microcontroller to manage multiple connections concurrently. This feature optimizes resource utilization and enables seamless communication with multiple tables.

*) Dedicated IP Addresses

Each hardware socket can be assigned a unique IP address. This design mirrors the self-service cafe model, where each table possesses its distinct IP address. Direct communication between tables and the ordering system is thus efficient and rapid.

*) Reduced Latency

The hardware TCP/IP stack and socket architecture contribute to reduced latency. Customer orders are processed promptly and communicated to the admin portal in real time, ensuring a seamless ordering experience.

*) Reliable Data Communication

The hardware TCP/IP stack manages packet segmentation, reassembly, error checking, and retransmissions. This robust data management ensures reliable transmission of customer orders to the server, minimizing data loss.

*) Efficient Resource Management

The W5300 TOE Shield's independent sockets and offloading capabilities optimize resource management. The microcontroller is freed from low-level networking tasks, focusing on higher-level application logic and enhancing responsiveness.

*) Scalability and Flexibility

The shield's independent sockets enable a scalable system architecture. As the cafe expands, additional tables can be accommodated with new sockets and unique IP addresses, ensuring an efficient ordering process.

*) Integration with STM32 Nucleo

The seamless integration of the W5300 TOE Shield with STM32 Nucleo simplifies setup and communication, enhancing compatibility and streamlining communication between components.

*) Real-time Order Updates

Real-time capabilities enable instant updates to the admin portal as customers place orders. Administrators gain immediate visibility into incoming orders, enhancing order management
 
  • HTTP Web Server 
 
An ethernet server is created on port 80 which awaits client connections. The server responds to client connections in 4 ways:
 
1.  When a customer scans the QR code it will initiate a GET request with table ID as a parameter, to the server IP (let's assume it to be 127.0.0.1), and then the GET request will be in a similar format
GET /?table_id=1 HTTP/1.1
Host: 127.0.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Now the server sends the menu form back to the user
 
2.  When a customer submits the form, a POST request to the server at path "127.0.0.1/order" with the ordered items along with table_id as post parameters. It is converted as JSON with table_id as the key and stored inside an array within STM32F429ZI.
POST /order HTTP/1.1
Host: 127.0.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36

cartItems=Coffee%3A1%2CBurger%3A1%3Ftable_id%3D1%3B
Now the server sends the order confirmation back to the user.
 
3. When an admin visits the admin portal at "127.0.0.1/admin", the GET request has the form
GET /admin HTTP/1.1
Host: 127.0.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Now the server responds with a list containing all the orders.
 
4. When the admin double taps on the order a GET request is sent to "127.0.0.1/delete" using AJAX with table ID as the parameter.
POST /delete HTTP/1.1
Host: 127.0.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36

table_id=1
Now the server deletes the order from the array where the order details are stored corresponding to the table_id.
 
  • Audio Notification System
 
In the context of this project, the STM32 Nucleo, DF Player Mini, and the speaker together create a responsive and effective audio alert system. The STM32 Nucleo handles the communication with the DF Player Mini and controls the speaker to generate audible alerts, ensuring that cafe administrators are promptly informed of new customer orders. This adds an extra layer of convenience and efficiency to the overall self-service ordering experience.
 
The DF Player Mini is a compact and versatile audio player module designed to simplify the process of adding sound effects or audio playback to electronic projects. It supports various audio formats and comes with onboard storage (typically via a microSD card). The DF Player Mini can be controlled using serial communication (UART), making it a convenient choice for projects like the audio alert system. It's capable of interfacing with the STM32 Nucleo to play predefined audio clips or sound effects. 
The speaker is the auditory output component of the audio alert system. It's responsible for converting electrical signals from the DF Player Mini, which represents audio data, into audible sound. Depending on the design of the system, the speaker could range from a small piezo buzzer to a larger, higher-quality audio speaker. When triggered by the STM32 Nucleo in response to an incoming order, the speaker emits a distinctive audio alert, notifying cafe administrators of new orders in real-time.
Circuit
 
OrderEZ device
 
The below photos show the progress of the device making.
 
Firmware

The firmware is written in Arduino IDE. It consists of two parts first one is the main Arduino code and the second one is the source. h in which all the website-related content is included here. The entire code is in the Github repo.

The webpage is developed with HTML, CSS and JS to dynamically handle all user interactions. The different files are attached here: 

  1. Home Page
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Smart Cafe</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
  <script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
  <script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
</head>
<body>
  <div class="wrapper">
    <div class="screen -left" id="shopItems">
        <div class="cart-icon" onclick="showCart()">
            <ion-icon name="cart" class="icon"></ion-icon>
        </div>
      <div class="app-bar">
      </div>
      <div class="title">Our items</div>
      <div class="shop-items">
        <div class="item"><div class="item-block"><div class="image-area" style="background-color: rgb(225, 231, 237);"><img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1315882/air-zoom-pegasus-36-mens-running-shoe-wide-D24Mcz-removebg-preview.png" class="image"></div><div class="name">Nike Air Zoom Pegasus 36</div><div class="description">The iconic Nike Air Zoom Pegasus 36 offers more cooling and mesh that targets breathability across high-heat areas. A slimmer heel collar and tongue reduce bulk, while exposed cables give you a snug fit at higher speeds.</div><div class="bottom-area"><div class="price">$108.97</div><div class="button" onclick="app.addToCart(item)"><p>ADD TO CART</p></div></div></div></div>
      </div>
    </div>
    <div class="screen -right" id="cartItems">
        <div class="cart-icon" onclick="hideCart()">
            <ion-icon name="cart" class="icon"></ion-icon>
        </div>
      <div class="app-bar">
      </div>
      <div class="title">Your cart</div>
      <div class="no-content" v-if="cartItems.length === 0">
      </div>
      <div class="cart-items">
      </div>
      <div id="checkoutButtonContainer">
      </div>
      <form id="checkoutForm" action="http://192.168.1.7/order.php" method="POST">
        <input type="hidden" name="cartItems" id="cartItemsInput">
        
    </form>
    </div>
  </div>
<style>
    body {
  font-family: "Rubik", sans-serif;
  color: #303841;
  overflow: hidden;
}
.wrapper {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: relative;
  flex-wrap: wrap;
  padding: 40px 20px;
  max-width: 720px;
  margin: 0 auto;
}
.wrapper::before {
  content: "";
  display: block;
  position: fixed;
  width: 300%;
  height: 100%;
  top: 50%;
  left: 50%;
  border-radius: 100%;
  transform: translateX(-50%) skewY(-8deg);
  background-color: #f6c90e;
  z-index: -1;
  animation: wave 8s ease-in-out infinite alternate;
}
@keyframes wave {
  0% {
    transform: translateX(-50%) skew(0deg, -8deg);
  }
  100% {
    transform: translateX(-30%) skew(8deg, -4deg);
  }
}

@media(max-width:700px) {
.screen {
    background-color: #fff;
    box-sizing: border-box;
    width: 340px;
    height: 600px;
    box-shadow: 0 3.2px 2.2px rgba(0, 0, 0, 0.02), 0 7px 5.4px rgba(0, 0, 0, 0.028), 0 12.1px 10.1px rgba(0, 0, 0, 0.035), 0 19.8px 18.1px rgba(0, 0, 0, 0.042), 0 34.7px 33.8px rgba(0, 0, 0, 0.05), 0 81px 81px rgba(0, 0, 0, 0.07);
    border-radius: 30px;
    overflow-y: scroll;
    padding: 0 28px;
    position: relative;
    margin-bottom: 20px;
    margin-left: 10%;
    }
.cart-icon{
    width: 3.5rem;
    height: 2.9rem;
    position: fixed;
    margin-left: 12rem;
    margin-top: -2.9rem;
    z-index: 0;
    background-color: #f6c90e;
    border-radius: 0.4rem 0.4rem 0 0;
    cursor: pointer;
}
.cart-icon .icon{
    width: 2rem;
    height: 2rem;
    margin-left: 0.5rem;
    margin-top: 0.3rem;
    cursor: pointer;
    z-index: 0;
    
}
#cartItems{
    display: none;
}
}
@media (min-width: 781px) {
    .screen {
    background-color: #fff;
    box-sizing: border-box;
    width: 340px;
    height: 600px;
    box-shadow: 0 3.2px 2.2px rgba(0, 0, 0, 0.02), 0 7px 5.4px rgba(0, 0, 0, 0.028), 0 12.1px 10.1px rgba(0, 0, 0, 0.035), 0 19.8px 18.1px rgba(0, 0, 0, 0.042), 0 34.7px 33.8px rgba(0, 0, 0, 0.05), 0 81px 81px rgba(0, 0, 0, 0.07);
    border-radius: 30px;
    overflow-y: scroll;
    padding: 0 28px;
    position: relative;
    margin-bottom: 20px;
    }
    .cart-icon{
        display: none;
    }
}

.screen::before {
  content: "";
  display: block;
  position: absolute;
  width: 300px;
  height: 300px;
  border-radius: 100%;
  background-color: #f6c90e;
  top: -20%;
  left: -50%;
  z-index: 0;
}
.screen::-webkit-scrollbar {
  display: none;
}
.screen > .title {
  font-size: 24px;
  font-weight: bold;
  margin: 20px 0;
  position: relative;
}
.app-bar {
  padding: 12px 0;
  position: relative;
}
.app-bar > .logo {
  display: block;
  width: 50px;
}
.shop-items {
  position: relative;
}
.item-block {
  padding: 40px 0 70px;
}
.item-block:first-child {
  padding-top: 0;
}
.item-block > .image-area {
  border-radius: 30px;
  height: 380px;
  display: flex;
  align-items: center;
  overflow: hidden;
}
.item-block > .image-area > .image {
  display: block;
  width: 100%;
  filter: drop-shadow(0 30px 20px rgba(0, 0, 0, 0.2));
}
.item-block > .name {
  font-size: 20px;
  font-weight: bold;
  margin: 26px 0 20px;
  line-height: 1.5;
}
.item-block > .description {
  font-size: 13px;
  color: #777;
  line-height: 1.8;
  margin-bottom: 20px;
}
.item-block > .bottom-area {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.item-block > .bottom-area > .price {
  font-size: 18px;
  font-weight: bold;
}
.item-block > .bottom-area > .button {
  cursor: pointer;
  background-color: #f6c90e;
  font-weight: bold;
  font-size: 14px;
  box-sizing: border-box;
  height: 46px;
  padding: 16px 20px;
  border-radius: 100px;
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
  transition: box-shadow 0.4s, background-color 0.2s;
  user-select: none;
  white-space: nowrap;
  position: relative;
  display: flex;
  align-items: center;
  overflow: hidden;
}
.item-block > .bottom-area > .button:hover {
  background-color: #f8d43f;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.item-block > .bottom-area > .button.-active {
  pointer-events: none;
  cursor: default;
}
.item-block > .bottom-area > .button > .cover {
  width: 16px;
  height: 16px;
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
}
.item-block > .bottom-area > .button > .cover > .check {
  width: 100%;
  height: 100%;
  transform: translate(-100%, -73%) rotate(-45deg);
  position: absolute;
  left: 50%;
  top: 50%;
}
.item-block > .bottom-area > .button > .cover > .check::before, .item-block > .bottom-area > .button > .cover > .check::after {
  content: "";
  display: block;
  background-color: #303841;
  position: absolute;
  left: 0;
  bottom: 0;
  border-radius: 10px;
}
.item-block > .bottom-area > .button > .cover > .check::before {
  width: 3px;
  height: 50%;
}
.item-block > .bottom-area > .button > .cover > .check::after {
  width: 100%;
  height: 3px;
}
.cart-items {
  position: relative;
}
.no-content {
  position: relative;
}
.no-content > .text {
  font-size: 14px;
}
.cart-item {
  display: flex;
  padding: 20px 0;
}
.cart-item > .right > .name {
  font-size: 14px;
  font-weight: bold;
  line-height: 1.5;
  margin-bottom: 10px;
}
.cart-item > .right > .price {
  font-size: 20px;
  font-weight: bold;
  margin-bottom: 16px;
}
.cart-item > .right > .count {
  display: flex;
  align-items: center;
}
.cart-item > .right > .count > .number {
  font-size: 14px;
  margin: 0 14px;
  width: 20px;
  text-align: center;
}
.cart-item > .right > .count .button {
  cursor: pointer;
  width: 28px;
  height: 28px;
  border-radius: 100%;
  background-color: #eee;
  font-size: 16px;
  font-weight: bold;
  display: flex;
  justify-content: center;
  align-items: center;
  transition: 0.2s;
  user-select: none;
}
.cart-item > .right > .count .button:hover {
  background-color: #ddd;
}
.cart-image {
  width: 90px;
  height: 90px;
  border-radius: 100%;
  background-color: #eee;
  overflow: hidden;
  align-items: center;
  justify-content: center;
  margin-right: 34px;
}
.cart-image > .image-wrapper > .image {
  display: block;
  width: 100%;
}
.buttonText-leave-active, .buttonText-enter-active {
  transition: opacity 0.2s, top 0.35s;
}
.buttonText-leave-to, .buttonText-enter {
  opacity: 0;
}
.cartList-enter-active {
  transition: all 2s;
}
.cartList-enter-active > .right > .name, .cartList-enter-active > .right > .price {
  transition: 0.4s;
}
.cartList-enter-active > .right > .name {
  transition-delay: 0.7s;
}
.cartList-enter-active > .right > .price {
  transition-delay: 0.85s;
}
.cartList-enter-active > .right > .count {
  transition: opacity 0.4s;
  transition-delay: 1s;
}
.cartList-enter-active .cart-image {
  transition: 0.5s cubic-bezier(0.79, 0.01, 0.22, 1);
}
.cartList-enter-active .cart-image > .image-wrapper {
  transition: 0.5s cubic-bezier(0.79, 0.01, 0.22, 1) 0.1s;
}
.cartList-enter > .right > .name, .cartList-enter > .right > .price {
  opacity: 0;
  transform: translateX(30px);
}
.cartList-enter > .right .count {
  opacity: 0;
}
.cartList-enter .cart-image {
  transform: scale(0);
}
.cartList-enter .cart-image > .image-wrapper {
  transform: scale(0);
}
.cartList-leave-active {
  transition: 0.7s cubic-bezier(0.79, 0.01, 0.22, 1);
  position: absolute;
}
.cartList-leave-to {
  transform: scale(0);
  opacity: 0;
}
.cartList-move {
  transition: 0.7s cubic-bezier(0.79, 0.01, 0.22, 1);
}
.noContent-enter-active, .noContent-leave-active {
  transition: opacity 0.5s;
  position: absolute;
}
.noContent-enter, .noContent-leave-to {
  opacity: 0;
}
.checkout-button {
    cursor: pointer;
    background-color: #f6c90e;
    font-weight: bold;
    font-size: 14px;
    box-sizing: border-box;
    height: 46px;
    padding: 16px 20px;
    border-radius: 100px;
    box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
    transition: box-shadow 0.4s, background-color 0.2s;
    user-select: none;
    white-space: nowrap;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    width: 40%;
  }
  #checkoutForm{
    display: none;
  }
</style>

<script>
    function showCart(){
        console.log("showCart() function is called.");
        $("#shopItems").hide();
        $("#cartItems").show();
       
    }
    function hideCart(){
        $("#cartItems").hide();
        $("#shopItems").show();
        
    }
    
    document.addEventListener('DOMContentLoaded', function() {
    const shopItemsContainer = document.querySelector('.shop-items');
    const cartItemsContainer = document.querySelector('.cart-items');

    const app = {
        shopItems: [],
        cartItems: [],

        increment(cartItem) {
            cartItem.count++;
            this.renderCartItems();
        },
    
        decrement(cartItem) {
            if (cartItem.count > 1) {
                cartItem.count--;
            } else {
                const itemIndex = this.cartItems.findIndex(item => item.id === cartItem.id);
                if (itemIndex !== -1) {
                    this.cartItems.splice(itemIndex, 1);
                    this.updateCheckoutButton();
                    this.fetchShopItems();
                }
            }
            this.renderCartItems();
            
        },
        checkout() {
            this.cartItems.forEach(item => {
                console.log(`${item.name} - Count: ${item.count}`);
            });
            if (this.cartItems.length > 0) {
                const cartItemParams = this.cartItems.map(item => `${item.name}:${item.count}`).join(',')+';';
        
                const cartItemsInput = document.getElementById('cartItemsInput');
                cartItemsInput.value = cartItemParams;

                // Trigger form submission
                const checkoutForm = document.getElementById('checkoutForm');
                checkoutForm.submit();
            }
        },

        init() {
            this.fetchShopItems();
            this.attachEventListeners();
        },

        fetchShopItems() {
            fetch("https://raw.githubusercontent.com/ShebinJoseJacob/SQ/main/list.json")
                .then(response => response.json())
                .then(data => {
                    this.shopItems = data.items;
                    this.renderShopItems();
                })
                .catch(error => {
                    console.error("Error fetching shop items:", error);
                });
        },

        renderShopItems() {
            shopItemsContainer.innerHTML = '';

            this.shopItems.forEach(item => {
                if (typeof item.inCart === 'undefined') {
                    item.inCart = false;
                }
                const itemElement = this.createShopItemElement(item);
                shopItemsContainer.appendChild(itemElement);
            });
        },

        createShopItemElement(item) {
            const itemDiv = document.createElement('div');
            itemDiv.className = 'item';
        
            const itemBlockDiv = document.createElement('div');
            itemBlockDiv.className = 'item-block';
        
            const imageAreaDiv = document.createElement('div');
            imageAreaDiv.className = 'image-area';
            imageAreaDiv.style.backgroundColor = item.color;
        
            const image = document.createElement('img');
            image.className = 'image';
            image.src = item.image;
            imageAreaDiv.appendChild(image);
        
            const nameDiv = document.createElement('div');
            nameDiv.className = 'name';
            nameDiv.textContent = item.name;
        
            const descriptionDiv = document.createElement('div');
            descriptionDiv.className = 'description';
            descriptionDiv.textContent = item.description;
        
            const bottomAreaDiv = document.createElement('div');
            bottomAreaDiv.className = 'bottom-area';
        
            const priceDiv = document.createElement('div');
            priceDiv.className = 'price';
            priceDiv.textContent = `$${item.price}`;
            bottomAreaDiv.appendChild(priceDiv);
        
            const buttonDiv = document.createElement('div');
            buttonDiv.className = 'button';
            buttonDiv.dataset.itemId = item.id;
            buttonDiv.textContent = 'ADD TO CART';
            bottomAreaDiv.appendChild(buttonDiv);
        
            itemBlockDiv.appendChild(imageAreaDiv);
            itemBlockDiv.appendChild(nameDiv);
            itemBlockDiv.appendChild(descriptionDiv);
            itemBlockDiv.appendChild(bottomAreaDiv);
            itemDiv.appendChild(itemBlockDiv);
        
            return itemDiv;
        },
        

        addToCart(itemId) {
            const selectedItem = this.shopItems.find(item => item.id === parseInt(itemId));
            if (selectedItem && !selectedItem.inCart) {
                selectedItem.inCart = true;
                const newItem = { ...selectedItem, count: 1 };
                this.cartItems.push(newItem);

                const animationTarget = document.querySelector(`.button[data-item-id="${itemId}"]`);
                animationTarget.textContent = '✓';
                animationTarget.style.pointerEvents = 'none';
                gsap.to(animationTarget, {
                    width: 46,
                    duration: 0.8,
                    ease: "power4"
                  });
                // Use your GSAP animation here
                this.renderCartItems();
                this.updateCheckoutButton();
            }
            setTimeout(() => {
                cartItemsContainer.scrollTop = cartItemsContainer.scrollHeight;
            });
        },
        renderCartItems() {
            cartItemsContainer.innerHTML = '';
            this.cartItems.forEach(cartItem => {
                const cartItemElement = this.createCartItemElement(cartItem);
                cartItemsContainer.appendChild(cartItemElement);
            });

            if (this.cartItems.length === 0) {
                const noContentElement = document.createElement('div');
                noContentElement.className = 'no-content';
                noContentElement.innerHTML = '<p class="text">Your cart is empty.</p>';
                cartItemsContainer.appendChild(noContentElement);
            }
            else{
                this.createCheckoutButton();
            }
            
        },

        updateCheckoutButton() {
            const checkoutButtonContainer = document.getElementById('checkoutButtonContainer');
            if (this.cartItems.length > 0) {
                checkoutButtonContainer.style.display = 'block';
            } else {
                checkoutButtonContainer.style.display = 'none';
            }
        },

        createCartItemElement(cartItem) {
            const cartItemDiv = document.createElement('div');
            cartItemDiv.className = 'cart-item';

            const leftDiv = document.createElement('div');
            leftDiv.className = 'left';

            const cartImageDiv = document.createElement('div');
            cartImageDiv.className = 'cart-image';

            const imageWrapperDiv = document.createElement('div');
            imageWrapperDiv.className = 'image-wrapper';

            const image = document.createElement('img');
            image.className = 'image';
            image.src = cartItem.image;
            imageWrapperDiv.appendChild(image);

            cartImageDiv.appendChild(imageWrapperDiv);
            leftDiv.appendChild(cartImageDiv);

            const rightDiv = document.createElement('div');
            rightDiv.className = 'right';

            const nameDiv = document.createElement('div');
            nameDiv.className = 'name';
            nameDiv.textContent = cartItem.name;

            const priceDiv = document.createElement('div');
            priceDiv.className = 'price';
            priceDiv.textContent = `$${cartItem.price.toFixed(2)}`;

            const countDiv = document.createElement('div');
            countDiv.className = 'count';

            const decrementButton = document.createElement('div');
            decrementButton.className = 'button';
            decrementButton.textContent = '<';
            decrementButton.addEventListener('click', () => this.decrement(cartItem));

            const numberDiv = document.createElement('div');
            numberDiv.className = 'number';
            numberDiv.textContent = cartItem.count;

            const incrementButton = document.createElement('div');
            incrementButton.className = 'button';
            incrementButton.textContent = '>';
            incrementButton.addEventListener('click', () => this.increment(cartItem));

            countDiv.appendChild(decrementButton);
            countDiv.appendChild(numberDiv);
            countDiv.appendChild(incrementButton);

            rightDiv.appendChild(nameDiv);
            rightDiv.appendChild(priceDiv);
            rightDiv.appendChild(countDiv);

            cartItemDiv.appendChild(leftDiv);
            cartItemDiv.appendChild(rightDiv);

            return cartItemDiv;
        },

        createCheckoutButton() {
            const checkoutButtonContainer = document.getElementById('checkoutButtonContainer');
            
            const existingCheckoutButton = document.querySelector('.checkout-button');
            if (existingCheckoutButton) {
                existingCheckoutButton.remove();
            }
    
            const checkoutButton = document.createElement('div');
            checkoutButton.className = 'checkout-button';
            checkoutButton.textContent = 'Checkout';
            checkoutButton.addEventListener('click', this.checkout.bind(this));
            checkoutButtonContainer.appendChild(checkoutButton);
        },


        attachEventListeners() {
            shopItemsContainer.addEventListener('click', event => {
                if (event.target.classList.contains('button')) {
                    const itemId = event.target.dataset.itemId;
                    this.addToCart(itemId);
                }
            });
        }
    };
    

    app.init();
});

</script>
</body>
</html>

2. Response Page 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Smart Cafe</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
  <script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>
</head>
<body>
  <div class="wrapper">
    <div class="screen -left">
      <div class="app-bar">
        <img class="logo" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1315882/pngwave.png" alt="Logo">
      </div>
      <div class="response">
        <lottie-player src="https://lottie.host/22e763a7-13d7-4b7f-8324-4a33b0b5b656/YlSUNUY2fG.json" background="#FFFFFF" speed="1" style="width: 300px; height: 300px; margin-left: -3%;" loop autoplay direction="1" mode="normal"></lottie-player>
    </div>
    <div class="response-text">Your Order Is Getting Ready</div>
    </div>
  </div>
<style>
body {
  font-family: "Rubik", sans-serif;
  color: #303841;
  overflow:hidden ;
}
.wrapper {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: relative;
  flex-wrap: wrap;
  padding: 40px 20px;
  max-width: 720px;
  margin: 0 auto;
}
.wrapper::before {
  content: "";
  display: block;
  position: fixed;
  width: 300%;
  height: 100%;
  top: 50%;
  left: 50%;
  border-radius: 100%;
  transform: translateX(-50%) skewY(-8deg);
  background-color: #f6c90e;
  z-index: -1;
  animation: wave 8s ease-in-out infinite alternate;
}
@keyframes wave {
  0% {
    transform: translateX(-50%) skew(0deg, -8deg);
  }
  100% {
    transform: translateX(-30%) skew(8deg, -4deg);
  }
}
@media(max-width:700px) {
.screen {
    background-color: #fff;
    box-sizing: border-box;
    width: 340px;
    height: 600px;
    box-shadow: 0 3.2px 2.2px rgba(0, 0, 0, 0.02), 0 7px 5.4px rgba(0, 0, 0, 0.028), 0 12.1px 10.1px rgba(0, 0, 0, 0.035), 0 19.8px 18.1px rgba(0, 0, 0, 0.042), 0 34.7px 33.8px rgba(0, 0, 0, 0.05), 0 81px 81px rgba(0, 0, 0, 0.07);
    border-radius: 30px;
    overflow-y: scroll;
    padding: 0 28px;
    position: relative;
    margin-bottom: 20px;
    margin-left: 10%;
    }
    .response{
        height: 300px;
        width: 300px;
        align-items: center;
    }
}
@media (min-width: 781px) {
    .screen {
    background-color: #fff;
    box-sizing: border-box;
    width: 340px;
    height: 600px;
    box-shadow: 0 3.2px 2.2px rgba(0, 0, 0, 0.02), 0 7px 5.4px rgba(0, 0, 0, 0.028), 0 12.1px 10.1px rgba(0, 0, 0, 0.035), 0 19.8px 18.1px rgba(0, 0, 0, 0.042), 0 34.7px 33.8px rgba(0, 0, 0, 0.05), 0 81px 81px rgba(0, 0, 0, 0.07);
    border-radius: 30px;
    overflow-y: scroll;
    padding: 0 28px;
    position: relative;
    margin-bottom: 20px;
    margin-left: 20%;
    }
}
.screen::before {
  content: "";
  display: block;
  position: absolute;
  width: 300px;
  height: 300px;
  border-radius: 100%;
  background-color: #f6c90e;
  top: -20%;
  left: -50%;
  z-index: 0;
}
.screen::-webkit-scrollbar {
  display: none;
}
.screen > .title {
  font-size: 24px;
  font-weight: bold;
  margin: 20px 0;
  position: relative;
}
.app-bar {
  padding: 12px 0;
  position: relative;
}
.app-bar > .logo {
  display: block;
  width: 50px;
}

.response-text {
    cursor: pointer;
    background-color: #f6c90e;
    font-weight: bold;
    font-size: 14px;
    box-sizing: border-box;
    height: 46px;
    padding: 16px 20px;
    border-radius: 100px;
    box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
    transition: box-shadow 0.4s, background-color 0.2s;
    user-select: none;
    white-space: nowrap;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    width: 75%;
    margin-left: 13%;
  }
</style>

</body>
</html>

3. Admin Page

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Smart Cafe</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
  <script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
  <script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Roboto&family=Rubik&family=Lora&display=swap" rel="stylesheet">
</head>
<body>
  <div class="wrapper">
    <div class="screen -left" id="shopItems">
      <div class="app-bar">
      </div>
      <div class="title">Orders</div>
      <div class="shop-items">
        
      </div>
    </div>
  </div>
<style>
body {
  font-family: "Rubik", sans-serif;
  color: #303841;
  overflow: hidden;
}
.wrapper {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: relative;
  flex-wrap: wrap;
  padding: 40px 20px;
  max-width: 720px;
  margin: 0 auto;
}
.wrapper::before {
  content: "";
  display: block;
  position: fixed;
  width: 300%;
  height: 100%;
  top: 50%;
  left: 50%;
  border-radius: 100%;
  transform: translateX(-50%) skewY(-8deg);
  background-color: #f6c90e;
  z-index: -1;
  animation: wave 8s ease-in-out infinite alternate;
}
@keyframes wave {
  0% {
    transform: translateX(-50%) skew(0deg, -8deg);
  }
  100% {
    transform: translateX(-30%) skew(8deg, -4deg);
  }
}

@media(max-width:700px) {
.screen {
    background-color: #fff;
    box-sizing: border-box;
    width: 340px;
    height: 600px;
    box-shadow: 0 3.2px 2.2px rgba(0, 0, 0, 0.02), 0 7px 5.4px rgba(0, 0, 0, 0.028), 0 12.1px 10.1px rgba(0, 0, 0, 0.035), 0 19.8px 18.1px rgba(0, 0, 0, 0.042), 0 34.7px 33.8px rgba(0, 0, 0, 0.05), 0 81px 81px rgba(0, 0, 0, 0.07);
    border-radius: 30px;
    overflow-y: scroll;
    padding: 0 28px;
    position: relative;
    margin-bottom: 20px;
    margin-left: 15%;
    }

}
@media (min-width: 781px) {
    .screen {
    background-color: #fff;
    box-sizing: border-box;
    width: 340px;
    height: 600px;
    box-shadow: 0 3.2px 2.2px rgba(0, 0, 0, 0.02), 0 7px 5.4px rgba(0, 0, 0, 0.028), 0 12.1px 10.1px rgba(0, 0, 0, 0.035), 0 19.8px 18.1px rgba(0, 0, 0, 0.042), 0 34.7px 33.8px rgba(0, 0, 0, 0.05), 0 81px 81px rgba(0, 0, 0, 0.07);
    border-radius: 30px;
    overflow-y: scroll;
    padding: 0 28px;
    position: relative;
    margin-bottom: 20px;
    }
}

.screen::before {
  content: "";
  display: block;
  position: absolute;
  width: 300px;
  height: 300px;
  border-radius: 100%;
  background-color: #f6c90e;
  top: -20%;
  left: -50%;
  z-index: 0;
}
.screen::-webkit-scrollbar {
  display: none;
}
.screen > .title {
  font-size: 24px;
  font-weight: bold;
  margin: 20px 0;
  position: relative;
}
.app-bar {
  padding: 12px 0;
  position: relative;
}
.app-bar > .logo {
  display: block;
  width: 50px;
}
.shop-items {
  position: relative;
}
.order-container{
  width: 90%;
  height: auto;
  padding: 1rem;
  margin-bottom: 1rem;
  margin-left: -0.3rem;
  border-radius: 1rem;
  font-family: 'Merriweather', serif;
  display: flex;
  position: relative;
  flex-direction: column;
  box-shadow: 0.3rem 0.3rem 0.6rem #E4EBF5, -0.3rem -0.3rem 10rem #e6eaef94;
}
.table-id{
  font-weight: 600;
  font-size: 1.3rem;
  font-family: 'Bebas Neue', sans-serif;
  padding-bottom: 0.3rem;
}

.item_name{
  padding-bottom: 0.2rem;
  font-size: 0.9rem;
  font-family: 'Lora', serif;
}
</style>

<script>
    
    document.addEventListener('DOMContentLoaded', function() {
    const shopItemsContainer = document.querySelector('.shop-items');

    const app = {
            shopItems: [],

            init() {
                this.renderShopItems();
                this.attachEventListeners();
            },

            renderShopItems() {
                shopItemsContainer.innerHTML = '';

                for (const tableId in orders) {
                    const items = orders[tableId];

                    // Create a container for each order
                    const orderContainer = document.createElement('div');
                    orderContainer.className = 'order-container';
                    
                    // Create an element to display the table ID
                    const tableIdElement = document.createElement('div');
                    tableIdElement.className = 'table-id';
                    tableIdElement.textContent = `Table ID: ${tableId}`;
                    orderContainer.appendChild(tableIdElement);

                    items.forEach((item, index) => {
                        if (item.name) {
                            const itemElement = this.createShopItemElement(item, index + 1);
                            orderContainer.appendChild(itemElement);
                        }
                    });

                    shopItemsContainer.appendChild(orderContainer);
                }
            },

            createShopItemElement(item, serialNumber) {
                const itemElement = document.createElement('div');
                itemElement.className = 'item';
                const itemName = item.name.replace(/\+/g, ' ');
                itemElement.innerHTML = `<div class="item_name">${serialNumber}. ${itemName} : ${item.count}</div>`;
                return itemElement;
            },
            
            attachEventListeners() {
                shopItemsContainer.addEventListener('dblclick', event => {
                    const tableElement = event.target.closest('.order-container');
                    if (tableElement) {
                        const tableIdElement = tableElement.querySelector('.table-id');
                        if (tableIdElement) {
                            const table_id = tableIdElement.textContent.replace('Table ID: ', '');
                            deleteOrder(table_id);
                            tableElement.remove();
                        }
                    }
                });
            }
        };
    
        function deleteOrder(table_id) {
        const url = "http://192.168.1.7/delete";
        const tableID = table_id + ";";
        const data = new URLSearchParams();
        data.append('table_id', tableID);


        fetch(url, {
            method: 'POST',
            body: data
        })
        .then(response => {
            if (!response.ok) {
              throw new Error(`HTTP error! Status: ${response.status}`);
            }
            // Check if the response body is empty
            if (response.status === 200) {
                return null; // Return null or handle as needed
            }

            return response.json();
        })
        .then(data => {
            console.log('Order deleted:', data);
        })
        .catch(error => {
            console.error('Error deleting order:', error);
        });
    }

    app.init();
});

</script>
</body>
</html>

4. Arduino code

#include <SPI.h>
#include <Ethernet.h>
#include <HttpClient.h>
#include <EthernetClient.h>
#include <ArduinoJson.h>
#include "source.h" // All Web Pages are written here
#include "DFRobotDFPlayerMini.h"
#include <HardwareSerial.h>


HardwareSerial Serial2(PA3,PD5);

// Create the Player object
DFRobotDFPlayerMini player;

int flag=0;

IPAddress ip(192,168,1,7);

// Define Ethernet Client parameters
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

EthernetServer server(80);              // Server declaring on port 80

DynamicJsonDocument jsonDoc(1024);
String jsonStr;
                      
void setup()
{

    //Initialize serial and wait for port to open:
    Serial.begin(9600);
    Serial2.begin(9600);
     if (player.begin(Serial2)) // Checking the df player mini and speaker
    {
     Serial.println("OK");
      player.volume(30);
      player.play(1);
    }
  else
  {
    Serial.println("Error");
  }

    // by default, the local IP address will be 192.168.1.7
    Ethernet.begin(mac,ip);
    
    // start the web server on port 80
    server.begin();

   
    
}

void loop()
{
    int t;
    char c;
    // compare the previous AP status to the current status
    IPAddress ip = Ethernet.localIP();
    
    // Wifi Server check
    EthernetClient client = server.accept(); // listen for incoming clients
    if (client) { // if you get a client,
        Serial.println("new client"); // print a message out the serial port
        String currentLine = ""; // make a String to hold incoming data from the client
        while (client.connected()) { // loop while the client's connected
            if (client.available()) { // if there's bytes to read from the client,
                c = client.read(); // read a byte, then
                if (c == '\n') {
                    if (currentLine.length() == 0) {
                        // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
                        // and a content-type so the client knows what's coming, then a blank line:
                        client.println("HTTP/1.1 200 OK");
                        client.println("Content-type:text/html");
                        client.println();
                        client.println(index_first); // Index page will be loaded
                        client.println(index_sec);
                        client.println(index_third);
                        client.println(index_fourth);
                        client.println(index_fifth);
                        client.println(index_sixth);
                        delay(10);
                        break;
                    }
                    else {
                        // if you got a newline, then clear currentLine:
                        currentLine = "";
                    }
                }
                else if (c != '\r') { // if you got anything else but a carriage return character,
                    currentLine += c; // add it to the end of the currentLine
                }

                // Check to see if the client request was a post
                if (currentLine.endsWith("POST /order")) {
                    currentLine = "";
                    while (client.connected()) {
                        if (client.available()) {
                            c = client.read(); // read a byte, then
                            if (c == '\n') { // if the byte is a newline character
                                //if (currentLine.length() == 0) break; // no length:  end of data request
                                currentLine = ""; // if you got a newline, then clear currentLine:
                            }
                            else if (c != '\r') currentLine += c; // if you got anything else but a carriage return character, add to string
                            if (currentLine.startsWith("cartItems=")){
                              Serial.println(currentLine);
                                if (currentLine.endsWith("%3B")){
                                    Serial.println(currentLine);
                                    client.println("HTTP/1.1 200 OK");
                                    client.println("Content-type:text/html");
                                    client.println();
                                    client.println(res); // Response page will be loaded
                                    client.stop();
                                    int delimiterIndex = currentLine.indexOf('=');
                                    if (delimiterIndex != -1) {
                                        String key = currentLine.substring(0, delimiterIndex);
                                        String value = currentLine.substring(delimiterIndex + 1);
                                        
                                        Serial.println("Key: " + key);
                                        Serial.println("Value: " + value);
                                        if (value.endsWith("%3B")) {
                                            value = value.substring(0, value.length() - 3);
                                        }
                                    
                                        // Extract table_id from input value
                                        int tableIdIndex = value.indexOf("table_id");
                                        if (tableIdIndex != -1) {
                                            String tableIdStr = value.substring(tableIdIndex + 11);
                                            Serial.println(tableIdStr);
                                            int tableId = tableIdStr.toInt();
                                            Serial.println("Table: " + String(tableId));
                                            
                                            // Create or get the JSON array using table_id as the key
                                            JsonArray orderItems = jsonDoc.createNestedArray(String(tableId));
                                            
                                            int startPos = 0;
                                            while (startPos < value.length()) {
                                                int colonIndex = value.indexOf("%3A", startPos);
                                                int commaIndex = value.indexOf("%2C", startPos);
                                                
                                                if (colonIndex == -1) {
                                                    break;
                                                }
                                                
                                                String itemName = value.substring(startPos, colonIndex);
                                                String itemCount = value.substring(colonIndex + 3, commaIndex != -1 ? commaIndex : value.length());
                                                
                                                JsonObject item = orderItems.createNestedObject();
                                                item["name"] = itemName;
                                                item["count"] = itemCount.toInt();
                                                
                                                startPos = commaIndex != -1 ? commaIndex + 3 : value.length();
                                            }
                                            
                                            // Serialize JSON to string
                                            jsonStr = "";
                                            serializeJson(jsonDoc, jsonStr);
                                            Serial.println("JSON Data: " + jsonStr);
                                            player.play(1); // Playing sound
                                        }
                                        else{
                                            Serial.println("? not found");
                                        }
                                        
                                    }
                                    break;
                                }
                            }
                        }
                    }
                    delay(10);
                } 

                if (currentLine.endsWith("GET /admin")) {
                  client.println("HTTP/1.1 200 OK");
                  client.println("Content-type:text/html");
                  client.println();
                  client.println(admin_first);
                  client.println("const orders = "+ jsonStr+";");
                  client.println(admin_second);
                  client.stop();
                }

               if (currentLine.endsWith("POST /delete")) {
                    currentLine = "";
                    while (client.connected()) {
                        if (client.available()) {
                            c = client.read(); // read a byte, then
                            if (c == '\n') { // if the byte is a newline character
                                //if (currentLine.length() == 0) break; // no length:  end of data request
                                currentLine = ""; // if you got a newline, then clear currentLine:
                            }
                            else if (c != '\r') currentLine += c; // if you got anything else but a carriage return character, add to string
                            if (currentLine.startsWith("table_id")){
                              if (currentLine.endsWith("%3B")){
                                int StartIndex = currentLine.indexOf('=');
                                String table_id = currentLine.substring(StartIndex + 1, currentLine.length() - 3); // Extract table_id
                                Serial.println("Table ID to Remove: " + table_id);
           
                                jsonDoc.remove(table_id);
                                jsonStr="";
                                serializeJson(jsonDoc, jsonStr);
                                Serial.println(jsonStr);
                       
                              client.println("HTTP/1.1 200 OK");
                              client.println("Content-type:text/html");
                              client.println();
                              //client.println(res); // Response page will be loaded
                              client.stop();
                              }
                            }
                        }
                    }
               }
                
            } 
        }

        // close the connection:
        client.stop();
        Serial.println(" client disconnected");
    }
}

Future Enhancements

  • Integration with online payment gateways.
  • Analytics and reporting features for admin insights.

 

 

 

 
 
Documents
  • Entire code

  • Circuit

  • Acrylic laser cutting file

Comments Write