Catégorie : Dev web

Bibliothèque PHP de traitement de fichiers logs ADIF v3

Logo QScope.org (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/08/logo_qscope_b NULL.png)Ceci est une nouvelle version de la bibliothèque PHP que j’avais précédemment publié (http://xv4y NULL.radioclub NULL.asia/2013/10/15/bibliotheque-php-de-traitement-de-fichiers-logs-adif-v2/). Elle accepte maintenant conforme à la norme ADIF v3 et accepte de manière transparente les fichiers au format traditionnels ADI ou au nouveau format ADX (XML). C’est la librairie que j’utilise pour mon site d’analyse de log en ligne QScope.org.

Ce code ne gagnerait pas un concours de beauté, mais il fonctionne. J’avoue que les tests avec le format XML ont été limités à l’exemple donné avec la norme ADIF v3, car je n’ai pas de logiciel capable de générer de tels fichiers. Aucune comparaison de performance n’a été faite entre les deux formats, mais je soupçonne que le format traditionnel est un peu plus rapide à importer, même si la limite viendra plus rapidement de la base de données que du parsing.

<?php

/*

=== ADIF v3 Parser library v2.00 (2015-03-10) ===

   This Library his based on work by Jason Harris KJ4IWX but has been simplified and improved for better ADIF compatiblity by Yannick DEVOS XV4Y
   Version 2.00 introduces transparent support for ADX (XML) file format coming with ADIF v3 specifications
   
   Copyright 2011-2013 Jason Harris KJ4IWX (Apache v2.00 License)
   Copyright 2015 Yannick DEVOS XV4Y (GNU GPL v3.00 License)
   Any commercial use is subject to authorization from the respective authors
	
	This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

---

	This PHP library is designed to work with the Jelix 1.5 PHP Framework and PHP 5.4.
	However, it can simply be called by any PHP program.
	
	Usage for ADI and ADX files (with Jelix):
		$ADIFParser = jClasses::createInstance('yourmodule~ADIFParser');
		$ADIFParser->initialize($complete_path_to_the_file);

		echo ($ADIFParser->get_header('adif_ver'));			// Will display the ADIF version
		$all_headers_array = $ADIFParser->get_header(); 	// Will return the whole headers in an array
		while(($record = $ADIFParser->get_record())!=-1) {	// Check for end of file
			if ($record!=false) {							// If line is empty we skip
				if(count($record) == 0) {
					break;
				};
				echo ($record['call']);
				echo ($record['qso_date']);
				echo ($record['mode']);
				// Do whatever you like with the ADIF fields...
			};
		};

	Fore more infos find contacts at:
	http://xv4y.radioclub.asia/
	http://www.qscope.org/

*/

class ADIFParser
{
	var $is_adx = false;	// default to ADI file (traditionnal format)
	var $data; 				// the adif data
	var $i; 				// the iterator
	var $headers = array();
	
	public function initialize($file) //this function parses the header
	{
		// We read the first line and check if it is XML (ADX) or not
		$this->file_handler = fopen($file,'r');
		$firstline = fgets($this->file_handler);
		if (stripos($firstline, "<?xml")!==false) $this->is_adx = true;
		fclose($this->file_handler);

		if (!$this->is_adx) {	// This is an old fashion ADI file format
			$eoh=$eof=false;
			$this->data='';
		
			// And we start from the begining
			$this->file_handler = fopen($file,'r');
			while (!$eoh && !$eof) {	// We will try to locate the <EOH> tag
				$newdata = fgets($this->file_handler);	// $this->file_handler contains a handler in the case of ADI files
				if (!$newdata) {
					$eof = true;
				} else {
					if (stripos($newdata, "<eoh>")===false) {
						$this->data .= $newdata;
					} else {
						$eoh = true;
					}
				};
			}
			if($eoh == false) //did we find the end of headers?
			{
				// If this ADIF file has no header (like FT5ZM), we need to re-open the file because we already went to the end
				fclose($this->file_handler);
				$this->file_handler = fopen($file,'r');
				return 0;
			};
		
		
			//get headers
		
			$this->i = 0;
			$in_tag = false;
			$tag = "";
			$value_length = "";
			$value = "";
			$pos = strlen($this->data)-1;
				
			while($this->i < $pos)
			{
				//skip comments
				if($this->data[$this->i] == "#")
				{
					while($this->i < $pos)
					{
						if($this->data[$this->i] == "\n")
						{
							break;
						}
				
						$this->i++;
					}
				}else{
					//find the beginning of a tag
					if($this->data[$this->i] == "<")
					{
						$this->i++;
						//record the key
						while($this->data[$this->i] < $pos && $this->data[$this->i] != ':')
						{
							$tag = $tag.$this->data[$this->i];
							$this->i++;
						}
					
						$this->i++; //iterate past the :
					
						//find out how long the value is
					
						while($this->data[$this->i] < $pos && $this->data[$this->i] != '>')
						{
							$value_length = $value_length.$this->data[$this->i];
							$this->i++;
						}
					
						$this->i++; //iterate past the >
					
						$len = (int)$value_length;
						//copy the value into the buffer
						while($len > 0 && $this->i < $pos)
						{
							$value = $value.$this->data[$this->i];
							$len--;
							$this->i++;
						};

						$this->headers[strtolower(trim($tag))] = $value; //convert it to lowercase and trim it in case of \r
						//clear all of our variables
						$tag = "";
						$value_length = "";
						$value = "";
					
					}
				}
			
				$this->i++;
			};
			return 1;
		} else {	// This is the new ADX (XML) file format
			$this->is_adx = true;
			$this->xml_reader = new XMLReader;
			$this->xml_reader->open($file); // In this case, $file contains a file path
			if (!$this->xml_reader) return -1;	// Not able to open the file

			while ($this->xml_reader->read() && $this->xml_reader->name != 'HEADER');		// Looking for the header

			if ($this->xml_reader->name == 'HEADER') {
				// Load the current xml element into simplexml
				$xml = simplexml_load_string($this->xml_reader->readOuterXML());
				$this->headers = array_change_key_case(json_decode(json_encode($xml),TRUE),CASE_LOWER);

			} else {
				return 0;	// No header, this should mean invalid file for ADX
			}
			while ($this->xml_reader->read() && $this->xml_reader->name != 'RECORD');		// Now we go to the first RECORD
			if ($this->xml_reader->name != 'RECORD') {
				$this->xml_reader->close();
				return -2;	// This is an empty or invalid ADX file
			}
		return 1;	// All is normal
		}
	}
	
	//the following function does the processing of the array into its key and value pairs
	public function record_to_array(&$record)
	{
		$return = array();
		for($a = 0; $a < strlen($record); $a++)
		{
			if($record[$a] == '<') //find the start of the tag
			{
				$tag_name = "";
				$value = "";
				$len_str = "";
				$len = 0;
				$a++; //go past the <
				while($record[$a] != ':') //get the tag
				{
					$tag_name = $tag_name.$record[$a]; //append this char to the tag name
					$a++;
				};
				$a++; //iterate past the colon
				while($record[$a] != '>' && $record[$a] != ':')
				{
					$len_str = $len_str.$record[$a];
					$a++;
				};
				if($record[$a] == ':')
				{
					while($record[$a] != '>')
					{
						$a++;
					};
				};
				$len = (int)$len_str;
				while($len > 0)
				{
					$a++;
					$value = $value.$record[$a];
					$len--;
				};
				$return[strtolower($tag_name)] = $value;
			};
			//skip comments
			if($record[$a] == "#")
			{
				while($a < strlen($record))
				{
					if($record[$a] == "\n")
					{
						break;
					}
					$a++;
				}
			}
		};
		return $return;
	}
	
	
	//finds the next record in the file
	public function get_record()
	{
		if (!$this->is_adx) {
			$record ='';
			$out_condition = false;
			while (!$out_condition) {
				$newdata = fgets($this->file_handler);
				if ($newdata===false) {
					$out_condition = true;
					fclose($this->file_handler);
					return -1;	// This is the end of file
				} else {
					$end = stripos($newdata , "<eor>");
					if($end !== false) { //we got a full line
						$record .= substr($newdata, 0, $end);
						$out_condition = true;
						return $this->record_to_array($record); //process and return output
					} else {
					$record .= $newdata;	// The line is not complete and we need to fetch a new string
					}
				}
			}
		} else {
			// Loop through the Records
			if ($this->xml_reader->name == 'RECORD') {
				// Load the current xml element into simplexml and we’re off and running!
				$xml = simplexml_load_string($this->xml_reader->readOuterXML());
				$this->xml_reader->next('RECORD');
				$array = array_change_key_case(json_decode(json_encode($xml),TRUE),CASE_LOWER);

				return $array;
			} else {
				$this->xml_reader->close();
				return -1;
			}
		}
 	}
	
	public function get_header($key='')	// Changed to return the whole array if no key provided
	{
		if(array_key_exists(strtolower($key), $this->headers))
		{
			return $this->headers[strtolower($key)];
		} else if (empty($key)) {
			return $this->headers;
		} else {
			return NULL;
		}
	}
	
}
?>

Plug-in PHP requêtes asynchrones PostgreSQL pour Jelix/jDb

Je mets à disposition au téléchargement ce plug-in jDb (Jelix) écrit en PHP qui est un driver PostgreSQL permettant les requêtes asynchrones avec la base de données (http://xv4y NULL.radioclub NULL.asia/ftp/JELIX_JDB_PGSQL_ASY NULL.zip).

Logo QScope.org (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/08/logo_qscope_b NULL.png)Dans le cadre de mon projet de statistiques en ligne QScope.org (http://www NULL.qscope NULL.org/), je me retrouve à faire des calculs assez intensifs sur la base de données PostgreSQL, en particulier pour calculer le temps d’opération et les nombres réels de QSO/heure en pointe. Sur les gros logs d’expéditions DX ou pour les logs étalés sur plusieurs mois, les requêtes sur la base de données peuvent se chiffrer en milliards de lignes… et jusque 10 minutes de temps d’exécution.

Logo Postgresql (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/09/Postgresqlelephant NULL.png)Si les performances de PostgreSQL sont très bonnes et qu’il gère admirablement bien les connexions multiples, il ne permet pas l’exécution parallèle des requêtes. Cette fonctionnalité est encore une exclusivité des bases de données commerciales comme Oracle, Sybase ou MS SQL Server. Cela veut dire qu’une grosse requête ne pourra être répartie sur plusieurs micro-processeurs (ou coeurs) d’un serveur. La solution pour gagner du temps c’est alors de lancer plusieurs requêtes en parallèle pour réduire le temps d’exécution et profiter au mieux des ressource du serveur. PHP étant nativement un langage interprété mono-thread, ceci n’est pas intuitif à mettre en place. Il dispose toutefois dans son pilote PostgreSQL de commandes permettant de lancer les requêtes de manière asynchrone et de récupérer les résultats ensuite. En ouvrant plusieurs connexions simultanées cela ouvre la voie à une forme de traitement parallèle.

Logo Jelix (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/09/logo_jelix NULL.png)Si vous utilisez le framework PHP Jelix (http://jelix NULL.org/), ce plugin est simple d’utilisation puisqu’il reprend le pilote jDb PostgreSQL d’origine en lui ajoutant 3 fonctions pour gérer les requêtes asynchrones. Dans l’archive ZIP (http://xv4y NULL.radioclub NULL.asia/ftp/JELIX_JDB_PGSQL_ASY NULL.zip), se trouve un Readme en français et en anglais qui vous donne l’API et les exemples pour utiliser ce plugin.

QScope avec K9W et TO2TT

Logo QScope.org (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/08/logo_qscope_b NULL.png)En ce moment j’ai peu de temps pour le blog. Mes diverses activités professionnelles prennent la première place, avec entre autres un projet d’ouverture de restaurant qui redevient d’actualité. Ensuite j’ai été pas mal sollicité sur QScope (http://www NULL.qscope NULL.org/), d’abord par des nouvelles statistiques suggérées par les utilisateurs, ensuite par les bogues à corriger puis finalement par des revues et bulletins DX qui souhaitent avoir plus de détails afin de publier un article complet sur le projet.

La bonne nouvelle c’est que les organisateurs de deux grosses Expéditions à Wake Island (K9W) (http://www NULL.wake2013 NULL.org/) et à Mayotte (TO2TT) (http://www NULL.i2ysb NULL.com/idt/) ont annoncé leur volonté d’utiliser QScope en interne pour l’organisation de leurs opérations. Les statistiques produites leur permettront d’avoir jour par jour toutes les informations pour mieux répartir les tours d’opérateurs et la répartition des activités par bandes et modes dans le temps. Le but étant de maximiser le nombre de QSOs afin de mieux “rentabiliser” le coût de ces organisations. Le nombre de QSOs pour de telles DXpeditions en fin d’opération s’élève vite à 100.000, et ces utilisateurs utiliseront les statistiques les plus coûteuses en terme de temps machine!

J’ai donc du passer du temps afin de “préparer l’avenir”. En effet, si maintenant l’application compte 600 utilisateurs enregistrés et plus de 3 millions de lignes de log téléchargées, l’effervescence des premiers jours s’est calmé en moyenne il y a 10 utilisateurs “actifs” par jour qui produisent des statistiques et 1,8 millions de lignes de log dans la base de données (les utilisateurs effacent souvent leur logs après avoir produit les statistiques). La base de données principale fait 250 Mo, et les index occupent autant. Quand on tient compte de la RAM du serveur occupée par le système, le SGBDR lui-même et les différents shared buffers, on se rend compte qu’on se rapproche vite de la capacité maximale disponible de 1 Go actuelle sur le serveur. Rapidement, au lieu de lire les données dans la mémoire cache rapide, c’est sur le disque dur très lent que le SGBDR ira chercher ses informations à traiter. Les temps de calcul seront multiplié par 10 au moins…

Deux solutions à cela : augmenter la RAM du serveur ou mieux répartir les données. La première solution est la plus rapide à mettre en oeuvre, le problème c’est que chez le fournisseur que j’ai choisi c’est aussi la plus coûteuse. Les prix sont très attractif pour l’entrée de gamme, mais ensuite on grimpe très vite. L’autre solution est donc beaucoup plus intéressante sur le long terme. La réponse que je souhaitais mettre en place s’appliquait en trois phases :

  1. Mettre en place le mécanisme de Paritionnement de Tables de PostgreSQL. Simple à mettre en place en théorie, même si entièrement manuel contrairement aux SGBDR commerciaux comme Oracle ou Sybase, il permet de diminuer la taille de chaque table à charger en RAM et la taille des index. Je pensais donc répartir mes utilisateurs sur 27 tables (les 26 lettres de l’alphabet plus un “fourre-tout”) en fonction de la première lettre de leur indicatif. Après quelques tâtonnements (une maquette avec 2 utilisateurs est bien plus facile qu’une base de production avec 600 comptes) tout fonctionnait parfaitement. Les tables les plus grosses faisaient 40 Mo et les index avaient bien diminué en taille. Le “hic” c’est que les performances étaient pires qu’avant!!! Le problème venait de la fonction choisie pour répartir les utilisateurs sur les différentes tables. Celle-ci était coûteuse en temps machine et la pénalité se paye à chaque requête. J’ai donc du faire marche arrière… Le partitionnement reste intéressant, mais pour une répartition plus légère à calculer (simple comparaison) comme celle sur une date.
  2. Donner la possibilité d’attribuer à chaque utilisateur une base de données différente. Là, le travail se résumait à pas mal de réécriture du code existant pour permettre de se connecter sur différentes base de données “à la volée”. Rien d’insurmontable surtout que j’avais envisagé la chose assez tôt. En pratique ça me permet d’isoler facilement les “gros” utilisateurs comme les DXpeditions. Ils feront leurs calculs gourmands sur une base de taille plus petite que la base principale, diminuant les temps de traitement. La compartimentation au niveau du serveur aidera aussi les utilisateurs de la base principale à être moins impactés puisque ce seront deux emplacements mémoire et deux emplacements disques différent qui seront consultés, limitant les phénomènes de “lock“.
  3. Permettre d’utiliser plusieurs petits serveurs plutôt qu’un gros. Techniquement la solution est la même que la précédente, sauf que cette fois-ci au lieu d’interroger une autre base de données du même serveur je vais la chercher sur une autre machine. Cela est très intéressant car en répartissant les utilisateurs sur plusieurs bases on évite que celles-ci grossissent de trop et deviennent trop lourdes à gérer. Du point de vue financier, c’est aussi le plus intéressant car en doublant le coût, on double réellement la capacité (CPU, RAM et disque) alors que dans la grille tarifaire du fournisseur, doubler le prix payé pour le serveur ne faisait que doubler la RAM. Le contrecoup c’est que ça fait deux systèmes à gérer et donc plus de temps à passer. Toutefois, si les deux systèmes sont identiques et disposent de bonnes procédures automatisées, cette maintenance reste limitée.

Comme vous le voyez, je suis assez satisfait du temps passé sur QScope car cela me permettra de répondre plus facilement à l’augmentation de la demande qui va venir avec la saison des concours et des DXpeditions. L’échec de la mise en place du paritionnement n’est que partiel, et dans la réalité le serveur se comporte mieux que je ne l’avais craint. En effet, en plus de la RAM physique de 1 Go qui est allouée, 4 Go de disque SSD très rapide sont vus par le système comme de la RAM et permettent de diminuer les temps de réponse du disque tant qu’on reste dans des limites acceptables.

Bientôt Noël…

Paul a fait sa lettre au père Noël cet après-midi, en commande un vélo (acheté ce matin) et un jouet “Wall-E” que le facteur a apporté il y a une semaine… Moi j’avais fait mon e-mail au papa Noël indien il y a 2 semaines et le colis est parti. Avec un peu de chance je l’aurai au pied du sapin!

La propagation reprend un peu et j’ai fait quelques contacts intéressants sur 18MHz. Bande très calme et signaux très confortables. La plus grosse tache solaire de ce cycle solaire (http://www NULL.spaceweather NULL.com/archive NULL.php?view=1&day=16&month=12&year=2009) (9 fois la taille de la Terre) a été observée. Elle est accompagnée de non pas une mais trois éjections de masse coronale qui vont toucher la Terre d’ici 1 à 2 jours. Ca devrait pas mal ioniser les couches E et F et donc offrir de belles possibilités de DX.

Sinon j’avance peu à peu sur la nouvelle version du site web (avec Jelix). Les principales fonctionnalités sont couvertes et du point de vue esthétique ça sera présentable. J’en suis à l’étape de la résolution des derniers petits bogues et de l’intégration du contenu de l’ancien site dans le nouveau… Travail fastidieux mais peut-être que tout sera prêt pour la fin d’année. Ensuite il me restera encore pas mal d’affinages à faire, mais j’ai le temps.

Demain je me lève à 2h30 pour aller chercher des clients à Saïgon… alors maintenant c’est dodo pour moi!

Générateur HF et pont de mesure

Analyseur d'antenne en kit monté (http://capheda NULL.files NULL.wordpress NULL.com/2009/11/antan_monte NULL.jpg)

Tout est soudé et fonctionne!

J’ai fini de monter la partie géné HF et pont de mesure de l’ANTAN. Encore une fois ça a fonctionné du premier coup. Il faut dire aussi que la réalisation reste assez simple et accessible aux débutants soigneux (ce que je n’étais pas il y a quelques années…). Le générateur peut monter jusque 50MHz. F6BQU dit de ne pas dépasser 30MHz car les mesures seront fausses… Je me demandais si le problème vient de la source du signal (le LT1799 est vendu pour 32MHz) qui ne produirait plus un signal propre, des diodes qui ne commutent plus assez vite ou du transfo HF qui “mécaniquement” ne suit plus… J’ai depuis reçu la réponse de Luc F6BQU que je reproduis ci-contre :

La bonne réponse est le transfo. En effet il y a beaucoup trop de spires sur un tore de forte perméabilité. Il faudrait réaliser un autre transfo avec seulement trois spires pour monter au-dessus de 30 MHz. Et également prévoir un montage plus adapté à ces fréquences.
Pour utiliser l’Antan en générateur HF, c’est envisageable en générateur simple, à défaut de mieux (quelques OM’s s’en servent). Mais le signal issu du LT1799 n’est pas un signal pur. Il est affecté d’un “jitter”, c’est-à-dire une variation très rapide de fréquence autour de la fréquence centrale. Pour une utilisation en analyseur d’antenne, il n’y a pas de problème, mais en géné pur et stable, à vous de voir.

Fréquencemètre en kit (http://capheda NULL.files NULL.wordpress NULL.com/2009/11/fp50_monte NULL.jpg)

Le fréquencemètre est monté et fonctionne

Ci-joint quelques photos avant que je ne mette sous tension (panne d’électricité quand j’ai pris les photos). Il ne me reste plus qu’à mettre le tout dans un boîtier, ce que je vais essayer de faire dans la journée ou demain… Nouvelle difficulté par contre : trouver des boutons! C’est tout de même dingue, sur les 2 magasins d’électronique que j’ai fait, pas moyen de trouver des boutons pour les potentiomètres… J’aimerai aussi trouver un condensateur variable, mais je pense que j’aurai plus de chance du côté des vendeur de postes radio d’occasion…