<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://zeus.ugent.be/">
  <id>https://zeus.ugent.be/</id>
  <title>Zeus WPI</title>
  <updated>2026-07-07T00:00:00Z</updated>
  <link rel="alternate" href="https://zeus.ugent.be/" type="text/html"/>
  <link rel="self" href="https://zeus.ugent.be/feed.xml" type="application/atom+xml"/>
  <author>
    <name></name>
    <uri></uri>
  </author>
  <entry>
    <id>tag:zeus.ugent.be,2026-07-07:/blog/25-26/12urenloop-uwb/</id>
    <title type="html">Building a local positioning system to track runners using Ultra-Wideband</title>
    <published>2026-07-07T00:00:00Z</published>
    <updated>2026-07-07T00:00:00Z</updated>
    <link rel="alternate" href="https://zeus.ugent.be/blog/25-26/12urenloop-uwb/" type="text/html"/>
    <content type="html">&lt;p&gt;&lt;a href="#nederlands"&gt;Klik hier om in het nederlands te lezen&lt;/a&gt;&lt;/p&gt;

&lt;h3 id="english"&gt;English&lt;/h3&gt;

&lt;p&gt;12Urenloop is an annual running race at the Sint-Pieters square in Ghent. Participating student association teams run a relay race for 12 hours, trying to complete as many laps as possible. Each team is assigned one baton, so they can only have one runner on the track at a time.&lt;/p&gt;

&lt;p&gt;Zeus WPI has been doing the lap counting at the 12Urenloop for a while: first through a manual counting system where runners’ bib numbers were entered by hand, then from 2011 onwards with an automatic counting system based on bluetooth modules in the batons. For more background on the historical hardware and software, you can read the &lt;a href="https://zeus.gent/blog/10-11/counting-laps-using-bluetooth-dongle-detection-on-the-12-urenloop/"&gt;2011 blog post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Between 2019 and 2022 the entire stack was overhauled, with more modern hardware, an estimated position of runners shown on the website, and extensive redundancy, fault tolerance and monitoring. There’s also a &lt;a href="https://zeus.gent/blog/22-23/12urenloop/"&gt;detailed blog post&lt;/a&gt; about that.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pics.zeus.gent/JA9I7ZXKXfOpGQyOoT1GsavWAn467bp3LzJj6Xq1.png" alt="12Urenloop circuit" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The live tracking site on 12urenloop.be, showing the associations’ team logos estimated positions moving on the track&lt;/em&gt;&lt;/p&gt;

&lt;h3 id="what-is-uwb"&gt;What is UWB?&lt;/h3&gt;

&lt;p&gt;Around the end of October this year I saw some online demos of UltraWideBand hardware using easily obtainable and relatively cheap hardware. UltraWideBand is a radio communication standard focused on centimeter-level localization of transmitters/receivers. This is possible partly because it uses a large chunk of the radio spectrum (500 MHz), which makes it very good at ignoring signal reflections. That means it can identify the very start of a received packet. If you pair that with a clock accurate to the picosecond level, and compare the time a packet was sent with the time it was received, you can calculate how long that packet was in the air (at the speed of light) between transmitter and receiver. Multiply that time by the speed of light and you get the distance between transmitter and receiver. If you can measure that distance to a moving UWB transmitter (tag) from multiple UWB transmitters at fixed positions, you can calculate the tag’s position. This is the same principle GPS uses, just on a smaller scale.&lt;/p&gt;

&lt;p&gt;This technology is used in, among other things, iPhones and AirTags, to show the distance and direction to an AirTag. It’s also used in newer car keys that unlock the car when you get close, because a UWB signal can be encrypted and is therefore resistant to &lt;a href="https://www.ace.aaa.com/insurance/advocacy/keyless-car-theft.html"&gt;relay attacks&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id="what-does-this-tech-make-possible"&gt;What does this tech make possible?&lt;/h3&gt;

&lt;p&gt;There had already been talk since 2021 about using UWB at the 12Urenloop, because it offers some major advantages:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Exact position measurement: the current bluetooth system gives a good estimate of progress on the circuit (by calculating speed), but no actual position measurements.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;With positioning, a lap can be counted at the exact moment someone crosses the start line. This isn’t possible with existing RFID systems used for running races, because at the 12Urenloop there’s a start line per team (at each association’s tent).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;More statistics about individual runners: maximum/minimum speed, cornering speed, etc.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;A cleaner signal, since there’s a lot of noise from bluetooth signals coming from the hundreds/thousands of spectators.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id="what-you-build-yourself-is-usually-cheaper"&gt;What you build yourself is usually cheaper&lt;/h3&gt;

&lt;p&gt;Ready-made RTLS (real-time location system) solutions exist, with all the hardware and software you need, but these are outside the budget of a race organized by students (a starter kit from the Ghent-based &lt;a href="https://www.pozyx.io/store"&gt;Pozyx&lt;/a&gt; already costs around 5000 euros).&lt;/p&gt;

&lt;p&gt;In recent years the price of UWB modules has dropped sharply, to around 19-25 euros per module for officially packaged Qorvo modules (used by Pozyx and interoperable with Apple’s U1 chip), and even as low as 12 euros for aftermarket packaged modules (clones) with an integrated microcontroller.&lt;/p&gt;

&lt;p&gt;So it seemed like a good time to explore how feasible and useful a UWB system could be at the 12Urenloop.&lt;/p&gt;

&lt;p&gt;On closer inspection, there’s practically no open-source software that supports positioning multiple tags, at least for microcontrollers outside the handful Qorvo supports. Almost all available code focuses on tracking a single tag with multiple anchors, since that’s also a lot simpler. When there are multiple tags and anchors, all UWB modules need to coordinate when they’re allowed to transmit; all communication happens over a single channel, so nothing can talk over anything else. Synchronization between modules needs to be within a few milliseconds to achieve a measurement rate of 1-10 times per second per tag.&lt;/p&gt;

&lt;p&gt;With that in mind, we ordered 5 DWM3000 UWB modules so we could build a setup with 3 anchors at fixed positions, and 2 tags whose position we determine.&lt;/p&gt;

&lt;p&gt;To drive the UWB modules we chose the ESP32-WROOM, because they’re cheap (4 euros), have bluetooth and wifi, and were already available in our basement clubroom. The code was written in the Arduino framework, so it’s also easy to port to other microcontrollers.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pics.zeus.gent/xF9JD804WaMrnsjo07In6RFjSGQiQMnfXsyrrOv9.jpg" alt="Qorvo DWM3000EVB dev boards" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two DWM3000 devboard shields on a Wemos D1 Uno (ESP32-WROOM board with arduino Uno form factor)&lt;/em&gt;&lt;/p&gt;

&lt;h3 id="tuning--testing"&gt;Tuning &amp;amp; testing&lt;/h3&gt;

&lt;p&gt;The DW3000 chipset on the UWB module is controlled over SPI, by reading and writing hundreds of registers and about a dozen commands. Qorvo (the manufacturer) provides an SDK to make certain functions easier to implement, but it only works for a handful of microcontrollers such as the NRF52 and STM32 families — not Espressif chips. Fortunately there’s a &lt;a href="https://github.com/Makerfabs/Makerfabs-ESP32-UWB-DW3000"&gt;port&lt;/a&gt; of the driver library to the Arduino framework.&lt;/p&gt;

&lt;p&gt;The focus of the first few weeks was to determine whether the DWM3000 UWB module was suitable for use at the 12Urenloop: is the module’s range far enough (the goal was at least 30 meters), is the measured distance accurate enough, and how often can you measure position when there are 15 tags (one per runner) on the circuit?&lt;/p&gt;

&lt;p&gt;After a lot of tuning of registers for the radio frame structure and transmit power, we managed to measure distance up to about 50 meters. With larger antennas on the anchors, this could probably be improved a lot further. Range does get worse when there’s no line of sight between the two modules, or when there’s a lot of metal nearby (both cause reflections of the radio signal).&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pics.zeus.gent/9QJiwHj9FDUv3Y4ekijv5ftBArf7o82ZtyHVm6KI.png" alt="Range test 40 meter results" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A graph of distance and RSSI values of a line of sight range test in open field, walking 40 meters forward, turning around a few times and coming back.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After a bit of calibration we managed to measure distance between 2 modules with about 2 centimeters of tolerance within 5 meters. At longer distances, a bit more calibration per module is needed to stay accurate, after that it can stay between 10 centimeters of tolerance using a double sided ranging scheme (both UWB modules measure the distance to each other).&lt;/p&gt;

&lt;p&gt;After these initial results, it was time to do a positioning test: 2 anchor modules were placed in the corner of our clubroom, and the third, tag module took turns measuring its distance to each anchor. The anchors send those distances to the tag over wifi via MQTT.&lt;/p&gt;

&lt;p&gt;The triangulated position was visualized in real time by a dashboard built with &lt;a href="https://bevy.org"&gt;Bevy&lt;/a&gt;.&lt;/p&gt;

&lt;video width="100%" aspect-ratio="1" controls=""&gt;
  &lt;source src="https://mattermost.zeus.gent/files/n9cp37pfxfgojr1f7beqoi3osc/public?h=4UiGkuFYTRZQ9xIhak6DNkQ_FP2tSEMUwgD-BkLStXg&amp;amp;bypass=true" type="video/mp4" /&gt;
&lt;/video&gt;

&lt;p&gt;&lt;em&gt;Left is the triangulation application’s live output, Right is our club room’s webcam&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To get this system working with 2 tags at high speed, without anyone talking over each other on the radio channel, synchronization is needed between anchors: they need to know when it’s their turn to send out a message, and when they need to wait so the rest can take their measurements. In the implementation, this works through a clock shared between all anchors: each anchor is assigned a time slot within an interval. With an interval of 1 second and time slots of 10 milliseconds, for example, you can make 100 distance measurements per second.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pics.zeus.gent/E4bLjCEk2NWWZVx2lXUn38EKBnJSGCfjsE6qrqhD.png" alt="scopesync screenshot" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Oscilloscope trace of an anchor (blue) and tag (red)’s transmit status (on or off), you can see the anchor is sending a ranging request to 2 tags, taking turns every assigned time slot. The red blip you see directly under each blue blip is the response of the red tag to the request from the blue anchor. the other red blips are responses to the other 2 anchors in the system, they are also sending out ranging requests in their assigned timeslots&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Distribution of the clock (a count of ticks on the UWB chip) happens through the existing UWB messages. Every time a message is sent between an anchor and a tag, they exchange their clock values, and the highest clock value is adopted by both parties (monotonically increasing clock).&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pics.zeus.gent/RpnwBADe6w9OMfF0Q3fjrcGjGeikSkUTpfoBDS8f.jpg" alt="Setup with 5 UWB modules" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The crazy setup with 5 UWB modules hooked up to a single laptop, we didn’t have an easy way to connect all UWB modules to one of our ESP32’s&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here’s a demo under ideal conditions with 3 anchors and 2 tags: measurements are coordinated at a rate of 60 Hz, which comes out to 60 / 2 / 3 = 10 position measurements per second. In theory, this timing could therefore update the position of 20 tags once per second.&lt;/p&gt;

&lt;video width="100%" aspect-ratio="1" controls=""&gt;
  &lt;source src="https://mattermost.zeus.gent/files/jbebfiyzgfywbmozkod176t74o/public?h=5_E_HyTIOl5SYYzaZ5DUgd1leVyDLLcwy-zEMyZzVJ0&amp;amp;bypass=true" type="video/mp4" /&gt;
&lt;/video&gt;

&lt;p&gt;&lt;em&gt;The graphics from the triangulation application was overlayed on a video recording of the test for analysis, some miscalibration remains here in certain regions&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To aggregate all these measurements in real time at the 12Urenloop and build a live-tracking visualization, some more software is needed. The current system uses 7 stations with Raspberry Pis spread around the circuit, connected via Ethernet. To test this prototype at this year’s edition of the 12Urenloop, the 3 anchors were connected to the Raspberry Pis via USB. A publisher script reads the measurement results from the anchors and sends those messages to a central RabbitMQ server in our control room (container). The real-time Bevy dashboard reads all the messages and performs all the triangulation logic, which lap counting can then be based on.&lt;/p&gt;

&lt;h3 id="supporting-more-anchors"&gt;Supporting more anchors&lt;/h3&gt;

&lt;p&gt;It’s estimated that 7-10 anchors will be needed to position tags across the entire 12Urenloop circuit, but that means at most 3 anchors will ever be within range of a tag at the same time. If every anchor still needed the channel to contact every tag individually, the measurement rate would drop quickly as the number of anchors grows.&lt;/p&gt;

&lt;p&gt;This can be solved by reusing time slots between anchors that can never be in range of a tag at the same time.&lt;/p&gt;

&lt;p&gt;With that, the proof of concept is complete. The system was tested on one corner of the 12Urenloop circuit the day before the race. The results were (after calibration) good enough in quality to support accurate tracking.&lt;/p&gt;

&lt;p&gt;In order to actually take this system in use, the tags will need to be integrated into the batons where the current bluetooth system now sits, and more extensive power and reliability measurements will need to be taken.&lt;/p&gt;

&lt;p&gt;The repository with all the code can be found on &lt;a href="https://github.com/12urenloop/Lapocalypse3000"&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A big thanks to 12Urenloop and everyone at Zeus that helped.&lt;/p&gt;

&lt;p&gt;You can contact me at &lt;a href="mailto:uwb@robinp.be"&gt;uwb@robinp.be&lt;/a&gt; .&lt;/p&gt;

&lt;h1 id="nederlands"&gt;Nederlands&lt;/h1&gt;

&lt;p&gt;De 12urenloop is een jaarlijkse loopwedstrijd op het Sint-Pietersplein in Gent. De deelnemende studentenverenigingen lopen in een estafetterace 12 uur zoveel mogelijk rondjes. Elk team krijgt één baton (doorgeefstok) toegekend, dus ze kunnen maar één loper tegelijk hebben.&lt;/p&gt;

&lt;p&gt;Zeus doet al even het tellen op de 12urenloop: eerst via een manueel telsysteem waarbij de rugnummers van de lopers worden ingegeven, dan vanaf 2011 met een automatisch telsysteem dat werkt via bluetooth-modules in de batons. Voor de rest van de context qua de historische hardware en software kan je de &lt;a href="https://zeus.gent/blog/10-11/counting-laps-using-bluetooth-dongle-detection-on-the-12-urenloop/"&gt;blogpost uit 2011&lt;/a&gt; lezen.&lt;/p&gt;

&lt;p&gt;In 2019-2022 werd de hele stack opnieuw vernieuwd, met modernere hardware, een ingeschatte positie van de lopers op de website, en uitgebreide redundancy, fault-tolerance en monitoring. Ook hier is een &lt;a href="https://zeus.gent/blog/22-23/12urenloop/"&gt;gedetaileerde blogpost&lt;/a&gt; over geschreven.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pics.zeus.gent/JA9I7ZXKXfOpGQyOoT1GsavWAn467bp3LzJj6Xq1.png" alt="12Urenloop circuit" /&gt;&lt;/p&gt;

&lt;h3 id="wat-is-uwb"&gt;Wat is UWB?&lt;/h3&gt;

&lt;p&gt;Rond eind oktober dit jaar zag ik online enkele demo’s van UltraWideBand hardware met gemakkelijk te verkrijgen en relatief goedkope hardware. UltraWideBand is een radiocommunicatie-standaard gefocust op centimeter-level localisatie van zenders of ontvangers. Dit kan o.a. omdat het een groot deel van het radiospectrum (500 Mhz) gebruikt en zo heel goed is in het negeren van reflecties van een signaal. Het kan dus gemakkelijk het begin van een ontvangen packet identificeren. Als je dat paart met een klok die accuraat is op picoseconden-niveau en je de verzendtijd van een pakket vergelijkt met de tijd van ontvangen, dan kan je berekenen hoe lang dat pakket in de lucht is geweest (aan de lichtsnelheid) tussen zender en ontvanger. Vermenigvuldig die tijd met de lichtsnelheid en je krijgt de afstand tussen zender en ontvanger. Als je die afstand tot een bewegende UWB zender (Tag) kan meten vanaf meerdere UWB zenders op een vaste positie, kan je de positie van de tag berekenen. Dit is hetzelfde principe dat GPS gebruikt, maar op een kleinere schaal.&lt;/p&gt;

&lt;p&gt;Deze technologie wordt o.a. gebruikt in Iphones en Airtags; om de afstand en richting naar de Airtag te tonen. Ook in nieuwe autosleutels die de auto openen als je dichtbij komt, omdat een UWB signaal geëncrypteerd kan zijn en zo resistant is tegen &lt;a href="https://www.ace.aaa.com/insurance/advocacy/keyless-car-theft.html"&gt;relay aanvallen&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id="wat-maakt-deze-technologie-mogelijk"&gt;Wat maakt deze technologie mogelijk?&lt;/h3&gt;

&lt;p&gt;Er werd sinds 2021 al gepraat over UWB gebruiken bij de 12Urenloop, want het biedt enkele grote voordelen:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Exacte positiemeting: Het huidige bluetooth-systeem geeft een goede inschatting van de voortgang op het circuit (door de snelheid te berekenen), maar geen positiemetingen.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Met positionering kan een ronde geteld op het exacte moment dat iemand over de startlijn gaat, Dit lukt niet met bestaande RFID systemen die voor loopraces worden gebruikt, want bij 12Urenloop is er een startlijn per team (aan de tent van elk team van verenigingen).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Meer statistieken over individuele lopers: Maximum/minimumsnelheid, snelheid in bochten etc.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Een signaal met minder ruis, want er is veel ruis door bluetooth-signalen van de honderden toeschouwers.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id="wat-je-zelf-doet-doe-je-meestal-goedkoper"&gt;Wat je zelf doet, doe je meestal goedkoper&lt;/h3&gt;

&lt;p&gt;Er bestaan kant en klare RTLS (realtime location system) oplossingen met alle hardware en software die je nodig hebt, maar deze zijn buiten het budget van een loopwedstrijd georganiseerd door studenten (De starter-kit van het Gentse &lt;a href="https://www.pozyx.io/store"&gt;Pozyx&lt;/a&gt; kost al rond de 5000 euro).&lt;/p&gt;

&lt;p&gt;De laatste jaren is de prijs van UWB-modules sterk gezakt naar rond de 19-25 euro per module voor de officiëel verpakte modules van Qorvo (Wordt gebruikt door Pozyx en is interoperabel met apple U1 chip), en zelf 12 euro voor aftermarket verpakte modules (clones) met een geïntegreerde microcontroller.&lt;/p&gt;

&lt;p&gt;Daarom leek het mij een goed moment om te verkennen hoe mogelijk en nuttig een UWB systeem kan zijn op de 12Urenloop.&lt;/p&gt;

&lt;p&gt;Bij nader inzien is er zo goed als geen open-source software die het positioneren van meerdere tags ondersteunt. Bijna alle beschikbare code focust op het tracken van één tag met meerdere anchors, want dat is ook veel simpeler. Wanneer er meerdere tags en anchors zijn moeten alle UWB modules coördineren wanneer ze mogen uitzenden; alle communicatie verloopt over één kanaal, dus er mag niet over elkaar gepraat worden. De synchronisatie tussen modules moet binnen de paar milliseconden zijn voor een meetsnelheid van 1-10 keer per seconde per tag.&lt;/p&gt;

&lt;p&gt;Dit wetend, hebben we 5 DWM3000 UWB modules besteld zodat we een opstelling kunnen maken van 3 anchors op vaste posities, en 2 tags om de positie van te bepalen.&lt;/p&gt;

&lt;p&gt;Om de UWB modules aan te sturen werd gekozen voor de ESP32-WROOM, omdat die goedkoop zijn (4 euro), bluetooth en wifi hebbben, en ook in ons kelderlokaal al aanwezig zijn. De code werd in het arduino-framework geschreven, dus is ook makkelijk over te brengen naar andere microcontrollers.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pics.zeus.gent/xF9JD804WaMrnsjo07In6RFjSGQiQMnfXsyrrOv9.jpg" alt="Qorvo DWM3000EVB dev boards" /&gt;&lt;/p&gt;

&lt;h3 id="tuning--testing-1"&gt;Tuning &amp;amp; testing&lt;/h3&gt;

&lt;p&gt;De DW3000 chipset van de UWB-module wordt aangesproken via SPI, door het lezen en schrijven van honderden registers en een tiental commandos. Qorvo (de fabrikant) stelt een SDK ter beschhikking om bepaalde functies simpeler te maken om te implementeren, maar deze werkt enkel voor een handvol microcontrollers zoals de NRF52 en STM32 familie, geen espressif chips. Gelukkig is er een community port van de driver library naar het Arduino framework.&lt;/p&gt;

&lt;p&gt;De focus van de eerste weken was om te bepalen of de DWM3000 UWB-module geschikt was om te gebruiken op de 12Urenloop: Is het meetbereik van de module ver genoeg (het doel was minstens 30 meter), is de gemeten afstand accuraat genoeg, en hoe vaak kan je de positie meten wanneer er 15 tags (1 per loper) op het circuit zijn.&lt;/p&gt;

&lt;p&gt;Na heel wat tunen van registers voor de structuur van de radio-frame en het uitzendvermogen lukte het om afstand te meten tot op ongeveer 50 meter afstand. Met grotere antennes aan de anchors kan dit waarschijnlijk nog veel verbeterd worden. Het bereik wordt wel slechter als er geen Visueel contact (Line Of Sight) tussen beide modules is of als er veel metaal in de buurt is (beide zorgen voor reflecties van het radiosignaal).&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pics.zeus.gent/9QJiwHj9FDUv3Y4ekijv5ftBArf7o82ZtyHVm6KI.png" alt="Range test 40 meter resultaten" /&gt;&lt;/p&gt;

&lt;p&gt;Na een beetje calibratie lukte het om afstand te meten tussen 2 modules met ongeveer 2 centimeter tolerantie binnen de 5 meter. Op langere afstand is er iets meer calibratie nodig per module om accuraat te meten.&lt;/p&gt;

&lt;p&gt;Na deze initiële resultaten was het tijd om een positionerings-test te doen: 2 Anchor-modules werden in de hoek van ons lokaal geplaatst, de derde Tag-module start beurtelings een meting van de afstand naar elke Anchor, de anchors sturen de afstanden naar de tag over wifi door via MQTT.&lt;/p&gt;

&lt;p&gt;De getrianguleerde positie werd in realtime gevisualiseerd door een dashboard gemaakt met &lt;a href="https://bevy.org"&gt;Bevy&lt;/a&gt; .&lt;/p&gt;

&lt;video width="100%" aspect-ratio="1" controls=""&gt;
  &lt;source src="https://mattermost.zeus.gent/files/n9cp37pfxfgojr1f7beqoi3osc/public?h=4UiGkuFYTRZQ9xIhak6DNkQ_FP2tSEMUwgD-BkLStXg&amp;amp;bypass=true" type="video/mp4" /&gt;
&lt;/video&gt;

&lt;p&gt;Om dit systeem te laten werken met 2 tags op hoge snelheid zonder dat er over elkaar wordt gepraat op het radiokanaal, is er synchronisatie nodig tussen anchors: Die moeten weten wanneer het aan hun beurt is om een bericht uit te sturen en wanneer ze moeten wachten om de rest hun metingen te laten doen. In de implementatie werkt dat door een gedeelde klok tussen alle anchors: Elke anchor krijgt een tijd-slot toegewezen in een interval. Met een interval van 1 seconde en tijd-slots van 10 milliseconden kan je bijvoorbeeld 100 afstandsmetingen maken per seconde.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pics.zeus.gent/E4bLjCEk2NWWZVx2lXUn38EKBnJSGCfjsE6qrqhD.png" alt="scopesync screenshot" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Oscilloscoop-trace van de transmit-status van een achor (blauw) en tag (rood), de anchor stuurt een ranging request uit naar 2 tags om de beurt. De rode blip direct onder elke blauwe blip is de respons van de rode tag op de request van de blauwe anchor. De andere rode blips zijn antwoorden naar de andere 2 anchors in het systeem, zij zenden ook requests uit in hun toegewezen timeslots&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;De distributie van de klok (telling van aantal ticks op de UWB-chip) werkt via de bestaande UWB-berichten. Elke keer een bericht gestuurd wordt tussen een anchor en tag wisselen ze elkaars klokwaarde uit, de hoogste klokwaarde wordt overgenomen door beide zenders.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pics.zeus.gent/RpnwBADe6w9OMfF0Q3fjrcGjGeikSkUTpfoBDS8f.jpg" alt="setup met 5 UWB modules" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;de crazy setup met 5 UWB modules aan één laptop, we hadden geen makkelijke manier om elke UWB module te verbinden met één van onze ESP32’s&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hier is een demo in ideale omstandigheden met 3 anchors en 2 tags: Metingen worden gecoördineerd op een snelheid van 60 Hz, dat zijn 60 / 2 / 3 = 10 positiemetingen per seconde. Theoretisch kan deze timing dus voor 20 tags de positie 1 keer per seconde updaten.&lt;/p&gt;

&lt;video width="100%" aspect-ratio="1" controls=""&gt;
  &lt;source src="https://mattermost.zeus.gent/files/jbebfiyzgfywbmozkod176t74o/public?h=5_E_HyTIOl5SYYzaZ5DUgd1leVyDLLcwy-zEMyZzVJ0&amp;amp;bypass=true" type="video/mp4" /&gt;
&lt;/video&gt;

&lt;p&gt;Om al deze metingen in realtime te aggregeren op de 12Urenloop om een live-tracking visualizatie te maken, is wat meer software nodig. Het huidige systeem gebruikt 7 stations met Raspberry-Pi’s verspreid over het circuit verbonden met Ethernet. Om dit prototype te testen op de 12Urenloop-editie van dit jaar werden de 3 anchors aangesloten via USB aan de raspberry pi’s. Een publisher-script leest de meetresultaten van de anchors en stuurt die berichten naar een centrale RabbitMQ-server in onze controlekamer (container) . Het realtime bevy-dashboard leest alle berichten en voert alle triangulatie-logica uit, daar kan dan een rondetelling op gebaseerd worden.&lt;/p&gt;

&lt;h3 id="meer-anchors-ondersteunen"&gt;Meer anchors ondersteunen&lt;/h3&gt;

&lt;p&gt;Er wordt geschat dat er 7-10 anchors zullen nodig zijn om op het volledige circuit van de 12Urenloop tags te kunnen positioneren, maar dan zullen maar maximaal 3 anchors ooit tegelijk binnen bereik van een tag zijn. Als elke anchor toch altijd het kanaal nodig heeft om elke tag apart te contacteren stort de snelheid van metingen snel naar beneden met het aantal anchors.&lt;/p&gt;

&lt;p&gt;Dit kan worden opgelost door het hergebruiken van timeslots tussen anchors die niet tegelijk in het bereik van een tag kunnen zijn.&lt;/p&gt;

&lt;p&gt;Daarmee is de basis van de proof-of-concept af. Het systeem werd getest op één bocht van het 12Urenloop circuit de dagg voor de wedstrijd. De resultaten waren (na calibratie) voldoende kwalitatief om een accurate tracking mee te doen.&lt;/p&gt;

&lt;p&gt;Om dit systeem daadwerkelijk in gebruik te nemen, moet de tag geïntegreerd worden in de baton waar nu het bluetooth-systeem in zit, en betere power- en betrouwbaarheidsmetingen zullen moeten genomen worden.&lt;/p&gt;

&lt;p&gt;De repository met alle code is te vinden op &lt;a href="https://github.com/12urenloop/Lapocalypse3000"&gt;Github&lt;/a&gt; .&lt;/p&gt;

&lt;p&gt;Een dikke merci aan 12Urenloop en iedereen van Zeus die hulp heeft geboden.&lt;/p&gt;

&lt;p&gt;Je kan me contacteren via &lt;a href="mailto:uwb@robinp.be"&gt;uwb@robinp.be&lt;/a&gt; .&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:zeus.ugent.be,2025-11-03:/blog/25-26/hortiroot-intro/</id>
    <title type="html">HortiRoot scanners</title>
    <published>2025-11-03T00:00:00Z</published>
    <updated>2025-11-03T00:00:00Z</updated>
    <link rel="alternate" href="https://zeus.ugent.be/blog/25-26/hortiroot-intro/" type="text/html"/>
    <content type="html">&lt;p&gt;Beste Zeussers en Zeusinnen,&lt;/p&gt;

&lt;p&gt;Enkele weken geleden werden we gecontacteerd door de onderzoeksgroep &lt;a href="https://www.ugent.be/bw/plants-and-crops/en/research/hortiroot"&gt;HortiRoot&lt;/a&gt;.
In hun lab hebben ze een redelijk aantal Epson &lt;em&gt;flatbed foto scanners&lt;/em&gt; omgebouwd om er kleine plantjes in petriplaten op te kunnen groeien.
Hiermee kunnen ze met korte tijdsintervallen scans nemen, om &lt;em&gt;timelapses&lt;/em&gt; te maken van de groei in hoge resolutie.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pics.zeus.gent/ImlHShBfCz0cCAXIKl1QEcGk3aEvKeFBuolFn3VA.png" alt="A metal frame holding 4 scanners in place, sitting in a lab. On the left there is an electrical box where the LED controls and Arduinos are housed." /&gt;&lt;/p&gt;

&lt;p&gt;De UGent workshop &lt;a href="https://www.ugent.be/we/en/faculty/faculty-offices/technical-scientific-workshop/overview"&gt;WE62&lt;/a&gt; heeft hun geholpen met de hardware mods:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Het deksel (lid) werd verwijderd. De elektronica verantwoordelijk voor de “closed” detectie wordt nu nagebootst door een Arduino.&lt;/li&gt;
  &lt;li&gt;Over het bed kan nu een kunststoffen plaat geschoven worden waar de petriplaten in passen.&lt;/li&gt;
  &lt;li&gt;Een frame werd gemaakt om alles op zijn plaats te houden, en zodat de scans &lt;em&gt;verticaal&lt;/em&gt; gemaakt kunnen worden.&lt;/li&gt;
  &lt;li&gt;Ook is er nu een ledstrip om semi-natuurlijk licht na te bootsen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Voor de software merkten de onderzoekers echter dat de UGent geen equivalent van die werkplaats heeft.
De onderzoekers van HortiRoot hebben zelf wat geprobeerd om de standaard Windows 10 Epson software te automatiseren met behulp van &lt;code&gt;pyautogui&lt;/code&gt;.
Uiteindelijk hadden ze wel een werkend systeem, dat momenteel nog steeds gebruikt wordt.
Onderhoudbaar is dit helemaal niet: automatisch de muis proberen bedienen schaalt niet bepaald goed…
Ze hadden ook de software voor &lt;em&gt;Ubuntu&lt;/em&gt; uitgeprobeerd, maar die avonturen waren jammer genoeg van korte duur omdat DICT geen ondersteundend personeel had met voldoende expertise hiermee.
Nu recent stak de &lt;a href="https://endof10.org"&gt;verplichte update naar Windows 11&lt;/a&gt; ook een spaak in het wiel.&lt;/p&gt;

&lt;p&gt;Na wat details van het project te horen wees DICT de onderzoeksgroep naar ons door.
Even later werden Xander, Hannes en ik hartelijk ontvangen op campus &lt;em&gt;Coupure&lt;/em&gt;.
In een labo vol groen konden wij de verschillende iteraties van de scanner mods aanschouwen.
Maar vooral van de vele &lt;em&gt;hacks&lt;/em&gt; die ze gevonden hadden om toch maar de software te doen werken, waren we onder de indruk.
Zo lukte het niet om de scan software te gebruiken met meerdere apparaten aangesloten, dus hadden ze als oplossing om elke scanner op een &lt;em&gt;aparte virtuele machine&lt;/em&gt; aan te sluiten.
Niet ideaal, dus probeerden ze om maar 1 apparaat tegelijk te tonen aan de software met behulp van &lt;em&gt;programmeerbare USB hubs&lt;/em&gt;.
Dit werkte niet meer in Windows 11, maar zelfs dan was dit vrij beperkt in aantal apparaten.
Ook hadden ze ontdekt dat ze doorheen dinsdagnacht geen scans konden doen, omdat DICT &lt;em&gt;&lt;a href="https://en.wikipedia.org/wiki/Patch_Tuesday"&gt;patch tuesdays&lt;/a&gt;&lt;/em&gt; doet…&lt;/p&gt;

&lt;p&gt;Na uitwisselen van ons intern telefoonnummer, werden even later 2 van de scanners geleverd aan de Zeuskelder.
Er zijn hier al wat mensen bezig geweest met de scanners om te helpen de opties te verkennen.
Wat ons meteen verrastte is dat de Linux software &lt;code&gt;epsonscan2&lt;/code&gt; die Epson publiceert, bijna volledig open-source en vrij is.
Bovendien is dit een grote bron aan documentatie van de interne werking van de scanners. De enige drempel is dat die deels in het Japans is 😅.&lt;/p&gt;

&lt;p&gt;Om deze software inderdaad werkend te krijgen, is een ander verhaal.
Wanneer de software probeerde te verbinden gingen de scanners steeds in een “internal error” state.
Door wat instrumentatie aan &lt;code&gt;esponscan2&lt;/code&gt; toe voegen en veel verschillende opstellingen te proberen, vonden we uiteindelijk een oplossing.
We merkten dat een scanner slechts éénmaal correct geïnitializeerd moest worden door andere software (zoals de windows 11 versie), waarna de scanner werkt met alle software.
Door de communicatie te onderscheppen, konden we een programma maken dat de scanners op dezelfde manier kan initializeren.
Waarom exact dit nodig is, is nog niet helemaal duidelijk.&lt;/p&gt;

&lt;p&gt;We zijn opgelucht dat dit werkt, omdat dit betekent dat we helemaal geen nood hebben aan Windows, virtuele machines, USB multiplexers of het automatisch navigeren van menu’s.
In de plaats kunnen we volledig werken met de betrouwbare en voorspelbare software die we gewend zijn.&lt;/p&gt;

&lt;p&gt;Het plan is om scanners per ~4 te groeperen, die samen aangestuurd worden door 1 &lt;em&gt;node&lt;/em&gt; (Raspberry Pi).
Deze &lt;em&gt;nodes&lt;/em&gt; worden ontdekt op het lokale netwerk door de backend van een webapplicatie.
Die webapplicatie stuurt dan requests uit om scans te nemen met bepaalde parameters, op regelmatige intervallen.
De geproduceerde afbeeldingen worden dan verplaatst van de node naar de machine waar de webapp draait.
Gebruikers kunnen dan die &lt;em&gt;timelapses&lt;/em&gt; beheren vanuit de webapp op het lokaal netwerk, en eveneens ook de afbeeldingen downloaden.&lt;/p&gt;

&lt;p&gt;Wat is reeds geïmplementeerd?&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Nodes:
    &lt;ul&gt;
      &lt;li&gt;Er is een script dat nieuw aangesloten scanners detecteert en ze meteen correct initializeert.
        &lt;ul&gt;
          &lt;li&gt;Dit gebeurt &lt;a href="https://git.zeus.gent/ZeusWPI/hortiroot-scanners/src/commit/f125641dc8/sc/source/sc/scanner/initialize.d#L40"&gt;hier&lt;/a&gt; door specifieke &lt;code&gt;USB_BULK&lt;/code&gt; pakketten te sturen, onder andere met de firmware blob.&lt;/li&gt;
          &lt;li&gt;Het script (&lt;code&gt;sc&lt;/code&gt;) is in D geschreven, zogoed als alles wordt gedaan via de &lt;code&gt;libusb&lt;/code&gt; C library.&lt;/li&gt;
          &lt;li&gt;Soms faalt het initializeren door een “permission denied”, omdat de udev rule nog niet geactiveerd is op het moment dat &lt;code&gt;libusb&lt;/code&gt; het apparaat detecteert.
Als &lt;em&gt;root&lt;/em&gt; uitvoeren fixt het…&lt;/li&gt;
          &lt;li&gt;Het protocol dat gebruikt wordt bovenop &lt;code&gt;USB_BULK&lt;/code&gt; is &lt;em&gt;ESC/I&lt;/em&gt;.
Enkele links naar documentatie hierover zijn &lt;a href="https://git.zeus.gent/ZeusWPI/hortiroot-scanners/src/commit/f125641dc8/epsonscan2"&gt;hier&lt;/a&gt; te vinden.&lt;/li&gt;
          &lt;li&gt;Dit protocol verder ontdekken kan nog steeds handig zijn:
Het lukt momenteel niet om elke scanner uniek te identificeren.
Ze dragen geen &lt;em&gt;serial number&lt;/em&gt; zoals de meeste USB devices.
Misschien is er wel een command om iets gelijkaardig op te slaan?
Ik zie ook verwijzingen naar een &lt;em&gt;non-volatile user variables&lt;/em&gt; api.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;De &lt;code&gt;epsonscan2&lt;/code&gt; software build vanuit de &lt;a href="https://git.zeus.gent/ZeusWPI/hortiroot-scanners"&gt;git repo&lt;/a&gt; met behulp van docker.
        &lt;ul&gt;
          &lt;li&gt;De build artifact is gemaakt voor Debian Bookworm.
Kijken of het mogelijk is te upgraden naar Trixie.&lt;/li&gt;
          &lt;li&gt;Er is een dependency op een non-free tool, die binair gepatcht wordt om de paden ook naar &lt;code&gt;/opt/epsonscan2&lt;/code&gt; te doen wijzen.
Deze prefix is gemakkelijker te gebruiken dan &lt;code&gt;.deb&lt;/code&gt; packages maken die naar &lt;code&gt;/usr&lt;/code&gt; uitpakken.&lt;/li&gt;
          &lt;li&gt;De software heeft een command-line versie, maar die kan nog niet een bepaalde scanner selecteren.
We kunnen dit zelf implementeren.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Webapp:
    &lt;ul&gt;
      &lt;li&gt;Nog niets!
Bepaal zelf welke taal/framework (of talen/frameworks, meer is beter right?).
Requirements in de repo te vinden.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kijk zeker eens in de &lt;a href="https://git.zeus.gent/ZeusWPI/hortiroot-scanners"&gt;git repo&lt;/a&gt; en join al vast het &lt;a href="https://mattermost.zeus.gent/zeus/channels/hortiroot-scanners"&gt;~horitroot-scanners&lt;/a&gt; kanaal op Mattermost!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;PS: Wat zouden wij kunnen scannen?&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:zeus.ugent.be,2025-08-31:/blog/25-26/augustus/</id>
    <title type="html">Wat heeft Zeus gedaan, de afgelopen maan?</title>
    <published>2025-08-31T00:00:00Z</published>
    <updated>2025-08-31T00:00:00Z</updated>
    <link rel="alternate" href="https://zeus.ugent.be/blog/25-26/augustus/" type="text/html"/>
    <content type="html">&lt;p&gt;We kijken even naar wat er is gebeurd tijdens de zomermaanden!&lt;/p&gt;

&lt;p&gt;Het is zomer, wat kun je anders doen dan je in een kelder verstoppen voor de hitte en een hoop code schrijven? Dat is in ieder geval gebeurd! De highlights zijn:&lt;/p&gt;

&lt;h4 id="zeus-profile-imageshttpsgithubcomzeuswpizpi"&gt;&lt;a href="https://github.com/ZeusWPI/ZPI"&gt;Zeus Profile Images&lt;/a&gt;&lt;/h4&gt;

&lt;p&gt;Zeus heeft nu profielfotos!&lt;/p&gt;

&lt;p&gt;Er is een centrale site om je profielfoto in te stellen: &lt;a href="https://zpi.zeus.gent/"&gt;https://zpi.zeus.gent/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Je foto zal (langzaam maar zeker) zichtbaar zijn in meerdere Zeus-applicaties. Op dit moment kun je je foto al aanschouwen in ZAUTH en EVENTS. We zijn ook van plan om Tap hier naar over te schakelen, dus als je graag je oude Tapfoto wilt gebruiken, is nu het moment om deze over te zetten!&lt;/p&gt;

&lt;h4 id="documentatie"&gt;Documentatie&lt;/h4&gt;

&lt;p&gt;We zijn aan het werken aan Zeus-documentatie! Omdat nieuwe bestuursleden tijdens de eerste vergadering heel wat nieuwe dingen moeten onthouden (en voor de oudere die iets vergeten zijn) zijn we bezig met informatie te verzamelen in 1 centraal punt over hoe Zeus werkt. Ook informatie over sysadmin zaken en over dingen in de kelder zullen hier verzameld worden. Een WIP versie is &lt;a href="https://git.zeus.gent/ZeusWPI/docs"&gt;hier&lt;/a&gt; te vinden. PRs, comments en bedenkingen zijn altijd welkom.&lt;/p&gt;

&lt;p&gt;Omdat we toch geen docs kunnen schrijven zonder code te schrijven hebben we ook vlug &lt;a href="https://github.com/ZeusWPI/ligny"&gt;onze eigen SSG&lt;/a&gt; geschreven.&lt;/p&gt;

&lt;h4 id="events"&gt;EVENTS&lt;/h4&gt;

&lt;p&gt;Ons eigen &lt;a href="https://github.com/ZeusWPI/events"&gt;event-management platform&lt;/a&gt; heeft een hele reeks updates gekregen. Het is bedoeld voor bestuur om events efficienter te plannen, te zorgen dat announcements op tijd worden gemaakt, en heeft een aantal andere superleuke features zoals automatisch powerpoints maken!&lt;/p&gt;

&lt;h4 id="keldermuzieksoftware"&gt;Keldermuzieksoftware&lt;/h4&gt;

&lt;p&gt;Rond muziek werden er ook leuke dingen gedaan: veel informatie over het huidig nummer is nu beschikbaar via onze MQTT broker, je kunt stemmen op nummers (in de kelder) via &lt;a href="https://github.com/ZeusWPI/ZODOM"&gt;ZODOM&lt;/a&gt; en informatie krijgen over nummers via &lt;a href="https://github.com/ZeusWPI/GUITAR"&gt;GUITAR&lt;/a&gt;. Ook hebben we nu terug een versterker! Hoezee!&lt;/p&gt;

&lt;h4 id="ledstrip"&gt;Ledstrip&lt;/h4&gt;

&lt;p&gt;Er werd gesleuteld aan de ledstrip: er is nu &lt;a href="https://github.com/ZeusWPI/ledstrip-spotify-progress"&gt;een muziek progress bar&lt;/a&gt; en er werden verbeteringen aangebracht in de &lt;a href="https://github.com/ZeusWPI/ledstrip_sandbox"&gt;besturingscode&lt;/a&gt;.&lt;/p&gt;

&lt;h4 id="tap"&gt;Tap&lt;/h4&gt;

&lt;p&gt;Tap heeft enkele UX verbeteringen gekregen.&lt;/p&gt;

&lt;h2 id="bestuur"&gt;Bestuur&lt;/h2&gt;

&lt;p&gt;Op vlak van bestuurzaken, zullen we binnenkort (9 september) samenzitten om de events van dit semester te plannen en ons voor te bereiden op de start van het academiejaar!&lt;/p&gt;

&lt;p&gt;Ook staan we in contact met &lt;a href="https://pelicano.be/"&gt;Pelicano&lt;/a&gt; die graag samen met ons hun eigen urenloop willen organiseren. De details (en of het mogelijk is) zijn we nog aan het bekijken, maar in ieder geval binnenkort meer nieuws hierover!&lt;/p&gt;

&lt;p&gt;We hebben ook onze eerste sponsor van dit jaar! De details liggen nog niet vast, maar we zullen samen een event organiseren.&lt;/p&gt;

&lt;p&gt;We krijgen vaak de vraag van een bedrijf (of andere instantie) of we een job willen verspreiden onder onze leden. Aangezien we geen fan zijn van dat zomaar in ~general to posten, hebben we het ~jobs kanaal gemaakt, zodat dit voor iedereen opt-in is. Bedrijven die ons sponsoren kunnen hier ook enkele jobs plaatsen (maar dit zal steeds door ons gaan).&lt;/p&gt;

&lt;p&gt;Hopelijk was het al een fijne zomer voor iedereen!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:zeus.ugent.be,2024-03-09:/blog/23-24/additional_problems/</id>
    <title type="html">Belgian Cyber Security Challenge 2024: Additional problems</title>
    <published>2024-03-09T00:00:00Z</published>
    <updated>2024-03-09T00:00:00Z</updated>
    <link rel="alternate" href="https://zeus.ugent.be/blog/23-24/additional_problems/" type="text/html"/>
    <content type="html">&lt;p&gt;&lt;strong&gt;Title&lt;/strong&gt;: Additional Problems&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Category&lt;/strong&gt;: cryptography&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Points&lt;/strong&gt;: 500&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solves&lt;/strong&gt;: 2&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Version 1.0 of our new encryption service has just launched!
It is blazingly fast and uses state-of-the-art encryption.
Stay tuned for version 2.0; I hear it will bring tons of improvements and security fixes.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://pics.zeus.gent/server.py"&gt;Download challenge files&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Access the server via &lt;code&gt;nc IP_ADDRESS PORT&lt;/code&gt; (the server is now down, but you can run it yourself)&lt;/p&gt;

&lt;h1 id="introduction"&gt;Introduction&lt;/h1&gt;

&lt;p&gt;This challenge proved to be among the hardest challenges at CSCBE, with only one team submitting a flag. The teams at Zeus WPI ended up in 2nd, 9th (virtual, since winners are not allowed to participate again), 52nd and 83rd and 104th place at the online qualifiers.&lt;/p&gt;

&lt;h1 id="description"&gt;Description&lt;/h1&gt;

&lt;p&gt;The challenge provides a Python file, which runs a server accessible through a TCP socket.
The server implements a form of homomorphic encryption: with homomorphic encryption, addition of two ciphertexts results in a ciphertext that decrypts to the addition of the two plaintexts.
Here, the encryption was implemented with a secret 128-bit prime &lt;code&gt;p&lt;/code&gt;, and an &lt;code&gt;N&lt;/code&gt; between 128 and 255. The cryptosystem works as follows:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#!python
def dghv_encrypt(p, N, m):
    """
    Encrypt a value to later decrypt with `dghv_decrypt`
    """
    assert 2**7 &amp;lt;= N &amp;lt; 2**8 # Normally this is 2, but by using a bigger `N` we can encode ASCII bytes instead of bits! That's much more efficient. All `N` in this range should be secure, so let's make it an assertion

    q = reduce(lambda x, y: x*y, [Crypto.Util.number.getPrime(128) for _ in range(8)]) # `q` can be any number, but as we all know, big primes are the safest numbers there are
    rmax = 2**128 / N / 4
    r = random.randint(0, rmax) # In v2.0, we will let `r` be negative as well as positive =&amp;gt; double the randomness!
    return p*q + N*r + m

def dghv_decrypt(p, N, c):
    """
    Since c = pq + Nr + m, we can find m as (c mod p) mod N!
    """
    return (c % p) % N

def dghv_add(c1, c2):
    """
    The sum of ciphertexts decodes to the sum of plaintexts!!!
    """
    return c1 + c2 # We will add bootstrapping to make this fully homomorphic in v2.0

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Encryption, decryption and adding works on byte-based granularity: every byte is encrypted separately.&lt;/p&gt;

&lt;p&gt;When first connecting, the server generates a new &lt;code&gt;p&lt;/code&gt;, encrypts the flag with it and prints the encrypted flag.
It then asks for an &lt;code&gt;N&lt;/code&gt; value and a (hexadecimal) plaintext that will be used in your first session.&lt;/p&gt;

&lt;p&gt;It then provides a menu where you have three options:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Start a new session, where you need to give an &lt;code&gt;N&lt;/code&gt; value and a plaintext; it will use &lt;code&gt;dghv_encrypt&lt;/code&gt; to set the ciphertext&lt;/li&gt;
  &lt;li&gt;Add an additional plaintext to the current session: it will encrypt that plaintext with &lt;code&gt;dghv_encrypt&lt;/code&gt;, then use &lt;code&gt;dghv_add&lt;/code&gt; to add it to the current ciphertext&lt;/li&gt;
  &lt;li&gt;Decrypt the ciphertext: it returns the result of &lt;code&gt;dghv_decrypt&lt;/code&gt; on the current ciphertext&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that other than the initial ciphertext printed at startup, we never have access to any ciphertexts.&lt;/p&gt;

&lt;h1 id="vulnerability"&gt;Vulnerability&lt;/h1&gt;

&lt;p&gt;If you add too many plaintexts, the &lt;code&gt;N*r&lt;/code&gt; terms become greater than &lt;code&gt;p&lt;/code&gt;, making the decryption step &lt;code&gt;(c % p) % N&lt;/code&gt; not work correctly anymore. Suppose you always send zero-filled plaintext, and it eventually decrypts to a value &lt;code&gt;x != 0&lt;/code&gt;, then we have:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;
(p*q + N*r + (m_0 + m_1 + m_... + m_n)) % p % N == x

because all message bytes m_n are 0, we have

(p*q + N*r) % p % N == x

now, let N*r &amp;gt; p, but smaller than 2*p (we can be sure of this,
since r only increases with maximum 2**128 / N / 4 per encryption
and p is a 128 bit prime), then we have

N*r = p + a.

By substituting N*r for p + a

(p*q + p + a) % p % N == x

which is then equal to

a % p % N == x

and thus, since a &amp;lt; p,

a % N = x

and since 0 &amp;lt; x &amp;lt; N,

p % N == N - x.

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We can then iterate through all prime values of N between 128 and 255 to get each value of &lt;code&gt;p % N_i&lt;/code&gt;; once we have these values, we can use &lt;a href="https://en.wikipedia.org/wiki/Chinese_remainder_theorem"&gt;the Chinese Remainder Theorem&lt;/a&gt; to calculate p (since the product of all those primes is bigger than 128 bits, p is uniquely determined).&lt;/p&gt;

&lt;h1 id="solution-script"&gt;Solution script&lt;/h1&gt;

&lt;p&gt;This script was written in the heat of the moment, so it’s not the cleanest, but it works.
It uses the excellent pwntools library.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#!python
from pwn import remote

conn = remote('additional_problems.challenges.cybersecuritychallenge.be', 1340)

def dghv_decrypt(p, N, c):
    """
    Since c = pq + Nr + m, we can find m as (c mod p) mod N!
    """
    return (c % p) % N


def add(conn, msg):
    print('add')
    conn.sendline(b'2')
    conn.sendline(msg)
    conn.recvuntil(b'&amp;gt; ')

def get(conn):
    print('get')
    conn.sendline(b'3')
    lines = conn.recvuntil(b'&amp;gt; ')
    hexed = [int(a, 16) for a in lines.decode().split('\n')[0].split(': ')[1].split(' ')]
    return hexed

def new(conn, N, msg):
    print('new')
    conn.sendline(b'1')
    conn.recvuntil(b'Choose N: ')
    conn.sendline(str(N).encode())
    conn.recvuntil(b'Message to encode (converted to hexadecimal): ')
    conn.sendline(msg)
    conn.recvuntil(b'&amp;gt; ')

conn.recvuntil(b"We've even encrypted our secret flag with it:\n")

encrypted = conn.recvuntil(b'\n\n').decode().strip()

# only in modified testserver
# p, encrypted = encrypted.split('\n', maxsplit=1)
# p = int(p.strip())

ciphertexts = [int(e.strip()) for e in encrypted.split('\n')]

# skip initial setup
conn.sendline(b'128')
conn.recvuntil(b'Message to encode (converted to hexadecimal): ')
conn.sendline((' '.join('00' for _ in range(30))).encode())
conn.recvuntil(b'&amp;gt; ')

# real shit
all_zeroes = (' '.join('00' for _ in range(10))).encode()

primes = [131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251]

mapped = {}


from functools import reduce
def chinese_remainder(m, a):
    sum = 0
    prod = reduce(lambda acc, b: acc*b, m)
    for n_i, a_i in zip(m, a):
        p = prod // n_i
        sum += a_i * mul_inv(p, n_i) * p
    return sum % prod
 
def mul_inv(a, b):
    b0 = b
    x0, x1 = 0, 1
    if b == 1: return 1
    while a &amp;gt; 1:
        q = a // b
        a, b = b, a%b
        x0, x1 = x1 - q * x0, x0
    if x1 &amp;lt; 0: x1 += b0
    return x1

for prime_idx, prime in enumerate(primes):
    new(conn, prime, all_zeroes)
    for i in range(32):
        add(conn, all_zeroes)
        received = get(conn)
        reduced = [d for d in received if d != 0]
        if reduced:
            mapped[prime] = prime - reduced[0]
            # only in modified testserver
            # assert (p % prime == mapped[prime])
            # print("OK!")
            break
        print(f'{prime_idx} of {len(primes)} {i=}')


moduli = [mapped[prime] for prime in primes]
recovered = chinese_remainder(primes, moduli)

print('FLAG=')
for cipher in ciphertexts:
    print(chr(dghv_decrypt(recovered, 128, cipher)), end='')

print('')
&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <id>tag:zeus.ugent.be,2023-12-07:/blog/23-24/esp32-reverse-engineering-continued/</id>
    <title type="html">Unveiling secrets of the ESP32 part 2: reverse engineering RX</title>
    <published>2023-12-07T00:00:00Z</published>
    <updated>2023-12-07T00:00:00Z</updated>
    <link rel="alternate" href="https://zeus.ugent.be/blog/23-24/esp32-reverse-engineering-continued/" type="text/html"/>
    <content type="html">&lt;p&gt;This is the second article in a series about reverse engineering the ESP32 Wi-Fi networking stack, with the goal of building our own open-source MAC layer. In the &lt;a href="https://zeus.ugent.be/blog/23-24/open-source-esp32-wifi-mac/"&gt;previous article&lt;/a&gt; in this series, we built static and dynamic analysis tools for reverse engineering. We also started reverse engineering the transmit path of sending packets, and concluded with a rough roadmap and a call for contributors.&lt;/p&gt;

&lt;p&gt;In this part, we’ll continue reverse engineering, starting with the ‘receiving packets’ functionality: last time, we succesfully transmitted packets. The goal of this part is to have both transmitting and receiving working. To prove that our setup is working, we’ll try to connect to an access point and send some UDP packets to a computer also connected to the network.&lt;/p&gt;

&lt;h1 id="receive-functionality"&gt;Receive functionality&lt;/h1&gt;

&lt;p&gt;As a short recap, the transmit functionality worked by:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Putting the packet you want to transmit in memory&lt;/li&gt;
  &lt;li&gt;Create a DMA (direct memory access) struct. This struct contains:
    &lt;ul&gt;
      &lt;li&gt;the address of the packet you want to transmit&lt;/li&gt;
      &lt;li&gt;the length and size of the packet (I haven’t entirely figured out the difference, but one always seems to be 32 bigger than the other one)&lt;/li&gt;
      &lt;li&gt;the address of the next packet (we set this to NULL to transmit a single packet)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Write some other memory peripherals to configure the settings for the packet you’re about to transmit&lt;/li&gt;
  &lt;li&gt;Write the address of the DMA struct to a memory mapped IO address&lt;/li&gt;
  &lt;li&gt;The hardware then automatically reads the DMA struct, and transmits the packet&lt;/li&gt;
  &lt;li&gt;After this is done, interrupt 0 will fire, telling us how succesful the transmission was&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The receive functionality seems to use the same DMA struct, but in a slightly different way:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Set up a linked list of DMA structs, where the &lt;code&gt;next&lt;/code&gt; field of the struct points to the next DMA struct in the linked list. The final DMA struct points to NULL. Every &lt;code&gt;address&lt;/code&gt; field points to a buffer, and the length and size fields are set to the size of the buffer.&lt;/li&gt;
  &lt;li&gt;Write the address of the first DMA struct to a memory mapped IO address (&lt;code&gt;WIFI_BASE_RX_DSCR&lt;/code&gt;). Now the setup is done, and we can receive packets.&lt;/li&gt;
  &lt;li&gt;When a packet is received by the hardware, it will put the packet into the address of the first available DMA struct. The &lt;code&gt;length&lt;/code&gt; field will indicate the length of the packet; the &lt;code&gt;size&lt;/code&gt; field will not be updated. The &lt;code&gt;has_data&lt;/code&gt; field will be set to 1.&lt;/li&gt;
  &lt;li&gt;Interrupt 0 will fire to notify the processor that a packet was received. This interrupt will notify a non-interrupt task that a packet was received. We should avoid to do much processing in the interrupt, since we want to return as quickly as possible.&lt;/li&gt;
  &lt;li&gt;Outside of the interrupt, we can then look at the linked list of DMA structs to see which ones have their &lt;code&gt;has_data&lt;/code&gt; bit set. The address buffers can then be passed up further in the Wi-Fi MAC stack. We want to avoid running out of DMA structs to receive packets into, so we have to extend the linked list. We could do it by just allocating a new DMA struct and space for a packet and putting it at the end of the DMA linked list, but this constant allocating and deallocating would be rather inefficient. Instead, we recycle existing DMA structs by resetting their fields and inserting them at the end of the linked list.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id="practicalities"&gt;Practicalities&lt;/h1&gt;

&lt;p&gt;Now we have a basic way to receive packets, but when we implemented this, no packets were received: this was likely because of the hardware MAC address filters: if you are a Wi-Fi device, there are a lot of packets flying in the air that you’re not interested in. For example, if you’re a station (for example, a phone) and are connected to an access point, you don’t really care about the packets other access points are sending to their stations. To avoid the overhead in also having to process ‘uninteresting’ packets, most Wi-Fi devices have a hardware filter where you can set the MAC addresses of packets you want to receive. The hardware will then filter out the packets with different MAC addresses, and will only forward packets with matching MAC addresses to the software.&lt;/p&gt;

&lt;p&gt;The ESP32 also seems to have this implemented, but luckily for us, the ESP32 also implements a sort of monitor mode (also known as promiscuous mode), where every packet that is receieved by the hardware is passed to the software. The ESP32 SDK has a call &lt;code&gt;esp_wifi_set_promiscuous(bool)&lt;/code&gt; where you can enable or disable this feature. When we enabled this, we did start to receive packets. We’ll eventually reverse engineer and implement hardware MAC address filtering as well, but for now, we’ll just filter in software.&lt;/p&gt;

&lt;h1 id="connecting-to-an-access-point"&gt;Connecting to an access point&lt;/h1&gt;

&lt;p&gt;Now that we have send and receive working, you’d think that we’d be able to connect to an access point and start sending packets, right? Well, not entirely: since this is such a big project, we only implemented the bare minimum to proceed in every phase. This is the same approach Ladybird takes to build a novel browser:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If you tried to build a browser one spec at a time, or even one feature at a time, you’d most likely run out of steam and lose interest altogether.
So instead of that, we tend to focus on building “vertical slices” of functionality. This means setting practical, cross-cutting goals, such as “let’s get twitter.com/awesomekling to load”, “let’s get login working on discord.com”, and other similar objectives.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This approach is very motivating, but sometimes bites you in the ass when you have to figure out why something is not working.&lt;/p&gt;

&lt;h2 id="step-1-using-scapy"&gt;Step 1: using Scapy&lt;/h2&gt;

&lt;p&gt;Before we start with the undertaking of connecting the ESP32 to an access point, we’ll first start by implementing connecting a regular USB Wi-Fi dongle to an access point by constructing and sending the packets ourselves to make sure we understand everything that’s needed; and so we’ll have a known-working reference implementation. We found &lt;a href="https://wlan1nde.wordpress.com/2016/08/24/fake-a-wlan-connection-via-scapy/"&gt;this blog post&lt;/a&gt; about using Scapy, a Python packet manipulator library, for connecting to an open access point. We need 4 packets to set up the connection:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Authentication, from client to AP&lt;/li&gt;
  &lt;li&gt;Authentication, from AP to client&lt;/li&gt;
  &lt;li&gt;Association request, from client to AP&lt;/li&gt;
  &lt;li&gt;Association response, from AP to client&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After that, if everything has gone well, we can send data frames from the client to the access point and they’ll get accepted. We extended the blog post code a bit to also send data frames at the end of the connection setup, and verified that everything was working. For the data frames, we used UDP packets, because we can just construct the packet once, and then keep sending it; UDP is stateless, unlike TCP.&lt;/p&gt;

&lt;h2 id="step-2-using-the-esp32"&gt;Step 2: using the ESP32&lt;/h2&gt;

&lt;p&gt;We implemented this on the ESP32, by copying the packets from Scapy and hardcoding the packet contents in the C source code. To make sure we could discern the ESP32 from the scapy implementation, we replace the MAC address of the adapter we use for testing with an arbitrary MAC address (&lt;code&gt;01:23:45:67:89:ab&lt;/code&gt;). When we then sent the packets, we saw that we received an ACK frame in response to our authentication, but we didn’t receive an authentication answer back from the AP. Even stranger, the ACK was towards a different MAC address: &lt;code&gt;00:23:45:67:89:ab&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Apparently, MAC addresses aren’t just 6 arbitrary bytes with the first 3 bytes being vendor specific: the last bit of the first byte indicates if the packet is unicast or multicast. By using the &lt;code&gt;01:...&lt;/code&gt; MAC address, we had sent multicast packets instead of unicast packets.&lt;/p&gt;

&lt;p&gt;After fixing this by using a different MAC address, we started to receive frames back from the access point. Because we didn’t implement sending ACKs back, we received every frame from the access point 4 times: since the access point didn’t receive any ACKs back, it would assume the packet was not received correctly. At that point, that wasn’t a problem: the AP would happily proceed with association request and response.&lt;/p&gt;

&lt;p&gt;However, when we started to send data packets, we’d immediately started to receive disassociation frames from the AP as a reply to our data packets. The only difference between the (working) Scapy implementation and the current ESP32 implementation, was not sending ACKs back; so I guess implementing that is necessary after all.&lt;/p&gt;

&lt;p&gt;Sending ACK frames back in software is not as easy as it seems though: the ACK frame needs to be sent exactly one SIFS (Short Interframe Space) time period after the last symbol of the received frame. For 802.11b, such a SIFS is only 10 microseconds; the round-trip-time through the hardware and software is already more than 10 us, so we can’t implement this in software. The proprietary network stack does send ACK frames back, so this must be implemented somehow. And indeed, sending ACKs is implemented in hardware: by writing to a memory-mapped IO address, you can configure a MAC address for which the hardware will automatically send back an ACK.&lt;/p&gt;

&lt;p&gt;After also implementing this, we received our first packets on the computer that had netcat listening for UDP packets 🎉&lt;/p&gt;

&lt;figure class="image "&gt;
  &lt;a href="https://pics.zeus.gent/kLBUMHjakrlOWduaxWCEysk2FeDW3uAnKRNgcSuv.jpg"&gt;
    &lt;img src="https://pics.zeus.gent/kLBUMHjakrlOWduaxWCEysk2FeDW3uAnKRNgcSuv.jpg" alt="First succesfully received data packets sent by ESP32" /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;First succesfully received data packets sent by ESP32&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Since we now implement the interrupt ourselves, we can send and receive frames, without any proprietary code &lt;em&gt;running&lt;/em&gt; (proprietary code is still used to initialize the hardware in the begin, but is not needed anymore after that).&lt;/p&gt;

&lt;p&gt;The current way of hardcoding the contents of packets was appropriate for the proof-of-concept showing that we can connect to an AP and send packets, but is not useable for our eventual goal. We’re searching for an open source implementation that handles the higher level functionality of the 802.11 MAC layer (constructing and parsing packets, knowing what packets to send when, …). For the higher layers, we can use the existing lwIP TCP/IP-stack on the ESP32.&lt;/p&gt;

&lt;p&gt;All code is available on the &lt;a href="https://github.com/esp32-open-mac/"&gt;esp32-open-mac GitHub organisation&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id="roadmap"&gt;Roadmap&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;☑ Send packets&lt;/li&gt;
  &lt;li&gt;☑ Receive packets&lt;/li&gt;
  &lt;li&gt;☑ Send ACK (acknowledgment) packets back if we receive a packet that is destined for us&lt;/li&gt;
  &lt;li&gt;☑ Implement hardware filtering based on MAC address so we don’t receive as much packets&lt;/li&gt;
  &lt;li&gt;☐ Find or build an open source 802.11 MAC implementation to construct the packets we want to send. The Linux kernel has mac80211, but including the full Linux kernel does not seem to be feasible. This is not ESP32-specific; we’d ideally find an implemenation where you can pass your own TX and RX functions, and they do the rest.&lt;/li&gt;
  &lt;li&gt;☐ Implement changing the wifi channel, rate, transmit power, …&lt;/li&gt;
  &lt;li&gt;☐ Implement the hardware initialization (now done by &lt;code&gt;esp_phy_enable()&lt;/code&gt;). This will be a hard undertaking, since all calibration routines will need to be implemented, but also has a high payoff: we’ll then have a completely blob-free firmware for the ESP32.&lt;/li&gt;
  &lt;li&gt;☐ Write SVD documentation for all reverse engineered registers. An SVD file is an XML file that describes the hardware features of a microcontroller, this makes it possible to automatically generate an API from the hardware description. Espressif already has an SVD file containing the documented hardware registers; we can document the undocumented registers and (automatically) merge them in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two hardest (but most important) tasks are implementing hardware initialization, and connecting our sending and receiving primitives to an open source 802.11 MAC stack.&lt;/p&gt;

&lt;h2 id="bonus-charlotte-breaking-everything"&gt;Bonus: Charlotte breaking everything&lt;/h2&gt;

&lt;p&gt;Charlotte playing music completely broke the setup: the music setup at our hackerspace works via RTP (Realtime Transport Protocol). Under the hood, RTP sends UDP packets containing the audio data to a multicast address; so these packets were also transmitted over the Wi-Fi. Because this was a lot of packets per second, the receive buffer was always full, and very few other packets could be received/ACKed. This made it clear that hardware filtering would need to be implemented sooner than later; reverse engineering turned out to be not as much work as expected.&lt;/p&gt;

&lt;p&gt;The hardware filtering seems to have two ‘slots’, for every slot you can filter on a destination MAC address and on a BSSID (not sure if you can do both in each slot or you have to choose). By default, the hardware will not let any packets through. The hardware will only send an ACK frame back if the packet was let through via one of the filters and was copied into an RX DMA buffer: packets that were copied into an RX DMA buffer because of promiscuous mode will not result in an ACK frame getting sent.&lt;/p&gt;

&lt;h2 id="questions-want-to-collaborate"&gt;Questions? Want to collaborate?&lt;/h2&gt;

&lt;p&gt;This is a sizeable project that could definitely use multiple contributors; I’d really like to collaborate with other people to create a fully functional, open-source Wi-Fi stack for the ESP32. If this sounds like something you’d like to work on, contact me via &lt;a class="email" href="mailto:%7a%65%75%73%62%6c%6f%67%40%64%65%76%72%65%6b%65%72%2e%62&amp;#101;"&gt;zeusblog@&lt;span&gt;not&lt;/span&gt;devreker.be&lt;/a&gt;, maybe we can have a weekly hacking session?&lt;/p&gt;

&lt;p&gt;As far as I know, this is the first undertaking to build an open source 802.11 MAC for an affordable microcontroller. If you want to financially support this project, you can wire money via &lt;a href="https://zeus.ugent.be/contact/#payment-info"&gt;https://zeus.ugent.be/contact/#payment-info&lt;/a&gt;, please put “ESP32” in the transaction description, so our treasurer knows what the money is for. Please do not donate if you’re a student or if you’re not financially independent. If you’re a company and would like to donate hardware (for example, a faraday cage or measuring equipment that might be useful), please contact me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nlnet.nl/project/ESP32-opendrivers/"&gt;This project&lt;/a&gt; was funded through the &lt;a href="https://nlnet.nl/core/"&gt;NGI0 Core Fund&lt;/a&gt;, a fund established by NLnet with financial support from the European Commission’s Next Generation Internet programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 101092990.&lt;/p&gt;

&lt;p&gt;Feel free to send me an email in case you have questions, you think something in this blog post could be worded better or you spotted a mistake.&lt;/p&gt;

</content>
  </entry>
</feed>

