HAL and scanners

abel deuring adeuring at gmx.net
Sat Dec 30 19:07:23 PST 2006


Joe Shaw wrote:
> Hi,
> 
> Étienne Bersac wrote:
>> So the problem is how the app can give to SANE an udi ?
> 
> Well, that depends on the design.  If SANE is HAL-aware, then yeah, just
> passing a UDI would be fine.  If not, then your API could be the wrapper
> that provides SANE with the details that it got from HAL.  (This is how
> libgphoto works, I believe.)
> 
>> The GLib-ish API is not that simple :). It provide GObject API similar
>> to GtkPrint (i intend to make it more consistent to GtkPrint API). All i
>> want is to drop the sane_get_devices call and replace it with a hal
>> call. But, hal must provide me at least all information provided in
>> Sane_Device_Descriptor struct (see
>> http://sane.alioth.debian.org/html/doc011.html#s4.2.8 or better:
>> http://sane.alioth.debian.org/sane2/0.08/doc011.html#s4.2.8 ). Or i wont
>> be able to use that device with hal.
> 
> I see.  It sounds like a good idea to generate the FDI files from the
> SANE information, as was suggested by Abel earlier in the thread.  That
> should give you the info you need, right?

I hope that this will work ;)

Attached is the current version of my fdi generator script, and an
"extended" sample Sane desc file that contains the information
required for SCSI scanners.

This gives at least the vendor and model fields from struct
SANE_Device as described in the Sane API (URLs above).
SANE_Device.type is at present restricted to "scanner".
SANE_Device.name (the "Sane-URL") will need a bit of work:

For a SCSI device, look up the SG device file name in the HAL data,
and use "backendname:/dev/sgX" as SANE_Device.name.

For USB scanners, look up usb_device.bus_number and
usb_device.linux.device_number, and build the Sane device name as
"backendname:libusb:bus_number:device_number". (disclaimer: I am not
sure, if all Sane backends for USB scanners use libusb. Whoever
dares to complain about possible inconsistencies: patches for libusb
support in a backend will be welcome ;)

Next steps I intend to make:

- "sell" the idea of the fdi file (and the associated work on the
*desc files: addition of SCSI IDs etc) to the Sane developers ;)
- optional integration of HAL UDIs and HAL device detection at least
in the Linux parts of sanei_scsi.c . (I don't have any development
experience with non-SCSI scanners, so I must leave the work for USB,
parallel port etc to other people...)

Sane's SCSI device detection for Linux can definitely profit from
HAL. At present it is basically a lot of guesswork based on the
content of /proc/scsi/scsi , and having a clear and (as far as I
understand HAL currently) straightforward way to detect a SCSI
scanner is an obvious advantage. So, be warned that I will probably
come back with a lot of stupid questions about the "fine print" of
HAL's C library ;)

Abel
-------------- next part --------------
#!/usr/bin/env python

"""
Copyright (c) Abel Deuring 2007 <adeuring at gmx.net>

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 2 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, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.

"""

# generates an .fdi file for Freedesktop's HAL project from Sane *desc
# files

import os, os.path, sys, re, getopt

whspace_re = re.compile(r'\s+')
string_re  = re.compile(r'\s*"(.*?)(?<!\\)"(.*)')

known_interfaces = (re.compile('(SCSI)'), 
                    re.compile('(USB)'), 
                    re.compile(r'(Parport\s?(\(.*?\))?)'), 
                    re.compile('(Serial port)'), 
                    re.compile('(IEEE-1394)'), 
                    re.compile('(JetDirect)'), 
                    re.compile('(Ethernet)'), 
                    re.compile('(Proprietary)')
                   )

class DeviceInfo:
    def __init__(self, backend, backend_version, device_type, vendor,
                 model, manpage, 
                 from_file, from_line):
        self.backend = backend
        self.backend_version = backend_version
        self.device_type = device_type
        self.vendor = vendor
        self.model = model
        self.manpage = manpage
        self.from_file = from_file
        self.from_line = from_line
        self.interfaces = []
    
    def warn(self, msg):
        sys.stderr.write(('warning: %s for backend %s,\n' +\
                         '  device %s %s, file %s line %i\n') %
                         (msg, self.backend, self.vendor, self.model, 
                          self.from_file, self.from_line))
    
    def write_descr_tag(self, f, tag, key, typ, val, indent):
        f.write('%s<%s key="%s" type="%s">%s</%s>\n'
                % (' '*indent, tag, key, typ, val, tag))
    
    def _fdi_devinfo(self, f):
        indent = 8
        self.write_descr_tag(f, 'append', 'info.capabilities', 'strlist',
                             self.device_type, indent)
        self.write_descr_tag(f, 'append', 'scanner.vendor', 'strlist',
                             self.vendor, indent)
        self.write_descr_tag(f, 'append', 'scanner.model', 'strlist',
                             self.model, indent)
        # FIXME: Should we set set scanner.is_supported or
        # scanner.sane.is_supported ?
        if not self.is_supported():
            self.write_descr_tag(f, 'merge', 'scanner.is_supported', 'bool',
                                 'false', indent)
        else:
            # FIXME: inconsistency, if backend A declares a device as
            # supported, and backend B declares is unsupported
            # The latter should be removed.
            self.write_descr_tag(f, 'merge', 'scanner.is_supported', 'bool',
                                 'true', indent)
            self.write_descr_tag(f, 'append', 'scanner.api', 'strlist',
                                 'sane', indent)
            self.write_descr_tag(f, 'append', 'scanner.sane.backend', 'strlist',
                                 self.backend, indent)
            self.write_descr_tag(f, 'append', 'scanner.sane.supportstatus', 'strlist',
                                 self.status, indent)
            
    
    def write_fdi(self, f, verbose):
        # check, if we have enough information; issue a warning
        # if not
        if not self.interfaces:
            if verbose:
                self.warn('no interface definitions')
            return
        supportstatus = getattr(self, 'status', None)
        if supportstatus == None:
            if verbose:
                self.warn('supportstatus not defined: no data written')
            return
        can_write = False
        usb_id = scsi_id = None
        for iface in self.interfaces:
            if iface == 'USB':
                usb_id = getattr(self, 'usb_id', None)
                can_write |= (usb_id != None)
                if usb_id == None and verbose:
                    self.warn('USB ID missing')
            elif iface == 'SCSI':
                scsi_id = getattr(self, 'scsi_id', None)
                can_write |= (scsi_id != None)
                if scsi_id == None and verbose:
                    self.warn('SCSI vendor/model ID missing')
            else:
                if verbose:
                    self.warn('output for interface %s not yet possible' % iface)
        if can_write:
            f.write('  <device>\n')
            if usb_id:
                f.write('    <match key="usb.vendor_id" int="%s">\n'
                        '      <match key="usb.product_id" int="%s">\n'
                        % usb_id)
                self._fdi_devinfo(f)
                f.write('      </match>\n'
                        '    </match>\n')
            if scsi_id:
                f.write('    <match key="scsi.vendor" string="%s">\n'
                        '      <match key="scsi.mmodel" string="%s">\n'
                        % scsi_id)
                self._fdi_devinfo(f)
                f.write('      </match>\n'
                        '    </match>\n')
            f.write('  </device>\n')

    def is_supported(self):
        return self.backend != 'unsupported' \
               and getattr(self, 'supportstatus', None) != 'unsupported'

class DescParserError(Exception):
    def __init__(self, value):
        self.value = value
    
    def __str__(self):
        return self.value

class DescParser:
    def __init__(self, filename, output, verbose):
        self.filename = filename
        inp = open(filename)
        line = inp.readline()
        self.line_no = 1
        backend = None
        backend_info = None
        backend_name = None
        backend_version = None
        device_type = None
        vendor = None
        manpage = None
        self.reset_model_info()
        while line:
            line = line.strip()
            # a line is empty, or starts with a ':keyword' or is a ';comment'
            if line and line[0] == ':':
                keyword, line = whspace_re.split(line, 1)
                if keyword == ':backend':
                    backend_name, line = self.get_string(line)
                elif keyword == ':version':
                    backend_version, line = self.get_string(line)
                elif keyword == ':devicetype':
                    device_type, line = self.get_kw(line)
                    if not device_type in (':scanner', ':stillcam', 
                                           ':vidcam', ':meta', ':api'):
                        raise DescParserError('file %s: Unexpected value for :devicetype in line %i: %s'
                                              % (self.filename, self.line_no, self.device_type))
                    device_type = device_type[1:]
                elif keyword == ':mfg':
                    vendor, line = self.get_string(line)
                elif keyword == ':model':
                    self.reset_model_info()
                    model, line = self.get_string(line)
                    backend = DeviceInfo(backend_name, backend_version,
                                         device_type, vendor, model, manpage,
                                         filename,
                                         self.line_no)
                    output.append(backend)
                elif keyword == ':manpage':
                    manpage, line = self.get_string(line)
                
                elif keyword in (':url', ':comment', ':desc', ':new'):
                    # FIXME: the url "meaning" is context specific: 
                    # can mean the backend's home page, a vendor website etc
                    # FIXME: comments might be somewhere useful
                    # :desc is only used for non-hardware devices,
                    # so it's safe to ignore
                    # 
                    pass
                
                elif backend == None:
                    raise DescParserError('file %s: unknown or backend specific keyword outside a backend definition in line %i: %s'
                                          % (self.filename, self.line_no, keyword))

                elif keyword == ':status':
                    status, line = self.get_kw(line)
                    if not status in (':untested', ':minimal',
                                      ':basic', ':good', ':complete',
                                      ':unsupported'):
                        raise DescParserError('file %s: Unexpected value for :status in line %i: %s'
                                              % (self.filename, self.line_no, self.status))
                    backend.status = status[1:]

                elif keyword == ':interface':
                    interfaces, line = self.get_string(line)
                    for test in known_interfaces:
                        mo = test.search(interfaces)
                        if mo:
                            backend.interfaces.append(mo.groups()[0])
                elif keyword == ':usbid':
                    usb_vendor, line = self.get_string(line)
                    if usb_vendor == 'ignore':
                        # FIXME: How should this be handled?
                        pass
                    else:
                        usb_product, line = self.get_string(line)
                        try:
                            int(usb_vendor, 16)
                            int(usb_product, 16)
                            backend.usb_id = (usb_vendor, usb_product)
                        except ValueError:
                            raise DescParserError('file %s: Invalid USB ID in line %i'
                                                  % (self.filename, self.line_no))
                elif keyword == ':scsiid':
                    scsi_vendor, line = self.get_string(line)
                    scsi_product, line = self.get_string(line)
                    backend.scsi_id = (scsi_vendor, scsi_product)
                else:
                     raise DescParserError('file %s: unexpected keyword %s in line %i'
                                           % (self.filename, keyword, self.line_no))
            elif line and line[0] != ';':
                raise DescParserError("file %s: Syntax error in line %i" % 
                                      (self.filename, self.line_no))
            
            line = inp.readline()
            self.line_no += 1
        inp.close()
        
    def reset_model_info(self):
        self.model = None
        self.status = None
        self.interfaces = []
        self.usb_id = None
        
    def get_string(self, line):
        mo = string_re.match(line)
        if mo == None:
            raise DescParserError("file %s: Syntax error in line %i, string expected" % 
                                  (self.filename, self.line_no))
        return mo.groups()
    
    def get_kw(self, line):
        line = line.strip()
        if line[0] != ':':
            raise DescParserError("file %s: Syntax error in line %i, keyword expected" 
                                  % (self.filename, self.line_no))
        res = whspace_re.split(line, 1)
        if len(res) < 2:
            return res[0], ''
        return res
    

def usage():
    print """\
Generate a HAL fdi file from Sane's *.desc files. Works at present only
for USB scanners and for "extended" desc files of SCSI scanners.

usage:

sanedesc2fdi.py [--OPTION]... [DESCDIR]

where OPTION can be

  verbose		prints lots of warnings and other info
  output=<filename>	write output to <file>. default: stdout
  with-unsupported	include information about scanners that are not
                        supported by Sane
  DESCDIR		directory with *.desc files. default: pwd
"""

try:
    opts, args = getopt.getopt(sys.argv[1:], '', ('verbose', 
                                                  'output=',
                                                  'with-unsupported',
                                                  'help'))
except getopt.GetoptError, val:
    print str(val)
    usage()
    sys.exit(1)

verbose = False
with_unsupported = False
out = sys.stdout
for opt, val in opts:
    if opt == '--output':
        out = open(val, 'w')
    elif opt == '--verbose':
        verbose = True
    elif opt == '--with-unsupported':
        with_unsupported = True
    elif opt == '--help':
        usage()
        sys.exit(0)

if len(args):
    dir = args[0]
else:
    dir = '.'
devlist = []
for filename in os.listdir(dir):
    if filename.endswith('.desc'):
        DescParser(os.path.join(dir, filename), devlist, verbose)



out.write("""<?xml version="1.0" encoding="utf-8"?>
<!-- fdi file for SANE supported scanners -->
<deviceinfo version="0.2">
""")

for dev in devlist:
    # FIXME: should be possible to optionally write data
    # for webcam backends etc
    print dev.device_type
    if dev.device_type == 'scanner' and (with_unsupported or dev.is_supported()):
        dev.write_fdi(out, verbose)

out.write("</deviceinfo>\n")
out.close()
-------------- next part --------------
:backend "sharp"                ; name of backend
:version "0.32"                 ; version of backend
;:status :beta                   ; :alpha, :beta, :stable, :new
:url "http://www.satzbau-gmbh.de/staff/abel/sane-sharp.html" 
				; backend's homepage
:manpage "sane-sharp"
:devicetype :scanner

:mfg "Sharp"                    ; name a manufacturer
:url "http://sharp-world.com/"

:model "JX-610"                 ; name models for above-specified mfg.
:interface "SCSI"
:status :good
:scsiid "SHARP" "JX610 SCSI"
:comment ""

:model "JX-250"                 ; name models for above-specified mfg.
:interface "SCSI"
:status :good
:scsiid "SHARP" "JX250 SCSI"
:comment ""

:model "JX-320"                 ; name models for above-specified mfg.
:interface "SCSI"
:status :good
:scsiid "SHARP" "JX320 SCSI"
:comment ""

:model "JX-325"                 ; name models for above-specified mfg.
:interface "SCSI"
:status :untested
:scsiid "SHARP" "JX325 SCSI"
:comment ""

:model "JX-330"                 ; name models for above-specified mfg.
:interface "SCSI"
:status :good
:scsiid "SHARP" "JX330 SCSI"
:comment ""

:model "JX-350"                 ; name models for above-specified mfg.
:interface "SCSI"
:status :good
:scsiid "SHARP" "JX350 SCSI"
:comment ""


More information about the hal mailing list