← Back to projects

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.

Source code ↗
Gestire's assembled controller with an ESP32, eight-channel relay board, LCD and a hand entering a code on the keypad.

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.

Original concept drawing of a keypad and display beside a locker bank divided into FPGA, DETPIC, Arduino, Raspberry Pi and ESP32 equipment areas.
The locker-bank concept from our presentation. The implemented controller has eight outputs; this larger cabinet layout was a design proposal.

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.

The original hardware demo: keypad input, the controller's display and relay outputs responding to an equipment operation. Watch on YouTube ↗

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:

Original logical architecture separating presentation, room and equipment reservations, data managers and SQLite.
The original logical design separated room and equipment operations. University identity-provider integration remained a plan; the prototype uses local accounts.
Original deployment diagram linking a Flutter client, Python Flask application server, database and locker controller.
The original deployment drawing. The final firmware uses C++/Arduino rather than the proposed MicroPython, and SQLite runs as an embedded database rather than a separate database server.

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.

Original room-browser mockup showing laboratories, classrooms and offices with seats, sockets and facilities.
Room-browser design from the project presentation. The Flutter implementation evolved from these layouts.
Original Records screen design combining equipment and room reservations with start and end times.
The Records design puts both kinds of booking in one place. These interface images are original mockups, not final-build screenshots.

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.

Equipment reservation mockup with borrowing duration and a usage-location field for an FPGA kit.
The reservation design collects how long the equipment is needed.
Pickup-confirmation mockup displaying a six-digit locker code and a two-minute collection window.
The next step gives the borrower something they can enter at the locker.

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 implemented request path. Consuming the code happens before the ESP32 activates the relay.

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.

Original equipment-return mockup with an optional observations field over the reservation history.
The proposed return screen included observations. The implemented return endpoint takes the session token and issues a code; it does not receive this observations field.

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.