Files
MSE-PI-E2EEDA-Plein-de-eeee…/report/main/04-design.typ

344 lines
19 KiB
Typst

#import "/metadata.typ": *
#import "architecture/description.typ": *
#import "03-analysis.typ": *
#pagebreak()
= #i18n("design-title", lang:option.lang) <sec:design>
#add-chapter()[
This section describes the project design from an holistic and then from a reductionist standpoint.
== Complete application <sec:design:complete>
The overall application design has been done without the use of any @llm.
The global architecture as given in @fig:top-level-architecture can be precised with @fig:top_level_archi_interface.
This precisions gives the various units to design and implement to have the complete system up and running.
Moreover, it also pinpoint the interfaces between each unit.
#top_level_archi_interface
Each unit design and implementation has been assigned to a team member, as indicated in @tab:work_repartition.
#figure(
table(
columns: (auto, auto),
align: center,
table.header("Unit", "Assigned member"),
[Nodes],[Adrien],
[Gateway],[Djelal],
[Database & API],[Rémi],
[User interface (Display)],[Ibrahima],
[Data validation & Forecasting],[Alison],
),
caption: [Work repartition amongst team member],
)<tab:work_repartition>
=== Nodes_interface
No @llm has been used for the design of this interface.
The interface between the nodes and the gateway is defined as follows.
#nodes_interface
=== Db_set & db_API <sec:design:interface:db>
#include "database/interface.typ"
=== Teams API
The teams @api is given by Microsoft.
As giving an exhaustive list of all available request would only bring noise to this document, the list of request ultimately used is given in @sec:impl:ui
== Nodes
No @llm has been used for the nodes design.
Each node firmware runs on a hardware from Nordic.\
Each node contains a sensor for the CO2 level, an hygrometer and a thermometer.
An additional sensor is added to some to get the windows binary state (open/closed).\
Each node has to broadcast its data over @ble.
The time between two broadcast is defined to be between 2 minutes and 6 hours.\
Considering these elements, the class diagram in @fig:nodes_class_diagram describes the firmware construction.
#let nodes_class_diagram = [
#figure(
image("../resources/img/nodes_class_diagram.svg", width: 90%),
caption: [Nodes class diagram]
) <fig:nodes_class_diagram>
]
#nodes_class_diagram
The generalisation of the "Sensor" class allows for logically efficient data retrieval during each loop iteration.
These iteration are described in the sequence diagram in @fig:nodes_sequence_diagram.
The maximal sleep time of 30 minutes allows to reduce the battery usage while keeping a reactive detection.\
The threshold of 400 ppm for the @co2 level is the level for a room without anyone in it.
#let nodes_sequence_diagram = [
#figure(
image("../resources/img/nodes_sequence_diagram.svg", width: 85%),
caption: [Nodes sequence diagram]
) <fig:nodes_sequence_diagram>
]
#nodes_sequence_diagram
== Gateway
DeepL Pro was used to translate this section from French to English, followed by @llm assistance to improve the academic style.
=== Role and architecture
The gateway runs permanently on a Raspberry Pi 4 installed in the
classroom. It listens to @ble advertising packets from the Thingy:52
nodes, decodes the sensor values, adds a @utc timestamp and publishes
the data to the @mqtt broker.
=== Technology choices
Python was chosen primarily because bleak and paho-mqtt directly
address the two communication needs of the gateway @ble and @mqtt.
The Raspberry Pi 4 has more than enough resources to run Python, unlike
the Thingy:52 which runs on a constrained microcontroller requiring C
and Zephyr. All code is encapsulated in a Gateway class whose structure is
described in @fig:gateway_class_diagram.
#let gateway_class_diagram = [
#figure(
image("../resources/img/class_diagram.svg", width: 50%),
caption: [Gateway class diagram]
) <fig:gateway_class_diagram>
]
#gateway_class_diagram
=== Asynchronous management with asyncio
The @ble scan and @mqtt publication run concurrently the scanner
permanently listens for advertising packets while the @mqtt client
maintains the broker connection in the background. Python's asyncio
module handles both in a single thread without blocking. The @mqtt
client runs in a dedicated background thread via loop_start(),
which allows publishing messages without interrupting the scan loop.
=== @ble:short architecture: passive advertising
The gateway listens passively to @ble advertising packets without
establishing any connection. This matches the firmware architecture —
the Thingy:52 manages its own sleep/wake cycle and broadcasts data
at variable intervals between 2 minutes and 6 hours. Packet filtering works in two steps. First, only packets with
company_id = 0xffff in the @ble manufacturer data are kept this
identifier is defined in the firmware specification. Second, only
packets whose payload is exactly 14 bytes are processed, matching
the format defined in the architecture document. The startup and data collection flows are described in @fig:gateway_sequence_startup and @fig:gateway_sequence_data (@appendix:gateway_seq).
#import "../resources/helper.typ": *
#let sequence_startup_chronos = {
import chronos: *
let g = actor("gateway",
disp_name: [Gateway\ (Raspberry PI)],
show-bottom:false
)
let b = actor("broker",
disp_name: [@mqtt:short Broker\ (RabbitMQ)],
show-bottom:false
)
g.display
b.display
_sep("Startup")
sync(g, g, [load config.json])
sync(g, b, [connect (@mqtt:short, @tls:short, auth)])
sync(b, g, [connected], dashed: true)
sync(g, g, [start @ble:short scan])
}
#let gateway_sequence_startup = [
#figure(
// image("../resources/img/sequence_startup.svg", width: 50%),
chronos.diagram(sequence_startup_chronos, width: 6.5cm),
caption: [Gateway sequence diagram Startup]
) <fig:gateway_sequence_startup>
]
#gateway_sequence_startup
=== Payload decoding
The firmware encodes sensor values as consecutive key/value pairs in
the manufacturer data payload, as defined in @tab:nodes_interface_content. Each key is one byte identifying the sensor type, followed by the
value bytes. Temperature is stored as an integer multiplied by 10 to avoid floating point on the embedded side 25.3°C is stored as 253. The company_id
is handled by the BLE stack and is not present in the raw bytes, so
decoding starts at index zero.
=== @mqtt:short interface
The gateway publishes on topic {gateway_id}/{thingy_mac}/update.
The gateway_id identifies the Pi and is mapped to the campus tag
in the database. The thingy_mac identifies the sensor and is mapped
to the room tag. This mapping is handled by the database manager. The timestamp is added on the gateway side to avoid clock synchronisation issues on the embedded system. The published JSON payload contains only the fields that were present
and valid in the received packet:
```json
{
"timestamp": "2026-05-13T12:57:59Z",
"temp": 27.8,
"humidity": 29,
"co2_ppm": 400,
"window_open": false,
"battery": 85
}
```
=== Security <sec:design:gateway:security>
The broker connection uses MQTTS — @mqtt over @tls — on port 80
(the standard port 8883 is blocked by the school network firewall).
Authentication uses username and password. The password is never in
the source code or versioned files — it is loaded at runtime from
secrets/mqtt.env via the MQTT_PASSWORD environment variable,
injected by systemd.
=== Configuration and deployment
All deployment-specific values are in config.json, excluded from
the repository. A config.example.json template is provided for
new deployments. Each Pi has its own config.json with a unique
gateway_id. The gateway runs as a systemd service that starts automatically on
boot and restarts on crash with a ten-second delay. This was
validated during testing — after a reboot, the service restarted
without any manual intervention.
=== Remote access via Tailscale
The Raspberry Pi is permanently deployed in a classroom. Tailscale was
installed on the Pi and on the developers' machines to allow @ssh access
from anywhere without configuring firewall rules or port forwarding.
Each device gets a stable IP in the Tailscale network regardless of
the physical network, making remote monitoring and code deployment
straightforward. This allowed monitoring gateway logs in real time and deploying code
updates remotely, particularly during the integration with our custom node firmware.
== Database & @api:short
#include "database/design.typ"
== User interface
The user interface is structured around two main views: an interactive floor map serving as the dashboard, and a per-room detail page.
*Layout and information hierarchy*
As illustrated in @fig:design_dashboard, the dashboard follows a two-column layout: the floor plan occupies the main area on the left, while a fixed legend sidebar sits on the right. This arrangement keeps the map (the primary focus) dominant, while the legend remains accessible without competing for attention. Navigation to the detail page is triggered by clicking a room directly on the map, preserving spatial context.
*@co2 color coding*
@co2 concentration is the central metric of the project. A six-level color scale was defined, ranging from green (Excellent, below 800 @ppm) to red (Critical, above 2000 @ppm), with intermediate levels covering Good, Moderate, Poor, and Very Poor. This encoding leverages pre-attentive visual processing: the air quality of every room is readable at a glance, without reading any numbers. The same scale is applied consistently across all views map badges, detail page, and history table.
*Room badges on the map*
Each sensor-equipped room is represented by a badge overlaid directly on the floor plan, displaying the room identifier and its current @co2 value. The badge background color reflects the @co2 level. Rooms without data display a neutral badge with a dash, so the absence of information is explicit rather than invisible.
*Tooltip*
Hovering over a room reveals a tooltip with the full set of current metrics: @co2, temperature, humidity, and window state. This allows quick comparison between rooms without leaving the map view.
#let design_dashboard = [
#figure(
image("../resources/img/ui_images/design_dashboard.png"),
caption: [Dashbord design]
) <fig:design_dashboard>
]
#design_dashboard
*Detail page structure*
As illustrated in @fig:design_details ,the detail page separates two temporal concerns: the left panel shows current metrics with @co2 as the dominant value, while the right panel displays the 24-hour history as a paginated table. This left-to-right structure mirrors a natural reading of "now" then "recent past".
*Feedback and data freshness*
A "Last updated" timestamp is displayed on both views so the user always knows whether the data is current. When no reading is available for a room, an explicit message is shown rather than leaving the space empty, avoiding any ambiguity between missing data and a zero value.
#let design_details = [
#figure(
image("../resources/img/ui_images/design_details.png"),
caption: [Details page design]
) <fig:design_details>
]
#design_details
#pagebreak()
== Physical model
Three physical models have been developed to represent the evolution of @co2 concentration within a room as a function of several parameters, which are detailed in the following sections.
@llm has been only used for some cases to help debugging the model.
=== Open data
Open data are analysed to characterise the temporal evolution of @co2 concentration in a room and to develop a physical model. Numerous studies have been conducted on @co2 levels in classrooms and meeting rooms, following the Covid-19 pandemic.
The relevant open data are listed below:
- Open data from the municipality of St. Gallen #cite(<python_asyncio>): installation of @co2 sensors in indoor spaces and meeting rooms of the city administration. The available data are the time evolution of @co2 concentration in rooms. However, these data provide neither the number of occupants nor the room volume, making the analysis difficult. Further information was requested to the municipality of St. Gallen, however no additional data were available.
- Open data related to the study #cite(<twardella_effect_2012-1>): measurement of @co2 concentration, temperature and humidity in several classrooms under different scenarios: usual (adjusted mechanical ventilation as usual and window-opening allowed), worse (no mechanical ventilation and no window-opening) and better (up-regulated mechanical ventilation and no window-opening). The case of interest for this study is the worse-case scenario. However, the available input parameters are insufficient to accurately determine the relationship between the evolution of @co2 concentration, the number of occupants and the room volume, as only mean values are provided.
- Simaria online simulator based on the project "Fresh Air in Swiss Schools" (cf. #cite(<Simaria>)), developed by the @ofsp to support school teachers in maintaining good indoor air quality throughout the day. The available input parameters are the room volume, the number of occupants, the number and duration of school periods and the number and duration of breaks. The simulator then generates a graph showing the evolution of @co2 concentration over time. This model is quite complete and was mainly used to develop the physical model. The only limitations are that the maximum number of occupants is restricted to 50, the evolution of @co2 concentration is given through qualitative air quality indicators such as good-excellent (from 400 to 1400 @ppm), sufficient (from 1400 to 2000 @ppm) and unacceptable (over 2000 @ppm), the initial @co2 concentration is not given and the temporal evolution of @co2 is linear. However, the dataset still provides an overall trend and a document provides the ranges associated with these indicators.
- Open data related to the study #cite(<INRS_Study>): real-time @co2:short level measurement in workspaces : installation of @co2 sensors in a meeting room, an office room and a room in a secondary school. The input parameters are the volume of the room, the number of occupants and the evolution of @co2 concentration over time. This study also contributed to the refinement of the physical model.
=== Characteristics of Provence classrooms
It was decided to study Space A of Provence building as part of this project.
The characteristics are presented in the table below. These data are used in the models described in the next sections.
#figure(
table(
columns: (auto, auto, auto, auto),
align: center,
table.header("Classroom [-]", "Volume [m³]", "Maximal student capacity [-]", "Total windows surface [m²]"),
[A2],[308],[40],[3.23],
[A3],[480],[78],[22.12],
[A4],[326],[48],[3.23],
[A5],[274],[32],[22.12],
[A6],[323],[58],[3.23],
[A7],[272],[36],[5.53],
),
caption: [Characteristics of Provence classrooms - Space A],
)<tab:Provence_characteristics>
=== No window-opening model
The data flow diagram of the first physical model is given in @fig:physical_model_no_window_opening_v1. The open data input parameters are the volume of the room, the number of occupants and the time evolution of @co2 concentration. Only data corresponding to closed windows are considered in this first case. These data are analysed using a python program, which plots the evolution of @co2 concentration for each case (as a function of room volume and number of occupants). The program then calculates the air volume per person and plot a graph representing the time at which the threshold is reached as a function of the given air volume per person. It should be noted that the program only considers data with an initial @co2 concentration ranging from 400 @ppm to 600 @ppm, which results in an approximation. The resulted affine function is then plotted and is employed to determine the time at which the threshold is reached based on the user-input parameters (room volume and number of occupants).
#figure(
image("../resources/img/Physical model/data flow diagram no window opening v1.png"),
caption: [Data flow diagram of the physical model no window-opening v1]
) <fig:physical_model_no_window_opening_v1>
#pagebreak()
A second physical model has been developed in order to simulate the time evolution of @co2 concentration (cf. @fig:physical_model_no_window_opening_v2). This model take into account the initial @co2 concentration to more accurately simulate the @co2 level evolution and predict the time reaching the threshold. The formula used in the physical model to represent the time evolution of @co2 concentration is given below:
$
C_"CO2" (t) = C_"CO2" (t=0) + frac(N.Q_"CO2_prod".t,V)
$
where $C_"CO2" (t=0)$ represents the initial @co2 concentration [@ppm], N denotes the number of people [-], V, the room volume [$m^3$], and $Q_"CO2_prod"$, the @co2 flow rate per person [l/h].
#figure(
image("../resources/img/Physical model/data flow diagram no window opening v2.png", width: 90%),
caption: [Data flow diagram of the physical model no window-opening v2]
) <fig:physical_model_no_window_opening_v2>
=== Window opening model
The previous model provides an overview of the time evolution of @co2 concentration while assuming that the windows of the considered room remain closed. The physical model below (@fig:physical_model_window_opening) represents the @co2 concentration evolution after windows opening, taking into account Space A in the Provence building used by HES-SO (cf. @tab:Provence_characteristics).
#figure(
image("../resources/img/Physical model/data flow diagram window opening .png", width: 90%),
caption: [Data flow diagram of the physical model considering window-opening]
) <fig:physical_model_window_opening>
The following formula, derived from the SIA 382/1 standard and #cite(<residential_ventilation>) is used to model the evolution of @co2 concentration over time after windows-opening, considering an air exchange rate.
$
C_"CO2" (t) = (C_"CO2_indoor" (t=0) - C_"CO2_outdoor" - frac(0.001 . Q_"CO2_prod", Q_"air")) . exp (frac(-Q_"air", V) . t) \
+ C_"CO2_outdoor" + frac(0.001 . Q_"CO2_prod", Q_"air")
$
where,
$C_"CO2_indoor" (t=0)$ #h(0.8cm) indoor @co2 concentration before window-opening [@ppm]
$C_"CO2_outdoor"$ #h(1.8cm) outdoor air concentration [@ppm]
$Q_"air"$ #h(3cm) incoming air flow rate [$m^3$/h]
$V$ #h(3.3cm) room volume [$m^3$]
$t$ #h(3.5cm) time [h]
The SIA standard specify an incoming air flow rate of 30 $m^3$/h. However, in this study, the total opening area of each room is known. The incoming air flow rate is therefore calculated based on the average incoming air velocity (assumed to be 0.5 m/s #cite(<heiselberg_characteristics_2001>)) and the total windows surface. It is worth noting that this program considers that all windows in each room are fully open during air renewal.
== Conclusion
The complete application is divided in explicit units as illustrated in @fig:top_level_archi_interface.
The interfaces between each units are described in @sec:design:complete.
Each of this interface allow the unit to be independently implemented as long as said interfaces are respected.
The design of each unit enables efficient review of their behaviour and efficient implementation.
]