Project
Gestire: from an equipment reservation to an open locker
A university room-booking and equipment-borrowing system connecting a Flutter app, a Flask API and an ESP32 with eight locker relay outputs.
Reserve an FPGA board in an app, receive a code, and enter it at a locker to collect the board. That was the equipment-borrowing flow behind Gestire, a project I built with Diogo Silva, Ivo Delgado and Martim Carvalho in 2023 for Análise de Sistemas at the University of Aveiro.
I handled most of the implementation: the Flutter app and API integration, backend authentication and reservation flows, database setup, locker hardware, ESP32 firmware and API tests. The finished prototype joins room booking and equipment borrowing in one application, with a physical controller for collecting and returning equipment.
From manual lockers to shared equipment
The idea came from everyday use of rooms and equipment at DETI. Finding a study space meant checking capacity, sockets and facilities. Borrowing a development board also meant arranging access to it. FPGA kits assigned to small groups could sit in manual lockers while nobody in that group needed them.
We wanted a shared catalogue where a student could find an available kit and collect it themselves. The original concept drawing shows how different types of equipment could occupy a bank of compartments around one keypad.
The hardware we built is visible in this demonstration. It has an ESP32, a matrix keypad, an I2C display and an eight-channel relay board, all mounted together.
Splitting the application, API and controller
Flutter handles browsing and forms. Flask owns account sessions, reservations, codes and equipment availability. SQLite stores the persistent records. The ESP32 only needs to collect a code and act on the server’s response.
The project reports capture the earlier architecture before every implementation choice was settled:
The client and controller both initiate HTTP requests. There is no server-pushed command channel to the locker. Accepting a code returns the compartment and operation in that same request, keeping the firmware independent of the catalogue and reservation screens.
Finding a room and remembering the booking
The app has Rooms, Equipment, Records and Account sections. Its LayoutBuilder chooses an extended navigation rail at widths of at least 1,000 pixels, a compact rail from 800, and bottom navigation below that. Catalogue cards adjust from one to three columns.
Text search runs in Flutter, while Flask applies structured filters for seats, sockets, room type and availability. Numeric filters mean “at least”: requesting twenty seats should also return a room with thirty. Room details include computers, oscilloscopes, signal generators, multimeters, projectors and whiteboards.
A room reservation sends a Unix start timestamp, duration and optional reason. The app converts minutes into seconds; the API requires a minimum of fifteen minutes and checks for existing bookings before inserting the reservation. The archived overlap check has an edge case when a new booking entirely surrounds an existing one, described in the technical notes.
The data model keeps catalogues and bookings separate. rooms describes facilities; equipments holds the compartment and current availability. reservations and equipment_reservations connect users to those items and store their start and end times. The Records endpoint returns up to one hundred of each user’s bookings in each category, ordered by start time.
A reservation becomes a pickup code
Equipment follows a different schedule: borrowing starts when the request is accepted. Flask checks the session, looks up the item, inserts an equipment reservation and marks the item unavailable. It then creates a six-digit PIN and keeps the pending operation in memory:
codes[code] = {
"expires": time.time() + 120,
"equipment_id": equipment_id,
"user_id": user_id,
"type": "get"
}
This excerpt from the reservation route captures the distinction between a booking and permission to open a compartment. The booking lives in SQLite. The code identifies one pending pickup, with its user, equipment and operation.
A scheduled callback runs after 120 seconds. If the code is still pending, it releases the equipment, annotates the reservation as uncollected and removes the code. Entering the PIN in time sends it to /api/locker/{code} with the locker’s shared secret. The API consumes it and replies with an operation such as get and a compartment such as 1A.
The implementation has a client/API mismatch worth preserving in its documentation: the equipment form labels duration in minutes but sends the raw value, which Flask interprets as seconds. Unlike the room form, it needs that conversion corrected. The optional reason field is also displayed without being included in the request.
Eight states and eight relay outputs
The firmware is C++ using the Arduino framework, built with PlatformIO. Its state machine has S_IDLE, S_INPUT, S_ABORTED, S_PROCESSING, S_INVALID, S_GET, S_PUT and S_ERROR states.
Input uses a 3-by-4 keypad. * deletes the previous digit and # cancels; entering the sixth digit submits automatically. While processing, the ESP32 sends the HTTP request and parses the JSON response with ArduinoJson. A 400 response selects the invalid-code state, while network and other response failures select the error state.
On success, the 20-by-4 LCD names the compartment and tells the borrower to collect or return the item. Compartments 1A–1D and 2A–2D map to eight GPIO outputs. Relays start HIGH and activate LOW. The selected output stays active for ten seconds before the controller returns to idle. This version uses blocking delays during that interval, so it handles one interaction at a time.
Returning the item and recovering from failure
The Records screen requests a new code to return equipment. Flask checks the most recent borrower’s user ID and the item’s availability. A return code uses put; requesting it alone does not make the item available. That change happens when the locker endpoint accepts the code.
There is still a gap between accepting a code and knowing what happened physically. The controller has no door-closure or item-presence sensor. The API can mark an item returned before anyone places it inside, and a lost response can consume a pickup code without the relay opening. A server restart also loses pending codes and timers while leaving reservations in SQLite.
Persisting pending operations and adding physical feedback would be the next steps. I would also make the reservation, availability and code changes transactional: the current database helper opens and commits each query separately. The original HTTP tests cover the API surface, but depend on shared data and do not establish recovery behavior; their locker test never reaches its request because its code is reset in setup.
The repository includes the Flutter client, firmware, reports and original Android, Linux and web builds. The former hosted application is retired. The technical reference records setup changes and the remaining prototype issues, including return-code expiry.