lucidiot's cybrecluster

Importer des scans Wi-Fi dans PostGIS

Lucidiot Informatique 2021-07-04
Le SQL des précédents articles vous a fait peur ? Attendez de voir les expressions régulières.


Bon. On a fait des promenades, on a récolté des fichiers TSV dans un format maintenant plutôt bien défini, et on a des GPX qu'on a déjà pu importer dans PostGIS. Maintenant, il est temps de mettre les données vraiment intéressantes, c'est-à-dire les réseaux Wi-Fi, dans notre base de données.

Commençons par définir notre objectif, c'est-à-dire la base de données de destination.

Structure en base de données

CREATE EXTENSION IF NOT EXISTS postgis;

CREATE TYPE wifi_encryption AS ENUM ('NONE', 'WEP', 'WPA1', 'WPA2', 'WPA1+2', '802.1X');

CREATE TABLE accesspoint (
    bssid MACADDR NOT NULL PRIMARY KEY,
    ssid VARCHAR(32),
    encryption wifi_encryption NOT NULL,
    password VARCHAR,
    notes TEXT DEFAULT '',
    CONSTRAINT open_no_password CHECK (encryption <> 'NONE'::wifi_encryption OR password IS NULL)
);

CREATE TABLE measurement (
    timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
    bssid MACADDR NOT NULL,
    channel INTEGER,
    signal INTEGER NOT NULL,
    position geometry(POINT, 4326),
    precision INTEGER,
    CONSTRAINT measurement_pk PRIMARY KEY (timestamp, bssid),
    CONSTRAINT accesspoint_fk FOREIGN KEY (bssid) REFERENCES accesspoint (bssid),
    CONSTRAINT precision_requires_position CHECK (precision IS NULL OR position IS NOT NULL),
    CONSTRAINT negative_signal CHECK (signal < 0),
    CONSTRAINT positive_precision CHECK (precision IS NULL OR precision >= 0)
);

CREATE TABLE trip (
    timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
    position geometry(POINT, 4326) NOT NULL,
    precision INTEGER NOT NULL,
    CONSTRAINT positive_precision CHECK (precision >= 0)
);

On a trois tables :

Le BSSID est supposé être unique, donc on en fait la clé primaire de notre table de points d'accès. Le ssid (ou ESSID) est défini comme ne pouvant dépasser 32 caractères dans les spécifications, et on autorise les valeurs nulles pour les réseaux cachés (qui n'ont du coup pas de SSID). Le chiffrement est représenté par une énumération, vu qu'on ne peut avoir que 6 valeurs possibles depuis notre format d'enregistrement ; cela réduit l'espace disque utilisé pour cette colonne car tout est traduit en des nombres entiers, et cela nous donne un peu plus de validation de données. On a une colonne pour indiquer le mot de passe si on l'a trouvé, et une colonne de notes arbitraires qui peut être utile pour commenter sur un réseau particulier. On empêche aussi de mettre un mot de passe sur un point d'accès qui est censé être complètement ouvert.

En ce qui concerne les mesures, il est quasi impossible vu nos implémentations de mesurer le même réseau à la même milliseconde, mais il pourrait être possible d'avoir deux mesures dans la même milliseconde, surtout si on a plusieurs scanners qui s'exécutent en parallèle. Notre clé primaire utilise donc à la fois l'horodatage et le nom du réseau. On stocke le canal utilisé par le réseau, ce qui peut changer si on repasse plusieurs fois devant le même réseau sur plusieurs jours, notamment parce que les routeurs essaient souvent d'utiliser des canaux Wi-Fi inoccupés pour éviter les interférences. On n'a pas toujours eu accès à cette information de canal donc la colonne reste nullable. On indique cependant toujours le signal, et puisqu'on utilise le RSSI exprimé en dBm, ce signal devrait toujours être inférieur à 0. Plus il se rapproche de zéro et mieux c'est, mais un 0 est inatteignable, surtout avec la qualité de nos équipements.

Import de fichier TSV

Voici à quoi ressemble une ligne du format de fichier TSV sorti de la Poutre :

18:37:03.863 45.123456  5.123456    1.123456    5821    FreeWifi_secure 11  -76 802.1X  68:A3:78:73:F7:42

Les deux premières colonnes donnent les coordonnées GPS, mais seulement lorsqu'elles ont changé ; autrement, les colonnes restent vides. Ensuite, on a la précision en mètres, le nombre de millisecondes entre l'obtention de la position GPS et l'envoi des données en série (ce qui peut se produire de façon décalée), le nom du réseau s'il n'est pas caché, le canal, la force estimée du signal, le mode de chiffrement, et le BSSID. Sur mon propre ESP, les 4 premières colonnes sont absentes vu que je n'ai aucune information GPS sortant directement de là. On notera que l'heure est ajoutée par l'application d'enregistrement de logs en série, qui utilise un espace et pas une tabulation ici.

On peut décrire la même structure en utilisant une table temporaire, dans laquelle on pourra importer notre TSV directement avant de mettre correctement à jour les données dans nos vraies tables :

CREATE TEMPORARY TABLE input (
    time TIME NOT NULL,
    lat DOUBLE PRECISION,
    lon DOUBLE PRECISION,
    hdop DOUBLE PRECISION,
    age INTEGER,
    ssid VARCHAR(32),
    channel INTEGER NOT NULL,
    signal INTEGER NOT NULL,
    encryption wifi_encryption NOT NULL,
    bssid MACADDR NOT NULL
);

On peut ensuite importer les données dans cette table en utilisant INSERT ou COPY FROM, ou le \copy FROM de psql:

\copy input FROM 'data.tsv' WITH (FORMAT CSV, DELIMITER E'\t');

Notez le E'\t'. Il m'a fallu pas mal de temps pour le trouver celui-là… SQL ne permet pas l'échappement de caractères, sauf l'apostophe : 'l''apostrophe'. Pour pouvoir insérer une tabulation, ça n'allait pas être très joli. J'ai découvert que PostgreSQL étend la syntaxe pour permettre d'utiliser des échappements dans le style du langage C avec le préfixe E, donc on peut mettre \t pour insérer une tabulation et faire comprendre à PostgreSQL que c'est un TSV et pas un CSV.

Et si on essaie de faire ça… Ça ne marchera pas. On peut avoir une multitude de messages d'erreur différents : caractères invalides, pas assez de colonnes sur une ligne, valeur non valide pour un type (le plus souvent les macaddr), etc.

Si on regarde un peu plus en détail la structure de nos données, on se rend compte de plusieurs problèmes :

Nettoyage des données

Il va donc nous falloir traiter les données en amont pour les valider et éviter les erreurs de PostgreSQL. Au lieu d'essayer d'écrire du code très complexe qui essaierait de deviner le contenu d'une ligne invalide ou d'autres logiques complexes du genre, j'ai utilisé une longue expression régulière. Longue, donc probablement illisible, mais pourtant elle n'est pas si compliquée que ça.

grep -Pha '^(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9].[0-9]{3}\s(?:(?:[0-8][0-9]|90)\.[0-9]{6}\t-?(?:[0-9]{1,2}|1(?:[0-7][0-9]|80))\.[0-9]{6}\t[0-9]{1,2}\.[0-9]{6}\t[0-9]*|\t\t\t)\t[^\t]*\t(?:[1-9]|1[0-4])\t-[0-9]{1,2}\t(?:NONE|WEP|WPA(?:[12]|1\+2)|802.1X)\t[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}$' data.tsv | sed 's/ /\t/'

On va passer en détail sur cette ligne de commande, en considérant que vous connaissez les bases des expressions régulières. Je conseille regex101 si vous voulez tester cette expression ou apprendre en profondeur les expressions régulières.

Horodatage

L'heure ajoutée par les applications d'enregistrement est au format 18:37:03.863, suivi d'un espace.

Quand l'heure est à un seul chiffre, un zéro est ajouté, donc l'heure a toujours deux caractères et le premier est 0, 1 ou 2. On pourrait donc écrire l'heure comme [0-2][0-9], vu que le second caractère peut être entre 0 et 9, mais ça veut dire qu'on autorise 24h, 25h, etc.; j'ai préféré valider un peu plus strictement.

J'avais fait un exercice sur regex101 qui consistait à valider une adresse IP, en interdisant les adresses de broadcast entre autres, et c'est ce qui m'avait fait découvrir la technique pour valider des intervalles de chiffres en utilisant des expressions régulières : Quand on a 0 ou 1, on doit pouvoir autoriser de 0 à 9, mais si on a un 2, on ne va que de 0 à 3 (20 à 23h). On peut utiliser un ou pour faire ça : ([01][0-9]|2[0-3]).

Dans la longue expression régulière au-dessus, j'utilise cependant (?:[01][0-9]|2[0-3]). Les parenthèses décrivent un groupe de capture : Une sous-partie de l'expression régulière que les implémentations pourront fournir comme chaîne séparée, par exemple pour permettre de récupérer l'heure seule dans l'horodatage et en faire quelque chose de spécial. C'est ce que j'utilisais pour récupérer l'année, le mois et le jour en une seule expression régulière pour traiter des dates chinoises avec jq. Mais le préfixe ?: indique que ce groupe est non-capturant ; il sera donc ignoré. J'ai pris l'habitude de marquer tous les groupes de capture où je me fiche de capturer comme non-capturants, pour éviter des confusions quand je veux véritablement capturer autre part.

Pour les heures et les minutes, on a juste besoin d'aller de 00 à 59, donc :[0-5][0-9]. Pour les millièmes de seconde, on va de 000 à 999, donc [0-9]{3}. Le {3} permet d'indiquer qu'on veut ça trois fois de suite, ce qui est un raccourci assez pratique.

Enfin, pour l'espace à la place de la tabulation, j'ai ajouté après le grep une commande sed qui transformera le premier espace venu en une tabulation, ce qui règle tout de suite ce problème.

GPS

Avec mon scanner à ESP8266, où il n'y a pas de GPS, cette partie n'existe pas du tout. Mais avec la Poutre™, on a 4 colonnes rien que pour ça.

Latitude

La latitude est souvent représentée avec un point cardinal, N 45°, mais ici c'est un nombre à virgule ; le positif indique le nord, et le négatif le sud. On a donc un nombre qui varie de -90.000000 à 90.000000. J'ajoute les zéros à la fin car ils sont toujours présents ; notre formatage avec printf dans le code des microcontrôleurs fait que ces nombres ont toujours 6 décimales. Cette fois cela dit, il n'y a pas de zéro en plus si on est en dessous de 10, donc on utilise -?[0-9]{1,2} pour dire qu'on cherche un signe moins optionnel et un chiffre une ou deux fois. Mais ça implique qu'on valide aussi de 91 à 99, ce qui n'est pas possible. Ça ne m'affectera pas vraiment dans mon cas vu que je suis en France et qu'on restera toujours entre 40 et 50 environ, mais j'ai quand même voulu l'ajouter. On recommence avec le ou : -?(?:[0-8][0-9]|90). Pour gérer les décimales, on rajoute juste \.[0-9]{6}. Notez qu'on échappe le . puisqu'il signifierait sinon n'importe quel caractère et pas seulement un point.

Longitude

Pour la longitude, c'est quasiment la même chose, mais ça va de -180° (180° à l'ouest de Greenwich) à 180° ; on peut donc aller de 0 à 99, puis de 100 à 179, ou atteindre 180 exactement : -?(?:[0-9]{1,2}|1(?:[0-7][0-9]|80))\.[0-9]{6}. On imbrique des groupes non-capturants maintenant, parce que si on n'a pas les deux chiffres, on a forcément un 1, suivi de 00 à 79 ou de 80.

Précision

La troisième colonne représente la dilution horizontale de la précision ou HDOP. Il existe d'autres notions de précision en GPS, mais vu qu'on ne se préoccupe pas du tout de l'altitude dans notre projet c'est la seule qui est vraiment pertinente. Là aussi, c'est un nombre à virgule avec 6 décimales. Je ne sais pas vraiment quelle est la valeur maximale de l'imprécision d'un GPS, mais je ne l'ai jamais vu dépasser 100 mètres, donc je considère que ça va de 0 à 99 : [0-9]{1,2}\.[0-9]{6}.

Âge

Enfin, la dernière colonne est "l'âge" : c'est une notion liée au fonctionnement du GPS de la Poutre. On n'envoie des lignes que toutes les quelques secondes, et entre temps, le micro-contrôleur se place dans une boucle d'attente où il va notamment surveiller le composant GPS, qui enverra s'il en a des nouvelles trames NMEA 0183 à traiter. Quand il y a des nouvelles trames, on envoie ça à TinyGPS++ qui les interprète et nous donne une position. En conséquence, le "fix" (la position GPS déterminée) peut avoir été obtenu quelques secondes avant qu'on envoie les données de scan Wi-Fi. Entre temps, on peut avoir marché ou bougé, donc ça peut rajouter de l'imprécision et ça nous importe.

L'âge n'est rien d'autre qu'un nombre de millisecondes, et j'autorise n'importe quelle quantité de chiffres ici même si je ne m'attends pas trop à avoir une position vieille d'une heure (3.6 millions de millisecondes) dans une ligne : [0-9]*.


Enfin, afin d'éviter d'envoyer trop de fois les mêmes coordonnées GPS et d'induire de la confusion notamment avec les notions d'âge, la Poutre est programmée pour n'envoyer les coordonnées qu'une seule fois lorsqu'elles changent. Par conséquent, pour la grande majorité des lignes, les 4 colonnes sont toutes vides. En fait, elles sont soit toutes vides, soit toutes remplies, donc on peut entourer les 4 colonnes dans un nouveau groupe non-capturant : (?:les colonnes remplies|\t\t\t)\t.

SSID

Le SSID (ou plutôt le BSSID), l'identifiant lisible par les humains et pouvant atteindre jusqu'à 32 caractères, est beaucoup plus simple : on peut aussi avoir une colonne vide, donc on va juste valider qu'on a n'importe quoi qui ne soit pas une tabulation ici : [^\t]*.

Canal

Les canaux Wi-Fi sont représentés par des numéros qui peuvent monter dans les centaines en fonction des standards utilisés ; il y en a suffisamment et avec suffisamment de régulations particulières, et de définitions variant selon les pays, que la Wikipédia anglaise a un article dédié aux canaux WLAN. Mais nos ESP ne sont capables que de capter des réseaux en Wi-Fi 2.4 GHz, donc les canaux se limitent de 1 à 14. On recommence : (?:[1-9]|1[0-4]).

RSSI

Le RSSI est une indication de l'intensité du signal reçu, exprimée en dBm. La pire valeur serait -100 dBm, et valeur idéale est de zéro, mais elle est tout simplement impossible à atteindre ; avec mon téléphone collé à l'antenne Wi-Fi, je ne vois actuellement que -39 dBm. On va quand même autoriser un seul chiffre même si je n'y crois pas du tout : -[0-9]{1,2}.

Sécurité

Pour encryption, on connait d'avance toutes les valeurs autorisées, donc on peut utiliser un groupe non-capturant sans trop réfléchir avec une expression par possibilité : (?:NONE|WEP|WPA(?:[12]|1\+2)|802.1X). J'ai quand même pris soin de factoriser les 3 valeurs possibles pour WPA (1, 2, ou 1+2).

BSSID

Enfin, il reste le BSSID. C'est une adresse MAC, et nos microcontrôleurs en font une représentation en 12 chiffres hexadécimaux en majuscules, en groupes de deux chiffres séparés par des :. Un chiffre hexadécimal peut se représenter par [0-9A-F], on peut ajouter {2} vu qu'on en a deux, et ajouter : pour chaque groupe.

Alors on pourrait se dire qu'on peut utiliser un groupe non-capturant et répéter ça 6 fois : (?:[0-9A-F]{2}:){6}. Ça ne marcherait pas puisqu'on aurait une : finale, mais même en mettant manuellement le dernier groupe ((?:[0-9A-F]{2}:){5}[0-9A-F]{2}), ça ne fonctionnerait pas non plus. Répéter un groupe, c'est répéter exactement ce qu'il y a été trouvé, donc ça ne fonctionnerait que pour 0E:0E:0E:0E:0E:FF par exemple.

On est donc obligés de tout répéter : [0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}.

Import dans les vraies tables

Maintenant qu'on a tout nettoyé et qu'on a pu tout insérer dans la table temporaire input, on peut commencer à remplir nos vraies tables.

On peut commencer par le plus simple : insérer les points d'accès de base.

INSERT INTO accesspoint (bssid, ssid, encryption)
SELECT DISTINCT ON (bssid) bssid, ssid, encryption
FROM input;

On utilise DISTINCT pour dédupliquer tous les réseaux, parce qu'on a probablement mesuré chaque réseau plus d'une fois ; les microcontrôleurs sont parfois capables de percevoir un réseau à plus de cents mètres, avec une force de signal ridiculement basse.

Viennent ensuite les deux autres tables, un peu plus compliquées. D'abord, les mesures :

SET TIMEZONE='Europe/Paris';

INSERT INTO measurement (timestamp, bssid, channel, signal, position, precision)
SELECT
    (date '2021-01-01' + time) AT TIME ZONE 'localtime',
    bssid,
    channel,
    signal,
    ST_SetSRID(ST_MakePoint(lon, lat), 4326),
    hdop
FROM input;

On utilise la même façon de construire la position que dans mon import GPX expliqué dans un article précédent. Mais pour la gestion de la date, c'est un peu plus compliqué.

On n'a rien d'autre que l'heure, mais on veut tenir compte des fuseaux horaires pour éviter des problèmes, notamment avec le passage à l'heure d'été, et il nous faut aussi la date. Additionner une valeur de type date et une de type time dans PostgreSQL nous donne un timestamp, et pour y ajouter une composante de fuseau horaire on utilise AT TIME ZONE, suivi du nom du fuseau horaire.

Si on utilisait AT TIME ZONE 'Europe/Paris' avec des paramètres par défaut, PostgreSQL comprendrait que notre heure originale était dans le fuseau horaire défini dans le paramètre global TIMEZONE et ferait la conversion. Donc si l'administrateur de la base de données a décidé que tout serait à l'heure de Tokyo, nos heures seraient décalées de 7 heures en été et 8 heures en hiver. Par défaut, ce paramètre est souvent défini à UTC, donc on aurait 1 heure ajoutée à nos données en hiver et 2 heures en été. Je définis donc de force le fuseau avec SET TIMEZONE='Europe/Paris' et je l'utilise directement avec AT TIME ZONE 'localtime' ; PostgreSQL ne fait alors aucune conversion.

Enfin, les parenthèses sont importantes pour gérer les heures d'été et d'hiver : sans les parenthèses, la priorité des opérations fait que PostgreSQL va d'abord ajouter le fuseau horaire à l'heure avant d'y ajouter la date. Il perd alors le contexte qui lui permet de savoir si on est en heure d'été ou d'hiver à la date des données, et assume donc qu'on utilise la date d'aujourd'hui. Si par exemple j'insère des données du 1 janvier alors que nous sommes le 1 juin, il peut y avoir un décalage d'une heure car PostgreSQL croit que nous sommes en heure d'été. Avec les parenthèses, le contexte de la date est ajouté avant la gestion des fuseaux horaires, donc le traitement des fuseaux se fait correctement.

Il ne reste plus que la dernière table, trip, qui n'est pas utile pour les mesures qu'on insère mais qui rendra potentiellement service aux autres mesures sans données GPS. En fait, je considère ici que la Poutre est aussi un logueur GPS à part et que ses coordonnées peuvent être utilisées pour les données sans GPS, grâce à la notion d'âge qu'on a décrit plus haut. Je fais les mêmes conversions qu'au-dessus pour l'horodatage, mais je retire le nombre de millisecondes défini par l'âge à la date, et j'ai l'heure d'obtention des coordonnées GPS. On tient aussi compte du fait que dans la plupart des lignes envoyées, les coordonnées seront absentes.

INSERT INTO trip (timestamp, position, precision)
SELECT
    (date '2021-01-01' + time) AT TIME ZONE 'localtime' - make_interval(secs => age / 1000),
    ST_SetSRID(ST_MakePoint(lon, lat), 4326),
    hdop
FROM input
WHERE lon IS NOT NULL
AND lat IS NOT NULL
AND hdop IS NOT NULL
AND age IS NOT NULL;

Gestion des mises à jour

Un dernier problème peut se produire à l'insertion des points d'accès Wi-Fi. Imaginons ce scénario : Je sors une première fois et je passe devant un point d'accès qui s'annonce comme s'appellant Pikachu, et « sécurisé » en WEP. J'importe mes données, et tout va bien, j'ai une ligne ajoutée dans ma base. Mais je sors une deuxième fois et repasse devant ce point d'accès. Son BSSID n'a pas changé (et il n'est pas censé le faire), mais le propriétaire a décidé de faire évoluer son réseau donc il s'appelle maintenant Raichu et est vraiment sécurisé, en WPA2. Si j'importe mes données, l'insertion va échouer.

Je peux juste ajouter un ON CONFLICT DO NOTHING à l'insertion pour ignorer toutes les lignes qui existent déjà, mais du coup je perds l'information que le réseau a un nouveau nom et surtout un nouveau chiffrement. Je vais peut-être vouloir aller sur place pour « tester aircrack-ng », mais je ne vais pas trouver de Pikachu ni de WEP. Alors on va gérer la mise à jour des réseaux existants avec un ON CONFLICT DO UPDATE :

INSERT INTO accesspoint (bssid, ssid, encryption)
SELECT DISTINCT ON (bssid) bssid, ssid, encryption
FROM input
ON CONFLICT (bssid) DO UPDATE
SET (ssid, encryption) = (EXCLUDED.ssid, EXCLUDED.encryption);

On doit indiquer sur quelle colonne le conflit a lieu, donc ici le bssid, et on peut ensuite mettre à jour la ligne existante ; les données qui n'ont pas pu être insérées seront du coup dans une table appelée EXCLUDED.

Script shell

J'ai emballé tout cet import en un seul script shell. Je lui donne la date, les données, et les paramètres de connexion à la base de données, et il s'occupe du reste.

#!/bin/sh
# Imports TSV Wi-Fi scan results into a PostgreSQL database.
# Usage: ./tsv_import.sh YYYY-MM-DD -h host -U user -d database < data.tsv
date="$1"
shift
grep -Pha '^(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9].[0-9]{3}\s(?:(?:[0-8][0-9]|90)\.[0-9]{6}\t-?(?:[0-9]{1,2}|1(?:[0-7][0-9]|80))\.[0-9]{6}\t[0-9]{1,2}\.[0-9]{6}\t[0-9]*|\t\t\t)\t[^\t]*\t(?:[1-9]|1[0-4])\t-[0-9]{1,2}\t(?:NONE|WEP|WPA(?:[12]|1\+2)|802.1X)\t[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}$' | sed 's/ /\t/' | psql "$@" \
    -c 'BEGIN;' \
    -c 'SET TIMEZONE='"'"'Europe/Paris'"'"';' \
    -c 'CREATE TEMPORARY TABLE input (
        time TIME NOT NULL,
        lat DOUBLE PRECISION,
        lon DOUBLE PRECISION,
        hdop DOUBLE PRECISION,
        age INTEGER,
        ssid VARCHAR(32),
        channel INTEGER NOT NULL,
        signal INTEGER NOT NULL,
        encryption wifi_encryption NOT NULL,
        bssid MACADDR NOT NULL
    );' \
    -c '\copy input FROM STDIN WITH (FORMAT CSV, DELIMITER E'"'"'\t'"'"')' \
    -c 'INSERT INTO accesspoint (bssid, ssid, encryption)
    SELECT DISTINCT ON (bssid) bssid, ssid, encryption
    FROM input
    ON CONFLICT (bssid) DO UPDATE
    SET (ssid, encryption) = (EXCLUDED.ssid, EXCLUDED.encryption);' \
    -c 'INSERT INTO measurement (timestamp, bssid, channel, signal, position, precision)
    SELECT
        (date '"'$date'"' + time) AT TIME ZONE '"'"'localtime'"'"',
        bssid,
        channel,
        signal,
        ST_SetSRID(ST_MakePoint(lon, lat), 4326),
        hdop
    FROM input;' \
    -c 'INSERT INTO trip (timestamp, position, precision)
    SELECT
        (date '"'$date'"' + time) AT TIME ZONE '"'"'localtime'"'"' - make_interval(secs => age / 1000),
        ST_SetSRID(ST_MakePoint(lon, lat), 4326),
        hdop
    FROM input
    WHERE lon IS NOT NULL
    AND lat IS NOT NULL
    AND hdop IS NOT NULL
    AND age IS NOT NULL;' \
    -c 'COMMIT;'

Une sortie de scans Wi-Fi se résume donc à ça :

  1. Brancher les microcontrôleurs et activer l'enregistrement sur le téléphone
  2. Activer l'enregistrement GPX sur OsmAnd
  3. Se balader
  4. Récupérer les fichiers sur le téléphone
  5. Importer le(s) fichier(s) GPX : ./gpx_import.sh -h … -U … -d … < fichier.gpx
  6. Importer le(s) fichier(s) TSV : ./tsv_import.sh 2021-07-04 -h … -U … -d … < fichier.tsv

Elle est pas belle la vie ?


Commentaires

Il n'y a pour l'instant aucun commentaire. Soyez le premier !