Yannik Richter
Published

COVID-19 Detector and Transmitter Bracelet (DeTra)

The bracelet detects symptoms of COVID-19/ SARS-CoV-2. It can wirelessly transmit the data to help centers or warn the person automatically

AdvancedFull instructions provided1,159
COVID-19 Detector and Transmitter Bracelet (DeTra)

Things used in this project

Hardware components

Heltec LoRa 32 (V2)
×1
Pimoroni MAX30105
×1
CJMCU-680 BME680
×1
GY-bme280
×1
Lithium Akku 3,7 V 1000 mAh
×1

Software apps and online services

Fusion
Autodesk Fusion
Arduino IDE
Arduino IDE
Node-RED
Node-RED
The Things Stack
The Things Industries The Things Stack

Story

Read more

Custom parts and enclosures

DeTra Housing

Fusion 360 3D-CAD-Model

Schematics

Schematic

Code

Node-Red

JSON
Just Import this code into Node-Red.
You only need to modify the MQTT Node.
[{"id":"748cf809.83aac8","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"8611c3a0.a6814","type":"mqtt in","z":"748cf809.83aac8","name":"","topic":"vitalora/devices/vitalora_1/up","qos":"2","datatype":"auto","broker":"be8a9aec.3a9d48","x":140,"y":560,"wires":[["f11c895a.d01d28"]]},{"id":"65c9d92f.436df8","type":"ui_chart","z":"748cf809.83aac8","name":"","group":"dc169156.6fcaa","order":1,"width":0,"height":0,"label":"History","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"200","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1060,"y":160,"wires":[[]]},{"id":"81cf9cc4.ae1fd","type":"ui_chart","z":"748cf809.83aac8","name":"","group":"ed7f61c0.5d054","order":1,"width":0,"height":0,"label":"History","chartType":"line","legend":"false","xformat":"HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"42","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1060,"y":760,"wires":[[]]},{"id":"da325a7c.8ea908","type":"ui_chart","z":"748cf809.83aac8","name":"","group":"7a5de841.154f28","order":1,"width":0,"height":0,"label":"History","chartType":"line","legend":"false","xformat":"HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"100","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1060,"y":580,"wires":[[]]},{"id":"46d2e63b.1747a8","type":"ui_gauge","z":"748cf809.83aac8","name":"","group":"7a5de841.154f28","order":2,"width":0,"height":0,"gtype":"gage","title":"Current Value","label":"%","format":"{{msg.payload}}","min":"0","max":"100","colors":["#b30300","#e6e600","#7bcb3a"],"seg1":"97","seg2":"80","x":1080,"y":540,"wires":[]},{"id":"6c26c7e2.2044b8","type":"ui_gauge","z":"748cf809.83aac8","name":"","group":"ed7f61c0.5d054","order":2,"width":0,"height":0,"gtype":"gage","title":"Current Value","label":"C","format":"{{msg.payload}}","min":"0","max":"42","colors":["#00b500","#e6e600","#ca3838"],"seg1":"37.5","seg2":"38.5","x":1080,"y":800,"wires":[]},{"id":"17b43a7b.686946","type":"ui_text","z":"748cf809.83aac8","group":"ad387074.882c4","order":2,"width":8,"height":5,"name":"","label":"Detection","format":"{{msg.payload}}","layout":"col-center","x":1040,"y":1120,"wires":[]},{"id":"aa4fcdf2.77102","type":"ui_gauge","z":"748cf809.83aac8","name":"","group":"dc169156.6fcaa","order":2,"width":0,"height":0,"gtype":"gage","title":"Current Value","label":"bbm","format":"{{msg.payload}}","min":0,"max":"200","colors":["#00b500","#e6e600","#ca3838"],"seg1":"80","seg2":"100","x":1080,"y":220,"wires":[]},{"id":"f11c895a.d01d28","type":"json","z":"748cf809.83aac8","name":"","property":"payload","action":"","pretty":false,"x":350,"y":560,"wires":[["6226c473.8933cc","ff0ec6d8.6a7618","41e8fbba.3f8674","b6c20810.921218","df7f480b.ecc6e8","5061a42d.e3079c"]]},{"id":"6226c473.8933cc","type":"function","z":"748cf809.83aac8","name":"Heartbeat","func":"msg.payload=msg.payload.payload_fields.HeartBeat;\nreturn msg;","outputs":1,"noerr":0,"x":540,"y":180,"wires":[["65c9d92f.436df8","5dc87b66.364834","aa4fcdf2.77102"]]},{"id":"ff0ec6d8.6a7618","type":"function","z":"748cf809.83aac8","name":"SpO2","func":"msg.payload=msg.payload.payload_fields.Spo2;\nreturn msg;","outputs":1,"noerr":0,"x":590,"y":560,"wires":[["da325a7c.8ea908","b36463fe.cf3e6","46d2e63b.1747a8"]]},{"id":"41e8fbba.3f8674","type":"function","z":"748cf809.83aac8","name":"Body Temperature","func":"msg.payload=msg.payload.payload_fields.Body_Temperature;\nreturn msg;","outputs":1,"noerr":0,"x":630,"y":740,"wires":[["81cf9cc4.ae1fd","8eb766fe.c3a8c8","6c26c7e2.2044b8"]]},{"id":"b6c20810.921218","type":"function","z":"748cf809.83aac8","name":"Acetone","func":"msg.payload=msg.payload.payload_fields.Aceton;\nreturn msg;","outputs":1,"noerr":0,"x":590,"y":1020,"wires":[["c6dc74a3.605028","b0874ebc.51804","491bbb9.67c8044"]]},{"id":"1e1f9e4a.7d1822","type":"ui_text","z":"748cf809.83aac8","group":"dc169156.6fcaa","order":3,"width":0,"height":0,"name":"","label":"Heartbeat Max","format":"{{msg.payload.max + 'bpm'}}","layout":"col-center","x":1080,"y":80,"wires":[]},{"id":"d9609897.055db8","type":"ui_text","z":"748cf809.83aac8","group":"dc169156.6fcaa","order":4,"width":0,"height":0,"name":"","label":"Heartbeat Min","format":"{{msg.payload.min + 'bpm'}}","layout":"col-center","x":1080,"y":120,"wires":[]},{"id":"56dfe723.ea24f8","type":"ui_text","z":"748cf809.83aac8","group":"dc169156.6fcaa","order":5,"width":0,"height":0,"name":"","label":"Heartbeat Average","format":"{{msg.payload.Heartbeataverage + 'bpm'}}","layout":"col-center","x":1090,"y":40,"wires":[]},{"id":"9a83c62a.df78d8","type":"ui_text","z":"748cf809.83aac8","group":"7a5de841.154f28","order":3,"width":0,"height":0,"name":"","label":" SpO2 Max","format":"{{msg.payload.max +'%'}}","layout":"col-center","x":1070,"y":420,"wires":[]},{"id":"946c38b.2bc24c8","type":"ui_text","z":"748cf809.83aac8","group":"7a5de841.154f28","order":4,"width":0,"height":0,"name":"","label":" SpO2 Min","format":"{{msg.payload.min +'%'}}","layout":"col-center","x":1060,"y":460,"wires":[]},{"id":"f376bec1.746e4","type":"ui_text","z":"748cf809.83aac8","group":"7a5de841.154f28","order":5,"width":0,"height":0,"name":"","label":"SpO2 Average","format":"{{msg.payload.Spo2average +'%'}}","layout":"col-center","x":1080,"y":500,"wires":[]},{"id":"d5267a5e.57cde8","type":"ui_text","z":"748cf809.83aac8","group":"ed7f61c0.5d054","order":3,"width":0,"height":0,"name":"","label":"Body Temperature Max","format":"{{msg.payload.max +'C'}}","layout":"col-center","x":1110,"y":640,"wires":[]},{"id":"5790f0e4.28bdd","type":"ui_text","z":"748cf809.83aac8","group":"ed7f61c0.5d054","order":4,"width":0,"height":0,"name":"","label":"Body Temperature Min","format":"{{msg.payload.min +'C'}}","layout":"col-center","x":1100,"y":680,"wires":[]},{"id":"b892f474.8fec08","type":"ui_text","z":"748cf809.83aac8","group":"ed7f61c0.5d054","order":5,"width":0,"height":0,"name":"","label":"Body Temperature Average","format":"{{msg.payload.BodyTempaverage +'C'}}","layout":"col-center","x":1120,"y":720,"wires":[]},{"id":"5dc87b66.364834","type":"function","z":"748cf809.83aac8","name":"Heartbeat (average,min,max)","func":"var data=context.get('data') || {};\nvar data2=context.get('data2') || {};\nvar period=86400000; //86400000ms = 1 day\nvar d=new Date();\nnow=d.getTime();\n\nif (data2[\"count\"]===undefined)\n{\n    data2[\"count\"]=0;\n}\n\nif (data2[\"max\"]===undefined)\n     data2[\"max\"]=msg.payload;\n if (data2[\"min\"]===undefined)\n     data2[\"min\"]=msg.payload;\n    if (data2[\"averageraw\"]===undefined)\n     data2[\"averageraw\"]=0 ;\n if (data2[\"stime\"]===undefined)  //start time\n     data2[\"stime\"]=now;\n \nvar count=data2.count;\ndata2.count+=1;\n\nif(count !==0)\n    {data.averageraw=((data.averageraw)+msg.payload);\n    data2.averageraw= data.averageraw/(count+1);\n    data2.Heartbeataverage = Math.round(data2.averageraw*100)/100;\n    }\nelse \n    data.averageraw=msg.payload;\n    \n//node.log(data2.count+\"  \"+data2.average);\n\nif(msg.payload>data2.max)\n    data2.max=msg.payload;\nif(msg.payload<data2.min)\n    data2.min=msg.payload;\nif ((data2[\"stime\"]+period*1000)<now)  // if times up\n{\n    \n    data = {};\n    context.set('data',data);\n    \n    data2 = {};\n    context.set('data2',data2);\n   \n   return null;\n    \n    \n}\ncontext.set('data',data); // if times not up\ncontext.set('data2',data2);\nvar out =JSON.stringify(data2);\nmsg.payload =out;\nreturn msg;\n","outputs":1,"noerr":0,"x":580,"y":120,"wires":[["11ed6fea.5d371"]]},{"id":"11ed6fea.5d371","type":"json","z":"748cf809.83aac8","name":"","property":"payload","action":"","pretty":false,"x":790,"y":120,"wires":[["56dfe723.ea24f8","d9609897.055db8","1e1f9e4a.7d1822"]]},{"id":"b36463fe.cf3e6","type":"function","z":"748cf809.83aac8","name":"SpO2(average,min,max)","func":"var data=context.get('data') || {};\nvar data2=context.get('data2') || {};\nvar period=86400000; //86400000ms = 1 day\nvar d=new Date();\nnow=d.getTime();\n\nif (data2[\"count\"]===undefined)\n{\n    data2[\"count\"]=0;\n}\n\nif (data2[\"max\"]===undefined)\n     data2[\"max\"]=msg.payload;\n if (data2[\"min\"]===undefined)\n     data2[\"min\"]=msg.payload;\n    if (data2[\"averageraw\"]===undefined)\n     data2[\"averageraw\"]=0 ;\n if (data2[\"stime\"]===undefined)  //start time\n     data2[\"stime\"]=now;\n \nvar count=data2.count;\ndata2.count+=1;\n\nif(count !==0)\n    {data.averageraw=((data.averageraw)+msg.payload);\n    data2.averageraw= data.averageraw/(count+1);\n    data2.Spo2average = Math.round(data2.averageraw*100)/100;\n    }\nelse \n    data.averageraw=msg.payload;\n    \n//node.log(data2.count+\"  \"+data2.average);\n\nif(msg.payload>data2.max)\n    data2.max=msg.payload;\nif(msg.payload<data2.min)\n    data2.min=msg.payload;\nif ((data2[\"stime\"]+period*1000)<now)  // if times up\n{\n    \n    data = {};\n    context.set('data',data);\n    \n    data2 = {};\n    context.set('data2',data2);\n   \n   return null;\n    \n    \n}\ncontext.set('data',data); // if times not up\ncontext.set('data2',data2);\nvar out =JSON.stringify(data2);\nmsg.payload =out;\nreturn msg;\n","outputs":1,"noerr":0,"x":650,"y":460,"wires":[["42fa4dc1.a32724"]]},{"id":"42fa4dc1.a32724","type":"json","z":"748cf809.83aac8","name":"","property":"payload","action":"","pretty":false,"x":790,"y":520,"wires":[["9a83c62a.df78d8","946c38b.2bc24c8","f376bec1.746e4"]]},{"id":"8eb766fe.c3a8c8","type":"function","z":"748cf809.83aac8","name":"Body Temperature(average,min,max)","func":"var data=context.get('data') || {};\nvar data2=context.get('data2') || {};\nvar period=86400000; //86400000ms = 1 day\nvar d=new Date();\nnow=d.getTime();\n\nif (data2[\"count\"]===undefined)\n{\n    data2[\"count\"]=0;\n}\n\nif (data2[\"max\"]===undefined)\n     data2[\"max\"]=msg.payload;\n if (data2[\"min\"]===undefined)\n     data2[\"min\"]=msg.payload;\n    if (data2[\"averageraw\"]===undefined)\n     data2[\"averageraw\"]=0 ;\n if (data2[\"stime\"]===undefined)  //start time\n     data2[\"stime\"]=now;\n \nvar count=data2.count;\ndata2.count+=1;\n\nif(count !==0)\n    {data.averageraw=((data.averageraw)+msg.payload);\n    data2.averageraw= data.averageraw/(count+1);\n    data2.BodyTempaverage = Math.round(data2.averageraw*100)/100;\n    }\nelse \n    data.averageraw=msg.payload;\n    \n//node.log(data2.count+\"  \"+data2.average);\n\nif(msg.payload>data2.max)\n    data2.max=msg.payload;\nif(msg.payload<data2.min)\n    data2.min=msg.payload;\nif ((data2[\"stime\"]+period*1000)<now)  // if times up\n{\n    \n    data = {};\n    context.set('data',data);\n    \n    data2 = {};\n    context.set('data2',data2);\n   \n   return null;\n    \n    \n}\ncontext.set('data',data); // if times not up\ncontext.set('data2',data2);\nvar out =JSON.stringify(data2);\nmsg.payload =out;\nreturn msg;\n","outputs":1,"noerr":0,"x":690,"y":640,"wires":[["19f8cfac.510c5"]]},{"id":"19f8cfac.510c5","type":"json","z":"748cf809.83aac8","name":"","property":"payload","action":"","pretty":false,"x":810,"y":700,"wires":[["d5267a5e.57cde8","5790f0e4.28bdd","b892f474.8fec08"]]},{"id":"c6dc74a3.605028","type":"function","z":"748cf809.83aac8","name":"Acetone (average,min,max)","func":"\nvar data2=context.get('data2') || {};\nvar period=86400000; //86400000ms = 1 day\nvar d=new Date();\nnow=d.getTime();\n\nif (data2[\"count\"]===undefined)\n{\n    data2[\"count\"]=0;\n}\nif (data2[\"count1\"]===undefined)\n{\n    data2[\"count1\"]=0;\n}\nif (data2[\"count2\"]===undefined)\n{\n    data2[\"count2\"]=0;\n}\n\nif (data2[\"Detected\"]===undefined)\n     data2[\"Detected\"]=msg.payload;\n if (data2[\"not Detected\"]===undefined)\n     data2[\"not Detected\"]=msg.payload;\n    if (data2[\"average\"]===undefined)\n     data2[\"average\"]=0 ;\n if (data2[\"stime\"]===undefined)  //start time\n     data2[\"stime\"]=now;\n \nvar count=data2.count;\nvar count1=data2.count1;\nvar count2=data2.count2;\ndata2.count2+=1;\n\n\n    \n//node.log(data2.count+\"  \"+data2.average);\n\nif(msg.payload=== 1)\n    data2.count+=1;\nif(msg.payload===0)\n    {data2.count1+=1;}\n    \n\n    \nif (count2 !== 0)\n{\n    data2.averageAD=data2.count/data2.count2;\n    data2.averageAND=data2.count1/data2.count2;\n    if(data2.averageAD > data2.averageAND)\n    data2.average='Aceton detected';\n    \nif(data2.averageAND>data2.averageAD)\n    data2.average='Aceton not detected'; \nif(data2.averageAND===data2.averageAD)\n    {data2.average='Balanced';\n    } }\nelse \ndata2.average=msg.payload;\n    \nif ((data2[\"stime\"]+period*1000)<now)  // if times up\n{\n    \n    \n    \n    data2 = {};\n    context.set('data2',data2);\n   \n   return null;\n    \n    \n}\n\ncontext.set('data2',data2);\nvar out =JSON.stringify(data2);\nmsg.payload =out;\nreturn msg;\n","outputs":1,"noerr":0,"x":680,"y":900,"wires":[["d0bad7a1.35c5a8"]]},{"id":"d150a41d.1790b8","type":"ui_text","z":"748cf809.83aac8","group":"ad387074.882c4","order":3,"width":0,"height":0,"name":"","label":"Acetone Detected","format":"{{msg.payload.count}}","layout":"col-center","x":1080,"y":900,"wires":[]},{"id":"3e057a53.d53cb6","type":"ui_text","z":"748cf809.83aac8","group":"ad387074.882c4","order":4,"width":0,"height":0,"name":"","label":"Acetone not Detected","format":"{{msg.payload.count1}}","layout":"col-center","x":1080,"y":940,"wires":[]},{"id":"3e3ceda3.944ba2","type":"ui_text","z":"748cf809.83aac8","group":"ad387074.882c4","order":5,"width":0,"height":0,"name":"","label":"Acetone Average","format":"{{msg.payload.average}}","layout":"col-center","x":1070,"y":980,"wires":[]},{"id":"d0bad7a1.35c5a8","type":"json","z":"748cf809.83aac8","name":"","property":"payload","action":"","pretty":false,"x":730,"y":960,"wires":[["d150a41d.1790b8","3e057a53.d53cb6","3e3ceda3.944ba2"]]},{"id":"b0874ebc.51804","type":"ui_chart","z":"748cf809.83aac8","name":"","group":"ad387074.882c4","order":1,"width":0,"height":0,"label":"History","chartType":"line","legend":"false","xformat":"HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"1","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1020,"y":1060,"wires":[[]]},{"id":"2793c890.352f78","type":"inject","z":"748cf809.83aac8","name":"","topic":"Reset 24 hours","payload":"0","payloadType":"num","repeat":"86400","crontab":"","once":false,"onceDelay":0.1,"x":570,"y":320,"wires":[["308cc1ff.381c0e","e82b3afe.97c358","de08bdd4.4db27"]]},{"id":"e82b3afe.97c358","type":"function","z":"748cf809.83aac8","name":"Reset Chart","func":"msg.payload=[];\nreturn msg;","outputs":1,"noerr":0,"x":750,"y":320,"wires":[["65c9d92f.436df8","da325a7c.8ea908","81cf9cc4.ae1fd","b0874ebc.51804"]]},{"id":"308cc1ff.381c0e","type":"function","z":"748cf809.83aac8","name":"Reset Gauge","func":"msg.payload = 0;\n\nreturn msg;","outputs":1,"noerr":0,"x":750,"y":280,"wires":[["46d2e63b.1747a8","aa4fcdf2.77102","6c26c7e2.2044b8"]]},{"id":"491bbb9.67c8044","type":"function","z":"748cf809.83aac8","name":"Acetone Decoder","func":"\nif(msg.payload===1)\nmsg.payload='Acetone Detected'\nelse \nif (msg.payload===0)\nmsg.payload='Acetone not Detected'\nreturn msg;","outputs":1,"noerr":0,"x":630,"y":1120,"wires":[["17b43a7b.686946"]]},{"id":"de08bdd4.4db27","type":"function","z":"748cf809.83aac8","name":"Reset Text","func":"msg.payload= ' ';\nreturn msg;","outputs":1,"noerr":0,"x":750,"y":360,"wires":[["17b43a7b.686946"]]},{"id":"df7f480b.ecc6e8","type":"debug","z":"748cf809.83aac8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":290,"y":780,"wires":[]},{"id":"5061a42d.e3079c","type":"function","z":"748cf809.83aac8","name":"Covid-19","func":"msg.payload=msg.payload.payload_fields.COVID;\nif (msg.payload===1)\n{msg.payload = 'Possible COVID-19 Infection';}\nreturn msg;","outputs":1,"noerr":0,"x":560,"y":1200,"wires":[["3749203e.ad2d3"]]},{"id":"3749203e.ad2d3","type":"ui_toast","z":"748cf809.83aac8","position":"top right","displayTime":"86400000","highlight":"","sendall":true,"outputs":0,"ok":"OK","cancel":"","raw":false,"topic":"","name":"","x":1040,"y":1200,"wires":[]},{"id":"be8a9aec.3a9d48","type":"mqtt-broker","z":"","name":"Vitalora","broker":"eu.thethings.network","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"dc169156.6fcaa","type":"ui_group","z":"","name":"Heartbeat","tab":"ab8a2d9e.013cc","order":1,"disp":true,"width":"8","collapse":false},{"id":"ed7f61c0.5d054","type":"ui_group","z":"","name":"Body Temperature","tab":"ab8a2d9e.013cc","order":3,"disp":true,"width":"8","collapse":false},{"id":"7a5de841.154f28","type":"ui_group","z":"","name":"Spo2","tab":"ab8a2d9e.013cc","order":2,"disp":true,"width":"8","collapse":false},{"id":"ad387074.882c4","type":"ui_group","z":"","name":"Acetone","tab":"ab8a2d9e.013cc","order":4,"disp":true,"width":"8","collapse":false},{"id":"ab8a2d9e.013cc","type":"ui_tab","z":"","name":"Diary","icon":"dashboard","order":3,"disabled":false,"hidden":false}]

The Things Network Decoder

C/C++
Insert this code in the payload formats to decode the sent data.
function Decoder(bytes, port) {
  // Decode an uplink message from a buffer
  // (array) of bytes to an object of fields.
  var decoded = {};

    if( bytes[1] == 1)
    {decoded.HeartBeat = bytes[0]; }
    else if (bytes[1] == 2)
    {decoded.Body_Temperature = (bytes[0] + (bytes[2] / 100)); }
    else if (bytes[1] == 3)
    {decoded.Spo2 = bytes[0];}
    else if (bytes[1] == 4)
    {decoded.Aceton = bytes[0];}
    else if (bytes[1] == 5)
    {decoded.HeartBeat = bytes[0];
    decoded.Spo2 = bytes[2];
    decoded.Body_Temperature = (bytes[3] + (bytes[4] / 100));
    }
    else if (bytes[1] == 6)
    {decoded.COVID = bytes[0];
    
    }

  return decoded;
}

DeTra

Code and libraries

Credits

Yannik Richter

Yannik Richter

1 project • 5 followers
Studying Mechatronic System Development at Pforzheim University

Comments