Category: C
Mar 28, 2024

There are a lot of great ways to get Bluetooth in your car these days; aftermarket head units, FM transmitters, battery-powered Bluetooth cassettes, dongles that go into the aux port, and even amplifiers with built-in Bluetooth are everywhere. New head units cost less than a pack of toilet paper. But what if you’re going for an aesthetic? What if you’re willing to dump inexplicable amounts of time, effort, and money into a bespoke solution that’s only slightly better?

custom circuit board
You know I’m down.

I recently bought a rust-free ’87 Civic as a project car, and since I don’t actually need it for anything I’ve just been having fun going overboard on repairs, maintenance, and cleaning. When it came time to add Bluetooth, it was a given that I’d keep the cool 80s radio, but rather than recharge a fake cassette tape or have a wart in the cigarette lighter, I decided on a no-compromises approach instead.

My first custom PCB, the Civic Radio Modernizer 3000, provides the following features:

  • Retains all original functionality except radio station presets
  • Remaps preset buttons 1, 2, and 3 to Radio, Tape, and Bluetooth
  • Remaps preset buttons 4, 5, and 6 to Previous, Play/Pause, and Next
  • Adds funky programmable light shows to the preset button LEDs
  • Adds RCA and 3.5mm outputs suitable for use with aftermarket amplifiers
  • Provides a hookup for steering wheel audio controls
  • Connects to a yet-to-be designed hub board for control and logging

Who needs docs?

The first challenge in adding Bluetooth to any factory radio is knowing where to solder. You’ve got to figure out both how audio gets to the amplifier, and how the radio switches its inputs.

Notably, every YouTube video I found stops short of the second point and just shows splicing a Bluetooth audio module into the cassette’s output. If you need to fool the cassette deck into activating with a fake tape anyway, they make Bluetooth cassettes.

I recommend getting a service manual for this step, but if you can’t, a multimeter and some intuition work too. In case you just happen to have a Honda 39100-SEO-A300-M1, these two pins on the bottom are the audio output just before the amplifier (Left channel toward back of radio, Right channel toward front):

Photo of bottom of radio, circuit board exposed

And this plug on the cassette unit is the input switcher. Gray (B+) is either shorted to Blue (B+RADIO) for radio or Purple (B+TAPE) for tape. If you leave Gray open, no sound from either goes through–a convenient third state just asking for Bluetooth!

Audio input selector plug with red arrow pointing at it

Aside from those points, I just wanted 12V accessory, GND, then a lead off each of the six buttons and LEDs that make up the radio preset buttons, for a total of 19 wires. That fits comfortably on a DB-25, so I just chopped a printer cable in half, drilled a hole in the radio, and soldered those in at the correct points.

Choosing a Bluetooth module

This is one part you want to get right, and something there’s a lot of conflicting information on. You can get cheap Bluetooth modules off AliExpress no problem, but even if you don’t care about sound quality a lot of them have horrendous startup and connection sounds. Take a listen to these train wrecks:

I decided to go with an AudioB module from TinySine Audio. They have working contact information, documentation, helpful tutorials, and pleasantly unobtrusive sounds that you can even overwrite. Plus for such a good product, they’re downright inexpensive.

Designing a circuit board

If there’s one thing I’ve learned from this project, it’s that the barrier to entry to designing circuit boards is far, far lower than I ever imagined. Sure I’ve been inundated for years with sponsor spots for PCBWay telling me how simple it is, but I never really believed it.

KiCad is a super easy-to-use yet powerful PCB design program that’s completely free and amazingly easy to learn. I recommend this 13-minute video by Mr.T’s Design Graveyard. I’d never used any PCB design software in my life and all it took was watching that video. I’d always imagined designing your own circuit board was some mythical height of technological ability, but no exaggeration, if you know some basic electronics you can be designing circuit boards today. Nobody can stop you.

For this board I just needed a way to read button inputs, light up LEDs, reroute the Gray (B+) wire, and short a low voltage output on the audio module to one of three inputs for playback controls. An Arduino Nano Every with some supporting hardware seemed like a pretty obvious choice.

Now my degree is in English, not lightning, so I apologize to any electrical engineers out there, but basically here’s the schematic in KiCad (project on Github):

an incredibly amateurish schematic
I’m sure there are rules and standards, but it’s too late now!

And then from that schematic it generates a board layout and you play connect-the-dots until you’re ready to export your board files and send them off to somewhere like PCBWay be produced. Go give Mouser Electronics all your money for the components, cry, and you’re set!

Here it is operating on the bench after being soldered up:



I do love overkill.

With a 3D printed case slapped on, this unit is now tucked happily under the dash bringing today’s tech to a radio from long ago. I couldn’t be happier with how boring it looks.

Car radio in dashboard

Should you do it? Hell no. Making boards is surprisingly cheap all things considered, but it’s not that cheap. This board costs $150 each to make (and no, I didn’t get it right on my first try). While I could have added a little more wiring to reduce my reliance on relays and cut down on component costs some, we’re talking maybe a $20 difference. If all you want is Spotify in your car, this is a terrible idea. I rarely even listen to music in the car, so who knows what I was thinking.

That said, I learned things and had a lot of fun. It certainly won’t be the last board I design.

Jan 5, 2017

Normally in web application development, clicks are a matter of listeners. Either a click hit a thing or it didn’t. When using the HTML5 canvas, this isn’t the case. Images and features are rendered onto it, but you can’t attach a listener to them. Instead, you monitor where these elements visually reside versus where clicks land.

If an object is rectangular and will never rotate, the solution is easy. If click X is between object start X and object end X, and click Y likewise, then it hit. If not, it didn’t.

In this post we’ll construct a simple series of functions for determining the same thing if the rectangle has rotated, then look at how the idea can apply to more complex shapes. Nothing new or groundbreaking, and nothing language-specific — consider this a StackOverflow answer.

Visualization

For a developer who hasn’t mathed in a while, this can seem tricky at first. Once you see a few images it’s perfectly sensible.

We’ll be drawing four triangles. Each one consists of the click point and two vertices of the object. We then compare the area of the object to the combined area of the triangles. If they’re the same, the click had to be in the object. If the triangles are larger, the click couldn’t have been in the object.

Rotated Rectangle - Click hit
If the click was inside, the areas are equal.
Rotated Rectangle - Click miss
If the click was outside, the combined area of the triangles is bigger than the area of the rectangle.

How much it’s rotated doesn’t matter. Where it’s at doesn’t matter. That’s literally all there is to this.

First things first

To figure out if the click hit, we’ll need four pieces of data:

  • The coordinates of the click
  • The position of the rectangle
  • The size of the rectangle
  • The angle of rotation of the rectangle

Those last three pieces we’re just putting together so we can find the vertices. Once we know the click position and the vertex positions, figuring out if the click hit is easy.

Finding the vertices

Since you’re probably not tracking the individual locations of the vertices and are instead tracking the position of the object, you’ll want to start with a function that can find these by the object’s size and position. How this will be done is hugely variable depending on where you’re measuring from (tracking left top? center?) and if/how the object has been scaled.

If we pretend like there’s no scaling or rotation and we’re measuring from left and top, the function may initially look like this:

<pre class="CodeMirror cm-s-monokai" data-setting="%1$s">// Find vertices before taking rotation into account
function findRectVertices(position, size) {
	var left = position[0];
	var right = position[0] + size[0];
	var top = position[1];
	var bottom = position[1] + size[1];
	
	return {
		LT: [ left, top ],
		RT: [ right, top ],
		RB: [ right, bottom ],
		LB: [ left, bottom ]
	};
}</pre>

Since this doesn’t take into account rotation yet, we’ll need a utility function to find a point’s new location after it’s been rotated by X degrees around another point:

<pre class="CodeMirror cm-s-monokai" data-setting="%1$s">/**
* Find point after rotation around another point by X degrees
*
* @param {Array} point The point to be rotated [X,Y]
* @param {Array} rotationCenterPoint The point that should be rotated around [X,Y]
* @param {Number} degrees The degrees to rotate the point
* @return {Array} Returns point after rotation [X,Y]
*/
function rotatePoint(point, rotationCenterPoint, degrees) {
	// Using radians for this formula
	var radians = degrees * Math.PI / 180;

	// Translate the plane on which rotation is occurring.
	// We want to rotate around 0,0. We'll add these back later.
	point[0] -= rotationCenterPoint[0];
	point[1] -= rotationCenterPoint[1];

	// Perform the rotation
	var newPoint = [];
	newPoint[0] = point[0] * Math.cos(radians) - point[1] * Math.sin(radians);
	newPoint[1] = point[0] * Math.sin(radians) + point[1] * Math.cos(radians);

	// Translate the plane back to where it was.
	newPoint[0] += rotationCenterPoint[0];
	newPoint[1] += rotationCenterPoint[1];

	return newPoint;
}</pre>

Modifying our previous findRectVertices  function to also use rotation may then look like this, assuming we’re rotating objects around their center:

<pre class="CodeMirror cm-s-monokai" data-setting="%1$s">/**
* Find the vertices of a rotating rectangle
*
* @param {Array} position From left, top [X,Y]
* @param {Array} size Lengths [X,Y]
* @param {Number} degrees Degrees rotated around center
* @return {Object} Arrays LT, RT, RB, LB [X,Y]
*/
function findRectVertices(position, size, degrees) {
	var left = position[0];
	var right = position[0] + size[0];
	var top = position[1];
	var bottom = position[1] + size[1];

	var center = [ right - left, bottom - top ];
	var LT = [ left, top ];
	var RT = [ right, top ];
	var RB = [ right, bottom ];
	var LB = [ left, bottom ];

	return {
		LT: rotatePoint(LT, center, degrees),
		RT: rotatePoint(RT, center, degrees),
		RB: rotatePoint(RB, center, degrees),
		LB: rotatePoint(LB, center, degrees)
	};
}</pre>

Calculating the areas

Now that we have all the necessary points, all that’s left are three things:

  • Calculate the area of the rectangle
  • Calculate the area of the triangles
  • Compare the two

If you recall high school geometry, point two is the loaded one there. To find the area of an unknown triangle we’d use Heron’s formula, but first the distances of each side will be needed. The two utility functions may look like this:

<pre class="CodeMirror cm-s-monokai" data-setting="%1$s">/**
* Distance formula
*
* @param {Array} p1 First point [X,Y]
* @param {Array} p2 Second point [X,Y]
* @return {Number} Returns distance between points
*/
function distance(p1, p2) {
	return Math.sqrt( Math.pow(p1[0] - p2[0], 2) + Math.pow(p1[1] - p2[1], 2) );
}

/**
* Heron's formula (triangle area)
*
* @param {Number} d1 Distance, side 1
* @param {Number} d2 Distance, side 2
* @param {Number} d3 Distance, side 3
* @return {Number} Returns area of triangle
*/
function triangleArea(d1, d2, d3) {
	// See https://en.wikipedia.org/wiki/Heron's_formula
	var s = (d1 + d2 + d3) / 2;
	return Math.sqrt(s * (s - d1) * (s - d2) * (s - d3));
}</pre>

With those functions ready, we can now write a complete function that performs the three bullet points and returns a simple True or False:

<pre class="CodeMirror cm-s-monokai" data-setting="%1$s">/**
* Determine if a click hit a rotated rectangle
*
* @param {Array} click Click position [X,Y]
* @param {Array} position Rect from left, top [X,Y]
* @param {Array} size Rect size as lengths [X,Y]
* @param {Number} degrees Degrees rotated around center
* @return {Boolean} Returns true if hit, false if miss
*/
function clickHit(click, position, size, degrees) {
	// Find the area of the rectangle
	// Round to avoid small JS math differences
	var rectArea = Math.round(size[0] * size[1]);

	// Find the vertices
	var vertices = findRectVertices(position, size, degrees);

	// Create an array of the areas of the four triangles
	var triArea = [
		// Click, LT, RT
		triangleArea(
			distance(click, vertices.LT),
			distance(vertices.LT, vertices.RT),
			distance(vertices.RT, click)
		),
		// Click, RT, RB
		triangleArea(
			distance(click, vertices.RT),
			distance(vertices.RT, vertices.RB),
			distance(vertices.RB, click)
		),
		// Click, RB, LB
		triangleArea(
			distance(click, vertices.RB),
			distance(vertices.RB, vertices.LB),
			distance(vertices.LB, click)
		),
		// Click, LB, LT
		triangleArea(
			distance(click, vertices.LB),
			distance(vertices.LB, vertices.LT),
			distance(vertices.LT, click)
		)
	];

	// Reduce this array with a sum function
	// Round to avoid small JS math differences
	triArea = Math.round(triArea.reduce(function(a,b) { return a + b; }, 0));

	// Finally do that simple thing we visualized earlier
	if (triArea > rectArea) {
		return true;
	}
	return false;
}</pre>

Common issues

By far the most frequent annoyance with this approach when working in the DOM is going to be getting good position numbers. So many variables can throw a wrench in how you’re measuring where the object is. If your reported object position doesn’t match up with the reported mouse or touch position, all of the above is useless.

Look for situations in which clicking near something instead of on it reports a hit, then try to identify elements that match the size of the inaccuracy. Everything from margins on the HTML or body elements, borders on divs, document scroll position, and box-sizing settings can play havoc on your ability to get clean measurements that match up with what’s plainly visible.

Going farther

This method isn’t just good for rectangles. It would be straightforward to apply to triangles, too, or for any other convex polygon. Consider how you might implement the ability to check if a click landed inside a hexagon. As a brain teaser, what would happen on a concave polygon?

Jul 20, 2016

While working on a new revision of a project, I moved from an Arduino Uno to an Arduino Mega. In theory these devices should handle SPI in the same manner, just with different pins. While MOSI, MISO, and SCK are on pins 11, 12, and 13 respectively on the Uno, those change to 51, 50, […]

Continue reading...
Sep 10, 2015

I was recently commissioned to build a machine that would perform a very interesting and unusual task: Count how many times a wire had been wound around a ring by measuring its electrical properties. This task was a relatively involved one, as it included a ground-up design of the circuitry, making choices about component prices […]

Continue reading...
Fork me on GitHub