Interface



Design and Ideation

I want to make a simple webapp that will be accessible from cellphones, no need to sign up or sign in, so that people can use it to buy and sell items on the lockers. The payment will be done on an outer platform (Zelle/Venmo/Paypal), and once approved by the seller/admin - the buyer would get the code access to the locker. For the class presentation I'll cut that part to mimic the transaction by just sending the code when hitting a button (without having to really pay and submit approval - to make the presentation quick and easy).

I started a draft for the flow of the user experience on the app on Figma:


The project will be hosted on my GitHub where you can check out the interface code as well as the backend Flask server. I decided to build the interface using SvelteKit because I'm already familiar with it and because it has good libraries for the functions I'll need, like uploading pictures or connecting to payment apps. Here's my GitHub project, including the front and backend: https://github.com/Kup135/BuyBye

Skeleton Building for Frontend + Updating Backend Flask

First step, I wanted to update the structure of my files to decide what goes on the frontend/backend. Here are the inputs that will eventually have to show up on the frontend:

    User input (inserted on a form):

  • Photos of the item being sold
  • Description (optional)
  • Payment options - to store for when buyer gets into the payments apps to pay
  • Price for the item (capped by the form to 25$)

  • ChatGPT or other API (optional):

  • Recommended description
  • Recommended price

  • Xiao boards on the lockers:

  • Status (open/close)
  • Current updated code on the Xiao - get the code and report back the current code
  • Get remote command to open/close in case of need

I looked online for a SvelteKit template that would be minimal and fitting for my needs and found Yuyutsu It's minimal, looks nice, and adaptable easily for smartphones - which is essential for my app.

Then I started building the web app, as a first attempt I want it to show the lockers with their open/close status that I already started working on for the backend Flask app:


@app.route('/update_locker', methods=['POST'])
def update_locker():
    """
    Updates the status of a specific locker.
    Expects a JSON payload with "locker_id" and "status".
    """
    LOCKERS_DATA_FILE = "lockers_data.json"

    # Load existing lockers data
    if os.path.exists(LOCKERS_DATA_FILE):
        with open(LOCKERS_DATA_FILE, "r") as file:
            locker_data = json.load(file)
    else:
        return jsonify({"error": "Locker data file not found!"}), 404

    # Parse the incoming JSON payload
    data = request.get_json()

    if not data or "locker_id" not in data or "status" not in data:
        return jsonify({"error": "Invalid request, must include 'locker_id' and 'status'"}), 400

    locker_id = data["locker_id"]
    status = data["status"]

    # Validate locker ID and status
    locker = next((l for l in locker_data if l["id"] == locker_id), None)
    if not locker:
        return jsonify({"error": f"Locker ID {locker_id} is invalid"}), 400
    if status not in [0, 1]:
        return jsonify({"error": f"Status {status} is invalid, must be 0 or 1"}), 400

    # Update the locker status
    locker["status"] = status

    # Save the updated data back to the JSON file
    with open(LOCKERS_DATA_FILE, "w") as file:
        json.dump(locker_data, file, indent=4)

    return jsonify({"message": f"Locker {locker_id} updated to {status}", "current_data": locker_data})

							
							

I tested the Flask on a local host and it worked well from my terminal with a curl command.

Next step is to link that to the Frontend, showing a small red/green circle bellow the locker description indicating whether it's closed or open.

I layed the groundwork for the interface to recieve the Xiao data on open/close, within the Svelte component that defines the locker post on the interface homepage, the main page uses JavaScript from Svelte to load the backend data onto the DOM:


<script>
	import { SERVER_HOST } from "$lib/config";
	import PostItem from "$lib/components/PostItem.svelte";
	import Pagination from "$lib/components/Pagination/Pagination.svelte";
	import { paginatedPosts } from "$lib/components/Pagination/paginatedPosts";
	import Seo from "$lib/components/Seo.svelte";
	import { siteTitle, siteDescription } from "$lib/constants";

	let lockers = [];
	let loading = true;
	let error = null;

	let category = "Buy"; // Default category

	// Fetch lockers dynamically from the backend
	const fetchLockers = async () => {
	console.log("Starting to fetch lockers..."); // Debugging before the fetch

	try {
		const response = await fetch(`${SERVER_HOST}/lockers`);
		if (!response.ok) throw new Error("Failed to fetch lockers");
		lockers = await response.json(); // Debug
		console.log("Fetched lockers:", lockers); // Debug
	} catch (err) {
		error = err.message;
		console.error("Error fetching lockers:", err); // Debug
	} finally {
		loading = false; // Stop showing the loading state
	}
	};

	fetchLockers();
									
	// Filtering logic to present lockers ready for seller (empty) / buyer (full)
	const filterLockers = () => {
	return lockers.filter(locker => locker.category.includes(category));
	};
</script>

<div class="filter-buttons">
	<button on:click={() => category = "Sell"}>Sell</button>
	<button on:click={() => category = "Buy"}>Buy</button>
</div>

{#if loading}
	<p>Loading lockers...</p>
{:else if error}
	<p>Error: {error}</p>
{:else}
	{#each filterLockers() as locker}
	<PostItem {locker} /> <!-- Pass locker data to PostItem -->
	{/each}
	<Pagination items={filterLockers()} itemsPerPage={4} />
{/if}
					
							  
							

And then the Svelte component for the post itself (PostItem.svelte) is used as the framework to feed the backend data into, here it is:


<script>
	import { SERVER_HOST } from "$lib/config";
	export let locker;
	$: ({ id, title, description, category, image, status } = locker);
</script>

<div class="post-item">
	<h2 class="title">{title}</h2>
	<img 
	width="800" 
	height="300" 
	src={category.includes("Buy") ? image : "src/lib/images/empty.jpg"} alt={description} />
	<p>{description}</p>
	<div class="status">
	<!-- Status indicator: green for open (1), red for closed (0) -->
	<span class="circle {status === 1 ? 'green' : 'red'}"></span>
	<span>{status === 1 ? 'Open' : 'Closed'}</span>
	</div>
</div>

<style>
	.post-item {
	margin-bottom: 3rem;
	}
	.title {
	text-decoration: none;
	}
	.title:hover {
	text-decoration: underline;
	}
	.status {
	margin-top: 1rem;
	display: flex;
	align-items: left;
	justify-content: left;
	gap: 0.5rem;
	}
	h2 {
	font-size: 1.8rem;
	}
	img {
	object-fit: cover;
	object-position: center;
	}
	.circle {
	width: 12px;
	height: 12px;
	border-radius: 50%;
	display: inline-block;
	}
	.green {
	background-color: green;
	}
	.red {
	background-color: red;
	}
	@media screen and (max-width: 768px) {
	img {
		height: 200px;
		object-fit: contain;
		object-position: center;
	}
	}
</style>
								

And this is how it looks, the red/green dot indicating if the locker is open or closed will have to be updated directly from the Xiaos:

The last check was to see the local server handles well manual push of status from the terminal and updates the frontend app (see the circle changing)

For the next step, I'll build a generic sell/buy page to demonstrate changing the code on the locker

Here it is updating the open_code on the Json at the backend from the SvelteKit interface:

This is the code snippet from the python Flask app that makes it happen:


@app.route('/set_open_code', methods=['POST'])
def set_open_code():
	"""
	sends the new code from the frontend to the json in the backend
	"""
	data = request.get_json()
	if not data or "locker_id" not in data or "new_code" not in data:
		return jsonify({"error": "Invalid request, must include 'locker_id' and 'new_code'"}), 400
	
	locker_id = str(data["locker_id"])
	new_code = str(data["new_code"])

	# Validate that new_code is exactly 4 digits
	if len(new_code) != 4 or not new_code.isdigit():
		return jsonify({"error": "Code must be exactly 4 numeric digits"}), 400

	LOCKERS_DATA_FILE = "lockers_data.json"
	if not os.path.exists(LOCKERS_DATA_FILE):
		return jsonify({"error": "lockers_data.json not found"}), 404

	# Load existing lockers data
	with open(LOCKERS_DATA_FILE, "r") as file:
		locker_data = json.load(file)

	# Find the locker by ID
	locker = next((l for l in locker_data if l["id"] == locker_id), None)
	if not locker:
		return jsonify({"error": f"Locker ID {locker_id} not found"}), 400

	# Update the open_code
	locker["open_code"] = new_code

	# Save the updated data back
	with open(LOCKERS_DATA_FILE, "w") as file:
		json.dump(locker_data, file, indent=4)

	return jsonify({"message": f"open_code for locker {locker_id} updated to {new_code}"}), 200
									
								


Integrate with Xiaos

Now it's time to connect everything back to the Xiaos and have the report to the interface.

There were some majors detours here in trying to deploy websockets and admitting defeat. So for now I'm sticking with GET and POST, which are perhaps more basic and won't allow a constant stable connection but at least I can get them to work. Perhaps in the future development I'll try rebuilding everything with websockets.

First, I updated python Flask app to have an endpoint for updating the Xiao:


@app.route('/get_open_code', methods=['GET'])
def get_open_code():
	"""
	endpoint for the Xiao to retrieve new code.
	"""
	locker_id = request.args.get("locker_id")
	if not locker_id:
		return jsonify({"error": "Must provide locker_id as query parameter"}), 400

	LOCKERS_DATA_FILE = "lockers_data.json"
	if not os.path.exists(LOCKERS_DATA_FILE):
		return jsonify({"error": "lockers_data.json not found"}), 404

	with open(LOCKERS_DATA_FILE, "r") as file:
		locker_data = json.load(file)

	# Find the locker by ID
	locker = next((l for l in locker_data if l["id"] == locker_id), None)
	if not locker:
		return jsonify({"error": f"Locker ID {locker_id} not found"}), 400

	code = locker.get("open_code", None)
	if code is None:
		return jsonify({"error": f"No open_code found for locker {locker_id}"}), 404

	return jsonify({"locker_id": locker_id, "open_code": code})
								

And after checking it responds on a browser and with curl, I updated a function in the embedded to fetch the new code from the server, once in the intiation and then prompted after every locker_close function (press * on the keypad), to be easily called for:



void fetchOpenCodeFromServer() {
	if (WiFi.status() == WL_CONNECTED) {
		HTTPClient http;
		http.begin(openCodeURL);
	
		int httpResponseCode = http.GET();
		if (httpResponseCode > 0) {
		String payload = http.getString();
		Serial.println("Open code response payload:");
		Serial.println(payload);
	
		int codeStart = payload.indexOf("\"open_code\":\"");
		if (codeStart != -1) {
			codeStart += strlen("\"open_code\":\"");
			int codeEnd = payload.indexOf("\"", codeStart);
			if (codeEnd != -1) {
			String newCode = payload.substring(codeStart, codeEnd);
			Serial.print("Updating open_code to: ");
			Serial.println(newCode);
			open_code = newCode; // Update the global open_code variable
			} else {
			Serial.println("Error: Could not find end quote for open_code.");
			}
		} else {
			Serial.println("Error: open_code not found in the response.");
		}
		} else {
		Serial.print("GET request failed, code: ");
		Serial.println(httpResponseCode);
		}
	
		http.end();
	} else {
		Serial.println("WiFi not connected, cannot fetch open_code.");
	}
	}
								

I had to make sure the server is defined in it's network reachable IP address, and not the machine local one, eventuall (after playing a little with the Xiao antenna) it worked:

After lots of back and forth, replacing the port, and praying, it worked - here you can see the entire sequence:


Like I mentioned earlier, all of the code (both backend and frontend) are available on my GitHub's buybye repo

It was just to big to clone into here... But you can find the embedded here and the python Flask app here.