diff --git a/report/bdd_architecture.typ b/report/bdd_architecture.typ new file mode 100644 index 0000000..2c4d369 --- /dev/null +++ b/report/bdd_architecture.typ @@ -0,0 +1,47 @@ +#import "/metadata.typ": * +#import "/tail/bibliography.typ": * +#import "/tail/glossary.typ": * + +#import "/resources/mse-title-page.typ": * + +#show:make-glossary +#register-glossary(entry-list) +#set text(region: option.region) + +//------------------------------------- +// Template config +// + +#show: thesis.with( + option: option + (type: "final"), + doc: doc, + school: school, + date: date, + tableof: tableof, + logos: logos, + custom-title-page: mse-title-page( + title: doc.title, + subtitle: "Architecture", + date: date + (submission: date.specification-submission), + ), +) + +// Declaration of honor will be remove with the next release of the HEI-SYND-THESIS template (probably next week) + +//------------------------------------- +// Content +// +#include "/main/specifications/03-context.typ" +#include "/main/architecture/description.typ" + +//------------------------------------- +// Glossary +// +#heading(numbering:none, outlined: false)[] +#make_glossary(gloss:gloss, title:i18n("gloss-title")) + +//------------------------------------- +// Bibliography +// +#make_bibliography(bib:bib, title:i18n("bib-title", lang: option.lang)) + diff --git a/report/main/01-abstract.typ b/report/main/01-abstract.typ new file mode 100644 index 0000000..3c5e5f2 --- /dev/null +++ b/report/main/01-abstract.typ @@ -0,0 +1,30 @@ +#import "/metadata.typ": * +#pagebreak() +#heading(numbering:none)[#i18n("abstract-title", lang:option.lang)] + +#option-style(type:option.type)[ + The abstract serves as a concise summary of your entire thesis, encapsulating key elements on a single page such as: + - General background information + - Objective(s) + - Approach and method + - Conclusions +] + +#lorem(50) + +#lorem(50) + +#lorem(50) + +#v(2em) +#if doc.at("keywords", default:none) != none {[ + + _*#i18n("keywords", lang: option.lang)*_: + + #enumerating-items( + items: doc.keywords, + italic: true + ) +]} + +#todo("Write abstract or remove section") diff --git a/report/main/02-introduction.typ b/report/main/02-introduction.typ new file mode 100644 index 0000000..42885ed --- /dev/null +++ b/report/main/02-introduction.typ @@ -0,0 +1,26 @@ +#import "/metadata.typ": * +#import "specifications/02-introduction.typ": * +#pagebreak() += #i18n("introduction-title", lang:option.lang) +This document contains the report for the @pi team "plein de eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" conducted during the Spring Semester 2025-2026. + +This project aims at delivering the best time and duration to air a room. +This delivery requires to record the room data, namely the air temperature, humidity, @co2 level and window opening status. +It also requires to store and display the recorded data. +Lastly, a notification is sent to the room should it be aired using its associated teams notification user. + +This document contains the project motivations, objectives, analysis, design implementation and takeaways in dedicated ordered sections. + +At the start of every subsection, the usage of @llm is indicated and stands for the corresponding subsection. +Any absence of such indication is to be understood as the work being fully done by a @llm, with the tasked student only standing as final acknowledgement. +The students associated with the subsection still is to be considered as author and reference on the topic amongst the team.\ +The sharing of tasks between the students is indicated in @sec:design. + +#infobox()[ + The implementation of the various unit are made available in the github repository available at + #align(center)[ + #link("https://github.com/PI-E2EEDA/Plein-de-eeeeeeeeee-project")[https://github.com/PI-E2EEDA/Plein-de-eeeeeeeeee-project] + ] + + Explicit access right may be required and shall be asked to the #link("https://github.com/fooltinkerer")[owner (Fooltinkerer)]. +] \ No newline at end of file diff --git a/report/main/03-analysis.typ b/report/main/03-analysis.typ new file mode 100644 index 0000000..0a78f33 --- /dev/null +++ b/report/main/03-analysis.typ @@ -0,0 +1,8 @@ +#import "/metadata.typ": * +#import "specifications/03-context.typ":* +#pagebreak() += #i18n("analysis-title", lang:option.lang) + +#add-chapter()[ + #spec_context +] diff --git a/report/main/04-design.typ b/report/main/04-design.typ new file mode 100644 index 0000000..94c4360 --- /dev/null +++ b/report/main/04-design.typ @@ -0,0 +1,343 @@ +#import "/metadata.typ": * +#import "architecture/description.typ": * +#import "03-analysis.typ": * +#pagebreak() += #i18n("design-title", lang:option.lang) + +#add-chapter()[ + This section describes the project design from an holistic and then from a reductionist standpoint. + == Complete application + 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], + ) + === 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 + #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] + ) + ] + #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] + ) + ] + #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] + ) +] +#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] + ) +] +#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 + +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] + ) +] +#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] + ) +] +#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(): 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(): 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()), 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(): 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], +) + +=== 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] +) + +#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"), + caption: [Data flow diagram of the 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 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"), + caption: [Data flow diagram of the physical model considering window-opening] +) + +The following formula, derived from the SIA 382/1 standard and #cite() 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()) 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. + +] diff --git a/report/main/05-implementation.typ b/report/main/05-implementation.typ new file mode 100644 index 0000000..88e43f4 --- /dev/null +++ b/report/main/05-implementation.typ @@ -0,0 +1,461 @@ +#import "/metadata.typ": * +// highlight box for security callouts +#pagebreak() += #i18n("implementation-title", lang:option.lang) + +#add-chapter()[ + == Complete application + While the complete application did not change from the Design in @sec:design, some adjustments has been added as improvements. + Namely, the _nodes\_interface_ has been added by a key/value, available in @tab:nodes_interface_addition + #figure( + table( + columns: (auto, auto, auto, auto), + align: center, + table.header("name", "key", "data construction", "data size"), + [Battery percent of charge],[0x05],[Integer giving the raw percentage],[1B], + ), + caption: [Nodes interface addition during the implementation], + ) + This addition in the _nodes\_interface_ has been motivated by the nodes uptime. + Observations reported that their battery ran out after few days instead of the expected weeks.\ + Hence, the battery level has been added to the values reported by the nodes. + == Nodes + No @llm has been used at any point during the nodes implementation. + + The nodes implementation followed the design for the major part. + The implementation has ultimately been done in C using ZephyrOS, hence the use of + classes is in spirit only. + The choice for this embedded @os was motivated by the symbiosis with Nordic hardware, alongside the + opportunity for the assigned student to try this solution out. Following are the major elements that differs from the nodes design. + + === Window opening status + The window opening status is observed with a switch. + This switch is expected to be pressed by a closed window, and released when the window is opened.\ + The switch is active low to ensure that a node without switch does not report the room window as opened. + + The wiring is configured such as the switch _NC_ pin shall be wired to the thingy _EXT0_ pin. + + #pagebreak() + === Battery level monitoring + The nodes monitoring has been observed as very scarce as its values are sent only in the BLE frames. + Furthermore, the nodes battery has been left around many question marks during the first integration.\ + Hence, the _battery_percent_unit_ has been added to the nodes. + This unit retrieve the battery voltage and extrapolate the level of charge, following the #link("https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/boards/nordic/battery")[nordic example] #footnote[https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/boards/nordic/battery]. + The resulting class diagram is given in @fig:nodes_class_diagram_impl + #let nodes_class_diagram_impl = [ + #figure( + image("../resources/img/nodes_class_diagram_impl.svg"), + caption: [Nodes class diagram following implementation] + ) + ] + #nodes_class_diagram_impl + === @ble period + The designed @ble behaviour doe snot comply with the protocol specifications. + As such, the @ble sending status is manually managed. + As described in @fig:nodes_sequence_diagram, the advertisement of data is done once per supervisor loop.\ + During this advertisement, the @ble data are update and the sending of data is activated. + After a short amount of time, the sending of data is disabled until the next advertising. + + The amount of time during which the sending is active has been empirically set to 500ms. + This time window ensures that the gateway is able to receive the measurements. + === Battery usage + Each nodes, with a complete battery, has been observed as running for 3 days before shutting down. + This is obviously too high and the reason has been found thanks to Rémi.\ + The air quality sensor CCS811 working frequency is described in @tab:ccs811_drive_mode + #figure( + table( + columns: (auto, auto), + align: center, + table.header("CCS811 drive mode value", "air quality sensing period [s]"), + [0], [disabled], + [1 (default)],[1], + [2], [10], + [3], [60], + [4], [0.25] + ), + caption: [Air quality sensor CCS811 drive mode], + ) + Setting the drive mode to 3 avoid 59 measures, hence improving the battery usage accordingly.\ + The battery standard usage is then demonstrated with @fig:nodes:battery_consumption + #let nodes_battery_consumption = [ + #figure( + image("../resources/img/nodes_battery_consumption.png", width: 90%), + caption: [Node battery consumption over time] + ) + ] + #nodes_battery_consumption + In @fig:nodes:battery_consumption, the peaks in the consumption are occurring + when the air level is measured. + The period for these peaks is 60 seconds, as intended by the CCS811's drive mode 3 + === Deployment + #let nodes_deployment_simple = [ + #figure( + image("../resources/img/node_naked.png", width: 90%), + caption: [Node deployment without window opening status sensor] + ) + ] + #let nodes_deployment_window = [ + #figure( + image("../resources/img/node_window.png",width: 75%), + caption: [Node deployment with window opening status sensor] + ) + ] + #let text_deployment = [ + Several nodes were deployed on site. + All but four were setup without window sensor. + As such, they did not need anything else that some glue to fix them on the wall. + This deployment is illustrated with the picture in @fig:nodes_deployment_simple. + ] + + #let uglify = false + + #if uglify [ + #text_deployment + #grid( + columns: (1.5fr, 1fr), + column-gutter: 2em, + align: center+horizon, + nodes_deployment_simple, + nodes_deployment_window + ) + ] else [ + #grid( + columns: (1.5fr, 1fr), + column-gutter: 2em, + grid.cell(align: left+top, text_deployment), + grid.cell(rowspan: 2, align: center+horizon, nodes_deployment_window), + grid.cell(align: center+bottom, nodes_deployment_simple) + ) + ] + + #v(1em) + For the remaining, window sensor has been wired accordingly. + This deployment is illustrated with the picture in @fig:nodes_deployment_window. + In @fig:nodes_deployment_window, the switch telling if the window is opened is glue in a way it is pressed when the window is closed and released otherwise. + The window is also opened, leading the switch to be released. + == Gateway +@llm was used for debugging specific technical code issues (@ble filtering, +payload decoding, asyncio thread safety, @mqtt reconnection) and to +support the redaction of this section. + +=== From design to implementation + +The design specified passive @ble advertising with @uuid filtering. +The final implementation differs on several points discovered during +development and integration with our custom node firmware.. + +=== First implementation: GATT connections + +The first approach used active GATT connections with the stock Nordic +firmware. The Pi connected to each Thingy:52 and subscribed to +characteristic notifications — temperature (ef680201), humidity +(ef680203) and @co2 (ef680204). This implementation allowed running an overnight test with two +Thingy:52 nodes placed in two separate rooms for seven hours, windows +closed. The first node was in a room with four occupants — @co2 rose +progressively from 400 @ppm to a peak of 1071 @ppm. The second was in +an empty room and stayed stable between 400 and 465 @ppm. This +confirmed that the data was meaningful and the communication chain +was working. However, this approach hit a hard wall with BlueZ — the Linux +Bluetooth stack does not support scanning and connecting +simultaneously. Any connection attempt while the scanner is active +raises org.bluez.Error.InProgress. The workaround was to stop the +scanner before each connection and restart it afterwards, which +introduced race conditions when multiple nodes were detected at the +same time. Some nodes also occasionally ignored connection attempts, +blocking the script until a ten-second timeout was added via +asyncio.wait_for(). + +=== Switch to passive advertising + +After a team review confirming that our custom node firmware uses +broadcasting rather than GATT, the implementation was fully +refactored. The GATT connection logic was removed entirely and +replaced by a passive BleakScanner. + +=== Discovering the right filter + +The original design used the service @uuid ef680100 to identify +Thingy:52 packets — this @uuid was found during the stock firmware +exploration phase and worked correctly at the time. When integrating +with our custom node firmware, no packets were detected despite the +node being physically nearby. A Python investigation script +(scan_uuid.py) was written to display all @ble devices in range with +their raw data. This revealed that our custom firmware does not +announce any service @uuid — instead it uses the manufacturer data +field with company_id = 0xffff, as defined in the firmware +specification. Note that 0xffff is not an officially registered +company identifier and should ideally be replaced by a dedicated +one such as 0x025A (HES-SO Valais) in a future iteration. + +The @uuid filter was replaced by a company_id filter. The scan script +also confirmed that the raw payload matched the expected format — +14 bytes with keys 0x01 to 0x05 — validating the consistency between +the firmware and the gateway. + +=== Integration challenges + +*False positives from third-party devices.* Several @ble devices in +the environment also use company_id = 0xffff, triggering the +gateway filter and flooding the logs. Adding a strict 14-byte payload +size check eliminated them — the Thingy:52 payload is always exactly +14 bytes as confirmed by the firmware author. + +*Non-standard payload from one node.* One node (DC:06:D9:40:7A:CB) +was broadcasting a 13-byte payload with an unknown key 0x05, +apparently flashed with a different firmware version. The exact size +filter excludes it automatically. + +*@co2 sensor warm-up.* After each reboot, the @co2 sensor +returns 0xFFFFFFFF during its warm-up period. The gateway discards +these values silently to avoid publishing invalid data to the +database. A proper error reporting protocol would require agreement +across all components — gateway, database and UI — which was not +defined at specification time and is out of scope for this version. + +*Network firewall.* The school network blocks outbound connections +on port 8883. The broker was reconfigured to also accept connections +on port 80 as a workaround. + +*@mqtt disconnection.* In production, the gateway occasionally lost its +connection to the broker due to network instability. The most common +error cases — disconnection, publish failure and failed initial +connection — are now detected and trigger os.exit(1), allowing +systemd to restart the gateway automatically within 10 seconds. +This crash-and-restart approach is a pragmatic solution — a proper +implementation would handle reconnection logic explicitly without +crashing, but this was sufficient for the current version and was +validated overnight with two automatic recoveries. + +=== @ble channel hopping and deduplication + +@ble advertising uses three dedicated channels (37, 38 and 39). When +a node broadcasts, it sends the same packet on all three channels +in quick succession. The BleakScanner can capture the same packet +on multiple channels within the same window, resulting in the same +measurement being published multiple times to the broker. + +To address this, a deduplication mechanism was implemented — the +gateway maintains a cache of the last publication timestamp per MAC +address and ignores packets from the same node received within a +10-second window. This value was defined during a team meeting and +is sufficient to filter duplicates from the same broadcast frame +without missing the next one, which arrives at minimum 2 minutes +later. + +=== Remote debugging via Tailscale + +Tailscale was installed on the Pi and on the developers' machines to +allow @ssh access from anywhere. This was essential during the custom firmware integration phase — monitoring logs and pushing code updates remotely +made the debugging loop much faster. + +#pagebreak() + + == Database & API + #include "database/implementation.typ" + + #pagebreak() + == User interface + === Overview +The UI is a single-page Angular application providing real-time air quality monitoring for building rooms. It consists of two main views: an interactive floor-plan map and a per-room detail page, both backed by a @rest @api served by a Go gateway that reads from InfluxDB. + === Technology choices +_Why Angular over React, Vue and Svelte?_ + +Four front-end frameworks were considered: React, Vue 3, Svelte, and Angular. + +*React* is the most widely adopted option and has the largest ecosystem. However, it is a library rather than a framework: routing, @http, forms, and dependency injection all require third-party packages, which increases maintenance burden in a team project. + +*Vue 3* offers a gentler learning curve and a Composition @api close in spirit to Angular Signals. It was a serious candidate, but its dependency injection model is weaker and its TypeScript support less strict than Angular's by default. + +*Svelte* compiles components to vanilla JavaScript, producing very small bundles. However, it lacks a built-in @http client, a mature DI system, and established patterns for large-scale reactive data flows. + +*Angular* was chosen for the following reasons: + +*• Built-in @http:short client with interceptors* : Basic Auth injection and error handling are first-class features, not library choices. + +*• Dependency injection* : Services are singletons by default (providedIn: 'root'), making shared state and polling logic straightforward to centralise. + +*• Strong TypeScript integration* : The compiler catches type mismatches between @api responses and component models at build time, which is valuable given that the backend response format (raw InfluxDB rows) required explicit mapping. + +*• Signals* : Reactive local state without the verbosity of RxJS BehaviorSubject for simple derived values, while RxJS is retained for asynchronous streams (@http polling). + +*• Team familiarity* : Prior exposure to Angular reduces framework-specific overhead and keeps focus on domain logic. + +The main trade-off is bundle size and framework verbosity: Angular produces larger initial bundles than Svelte or Vue and requires more boilerplate for simple components. For a real-time monitoring dashboard served over a local network, this was considered acceptable. + +=== Interactive Room Map (room-map) +#let room-with-data = [ + #figure( + image("../resources/img/ui_images/dashbord_room_with_data.png", width: 100%), + caption: [Room with data], + alt: "Full dashboard view: floor plan with CO₂ badges, legend sidebar visible, with room data and different CO₂ colors" + ) +] +#room-with-data + +As illustrated in @fig:room-with-data the floor plan is a static @svg file served as a public asset and injected via innerHTML after being fetched over HTTP. A separate @svg overlay, sharing the same viewBox, renders @co2 badges positioned on each room’s geometric center. + + +*Pan & zoom* is implemented with pointer events and CSS transform: translate / scale on a canvas
. Touch support is enabled via touch-action: none. The wheel event listener is registered outside Angular’s zone (NgZone.runOutsideAngular) to avoid triggering change detection on every scroll tick. A fitToView() call is deferred 400 ms after ngAfterViewInit to ensure the @svg dimensions are known before computing the initial scale. + +*Polling *is set to 30 seconds, fetching /api/v1/rooms/:id/current for each room with a sensor in parallel via forkJoin. The last-updated timestamp reflects the moment the @http response is received (browser local time), not the sensor timestamp. + +=== Room Detail Page (room-details-panel) +#let room-details-panel = [ + #figure( + image("../resources/img/ui_images/detail_panel.png", width: 100%), + caption: [Room details panel], + alt: "Detail page of a room with real sensor data: CO₂ hero card, metric cards, and history table with several rows." + ) +] +#room-details-panel + +As illustrated in @fig:room-details-panel the page is split into a fixed-height header, a left panel showing current metrics (@co2 hero card, temperature, humidity, window state), and a right panel showing a paginated 24-hour history table. + +A single combineLatest pipeline, gated by takeUntilDestroyed, merges three concurrent streams: room metadata, latest reading (polled every 15 s), and history (polled every 15 s). This avoids multiple independent subscriptions and ensures all three values are updated atomically on each cycle. + +When the @api returns no data, the UI displays explicit messages rather than leaving panels empty, preventing any ambiguity between missing data and a zero value. + +@co2 levels are determined by thresholds defined in @co2\-levels.config.ts, shared across the map badges, the detail page, and aligned with the Go gateway and the Teams notification service. + +=== REST Service Integration +The Go @api exposes endpoints under /api/v1/ with Basic Auth and a CORS policy restricted to \*.e.kb28.ch. Two adaptations were required: +1. *Angular dev proxy* (proxy.conf.json) : Forwards /api requests from localhost:4200 to https://api.db.e.kb28.ch, bypassing the browser CORS restriction during development. +2. *Response mapping* : The Go @api returns raw InfluxDB rows (co2_ppm, temp, window: bool, …). A toSensorReading() function in SensorService normalises these fields into the app’s SensorReading model before they reach any component. +A functional basicAuthInterceptor is registered via provideHttpClient(withInterceptors([…])). It is a no-op when credentials are empty, allowing unauthenticated development against a server with auth disabled. + +=== CI/CD Pipeline + +Two GitHub Actions workflows cover the UI. The first (`angular-ci.yml`) runs on every pull request targeting `main` and acts as the quality gate. The second (`ui.yml`) extends this into a full build-and-deploy pipeline on every push to `main`. + +==== Quality gate (pull-request checks) + +The `angular-ci.yml` workflow runs on Node 20, scoped to the `ui/` path, and executes the following steps in order: + +*• TypeScript compilation* : `tsc --noEmit` catches type errors independently of the build step. + +*• Lint* : ESLint enforces code style rules across all source files. + +*• Prettier check* : `format:check` fails the build if any file is not formatted, keeping the diff clean in reviews. + +*• Dev and production builds* : Both configurations are built to catch environment-specific issues early (e.g. a missing prod-only environment field). + +*• Unit tests* : Karma runs headless in ChromeHeadless with code coverage enabled. The coverage report is uploaded as a GitHub Actions artefact (7-day retention). + +==== Build-and-deploy pipeline (main branch) + +As illustrated in @fig:ci-cd, the `ui.yml` workflow is structured as three sequential jobs gated by `needs`: + +*1. Build & test (`ci`)* — identical quality checks to the PR gate, plus credential injection: a `sed` pass replaces `__API_USERNAME__` and `__API_PASSWORD__` in `environment.prod.ts` with values from GitHub Secrets immediately before the production build. The plaintext credentials never touch the source tree or the container image filesystem. + +*2. Docker build & push (`docker`)* — only runs when the `ci` job succeeds and the branch is `main`. The image is built from `ui/` and pushed to GitHub Container Registry (`ghcr.io`) under two tags: a commit-SHA tag for traceability and a `latest` tag for the deploy step to reference. The image name is normalised to lowercase to satisfy Docker Hub naming rules. + +*3. Deploy (`deploy`)* — connects to the physical server over @ssh using a certificate-authenticated key pair stored as GitHub Secrets. The deploy script pulls the exact SHA-tagged image, stops and removes the previous container, and starts a new one with `--restart unless-stopped` on port 80. The private key is written to a temporary file and deleted immediately after the @ssh session closes. + +#let ci-cd = [ + #figure( + image("../resources/img/ui_images/ci_cd.png"), + caption: [CI/CD Pipeline], + alt: "CI/CD Pipeline Execution" + ) +] +#ci-cd + +==== Secret management + +Four secrets are required: `API_USERNAME`, `API_PASSWORD` (injected into the production build), `SSH_PRIVATE_KEY` + `SSH_CERTIFICATE` (deploy authentication), `SSH_HOST`, `SSH_PORT`, and `SSH_USER` (server coordinates). None of these values appear in any committed file; the dev environment uses empty strings and proxies requests locally via `proxy.conf.json`. + + === Notification + +Spring Boot 3.3.5 microservice (Java 17) that polls @co2 sensor data from the +backend @api and sends Telegram alerts when air quality degrades as illustrated in @fig:notification. + +#let notification = [ + #figure( + image("../resources/img/ui_images/notification.png", width: 90%), + caption: [Telegram notification], + alt: "Telegram alert" + ) +] +#notification + +=== Application Security + +==== Secret Management + + + *Zero secrets in source code.* All sensitive values are injected exclusively + via environment variables never hardcoded, never logged. + + *FindSecBugs caught* `DM_DEFAULT_ENCODING` on `getBytes()` during SAST + integration fixed immediately by switching to `getBytes(StandardCharsets.UTF_8)`. + + +==== STRIDE Threat Model +The notification microservice was analysed using the STRIDE framework, +identifying five threat categories and their corresponding controls, +as summarised in @tab:threat_model. +#figure( + table( + columns: (auto, 1fr, 1fr), + stroke: 0.5pt, + [*Threat*], [*Scenario*], [*Control*], + [Spoofing], [Impersonating the @co2 backend], [Basic Auth + URL injected via GitHub Secret], + [Tampering], [Altering sensor readings], [Read-only, service never writes to the API], + [Info. Disclosure], [Leaking Telegram bot token], [Env vars only, masked in CI logs], + [Denial of Service], [Telegram alert spam], [Deduplication via `ConcurrentHashMap`], + [Elevation of Privilege], [Unauthorized channel access], [Private channel, bot is sole admin], + ), + caption: [STRIDE threat model for the notification microservice], + ) + As shown in @tab:threat_model, each identified threat is mitigated at + the service level without relying on external infrastructure. Spoofing + of the CO₂ backend is prevented by Basic Auth with credentials injected + via GitHub Secrets, never hardcoded. Tampering is structurally impossible + as the service operates in read-only mode against the API. Token + disclosure is avoided by confining the Telegram bot token to environment + variables, masked in CI logs. Alert flooding is controlled by a + `ConcurrentHashMap`-based deduplication mechanism that suppresses repeated + notifications for the same condition. Finally, unauthorised channel access + is prevented by restricting the Telegram channel to a private scope with + the bot as sole administrator. + + #pagebreak() +==== Security Testing Pipeline + + + Deployment is gated behind three blocking security jobs, as shown in + @tab:security_test_pipeline. If any fails, the Docker image is not + built and the service is not deployed. + + #figure( + table( + columns: (auto, auto, auto, 1fr), + stroke: 0.5pt, + [*Job*], [*Tool*], [*Blocking*], [*Checks*], + [`ci`], [SpotBugs + FindSecBugs], [Yes], [Java static vulns, encoding, crypto], + [`sast-codeql`], [CodeQL], [Yes], [Injection, SSRF, unsafe deserialization], + [`dast`], [OWASP ZAP], [Yes], [HTTP headers, runtime misconfigs], + [`dependency-check`], [OWASP Dep. Check], [No], [CVEs in third-party libraries], + ), + caption: [Security test pipeline for the notification microservice], + ) + As detailed in @tab:security_test_pipeline, three jobs are strictly + blocking: `ci` runs SpotBugs with the FindSecBugs plugin to catch + Java-specific vulnerabilities such as encoding issues and unsafe + cryptographic usage; `sast-codeql` performs semantic analysis to detect + injection flaws, SSRF, and unsafe deserialization; and `dast` runs an + OWASP ZAP baseline scan against the running service to identify HTTP + header misconfigurations and runtime exposure. The `dependency-check` + job is non-blocking but reports CVEs in third-party libraries for + visibility without halting delivery. + == Physical model +The physical models follows the design. + + == Conclusion + The integration of each unit has been eased thanks to the corresponding interfaces definition beforehand.\ + Said integration suffered some minor issues, all of them small enough to be managed in a small amount of hours of work. + + While some issues remains, as listed in @sec:validation, the complete application can be demonstrated. + Furthermore, the implementation of the gateway and database enables efficient modification of the deployment on site.\ + For instance, moving a node requires only to move it physically and modify the mapping file accordingly. +] diff --git a/report/main/06-validation.typ b/report/main/06-validation.typ new file mode 100644 index 0000000..4237711 --- /dev/null +++ b/report/main/06-validation.typ @@ -0,0 +1,208 @@ +#import "/metadata.typ": * +#pagebreak() += #i18n("validation-title", lang:option.lang) + +#add-chapter()[ + // == Complete application + The application as a whole allows to retrieve the environmental data in the A2 and A3 room at the Provence campus. + These data are then formatted by the gateway and published on @mqtt topics. + Said topics are listened by the database, ultimately storing them and making them available through an @api. + This @api enable the live display on the user interface alongside letting the physical model validate the data and forecast when the room should be aired. + + The major difference with the intended design lies in the notifications being delivered in Telegram instead of Teams. + This choice has been forced due to the school's network restrictions. + Since the goal for this project is to demonstrate the feature, the effort were redirected. + == Nodes + The implicit goal for the nodes was to have devices retrieving the environmental values in a room. + Furthermore, this devices were expected to work during several weeks without the need to manipulate them. + + The first objective is fulfilled, as the temperature, humidity and @co2 level are periodically advertised over @ble.\ + However the nodes as-is can hardly be considered as "low-power" since they should run for many month without recharge. + The definitive reason could not be found, even after talk with nordic tech support. + Said guys pointed out that the problem may lie within the thingy52 hardware itself as it was not designed to be actually low-power. + Hence, the firmware itself has been properly implemented to be low-power, but requires the design of a dedicated hardware. + This topic shall be part of the project's next iteration. + + Regrettably, the effort available for the project did not allow to implement automated tests. + As such, the only test conducted were quick ones prior to the various integration. + Considering that the nodes are stable enough, automated test shall be amongst the first tasks for the + next project's iteration. + == Gateway +DeepL Pro was used to translate this section from French to English, followed by @llm assistance to improve the academic style. + +The gateway successfully fulfills its role — environmental data from +the Thingy:52 nodes is received, decoded and forwarded to the database +in real conditions at school with multiple nodes running simultaneously. + +The systemd service proved reliable in production. Two unexpected +disconnections due to network instability were detected and recovered +automatically overnight without manual intervention, confirming that +the crash-and-restart mechanism works as intended. + +Several limitations were identified but not addressed within the +available time. Duplicate packets — a consequence of @ble broadcasting +on three channels — are now filtered by the 10-second deduplication +window. Invalid @co2 values during sensor warm-up are silently discarded +without error reporting to the database; a proper error protocol would +require agreement across all components and is left for the next +iteration. + == Database & @api:short + #include "database/validation.typ" + == User interface + === Positive Outcomes + +*Separation of concerns is well maintained.* Components hold no business logic; all data fetching and transformation is isolated in services. Mock fallbacks have been deliberately removed from `SensorService` so the UI reflects real @api state rather than hiding errors with synthetic data. + +*Responsive design covers all target breakpoints.* The three-breakpoint strategy (1100 px / 768 px / 480 px) produces a usable layout on desktop, tablet, and mobile without separate component trees. + +*Polling teardown is correct.* The use of `takeUntilDestroyed` and explicit `ngOnDestroy` cleanup eliminates the two most common Angular memory-leak patterns (forgotten subscriptions, orphaned timers). + +*The proxy-based CORS workaround is transparent.* No component or service is aware of whether it is running against a local or remote backend. + +*Credentials are never stored in source.* @api credentials are injected at build time by the CI/CD pipeline via GitHub Secrets and replaced by empty strings in the dev environment. A `git filter-repo` pass was applied to remove a previously committed plaintext password from the full repository history. + +*A full CI/CD pipeline is in place.* Every pull request runs TypeScript compilation, linting, Prettier formatting, both dev and production builds, and headless unit tests before merge. Pushes to `main` additionally build and push a Docker image to GHCR and deploy it to the physical server over SSH, with no manual intervention required. + +=== Negative Outcomes and Gaps + +*No deduplication of polling on tab switch.* When the browser tab is hidden, polling continues at the same interval. Using the Page Visibility @api to pause polling when the tab is inactive would reduce unnecessary network traffic. + +*`bypassSecurityTrustHtml` introduces an XSS surface.* The SVG is fetched from the same origin (`/public/plan.svg`) which mitigates the risk, but the Angular security model is explicitly bypassed. A Content Security Policy header on the server would reduce residual risk. + +*Room IDs are hardcoded in `rooms-layout.config.ts`.* The mapping between physical room identifiers (as stored in InfluxDB) and layout coordinates is static. Any room addition on the hardware side requires a manual code change and redeployment of the UI. + +*@co2 thresholds are partially duplicated.* The same threshold values appear in `co2-levels.config.ts` (UI), `notification_service/application.yml` (Telegram notifier), and the Go service configuration. The UI spec is now config-driven, reducing the risk of intra-UI divergence, but cross-service consistency still relies on manual coordination. A single source of truth, either a shared config endpoint or a well-documented constant, would eliminate the remaining risk. + +== Physical model +The results of the physical models are given below. + +@llm has been only used for some cases to help debugging the models. + +=== Results of no window-opening model +As described in the Design part, the first model reads the data from open data files and plot the time at which the threshold of 1400 @ppm is reached for a given air volume per person as illustrated @fig:time_reaching_threshold_v1. +#figure( + image("../resources/img/Time_reaching_threshold.png", width: 90%), + caption: [Time reaching threshold] +) + +#pagebreak() + +As can be seen in the graph, the open data from #cite(), plotted as dark blue crosses, do not allow a clear trend, since the air volume per person is a mean value and therefore identical for all classrooms. +On the other hand, the data from the simulator of air quality #cite() are highly useful, as different cases can be tested and a general trend emerges. This leads to an affine function enabling the determination of the time reaching the threshold (1400 @ppm) as a function of room volume and number of occupants. +$ +t_"threshold" = frac((frac(V,N) - 0.101), 0.305) +$ + +where V, represents the room volume and N, the number of occupants. + +In the data from #cite(), only one scenario reaches the 1400 @ppm threshold (represented by an orange point on the graph). When comparing with the affine function, a relative deviation of approximately 30% can be observed. As mentioned in the Design part, this model considers an initial @co2 concentration ranging from 400 to 600 @ppm and is thus valid only following air renewal. + +A second physical model has been developed to take into account the initial @co2 concentration and give the time evolution of @co2 level. The formula mentioned in the Design part is applied to model the @co2 concentration over time. The model is then compared with the open data and adjusted to the data as illustrated @fig:CO2_concentration_simaria, @fig:CO2_concentration_INRS_secondary_school_classroom and @fig:CO2_concentration_INRS_office_room. + +#figure( + image("../resources/img/Physical model/CO2 concentration over time Simaria.png", width: 100%), + caption: [Comparison of @co2:short concentration over time between open data from Simaria #cite() (continuous curves) and the model (dashed curve)] +) + +#figure( + image("../resources/img/Physical model/CO2 concentration over time INRS secondary school.png", width: 100%), + caption: [Comparison of @co2:short concentration over time between open data from the secondary school classroom #cite() (blue curve) and the model (orange dashed curve)] +) + +#figure( + image("../resources/img/Physical model/CO2 concentration over time INRS office room.png", width: 100%), + caption: [Comparison of @co2:short concentration over time between open data from the office room #cite() (blue curve) and the model (orange dashed curve)] +) + +#pagebreak() + +It is therefore possible for the user to input a room volume, a number of occupants, and an initial @co2 concentration in order to obtain the evolution of @co2 concentration over time and the time at which the 1400 @ppm threshold is reached as illustrated @fig:CO2_concentration_user_parameters and @fig:CO2_concentration_user. + +#figure( + image("../resources/img/Physical model/Window user no window opening model.png", width: 80%), + caption: [User input parameters and result using no window-opening model] +) + +#figure( + image("../resources/img/Physical model/CO2 concentration over time user.png", width: 100%), + caption: [@co2:short concentration over time with user input parameters (V = 308 $m^3$, N = 40, initial @co2:short concentration = 500 @ppm)] +) + +#pagebreak() + +=== Results of window-opening model + +The window-opening model calculates the @co2:short concentration at each time step after the 1400 @ppm threshold is reached for a considered room. It is assumed that all windows of the considered classroom are fully opened. This also provides an estimation of the required window-opening duration to renew indoor air. @fig:CO2_concentration_window_opening_model_all_rooms represents an overview of the evolution of @co2:short concentration over time for 5 classrooms of Space A. It can be noticed that the window-opening duration varies significantly from one classroom to another, depending on the ratio between the room volume and the total windows surface. Indeed, the window-opening time for indoor air renewal is approximately 3 minutes in classroom A3 and A5, whereas it can reach 18 minutes for classrooms A2 and A6. +In contrast to the increasing evolution of @co2:short concentration, the decrease in @co2:short level after window opening is difficult to compare with the open data, as they do not specify the total windows surface. Therefore, only experimental sensor data from the Provence classrooms can be used to adjust the model. It was decided to prioritise instrumentation in Room A2 due to its slower evolution of @co2:short concentration decrease, which provides more data points. + +#figure( + image("../resources/img/Physical model/CO2 concentration window opening A2 to A6.png", width: 90%), + caption: [Overview of @co2:short concentration over time for Provence classrooms, starting from 1400 @ppm] +) + +#pagebreak() + +The user's input parameters are given @fig:CO2_concentration_window_opening_user_parameters. He no longer needs to enter the room volume, but only the room name, the number of students and the initial @co2:short concentration. The physical model then outputs the time to reach the threshold and the corresponding window-opening time. It can also display the corresponding @co2:short concentration over time graph as illustrated @fig:CO2_concentration_window_opening_model_user. + +#figure( + image("../resources/img/Physical model/Window user window opening model.png", width: 80%), + caption: [User input parameters and result using window-opening model] +) + +#figure( + image("../resources/img/Physical model/CO2 concentration over time user window opening model.png", width: 100%), + caption: [@co2:short concentration over time with user input parameters (classroom number = A2, N = 30, initial @co2:short concentration = 700 @ppm)] +) + +#pagebreak() + +=== Comparison of experimental data and the physical model + +@fig:CO2_concentration_comparison_exp_model presents a comparison between the measured data and the model for classroom A2 during a lecture. Six sensors have been installed. Two are located near the windows to monitor their status (blue and green curves on the graph), and four on walls away from the windows (purple, red, orange, brown curves). Data are missing from 45 to 90 minutes. However the measurements continue afterwards which makes it possible to analyse the data. +The window open/ closed status is indicated by the blue vertical dotted lines. It is therefore assumed that the windows are closed before reaching the status open (= 1) after approximately 90 minutes. A student present in the room confirmed that the windows not equipped with the sensors were closed when the instrumented ones were closed, ensuring consistency with the model conditions. Eleven people were present in the room. +A trend curve is determined for each experimental data using a python program and is identified by dashed lines on the graph. +The black line represents the model result which takes into account the classroom name, the number of students and the initial @co2:short level. + +#figure( + image("../resources/img/Physical model/Comparison_expdata_model.png", width: 90%), + caption: [@co2:short concentration over time of acquired data from classroom A2 and comparison with the model] +) + +It is observed that all data except the purple one do not show significant increase in @co2:short over time. These data are therefore not used to adjust the model. At first sight, the data from the purple curve seem to be correct as an increase up to 800 @ppm can be observed. However, when this data is compared with the model the value is found to be approximately two times lower than expected. + +Only this set of data was available and they are not sufficient to adjust the model. Moreover, when comparing the model with open data, the gap is not as significant. +It appears that the sensors output data are most likely not accurate. It would be relevant to compare the sensor data with a calibrated reference sensor. + +// #pagebreak() + +=== Recommendations for ensuring good indoor air quality + +The time reaching threshold of 1400 @ppm and windows-opening duration are given in @tab:Provence_recommandations for each classroom, assuming maximum number of students. These data are conservative, as the maximum number of students is rarely reached. + +It is therefore recommended to : +- Open windows after one-hour lecture +- Fully open windows when airing the classroom in order to optimise ventilation and reduce the required duration. The duration highly depends on the classroom. An average of 5 minutes can be considered. It is also the recommended duration mentioned in the literature. + + #figure( + table( + columns: (auto, auto, auto, auto), + align: center, + table.header("Classroom [-]", "Maximal student capacity [-]", "time reaching 1400 ppm threshold [min]","windows-opening duration [min]"), + [A2],[40],[30],[17], + [A3],[78],[24],[4], + [A4],[48],[27],[18], + [A5],[32],[34],[2], + [A6],[58],[22],[18], + [A7],[36],[30],[9], + ), + caption: [Recommendations for ensuring good indoor air quality in Provence - Space A, assuming full ventilation prior to the start of the lecture (i.e. approximately 400 @ppm:short)], +) + + == Conclusion + The @mvp for the project stores the environmental value in a database and delivers a notification when the room needs to be aired. + This @mvp also planned forecasting, but has been cut from the planning following the team reduction. + + Some effort should still be done to provide the complete set of expected features, mostly to have "low-power" nodes and notifications + in Teams. + However, the project as delivered at it's end can serve as a good basis for a second iteration, which could add the aforementioned features. +] diff --git a/report/main/07-conclusion.typ b/report/main/07-conclusion.typ new file mode 100644 index 0000000..4cd85da --- /dev/null +++ b/report/main/07-conclusion.typ @@ -0,0 +1,72 @@ +#import "/metadata.typ": * +#import "objectives.typ":* + +#pagebreak() += #i18n("conclusion-title", lang:option.lang) + +== Project conclusion + Considering the objectives in @sec:objectives, the project takeaways are as follows. + + The solution provided at the project's end allows to store the temperature, humidity and @co2 level in the equipped room. + Hence, the objective MO1 is fulfilled. + + Independently, a user interface is made available to display the room environmental data. + As this user interface request the database, it allows the display of both live and stored data. + Hence, the objective MO2 is fulfilled. + + The solution is able to send a notification when the room @co2 level reach the 1200 @ppm threshold. + However, the objective MO3 asks for a forecasting using a physical model. + Even if said physical model were studied during this project, as they are not included for the notification's sending, the objective + MO3 can not be considered as fulfilled. + + Moreover, the forecasting used for the recommendation are based on the evolution of the @co2 level based on the amount of people in the room. + However, the outside temperature is not part of the analysis nor, incidentally, the airing recommendation.\ + This absence is mainly explained by the team reduction. + The dropping member was initially tasked to implemented the notification system and the outside temperature retrieval. + While the MO3 was adjusted, an oversight from the team let this objective as unchanged. + As such, not enough effort were made available for the fulfilling of MO4. + + Likewise, the forecasting could not be implemented thus forbidding the display of the data it would have provided. + As such, the optional objective OO1 could not be fulfilled. + + Lastly, the time between the sending of two data point by a given node evolves based on the room recorded values. + Hence, the optional objective OO2 is fulfilled. + +== Encountered difficulties + The project encountered some difficulties impacting the overall result. + While some are typically integration mishaps and is documented in @sec:impl, other are wider. + + Namely, the team effort reduction following the dropout of a member impacted significantly the results. + As indicated in @sec:objectives, the client has accepted to adjust the project's objectives accordingly. + + As warned by the client at the project's start, the actual room data arrived late in the project. + The solving of every observed issues lead to stable data available only two weeks prior to the project end. + This short time did not allow to give their full purpose to the data nor retrieve user feedback. + + Lastly, the project still suffers from noticeable issues that are documented prior and in the GitHub issues. + +== Future perspectives + While the project next iteration features shall be decided then, the current team recommend the following, without any wanted order: + - Deployment in every room at the Provence campus. + - Talk with the administration to send the notification on teams, since it is the standard communication medium. + - Build a dedicated board for the nodes, enabling the "low-power" that is sought for these elements. + - Implement a forecasting, including the outside temperature as intended at the very start of the project. + This forecasting shall include de room temperature as it also impact the people focus. + + The github repository contains several opened issues tracking new features idea and observed issue to solve. + The next iteration's team is welcomed to consider these issues. + + Furthermore, the following would improve the product usability, but requires dedicated development. + + - Counting the amount of people in a room is paramount to have the physical model work properly. + Hence, having an automated way to count such shall be investigated. + A way to do so may be to have a camera and process the images. + This solution comes with high security around personal data protection.\ + Another way that could be investigated is to estimate the amount of people from the @co2 level evolution. + A dedicated step in the deployment may enable efficient estimation. + - The retrieval on the window opening status could also be improved as the deployment remains very tinkered. + Dedicated casing could be designed and the room door may also be equipped. + Furthermore, a more general way could be investigated as the deployed one requires to have every window be equipped and by extension one node per window. + Moreover, a room may have hard-to-access window and thus could not be equipped easily. + - The @co2 level in a room with automated airing or special insulation may require modifications in the physical model. + These shall be investigated in a later iteration. \ No newline at end of file diff --git a/report/main/architecture/description.typ b/report/main/architecture/description.typ new file mode 100644 index 0000000..bbfbe70 --- /dev/null +++ b/report/main/architecture/description.typ @@ -0,0 +1,55 @@ +#import "/metadata.typ": * +#pagebreak() += Global BDD +The global architecture as given in @fig:top-level-architecture can be detailed with @fig:top_level_archi_interface. +The following gives shape to the aforementioned interfaces. + +#let top_level_archi_interface = [ + #figure( + image("../../resources/img/bdd_complete.svg"), + caption: [Top-level architecture, including interfaces between units] + ) +] +#top_level_archi_interface + +The various interfaces available in @fig:top_level_archi_interface are composed of the following data: +- #underline[Nodes interface]: + #let nodes_interface = [A node advertise its data over @ble using key/values with the following content: + #figure( + table( + columns: (auto, auto, auto, auto), + align: center, + table.header("name", "key", "data construction", "data size"), + [Window opening status],[0x01],[0/1],[1B], + [Humidity],[0x02],[Integer giving the raw percentage],[1B], + [Temperature],[0x03],[Integer in the range [0,500]],[2B], + [CO2 level [ppm]],[0x04],[Integer in the range [400,5000]],[4B] + ), + caption: [Data communicated in the nodes_interface], + ) + This description considers the following elements: + - Each key is held on 1 Byte + - The conversion from the held data for the temperature is: $"temperature" = "data"_"temp"/10 $ \ + The resulting temperature is within the range [0,50] °C with one digit decimal. + - For each key, any value outside of the associated range indicates that the node failed to get a valid value. + - For a given node, the time between two broadcast of data is variable, with the shortest time being 1 minutes and the longest time being 6 hours. +] + #nodes_interface +- #underline[db set] : MQTT topic named "{id_gateway}/{node_mac}/update" with the following content: + ```JSON + { + "timestamp" : , + "co2_ppm" : , + "temp" : , + "humidity" : + } + ``` + This description considers the following elements: + - The timestamp format respect the #link("https://en.wikipedia.org/wiki/ISO_8601")[ISO 8601] definition. + As the standard allows variations, the format is given as _YYYY-MM-DDThh:mm:ssZ_. + This choice enables efficient time manipulation on all ends. + A timestamp considering the milliseconds is not required as the recorded data should stay stable over several consecutive seconds + - Upon receiving a data point from a node, the gateway adds the timestamp and publish the content on the MQTT channel. +- #underline[db API] : the content for this interface in tracked in #link("https://github.com/PI-E2EEDA/Plein-de-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-project/issues/7")[issue \#7]. +- #underline[teams API] : The teams API is to be followed and as such is kept absent from this document. + \ No newline at end of file diff --git a/report/main/database/class-db.png b/report/main/database/class-db.png new file mode 100644 index 0000000..34e5ba6 Binary files /dev/null and b/report/main/database/class-db.png differ diff --git a/report/main/database/design.typ b/report/main/database/design.typ new file mode 100644 index 0000000..c9373e9 --- /dev/null +++ b/report/main/database/design.typ @@ -0,0 +1,48 @@ +#import "/metadata.typ": * + +Only natural stupidity was used to for the design of this part. But some key design are reused from an hobby project developed in parallel of the current one. The database part actually represent a aggregation of all small part all running around database on the cloud. We will talk more about the server part in the @sec:impl:db. + +The database part have several missions. To achieves all objectives, a GO service is designed in several module. +Before see the service itself, a little talk on the stack. For the database, we choose Influx DB 3. Mainly because it's a time-series database already known by part of the team. Another option would be to use a PostgreSQL database with the timescale extension. Concerning the @mqtt broker, we choose RabbitMQ for mainly the same reason, because it's already known by part of the team. + +The key design in the Go service is to represent data with a DataPoint. A DataPoint represent a measure with a list of topics, a timestamp and a generic type for the value. It allows to share DataPoints trough gateway that have the same input and output interface. Event the database part is developed in Go, a pseudo class diagram in @fig:class-db represent this DataPoint shared between gateways. + +#pagebreak() + +So, we need an @mqtt gateway to connect to the broker to get updates of sensors. To achieves to connect to this broker, a module @mqtt is represented as mqttGateway in the @fig:class-db. This @mqtt gateway is mainly used to subscribe to the desire topic and receive a DataPoint defined previously + +#figure( + image("class-db.png"), + caption: [Class diagram for database part] +) + +We also need a influx gateway to push this received DataPoint to the database. It's the magic of the DataPoint, no need to map each field. We only translate topic, from node id to room and we can forward the DataPoint to the Influx gateway. We see the full sequence from the broker to the database in the @fig:seq:toDB + +Finally, we also have a rest gateway to allow user to request data from the database trough with several endpoint as defined in @sec:design:interface:db. Mostly use by the user interface. We can observe a general application in @fig:seq:fromDB. + + +#let fig-toDB = [ + #import "sequence.typ": * + #figure( + chronos.diagram(toDB, width: 100%), + caption: [Sequence from broker to DB] + ) +] + +#let fig-fromDB = [ + #import "sequence.typ": * + #figure( + chronos.diagram(fromDB, width: 100%), + caption: [Sequence to REST from DB] + ) +] + +#grid( + columns: (1fr, 1fr), + column-gutter: 1em, + align: bottom+center, + fig-toDB, + fig-fromDB +) + +#pagebreak() \ No newline at end of file diff --git a/report/main/database/implementation.typ b/report/main/database/implementation.typ new file mode 100644 index 0000000..d03c6ed --- /dev/null +++ b/report/main/database/implementation.typ @@ -0,0 +1,20 @@ +#import "/metadata.typ": * + +#let physical_diagram = [ + #figure( + image("physic.png", width: 90%), + caption: [Physical diagram] + ) +] + +Before to be able to implement and up any service, an important part was to be able to host everything. For this project, as a academic project that don't have specific requirements, we use the Rémi's homelab server. This server is base on Proxmox, so we get a virtual machine for this project with a traefik router on \*.e.kb28.ch. We have several service as show on the figure bellow. The main stack is composed by a RabbitMQ node for the @mqtt broker, an Influx DB 3 stack and a Go service that act as a gateway between the @mqtt broker and the database and the @rest @api. All this stack run on a single docker node: The "server" on @fig:diagram_physic + +#physical_diagram + +As this server is used to deploy the project but also for the development phase, it was important to give secure access to several member of the team. For the connexion to the server, we used @ssh certificates. Unlike regular @ssh key, here we have a server certificate that sign the key of each member for a certain period of time for a specific username. That avoid to add public key of all user on the server and only have the public key of the server's certificate. + +Another security point was to add authentication around the data. An @sso middleware for the web interface of Influx by Authentik, a standard token for the @rest @api and encapsulate @mqtt connexion in @tls as describe in @sec:design:gateway:security. + +When the server with both broker and database are available, we can concentrate of developing the service that connect the database to other system. This service was developed in Go as it's a efficient language for server side services. Some initial part of this service as the mqttGateway and influxGateway was reuse from an hobby project made with friends in parallel of this @pi. Concerned commits are clearly identified as are those more or less assisted by @llm. + +The service is implemented as the design. A slight change concerning the mapping process was made during the implementation. It was initially to static for support our development phase. It was decided 2 changes. First, we look for mapping node to room on the retrieval trough the @rest @api instead of doing the mapping when we push data to the database from the broker. It was because some node was placed in Provence without correctly identify with node id was placed on wich room. So we decided to push the node id in the database and map on the retrieval. Second change was to load info from this mapping fil dinymacally to avoid restarting the container after a change. Despite the lost of performance, it allow us more flexibility during our development phase. \ No newline at end of file diff --git a/report/main/database/interface.typ b/report/main/database/interface.typ new file mode 100644 index 0000000..5d7e6f7 --- /dev/null +++ b/report/main/database/interface.typ @@ -0,0 +1,46 @@ +#import "/metadata.typ": * + +No @llm has been used for the design of deposal and retrievals interfaces. + +The database is used to store data, we need so to be able to push data and retrieve in several form the stored data. + +#let mqtt-json-payload = [ + #figure( + table( + columns: (1fr, 1fr), + align: horizon+left, + [*timestamp*], [D/T (RFC3339)], + [*temp*],[float], + [*humidity*],[float], + [*co2_ppm*],[integer], + [*window_open*],[boolean], + [*battery*],[integer] + ), + caption: [@mqtt:short Json payload] + ) +] +#grid( + columns: (1fr, 1fr), + column-gutter: 2em, + [ + To put data in the Database, we use an @mqtt source of data. The @mqtt payload is a list of key-value as follow defined in @mqtt-json-payload. All of this key are translated as `Fields` in influx DB. Topic of the @mqtt are translated in `Tags` in Influx. + + So, all data arrive in the database trough the an @mqtt message + ], + mqtt-json-payload +) + +To get data out of the database, we have several @rest @api all available at: #link("https://doc.db.e.kb28.ch/")[doc.db.e.kb28.ch]. + +The main route is for the user interface with the following endpoint: +- `/rooms`: Return the list of rooms available on the database +- `/rooms/{room-id}/current`: Return the last values for the select room +- `/rooms/{room-id}/history`: Return the history (default last 24h) of the selected room +- `/rooms/high-co2`: Return the list of room with high value of @co2 + +It's also possible to get all the data in a @csv:short file trough a dedicated @rest endpoint. This is specially usefull for validate model by Alison. +- `/export/csv?node=&from=&to=`: \ + Return a @csv with all the measurement point of the selected node between the start and stop date-time. + +A another special endpoint was added during the project to monitor the battery level of each node: +- `/battery`: Return the list of node with the corresponding battery level. If message was present in the last 30min, the node is declared offline \ No newline at end of file diff --git a/report/main/database/physic.png b/report/main/database/physic.png new file mode 100644 index 0000000..146789f Binary files /dev/null and b/report/main/database/physic.png differ diff --git a/report/main/database/sequence.typ b/report/main/database/sequence.typ new file mode 100644 index 0000000..95b999f --- /dev/null +++ b/report/main/database/sequence.typ @@ -0,0 +1,86 @@ +#import "../../resources/helper.typ": * +#let b = actor("broker", + disp_name: [@mqtt\ broker], + shape: "queue", + show-bottom:false +) +#let mg = actor("mqtt", + disp_name: [@mqtt\ gateway], + show-bottom:false +) +#let ig = actor("influx", + disp_name: [influx\ gateway], + show-bottom:false +) +#let rg = actor("rest", + disp_name: [rest\ gateway], + show-bottom:false +) +#let main = actor("main", + disp_name: [Main], + shape: "control", + show-bottom:false +) +#let db = actor("db", + disp_name: [Influx DB], + shape: "database", + show-bottom:false +) +#let u = actor("user", + disp_name: [User], + shape: "actor", + show-bottom:false +) + +#set align(center+top) +#v(1.5em) + +#let toDB = { + import chronos: * + b.display + mg.display + main.display + ig.display + db.display + + _col(b.name, mg.name, width: 2cm) + _col(mg.name, main.name, width: 2cm) + _col(main.name, ig.name, width: 2cm) + _col(ig.name, db.name, width: 2cm) + + sync(main, mg, "New", create-dst: true) + sync(mg, b, "subscribe") + sync(main, ig, "New", create-dst: true) + async(b, mg, "message") + sync(mg, main, "DataPoint") + sync(main, main, "map topics") + sync(main, ig, "DataPoint") + async(ig, db, "flush") + +} +#let fromDB = { + import chronos: * + db.display + ig.display + main.display + rg.display + u.display + + _col(db.name, ig.name, width: 2cm) + _col(ig.name, main.name, width: 2cm) + _col(main.name, rg.name, width: 2cm) + _col(rg.name, u.name, width: 2cm) + + async(u, rg, "Request") + sync(rg, main, "getNodes") + sync(main, rg, "", dashed: true) + sync(rg, ig, "Query") + sync(ig, db, "") + sync(db, ig, "", dashed: true) + sync(ig, rg, "", dashed: true) + sync(rg, u, "", dashed: true) + +} + +#chronos.diagram(toDB) +#chronos.diagram(fromDB) diff --git a/report/main/database/validation.typ b/report/main/database/validation.typ new file mode 100644 index 0000000..d1f17d7 --- /dev/null +++ b/report/main/database/validation.typ @@ -0,0 +1,6 @@ +#import "/metadata.typ": * + +The link between data arriving on the broker, the database on the server and the user interface work as expected. Some endpoint on the @rest @api was added to simplify life during the development as the battery endpoint to monitor the battery level of each node. + +As the service is made in Go, it allow to limit the resources usage, a important point on the actual context. The full server with the @mqtt broker, the database, the gateway service connection the database to everything and finally the user interface webpage, all running on Docker use less than 2Go of RAM and 20Go of disk memory. It would so possible to run the entire cloud part on the cheapest Infomaniak's VPS lite for roughly 30CHF a year. + diff --git a/report/main/objectives.typ b/report/main/objectives.typ new file mode 100644 index 0000000..7da9ec8 --- /dev/null +++ b/report/main/objectives.typ @@ -0,0 +1,18 @@ +#import "/metadata.typ": * +#import "specifications/05-objectives.typ": * +#pagebreak() += Objectives + +#add-chapter()[ + The objectives for the project that has been validated in its early phase are listed below. + Mandatory objectives are listed as MOx, optional objectives as OOx and personal objectives as POx. + + #objectives-mo + #v(1em) + #objectives-oo + #v(1em) + #objectives-po + + The MO3 has been modified during the 8th week in response to Dimitri dropout. + The student's personal objective has been kept nonetheless. +] \ No newline at end of file diff --git a/report/main/specifications/02-introduction.typ b/report/main/specifications/02-introduction.typ new file mode 100644 index 0000000..32dabeb --- /dev/null +++ b/report/main/specifications/02-introduction.typ @@ -0,0 +1,11 @@ +#import "/metadata.typ": * +#pagebreak() += #i18n("introduction-title", lang:option.lang) + +This document contains the specifications for the Inter-disciplinarity Project (PI) team "plein de eeeeeeeeeeeeeeeeeeeeeeeeeee" conducted during the Spring Semester 2025-2026. + +This project aims at delivering the best time and duration to air a room. +The recording of environment values inside the room, especially the air temperature, CO2 level and window opening status, alongside the outside temperature are paramount to provide the data. +Furthermore, the delivering of the data is done, among other ways, using explicit notification to the room users to ensure the project usefulness. + +This document contains the project motivations, objectives and planned tasks. diff --git a/report/main/specifications/03-context.typ b/report/main/specifications/03-context.typ new file mode 100644 index 0000000..6b578e3 --- /dev/null +++ b/report/main/specifications/03-context.typ @@ -0,0 +1,74 @@ +#import "/metadata.typ": * +#pagebreak() += Context + +#let spec_context = [ +The @ofsp emphasises that insufficient ventilation can lead to increased @co2 levels #cite(), which can negatively affect cognitive performance and health. + +It is advised to monitor indoor air quality in classrooms using @co2 concentration sensor. The @co2 level should not exceed a threshold of 1400 @ppm. When this threshold is exceeded, the room should be ventilated until the concentration falls below 1000 @ppm. For reference, outdoor air typically contains around 400–500 @ppm of @co2. +These thresholds are summarised using @tab:co2_levels. + +The @ofsp recommends ventilating classrooms effectively before each lecture and during breaks by opening the windows fully to renew the air quickly. To minimise heat loss and prevent the cooling of walls in winter, the ventilation period should remain short (a duration of 3 to 5 minutes is generally recommended). + +Minimising heat loss reduces heating demand, contributing to the objectives of Switzerland’s Energy Strategy 2050, namely reducing energy consumption, improving efficiency and lowering greenhouse gas emissions. + +An air quality simulation tool named SIMARIA #footnote[https://simaria.ch/fr/simaria] has been developed to assist teachers in planning the timing and duration of window ventilation, based on classroom size, number of students, and class schedules. + +In this context, HES‑SO focused on this issue and installed @co2 sensors in the classrooms. Indeed, the long duration of class periods (2h30), combined with windows often remaining closed before, during, and after lessons, can result in high @co2 concentrations. Moreover, some classrooms can accommodate a large number of students, which also contributes to the increase in @co2 concentrations. + +Sensors have been installed to alert teachers and students to the @co2 levels in the room, encouraging ventilation. However, it has been observed that some students are unaware of their presence, and both students and teachers often do not pay attention to them. The timing and duration of window opening depend on the individual, often without regard to the @co2 levels indicated on the sensors and without consideration of the resulting energy loss. + +#figure( + table( + columns: (auto, auto), + align: center, + table.header("co2 level [ppm]", "influence"), + [400],[Normal outdoor air], + [400-1000],[Typically found in ventilated rooms], + [1000-2000],[Medium air quality \ usually reached during a 2 hour course without airing the room], + [>2000],[Poor air quality \ Reported headache, loss of focus, poor concentration, ...], + ), + caption: [Noticeable CO2 level threshold] +) +The @tab:co2_levels considers recent review #cite(). + +#let top-level-architecture-get-data = [ + #figure( + image("/resources/img/PI-get_data.drawio.png"), + caption: [Top level architecture to get measurement], + alt: "Thingy52 --BLE secure advertising--> Raspberry Pi --MQTT--> RabittMQ --GO--> InfluxDB" + ) +] +// #top-level-architecture-get-data + +#let top-level-architecture-lstm-notify = [ + #figure( + image("../../resources/img/PI-lstm.drawio.png"), + caption: [Top level architecture to analyse data and notify], + alt: "Influx + MeteoSwiss --> LSTM --> Teams notification" + ) +] +// #top-level-architecture-lstm-notify + +#let top-level-architecture = [ + #figure( + image("../../resources/img/PI-top-level.drawio.png"), + caption: [Top level architecture], + alt: "Thingy52 --BLE secure advertising--> Raspberry Pi --MQTT--> RabittMQ --GO--> InfluxDB | Influx + MeteoSwiss --> LSTM --> Teams notification | InfluxDB --> MatLab --> InfluxDB" + ) +] + +As indicated in @fig:top-level-architecture, the planned system consists in nodes retrieving the environmental data. +These data are sent to a gateway, running on a raspberryPI, with BLE.\ +The gateway then aggregates the various data and sends them, added by a timestamp, to a database. +The database offers an API allowing the insertion and the retrieval of the stored data. +A physical model ensures that each point of data is coherent with the one coming before and after it.\ +These data can be either displayed, with regards to a given timestamp, of fed to a forecasting model. +Ultimately, the model generate prediction on the upcoming points of data and sends notifications using Microsoft teams to the room users. +The use of Microsoft teams is motivated by the shared use by the students and teachers.\ +The notifications are indicating when to open, respectively close, the room windows to ensure that the air quality remains proper for a course while reducing the heat transfers with the outside to the minimum. +#top-level-architecture + +] +#spec_context + diff --git a/report/main/specifications/04-constraints.typ b/report/main/specifications/04-constraints.typ new file mode 100644 index 0000000..6ae64ae --- /dev/null +++ b/report/main/specifications/04-constraints.typ @@ -0,0 +1,17 @@ +#import "/metadata.typ": * +#pagebreak() += Constraints + +The following points are known elements that may slow or stop the project unless addressed in the early step of the project. +#let constraints = [ + #set enum(numbering: "C1:") + + The used components needs to be ordered before #date.ordering-components.display("[day] [month repr:long] [year]") #pause + + A set of placeholder data is available before #(date.specification-submission + duration(days: 7)).display("[day] [month repr:long] [year]"). + These data are to be used while waiting for the sensors and the database. + A noticeable care needs to be given to the similarities between the placeholders data and the actual ones that will be retrieved.\ + Furthermore, data from the existing sensors can be retrieved manually while waiting for the actual data. #pause + + The minimal role required to send notification on teams is available for the user associated to the project before the #date.objective-completed.display("[day] [month repr:long] [year]"). + + The available freedom around the sensors placement and holding are to be known by the #date.first-environmental-data-in-db.display("[day] [month repr:long] [year]"). +] + +#constraints \ No newline at end of file diff --git a/report/main/specifications/05-objectives.typ b/report/main/specifications/05-objectives.typ new file mode 100644 index 0000000..e32c369 --- /dev/null +++ b/report/main/specifications/05-objectives.typ @@ -0,0 +1,110 @@ +#import "/metadata.typ": * +#pagebreak() += Objectives + +In the various objectives, the "environment data" refers, at least, to the room temperature, the CO2 level in the room and the room's windows opening status.\ +Each objective are either mandatory (MOx), optional (OOx) or personal (POx). +In the latter case, the associated student is indicated alongside. +#v(1.5em) + +#set enum(indent: 1em) + +////////////////////////////// +////////////////////////////// + + +#let objectives = [ + #show: el.config.ref + #show: el.default-enum-list + + #set enum(numbering: "MO1") + + The environmental data for the observed rooms are stored in a database with an associated timestamp. #el.elabel() #pause + + The stored environmental data can be displayed in a 2D map. + The display allows to show both stored and live data. #el.elabel() #pause + + Using the stored environmental data, alongside external meteorological data, the system send a notification when the windows should be opened or closed. + This recommendations are based on the minimal time required to reduce the @co2 level in the room. #el.elabel() #pause + + The changes in temperature when airing the room, with regards to outside temperature, are analysed and used for the window opening time recommendations. #el.elabel() #pause + + #v(1.5em) + + #set enum(numbering: "OO1") + + The display in MO2 /*@mo:map*/ also allows to display the prevision outputted by MO3 /*@mo:recommendation*/ + + The frequency of recorded data can be adjusted by the sensor to increase the autonomy + + #v(1.5em) + + #set enum(numbering: "PO1") + + [Adrien]: A low-power firmware is fully developed on a nordic chip using Zephyr as the embedded OS. + The emphasise on the low-power elements allows for the firmware to run for days without charging the device again.\ + Both these technologies are new for the student. + + + [Dimitri] A machine learning model is developed to predict the optimal moments to open windows, balancing @co2 reduction against thermal losses. The model is trained on historical sensor data and external weather data. Working with time series data and applying machine learning to a physical system are both new competencies for the student. + + + [Ibrahima]: A secure microservices architecture is developed using Spring Boot with emphasis on production-grade security (mutual TLS for IoT devices, JWT RS256 authentication, RBAC authorization, GDPR compliance). The system processes real-time sensor data via MQTT and delivers live updates through WebSocket to an Angular dashboard. Both distributed systems security and IoT protocol implementation are new competencies for the student. + + + [Alison]: A physical model will be developed using Matlab or Python to analyse the evolution of @co2 concentration as a function of several parameters, such as the classroom volume, the number of students, and the duration of window opening. The model will also estimate the thermal losses associated with window ventilation and will provide recommendations to optimise both indoor air quality and energy performance. As the student has limited experience in programming, the development of this model will also represent an opportunity to acquire and strengthen these skills. + + + [Djelal]: A communication layer will be developed to bridge the sensor nodes and the backend infrastructure, using a Raspberry Pi as a BLE-to-MQTT gateway. The gateway will continuously collect environmental data (CO₂, temperature, and window status) from multiple Nordic Thingy:52 nodes via BLE GATT services, enrich each measurement with a timestamp, and publish it to a Mosquitto MQTT broker in a structured JSON format. The design will emphasise reliability through automatic reconnection mechanisms and a clean abstraction between the BLE acquisition layer and the MQTT publishing layer. A basic security layer will also be implemented, including access control on the BLE side to restrict communication to known devices, and authentication mechanisms on the MQTT broker (username/password) to prevent unauthorized data injection. While this approach does not provide full end-to-end security, it reflects a lightweight and realistic security model for a constrained IoT deployment. Both BLE communication protocols and MQTT-based IoT messaging represent new competencies for me, and this project constitutes a significant challenge that I have chosen to undertake. Directly aligned with my orientation in communication systems, it provides an opportunity to apply and deepen these skills in a concrete, real-world context. + + #let remi-serious-objectiv = [ + A data infrastructure is automated, deployed both in productive and development environment. The database is automatically configured and populated by external MeteoSwiss data. + The student didn't experiment automatic deploy /configuration and multi-environment. + ] + #let remi-fun-objectiv = [Solve Maxwell's equations] + + [Rémi]: #remi-serious-objectiv + +] + +#let objectives-mo = [ + #show: el.config.ref + #show: el.default-enum-list + + #set enum(numbering: "MO1") + + The environmental data for the observed rooms are stored in a database with an associated timestamp. #el.elabel() #pause + + The stored environmental data can be displayed in a 2D map. + The display allows to show both stored and live data. #el.elabel() #pause + + Using the stored environmental data and known physical model, the system send a notification when the windows should be opened. This recommendations are based on the co2 level in the room. #el.elabel() #pause + + The changes in temperature when airing the room, with regards to outside temperature, are analysed and used for the window opening time recommendations. #el.elabel() #pause +] + +#let objectives-oo = [ + #show: el.config.ref + #show: el.default-enum-list + + #set enum(numbering: "OO1") + + The display in MO2 /*@mo:map*/ also allows to display the prevision outputted by MO3 /*@mo:recommendation*/ + + The frequency of recorded data can be adjusted by the sensor to increase the autonomy +] + +#let objectives-po = [ + #show: el.config.ref + #show: el.default-enum-list + + #set enum(numbering: "PO1") + + [Adrien]: A low-power firmware is fully developed on a nordic chip using Zephyr as the embedded @os. + The emphasise on the low-power elements allows for the firmware to run for days without charging the device again.\ + Both these technologies are new for the student. + + + [Dimitri] A machine learning model is developed to predict the optimal moments to open windows, balancing @co2 reduction against thermal losses. The model is trained on historical sensor data and external weather data. Working with time series data and applying machine learning to a physical system are both new competencies for the student. + + + [Ibrahima]: A secure microservices architecture is developed using Spring Boot with emphasis on production-grade security (mutual @tls for @iot devices, JWT RS256 authentication, RBAC authorization, GDPR compliance). The system processes real-time sensor data via @mqtt and delivers live updates through WebSocket to an Angular dashboard. Both distributed systems security and IoT protocol implementation are new competencies for the student. + + + [Alison]: A physical model will be developed using Matlab or Python to analyse the evolution of @co2 concentration as a function of several parameters, such as the classroom volume, the number of students, and the duration of window opening. The model will also estimate the thermal losses associated with window ventilation and will provide recommendations to optimise both indoor air quality and energy performance. As the student has limited experience in programming, the development of this model will also represent an opportunity to acquire and strengthen these skills. + + + [Djelal]: A communication layer will be developed to bridge the sensor nodes and the backend infrastructure, using a Raspberry Pi as a BLE-to-MQTT gateway. The gateway will continuously collect environmental data (@co2, temperature, and window status) from multiple Nordic Thingy:52 nodes via @ble GATT services, enrich each measurement with a timestamp, and publish it to a Mosquitto @mqtt broker in a structured JSON format. The design will emphasise reliability through automatic reconnection mechanisms and a clean abstraction between the @ble acquisition layer and the @mqtt publishing layer. A basic security layer will also be implemented, including access control on the @ble side to restrict communication to known devices, and authentication mechanisms on the @mqtt broker (username/password) to prevent unauthorized data injection. While this approach does not provide full end-to-end security, it reflects a lightweight and realistic security model for a constrained @iot deployment. Both @ble communication protocols and @mqtt\-based @iot messaging represent new competencies for me, and this project constitutes a significant challenge that I have chosen to undertake. Directly aligned with my orientation in communication systems, it provides an opportunity to apply and deepen these skills in a concrete, real-world context. + + #let remi-serious-objectiv = [ + A data infrastructure is automated, deployed both in productive and development environment. The database is automatically configured and populated by external MeteoSwiss data. + The student didn't experiment automatic deploy /configuration and multi-environment. + ] + #let remi-fun-objectiv = [Solve Maxwell's equations] + + [Rémi]: #remi-serious-objectiv + +] + +sec:spec:objectives +#v(1.5em) +#objectives-oo +#v(1.5em) +#objectives-po + diff --git a/report/main/specifications/06-planned_tasks.typ b/report/main/specifications/06-planned_tasks.typ new file mode 100644 index 0000000..c415bf7 --- /dev/null +++ b/report/main/specifications/06-planned_tasks.typ @@ -0,0 +1,211 @@ +#import "/metadata.typ": * +#pagebreak() +#let base-url = "https://github.com/PI-E2EEDA/Plein-de-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-project/issues/" +#let github-link(id) = { + link(base-url+str(id))[\##str(id)] +} +#let github-link-name(id, name) = { + link(base-url+str(id))[#name] +} +#let issue( + id, + name, + sp: 0, + link: (), + description: none, +) = { + ( + github-link(id), + github-link-name(id, name), + for l in link [\##l ], + if description == none [] else {description}, + sp +)} + +#let display(workload) = { + let task = () + for w in workload { + task.push(w.at(0)) + task.push(w.at(1)) + task.push(w.at(2)) + task.push(w.at(3)) + task.push(str(w.at(4))) + } + task +} + + += Planned tasks + +Below are listed the general tasks to execute during this project. +Each tasks may be subdivided into smaller ones in the #link(base-url)[*GitHub board*]. + +#let workload = {( + issue(1, [Sample Zephyr], description: [Getting to know with BLE secure advertising and Zephyr], sp: 1), + issue(2, link: (2,), [Node firmware definition], description: [Defining each nodes software (and potential bootloader)], sp: 1), + issue(3, link: (2, 4), [Node implementation], description: [Implementation of the nodes, including low-power care], sp: 8), + issue(4, [Gateway firmware definition], description: [Definition of the gateway software, including communication with the nodes], sp: 1), + issue(5, link: (4,), [Gateway implementation], description: [Implementation of the gateway software], sp: 6), + issue(6, [Database setup], description: [Setup of the temporal data storage, including a service to add new values], sp: 3), + issue(7, [DB data retrieval], link: (6,), description: [Implementation of an API allowing to retrieve data from the DB], sp: 4), + issue(8, [2D map GUI], link: (7,), description: [Implementation of a service displaying the sensor data at a given time], sp: 6), + issue(9, [MeteoSwiss API grasping], description: [Retrieve MeteoSwiss data to get the outside environmental data], sp: 3), + issue(10, [Windows opening forecasting], description: [Training of a model telling the optimal windows opening time], sp: 8), + issue(11, [Notification setup], link: (10,), description: [Considering the window opening recommendation, sends a notification to the room users], sp: 3), + issue(12, [Analysis of the room data evolution], description: [Analysis of the temperature and CO2 level evolution regarding window opening status over time], sp: 7), + issue(13, [Documentation], description: [Report and code comments], sp: 9), +)} + +#let sum-sp = 0 +#for w in workload { + sum-sp += w.at(4) +} + +// #show table.cell.where(y: 0): strong +// #set table( +// stroke: (x, y) => if y == 0 { +// (bottom: 0.7pt + black) +// }, +// align: (x, y) => ( +// if x == 4 { right } +// else { left } +// ) +// ) + +#set par(justify: false) +#table( + columns:( + // 2.5em, + 2fr, + // 3.5em, + 5fr, + 2em + ), + align: left, + table.header( + // "ID", + "Name", + // "Linked", + "Description", + "SP" + ), + ..{ + let task = () + for w in workload { + // task.push(w.at(0)) + task.push(w.at(1)) + // task.push(w.at(2)) + task.push(w.at(3)) + task.push(align(center)[#str(w.at(4))]) + } + task + }, + table.cell(stroke: none)[], + table.cell(stroke: none)[#align(right)[Total:]], + table.cell(stroke: none)[#sum-sp], +) + +The various fields are assigned considering @tab:repartition. +#figure( + table( + columns: (auto, auto), + align: center, + table.header("Field", "Assigned to"), + [Team lead | Administrative elements],[Adrien], + [Embedded Software],[Adrien, Rémi], + [Communication between components],[Rémi, Djelal], + [Database interfacing],[Ibrahima], + [Machine learning | Data exploitation],[Ibrahima, Dimitri], + [Physic Modelling],[Alison] + ) +) + +#let dispatch(id, sp) = { + let task = workload.at(id - 1) + ([#link(task.at(0).dest)[#task.at(0).body - #task.at(1)] (#sp/#task.at(4))], sp) +} + +#let display-dispatching(person) = { + let total = 0.0 + for d in person { + d.at(0) + linebreak() + total = total + d.at(1) + } + underline[Total SP: #str(total)] + +} + +#let Djelal = [ + #underline[A. Djelal]\ + #display-dispatching(( + dispatch(1, 0.5), + dispatch(3, 1), + dispatch(4, 1), + dispatch(5, 6), + dispatch(13, 1.5), + )) +] + +#let Adrien = [ + #underline[B. Adrien]\ + #display-dispatching(( + dispatch(1, 0.5), + dispatch(2, 1), + dispatch(3, 7), + dispatch(13, 1.5), + )) +] + +#let Rémi = [ + #underline[H. Rémi]\ + #display-dispatching(( + dispatch(6, 3), + dispatch(7, 4), + dispatch(9, 1.5), + dispatch(13, 1.5), + )) +] + +#let Dimitri = [ + #underline[I. Dimitri]\ + #display-dispatching(( + dispatch(9, 1.5), + dispatch(10, 7), + dispatch(13, 1.5), + )) +] + +#let Alison = [ + #underline[L. Alison]\ + #display-dispatching(( + dispatch(8, 0.5), + dispatch(10, 1), + dispatch(12, 7), + dispatch(13, 1.5), + )) +] + +#let Ibrahima = [ + #underline[S. Ibrahima]\ + #display-dispatching(( + dispatch(8, 5.5), + dispatch(11, 3), + dispatch(13, 1.5), + )) +] + +#set text(size: 0.8em) +#grid( + columns: (1fr, 1fr), + column-gutter: 1em, + row-gutter: 2em, + Djelal, + Adrien, + Rémi, + Dimitri, + Alison, + Ibrahima, +) + + diff --git a/report/main/specifications/07-deadlines.typ b/report/main/specifications/07-deadlines.typ new file mode 100644 index 0000000..ab3c2a7 --- /dev/null +++ b/report/main/specifications/07-deadlines.typ @@ -0,0 +1,25 @@ +#import "/metadata.typ": * +#pagebreak() + +#let deadline(name, date) = {( + date.display("[day].[month]"), + name, +)} + += Deadline + +The following dates serves as deadlines for the project. +#figure( + table( + columns: (auto, auto), + [*Date*],[*Purpose*], + ..deadline("Specs validation", date.specification-submission), + ..deadline("Ordering of components", date.ordering-components), + ..deadline("First environmental data stored in the database", date.first-environmental-data-in-db), + ..deadline("Mid-term presentation (including prototype demo)", date.mid-term-presentation), + ..deadline("Full data retrieval, forecasting and display", date.objective-completed), + ..deadline("Project report submission", date.submission), + ..deadline("Project defence", date.defence), + ..deadline("Project presentation in front of other groups", date.pitch), + ) +) \ No newline at end of file diff --git a/report/main/specifications/bom.typ b/report/main/specifications/bom.typ new file mode 100644 index 0000000..bbeba7d --- /dev/null +++ b/report/main/specifications/bom.typ @@ -0,0 +1,74 @@ +#import "/metadata.typ": * +#pagebreak() += BOM + +In @tab:bom, the indicated prices are in CHF. + +#figure( + table( + columns: (auto, auto, auto, auto, auto), + align: center, + table.header("Component", "Price", "Nb unit", "Total", "Note"), + + link("https://www.digikey.ch/fr/products/detail/nordic-semiconductor-asa/NRF6936/7175577?s=N4IgTCBcDaIC4AsCWA7A5gTwAQFYIF0BfIA")[Thingy 52], + [32.39], + [25], + [0], + [Supplied by the good will of the HES-SO Valais/Wallis], + + link("https://www.digikey.ch/fr/products/detail/rakwireless-technology-limited/raspberry-pi-4-model-b-4gb/17138043")[Raspberry Pi 4], + [111.17], + [1], + [0], + [Supplied by the good will of the HES-SO Valais/Wallis], + + [DB Influx], + [0], + [1], + [0], + [Open-source, hosted by the good Will of M. Heredero], + + [MatLab License], + [0], + [1], + [0], + [Free for students], + + [Mosquitto MQTT Broker], + [0], + [1], + [0], + [Open-source], + + [Python Bleak library], + [0], + [1], + [0], + [BLE scanning, open-source], + + [Python paho-mqtt library], + [0], + [1], + [0], + [MQTT client, open-source], + + [ML model training], + [0], + [1], + [0], + [Done locally], + + [ML inference], + [0], + [1], + [0], + [AWS free tier or hosted locally], + + link("https://www.digikey.ch/en/products/detail/e-switch/MS0850502F020S1A/1628279")[MS08050502F020S1A], + [0.93], + [10], + [9,3], + [Limit switch], + + ), +) diff --git a/report/main/specifications/deliverables.typ b/report/main/specifications/deliverables.typ new file mode 100644 index 0000000..771e028 --- /dev/null +++ b/report/main/specifications/deliverables.typ @@ -0,0 +1,18 @@ +#import "/metadata.typ": * +#pagebreak() += Deliverables + +The following deliverables are expected by the end of the project. +#v(1.5em) +#let deliverables = [ + #show: el.config.ref + #show: el.default-enum-list + #set enum(numbering: "Dl1") + + Software executable on the defined nodes hardware delivering the CO2 level and temperature. + + Software executable on the defined gateway hardware retrieving the data from the nodes and pushing them to a database + + Database, with an associated API, to store and retrieve the data sent by the gateway + + Data model indicating if data from a node is coherent when compared to data adjacent in the database + + Data model forecasting each node data. + The forecasting duration is to be defined. +] +#deliverables \ No newline at end of file diff --git a/report/meetings/260312-specifications/slides.typ b/report/meetings/260312-specifications/slides.typ new file mode 100644 index 0000000..995c179 --- /dev/null +++ b/report/meetings/260312-specifications/slides.typ @@ -0,0 +1,119 @@ +#import "/metadata.typ": * +#import "/tail/bibliography.typ": * +#import "/tail/glossary.typ": * + +#import "/resources/slides.typ": * + +#show:make-glossary +#register-glossary(entry-list) + +#let HANDOUT = true +#let NOTES = false + +#show: metropolis-theme.with( + aspect-ratio: "16-9", + config-info( + title: title_style(doc.title), + subtitle: [ + #doc.subtitle \ + Specification presentation + ], + author: enumerating-authors(items: doc.author, multiline: false ), + date: date.specification-submission, + institution: school.name, + ), + footer: self => [#self.info.institution], + ..get-config(), + config-common( + handout: HANDOUT, + show-notes-on-second-screen: if NOTES {right} else {none} + ) +) +#set text(region: "gb") + +#show: myglobals + +#title-slide() + +// = Intro +// --- +// #import "../../main/specifications/02-introduction.typ": * + += Context +--- +#import "../../main/specifications/03-context.typ": * + += Objectives +--- +#import "../../main/specifications/05-objectives.typ": * +#objectives + +== Top level +//#top-level-architecture + += Constraint +--- +#import "../../main/specifications/04-constraints.typ": * +#constraints + += Planned Tasks +--- +#import "../../main/specifications/06-planned_tasks.typ" as pt + +#let display-task(task) = if task != none {table( + columns: (4fr, 1fr), + rows: (1fr, 2fr), + stroke: none, + + table.cell(stroke: (bottom: 1pt))[#task.at(0) - #task.at(1)], + str(task.at(4)), + table.cell(colspan: 2)[ + #task.at(3) + ], +)} else [] + +#for i in range(int(pt.workload.len()/4+1)) { + let task1 = if (i*4>=pt.workload.len()) {none} else {pt.workload.at((i*4))} + let task2 = if (i*4+1>=pt.workload.len()) {none} else {pt.workload.at((i*4)+1)} + let task3 = if (i*4+2>=pt.workload.len()) {none} else {pt.workload.at((i*4)+2)} + let task4 = if (i*4+3>=pt.workload.len()) {none} else {pt.workload.at((i*4)+3)} + grid( + columns: (1fr, 1fr), + rows: (1fr, 1fr), + column-gutter: 3em, + row-gutter: 2em, + + if task1 != none [#display-task(task1)], + if task2 != none [#pause #display-task(task2)], + if task3 != none [#pause #display-task(task3)], + if task4 != none [#pause #display-task(task4)], + + ) + [---] +} + +== Dispatching +#set text(size: 0.73em) +#grid( + columns: (1fr, 1fr, 1fr), + column-gutter: 1em, + row-gutter: 2em, + align: top+left, + pt.Djelal, pause, + pt.Adrien, pause, + pt.Rémi, pause, + pt.Dimitri, pause, + pt.Alison, pause, + pt.Ibrahima, +) + + +#focus-slide[ + Questions? +] + +#{ + // set text(size: 0pt) + make_glossary(gloss:gloss, title:i18n("gloss-title")) +} + diff --git a/report/meetings/260416-midterm/slides.typ b/report/meetings/260416-midterm/slides.typ new file mode 100644 index 0000000..14cb998 --- /dev/null +++ b/report/meetings/260416-midterm/slides.typ @@ -0,0 +1,241 @@ +#import "/metadata.typ": * +#import "/tail/bibliography.typ": * +#import "/tail/glossary.typ": * + +#import "/resources/slides.typ": * + +#show:make-glossary +#register-glossary(entry-list) + +#let HANDOUT = false +#let NOTES = false + +#show: metropolis-theme.with( + aspect-ratio: "16-9", + config-info( + title: title_style(doc.title), + subtitle: [ + // #doc.subtitle \ + Team Plein de eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + \ + Midterm presentation + ], + author: enumerating-authors(items: doc.author, multiline: false ), + date: date.mid-term-presentation, + institution: school.name, + ), + footer: self => [#self.info.institution], + ..get-config(), + config-common( + handout: HANDOUT, + show-notes-on-second-screen: if NOTES {right} else {none} + ) +) +#set text(region: "gb") + +#show: myglobals + +#title-slide() + +// 20 min presentation +// 20 min Q&A + += Intro + +== Architecture +#figure( + image("../../resources/img/PI-top-level.drawio.png"), + caption: [Top level architecture], + alt: "Thingy52 --BLE secure advertising--> Raspberry Pi --MQTT--> RabittMQ --GO--> InfluxDB | Influx + MeteoSwiss --> LSTM --> Teams notification | InfluxDB --> MatLab --> InfluxDB" +) + +--- +#figure( + image("../../resources/img/bdd_complete.svg"), + caption: [Top-level architecture, including interfaces between units] +) + +== Objectives +#import "../../main/specifications/05-objectives.typ": * +#objectives-mo +#objectives-oo + += Work done + +== Nodes +#figure( + image("../../resources/img/nodes_class_diagram.svg"), + caption: [Nodes class diagram] +) + +--- +#figure( + image("../../resources/img/nodes_sequence_diagram.svg"), + caption: [Nodes sequence diagram] +) + +== Gateway +- Listening on BLE +- Sending on MQTT + +== DB Influx +- Server setup (LXC, Docker, ssh certificates, Traefik, DNS challenge) #emoji.checkmark #pause +- Influx DB 3 #emoji.checkmark #pause +- RabbitMQ #emoji.checkmark #pause +- Gateway MQTT -> DB #emoji.circle.orange #pause +- DB -> REST #emoji.crossmark + +== User Interface +- CI/CD +#pause +- Interactive 2D Map +#pause +- Air quality history + +== Data usage +=== Open data analysis + +Open data analysis (no mechanical ventilation, no window opening): +- Study of the variation in @co2 levels over time in several classrooms in Munich. #cite() + +- Study of the variation in @co2 level over time for offices and secondary school. #cite() + +- Online air quality calculator for classrooms "Simaria" developt by @ofsp:short. + +--- +=== Time evolution of @co2:short level +#figure( + image("../../resources/img/Time_evolution_CO2_Twardella.png", height: 85%), + caption: [Time evolution of @co2:short level for #cite()] +) + +--- + +#grid( + columns: (1fr, 1fr), + [ + #figure( + image("../../resources/img/Time_evolution_CO2_INRS_office.png"), + caption: [Time evolution of @co2:short level in an office for #cite()] + ) + ], [ + #figure( + image("../../resources/img/Time_evolution_CO2_INRS_secondaryschool.png"), + caption: [Time evolution of @co2:short level in a secondary school for #cite()] + ) + ] +) + +#grid( + columns: (1fr, 1fr), + [ + #figure( + image("../../resources/img/Time_evolution_CO2_INRS_meetingroom.png"), + caption: [Time evolution of @co2:short level in a meeting room for #cite()] + ) + ], + [ + #figure( + image("../../resources/img/Time_evolution_CO2_Simaria.png"), + caption: [Time evolution of @co2:short for Simaria calculator for a same volume and variation of students number] + ) + ] +) +--- +=== Time evolution of @co2:short level - Case study classroom A4 Provence + +#grid( + columns: (1fr, 1fr), + [ + #figure( + image("../../resources/img/Excerpt case study Provence Simaria.png"), + caption: [Simaria interface: input] + ) + ], + [ + #figure( + image("../../resources/img/Excerpt cas study Provence Simaria _results.png"), + caption: [Simaria interface: results] + ) + ] +) + +- Threshold 1400 ppm reached after 22 min and 2000 ppm after 38 min. + +--- +=== Calculation of the time taken to reach the threshold of 1400 ppm for each data set + +#figure( + image("../../resources/img/Time_reaching_threshold.png"), + caption: [Time reaching threshold of 1400 ppm in function of air volume per person] +) + +#grid( + columns: (1fr, 1fr), + [ + #figure( + image("../../resources/img/User action.png"), + caption: [User action] + ) + ], + [ + #figure( + image("../../resources/img/User action result.png"), + caption: [Result user action] + ) + ] +) +== Forecasting +bus factor +#{ + set text(size: 2em) + linebreak() + emoji.face.cry + linebreak() + emoji.bus +} + += Demo + += Conclusion + +== Objectives adjusted +- Kept: +MO1: The environmental data for the observed rooms are stored in a database with an associated timestamp\ +MO2: The stored environmental data can be displayed in a 2D map. The display allows to show both stored and live data.\ +MO4: The changes in temperature when airing the room, with regards to outside temperature, are analysed and used for the window opening time recommendations.\ +--- +- Changed: +MO3: #strike[Using the stored environmental data, alongside external meteorological data, the system send a notification when the windows should be opened or closed.]\ +The system sends a notification when the measured CO2 level overreaches the 1400ppm threshold + +== Next steps +- Nodes: Implementation and integration with the Gateway +#pause +- Gateway: Integration of nodes. Nice-to-have : encrypted communication with nodes. +#pause +- DB: Communication with MQTT and retrieval API +#pause +- User Interface: API integration and live CO2 level alarm +#pause +- Data usage: Improvement of python program +#pause +- Forecasting: Nice-to-have, shall effort be available +#pause +- All: Documentation and reviews + + + +//------------------------------------- +// Bibliography +// +#pagebreak() +#make_bibliography(bib:bib, title:i18n("bib-title", lang: option.lang)) + +//------------------------------------- +// Glossary +// +#{ + // set text(size: 0.5em) + make_glossary(gloss:gloss, title:i18n("gloss-title")) +} \ No newline at end of file diff --git a/report/meetings/260611-final/slides.typ b/report/meetings/260611-final/slides.typ new file mode 100644 index 0000000..5761a72 --- /dev/null +++ b/report/meetings/260611-final/slides.typ @@ -0,0 +1,73 @@ +#import "/metadata.typ": * +#import "/tail/bibliography.typ": * +#import "/tail/glossary.typ": * + +#import "/resources/slides.typ": * + +#show:make-glossary +#register-glossary(entry-list) + +#let HANDOUT = true +#let NOTES = false + +#show: metropolis-theme.with( + aspect-ratio: "16-9", + config-info( + title: title_style(doc.title), + subtitle: doc.subtitle, + author: enumerating-authors(items: doc.author, multiline: false ), + date: date.defence, + institution: school.name, + ), + footer: self => [#self.info.institution], + ..get-config(), + config-common( + handout: HANDOUT, + show-notes-on-second-screen: if NOTES {right} else {none} + ) +) +#set text(region: "gb") + +#show: myglobals + +#title-slide() + +// 20 min presentation +// 5 (students) + 25 (teacher) min Q&A + +/* +technical -> each section should go for around (3min / pers) +- Node: Adrien +- Gateway: Djelal +- DB: Rémi +- UI: Ibrahima +- Physique: Alison + +general (50s/pers) +1. Context (dimitri) (Rémi) +2. architecture + choix (BLE, MQTT, Influx, ) (Ibrahima) +3. organisation et gestion des tâches et resources (Djelal) + +TECHNIQUE + +4. regard critique (adrien) +5. developement futur (Alison) + +// Slide in english +// Presentation en français + +*/ + += Intro + +== foo + +Yolo + +== bar +Hello world + += Conclusion + +--- +It's the end of the world \ No newline at end of file diff --git a/report/metadata.typ b/report/metadata.typ new file mode 100644 index 0000000..27b3121 --- /dev/null +++ b/report/metadata.typ @@ -0,0 +1,188 @@ +#import "@preview/hei-synd-thesis:0.4.0": * +#import "@preview/itemize:0.2.0" as el +#import "@preview/touying:0.6.1": * +#import "@preview/codelst:2.0.2": sourcecode +#import "@preview/pinit:0.2.2": * + +//------------------------------------- +// Document options +// +#let option = ( + type: sys.inputs.at("type", default:"draft"), // [draft|final] + lang: "en", + region: "gb", + template: "thesis", // [thesis/midterm] +) +//------------------------------------- +// Optional generate titlepage image +// +#import "@preview/fractusist:0.3.2":* +#let project-logo= dragon-curve( + 12, + step-size: 1.6, + stroke: stroke( + paint: gradient.radial(..color.map.rocket), + thickness: 0.5pt, join: "round" + ) +) + +//------------------------------------- +// Metadata of the document +// +#let doc = ( + title : "PI E2EEDA", + subtitle : "Team: Plein de eeeeeeeeeeeeeeeeeeeeee", + abbr : "EEEEEE", + // author : ( + // gender : "inclusive", // ["masculin"|"feminin"|"inclusive"] + // name : "Djelal A., Adrien B., Rémi H., Dimitri I., Alison L., Ibrahima S.", + // degree : "Master", + // affiliation : "HES-SO", + // place : "Provence", + // ), + author :( + ( + gender: "masculin", + name: "Djelal Avdil", + abbr: "ADD", + email: "djelal.avdil@master.hes-so.ch", + institute: "HES-SO", + ), + ( + gender: "masculin", + name: "Adrien Dino Balleyguier", + abbr: "BAD", + email: "adriendino.balleyguier@hefr.ch", + institute: "HEIA-FR", + ), + ( + gender: "masculin", + name: "Rémi Heredero", + abbr: "HER", + email: "remi.heredero@hevs.ch", + institute: "HEVS", + ), + // ( + // gender: "masculin", + // name: "Dimitri Imfeld", + // abbr: "IMD", + // email: "dimitri.imfeld@master.hes-so.ch", + // institute: "HES-SO", + // ), + ( + gender: "feminin", + name: "Alison Lecointre", + abbr: "LEA", + email: "alison.lecointre@master.hes-so.ch", + institute: "HES-SO", + ), + ( + gender: "masculin", + name: "Ibrahima Seye", + abbr: "SEI", + email: "ibrahima.seye@master.hes-so.ch", + institute: "HES-SO", + ), + ), + keywords : ("HES-OS", "Master", "CS", "EnEn", "IoT", "ML"), + version : "v0.1.0", +) + +#let data-page = none // [bytes|none] + +#let professors = ( + ( + institute: "HEIA-FR", + name: "Prof. Serge Ayer", + email: "serge.ayer@hefr.ch", + ), + ( + institute: "HEIA-FR", + name: "Prof. Luca Haab", + email: "luca.haab@hefr.ch", + ), + ( + institute: "HEVS", + name: "Prof. Fabrizzio Sossan", + email: "fabrizio.sossan@hevs.ch", + ) +) +#let expert= none +#let school= ( + name: "HES-SO Master", + orientation: none, + specialisation: none, +) +school.name = "University of Applied Sciences Western Switzerland, HES-SO" +school.shortname = "HES-SO" + +#let date = ( + today: datetime.today(), + specification-submission: datetime(year: 2026, month: 3, day: 12), + ordering-components: datetime(year: 2026, month: 3, day: 19), + first-environmental-data-in-db: datetime(year: 2026, month: 4, day: 09), + mid-term-presentation: datetime(year: 2026, month: 4, day: 16), + objective-completed: datetime(year: 2026, month: 5, day: 28), + submission: datetime(year: 2026, month: 6, day: 4), + defence: datetime(year: 2026, month: 6, day: 11), + pitch: datetime(year: 2026, month: 6, day: 25), +) + +#let logos = ( + main: project-logo, + topleft: if option.lang == "fr" or option.lang == "de" { + image("/resources/img/logos/hei-defr.svg", width: 6cm) + } else { + image("/resources/img/logos/hei-en.svg", width: 6cm) + }, + topright: image("/resources/img/logos/hesso-logo.svg", width: 4cm), + bottomleft: image("/resources/img/logos/hevs-pictogram.svg", width: 4cm), + bottomright: image("/resources/img/logos/swiss_universities-valais-excellence-logo.svg", width: 5cm), + logo-hesso: image("/resources/img/logos/logo-hesso.png", width: 5cm), + logo-hesso-master: image("/resources/img/logos/logo-hesso-master.jpg", width: 3cm), + logo-mse: image("/resources/img/logos/logo-mse.png", width: 16cm), +) + +//------------------------------------- +// Settings +// +#let tableof = ( + toc: true, + tof: false, + tot: false, + tol: false, + toe: false, + maxdepth: 2, +) + +#let gloss = true +#let appendix = true +#let bib = ( + display : true, + path : "/tail/bibliography.bib", + style : "ieee", //"apa", "chicago-author-date", "chicago-notes", "mla" +) + +//------------------------------------- +// Utilities +// + +#let warn(element, message) = { + box( + stroke: red + 2pt, + outset: 2pt, + { + element + place( + top + left, + dx: -1pt, + dy: -1pt, + box( + fill: yellow, + inset: .2em, + message, + ), + ) + }, + ) +} \ No newline at end of file diff --git a/report/resources/code/rustfmt.toml b/report/resources/code/rustfmt.toml new file mode 100644 index 0000000..d5e254e --- /dev/null +++ b/report/resources/code/rustfmt.toml @@ -0,0 +1,10 @@ +edition = "2021" + +tab_spaces = 2 +max_width = 80 + +#indent_style = "Block" +#trailing_comma = "Always" + +#merge_imports = true +reorder_imports = true diff --git a/report/resources/code/uss-rustacean.rs b/report/resources/code/uss-rustacean.rs new file mode 100644 index 0000000..3ff95f7 --- /dev/null +++ b/report/resources/code/uss-rustacean.rs @@ -0,0 +1,22 @@ +struct Starship { + name: String, + position: (f64, f64, f64), +} + +impl Starship { + fn new(name: &str, position: (f64, f64, f64)) -> Self { + Self { + name: name.into(), + position, + } + } + fn distance_to(&self, dest: (f64, f64, f64)) -> f64 { + ((dest.0 - self.position.0).powi(2) + + (dest.1 - self.position.1).powi(2) + + (dest.2 - self.position.2).powi(2)) + .sqrt() + } + fn optimal_warp(&self, distance: f64) -> f64 { + (distance / 10.0).sqrt().min(9.0) + } +} diff --git a/report/resources/helper.typ b/report/resources/helper.typ new file mode 100644 index 0000000..7f48ac9 --- /dev/null +++ b/report/resources/helper.typ @@ -0,0 +1,94 @@ +#import "@preview/hei-synd-thesis:0.2.3": * +#import "@preview/wrap-it:0.1.1": * + +#let iconbox( + width: 100%, + radius: 4pt, + border: 4pt, + inset: 10pt, + outset: -10pt, + linecolor: code-border, + icon: none, + iconheight: 1cm, + body +) = { + if body != none { + align(left, + rect( + stroke: (left:linecolor+border, rest:code-border+0.1pt), + radius: (left:0pt, right:radius), + fill: code-bg, + outset: (left:outset, right:outset), + inset: (left:inset*2, top:inset, right:inset*2, bottom:inset), + width: width)[ + #if icon != none { + align(left, + table( + stroke:none, + align:left+horizon, + columns: (auto,auto), + image(icon, height:iconheight), [#body] + ) + ) + } else { + body + } + ] + ) + } +} + +#let llm-proofread = iconbox( + icon:"/resources/img/logos/chatgpt.svg", + linecolor: hei-blue +)[ + #text(size: small)[ + Assistance for the reformulation and paraphrasing of this document was provided by Gemini 3.1. + Regardless of this assistance, all original code and text remain the work of the original author. +]] + +#let ai-warning(txt) = grid( + columns: 2, + align: horizon, + column-gutter: 1em, + image("/resources/img/logos/chatgpt.svg", width: 2em), + [#txt] +) + +#import "@preview/chronos:0.3.0" + +#let actor( + name, + disp_name: none, + ..args +) = { + let dn = name + if disp_name != none { + dn = align(center+horizon)[#disp_name] + } + let par = chronos._par(name, display-name: dn, ..args.named()) + let actor = ( + "display": par, + "name" : name + ) + return actor +} + +#let sync(actor1, actor2, comment, ..args) = { + return chronos._seq( + actor1.name, actor2.name, + comment: comment, + comment-align: "start", + ..args.named() + ) +} +#let async(actor1, actor2, comment, slant: auto, ..args) = { + return chronos._seq( + actor1.name, actor2.name, + comment: comment, + comment-align: "start", + end-tip: ">>", + slant: slant, + ..args.named() + ) +} \ No newline at end of file diff --git a/report/resources/img/Excerpt cas study Provence Simaria _results.png b/report/resources/img/Excerpt cas study Provence Simaria _results.png new file mode 100644 index 0000000..0f7560e Binary files /dev/null and b/report/resources/img/Excerpt cas study Provence Simaria _results.png differ diff --git a/report/resources/img/Excerpt case study Provence Simaria.png b/report/resources/img/Excerpt case study Provence Simaria.png new file mode 100644 index 0000000..2a2e9c6 Binary files /dev/null and b/report/resources/img/Excerpt case study Provence Simaria.png differ diff --git a/report/resources/img/PI-get_data.drawio.png b/report/resources/img/PI-get_data.drawio.png new file mode 100644 index 0000000..f6f48a2 Binary files /dev/null and b/report/resources/img/PI-get_data.drawio.png differ diff --git a/report/resources/img/PI-lstm.drawio.png b/report/resources/img/PI-lstm.drawio.png new file mode 100644 index 0000000..a76cb4b Binary files /dev/null and b/report/resources/img/PI-lstm.drawio.png differ diff --git a/report/resources/img/PI-top-level.drawio.png b/report/resources/img/PI-top-level.drawio.png new file mode 100644 index 0000000..a98d9da Binary files /dev/null and b/report/resources/img/PI-top-level.drawio.png differ diff --git a/report/resources/img/Physical model/CO2 concentration over time INRS meeting room.png b/report/resources/img/Physical model/CO2 concentration over time INRS meeting room.png new file mode 100644 index 0000000..e1cc52f Binary files /dev/null and b/report/resources/img/Physical model/CO2 concentration over time INRS meeting room.png differ diff --git a/report/resources/img/Physical model/CO2 concentration over time INRS office room.png b/report/resources/img/Physical model/CO2 concentration over time INRS office room.png new file mode 100644 index 0000000..6cc284c Binary files /dev/null and b/report/resources/img/Physical model/CO2 concentration over time INRS office room.png differ diff --git a/report/resources/img/Physical model/CO2 concentration over time INRS secondary school.png b/report/resources/img/Physical model/CO2 concentration over time INRS secondary school.png new file mode 100644 index 0000000..59cfa2a Binary files /dev/null and b/report/resources/img/Physical model/CO2 concentration over time INRS secondary school.png differ diff --git a/report/resources/img/Physical model/CO2 concentration over time Simaria.png b/report/resources/img/Physical model/CO2 concentration over time Simaria.png new file mode 100644 index 0000000..fd769d6 Binary files /dev/null and b/report/resources/img/Physical model/CO2 concentration over time Simaria.png differ diff --git a/report/resources/img/Physical model/CO2 concentration over time user window opening model.png b/report/resources/img/Physical model/CO2 concentration over time user window opening model.png new file mode 100644 index 0000000..1f115a7 Binary files /dev/null and b/report/resources/img/Physical model/CO2 concentration over time user window opening model.png differ diff --git a/report/resources/img/Physical model/CO2 concentration over time user.png b/report/resources/img/Physical model/CO2 concentration over time user.png new file mode 100644 index 0000000..6f00896 Binary files /dev/null and b/report/resources/img/Physical model/CO2 concentration over time user.png differ diff --git a/report/resources/img/Physical model/CO2 concentration window opening A2 to A6.png b/report/resources/img/Physical model/CO2 concentration window opening A2 to A6.png new file mode 100644 index 0000000..647e455 Binary files /dev/null and b/report/resources/img/Physical model/CO2 concentration window opening A2 to A6.png differ diff --git a/report/resources/img/Physical model/Comparison_expdata_model.png b/report/resources/img/Physical model/Comparison_expdata_model.png new file mode 100644 index 0000000..6492202 Binary files /dev/null and b/report/resources/img/Physical model/Comparison_expdata_model.png differ diff --git a/report/resources/img/Physical model/Window user no window opening model.png b/report/resources/img/Physical model/Window user no window opening model.png new file mode 100644 index 0000000..535fc78 Binary files /dev/null and b/report/resources/img/Physical model/Window user no window opening model.png differ diff --git a/report/resources/img/Physical model/Window user window opening model.png b/report/resources/img/Physical model/Window user window opening model.png new file mode 100644 index 0000000..091d306 Binary files /dev/null and b/report/resources/img/Physical model/Window user window opening model.png differ diff --git a/report/resources/img/Physical model/data flow diagram no window opening v1.png b/report/resources/img/Physical model/data flow diagram no window opening v1.png new file mode 100644 index 0000000..7eed3c2 Binary files /dev/null and b/report/resources/img/Physical model/data flow diagram no window opening v1.png differ diff --git a/report/resources/img/Physical model/data flow diagram no window opening v2.png b/report/resources/img/Physical model/data flow diagram no window opening v2.png new file mode 100644 index 0000000..2acd20d Binary files /dev/null and b/report/resources/img/Physical model/data flow diagram no window opening v2.png differ diff --git a/report/resources/img/Physical model/data flow diagram window opening .png b/report/resources/img/Physical model/data flow diagram window opening .png new file mode 100644 index 0000000..a9f7226 Binary files /dev/null and b/report/resources/img/Physical model/data flow diagram window opening .png differ diff --git a/report/resources/img/Time_evolution_CO2_INRS_meetingroom.png b/report/resources/img/Time_evolution_CO2_INRS_meetingroom.png new file mode 100644 index 0000000..cd8adfe Binary files /dev/null and b/report/resources/img/Time_evolution_CO2_INRS_meetingroom.png differ diff --git a/report/resources/img/Time_evolution_CO2_INRS_office.png b/report/resources/img/Time_evolution_CO2_INRS_office.png new file mode 100644 index 0000000..91aefb1 Binary files /dev/null and b/report/resources/img/Time_evolution_CO2_INRS_office.png differ diff --git a/report/resources/img/Time_evolution_CO2_INRS_secondaryschool.png b/report/resources/img/Time_evolution_CO2_INRS_secondaryschool.png new file mode 100644 index 0000000..e420c47 Binary files /dev/null and b/report/resources/img/Time_evolution_CO2_INRS_secondaryschool.png differ diff --git a/report/resources/img/Time_evolution_CO2_Simaria.png b/report/resources/img/Time_evolution_CO2_Simaria.png new file mode 100644 index 0000000..511bf33 Binary files /dev/null and b/report/resources/img/Time_evolution_CO2_Simaria.png differ diff --git a/report/resources/img/Time_evolution_CO2_Twardella.png b/report/resources/img/Time_evolution_CO2_Twardella.png new file mode 100644 index 0000000..b0d17d9 Binary files /dev/null and b/report/resources/img/Time_evolution_CO2_Twardella.png differ diff --git a/report/resources/img/Time_reaching_threshold.png b/report/resources/img/Time_reaching_threshold.png new file mode 100644 index 0000000..9e04d55 Binary files /dev/null and b/report/resources/img/Time_reaching_threshold.png differ diff --git a/report/resources/img/User action result.png b/report/resources/img/User action result.png new file mode 100644 index 0000000..6ba51a0 Binary files /dev/null and b/report/resources/img/User action result.png differ diff --git a/report/resources/img/User action.png b/report/resources/img/User action.png new file mode 100644 index 0000000..41d0061 Binary files /dev/null and b/report/resources/img/User action.png differ diff --git a/report/resources/img/bdd_complete.svg b/report/resources/img/bdd_complete.svg new file mode 100644 index 0000000..1f68da1 --- /dev/null +++ b/report/resources/img/bdd_complete.svg @@ -0,0 +1 @@ +nodes 1nodes 2nodes ...gatewaydatabasedata validationdisplayforecastingteamsnodes interfacenodes interfacenodes interfacedb setdb APIteams API \ No newline at end of file diff --git a/report/resources/img/class_diagram.svg b/report/resources/img/class_diagram.svg new file mode 100644 index 0000000..e924ff2 --- /dev/null +++ b/report/resources/img/class_diagram.svg @@ -0,0 +1,57 @@ +Gatewaygateway_id: strmqtt_broker: strmqtt_port: intmqttc: MQTTClientEXPECTED_PAYLOAD_SIZE: 14DEDUP_WINDOW_SECONDS: 10INVALID_VALUE: 0xFFFFFFFFdecode_payload(data: bytes): dictpublish(mac: str, data: dict)is_duplicate(mac: str): boolon_device_found(device, adv_data)on_connect(...)on_disconnect(...)on_publish(...)run()BleakScannerstart()stop()MQTTClientconnect()publish()tls_set()username_pw_set()loop_start() \ No newline at end of file diff --git a/report/resources/img/icon.svg b/report/resources/img/icon.svg new file mode 100644 index 0000000..e9f580c --- /dev/null +++ b/report/resources/img/icon.svg @@ -0,0 +1,459 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/report/resources/img/logos/chatgpt.svg b/report/resources/img/logos/chatgpt.svg new file mode 100644 index 0000000..e81233f --- /dev/null +++ b/report/resources/img/logos/chatgpt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/report/resources/img/logos/hei-defr.svg b/report/resources/img/logos/hei-defr.svg new file mode 100644 index 0000000..f973db4 --- /dev/null +++ b/report/resources/img/logos/hei-defr.svg @@ -0,0 +1,280 @@ + +image/svg+xml diff --git a/report/resources/img/logos/hei-en.svg b/report/resources/img/logos/hei-en.svg new file mode 100644 index 0000000..a0ff283 --- /dev/null +++ b/report/resources/img/logos/hei-en.svg @@ -0,0 +1,170 @@ + +image/svg+xml diff --git a/report/resources/img/logos/hesso-logo.svg b/report/resources/img/logos/hesso-logo.svg new file mode 100644 index 0000000..c7fda57 --- /dev/null +++ b/report/resources/img/logos/hesso-logo.svg @@ -0,0 +1,390 @@ + +image/svg+xml \ No newline at end of file diff --git a/report/resources/img/logos/hevs-pictogram.svg b/report/resources/img/logos/hevs-pictogram.svg new file mode 100644 index 0000000..48ae8ce --- /dev/null +++ b/report/resources/img/logos/hevs-pictogram.svg @@ -0,0 +1,130 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/report/resources/img/logos/logo-hesso-master.jpg b/report/resources/img/logos/logo-hesso-master.jpg new file mode 100644 index 0000000..5f56533 Binary files /dev/null and b/report/resources/img/logos/logo-hesso-master.jpg differ diff --git a/report/resources/img/logos/logo-hesso.png b/report/resources/img/logos/logo-hesso.png new file mode 100644 index 0000000..8d73036 Binary files /dev/null and b/report/resources/img/logos/logo-hesso.png differ diff --git a/report/resources/img/logos/logo-mse.png b/report/resources/img/logos/logo-mse.png new file mode 100644 index 0000000..281f17a Binary files /dev/null and b/report/resources/img/logos/logo-mse.png differ diff --git a/report/resources/img/logos/mse.png b/report/resources/img/logos/mse.png new file mode 100644 index 0000000..ddc8792 Binary files /dev/null and b/report/resources/img/logos/mse.png differ diff --git a/report/resources/img/logos/swiss_universities-valais-excellence-logo.svg b/report/resources/img/logos/swiss_universities-valais-excellence-logo.svg new file mode 100644 index 0000000..ffc4f11 --- /dev/null +++ b/report/resources/img/logos/swiss_universities-valais-excellence-logo.svg @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/report/resources/img/logos/synd.svg b/report/resources/img/logos/synd.svg new file mode 100644 index 0000000..a4626ab --- /dev/null +++ b/report/resources/img/logos/synd.svg @@ -0,0 +1,44 @@ + + + + diff --git a/report/resources/img/node_naked.png b/report/resources/img/node_naked.png new file mode 100644 index 0000000..d2b054d Binary files /dev/null and b/report/resources/img/node_naked.png differ diff --git a/report/resources/img/node_window.png b/report/resources/img/node_window.png new file mode 100644 index 0000000..df2508d Binary files /dev/null and b/report/resources/img/node_window.png differ diff --git a/report/resources/img/nodes_battery_consumption.png b/report/resources/img/nodes_battery_consumption.png new file mode 100644 index 0000000..4e00944 Binary files /dev/null and b/report/resources/img/nodes_battery_consumption.png differ diff --git a/report/resources/img/nodes_class_diagram.svg b/report/resources/img/nodes_class_diagram.svg new file mode 100644 index 0000000..5b63eeb --- /dev/null +++ b/report/resources/img/nodes_class_diagram.svg @@ -0,0 +1 @@ +SupervisorBLE_advertiseradvertise(data)Sensorinit(...)get_value()Window_statusHygrometerThermometerCO2_level \ No newline at end of file diff --git a/report/resources/img/nodes_class_diagram_impl.svg b/report/resources/img/nodes_class_diagram_impl.svg new file mode 100644 index 0000000..d4b2f4b --- /dev/null +++ b/report/resources/img/nodes_class_diagram_impl.svg @@ -0,0 +1 @@ +SupervisorBLE_advertiseradvertise(data)Sensorinit(...)get_value()Window_statusHygrometerThermometerCO2_levelBattery_level \ No newline at end of file diff --git a/report/resources/img/nodes_sequence_diagram.svg b/report/resources/img/nodes_sequence_diagram.svg new file mode 100644 index 0000000..275d87e --- /dev/null +++ b/report/resources/img/nodes_sequence_diagram.svg @@ -0,0 +1 @@ +SupervisorSensorsBLE_advertiserSupervisorSupervisorSensorsSensorsBLE_advertiserBLE_advertiserget_value()valueadvertise(data)alt[CO2 level above 400 OR windows is open]sleep 2 minutes[else]sleep for [last sleep time + 2 minutes]max sleep time = 30 minutes \ No newline at end of file diff --git a/report/resources/img/placeholder.svg b/report/resources/img/placeholder.svg new file mode 100644 index 0000000..1911dab --- /dev/null +++ b/report/resources/img/placeholder.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/report/resources/img/project-logo.svg b/report/resources/img/project-logo.svg new file mode 100644 index 0000000..803c4aa --- /dev/null +++ b/report/resources/img/project-logo.svg @@ -0,0 +1,1057 @@ + + + + diff --git a/report/resources/img/sequence_data_collection.svg b/report/resources/img/sequence_data_collection.svg new file mode 100644 index 0000000..d3e503a --- /dev/null +++ b/report/resources/img/sequence_data_collection.svg @@ -0,0 +1,52 @@ +BLE broadcasterBLE broadcasterGateway(Raspberry Pi)Gateway(Raspberry Pi)MQTT Broker(RabbitMQ)MQTT Broker(RabbitMQ)Database(InfluxDB)Database(InfluxDB)Data collectionBLE advertising packetalt[company_id != 0xffff]discard packetcheck payload sizealt[payload size != 14 bytes]discard packet(non-Thingy device)decode_payload()alt[unknown key in payload]discard packetlog debugalt[CO2 == 0xFFFFFFFF]discard CO2 value(sensor not ready)alt[duplicate within 10 seconds]discard packet(deduplication)add UTC timestamppublish JSON{gateway_id}/{mac}/updateon_publish confirmed \ No newline at end of file diff --git a/report/resources/img/sequence_startup.svg b/report/resources/img/sequence_startup.svg new file mode 100644 index 0000000..0c55c90 --- /dev/null +++ b/report/resources/img/sequence_startup.svg @@ -0,0 +1,25 @@ +Gateway(Raspberry Pi)Gateway(Raspberry Pi)MQTT Broker(RabbitMQ)MQTT Broker(RabbitMQ)Startupload config.jsonconnect (MQTTS, TLS, auth)connectedstart BLE scan \ No newline at end of file diff --git a/report/resources/img/signature.svg b/report/resources/img/signature.svg new file mode 100644 index 0000000..6a92d19 --- /dev/null +++ b/report/resources/img/signature.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/report/resources/img/specifications.svg b/report/resources/img/specifications.svg new file mode 100644 index 0000000..97593a6 --- /dev/null +++ b/report/resources/img/specifications.svg @@ -0,0 +1,3442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HES-SO Valais + SYND + ETE + TEVI + + + + + + + + + + + + + + X + X + X + + + + + + + + + + + + + + + + + + + + + Données du travail de diplôme + Daten der Di + p + lomarbeit + FO 1.2.02.07.CB + haj/11/01/2016 + + + + + + + + + + + Rapport reçu le / + + Visa du secrétariat / + + Filière / StudiengangSYNDAnnée académique / Studienjahryyyy/yyNo TD / Nr. DAit/2019/84 + + + + + + + + + + + + + + Mandant / Auftraggeber + + + + HES + + SO Valais + + Industrie + + Etablissement partenaire PartnerinstitutionEtudiant / StudentSurname Name + + / Ausführungsort + + + + HES + + SO Valais + + Industrie + + Etablissement partenaire Partnerinstitution + + + + + + + + + + + + Professeur / DozentSilvan Zahno + + + + + + + + + + Travail confidentiel / vertrauliche Arbeit + + oui / ja + 1 + + + + non / nein Expert / Experte + (données complètes) + + + + + + + + + + + + + + + + + + Titre / Titel + Titel + + + + + + + + Description / Beschreibung + Objectifs / Ziele: + + + + + + + + + + + Signature ou visa / Unterschrift oder Visum + Délais / Termine + + + + + + + + + + + + + Leiter der Vertiefungsrichtung / Studiengang: + + + Etudiant / Student : + + + Attribution du thème / Ausgabe des Auftrags: + dd.mm.yyyy + Présentation intermédiaire / Zwischenpräsentation + dd + + dd.mm.yyyy + Remise du rapport / Abgabe des Schlussberichts: + dd.mm.yyyy, hh:mm + Expositions / Ausstellungen der Diplomarbeiten: + dd,dd + + dd.mm.yyyy + Défense orale / Mündliche Verfechtung: + dd + + dd.mm.yyyy + + + + + + + + + + + + 1 + + + - + + diplôme. + Durch seine Unterschrift verpflichtet sich der/die Student/in, sich an die Richtlinie DI.1.2.02.07 der Diplomarbeit zu halten. + + + diff --git a/report/resources/img/ui_images/ci_cd.png b/report/resources/img/ui_images/ci_cd.png new file mode 100644 index 0000000..3db7269 Binary files /dev/null and b/report/resources/img/ui_images/ci_cd.png differ diff --git a/report/resources/img/ui_images/dashbord_room_with_data.png b/report/resources/img/ui_images/dashbord_room_with_data.png new file mode 100644 index 0000000..004bd1f Binary files /dev/null and b/report/resources/img/ui_images/dashbord_room_with_data.png differ diff --git a/report/resources/img/ui_images/dashbord_room_without_data.png b/report/resources/img/ui_images/dashbord_room_without_data.png new file mode 100644 index 0000000..7d58730 Binary files /dev/null and b/report/resources/img/ui_images/dashbord_room_without_data.png differ diff --git a/report/resources/img/ui_images/dashbord_without_tooltip.png b/report/resources/img/ui_images/dashbord_without_tooltip.png new file mode 100644 index 0000000..77b7008 Binary files /dev/null and b/report/resources/img/ui_images/dashbord_without_tooltip.png differ diff --git a/report/resources/img/ui_images/design_dashboard.png b/report/resources/img/ui_images/design_dashboard.png new file mode 100644 index 0000000..d579578 Binary files /dev/null and b/report/resources/img/ui_images/design_dashboard.png differ diff --git a/report/resources/img/ui_images/design_details.png b/report/resources/img/ui_images/design_details.png new file mode 100644 index 0000000..71dfb40 Binary files /dev/null and b/report/resources/img/ui_images/design_details.png differ diff --git a/report/resources/img/ui_images/detail_panel.png b/report/resources/img/ui_images/detail_panel.png new file mode 100644 index 0000000..91b40ca Binary files /dev/null and b/report/resources/img/ui_images/detail_panel.png differ diff --git a/report/resources/img/ui_images/empty_room_details_panel.png b/report/resources/img/ui_images/empty_room_details_panel.png new file mode 100644 index 0000000..8defa2b Binary files /dev/null and b/report/resources/img/ui_images/empty_room_details_panel.png differ diff --git a/report/resources/img/ui_images/notification.png b/report/resources/img/ui_images/notification.png new file mode 100644 index 0000000..b99d369 Binary files /dev/null and b/report/resources/img/ui_images/notification.png differ diff --git a/report/resources/measures/A2-C2_64_0F_68_35_3E.csv b/report/resources/measures/A2-C2_64_0F_68_35_3E.csv new file mode 100644 index 0000000..e8dfd60 --- /dev/null +++ b/report/resources/measures/A2-C2_64_0F_68_35_3E.csv @@ -0,0 +1,32 @@ +time,co2,temperature,humidity,windows,battery +2026-05-27T14:21:48,0,29.2,29,0,47 +2026-05-27T14:25:49,400,29.3,50,0,47 +2026-05-27T14:31:49,400,29.2,50,0,47 +2026-05-27T14:39:50,423,29.3,50,0,47 +2026-05-27T14:41:50,400,29.2,50,0,47 +2026-05-27T14:51:51,400,29.2,51,0,47 +2026-05-27T14:59:52,400,29.3,51,0,47 +2026-05-27T15:09:52,413,29.3,51,0,47 +2026-05-27T15:11:53,427,29.5,52,0,44 +2026-05-27T15:13:53,421,29.3,52,0,47 +2026-05-27T15:15:54,400,29.5,51,0,47 +2026-05-27T15:19:55,416,29.5,52,0,44 +2026-05-27T15:21:55,406,29.5,52,0,44 +2026-05-27T15:23:55,400,29.5,52,0,47 +2026-05-27T15:27:56,407,29.5,52,0,44 +2026-05-27T15:31:57,422,29.5,52,0,47 +2026-05-27T15:35:58,452,29.6,52,0,47 +2026-05-27T15:39:59,439,29.6,52,0,44 +2026-05-27T15:41:59,403,29.6,52,0,44 +2026-05-27T15:44:00,435,29.6,52,0,47 +2026-05-27T16:30:11,481,29.8,51,0,47 +2026-05-27T16:32:11,461,29.7,50,0,44 +2026-05-27T16:34:12,480,29.7,50,0,44 +2026-05-27T16:36:12,483,29.7,50,0,47 +2026-05-27T16:38:13,459,29.7,50,0,47 +2026-05-27T16:40:13,459,29.7,49,0,44 +2026-05-27T16:42:14,461,29.7,48,0,44 +2026-05-27T16:44:14,475,29.7,48,0,47 +2026-05-27T16:48:16,524,29.7,48,0,47 +2026-05-27T16:50:16,534,29.7,48,0,44 +2026-05-27T16:52:16,508,29.7,47,0,47 diff --git a/report/resources/measures/A2-C6_7E_0A_DE_DA_74.csv b/report/resources/measures/A2-C6_7E_0A_DE_DA_74.csv new file mode 100644 index 0000000..1a9e551 --- /dev/null +++ b/report/resources/measures/A2-C6_7E_0A_DE_DA_74.csv @@ -0,0 +1,34 @@ +time,co2,temperature,humidity,windows,battery +2026-05-27T14:26:35,400,30.1,50,0,38 +2026-05-27T14:40:36,400,29.8,50,0,38 +2026-05-27T14:50:37,407,30,50,0,40 +2026-05-27T14:54:38,459,29.8,50,0,40 +2026-05-27T14:56:38,447,29.8,50,0,38 +2026-05-27T14:58:39,441,30,50,0,40 +2026-05-27T15:00:39,423,30,50,0,40 +2026-05-27T15:02:40,400,30,50,0,38 +2026-05-27T15:06:40,413,30,51,0,38 +2026-05-27T15:08:41,409,30,51,0,38 +2026-05-27T15:12:42,424,30,51,0,38 +2026-05-27T15:14:42,413,30,51,0,38 +2026-05-27T15:18:43,452,30,51,0,40 +2026-05-27T15:20:44,422,30,51,0,40 +2026-05-27T15:26:45,427,30.1,52,0,38 +2026-05-27T15:28:46,434,30.1,51,0,38 +2026-05-27T15:30:46,442,30.1,52,0,40 +2026-05-27T15:32:47,438,30.1,51,0,40 +2026-05-27T15:34:47,503,30.1,52,0,40 +2026-05-27T15:38:48,452,30.1,52,0,38 +2026-05-27T15:40:49,482,30.1,52,0,38 +2026-05-27T15:42:49,447,30.1,52,0,38 +2026-05-27T16:29:01,443,30.3,50,0,38 +2026-05-27T16:31:01,435,30.3,49,0,38 +2026-05-27T16:33:02,465,30.3,49,0,38 +2026-05-27T16:35:02,463,30.3,49,0,38 +2026-05-27T16:39:04,445,30.3,49,0,38 +2026-05-27T16:41:04,444,30.3,48,0,38 +2026-05-27T16:43:04,414,30.3,48,0,38 +2026-05-27T16:45:05,450,30.3,47,0,38 +2026-05-27T16:49:06,470,30.3,47,0,38 +2026-05-27T16:51:07,463,30.3,48,0,38 +2026-05-27T16:53:07,421,30.3,47,0,38 diff --git a/report/resources/measures/A2-C6_95_1B_A6_49_E6.csv b/report/resources/measures/A2-C6_95_1B_A6_49_E6.csv new file mode 100644 index 0000000..6050905 --- /dev/null +++ b/report/resources/measures/A2-C6_95_1B_A6_49_E6.csv @@ -0,0 +1,33 @@ +time,co2,temperature,humidity,windows,battery +2026-05-27T15:02:17,451,31,48,0,6 +2026-05-27T15:04:17,406,31,48,0,5 +2026-05-27T15:06:18,417,31,48,0,5 +2026-05-27T15:08:18,408,31.1,48,0,5 +2026-05-27T15:10:19,405,31,47,0,5 +2026-05-27T15:12:19,418,31.1,48,0,5 +2026-05-27T15:14:20,422,31.1,48,0,5 +2026-05-27T15:16:20,414,31.1,48,0,5 +2026-05-27T15:18:21,414,31.1,48,0,5 +2026-05-27T15:20:21,443,31.1,48,0,5 +2026-05-27T15:22:22,405,31.1,48,0,5 +2026-05-27T15:24:22,400,31.1,48,0,5 +2026-05-27T15:28:23,443,31.1,48,0,5 +2026-05-27T15:30:23,422,31.2,48,0,5 +2026-05-27T15:34:24,410,31.2,48,0,5 +2026-05-27T15:36:25,414,31.2,48,0,5 +2026-05-27T15:38:25,414,31.2,48,0,5 +2026-05-27T15:40:26,405,31.2,48,0,5 +2026-05-27T15:42:26,410,31.3,48,0,5 +2026-05-27T16:28:37,418,31.6,47,1,5 +2026-05-27T16:30:38,400,31.6,44,1,5 +2026-05-27T16:32:38,425,31.6,45,1,5 +2026-05-27T16:34:39,425,31.6,45,1,5 +2026-05-27T16:36:39,435,31.6,45,1,5 +2026-05-27T16:38:40,410,31.6,45,1,5 +2026-05-27T16:40:40,423,31.6,45,1,5 +2026-05-27T16:42:41,400,31.5,44,1,5 +2026-05-27T16:44:42,441,31.5,44,1,5 +2026-05-27T16:46:42,441,31.5,43,1,5 +2026-05-27T16:48:42,452,31.3,44,1,5 +2026-05-27T16:50:43,408,31.5,43,1,5 +2026-05-27T16:52:44,400,31.5,42,1,5 diff --git a/report/resources/measures/A2-E1_C0_30_15_4E_89.csv b/report/resources/measures/A2-E1_C0_30_15_4E_89.csv new file mode 100644 index 0000000..ecf8fc3 --- /dev/null +++ b/report/resources/measures/A2-E1_C0_30_15_4E_89.csv @@ -0,0 +1,36 @@ +time,co2,temperature,humidity,windows,battery +2026-05-27T14:22:05,0,29.7,30,0,47 +2026-05-27T14:32:06,400,30,50,0,47 +2026-05-27T14:40:06,415,29.8,52,0,47 +2026-05-27T14:42:07,451,29.8,51,0,47 +2026-05-27T14:44:08,416,30,51,0,47 +2026-05-27T14:46:08,400,29.8,51,0,47 +2026-05-27T14:54:09,426,30,52,0,47 +2026-05-27T14:56:10,400,30,50,0,47 +2026-05-27T15:00:11,420,30,51,0,47 +2026-05-27T15:02:11,400,30.1,51,0,47 +2026-05-27T15:08:12,442,30,53,0,47 +2026-05-27T15:12:13,421,30.1,51,0,47 +2026-05-27T15:16:14,436,30.1,54,0,47 +2026-05-27T15:18:14,409,30.1,50,0,47 +2026-05-27T15:20:15,432,30.1,50,0,47 +2026-05-27T15:22:16,436,30.1,50,0,47 +2026-05-27T15:24:16,450,30.2,52,0,47 +2026-05-27T15:26:16,436,30.1,53,0,47 +2026-05-27T15:28:17,425,30.2,52,0,47 +2026-05-27T15:30:18,428,30.1,52,0,47 +2026-05-27T15:32:18,445,30.2,51,0,47 +2026-05-27T15:34:18,497,30.1,52,0,47 +2026-05-27T15:36:19,423,30.2,52,0,47 +2026-05-27T15:38:20,459,30.2,51,0,47 +2026-05-27T15:40:20,459,30.2,53,0,47 +2026-05-27T15:42:20,427,30.2,52,0,47 +2026-05-27T16:28:32,520,30.5,50,0,47 +2026-05-27T16:30:32,482,30.3,49,0,47 +2026-05-27T16:32:33,463,30.5,49,0,47 +2026-05-27T16:34:34,475,30.5,48,0,47 +2026-05-27T16:36:34,480,30.5,49,0,47 +2026-05-27T16:38:34,473,30.5,48,0,44 +2026-05-27T16:42:36,448,30.5,48,0,47 +2026-05-27T16:44:36,495,30.5,49,0,47 +2026-05-27T16:52:38,518,30.5,47,0,44 diff --git a/report/resources/measures/A2-E8_F3_0A_F7_3B_F3.csv b/report/resources/measures/A2-E8_F3_0A_F7_3B_F3.csv new file mode 100644 index 0000000..08f8433 --- /dev/null +++ b/report/resources/measures/A2-E8_F3_0A_F7_3B_F3.csv @@ -0,0 +1,37 @@ +time,co2,temperature,humidity,windows,battery +2026-05-27T14:21:29,0,30.1,29,0,33 +2026-05-27T14:25:30,400,30.5,50,0,31 +2026-05-27T14:31:30,400,30.3,50,0,36 +2026-05-27T14:39:31,400,30.2,51,0,36 +2026-05-27T14:49:31,421,30.2,50,0,33 +2026-05-27T14:51:31,414,30.2,51,0,33 +2026-05-27T14:53:32,407,30.2,51,0,33 +2026-05-27T14:57:33,414,30.2,50,0,33 +2026-05-27T14:59:34,425,30.2,51,0,36 +2026-05-27T15:01:34,424,30.2,50,0,33 +2026-05-27T15:07:35,436,30.2,51,0,33 +2026-05-27T15:09:36,436,30.2,51,0,36 +2026-05-27T15:13:37,459,30.2,51,0,33 +2026-05-27T15:15:38,463,30.2,51,0,33 +2026-05-27T15:17:38,475,30.2,51,0,36 +2026-05-27T15:19:39,496,30.2,51,0,33 +2026-05-27T15:21:39,501,30.3,52,0,33 +2026-05-27T15:23:40,505,30.3,51,0,33 +2026-05-27T15:25:40,496,30.2,51,0,36 +2026-05-27T15:29:41,523,30.3,52,0,33 +2026-05-27T15:31:42,551,30.3,52,0,33 +2026-05-27T15:33:42,570,30.3,51,0,33 +2026-05-27T15:35:43,596,30.3,52,0,33 +2026-05-27T15:37:43,551,30.3,52,0,33 +2026-05-27T15:39:44,555,30.3,52,0,33 +2026-05-27T15:41:44,578,30.3,52,0,33 +2026-05-27T15:43:45,549,30.3,52,0,33 +2026-05-27T16:29:56,780,30.6,51,0,33 +2026-05-27T16:31:57,780,30.6,51,0,33 +2026-05-27T16:33:57,791,30.6,51,0,33 +2026-05-27T16:35:58,708,30.6,50,0,33 +2026-05-27T16:37:58,754,30.6,50,0,33 +2026-05-27T16:42:00,754,30.5,50,0,33 +2026-05-27T16:46:00,802,30.6,49,0,33 +2026-05-27T16:48:01,834,30.6,49,0,33 +2026-05-27T16:50:01,915,30.5,49,0,33 diff --git a/report/resources/measures/A2-F5_80_05_76_53_F0.csv b/report/resources/measures/A2-F5_80_05_76_53_F0.csv new file mode 100644 index 0000000..146a577 --- /dev/null +++ b/report/resources/measures/A2-F5_80_05_76_53_F0.csv @@ -0,0 +1,57 @@ +time,co2,temperature,humidity,windows,battery +2026-05-27T14:01:17,428,31.8,45,0,53 +2026-05-27T14:03:17,461,31.7,45,0,53 +2026-05-27T14:05:18,487,31.6,45,0,51 +2026-05-27T14:07:18,508,31.6,45,0,51 +2026-05-27T14:11:19,497,31.5,45,0,51 +2026-05-27T14:15:20,549,31.3,46,0,51 +2026-05-27T14:17:21,480,31.3,46,0,51 +2026-05-27T14:21:21,478,31.2,46,0,53 +2026-05-27T14:23:22,478,31.2,46,0,51 +2026-05-27T14:25:22,548,31.1,46,0,53 +2026-05-27T14:27:23,508,31.1,46,0,51 +2026-05-27T14:29:23,492,31.1,46,0,53 +2026-05-27T14:31:24,497,31.1,47,0,53 +2026-05-27T14:33:24,534,31.1,47,0,53 +2026-05-27T14:35:25,475,31.1,47,0,51 +2026-05-27T14:39:26,487,31,47,0,51 +2026-05-27T14:41:27,457,31,47,0,51 +2026-05-27T14:43:27,461,31,48,0,53 +2026-05-27T14:45:28,450,31,47,0,51 +2026-05-27T14:47:28,482,31,47,0,51 +2026-05-27T14:49:28,482,30.8,47,0,51 +2026-05-27T14:51:29,485,30.8,47,0,51 +2026-05-27T14:53:30,543,30.8,47,0,53 +2026-05-27T14:55:30,551,30.8,47,0,51 +2026-05-27T14:57:31,551,30.8,47,0,51 +2026-05-27T14:59:31,543,30.8,48,0,53 +2026-05-27T15:03:32,565,30.8,48,0,53 +2026-05-27T15:05:33,611,30.8,48,0,51 +2026-05-27T15:07:33,581,30.8,49,0,53 +2026-05-27T15:09:33,555,30.8,48,0,51 +2026-05-27T15:11:34,548,30.8,48,0,51 +2026-05-27T15:13:35,581,30.8,48,0,51 +2026-05-27T15:15:35,602,30.8,48,0,51 +2026-05-27T15:17:36,570,30.7,49,0,51 +2026-05-27T15:19:36,567,30.8,48,0,51 +2026-05-27T15:23:37,588,30.8,49,0,51 +2026-05-27T15:25:38,602,30.8,49,0,51 +2026-05-27T15:27:38,594,30.8,49,0,51 +2026-05-27T15:29:38,594,30.8,49,0,51 +2026-05-27T15:35:40,623,30.8,49,0,53 +2026-05-27T15:37:41,638,30.8,49,0,51 +2026-05-27T15:39:41,623,30.8,49,0,51 +2026-05-27T15:41:42,623,30.8,49,0,51 +2026-05-27T15:43:42,615,30.8,49,0,53 +2026-05-27T16:29:54,553,31,48,1,51 +2026-05-27T16:31:54,575,31,47,1,51 +2026-05-27T16:33:55,627,31,47,1,51 +2026-05-27T16:35:55,599,31,47,1,51 +2026-05-27T16:37:56,572,31,46,1,51 +2026-05-27T16:39:56,519,31,46,1,51 +2026-05-27T16:41:57,530,31,47,1,51 +2026-05-27T16:43:57,552,31,46,1,51 +2026-05-27T16:45:58,580,31,46,1,51 +2026-05-27T16:47:58,679,31,46,1,51 +2026-05-27T16:49:59,645,31,46,1,51 +2026-05-27T16:51:59,688,31,46,1,51 diff --git a/report/resources/measures/example.typ b/report/resources/measures/example.typ new file mode 100644 index 0000000..b855eff --- /dev/null +++ b/report/resources/measures/example.typ @@ -0,0 +1,23 @@ +#import "/resources/measures/plot_measures.typ": * + +#let start-date = datetime(year: 2026, month: 5, day: 27, hour: 14, minute: 0, second: 0) +#let stop-date = datetime(year: 2026, month: 5, day: 27, hour: 17, minute: 59, second: 59) + +// #let node = "A2-C2_64_0F_68_35_3E" +// #let node = "A2-C6_7E_0A_DE_DA_74" +// #let node = "A2-E1_C0_30_15_4E_89" +// #let node = "A2-E8_F3_0A_F7_3B_F3" +// #let node = "A2-C6_95_1B_A6_49_E6" // Windows +#let node = "A2-F5_80_05_76_53_F0" // Windows + += Indoor Air Quality Analysis +Data range: #start-date.display("[day] [month repr:long] [year] [hour]h[minute]") to #stop-date.display("[day] [month repr:long] [year] [hour]h[minute]") + + +== CO2 Level #node + +#plot_co2("/resources/measures/" + node + ".csv", start-date, stop-date) + +== Temperature / Humidity #node + +#plot_temp_hum("/resources/measures/" + node + ".csv", start-date, stop-date) diff --git a/report/resources/measures/plot_measures.typ b/report/resources/measures/plot_measures.typ new file mode 100644 index 0000000..77ba265 --- /dev/null +++ b/report/resources/measures/plot_measures.typ @@ -0,0 +1,148 @@ +#import "@preview/lilaq:0.6.0" as lq + +#let epoch = datetime(year: 1970, month: 01, day: 01, hour: 0, minute: 0, second: 0) +#let unix-to-datetime(stamp) = epoch + duration(seconds: int(stamp)) +#let datetime-to-unix(dt) = int((dt - epoch).seconds()) + +#let parse-iso-datetime(s) = { + let parts = s.split("T") + let date-part = parts.at(0).split("-").map(int) + let time-part = parts.at(1).split(":").map(int) + datetime( + year: date-part.at(0), + month: date-part.at(1), + day: date-part.at(2), + hour: time-part.at(0), + minute: time-part.at(1), + second: time-part.at(2) + ) +} + +#let window-regions(filtered-windows, filtered-times-dt) = { + let regions = () + let i = 0 + while i < filtered-windows.len() { + let start-time = filtered-times-dt.at(i) + let status = filtered-windows.at(i) + let j = i + // Find end of consecutive region with same status + while j < filtered-windows.len() and filtered-windows.at(j) == status { + j += 1 + } + let end-time = if j < filtered-times-dt.len() { + filtered-times-dt.at(j - 1) + } else { + filtered-times-dt.at(filtered-times-dt.len() - 1) + } + // Add small duration to end time to make rectangle visible + let end-time-dt = end-time + duration(seconds: 60) + + let fill-color = if status == 0 { none } else { rgb("#e8e8e8") } + + regions.push(lq.rect( + start-time, + 0%, + width: end-time-dt - start-time, + height: 100%, + fill: fill-color, + stroke: none, + z-index: 1 + )) + + i = j + } + regions +} + +#let plot_co2(path, start-date, stop-date) = [ + #let start-unix = datetime-to-unix(start-date) + #let stop-unix = datetime-to-unix(stop-date) + + #let data = lq.load-txt( + read(path), + header: true, + converters: (time: parse-iso-datetime, rest: float) + ) + #let times = data.at("time") + #let co2s = data.at("co2") + #let windows = data.at("windows") + + // Filter data by date range + #let filtered-indices = range(times.len()).filter(i => { + let t = times.at(i) + t >= start-date and t <= stop-date + }) + + #let filtered-times-dt = filtered-indices.map(i => times.at(i)) + #let filtered-co2s = filtered-indices.map(i => co2s.at(i)) + #let filtered-windows = filtered-indices.map(i => windows.at(i)) + #let regions = window-regions(filtered-windows, filtered-times-dt) + + #lq.diagram( + width: 100%, + height: 7cm, + title: [CO2 Level over Time], + xlabel: [Time], + ylabel: [CO2 Level (ppm)], + yaxis: (lim: (400, 1500), exponent: 0), + xaxis: (format-ticks: lq.tick-format.datetime.with( + format-offset: (datetimes, period: none) => none, + )), + ..regions, + lq.plot(filtered-times-dt, filtered-co2s, label: [CO2 Level], color: rgb("#388e3c")) + ) +] + +#let plot_temp_hum(path, start-date, stop-date) = [ + #let start-unix = datetime-to-unix(start-date) + #let stop-unix = datetime-to-unix(stop-date) + + #let data = lq.load-txt( + read(path), + header: true, + converters: (time: parse-iso-datetime, rest: float) + ) + #let times = data.at("time") + #let temps = data.at("temperature") + #let hums = data.at("humidity") + #let windows = data.at("windows") + + + // Filter data by date range + #let filtered-indices = range(times.len()).filter(i => { + let t = times.at(i) + t >= start-date and t <= stop-date + }) + + #let filtered-times-dt = filtered-indices.map(i => times.at(i)) + #let filtered-temps = filtered-indices.map(i => temps.at(i)) + #let filtered-hums = filtered-indices.map(i => hums.at(i)) + #let filtered-times-dt = filtered-indices.map(i => times.at(i)) + #let filtered-windows = filtered-indices.map(i => windows.at(i)) + #let regions = window-regions(filtered-windows, filtered-times-dt) + + #lq.diagram( + width: 100%, + height: 7cm, + title: [Temperature and Humidity over Time], + xlabel: [Time], + ylabel: [Temperature (°C)], + yaxis: (lim: (20, 35)), + xaxis: (format-ticks: lq.tick-format.datetime.with( + format-offset: (datetimes, period: none) => none, + )), + ..regions, + lq.plot(filtered-times-dt, filtered-temps, label: [Temperature], color: rgb("#d32f2f")), + lq.yaxis( + position: right, + label: [Humidity (%)], + lim: (30, 55), + lq.plot(filtered-times-dt, filtered-hums, label: [Humidity], color: rgb("#1976d2")) + ) + ) +] + + + + + diff --git a/report/resources/mse-title-page.typ b/report/resources/mse-title-page.typ new file mode 100644 index 0000000..cb9181e --- /dev/null +++ b/report/resources/mse-title-page.typ @@ -0,0 +1,107 @@ +// +// Description: Custom pages for the thesis template +// Author : Sierro Yann + +#import "/metadata.typ": * + +#let l = { + line(length: 100%, stroke: 0.5pt) +} + +#let mse-title-page( + title: doc.title, + subtitle: doc.subtitle, + school: school, + authors: doc.author, + professors: professors, + expert: expert, + date: date, + logos: logos, +) = { + set page( + margin: ( + top:3.0cm, + bottom:3.0cm, + rest:3.5cm + ) + ) + //------------------------------------- + // Page content + // + align(center)[ + #logos.logo-hesso + + #v(1cm) + + #logos.logo-mse + + #v(1cm) + + #text(size:20pt)[ + #school.orientation + ] + + #v(0.5cm) + + #text(title, size:30pt) + + #v(0.5cm) + + #text(subtitle, size:24pt) + + #v(0.5cm) + + // #text(size:22pt, doc.author.name) + #align(center, [#text(size:large, [ + #enumerating-authors( + items: authors, + multiline: true, + ) + #v(2em) + ])]) + ] + + v(1cm) + align(center, text(size: 1.5em)[ + Spring Semester #date.submission.display("[Year]") + ]) + v(1cm) + + grid( + columns: (1fr, 1fr), + align: left + horizon, + + text(size:12pt)[ + + #if professors != none [ + Professor: \ + // - #professor.name + // - #professor.email + // - #professor.affiliation + #enumerating-authors( + items: professors, + multiline: true, + ) + ] + ], + + text(size:12pt)[ + + #if expert != none [ + Expert: \ + - #expert.name + - #expert.email + - #expert.affiliation + ] + ] + ) + + v(1cm) + + text(size:15pt)[ + _Submission date of the report_ \ + #date.submission.display() + ] + +} + diff --git a/report/resources/slides.typ b/report/resources/slides.typ new file mode 100644 index 0000000..e22bb93 --- /dev/null +++ b/report/resources/slides.typ @@ -0,0 +1,132 @@ +#import "@preview/touying:0.6.1": * +#import "@preview/codelst:2.0.2": sourcecode +#import "@preview/pinit:0.2.2": * +#import themes.metropolis: * +#import "@preview/note-me:0.5.0": * +#let the_date = datetime.today() + +#let myred = rgb(80%,0%,0%) +#let myblue = rgb(0%,40%,58%) +#let myorange = rgb(75%,31%,9%) +#let mygreen = rgb(24%,50%,19%) +#let mylightgreen = rgb(50%,70%,40%) +#let myyellow = rgb(100%,88%,26%) +#let myviolet = rgb(48%,47%,72%) +#let mypurple = rgb(69%,45%,69%) +#let mylightorange = rgb(96%,51%,22%) +#let mypink = rgb(85%,51%,72%) +#let almostblack = rgb("#23373B") +#let hesso-blue = rgb("#00609c") +#let hesso-grey = rgb("#968b83") + + +#let default_text_size = 24pt + +#let get-config() = { + return ( + config-colors( + primary: rgb("#00609c"), + primary-light: rgb("#968b83"), + secondary: rgb("#0071b7"), + // neutral-lightest: rgb("#00609c"), + neutral-dark: rgb("#01588f"), + neutral-darkest: rgb("#004977"), + ), + config-methods( + init: (self: none, body) => { + show strong: it => text(fill: almostblack, it) + body + }, + ), + config-common( + datetime-format: "[month repr:long] [day], [year]", + // handout: true, + ), + + ) +} + +#let title_style(body) = [ + #set text(size: 36pt, weight: "bold") + #image("/resources/img/logos/mse.png", width: 60%) + // #place( + // top+right, + // dy: -2.5em, + // dx: 4em, + // float: false, + // clearance: 0.5em, + // image("/resources/img/logos/hydro.png", height: 1.5em) + // ) + #body +] + +#let myglobals(doc) = [ + // #set page(background: image("swiss_universities.png")) + // #set text(font: "CMU Sans Serif", size: default_text_size) + // #set text(font: "Ubuntu", size: default_text_size) + #show footnote.entry: set text(size: 18pt) + // #show link: underline + // #show link: it => text(fill: myblue, it) + #doc] + +#let strong(body) = [#set text(weight: "bold") + #body] + +#let red(body) = [#set text(fill: myred) + #body] + +#let green(body) = [#set text(fill: mygreen) + #body] + +#let blue(body) = [#set text(fill: myblue) + #body] + +#let orange(body) = [#set text(fill: myorange) + #body] + +#let purple(body) = [#set text(fill: mypurple) + #body] + +#let code_frame = block.with( + stroke: 0.1mm + rgb(70%, 70%, 80%), + inset: 3mm, + radius: 8pt, + fill: rgb(94%, 94%, 94%), +) + +// Usage example: +// #code(size:16pt,```c +// char name = "Jane"; +// printf("Hey %s\n", name); +// ```) +#let code(code, size: 20pt, nb:none, highlight:()) = [#set text(size: size) + #v(-5mm) + #set par(justify: false) + #sourcecode(numbering:nb, frame: code_frame, highlighted: highlight, code)] + +// Usage example: +// #items(15mm)[ +// - Item 1 +// - Item 2 +// - Item 3 +// ] +#let items(vspace, body) = [ + #par[ + #show list.item: it => { + it + v(vspace) + } + #body]] + +// Usage examples: +// #par(al:right)[ +// blah blah +// ] +// #par(size:16pt,al:center)[ +// blah blah +// ] +#let par(size: default_text_size, al: left, body) = [ + #set align(al) + #set text(size: size) + #body +] diff --git a/report/specifications.typ b/report/specifications.typ new file mode 100644 index 0000000..20642f6 --- /dev/null +++ b/report/specifications.typ @@ -0,0 +1,53 @@ +#import "/metadata.typ": * +#import "/tail/bibliography.typ": * +#import "/tail/glossary.typ": * + +#import "/resources/mse-title-page.typ": * + +#show:make-glossary +#register-glossary(entry-list) +#set text(region: option.region) + +//------------------------------------- +// Template config +// + +#show: thesis.with( + option: option + (type: "final"), + doc: doc, + school: school, + date: date, + tableof: tableof, + logos: logos, + custom-title-page: mse-title-page( + title: doc.title, + subtitle: "Specifications v1.1", + date: date + (submission: date.specification-submission), + ), +) + +// Declaration of honor will be remove with the next release of the HEI-SYND-THESIS template (probably next week) + +//------------------------------------- +// Content +// +#include "/main/specifications/02-introduction.typ" +#include "/main/specifications/03-context.typ" +#include "/main/specifications/04-constraints.typ" +#include "/main/specifications/05-objectives.typ" +#include "/main/specifications/deliverables.typ" +#include "/main/specifications/06-planned_tasks.typ" +#include "/main/specifications/07-deadlines.typ" +#include "/main/specifications/bom.typ" + +//------------------------------------- +// Glossary +// +#heading(numbering:none, outlined: false)[] +#make_glossary(gloss:gloss, title:i18n("gloss-title")) + +//------------------------------------- +// Bibliography +// +#make_bibliography(bib:bib, title:i18n("bib-title", lang: option.lang)) + diff --git a/report/tail/a-appendix.typ b/report/tail/a-appendix.typ new file mode 100644 index 0000000..f9f80bc --- /dev/null +++ b/report/tail/a-appendix.typ @@ -0,0 +1,10 @@ +#import "/metadata.typ": * +#pagebreak() += #i18n("appendix-title", lang: option.lang) + +== Gateway sequence diagram + +#figure( + image("../resources/img/sequence_data_collection.svg"), + caption: [Gateway sequence diagram — Data collection] +) diff --git a/report/tail/bibliography.bib b/report/tail/bibliography.bib new file mode 100644 index 0000000..4068880 --- /dev/null +++ b/report/tail/bibliography.bib @@ -0,0 +1,166 @@ +@article{zahnoDynamicProjectPlanning2023, + title = {Dynamic Project Planning with Digital Twin}, + author = {Zahno, Silvan and Corre, Jérôme and Petrovic, Darko and Mottiez, Gilles and Fracheboud, Loïc and Amand, Axel and Devènes, Steve and Maître, Gilbert and Carrino, Francesco}, + date = {2023-05-09}, + journaltitle = {Frontiers in Manufacturing Technology}, + shortjournal = {Front. Manuf. Technol.}, + volume = {3}, + publisher = {Frontiers}, + issn = {2813-0359}, + doi = {10.3389/fmtec.2023.1009633}, + url = {https://www.frontiersin.org/journals/manufacturing-technology/articles/10.3389/fmtec.2023.1009633/full}, + urldate = {2025-01-08}, + abstract = {{$<$}p{$>$}The digital twin (DT) concept plays a crucial role in Industry 4.0 and the digitalization of manufacturing processes. A DT is a virtual representation of a physical object, system, or process, designed to accurately reflect its real-world counterpart. In manufacturing, existing process data are often incomplete and do not qualify as a DT. However, with the help of specialized communication frameworks and cheaper, easier-to-use sensors, it is possible to integrate the existing manufacturing execution system (MES) and enterprise resource planning (ERP) data with the missing data gathered from the shop floor to create a comprehensive DT. In this paper, we present a digital shop floor decision support system (DSS) for non-linear aluminum manufacturing production. The system is split into five main components: digitization of shop floor orders; merging and sorting of MES, ERP, and shop floor data; custom and genetic optimization algorithms for the aging furnace production step; layout construction mechanism for optimal placement and stacking of orders in the furnace; and a user-friendly graphical user interface (GUI). The system’s performance was evaluated through three tests. The first test measured the efficiency of digitization, the second aimed to quantify time saved in finding packets in the hall, and the last test measured the impact of the optimizer on furnace productivity. The results revealed a 23.5\% improvement in furnace capacity, but limitations were identified due to usability and human intervention.{$<$}/p{$>$}}, + langid = {english}, + keywords = {3D packaging,genetic algorithms,Greed Algorithms (GA),Real-time monitoring,Real-time optimization,shop floor optimization,user-center design}, + file = {/Users/zas/Zotero/storage/XFE2V7Y8/Zahno et al. - 2023 - Dynamic project planning with digital twin.pdf} +} + +@misc{ofsp_ventilation, + author = {OFSP}, + title = {{Ventilation dans les écoles}}, + url = {https://www.bag.admin.ch/fr/ventilation-dans-les-ecoles}, + note = "[Online; accessed 15-March-2026]" +} + +@article{co2_levels_review, + title = {Carbon dioxide guidelines for indoor air quality: a review}, + volume = {34}, + ISSN = {1559-064X}, + url = {http://dx.doi.org/10.1038/s41370-024-00694-7}, + DOI = {10.1038/s41370-024-00694-7}, + number = {4}, + journal = {Journal of Exposure Science & Environmental Epidemiology}, + publisher = {Springer Science and Business Media LLC}, + author = {Mendell, Mark J. and Chen, Wenhao and Ranasinghe, Dilhara R. and Castorina, Rosemary and Kumagai, Kazukiyo}, + year = {2024}, + month = jun, + pages = {555–569} +} + +@article{twardella_effect_2012-1, + title = {Effect of classroom air quality on students’ concentration: results of a cluster-randomized cross-over experimental study}, + volume = {22}, + copyright = {© 2012 John Wiley \& Sons A/S}, + issn = {1600-0668}, + shorttitle = {Effect of classroom air quality on students’ concentration}, + url = {https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1600-0668.2012.00774.x}, + doi = {10.1111/j.1600-0668.2012.00774.x}, + abstract = {To assess the effect of indoor air quality as indicated by the median carbon dioxide (CO2) level in the classroom on the concentration performance (CP) of students, a cross-over cluster-randomized experimental study was conducted in 20 classrooms with mechanical ventilation systems. Test conditions ‘worse’ (median CO2 level on average 2115 ppm) and ‘better’ (median CO2 level on average 1045 ppm) were established by the regulation of the mechanical ventilation system on two days in one week each in every classroom. Concentration performance was quantified in students of grade three and four by the use of the d2-test and its primary parameter ‘CP’ and secondary parameters ‘total number of characters processed’ (TN) and ‘total number of errors’ (TE). 2366 d2-tests from 417 students could be used in analysis. In hierarchical linear regression accounting for repeated measurements, no significant effect of the experimental condition on CP or TN could be observed. However, TE was increased significantly by 1.65 (95\% confidence interval 0.42–2.87) in ‘worse’ compared to ‘better’ condition. Thus, low air quality in classrooms as indicated by increased CO2 levels does not reduce overall short-term CP in students, but appears to increase the error rate. Practical Implications This study could not confirm that low air quality in classrooms as indicated by increased CO2 levels reduces short-term concentration performance (CP) in students; however, it appears to affect processing accuracy negatively. To ensure a high level of accuracy, good air quality characterized, for example, by low CO2 concentration should be maintained in classrooms.}, + language = {en}, + number = {5}, + urldate = {2026-04-13}, + journal = {Indoor Air}, + author = {Twardella, D. and Matzen, W. and Lahrz, T. and Burghardt, R. and Spegel, H. and Hendrowarsito, L. and Frenzel, A. C. and Fromme, H.}, + year = {2012}, + note = {\_eprint: https://onlinelibrary.wiley.com/doi/pdf/10.1111/j.1600-0668.2012.00774.x}, + keywords = {Attention, Carbon Dioxide, Indoor Air, School, Students}, + pages = {378--387} +} + +@misc{St.Gallen_data, + title = {CO2 Sensoren in den Innenräumen der Stadtverwaltung St.Gallen}, + url = {https://daten.stadt.sg.ch/explore/dataset/co2-sensoren-innenraume-stadt-stgallen/table/?disjunctive.name&disjunctive.tags_name&refine.tags_name=Export+Opendata+DTB+%2F+Datenaggregationsgruppen+%2F+CO2+-+Sam&refine.tags_name=Export+Opendata+DTB+%2F+Datenaggregationsgruppen+%2F+CO2+-+Airica+Senses&dataChart=eyJxdWVyaWVzIjpbeyJjaGFydHMiOlt7InR5cGUiOiJsaW5lIiwiZnVuYyI6IkFWRyIsInlBeGlzIjoiZGF0YV9jbzIiLCJzY2llbnRpZmljRGlzcGxheSI6dHJ1ZSwiY29sb3IiOiIjZmYwMDAwIn1dLCJ4QXhpcyI6Im1lYXN1cmVkX2F0X25ldyIsIm1heHBvaW50cyI6bnVsbCwidGltZXNjYWxlIjoiZGF5Iiwic29ydCI6IiIsImNvbmZpZyI6eyJkYXRhc2V0IjoiY28yLXNlbnNvcmVuLWlubmVucmF1bWUtc3RhZHQtc3RnYWxsZW4iLCJvcHRpb25zIjp7ImRpc2p1bmN0aXZlLm5hbWUiOnRydWUsImRpc2p1bmN0aXZlLnRhZ3NfbmFtZSI6dHJ1ZSwicmVmaW5lLnRhZ3NfbmFtZSI6WyJFeHBvcnQgT3BlbmRhdGEgRFRCIC8gRGF0ZW5hZ2dyZWdhdGlvbnNncnVwcGVuIC8gQ08yIC0gU2FtIiwiRXhwb3J0IE9wZW5kYXRhIERUQiAvIERhdGVuYWdncmVnYXRpb25zZ3J1cHBlbiAvIENPMiAtIEFpcmljYSBTZW5zZXMiXX19fV0sImRpc3BsYXlMZWdlbmQiOnRydWUsImFsaWduTW9udGgiOnRydWUsInRpbWVzY2FsZSI6IiJ9}, + note = "[Online; accessed 22-Mai-2026]" +} + +@article{residential_ventilation, + author = {Heinrich Huber}, + title = {Ventilation des habitations - Planification - Réalisation - Exploitation}, + year = {2022}, + month = jan, + url = {https://www.suisseenergie.ch/search/?searchInput=ventilation+des+habitations+heinrich+huber}, +} + +@misc{Simaria, + title = {Simaria - Simulateur de la qualité de l'air pour les salles de classe}, + url = {https://simaria.ch/fr/simaria}, + note = "[Online; accessed 22-Mai-2026]" +} + +@misc{INRS_Study, + title = {Mesure en temps réel du dioxyde de carbone dans les espaces de travail - {Publication} scientifique - {INRS}}, + url = {https://www.inrs.fr/inrs/recherche/etudes-publications-communications/doc/publication.html?refINRS=ET2022-004/P2022-084/nt96}, + abstract = {Le dioxyde de carbone (CO2) est considéré comme un indicateur du taux de renouvellement de l’air dans des locaux de travail à pollution non spécifique. Dans le contexte de la pandémie de Covid-19, la mesure du taux de CO2 pour assurer une ventilation}, + language = {fr}, + urldate = {2026-04-14}, +} + +@article{heiselberg_characteristics_2001, + title = {Characteristics of airflow from open windows}, + series = {859-869}, + abstract = {In natural ventilation systems fresh air is often provided through opening of windows. However, the knowledge of the performance of windows is rather limited, especially with regard to their impact on thermal comfort and draught risk in the occupied zone. This paper describes and summarizes the results of a series of laboratory measurements that is performed on two di3erent window types to determine the characteristics of the air \$ow in rooms. It is shown that the air \$ow can be described by traditional theory for jets and strati4ed \$ow and semi-empirical \$ow element models are developed for estimation of thermal comfort parameters in the occupied zone. c© 2001 Elsevier Science Ltd. All rights reserved.}, + author = {Heiselberg, Per and Svidt, Kjeld and Nielsen, Peter V}, + date = {2001}, + langid = {english} +} + +@misc{nordic_thingy52, + author = {{Nordic Semiconductor}}, + title = {{Nordic Thingy:52 User Guide}}, + url = {https://docs.nordicsemi.com/bundle/ug_thingy52/page/UG/thingy52/intro/frontpage.html}, + note = "[Online; accessed 2026]" +} + +@misc{bluetooth_ble_advertising, + author = {{Bluetooth SIG}}, + title = {{Bluetooth Low Energy: It starts with advertising}}, + url = {https://www.bluetooth.com/blog/bluetooth-low-energy-it-starts-with-advertising/}, + note = "[Online; accessed 2026]" +} + +@misc{bluetooth_advertising_works, + author = {{Bluetooth SIG}}, + title = {{Advertising works, part 1}}, + url = {https://www.bluetooth.com/blog/advertising-works-part-1/}, + note = "[Online; accessed 2026]" +} + +@misc{paho_mqtt_python, + author = {{Eclipse Foundation}}, + title = {{Eclipse Paho MQTT Python Client}}, + url = {https://eclipse.dev/paho/files/paho.mqtt.python/html/index.html}, + note = "[Online; accessed 2026]" +} + +@misc{python_asyncio, + author = {{Python Software Foundation}}, + title = {{asyncio — Asynchronous I/O — Python 3 documentation}}, + url = {https://docs.python.org/3/library/asyncio.html}, + note = "[Online; accessed 2026]" +} + +@misc{novelbits_ble_advertisements, + author = {Novel Bits}, + title = {{How Bluetooth Low Energy Works: Advertisements (Part 1)}}, + url = {https://novelbits.io/bluetooth-low-energy-advertisements-part-1/}, + note = "[Online; accessed 2026]" +} + +@misc{nordic_academy_advertising, + author = {{Nordic Semiconductor}}, + title = {{Advertising process - Nordic Developer Academy}}, + url = {https://academy.nordicsemi.com/courses/bluetooth-low-energy-fundamentals/lessons/lesson-2-bluetooth-le-advertising/topic/advertising-process/}, + note = "[Online; accessed 2026]" +} + +@misc{tailscale_blog, + author = {Arslan, Fatih}, + title = {{Tailscale is pretty useful}}, + url = {https://blog.6nok.org/tailscale-is-pretty-useful/}, + note = "[Online; accessed 2026]" +} + +@misc{asyncio_practical_guide, + author = {Moraneus}, + title = {{Mastering Python's Asyncio: A Practical Guide}}, + url = {https://medium.com/@moraneus/mastering-pythons-asyncio-a-practical-guide-0a673265cf04}, + note = "[Online; accessed 2026]" +} + +@misc{bleak_scanner, + author = {{Bleak Contributors}}, + title = {{BleakScanner class — bleak documentation}}, + url = {https://bleak.readthedocs.io/en/latest/api/scanner.html}, + note = "[Online; accessed 2026]" +} diff --git a/report/tail/bibliography.typ b/report/tail/bibliography.typ new file mode 100644 index 0000000..8adc766 --- /dev/null +++ b/report/tail/bibliography.typ @@ -0,0 +1,18 @@ +#import "/metadata.typ": * + +#let make_bibliography( + bib:( + display: true, + path: "/tail/bibliography.bib", + style: "ieee", //"apa", "chicago-author-date", "chicago-notes", "mla" + ), + title: i18n("bib-title", lang: option.lang), +) = {[ + #if bib.display == true {[ + // #pagebreak() + #bibliography(title: title, bib.path, style:bib.style) + ]} else{[ + #set text(size: 0pt) + #bibliography(title: "", bib.path, style:bib.style) + ]} +]} diff --git a/report/tail/glossary.typ b/report/tail/glossary.typ new file mode 100644 index 0000000..79f5f77 --- /dev/null +++ b/report/tail/glossary.typ @@ -0,0 +1,162 @@ +#import "/metadata.typ": * + +#let entry-list = ( + ( + key: "hei", + short: "HEI", + long: "Haute École d'Ingénierie", + group: "University" + ), + ( + key: "synd", + short: "SYND", + long: "Systems Engineering", + group: "University" + ), + ( + key: "it", + short: "IT", + long: "Infotronics", + group: "University" + ), + ( + key: "rust", + short: "Rust", + plural: "Rust programs", + long: "Rust Programming Language", + description: "Rust is a modern systems programming language focused on safety, speed, and concurrency. It prevents common programming errors such as null pointer dereferencing and data races at compile time, making it a preferred choice for performance-critical applications.", + group: "Programming Language" + ), + ( + key: "co2", + short: $C O_2$, + long: "Carbon dioxide", + ), + ( + key: "ofsp", + short: "OFSP", + long: "Federal Office of Public Health" + ), + ( + key: "ppm", + short: "ppm", + long: "Parts per Milion" + ), + ( + key: "mqtt", + short: "MQTT", + long: "Message Queue Telemetry Transport", + description: "A lightweight, publish–subscribe, machine-to-machine network protocol for message queue/message queuing service." + ), + ( + key: "rest", + short: "REST", + long: "Representational State Transfert" + ), + ( + key: "api", + short: "API", + long: "Application Programming Interface" + ), + ( + key: "csv", + short: "CSV", + long: "Comma-Separated Values" + ), + ( + key: "sso", + short: "SSO", + long: "Single Sign On" + ), + ( + key: "tls", + short: "TLS", + long: "Transport Layer Security" + ), + ( + key: "ble", + short: "BLE", + long: "Bluetooth Low Energy" + ), + ( + key: "llm", + short: "LLM", + long: "Large Language Model", + description: "Tools commonly referenced as \"AI\" with the likes of Copilot, claude, chatGPT, ..." + ), + ( + key: "pi", + short: "PI", + long: "Project Inter-disciplinaire" + ), + ( + key: "os", + short: "OS", + long: "Operating System" + ), + ( + key: "iot", + short: "IoT", + long: "Internet of Things" + ), + ( + key: "utc", + short: "UTC", + long: "Coordinated Universal Time" + ), + ( + key: "ssh", + short: "SSH", + long: "Secure SHell" + ), + ( + key: "uuid", + short: "UUID", + long: "Univerally unique identifier" + ), + ( + key: "mvp", + short: "MVP", + long: "Minimal Viable Product" + ), + ( + key: "http", + short: "HTTP", + long: "Hypertext Transfer Protocol" + ), + ( + key: "svg", + short: "SVG", + long: "Scalable Vector Graphics" + ) +) + +#let make_glossary( + gloss:true, + title: i18n("gloss-title", lang: option.lang), +) = {[ + #if gloss == true {[ + #pagebreak() + #set heading(numbering: none) + = #title + #print-glossary( + entry-list, + // show all term even if they are not referenced, default to true + show-all: false, + // disable the back ref at the end of the descriptions + disable-back-references: false, + deduplicate-back-references: true + ) + ]} else{[ + #set text(size: 0pt) + #title + #print-glossary( + entry-list, + // show all term even if they are not referenced, default to true + show-all: false, + // disable the back ref at the end of the descriptions + disable-back-references: false, + + ) + ]} +]} diff --git a/report/test.typ b/report/test.typ new file mode 100644 index 0000000..3335e3c --- /dev/null +++ b/report/test.typ @@ -0,0 +1,5 @@ +#import "metadata.typ": * + +#import "@preview/suiji:0.5.1": * +#let rng = gen-rng-f(42) +#rng \ No newline at end of file diff --git a/report/thesis.typ b/report/thesis.typ new file mode 100644 index 0000000..0348a5d --- /dev/null +++ b/report/thesis.typ @@ -0,0 +1,101 @@ +// +// Description: Main document to stitch everything together +// +#import "/metadata.typ": * +#import "/tail/bibliography.typ": * +#import "/tail/glossary.typ": * +#import "/resources/mse-title-page.typ": * +#show:make-glossary +#register-glossary(entry-list) +#set text(region: "gb") + +//------------------------------------- +// Template config +// +#show: thesis.with( + option: option, + doc: doc, + school: school, + date: date, + tableof: tableof, + logos: logos, + custom-title-page: mse-title-page(), +) + +#show raw: set text(size: 0.8em) +#show figure: fig => { + if not fig.body.func() in (image, table, layout, raw) { + let f = fig.body.func() + return fig + } + if not fig.has("label") { + return warn(fig, "Unlabelled") + } + let lbl = fig.label + let refs = query(ref.where(target: lbl)) + if refs.len() == 0 { + return warn(fig, "Unreferenced") + } + fig +} + +#outline( + title: i18n("tof-title", lang: "en"), + target: figure.where(kind: image), + indent: auto, + depth: tableof.maxdepth, +) +#outline( + title: i18n("tot-title", lang: "en"), + target: figure.where(kind: table), + indent: auto, + depth: tableof.maxdepth, +) + +//------------------------------------- +// Content +// +#include "/main/02-introduction.typ" + + +#pagebreak() += Objectives +#import "/main/objectives.typ": * +#add-chapter()[ + The objectives for the project that has been validated in its early phase are listed below. + Mandatory objectives are listed as MOx and optional objectives as OOx. + + #objectives-mo + #v(1em) + #objectives-oo + + The MO3 has been modified during the 8th week in response to Dimitri dropout. + The student's personal objective has been kept nonetheless. +] + +#include "/main/03-analysis.typ" +#include "/main/04-design.typ" +#include "/main/05-implementation.typ" +#include "/main/06-validation.typ" +#include "/main/07-conclusion.typ" + +//------------------------------------- +// Glossary +// +#make_glossary(gloss:gloss, title:i18n("gloss-title", lang: option.lang)) + +#pagebreak() + +//------------------------------------- +// Bibliography +// +#make_bibliography(bib:bib, title:i18n("bib-title", lang: option.lang)) + +//------------------------------------- +// Appendix +// +#if appendix == true {[ + #counter(heading).update(0) + #set heading(numbering:"A") + #include "/tail/a-appendix.typ" +]}