cellidresolver: synthesize a cell tower if no exact match is found in the database
This commit is contained in:
@@ -56,13 +56,58 @@ class CellIdResolver(ResolverBase):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _resolve_cell(self, radiotype, opc, lac, cid) -> Optional[dict]:
|
def search_nearby(self, radiotype, mcc, net, area, cell) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
synthesize a cell tower from towers whose `cell` id is close to the id of interest,
|
||||||
|
under the assumption that towers with similar cell ids have similar location.
|
||||||
|
this assumption is crudely validated, by aborting if the distance between sampled
|
||||||
|
towers is absurdly large.
|
||||||
|
"""
|
||||||
|
M_PER_DEG = 111319
|
||||||
|
NUM_SAMPLES = 4 #< take this many nearest towers
|
||||||
|
MAX_RANGE = 20000 #< if the uncertainty is > this many meters, just return `None`
|
||||||
|
cur = self.con.cursor()
|
||||||
|
# radiotype is not required in observations
|
||||||
|
radio = radiotype.upper() if radiotype is not None else 'GSM'
|
||||||
|
|
||||||
|
# TODO: replace this "fetchone" with a sampling
|
||||||
|
nets = cur.execute("""
|
||||||
|
SELECT * FROM cell WHERE mcc = ? AND net = ? AND area = ?
|
||||||
|
ORDER BY abs(cell - ?);
|
||||||
|
""", (mcc, net, area, cell)).fetchmany(NUM_SAMPLES)
|
||||||
|
|
||||||
|
if not nets or len(nets) != NUM_SAMPLES:
|
||||||
|
return None
|
||||||
|
|
||||||
|
synth = {}
|
||||||
|
for field in 'lat', 'lon', 'range':
|
||||||
|
# float fields
|
||||||
|
synth[field] = sum(n[field] for n in nets) / NUM_SAMPLES
|
||||||
|
for field in 'updated', 'created', 'samples':
|
||||||
|
# int fields
|
||||||
|
synth[field] = sum(n[field] for n in nets) // NUM_SAMPLES
|
||||||
|
|
||||||
|
# guess a range, based on the standard deviation of sampled tower locations.
|
||||||
|
# each degree of latitude is about 100km, and the range field is in meters.
|
||||||
|
sum_of_squares = 0
|
||||||
|
for n in nets:
|
||||||
|
sum_of_squares += ((n['lat'] - synth['lat']) * M_PER_DEG) ** 2 + ((n['lon'] - synth['lon']) * M_PER_DEG) ** 2
|
||||||
|
rms = (sum_of_squares / NUM_SAMPLES)**0.5
|
||||||
|
log.debug(f'synthesized cell tower from {NUM_SAMPLES} real towers with {rms:.3}m std. dev')
|
||||||
|
synth['range'] += rms
|
||||||
|
|
||||||
|
if synth['range'] > MAX_RANGE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return synth
|
||||||
|
|
||||||
|
def _resolve_cell(self, radiotype, opc, lac, cid, search_fn) -> Optional[dict]:
|
||||||
radio = radiotype.upper()
|
radio = radiotype.upper()
|
||||||
mcc, mnc = opc_to_mcc_mnc(opc)
|
mcc, mnc = opc_to_mcc_mnc(opc)
|
||||||
if mcc is None:
|
if mcc is None:
|
||||||
log.debug(f'opc to mcc,mnc conversion failed for {radio} {opc}_{lac}_{cid}')
|
log.debug(f'opc to mcc,mnc conversion failed for {radio} {opc}_{lac}_{cid}')
|
||||||
return None
|
return None
|
||||||
netd = self.search_network(radio, mcc, mnc, lac, cid)
|
netd = search_fn(radio, mcc, mnc, lac, cid)
|
||||||
if netd is None:
|
if netd is None:
|
||||||
log.debug(f'Resolving failed for {radio} {opc}_{lac}_{cid}')
|
log.debug(f'Resolving failed for {radio} {opc}_{lac}_{cid}')
|
||||||
return None
|
return None
|
||||||
@@ -77,7 +122,10 @@ class CellIdResolver(ResolverBase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def resolve_cell(self, radio, opc, lac, cid, **kwargs) -> Optional[dict]:
|
async def resolve_cell(self, radio, opc, lac, cid, **kwargs) -> Optional[dict]:
|
||||||
return self._resolve_cell(radio, opc, lac, cid)
|
best_match = self._resolve_cell(radio, opc, lac, cid, self.search_network)
|
||||||
|
if best_match is None:
|
||||||
|
best_match = self._resolve_cell(radio, opc, lac, cid, self.search_nearby)
|
||||||
|
return best_match
|
||||||
|
|
||||||
async def resolve_wifi(self, radio, mac, **kwargs) -> Optional[dict]:
|
async def resolve_wifi(self, radio, mac, **kwargs) -> Optional[dict]:
|
||||||
return None
|
return None
|
||||||
|
Reference in New Issue
Block a user