Redhatter (VK4MSL)

OpenERP: Mail gateway processing

I’ll be posting up a series of notes on various aspects of OpenERP. This is largely as a brain-dump for my own reference, but might prove interesting for others who may be facing the same problems I have faced in my maintenance of my work’s OpenERP instance.

OpenERP’s mail gateway system is a generic interface for manipulating objects according to the reception of incoming emails. The system can either receive emails via a script ran by the mail server, or by periodic collection of a POP3/IMAP mail account. Each dedicated account is allocated for processing of one kind of object. You can also configure outbound email SMTP servers for sending email traffic — these can be accessed and utilised by any object.

Sending emails from OpenERP objects

Configuring outbound email

Configuring outbound email is a fairly straightforward affair. After installing the ‘mail’ module (or one of its dependents) you should be able to see “Outgoing Mail Servers” under Settings/Configuration/Email.

The configuration here is not much different to any email client you might otherwise configure; give the host name of the SMTP server, port and connection security options, click save and you’re done. Unless you’ve got a fairly specialised set-up, you should only need to configure the one mail server.

Sending arbitrary messages: mail.message

As the saying goes, there is more than one way to skin a cat, and here is no different. There are in fact, two interfaces for sending emails. The first of these is mail.message, which is useful for sending arbitrary messages. It defines a method, schedule_with_attach which, as the name suggests, schedules an email for delivery, optionally with an attachment or two.

def schedule_with_attach(self, cr, uid, email_from, email_to, subject,
        body, model=False, email_cc=None, email_bcc=None,
        reply_to=False, attachments=None, message_id=False,
        references=False, res_id=False, subtype='plain', headers=None,
        mail_server_id=False, auto_delete=False,
        context=None):
    """ Schedule sending a new email message, to be sent the next time the
        mail scheduler runs, or the next time :meth:`process_email_queue` is
        called explicitly.

    :param string email_from: sender email address
    :param list email_to: list of recipient addresses (to be joined with commas)
    :param string subject: email subject (no pre-encoding/quoting necessary)
    :param string body: email body, according to the ``subtype`` (by default, plaintext).
        If html subtype is used, the message will be automatically converted
        to plaintext and wrapped in multipart/alternative.
    :param list email_cc: optional list of string values for CC header (to be joined with commas)
    :param list email_bcc: optional list of string values for BCC header (to be joined with commas)
    :param string model: optional model name of the document this mail is related to (this will also
        be used to generate a tracking id, used to match any response related to the
        same document)
    :param int res_id: optional resource identifier this mail is related to (this will also
        be used to generate a tracking id, used to match any response related to the
        same document)
    :param string reply_to: optional value of Reply-To header
    :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'),
        must match the format of the ``body`` parameter. Default is 'plain',
        making the content part of the mail "text/plain".
    :param dict attachments: map of filename to filecontents, where filecontents is a string
        containing the bytes of the attachment
    :param dict headers: optional map of headers to set on the outgoing mail (may override the
        other headers, including Subject, Reply-To, Message-Id, etc.)
    :param int mail_server_id: optional id of the preferred outgoing mail server for this mail
    :param bool auto_delete: optional flag to turn on auto-deletion of the message after it has been
        successfully sent (default to False)"""

So in your method, you might call it with something like this:

def send_mail(cr, uid, ids, ... context=None):
    if not isinstance(ids, list):
        ids = [ids]

    msg_pool = self.pool.get('mail.message')
    for object in self.browse(cr, uid, ids, context=context):
        # Constructing the body text. This can be done a variety of ways
        body_text = '''An example email body using some fields from the object.
Object name: %s
Object ID: %s
''' % (object.name, object.id)

        msg_pool.schedule_with_attach(cr, uid, email_from='some@email.address',
                email_to=[  'a@list.of',
                            'people@for.the',
                            'to@list' ],
                subject='Your Spam',
                body=body_text, # Constructed above
                email_cc=['same@deal'], # or False for no CCs
                email_bcc=['etc@somewhere'],
                ..., context=context)

If you trigger this method, then look in your outbound message queue (Settings/Email/Messages) you’ll see the message queued for delivery. At some point the scheduler in OpenERP will send this message to the destination address. This is done every few minutes or so.

Sending templated messages: email.template

This sort of hard-coded message is all very well, but they aren’t pretty, and they certainly aren’t user customisable. The project module gets around the user customisability by providing fields for a header and footer with a fixed set of fields, but if you want to do something nicer, you’re out of luck. We had a need to be able to notify people when tasks were assigned to them, or whenever the task was closed. I initially used the above approach, but then investigated the email.template method.

Under “Settings/Configuration/Email/Templates” you’ll see a list of such templates. Essentially you can fill any field from the source object in, at any position. In each field, any text inside ${ } blocks is executed as Python code. There seems to be more options here for formatting, but so far that’s what I definitely know. The object being formatted gets passed in as object, and the context dict is exposed as ctx. You’ve got the ability to customise both HTML and plain-text versions, and to attach a report or existing files if you so choose.

So for my project task notification, I took the following approach. Firstly, to save me a lot of messy code, I augmented the task object with a function field which enumerated all the people that were to be notified. The idea was to enhance the “Warn Manager”/”Warn Customer”, replacing that implementation with this one. So in a custom module, I code up a derived model like so:

class project_task(osv.osv):
    '''
    Project Task notification hooks.  This allows us to send an email
    when a task is created or updated.
    '''
    _name = "project.task"
    _inherit = "project.task"

    def _get_mailing_list(self, cr, uid, ids, field_name, arg, context=None):
        '''Fetch the list of email addresses that will be sent notifications.'''

        ret = {}
        for task in self.browse(cr, uid, ids, context=context):
            mail_list = set()

            if task.user_id and task.user_id.notify_on_task \
                    and task.user_id.user_email:
                mail_list.add(task.user_id.user_email)

            if task.manager_id and (task.manager_id.notify_on_task or      \
                     (task.project_id and task.project_id.warn_manager))  \
                        and task.manager_id.user_email:
                mail_list.add(task.manager_id.user_email)

            if task.project_id and task.project_id.warn_customer:
                for partner in filter(lambda p : p, [task.partner_id,
                        task.project_id.partner_id]):
                    if partner.address and partner.address[0].email:
                        mail_list.add(partner.address[0].email)

            # We have a set of relevant email addresses, convert to a sorted
            # list and put it in the returned output.
            ret[task.id] = u','.join(sorted(mail_list))
        return ret

    _columns = {
        'notification_mailing_list': fields.function(_get_mailing_list, type='text',
                string='Email addresses to notify', store=False),
    }

You’ll notice that here, res.users has an additional field; notify_on_task, this is a per-user flag, configurable in the user preferences that lets users decide whether they’ll personally get nagged or not. I use a set, as we only want to add each address once.

We can now use this field in the template to populate the To: field, rather than a very messy lambda expression to do the same.

Next, we create the template. Now, you can (and I did) create this in the user interface, the following shows how to do it inside the XML files for a module. This was lifted from the edi demo and tweaked.

    <!-- Mail template and workflow bindings are done in a NOUPDATE block
         so users can freely customize/delete them -->
    <data noupdate="1">
        <!--Email template -->
        <record id="email_template_task" model="email.template">
            <field name="name">Automated Task Update Notification Mail</field>
            <field name="email_from">${ctx.get('uid_email') or 'no-body@localhost.localdomain'}</field>
            <field name="subject">Task #${object.id}: ${object.name} [${object.state.title()}]</field>
            <field name="email_to">${object.notification_mailing_list}</field>
            <field name="model_id" ref="project.model_project_task"/>
            <field name="body_html"><![CDATA[
            <p>Hello, The following task has recently been updated:</p>
            <ul>
                <li>${'<i>(Updated)</i> ' if 'name' in ctx.get('changed_fields',[]) else ''}Name: ${object.name} [Task ID #${object.id}]</li>
                <li>${'<i>(Updated)</i> ' if 'state' in ctx.get('changed_fields',[]) else ''}State: ${object.state.title()}</li>
                <li>${'<i>(Updated)</i> ' if 'project_id' in ctx.get('changed_fields',[]) else ''}Project: ${object.project_id.name if object.project_id else 'No project'}</li>
                <li>${'<i>(Updated)</i> ' if 'user_id' in ctx.get('changed_fields',[]) else ''}Assignee: ${object.user_id.name if object.user_id else 'Nobody'}</li>
                <li>Manager: ${object.manager_id.name if object.manager_id else 'Nobody'}</li>
                <li>${'<i>(Updated)</i> ' if 'total_hours' in ctx.get('changed_fields',[]) else ''}Total Hours: ${object.total_hours}.
                        ${'<i>(Updated)</i> ' if 'remaining_hours' in ctx.get('changed_fields',[]) else ''}Remaining Hours: ${object.remaining_hours}
                        Progress: ${object.progress}%</li>
                <li>${'<i>(Updated)</i> ' if 'date_start' in ctx.get('changed_fields',[]) else ''}Start Date: ${object.date_start or 'None'}</li>
                <li>${'<i>(Updated)</i> ' if 'date_end' in ctx.get('changed_fields',[]) else ''}End Date: ${object.date_end or 'None'}</li>
                <li>${'<i>(Updated)</i> ' if 'date_deadline' in ctx.get('changed_fields',[]) else ''}Deadline: ${object.date_deadline or 'None'}</li>
            </ul>
            <h2>${'<i>(Updated)</i> ' if 'description' in ctx.get('changed_fields',[]) else ''}Description:</h2>
            <pre>${object.description or ''}</pre>
            <h2>${'<i>(Updated)</i> ' if 'notes' in ctx.get('changed_fields',[]) else ''}Notes:</h2>
            <pre>${object.notes or ''}</pre>
            ]]></field>
            <field name="body_text"><![CDATA[
Hello, The following task has recently been updated:

 * ${'(Updated) ' if 'name' in ctx.get('changed_fields',[]) else ''}Name: ${object.name} [Task ID #${object.id}]
 * ${'(Updated) ' if 'state' in ctx.get('changed_fields',[]) else ''}State: ${object.state.title()}
 * ${'(Updated) ' if 'project_id' in ctx.get('changed_fields',[]) else ''}Project: ${object.project_id.name if object.project_id else 'No project'}
 * ${'(Updated) ' if 'user_id' in ctx.get('changed_fields',[]) else ''}Assignee: ${object.user_id.name if object.user_id else 'Nobody'}
 * Manager: ${object.manager_id.name if object.manager_id else 'Nobody'}
 * ${'(Updated) ' if 'total_hours' in ctx.get('changed_fields',[]) else ''}Total Hours: ${object.total_hours}  ${'(Updated) ' if 'remaining_hours' in ctx.get('changed_fields',[]) else ''}Remaining Hours: ${object.remaining_hours}  Progress: ${object.progress}%
 * ${'(Updated) ' if 'date_start' in ctx.get('changed_fields',[]) else ''}Start Date: ${object.date_start or ''}
 * ${'(Updated) ' if 'date_end' in ctx.get('changed_fields',[]) else ''}End Date: ${object.date_end or ''}
 * ${'(Updated) ' if 'date_deadline' in ctx.get('changed_fields',[]) else ''}Deadline: ${object.date_deadline or ''}

${'(Updated) ' if 'description' in ctx.get('changed_fields',[]) else ''}Description:
${object.description or ''}

${'(Updated) ' if 'notes' in ctx.get('changed_fields',[]) else ''}Notes:
${object.notes or ''}
            ]]></field>
        </record>

You’ll notice I use ctx.get('uid_email') for the From address. When I call the template up, I can pass in the From address via the context, calling the key uid_email, and it will be filled in. I also have a list of fields that have changed; changed_fields — this is as simple as grabbing vals.keys() from within a write method. To whistle up the template and send the email, I defined a method which I can call.

    def _send_email(self, cr, uid, ids, context=None):
        '''
        Send an email relating to the given task IDs.
        '''
        if not context:
            context = {}

        # Grab the tasks
        tasks = self.browse(cr, uid, ids, context=context)

        # Filter out the tasks for which no emails will be sent.
        tasks = filter(lambda t : \
                (t.user_id and t.user_id.notify_on_task) or \
                (t.manager_id and t.manager_id.notify_on_task) or \
                (t.project_id and \
                    (t.project_id.warn_manager or \
                     t.project_id.warn_customer)), tasks)
        if not tasks:
            # If we've eliminated them all, stop here.
            return None

        # Mail message template handler
        model_pool = self.pool.get('ir.model')
        template_pool = self.pool.get('email.template')
        template_id = template_pool.search(cr, uid, [
            ('model_id','in',model_pool.search(cr, uid,
                                               [('model','=',self._name)],
                                               context=context)),
        ], context=context)
        if template_id:
            # Use the first one
            template_id = template_id.pop(0)

            # Get the current user's email address
            user_pool = self.pool.get('res.users')
            user = user_pool.browse(cr, uid, uid, context=context)
            context['uid_email'] = user.user_email

            # Send the emails for each task
            map(lambda t : template_pool.send_mail(cr, uid, template_id, \
                        t.id, context=context), tasks)

This method performs the following steps:

  1. It filters out the tasks for which no emails will be sent; if no one has elected to receive notifications for a task, then let’s not waste our time.
  2. We then look for the template used to format tasks. To do this, we must first know what ID corresponds to a project.task, so we do a search, then
    use its result in the domain expression to search the templates.
  3. If we find a template ID; there should only be one, so we pick the first of the list. searchalways returns a list.
  4. We look up the current user, and try to find their email address, so that the email will be sent from the person making the changes.
  5. The map function passes each individual task in to the send_mail method.

The send_mail method of email.template has the following structure:

    def send_mail(self, cr, uid, template_id, res_id, force_send=False, context=None):
        """Generates a new mail message for the given template and record,
           and schedules it for delivery through the ``mail`` module's scheduler.

           :param int template_id: id of the template to render
           :param int res_id: id of the record to render the template with
                              (model is taken from the template)
           :param bool force_send: if True, the generated mail.message is
                immediately sent after being created, as if the scheduler
                was executed for this message only.
           :returns: id of the mail.message that was created 
        """

Above, res_id was passed the ID of the task. We also do some magic injecting values into context so they’ll appear in the template. The final step in our mail template integration is to actually make use of the new method. I wanted to send an email when the task was updated, much like Bugzilla’s updates. So I hooked the create and write methods of project.task:

    # Override the action handlers so that we can slip our email out.
    def create(self, cr, uid, vals, context=None):
        id = super(vrt_project_task, self).create(cr, uid, vals, context=context)
        self._send_email(cr, uid, [id], context=context)
        return id

    # What fields do we notify on?  TODO: make this configurable?
    NOTIFY_ON   =   {   'user_id':  lambda val : True,
                        'state':    lambda val : val in ('cancelled','done'),
    }

    def write(self, cr, uid, ids, vals, context=None):
        res = super(vrt_project_task, self).write(cr, uid, ids,
                vals, context=context)
        notify = False
        for field, do_check in self.NOTIFY_ON.iteritems():
            if (field in vals) and do_check(vals[field]):
                # We found a field of interest
                notify = True
                break

        # Send an email describing what changed
        if notify:
            if not context:
                context = {}
            else:
                context = context.copy()
            # List the fields that changed so we can highlight them
            # in the email.
            context['changed_fields'] = vals.keys()
            if isinstance(ids, (int,long)):
                ids = [ids]
            self._send_email(cr, uid, ids, context=context)
        return res

If you process the tasks one-by-one, it is possible to read the state of the task first, do the write, then send an email off for that message telling the recipients how everything changed, but that was not necessary here, thus hasn’t been implemented. One thing that irritates me is the hard-coded list of notification fields, but it’ll do for now.

Receiving emails in OpenERP

Sending emails is only half the story however. It’d be nice, for example, to have a form on a website that people can use to contact sales staff and make enquiries, and for this form to turn into a lead. One such approach would be to use one of many off-the-shelf form posting scripts, and to parse the email within OpenERP. The approach I am specifically looking at, is using Python-code Server Actions to parse the email.

For now I’ve focussed my attention on bringing the email in to OpenERP and doing some processing on it without actually creating leads, as this is more an exploration of how the processing all works.

Setting up the inbound email account

To start off with, you must have a dedicated email address that will receive these notifications. OpenERP then checks this via IMAP or POP3 periodically, and for each new message, will either create a new document or run a server action.

Under “Settings/Configuration/Email/Incoming Mail Servers”, configure a new account, specifying the usual login details. Below this, you’ll notice the fields below the heading “Actions to Perform on Incoming Mails”. This is where OpenERP is told what to do. “Create a new record” defines what sort of object is to be processed.

I used mail.message initially, then discovered I got some funny errors. For everything to work, the object chosen needs to inherit mail.thread. In fact, for testing you can even use mail.thread, and this is what I later used. For CRM stuff, crm.lead inherits mail.thread so I’ll probably use that in the final implementation.

Creating the server action

When the server creates the object it can trigger a server action at the same time. This server action can take a number of forms, but the one of most interest is the Python Code server action. As a test, I made a server action that took a mail.thread, then posted a response email for each message seen, a bit like the autoanswer alias on vger.kernel.org. Creating the server action, I specified mail.thread as the object and used the following Python code:

if context.get('active_id'):
    thread = self.browse(cr, uid, context.get('active_id'), context=context)
    for msg in thread.message_ids:
        msg_body = '''Test server action -- email received:
From: %s
To: %s
CC: %s
Subject: %s
Headers:
  %s
TEXT:
  %s
HTML:
  %s
''' % (
    msg.email_from,
    msg.email_to,
    msg.email_cc,
    msg.subject,
    msg.headers,
    msg.body_text,
    msg.body_html,
)
        pool.get('mail.message').schedule_with_attach(cr, uid, (msg.email_to or 'me@mydomain.com.au').split(',')[0], [msg.email_from or 'me@mydomain.com.au'], 'Test email reply [Re: %s]' % msg.subject, msg_body, context=context)

Probably worth noting, I suspect the self.browse() bit and the context.get('active_id') bit is not necessary, as that is what I’d imagine object is for, but initially I didn’t have the right model chosen, so I suspect that bit of code could be re-worked.

The plan now, is to use the YAML module to parse the message body so that the fields can be sent from the website in a human-readable and unambiguous form.

I’ll be doing some further experimentation, but for now that’s where I’ve gotten. No doubt, there’ll be updates as I discover more about what goes on here.

Telstra, WhoTF is this?

I think our telecommunications supplier has some explaining to do in regards to this issue.

Now, I’m not overly concerned that my usage is being tracked internally by Telstra. A lot of this recording is for tracking abuse of their network, and for billing purposes. This is fine, I have no quarms with that.

However, the above linked article, which I initially heard about on the radio this morning, discusses a more sinester form of tracking.

Here, I have keyed in a special URL… observe the access logs:

www.longlandclan.yi.org 149.135.145.110 - - [27/Jun/2012:09:57:28 +1000] "GET /~stuartl/test.htm HTTP/1.1" 200 102
www.longlandclan.yi.org 50.56.58.47 - - [27/Jun/2012:09:57:28 +1000] "GET /~stuartl/test.htm HTTP/1.0" 200 102

Now, you’ll note there wasn’t one, but two hits. Why? One is clearly from the phone I’m using, as it so happens my phone is hiding behind 149.135.145.110, one of Telstra’s many Carrier NAT gateways (and shame on you Telstra for using carrier NAT).

Who’s this other one? Someone on Rackspace, a US hosting company. What business is my Internet traffic to this other party?

The saving grace for me, most of my traffic is to the APRS-IS network, with some HTTP traffic checking that my tracker has my location up-to-date and the odd query here and there. Maybe a gratuituous download of an ISO or system updates towards the end of the billing period. They’ll get pretty bored with my NextG usage, there’d be hardly anything of commercial value there.

Others however, may have more reason to feel violated. Telstra have some explaining to do.

Full-duplex streaming between sound cards with GStreamer

Some may recall my old set up I used to record the AWNOI net. A bit fiddly, but it worked, and worked well. However the machine I used was short-lived. Basically, I wanted to stream in both directions between a sound device connected to a HF radio transceiver, and a USB wireless headset, with a feed being recorded to disk.

The problem with newer sound devices is the rather limited sync range possible with the modern audio CODECs. Many will not do sample rates that aren’t a multiple of 44.1kHz or 48kHz, and I have a headset that won’t record any other sample rate other than 16kHz. ALSA’s plug: doesn’t play nice with JACK, and I shelved the whole project for later.

Well, tonight I did some tinkering with gstreamer to see if it could do the routing I needed. Certainly all the building blocks were there, I just had to get the pipeline right. A bit of jiggling of parameters, and I managed to get audio going in both directions, and to a RIFF wave file to boot. I’ve put it in a shell script for readability/maintainability:

#!/bin/sh
# GStreamer bi-directional full-duplex audio routing script
# Stuart Longland VK4MSL

CAPT_BUFTIME=50000
CAPT_LATTIME=25000
PLAY_BUFTIME=20000
PLAY_LATTIME=1000

STREAM_FMT=audio/x-raw-int,channels=1,width=16,depth=16,rate=16000
OUTPUT_FMT=audio/x-raw-int,channels=2,width=16,depth=16,rate=16000
OUTPUT="${1:-out.wav}"

HEADSET_DEV=hw:Headset
HEADSET_CAPT_FMT=audio/x-raw-int,channels=1,width=16,depth=16,rate=16000
HEADSET_CAPT_BUFTIME=${CAPT_BUFTIME}
HEADSET_CAPT_LATTIME=${CAPT_LATTIME}
HEADSET_PLAY_FMT=audio/x-raw-int,channels=2,width=16,depth=16,rate=48000
HEADSET_PLAY_BUFTIME=${PLAY_BUFTIME}
HEADSET_PLAY_LATTIME=${PLAY_LATTIME}

SNDCARD_DEV=hw:NVidia
SNDCARD_CAPT_FMT=audio/x-raw-int,channels=2,width=16,depth=16,rate=48000
SNDCARD_CAPT_BUFTIME=${CAPT_BUFTIME}
SNDCARD_CAPT_LATTIME=${CAPT_LATTIME}
SNDCARD_PLAY_FMT=audio/x-raw-int,channels=2,width=16,depth=16,rate=48000
SNDCARD_PLAY_BUFTIME=${PLAY_BUFTIME}
SNDCARD_PLAY_LATTIME=${PLAY_LATTIME}

exec    gst-launch-0.10 \
    alsasrc device=${HEADSET_DEV} \
            name=headset-capt \
            slave-method=resample \
            buffer-time=${HEADSET_CAPT_BUFTIME} \
            latency-time=${HEADSET_CAPT_LATTIME} \
        ! ${HEADSET_CAPT_FMT} \
        ! audioresample \
        ! audioconvert \
        ! ${STREAM_FMT} \
        ! queue \
        ! tee name=headset \
        ! audioresample \
        ! audioconvert \
        ! ${SNDCARD_PLAY_FMT} \
        ! alsasink device=${SNDCARD_DEV} \
            name=sndcard-play \
            buffer-time=${SNDCARD_PLAY_BUFTIME} \
            latency-time=${SNDCARD_PLAY_LATTIME} \
    alsasrc device=${SNDCARD_DEV} \
            name=sndcard-capt \
            slave-method=resample \
            buffer-time=${SNDCARD_CAPT_BUFTIME} \
            latency-time=${SNDCARD_CAPT_LATTIME} \
        ! ${SNDCARD_CAPT_FMT} \
        ! audioresample \
        ! audioconvert \
        ! ${STREAM_FMT} \
        ! queue \
        ! tee name=soundcard \
        ! audioresample \
        ! audioconvert \
        ! ${HEADSET_PLAY_FMT} \
        ! alsasink device=${HEADSET_DEV} \
            name=headset-play \
            buffer-time=${HEADSET_PLAY_BUFTIME} \
            latency-time=${HEADSET_PLAY_LATTIME} \
    interleave name=recorder-in \
        ! audioconvert \
        ! audioresample \
        ! ${OUTPUT_FMT} \
        ! wavenc \
        ! filesink location="${OUTPUT}" \
    headset. \
        ! queue \
        ! recorder-in.sink1 \
    soundcard. \
        ! queue \
        ! recorder-in.sink0

Now the fun begins ironing out the kinks in my data cable for the FT-897. At present, it works for receive, and seems to work for transmit. I use VOX on the radio itself and keep the headset’s microphone on mute when I don’t want to transmit.

At present, I was getting a bit of distorted audio coming back through the headset when I transmitted, almost certainly RF-pickup in the cable and line-in circuitry of the computer’s sound card. I’ll have to see if I can filter it out, but the real test will be seeing if such distortion is present on the outgoing signal — or rather, if it’s significantly audible to be a problem.

Inside the GL4Ever Flytouch III

Well, it’s been a while since I touched this tablet.  I basically chucked it in a corner in disgust after it shat itself rather unceremoniously on the trip before we even got to the NSW/Victorian border.  By “shat” itself, I mean corrupting files on the internal microSD card, intermittent device resets, display flickers, all the hallmarks of a dry joint.

The seller on eBay that sold me the device have been completely unresponsive as to the problems, so looks like I kissed about $250 goodbye.  Ahh well, such is life.  They are still being sold on eBay, but buyer beware, they are cheap, and it’s pot luck whether yours is cheerful, or nasty like mine.  If you want something reliable, look elsewhere.

Having made this mistake, well, I’m looking to make lemonade from the lemon.  First step, was to figure out what on earth I had.  So out with the screwdriver.

You’ll notice on the top and bottom of the unit, there are four small plugs concealing screws.  These hold the LCD screen assembly in place.  Undo these, then you need to carefully work your way around and release the clips that hold the LCD screen assembly.  Do not try to detach the LCD touch panel from the LCD!  I initially couldn’t get it to budge, so I tried doing exactly this in the hunt for possible hidden screws (there were none).  This was the end result:

Shattered Flytouch III touch panel

Why one should not try to detach the touch panel.

Never mind I say… the unit was just about destined for the bin as it was.  External USB HID devices work for what I’m after, but it’ll mean any touch-related fun is out unless I can pick up a replacement 4-wire panel.  Element14 and RS have them at >$80, to which I say, bugger it, I’ll do without.

Having pulled the unit apart, the main PCB is held to the back shell by a few screws, one thing is immediately apparent.  The whole device is based on what looks to be a fairly generic System-on-Module based around the Vimicro VC0882BCXA System-on-Chip, and the Vimicro VC7822EL companion chip.

Flytouch III PCB

Top left is a Wifi module based on the Realtek RTL8111, and to the right, the GPS module (which hooks to one of the serial ports from what I recall).  Down the bottom of the image are the USB ports.  Near the HDMI socket is a Silicon Image SiI9022ACNU HDMI transmitter.

The system on module looks interesting, and I’m curious to find out more about it, as for hobby projects, the pins are not too small to deal with using a soldering iron.  The OS and boot loader exist on the microSD card.  I tried putting a 16GB card in, but evidently I wasn’t getting the partition table right as it wouldn’t boot.  I haven’t tried hooking up a serial port as yet, so it’s hard to know what is wrong.  Some research indicates that ttyS0 lurks on this board just near the aforementioned microSD socket:

The system on module within the Flytouch III

The system on module within the Flytouch III

I haven’t spotted the bad joints that were giving me grief. In fact, having gotten it out of the case, I find the top USB port (flakey from day one) seems to be behaving, and I’ve had no issues with it running with the case apart.  Otherwise I’d be running a soldering iron over a few joints just to make sure everything was right.

Next step?  Well for now, I’ll put it back together (minus touchscreen) and put it aside.  I’ll have a look at tacking a connector onto those serial pins, with a level shifter so I can interact with the serial console.

Having gotten bootloader access, I should be able to debug the SD card cloning issue, then I can have a close gander at what the current u-boot and kernel are doing to tickle the hardware.  End game?  Well, Android isn’t much use without a touch screen, so I’ll be probably hacking together a Gentoo-based environment with some amateur radio related software.  We shall see.

Experiences with the Yaesu VX-8DR

Prior to my road trip to LCA 2012 Ballarat, I bought a new toy, namely a Yaesu VX8-DR handheld.

At that point it turned up only just before I was due to leave, so I wasn’t able to get the accessories I wanted. I cobbled together my own 12V charger lead by snipping the original power supply and soldering on a cigarette lighter socket, but otherwise I used the handheld in its out-of-the-box configuration.

Having gotten back, I have purchased the FGPS-2 GPS module, CT-136 GPS adaptor and the BU-1 Bluetooth module.

Transceiver performance

The set works quite well. The antenna is pretty deaf and useless on 6m, maybe I can get a better after-market tri-bander whip, but on 2m and 70cm it works reasonably well. I’ve heard APRS traffic over distances of 100km, and even been heard on APRS by a digipeater some 90km away.

Audio quality is good, both transmit and receive. Plug in a pair of stereo headphones, and the wideband FM receiver sounds excellent; in stereo to boot.

Probably my biggest nit, is you can’t simultaneously charge and externally power the set. To charge, you must either detach the battery and drop it into a separate charger cradle (an optional extra) or turn off the set.

GPS Performance

When I purchased the VX-8DR, it was a real toss up between it and the VX-8GR. The reason I went the VX-8DR was because it had 6m, and Bluetooth. Having gotten the GPS, I’ve run into the problem a lot have reported; the GPS module is deaf as a post.

The VX8-GR doesn’t improve on this either. However, the good news, is that because my module is external, I can (1) mount it in a better spot, or (2) replace it with a better compatible module.  For VX8-GR owners, this is the end of the road, they can do nothing but moan to Yaesu.  I at least have options.

The module is mounted vertically inside the FGPS-2 casing. Usually with GPS modules such as these, they embed a small patch antenna, whose radiation pattern is perpendicular to the plane of the antenna surface. Being vertical, this means when you hold the radio vertically (as you normally would), GPS reception is poor because the radiation pattern is directly in front of the radio.

The radio seems to perform a lot better, if the radio is held with the screen facing upwards towards the sky. It’ll even work inside my house if I do this. It seems this is a screw-up on par with the iPhone 4.  Another alternative is to replace the module, the FGPS-2 apparently uses 9600 baud serial with NMEA format strings.  However, it seems the parser in the VX-8 is rather crude.  I have a module that does NMEA at 4800 baud, so I’ll either need to coax it up to 9600, or use a microcontroller to buffer and convert rates, and perhaps do some tweaking of the sentence format to make up for the VX-8’s shortcomings.

My hunch; if I make an alternative bracket to the CT-136 adaptor, I can nail this, and another problem, the inability to plug in the GPS and a headset. I have the CT-M11 cable, and thus I plan to make a bracket to connect the FGPS-2 to the end of this cable; allowing me to also plug in a wired headset.

Bluetooth

I bought the Bluetooth as an insurance policy to give me another means of interfacing a headset. Then began the fun of getting it to work with my headsets. I have a couple; a Bullant earmuff-headset, a lightweight mono Digitech headset, and a “MyTalker” headset.

The first was one set I bought some years ago, back when the Bluez was far less stable than it is today, and also long before I was into Amateur Radio or possessed a Bluetooth-capable phone. I tried pairing using a USB Bluetooth dongle, but had little luck, so they got put on one side. Also despite advertising being able to stream music, it only supports HFP and HSP profiles, so you get to listen to your tunes in 8kHz 8-bit mono. They are sold at some hardware stores, such as Mitre 10 The Gap (where I bought my set).

The handheld did pair with this set, but I couldn’t get PTT to work, and the headset itself also had a few faults; namely it was always noisy, and the broadcast receiver stopped working, so I’ve taken them apart for now to see if I can fix these issues. I can key the radio up using the radio’s PTT, but then both internal and headset microphones go live.

The second set is sold by Jaycar, catalog number AA2080. This would be my preferred set to use with the radio as it can pair with two devices simultaneously. It supports the same profiles as the earmuffs, but it’s at least more lightweight.

The BU-1 takes one look at this set, and turns its nose up at it, with the VX-8 giving up and displaying “PAIRING ERROR”.

I also bought the MyTalker set from Jaycar, catalog number XC4894. This set is much like the earmuffs. It embeds its own microphone, but the unit itself provides a 3.5mm socket for you to plug in your own headphones, or use the supplied earphones (which are awful and uncomfortable, don’t use them). At the other end of the unit, is a lead terminated with a 3.5mm plug to plug into a music player. I’ve modded this set to be able to use an external microphone, switchable between a transceiver and the Bluetooth set, allowing a headset connected to a radio to also connect to a phone. I’m still working on this bit.

The VX-8 treats this set with much the same contempt as the mono headset before.

Today, I poppsed in and bought a more expensive set; this time I looked for A2DP functionality, Jaycar have one, catalog number AA2082. Like the AA2080 it can talk to two devices, unlike the AA2080 it supports AVRCP and A2DP. Also, not advertised, is it can function as an analogue headset; supplied in the box is a dual 3.5mm to mini-USB cable that can plug into the headset and allow you to use it with a non-Bluetooth capable device.

I plugged it into the bicycle’s battery to charge on the way home. When I got home, I read the instructions (which are in awful Chinglish). Basically, the English translation of the pairing instructions go like this:

  1. Hold in the MFB button (the centre one on the right ear-cup) in for several seconds. You will hear the voice prompts “Hello”, followed by “Enter Pin Code 0000 on phone”.
  2. When you hear the latter prompt, tell your device to start looking for the headset
  3. When it finds a device called “AA2082”, select it, and enter 0000 as the pin code

So, the steps I followed:

  1. Turn on the VX-8
  2. Hold in the MENU key to bring up the Set menu, then select BLUETOOTH PCODE
  3. Enter 0000 on the keypad.
  4. Hold in the MFB button on the headset until you hear the “Enter Pin Code” prompt
  5. Hit V/M on the VX-8
  6. After a few brief moments, you should see “PAIRING COMPLETE”, press PTT to confirm.

Having got this working, I notice a few things:

  • Stereo (A2DP) sounds a little weird, perfectly clear, but the compression is apparent. I’ll experiment with the laptop later to see if it’s the headset or the radio.
  • Mono works well, pressing MFB toggles PTT on the VX-8. VOX doesn’t seem to work, but no great loss as I find VOX to be a disaster when outdoors.
  • In mono mode, a buzzing is apparent on the received audio. This isn’t audible on transmitted audio, nor did I notice this on received audio when I tried using the headset with my mobile phone.
  • Range seems to be quite restricted, possibly due to where the module is installed it doesn’t get the reception it perhaps needs. A2DP suffers more from this than HFP, with drop-outs being frequent. Again, I’ll need to do some experimentation with the laptop, and perhaps some experimentation with the radio without the battery installed to see if that helps performance.

I’m tossing up whether I get one of these motorcycle Bluetooth headsets.  I ride on the bicycle quite a lot, and at the moment I use headsets embedded in the helmet that are home-built from old computer headsets.  The longevity of the microphone seems to be the biggest problem  I also am on the look-out for an earmuff headset for things like the Imbill car rally, ideally one that can do A2DP.  The Bullant ones I know can’t do this.  I see some earmuffs in the $400+ price bracket that offer Bluetooth, but no idea if that includes A2DP, and frankly, I shudder at that price.

The motorcycle ones are designed to fit a wide range of helmets, and they look as if they’ll fit a set of cheap regular earmuffs quite well.  They typically sell for about $200, support A2DP, multiple devices, and intercom.  Add in $30 for a set of earmuffs, and it makes this a much more attractive option.

More experimentation will be needed I think, but this is looking promising.  I’ll probably post up more details as I come across them.

It’d be nice if Yaesu had been a bit more up-front on what the BU-1 supports: the AA2080 supports both HFP and HSP, yet the BU-1 won’t touch it, the Bullant set supports the same profiles yet the BU-1 works fine with it.  The reasoning for this is not clear, but it does seem that it’ll reliably talk to A2DP capable headsets, so maybe that is a starting point for others.

Likewise with the CT-136, I’ll see if I can fabricate a bracket using the CT-M11 cable, and see where that gets me.

GL4Ever Flytouch III: The internal SD card

Well, further analysis today. The Flytouch III seems to boot off an embedded SD card. I don’t know if it is removable or not, for now I’ll assume no.

Having gained root access earlier, I was able to use dd and nc to siphon off a copy of the internal SD card, which appears as /dev/block/mmcblk0. To grab a copy, first plug the unit into Ethernet (it’ll be faster, trust me) and have another Linux box handy:

Start up netcat on a Linux system:
$ busybox nc -l -p 8123 > tablet.img

Then on the tablet, become root:
$ /system/bin/su

Then start copying to the other system (here; its IP is 12.23.34.45):
# dd if=/dev/block/mmcblk0 | nc 12.23.34.45 8123

Sit back and wait, it should be done in about 5 minutes. Now if you look at the partition table, you’ll see the following:

Disk tablet.img: 482 cylinders, 255 heads, 63 sectors/track
Units = sectors of 512 bytes, counting from 0

   Device Boot    Start       End   #sectors  Id  System
tablet.img1            63   5535320    5535258   b  W95 FAT32           < -- User applications, data live here
tablet.img2       5535321   7612181    2076861   5  Extended
tablet.img3       7612248   7677783      65536  bb  Boot Wizard hidden  <-- Kernel?
tablet.img4       7677784   7743319      65536  bb  Boot Wizard hidden  <-- UBoot?
tablet.img5       5535384   6059608     524225  83  Linux               <-- /system partition
tablet.img6       6059672   7595608    1535937  83  Linux               <-- Android internal?
tablet.img7       7595672   7611992      16321  83  Linux               <-- ???

Partitions 3 and 4 are a complete mystery. They’re not a standard Linux file system, but, the former appears to hold a copy of the Linux kernel, and the latter seems to hold a copy of UBoot. You can bust the image apart using the following script:


/sbin/sfdisk -uS -l tablet.img | grep ^tablet.img | while read part; do
pn=$( echo "$part" | cut -c 11-11 );
s=$( echo "$part" | cut -c 13-25 );
l=$( echo "$part" | cut -c 36-48 );
echo "[$pn][$s][$l]";
dd if=tablet.img of=tablet-$pn.img skip=$(( $s )) count=$(( $l ));
done

You might have to play with column offsets.

The initial part of partition 3 looks like this:

00000000  41 4e 44 52 4f 49 44 21  c0 d7 4b 00 00 80 00 10  |ANDROID!..K.....|
00000010  b5 2a 15 00 00 00 00 11  00 00 00 00 00 00 f0 10  |.*..............|
00000020  00 01 00 10 00 08 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000240  b8 29 4b 8c 7c d2 1f 65  cf b3 3a 78 bc 87 c0 61  |.)K.|..e..:x...a|
00000250  2e 24 79 a5 00 00 00 00  00 00 00 00 00 00 00 00  |.$y.............|
00000260  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000800  27 05 19 56 43 d9 c4 f4  4e ab c7 11 00 4b d7 80  |'..VC...N....K..|
00000810  80 00 80 00 80 00 80 00  d5 42 0e 53 05 02 02 00  |.........B.S....|
00000820  4c 69 6e 75 78 2d 32 2e  36 2e 33 35 2e 37 00 00  |Linux-2.6.35.7..|
00000830  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000840  d3 f0 21 e3 10 9f 10 ee  56 00 00 eb 05 a0 b0 e1  |..!.....V.......|
00000850  52 00 00 0a 6c 00 00 eb  05 80 b0 e1 4f 00 00 0a  |R...l.......O...|
00000860  7b 00 00 eb 13 00 00 eb  c0 d0 9f e5 00 e0 8f e2  |{...............|
00000870  10 f0 8a e2 30 5f 11 ee  02 50 85 e3 30 5f 01 ee  |....0_...P..0_..|
00000880  02 00 80 e3 1f 50 a0 e3  10 5f 03 ee 10 4f 02 ee  |.....P..._...O..|

Note the rather prominent “Linux-2.6.35.7”. Similarly, if we pick through partition 4:

00020eb0  11 12 a0 41 10 13 a0 51  30 1c 81 41 10 02 a0 e1  |...A...Q0..A....|
00020ec0  1e ff 2f e1 ff ff ff ff  ff ff ff ff ff ff ff ff  |../.............|
00020ed0  00 10 05 60 20 10 05 60  00 13 05 60 20 13 05 60  |...` ..`...` ..`|
00020ee0  40 13 05 60 00 16 05 60  20 16 05 60 00 19 05 60  |@..`...` ..`...`|
00020ef0  20 19 05 60 00 1c 05 60  20 1c 05 60 40 1c 05 60  | ..`...` ..`@..`|
00020f00  55 2d 42 6f 6f 74 20 32  30 31 30 2e 30 36 20 28  |U-Boot 2010.06 (|
00020f10  4f 63 74 20 32 39 20 32  30 31 31 20 2d 20 31 37  |Oct 29 2011 - 17|
00020f20  3a 32 37 3a 30 31 29 00  18 13 ea 80 20 13 ea 80  |:27:01)..... ...|
00020f30  27 13 ea 80 2e 13 ea 80  35 13 ea 80 3c 13 ea 80  |'.......5...< ...|
00020f40  43 13 ea 80 4a 13 ea 80  51 13 ea 80 58 13 ea 80  |C...J...Q...X...|
00020f50  5f 13 ea 80 67 13 ea 80  6f 13 ea 80 77 13 ea 80  |_...g...o...w...|
00020f60  7f 13 ea 80 87 13 ea 80  8f 13 ea 80 97 13 ea 80  |................|

Gaining root access on the Android 2.3-based GL4Ever Flytouch III

Yes, I’ve joined this century and bought myself a tablet. Lately, I’ve found myself needing some means of navigating in strange areas whilst on the bicycle, and while pieces of paper work — if you’re organised enough to print them out in advance and not ride too fast (otherwise they disappear with the wind), I’ve found there are a number of shortcomings with this.

Since I like open source, and didn’t like the idea of spending several hundred on a hand-held GPS with proprietary firmware & map data which I need to constantly purchase updates for, I opted for the cheapskate route.  I picked up a GL4Ever Flytouch III Tablet off eBay.  The unit I have came loaded with Android 2.3 (Gingerbread).

Ultimately I may replace the OS, or at least, the kernel, soon as I have sources for it, but in the meantime, it runs what it came with.  I have however, already managed to gain root access.

Those who might do a search for how to do so, may come across this guide.  I tried this first, and found I had no joy.  USB Debugging was enabled out-of-the-box on the unit I have, but z4root did not successfully enable root access.  The following are my notes on how I gained a shell with root access on the device.  Ohh, and I warn you, there is no warranty given in the instructions below.  If it breaks, you get to keep the pieces.

  1. Download and install Gingerbreak.
  2. Run Gingerbreak, it will run for a while, before resetting the device.  Upon starting, you should now notice you have a Superuser application installed.
  3. Next, install Android Terminal.
  4. Now, run /system/bin/su.

/bin/su is a symbolic link to /bin/busybox which was installed without the setuid bit, and is broken anyway, you’ll find if you do add a setuid bit, it will report that it can’t find the ‘root‘ user.  This system has no /etc/passwd or equivalent user database, so it has no idea who ‘root‘ is, but it knows who UID 0 is, and that’s what matters.  The latter ‘su‘ you’ll find has the necessary permissions, and knows about UID 0.

Other things I’ve found… the operating system lurks on a SD card embedded in the device.  Or at least, it’s presented as a SD card; /dev/block/mmcblk0.  The user-accessible SD-card port is /dev/block/mmcblk1.  You can verify this by ejecting the card, doing a ls /dev/block, then inserting a card and repeating.

On my TODO list, is to make a DD-copy of this block device, and pick through to see how one swaps out the kernel.  I’ll post notes if I figure this out.  I am also yet to obtain the kernel sources, I’ll chase those up before long.

“Journaling could not be enabled”

I struck this little jem today whilst shuffling the partitions around on my MacBook. (In my wisdom, I had made my MacOS X partitions waaay too big, and my Linux partitions waaay too small.)

The back-story is that I had made my MacOS X root and /Users partitions too big. I had successfully shrunk both, however, I discovered MacOS X’s Disk Utility does not support moving partitions, only resizing.  So, I created a new non-journalled HFS+ partition, booted into a Linux LiveCD, used rsync to clone the data.

All good and well, except after I deleted the old partition, I found I could not resize a non-journalled partition.  Fine, I hit “Enable Journaling”… no dice, it wouldn’t do it, and wouldn’t explain why.

So I was in a pickle.  The partition was too small for my needs, there was room to grow it, but it couldn’t do that unless journalling was enabled, and it wouldn’t enable it for me.

Further investigation, I fire up the Terminal and have a squiz on the command line, I spot something rather interesting:

vk4msl-mb:~ root# cd /Volumes/Home
vk4msl-mb:Home root# ls
.DS_Store		.fseventsd		Shared
.Spotlight-V100		.journal		stuartl
.TemporaryItems		.journal_info_block
.Trashes		.localized

Ohh yes, that might be a probable cause. rsync it seems, copied the .journal file over. And thus when Disk Utility open()‘s .journal (presumably with O_CREAT), the MacOS X kernel reports EEXIST (the file already exists).

I tried renaming it (with mv):

vk4msl-mb:Home root# mv .journal{,.old}
mv: rename .journal to .journal.old: Operation not permitted

Okay, that didn’t work. That said, the old partition was journalled, this one is not. So it probably doesn’t contain anything of great relevance now. It does have data:

vk4msl-mb:Home root# ls -ld .journal
----------@ 1 root  wheel  25165824 Apr  4  2011 .journal

So I decided to kiss it goodbye:

vk4msl-mb:Home root# rm .journal
override ---------  root/wheel uappnd,uchg,nodump,opaque for .journal? y
vk4msl-mb:Home root# rm .journal_info_block 
override ---------  root/wheel uappnd,uchg,opaque for .journal_info_block? y
vk4msl-mb:Home root# ls
.DS_Store	.TemporaryItems	.fseventsd	Shared
.Spotlight-V100	.Trashes	.localized	stuartl

All gone. Having done this, I now found that Disk Utility was more than happy to not only enable journalling, but grow the partition as I originally asked.

vk4msl-mb:~ root# mount
/dev/disk0s2 on / (hfs, local, journaled)
devfs on /dev (devfs, local, nobrowse)
/dev/disk0s3 on /Volumes/Home (hfs, local, journaled)
map -hosts on /net (autofs, nosuid, automounted, nobrowse)
map auto_home on /home (autofs, automounted, nobrowse)
/dev/disk0s7 on /Volumes/Data (hfs, local)
vk4msl-mb:~ root# df -h
Filesystem      Size   Used  Avail Capacity  Mounted on
/dev/disk0s2    93Gi   20Gi   73Gi    22%    /
devfs          123Ki  123Ki    0Bi   100%    /dev
/dev/disk0s3   242Gi   82Gi  160Gi    34%    /Volumes/Home
map -hosts       0Bi    0Bi    0Bi   100%    /net
map auto_home    0Bi    0Bi    0Bi   100%    /home
/dev/disk0s7    84Gi  893Mi   83Gi     2%    /Volumes/Data

If others find themselves in this sticky situation, this might be a way out. I would strongly advise they back up any data before messing with file systems in this manner however.

Broadcom Wireless related ebuilds

Hi all…

I got fed up of restoring my firmware for the Broadcom wireless chip in my late-2008 model MacBook.  Anyone who has one of these might find the current in-tree versions of net-wireless/b43-firmware is missing files needed by the modern b43 driver (namely ucode16_mimo.fw), and net-wireless/b43-fwcutter doesn’t well, cut it, for extracting the newer files.

If you’ve got a newer 802.11n-based Broadcom chip, you might find the following ebuilds handy:

  • net-wireless/b43-firmware-5.10.56.27.3
  • net-wireless/b43-fwcutter-015 and net-wireless/b43-fwcutter-9999

The first is the firmware mentioned in this post.  It needs a newer fwcutter binary than is provided in Portage.  You’ve got the choice of the latest version, or the bleeding edge via git.  Both work at time of writing, although neither are guaranteed.

The ebuilds are not in-tree, I’ll leave that for the actual maintainer for these ebuilds to pick them up if desired, I’ve put them in an overlay accessed via the following command:

git clone git://git.longlandclan.yi.org/overlays/b43.git

Or you can take a squiz via gitweb.