| @@ -1,320 +0,0 @@ | |||||
| <mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/28.2.5 Chrome/138.0.7204.251 Electron/37.6.1 Safari/537.36" version="28.2.5"> | |||||
| <diagram name="Pagina-1" id="Fpn7J8KQqI0Mq3TqwKSc"> | |||||
| <mxGraphModel dx="1028" dy="611" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0"> | |||||
| <root> | |||||
| <mxCell id="0" /> | |||||
| <mxCell id="1" parent="0" /> | |||||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-9" value="localization algorithm" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||||
| <mxGeometry x="149" y="334" width="70" height="70" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-11" value="API Call From API Server Logic" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="302" y="439" width="190" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-1" value="API Server<br>(FastAPI)<div><br></div><div><br><br><br>+<br><br>Logic Connector</div>" style="whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||||
| <mxGeometry x="613" y="330" width="204" height="204" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-2" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.02;exitY=0.704;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;exitPerimeter=0;startArrow=none;startFill=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-18" target="krLT08NrNyH7hn3MfuL1-19" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="680" y="240" as="sourcePoint" /> | |||||
| <mxPoint x="680" y="40" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-59" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-2" vertex="1" connectable="0"> | |||||
| <mxGeometry x="0.1209" y="1" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-3" value="BackEnd" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="508" y="770" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-4" value="FrontEnd" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="975" y="770" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-5" value="" style="endArrow=none;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0;exitY=0;exitDx=50;exitDy=0;exitPerimeter=0;endFill=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-24" target="krLT08NrNyH7hn3MfuL1-1" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="680" y="700" as="sourcePoint" /> | |||||
| <mxPoint x="585" y="595" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-7" value="WebGui<br>Config" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> | |||||
| <mxGeometry x="950" y="312" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-8" value="WebGui<br>Status" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> | |||||
| <mxGeometry x="950" y="399" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-9" value="WebGui<br>Alarm" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> | |||||
| <mxGeometry x="950" y="585" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-10" value="WebGui<br>QueryLog" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> | |||||
| <mxGeometry x="950" y="677" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-11" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-1" target="krLT08NrNyH7hn3MfuL1-7" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="750" y="160" as="sourcePoint" /> | |||||
| <mxPoint x="800" y="110" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-12" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-1" target="krLT08NrNyH7hn3MfuL1-8" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="840" y="428" as="sourcePoint" /> | |||||
| <mxPoint x="1060" y="280" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-13" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-1" target="krLT08NrNyH7hn3MfuL1-9" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="840" y="395" as="sourcePoint" /> | |||||
| <mxPoint x="1060" y="360" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-14" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-1" target="krLT08NrNyH7hn3MfuL1-10" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="820" y="450" as="sourcePoint" /> | |||||
| <mxPoint x="1020" y="531" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-15" value="WebGui<br>QueryStatus" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> | |||||
| <mxGeometry x="950" y="487" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-16" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-1" target="krLT08NrNyH7hn3MfuL1-15" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="850" y="309" as="sourcePoint" /> | |||||
| <mxPoint x="1050" y="300" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-17" value="Operator" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="1114" y="560" width="30" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-18" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||||
| <mxGeometry x="1012" y="60" width="50" height="50" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-19" value="Gateway&nbsp;" style="triangle;whiteSpace=wrap;html=1;direction=west;" parent="1" vertex="1"> | |||||
| <mxGeometry x="644" y="79" width="90" height="80" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-20" value="MQTT<br>Broker" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;" parent="1" vertex="1"> | |||||
| <mxGeometry x="86" y="79" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-21" value="LOG" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" parent="1" vertex="1"> | |||||
| <mxGeometry x="115" y="613" width="60" height="80" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-23" value="<br>Tracker<br>LOCALIZATION<br>(state)" style="shape=internalStorage;whiteSpace=wrap;html=1;backgroundOutline=1;" parent="1" vertex="1"> | |||||
| <mxGeometry x="318" y="240" width="141" height="80" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-24" value="USER<br>CONFIG" style="shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;darkOpacity=0.05;darkOpacity2=0.1;" parent="1" vertex="1"> | |||||
| <mxGeometry x="665" y="676" width="120" height="80" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-25" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||||
| <mxGeometry x="1012" y="120" width="50" height="50" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-26" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||||
| <mxGeometry x="1012" y="180" width="50" height="50" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-27" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||||
| <mxGeometry x="1012" y="240" width="50" height="50" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-28" value="Gateway&nbsp;" style="triangle;whiteSpace=wrap;html=1;direction=west;" parent="1" vertex="1"> | |||||
| <mxGeometry x="650" y="193" width="87" height="80" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-29" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-25" target="krLT08NrNyH7hn3MfuL1-19" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="990" y="150" as="sourcePoint" /> | |||||
| <mxPoint x="170" y="120" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-60" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-29" vertex="1" connectable="0"> | |||||
| <mxGeometry x="0.038" y="3" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-30" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-26" target="krLT08NrNyH7hn3MfuL1-28" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="140" y="160" as="sourcePoint" /> | |||||
| <mxPoint x="220" y="165" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-61" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-30" vertex="1" connectable="0"> | |||||
| <mxGeometry x="0.0103" y="2" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-31" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-27" target="krLT08NrNyH7hn3MfuL1-28" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="130" y="180" as="sourcePoint" /> | |||||
| <mxPoint x="773" y="220" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-62" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-31" vertex="1" connectable="0"> | |||||
| <mxGeometry x="0.0203" y="-2" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-32" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1.017;entryY=0.38;entryDx=0;entryDy=0;entryPerimeter=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-19" target="krLT08NrNyH7hn3MfuL1-20" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="210" y="160" as="sourcePoint" /> | |||||
| <mxPoint x="290" y="165" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-63" value="IP MQTT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-32" vertex="1" connectable="0"> | |||||
| <mxGeometry x="-0.2888" y="-1" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-33" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-28" target="krLT08NrNyH7hn3MfuL1-20" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="280" y="190" as="sourcePoint" /> | |||||
| <mxPoint x="370" y="240" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-64" value="IP MQTT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-33" vertex="1" connectable="0"> | |||||
| <mxGeometry x="-0.2558" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-34" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" target="krLT08NrNyH7hn3MfuL1-20" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="400" y="281" as="sourcePoint" /> | |||||
| <mxPoint x="400" y="190" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-36" value="<br>Tracker Sensors:<br>Alarm, Temp, <br>battery level<br>...<br>Heartbest<br>(state)" style="shape=internalStorage;whiteSpace=wrap;html=1;backgroundOutline=1;dx=20;dy=10;" parent="1" vertex="1"> | |||||
| <mxGeometry x="318" y="486" width="140" height="104" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-37" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.358;exitDx=0;exitDy=0;startArrow=none;startFill=0;endFill=1;exitPerimeter=0;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="196.3699999999999" y="466.95000000000005" as="sourcePoint" /> | |||||
| <mxPoint x="610" y="430" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-39" value="" style="endArrow=none;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endFill=0;exitX=0.949;exitY=0.3;exitDx=0;exitDy=0;exitPerimeter=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" target="krLT08NrNyH7hn3MfuL1-23" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="310" y="350" as="sourcePoint" /> | |||||
| <mxPoint x="385" y="640" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-40" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=1;exitDx=0;exitDy=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" target="krLT08NrNyH7hn3MfuL1-36" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="350" y="360" as="sourcePoint" /> | |||||
| <mxPoint x="350" y="460" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-41" value="" style="endArrow=classic;html=1;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryPerimeter=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" target="krLT08NrNyH7hn3MfuL1-21" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="450" y="460" as="sourcePoint" /> | |||||
| <mxPoint x="360" y="560" as="targetPoint" /> | |||||
| <Array as="points" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-46" value="Presense<br>CORE" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||||
| <mxGeometry x="90" y="390" width="110" height="110" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-52" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="1101" y="70" width="30" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-56" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="1102" y="181" width="30" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-69" value="" style="endArrow=none;html=1;rounded=0;" parent="1" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="10" y="10" as="sourcePoint" /> | |||||
| <mxPoint x="1160" y="10" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-71" value="" style="endArrow=none;html=1;rounded=0;" parent="1" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="10" y="810" as="sourcePoint" /> | |||||
| <mxPoint x="1160" y="810" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-72" value="" style="endArrow=none;html=1;rounded=0;" parent="1" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="1160" y="810" as="sourcePoint" /> | |||||
| <mxPoint x="1160" y="10" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-73" value="" style="endArrow=none;html=1;rounded=0;" parent="1" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="10" y="10" as="sourcePoint" /> | |||||
| <mxPoint x="10" y="810" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-74" value="<b><font style="font-size: 27px;">ResLevis DIAGRAM</font></b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="398" y="30" width="370" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="krLT08NrNyH7hn3MfuL1-75" value="<b style=""><font style="font-size: 13px;">Ver 2.0 2025/10/09</font></b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="410" y="49" width="370" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="uKMjvkko2EQDpS27TLVR-1" value="<br>GW <br>and Tracker definition<br>model<br>supported<br>Decode-lib<br>Raw-Data<div><br></div>" style="shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;darkOpacity=0.05;darkOpacity2=0.1;" parent="1" vertex="1"> | |||||
| <mxGeometry x="334" y="613" width="110" height="126" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="uKMjvkko2EQDpS27TLVR-4" value="topic<br>Subscribe<br>publish_out" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="72" y="331" width="76" height="47" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="uKMjvkko2EQDpS27TLVR-5" value="topic<br>Publish<br>publish_out" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="532" y="133" width="76" height="47" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-1" value="" style="endArrow=classic;html=1;rounded=0;endFill=1;exitX=0;exitY=0;exitDx=0;exitDy=53;exitPerimeter=0;entryX=0.687;entryY=0.962;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="uKMjvkko2EQDpS27TLVR-1" target="krLT08NrNyH7hn3MfuL1-46" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="416" y="695" as="sourcePoint" /> | |||||
| <mxPoint x="250" y="620" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-2" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="100" y="560" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-3" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="668" y="591" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-4" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="230" y="560" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-6" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="210" y="290" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-7" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="230" y="478" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-8" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="100" y="273" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-9" value="API Call From CORE" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||||
| <mxGeometry x="316" y="403" width="142" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-10" value="" style="endArrow=none;html=1;rounded=0;exitX=0.964;exitY=0.656;exitDx=0;exitDy=0;startArrow=classic;startFill=1;endFill=0;exitPerimeter=0;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" edge="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="206" y="463.5" as="sourcePoint" /> | |||||
| <mxPoint x="610" y="462" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-2" value="" style="rounded=0;whiteSpace=wrap;html=1;opacity=40;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"> | |||||
| <mxGeometry x="60" y="220" width="460" height="550" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-3" value="<b>Developed by SenLab</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="68" y="734" width="84" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-4" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;opacity=40;" vertex="1" parent="1"> | |||||
| <mxGeometry x="560" y="300" width="280" height="470" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-5" value="<b>Developed by AFA Systems</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="568" y="734" width="84" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-6" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;opacity=40;" vertex="1" parent="1"> | |||||
| <mxGeometry x="870" y="300" width="230" height="470" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-7" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;opacity=40;" vertex="1" parent="1"> | |||||
| <mxGeometry x="560" y="300" width="310" height="100" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-8" value="<b>Developed Maestry&nbsp;</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="867" y="737" width="84" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-11" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;endFill=1;exitX=1;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="CsGlrJDhfrg_2RR0sWjT-9" target="krLT08NrNyH7hn3MfuL1-23"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="350" y="513" as="sourcePoint" /> | |||||
| <mxPoint x="474" y="370" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-13" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="235" y="342.5" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| </root> | |||||
| </mxGraphModel> | |||||
| </diagram> | |||||
| </mxfile> | |||||
| @@ -1,354 +0,0 @@ | |||||
| <mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0" version="28.2.7"> | |||||
| <diagram name="Pagina-1" id="Q54yxviGp2ibMrAPitEd"> | |||||
| <mxGraphModel dx="1426" dy="799" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"> | |||||
| <root> | |||||
| <mxCell id="0" /> | |||||
| <mxCell id="1" parent="0" /> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-2" value="API Call From API Server Logic" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;rotation=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="360" y="460" width="190" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-3" value="API Server<br>(FastAPI)<div><br></div><div><br><br><br>+<br><br>Logic Connector</div>" style="whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||||
| <mxGeometry x="613" y="330" width="204" height="204" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-4" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.02;exitY=0.704;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;exitPerimeter=0;startArrow=none;startFill=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-20" target="9h2_EveRN_1U4OZwnWZC-21"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="680" y="240" as="sourcePoint" /> | |||||
| <mxPoint x="680" y="40" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-5" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-4"> | |||||
| <mxGeometry x="0.1209" y="1" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-6" value="BackEnd" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="508" y="770" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-7" value="FrontEnd" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="975" y="770" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-8" value="" style="endArrow=none;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0;exitY=0;exitDx=50;exitDy=0;exitPerimeter=0;endFill=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-25" target="9h2_EveRN_1U4OZwnWZC-3"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="680" y="700" as="sourcePoint" /> | |||||
| <mxPoint x="585" y="595" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-9" value="WebGui<br>Config" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="950" y="312" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-10" value="WebGui<br>Status" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="950" y="399" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-11" value="WebGui<br>Alarm" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="950" y="585" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-12" value="WebGui<br>QueryLog" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="950" y="677" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-13" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-3" target="9h2_EveRN_1U4OZwnWZC-9"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="750" y="160" as="sourcePoint" /> | |||||
| <mxPoint x="800" y="110" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-14" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-3" target="9h2_EveRN_1U4OZwnWZC-10"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="840" y="428" as="sourcePoint" /> | |||||
| <mxPoint x="1060" y="280" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-15" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-3" target="9h2_EveRN_1U4OZwnWZC-11"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="840" y="395" as="sourcePoint" /> | |||||
| <mxPoint x="1060" y="360" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-16" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-3" target="9h2_EveRN_1U4OZwnWZC-12"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="820" y="450" as="sourcePoint" /> | |||||
| <mxPoint x="1020" y="531" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-17" value="WebGui<br>QueryStatus" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="950" y="487" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-18" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-3" target="9h2_EveRN_1U4OZwnWZC-17"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="850" y="309" as="sourcePoint" /> | |||||
| <mxPoint x="1050" y="300" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-19" value="Operator" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="1114" y="560" width="30" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-20" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||||
| <mxGeometry x="1012" y="60" width="50" height="50" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-21" value="Gateway&nbsp;" style="triangle;whiteSpace=wrap;html=1;direction=west;" vertex="1" parent="1"> | |||||
| <mxGeometry x="644" y="79" width="90" height="80" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-22" value="MQTT<br>Broker" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="62" y="79" width="120" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-23" value="LOG" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" vertex="1" parent="1"> | |||||
| <mxGeometry x="91" y="610" width="60" height="80" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-24" value="<br>Tracker<br>LOCALIZATION<br>(state)" style="shape=internalStorage;whiteSpace=wrap;html=1;backgroundOutline=1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="269" y="241" width="141" height="80" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-25" value="USER<br>CONFIG" style="shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;darkOpacity=0.05;darkOpacity2=0.1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="665" y="676" width="120" height="80" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-26" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||||
| <mxGeometry x="1012" y="120" width="50" height="50" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-27" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||||
| <mxGeometry x="1012" y="180" width="50" height="50" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-28" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||||
| <mxGeometry x="1012" y="240" width="50" height="50" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-29" value="Gateway&nbsp;" style="triangle;whiteSpace=wrap;html=1;direction=west;" vertex="1" parent="1"> | |||||
| <mxGeometry x="650" y="193" width="87" height="80" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-30" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-26" target="9h2_EveRN_1U4OZwnWZC-21"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="990" y="150" as="sourcePoint" /> | |||||
| <mxPoint x="170" y="120" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-31" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-30"> | |||||
| <mxGeometry x="0.038" y="3" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-32" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-27" target="9h2_EveRN_1U4OZwnWZC-29"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="140" y="160" as="sourcePoint" /> | |||||
| <mxPoint x="220" y="165" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-33" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-32"> | |||||
| <mxGeometry x="0.0103" y="2" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-34" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-28" target="9h2_EveRN_1U4OZwnWZC-29"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="130" y="180" as="sourcePoint" /> | |||||
| <mxPoint x="773" y="220" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-35" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-34"> | |||||
| <mxGeometry x="0.0203" y="-2" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-36" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1.017;entryY=0.38;entryDx=0;entryDy=0;entryPerimeter=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-21" target="9h2_EveRN_1U4OZwnWZC-22"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="210" y="160" as="sourcePoint" /> | |||||
| <mxPoint x="290" y="165" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-37" value="IP MQTT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-36"> | |||||
| <mxGeometry x="-0.2888" y="-1" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-38" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-29" target="9h2_EveRN_1U4OZwnWZC-22"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="280" y="190" as="sourcePoint" /> | |||||
| <mxPoint x="370" y="240" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-39" value="IP MQTT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-38"> | |||||
| <mxGeometry x="-0.2558" relative="1" as="geometry"> | |||||
| <mxPoint as="offset" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-40" value="" style="endArrow=classic;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endFill=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-76" target="9h2_EveRN_1U4OZwnWZC-22"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="146" y="260" as="sourcePoint" /> | |||||
| <mxPoint x="400" y="190" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-41" value="<br>Tracker Sensors:<br>Alarm, Temp, <br>battery level<br>...<br>Heartbest<br>(state)" style="shape=internalStorage;whiteSpace=wrap;html=1;backgroundOutline=1;dx=20;dy=10;" vertex="1" parent="1"> | |||||
| <mxGeometry x="420" y="541" width="140" height="104" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-42" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.25;exitDx=0;exitDy=0;startArrow=none;startFill=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-84"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="200" y="429.3800000000001" as="sourcePoint" /> | |||||
| <mxPoint x="610" y="430" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-44" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endFill=1;exitX=1;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-84" target="9h2_EveRN_1U4OZwnWZC-41"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="300" y="500" as="sourcePoint" /> | |||||
| <mxPoint x="350" y="460" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-45" value="" style="endArrow=classic;html=1;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryPerimeter=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-76" target="9h2_EveRN_1U4OZwnWZC-23"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="145" y="500" as="sourcePoint" /> | |||||
| <mxPoint x="360" y="560" as="targetPoint" /> | |||||
| <Array as="points" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-47" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="1101" y="70" width="30" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-48" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="1102" y="181" width="30" height="60" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-49" value="" style="endArrow=none;html=1;rounded=0;" edge="1" parent="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="10" y="10" as="sourcePoint" /> | |||||
| <mxPoint x="1160" y="10" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-50" value="" style="endArrow=none;html=1;rounded=0;" edge="1" parent="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="10" y="810" as="sourcePoint" /> | |||||
| <mxPoint x="1160" y="810" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-51" value="" style="endArrow=none;html=1;rounded=0;" edge="1" parent="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="1160" y="810" as="sourcePoint" /> | |||||
| <mxPoint x="1160" y="10" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-52" value="" style="endArrow=none;html=1;rounded=0;" edge="1" parent="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="10" y="10" as="sourcePoint" /> | |||||
| <mxPoint x="10" y="810" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-53" value="<b><font style="font-size: 27px;">ResLevis DIAGRAM</font></b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="398" y="30" width="370" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-54" value="<b style=""><font style="font-size: 13px;">Ver 2.0 2025/10/09</font></b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="410" y="49" width="370" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-56" value="topic<br>Subscribe<br>publish_out" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="54" y="170" width="76" height="47" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-57" value="topic<br>Publish<br>publish_out" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="532" y="133" width="76" height="47" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-59" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="100" y="560" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-60" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="668" y="591" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-61" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="152" y="498" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-62" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="206" y="312" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-63" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="330" y="530" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-64" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="100" y="273" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-65" value="API Call From CORE" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="384" y="410" width="142" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-66" value="" style="endArrow=none;html=1;rounded=0;exitX=1.017;exitY=0.606;exitDx=0;exitDy=0;startArrow=classic;startFill=1;endFill=0;exitPerimeter=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-84"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="196.03999999999996" y="462.1600000000001" as="sourcePoint" /> | |||||
| <mxPoint x="610" y="462" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-68" value="<b>Developed by SenLab</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="68" y="734" width="84" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-69" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;opacity=40;" vertex="1" parent="1"> | |||||
| <mxGeometry x="560" y="300" width="280" height="470" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-70" value="<b>Developed by AFA Systems</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="568" y="734" width="84" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-71" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;opacity=40;" vertex="1" parent="1"> | |||||
| <mxGeometry x="870" y="300" width="230" height="470" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-72" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;opacity=40;" vertex="1" parent="1"> | |||||
| <mxGeometry x="560" y="300" width="310" height="100" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-73" value="<b>Developed Maestry&nbsp;</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="867" y="737" width="84" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-76" value="Kafka" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="72" y="420" width="98" height="70" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-77" value="Decoder" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="18" y="350" width="68" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-78" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;exitX=0.691;exitY=0.867;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-77" target="9h2_EveRN_1U4OZwnWZC-76"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="50" y="410" as="sourcePoint" /> | |||||
| <mxPoint x="100" y="360" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-79" value="R/W" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="62" y="390" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-80" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;entryX=0;entryY=1;entryDx=0;entryDy=0;exitX=1;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-76" target="9h2_EveRN_1U4OZwnWZC-24"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="190" y="360" as="sourcePoint" /> | |||||
| <mxPoint x="250" y="310" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-84" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="230" y="412.5" width="120" height="85" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-89" value="Scorpio broker" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="260" y="440.5" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-91" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-76" target="9h2_EveRN_1U4OZwnWZC-84"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="180" y="530" as="sourcePoint" /> | |||||
| <mxPoint x="230" y="480" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-92" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.518;entryY=0.008;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-84" target="9h2_EveRN_1U4OZwnWZC-98"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="300" y="640" as="sourcePoint" /> | |||||
| <mxPoint x="284.5" y="621" as="targetPoint" /> | |||||
| <Array as="points" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-93" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||||
| <mxGeometry x="212" y="561" width="60" height="30" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-97" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-89" target="9h2_EveRN_1U4OZwnWZC-89"> | |||||
| <mxGeometry relative="1" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-98" value="<br>GW <br>and Tracker definition<br>model<br>supported<br>Decode-lib<br>Raw-Data<div><br></div>" style="shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;darkOpacity=0.05;darkOpacity2=0.1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="235" y="621" width="110" height="126" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-99" value="Redis cache" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||||
| <mxGeometry x="254.5" y="342" width="90.5" height="50" as="geometry" /> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-101" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.403;entryY=0.98;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-84" target="9h2_EveRN_1U4OZwnWZC-99"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="260" y="440" as="sourcePoint" /> | |||||
| <mxPoint x="310" y="390" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-102" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1"> | |||||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||||
| <mxPoint x="170" y="420" as="sourcePoint" /> | |||||
| <mxPoint x="260" y="390" as="targetPoint" /> | |||||
| </mxGeometry> | |||||
| </mxCell> | |||||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-104" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;opacity=40;" vertex="1" parent="1"> | |||||
| <mxGeometry x="18" y="230" width="392" height="540" as="geometry" /> | |||||
| </mxCell> | |||||
| </root> | |||||
| </mxGraphModel> | |||||
| </diagram> | |||||
| </mxfile> | |||||
| @@ -1,44 +0,0 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| A01;0;PT-MAGA;200;50;107;BC-01;C3:00:00:57:B9:ED | |||||
| A02;0;PT-MAGA;600;200;107;BC-02;C3:00:00:57:B9:E9 | |||||
| A03;0;PT-MAGA;175;1535;107;BC-03;C3:00:00:57:B9:F0 | |||||
| A04;0;PT-MAGA;620;1535;107;BC-04;C3:00:00:57:B9:E4 | |||||
| A05;0;PT-MAGA;580;710;72;BC-05;C3:00:00:57:B9:FA | |||||
| A06;0;PT-MENS;800;1065;142;BC-06;C3:00:00:57:B9:F9 | |||||
| A07;0;PT-MENS;2165;315;107;BC-07;C3:00:00:57:B9:F8 | |||||
| A08;0;PT-MENS;2195;50;142;BC-08;C3:00:00:57:B9:F7 | |||||
| A09;0;PT-MENS;2895;50;142;BC-09;C3:00:00:57:B9:EA | |||||
| A10;0;PT-MENS;2710;250;78;BC-10;C3:00:00:57:B9:EB | |||||
| A11;0;PT-AMMI;1585;870;141;BC-11;C3:00:00:57:B9:EE | |||||
| A12;0;PT-AMMI;1585;1540;141;BC-12;C3:00:00:57:B9:E2 | |||||
| A13;0;PT-AMMI;2130;875;107;BC-13;C3:00:00:57:B9:E5 | |||||
| A14;0;PT-AMMI;2095;1540;141;BC-14;C3:00:00:57:B9:D5 | |||||
| A15;0;PT-AMMI;1875;1200;73;BC-15;C3:00:00:57:B9:EC | |||||
| A16;0;PT-PROD;2180;875;107;BC-16;C3:00:00:57:B9:D8 | |||||
| A17;0;PT-PROD;2180;1540;141;BC-17;C3:00:00:57:B9:E1 | |||||
| A18;0;PT-PROD;2930;880;141;BC-18;C3:00:00:57:B9:F3 | |||||
| A19;0;PT-PROD;2895;1530;141;BC-19;C3:00:00:57:B9:E0 | |||||
| A20;0;PT-PROD;2650;1180;107;BC-20;C3:00:00:57:B9:EF | |||||
| A21;1;P1-NETW;800;1050;107;BC-21;C3:00:00:57:B9:E6 | |||||
| A22;1;P1-NETW;850;1545;107;BC-22;C3:00:00:57:B9:D4 | |||||
| A23;1;P1-NETW;1425;1050;107;BC-23;C3:00:00:57:B9:E8 | |||||
| A24;1;P1-NETW;1400;1530;107;BC-24;C3:00:00:57:B9:F1 | |||||
| A25;1;P1-NETW;1195;1315;72;BC-25;C3:00:00:57:B9:E7 | |||||
| A26;1;P1-RIUNI;2190;50;107;BC-26;C3:00:00:57:B9:D6 | |||||
| A27;1;P1-RIUNI;2180;465;107;BC-27;C3:00:00:57:B9:D7 | |||||
| A28;1;P1-RIUNI;2890;50;107;BC-28;C3:00:00:57:B9:F6 | |||||
| A29;1;P1-RIUNI;2525;465;76;BC-29;C3:00:00:57:B9:F2 | |||||
| A30;1;P1-RIUNI;2540;280;69;BC-30;C3:00:00:57:B9:D3 | |||||
| A31;1;P1-SOFT;1895;865;107;BC-31;C3:00:00:57:B9:F4 | |||||
| A32;1;P1-SOFT;1900;1535;107;BC-32;C3:00:00:57:B9:D9 | |||||
| A33;1;P1-SOFT;2320;870;72;BC-33;C3:00:00:57:B9:F5 | |||||
| A34;1;P1-SOFT;2330;1530;107;BC-34;C3:00:00:57:B9:DA | |||||
| A35;1;P1-SOFT;2065;1190;20;BC-35;C3:00:00:57:B9:DB | |||||
| A36;1;P1-CUCO;2370;865;107;BC-36;C3:00:00:57:B9:DC | |||||
| A37;1;P1-CUCO;2380;1535;93;BC-37;C3:00:00:57:B9:DD | |||||
| A38;1;P1-CUCO;2940;870;93;BC-38;C3:00:00:57:B9:E3 | |||||
| A39;1;P1-CUCO;2905;1540;93;BC-39;C3:00:00:57:B9:DF | |||||
| A40;1;P1-CUCO;2550;1360;72;BC-40;C3:00:00:57:B9:DE | |||||
| A41;1;P1-AMOR;830;50;100;BC-41;C3:00:00:39:47:DF | |||||
| A42;1;P1-DINO;1788;50;117;BC-42;C3:00:00:39:47:E2 | |||||
| A43;1;TESTER;1026;1050;122;BC-43;C3:00:00:39:47:C4 | |||||
| @@ -0,0 +1,158 @@ | |||||
| version: "2" | |||||
| services: | |||||
| db: | |||||
| image: postgres | |||||
| container_name: db | |||||
| restart: always | |||||
| ports: | |||||
| - "127.0.0.1:5432:5432" | |||||
| environment: | |||||
| - POSTGRES_USER=postgres | |||||
| - POSTGRES_PASSWORD=postgres | |||||
| healthcheck: | |||||
| test: ["CMD-SHELL", "pg_isready -U postgres"] | |||||
| interval: 5s | |||||
| timeout: 5s | |||||
| retries: 5 | |||||
| start_period: 30s | |||||
| kafdrop: | |||||
| image: obsidiandynamics/kafdrop | |||||
| restart: "no" | |||||
| ports: | |||||
| - "127.0.0.1:9000:9000" | |||||
| environment: | |||||
| KAFKA_BROKERCONNECT: "kafka:29092" | |||||
| depends_on: | |||||
| - "kafka" | |||||
| kafka: | |||||
| image: apache/kafka:3.9.0 | |||||
| restart: "no" | |||||
| ports: | |||||
| # - "127.0.0.1:2181:2181" | |||||
| - "127.0.0.1:9092:9092" | |||||
| - "127.0.0.1:9093:9093" | |||||
| healthcheck: # <-- ADD THIS BLOCK | |||||
| test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --list"] | |||||
| interval: 10s | |||||
| timeout: 5s | |||||
| retries: 10 | |||||
| start_period: 20s | |||||
| environment: | |||||
| KAFKA_NODE_ID: 1 | |||||
| KAFKA_PROCESS_ROLES: broker,controller | |||||
| KAFKA_LISTENERS: INTERNAL://:29092,EXTERNAL://:9092,CONTROLLER://127.0.0.1:9093 | |||||
| KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:29092,EXTERNAL://localhost:9092 | |||||
| KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER | |||||
| KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT | |||||
| KAFKA_CONTROLLER_QUORUM_VOTERS: 1@127.0.0.1:9093 | |||||
| KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL | |||||
| KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 | |||||
| KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 | |||||
| KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 | |||||
| KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 | |||||
| KAFKA_NUM_PARTITIONS: 3 | |||||
| kafka-init: | |||||
| image: apache/kafka:3.9.0 | |||||
| command: [ "sh", "-c", "ls -l /tmp/create_topic.sh && /tmp/create_topic.sh" ] | |||||
| depends_on: | |||||
| kafka: | |||||
| condition: service_healthy | |||||
| volumes: | |||||
| - ./init-scripts/create_topic.sh:/tmp/create_topic.sh | |||||
| environment: | |||||
| - TOPIC_NAMES=topic1,topic2,topic3 | |||||
| valkey: | |||||
| image: valkey/valkey:9.0.0 | |||||
| container_name: valkey | |||||
| ports: | |||||
| - "127.0.0.1:6379:6379" | |||||
| presense-decoder: | |||||
| build: | |||||
| context: ../ | |||||
| dockerfile: build/package/Dockerfile.dev | |||||
| image: presense-decoder | |||||
| container_name: presense-decoder | |||||
| environment: | |||||
| - KAFKA_URL=kafka:29092 | |||||
| depends_on: | |||||
| kafka-init: | |||||
| condition: service_completed_successfully | |||||
| db: | |||||
| condition: service_healthy | |||||
| restart: always | |||||
| volumes: | |||||
| - ../:/app | |||||
| command: air --build.cmd "go build -buildvcs=false -o /tmp/decoder ./cmd/decoder" --build.bin "/tmp/decoder" | |||||
| presense-server: | |||||
| build: | |||||
| context: ../ | |||||
| dockerfile: build/package/Dockerfile.dev | |||||
| image: presense-server | |||||
| container_name: presense-server | |||||
| environment: | |||||
| - VALKEY_URL=valkey:6379 | |||||
| - KAFKA_URL=kafka:29092 | |||||
| - DBHost=db | |||||
| - DBUser=postgres | |||||
| - DBPass=postgres | |||||
| - DBName=postgres | |||||
| ports: | |||||
| - "127.0.0.1:1902:1902" | |||||
| depends_on: | |||||
| valkey: | |||||
| condition: service_started | |||||
| kafka-init: | |||||
| condition: service_completed_successfully | |||||
| db: | |||||
| condition: service_healthy | |||||
| restart: always | |||||
| volumes: | |||||
| - ../:/app | |||||
| command: air --build.cmd "go build -buildvcs=false -o /tmp/server ./cmd/server" --build.bin "/tmp/server" | |||||
| presense-bridge: | |||||
| build: | |||||
| context: ../ | |||||
| dockerfile: build/package/Dockerfile.dev | |||||
| image: presense-bridge | |||||
| container_name: presense-bridge | |||||
| environment: | |||||
| - KAFKA_URL=kafka:29092 | |||||
| - MQTT_HOST=192.168.1.101 | |||||
| - MQTT_USERNAME=user | |||||
| - MQTT_PASSWORD=pass | |||||
| depends_on: | |||||
| kafka-init: | |||||
| condition: service_completed_successfully | |||||
| db: | |||||
| condition: service_healthy | |||||
| restart: always | |||||
| volumes: | |||||
| - ../:/app | |||||
| command: air --build.cmd "go build -buildvcs=false -o /tmp/bridge ./cmd/bridge" --build.bin "/tmp/bridge" | |||||
| presense-location: | |||||
| build: | |||||
| context: ../ | |||||
| dockerfile: build/package/Dockerfile.dev | |||||
| image: presense-location | |||||
| container_name: presense-location | |||||
| environment: | |||||
| - KAFKA_URL=kafka:29092 | |||||
| depends_on: | |||||
| kafka-init: | |||||
| condition: service_completed_successfully | |||||
| db: | |||||
| condition: service_healthy | |||||
| restart: always | |||||
| volumes: | |||||
| - ../:/app | |||||
| command: air --build.cmd "go build -buildvcs=false -o /tmp/location ./cmd/location" --build.bin "/tmp/location" | |||||
| @@ -0,0 +1,7 @@ | |||||
| FROM golang:1.25.0 | |||||
| WORKDIR /app | |||||
| RUN go install github.com/air-verse/air@latest | |||||
| # Pre-download dependencies for the whole workspace | |||||
| COPY go.mod go.sum ./ | |||||
| RUN go mod download | |||||
| CMD ["air"] | |||||
| @@ -42,7 +42,6 @@ func mqtthandler(writer *kafka.Writer, topic string, message []byte, appState *a | |||||
| } | } | ||||
| // fmt.Printf("reading: %+v\n", reading) | // fmt.Printf("reading: %+v\n", reading) | ||||
| if !appState.BeaconExists(reading.MAC) { | if !appState.BeaconExists(reading.MAC) { | ||||
| fmt.Printf("Not tracking beacon: %s\n", reading.MAC) | |||||
| continue | continue | ||||
| } | } | ||||
| @@ -1,17 +0,0 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;GatewayName;MAC | |||||
| C01;0;PT-MAGA;220;250;13;GU-01;ac:23:3f:c1:dd:3c | |||||
| C02;0;PT-FORM;825;745;13;GU-02;ac:23:3f:c1:dd:49 | |||||
| C03;0;PT-LVNS;825;1435;13;GU-03;ac:23:3f:c1:dc:ee | |||||
| C04;0;PT-RECE;2010;620;13;GU-04;ac:23:3f:c1:dd:40 | |||||
| C05;0;PT-AMMI;1785;1260;13;GU-05;ac:23:3f:c1:dd:51 | |||||
| C06;0;PT-PROD;2720;1220;13;GU-06;ac:23:3f:c1:dd:48 | |||||
| C07;0;PT-BATH;2800;655;13;GU-07;ac:23:3f:c1:dd:50 | |||||
| C08;0;PT-MENS;2580;490;13;GU-08;ac:23:3f:c1:dc:d3 | |||||
| C09;1;P1-AMOR;900;50;13;GU-09;ac:23:3f:c1:dd:55 | |||||
| C10;1;P1-NETW;1310;1440;13;GU-10;ac:23:3f:c1:dc:d1 | |||||
| C11;1;P1-DINO;1662;480;13;GU-11;ac:23:3f:c1:dc:cb | |||||
| C12;1;P1-COMM;1575;1455;13;GU-12;ac:23:3f:c1:dc:d2 | |||||
| C13;1;P1-SOFT;2290;965;13;GU-13;ac:23:3f:c1:dd:31 | |||||
| C14;1;P1-CUCO;2860;1120;13;GU-14;ac:23:3f:c1:dd:4b | |||||
| C15;1;P1-BATH;2740;710;13;GU-15;ac:23:3f:c1:dd:4e | |||||
| C16;1;P1-RIUN;2180;355;13;GU-16;ac:23:3f:c1:dc:cd | |||||
| @@ -1,28 +0,0 @@ | |||||
| [Unit] | |||||
| Description=Presense | |||||
| PartOf=podman.service | |||||
| Wants=network-online.target podman-conf-login.service | |||||
| After=podman.service network-online.target podman-conf-login.service | |||||
| StartLimitIntervalSec=0 | |||||
| [Container] | |||||
| Image=presense-go:latest | |||||
| ContainerName=presense | |||||
| PodmanArgs=-a stdout -a stderr | |||||
| Network=sandbox.network | |||||
| PublishPort=127.0.0.1:1902:8080 | |||||
| Environment=HTTP_HOST_PATH=0.0.0.0:8080 | |||||
| Environment=HTTPWS_HOST_PATH=0.0.0.0:8088 | |||||
| Environment=MQTT_HOST=emqx:1883 | |||||
| Environment=MQTT_USERNAME=sandbox | |||||
| Environment=MQTT_PASSWORD=sandbox2025 | |||||
| Environment=MQTT_CLIENT_ID=presence-detector | |||||
| Environment=DB_PATH=.presence.db | |||||
| [Service] | |||||
| Restart=always | |||||
| TimeoutStartSec=infinity | |||||
| RestartSec=5 | |||||
| [Install] | |||||
| WantedBy=multi-user.target podman.service | |||||
| @@ -1,130 +0,0 @@ | |||||
| # Unit Tests Documentation | |||||
| This directory contains comprehensive unit tests for the high-priority internal packages of the AFASystems presence detection system. | |||||
| ## Test Coverage | |||||
| The following files have been thoroughly tested: | |||||
| 1. **`distance_test.go`** - Tests for distance calculation utilities | |||||
| - `CalculateDistance()` - Distance calculation from RSSI and TX power | |||||
| - `twosComp()` - Two's complement hex conversion | |||||
| - `ValidateRSSI()` - RSSI value validation | |||||
| - `ValidateTXPower()` - TX power validation | |||||
| - Edge cases and real-world scenarios | |||||
| 2. **`beacons_test.go`** - Tests for beacon parsing utilities | |||||
| - `ParseADFast()` - Advertising Data structure parsing | |||||
| - `RemoveFlagBytes()` - Bluetooth flag bytes removal | |||||
| - `LoopADStructures()` - Beacon type detection and parsing | |||||
| - `isValidADStructure()` - AD structure validation | |||||
| - Beacon format support: Ingics, Eddystone TLM, Minew B7 | |||||
| 3. **`typeMethods_test.go`** - Tests for model type methods | |||||
| - `Hash()` - Beacon event hash generation with battery rounding | |||||
| - `ToJSON()` - JSON marshaling for beacon events | |||||
| - `convertStructToMap()` - Generic struct-to-map conversion | |||||
| - `RedisHashable()` - Redis hash map conversion for HTTPLocation and BeaconEvent | |||||
| - JSON roundtrip integrity tests | |||||
| 4. **`mqtthandler_test.go`** - Tests for MQTT message processing | |||||
| - `MqttHandler()` - Main MQTT message processing with JSON/CSV input | |||||
| - `parseButtonState()` - Button counter parsing for different beacon formats | |||||
| - Kafka writer integration (with mock) | |||||
| - Hostname extraction from MQTT topics | |||||
| - Error handling and edge cases | |||||
| ## Running Tests | |||||
| ### Run All Tests | |||||
| ```bash | |||||
| go test ./test/... -v | |||||
| ``` | |||||
| ### Run Specific Test File | |||||
| ```bash | |||||
| go test ./test/distance_test.go -v | |||||
| go test ./test/beacons_test.go -v | |||||
| go test ./test/typeMethods_test.go -v | |||||
| go test ./test/mqtthandler_test.go -v | |||||
| ``` | |||||
| ### Run Tests for Specific Function | |||||
| ```bash | |||||
| go test ./test/distance_test.go -run TestCalculateDistance -v | |||||
| go test ./test/beacons_test.go -run TestParseADFast -v | |||||
| go test ./test/typeMethods_test.go -run TestHash -v | |||||
| go test ./test/mqtthandler_test.go -run TestMqttHandlerJSONArrayInput -v | |||||
| ``` | |||||
| ### Run Benchmarks | |||||
| ```bash | |||||
| # Run all benchmarks | |||||
| go test ./test/... -bench=. | |||||
| # Run specific benchmarks | |||||
| go test ./test/distance_test.go -bench=BenchmarkCalculateDistance -v | |||||
| go test ./test/beacons_test.go -bench=BenchmarkParseADFast -v | |||||
| go test ./test/typeMethods_test.go -bench=BenchmarkHash -v | |||||
| go test ./test/mqtthandler_test.go -bench=BenchmarkMqttHandlerJSON -v | |||||
| ``` | |||||
| ### Run Tests with Coverage Report | |||||
| ```bash | |||||
| go test ./test/... -cover | |||||
| go test ./test/... -coverprofile=coverage.out | |||||
| go tool cover -html=coverage.out -o coverage.html | |||||
| ``` | |||||
| ### Run Tests with Race Detection | |||||
| ```bash | |||||
| go test ./test/... -race -v | |||||
| ``` | |||||
| ## Test Organization | |||||
| Each test file follows Go testing conventions with: | |||||
| - **Function tests**: Individual function behavior testing | |||||
| - **Edge case tests**: Boundary conditions and error scenarios | |||||
| - **Integration tests**: Multi-function workflow testing | |||||
| - **Benchmark tests**: Performance measurement | |||||
| - **Table-driven tests**: Multiple test cases with expected results | |||||
| ## Mock Objects | |||||
| The mqtthandler tests use a `MockKafkaWriter` to simulate Kafka operations without requiring a running Kafka instance. This allows for: | |||||
| - Deterministic test results | |||||
| - Failure scenario simulation | |||||
| - Message content verification | |||||
| - Performance benchmarking | |||||
| ## Known Limitations | |||||
| - **CSV Processing**: The original CSV handler in `mqtthandler.go` contains `os.Exit(2)` calls which make it untestable. The test demonstrates the intended structure but cannot fully validate CSV processing due to this design choice. | |||||
| - **External Dependencies**: Tests use mocks for external systems (Kafka) to ensure tests remain fast and reliable. | |||||
| ## Best Practices Demonstrated | |||||
| These tests demonstrate several Go testing best practices: | |||||
| 1. **Table-driven tests** for multiple scenarios | |||||
| 2. **Subtests** for logical test grouping | |||||
| 3. **Benchmark tests** for performance measurement | |||||
| 4. **Mock objects** for dependency isolation | |||||
| 5. **Error case testing** for robustness validation | |||||
| 6. **Deterministic testing** with consistent setup and teardown | |||||
| ## Running Tests in CI/CD | |||||
| For automated testing environments: | |||||
| ```bash | |||||
| # Standard CI test run | |||||
| go test ./test/... -race -cover -timeout=30s | |||||
| # Performance regression testing | |||||
| go test ./test/... -bench=. -benchmem | |||||
| ``` | |||||
| This comprehensive test suite ensures the reliability and correctness of the core business logic in the AFASystems presence detection system. | |||||
| @@ -1,560 +0,0 @@ | |||||
| package utils | |||||
| import ( | |||||
| "testing" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | |||||
| ) | |||||
| func TestParseADFast(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| input []byte | |||||
| expected [][2]int | |||||
| }{ | |||||
| { | |||||
| name: "Empty input", | |||||
| input: []byte{}, | |||||
| expected: [][2]int{}, | |||||
| }, | |||||
| { | |||||
| name: "Single AD structure", | |||||
| input: []byte{0x02, 0x01, 0x06}, | |||||
| expected: [][2]int{{0, 2}}, | |||||
| }, | |||||
| { | |||||
| name: "Multiple AD structures", | |||||
| input: []byte{0x02, 0x01, 0x06, 0x03, 0x02, 0x01, 0x02}, | |||||
| expected: [][2]int{{0, 2}, {3, 6}}, | |||||
| }, | |||||
| { | |||||
| name: "Complex AD structures", | |||||
| input: []byte{0x02, 0x01, 0x06, 0x1A, 0xFF, 0x4C, 0x00, 0x02, 0x15, 0xE2, 0xC5, 0x6D, 0xB5, 0xDF, 0xFB, 0x48, 0xD2, 0xB0, 0x60, 0xD0, 0xF5, 0xA7, 0x10, 0x96, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xC5}, | |||||
| expected: [][2]int{{0, 2}, {2, 28}}, | |||||
| }, | |||||
| { | |||||
| name: "Zero length AD structure", | |||||
| input: []byte{0x00, 0x01, 0x06, 0x03, 0x02, 0x01, 0x02}, | |||||
| expected: [][2]int{{2, 5}}, | |||||
| }, | |||||
| { | |||||
| name: "AD structure exceeding bounds", | |||||
| input: []byte{0x05, 0x01, 0x06}, | |||||
| expected: [][2]int{}, | |||||
| }, | |||||
| { | |||||
| name: "Incomplete AD structure", | |||||
| input: []byte{0x03, 0x01}, | |||||
| expected: [][2]int{}, | |||||
| }, | |||||
| { | |||||
| name: "Valid then invalid structure", | |||||
| input: []byte{0x02, 0x01, 0x06, 0xFF, 0x01, 0x06}, | |||||
| expected: [][2]int{{0, 2}}, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := ParseADFast(tt.input) | |||||
| if len(result) != len(tt.expected) { | |||||
| t.Errorf("ParseADFast() length = %v, expected %v", len(result), len(tt.expected)) | |||||
| return | |||||
| } | |||||
| for i, r := range result { | |||||
| if r[0] != tt.expected[i][0] || r[1] != tt.expected[i][1] { | |||||
| t.Errorf("ParseADFast()[%d] = %v, expected %v", i, r, tt.expected[i]) | |||||
| } | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestRemoveFlagBytes(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| input []byte | |||||
| expected []byte | |||||
| }{ | |||||
| { | |||||
| name: "Empty input", | |||||
| input: []byte{}, | |||||
| expected: []byte{}, | |||||
| }, | |||||
| { | |||||
| name: "Single byte input", | |||||
| input: []byte{0x01}, | |||||
| expected: []byte{0x01}, | |||||
| }, | |||||
| { | |||||
| name: "No flag bytes", | |||||
| input: []byte{0x02, 0x01, 0x06, 0x03, 0x02, 0x01}, | |||||
| expected: []byte{0x02, 0x01, 0x06, 0x03, 0x02, 0x01}, | |||||
| }, | |||||
| { | |||||
| name: "With flag bytes", | |||||
| input: []byte{0x02, 0x01, 0x06, 0x1A, 0xFF, 0x4C, 0x00, 0x02}, | |||||
| expected: []byte{0x1A, 0xFF, 0x4C, 0x00, 0x02}, | |||||
| }, | |||||
| { | |||||
| name: "Flag type is 0x01", | |||||
| input: []byte{0x02, 0x01, 0x06, 0x05, 0x01, 0x02, 0x03, 0x04}, | |||||
| expected: []byte{0x05, 0x01, 0x02, 0x03, 0x04}, | |||||
| }, | |||||
| { | |||||
| name: "Flag type is not 0x01", | |||||
| input: []byte{0x02, 0x02, 0x06, 0x05, 0x01, 0x02, 0x03, 0x04}, | |||||
| expected: []byte{0x02, 0x02, 0x06, 0x05, 0x01, 0x02, 0x03, 0x04}, | |||||
| }, | |||||
| { | |||||
| name: "Length exceeds bounds", | |||||
| input: []byte{0xFF, 0x01, 0x06}, | |||||
| expected: []byte{0xFF, 0x01, 0x06}, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := RemoveFlagBytes(tt.input) | |||||
| if len(result) != len(tt.expected) { | |||||
| t.Errorf("RemoveFlagBytes() length = %v, expected %v", len(result), len(tt.expected)) | |||||
| return | |||||
| } | |||||
| for i, b := range result { | |||||
| if b != tt.expected[i] { | |||||
| t.Errorf("RemoveFlagBytes()[%d] = %v, expected %v", i, b, tt.expected[i]) | |||||
| } | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestIsValidADStructure(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| data []byte | |||||
| expected bool | |||||
| }{ | |||||
| { | |||||
| name: "Empty data", | |||||
| data: []byte{}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Single byte", | |||||
| data: []byte{0x01}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Valid minimal structure", | |||||
| data: []byte{0x01, 0x01}, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Valid structure", | |||||
| data: []byte{0x02, 0x01, 0x06}, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Zero length", | |||||
| data: []byte{0x00, 0x01, 0x06}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Length exceeds data", | |||||
| data: []byte{0x05, 0x01, 0x06}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Length exactly matches", | |||||
| data: []byte{0x02, 0x01, 0x06}, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Large valid structure", | |||||
| data: []byte{0x1F, 0xFF, 0x4C, 0x00, 0x02, 0x15, 0xE2, 0xC5, 0x6D, 0xB5, 0xDF, 0xFB, 0x48, 0xD2, 0xB0, 0x60, 0xD0, 0xF5, 0xA7, 0x10, 0x96, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xC5, 0x01, 0x02, 0x03, 0x04, 0x05}, | |||||
| expected: true, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := isValidADStructure(tt.data) | |||||
| if result != tt.expected { | |||||
| t.Errorf("isValidADStructure() = %v, expected %v", result, tt.expected) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestCheckIngics(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| ad []byte | |||||
| expected bool | |||||
| }{ | |||||
| { | |||||
| name: "Valid Ingics beacon", | |||||
| ad: []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0x12, 0x34, 0x01}, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Invalid - too short", | |||||
| ad: []byte{0x05, 0xFF, 0x59, 0x00}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Invalid - wrong manufacturer ID", | |||||
| ad: []byte{0x08, 0xFF, 0x59, 0x01, 0x80, 0xBC, 0x12, 0x34, 0x01}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Invalid - wrong type", | |||||
| ad: []byte{0x08, 0xFE, 0x59, 0x00, 0x80, 0xBC, 0x12, 0x34, 0x01}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Valid with minimum length", | |||||
| ad: []byte{0x06, 0xFF, 0x59, 0x00, 0x80, 0xBC}, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Empty data", | |||||
| ad: []byte{}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Partial match only", | |||||
| ad: []byte{0x06, 0xFF, 0x59, 0x00, 0x80}, | |||||
| expected: false, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := checkIngics(tt.ad) | |||||
| if result != tt.expected { | |||||
| t.Errorf("checkIngics() = %v, expected %v", result, tt.expected) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestParseIngicsState(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| ad []byte | |||||
| expected model.BeaconEvent | |||||
| }{ | |||||
| { | |||||
| name: "Valid Ingics data", | |||||
| ad: []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0x34, 0x12, 0x05}, | |||||
| expected: model.BeaconEvent{ | |||||
| Battery: 0x1234, // 4660 in little endian | |||||
| Event: 0x05, | |||||
| Type: "Ingics", | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "Zero battery", | |||||
| ad: []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0x00, 0x00, 0x00}, | |||||
| expected: model.BeaconEvent{ | |||||
| Battery: 0, | |||||
| Event: 0, | |||||
| Type: "Ingics", | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "Max battery value", | |||||
| ad: []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0xFF, 0xFF, 0xFF}, | |||||
| expected: model.BeaconEvent{ | |||||
| Battery: 0xFFFF, | |||||
| Event: 0xFF, | |||||
| Type: "Ingics", | |||||
| }, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := parseIngicsState(tt.ad) | |||||
| if result.Battery != tt.expected.Battery { | |||||
| t.Errorf("parseIngicsState() Battery = %v, expected %v", result.Battery, tt.expected.Battery) | |||||
| } | |||||
| if result.Event != tt.expected.Event { | |||||
| t.Errorf("parseIngicsState() Event = %v, expected %v", result.Event, tt.expected.Event) | |||||
| } | |||||
| if result.Type != tt.expected.Type { | |||||
| t.Errorf("parseIngicsState() Type = %v, expected %v", result.Type, tt.expected.Type) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestCheckEddystoneTLM(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| ad []byte | |||||
| expected bool | |||||
| }{ | |||||
| { | |||||
| name: "Valid Eddystone TLM", | |||||
| ad: []byte{0x12, 0x16, 0xAA, 0xFE, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04}, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Invalid - too short", | |||||
| ad: []byte{0x03, 0x16, 0xAA}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Invalid - wrong type", | |||||
| ad: []byte{0x12, 0x15, 0xAA, 0xFE, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Invalid - wrong company ID", | |||||
| ad: []byte{0x12, 0x16, 0xAA, 0xFF, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Invalid - wrong TLM type", | |||||
| ad: []byte{0x12, 0x16, 0xAA, 0xFE, 0x21, 0x00, 0x01, 0x02, 0x03, 0x04}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Valid with minimum length", | |||||
| ad: []byte{0x04, 0x16, 0xAA, 0xFE, 0x20}, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Empty data", | |||||
| ad: []byte{}, | |||||
| expected: false, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := checkEddystoneTLM(tt.ad) | |||||
| if result != tt.expected { | |||||
| t.Errorf("checkEddystoneTLM() = %v, expected %v", result, tt.expected) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestParseEddystoneState(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| ad []byte | |||||
| expected model.BeaconEvent | |||||
| }{ | |||||
| { | |||||
| name: "Valid Eddystone TLM data", | |||||
| ad: []byte{0x12, 0x16, 0xAA, 0xFE, 0x20, 0x34, 0x12, 0x78, 0x56, 0x00}, | |||||
| expected: model.BeaconEvent{ | |||||
| Battery: 0x1234, // 4660 in big endian (note: different from Ingics) | |||||
| Type: "Eddystone", | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "Zero battery", | |||||
| ad: []byte{0x12, 0x16, 0xAA, 0xFE, 0x20, 0x00, 0x00, 0x78, 0x56, 0x00}, | |||||
| expected: model.BeaconEvent{ | |||||
| Battery: 0, | |||||
| Type: "Eddystone", | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "Max battery value", | |||||
| ad: []byte{0x12, 0x16, 0xAA, 0xFE, 0x20, 0xFF, 0xFF, 0x78, 0x56, 0x00}, | |||||
| expected: model.BeaconEvent{ | |||||
| Battery: 0xFFFF, | |||||
| Type: "Eddystone", | |||||
| }, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := parseEddystoneState(tt.ad) | |||||
| if result.Battery != tt.expected.Battery { | |||||
| t.Errorf("parseEddystoneState() Battery = %v, expected %v", result.Battery, tt.expected.Battery) | |||||
| } | |||||
| if result.Type != tt.expected.Type { | |||||
| t.Errorf("parseEddystoneState() Type = %v, expected %v", result.Type, tt.expected.Type) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestCheckMinewB7(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| ad []byte | |||||
| expected bool | |||||
| }{ | |||||
| { | |||||
| name: "Valid Minew B7", | |||||
| ad: []byte{0x08, 0x16, 0xE1, 0xFF, 0x01, 0x02, 0x03, 0x04}, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Invalid - too short", | |||||
| ad: []byte{0x03, 0x16, 0xE1}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Invalid - wrong type", | |||||
| ad: []byte{0x08, 0x15, 0xE1, 0xFF, 0x01, 0x02, 0x03, 0x04}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Invalid - wrong company ID", | |||||
| ad: []byte{0x08, 0x16, 0xE1, 0xFE, 0x01, 0x02, 0x03, 0x04}, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Valid with minimum length", | |||||
| ad: []byte{0x04, 0x16, 0xE1, 0xFF}, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Empty data", | |||||
| ad: []byte{}, | |||||
| expected: false, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := checkMinewB7(tt.ad) | |||||
| if result != tt.expected { | |||||
| t.Errorf("checkMinewB7() = %v, expected %v", result, tt.expected) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestLoopADStructures(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| data []byte | |||||
| ranges [][2]int | |||||
| id string | |||||
| expected model.BeaconEvent | |||||
| }{ | |||||
| { | |||||
| name: "Ingics beacon found", | |||||
| data: []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0x34, 0x12, 0x05, 0x02, 0x01, 0x06}, | |||||
| ranges: [][2]int{{0, 8}, {8, 11}}, | |||||
| id: "test-beacon", | |||||
| expected: model.BeaconEvent{ | |||||
| ID: "test-beacon", | |||||
| Name: "test-beacon", | |||||
| Battery: 0x1234, | |||||
| Event: 0x05, | |||||
| Type: "Ingics", | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "Eddystone beacon found", | |||||
| data: []byte{0x02, 0x01, 0x06, 0x12, 0x16, 0xAA, 0xFE, 0x20, 0x34, 0x12, 0x78, 0x56}, | |||||
| ranges: [][2]int{{0, 2}, {2, 14}}, | |||||
| id: "eddystone-test", | |||||
| expected: model.BeaconEvent{ | |||||
| ID: "eddystone-test", | |||||
| Name: "eddystone-test", | |||||
| Battery: 0x1234, | |||||
| Type: "Eddystone", | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "Minew B7 beacon found", | |||||
| data: []byte{0x08, 0x16, 0xE1, 0xFF, 0x01, 0x02, 0x03, 0x04, 0x02, 0x01, 0x06}, | |||||
| ranges: [][2]int{{0, 8}, {8, 11}}, | |||||
| id: "minew-test", | |||||
| expected: model.BeaconEvent{ | |||||
| ID: "minew-test", | |||||
| Name: "minew-test", | |||||
| Type: "", // Minew B7 returns empty BeaconEvent | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "No matching beacon type", | |||||
| data: []byte{0x02, 0x01, 0x06, 0x03, 0x02, 0x01, 0x02}, | |||||
| ranges: [][2]int{{0, 2}, {2, 5}}, | |||||
| id: "unknown-test", | |||||
| expected: model.BeaconEvent{}, | |||||
| }, | |||||
| { | |||||
| name: "Invalid AD structure", | |||||
| data: []byte{0x02, 0x01, 0x06, 0xFF, 0x01, 0x06}, | |||||
| ranges: [][2]int{{0, 2}, {2, 4}}, | |||||
| id: "invalid-test", | |||||
| expected: model.BeaconEvent{}, | |||||
| }, | |||||
| { | |||||
| name: "Empty data", | |||||
| data: []byte{}, | |||||
| ranges: [][2]int{}, | |||||
| id: "empty-test", | |||||
| expected: model.BeaconEvent{ | |||||
| ID: "empty-test", | |||||
| Name: "empty-test", | |||||
| }, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := LoopADStructures(tt.data, tt.ranges, tt.id) | |||||
| if result.ID != tt.expected.ID { | |||||
| t.Errorf("LoopADStructures() ID = %v, expected %v", result.ID, tt.expected.ID) | |||||
| } | |||||
| if result.Name != tt.expected.Name { | |||||
| t.Errorf("LoopADStructures() Name = %v, expected %v", result.Name, tt.expected.Name) | |||||
| } | |||||
| if result.Type != tt.expected.Type { | |||||
| t.Errorf("LoopADStructures() Type = %v, expected %v", result.Type, tt.expected.Type) | |||||
| } | |||||
| if result.Battery != tt.expected.Battery { | |||||
| t.Errorf("LoopADStructures() Battery = %v, expected %v", result.Battery, tt.expected.Battery) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestLoopADStructuresPriority(t *testing.T) { | |||||
| // Test that Ingics is checked first | |||||
| data := []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0x34, 0x12, 0x05, 0x12, 0x16, 0xAA, 0xFE, 0x20, 0x78, 0x56} | |||||
| ranges := [][2]int{{0, 8}, {8, 15}} | |||||
| result := LoopADStructures(data, ranges, "priority-test") | |||||
| // Should detect Ingics first, not Eddystone | |||||
| if result.Type != "Ingics" { | |||||
| t.Errorf("LoopADStructures() Type = %v, expected Ingics (priority test)", result.Type) | |||||
| } | |||||
| } | |||||
| // Benchmark tests | |||||
| func BenchmarkParseADFast(b *testing.B) { | |||||
| data := []byte{0x02, 0x01, 0x06, 0x1A, 0xFF, 0x4C, 0x00, 0x02, 0x15, 0xE2, 0xC5, 0x6D, 0xB5, 0xDF, 0xFB, 0x48, 0xD2, 0xB0, 0x60, 0xD0, 0xF5, 0xA7, 0x10, 0x96, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xC5} | |||||
| for i := 0; i < b.N; i++ { | |||||
| ParseADFast(data) | |||||
| } | |||||
| } | |||||
| func BenchmarkRemoveFlagBytes(b *testing.B) { | |||||
| data := []byte{0x02, 0x01, 0x06, 0x1A, 0xFF, 0x4C, 0x00, 0x02, 0x15, 0xE2, 0xC5} | |||||
| for i := 0; i < b.N; i++ { | |||||
| RemoveFlagBytes(data) | |||||
| } | |||||
| } | |||||
| @@ -1,294 +0,0 @@ | |||||
| package test | |||||
| import ( | |||||
| "testing" | |||||
| "github.com/AFASystems/presence/internal/pkg/common/utils" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | |||||
| ) | |||||
| func TestCalculateDistance(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| adv model.BeaconAdvertisement | |||||
| expected float64 | |||||
| }{ | |||||
| { | |||||
| name: "Strong signal - close distance", | |||||
| adv: model.BeaconAdvertisement{ | |||||
| RSSI: -30, | |||||
| TXPower: "59", // 89 in decimal | |||||
| }, | |||||
| expected: 0.89976, // Close to minimum | |||||
| }, | |||||
| { | |||||
| name: "Medium signal", | |||||
| adv: model.BeaconAdvertisement{ | |||||
| RSSI: -65, | |||||
| TXPower: "59", | |||||
| }, | |||||
| expected: 1.5, // Medium distance | |||||
| }, | |||||
| { | |||||
| name: "Weak signal - far distance", | |||||
| adv: model.BeaconAdvertisement{ | |||||
| RSSI: -95, | |||||
| TXPower: "59", | |||||
| }, | |||||
| expected: 8.0, // Far distance | |||||
| }, | |||||
| { | |||||
| name: "Equal RSSI and TX power", | |||||
| adv: model.BeaconAdvertisement{ | |||||
| RSSI: -59, | |||||
| TXPower: "59", | |||||
| }, | |||||
| expected: 1.0, // Ratio = 1.0 | |||||
| }, | |||||
| { | |||||
| name: "Very strong signal", | |||||
| adv: model.BeaconAdvertisement{ | |||||
| RSSI: -10, | |||||
| TXPower: "59", | |||||
| }, | |||||
| expected: 0.89976, // Minimum distance | |||||
| }, | |||||
| { | |||||
| name: "Negative TX power (two's complement)", | |||||
| adv: model.BeaconAdvertisement{ | |||||
| RSSI: -70, | |||||
| TXPower: "C6", // -58 in decimal | |||||
| }, | |||||
| expected: 1.2, // Medium distance | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := utils.CalculateDistance(tt.adv) | |||||
| // Allow for small floating point differences | |||||
| if result < tt.expected*0.9 || result > tt.expected*1.1 { | |||||
| t.Errorf("CalculateDistance() = %v, expected around %v", result, tt.expected) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestCalculateDistanceEdgeCases(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| adv model.BeaconAdvertisement | |||||
| expected float64 | |||||
| }{ | |||||
| { | |||||
| name: "Zero RSSI", | |||||
| adv: model.BeaconAdvertisement{ | |||||
| RSSI: 0, | |||||
| TXPower: "59", | |||||
| }, | |||||
| expected: 0.0, | |||||
| }, | |||||
| { | |||||
| name: "Invalid TX power", | |||||
| adv: model.BeaconAdvertisement{ | |||||
| RSSI: -50, | |||||
| TXPower: "XYZ", | |||||
| }, | |||||
| expected: 0.0, // twosComp returns 0 for invalid input | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := utils.CalculateDistance(tt.adv) | |||||
| if result != tt.expected { | |||||
| t.Errorf("CalculateDistance() = %v, expected %v", result, tt.expected) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestValidateRSSI(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| rssi int64 | |||||
| expected bool | |||||
| }{ | |||||
| { | |||||
| name: "Valid RSSI - strong signal", | |||||
| rssi: -30, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Valid RSSI - weak signal", | |||||
| rssi: -100, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Valid RSSI - boundary low", | |||||
| rssi: -120, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Valid RSSI - boundary high", | |||||
| rssi: 0, | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Invalid RSSI - too strong", | |||||
| rssi: 10, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Invalid RSSI - too weak", | |||||
| rssi: -130, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Invalid RSSI - just below boundary", | |||||
| rssi: -121, | |||||
| expected: false, | |||||
| }, | |||||
| { | |||||
| name: "Invalid RSSI - just above boundary", | |||||
| rssi: 1, | |||||
| expected: false, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := utils.ValidateRSSI(tt.rssi) | |||||
| if result != tt.expected { | |||||
| t.Errorf("ValidateRSSI() = %v, expected %v", result, tt.expected) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestValidateTXPower(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| txPower string | |||||
| expected bool | |||||
| }{ | |||||
| { | |||||
| name: "Valid TX power - positive", | |||||
| txPower: "59", | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Valid TX power - negative", | |||||
| txPower: "C6", | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Valid TX power - zero", | |||||
| txPower: "00", | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Valid TX power - max positive", | |||||
| txPower: "7F", | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Valid TX power - max negative", | |||||
| txPower: "80", | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Valid TX power - boundary negative", | |||||
| txPower: "81", // -127 | |||||
| expected: true, | |||||
| }, | |||||
| { | |||||
| name: "Invalid TX power string", | |||||
| txPower: "XYZ", | |||||
| expected: true, // twosComp returns 0, which is valid | |||||
| }, | |||||
| { | |||||
| name: "Empty TX power", | |||||
| txPower: "", | |||||
| expected: true, // twosComp returns 0, which is valid | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := utils.ValidateTXPower(tt.txPower) | |||||
| if result != tt.expected { | |||||
| t.Errorf("ValidateTXPower() = %v, expected %v", result, tt.expected) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestCalculateDistanceConsistency(t *testing.T) { | |||||
| // Test that the function is deterministic | |||||
| adv := model.BeaconAdvertisement{ | |||||
| RSSI: -65, | |||||
| TXPower: "59", | |||||
| } | |||||
| result1 := utils.CalculateDistance(adv) | |||||
| result2 := utils.CalculateDistance(adv) | |||||
| if result1 != result2 { | |||||
| t.Errorf("CalculateDistance() is not deterministic: %v != %v", result1, result2) | |||||
| } | |||||
| } | |||||
| func TestCalculateDistanceRealWorldScenarios(t *testing.T) { | |||||
| scenarios := []struct { | |||||
| name string | |||||
| rssi int64 | |||||
| txPower string | |||||
| expectedRange [2]float64 // min, max expected range | |||||
| }{ | |||||
| { | |||||
| name: "Beacon very close (1m)", | |||||
| rssi: -45, | |||||
| txPower: "59", // 89 decimal | |||||
| expectedRange: [2]float64{0.5, 1.5}, | |||||
| }, | |||||
| { | |||||
| name: "Beacon at medium distance (5m)", | |||||
| rssi: -75, | |||||
| txPower: "59", | |||||
| expectedRange: [2]float64{3.0, 8.0}, | |||||
| }, | |||||
| { | |||||
| name: "Beacon far away (15m)", | |||||
| rssi: -95, | |||||
| txPower: "59", | |||||
| expectedRange: [2]float64{10.0, 25.0}, | |||||
| }, | |||||
| } | |||||
| for _, scenario := range scenarios { | |||||
| t.Run(scenario.name, func(t *testing.T) { | |||||
| adv := model.BeaconAdvertisement{ | |||||
| RSSI: scenario.rssi, | |||||
| TXPower: scenario.txPower, | |||||
| } | |||||
| result := utils.CalculateDistance(adv) | |||||
| if result < scenario.expectedRange[0] || result > scenario.expectedRange[1] { | |||||
| t.Errorf("CalculateDistance() = %v, expected range %v", result, scenario.expectedRange) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| // Benchmark tests | |||||
| func BenchmarkCalculateDistance(b *testing.B) { | |||||
| adv := model.BeaconAdvertisement{ | |||||
| RSSI: -65, | |||||
| TXPower: "59", | |||||
| } | |||||
| for i := 0; i < b.N; i++ { | |||||
| utils.CalculateDistance(adv) | |||||
| } | |||||
| } | |||||
| @@ -1,568 +0,0 @@ | |||||
| package mqtthandler | |||||
| import ( | |||||
| "context" | |||||
| "encoding/json" | |||||
| "testing" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | |||||
| "github.com/segmentio/kafka-go" | |||||
| ) | |||||
| // MockKafkaWriter implements a mock for kafka.Writer interface | |||||
| type MockKafkaWriter struct { | |||||
| Messages []kafka.Message | |||||
| ShouldFail bool | |||||
| WriteCount int | |||||
| } | |||||
| func (m *MockKafkaWriter) WriteMessages(ctx context.Context, msgs ...kafka.Message) error { | |||||
| m.WriteCount++ | |||||
| if m.ShouldFail { | |||||
| return &kafka.Error{ | |||||
| Err: &ErrMockWrite{}, | |||||
| Cause: nil, | |||||
| Context: msgs[0], | |||||
| } | |||||
| } | |||||
| m.Messages = append(m.Messages, msgs...) | |||||
| return nil | |||||
| } | |||||
| // ErrMockWrite is a mock error for testing | |||||
| type ErrMockWrite struct{} | |||||
| func (e *ErrMockWrite) Error() string { | |||||
| return "mock write error" | |||||
| } | |||||
| // Mock Kafka Close method (required for Writer interface) | |||||
| func (m *MockKafkaWriter) Close() error { | |||||
| return nil | |||||
| } | |||||
| func TestMqttHandlerJSONArrayInput(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| topicName []byte | |||||
| message []byte | |||||
| expectedMsgs int | |||||
| shouldFail bool | |||||
| }{ | |||||
| { | |||||
| name: "Valid JSON array with multiple readings", | |||||
| topicName: []byte("presence/gateway-001"), | |||||
| message: []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"}]`), | |||||
| expectedMsgs: 1, | |||||
| shouldFail: false, | |||||
| }, | |||||
| { | |||||
| name: "JSON array with multiple beacons", | |||||
| topicName: []byte("presence/gateway-002"), | |||||
| message: []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"},{"timestamp":"2023-01-01T00:00:01Z","type":"Beacon","mac":"11:22:33:44:55:66","rssi":-75,"rawData":"0201060303E1FF1200005678"}]`), | |||||
| expectedMsgs: 2, | |||||
| shouldFail: false, | |||||
| }, | |||||
| { | |||||
| name: "JSON array with gateway reading (should be skipped)", | |||||
| topicName: []byte("presence/gateway-003"), | |||||
| message: []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Gateway","mac":"GG:AA:TT:EE:WA:Y","rssi":-20,"rawData":"gateway-data"},{"timestamp":"2023-01-01T00:00:01Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"}]`), | |||||
| expectedMsgs: 1, // Only beacon should be processed | |||||
| shouldFail: false, | |||||
| }, | |||||
| { | |||||
| name: "JSON array with only gateways (should be skipped)", | |||||
| topicName: []byte("presence/gateway-004"), | |||||
| message: []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Gateway","mac":"GG:AA:TT:EE:WA:Y","rssi":-20,"rawData":"gateway-data"}]`), | |||||
| expectedMsgs: 0, // All gateways should be skipped | |||||
| shouldFail: false, | |||||
| }, | |||||
| { | |||||
| name: "Invalid JSON array", | |||||
| topicName: []byte("presence/gateway-005"), | |||||
| message: []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"`), | |||||
| expectedMsgs: 0, | |||||
| shouldFail: false, // Should not panic, just log error | |||||
| }, | |||||
| { | |||||
| name: "Empty JSON array", | |||||
| topicName: []byte("presence/gateway-006"), | |||||
| message: []byte(`[]`), | |||||
| expectedMsgs: 0, | |||||
| shouldFail: false, | |||||
| }, | |||||
| { | |||||
| name: "JSON array with null readings", | |||||
| topicName: []byte("presence/gateway-007"), | |||||
| message: []byte(`[null]`), | |||||
| expectedMsgs: 0, | |||||
| shouldFail: false, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| mockWriter := &MockKafkaWriter{ | |||||
| Messages: make([]kafka.Message, 0), | |||||
| ShouldFail: tt.shouldFail, | |||||
| } | |||||
| // Capture log output (you might want to use a test logger here) | |||||
| MqttHandler(mockWriter, tt.topicName, tt.message) | |||||
| if len(mockWriter.Messages) != tt.expectedMsgs { | |||||
| t.Errorf("MqttHandler() wrote %d messages, expected %d", len(mockWriter.Messages), tt.expectedMsgs) | |||||
| } | |||||
| // Verify message content if we expected messages | |||||
| if tt.expectedMsgs > 0 && len(mockWriter.Messages) > 0 { | |||||
| for i, msg := range mockWriter.Messages { | |||||
| var adv model.BeaconAdvertisement | |||||
| err := json.Unmarshal(msg.Value, &adv) | |||||
| if err != nil { | |||||
| t.Errorf("MqttHandler() message %d is not valid BeaconAdvertisement JSON: %v", i, err) | |||||
| } | |||||
| // Verify hostname extraction | |||||
| expectedHostname := "gateway-007" // Extracted from topicName | |||||
| if adv.Hostname != expectedHostname { | |||||
| t.Errorf("MqttHandler() hostname = %v, expected %v", adv.Hostname, expectedHostname) | |||||
| } | |||||
| } | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestMqttHandlerCSVInput(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| topicName []byte | |||||
| message []byte | |||||
| shouldProcess bool | |||||
| }{ | |||||
| { | |||||
| name: "Valid CSV format", | |||||
| topicName: []byte("presence/gateway-001"), | |||||
| message: []byte("timestamp,AA:BB:CC:DD:EE:FF,-65,0201060303E1FF1200001234,1001,field6\n"), | |||||
| shouldProcess: true, | |||||
| }, | |||||
| { | |||||
| name: "CSV with button data", | |||||
| topicName: []byte("presence/gateway-002"), | |||||
| message: []byte("timestamp,AA:BB:CC:DD:EE:FF,-65,02010612FF5901C0012345678,1234,field6\n"), | |||||
| shouldProcess: true, | |||||
| }, | |||||
| { | |||||
| name: "CSV with insufficient fields", | |||||
| topicName: []byte("presence/gateway-003"), | |||||
| message: []byte("timestamp,AA:BB:CC:DD:EE:FF,-65,0201060303E1FF1200001234\n"), | |||||
| shouldProcess: false, // Should log error and return early | |||||
| }, | |||||
| { | |||||
| name: "Empty CSV", | |||||
| topicName: []byte("presence/gateway-004"), | |||||
| message: []byte(""), | |||||
| shouldProcess: false, | |||||
| }, | |||||
| { | |||||
| name: "CSV with wrong field count", | |||||
| topicName: []byte("presence/gateway-005"), | |||||
| message: []byte("field1,field2,field3\n"), | |||||
| shouldProcess: false, | |||||
| }, | |||||
| { | |||||
| name: "CSV with non-numeric RSSI", | |||||
| topicName: []byte("presence/gateway-006"), | |||||
| message: []byte("timestamp,AA:BB:CC:DD:EE:FF,invalid,0201060303E1FF1200001234,1001,field6\n"), | |||||
| shouldProcess: false, // Should fail on ParseInt | |||||
| }, | |||||
| { | |||||
| name: "CSV with non-numeric field6", | |||||
| topicName: []byte("presence/gateway-007"), | |||||
| message: []byte("timestamp,AA:BB:CC:DD:EE:FF,-65,0201060303E1FF1200001234,1001,invalid\n"), | |||||
| shouldProcess: false, // Should fail on Atoi | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| mockWriter := &MockKafkaWriter{ | |||||
| Messages: make([]kafka.Message, 0), | |||||
| ShouldFail: false, | |||||
| } | |||||
| // Note: The CSV handler in the original code has an os.Exit(2) which makes it untestable | |||||
| // This test will fail due to os.Exit, but demonstrates the intended test structure | |||||
| // In a real scenario, you'd want to refactor the code to avoid os.Exit | |||||
| defer func() { | |||||
| if r := recover(); r != nil { | |||||
| // Expected due to os.Exit in original code | |||||
| if tt.shouldProcess { | |||||
| t.Errorf("MqttHandler() should not panic for valid CSV input: %v", r) | |||||
| } | |||||
| } | |||||
| }() | |||||
| // This will panic due to os.Exit(2) in the original code when field6 is invalid | |||||
| // In a real refactor, you'd replace os.Exit with error return | |||||
| if !tt.shouldProcess && string(tt.message) == "timestamp,AA:BB:CC:DD:EE:FF,-65,0201060303E1FF1200001234,1001,invalid\n" { | |||||
| // Skip the case that will definitely panic | |||||
| return | |||||
| } | |||||
| MqttHandler(mockWriter, tt.topicName, tt.message) | |||||
| // CSV processing doesn't write to Kafka in the current implementation | |||||
| if len(mockWriter.Messages) != 0 { | |||||
| t.Errorf("MqttHandler() CSV processing should not write to Kafka, but wrote %d messages", len(mockWriter.Messages)) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestParseButtonState(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| raw string | |||||
| expected int64 | |||||
| }{ | |||||
| { | |||||
| name: "Ingics button format - minimal length", | |||||
| raw: "0201060303E1FF12", | |||||
| expected: 0, // Too short for button field | |||||
| }, | |||||
| { | |||||
| name: "Ingics button format - exact length", | |||||
| raw: "0201060303E1FF123456", | |||||
| expected: 0x3456, // 13398 in decimal | |||||
| }, | |||||
| { | |||||
| name: "Ingics button format - longer", | |||||
| raw: "0201060303E1FF12000012345678AB", | |||||
| expected: 0x78AB, // 30891 in decimal | |||||
| }, | |||||
| { | |||||
| name: "Ingics button format - zero button", | |||||
| raw: "0201060303E1FF1200000000", | |||||
| expected: 0, | |||||
| }, | |||||
| { | |||||
| name: "Ingics button format - max button", | |||||
| raw: "0201060303E1FF12FFFFFFFF", | |||||
| expected: 0xFFFF, // 65535 in decimal | |||||
| }, | |||||
| { | |||||
| name: "Minew button format - minimal length", | |||||
| raw: "02010612FF590", | |||||
| expected: 0, // Too short for counter field | |||||
| }, | |||||
| { | |||||
| name: "Minew button format - exact length", | |||||
| raw: "02010612FF590112", | |||||
| expected: 0x12, // 18 in decimal | |||||
| }, | |||||
| { | |||||
| name: "Minew button format - longer", | |||||
| raw: "02010612FF5901C0012345678", | |||||
| expected: 0x78, // 120 in decimal | |||||
| }, | |||||
| { | |||||
| name: "Minew button format - zero counter", | |||||
| raw: "02010612FF5901C000", | |||||
| expected: 0, | |||||
| }, | |||||
| { | |||||
| name: "Minew button format - max counter", | |||||
| raw: "02010612FF5901C0FF", | |||||
| expected: 0xFF, // 255 in decimal | |||||
| }, | |||||
| { | |||||
| name: "Invalid prefix", | |||||
| raw: "0201060303E1FE120000123456", | |||||
| expected: 0, | |||||
| }, | |||||
| { | |||||
| name: "Invalid hex characters", | |||||
| raw: "0201060303E1FF12ZZZZ", | |||||
| expected: 0, | |||||
| }, | |||||
| { | |||||
| name: "Empty string", | |||||
| raw: "", | |||||
| expected: 0, | |||||
| }, | |||||
| { | |||||
| name: "Single character", | |||||
| raw: "0", | |||||
| expected: 0, | |||||
| }, | |||||
| { | |||||
| name: "Non-hex characters mixed", | |||||
| raw: "0201060303E1FF12GHIJ", | |||||
| expected: 0, | |||||
| }, | |||||
| { | |||||
| name: "Lowercase hex", | |||||
| raw: "0201060303e1ff120000123456", | |||||
| expected: 0, // Should be converted to uppercase | |||||
| }, | |||||
| { | |||||
| name: "Mixed case hex", | |||||
| raw: "0201060303e1FF120000123456", | |||||
| expected: 0x3456, // Should work after case conversion | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result := parseButtonState(tt.raw) | |||||
| if result != tt.expected { | |||||
| t.Errorf("parseButtonState() = %v, expected %v", result, tt.expected) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestParseButtonStateEdgeCases(t *testing.T) { | |||||
| // Test that Ingics format is checked before Minew format | |||||
| ingicsRaw := "0201060303E1FF123456" | |||||
| minewRaw := "02010612FF590112" | |||||
| ingicsResult := parseButtonState(ingicsRaw) | |||||
| minewResult := parseButtonState(minewRaw) | |||||
| // Both should work, but Ingics should use bytes 34:38, Minew should use bytes 22:24 | |||||
| if ingicsResult != 0x3456 { | |||||
| t.Errorf("parseButtonState() Ingics format failed: got %v, want %v", ingicsResult, 0x3456) | |||||
| } | |||||
| if minewResult != 0x12 { | |||||
| t.Errorf("parseButtonState() Minew format failed: got %v, want %v", minewResult, 0x12) | |||||
| } | |||||
| // Test with overlapping patterns (unlikely but good to test) | |||||
| overlapRaw := "0201060303E1FF122FF590112" | |||||
| overlapResult := parseButtonState(overlapRaw) | |||||
| // Should match Ingics pattern and use bytes 34:38 | |||||
| expectedOverlap := int64(0) // There are no bytes 34:38 in this string | |||||
| if overlapResult != expectedOverlap { | |||||
| t.Errorf("parseButtonState() overlap case: got %v, want %v", overlapResult, expectedOverlap) | |||||
| } | |||||
| } | |||||
| func TestHostnameExtraction(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| topicName []byte | |||||
| expectedHost string | |||||
| }{ | |||||
| { | |||||
| name: "Simple topic", | |||||
| topicName: []byte("presence/gateway-001"), | |||||
| expectedHost: "gateway-001", | |||||
| }, | |||||
| { | |||||
| name: "Topic with multiple segments", | |||||
| topicName: []byte("home/office/floor3/gateway-A123"), | |||||
| expectedHost: "home", | |||||
| }, | |||||
| { | |||||
| name: "Topic with numbers only", | |||||
| topicName: []byte("12345"), | |||||
| expectedHost: "12345", | |||||
| }, | |||||
| { | |||||
| name: "Single segment topic", | |||||
| topicName: []byte("singlegateway"), | |||||
| expectedHost: "singlegateway", | |||||
| }, | |||||
| { | |||||
| name: "Topic with empty segments", | |||||
| topicName: []byte("//gateway//001//"), | |||||
| expectedHost: "", // First non-empty segment after split | |||||
| }, | |||||
| { | |||||
| name: "Empty topic", | |||||
| topicName: []byte(""), | |||||
| expectedHost: "", | |||||
| }, | |||||
| { | |||||
| name: "Topic with special characters", | |||||
| topicName: []byte("presence/gateway-with-dashes_and_underscores"), | |||||
| expectedHost: "presence", | |||||
| }, | |||||
| { | |||||
| name: "Topic starting with slash", | |||||
| topicName: []byte("/presence/gateway-001"), | |||||
| expectedHost: "", // First segment is empty | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| mockWriter := &MockKafkaWriter{ | |||||
| Messages: make([]kafka.Message, 0), | |||||
| } | |||||
| // Create a simple JSON message that will be processed | |||||
| message := []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"}]`) | |||||
| MqttHandler(mockWriter, tt.topicName, message) | |||||
| if len(mockWriter.Messages) > 0 { | |||||
| var adv model.BeaconAdvertisement | |||||
| err := json.Unmarshal(mockWriter.Messages[0].Value, &adv) | |||||
| if err != nil { | |||||
| t.Errorf("Failed to unmarshal Kafka message: %v", err) | |||||
| return | |||||
| } | |||||
| if adv.Hostname != tt.expectedHost { | |||||
| t.Errorf("Hostname extraction = %v, expected %v", adv.Hostname, tt.expectedHost) | |||||
| } | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestKafkaWriteFailure(t *testing.T) { | |||||
| mockWriter := &MockKafkaWriter{ | |||||
| Messages: make([]kafka.Message, 0), | |||||
| ShouldFail: true, | |||||
| } | |||||
| topicName := []byte("presence/test-gateway") | |||||
| message := []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"}]`) | |||||
| // This should handle the write error gracefully (it sleeps for 1 second) | |||||
| MqttHandler(mockWriter, topicName, message) | |||||
| // No messages should have been written successfully | |||||
| if len(mockWriter.Messages) != 0 { | |||||
| t.Errorf("Expected 0 messages on write failure, got %d", len(mockWriter.Messages)) | |||||
| } | |||||
| // Should have attempted to write | |||||
| if mockWriter.WriteCount != 1 { | |||||
| t.Errorf("Expected 1 write attempt, got %d", mockWriter.WriteCount) | |||||
| } | |||||
| } | |||||
| func TestMessageMarshaling(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| reading model.RawReading | |||||
| }{ | |||||
| { | |||||
| name: "Standard beacon reading", | |||||
| reading: model.RawReading{ | |||||
| Timestamp: "2023-01-01T00:00:00Z", | |||||
| Type: "Beacon", | |||||
| MAC: "AA:BB:CC:DD:EE:FF", | |||||
| RSSI: -65, | |||||
| RawData: "0201060303E1FF1200001234", | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "Beacon with special characters in MAC", | |||||
| reading: model.RawReading{ | |||||
| Timestamp: "2023-01-01T00:00:00Z", | |||||
| Type: "Beacon", | |||||
| MAC: "AA:BB:CC:DD:EE:FF", | |||||
| RSSI: -75, | |||||
| RawData: "02010612FF5901C0012345678", | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "Beacon with extreme RSSI values", | |||||
| reading: model.RawReading{ | |||||
| Timestamp: "2023-01-01T00:00:00Z", | |||||
| Type: "Beacon", | |||||
| MAC: "11:22:33:44:55:66", | |||||
| RSSI: -120, | |||||
| RawData: "0201060303E1FF120000ABCD", | |||||
| }, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| mockWriter := &MockKafkaWriter{ | |||||
| Messages: make([]kafka.Message, 0), | |||||
| } | |||||
| // Create JSON array with our test reading | |||||
| readings := []model.RawReading{tt.reading} | |||||
| message, err := json.Marshal(readings) | |||||
| if err != nil { | |||||
| t.Fatalf("Failed to marshal test reading: %v", err) | |||||
| } | |||||
| topicName := []byte("presence/test-gateway") | |||||
| MqttHandler(mockWriter, topicName, message) | |||||
| if len(mockWriter.Messages) != 1 { | |||||
| t.Errorf("Expected 1 message, got %d", len(mockWriter.Messages)) | |||||
| return | |||||
| } | |||||
| // Verify the message can be unmarshaled back to BeaconAdvertisement | |||||
| var adv model.BeaconAdvertisement | |||||
| err = json.Unmarshal(mockWriter.Messages[0].Value, &adv) | |||||
| if err != nil { | |||||
| t.Errorf("Failed to unmarshal Kafka message: %v", err) | |||||
| return | |||||
| } | |||||
| // Verify fields match the original reading | |||||
| if adv.MAC != tt.reading.MAC { | |||||
| t.Errorf("MAC mismatch: got %v, want %v", adv.MAC, tt.reading.MAC) | |||||
| } | |||||
| if adv.RSSI != int64(tt.reading.RSSI) { | |||||
| t.Errorf("RSSI mismatch: got %v, want %v", adv.RSSI, tt.reading.RSSI) | |||||
| } | |||||
| if adv.Data != tt.reading.RawData { | |||||
| t.Errorf("Data mismatch: got %v, want %v", adv.Data, tt.reading.RawData) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| // Benchmark tests | |||||
| func BenchmarkParseButtonState(b *testing.B) { | |||||
| raw := "0201060303E1FF12000012345678AB" | |||||
| for i := 0; i < b.N; i++ { | |||||
| parseButtonState(raw) | |||||
| } | |||||
| } | |||||
| func BenchmarkMqttHandlerJSON(b *testing.B) { | |||||
| mockWriter := &MockKafkaWriter{ | |||||
| Messages: make([]kafka.Message, 0), | |||||
| } | |||||
| topicName := []byte("presence/benchmark-gateway") | |||||
| message := []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"}]`) | |||||
| b.ResetTimer() | |||||
| for i := 0; i < b.N; i++ { | |||||
| MqttHandler(mockWriter, topicName, message) | |||||
| mockWriter.Messages = mockWriter.Messages[:0] // Reset messages | |||||
| } | |||||
| } | |||||
| func BenchmarkMqttHandlerMultipleBeacons(b *testing.B) { | |||||
| mockWriter := &MockKafkaWriter{ | |||||
| Messages: make([]kafka.Message, 0), | |||||
| } | |||||
| topicName := []byte("presence/benchmark-gateway") | |||||
| message := []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"},{"timestamp":"2023-01-01T00:00:01Z","type":"Beacon","mac":"11:22:33:44:55:66","rssi":-75,"rawData":"02010612FF5901C0012345678"}]`) | |||||
| b.ResetTimer() | |||||
| for i := 0; i < b.N; i++ { | |||||
| MqttHandler(mockWriter, topicName, message) | |||||
| mockWriter.Messages = mockWriter.Messages[:0] // Reset messages | |||||
| } | |||||
| } | |||||
| @@ -1,644 +0,0 @@ | |||||
| package model | |||||
| import ( | |||||
| "testing" | |||||
| ) | |||||
| func TestBeaconEventHash(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| be BeaconEvent | |||||
| expected []byte | |||||
| }{ | |||||
| { | |||||
| name: "Basic beacon event", | |||||
| be: BeaconEvent{ | |||||
| ID: "beacon-1", | |||||
| Name: "Test Beacon", | |||||
| Type: "Ingics", | |||||
| Battery: 1000, | |||||
| Event: 1, | |||||
| }, | |||||
| expected: nil, // We'll test that it produces a consistent hash | |||||
| }, | |||||
| { | |||||
| name: "Same beacon with different battery should produce same hash", | |||||
| be: BeaconEvent{ | |||||
| ID: "beacon-1", | |||||
| Name: "Test Beacon", | |||||
| Type: "Ingics", | |||||
| Battery: 1009, // 1000 + 9, should round to 1000 | |||||
| Event: 1, | |||||
| }, | |||||
| expected: nil, | |||||
| }, | |||||
| { | |||||
| name: "Different ID should produce different hash", | |||||
| be: BeaconEvent{ | |||||
| ID: "beacon-2", | |||||
| Name: "Test Beacon", | |||||
| Type: "Ingics", | |||||
| Battery: 1000, | |||||
| Event: 1, | |||||
| }, | |||||
| expected: nil, | |||||
| }, | |||||
| { | |||||
| name: "Different event should produce different hash", | |||||
| be: BeaconEvent{ | |||||
| ID: "beacon-1", | |||||
| Name: "Test Beacon", | |||||
| Type: "Ingics", | |||||
| Battery: 1000, | |||||
| Event: 2, | |||||
| }, | |||||
| expected: nil, | |||||
| }, | |||||
| { | |||||
| name: "Zero values", | |||||
| be: BeaconEvent{ | |||||
| ID: "", | |||||
| Name: "", | |||||
| Type: "", | |||||
| Battery: 0, | |||||
| Event: 0, | |||||
| }, | |||||
| expected: nil, | |||||
| }, | |||||
| { | |||||
| name: "Special characters", | |||||
| be: BeaconEvent{ | |||||
| ID: "beacon!@#$%^&*()", | |||||
| Name: "Test\nBeacon\tWith\tTabs", | |||||
| Type: "Special-Type_123", | |||||
| Battery: 1000, | |||||
| Event: 1, | |||||
| }, | |||||
| expected: nil, | |||||
| }, | |||||
| } | |||||
| // Test that Hash produces consistent results | |||||
| hashes := make([][]byte, len(tests)) | |||||
| for i, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| hashes[i] = tt.be.Hash() | |||||
| // Hash should always be 32 bytes for SHA256 | |||||
| if len(hashes[i]) != 32 { | |||||
| t.Errorf("Hash() length = %v, expected 32", len(hashes[i])) | |||||
| } | |||||
| // Hash should not be empty unless all fields are empty | |||||
| if len(hashes[i]) == 0 && (tt.be.ID != "" || tt.be.Name != "" || tt.be.Type != "") { | |||||
| t.Errorf("Hash() should not be empty for non-empty beacon event") | |||||
| } | |||||
| }) | |||||
| } | |||||
| // Test that same input produces same hash | |||||
| be1 := BeaconEvent{ | |||||
| ID: "test-beacon", | |||||
| Name: "Test", | |||||
| Type: "Ingics", | |||||
| Battery: 1000, | |||||
| Event: 1, | |||||
| } | |||||
| hash1 := be1.Hash() | |||||
| hash2 := be1.Hash() | |||||
| if string(hash1) != string(hash2) { | |||||
| t.Errorf("Hash() should be deterministic: %v != %v", hash1, hash2) | |||||
| } | |||||
| // Test battery rounding | |||||
| beBattery1 := BeaconEvent{ | |||||
| ID: "test-beacon", | |||||
| Name: "Test", | |||||
| Type: "Ingics", | |||||
| Battery: 1005, // Should round to 1000 | |||||
| Event: 1, | |||||
| } | |||||
| beBattery2 := BeaconEvent{ | |||||
| ID: "test-beacon", | |||||
| Name: "Test", | |||||
| Type: "Ingics", | |||||
| Battery: 1000, | |||||
| Event: 1, | |||||
| } | |||||
| hashBattery1 := beBattery1.Hash() | |||||
| hashBattery2 := beBattery2.Hash() | |||||
| if string(hashBattery1) != string(hashBattery2) { | |||||
| t.Errorf("Hash() with battery rounding should be same: %v != %v", hashBattery1, hashBattery2) | |||||
| } | |||||
| } | |||||
| func TestBeaconEventToJSON(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| be BeaconEvent | |||||
| expectedError bool | |||||
| }{ | |||||
| { | |||||
| name: "Valid beacon event", | |||||
| be: BeaconEvent{ | |||||
| ID: "beacon-1", | |||||
| Name: "Test Beacon", | |||||
| Type: "Ingics", | |||||
| Battery: 1000, | |||||
| Event: 1, | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Empty beacon event", | |||||
| be: BeaconEvent{}, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Beacon with special characters", | |||||
| be: BeaconEvent{ | |||||
| ID: "beacon-with-special-chars!@#$%", | |||||
| Name: "Name with unicode: 测试", | |||||
| Type: "Type-With-Dashes_and_underscores", | |||||
| Battery: 12345, | |||||
| Event: 255, | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Beacon with maximum values", | |||||
| be: BeaconEvent{ | |||||
| ID: "max-beacon", | |||||
| Name: "Maximum Values Test", | |||||
| Type: "MaxType", | |||||
| Battery: 0xFFFFFFFF, // Max uint32 | |||||
| Event: 2147483647, // Max int32 | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Zero values", | |||||
| be: BeaconEvent{ | |||||
| ID: "", | |||||
| Name: "", | |||||
| Type: "", | |||||
| Battery: 0, | |||||
| Event: 0, | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result, err := tt.be.ToJSON() | |||||
| if (err != nil) != tt.expectedError { | |||||
| t.Errorf("ToJSON() error = %v, expectedError %v", err, tt.expectedError) | |||||
| return | |||||
| } | |||||
| if !tt.expectedError { | |||||
| // Result should not be nil | |||||
| if result == nil { | |||||
| t.Error("ToJSON() result should not be nil") | |||||
| return | |||||
| } | |||||
| // Result should not be empty JSON | |||||
| if string(result) == "null" { | |||||
| t.Error("ToJSON() result should not be 'null'") | |||||
| } | |||||
| // Basic JSON validation - should start and end with braces for object | |||||
| if len(result) > 0 && result[0] != '{' && result[0] != '[' { | |||||
| t.Errorf("ToJSON() result should be valid JSON, got: %s", string(result)) | |||||
| } | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestConvertStructToMap(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| input any | |||||
| expectedError bool | |||||
| }{ | |||||
| { | |||||
| name: "Valid BeaconEvent", | |||||
| input: BeaconEvent{ID: "test", Type: "Ingics"}, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Valid HTTPLocation", | |||||
| input: HTTPLocation{Method: "POST", ID: "test"}, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Valid struct", | |||||
| input: struct{ Name string }{Name: "test"}, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Nil input", | |||||
| input: nil, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "String input", | |||||
| input: "test string", | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Map input", | |||||
| input: map[string]any{"test": "value"}, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Slice input", | |||||
| input: []string{"test1", "test2"}, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Complex struct with nested structures", | |||||
| input: struct { | |||||
| SimpleField string | |||||
| Nested struct { | |||||
| InnerField int | |||||
| } | |||||
| SliceField []string | |||||
| MapField map[string]any | |||||
| }{ | |||||
| SimpleField: "test", | |||||
| Nested: struct{ InnerField int }{InnerField: 123}, | |||||
| SliceField: []string{"a", "b", "c"}, | |||||
| MapField: map[string]any{"key": "value"}, | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Struct with channel field", | |||||
| input: struct{ Ch chan int }{Ch: make(chan int)}, | |||||
| expectedError: true, // Channels cannot be marshaled to JSON | |||||
| }, | |||||
| { | |||||
| name: "Struct with function field", | |||||
| input: struct{ Func func() }{Func: func() {}}, | |||||
| expectedError: true, // Functions cannot be marshaled to JSON | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result, err := convertStructToMap(tt.input) | |||||
| if (err != nil) != tt.expectedError { | |||||
| t.Errorf("convertStructToMap() error = %v, expectedError %v", err, tt.expectedError) | |||||
| return | |||||
| } | |||||
| if !tt.expectedError { | |||||
| // Result should be a map | |||||
| if result == nil && tt.input != nil { | |||||
| t.Error("convertStructToMap() result should not be nil for non-nil input") | |||||
| } | |||||
| // For valid inputs, result should be a map | |||||
| if tt.input != nil { | |||||
| if _, ok := result.(map[string]any); !ok && result != nil { | |||||
| t.Errorf("convertStructToMap() result should be a map[string]any, got %T", result) | |||||
| } | |||||
| } | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestHTTPLocationRedisHashable(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| location HTTPLocation | |||||
| expectedError bool | |||||
| }{ | |||||
| { | |||||
| name: "Valid location", | |||||
| location: HTTPLocation{ | |||||
| Method: "POST", | |||||
| PreviousConfidentLocation: "room1", | |||||
| Distance: 5.5, | |||||
| ID: "beacon-123", | |||||
| Location: "room2", | |||||
| LastSeen: 1634567890, | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Minimal location", | |||||
| location: HTTPLocation{ | |||||
| Method: "GET", | |||||
| ID: "beacon-1", | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Zero values", | |||||
| location: HTTPLocation{}, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Location with special characters", | |||||
| location: HTTPLocation{ | |||||
| Method: "CUSTOM", | |||||
| ID: "beacon-with-special-chars!@#$%", | |||||
| Location: "Room-with-unicode: 测试", | |||||
| Distance: -123.456, // Negative distance | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Maximum values", | |||||
| location: HTTPLocation{ | |||||
| Method: "MAX", | |||||
| PreviousConfidentLocation: "max-room", | |||||
| Distance: 9223372036854775807, // Max int64 as float64 | |||||
| ID: "max-beacon-id-12345678901234567890", | |||||
| Location: "max-location-name", | |||||
| LastSeen: 9223372036854775807, // Max int64 | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result, err := tt.location.RedisHashable() | |||||
| if (err != nil) != tt.expectedError { | |||||
| t.Errorf("HTTPLocation.RedisHashable() error = %v, expectedError %v", err, tt.expectedError) | |||||
| return | |||||
| } | |||||
| if !tt.expectedError { | |||||
| // Result should be a map | |||||
| if result == nil { | |||||
| t.Error("HTTPLocation.RedisHashable() result should not be nil") | |||||
| return | |||||
| } | |||||
| resultMap, ok := result.(map[string]any) | |||||
| if !ok { | |||||
| t.Errorf("HTTPLocation.RedisHashable() result should be a map[string]any, got %T", result) | |||||
| return | |||||
| } | |||||
| // Check that expected fields are present | |||||
| expectedFields := []string{"method", "previous_confident_location", "distance", "id", "location", "last_seen"} | |||||
| for _, field := range expectedFields { | |||||
| if _, exists := resultMap[field]; !exists { | |||||
| t.Errorf("HTTPLocation.RedisHashable() missing expected field: %s", field) | |||||
| } | |||||
| } | |||||
| // Check JSON tags are respected | |||||
| if _, exists := resultMap["Method"]; exists { | |||||
| t.Error("HTTPLocation.RedisHashable() should use JSON field names, not struct field names") | |||||
| } | |||||
| if _, exists := resultMap["method"]; !exists { | |||||
| t.Error("HTTPLocation.RedisHashable() should contain 'method' field (JSON tag)") | |||||
| } | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestBeaconEventRedisHashable(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| be BeaconEvent | |||||
| expectedError bool | |||||
| }{ | |||||
| { | |||||
| name: "Valid beacon event", | |||||
| be: BeaconEvent{ | |||||
| ID: "beacon-123", | |||||
| Name: "Test Beacon", | |||||
| Type: "Ingics", | |||||
| Battery: 1000, | |||||
| Event: 1, | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Minimal beacon event", | |||||
| be: BeaconEvent{ | |||||
| ID: "test", | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Zero values", | |||||
| be: BeaconEvent{}, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Beacon event with special characters", | |||||
| be: BeaconEvent{ | |||||
| ID: "beacon-!@#$%^&*()", | |||||
| Name: "Name with unicode: 测试", | |||||
| Type: "Special-Type_123", | |||||
| Battery: 12345, | |||||
| Event: 255, | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| { | |||||
| name: "Maximum values", | |||||
| be: BeaconEvent{ | |||||
| ID: "max-beacon-id", | |||||
| Name: "Maximum Values Test", | |||||
| Type: "MaxType", | |||||
| Battery: 0xFFFFFFFF, // Max uint32 | |||||
| Event: 2147483647, // Max int32 | |||||
| }, | |||||
| expectedError: false, | |||||
| }, | |||||
| } | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result, err := tt.be.RedisHashable() | |||||
| if (err != nil) != tt.expectedError { | |||||
| t.Errorf("BeaconEvent.RedisHashable() error = %v, expectedError %v", err, tt.expectedError) | |||||
| return | |||||
| } | |||||
| if !tt.expectedError { | |||||
| // Result should be a map | |||||
| if result == nil { | |||||
| t.Error("BeaconEvent.RedisHashable() result should not be nil") | |||||
| return | |||||
| } | |||||
| resultMap, ok := result.(map[string]any) | |||||
| if !ok { | |||||
| t.Errorf("BeaconEvent.RedisHashable() result should be a map[string]any, got %T", result) | |||||
| return | |||||
| } | |||||
| // Check that expected fields are present | |||||
| expectedFields := []string{"name", "id", "type", "battery", "event"} | |||||
| for _, field := range expectedFields { | |||||
| if _, exists := resultMap[field]; !exists { | |||||
| t.Errorf("BeaconEvent.RedisHashable() missing expected field: %s", field) | |||||
| } | |||||
| } | |||||
| // Check JSON tags are respected (BeaconEvent fields are not tagged with JSON, so field names should be lowercase) | |||||
| if _, exists := resultMap["Name"]; exists { | |||||
| t.Error("BeaconEvent.RedisHashable() should use lowercase field names") | |||||
| } | |||||
| if _, exists := resultMap["name"]; !exists { | |||||
| t.Error("BeaconEvent.RedisHashable() should contain 'name' field") | |||||
| } | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestHashConsistencyWithBatteryRounding(t *testing.T) { | |||||
| // Test that Hash() is consistent with battery rounding | |||||
| testCases := []struct { | |||||
| battery1 uint32 | |||||
| battery2 uint32 | |||||
| shouldMatch bool | |||||
| }{ | |||||
| {1000, 1009, true}, // Same rounding range | |||||
| {1000, 1010, false}, // Different rounding range | |||||
| {0, 9, true}, // Zero range | |||||
| {100, 104, true}, // Same range (100-109 rounds to 100) | |||||
| {100, 110, false}, // Different ranges | |||||
| {4294967295, 4294967289, true}, // Max value range | |||||
| } | |||||
| for i, tc := range testCases { | |||||
| t.Run(fmt.Sprintf("BatteryRoundCase_%d", i), func(t *testing.T) { | |||||
| be1 := BeaconEvent{ | |||||
| ID: "test-beacon", | |||||
| Name: "Test", | |||||
| Type: "Ingics", | |||||
| Battery: tc.battery1, | |||||
| Event: 1, | |||||
| } | |||||
| be2 := BeaconEvent{ | |||||
| ID: "test-beacon", | |||||
| Name: "Test", | |||||
| Type: "Ingics", | |||||
| Battery: tc.battery2, | |||||
| Event: 1, | |||||
| } | |||||
| hash1 := be1.Hash() | |||||
| hash2 := be2.Hash() | |||||
| hashesMatch := string(hash1) == string(hash2) | |||||
| if hashesMatch != tc.shouldMatch { | |||||
| t.Errorf("Hash consistency mismatch: battery1=%d, battery2=%d, hashesMatch=%v, shouldMatch=%v", | |||||
| tc.battery1, tc.battery2, hashesMatch, tc.shouldMatch) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestJSONMarshalUnmarshalRoundtrip(t *testing.T) { | |||||
| original := BeaconEvent{ | |||||
| ID: "roundtrip-test", | |||||
| Name: "Roundtrip Test", | |||||
| Type: "TestType", | |||||
| Battery: 12345, | |||||
| Event: 42, | |||||
| } | |||||
| // Test that ToJSON produces valid JSON that can be unmarshaled back | |||||
| jsonData, err := original.ToJSON() | |||||
| if err != nil { | |||||
| t.Fatalf("ToJSON() error: %v", err) | |||||
| } | |||||
| var unmarshaled BeaconEvent | |||||
| err = json.Unmarshal(jsonData, &unmarshaled) | |||||
| if err != nil { | |||||
| t.Fatalf("json.Unmarshal() error: %v", err) | |||||
| } | |||||
| // Verify roundtrip integrity | |||||
| if unmarshaled.ID != original.ID { | |||||
| t.Errorf("Roundtrip ID mismatch: got %v, want %v", unmarshaled.ID, original.ID) | |||||
| } | |||||
| if unmarshaled.Name != original.Name { | |||||
| t.Errorf("Roundtrip Name mismatch: got %v, want %v", unmarshaled.Name, original.Name) | |||||
| } | |||||
| if unmarshaled.Type != original.Type { | |||||
| t.Errorf("Roundtrip Type mismatch: got %v, want %v", unmarshaled.Type, original.Type) | |||||
| } | |||||
| if unmarshaled.Battery != original.Battery { | |||||
| t.Errorf("Roundtrip Battery mismatch: got %v, want %v", unmarshaled.Battery, original.Battery) | |||||
| } | |||||
| if unmarshaled.Event != original.Event { | |||||
| t.Errorf("Roundtrip Event mismatch: got %v, want %v", unmarshaled.Event, original.Event) | |||||
| } | |||||
| } | |||||
| // Benchmark tests | |||||
| func BenchmarkHash(b *testing.B) { | |||||
| be := BeaconEvent{ | |||||
| ID: "benchmark-beacon", | |||||
| Name: "Benchmark Test", | |||||
| Type: "Ingics", | |||||
| Battery: 1000, | |||||
| Event: 1, | |||||
| } | |||||
| for i := 0; i < b.N; i++ { | |||||
| be.Hash() | |||||
| } | |||||
| } | |||||
| func BenchmarkToJSON(b *testing.B) { | |||||
| be := BeaconEvent{ | |||||
| ID: "benchmark-beacon", | |||||
| Name: "Benchmark Test", | |||||
| Type: "Ingics", | |||||
| Battery: 1000, | |||||
| Event: 1, | |||||
| } | |||||
| for i := 0; i < b.N; i++ { | |||||
| be.ToJSON() | |||||
| } | |||||
| } | |||||
| func BenchmarkConvertStructToMap(b *testing.B) { | |||||
| be := BeaconEvent{ | |||||
| ID: "benchmark-beacon", | |||||
| Name: "Benchmark Test", | |||||
| Type: "Ingics", | |||||
| Battery: 1000, | |||||
| Event: 1, | |||||
| } | |||||
| for i := 0; i < b.N; i++ { | |||||
| convertStructToMap(be) | |||||
| } | |||||
| } | |||||