├── .gitignore ├── .gitattributes ├── output_3_1.png ├── output_4_0.png ├── output_5_1.png ├── animations ├── loci.gif ├── locus.gif └── tdoa.gif ├── lorawantm_specification_-v1.1.pdf ├── LICENSE ├── tex ├── 2103f85b8b1477f430fc407cad462224.svg ├── 4ad941990ade99427ec9730e46ddcdd4.svg ├── deceeaf6940a8c7a5a02373728002b0f.svg ├── 332cc365a4987aacce0ead01b8bdcc0b.svg ├── 55a049b8f161ae7cfeb0197d75aff967.svg ├── 6df6ddacc987bd7a5070beafef47fcc1.svg ├── 23eda6b0b8aec6ac41ed8b7cb41c0942.svg ├── 02ab12d0013b89c8edc7f0f2662fa7a9.svg ├── 19e3f7018228f8a8c6559d0ea5500aa2.svg ├── efcf8d472ecdd2ea56d727b5746100e3.svg ├── 3177e934cf575c08431076a1a5479ba5.svg ├── ab518d5c66f0b911c92e35acce6ab925.svg ├── 127ea00ccd4d4c46555e5e370bbab69d.svg ├── 1447b57eb35586ec64a4d8d2fbb653e0.svg ├── 81e4c10c4a5aefbf7d79a0aa3c401941.svg ├── 6c775160070f36a222c6974886b84a63.svg ├── 14b25fe693db4861d3fec47ef3796836.svg ├── 1a9f68fae14a5f2c3944f20c7746c582.svg ├── 230a4b7a9ac468f0ec9acbf28016d276.svg ├── 94cf9bfabcb071b6b432112626ffd8a3.svg ├── 7b40691d3fcdab7f1174d83e2de738c6.svg ├── a4c6de18edfc65fe4376b9438c00ec4b.svg ├── ce931c5e3797ea59637a36c0ee4a2e04.svg ├── 7146d7ec04600031453049bed6b8a9b0.svg ├── 8182282dad754f66fe56591253699990.svg └── 10b4e50a7a481d84f1c0d380611ed3d3.svg ├── multilaterate.py └── readme.tex.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .ipynb_checkpoints/ 3 | *.pyc 4 | *.DS_Store 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /output_3_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jurasofish/multilateration/HEAD/output_3_1.png -------------------------------------------------------------------------------- /output_4_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jurasofish/multilateration/HEAD/output_4_0.png -------------------------------------------------------------------------------- /output_5_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jurasofish/multilateration/HEAD/output_5_1.png -------------------------------------------------------------------------------- /animations/loci.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jurasofish/multilateration/HEAD/animations/loci.gif -------------------------------------------------------------------------------- /animations/locus.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jurasofish/multilateration/HEAD/animations/locus.gif -------------------------------------------------------------------------------- /animations/tdoa.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jurasofish/multilateration/HEAD/animations/tdoa.gif -------------------------------------------------------------------------------- /lorawantm_specification_-v1.1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jurasofish/multilateration/HEAD/lorawantm_specification_-v1.1.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 michael 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tex/2103f85b8b1477f430fc407cad462224.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tex/4ad941990ade99427ec9730e46ddcdd4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tex/deceeaf6940a8c7a5a02373728002b0f.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tex/332cc365a4987aacce0ead01b8bdcc0b.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tex/55a049b8f161ae7cfeb0197d75aff967.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tex/6df6ddacc987bd7a5070beafef47fcc1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tex/23eda6b0b8aec6ac41ed8b7cb41c0942.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tex/02ab12d0013b89c8edc7f0f2662fa7a9.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tex/19e3f7018228f8a8c6559d0ea5500aa2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tex/efcf8d472ecdd2ea56d727b5746100e3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tex/3177e934cf575c08431076a1a5479ba5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tex/ab518d5c66f0b911c92e35acce6ab925.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tex/127ea00ccd4d4c46555e5e370bbab69d.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tex/1447b57eb35586ec64a4d8d2fbb653e0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tex/81e4c10c4a5aefbf7d79a0aa3c401941.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tex/6c775160070f36a222c6974886b84a63.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tex/14b25fe693db4861d3fec47ef3796836.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tex/1a9f68fae14a5f2c3944f20c7746c582.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /multilaterate.py: -------------------------------------------------------------------------------- 1 | ''' Draw loci corresponding to radio transmission multilateration. 2 | 3 | This program plots loci of possible transmitter locations for the scenario 4 | where there are radio towers 5 | at known locations and a transmitter at an unknown location. The radio 6 | towers accurately timestamp when they receive the transmission, allowing 7 | time difference of arrival (TDOA) to be determined. This forms a 8 | multilateration problem, producing n-1 loci where n is the number 9 | of towers. 10 | Only the 2-dimensional case is considered. It is assumed that the effect 11 | on TDOA fron the vertical component of the transmission path is negligible. 12 | For example, a path that is 5km horizontally and 500m vertically is 13 | in total 5.025km ((5**2 + 0.5**2)**0.5). Depending on clock noise this could 14 | be considered negligible. 15 | 16 | ''' 17 | 18 | import numpy as np 19 | import math 20 | 21 | 22 | def get_locus(tower_1, tower_2, time_1, time_2, v, delta_d, max_d): 23 | ''' Return a locus in x, y given two towers and their recieve times. 24 | 25 | Given two towers at locations tower_1 and tower_2, a message transmitted 26 | at some arbitrary time at location (x_t, y_t), and the times at which 27 | the towers received the transmission, the set of possible 28 | locations of the transmission is defined by the locus of the intersection 29 | of two circles with one circle around each tower and the difference in 30 | radius of the circles defined by the difference in receive tiemes 31 | of the transmission and the propogation speed of the transmission. 32 | 33 | Args: 34 | tower_1 (tuple): (x, y) of one tower. 35 | tower_2 (tuple): (x, y) of other tower. 36 | time_1 (float): Transmission recieve time at tower_1. 37 | time_2 (float): Transmission recieve time at tower_2. 38 | v (int): Speed of transmission propogation. 39 | delta_d (int): Metre increments to radii of circles when generating 40 | locus of circle intersection. 41 | max_d (int): Max distance a transmission will be from the tower that 42 | first received the transmission. This puts an upper bound on the 43 | radii of the circle, thus limiting the size of the locus to be 44 | near the towers. 45 | 46 | Returns 47 | list of form [x, y], with: 48 | x: list of x values of locus. 49 | y: list of y values of locus. 50 | ''' 51 | # two lines, x0/y0 and x1/y1 corresponding to the two intersections of the 52 | # circles. These will be concateneated at the end to form a single line. 53 | x0 = [] 54 | x1 = [] 55 | y0 = [] 56 | y1 = [] 57 | 58 | # The radii have constant difference of t_delta_d. "time delta difference" 59 | t_delta_d = abs(time_1 - time_2) * v 60 | 61 | # Determine which tower received the transmission first. 62 | if(time_1 < time_2): 63 | circle1 = (tower_1[0], tower_1[1], 0) 64 | circle2 = (tower_2[0], tower_2[1], t_delta_d) 65 | else: 66 | circle1 = (tower_2[0], tower_2[1], 0) 67 | circle2 = (tower_1[0], tower_1[1], t_delta_d) 68 | 69 | # Iterate over all potential radii. 70 | for _ in range(int(max_d)//int(delta_d)): 71 | intersect = circle_intersection(circle1, circle2) 72 | if(intersect is not None): 73 | x0.append(intersect[0][0]) 74 | x1.append(intersect[1][0]) 75 | y0.append(intersect[0][1]) 76 | y1.append(intersect[1][1]) 77 | 78 | circle1 = (circle1[0], circle1[1], circle1[2]+delta_d) 79 | circle2 = (circle2[0], circle2[1], circle2[2]+delta_d) 80 | 81 | # Reverse so the concatenated locus is continous. Could reverse only 82 | # x1/y1 instead if you wanted. 83 | x0 = list(reversed(x0)) 84 | y0 = list(reversed(y0)) 85 | 86 | # Concatenate 87 | x = x0 + x1 88 | y = y0 + y1 89 | 90 | return [x, y] 91 | 92 | 93 | def get_loci(rec_times, towers, v, delta_d, max_d): 94 | ''' Return a set of loci on which a transmission may have occurred. 95 | 96 | Args: 97 | rec_times (np.array 1D): The times at which the towers recieved 98 | the transmission, in seconds. Element i corresponds to tower i. 99 | towers (np.array 2D): Locations of towers. Tower i is located at 100 | (x, y) = (towers[i][0], towers[i][1]) 101 | v (int): Speed of transmission propogation. 102 | delta_d (int): Metre increments to radii of circles when generating 103 | locus of circle intersection. 104 | max_d (int): Max distance a transmission will be from the tower that 105 | first received the transmission. This puts an upper bound on the 106 | radii of the circle, thus limiting the size of the locus to be 107 | near the towers. 108 | 109 | Returns 110 | list of tuples, where each tuple contains a list of x and a list of 111 | y elements. 112 | ''' 113 | 114 | if(rec_times.shape[0] == 0): 115 | return [] # return no loci 116 | 117 | loci = [] 118 | 119 | # Tower that receives the transmission first. 120 | first_tower = int(np.argmin(rec_times)) 121 | # Iterate over all other towers. 122 | for j in [x for x in range(towers.shape[0]) if x!= first_tower]: 123 | # print('tower', str(first_tower), 'to', str(j)) 124 | locus = get_locus(tower_1=(towers[first_tower][0], 125 | towers[first_tower][1]), 126 | tower_2=(towers[j][0], towers[j][1]), 127 | time_1=rec_times[first_tower], 128 | time_2=rec_times[j], 129 | v=v, delta_d=delta_d, max_d=max_d) 130 | # Sometimes empty locus is produced depending on geometry of the 131 | # situation. Discard these. 132 | if(len(locus[0]) > 0): 133 | loci.append(locus) 134 | return loci 135 | 136 | 137 | def circle_intersection(circle1, circle2): 138 | ''' Calculate intersection points of two circles. 139 | from https://gist.github.com/xaedes/974535e71009fa8f090e 140 | 141 | Args: 142 | circle1: tuple(x,y,radius) 143 | circle2: tuple(x,y,radius) 144 | 145 | Returns 146 | tuple of intersection points (which are (x,y) tuple) 147 | 148 | >>> circle_intersection((-0.5, 0, 1), (0.5, 0, 1)) 149 | ((0.0, -0.8660254037844386), (0.0, 0.8660254037844386)) 150 | >>> circle_intersection((-1, 0, 1), (1, 0, 1)) 151 | ((0.0, 0.0), (0.0, 0.0)) 152 | 153 | ''' 154 | x1,y1,r1 = circle1 155 | x2,y2,r2 = circle2 156 | # http://stackoverflow.com/a/3349134/798588 157 | # d is euclidean distance between circle centres 158 | dx,dy = x2-x1,y2-y1 159 | d = math.sqrt(dx*dx+dy*dy) 160 | if d > r1+r2: 161 | # print('No solutions, the circles are separate.') 162 | return None # No solutions, the circles are separate. 163 | elif d < abs(r1-r2): 164 | # No solutions because one circle is contained within the other 165 | # print('No solutions because one circle is contained within the other') 166 | return None 167 | elif d == 0 and r1 == r2: 168 | # Circles are coincident - infinite number of solutions. 169 | # print('Circles are coincident - infinite number of solutions.') 170 | return None 171 | 172 | a = (r1*r1-r2*r2+d*d)/(2*d) 173 | h = math.sqrt(r1*r1-a*a) 174 | xm = x1 + a*dx/d 175 | ym = y1 + a*dy/d 176 | xs1 = xm + h*dy/d 177 | xs2 = xm - h*dy/d 178 | ys1 = ym - h*dx/d 179 | ys2 = ym + h*dx/d 180 | 181 | return ((xs1,ys1),(xs2,ys2)) 182 | -------------------------------------------------------------------------------- /tex/230a4b7a9ac468f0ec9acbf28016d276.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tex/94cf9bfabcb071b6b432112626ffd8a3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tex/7b40691d3fcdab7f1174d83e2de738c6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tex/a4c6de18edfc65fe4376b9438c00ec4b.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tex/ce931c5e3797ea59637a36c0ee4a2e04.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tex/7146d7ec04600031453049bed6b8a9b0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tex/8182282dad754f66fe56591253699990.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tex/10b4e50a7a481d84f1c0d380611ed3d3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /readme.tex.md: -------------------------------------------------------------------------------- 1 | 2 | # Multilateration in 2D: IoT/LoRaWAN Mass Surveillance 3 | "*Multilateration (MLAT) is a surveillance technique based on the measurement of the difference in distance to two stations at known locations by broadcast signals at known times.*" - [en.wikipedia.org/wiki/Multilateration](http://en.wikipedia.org/wiki/Multilateration) 4 | 5 | ## Abstract 6 | A single motivated individual with several hundred USD and a hobbyist level of competency in electronics and programming would be able to set up a mass surveillance system to track individual LoRa IoT devices on the scale of a small city, provided that the geography of the city is accommodating to the placement of receiving devices (e.g. surrounded by hills). 7 | 8 | ## Locating the source of a signal transmission - intuition 9 | 10 | When a signal is transmitted by a radio device it propagates through the atmosphere at approximately the speed of light - that is, approximately $v = 3 \text{e} 8ms^{-1}$. 11 | 12 | If this signal is received by two receivers (towers), the time difference of arrival ($\text{TDOA}$) between the first and second tower can be determined. $\text{TDOA} = t_1 - t_0$ where $t_0$ is the time the signal is received first and $t_1$ is the time the signal is received second. The towers require synchronized high accuracy clocks, which can be achieved in practice with a GPS clock on each tower. 13 | 14 | The below animation shows a signal propagating from a transmitter "Tx" and being received by two towers at different times, allowing the $\text{TDOA}$ to be calculated. 15 | 16 |

17 | 18 | This $\text{TDOA}$ can be used to determine the *difference* in distance that the transmitter is located from the towers. For example, assume that the transmitter is on a circle of radius $d$ metres from tower 1, where the signal was received first. Then it must also be on a circle of radius $d+v \times \text{TDOA}$ metres from tower 0, where the signal was received second. The device must then lie at the intersection of the two circles, giving two possible locations (or one if the circles only touch, or none if the circles do not touch). 19 | 20 | By iterating over a range of values for $d$ and at each iteration finding the intersection of the two circles, a locus of possible transmitter locations can be determined. This produces a hyperbolic curve on which the transmitter lies. Notably, it is not required to know *when* the signal was first transmitted - as you would with trilateration - and so no communication with the transmitter is required beyond simply identifying the signal. 21 | 22 | The below animation shows circles of increasing radius around the two towers and the resulting hyperbolic locus of intersections as $d$ is increased. The circle around tower 1 has a radius of $r_1=d$, and the circle around tower 0 has a radius of $r_0=d+\text{TDOA} \times v$ (noting $\text{TDOA} \times v$ is constant and $\text{TDOA}$ comes from the previous animation). 23 | 24 |

25 | 26 | With $n$ towers, this process can be repeated between the tower that first received the message and every other tower to produce $n-1$ loci. In general, with three towers the device location can be narrowed down to at least two positions and with four towers to exactly one position. There are some cases, depending on the relative geometry of the towers and transmitter and the error in the timestamping, where this is not the case. 27 | 28 |

29 | 30 | The accuracy of the determined location will depend on the accuracy of the timestamping, the effects of diffraction and obstacles on path length, the relative geometry of the transmitter and towers, etc. The approximation of the problem to two dimensions will also introduce error. 31 | 32 | ## Analysis 33 | Multilateration was just explained in an intuitive manner. However, it is convenient to have a set of mathematical expressions that describe the system if the location of the transmitter is to be found. This analysis is inspired heavily by [André Andersen's](http://blog.andersen.im/2012/07/signal-emitter-positioning-using-multilateration) similar work. 34 | 35 | Consider, in Euclidean $\mathbb{R}^{2}$ space, a transmitter located at $\vec{x}$ whose transmission at time $t_0$ propagates at speed $v \ ms^{-1}$ and is received by a set of $n$ towers. Let the tower that receives this transmission first be at location $\vec{p}_c$ with receive time $t_c$. Let the remaining $n-1$ towers be located at $\vec{p}_i$ with transmission receive times $t_i$. 36 | 37 | For the first tower, $\vec{p}_c$, we can say that the distance between the transmitter $\vec{x}$ and the tower is equal to the transmission propagation speed multiplied by the time of flight. 38 | $$ 39 | \|\vec{x} - \vec{p}_c\| = v(t_c-t_0) 40 | $$ 41 | 42 | We can make a similar statement for each of the other towers. 43 | $$ 44 | \begin{align} 45 | \|\vec{x} - \vec{p}_i\| &= v(t_i-t_0) \\ 46 | &= v(t_i - t_c + t_c - t_0) \\ 47 | &= v(t_i - t_c) + v(t_c - t_0) \\ 48 | \end{align} 49 | $$ 50 | 51 | Combining these two expressions we obtain the following, where the only unknown is $\vec{x}$. 52 | $$ 53 | \|\vec{x} - \vec{p}_c\| = \|\vec{x} - \vec{p}_i\| - v(t_i - t_c) 54 | $$ 55 | 56 | Expanding this out we obtain a set of $n-1$ expressions, where there are $n$ towers in total. Subscript $x$ and $y$ denote the x and y components of a vector respectively. 57 | $$ 58 | \begin{align} 59 | &\|\vec{x} - \vec{p}_c\| - \|\vec{x} - \vec{p}_i\| + v(t_i - t_c) &= 0 &, \quad i = 1, ..., n-1 \\ 60 | \Rightarrow \ & \sqrt{(\vec{x}_x - \vec{p}_{c,x})^2 + (\vec{x}_y - \vec{p}_{c,y})^2} 61 | - \sqrt{(\vec{x}_x - \vec{p}_{i,x})^2 + (\vec{x}_y - \vec{p}_{i,y})^2} 62 | + v(t_i - t_c) &= 0 &, \quad i = 1, ..., n-1 \\ 63 | \end{align} 64 | $$ 65 | 66 | Each of these equations represents a single hyperbola. These hyperbolas can be plotted individually, producing the same graphs shown above by taking circle intersections, or they can be solved by finding a value of $\vec{x}$ that minimizes their error. 67 | 68 | 69 | ## Implications: IoT and LoRaWAN 70 | 71 | Multilateration can be used to locate the source of a transmission if 72 | 1. the transmission can be received in several locations, and 73 | 2. the receive time can be accurately recorded at each location. 74 | 75 | Furthermore, if one transmitter's signal contains a persistent and unencrypted transmitter ID then it will be possible to track the location of that device. 76 | 77 | One emerging IoT technology, LoRa/LoRaWAN, allows these requirements to be satisfied cheaply with hobbyist equipment while also transmitting a pseudo-persistent unencrypted device ID. Every device in a LoRaWAN network is given a 32-bit device address - akin to an IPv4 or IPv6 address - that uniquely identifies it on a network. This address is not globally unique, but is unique within a single LoRaWAN network. This address is assigned by the network operator and can be re-assigned at any time, though typically the same address is used for a substantial length of time, making the address pseudo-persistent - again akin to an IPv4 or IPv6 address. In cases where the device is activated by personalization, the device will always use the same address. 78 | 79 | Sections 4.3.1.6, 4.3.3, and 4.4.2 of the LoRaWAN [specification](./lorawantm_specification_-v1.1.pdf) specify the encryption of uplink messages (messages sent from an IoT device to the network). Notably, the device address (DevAddr) is never encrypted; in fact it is required that the address be known (unencrypted) in order to decrypt the encrypted portions of the message. Any person listening for LoRa transmissions will receive these messages and will be able to read the device address. 80 | 81 | LoRa transmissions can typically travel at least 1km, and distances of up to at least 15km can be attained in real-world conditions. As a result, only a low density of receivers need be installed in order to receive all messages over a large geographical area. For example the city of Wellington, New Zealand, is surrounded by hills. A handful of receivers atop, or partway up, some of the hills would be sufficient to cover the entire city. To cover the main portion of the city with three or four overlapping receivers, as required for multilateration, would not be difficult. 82 | 83 | LoRa receivers with GPS are cheap - the Dragino LoRa/GPS Shield for Arduino comes with a LoRa module and a GPS module with 10ns timing accuracy and typically sells for about 50 USD. This device is additionally easy to configure and use, with an abundance of resources available online. 84 | 85 | ## Summary 86 | Given these three points - LoRa messages contain a pseudo-persistent unique ID, LoRa messages can be received over large distances, and LoRa messages can be received and accurately timestamped with cheap hobbyist equipment - there is a very real possibility to create a passive mass surveillance network that tracks individual LoRa devices. This is a severe privacy concern for applications such as vehicle, person (e.g. elderly), and goods tracking. 87 | 88 | ## This notebook 89 | 90 | This notebook presents a small python script that produces a set of loci of possible transmitter locations given a set of towers with locations and signal receive times. Additionally, the code used to create the animations is given. 91 | 92 | 93 | ```python 94 | from multilaterate import get_loci 95 | import matplotlib.pyplot as plt 96 | import numpy as np 97 | from scipy.optimize import least_squares 98 | ``` 99 | 100 | 101 | ```python 102 | # How many towers. All towers recieve the transmission. 103 | num_towers = 4 104 | 105 | # Metre length of a square containing the transmitting 106 | # device, centred around (x, y) = (0, 0). Device will be randomly placed 107 | # in this area. 108 | tx_square_side = 5e3 109 | 110 | # Metre length of a square containing the towers, 111 | # centred around (x, y) = (0, 0). towers will be randomly placed 112 | # in this area. 113 | rx_square_side = 25e3 114 | 115 | # Speed of transmission propogation. Generally equal to speed of 116 | # light for radio signals. 117 | v = 3e8 118 | 119 | # Time at which transmission is performed. Really just useful to 120 | # make sure the code is using relative times rather than depending on one 121 | # of the receive times being zero. 122 | t_0 = 2.5 123 | 124 | # Metre increments to radii of circles when generating locus of 125 | # circle intersection. 126 | delta_d = int(100) 127 | 128 | # Max distance a transmission will be from the tower that first 129 | # received the transmission. This puts an upper bound on the radii of the 130 | # circle, thus limiting the size of the locus to be near the towers. 131 | max_d = int(20e3) 132 | 133 | # Standard deviation of noise added to the 134 | # receive times at the towers. Mean is zero. 135 | rec_time_noise_stdd = 1e-6 136 | 137 | # Whether to plot circles that would be 138 | # used if performing trilateration. These are circles that are centred 139 | # on the towers and touch the transmitter site. 140 | plot_trilateration_circles = False 141 | 142 | # Whether to plot a straight line 143 | # between every pair of towers. This is useful for visualising the 144 | # hyperbolic loci focal points. 145 | plot_lines_between_towers = False 146 | ``` 147 | 148 | 149 | ```python 150 | # Generate towers with x and y coordinates. 151 | # for tower i: x, y = towers[i][0], towers[i][1] 152 | towers = (np.random.rand(num_towers, 2)-0.5) * rx_square_side 153 | print('towers:\n', towers) 154 | 155 | # location of transmitting device with tx[0] being x and tx[1] being y. 156 | tx = (np.random.rand(2)-0.5) * tx_square_side 157 | print('tx:', tx) 158 | 159 | # Distances from each tower to the transmitting device, 160 | # simply triangle hypotenuse. 161 | # distances[i] is distance from tower i to transmitter. 162 | distances = np.array([ ( (x[0]-tx[0])**2 + (x[1]-tx[1])**2 )**0.5 163 | for x in towers]) 164 | print('distances:', distances) 165 | 166 | # Time at which each tower receives the transmission. 167 | rec_times = distances/v + t_0 168 | # Add noise to receive times 169 | rec_times += np.random.normal(loc=0, scale=rec_time_noise_stdd, 170 | size=num_towers) 171 | print('rec_times:', rec_times) 172 | 173 | # Get the loci. 174 | loci = get_loci(rec_times, towers, v, delta_d, max_d) 175 | 176 | # Plot towers and transmission location. 177 | fig, ax = plt.subplots(figsize=(5,5)) 178 | max_width = max(tx_square_side, rx_square_side)/2 179 | ax.set_ylim((max_width*-1, max_width)) 180 | ax.set_xlim((max_width*-1, max_width)) 181 | for i in range(towers.shape[0]): 182 | x = towers[i][0] 183 | y = towers[i][1] 184 | ax.scatter(x, y) 185 | ax.annotate('Tower '+str(i), (x, y)) 186 | ax.scatter(tx[0], tx[1]) 187 | ax.annotate('Tx', (tx[0], tx[1])) 188 | 189 | # Iterate over every unique combination of towers and plot nifty stuff. 190 | for i in range(num_towers): 191 | if(plot_trilateration_circles): 192 | # Circle from tower i to tx site 193 | circle1 = (towers[i][0], towers[i][1], distances[i]) 194 | circle = plt.Circle((circle1[0], circle1[1]), 195 | radius=circle1[2], fill=False) 196 | ax.add_artist(circle) 197 | for j in range(i+1, num_towers): 198 | if(plot_lines_between_towers): 199 | # Line between towers 200 | ax.plot((towers[i][0], towers[j][0]), 201 | (towers[i][1], towers[j][1])) 202 | 203 | for locus in loci: 204 | ax.plot(locus[0], locus[1]) 205 | plt.show() 206 | 207 | ``` 208 | 209 | towers: 210 | [[ -2006.09826991 -6818.07867897] 211 | [ 101.31584818 8158.92916584] 212 | [-10893.22757653 3893.40549794] 213 | [ -8595.14625748 8895.4077718 ]] 214 | tx: [ 731.70475905 2184.19298779] 215 | distances: [ 9409.38151992 6007.90001384 11749.91315762 11490.45489794] 216 | rec_times: [2.50003146 2.50002049 2.50003941 2.50003865] 217 | 218 | 219 | 220 | ![png](output_3_1.png) 221 | 222 | 223 | 224 | ```python 225 | # plot the same hyperbola using the derived expressions. 226 | 227 | fig, ax = plt.subplots(figsize=(5,5)) 228 | max_width = max(tx_square_side, rx_square_side)/2 229 | ax.set_ylim((max_width*-1, max_width)) 230 | ax.set_xlim((max_width*-1, max_width)) 231 | 232 | c = np.argmin(rec_times) # Tower that received first. 233 | p_c = towers[c] 234 | t_c = rec_times[c] 235 | 236 | x = np.linspace(towers[i][0] - 50000, towers[i][0] + 50000, 100) 237 | y = np.linspace(towers[i][1] - 50000, towers[i][1] + 50000, 100) 238 | x, y = np.meshgrid(x, y) 239 | 240 | for i in range(num_towers): 241 | if i == c: 242 | continue 243 | 244 | p_i = towers[i] 245 | t_i = rec_times[i] 246 | 247 | plt.contour( 248 | x, y, 249 | ( 250 | np.sqrt((x-p_c[0])**2 + (y-p_c[1])**2) 251 | - np.sqrt((x-p_i[0])**2 + (y-p_i[1])**2) 252 | + v*(t_i - t_c) 253 | ), 254 | [0]) 255 | ``` 256 | 257 | 258 | ![png](output_4_0.png) 259 | 260 | 261 | 262 | ```python 263 | # Solve the location of the transmitter. 264 | 265 | c = np.argmin(rec_times) 266 | p_c = np.expand_dims(towers[c], axis=0) 267 | t_c = rec_times[c] 268 | 269 | # Remove the c tower to allow for vectorization. 270 | all_p_i = np.delete(towers, c, axis=0) 271 | all_t_i = np.delete(rec_times, c, axis=0) 272 | 273 | def eval_solution(x): 274 | """ x is 2 element array of x, y of the transmitter""" 275 | return ( 276 | np.linalg.norm(x - p_c, axis=1) 277 | - np.linalg.norm(x - all_p_i, axis=1) 278 | + v*(all_t_i - t_c) 279 | ) 280 | 281 | # Initial guess. 282 | x_init = [0, 0] 283 | 284 | # Find a value of x such that eval_solution is minimized. 285 | # Remember the receive times have error added to them: rec_time_noise_stdd. 286 | res = least_squares(eval_solution, x_init) 287 | 288 | print(f"Actual emitter location: ({tx[0]:.1f}, {tx[1]:.1f}) ") 289 | print(f"Calculated emitter locaion: ({res.x[0]:.1f}, {res.x[1]:.1f})") 290 | print(f"Error in metres: {np.linalg.norm(tx-res.x):.1f}") 291 | 292 | # And now plot the solution. 293 | fig, ax = plt.subplots(figsize=(5,5)) 294 | max_width = max(tx_square_side, rx_square_side)/2 295 | ax.set_ylim((max_width*-1, max_width)) 296 | ax.set_xlim((max_width*-1, max_width)) 297 | 298 | for locus in loci: 299 | ax.plot(locus[0], locus[1]) 300 | 301 | ax.scatter(tx[0], tx[1], color='red') 302 | ax.annotate('Actual', (tx[0], tx[1])) 303 | 304 | ax.scatter(res.x[0], res.x[1], color='blue') 305 | ax.annotate('Solved', (res.x[0], res.x[1])) 306 | 307 | plt.show() 308 | 309 | ``` 310 | 311 | Actual emitter location: (731.7, 2184.2) 312 | Calculated emitter locaion: (711.6, 2129.4) 313 | Error in metres: 58.4 314 | 315 | 316 | 317 | ![png](output_5_1.png) 318 | 319 | 320 | ## Animations 321 | Below cells generate the animations used above. Some variable re-use. 322 | 323 | 324 | ```python 325 | import matplotlib.pyplot as plt 326 | import numpy as np 327 | from matplotlib.animation import FuncAnimation 328 | from IPython.display import HTML, Image 329 | from multilaterate import get_locus 330 | 331 | # For animation GIF output: 332 | # brew install imagemagick 333 | # brew works on mac, different for different OS I guess. 334 | 335 | # If using to_html5_video: 336 | ''' 337 | plt.rcParams['animation.ffmpeg_path'] = '/usr/local/Cellar/' \ 338 | 'ffmpeg/4.0.2/bin/ffmpeg' 339 | ''' 340 | 341 | 342 | towers = np.array([[5e3, 13e3], [16e3, 5e3], [5e3, 7e3], 343 | [12.5e3, 1e3]]) 344 | tx = np.array([15e3, 8e3]) 345 | distances = np.array([ ( (x[0]-tx[0])**2 + (x[1]-tx[1])**2 )**0.5 346 | for x in towers]) 347 | rec_times = distances/v 348 | 349 | v = 3e8 350 | delta_d = 10 351 | ``` 352 | 353 | 354 | ```python 355 | ## Create a visualisation for TDOA 356 | 357 | # Plot towers and transmission location. 358 | fig, ax = plt.subplots(figsize=(5,5)) 359 | ax.set_ylim((0, 20e3)) 360 | ax.set_xlim((0, 20e3)) 361 | for i in range(2): 362 | x = towers[i][0] 363 | y = towers[i][1] 364 | ax.scatter(x, y) 365 | ax.annotate('Tower '+str(i), (x, y)) 366 | ax.scatter(tx[0], tx[1]) 367 | ax.annotate('Tx', (tx[0], tx[1])) 368 | 369 | circle = plt.Circle((tx[0], tx[1]), 370 | radius=1, fill=False) 371 | ax.add_artist(circle) 372 | 373 | # Annotations, to be updated during animation 374 | cur_time = ax.annotate('t = 0', (1e3, 19e3)) 375 | t0 = ax.annotate('Tower 0 received at t = ', (1e3, 18e3)) 376 | t1 = ax.annotate('Tower 1 received at t = ', (1e3, 17e3)) 377 | TDOA_ann = ax.annotate('TDOA = ', (1e3, 16e3)) 378 | 379 | v_vec = ax.arrow(tx[0], tx[1], 0, 1e3, 380 | head_width=500, head_length=500, fc='k', ec='k') 381 | v_ann = ax.annotate('v = {:.0E} m/s'.format(v), (tx[0], tx[1]+1e3)) 382 | 383 | n_frames = 300 384 | max_seconds = 50e-6 385 | 386 | t0_rec = 0 387 | t1_rec = 0 388 | TDOA = 0 389 | 390 | def animate(i): 391 | global t0_rec, t1_rec, TDOA, v_vec, v_ann 392 | 393 | t = i/n_frames * max_seconds 394 | radius = v * t 395 | 396 | v_vec.remove() 397 | v_vec = ax.arrow(tx[0], tx[1]+radius, 0, 1e3, 398 | head_width=500, head_length=500, fc='k', ec='k') 399 | v_ann.set_position((tx[0] - 0.5e3, tx[1]+radius+1.5e3)) 400 | 401 | circle.radius = radius 402 | 403 | cur_time.set_text('t = {:.2E} s'.format(t)) 404 | 405 | if(t >= rec_times[0] and t0_rec == 0): 406 | t0_rec = t 407 | t0.set_text(t0.get_text() + '{:.2E} s'.format(t0_rec)) 408 | if(t >= rec_times[1] and t1_rec == 0): 409 | t1_rec = t 410 | t1.set_text(t1.get_text() + '{:.2E} s'.format(t1_rec)) 411 | if(t0_rec != 0 and t1_rec != 0 and TDOA == 0): 412 | TDOA = abs(t1_rec-t0_rec) 413 | TDOA_ann.set_text(TDOA_ann.get_text() \ 414 | + '{:.2E} s'.format(TDOA)) 415 | 416 | anim = FuncAnimation(fig, animate, 417 | frames=n_frames, interval=16.6, blit=False) 418 | 419 | # HTML(anim.to_html5_video()) # doesn't play nice with github markdown 420 | anim.save('animations/tdoa.gif', writer='imagemagick', fps=60) 421 | plt.close() 422 | Image(url='animations/tdoa.gif') 423 | 424 | ``` 425 | 426 | 427 | ```python 428 | ## Create a visualisation for locus 429 | 430 | TDOA_dist = v*TDOA 431 | 432 | # Plot towers and transmission location. 433 | fig, ax = plt.subplots(figsize=(5,5)) 434 | ax.set_ylim((0, 20e3)) 435 | ax.set_xlim((0, 20e3)) 436 | for i in range(2): 437 | x = towers[i][0] 438 | y = towers[i][1] 439 | ax.scatter(x, y) 440 | ax.annotate('Tower '+str(i), (x, y)) 441 | ax.scatter(tx[0], tx[1]) 442 | ax.annotate('Tx', (tx[0], tx[1])) 443 | 444 | circle0 = plt.Circle((towers[0][0], towers[0][1]), 445 | radius=1e3, fill=False) 446 | circle1 = plt.Circle((towers[1][0], towers[1][1]), 447 | radius=1e3, fill=False) 448 | ax.add_artist(circle0) 449 | ax.add_artist(circle1) 450 | 451 | # Annotations, to be updated during animation 452 | cur_d_ann = ax.annotate('d = 0', (1e3, 19e3)) 453 | r0_ann = ax.annotate('Tower 0 radius r0 = ', (1e3, 18e3)) 454 | r1_ann = ax.annotate('Tower 1 radius r1 = ', (1e3, 17e3)) 455 | 456 | n_frames = 300 457 | max_d = 10e3 458 | 459 | locus_plot, = ax.plot([], [], marker=None) 460 | 461 | def animate(i): 462 | # global t0_rec, t1_rec, TDOA, v_vec, v_ann 463 | 464 | d = i/n_frames * max_d 465 | 466 | circle0.radius = d + TDOA_dist 467 | circle1.radius = d 468 | 469 | cur_d_ann.set_text('d = {:.2E} m'.format(d)) 470 | r0_ann.set_text('Tower 0 radius r0 = {:.2E} m'.format(d 471 | + TDOA_dist)) 472 | r1_ann.set_text('Tower 1 radius r1 = {:.2E} m'.format(d)) 473 | 474 | locus = get_locus(tower_1 = (towers[0][0], towers[0][1]), 475 | tower_2 = (towers[1][0], towers[1][1]), 476 | time_1 = t0_rec, time_2 = t1_rec, 477 | v = v, delta_d = delta_d, max_d=d) 478 | locus_plot.set_xdata(locus[0]) 479 | locus_plot.set_ydata(locus[1]) 480 | 481 | anim = FuncAnimation(fig, animate, 482 | frames=n_frames, interval=16.6, blit=False) 483 | 484 | # HTML(anim.to_html5_video()) # doesn't play nice with github markdown 485 | anim.save('animations/locus.gif', writer='imagemagick', fps=60) 486 | plt.close() 487 | Image(url='animations/locus.gif') 488 | ``` 489 | 490 | 491 | ```python 492 | ## Create a visualisation for loci 493 | 494 | # Plot towers and transmission location. 495 | fig, ax = plt.subplots(figsize=(5,5)) 496 | ax.set_ylim((0, 20e3)) 497 | ax.set_xlim((0, 20e3)) 498 | 499 | for i in range(towers.shape[0]): 500 | x = towers[i][0] 501 | y = towers[i][1] 502 | ax.scatter(x, y) 503 | ax.annotate('Tower '+str(i), (x, y)) 504 | ax.scatter(tx[0], tx[1]) 505 | ax.annotate('Tx', (tx[0], tx[1])) 506 | 507 | circles = [] 508 | locus_plots = [] 509 | for i in range(towers.shape[0]): 510 | circle = plt.Circle((towers[i][0], towers[i][1]), 511 | radius=0, fill=False) 512 | ax.add_artist(circle) 513 | circles.append(circle) 514 | 515 | locus_plot, = ax.plot([], [], marker=None) 516 | locus_plots.append(locus_plot) 517 | 518 | # Annotations, to be updated during animation 519 | cur_d_ann = ax.annotate('d = 0', (1e3, 19e3)) 520 | 521 | n_frames = 600 522 | max_d = 10e3 523 | 524 | def animate(i): 525 | # global t0_rec, t1_rec, TDOA, v_vec, v_ann 526 | 527 | d = i/n_frames * max_d 528 | cur_d_ann.set_text('d = {:.2E} m'.format(d)) 529 | 530 | first_tower = int(np.argmin(rec_times)) 531 | circles[first_tower].radius = d 532 | 533 | for j in [x for x in range(towers.shape[0]) if x!= first_tower]: 534 | # print('tower', str(first_tower), 'to', str(j)) 535 | locus = get_locus(tower_1=(towers[first_tower][0], 536 | towers[first_tower][1]), 537 | tower_2=(towers[j][0], towers[j][1]), 538 | time_1=rec_times[first_tower], 539 | time_2=rec_times[j], 540 | v=v, delta_d=delta_d, max_d=d) 541 | locus_plots[j].set_xdata(locus[0]) 542 | locus_plots[j].set_ydata(locus[1]) 543 | 544 | TDOA_j = v * (rec_times[j] - rec_times[first_tower]) 545 | circles[j].radius = d + TDOA_j 546 | 547 | anim = FuncAnimation(fig, animate, 548 | frames=n_frames, interval=16.6, blit=False) 549 | 550 | # HTML(anim.to_html5_video()) # doesn't play nice with github markdown 551 | anim.save('animations/loci.gif', writer='imagemagick', fps=60) 552 | plt.close() 553 | Image(url='animations/loci.gif') 554 | ``` 555 | --------------------------------------------------------------------------------