OBEX

From the OBEX 1.3 specification:

OBEX is a compact, efficient, binary protocol that enables a wide range of devices to exchange data in a simple and spontaneous manner. ... OBEX performs a function similar to HTTP, a major protocol underlying the World Wide Web.

Originally directed towards IrDA, OBEX is now commonly used over Bluetooth and can be layered on TCP/IP as well. Tcl implementations of OBEX include the obex package and the implementation from Anton Kovalenko below.

Anton Kovalenko: OBEX is a shorthand for IrDA OBject EXchange protocol. Here is an implementation of OBEX client/server, pure tclish. It needs my infrared extension to work with IrDA stack, and, in case of tcl 8.4, forward-compatible dict.

There may be a lot of bad things to say about this package. It doesn't include any manual, it uses its own ad-hoc OOP (SNIT would be more reasonable here). I don't have any spare time to polish it now. This extension may be useful for someone as a starting point for a nice OBEX package.


    package require base64
    namespace eval OBEX {
        namespace eval _utils {
            variable DEBUG 1
            if {$DEBUG} {
                proc DEBUG {str} {
                    puts [uplevel 1 [list subst $str]]
                }
            } else {proc DEBUG {str} {}}

            interp alias {} [namespace current]::lpfn {} namespace which
            proc to_short {var} {
                upvar 1 $var v
                set v [expr {$v & 0xFFFF}]
                return $v
            }
            proc to_byte {var} {
                upvar 1 $var v
                set v [expr {$v & 0xFF}]
                return $v
            }
            proc to_hex {var} {
                upvar 1 $var v
                set v [format 0x%02X [expr {$v & 0xFF}] ]
                return $v
            }

            variable HDRS [dict create \
                    count 0xC0 \
                    name 0x01 \
                    type 0x42 \
                    length 0xC3 \
                    timestamp 0x44 \
                    timestamp-4 0xC4 \
                    description 0x05 \
                    target 0x46 \
                    http 0x47 \
                    body 0x48 \
                    eob 0x49 \
                    who 0x4A \
                    connection-id 0xCB \
                    parameters 0x4C \
                    auth-challenge 0x4D \
                    auth-response 0x4E \
                    creator-id 0xCF \
                    wan-uuid 0x50 \
                    object-class 0x51 \
                    session-parameters 0x52 \
                    session-sequence-number 0x93 ]
            dict for {k v} $HDRS {dict set rvHDRS [expr {$v}] $k}
            variable OPCODES [dict create \
                    connect 0x80 \
                    disconnect 0x81 \
                    put 0x02 \
                    get 0x03 \
                    put-final 0x82 \
                    get-final 0x83 \
                    chdir 0x85 ]

            dict for {k v} $OPCODES {dict set rvOPCODES [expr {$v}] $k}

            proc gethid {hdr} {
                variable HDRS
                if {![string is integer $hdr]} {
                   return [dict get $HDRS $hdr]
                }
                return $hdr
            }
            proc getopname {id} {
                variable rvOPCODES
                to_byte id
                if {[dict exists $rvOPCODES $id]} {
                    return [dict get $rvOPCODES $id]
                }
                return $id
            }

            proc gethname {id} {
                variable rvHDRS
                to_byte id
                if {[dict exists $rvHDRS $id ]} {
                    return [dict get $rvHDRS $id]
                }
                return $id
            }

            if {[string equal $::tcl_platform(byteOrder) littleEndian]} {
                proc brev {str} {
                    set r {}
                    foreach {b1 b2} [split $str {}] {
                        append r $b2 $b1
                    }
                    return $r
                }
            } else {
                proc brev {str} {set str}
            }

            proc ctounicode {str} {
                return [brev [encoding convertto unicode $str] ]
            }
            proc cfromunicode {str} {
                return [encoding convertfrom unicode [brev $str] ]
            }

            # Formatting header for transmission
            proc fh {hdr hdata} {
                DEBUG {Fh: $hdr , $hdata}
                set hval [gethid $hdr]
                set htype [expr {($hval & 0xC0)>>6}]
                set r [binary format c $hval]
                switch $htype {
                    0 {
                        set cbin [ctounicode $hdata]
                        append cbin [binary format x2]
                        append r [binary format S [
                            expr {[string length $cbin]+3}]] $cbin
                    }
                    1 {
                        append r [binary format S [
                            expr {[string length $hdata]+3}]] $hdata
                    }
                    2 {
                        append r [binary format c $hdata]
                    }
                    3 {
                        append r [binary format I $hdata]
                    }
                }
                return $r
            }

            proc fhs {args} {
                flattenargs
                set data {}
                foreach {h v} $args { append data [fh $h $v] }
                return $data
            }
            # convert headers from binary form to keyed-list
            proc parse_headers {data} {
                set r [list]
                while {[string length $data]} {
                    binary scan $data c hval
                    switch [expr {($hval & 0xC0)>>6}] {
                        0 {
                            binary scan $data cS byte length
                            to_short length
                            set utext [string range $data 3 [expr {$length - 3}] ]
                            set v [cfromunicode $utext]
                            set drop $length
                        }
                        1 {
                            binary scan $data cS byte length
                            to_short length
                            set v [string range $data 3 [expr {$length - 1}]]
                            set drop $length
                        }
                        2 {
                            binary scan $data cc byte quantity
                            set v [to_byte quantity]
                            set drop 2
                        }
                        3 {
                            binary scan $data cI byte quantity
                            set v $quantity
                            set drop 5
                        }
                    }
                    dict set  r [gethname [to_byte hval]] $v
                    set data [string range $data $drop end]
                }
                return $r
            }

            # format an operation
            proc fop {opcode data} {
                set r [binary format c $opcode]
                append r [binary format S [expr {[string length $data]+3}]]
                append r $data
                return $r
            }

            proc flattenargs {} {
                upvar 1 args _args
                set limit 200
                while {[llength $_args]%2} {
                    if {![incr limit -1]} {
                        return -code error "Too many levels"
                    }
                    set _args [concat [lindex $_args 0] [lreplace $_args 0 0] ]
                }
                return
            }
            proc f_connect {args} {
                # OBEX 1.0, Flags=0, MRU=8k 
                flattenargs
                return \
                    [fop 0x80 [binary format ccSa* 0x10 0x00 0x4000 [fhs $args] ] ]
            }

            proc fr_connect {args} {
                flattenargs
                return \
                    [fop 0xA0 [ binary format ccSa* 0x10 0x00 0x4000 [fhs $args] ] ]
            }

            proc f_generic {opc args} {
                variable OPCODES
                flattenargs
                DEBUG {Fg: $opc,$args}
                if {[dict exists $OPCODES $opc ]} {
                    set opc [dict get $OPCODES $opc]
                }
                return [fop $opc [fhs $args]]
            }

            proc fr_generic {opc args} {
                flattenargs
                return [fop $opc [fhs $args]]
            }

            proc f_setpath {flags args} {
                flattenargs
                dict set flagbits up 1 nocreate 2
                set fb 0
                foreach flag $flags {
                    incr fb [dict get $flabgits $flag]
                }
                return [fop 0x85 [binary format cca* $fb 0 [fhs $args]]]
            }

            variable buffer [dict create]
            proc packet_splitter {fh handler} {
                variable buffer
                if {[catch {read $fh} data]||[eof $fh]} {
                    fileevent $fh readable {}
                    DEBUG {Unwinding...}
                    # close $fh
                    dict unset buffer $fh
                    after idle $handler [list {}]
                    return
                }
                dict append buffer $fh $data
                set input [dict get $buffer $fh]
                if {[binary scan $input cS opcode length]!=2} {
                    DEBUG {Not even 3 bytes...}
                    return
                }
                to_short length
                DEBUG {Length $length...}
                if {[string length $input]>=$length} {
                    DEBUG {Yeah! data is here...}
                    after idle $handler \
                        [list [ string range $input 0 [expr {$length-1}]] ]
                    dict set buffer $fh [string range $input $length end]
                } 
            }

            proc packet_parse_request {str} {
                binary scan $str cS resp length
                to_byte resp
                to_short length
                if {$resp==0x80} {
                    binary scan $str cSccS _ _ version flags mtu
                    set data [parse_headers [string range $str 7 end]]
                    lappend data MTU $mtu
                } elseif {$resp==0x85} {
                    binary scan $str cScc _ _ flags _
                    set data [parse_headers [string range $str 5 end]]
                    lappend data UP [expr {$flags&1}] NOCREATE [expr {$flags&2!=0}]
                } else {
                    set data [parse_headers [string range $str 3 end]]
                }
                return [list [getopname $resp] $data]
            }
            proc put {fh data} {
                puts -nonewline $fh $data
                flush $fh
            }
            namespace export *
        }

        namespace eval server {
            # OBEX::server uses the ad-hoc oop 
            # OBEX::server::Class MyServer {Push}
            namespace import [namespace parent]::_utils::*
            namespace export \[A-Z\]*
            # Accept --
            # Pass the socket to the OBEX::server
            proc Accept {fh {class Default} args} {
                variable state
                dict set state $fh [dict create]
                dict set state $fh mtu   255
                dict set state $fh class $class
                set incoming [lpfn incoming]
                fconfigure $fh -translation binary -blocking no
                fileevent $fh readable \
                    [ list [lpfn packet_splitter] $fh [list $incoming $fh] ]
                callback $class Init $fh $args
            }
            proc callback {class method instance args} {
                variable hooks
                variable state

                dict set state  thisclass $class
                dict set state  this $instance

                if {[dict exists $hooks $class $method]} {
                    set r [uplevel #0 [dict get $hooks $class $method] $args]
                    return $r
                }
                return 
            }
            proc _method {{ivar {}}} {
                variable state
                upvar 1 args args this this thisclass thisclass
                set this [dict get $state this]
                set thisclass [dict get $state thisclass]
                flattenargs
                uplevel 1 [list upvar #0 ::OBEX::server::IV:$this $ivar]
            }

            proc Method {name body} {
                variable hooks
                set upns [uplevel 1 {namespace current}]
                DEBUG {method $upns $name}
                if {[string match ::OBEX::server::cls* $upns]} {
                    DEBUG {$upns $name is inline}
                    dict set hooks [namespace tail $upns] $name  ${upns}::$name
                }
                uplevel 1 [
                    list proc $name args "[lpfn _method];$body"
                    ]
            }
            proc Call {method args} {
                variable state
                upvar 1 this this thisclass thisclass
                callback $thisclass $method $this $args
            }

            proc Class {name inhlist map} {
                variable hooks
                dict set hooks $name [dict create]
                foreach super $inhlist {
                    dict for {k v} [dict get $hooks $super] {
                        dict set hooks $name $k $v
                    }
                }
                if {[llength $map]==1} {
                    # Auto-binding commands
                    if {![string equal [namespace current] \
                            [uplevel 1 {namespace current}]]} {
                        uplevel 1 [
                            list namespace import \
                                [namespace current]::arg: \
                                [namespace current]::\[A-Z\]*
                            ]
                    }
                    set map [uplevel 1 {namespace current}]::$map
                    set len [string length $map]
                    DEBUG {Using $map for defining $name}
                    DEBUG {having [info commands ${map}*]}
                    foreach command [info commands ${map}*] {
                        dict set hooks $name [string range $command $len end]  \
                            $command
                    }
                } else {
                    namespace eval cls::$name \
                        [ list namespace import \
                            [namespace current]::arg: \
                            [namespace current]::\[A-Z\]* 
                        ]
                    namespace eval cls::$name $map
                }
            }

            proc arg: {key {dv {}}} {
                upvar 1 args args
                if {[dict exists $args $key]} {
                    return [dict get $args $key]
                } else {
                    return $dv
                }
            }
            namespace export arg:

            proc incoming {fh packet} {
                variable state
                set class [dict get $state $fh class]
                DEBUG {Got packet of length [string length $packet]}
                if {![string length $packet]} {
                    DEBUG {Zero-length packet!}
                    callback $class Destroy $fh
                    catch {close $fh}
                    dict unset state $fh
                    return
                }
                set data {}
                set resp {}
                foreach {resp data} [packet_parse_request $packet] {break}
                set hs {}
                set rc 0xD0
                foreach {rc hs} [callback $class OBEX.$resp $fh $data] {
                    break
                }
                DEBUG {About to respond with $rc $hs}
                if {[string equal $resp connect]} {
                    put $fh [fr_connect $hs]
                } else {
                    put $fh [fr_generic $rc $hs]
                }
            }
            # Now let's specify default server...
            Method Default.Result.Ok {
                return [list 0xA0 ""]
            }
            Method Default.Result.Error {
                return [list 0xD0 ""]
            }
            Method Default.Result.Continue {
                return [list 0x90 ""]
            }
            Method Default.Result.NotFound {
                return [list 0xC4 ""]
            }
            Method Default.OBEX.connect  {
                Call SendSuccess
            }
            Method Default.OBEX.put-final  {
                foreach {h v} $args {dict set (properties) $h $v}
                set (properties) [dict remove $(properties) body eob]
                append (body) [arg: body] [arg: eob]
                set err [catch {Call Received $(properties) body $(body)} msg]
                unset (body)
                if {!$err} {
                    return [Call Result.Ok]
                } else {
                    if {![string length $msg ]} {
                        return [Call Result.Error]
                    } else {
                        return [lindex $msg 0]
                    }
                }
            }
            Method Default.OBEX.chdir  {
                Call Result.Continue
            }
            Method Default.OBEX.get-final  {
                Call Result.NotFound
            }
            Method Default.OBEX.get  {
                Call Result.Continue
            }
            Method Default.OBEX.put  {
                foreach {h v} $args {dict set (properties) $h $v}
                Call Result.Continue
            }
            Method Default.OBEX.disconnect  {
                Call Result.Ok
            }
            Method Default.Received {
                DEBUG {-----------------Received file:}
                DEBUG {[arg: body]}
                DEBUG {Properties: $(properties)}
            }
            Class Default {} Default.

            Class Push {Default} {
                Method Init {
                    set (options) $args
                }
                Method Received {
                    uplevel #0 [list [
                        dict get $(options) -reader] [arg: name] [arg: body]]
                }
            }
        }

        namespace eval client {
            namespace import [namespace parent]::_utils::*
            # 1. the initial state of socket is idle
            # 2. when the client sends async request,
            #    then it's appended to the socket's queue
            # 3. if the queue was empty then queuerunner 
            #    is scheduled [after idle].
            # -------------
            # The queue element is a list of:

            variable state [dict create]

            proc Acquire {fh} {
                variable state
                dict set state $fh [dict create]
                dict set state $fh mtu 255
                dict set state $fh queue [list] 
                dict set state $fh qtail 0
                dict set state $fh qhead 0
                fconfigure $fh -blocking no -translation binary
                set incoming [lpfn incoming]
                fileevent $fh readable \
                    [list [lpfn packet_splitter] $fh [list $incoming $fh]]
            }
            proc qpop {fh} {
                variable state
                set qhead [dict get $state $fh qhead]
                incr qhead
                dict set state $fh qhead $qhead
                dict set state $fh queue \
                    [lreplace [dict get $state $fh queue] 0 0] 
            }
            proc qhead {fh args} {
                variable state
                if {[llength $args]} {
                    dict set state $fh queue \
                        [lreplace [dict get $state $fh queue] \
                        0 0 [lindex $args 0]] 
                } else {
                    return [lindex [dict get $state $fh queue] 0]
                }
            }
            proc qptr {ptr fh} {
                variable state
                switch -exact $ptr {
                    head {return [dict get $state $fh qhead]}
                    tail {return [dict get $state $fh qtail]}
                }
            }

            proc qpush {fh data} {
                variable state
                set qtail [dict get $state $fh qtail]
                set quid $qtail
                incr qtail
                dict set state $fh qtail $qtail
                set q [dict get $state $fh queue]
                lappend q $data
                dict set state $fh queue $q
                return $quid
            }

            proc qlength {fh} {
                variable state
                return [ llength [ dict get $state $fh queue ] ]
            }

            proc incoming {fh packet} {
                variable state
                variable callbacks
                variable results
                if {![string length $packet ]} {
                    dict unset state $fh
                    array unset results $fh,* 
                    array unset callbacks $fh,* 
                    close $fh
                    return
                }
                foreach thisop [dict get $state $fh queue] {break}
                if {![info exists thisop]} {return}
                binary scan $packet cSa* resp _ data 
                to_byte resp
                DEBUG {$resp packet}
                set result [eval $thisop [list $fh $resp $data]]
                set qh [qptr head $fh]
                if {$resp!=0x90} {
                    DEBUG {Non-intermediate packet $resp}
                    if {[info exists callbacks($fh,$qh)]} {
                        DEBUG {Calling back}
                        after idle $callbacks($fh,$qh) $result
                        unset callbacks($fh,$qh)
                    } else {
                        DEBUG {Setting results $fh,$qh}
                        set results($fh,$qh) $result
                    }
                    qpop $fh
                    after idle [list [lpfn qrunner] $fh]
                } else {
                    qhead $fh $result
                }
            }
            proc qrunner {fh} {
                variable state
                if {![qlength $fh]} { return }
                qhead $fh [eval [qhead $fh] [list $fh 000 ""]]
            }
            proc runq {fh} {
                if {![qlength $fh]} { 
                    after idle [list [lpfn qrunner] $fh]
                }
            }
            proc schedule {fh script cb} {
                variable results
                variable callbacks
                set id [qpush $fh $script]
                if {![string length $cb]} {
                    DEBUG {Will wait $fh,$id}
                    set  results($fh,$id) {}
                    vwait OBEX::client::results($fh,$id)
                    set r $results($fh,$id)
                    unset results($fh,$id)
                } else {
                    DEBUG {Will not wait: $cb}
                    set callbacks($fh,$id) $cb
                    set r $id
                }
                return $r
            }

            proc Connect {fh headers {cb {}}} {
                variable state 
                runq $fh
                schedule $fh [list do_connect $headers] $cb
            }

            proc do_connect {headers fh code data} {
                variable state
                DEBUG {do_connect $headers}
                put $fh [f_connect $headers]
                return do_connect_confirm
            }
            proc do_connect_confirm {fh code data} {
                variable state
                binary scan data ccSa* ver flags mtu rest
                to_short mtu
                dict set state $fh mtu $mtu
                return [parse_headers $rest]
            }

            proc GetFile {fh headers {cb {}}} {
                runq $fh
                schedule $fh [list do_get $headers] $cb
            }
            proc do_get {headers fh code data} {
                variable state
                variable bodies
                DEBUG {do_get $headers $code}
                if {$code} {
                    array set gh [parse_headers $data]
                    if {[info exists gh(body)]} {
                        dict append bodies $fh $gh(body) 
                    }
                    if {[info exists gh(eob)]} {
                        dict append bodies $fh $gh(eob) 
                    }
                    if {$code != 0x90} {
                        set r {}
                        if {[dict exists $bodies $fh]} {
                            set r [dict get $bodies $fh]
                            dict unset bodies $fh
                        }
                        return $r
                    }
                }
                set limit [dict get $state $fh mtu ]
                incr limit -3
                set chunk {}
                set op get-final
                foreach {h v} $headers {
                    set piece [fh $h $v]
                    incr limit -[string length $piece]
                    if {$limit<0} { set op get; break }
                    lappend chunk $h $v
                    set headers [lreplace $headers 0 1]
                }
                put $fh [f_generic $op $chunk]
                return [list do_get $headers]
            }
            proc PutFile {fh headers body {cb {}}} {
                runq $fh
                schedule $fh [list do_put $headers $body] $cb
            }
            proc do_put {headers body fh code data} {
                variable state
                DEBUG {do_put $headers $code}
                if {($code) && ($code!=0x90) } {
                    return [parse_headers $data]
                }
                set limit [dict get $state $fh mtu ]
                incr limit -3
                set chunk {}
                set op put
                set bh body
                foreach {h v} $headers {
                    set piece [fh $h $v]
                    incr limit -[string length $piece]
                    if {$limit<0} { break }
                    lappend chunk $h $v
                    set headers [lreplace $headers 0 1]
                }
                incr limit -3
                if {$limit>0} {
                    set bchunk [string range $body 0 [expr {$limit-1}]] 
                    set body [string range $body $limit end]
                    if {![string length $body]} {
                        set op put-final
                        set bh eob
                    }
                }
                put $fh [f_generic $op $chunk]
                return [list do_put $headers $body]
            }

            proc Disconnect {fh headers {cb {}}} {
                runq $fh
                schedule $fh [list do_disconnect $headers] $cb
            }
            proc do_disconnect {headers fh code data} {
                if {$code} { return }
                put $fh [f_generic disconnect $headers]
                return {do_disconnect {}}
            }
        }
    }

And a couple of examples.

OBEX client

    # OBEX client
    # given a mobile phone with IrMC sync support, retrieves the phonebook.
    package require irdasock
    proc OBEXtest {fh} {
        OBEX::client::Acquire $fh
        set r [OBEX::client::Connect $fh {target IRMC-SYNC} ]
        puts "Connected(hdrs=$r)."
        set pb [OBEX::client::GetFile $fh {name telecom/pb.vcf target IRMC-SYNC} ]
        puts "Got File "
        puts $pb
        set fd [open  card.vcf w]
        puts -nonewline $fd $pb
        close $fd
        OBEX::client::Disconnect $fh {target IRMC-SYNC}
    }

    set dev {}
    puts "Waiting for some device to be plugged..."
    while {$dev eq ""} {
        catch {
            set dev [lindex [set devs [irda::discover]] 0 0]
        }
        after 1000
    }

    foreach {id name hints} [lindex $devs 0] {break}
    puts "Device $name ([format 0x%08x $id]): $hints"


    set sock [irda::connect $dev IrDA:OBEX]
    fconfigure $sock -translation binary
    ODBCtest $sock

OBEX server

    package require irdasock
    irda::server IrDA:OBEX ConnectMe
    proc ConnectMe {sock id} {
        puts "Passing socket to server..."
        OBEX::server::Accept $sock Push -reader RecvFile
        return
        fconfigure $sock -translation binary
        foreach dev [irda::discover] {
            foreach {did name hints} $dev {
                if {$did==$id} {
                    puts "OBEX connection from $name"
                    break
                }
            }
        }
        foreach {ch cr} [pget $sock] {break}
        puts "First operation is $ch"
        puts [OBEX::parse_headers [string range $cr 4 end]]
    }

    proc RecvFile {name body} {
        puts "Received $name, body:\n$body\n"
    }
    vwait forever

See OBEXTool.