Beam me up

Für die meisten Betriebssysteme gibt es kommerzielle oder freie Werkzeuge zum Abgleich der Datenbestände zwischen PalmPilot und stationärem Rechner. Wer allerdings seine Pilot-Adressen mit einer beliebigen SQL-Datenbank unter Unix synchronisieren will, ist auf Eigenbauten angewiesen - vorzugsweise in Perl.

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen
Lesezeit: 11 Min.
Von
  • Christian Kirsch
Inhaltsverzeichnis

PalmComputings digitale Assistenten kommen mit Software für Windows- und Mac-Rechner, die den Abgleich stationärer und mobiler Datenbestände erlauben. Mit pilot-link gibt es Ähnliches auch für Unix, und GUI-Oberflächen sind ebenfalls verfügbar. All diesen Lösungen ist gemeinsam, dass sie die Daten auf dem PC in einem eigenen Format halten, auf das der Zugriff von anderen Applikationen schwierig, wenn nicht ausgeschlossen ist.

Zumindest für die Adressdaten wäre die Synchronisierung mit einer externen Datenbank sinnvoll, sodass sie aus beliebigen anderen Applikationen nutzbar sind. Einige Experimente führten zu einem Perl-Script, mit dem sich das realisieren lässt. Wer damit spielen möchte, sollte jedoch unbedingt vorher die Daten seines Piloten und der Datenbank sichern.

Welche Datenbank man benutzt, ist grundsätzlich gleichgültig, solange sie Standard-SQL bietet und über automatische Zähler verfügt. Diese sind beispielsweise in Oracle als Sequence, in MySQL als auto_increment vorhanden. Um die Datenbank mit Perl bedienen zu können, sind das DBI-Modul und der passende DBD-Treiber erforderlich. Das Beispiel benutzt MySQL, für andere Datenbanken sollten nur minimale Änderungen nötig sein.

Eine Adresse auf dem Piloten besteht aus 18 sichtbaren Feldern, dazu kommen eine eindeutige ID und ein paar Flags. Eines davon registriert, ob sich der Eintrag seit der letzten Synchronisierung geändert hat, ein anderes markiert gelöschte Adressen. Zweckmäßigerweise sollte die Struktur der Host-Tabelle dieses Modell widerspiegeln. Eine mögliche Adresstabelle zeigt Listing 1. Das Feld nummer enthält eine für jeden Datensatz eindeutige Zahl als Primärschlüssel, die die Datenbank beim Einfügen einer neuen Adresse selbsttätig erzeugen sollte. Der Pilot erlaubt das Zuordnen von Adressen zu Kategorien, etwa um die Telefonnummern der Familienangehörigen unter ‘Privat’ und die der Arbeitskollegen unter ‘Beruf’ zusammenzufassen. Bezeichnungen sind im Piloten frei wählbar, er speichert mit den Adressen lediglich die Nummer der jeweiligen Kategorie. Analog findet sich die Host-Tabelle kategorie als ganze Zahl. Schließlich ist es zweckmäßig, das Datum der letzten Änderung eines Eintrags zu kennen. Dafür ist lastmod zuständig, hier als timestamp definiert. MySQL setzt dafür beim Ändern des Eintrags automatisch das aktuelle Datum ein.

Mehr Infos

Listing 1

nummer          int(10)
nachname char(50)
vorname char(50)
firma char(50)
strasse char(50)
land char(50)
plz char(15)
ort char(50)
telpriv char(25)
telgesch char(25)
telhandy char(25)
telandere char(25)
fax char(25)
email char(50)
anrede char(25)
abteilung char(50)
titel char(50)
briefanred char(50)
bundland char(50)
kategorie int(11)
lastmod timestamp(14)

Äquivalente Datenstrukturen sind jedoch nur die halbe Miete. Kniffliger ist die Logik des Synchronisierens überhaupt. Anwender wollen sowohl auf dem Host als auch auf dem Piloten jederzeit Daten hinzufügen, löschen und ändern, und nach der Synchronisierung auf beiden Maschinen denselben Stand haben. Das Script erledigt den Abgleich in drei Schritten:

  • Lesen der gesamten Pilot-Daten in einen Hash;
  • Lesen jedes einzelnen Host-Eintrags und Vergleich mit den Pilot-Adressen, dabei alle auf beiden Seiten vorhandenen Einträge aus dem Hash entfernen;
  • Die restlichen Hash-Elemente zum Host kopieren - das sind alle Adressen, die auf dem Piloten seit der letzten Synchronisierung hinzugekommen sind.

Die teuflischen Details verbergen sich im zweiten Schritt. Ist eine Host-Adresse nicht auf dem Piloten vorhanden, kopiert sie das Script dorthin - falls sie nicht in der Liste der dort gelöschten vorhanden ist. Gibt es sie schon auf beiden Plattformen, sind drei Fälle zu unterscheiden:

  • Hat sich der Eintrag auf dem Piloten seit dem letzten Abgleich verändert, aber nicht auf dem Host, landen die Änderungen auf letzterem.
  • Ist umgekehrt der Host-Datensatz seit der letzten Synchronisierung bei gleich gebliebenen Palm-Daten geändert worden, werden diese aktualisiert.
  • Kritisch bleibt die Situation, wenn es Änderungen auf beiden Plattformen gab. Hier muss die Entscheidung der Anwenderin überlassen bleiben, welcher Datensatz der richtige ist.

Fehlt noch das Löschen. Vom Piloten entfernte Datensätze bleiben zunächst dort erhalten, sie sind lediglich mit zwei Flags markiert. Das erste steht für ‘gelöscht’, ein zweites ist gesetzt, wenn eine Archivkopie auf dem PC bleiben soll. In diesem Fall ist es das einfachste, die Einträge auf dem Host und dem Piloten nicht zu ändern: Der Pilot zeigt die Einträge nicht mehr an, auf dem Host sind sie aber weiterhin vorhanden. Fehlt das Archiv-Flag, löscht man einfach die Adresse von beiden Rechnern. Völlig sicher ist dieses Verfahren nicht: Wer mit pilot-xfer -p alle markierten Datensätze aus den Pilot-Datenbanken entfernt, löst dadurch bei der nächsten Synchronisierung mit dem Script die Übertragung der gelöschten, aber auf dem Host archivierten Adressen aus. Hier würde nur ein passendes Flag in der Host-Datenbank weiterhelfen.

Mit drei Optionen ist das Verhalten des Scripts steuerbar: d|ebug sorgt für ein Protokoll auf der Standardausgabe, s|imulate gibt lediglich aus, was passieren würde. r|eplace legt fest, ob die Adresse auf dem Piloten (replace=host) oder auf dem Host (replace=pilot) gelten soll, wenn sich beide seit dem letzten Abgleich geändert haben. Für die Auswertung dieser Optionen sorgt das Modul Getopt::Long.

Mehr Infos

Listing 2

  1 #!/usr/bin/perl -w
2
3 use PDA::Pilot;
4 use DBI;
5 use Data::Dumper;
6 use Getopt::Long;
7 use Time::Local;
8
...
113 #
114 my $dlp = PDA::Pilot::accept($socket);
115
116 my $uinfo = $dlp->getUserInfo();
117 my $lastsync = $uinfo->{'successfulSyncDate'};
...
121 my $db = $dlp->open("AddressDB");
122
123 $dlp->getStatus();
124
125 my $app = $db->getAppBlock;
...
129 my @phone_labels = @{$app->{'phoneLabel'}};
130
131 my @default_phone;
132
133 @default_phone = (0, 1, 2, 4, 7);
134
135 my @category_labels = @{$app->{'categoryName'}};
...
139 my %category_numbers;
140 my %number_categories;
141 my $i = 0;
142 foreach (@category_labels) {
143 next unless $_;
144 $category_numbers{$_} = $app->{'categoryID'}[$i];
145 $number_categories{$app->{'categoryID'}[$i]} = $i;
146 $i++;
147 }
...
154 $i = 0;
155 my %recordlist;
156 my $r;
157 my %deleted = ();
158 #
159 # Alle Daten aus AdressDB holen und in lokalem Hash speichern
160 #
161 while (defined($r = $db->getRecord($i++))) {
...
166 if ($r->{'deleted'}) {
167 $deleted{$r->{'id'}} = $r->{'archived'};
168 next;
169 }
...
171 my @entries = @{$r->{'entry'}};
172 my $id = $r->{'id'};
173 my $j=0;
174 my ($val,$field);
175 $recordlist{$id} = {};
176 my %entrylist = ();
177 my @fieldnames = ();
178 foreach $val (@entries) {
179 $field = $pilot_entries[$j];
180 $field = findphone($r,$1)
181 if ($field =~ /Phone(\d)/) ;
182 #
183 # Bereits vorhandene Einträge nicht überschreiben!
184 #
185 $entrylist{$field} = $val unless
186 $entrylist{$field};
187 $j++;
188 }
189 $entrylist{'modified'} = $r->{'modified'};
190 $entrylist{'phoneLabel'} = $r->{'phoneLabel'};
191 $entrylist{'showPhone'} = $r->{'showPhone'};
192 $entrylist{'category'} = $r->{'category'};
193 $recordlist{$id} = \%entrylist;
194 } # while defined $r
...
202 $dlp->tickle;
203
204 my $dbh = DBI->connect("DBI:$RDBMS:$DB","$USER", $PWD);
...
211 my @ids = grep { ! $deleted{$_} } keys %deleted ;
212 my $query;
213 if (scalar(@ids)) {
214 $query = "DELETE FROM $TABLE WHERE $primary_key IN ( ".
215 join (',', @ids) . ")";
216
217 $dbh->do($query) unless $simulate;
...

228 foreach (@ids) {
230 $db->deleteRecord($_) unless $simulate;
...
232 }
...
240 my %DB_record = map { ($_, undef) } values(%pilot_to_DB);
241 $DB_record{$primary_key} = undef;
242 $DB_record{$category} = undef;
243 $DB_record{$timestamp} = undef;
...

249 my @DB_fields = sort keys(%DB_record);
250
251 $query = "SELECT " . join (',',(@DB_fields)) .
252 " FROM $TABLE order by $primary_key";
...

257 my $sth = $dbh->prepare($query);
258 $sth->execute();
259 $i = 1;
260 foreach (@DB_fields) {
261 $sth->bind_col($i++,\$DB_record{$_});
262 }
263
264 while ($sth->fetch()) {
265 $dlp->tickle;
266 my $id = $DB_record{$primary_key};
267 my $k;
268 my $modsec = stamptoepoch($DB_record{$timestamp});
...
273 if (! exists($recordlist{$id}) && !exists($deleted{$id})){
274
275 host_to_pilot($db,$DB_record{$primary_key},\%DB_record);
276
277 } else {
...
281 my $r = $recordlist{$id};
...
286 if ($r->{'modified'}) {
...
293 if ($lastsync > $modsec) {
294 pilot_to_host($dbh, $id, \%DB_record, $r);
295 } else {
...
300 if ($replace =~ /host/) {
301 $db->deleteRecord($id);
302 pilot_to_host($dbh, $id,\%DB_record, $r);
303 } elsif ($replace =~ /pilot/) {
304 host_to_pilot($db,$DB_record{$primary_key},
\%DB_record);
...
309 }
310 }
311 } elsif ($modsec > $lastsync) {
...

326 $db->deleteRecord($id) unless $simulate;
...
329 host_to_pilot($db,$id,\%DB_record);
...
340 delete($recordlist{$id});
341 }
342} # while $sth->fetch
...
355 foreach $k (keys %recordlist) {
356 $dlp->tickle;
357 my $r = $recordlist{$k};
358 $db->deleteRecord($k) unless $simulate;
...
380 next unless scalar(keys %fields);
381 my $query = "insert into adressen ( " .
382 join(',',sort (keys %fields)) . ") values (" .
383 join(',', map { $fields{$_}} sort (keys %fields)) . ")";
...
387 $dbh->do($query) if (!$simulate);
...

391 my $copy = $db->newRecord;
392 $copy->{'id'} = $sth->{'mysql_insertid'};
393 $copy->{'entry'} = $r->{'entry'};
394 $copy->{'phoneLabel'} = $r->{'phoneLabel'};
395 $copy->{'showPhone'} = $r->{'showPhone'};
396 $copy->{'category'} = $r->{'category'};
...
399 $db->setRecord($copy) unless $simulate;
400 }
...

410 $dbh->do("delete from kategorien");
411 $query = "insert into kategorien values (?,?)";
412 $sth = $dbh->prepare($query);
413 foreach (keys %category_numbers) {
414 $sth->execute($category_numbers{$_},$_);
415 }
...

424 $dlp->log("MySQL-Sync done");
425 $db->resetFlags();
426 $uinfo->{'lastSyncDate'} = time();
427 $uinfo->{'successfulSynCate'} = time();
428 $dlp->setUserInfo($uinfo);

Nach dem Herstellen der Verbindung zum Piloten in Zeile 114 besorgt sich das Script das Datum der letzten Synchronisierung und speichert es in $lastsync. Anschließend öffnet es die Adressdatenbank und sorgt in Zeile 123 dafür, dass der Pilot ‘Synching AdressDB’ anzeigt. Das dazu benutzte getStatus() ist irreführend benannt, es löst letztendlich nur den openConduit-Aufruf aus, der als Nebeneffekt eben den Namen der benutzten Datenbank im Display anzeigt. Anschließend besorgt sich das Script die benutzten Telefon- und Kategorienbeschriftungen.

Adressen auf dem Piloten können fünf Telefonnummern enthalten, insgesamt gibt es acht verschiedene Arten solcher Nummern, zu denen auch E-Mail-Adressen gehören. Das Array @{$app->{’phoneLabel’}} enthält die Namen der verfügbaren Arten von Nummern, in der US-Version des PalmOS etwa [Work Home Fax Other Email Main Pager Mobile]. Jede einzelne Adresse enthält im Array $record->{’phonelabel’} fünf Zahlen, die angeben, welche Nummer mit welchem Eintrag beschriftet ist. Steht dort also [0 3 5 1 7], dann gehört die erste Nummer dieser Adresse zum Arbeitsplatz, die zweite zum Fax, die dritte ist die Mail-Adresse, die vierte der Privatanschluss und die letzte die Handynummer. Ähnlich verläuft die Zuordnung eines Eintrags zu einer Kategorie. Da Palm-Besitzer diese (anders als die Beschriftung der Telefoneinträge) selbst ändern und ergänzen können, ist die Zuordnung zwischen Nummern und Beschriftungen etwas komplizierter. Das Script speichert sie ab Zeile 139 in zwei Hashes (%category_numbers und %number_categories).

Anschließend werden alle Einträge der Pilot-Datenbank ausgelesen (ab Zeile 161). Ein gesetztes ‘Löschen’-Flag führt zu einem neuen Eintrag in %deleted, dessen Schlüssel das id-Feld des Datensatzes ist. Als Wert steht dort das ‘Archiv’-Flag: Eine 1, falls der Eintrag auf dem Host verbleiben soll, sonst eine Null. Nicht zum Löschen markierte Adressen schreibt das Script ab Zeile 171 in den Hash %recordlist. Seine Schlüssel sind wiederum die id-Felder, die dem Primary Key der Host-Datenbank entsprechen. Als Wert enthält er einen anonymen Hash, der die eigentlichen Daten (Vorname, Name et cetera), das ‘Geändert’-Flag und die Kategorie enthält.

Ist das erledigt, beginnt die Verarbeitung der einzelnen Datensätze der Host-Datenbank. Da das ein wenig dauern kann, sorgt $dlp->tickle in Zeile 202 dafür, dass der Pilot nicht wegen Inaktivität die Verbindung abbricht. Nach dem Öffnen der Datenbank in Zeile 204 entfernt das Script zunächst alle Datensätze vom Host, die auf dem Piloten zum Löschen markiert und ohne Archivflag sind (Zeile 213). Das in Zeile 211 benutzte grep sammelt dazu alle Schlüssel aus %deleted, deren zugehöriger Wert Null ist, im Array @ids. Der nächste Schritt entfernt diese Einträge auch vom Piloten (ab Zeile 228). Dadurch bleiben die Adressen, die zum Löschen markiert sind und von denen eine Archivkopie auf dem Host bleiben soll, auf beiden Plattformen erhalten. Auf dem Piloten sind sie nicht länger sichtbar, werden jedoch weiterhin beim Auslesen der Datensätze mit den beiden Flags übertragen.

Zeile 240 baut den Hash %DB_record auf, der jeweils einen Host-Datensatz aufnimmt. Er enthält als Schlüssel die interessierenden Feldnamen der Datenbank. Das Array @DB_fields speichert vorübergehend dieselben Feldnamen, allerdings in einer definierten Reihenfolge (die der Hash nicht garantieren kann). Diese stellt sicher, dass die in Zeile 261 stattfindende Zuordnung von Hash-Werten zu den Feldern der SQL-Abfrage in Zeile 251 korrekt ist.

Ab Zeile 264 findet der Abgleich der beiden Datenbestände wie oben beschrieben statt. Die Hauptarbeit erledigen dabei die Routinen host_to_pilot() und pilot_to_host(). Wiederum sorgt $dlp->tickle vor der Verarbeitung jedes Datensatzes dafür, dass die Verbindung zwischen Pilot und Host bestehen bleibt. Die Routine stamptoepoch() konvertiert MySQLs timestamp-Datentyp in die Anzahl der seit 1. Januar 1970 verstrichenen Sekunden - in diesen Einheiten speichert der Pilot seine Daten.

Ein Detail ist noch interessant: Datensätze auf dem Piloten können nicht einfach verändert werden, man muss sie zunächst löschen und dann mit den modifizierten Werten neu erzeugen. Deshalb enthält das Script beispielsweise in Zeile 301 einen Aufruf von dlp->deleteRecord(). Jeden verarbeiteten Pilot-Datensatz entfernt das Script in Zeile 340 aus dem Hash. Dadurch enthält er am Ende der while-Schleife nur solche Adressen, die in der Host-Datenbank noch fehlen.

Die foreach-Schleife ab Zeile 355 löscht jeden dieser übrig gebliebenen Einträge vom Piloten (Zeile 358) und überträgt die Daten an den Host (Zeilen 381 bis 387). Anschließend besorgt es sich den von MySQL dabei vergebenen neuen Wert des Primary Key und erzeugt aus diesem und den Daten den Eintrag mit setRecord() erneut auf dem Piloten. Dadurch stimmen das id-Feld auf diesem und der Primary Key auf dem Host für jeden Datensatz immer überein.

Zu guter Letzt aktualisiert das Script die Kategorien auf dem Host. Dem liegt die naive Annahme zugrunde, dass man diese nur auf dem mobilen Gerät modifiziert. Wer sowohl auf dem Host als auch auf dem Piloten daran herumspielen will, muss an dieser Stelle noch ein bisschen Code schreiben. Zur Zeit löscht das Script einfach alle vorhandenen Kategorien und erzeugt die Tabelle dann neu (Zeilen 410 bis 415). Zum Schluss räumt es auf: $dlp->log() erzeugt einen aktuellen Log-Eintrag auf dem Piloten, $db->resetFlags() setzt alle Änderungsmarkierungen der Datenbank zurück und $dlp->setUserInfo() schließlich aktualisiert das Datum des letzten HotSync.

Mehr Infos

iX-TRACT

  • Übliche Software zum Synchronisieren des PalmPilot speichert die Daten in einem proprietären Format.
  • Ein Perl-Script realisiert den Abgleich der Pilot-Adressen mit einer SQL-Datenbank.
  • Haben sich Daten auf beiden Plattformen geändert, ist menschliches Eingreifen nötig.

(ck)