Final Project Masterpiece

Inventory Tracking System for Storerooms

Prepared by Tshepho | Last Updated: May 27, 2025, 04:23 AM CAT

1. What will it do?

The Inventory Tracking System will automate tool and item management in a storeroom by:

This system enhances efficiency, reduces loss, and ensures reliability with a manual fallback if the RFID reader fails.

2. Who's done what beforehand?

3. What sources will you use?

  • Hardware Documentation:
  • Software Resources:

    • Python 3.13.3 with tkinter and csv documentation.

    • Python 3.13.3
      Python 3.31.3

    • Solid Works Software
    • Solidworks
  • Inspiration:
    • Andrew’s FabAcademy project outline (May 15, 2025).
    • GitHub projects using RDR-7081AKU with Python.
  • Local Fab Lab: Access to 3D printers, laser cutters, and milling tools.

4. What will you design?

  • 3D Design: A custom 150x130x92 mm PLA enclosure using Solidworks to house the Raspberry Pi, RDR-7081AKU, RFID-RC522, LCD and USB ports, with slots for user access.

  • Top Cover
  • Drawing top_cover
  • Drawing top Cover

  • Casing

  • Casing progress

    Casing progress
  • 2D Design: Laser-cut acrylic templates (20x20 mm) for RFID tag placement on tools.
  • Electronics Design: A custom PCB layout in KiCAD to distribute power and connect the Raspberry Pi to a local I2C LCD (optional redundancy display).


  • Schematic RP2040 Dev Board

  • Software Design: Python script for Tkinter GUI, inventory management, and RFID processing.
  • import tkinter as tk
    from tkinter import ttk, scrolledtext, messagebox
    import serial
    import csv
    import threading
    import time
    from datetime import datetime
    from RPLCD.i2c import CharLCD
    
    class InventoryGUI:
        def __init__(self, root):
            self.root = root
            self.root.title("Inventory Tracking System")
            self.root.geometry("800x600")
    
            self.inventory_lock = threading.Lock()
    
            # Serial port for RP2040 (Raspberry Pi Pico)
            try:
                self.pico_serial = serial.Serial('/dev/ttyACM0', 9600, timeout=1)
            except serial.SerialException:
                self.pico_serial = None
                print("Warning: RP2040 serial port not found.")
    
            # Optional serial port for other devices
            try:
                self.serial_port = serial.Serial('/dev/ttyUSB0', 9600, timeout=1)  # Adjust if needed
            except serial.SerialException:
                self.serial_port = None
                print("Warning: General serial port not found.")
    
            self.inventory = {
                "Hammer": 5,
                "Screwdriver": 10,
                "Wrench": 8,
                "Pliers": 3
            }
            self.max_inventory = {
                "Hammer": 10,
                "Screwdriver": 20,
                "Wrench": 15,
                "Pliers": 5
            }
    
            self.last_uid = ""
            self.status_message = "Waiting for scan..."
            self.verification_status = "Pending"
            self.last_action = ""
            self.failover_mode = False
            self.selected_tool = "Hammer"
    
            # Initialize LCD (I2C address 0x27, 16x2 display)
            try:
                self.lcd = CharLCD(i2c_expander='PCF8574', address=0x27, port=1, cols=16, rows=2)
                self.lcd.clear()
                self.lcd.write_string("Waiting for scan")
            except Exception as e:
                print(f"Warning: LCD initialization failed: {e}")
                self.lcd = None
    
            # Flag and entry for RDR-7081AKU keystrokes
            self.rdr_7081_input = ""
            self.uid_entry = tk.Entry(root)
            self.uid_entry.pack()
            self.uid_entry.bind("", self.process_rdr_7081_input)
    
            self.create_gui()
    
            # Start RFID scanning thread for RP2040
            threading.Thread(target=self.handle_rfid_scans, daemon=True).start()
            # Start thread to process RDR-7081AKU input
            threading.Thread(target=self.handle_rdr_7081_input_thread, daemon=True).start()
    
            self.update_gui()
    
        def create_gui(self):
            status_frame = ttk.LabelFrame(self.root, text="System Status", padding=10)
            status_frame.pack(fill="x", padx=10, pady=5)
    
            self.status_label = ttk.Label(status_frame, text="Status: Waiting for scan...")
            self.status_label.pack(anchor="w")
            self.uid_label = ttk.Label(status_frame, text="Last Scanned UID: None")
            self.uid_label.pack(anchor="w")
            self.verify_label = ttk.Label(status_frame, text="Verification: Pending")
            self.verify_label.pack(anchor="w")
            self.action_label = ttk.Label(status_frame, text="Last Action: None")
            self.action_label.pack(anchor="w")
    
            borrower_frame = ttk.LabelFrame(self.root, text="Borrower Registration", padding=10)
            borrower_frame.pack(fill="x", padx=10, pady=5)
    
            ttk.Label(borrower_frame, text="Select Tool:").pack(anchor="w")
            self.tool_var = tk.StringVar(value="Hammer")
            tool_dropdown = ttk.Combobox(borrower_frame, textvariable=self.tool_var, values=list(self.inventory.keys()))
            tool_dropdown.pack(anchor="w")
            tool_dropdown.bind("<>", self.update_selected_tool)
    
            ttk.Label(borrower_frame, text="Borrower UID:").pack(anchor="w")
            self.borrower_entry = ttk.Entry(borrower_frame)
            self.borrower_entry.pack(anchor="w")
    
            ttk.Button(borrower_frame, text="Register Borrower", command=self.register_borrower).pack(anchor="w", pady=5)
            ttk.Button(borrower_frame, text="Scan with RP2040/RC522", command=self.trigger_rfid_scan).pack(anchor="w")
    
            inventory_frame = ttk.LabelFrame(self.root, text="Inventory", padding=10)
            inventory_frame.pack(fill="both", expand=True, padx=10, pady=5)
    
            self.inventory_tree = ttk.Treeview(inventory_frame, columns=("Tool", "Quantity", "Increase", "Decrease"), show="headings")
            self.inventory_tree.heading("Tool", text="Tool")
            self.inventory_tree.heading("Quantity", text="Quantity")
            self.inventory_tree.heading("Increase", text="Add")
            self.inventory_tree.heading("Decrease", text="Remove")
            self.inventory_tree.column("Tool", width=150)
            self.inventory_tree.column("Quantity", width=100)
            self.inventory_tree.column("Increase", width=50)
            self.inventory_tree.column("Decrease", width=50)
            self.inventory_tree.pack(fill="both", expand=True)
    
            self.update_inventory_display()
            self.inventory_tree.bind("", self.handle_tree_click)
    
            log_frame = ttk.LabelFrame(self.root, text="Transaction Logs", padding=10)
            log_frame.pack(fill="both", expand=True, padx=10, pady=5)
    
            self.log_text = scrolledtext.ScrolledText(log_frame, height=10, wrap=tk.WORD)
            self.log_text.pack(fill="both", expand=True)
    
            self.load_transaction_logs()
    
        def update_selected_tool(self, event):
            self.selected_tool = self.tool_var.get()
    
        def trigger_rfid_scan(self):
            if self.pico_serial:
                self.status_message = "Triggered RP2040/RC522 scan..."
                self.update_lcd(self.status_message)
            else:
                self.status_message = "RP2040 serial port not available."
    
        def handle_rfid_scans(self):
            while True:
                try:
                    if self.pico_serial and self.pico_serial.in_waiting > 0:
                        message = self.pico_serial.readline().decode().strip()
                        if message.startswith("RFID:"):
                            self.last_uid = message.split("RFID:")[1]
                            self.status_message = f"Scanned via RP2040/RC522: {self.last_uid}"
                            self.verification_status = "Pending"
                            self.update_lcd(self.status_message)
                            self.process_rfid_scan(self.last_uid)
                    time.sleep(0.1)  # Small delay to prevent CPU overload
                except Exception as e:
                    self.status_message = f"RFID scan error: {e}"
                    self.update_lcd(self.status_message)
                    print(f"Error during RFID scan: {e}")
                    time.sleep(1)
    
        def handle_rdr_7081_input_thread(self):
            while True:
                if self.rdr_7081_input and "\n" in self.rdr_7081_input:
                    uid = self.rdr_7081_input.strip()
                    if uid:
                        self.last_uid = uid
                        self.status_message = f"Scanned via RDR-7081AKU: {self.last_uid}"
                        self.verification_status = "Pending"
                        self.update_lcd(self.status_message)
                        self.process_rfid_scan(self.last_uid)
                    self.rdr_7081_input = ""
                time.sleep(0.1)
    
        def process_rdr_7081_input(self, event):
            """Process keystrokes from RDR-7081AKU card reader"""
            if event.char:  # Only process printable characters
                self.rdr_7081_input += event.char
    
        def process_rfid_scan(self, uid):
            is_checked_out = self.check_transaction_status(uid, self.selected_tool)
            with self.inventory_lock:
                if is_checked_out:
                    self.verification_status = "Valid UID (Return)"
                    self.last_action = "CHECKIN"
                    self.inventory[self.selected_tool] += 1
                    if self.inventory[self.selected_tool] > self.max_inventory[self.selected_tool]:
                        self.inventory[self.selected_tool] = self.max_inventory[self.selected_tool]
                    self.log_transaction(self.selected_tool, uid, "CHECKIN")
                else:
                    try:
                        with open("transactions.csv", "r") as file:
                            uids = [row[1] for row in csv.reader(file) if len(row) >= 3 and row[2] == "CHECKOUT"]
                        if uid in uids:
                            self.verification_status = "Duplicate UID (Checkout)"
                            self.last_action = "FAILED"
                        else:
                            self.verification_status = "Valid UID (Checkout)"
                            self.last_action = "CHECKOUT"
                            self.inventory[self.selected_tool] -= 1
                            if self.inventory[self.selected_tool] < 0:
                                self.inventory[self.selected_tool] = 0
                            self.log_transaction(self.selected_tool, uid, "CHECKOUT")
                    except FileNotFoundError:
                        self.verification_status = "Valid UID (Checkout)"
                        self.last_action = "CHECKOUT"
                        self.inventory[self.selected_tool] -= 1
                        if self.inventory[self.selected_tool] < 0:
                            self.inventory[self.selected_tool] = 0
                        self.log_transaction(self.selected_tool, uid, "CHECKOUT")
            self.update_inventory_display()
    
        def update_lcd(self, message):
            if self.lcd:  # Check if LCD is initialized
                try:
                    self.lcd.clear()
                    self.lcd.write_string(message[:16])  # Truncate to 16 characters for 16x2 LCD
                except Exception as e:
                    print(f"Error updating LCD: {e}")
    
        def register_borrower(self):
            borrower_uid = self.borrower_entry.get().strip()
            if not borrower_uid:
                messagebox.showerror("Error", "Please enter a Borrower UID.")
                return
    
            self.last_uid = borrower_uid
            is_checked_out = self.check_transaction_status(borrower_uid, self.selected_tool)
    
            with self.inventory_lock:
                if is_checked_out:
                    self.verification_status = "Valid UID (Return)"
                    self.last_action = "CHECKIN"
                    self.inventory[self.selected_tool] += 1
                    if self.inventory[self.selected_tool] > self.max_inventory[self.selected_tool]:
                        self.inventory[self.selected_tool] = self.max_inventory[self.selected_tool]
                        messagebox.showwarning("Warning", f"Maximum quantity reached for {self.selected_tool}.")
                    self.log_transaction(self.selected_tool, borrower_uid, "CHECKIN")
                else:
                    try:
                        with open("transactions.csv", "r") as file:
                            uids = [row[1] for row in csv.reader(file) if len(row) >= 3 and row[2] == "CHECKOUT"]
                        if borrower_uid in uids:
                            self.verification_status = "Duplicate UID (Checkout)"
                            self.last_action = "FAILED"
                            messagebox.showerror("Error", "This UID is already checked out.")
                            return
                    except FileNotFoundError:
                        pass
                    self.verification_status = "Valid UID (Checkout)"
                    self.last_action = "CHECKOUT"
                    self.inventory[self.selected_tool] -= 1
                    if self.inventory[self.selected_tool] < 0:
                        self.inventory[self.selected_tool] = 0
                        messagebox.showwarning("Warning", "Quantity cannot go below 0.")
                    self.log_transaction(self.selected_tool, borrower_uid, "CHECKOUT")
    
            self.update_inventory_display()
            if self.pico_serial:
                self.pico_serial.write(f"TEXT:{self.last_action} {self.last_uid}\n".encode())
            self.borrower_entry.delete(0, tk.END)
    
        def update_inventory_display(self):
            self.inventory_tree.delete(*self.inventory_tree.get_children())
            for tool, quantity in self.inventory.items():
                self.inventory_tree.insert("", "end", values=(tool, quantity, "+", "−"))
    
        def handle_tree_click(self, event):
            item = self.inventory_tree.selection()
            if not item:
                return
            item = item[0]
            column = self.inventory_tree.identify_column(event.x)
            tool = self.inventory_tree.item(item, "values")[0]
            with self.inventory_lock:
                if column == "#3":
                    if self.inventory[tool] < self.max_inventory[tool]:
                        self.inventory[tool] += 1
                        self.log_transaction(tool, "Manual", "MANUAL_ADD")
                    else:
                        messagebox.showwarning("Warning", f"Maximum quantity reached for {tool}.")
                elif column == "#4" and self.inventory[tool] > 0:
                    self.inventory[tool] -= 1
                    self.log_transaction(tool, "Manual", "MANUAL_SUBTRACT")
            self.update_inventory_display()
    
        def load_transaction_logs(self):
            try:
                with open("transactions.csv", "r") as file:
                    reader = csv.reader(file)
                    logs = list(reader)
                    self.log_text.delete(1.0, tk.END)
                    if not logs or logs[0] != ["tool", "borrower", "action", "timestamp"]:
                        self.log_text.insert(tk.END, "No transactions yet.\n")
                        return
                    logs = logs[1:]
                    for log in logs[-10:]:
                        if len(log) >= 4:
                            self.log_text.insert(tk.END, f"{log[3]}: {log[0]} - {log[1]} - {log[2]}\n")
            except FileNotFoundError:
                with open("transactions.csv", "w", newline="") as file:
                    writer = csv.writer(file)
                    writer.writerow(["tool", "borrower", "action", "timestamp"])
                self.log_text.delete(1.0, tk.END)
                self.log_text.insert(tk.END, "No transactions yet.\n")
    
        def check_transaction_status(self, borrower, tool):
            try:
                with open("transactions.csv", "r") as file:
                    reader = csv.reader(file)
                    transactions = list(reader)[1:]
                    for transaction in reversed(transactions):
                        if len(transaction) >= 3 and transaction[0] == tool and transaction[1] == borrower:
                            return transaction[2] == "CHECKOUT"
            except FileNotFoundError:
                return False
            return False
    
        def handle_serial_communication(self):
            while True:
                try:
                    if self.serial_port and self.serial_port.in_waiting > 0:
                        message = self.serial_port.readline().decode().strip()
                        if message.startswith("VERIFY:"):
                            uid = message.split(":")[1]
                            is_checked_out = self.check_transaction_status(uid, self.selected_tool)
                            with self.inventory_lock:
                                if is_checked_out:
                                    self.verification_status = "Valid UID (Return)"
                                    self.serial_port.write("CONFIRM:VALID\n".encode())
                                    self.last_action = "CHECKIN"
                                    self.inventory[self.selected_tool] += 1
                                    if self.inventory[self.selected_tool] > self.max_inventory[self.selected_tool]:
                                        self.inventory[self.selected_tool] = self.max_inventory[self.selected_tool]
                                    self.log_transaction(self.selected_tool, uid, "CHECKIN")
                                else:
                                    try:
                                        with open("transactions.csv", "r") as file:
                                            rows = list(csv.reader(file))[1:]
                                            uids = [r[1] for r in rows if len(r) >= 3 and r[2] == "CHECKOUT"]
                                        if uid in uids:
                                            self.verification_status = "Duplicate UID (Checkout)"
                                            self.serial_port.write("CONFIRM:INVALID\n".encode())
                                            self.last_action = "FAILED"
                                        else:
                                            self.verification_status = "Valid UID (Checkout)"
                                            self.serial_port.write("CONFIRM:VALID\n".encode())
                                            self.last_action = "CHECKOUT"
                                            self.inventory[self.selected_tool] -= 1
                                            if self.inventory[self.selected_tool] < 0:
                                                self.inventory[self.selected_tool] = 0
                                            self.log_transaction(self.selected_tool, uid, "CHECKOUT")
                                    except FileNotFoundError:
                                        self.verification_status = "Valid UID (Checkout)"
                                        self.serial_port.write("CONFIRM:VALID\n".encode())
                                        self.last_action = "CHECKOUT"
                                        self.inventory[self.selected_tool] -= 1
                                        if self.inventory[self.selected_tool] < 0:
                                            self.inventory[self.selected_tool] = 0
                                        self.log_transaction(self.selected_tool, uid, "CHECKOUT")
                except Exception as e:
                    self.status_message = f"Serial error: {e}"
                    self.update_lcd(self.status_message)
                    time.sleep(1)
    
        def log_transaction(self, tool, borrower, action):
            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            with open("transactions.csv", "a", newline="") as file:
                writer = csv.writer(file)
                writer.writerow([tool, borrower, action, timestamp])
            self.load_transaction_logs()
    
        def update_gui(self):
            self.status_label.config(text=f"Status: {self.status_message}")
            self.uid_label.config(text=f"Last Scanned UID: {self.last_uid}")
            self.verify_label.config(text=f"Verification: {self.verification_status}")
            self.action_label.config(text=f"Last Action: {self.last_action}")
            if self.pico_serial and self.last_uid:
                try:
                    self.pico_serial.write(f"TEXT:{self.last_action} {self.last_uid}\n".encode())
                except Exception as e:
                    print(f"Error writing to Pico serial: {e}")
            self.root.after(1000, self.update_gui)
    
        def on_closing(self):
            if self.pico_serial:
                self.pico_serial.close()
            if self.serial_port:
                self.serial_port.close()
            if self.lcd:
                self.lcd.clear()
            self.root.destroy()
    
    if __name__ == "__main__":
        try:
            root = tk.Tk()
            app = InventoryGUI(root)
            root.protocol("WM_DELETE_WINDOW", app.on_closing)
            root.mainloop()
        except KeyboardInterrupt:
            print("\nKeyboardInterrupt received. Shutting down gracefully...")
            if 'app' in locals():
                app.on_closing()
                        

5. What materials and components will be used?

  • Materials:
    • PLA filament (~100g) for 3D printing the enclosure.

    • PLA Print

      PLA Roll

  • Components:
    • Raspberry Pi 3B+

    • Pi

    • RF Ideas RDR-7081AKU USB RFID reader.

    • RDR-7081AKU

    • 20 RFID tags (13.56 MHz MIFARE Classic 1K).

    • RFID TAG

    • Optional 16x2 I2C LCD (for redundancy display).

    • I2C LCD 16x2

    • MicroSD card (8 GB+).

    • MicroSD

    • Jumper wires, headers, USB cables.

    • Milled PCB

    • Custom PCB (milled or ordered).

    • Milled PCB
      Milled PCB

6. Where will they come from?

  • Local Fab Lab: PLA filament, 3D printer, laser cutter, milling tools, Copper Clad sheet
  • Retail:
    • RDR-7081AKU, RFID tags, RFID-RC522
    • LCD, MicroSD card .
    • SEEED XIAO RP2040
    • Raspberry Pi, jumper wires, headers

7. How much will they cost?

  • New Components:
    • Raspberry Pi, wires ($47.10).
    • RFID tags multicolor(20) ~$10.
    • PLA filament (~900g) ~$16.99.
    • MicroSD card (8 GB) ~$8.98.
    • LCD ~$12.99.
    • PCB (custom) ~$10.
    • Misc (headers, M3 bolts and nuts, M3 self tapping screws) ~$4.
    • RDR-7081AKU ($124.99)
  • Total New Cost: ~$235.05 (The cost is higher because of the RDR-7081AKU).

8. What parts and systems will be made?

  • Made:
    • 3D-printed PLA enclosure (additive fabrication).



    • System Intergration - RP2040, RFID-RC522, Raspberry Pi 3B, LCD and RF Ideas RDR-7081AKU

    • PCB Schematic

      PCB Editor

      Assembly

    • Python Tkinter GUI and CSV system (software design and system integration).


  • Bought: Core components (RDR-7081AKU, RFID tags, Raspberry Pi, MicroSD).

9. What processes will be used?

  • 3D Design: Solidworks for Casing design, 3D printing with PLA (additive).
  • Assembly
  • Electronics Design: KiCAD for PCB layout, milling


  • Embedded Programming: Python for Raspberry Pi GUI and RFID processing.
  • GUI
  • System Integration: Combining hardware (PCB, enclosure) and software (Python).
  • System Integration
  • Packaging: Assembling components into the 3D-printed casing.
  • Tool Scanner

10. What questions need to be answered?

  • Will the RDR-7081AKU reliably scan all 20 tags in a busy storeroom?
  • The RDR-7081AKU's reliability in scanning all 20 tags in a busy storeroom depends on several factors. The device is designed to read 125 kHz RFID tags with a typical range of a few centimeters, and the script processes input via the

    process_rdr_7081_input
    method, which handles keystroke events terminated by a newline. In a busy environment, factors like tag proximity, interference from metal objects, or overlapping signals could affect performance. The current implementation includes a thread
    handle_rdr_7081_input_thread
    to continuously monitor input, but it lacks explicit anti-collision handling for multiple tags. Test results will indicate how it fares For 20 tags, testing under storeroom conditions (tag spacing, reader positioning) is recommended. If issues arise, consider adjusting the reader's sensitivity or integrating a more robust RFID reader with anti-collision support.

  • Is the 150x130x91 mm enclosure sufficient for the Raspberry Pi and RDR-7081AKU?
  • Can the optional LCD enhance redundancy without complicating the design?
  • Will the manual entry redundancy handle all edge cases (e.g., typos)?

11. How will it be evaluated?

  • Functionality:
    • RDR-7081AKU scans and logs transactions correctly (test with 10 tools, 5 borrowers).


    • Manual entry updates inventory and logs transactions.
    • GUI reflects real-time changes.
  • Design Quality:
    • Enclosure fits all components with accessible USB ports. All Components iside the casing, RDR-7081AKU has a pocket on top
    • Casing Components
    • PCB routes power cleanly, tested with multimeter.
    • Multimeter Test
  • Performance:
    • RFID scan speed 2 seconds.
    • GUI updates within 2 seconds of actions.
  • Documentation:
    • Submit schematics (KiCAD), code (Python), 3D/STL files, photos/videos.
    • Files for Download:

      Zip Files for Downloads

      Inventry Management Project files for Download

    • I have always wanted a system like this one and FabAcademy has really opened my eyes that such systems can be self made when you have the right tools. I have leart that concepts such as machine learning machine leaning, digital fabrication can all be combined to eqip us to do alomst anything.


  • Individual Mastery: The system I have built is compact and can be carried to any storeroom with ease, its light, compact and as long as you have a
    USB to C
    ,
    HDMI
    ,
    ethernet cable with ready line internet or WIFI where you can SSH the 
                        system can work.
    I have learnt skills in each unit being design, fabrication, electronics, programming, integration, dessemination, interllectual property and income generation.