Skip to main content

Il fait chaud ici ou comment construire un station météorologique

Paru le 20 février 2007

Scott Hanselman :  Scott Hanselman's Computer Zen
Difficulté : Facile
Durée : 3-6 heures
Coût : $100-$200
Logiciel :  Visual Studio Express Editions
Matériel :  Capteur de températures & Kit d'interface
Télécharger :  Télécharger

It's Hot in Here 

Résumé : Dans le second volet de la colonne « Some Assembly Required », Scott Hanselman explique comment utiliser Visual 2005 Express Edition et .NET Framweork 2.0 pour extraire des informations météorologiques non seulement d'un Service Web, mais également d'un thermomètre analogique « Phidget » local. Il aborde les interfaces, le chargement dynamique d'assembly, ainsi que du graphisme et du dessin de base avec WinForms.

Une interface sur le monde

Bienvenue dans le second volet de ma nouvelle colonne d'amateur MSDN : « Some Assembly Required ». Cette colonne traite de l'utilisation de C# et de .NET Framework pour créer une interface avec les gadgets et le matériel - de votre téléphone portable à votre lecteur MP3, de votre TV à votre PC?Windows Media Center, de vos ports COM à un périphérique de GPS.


Phidgets

Un de ces jours, je vais faire un robot de « l'homme pauvre » avec C#. Ce sera cool de le faire interagir avec le monde extérieur, peut-être utiliser une vieille webcam pour ses yeux, et peut-être un corps type Roomba. Mais, commençons par le commencement. J'ai découvert ce site fantastique d'amateur appelé Phidgets. Sur ce site, on peut lire : " Les phidgets sont un ensemble facile à utiliser de blocs de construction pour capter et contrôler à faible coût depuis votre PC. En utilisant le protocole USB (Universal Serial Bus) commebase pour tous les phidgets, la complexité est gérée derrière une API (Application Programming Interface) facile à utiliser et robuste ». C'est le genre de chose qui me serait très utile pour commencer à utiliser des capteurs matériels avec .NET.

Lorsque je lis cette description marketing, l'amateur et le radin que je suis vois « Phidgetsbla blafacilebla blafaible coûtbla blaAPI ». De la musique pour mes oreilles. Sur leur site, j'ai pu lire : « les applications peuvent être développées rapidement dans Visual Basic [6], avec la prise en charge bêta pour C, C++, Delphi et Foxpro » Hum, et pas .NET. Bon, s'ils prennent en charge Visual Basic 6, cela signifie qu'ils prennent en charge COM et que je peux les utiliser dans .NET. Très bien. Mais que puis-je créer ?

Il commence à faire chaud ici, débarrassons-nous de tout ce code

Toutes mes plantes meurent dans mon bureau. Je soupçonne que la température n'est pas régulière et qu'il fait très froid la nuit et très chaud le jour pendant le week-end . Je vais créer un moniteur de températures qui va suivre la température de mon bureau. Toutefois, ce serait bien de connaître la température extérieure en même temps, pour le contexte. J'ai pris un détecteur de température et un kit d'interface sur le site Phidgets. Le kit est extensible avec toute sorte de choses comme RFID, détecteurs de mouvements et curseurs.

Le logiciel Phidgets installe une DLL COM classique et c'est tout. C'est pas plus compliqué que ça. J'irai dans les détails plus tard. D'abord, je dois découvrir comment obtenir la température intérieure et extérieure.

Programmation orientée interface

Je veux obtenir la température extérieure et intérieure. Le Phidget me donnera la température intérieure et pourquoi pas utiliser un service Web pour la température extérieure ? C'est simple et l'information existe déjà. J'ai effectué quelques recherches et découvert que CapeClear a un charmant service Web météorologique pour aéroport avec WSDL disponible. Visual Studio est capable de gérer le WSDL et nous donne les types de données et un « point de terminaison » à utiliser. Cela nous donnera la température à l'aéroport de Portland (PDX ou tout autre aéroport de votre choix).

Il y a de la banalité dans l'air. Bien que la météo intérieure vienne d'une mystérieuse bibliothèque COM non gérée et que la météo extérieure d'un mystérieux service Web WSDL étranger, les deux me donnent la température actuelle en Celcius.

Une « interface » est un contrat qui peut être décrit en code. Et vous pouvez écrire une classe qui promet de suivre les règles de cette interface. Par exemple, je crée un contrat ICurrentTemperature (« I » pour Interface).

Visual C#

namespace ICurrentTemperature{
public interface ICurrentTemperatureProvider{
double GetCurrentTemperatureInCelcius();
}
}

Visual Basic

Public Interface ICurrentTemperatureProvider
Function GetCurrentTemperatureInCelcius() As Double
End Interface

Cette interface ne décrit pas exactement COMMENT la température sera récupérée, juste qu'elle le sera. La mise en oeuvre est un détail qui est laissé à notre propre appréciation. Je vais donc créer deux classes, une qui parle au phidget et une autre qui parle au service météorologique. Les deux vont renvoyer la température en Celcius. Je créerai ensuite une application WinForms qui charge et exécute dynamiquement tout ICurrentTemperatureProviders disponible et représente les températures dans un graphique.

Tout d'abord, créez une petite bibliothèque de classes contenant seulement l'interface ICurrentTemperatureProvider. En la séparant des autres assemblys, nous voulons que le contrat puisse être librement partagé et référencé par les autres projets que nous créerons. À la fin, nous aurons un total de quatre assemblys :

  • ICurrentTemperature.dll : l'interface partagée par tous les projets
  • PhidgetTemp.dll : l'assembly qui implémente ICurrentTemperatureProvider et parle Interopérabilité COM au périphérique Phidget
  • WebServiceTemp.dll : l'assembly qui implémente ICurrentTemperatureProvider et parle services Web au service météorologique global
  • TemperatureMonitor.exe : l'application WinForms qui exécutera dynamiquement tous les assemblys du répertoire actif et la méthode GetCurrentTemperatureInCelcius et qui représentera les résultats dans un graphique

Appel de la bibliothèque COM Phidget depuis .NET

1114
Chaque bibliothèque COM est différente, et celles qui sont spécifiques à un matériel peuvent fournir une vue sur le matériel qui n'est pas évidente de prime abord. Pour ajouter une référence à une bibliothèque COM et générer automatiquement un affichage .NET ou un « assembly Interop » pour cette bibliothèque, cliquez avec le bouton droit de la souris sur la section Références de votre assembly PhidgetsTemp, puis cliquez sur « Ajouter une référence ». Sélectionnez l'onglet COM, puis la bibliothèque COM Phidgets. Une nouvelle Interop.PHIDGET.dll est alors créée et apparaît dans le répertoire de sortie de votre projet lors de la compilation. Lorsque votre projet .NET appelle ce qui ressemble à des méthodes dans la bibliothèque COM sous-jacente, vous appelez en fait ce RCW(Runtime Callable Wrapper) qui gère tout ce qui est Interop COM. Le résultat est une expérience transparente dans laquelle le développeur .NET peut utiliser toutes les bibliothèques disponibles. Ce serait dommage que je ne puisse pas utiliser le matériel de phidgets juste parce qu'il n'y a pas de bibliothèque .NET native disponible !

La bibliothèque COM Phidget a une classe PhidgetManager qui nous permet d'initialiser le système et d'atteindre le IPhidgetInterfaceKit et ses capteurs. Puisqu'il y a une initialisation unique du système, nous allons appeler get_SensorValue sur le port analogique sur lequel est branché le moniteur de température.

Le kit d'Interface de Phidget mesure 1 000 étapes entre 0 et 5 volts lorsqu'il renvoie les valeurs. Ceci signifie qu'une unité pour une valeur du capteur est 5 mV (0,005 V). En observant la feuille de données du matériel pour le détecteur de température analogique, nous obtenons l'équation suivante pour convertir la température en Celcius :

TempInCelcius = (SensorValue - 200)/4

Visual C#

namespace PhidgetTemp{
public class PhidgetTemp : ICurrentTemperatureProvider{
public double GetCurrentTemperatureInCelcius()
if(initialized == false){
PhidgetTemp.Initialize();
}
double sensorValue = (double)kit.get_SensorValue(analogPort);
double temp = ((sensorValue - 200) / 4);
return temp;
//SNIP...

}

}

Visual Basic

Public Class PhidgetTemp Implements ICurrentTemperatureProvider
Public Function GetCurrentTemperatureInCelcius() As Double
Implements ICurrentTemperature.ICurrentTemperatureProvider.
GetCurrentTemperatureInCelcius
If initialized = False Then
PhidgetTemp.Initialize()
End If
Dim sensorValue As Double = CType(kit.SensorValue(analogPort), Double)
Dim temp As Double = ((sensorValue - 200) / 4)
Return temp
End Function

Appel du service météorologique global

Ajouteruneréférenceweb

L'ajout d'une référence à un service Web est très similaire à l'ajout d'une référence de bibliothèque ordinaire, sauf bien sûr, que le véritable travail se passe ailleurs. Lorsque nous avons ajouté la référence à la bibliothèque COM Phidgets, la dll Phidgets.dll a fourni assez d'informations à .NET Framework pour générer un « proxy » convenable ressemblant à une bibliothèque .NET. Ceci nous permet d'utiliser cette bibliothèque dans n'importe quel langage prenant en charge .NET. La boîte de dialogue « Ajouter une référence Web » requiert une URL et nous allons entrer l'emplacement du fichier WSDL (Web Services Description Language) du service météorologique global. Lorsque nous cliquons sur Ajouter une référence, Visual Studio crée un proxy joignable pour le service.

Nous allons maintenant créer une autre implémentation de ICurrentTemperatureProvider, qui appelle cette fois le service météorologique global plutôt que le Phidget.

Visual C#

namespace WebServiceTemp
{
public class WebServiceTemp : ICurrentTemperatureProvider
{
public double GetCurrentTemperatureInCelcius()
{
com.capescience.live.GlobalWeather ws =
new com.capescience.live.GlobalWeather();
com.capescience.live.WeatherReport report =
ws.getWeatherReport(System.Configuration.ConfigurationSettings.AppSettings["AirportCode"]);
return report.temperature.ambient;
}
}

}

Visual Basic

Public Class WebServiceTemp Implements ICurrentTemperatureProvider
Public Function GetCurrentTemperatureInCelcius1() As Double
Implements ICurrentTemperature.ICurrentTemperatureProvider.
GetCurrentTemperatureIn Celcius
Dim ws As com.capescience.live.GlobalWeather =
New com.capescience.live.GlobalWeather
ws.Url = ConfigurationSettings.AppSettings
("TemperatureMonitor.com.capescience.live.GlobalWeather")
Dim airportCode As String = ConfigurationSettings.AppSettings
("AirportCode")
Dim report As com.capescience.live.WeatherReport =
ws.getWeatherReport(airportCode)
Return report.temperature.ambient
End Function
End Class

À ce stade, nous devrions avoir trois assemblys. Un assembly contenant uniquement l'interface ICurrentTemperatureProvider, un pour l'implémentation de Phidget et un pour l'implémentation du service Web.

Tout est dans les contrats

Juste une petite remarque ici - nous pourrions certainement nous contenter d'écrire une application WinForms monolithique qui appelerait directement la bibliothèque Phidgets et le service Web météorologique, mais il y a un avantage dans cette méthode en apparence détournée. En plus d'en apprendre plus sur les interfaces, nous rendons notre application plus modulaire en la fractionnant en « services » ou zones fonctionnelles. Non seulement vous pouvez appliquer ces méthodes à vos propres projets et passe-temps, mais vous pouvez également commencer à observer ce modèle dans les applications que vous utilisez au quotidien. Que feraient Excel ou Word sans plug-ins ? Comment une application de graphiques fonctionnerait-elle sans les filtres et formats enfichables ? En utilisant un contrat externe comme ITemperatureProvider (ou n'importe quel contrat), nous exprimons notre intention de manière plus explicite en tant que programmeurs. C'est là tout l'intérêt de la programmation, exprimer clairement votre intention - pas uniquement à l'ordinateur, mais également aux autres programmeurs.

Collecte de données

Maintenant, revenons à nos moutons. Notre nouvelle application WinForms TemperatureMonitor doit rechercher des assemblys qui implémentent ICurrentTemperatureProvider dans le répertoire actif. En d'autres termes, rechercher des assemblys qu'il peut appeler. N'oubliez pas que notre application ne sait rien sur les Phidgets ou les services Web ; elle connaît uniquement ICurrentTemperatureProvider.

À l'aide de la nouvelle fonctionnalité Génériques de .NET 2.0, nous allons créer une liste de ICurrentTemperatureProviders. Nous chargerons cette liste en chargeant tout les assemblys et en y recherchant tous les types qui implémentent ICurrentTemperatureProvider. Si nous en trouvons un, nous le créerons et enregistrerons les instances dans notre liste. Nous consulterons ensuite la liste et en fait tous les getCurrentTemperatureInCelcius. (REMARQUE : Il existe des raisons de ne pas utiliser Assembly.LoadFrom, mais il a répondu à nos attentes ici. Nous pourrions également, en guise d'amélioration, renoncer à l'analyse dynamique et inclure les noms complets des assemblys à charger dans le fichier de configuration de notre application).

Visual C#

List<ICurrentTemperatureProvider> TemperatureImplementations =
new List<ICurrentTemperatureProvider>();
private List<ICurrentTemperatureProvider>
GetTemperatureImplementations(string dir)
{
List<ICurrentTemperatureProvider> retVal =
new List<ICurrentTemperatureProvider>();

foreach(string f in Directory.GetFiles(dir,"*.dll"))
{
try
{
Assembly a = Assembly.LoadFrom(f);
foreach (Type t in a.GetTypes())
{
if (null != t.GetInterface(typeof(ICurrentTemperatureProvider).FullName))
{
ICurrentTemperatureProvider i = Activator.CreateInstance(t) as ICurrentTemperatureProvider;
if (null != i)
{
retVal.Add(i);
}
}
}

}

catch (BadImageFormatException)
{
//skip bad assemblies.
}
}

return retVal;
}

Visual Basic

Dim TemperatureImplementations
as New List(of ICurrentTemperatureProvider)

Private Function GetTemperatureImplementations(
ByVal dir As String)
As
List(Of ICurrentTemperatureProvider)

Dim retVal As New List(Of ICurrentTemperatureProvider)

For Each f As
String In Directory.GetFiles(dir,
"*.dll")
Try

Dim a As Assembly =
Assembly.LoadFrom(f)
For
Each t As Type In a.GetTypes()

If t.GetInterface(GetType (ICurrentTemperatureProvider
). FullName) IsNot
Nothing Then

Dim i As ICurrentTemperatureProvider =

CType(Activator.CreateInstance(t),
ICurrentTemperatureProvider)

If i IsNot Nothing Then
retVal.Add(i)

End If

End If
Next

Catch ex As BadImageFormatException

'skip bad assemblies.
End
Try
Next
Return retVal
End Function

Plus tard, lorsque nous voulons appeler toutes nos implémentations (dans ce cas, nous en attendons deux, le Phidget et le service Web), nous pouvons juste les parcourir et les appeler de la manière suivante :

Visual C#

foreach (ICurrentTemperatureProvider temp
in TemperatureImplementations)
{
try
{

double i = temp.GetCurrentTemperatureInCelcius();
listBox1.Items.Add(i +
" from " + temp.GetType().Name);
}

catch (Exception ex)
{
listBox1.Items.Add(ex.ToString());
}
}

Visual Basic

For Each temp
As ICurrentTemperatureProvider In TemperatureImplementations
Try
Dim name As
String = DirectCast(temp, Object).
GetType().Name
Dim i
As Double = temp.GetCurrentTemperatureInCelcius()
AddTemperatureRecord(name, i)
listBox1.Items.Add(i &
" from " & name)
Catch ex
As Exception
listBox1.Items.Add(ex.ToString())

End Try
Timer1.Start()
PictureBox1.Invalidate()

Next

Remarquez qu'aucune conversion n'est nécessaire dans la boucle ForEach, car les variables TemperatureImplementations ont été déclarées à l'aide de génériques. Nous sommes donc sûrs du type de leur contenu.

List<ICurrentTemperatureProvider> TemperatureImplementations =
new List<ICurrentTemperatureProvider>();

Je stockerai les enregistrements que nous avons recueillis dans une structure de données TemperatureRecord interne et appellerai notre boucle sur un intervalle. Nous pourrions utiliser le nouveau composant .NET 2.0 BackgroundWorker, mais le contrôle Timer standard nous suffit ici.

Comme les TemperatureRecords recueillis sont enregistrés, nous allons les dessiner dans une PictureBox, faisant ce que j'appelle un « graphique de l'homme pauvre ». Je fais mon propre dessin pour deux raisons : Un, c'est amusant et mes besoins sont simples et deux, je ne voulais pas acheter un composant de graphique. Je laisse au lecteur le soin d'acheter un tel composant. Je ne veux pas que n'importe qui puisse ajouter des enregistrements à ma collection pendant que j'en extrais les données, je vais donc la verrouiller avant de commencer le tableau. Cette méthode dit, en pseudocode :

Pour chaque genre de TemperatureRecord, obtenir une couleur unique
Pour chaque TemperatureRecord de ce genre, en commençant par le plus récent
Le représenter sous forme de point sur un graphique, en commençant à droite
Si vous atteignez le bord gauche, arrêter de dessiner les vieux enregistrements.

Visual C#

private void UpdatePanel(Graphics g)
{

lock (objectLock)
{
using (SolidBrush white =
new SolidBrush(Color.WhiteSmoke))

using (SolidBrush black = new SolidBrush(Color.Black))

using (SolidBrush inside = new SolidBrush(Color.Blue))

using (SolidBrush outside = new SolidBrush(Color.Red))

using (Pen twoWidePen = new Pen(black, 2))
{
g.FillRectangle(white, 0, 0,
pictureBox1.Width, pictureBox1.Height);

int colorIndex = 0;
//draw each
plugin in a different color
foreach (
string name in TemperatureRecordsDictionary.Keys)
{

using (Pen coloredPen = new Pen(colors[colorIndex], 2))
{

int x = pictureBox1.Width;

//start slightly off screen
Point previousPoint =
new Point(
pictureBox1.Width+1, pictureBox1.Height);

//draw the most recent records first

for (int i = TemperatureRecordsDictionary[name].
Count - 1; i >= 0; i--)
{
TemperatureRecord record =
TemperatureRecordsDictionary[name][i];

//y-scale is 0 to 100 Celcius

// This was trivial, although I should have used a

// Matrix transform

int y =
pictureBox1.Height - (
int)((record.tempCelcius *
pictureBox1.Height) / 100);
Point newPoint =
new Point(x, y);
g.DrawLine(coloredPen, previousPoint, newPoint);
previousPoint = newPoint;
x -= 10;

if (x <= 0)
{

//we're reaching the left edge, stop drawing

break;
}
}
}
colorIndex++;

if (colorIndex > 12)

throw new ArgumentOutOfRangeException(

"I've only got 12 colors to use, sorry!");
}

//x-axis
int smallOffset = 1;
g.DrawLine(twoWidePen, smallOffset,
pictureBox1.Height - smallOffset,
pictureBox1.Width - smallOffset,
pictureBox1.Height - smallOffset);

//y-axis
g.DrawLine(twoWidePen, smallOffset,
pictureBox1.Height - smallOffset,
smallOffset, smallOffset);
}
}
}

Visual Basic

Sub UpdatePanel(ByVal g
As Graphics)
SyncLock objectLock
Using white
As New SolidBrush(Color.WhiteSmoke)
Using black
As New SolidBrush(Color.Black) Using inside
As New SolidBrush(Color.Blue) Using outside
As New SolidBrush(Color.Red) Using twoWidePen
As New Pen(black, 2)
g.FillRectangle(white, 0, 0, PictureBox1.Width,
PictureBox1.Height)
Dim colorIndex As Integer = 0
'draw each plugin in a different color
For Each name As
String In
TemperatureRecordsDictionary.Keys
Using coloredPen
As New Pen(colors(colorIndex), 2)
Dim x As Integer = PictureBox1.Width
'start slightly off screen
Dim previousPoint As New Point(PictureBox1.Width
+1, PictureBox1.Height)
'draw the most recent records first
For i As Integer=TemperatureRecordsDictionary(
name).Count 1
To 0 Step -1
Dim record As TemperatureRecord =
TemperatureRecordsDictionary(name)(i)
'y-scale is 0 to 100 Celcius
' This was trivial, although I should have
'used a Matrix transform
Dim y As Integer = PictureBox1.Height –
((record.tempCelcius * PictureBox1.Height)
/ 100)
Dim newPoint As New Point(x, y)
g.DrawLine(coloredPen, previousPoint,
newPoint)
previousPoint = newPoint
x -= 10
If x <= 0 Then
'we're reaching the left edge,
' stop drawing
Exit For
End If
Next
End Using
colorIndex += 1
If colorIndex > 12 Then
Throw New ArgumentOutOfRangeException (
"I've only got 12 colors to use, sorry!")
Next 'x-axis
Dim smallOffset As Integer = 1
g.DrawLine(twoWidePen, smallOffset, PictureBox1.Height –
smallOffset, PictureBox1.Width - smallOffset,
PictureBox1.Height - smallOffset)

'y-axis g.DrawLine(twoWidePen, smallOffset, PictureBox1.Height –
smallOffset, smallOffset, smallOffset)
End Using End Using
End Using End Using
End Using End SyncLock
End Sub

Temperaturemonitor

Certaines choses sont intéressantes dans cet extrait de code. Je suis de la vieille école des GDI et, à l'époque, il fallait tout nettoyer derrière soi. Chaque fois que vous créer un stylo, vous le supprimez. Vous créez un pinceau, vous le supprimez. Dans le monde de GDI + et de .NET Framework, on me dit que ce n'est plus si important, mais les vieilles habitudes ont la vie dure. J'aime BEAUCOUP utiliser l'instruction using. C'est une grande instruction intégrée aux langages. Elle dit : « j'utilise ceci dans cette étendue. Lorsque c'est fait, appelez Dispose () pour moi ». Les types qui implémentent IDisposable ont une instruction Dispose() et nettoient toutes les ressources physiques en leur possession lorsque Dispose() est appelé. Si je n'appelle pas Dispose(), elle serait finalement appelée lors de la finalisation de l'objet (collecte de déchets). Cependant, je n'ai aucun contrôle sur le moment où cela survient, mais je peux contrôler le moment où Dispose() est appelée. Je préfère toujours privilégier l'explicite à l'implicite, si je le peux.

Conclusion

Beaucoup de choses peuvent être développées, ajoutées et améliorées dans ce projet. Voici quelques idées pour commencer :

  • Créez votre propre interface de collecte d'informations et représentez-les sur un graphique.
    • Un suiveur de cotations boursières à représentation dynamique via des services Web
    • Les visites sur votre blog ou site Web
    • L'accès réseau via PerfMon ou WMI
  • Ajoutez la consignation des détails dans un fichier texte
  • Procurez-vous un détecteur de mouvements Phidgets et consignez chaque entrée en douce dans votre bureau
  • Remplacez le contrôle Timer par BackgroundWorker pour rendre l'interface plus réactive
  • Faites une représentation de la météo via le service météorologique global de plusieurs lieux
  • Développez (eh, améliorez) le graphique en utilisant un graphique 3D comme le graphique de Dundas
  • Améliorez vous-même le graphique en ajoutant des coches sur les axes et une légende

Amusez-vous et ne craignez pas les mots : Some Assembly Required !

 Haut de page