sms2unicode.tcl

 

J'ai acheté un eeePC avec le modem USB 3G Huawei. L'eeePC fonctionne sous Linux Xandros, une distribution basée sur Debian.

Aucun programme n'est installé pour lire ou envoyer des SMS. Contrairement à l'offre SFR Vodafone pour Windows Microsoft.

J'ai trouvé le package vodafone-mobile-connect-card-driver-for-linux_1.99.17_i386.deb. Il fait 1.6M et demande de nombreux autres packages pour s'installer. Il est écrit en python avec une interface Gtk.

Tous ça pour lire quelques SMS. On doit pouvoir faire plus simple.

Premièrement pour récupérer les SMS sur le modem, pourquoi ne pas utiliser Minicom et la commande AT+CMGL=4 . Ce qui donne:

    +CMGL: 0,1,,60
    07913306092045F004038121F346008020802133840033D202FCED2697EB721D280672BFEBF672B80E6A97E7F3F0B90CFA836232DD6CE60249C37078995DD68362B2990B
    +CMGL: 1,1,,155
    07913306091093F0040485810000F180208021137540A049B7F90D9A1AA53AFB9B2E2F839C6F10B90C3A4E9BA0F29C0E629741309B8C16ABD968379CCBFAA6CBCBA0F19B5C06C1CBF2F91B549ED341EC3268468BD55C4936085E96B7CB7410B90C3A17E46539C8FEA6CBCBA0319C5E0695DDA06B51C8BA06A1A0771D644F8741EC3228679BB940CD42FB2D4FCFCB7A10BB0C2AD341E7B09C5CD683D865D0BC3C9697E9
    +CMGL: 2,1,,136
    07913306091093F0040485810000F1802090415350408A49B7F90D9A1AA53AFB9B2E2F83DCEFBABD1CAE83C66F7219042FCBE76FB7BBCC0695E77410BB0CD2816AB11BCC054AB241F6777D0E8297E5ED321D443E85C7E302B92C0785EB78D0BC2CB7A7C7E539686A94BA40CDB27C9C0691CB207619D42EB4DFF2F4BC2C0795E9207619740ECBC96539685E1ECBCB7417
    +CMGL: 3,1,,109
    07913306092045F0040AA0609045902700008030206191710067D202FCED2697EB72D0D42875299865104CF682CD5E301CE80F8AD9D0311B0B342E83C66FB9BC3C87BFDDE4B09B0E0A83C6E8B27C8C2E80FE20FBBB3E07A9DF6937595E06CDC3EE39881D4ECFE76539885C06B5CBF379F85C062900
    +CMGL: 4,1,,136
    07913306091093F0040485810000F1803040211323408A49B7F90D9A1AA53AFB9B2E2F83DCEFBABD1CAE83C66F7219042FCBE76FB7BBCC0695E77410BB0CD28162339ACC054AB241F6777D0E8297E5ED321D443E85C7E302B92C0785EB78D0BC2CB7A7C7E539686A94BA40CDB27C9C0691CB207619D42EB4DFF2F4BC2C0795E9207619740ECBC96539685E1ECBCB7417
    +CMGL: 6,1,,75
    07913306092045F004038121F346008030122130940044D202FCED2697EB721D480672BFEBF672B88E07B5CBF379F85C9EEF40E4B2DC9D2ECB416138BCCC06FD4131990E367381A46138BCCC2EEB4131D9CC05

A l'adresse http://www.developershome.com/sms/cmgrCommand3.asp toutes les explications sur le formats de ces données.

En gros elles contiennent des infos sur la date du SMS les numéros de téléphones et le message du SMS codé dans le "GSM 7-bit default alphabet".

Je n'ai pas trouvé de petit programme pour traduire ces données, d'où l'idée d'en écrire un en TCL. C'est sur il est pas beau, on doit pouvoir faire bien mieux. Pour les conversions de base des nombres je fait appel à l'utilitaire Unix bc. Je ne traite pas les caractères 0x1B ESCAPE TO EXTENSION TABLE, pour le moment. Et il n'est pas très rapide.

    #!/usr/bin/tclsh
    ##sms2unicode.tcl
    ##25/03/08

    #-----------------------------------------------------------------
    #                              Help
    #-----------------------------------------------------------------

    proc Help {} {
       puts "Usage: sms2unicode.tcl \[options\]  filename.txt           "
       puts "                     -             read on stdin           "
       puts "                     -h            print help              "
       puts ""
       exit 0
    }

    #-------------------------------------------------------------------------
    #                           NumberConvert
    #-------------------------------------------------------------------------

    proc NumberConvert { number } {
       set _number ""
       while { $number != "" } {
	  set n      [ string range $number 0 1   ]
	  set number [ string range $number 2 end ]
	  set _number "$_number [string index $n 1][string index $n 0]"
       }
       set number [ regsub "F" $_number "" ]
       return $number
    }

    #-------------------------------------------------------------------------
    #                         Caracteres table
    #------------------------------------------------------------------------

    set _table {
    0x00	0x0040	#	COMMERCIAL AT
    #0x00	0x0000	#	NULL (see note above)
    0x01	0x00A3	#	POUND SIGN
    0x02	0x0024	#	DOLLAR SIGN
    0x03	0x00A5	#	YEN SIGN
    0x04	0x00E8	#	LATIN SMALL LETTER E WITH GRAVE
    0x05	0x00E9	#	LATIN SMALL LETTER E WITH ACUTE
    0x06	0x00F9	#	LATIN SMALL LETTER U WITH GRAVE
    0x07	0x00EC	#	LATIN SMALL LETTER I WITH GRAVE
    0x08	0x00F2	#	LATIN SMALL LETTER O WITH GRAVE
    0x09	0x00E7	#	LATIN SMALL LETTER C WITH CEDILLA
    #0x09	0x00C7	#	LATIN CAPITAL LETTER C WITH CEDILLA (see note above)
    0x0A	0x000A	#	LINE FEED
    0x0B	0x00D8	#	LATIN CAPITAL LETTER O WITH STROKE
    0x0C	0x00F8	#	LATIN SMALL LETTER O WITH STROKE
    0x0D	0x000D	#	CARRIAGE RETURN
    0x0E	0x00C5	#	LATIN CAPITAL LETTER A WITH RING ABOVE
    0x0F	0x00E5	#	LATIN SMALL LETTER A WITH RING ABOVE
    0x10	0x0394	#	GREEK CAPITAL LETTER DELTA
    0x11	0x005F	#	LOW LINE
    0x12	0x03A6	#	GREEK CAPITAL LETTER PHI
    0x13	0x0393	#	GREEK CAPITAL LETTER GAMMA
    0x14	0x039B	#	GREEK CAPITAL LETTER LAMDA
    0x15	0x03A9	#	GREEK CAPITAL LETTER OMEGA
    0x16	0x03A0	#	GREEK CAPITAL LETTER PI
    0x17	0x03A8	#	GREEK CAPITAL LETTER PSI
    0x18	0x03A3	#	GREEK CAPITAL LETTER SIGMA
    0x19	0x0398	#	GREEK CAPITAL LETTER THETA
    0x1A	0x039E	#	GREEK CAPITAL LETTER XI
    0x1B	0x00A0	#	ESCAPE TO EXTENSION TABLE (or displayed as NBSP, see note above)
    0x1B0A	0x000C	#	FORM FEED
    0x1B14	0x005E	#	CIRCUMFLEX ACCENT
    0x1B28	0x007B	#	LEFT CURLY BRACKET
    0x1B29	0x007D	#	RIGHT CURLY BRACKET
    0x1B2F	0x005C	#	REVERSE SOLIDUS
    0x1B3C	0x005B	#	LEFT SQUARE BRACKET
    0x1B3D	0x007E	#	TILDE
    0x1B3E	0x005D	#	RIGHT SQUARE BRACKET
    0x1B40	0x007C	#	VERTICAL LINE
    0x1B65	0x20AC	#	EURO SIGN
    0x1C	0x00C6	#	LATIN CAPITAL LETTER AE
    0x1D	0x00E6	#	LATIN SMALL LETTER AE
    0x1E	0x00DF	#	LATIN SMALL LETTER SHARP S (German)
    0x1F	0x00C9	#	LATIN CAPITAL LETTER E WITH ACUTE
    0x20	0x0020	#	SPACE
    0x21	0x0021	#	EXCLAMATION MARK
    0x22	0x0022	#	QUOTATION MARK
    0x23	0x0023	#	NUMBER SIGN
    0x24	0x00A4	#	CURRENCY SIGN
    0x25	0x0025	#	PERCENT SIGN
    0x26	0x0026	#	AMPERSAND
    0x27	0x0027	#	APOSTROPHE
    0x28	0x0028	#	LEFT PARENTHESIS
    0x29	0x0029	#	RIGHT PARENTHESIS
    0x2A	0x002A	#	ASTERISK
    0x2B	0x002B	#	PLUS SIGN
    0x2C	0x002C	#	COMMA
    0x2D	0x002D	#	HYPHEN-MINUS
    0x2E	0x002E	#	FULL STOP
    0x2F	0x002F	#	SOLIDUS
    0x30	0x0030	#	DIGIT ZERO
    0x31	0x0031	#	DIGIT ONE
    0x32	0x0032	#	DIGIT TWO
    0x33	0x0033	#	DIGIT THREE
    0x34	0x0034	#	DIGIT FOUR
    0x35	0x0035	#	DIGIT FIVE
    0x36	0x0036	#	DIGIT SIX
    0x37	0x0037	#	DIGIT SEVEN
    0x38	0x0038	#	DIGIT EIGHT
    0x39	0x0039	#	DIGIT NINE
    0x3A	0x003A	#	COLON
    0x3B	0x003B	#	SEMICOLON
    0x3C	0x003C	#	LESS-THAN SIGN
    0x3D	0x003D	#	EQUALS SIGN
    0x3E	0x003E	#	GREATER-THAN SIGN
    0x3F	0x003F	#	QUESTION MARK
    0x40	0x00A1	#	INVERTED EXCLAMATION MARK
    0x41	0x0041	#	LATIN CAPITAL LETTER A
    #0x41	0x0391	#	GREEK CAPITAL LETTER ALPHA
    0x42	0x0042	#	LATIN CAPITAL LETTER B
    #0x42	0x0392	#	GREEK CAPITAL LETTER BETA
    0x43	0x0043	#	LATIN CAPITAL LETTER C
    0x44	0x0044	#	LATIN CAPITAL LETTER D
    0x45	0x0045	#	LATIN CAPITAL LETTER E
    #0x45	0x0395	#	GREEK CAPITAL LETTER EPSILON
    0x46	0x0046	#	LATIN CAPITAL LETTER F
    0x47	0x0047	#	LATIN CAPITAL LETTER G
    0x48	0x0048	#	LATIN CAPITAL LETTER H
    #0x48	0x0397	#	GREEK CAPITAL LETTER ETA
    0x49	0x0049	#	LATIN CAPITAL LETTER I
    #0x49	0x0399	#	GREEK CAPITAL LETTER IOTA
    0x4A	0x004A	#	LATIN CAPITAL LETTER J
    0x4B	0x004B	#	LATIN CAPITAL LETTER K
    #0x4B	0x039A	#	GREEK CAPITAL LETTER KAPPA
    0x4C	0x004C	#	LATIN CAPITAL LETTER L
    0x4D	0x004D	#	LATIN CAPITAL LETTER M
    #0x4D	0x039C	#	GREEK CAPITAL LETTER MU
    0x4E	0x004E	#	LATIN CAPITAL LETTER N
    #0x4E	0x039D	#	GREEK CAPITAL LETTER NU
    0x4F	0x004F	#	LATIN CAPITAL LETTER O
    #0x4F	0x039F	#	GREEK CAPITAL LETTER OMICRON
    0x50	0x0050	#	LATIN CAPITAL LETTER P
    #0x50	0x03A1	#	GREEK CAPITAL LETTER RHO
    0x51	0x0051	#	LATIN CAPITAL LETTER Q
    0x52	0x0052	#	LATIN CAPITAL LETTER R
    0x53	0x0053	#	LATIN CAPITAL LETTER S
    0x54	0x0054	#	LATIN CAPITAL LETTER T
    #0x54	0x03A4	#	GREEK CAPITAL LETTER TAU
    0x55	0x0055	#	LATIN CAPITAL LETTER U
    #0x55	0x03A5	#	GREEK CAPITAL LETTER UPSILON
    0x56	0x0056	#	LATIN CAPITAL LETTER V
    0x57	0x0057	#	LATIN CAPITAL LETTER W
    0x58	0x0058	#	LATIN CAPITAL LETTER X
    #0x58	0x03A7	#	GREEK CAPITAL LETTER CHI
    0x59	0x0059	#	LATIN CAPITAL LETTER Y
    0x5A	0x005A	#	LATIN CAPITAL LETTER Z
    #0x5A	0x0396	#	GREEK CAPITAL LETTER ZETA
    0x5B	0x00C4	#	LATIN CAPITAL LETTER A WITH DIAERESIS
    0x5C	0x00D6	#	LATIN CAPITAL LETTER O WITH DIAERESIS
    0x5D	0x00D1	#	LATIN CAPITAL LETTER N WITH TILDE
    0x5E	0x00DC	#	LATIN CAPITAL LETTER U WITH DIAERESIS
    0x5F	0x00A7	#	SECTION SIGN
    0x60	0x00BF	#	INVERTED QUESTION MARK
    0x61	0x0061	#	LATIN SMALL LETTER A
    0x62	0x0062	#	LATIN SMALL LETTER B
    0x63	0x0063	#	LATIN SMALL LETTER C
    0x64	0x0064	#	LATIN SMALL LETTER D
    0x65	0x0065	#	LATIN SMALL LETTER E
    0x66	0x0066	#	LATIN SMALL LETTER F
    0x67	0x0067	#	LATIN SMALL LETTER G
    0x68	0x0068	#	LATIN SMALL LETTER H
    0x69	0x0069	#	LATIN SMALL LETTER I
    0x6A	0x006A	#	LATIN SMALL LETTER J
    0x6B	0x006B	#	LATIN SMALL LETTER K
    0x6C	0x006C	#	LATIN SMALL LETTER L
    0x6D	0x006D	#	LATIN SMALL LETTER M
    0x6E	0x006E	#	LATIN SMALL LETTER N
    0x6F	0x006F	#	LATIN SMALL LETTER O
    0x70	0x0070	#	LATIN SMALL LETTER P
    0x71	0x0071	#	LATIN SMALL LETTER Q
    0x72	0x0072	#	LATIN SMALL LETTER R
    0x73	0x0073	#	LATIN SMALL LETTER S
    0x74	0x0074	#	LATIN SMALL LETTER T
    0x75	0x0075	#	LATIN SMALL LETTER U
    0x76	0x0076	#	LATIN SMALL LETTER V
    0x77	0x0077	#	LATIN SMALL LETTER W
    0x78	0x0078	#	LATIN SMALL LETTER X
    0x79	0x0079	#	LATIN SMALL LETTER Y
    0x7A	0x007A	#	LATIN SMALL LETTER Z
    0x7B	0x00E4	#	LATIN SMALL LETTER A WITH DIAERESIS
    0x7C	0x00F6	#	LATIN SMALL LETTER O WITH DIAERESIS
    0x7D	0x00F1	#	LATIN SMALL LETTER N WITH TILDE
    0x7E	0x00FC	#	LATIN SMALL LETTER U WITH DIAERESIS
    0x7F	0x00E0	#	LATIN SMALL LETTER A WITH GRAVE
    }

    # Create array table

    foreach line [ split $_table "\n" ] {
	set line [ split $line ]
	set table([lindex $line 0]) [lindex $line 1]
    }

    #----------------------------------------------------------------------------------
    #                                MAIN
    #----------------------------------------------------------------------------------

    if { [ llength $argv ] == 0 } { Help }

    while { [ llength $argv ] } {
       set a [ lrange $argv 0 0 ]

       switch -glob -- $a {

	  -h { Help }

	  -  { set data [ read stdin ] }

	  -* { puts "$argv0: syntax error $a"; exit 1 }

	   * { set FILENAME [ lrange   $argv 0 0 ]
	       set fd [ open "$FILENAME" r ]
	       set data [ read $fd ]
	       close $fd
	       break
	     }
       }
       set argv [ lrange $argv 1 end ]
    }

    # +CMGL: index,message_status,address,[address_text],[service_center_time_stamp][,address_type,sms_message_body_length]<CR><LF>sms_message_body[<CR><LF>+CMGL: ...]
    #
    # The message_status Field
    # +CMGL: 0,1,,60
    # +CMGL: 1,1,,155
    # +CMGL: 2,1,,136

    set data [ split $data "\n" ]

    set l [ lindex $data 0 ]
    set l [ split $l "," ]

    # Message index
    set m [ lindex $l 0]
    set m [ split $m ":" ]
    puts "Message n°[lindex $m 1]"

    # TPDU lenght
    set l [ lindex $l end ]
    set l [ expr $l * 2 ]

    set data [ lindex $data 1 ]

    set SMCS [ string range $data 0 end-$l ]
    set l [ expr $l - 1 ]
    set TPDU [ string range $data end-$l end ]

    #--------------------------------------------------------------------
    # SMCS traitement

    # 1  2  3
    # 07 91 3306092045F0
    # 07 91 3306091093F0
    # 07 91 3306091093F0
    # 07 91 3306092045F0
    #
    # 1: Length of the Second and Third Sub-fields
    # 2: Type of SMSC Number
    # 3: SMSC Number

    set n [ string range $SMCS 4 end ]
    puts "SMSC Number: [ NumberConvert $n ]"

    #--------------------------------------------------------------------
    # TPDU traitement

    # 1  2  3  4          5  6  7              8
    # 04 0A A0 6090459027 00 00 80302061917100 67
    # 04 0A A0 6090459027 00 00 80304041549000 67
    # 04 03 81 21F3       46 00 80208021338400 33
    # 04 04 85 8100       00 F1 80208021137540 A0
    # 04 04 85 8100       00 F1 80209041535040 8A
    # 04 04 85 8100       00 F1 80304021132340 8A
    # 04 03 81 21F3       46 00 80301221309400 44
    #
    # 1:
    # 2: Length of the Sender Phone Number
    # 3: Type of the Sender Phone Number
    # 4: Sender Phone Number
    # 5: Protocol Identifier
    # 6: Data Coding Scheme
    # 7: Service Center Time Stamp
    # 8: Length of the SMS Message Body

    # Phone number lenght
    set l [ string range $TPDU 2 3 ]
    set l [ exec echo "ibase=16; $l" | bc ]
    set l [ expr $l + ($l%2) ]

    # Phone number
    set i [ expr $l + 5 ]
    set n [ string range $TPDU 6 $i ]
    puts "Sender Phone Number: [ NumberConvert $n ]"

    # Date
    set i0 [ expr 10 + $l ]
    set i1 [ expr 23 + $l ]
    set d [ string range $TPDU $i0 $i1 ]

    set d [ NumberConvert $d ]
    set d [ string trim $d   ]
    # 08 02 08 12 31 57 04
    set d [ split $d ]

    set day "[lindex $d 1 ] [lindex $d 2] [lindex $d 0]"
    set day [ join $day "/" ]
    set hour "[expr [lindex $d 3]+([lindex $d 6]/4)] [lindex $d 4] [lindex $d 5]"
    set hour [ join $hour ":" ]

    puts [ exec date -d "$day $hour" ]

    # Message body
    set i [ expr 26 + $l ]
    set data [ string range $TPDU $i end ]

    #-------------------------------------------------------------------
    # Convert Hexa -> bin
    set _data ""

    while { $data != "" } {
       set val [ string range $data 0 1 ]
       set data [ string range $data 2 end ]
       set val [ exec echo "ibase=16; $val" | bc ]
       set val [ exec echo "obase=2 ; $val" | bc ]
       set val [ format "%08s" $val ]
       set _data "$val$_data"
    }

    #--------------------------------------------------------------------
    # 7 bits split

    set data $_data
    set _data {}

    while { $data != "" } {
       set val [ string range $data end-6 end ]
       set data [ string range $data 0 end-7 ]
       lappend _data $val
    }

    #----------------------------------------------------------------------
    # Convert 7 bits to unicode

    foreach val $_data {
      set val [ exec echo "ibase=2; $val" | bc ]
      set val [ exec echo "obase=16; $val" | bc ]
      set val [ format "%02s" $val ]
      set val [ lindex [ array get table "0x$val" ] 1 ]
      puts -nonewline [ format "%c" $val ]
    }

    puts ""

Après passage des données dans ce programme, on obtient:

    ------------------------------------------------------------------
    Message n° 0
    SMSC Number:  33 60 90 02 54 0
    Sender Phone Number:  12 3
    vendredi 8 février 2008, 12:33:48 (UTC+0100)
    Répondeur: 1 nouveau message à 12:33. Rappelez 123.@
    ------------------------------------------------------------------
    Message n° 1
    SMSC Number:  33 60 90 01 39 0
    Sender Phone Number:  18 00
    vendredi 8 février 2008, 13:31:57 (UTC+0100)
    Info SFR:votre No de GSM est le 0624156478.Votre code perso est le 3415.Il permet de gérer votre cpte en WEB,WAP ou via le 963. Mémorisez le et gardez le secret
    ------------------------------------------------------------------
    Message n° 2
    SMSC Number:  33 60 90 01 39 0
    Sender Phone Number:  18 00
    samedi 9 février 2008, 15:35:05 (UTC+0100)
    Info SFR:votre nouveau code personnel est le ...............

Pour automatiser tous cela j'ai un petit script shell qui lance Minicom et passe les données brutes à sms2unicode.tcl pour le décodage. Ce script montre comment utiliser tclsh dans un script shell grâce aux documents en ligne.

    #!/bin/sh

    sudo rm capture

    # Appel de Minicom pour lire les SMS

    (
    cat <<'EOF'
    expect {
       "OK"
       timeout 4 break
    }
    send AT+CMGL=4
    expect {
       "OK" sleep 2
       timeout 2 break
    }
    ! killall minicom
    EOF
    ) > script
    sudo minicom -S script -C capture

    # Conversion des SMS vers l'unicode

    (
    tclsh <<'EOF'
       set fd [ open "capture" r ]
       set lines [ read $fd ]
       close $fd
       set lines [ split $lines "\n" ]
       set lines [ lrange $lines 2 end-2 ]
       puts "------------------------------------------------------------------"
       foreach {l1 l2} $lines {
	  puts [ exec echo -e "$l1\n$l2" | sms2unicode.tcl - ]
	  puts "------------------------------------------------------------------"
       }
    EOF
    )

2008-03-28 EH Quelques commentaires à propos de la vitesse d'exécution: Pourquoi s'embêter à utiliser minicom, alors que Tcl peut parfaitement bien dialoguer directement avec le port série du modem ? De même, les très nombreux appels a bc pourront avantageusement être remplacé par la commande Tcl binary. Enfin (voire surtout), la boucle de conversation 7 bits vers unicode pourra avantageusement être remplacée par un appel direct a 'string map' depuis une liste de correspondance en lieu et place du tableau 'table'. La vitesse finale sera alors très (et même bien plus) nettement améliorée.

2008-04-13 Philippe Cassignol Un grand merci pour vos conseil. J'ai réécrit sms2unicode.tcl avec les commandes Tcl binary et string map. Commandes que je ne connaissais pas, dire mon niveau, ça marche évidemment beaucoup mieux. Pour ne pas encombrer ce wiki voir à l'adresse http://pagesperso-orange.fr/philippe.cassignol/box/bin/sms2unicode.tcl

Pour ma défense, l'informatique n'est pas mon métier, juste un loisir. Je suis viticulteur.

Pour ce qui est de Minicom, je le trouve très pratique à utiliser, il configure les paramètres du port série (vitesse parité contrôle de flux ...), il initialise et RAZ le modem automatiquement, il vérifie si le modem n'est pas utilisé par une autre application ainsi qu'une foule d'autres options (Chaîne de non-connexion, temps d'appel max, nombre d'essais ...). Et son langage de script est très simple.

Réécrire tout cela en Tcl avec open fconfigure ... dépasse mes compétences.

Et puis la philosophie d'Unix, n'est-elle pas de faire de petits programmes simples qui font une seule chose à la fois et qui le font bien. Et ensuite d'assembler tous cela dans des scripts shell ?

2008-04-14 - Kroc - Diantre : quel niveau de connaissance en informatique pour un viticulteur, et tant au niveau technique que philosophique ! J'étais plutôt habitué à voir l'inverse (ce qui est bien moins impressionnant, il faut l'avouer). Plus sérieusement, pour un exemple de "minicom light" en Tcl il y a SerPort Chat ; le code n'est pas très compliqué, ce qui devrait le rendre assez facile à intégrer dans sms2unicode.

2008-04-14 - CK - En effet. Philippe, on pourrait penser que vous êtes un programmeur qui fait aussi viticulteur! :-) Bravo! Si votre vin est aussi bon que votre code, je suis prêt è me saoûler avec! :-)