The v3 mount looks good, but I wasn't happy with fit and finish of the cover, or the surface look of the mount itself. I ended up printing a new cover the is slightly wider and deeper to attempt get rid of any play (and potential rattles). The new cover is an improvement, but still isn't perfect. Having a single screw holding it from the middle probably won't work long term, but I'm going to try it before going for a more permanent adhesion solution. I did line the charger pocket with fuzzy tape to help with rattling and the irregular shape of the charger's top side.
I also hit everything with an additional round of sanding. I usually sand my PLA projects with 200 grit, and then finish with 400. I don't usually paint my prints. After sanding I wash them down with soap, and dry. Then I apply vaseline as a finish. Cheap and easy, and ends up looking pretty decent. Here's how the final, assembled, and tested, mount looks, just prior to installing in the car.
My first thought was to use the open power pin on the white connector in the driver's foot well. Apparently, Tesla starting using that (making it unavailable to me) late 2019. Unfortunately, my June 2019 car has the pin occupied (apparently used to power the tow hitch). The next best option was to tap the wire that feeds power to the 12v plug in the center console. This is the first time I do something permanent (and not reversible) to this car, so I was a little nervous. I've done this many times in the past, but I've never had a computer on wheels for a car before.
The taps I used
The tap is in, the rectangle is where the connector will insert
To be able to tap cleanly, I wanted to use a t-tap with a spade connector. It's a little tricky because the wire I'm tapping to a much larger gauge than the one I'm connecting. So I used a blue (16 awg) tap and a red connector (20 awg).
The plan for ground is to use the screw just about the push clip on the trim panel. I'll use a ring connector for this, and again, I have to use a huge yellow one to clear the screw, even though the wire I'm connecting is a much smaller gauge. To even be able to crimp it I had the trim away the plastic shroud. I'm not super proud of the results, but it'll do.
The positive and ground wires. Forgot the shrink wrap on the red, and used one too small on the black. Good grief!
Close up of tap connector in
Both wires connected
Shout out to Tesla Owners Online, the monster thread on tapping into power sources on the Model 3 was super useful!
Here's how to safely tap 12v power for add-ons - Tesla Owners Online
Before connecting all of the above the above, I had to figure out how to get the cable from the steering wheel to the tap/ground area. I first tried just shoving it in the steering wheel gap with the dash. That didn't work, it snagged. I then got a larger, stiffer cable to do the fishing. Still didn't work, but going in the other direction (from side to steering column) did! So I taped the power supply cable to the "fish" wire and pulled it through. I was also able to route it neatly down the side as shown in the pictures. Overall, pretty painless. Didn't need to pull the wood dash.
Fishing the cable. In right, tape to cable, and pull left through
Routing the cable down from the dash
All buttoned up
Side note: removing the v1 mount was really easy, and didn't leave any adhesive residue at all! Really happy about that, I was a little apprehensive, as I know 3M double sided tape creates a really strong bond. I just used a section of fishing wire, slid it under, and pulled the whole thing through. No heating required.
Obviously before buttoning everything up, I checked that power was being delivered properly. I used my Kindle, as it was one hand. No issue, it charged just fine!
But of course, it can't be that easy! When I tested with the real mount, it didn't work. Damn it! In the end, it's the charger that just stopped working. Grr. I contacted the manufacturer, and they just reimbursed me. I had not asked for that, but rather that they help me diagnose the problem and fix it. Not interested, I guess. So, I have another one on order, hoping mine was just a dud (but there is a comment from someone else having the same issue). If that doesn't work, I'll have to source a different one and re-design the mount, obviously something I'm trying to avoid.
So in the meantime, I guess it's back to software...
There's lots I want to improve about the cluster itself, and I'll get to that once I'm done with all this mounting business. There is one mounting related thing I'd like to do in software though: automatically starting the cluster app when I slide it into the mount. Convenience is the whole point of this thing, and having to tap icons to start an app isn't.
My first ideas where really complicated, involving looking at location and specific CAN signals to figure out if my phone is in the car. After way too long, I realized there's a much simpler solution: the dash mount is the only place I charge my phone horizontally! So, if I detect that wireless charging has started, and the phone is in landscape mode, launch the cluster. Easy.
// Listen for battery notifications to detect when the phone starts wireless charging.
// This, vombine with checking that the phone is in landscape mode, automates starting the
// instrument cluster when the phone is placed into the car mount holder (in all my other
// wireless chargers, the phone sits upright).
private boolean activateOnNextBatteryChange = false;
private BroadcastReceiver batteryBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Battery broadcast receive");
String action = intent.getAction();
if (action != null) {
if (action.equals(Intent.ACTION_POWER_CONNECTED)) {
Log.d(TAG, "Power connected");
activateOnNextBatteryChange = true;
}
else if (action.equals(Intent.ACTION_BATTERY_CHANGED) && activateOnNextBatteryChange) {
activateOnNextBatteryChange = false;
int pluggedState = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
if (pluggedState == BatteryManager.BATTERY_PLUGGED_WIRELESS) {
Log.d(TAG, "Plugged into wireless");
Display display = ((WindowManager)getSystemService(WINDOW_SERVICE)).getDefaultDisplay();
int rotation = display.getRotation();
if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
showInstrumentCluster();
}
}
}
}
}
}
Of course, it's not that easy. Ever.
The above code works, kinda, but there a number of issues:
You can't launch an activity in Android from the background anymore. Ugh. I get that Google wants to limit disruptions developers can cause, but my phone , my app - just let me know do it already!
If the phone is locked, this doesn't work as the cluster will only be shown once I unlock the phone, which is an extra step, and what I'm trying to get rid of by automating.
If the phone is on a screen that's portrait locked when it's inserted into the mount, it'll report ROTATION_0, not the true orientation. For that, I'll have to use the physical sensors. This is annoying because it's more work (and math), but also because I can't just ask for sensor values, I have to subscribe to sensor events, which is wasteful extra processing. On the plus side, I'll be able to differentiate between the phone lying flat versus upright (like it is in the mount).
In the end, I got it all working as I explain below, but it's really not an elegant solution, and was very finicky to get working.
I found a workaround to problem (1): full screen intent notifications. I can basically launch my app by creating a high-priority "full screen" notification, which under certain conditions, will do what I want. Here's the code that does just that.
void showInstrumentCluster() {
NotificationManager manager = ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE));
if (manager == null) {
Log.e(TAG, "Notification manager not available");
return;
}
Intent fullScreenIntent = new Intent(this, InstrumentClusterActivity.class);
PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(this, INSTRUMENT_CLUSTER_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_swap_horiz_black_24dp)
.setContentTitle("Onyx M2")
.setContentTitle("Instrument Cluster")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setFullScreenIntent(fullScreenPendingIntent, true)
.build();
manager.notify(INSTRUMENT_CLUSTER_NOTIFICATION_ID, notification);
}
The "certain conditions" parts means that it only launches directly into my app if Android reckons I'm not "actively using" my phone. In practice, this means I need to "turn it off" before inserting it to get the desired effect. If I don't, I get a popup instead that I must tap to start the cluster. Not a terrible solution.
Of course, the above is kinda useless if the phone locks itself (which mine does - does anyone still use an unlocked phone? I hope not!). Android does have some controls that sorta-kinda can be made to work to achieve what I want:
Flag my activity to allow showing its contents even if the phone is locked
Turning on the screen (which may be off, in my use case) when started
Getting rid of the screen that asks for lock screen pass code
The code is pretty simple, at the end of the day, but figuring out this is what needed to be done wasn't. Also, it only works if neither the full screen notification or the activity itself are "open" - so I'll have to add something that kills the app when you navigate away, and ditto for the notification.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_instrument_cluster);
setShowWhenLocked(true);
setTurnScreenOn(true);
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
if (keyguardManager != null) {
keyguardManager.requestDismissKeyguard(this, null);
}
// ... other stuff
}
Also, the above, while it works, there is a slight flicker of part of the key guard (usually the 1 key is drawn) as the phone is switching to landscape mode, before my app has cleared the screen. Acceptable, but not perfect - this is kinda becoming a theme of this project. 😥
To solve the notification "sticking" issue, I tried just cancelling it once the cluster activity started (in onCreate()), and for once, that just worked.
// dismiss the notification that probably started the activity, as the fullscreen
// intent is being used a vehicle to launch the activity from the background, it's not
// a real notification, and should never be visible as such
NotificationManager manager = ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE));
if (manager != null) {
manager.cancel(RelayService.INSTRUMENT_CLUSTER_NOTIFICATION_ID);
}
To make the instrument cluster go away as soon as the phone is turned off, or taken out of the holder, it's as easy as calling finish() on the activity. However, doing this keeps it in the recent apps lists, which I don't want. But thankfully, there's a finishAndRemoveTask() that does what I want.
Now, there are a couple of ways you could want to "navigate away" from the app.
Turn off the phone
Swipe to "go back"
Pull out of the holder
To implement (3), I have to send some sort of message to the activity to kill itself. And I don't have that construct right now. The service currently uses a Messenger to talk to the main activity, but this isn't a great construct for working with multiple activities (only one can be bound at a time). It also occurs to me that I might need to actually use the phone while it's in the holder, and as such, it would be better if the notification stayed "active" for the duration of it being in the holder. So, instead of cancelling the notification from the activity's onCreate(), I'll have the relay service do that when it detects that the phone isn't charging anymore.
And here's where it all blew up. Conceptually, I want to have the service maintain the "in the holder" state, and have the activity be able to observe this value and when it sees it's no longer in the holder, it calls finish().
So I need to change to using something Android calls LiveData. It's a really good abstraction, but I'll have to retrofit the main activity's communications with the service. LivedData is also activity life cycle aware, so it'll do the right thing based on the state of the activity, which simplifies cleanup and is more robust.
Side note: during all of this, the data coming from the car started acting really weird, unreliable, and slow. I spent hours reverting most of my changes trying to figure what broke. Nothing fixed it. Turns out, the damn SD CARD WAS FULL. I thought my M2 firmware dealt with that case, but apparently not. It still freaks me out when data stops flowing, and I doubt everything I've done!
So the above plan worked out pretty well. I ended up, after a lot of trial and error, with the service maintaining the "in holder" state, and the activity responding to changes by killing itself if the phone is removed from the holder (and I added a check that the activity was started by being inserted into the holder, or it would misbehave when started by tapping the icon in the launcher).
relayService.getInHolder().observe(InstrumentClusterActivity.this, value -> {
Log.d(TAG, "InHolder Changed: " + value);
if (!value) {
String action = getIntent().getAction();
if (action != null && action.equals("onyx.intent.action.IN_HOLDER")) {
finishAndRemoveTask();
}
}
}
While I was at it, I refactored the communication between the main configuration activity and the service to use LiveData also. (This is for display the "connection" state in the main activity.)
It occurs to me I haven't shown this screen yet. This is how I configure the "Onyx M2" device with Bluetooth to talk to the right server, and setup home wifi and optionally hotspot wifi. It can be reconfigured at any time by hitting the sync button at the bottom.
The screen above shows that M2 is talking to the phone, and the phone is talking to the cloud, so the car <> cloud link is up and ready to go. When the car is out of range or asleep, it looks like the image on the left.
I decided to defer this work, as I figured out that the way I launch the instrument cluster doesn't do anything if it's past my bedtime (because I have bedtime mode on), so charging in my bedroom just works without having to worry about the position of the phone.
I finally received my replacement charger, and it works. The software is ready-ish. So I'm good to go! Connecting the power to the charger, threading it through the base went fine, and I used double-sided tape to fasten the entire assembly. The only thing that's not perfect is that the new charger is not as strong as the previous one, so it doesn't always start charging - sometimes I need to apply a little pressure to the back to make it go. This I'll address by mounting it closer to the back panel (I have a spacer there now, because I could and thought it would be good to prevent over-heating). But otherwise, it's so cool to slide it in and see it light up, and then go away as soon as I slide it back out! Super happy!
From the driver's seat
From passenger seat
Without the phone
Looking in from the driver side
Profile view
From the front windshield