Raspberry Pi Carputer 2 – XBMC Skin & Scripts

This post has been a long time coming. In fact, so long that I no longer have the Zafira that I installed the carputer in. But I still have all the code and workings out burning a hole in my hard drive, and questions keep coming in asking how it was all done. So I’ll try to explain a little more and put some code up.

If you want to skip my explanations and go straight to the code, I’ve put various bits on Github:

Getting data into XBMC

One of the biggest challenges with using XBMC as a display is getting the data from GPIO and from your OBD bluetooth chip onto XBMC’s display. For this I wrote a couple of services, based on an album info-finding script, to set and get properties on a particular window of the skin. In my application, the service starts on load of the ‘Home’ window. This is the window that I adapted to show various data. I kept the music and volume data so that this Home window is what you’ll nearly always be looking at.

Running the script is as simple as:

<onload>Runscript(script.OBD, backend=true))</onload>

for the OBD script.
Or

<onload>Runscript(script.GPIO, backend=true, ins=0, ins=1)</onload>

for the GPIO script – notice the ‘ins’ passed to it; this tells the script to set these particular GPIO pins to IN and keep the skin property up-to-date.

I chose to use the ‘gpio’ command and run it with os.system call from python for compatibility – this command can be run as a normal user once it’s set up, if I remember correctly. And while it doesn’t monitor the IN pins as an interrupt, the service checks every 1/2 a second so for the purpose of illuminating lights on the screen (making bits visible) it’s fine.

Toggling GPIO

<code><onfocus>SetProperty(GPIOon, 17)</onfocus>
<onunfocus>SetProperty(GPIOoff, 17)</onunfocus>

should do it. Notice this sets a property on the skin. The GPIO script has this little bit of code in its loop that checks those properties:

if xbmc.getCondVisibility("!IsEmpty(Window(home).Property(GPIOon))"):
 self.ons.append(self.window.getProperty('GPIOon'))
 self._StartOnActions()
 self.window.clearProperty('GPIOon')

Displaying data

For simple on/off values, as you’ll get from the GPIO script, you’ll probably want to make an image or a control group visible or not:

<control>
<description>Test</description>
<type>image</type>
<id>0</id>
<width>63</width>
<height>60</height>
<posx>100</posx>
<posy>128</posy>
<texture>handbrake.png</texture>
<visible>SubString(Window(home).Property(GPIO0), "0")</visible>
</control>

For other values, here’s an example of what I used for the OBD info:

<!-- OBD Stuff -->
<control type="group" id="77">
<visible>SubString(Window(home).Property(OBD-Conn), "OK")</visible>

<control>
<description>BattImg</description>
<type>image</type>
<id>0</id>
<width>40</width>
<height>40</height>
<posx>490</posx>
<posy>60</posy>
<texture>battery.png</texture>
</control>

<control>
<description>Vbatt</description>
<type>label</type>
<id>555</id>
<posx>530</posx>
<posy>78</posy>
<info>Window(home).Property(OBD-Voltage),,v</info>
<font>alwin24bold</font>
<align>left</align>
<textcolor>ffff6103</textcolor>
<!--<visible>!Control.HasFocus(5)</visible>-->
</control>


<control>
<description>BR-Throttle</description>
<type>label</type>
<id>93</id>
<posx>635</posx>
<posy>537</posy>
<font>alwin24bold</font>
<textcolor>ffff6103</textcolor>
<label>ACC ped</label>
<alignx>center</alignx>
<info>Window(home).Property(OBD-Throttle),,%</info>
</control>

<control>
<description>TR-MPG</description>
<type>label</type>
<id>93</id>
<posx>625</posx>
<posy>9</posy>
<font>alwin24bold</font>
<textcolor>ffff6103</textcolor>
<alignx>center</alignx>
<info>Window(home).Property(OBD-MPG)</info>
</control>


<control type="fadelabel" id="95">
<description>TL-AvMPG</description>
<posx>6</posx>
<posy>9</posy>
<width>200</width>
<visible>true</visible>
<scroll>false</scroll>
<scrollout>false</scrollout>
<pauseatend>5000</pauseatend>
<label>Trip: 0 mi</label>
<info>Window(home).Property(OBD-AvMPG),Av:, mpg</info>
<info>Window(home).Property(OBD-TripL),T: , L</info>
<info>Window(home).Property(OBD-TripP),T: ,p</info>
<info>Window(home).Property(OBD-TripMi),T: , Mi</info>
<font>alwin24bold</font>
<resetonlabelchange>false</resetonlabelchange>
<!--<textcolor>FFB2D4F5</textcolor>-->
<textcolor>ffff6103</textcolor>
</control>
<control id="671">
<description>ThrottleProgressbar</description>
<type>progress</type>
<posx>577</posx>
<posy>53</posy>
<width>101</width>
<height>461</height>
<reveal>true</reveal>
<!--<info>Window(home).Property(OBD-BoostPercent)</info>-->
<texturebg>progress_mid-homeR.png</texturebg>-->
<lefttexture>-</lefttexture>
<midtexture>progress_back-homeR.png</midtexture>
<righttexture>-</righttexture>
<!--<overlaytexture>progress_over_vertL.png</overlaytexture>-->
</control>

...

There are a few good examples there.

  • Firstly, the whole group doesn’t function if OBD is not connected.
  • Second, notice the formatting on the <info> tags – <info>[value],PREFIX,SUFFIX</info>
  • And notice the TL-AvMPG section: It cycles through a few properties, changing every 5s.
  • Then the ThrottleProgressbar is a progress bar with distinct texture .png images. I just used the ‘mid’ texture and the reveal tag, to make it more natural.

I regret not having taken more videos of this but this video shows how the progress bars work.

The skin

The  XBMC-Skin.seventft skin was originally made by a German guy named djtoll. It’s an awesome skin and some serious work has gone into it. When I found it, it wasn’t compatible with XBMC / Kodi version 12 and up, since they changed the way things work. My github version is one that I’ve made work with XBMC 12+, but only for my particular needs. It will only work at PAL16x9 resolution, because that’s what I used. And the progress bar images, etc. that I added are only in that resolution.

Pull requests welcome!

I’ve seen this said many times about open source projects, and I’m trying to learn what it means. Yes, I’m new to github and all that but it would be great if this project got better and compatible with more cars now the source it out there.

If you use the seventft skin in another resolution, and add compatibility, please try to merge your changes in. It would be great to see my work our work benefit other people.

Fiddling Office365 ADFS to work like on-premise exchange

Office 365 uses ADFS (Active Directory Federated Services) to log on users.

ADFS isn’t true Single Sign-on, it’s more like SAML token based: The idea is, you visit a service provider (like Office365) and they redirect you to your organisation’s ADFS service to log on. Then you get directed back with an access token. Here at the university, we have two internal ADFS servers, Network Load-Balanced, plus an ADFS proxy for external.

The Microsoft model is that these servers are separated by your split-brain DNS: Anything that looks up, for example, sso.bolton.ac.uk on the internal DNS gets the internal ADFS(s) and anything outside your network looks up the same and gets your ADFS Proxy server. So by default, the internal servers use NTLM authentication so users on your network don’t have to re-enter their credentials.

Problem: Not all devices that point to the internal DNS are domain-joined PCs

I said devices there for a reason: Our wireless users get the internal DNS. As do student halls of residence. So anyone using a wireless laptop that goes to our http://cloudmail.bolton.ac.uk Office365 login URL (a cname that points to outlook.com and gets redirected to our SSO ADFS login page) will be presented with a horrible browser username/password challenge box. Another problem with the default configuration, specific to Office 365, is that staff don’t have mailboxes on Office365. So, trying to access a test student mailbox gives the UserHasNoMailboxException:

nomailbox

NTLM is great, and we want it for domain-joined student PCs. How do we differentiate?

You can change the default authentication method that ADFS uses in the web.config file (usu. c:\inetpub\adfs\ls\web.config) by swapping these lines:

<microsoft.identityServer.web>
 <localAuthenticationTypes>
 <add name="Forms" page="FormsSignIn.aspx" />
 <add name="Integrated" page="auth/integrated/" />
 </localAuthenticationTypes>

This makes forms-based authentication the default. And you can customise the login page by following this guide: http://blogs.technet.com/b/stevenha/archive/2012/11/12/customizing-the-adfs-forms-based-login-page.aspx

That solves the problem of NTLM trying to auto-login all users on our network. But here’s how our exchange environment currently works for students:

  • Users type in http://webmail.bolton.ac.uk and are presented with a login form, whether off-campus or on-campus.
  • Students get a shortcut on their desktop and start menu pointing to http://exchange-server/exchange (or /owa for exchange 2010+). This logs them in with NTLM automatically.

This makes sense for a couple of reasons: You might want to access your email from another user’s computer. Perhaps a few students are working in a group around one PC. Or a student comes to the help desk with an email query. They can go to the webmail URL and enter their username/password. Then click ‘logout’ or close the browser after. At the same time, you would expect clicking a shortcut on your desktop to know who you are and not need your password again.

Can we replicate the behaviour of exchange?

An administrator at Newcastle University had a similar problem. Not all their internal devices are domain-joined, so they came up with the solution to manipulate the user-agent string on their domain-joined browsers. Details here: https://blogs.ncl.ac.uk/isg/?p=296

That would almost work for us, but we don’t just want all domain-joined PCs using NTLM. We want the user (or their shortcut at least) to choose.

How to find another URL

My hope was to use the /adfs/ls/IdpInitiatedSignon.aspx along with a RelayState string to take users to the login page first, then get ADFS to spit them out to Office365. I installed ADFS Update Rollup 2, added the line <useRelayStateForIdpInitiatedSignOn enabled=”true” /> to web.config, and tried to work out the RPID and RelayState strings. I failed. The closest I got was using the LoginToRP querystring. But that still came up with errors; outlook.com wouldn’t accept the SAML token unless the browser had been there first. Details on RelayState can be found in this technet article.

Then I used the SAML tracer plugin for Firefox. (https://addons.mozilla.org/en-US/firefox/addon/saml-tracer/) – it allows you to see all the steps in your redirection journey without having to do quick URL-copies or pick through your history. It turns out that the cloudmail.bolton.ac.uk CNAME takes you to

https://www.outlook.com/owa/?targetname=cloudmail.bolton.ac.uk

and then a URL that looks like this:

https://sso.bolton.ac.uk/adfs/ls/?wa=wsignin1.0&wtrealm=urn:federation:MicrosoftOnline&wctx=wa%3Dwsignin1.0%26rpsnv%3D3%26ct%3D1397212618%26rver%3D6.1.6206.0%26wp%3DMBI_KEY%26wreply%3Dhttps:%252F%252Fwww.outlook.com%252Fowa%252F%26id%3D260563%26whr%3Dbolton.ac.uk%26CBCXT%3Dout

once you’ve logged on, ADFS spits you out here:

https://outlook.office365.com/owa/?exsvurl=1&ll-cc=1033&modurl=0&realm=bolton.ac.uk

Well… If you attempt to access the above URL before you log in, you get taken to your SSO page with a querystring thus:

https://sso.bolton.ac.uk/adfs/ls/?wa=wsignin1.0&wtrealm=urn:federation:MicrosoftOnline&wctx=wa%3Dwsignin1.0%26rpsnv%3D3%26ct%3D1397212663%26rver%3D6.1.6206.0%26wp%3DMBI_SSL%26wreply%3Dhttps:%252F%252Foutlook.office365.com:443%252Fowa%252F%253Fexsvurl%253D1%2526ll-cc%253D1033%2526modurl%253D0%2526realm%253Dbolton.ac.uk%26id%3D260563%26whr%3Dbolton.ac.uk%26CBCXT%3Dout

Spot the difference? The first URL contains a URL-encoded “reply=http://www.outlook.com” while the second contains “reply=https://outlook.office365.com”. This makes sense; it reflects the URL we visited before we got redirected. I now have my two URLs, and using similar code to the guys at Newcastle university I can make ADFS use either forms-based auth or NTLM.

The internal ADFS boxes now have the following in their adfs\ls\FormsSignIn.aspx.cs:

 using System;
using System.Web;
using Microsoft.IdentityServer.Web;
using Microsoft.IdentityServer.Web.UI;

/// <summary>
/// Attempts to authenticate the user via HTTP Forms Authentication.
/// </summary>
public partial class FormsSignIn : FormsLoginPage
{
 protected void Page_Load( object sender, EventArgs e )
 {
 int pos = Request.RawUrl.IndexOf('?');
 int len = Request.RawUrl.Length;
 string rawq = Request.RawUrl.Substring(pos + 1, len - pos - 1);

//Convert query string (qs) to a string
 string qs = HttpUtility.ParseQueryString(rawq).ToString();
 if(qs.IndexOf("outlook.office365.com") > -1)
 {
 Response.Redirect("/adfs/ls/auth/integrated/?" + qs, true);
 }
 else
 {
 //Carry on and do Forms Based Authentication
 } 

 

Easy. Now when all students have moved to Office365, we simply change their webmail shortcuts to

https://outlook.office365.com/owa/?exsvurl=1&ll-cc=1033&modurl=0&realm=bolton.ac.uk

Caveats:

Updates to ADFS may break this.

Updates to how Office365 works may break this. The outlook.office365.com URL may cease to function.

But we’re happy – we can use Office365 like we use on-premise exchange without having to see that horrible sad face.

Raspberry Pi Car-puter

The problem

We down-graded but up-sized our car, to a Vauxhall / Opel Zafira. It’s only a 2002 model but does come with cruise control, air con and all electric windows, mirrors, etc. However, unlike our old Peugeot 307, it doesn’t tell you what you’ve set the cruise control to. Or, in fact, a very accurate speed reading. And the built-in trip computer only shows one piece of information at a time; you have to scroll all the way round 7 options to get from average MPG to instant, for example.

Zafira-display

The stock display (not mine)

The Solution – A Raspberry Pi, of course!

PiPuter

Car Raspberry Pi computer


I got myself an OBD II bluetooth interface off ebay for about £7. The OBD port on the Zafira sits just underneath the handbrake lever, and gets you connected to the engine ECU, as well as the ABS and power steering ECUs.

I started experimenting with some android apps to start with. Most of these use the open PIDS available here on Wikipedia. The Mode 1 PIDS give you a fair amount of information – officially. But on my car, they didn’t. I could get RPM, speed and water temperature, but not much else. To replace my OEM display, I’d need at least to get hold of the Miles Per Gallon and as many other readings as I could. So, I found a couple of Opel-specific apps that gave some other bits of information – MAF flow rate, injection fuel quantity, fuel temperature, and lots of other goodies. That’s enough to work out MPG! If only I could get my hands on these manufacturer-specific PIDs, I’d be able to get the same readings on my Pi and display them nicely.

Hacking the bluetooth comms

At this point I had Xbian running on the R-pi, and had installed a bluetooth dongle and got comms established between the Pi and the scanner. See this page on how TODO!. I tried looking for ways to snoop the serial comms between my android phone and the scanner. Apparently, eavesdropping on bluetooth comms is extremely difficult, since it channel-hops constantly and you won’t know which channel it about to hop to unless you were there and on the right channel at the start of the conversation. Suffice to say, a standard £1 bluetooth dongle won’t let you eavesdrop on communications.

The OBD comms

So, how do I find out what to send to my ECU to get the needed telemetry?

Strace!

Strace is a linux command that helps you debug processes. Importantly for us, it logs writes to the serial port. So, run it on android as root and it’ll tell you what your processes are talking about.

Below is a snippet of what I got back from strace after attaching it to the right process. I had to root my phone, copy in an strace binary, run it as root, and point it to the right Process ID.

syscall_983042(0x5dde6edc, 0x5dde6ee0, 0, 0xfff, 0x408d5110, 0x5dde6edc, 0x5dde5831, 0xf0002, 0, 0x5dde6ee0, 0xfca8f7fe, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5dde6edc, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
write(56, "A", 1) = 1
write(56, "T", 1) = 1
write(56, "E", 1) = 1
write(56, "0", 1) = 1
write(56, "\r\n", 2) = 2
write(56, "A", 1) = 1
write(56, "T", 1) = 1
write(56, "L", 1) = 1
write(56, "0", 1) = 1
write(56, "\r\n", 2) = 2
write(56, "A", 1) = 1
write(56, "T", 1) = 1
write(56, "A", 1) = 1
write(56, "L", 1) = 1
write(56, "\r\n", 2) = 2
syscall_983042(0x5dddf6c4, 0x5dddf6c8, 0, 0xfff, 0x408d5110, 0x5dddf6c4, 0x5dde6eb5, 0xf0002, 0, 0x5dddf6c8, 0xfbf6f007, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5dddf6c4, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
write(56, "A", 1) = 1
write(56, "T", 1) = 1
write(56, "S", 1) = 1
write(56, "P", 1) = 1
write(56, "5", 1) = 1
syscall_983042(0x5dde79cc, 0x5dde79dc, 0, 0xfff, 0x5dde79cc, 0x408cff90, 0x56e67af8, 0xf0002, 0x408d5110, 0x1fe9, 0, 0x408d5190, 0, 0xbec9b7a8, 0x408a
9b63, 0x400e4918, 0x40000010, 0x5dde79cc, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
write(56, "\r\n", 2) = 2
syscall_983042(0x5ddda8ac, 0x5ddda8b0, 0, 0xfff, 0x408d5110, 0x5ddda8ac, 0x5dde7599, 0xf0002, 0, 0x5ddda8b0, 0xfe74f00c, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5ddda8ac, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
write(56, "A", 1) = 1
write(56, "T", 1) = 1
write(56, "H", 1) = 1
write(56, "1", 1) = 1
write(56, "\r\n", 2) = 2
syscall_983042(0x5dde7db4, 0x5dde7db8, 0, 0xfff, 0x408d5110, 0x5dde7db4, 0x5dde7ef1, 0xf0002, 0, 0x5dde7db8, 0x4300e09c, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5dde7db4, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
write(56, "A", 1) = 1
write(56, "T", 1) = 1
write(56, "S", 1) = 1
write(56, "T", 1) = 1
write(56, "F", 1) = 1
write(56, "F", 1) = 1
write(56, "\r\n", 2) = 2
write(56, "A", 1) = 1
write(56, "T", 1) = 1
write(56, "S", 1) = 1
syscall_983042(0x5dde79c0, 0x5dde79c4, 0, 0xfff, 0x408d5110, 0x5dde79c0, 0x5dde7f31, 0xf0002, 0, 0x5dde79c4, 0x4300e2b6, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5dde79c0, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
write(56, "H", 1) = 1
write(56, "8", 1) = 1
write(56, "1", 1) = 1
write(56, "1", 1) = 1
write(56, "1", 1) = 1
write(56, "F", 1) = 1
syscall_983042(0x5dde85c8, 0x5dde85cc, 0, 0xfff, 0x408d5110, 0x5dde85c8, 0x5dde72a9, 0xf0002, 0, 0x5dde85cc, 0xfe6ef7fe, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5dde85c8, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
write(56, "1", 1) = 1
write(56, "\r\n", 2) = 2
syscall_983042(0x5dde77f4, 0x5dde7804, 0, 0xfff, 0x5dde77f4, 0x408cff90, 0x56e939e8, 0xf0002, 0x408d5110, 0x1fd5, 0, 0x408d5190, 0, 0xbec9b7a8, 0x408a
9b63, 0x400e4918, 0x40000010, 0x5dde77f4, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
syscall_983042(0x5dde89bc, 0x5dde89c0, 0, 0xfff, 0x408d5110, 0x5dde89bc, 0x5dde8b0d, 0xf0002, 0, 0x5dde89c0, 0x4300e0a6, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5dde89bc, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
syscall_983042(0x5dde8b48, 0x5dde8b4c, 0, 0xfff, 0x408d5110, 0x5dde8b48, 0x5dde89fd, 0xf0002, 0, 0x5dde8b4c, 0x4300e758, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5dde8b48, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
write(56, "3", 1) = 1
write(56, "E", 1) = 1
write(56, "\r\n", 2) = 2
syscall_983042(0x5dde6d70, 0x5dde6d74, 0, 0xfff, 0x408d5110, 0x5dde6d70, 0x5dde8eb5, 0xf0002, 0, 0x5dde6d74, 0xf8a0f002, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5dde6d70, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
syscall_983042(0x5dde8f10, 0x5dde8f20, 0, 0xfff, 0x5dde8f10, 0x408cff90, 0x56d91908, 0xf0002, 0x408d5110, 0x1fb7, 0, 0x408d5190, 0, 0xbec9b7a8, 0x408a
9b63, 0x400e4918, 0x40000010, 0x5dde8f10, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
syscall_983042(0x5dde8f04, 0x5dde8f08, 0, 0xfff, 0x408d5110, 0x5dde8f04, 0x5dde8f71, 0xf0002, 0, 0x5dde8f08, 0x4300e034, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5dde8f04, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
syscall_983042(0x5dde5ba0, 0x5dde5ba4, 0, 0xfff, 0x408d5110, 0x5dde5ba0, 0x5dde7599, 0xf0002, 0, 0x5dde5ba4, 0xfcfaf001, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5dde5ba0, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
syscall_983042(0x5dde6514, 0x5dde6518, 0, 0xfff, 0x408d5110, 0x5dde6514, 0x5dde9449, 0xf0002, 0, 0x5dde6518, 0xff98f002, 0x2, 0, 0xbec9b7b8, 0x408a9b6
3, 0x400e4918, 0x40000010, 0x5dde6514, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
syscall_983042(0x5dde77e8, 0x5dde77ec, 0, 0xfff, 0x408d5110, 0x5dde77e8, 0x5dde8769, 0xf0002, 0, 0x5dde77ec, 0xffbef000, 0x417321d8, 0, 0xbec9b7b8, 0x
408a9b63, 0x400e4918, 0x40000010, 0x5dde77e8, 0, 0, 0xc764, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
write(56, "1", 1) = 1
write(56, "A", 1) = 1
write(56, "8", 1) = 1
write(56, "0", 1) = 1
write(56, "\r\n", 2) = 2

If you’re familiar with AT commands at all, you may recognize some of the command sent. Either way, the ELM327 AT commands sheet has them all listed. Of course, my £7 dongle is not an official ELM327 based device, it’s a Chinese copy! But they did keep the AT commands the same for compatibility.

So, here’s how one piece of software initializes the bus:

atz #reset
ete0 #echo off
ATL0 #linefeeds off
ATAL #allow long messages &gt;7
ATSP5 #Set protocol ISO 14230-4 (KWP FAST)
ATH1 #headers on
ATSTFF #timeout big (ST32 is good)
ATSH8111F1 #header 8111F1

I already know from research that my Bosch EDC15M ECU communicates over KWP, but what’s really interesting here is the header, 8111F1. Thanks to a Russian site with an explanation of KWP, I could work out what this header means (see below). The communications continue:

3E #tester present
1A80 #ECU ID table
1A81
1800FF00 &nbsp; #Mode 18 request for DTCs
3E
82
ATZ

ATSH8128F1 #28 is the ABS
3E
3E
3E
82
ATZ

ATSH8131F1 #31 is the EPS
3E
1A80
1A81

2101

2101

2101

KWP

The KeyWord Protocol, KWP2000 is not very well documented as far as I can find. One amazingly helpful resource was a Russian site with a document detailing one Russian ECU manufacturer’s implementation of KWP. Of course, their implementation is not exactly the same as Opel/Vauxhall’s, but it gives an insight into ISO 14230, which otherwise is inaccessible to those not willing to pay tens of thousands for access to the standard. It’s worth a read – open the page in Chrome if you do want a look, and it’ll translate it into English-ish for you.

Here’s my notes on it:

Header: Format – Target – Source – [Len]
Rest of message: Sld Data (63bytes)[255] Checksum

So, the header 81 11 F1 means:       Format 81 (Start Comms, as per below) —–  TO 11 (as above, 28 refers to the ABS in this car and 31 is the EPS controller) —– FROM F1 (I think this is abritrary, as long as it’s no used on the bus)

Format name                 Answer (id always 7f; bit 6=1)
81     startComms           c1
82     stopComms            c2
01     readDataByPID
10     startDiagnosticSess 50
20     stopDiags           60
11     ecuReset            51
12     freezeframeData
14     clearDTC            54
18     readDTCs            58
1A     readECUID           5A
21     readDatabyLocID     61
22     readDatabyCommonID + 2 bytes
23     readMembyAddr       63
25     StopRepeatedDatatrans
26     SetDataRate
27     SecurityAccess
28     DisableNormComms
2C     DefineLocalID
2E     WriteDatabyCommonID
2F     IOcontrolByCommonID
30     ioControlByLocID    70
3B     writeDataByLocID    7B Corresponding to 21
3E     testerPresent       7E Apparently send this every 2.5s
GMLAN-specific:
A2     ReportProgrammingState
A5     EnterProgramming
A9     CheckCodes
AA     ReadDPID
AE     DeviceControl

So those are the header types, now for the query payload:

10 startDiagnosticSess packet can be followed with 81 (diagnostic mode pls) and baud rate (0A, normal:26, high:39, enhanced)
	To stop send a 20 (should get 60 back. 7F is bad news)
3E Testerpresent followed by 01 (reponse pls) or 02 (its ok don't reply)
11 ecuReset followed by 01 for power on
1A ecuID  followed by 1 byte:
	80 - complete table. 
	1A 80 response:
		5a 80 [3-21]VIN [22-37]ECUhardwareno [38-57]sysSupplierECUSW [58-72]Engine [73-79]repairshopCode [80-89]progDate [90-97]vehManECUID
		5A		90			91					92						93				94						98			9f
1A A0 possibly ODO reading?
1A 90 VIN

14 clearDTC followed by 0000 Pwertrain or FF00 All systems. 54 reply contains same 2 bytes

18 ReadDTCs complicated

21 readDatabylocID:
	01 - AftersalesServiceRecord (->128b)
	02 - endOfAssemblyLine (->128)
	03 - factoryTest (->128)
	A0 - immobilizerRecord (2)
	A1 - Body serial No (7)
	A2 - Engine Serial No (7)
	A3 - Manufacture date (10)
		A1 for example gives 61 A1 +7 bytes returned
	C3 - Possibly TRANS
	C4 - Aircon request at byte 1
	D3 - Cruise control switches??

21 A0 Immobilizer record
21 30 IO control
21 30 IObelow Param state 
IOs:
01	injector1OutputControl	This setting informs the control unit, the tester prompts direct control of the nozzle 1.	I1OC
02	injector2OutputControl	This setting informs the control unit, the tester prompts direct control nozzle 2.	I2OC
03	injector3OutputControl	This setting informs the control unit, the tester prompts direct control of the nozzle 3.	I3OC
04	injector4OutputControl	This setting informs the control unit, the tester prompts direct control nozzle 4.	I4OC
05	ignition1OutputControl	This setting informs the control unit, the tester prompts direct control of the ignition coil 1 and 4 cylinders.	IGN1OC
06	ignition2OutputControl	This setting informs the control unit, the tester prompts direct control of the ignition coil 2 and 3 cylinders.	IGN2OC
09	fuelPumpRelayOutputControl	This setting informs the control unit, the tester prompts direct control of the fuel pump relay.	FPROC
0A	coolingSytemFanRelayOutputControl	This setting informs the control unit, the tester prompts direct control of the cooling fan relay motor.	CSFROC
0B	airConditionRelayOutputControl	This setting informs the control unit, the tester prompts direct control relay unit.	ACROC
0C	malfunctionIndicationLampOutputControl	This setting informs the control unit, the tester prompts direct control of the MIL.	MILOC
0 D	canisterPurgeValve OutputControl	This setting informs the control unit, the tester prompts direct control canister purge valve.	CPV OC
41	idleStepMotorPositionAdjustment	This setting informs the control unit, the tester prompts direct position control idle speed regulator.	ISMPA
42	idleEngineSpeedAdjustment	This setting informs the control unit, the tester prompts direct control idle speed.	IESA

Params: 00 returntoECU. 01 reportState. 02 reportIOconditions. 03 reportIOscaling. 04 resetDefault. 05 freezeCurr. 06 executeControlOption. 07 shortTermAdjust. 08 longTermAdjust. 09 reportIOcalibration
---------------
---------------
23 Read mem by address
23 MemType=0 AddrMSB AddrLSB Memsize
Response 63 xx xx xx xx
-----
3B Write Data by LocID
90 - VIN
98 - RepairShopCode
A1 bodySerNum
A2 engineSerNum
A3 manufactDate

Eg. 3B 90 + 19 ASCII chars
3B 90 xx xx xx xx -> Wait for flow control??? 30 00 00
Response 7B 90 or 7F 3B errCode

------------
27 Security
27 subFunct
01	SeedRequest
02	KeyResponse
03	SeedAnother
04	ResponseAnother.....

Bored yet?

I won’t go on too much more about KWP, suffice to say sending a header 8111F1 and payload 2101 gives the “AftersalesServiceRecord”, which contains all the data we need:

2101: 80 F1 11 4C 61 01 00 00 00 00 00 00 00 00 0C 0C 00 00 0B C1 00 00 02 4C 00 00 00 00 03 E3 00 00 00 00 32 C8 03 E7 04 29 03 38 E5 62 00 00 00 00 03 84 00 00 00 00 09 3D 00 00 0C 09 A0 00 01 90 00 00 A9 00 00 20 00 00 01 BD 0C 2F 03 E8 00 02 9D

The response starts 80 F1 11 (Response, TO F1, from 11 (ECU)

Then 4C = Error (OK) code, and:

21 01 Responses:
# 1	Positive response readDataByLocalIdentifier	61	no
# 2	afterSalesServiceRecordLocalIdentifier	01	no
# 3	Word of equipment 1	08	no
# 4	Word of picking 2	35	no
# 5	The word mode 1	XX	no
# 6	Word of mode 2	XX	no
# 7	Memory word current faults 1	XX	no
# 8	Memory word current faults 2	XX	no
# 9	Memory word current faults 3	XX		no	
# 10	Memory word fault current 4	XX	no

Then things get interesting. The following pairs of bytes (WORDs) give all sorts of info: Throttle position, coolant, air and oil temperature, desired & actual RPM, desired & actual boost pressure, EGR pulse ratio, road speed, cruise control set speed, battery voltage, brake pedal position, MAF flow rate, air pressure and others.

Which byte is which?

Sorry, I’m not going to tell you that! It took ages of getting sensor readings under different conditions – different temperatures, different load conditions, lots of throttle blipping to figure out which reading is the ‘desired’ value and which is the ‘actual’ value for many of the values. Oh, and things like the temperatures being measured in Kelvin instead of degrees (free clue!)

EDIT:

That wasn’t very nice, sorry. The info is all in the Github project XBMC-rpi-service.skin.OBD and some explanation is in my followup post here

Excel PID working out – This .xlsx file has my workings out of how I figured out which byte of the 2101 response was which. It’s not nicely formatted but scroll to the right to see my formulae and adjusted values

2101-on.txt has the original values used to create the above spreadsheet

I seem to have lost the original python code used to generate that .txt file, but it’s pretty straightforward: I just kept sending 2101 requests after setting the header ATSH8111F1. Once I figured out that the values came in byte pairs, I used excel to translate these to numbers, and a knowledge of how the engine works to figure out which data should trend which way.

Back to the Pi

So, how does the Pi work?

The Raspberry Pi is running the Xbian build of XBMC – it’s the fastest by far, and has most bits you’ll need pre-installed.

One thing that got missed out for some reason is libtiff. If you get error messages like the below in your logs (/home/xbian/.xbmc/temp/xbmc.log), you’ll need to install libtiff4.

15:46:20 T:2913350720   DEBUG: Loading: /usr/local/lib/xbmc/system/ImageLib-arm.so
15:46:20 T:2913350720   ERROR: Unable to load /usr/local/lib/xbmc/system/ImageLib-arm.so, reason: libtiff.so.4: cannot open shared object file: No such file or directory
15:46:20 T:2837853248   DEBUG: SECTION:LoadDLL(special://xbmcbin/system/ImageLib-arm.so)
15:46:20 T:2837853248   DEBUG: Loading: /usr/local/lib/xbmc/system/ImageLib-arm.so
15:46:20 T:2837853248   ERROR: Unable to load /usr/local/lib/xbmc/system/ImageLib-arm.so, reason: libtiff.so.4: cannot open shared object file: No such file or directory
^C
[email protected]:/home/xbian# apt-get install libtiff4

I did a lot of work on getting the GPIO working in and out straight from the XBMC skin. I’ll cover that in a separate post.

Bluetooth

Here’s what I did to get bluetooth connecting to the ELM327 scanner:

[email protected]:/home/xbian# apt-get install bluez

[email protected]:/home/xbian# hcitool scan
Scanning ...
        00:01:E3:8B:AE:43       Mark
        00:01:E3:8E:F4:E9       Ant Dect
        00:0D:18:27:FA:E6       Vgate

[email protected]:/home/xbian# cat /etc/bluetooth/rfcomm.conf
#
# RFCOMM configuration file.
#

   rfcomm99 {
       bind yes;
       device 00:0D:18:27:FA:E6;
       channel 1;
       comment "ELM327 based OBD II test tool";
   }

[email protected]:~# cat /etc/udev/rules.d/99-custom.rules 
KERNEL=="rfcomm99", ATTR{address}=="00:0d:18:27:fa:e6", ATTR{channel}=="1", OWNER="xbian", GROUP="dialout", SYMLINK+="elm327"

cat /etc/init.d/elm327
#!/bin/bash
DevNum=99       # DevNum is depending on the rfcom settings /etc/bluetooth/rfcom.cfg
case $1 in
start)
	 rfcomm bind $DevNum
	 ;;
stop)
	 rfcomm release $DevNum
	 ;;
status)
	rfcomm show $DevNum
	;;
*)
	cat&amp;lt;

And some testing:

apt-get install python-serial

screen /dev/rfcomm99 38400

python to start Python.
import serial
ser = serial.Serial('/dev/rfcomm99', 38400, timeout=1)
Nser.write("01 0D \r")
speed_hex = ser.readline().split(' ')
speed = float(int('0x'+speed_hex[3], 0 ))
print 'Speed: ', speed, 'km/h'


See my followup post for more info and links to github projects

Adding Wake-on-LAN to MS RemoteApps

I like RD Web Access and RD GatewayUntitled

If you haven’t read up on it much, RD Gateway allows you to connect to Remote Desktop (either a server or workstation) through a https / SSL tunnel. Once you set things up, the default web interface gives you a login screen, a RemoteApps tab and a Remote Desktop tab.

RemoteApp is brilliant – we’re using it together with App-V for RDS to bring our line-of-business apps out in an accessible but secure way for off-campus staff. Applications run as if they’re running on your desktop, even though they’re tunneling through https, and running on an internal RDS server behind several firewalls.

Of course, you can disable the Remote Desktop tab, and you can limit who can connect and what they connect to through the RD Gateway. We’ve recently been experimenting, adding a few desktops to the security groups so staff can get access to their full desktop. But, of course, we don’t want to leave them powered up all the time. That’s where WOL comes in handy.

The Plan

We’re running SCCM 2007 R3, which has power-management features built in. And does wake-on-lan, but only for advertisements it’s scheduled. I didn’t want to go messing with it by adding entries to the database for it to process. So I went for a more straight-forward approach:

  • User enters computer name, clicks Check Power button
  • Xmlhttp request goes off from the page to the RD web server. A simple ASP page forwards this request to the SCCM Management Server (in a handy place firewall-wise); this sends one ping off to the given computer.
  • Javascript in page processes this response. If power is off, a button is presented to send a Wake-up packet.
  • Wake-up button works in similar way; request is proxied and processed in ASP on the SCCM server. This looks up the computer in the Network_DATA table in the ConfigManager database, gets its MAC address and IP address and sends the packet with an open-source executable.
  • Javascript interval timer checks when the computer starts pinging and updates the status on the page. A timeout after 100 secs tells the user to give up.

The Code

The javascript below was added to Desktops.aspx (c:\Windows\Web\RDWeb\Pages\en-US on the RDWeb server). It should be pretty obvious what it does:

var checkTimeHandle;
function checkPing(){
   var pcName = document.getElementById('MachineName').value;
   xmlhttp=new XMLHttpRequest();
   xmlhttp.open("GET","wol.asp?action=ping&host="+pcName,false);

   xmlhttp.onreadystatechange=function()
   {
      if (xmlhttp.readyState==4) {
         var statEl = document.getElementById('PCstatus');

         if(xmlhttp.responseText=='1'){
            statEl.style.color = "green";
            statEl.innerText = "Switched on";
         }else if(xmlhttp.responseText=='0'){
            statEl.style.color = "red";
            if(statEl.innerText.indexOf('aking up')>-1){
               statEl.innerText += '.';
            }else{
               statEl.innerText = 'Switched Off';
               document.getElementById('wolBtn').style.display='';
            }
         }else{
            statEl.style.color = "red";
            statEl.innerText = "PC Not found. Check spelling.";
         }

         if(statEl.innerText.indexOf('aking up')==-1) document.getElementById('checkBtn').style.display = 'none';
      }
   };
   xmlhttp.send();
}

function sendWOL(){
   var pcName = document.getElementById('MachineName').value;
   var statEl = document.getElementById('PCstatus');
   xmlhttp=new XMLHttpRequest();
   xmlhttp.open("GET","wol.asp?action=wol&host="+pcName,false);

   xmlhttp.onreadystatechange=function(){
      if (xmlhttp.readyState==4) {
         document.getElementById('wolBtn').style.display='none';
         if(xmlhttp.responseText.indexOf('sent')>-1){
            statEl.innerText = 'Waking up';
            document.getElementById('checkBtn').style.display = '';
            checkTimeHandle=setInterval('checkPing()', 3000);
            setTimeout('wolTimeout();', 101000);
         }else{
            statEl.innerText = 'Wake-up failed';
         }
      }
   };
   xmlhttp.send();
}

function wolTimeout(){
   if(checkTimeHandle!='') clearInterval(checkTimeHandle);
   if(document.getElementById('PCstatus').innerText.indexOf('aking up')>-1) document.getElementById("PCstatus").innerText="Wake-up failed. Did you switch off at the wall??";
}

You’ll need to add a couple of buttons after the MachineName input:

<button id="checkBtn" style="display: none;" onclick="jscript:checkPing();return false;" type="button">Check Power</button>
<button id="wolBtn" style="display: none;" onclick="jscript:sendWOL();return false;" type="button">Send Wake-up</button>

…and modify the checkKey function:

function checkKey(obj)
{
   if ((window.event.keyCode == 13) && (obj.value.length >= 1))
   {
      window.event.cancelBubble = true;
      window.event.returnValue = false;
      BtnConnect();
   }
   else if ((window.event.keyCode == 13) && (obj.value.length == 0))
   {
      document.getElementById("ButtonConnect").disabled = true;
      var L_errMsgServerInvalid2_Text = "The server name specified is invalid.";
      var retval = TSMsgBox(L_errMsgServerInvalid2_Text, vbCritical, L_sTitle_Text);
      window.event.cancelBubble = true;
      window.event.returnValue = false;
   }
   else
   {
      document.getElementById('PCstatus').innerText='';
      document.getElementById('checkBtn').style.display='';
      if(checkTimeHandle!='') clearInterval(checkTimeHandle);
      obj.style.color="black";
      document.getElementById("ButtonConnect").disabled = false;
      window.event.cancelBubble = true;
   }
}

Of course, the above isn’t perfect. Just a proof of concept, really. Use at your own risk!

As for the actual pinging and WOLing, here’s the back-end script. Note that this is designed to be called with an XMLHTTP request that passes a username and password (for DB connection and execute permissions), as well as filtering the incoming text at bit more.

<%
Dim sIP, oShell, oExec, sCommand, sOutput
sIP = request.querystring("host")
if instr(sIP, " ")>0 or instr(sIP, "'")>0 or instr(sIP, "^")>0 then response.end

action = request.querystring("action")

if action = "ping" then
    sCommand = "%comspec% /c @echo off & for /f ""tokens=7"" %q in ('ping -n 1 -w 25 " & sIP & "^|find /i ""Received =""') do echo %q"
    Set oShell = CreateObject("WScript.Shell")
    Set oExec = oShell.Exec(sCommand)
    sOutput = left(oExec.StdOut.ReadAll, 1)
elseif action = "wol" then
        strConnection = "Driver={SQL Server};Server=localhost;Database=SMS_UOB;"

        Set conn = Server.CreateObject("ADODB.Connection")
        conn.Open strConnection

        Set rs = Server.CreateObject("ADODB.recordset")
        strSQL = "SELECT * FROM Network_DATA WHERE IPAddress0 != 'NULL' AND DNSHostName00 = '"&sIP&"'"
        rs.open strSQL, conn, 3,3

        rs.MoveFirst
        WHILE NOT rs.EOF
            mac = rs("MACAddress0")
            if instr(rs("IPAddress0"), ",")>0 then
                ip = split(rs("IPAddress0"), ",")(0)
            else
                ip = rs("IPAddress0")
            end if
            rs.MoveNext
        WEND

        if mac <> "" then
            sCommand = "C:\inetpub\wwwroot\wol\wolcmd.exe "&mac&" "&ip&" 255.255.255.0"
            Set oShell = CreateObject("WScript.Shell")
            Set oExec = oShell.Exec(sCommand)
            op = sCommand & vbcrlf & oExec.StdOut.ReadAll
            if instr(op, "sent")>0 then sOutput = "WOL sent"
        end if
end if

response.write sOutput
%>

And there it is; users can log in and easily wake their computer from anywhere, on the same page as the remote desktop gateway link. I won’t give you the code to the ‘wol.asp’ page – it just filters inputs and passes a GET request to the sccm server. I will recommend that you add something like the following to the top, though:

if(Request.ServerVariables("REMOTE_USER") = "") then
   response.redirect "simpleLogin.aspx"
   response.end
end if

Windows 7 x64 printing through a 32-bit print server 0x000007e error

This week, we bit the bullet and installed Windows 7 on all 100 machines in the university library. Of course, the sensible thing to do would be to test everything works first. But where’s the fun in that?

So, 64-bit Windows 7 installed, we checked all was okay. Oops… x64 windows needs x64 printer drivers. That wasn’t a problem, and most printers were working within an hour or so. However, some of the HP printers that are in the library, particularly the HP colour laserjet 9500, proved more troublesome.
64-bit drivers added to the Server 2003 x86 server, the client machines would download the driver (we were using the HP Universal print driver PCL 6) but then fail with the following error when trying to connect to the printer:

Windows cannot connect to the printer.
Operation failed with error 0x0000007e.

So we tried PCL5. No luck. None of the HP drivers would work. Googling the error showed lots of people in the same position – with 64-bit Windows Vista or Windows 7 and a 32-bit print server. The only suggested workaround is adding the printer manually to a workstation as a local printer, and picking up the driver from a CD or download that way (not from the print server).

So… in comes procmon. Not long before the dreaded error message, spoolsv.exe looks in a registry key HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print\Providers\Client Side Rendering Print Provider\Servers\[SERVERNAME]\Printers\{0000-guid-of-printer-00}\CopyFiles\BIDI\Module that tells it to go find (system32) spool\DRIVERS\W32X86\3\hpcpn081.dll. This doesn’t exist, hence the ‘module not found’ error 7e. The driver installation does, interestingly, copy a newer hpcpn104.dll into this directory. Getting warmer!

Copying hpcpn081.dll from \\PRINTSERVER\print$\x64\3 to c:\windows\system32\spool\drivers\w32x86\3… printer installs successfully! So, why is it copying the wrong dll? Well, a short dig through the server’s registry reveals a key similar to the one on the client, in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print\Printers\[PRINTERNAME]\CopyFiles\BIDI. The “Module” value points to spool\DRIVERS\W32X86\3\hpcpn104.dll. Change this to hpcpn081.dll and hey presto, all is fixed.

No having to deploy drivers to all machines or send the desktop teams round… just tweak this value for each of your stubborn printers!

Just go to your print server and change

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print\Printers\[PRINTERNAME]\CopyFiles\BIDI\Module

from hpcpn104.dll to hpcpn081.dll