Dash mounted phone instrument cluster

Tuesday, November 3, 2020

The idea and the plan

About 2 weeks before starting this log, I decided that having gauges on the main display (I call main display the one and only stock screen in the Model 3) wasn't really great for 2 main reasons:

  1. It's just not a convenient location for glancing at real time data while driving. The left-hand column of the display is okay-ish, but further to the right, it's really not a good idea to be staring at while in motion.

  2. The browser window hides the awesome map, which is super useful almost all the time. Not to mention my family complaining about my "things" hiding the map.

So in the spirit of other projects that mount screens behind the steering wheel, I set out to build a quick and dirty proof of concept. Before going too far down the rabbit hole, I wanted to make sure I actually liked the location for gauge display, and that the general size would work.

I toyed with lots of ideas, and finally settled on this as the plan for the PoC:

  1. I'll use my phone as a display. This will make it easy as I don't have to go buy anything, and I don't have to muck around with under powered RPI-based solutions.

  2. The display will be mounted to the steering wheel column, on the same plane as the main display, and at the same tilt angle. I'll 3d print a rough mount, and fasten it on the shroud with 2 sided tape.

  3. For software, again to go fast, I'll just use my existing Onyx M2 Dashboard project. The app is now based on React, and should be easy to make a new component to display as an instrument cluster.

  4. I'll host the new React web component in a fullscreen, always-on WebView from my existing Onyx M2 Mobile App software (it's Android).

  5. I'll just use my existing Web Socket server CANbus link (a no development choice), but eventually I'll build a direct-to-phone-by-bluetooth option in the app so I don't need a cloud round trip.

Building the mount

I started by designing the simplest mount I could think of that would have a good contact patch with the steering shroud, and allow my to slip my phone in when I'm driving. It looked like this:

Image 1

I eye-balled the height, and exported to STL for printing. I went mostly stock setting for my Ender 3, but setting the infill to 20%. I wasn't sure how stiff I needed to make this thing, and whether it would be able to take the weight on the phone being sloshed around.

Image 2

Roughly 8 hours later, my car had a mount ready for my phone to be slid into position behind the steering wheel.

Image 3

Modifying the mobile app

While the mount was printing, I got to work on the software. First order of business was to add an Instrument Cluster menu option in https://github.com/onyx-m2/onyx-m2-mobile-app. I decided to make this a separate activity. Wiring it to the menu was as simple as adding this to the menu handler.

case R.id.action_instrument_cluster: {
    Intent intent = new Intent(this, InstrumentClusterActivity.class);
    startActivity(intent);
    return true;
}

The activity will be a straight up web app, so the activity window itself is super simple.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="<http://schemas.android.com/apk/res/android>"
    xmlns:app="<http://schemas.android.com/apk/res-auto>"
    xmlns:tools="<http://schemas.android.com/tools>"
    android:id="@+id/frameLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000"
    android:keepScreenOn="true"
    tools:context=".ui.InstrumentClusterActivity">

    <WebView
        android:id="@+id/fullscreen_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

The only 'special' setting is android:keepScreenOn="true", which is all you need to have Android keep the screen on while your activity is in foreground. Pretty cool that this just works.

The code itself needed to do a few simple tasks, all doable from onCreate: setup for full screen display (i.e zoomed and no phone status information) , give the web view the permissions needed to run the React web app, and load it up.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_instrument_cluster);
    WebView webView = (WebView)findViewById(R.id.fullscreen_content);
    webView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
            | View.SYSTEM_UI_FLAG_FULLSCREEN
            | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
    WebSettings settings = webView.getSettings();
    settings.setJavaScriptEnabled(true);
    settings.setDomStorageEnabled(true);
    webView.loadUrl("https://onyx-m2-dashboard.net/eic");
}

(The /eic is for electronic instrument cluster.) With this in place, I was ready to start writing some React code to actually make an instrument cluster.

Creating the instrument cluster software

At this point, I wasn't really sure what I wanted in the display, but I had a million different ideas. What's really nice with this setup is that I can try stuff super easily and just drive around with it for a couple of days and see if I like it.

At any rate, I started with a simple two dial design. I wasn't consciously design to look like anything, but looking at the result, I realized that I basically created an improved version of my old BMW's gauge cluster. 😂

Here's what the first version looked like.

Image 4

So basically,

  1. Left dial for speed and odometer as a secondary value. The colour does nothing for now, but the idea is to read the speed limit and colour-code the various states of being "over the limit". I've lots of ideas of cool things to add to this dial, which I'll get to later.

  2. Right dial is drive or regen power, goes green for regen, and secondary is consumption. I haven't built a trip computer yet, but the thought is to display trip consumption here.

  3. Lower-right corner is battery soc. It changes colour to yellow and red like the car's main display.

  4. Tire pressure display is as shown. The eventual plan is to make it possible to select different data to be display here, possibly scolling through them with the right steering wheel scroll button.

  5. The center part of the display has indicator lights. Initially these were:

    1. Brake light. I hate not knowing when my brake light turns during "regen braking". Now I don't have to guess (and it not always how I though the car did it).

    2. ESP light, because, race car. 😬

    3. Autopilot light, with a nice little feature that turns it yellow in real time when the car thinks you don't have your hands on the wheel. This is pretty to figure out how to hold the wheel so as not to get warnings.

    4. Turn signals

The code for all this is in a separate branch at https://github.com/onyx-m2/onyx-m2-dashboard/tree/eic until I clean it up a little. It was actually pretty gratifying how easy it was to build this using my Onyx M2 Project.

Writing this stuff in React was a pleasure, and I ended up with nice modularity. For example, the Autopilot indicator looks like this:

export default function AutopilotIndicator(props) {
  const theme = useContext(ThemeContext)
  const [state, states] = useNamedValuesSignalState('DAS_autopilotState', 'SNA')
  const [handsOnState, handsOnStates] = useNamedValuesSignalState('DAS_autopilotHandsOnState', 'SNA')

  // if ap is not active, or there's no signal from the hands on detection, don't light
  // up the indicator
  var visible = true
  if (state == states.SNA || state == states.DISABLED || state == states.UNAVAILABLE) {
    visible = false
  }

  var color = theme.indicator.grey
  if (state == states.ACTIVE_NOMINAL) {
    switch (handsOnState) {
      case handsOnStates.SNA:
      case handsOnStates.REQD_DETECTED:
      case handsOnStates.NOT_REQD:
        color = theme.indicator.blue
        break

      case handsOnStates.REQD_NOT_DETECTED:
        color = theme.indicator.yellow
        break

      case handsOnStates.REQD_VISUAL:
        color = theme.indicator.orange
        break

      case handsOnStates.REQD_CHIME_1:
      case handsOnStates.REQD_CHIME_2:
      case handsOnStates.REQD_SLOWING:
      case handsOnStates.REQD_STRUCK_OUT:
      case handsOnStates.SUSPENDED:
        color = theme.indicator.red
        break
    }
  }
  return (
    <FadeableComponent {...props} visible={visible}>
      <SteeringWheelIcon width="100%" height="100%" fill={color} />
    </FadeableComponent>
  )
}

Super happy how writing gauges and indicators turned out, and I look forward to greatly expanding what I do with this.

Testing all the pieces together

At this point, I was ready to slide the phone into the mount and go for a drive. So I did. And somewhat to my surprise, I just worked. I had a very usable instrument cluster.

Here's how it looks in the car.

Image 5
Image 6

My first reaction (after my initial amazement that it actually worked) was how beautiful the screen was. My artwork aside, a high-res AMOLED display is sooooo good in this application. I wish the main Tesla display was this good!

The next thing ah-ah moment was the realization of how good it was to have gauges behind the steering wheel again. I've been driving a Model 3 for a year and a half, and hadn't really missed a more traditional cluster. But now I'm never going back!

So this project will probably continue...

© 2021 John McCalla